5.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 the CuPy and SciPy examples below.
- system('PythonSparse', config)
Create a linear system that forwards sparse matrix data to a Python solver object.
configis adictof solver options; see the table below.
|
Python object with a |
|
Sparse storage format: |
|
Buffers the solver may modify: |
Note
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 matrix data as keyword arguments: memoryviews for arrays and scalars for metadata. Use numpy.frombuffer to interpret memoryviews as NumPy arrays without copying. The keyword structure depends on scheme:
CSR/CSC format — compressed storage uses index_ptr and indices:
|
memoryview |
Row pointers (CSR) or column pointers (CSC), int32 |
|
memoryview |
Column indices (CSR) or row indices (CSC), int32 |
|
memoryview |
Matrix coefficients (float64) |
|
memoryview |
Right-hand side vector (float64) |
|
memoryview |
Solution buffer (float64, writable). Write the solution in place. |
|
int |
Number of equations |
|
int |
Number of nonzeros |
|
str |
|
|
str |
|
COO format — coordinate storage uses row and col instead of index_ptr / indices:
|
memoryview |
Row indices for each nonzero, int32 (COO only) |
|
memoryview |
Column indices for each nonzero, int32 (COO only) |
|
memoryview |
Matrix coefficients (float64) |
|
memoryview |
Right-hand side vector (float64) |
|
memoryview |
Solution buffer (float64, writable). Write the solution in place. |
|
int |
Number of equations |
|
int |
Number of nonzeros |
|
str |
|
|
str |
|
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.
5.3.10.1. Example — CuPy conjugate gradient (GPU)¶
This example uses CuPy and cupyx.scipy.sparse.linalg.cg to solve the linear system on an NVIDIA GPU:
import numpy as np
import cupy as cp
import cupyx.scipy.sparse as cupyx_sparse
import cupyx.scipy.sparse.linalg as cupyx_splinalg
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 = cupyx_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_splinalg.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'})
5.3.10.2. Example — SciPy direct solve (CPU)¶
This example uses SciPy factorized (sparse LU) on CPU:
import numpy as np
import scipy.sparse as sp
import scipy.sparse.linalg as sp_linalg
import openseespy.opensees as ops
class SciPyFactorizedSolver:
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 = SciPyFactorizedSolver()
ops.system('PythonSparse', {'solver': solver, 'scheme': 'CSR'})
Code developed by: gaaraujo
See also
PythonSparse eigen solver — PythonSparse interface for generalized eigenvalue problems (
eigen('PythonSparse', ...))EXAMPLES/SolverBenchmark/benchmark_python_sparse.py — benchmark PythonSparse against native solvers