3.2.3.10. PythonSparse System
The PythonSparse system delegates linear system solves (\(\mathbf{A}\mathbf{x} = \mathbf{b}\)) to user-defined Python objects. Sparse matrix buffers (CSR, CSC, or COO format) are exposed to Python via zero-copy memoryviews, enabling efficient integration with SciPy, CuPy, nvmath, or custom solvers without writing C++ wrappers or recompiling OpenSees. See CuPy (GPU) and SciPy (CPU) examples below.
- system PythonSparse <dict>?
Create a linear system that forwards sparse matrix data to a Python solver object. The dict is the solver configuration; see table below.
Key |
Type |
Description |
|---|---|---|
solver |
Python object |
A Python object with a |
scheme |
string |
Sparse format: |
writable |
string or list |
Buffers the solver may modify: |
Note
The PythonSparse interface is available only when using the Python interpreter (OpenSeesPy). Enabling writable for values or rhs allows in-place updates of the stiffness matrix and right-hand side vector; use only if you know what you are doing.
The solver object must implement a solve(**kwargs) method. OpenSees passes the matrix data as keyword arguments: memoryviews (buffer-like objects) for arrays, and scalars for metadata. Use numpy.frombuffer to interpret memoryviews as NumPy arrays without copying. The structure format depends on scheme:
CSR/CSC format — compressed storage uses index_ptr and indices:
Keyword |
Type |
Description |
|---|---|---|
index_ptr |
memoryview |
Row pointers (CSR) or column pointers (CSC), int32. |
indices |
memoryview |
Column indices (CSR) or row indices (CSC), int32. |
values |
memoryview |
Matrix coefficients (float64). |
rhs |
memoryview |
Right-hand side vector (float64). |
x |
memoryview |
Solution buffer (float64, writable). Write the solution in place. |
num_eqn |
int |
Number of equations. |
nnz |
int |
Number of nonzeros. |
matrix_status |
string |
|
storage_scheme |
string |
|
COO format — coordinate storage uses row and col instead of index_ptr/indices:
Keyword |
Type |
Description |
|---|---|---|
row |
memoryview |
Row indices for each nonzero, int32 (COO only). |
col |
memoryview |
Column indices for each nonzero, int32 (COO only). |
values |
memoryview |
Matrix coefficients (float64). |
rhs |
memoryview |
Right-hand side vector (float64). |
x |
memoryview |
Solution buffer (float64, writable). Write the solution in place. |
num_eqn |
int |
Number of equations. |
nnz |
int |
Number of nonzeros. |
matrix_status |
string |
|
storage_scheme |
string |
|
The solve method should return 0 on success, or a negative value to indicate failure.
Warning
In-place output required
You must write the solution directly into the x buffer. OpenSees uses this buffer; it does not use return values or new arrays.
Do: Use in-place assignment, e.g. np.frombuffer(x, dtype=np.float64, count=num_eqn)[:] = result
Do not: Return the solution, assign to a new variable, or write to a copy. The result will be ignored and the analysis will use uninitialized or stale data.
Example — CuPy Conjugate Gradient (GPU)
This example uses CuPy and its cg (conjugate gradient) solver to solve the linear system on an NVIDIA GPU. Note the use of np.frombuffer(array, dtype)[:] = result to write the solution in place:
import numpy as np
import cupy as cp
import cupyx.scipy.sparse.linalg
import openseespy.opensees as ops
class CuPyCGSolver:
def __init__(self, rtol=1e-5, atol=1e-12, maxiter=None):
self.rtol = rtol
self.atol = atol
self.maxiter = maxiter
self.A = None
def solve(self, **kwargs):
index_ptr = kwargs['index_ptr']
indices = kwargs['indices']
values = kwargs['values']
rhs = kwargs['rhs']
x = kwargs['x']
num_eqn = kwargs['num_eqn']
nnz = kwargs['nnz']
matrix_status = kwargs['matrix_status']
indptr = np.frombuffer(index_ptr, dtype=np.int32, count=num_eqn + 1)
idx = np.frombuffer(indices, dtype=np.int32, count=nnz)
vals = np.frombuffer(values, dtype=np.float64, count=nnz)
if matrix_status == 'STRUCTURE_CHANGED' or self.A is None:
self.A = cp.sparse.csr_matrix(
(cp.asarray(vals), cp.asarray(idx), cp.asarray(indptr)),
shape=(num_eqn, num_eqn)
)
elif matrix_status == 'COEFFICIENTS_CHANGED':
self.A.data[:] = cp.asarray(vals)
rhs_gpu = cp.asarray(np.frombuffer(rhs, dtype=np.float64, count=num_eqn))
x_gpu, info = cupyx.scipy.sparse.linalg.cg(
self.A, rhs_gpu, tol=self.rtol, atol=self.atol, maxiter=self.maxiter
)
np.frombuffer(x, dtype=np.float64, count=num_eqn)[:] = cp.asnumpy(x_gpu)
return -int(info)
solver = CuPyCGSolver(rtol=1e-8, atol=1e-12, maxiter=1000)
ops.system('PythonSparse', {'solver': solver, 'scheme': 'CSR'})
Example — SciPy Direct Solve (CPU)
This example uses SciPy and its factorized (sparse LU factorization) for a direct solve on CPU:
import numpy as np
import scipy.sparse as sp
import scipy.sparse.linalg as sp_linalg
import openseespy.opensees as ops
class SciPyspSolveSolver:
def __init__(self):
self.A = None
self._solve_func = None
def solve(self, **kwargs):
index_ptr = kwargs['index_ptr']
indices = kwargs['indices']
values = kwargs['values']
rhs = kwargs['rhs']
x = kwargs['x']
num_eqn = kwargs['num_eqn']
nnz = kwargs['nnz']
matrix_status = kwargs['matrix_status']
indptr = np.frombuffer(index_ptr, dtype=np.int32, count=num_eqn + 1)
idx = np.frombuffer(indices, dtype=np.int32, count=nnz)
vals = np.frombuffer(values, dtype=np.float64, count=nnz)
if matrix_status != 'UNCHANGED' or self._solve_func is None:
self.A = sp.csr_matrix((vals.copy(), idx.copy(), indptr.copy()),
shape=(num_eqn, num_eqn))
self._solve_func = sp_linalg.factorized(self.A)
rhs_arr = np.frombuffer(rhs, dtype=np.float64, count=num_eqn)
x_arr = np.frombuffer(x, dtype=np.float64, count=num_eqn)
x_arr[:] = self._solve_func(rhs_arr)
return 0
solver = SciPyspSolveSolver()
ops.system('PythonSparse', {'solver': solver, 'scheme': 'CSR'})
Code developed by: gaaraujo
See also
eigenPythonSparse — PythonSparse interface for generalized eigenvalue problems
The EXAMPLES/SolverBenchmark script in the OpenSees repository benchmarks PythonSparse against native solvers.