Adding a solver¶
This page is a short checklist for adding a new solver to the package. Submit changes as a pull request on GitHub. For how OpenSees calls solvers at runtime, see the PythonSparse interface.
Before you start¶
Pick the solve type and the module to edit:
| Solve type | Base class | Typical module |
|---|---|---|
Linear Ax = b |
LinearSolver |
scipy, cupy, or nvmath |
Eigen K φ = λ M φ |
EigenSolver |
scipy or cupy |
Copy an existing solver that is closest to yours:
| Kind | Copy from |
|---|---|
CPU iterative (cg, gmres) |
scipy/__init__.py → _CG |
| CPU direct | scipy/__init__.py → _SpSolve or _Umfpack |
| GPU iterative | cupy/__init__.py |
| GPU direct (nvmath) | nvmath/__init__.py → _DirectSolver |
| Eigen (ARPACK) | scipy/__init__.py → _Eigsh |
Shared OpenSees plumbing (buffers, caching, stats) lives in _base.py. Your
solver only needs to implement the backend hooks and one solve method.
Steps¶
1. Add a private solver class¶
In the right __init__.py, add a class that mixes in the backend and the base:
Implement:
_solve_system(linear) or_solve_eigen(eigen) — call the numerical library.__init__— store options and buildself._paramswith every constructor argument. OpenSees usescopy.copy(solver);_paramsmust be enough to recreate the instance.
For direct solvers, reuse work when OpenSees sends matrix_status='UNCHANGED'.
Refresh on 'COEFFICIENTS_CHANGED'; rebuild on 'STRUCTURE_CHANGED'. See _SpSolve
for a minimal example.
For iterative solvers with a preconditioner, users pass M=precond.jacobi (or
similar). Built-in preconditioners live in scipy/precond.py or cupy/precond.py.
2. Add a public solver constructor¶
Add a function that returns your class and append its name to __all__:
def my_solver(*, scheme=None, writable="none", debug=False, dtype=np.float64) -> _MySolver:
"""Configure ... for OpenSees PythonSparse."""
return _MySolver(scheme=scheme or "CSR", writable=writable, debug=debug, dtype=dtype)
Use the docstring fragments in _docstrings.py (_OPENSEES_LINEAR, _LINEAR_RETURNS,
etc.) so constructor docs stay consistent.
Match the underlying library’s keyword names where you can. Do not expose A, b,
K, or M — OpenSees supplies those at solve time.
3. Optional dependency (only if needed)¶
If the solver needs an extra package:
- Add an optional extra in
pyproject.toml. - Lazy-import inside a
_import_*()helper (seescipy/_base.pyfor umfpack). - Raise a clear
ImportErrorwith an install hint when the package is missing.
The module should import without the extra installed; only calling the constructor should require it.
4. Tests¶
In tests/test_solvers.py (or tests/test_eigen.py for eigen):
- Build fake OpenSees kwargs with
csr_linear_kwargs()orcsr_eigen_kwargs()fromtests/conftest.py. - Solve a small known system; assert status
0and correctx(or eigenvalues). - For direct solvers, add a
matrix_statuscaching test (seetest_matrix_status_caching).
Use pytest.importorskip(...) when the backend is optional.
5. Example script¶
Add examples/solvers/<backend>_<name>.py using _brick_common.py (see
examples/solvers/scipy_spsolve.py). End with Passed! so tests/test_examples.py
can smoke-test it.
6. Docs¶
- Add a row to the table in
docs/api/index.md(andREADME.mdif it is a notable default). - Note the change in
CHANGELOG.md.
API pages under docs/api/ are generated from docstrings; a good constructor docstring is
usually enough.
Done checklist¶
- [ ] Constructor in
__all__ - [ ]
self._paramsset in__init__ - [ ] Direct solver respects
matrix_status - [ ] Unit test with synthetic OpenSees kwargs
- [ ] Example script prints
Passed! - [ ]
pytestpasses
References¶
- Base classes and hooks:
src/openseespy_solvers/_base.py - CPU backend mixin:
src/openseespy_solvers/scipy/_base.py - GPU backend mixin:
src/openseespy_solvers/cupy/_base.py - Synthetic test kwargs:
tests/conftest.py - OpenSees buffer contract: PythonSparse interface