Skip to content

openseespy_solvers.scipy

CPU sparse linear and eigen solver constructors.

This module follows the naming and keyword conventions of scipy.sparse.linalg where possible. The main difference is that constructors return OpenSeesPy-compatible solver objects; OpenSeesPy supplies A, b, K, and M at solve time.

Solving Linear Problems

Direct methods:

Constructor scipy analogue Description
spsolve scipy.sparse.linalg.spsolve / splu Sparse direct solver using SuperLU
umfpack scikits.umfpack Sparse direct solver using UMFPACK

Iterative methods:

Constructor scipy analogue Description
cg scipy.sparse.linalg.cg Conjugate Gradient
gmres scipy.sparse.linalg.gmres Generalized Minimal Residual

Eigenvalue Problems

Constructor scipy analogue Description
eigsh scipy.sparse.linalg.eigsh ARPACK-based generalized symmetric eigen solve
lobpcg scipy.sparse.linalg.lobpcg Locally Optimal Block Preconditioned Conjugate Gradient

OpenSeesPy Usage

from openseespy_solvers.scipy import spsolve

solver = spsolve()
ops.system("PythonSparse", solver.to_openseespy())

For eigen analysis:

from openseespy_solvers.scipy import eigsh

solver = eigsh(tol=1e-8)
lam = ops.eigen("PythonSparse", num_modes, solver.to_openseespy())

Notes

spsolve and umfpack are both sparse direct solvers. spsolve uses scipy.sparse.linalg's SuperLU path and is available with the base install. umfpack requires the optional scikit-umfpack package:

python -m pip install "openseespy-solvers[umfpack]"

On Windows, install scikit-umfpack from conda-forge instead.

Parameters documented below follow scipy naming where possible. Extra parameters such as scheme, writable, debug, and dtype control the OpenSeesPy integration.

Function Reference

Sparse linear algebra solvers for OpenSeesPy (scipy.sparse.linalg backend).

This module provides solver objects that wrap :mod:scipy.sparse.linalg for OpenSeesPy's PythonSparse linear and eigen commands. Factory signatures match scipy.sparse.linalg except that A and b (or K and M) are supplied by OpenSees at solve time.

Each factory returns a solver object. Register it with OpenSees using :meth:~openseespy_solvers._base.BaseOpenSeesSolver.to_openseespy::

from openseespy_solvers.scipy import cg
solver = cg(rtol=1e-8, M=precond.jacobi)
ops.system("PythonSparse", solver.to_openseespy())
Submodules

precond Preconditioner factories for use with M=.

See Also

scipy.sparse.linalg Underlying sparse linear algebra routines. openseespy_solvers.cupy GPU backend with the same interface.

spsolve

spsolve(*, permc_spec: str = 'COLAMD', scheme: str | None = None, writable: str | list[str] = 'none', debug: bool = False, dtype: Any = float64) -> _SpSolve
Source code in src/openseespy_solvers/scipy/__init__.py
def spsolve(
    *,
    permc_spec: str = "COLAMD",
    scheme: str | None = None,
    writable: str | list[str] = "none",
    debug: bool = False,
    dtype: Any = np.float64,
) -> _SpSolve:
    r"""Configure a sparse direct solver for OpenSees ``PythonSparse``.

    Uses :func:`scipy.sparse.linalg.splu` internally. The LU factorization is
    reused when OpenSees reports ``matrix_status='UNCHANGED'``.

    Parameters
    ----------
    permc_spec : str, optional
        Column permutation applied during LU factorization. Passed to
        :func:`scipy.sparse.linalg.splu`. Default is ``'COLAMD'``.
    """ + _OPENSEES_LINEAR + _LINEAR_RETURNS + _LINEAR_NOTES + """
    See Also
    --------
    scipy.sparse.linalg.spsolve
    scipy.sparse.linalg.splu

    Examples
    --------
    >>> from openseespy_solvers.scipy import spsolve
    >>> solver = spsolve(permc_spec="COLAMD")
    >>> cfg = solver.to_openseespy()
    >>> cfg["scheme"]
    'CSR'
    """
    return _SpSolve(
        permc_spec=permc_spec, scheme=scheme or "CSR", writable=writable, debug=debug, dtype=dtype
    )

umfpack

umfpack(*, scheme: str | None = None, writable: str | list[str] = 'none', debug: bool = False, dtype: Any = float64) -> _Umfpack
Source code in src/openseespy_solvers/scipy/__init__.py
def umfpack(
    *,
    scheme: str | None = None,
    writable: str | list[str] = "none",
    debug: bool = False,
    dtype: Any = np.float64,
) -> _Umfpack:
    r"""Configure a 64-bit UMFPACK direct solver for OpenSees ``PythonSparse``.

    Wraps ``scikits.umfpack.UmfpackContext("dl")`` (the double / 64-bit-integer
    UMFPACK context) to solve ``Ax = b``. The symbolic factorization is reused
    while the sparsity structure is unchanged and is recomputed only when
    OpenSees reports ``matrix_status='STRUCTURE_CHANGED'``; the numeric
    factorization is refreshed on ``'COEFFICIENTS_CHANGED'`` and reused on
    ``'UNCHANGED'``.

    Unlike :func:`spsolve` (SuperLU), this uses the UMFPACK library and 64-bit
    (``int64``) indices, which suits very large systems whose index count
    exceeds the 32-bit range. Requires the optional ``scikit-umfpack`` package
    (``python -m pip install "openseespy-solvers[umfpack]"``); the import is deferred until
    this factory is called.

    Parameters
    ----------
    """ + _OPENSEES_LINEAR + _LINEAR_RETURNS + _LINEAR_NOTES + """
    The OpenSees CSR buffers are assembled into a `scipy.sparse` matrix and converted to
    CSC for UMFPACK, so non-symmetric systems are solved correctly.

    Raises
    ------
    ImportError
        If ``scikit-umfpack`` is not installed (raised when the solver is
        instantiated, not on import).

    See Also
    --------
    spsolve
    scipy.sparse.linalg.spsolve

    Examples
    --------
    >>> from openseespy_solvers.scipy import umfpack  # doctest: +SKIP
    >>> solver = umfpack()  # doctest: +SKIP
    >>> solver.to_openseespy()["scheme"]  # doctest: +SKIP
    'CSR'
    """
    return _Umfpack(scheme=scheme or "CSR", writable=writable, debug=debug, dtype=dtype)

cg

cg(x0: Any = None, *, rtol: float = 1e-05, atol: float = 0.0, maxiter: int | None = None, M: Any = None, callback: Any = None, scheme: str | None = None, writable: str | list[str] = 'none', debug: bool = False, dtype: Any = float64) -> _CG
Source code in src/openseespy_solvers/scipy/__init__.py
def cg(
    x0: Any = None,
    *,
    rtol: float = 1e-5,
    atol: float = 0.0,
    maxiter: int | None = None,
    M: Any = None,
    callback: Any = None,
    scheme: str | None = None,
    writable: str | list[str] = "none",
    debug: bool = False,
    dtype: Any = np.float64,
) -> _CG:
    r"""Configure a Conjugate Gradient solver for OpenSees ``PythonSparse``.

    Uses :func:`scipy.sparse.linalg.cg` internally to solve ``Ax = b`` for
    symmetric, positive-definite ``A``.

    Parameters
    ----------
    x0 : ndarray, optional
        Starting guess for the solution. Forwarded to `scipy.sparse.linalg` when not ``None``.
    rtol, atol : float, optional
        Relative and absolute tolerances for convergence. For convergence,
        ``norm(b - A @ x) <= max(rtol*norm(b), atol)`` must hold. Defaults are
        ``rtol=1e-5`` and ``atol=0.0``.
    maxiter : int, optional
        Maximum number of iterations.
    M : {sparse matrix, LinearOperator, callable}, optional
        Preconditioner for ``A``. If ``M`` is callable, it must accept the
        assembled matrix ``A`` and return a preconditioner; see
        :mod:`openseespy_solvers.scipy.precond`.
    callback : callable, optional
        User-supplied function called after each iteration as ``callback()``.
    """ + _OPENSEES_LINEAR + _LINEAR_RETURNS + _LINEAR_NOTES + """
    See Also
    --------
    scipy.sparse.linalg.cg
    openseespy_solvers.scipy.precond.jacobi
    openseespy_solvers.scipy.precond.ilu

    Examples
    --------
    >>> from openseespy_solvers.scipy import cg, precond
    >>> solver = cg(rtol=1e-8, M=precond.jacobi)
    >>> solver.to_openseespy()["scheme"]
    'CSR'
    """
    return _CG(
        x0,
        rtol=rtol,
        atol=atol,
        maxiter=maxiter,
        M=M,
        callback=callback,
        scheme=scheme or "CSR",
        writable=writable,
        debug=debug,
        dtype=dtype,
    )

gmres

gmres(x0: Any = None, *, rtol: float = 1e-05, atol: float = 0.0, restart: int | None = None, maxiter: int | None = None, M: Any = None, callback: Any = None, scheme: str | None = None, writable: str | list[str] = 'none', debug: bool = False, dtype: Any = float64) -> _GMRES
Source code in src/openseespy_solvers/scipy/__init__.py
def gmres(
    x0: Any = None,
    *,
    rtol: float = 1e-5,
    atol: float = 0.0,
    restart: int | None = None,
    maxiter: int | None = None,
    M: Any = None,
    callback: Any = None,
    scheme: str | None = None,
    writable: str | list[str] = "none",
    debug: bool = False,
    dtype: Any = np.float64,
) -> _GMRES:
    r"""Configure a GMRES solver for OpenSees ``PythonSparse``.

    Uses :func:`scipy.sparse.linalg.gmres` internally to solve ``Ax = b``.

    Parameters
    ----------
    x0 : ndarray, optional
        Starting guess for the solution.
    rtol, atol : float, optional
        Relative and absolute tolerances for convergence. Defaults are
        ``rtol=1e-5`` and ``atol=0.0``.
    restart : int, optional
        Number of iterations between restarts.
    maxiter : int, optional
        Maximum number of iterations.
    M : {sparse matrix, LinearOperator, callable}, optional
        Preconditioner for ``A``.
    callback : callable, optional
        User-supplied function called after each iteration.
    """ + _OPENSEES_LINEAR + _LINEAR_RETURNS + _LINEAR_NOTES + """
    See Also
    --------
    scipy.sparse.linalg.gmres
    cg

    Examples
    --------
    >>> from openseespy_solvers.scipy import gmres
    >>> solver = gmres(rtol=1e-8, restart=30)
    >>> solver.backend
    'scipy'
    """
    return _GMRES(
        x0,
        rtol=rtol,
        atol=atol,
        restart=restart,
        maxiter=maxiter,
        M=M,
        callback=callback,
        scheme=scheme or "CSR",
        writable=writable,
        debug=debug,
        dtype=dtype,
    )

eigsh

eigsh(*, sigma: float | None = None, which: str = 'LM', v0: Any = None, ncv: int | None = None, maxiter: int | None = None, tol: float = 0.0, mode: str = 'normal', scheme: str | None = None, debug: bool = False, dtype: Any = float64) -> _Eigsh
Source code in src/openseespy_solvers/scipy/__init__.py
def eigsh(
    *,
    sigma: float | None = None,
    which: str = "LM",
    v0: Any = None,
    ncv: int | None = None,
    maxiter: int | None = None,
    tol: float = 0.0,
    mode: str = "normal",
    scheme: str | None = None,
    debug: bool = False,
    dtype: Any = np.float64,
) -> _Eigsh:
    r"""Configure a sparse eigenvalue solver for OpenSees ``PythonSparse``.

    Matches OpenSees ``eigsh``: shift-invert with ``sigma=0`` when
    ``find_smallest=True``; plain ARPACK when ``find_smallest=False`` unless
    ``sigma`` is set. ARPACK always uses ``which='LM'`` (OpenSees reference).

    Parameters
    ----------
    sigma : float, optional
        Shift for shift-invert. Default ``0.0`` when ``find_smallest=True``.
    which : {'LM', 'SM', ...}, optional
        Accepted for API compatibility; solves always use ``'LM'`` like OpenSees.
    v0 : ndarray, optional
        Starting vector for ARPACK.
    ncv : int, optional
        Number of Lanczos vectors.
    maxiter : int, optional
        Maximum number of iterations.
    tol : float, optional
        Convergence tolerance. Default is ``0.0``.
    mode : {'normal', 'buckling', 'cayley'}, optional
        Eigenproblem mode. Default is ``'normal'``.
    """ + _OPENSEES_EIGEN + _EIGEN_RETURNS + _EIGEN_NOTES + """
    See Also
    --------
    scipy.sparse.linalg.eigsh
    lobpcg

    Examples
    --------
    >>> from openseespy_solvers.scipy import eigsh
    >>> solver = eigsh(tol=1e-8)
    >>> solver.scheme
    'CSR'
    """
    return _Eigsh(
        sigma=sigma,
        which=which,
        v0=v0,
        ncv=ncv,
        maxiter=maxiter,
        tol=tol,
        mode=mode,
        scheme=scheme or "CSR",
        debug=debug,
        dtype=dtype,
    )

lobpcg

lobpcg(*, X: Any = None, M: Any = None, tol: float | None = None, maxiter: int = 20, largest: bool = False, rng: Any = None, scheme: str | None = None, debug: bool = False, dtype: Any = float64) -> _Lobpcg
Source code in src/openseespy_solvers/scipy/__init__.py
def lobpcg(
    *,
    X: Any = None,
    M: Any = None,
    tol: float | None = None,
    maxiter: int = 20,
    largest: bool = False,
    rng: Any = None,
    scheme: str | None = None,
    debug: bool = False,
    dtype: Any = np.float64,
) -> _Lobpcg:
    r"""Configure a LOBPCG eigenvalue solver for OpenSees ``PythonSparse``.

    Uses :func:`scipy.sparse.linalg.lobpcg` internally to solve the generalized
    eigenproblem ``K x = \lambda M x``.

    Parameters
    ----------
    X : ndarray, optional
        Initial approximation to the eigenvectors. If ``None``, a random matrix
        with ``num_modes`` columns is generated using ``rng``.
    M : {sparse matrix, LinearOperator, callable}, optional
        Optional preconditioner passed to LOBPCG as ``M=``. A callable is
        invoked as ``M(K)``; see :mod:`openseespy_solvers.scipy.precond`.
    tol : float, optional
        Convergence tolerance.
    maxiter : int, optional
        Maximum number of iterations. Default is ``20``.
    largest : bool, optional
        If ``True``, compute largest eigenvalues. Default is ``False``.
    rng : {None, int, `numpy.random.Generator`}, optional
        Random number generator used when ``X`` is ``None``.
    """ + _OPENSEES_EIGEN + _EIGEN_RETURNS + _EIGEN_NOTES + """
    See Also
    --------
    scipy.sparse.linalg.lobpcg
    eigsh

    Examples
    --------
    >>> from openseespy_solvers.scipy import lobpcg
    >>> solver = lobpcg(tol=1e-8, maxiter=40)
    >>> solver.backend
    'scipy'
    """
    return _Lobpcg(
        X=X,
        M=M,
        tol=tol,
        maxiter=maxiter,
        largest=largest,
        rng=rng,
        scheme=scheme or "CSR",
        debug=debug,
        dtype=dtype,
    )