openseespy-solvers 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openseespy_solvers/__init__.py +41 -0
- openseespy_solvers/_base.py +428 -0
- openseespy_solvers/_docstrings.py +64 -0
- openseespy_solvers/_dtype.py +45 -0
- openseespy_solvers/_factorization.py +86 -0
- openseespy_solvers/_hybrid.py +319 -0
- openseespy_solvers/_sparse.py +134 -0
- openseespy_solvers/cupy/__init__.py +845 -0
- openseespy_solvers/cupy/_base.py +99 -0
- openseespy_solvers/cupy/precond.py +208 -0
- openseespy_solvers/exceptions.py +34 -0
- openseespy_solvers/hybrid.py +5 -0
- openseespy_solvers/nvmath/__init__.py +235 -0
- openseespy_solvers/nvmath/_base.py +250 -0
- openseespy_solvers/scipy/__init__.py +712 -0
- openseespy_solvers/scipy/_base.py +78 -0
- openseespy_solvers/scipy/precond.py +145 -0
- openseespy_solvers/stats.py +32 -0
- openseespy_solvers-0.1.0.dist-info/METADATA +166 -0
- openseespy_solvers-0.1.0.dist-info/RECORD +22 -0
- openseespy_solvers-0.1.0.dist-info/WHEEL +4 -0
- openseespy_solvers-0.1.0.dist-info/licenses/LICENSE +29 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Ready-to-use PythonSparse solvers for OpenSeesPy."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib import import_module
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from openseespy_solvers.exceptions import (
|
|
9
|
+
BackendNotAvailableError,
|
|
10
|
+
InvalidOpenSeesDataError,
|
|
11
|
+
OpenSeesSolverError,
|
|
12
|
+
SolverConvergenceError,
|
|
13
|
+
UnsupportedComputeDtypeError,
|
|
14
|
+
UnsupportedStorageSchemeError,
|
|
15
|
+
)
|
|
16
|
+
from openseespy_solvers.hybrid import hybrid
|
|
17
|
+
|
|
18
|
+
__version__ = "0.1.0"
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"BackendNotAvailableError",
|
|
22
|
+
"InvalidOpenSeesDataError",
|
|
23
|
+
"OpenSeesSolverError",
|
|
24
|
+
"SolverConvergenceError",
|
|
25
|
+
"UnsupportedComputeDtypeError",
|
|
26
|
+
"UnsupportedStorageSchemeError",
|
|
27
|
+
"__version__",
|
|
28
|
+
"cupy",
|
|
29
|
+
"hybrid",
|
|
30
|
+
"scipy",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from openseespy_solvers import cupy as cupy
|
|
35
|
+
from openseespy_solvers import scipy as scipy
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def __getattr__(name: str):
|
|
39
|
+
if name in {"scipy", "cupy"}:
|
|
40
|
+
return import_module(f"openseespy_solvers.{name}")
|
|
41
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"""Base solver classes for the OpenSees PythonSparse interfaces.
|
|
2
|
+
|
|
3
|
+
These bases own the parts that are *identical for every backend*: wrapping the
|
|
4
|
+
OpenSees memoryviews, the ``matrix_status`` caching pattern, writing the solution
|
|
5
|
+
in place, statistics, ``formAp``, ``to_openseespy`` and ``copy`` support.
|
|
6
|
+
|
|
7
|
+
Backend-specific numerics are provided by small hooks that each namespace
|
|
8
|
+
(``openseespy_solvers.scipy``, ``.cupy``, ...) implements:
|
|
9
|
+
|
|
10
|
+
* ``_build_matrix`` / ``_update_matrix`` - assemble the cached sparse matrix
|
|
11
|
+
* ``_to_device`` / ``_to_host`` - host<->device transfer (identity on CPU)
|
|
12
|
+
* ``_matvec`` - ``A @ v``
|
|
13
|
+
* ``_is_sparse`` / ``_is_linear_operator`` - preconditioner detection
|
|
14
|
+
* ``_solve_system`` (linear) / ``_solve_eigen`` (eigen) - the actual library call
|
|
15
|
+
|
|
16
|
+
A backend that does not fit this shape (e.g. a distributed PETSc backend) can
|
|
17
|
+
override ``solve`` directly; nothing here is mandatory beyond the hooks a given
|
|
18
|
+
solver uses.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import copy
|
|
24
|
+
import time
|
|
25
|
+
from abc import ABC, abstractmethod
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
|
|
30
|
+
from openseespy_solvers._dtype import OPENSEES_BUFFER_DTYPE, resolve_compute_dtype
|
|
31
|
+
from openseespy_solvers._sparse import parse_eigen_values, parse_sparse_arrays
|
|
32
|
+
from openseespy_solvers.exceptions import InvalidOpenSeesDataError
|
|
33
|
+
from openseespy_solvers.stats import EigenSolverStats, LinearSolverStats
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _normalize_writable(writable: str | list[str]) -> str:
|
|
37
|
+
if isinstance(writable, list):
|
|
38
|
+
return ",".join(writable)
|
|
39
|
+
return writable
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class BaseOpenSeesSolver(ABC):
|
|
43
|
+
"""Shared behavior for linear and eigen solvers."""
|
|
44
|
+
|
|
45
|
+
#: Backend name, set as a class attribute by each namespace mixin.
|
|
46
|
+
backend: str = "base"
|
|
47
|
+
#: Whether matrices/vectors live on a device (GPU) rather than host memory.
|
|
48
|
+
_on_device: bool = False
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
*,
|
|
53
|
+
scheme: str = "CSR",
|
|
54
|
+
debug: bool = False,
|
|
55
|
+
dtype: Any = np.float64,
|
|
56
|
+
) -> None:
|
|
57
|
+
self.scheme = scheme
|
|
58
|
+
self.debug = debug
|
|
59
|
+
self._compute_dtype = resolve_compute_dtype(dtype)
|
|
60
|
+
self._matrix: Any | None = None
|
|
61
|
+
self._k_matrix: Any | None = None
|
|
62
|
+
self._m_matrix: Any | None = None
|
|
63
|
+
self._params: dict[str, Any] = {}
|
|
64
|
+
|
|
65
|
+
# -- backend hooks ---------------------------------------------------
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
def _build_matrix(
|
|
69
|
+
self,
|
|
70
|
+
values: np.ndarray,
|
|
71
|
+
indices: np.ndarray,
|
|
72
|
+
indptr: np.ndarray,
|
|
73
|
+
shape: tuple[int, int],
|
|
74
|
+
fmt: str,
|
|
75
|
+
) -> Any:
|
|
76
|
+
"""Build a new sparse matrix in the backend's format (owns its data)."""
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def _update_matrix(self, matrix: Any, values: np.ndarray) -> Any:
|
|
80
|
+
"""Refresh coefficients (in place where possible) and return the matrix."""
|
|
81
|
+
|
|
82
|
+
@abstractmethod
|
|
83
|
+
def _to_device(self, array: np.ndarray) -> Any:
|
|
84
|
+
"""Transfer a host array to the backend (identity on CPU)."""
|
|
85
|
+
|
|
86
|
+
@abstractmethod
|
|
87
|
+
def _to_host(self, array: Any) -> np.ndarray:
|
|
88
|
+
"""Transfer a backend array back to a NumPy host array."""
|
|
89
|
+
|
|
90
|
+
@abstractmethod
|
|
91
|
+
def _matvec(self, matrix: Any, vector: Any) -> Any:
|
|
92
|
+
"""Return ``matrix @ vector`` in the backend."""
|
|
93
|
+
|
|
94
|
+
# -- OpenSees config -------------------------------------------------
|
|
95
|
+
|
|
96
|
+
def to_openseespy(
|
|
97
|
+
self,
|
|
98
|
+
*,
|
|
99
|
+
scheme: str | None = None,
|
|
100
|
+
writable: str | list[str] | None = None,
|
|
101
|
+
) -> dict[str, Any]:
|
|
102
|
+
"""Return the configuration dict for OpenSeesPy ``PythonSparse`` commands.
|
|
103
|
+
|
|
104
|
+
Parameters
|
|
105
|
+
----------
|
|
106
|
+
scheme : {'CSR', 'CSC'}, optional
|
|
107
|
+
Sparse storage scheme. Default is the value given at construction.
|
|
108
|
+
writable : str or list of str, optional
|
|
109
|
+
Writable buffers declared to OpenSees. Overrides the constructor
|
|
110
|
+
value when provided.
|
|
111
|
+
|
|
112
|
+
Returns
|
|
113
|
+
-------
|
|
114
|
+
config : dict
|
|
115
|
+
Dictionary with keys ``solver``, ``scheme``, and optionally
|
|
116
|
+
``writable``. Pass directly to ``ops.system('PythonSparse', ...)``
|
|
117
|
+
or ``ops.eigen('PythonSparse', ...)``.
|
|
118
|
+
|
|
119
|
+
Examples
|
|
120
|
+
--------
|
|
121
|
+
>>> from openseespy_solvers.scipy import cg
|
|
122
|
+
>>> solver = cg()
|
|
123
|
+
>>> cfg = solver.to_openseespy()
|
|
124
|
+
>>> sorted(cfg.keys())
|
|
125
|
+
['scheme', 'solver', 'writable']
|
|
126
|
+
"""
|
|
127
|
+
cfg: dict[str, Any] = {
|
|
128
|
+
"solver": self,
|
|
129
|
+
"scheme": scheme or self.scheme,
|
|
130
|
+
}
|
|
131
|
+
if writable is not None:
|
|
132
|
+
cfg["writable"] = _normalize_writable(writable)
|
|
133
|
+
elif hasattr(self, "writable"):
|
|
134
|
+
cfg["writable"] = _normalize_writable(self.writable)
|
|
135
|
+
return cfg
|
|
136
|
+
|
|
137
|
+
# -- matrix caching --------------------------------------------------
|
|
138
|
+
|
|
139
|
+
def _values_for_compute(self, values: np.ndarray) -> np.ndarray:
|
|
140
|
+
return np.asarray(values, dtype=self._compute_dtype)
|
|
141
|
+
|
|
142
|
+
def _write_opensees_buffer(self, buf: np.ndarray, value: Any) -> None:
|
|
143
|
+
buf[:] = np.asarray(self._to_host(value), dtype=OPENSEES_BUFFER_DTYPE)
|
|
144
|
+
|
|
145
|
+
def _current_matrix(self, kwargs: dict[str, Any], *, values_key: str = "values") -> Any:
|
|
146
|
+
arrays = parse_sparse_arrays(kwargs, values_key=values_key)
|
|
147
|
+
values = self._values_for_compute(arrays.values)
|
|
148
|
+
status = kwargs["matrix_status"]
|
|
149
|
+
if status == "STRUCTURE_CHANGED" or self._matrix is None:
|
|
150
|
+
self._matrix = self._build_matrix(
|
|
151
|
+
values, arrays.indices, arrays.indptr, arrays.shape, arrays.fmt
|
|
152
|
+
)
|
|
153
|
+
elif status == "COEFFICIENTS_CHANGED":
|
|
154
|
+
self._matrix = self._update_matrix(self._matrix, values)
|
|
155
|
+
return self._matrix
|
|
156
|
+
|
|
157
|
+
def _current_eigen_matrices(self, kwargs: dict[str, Any]) -> tuple[Any, Any]:
|
|
158
|
+
arrays = parse_sparse_arrays(kwargs, values_key="k_values")
|
|
159
|
+
k_values = self._values_for_compute(arrays.values)
|
|
160
|
+
m_values = self._values_for_compute(parse_eigen_values(kwargs))
|
|
161
|
+
status = kwargs["matrix_status"]
|
|
162
|
+
if status == "STRUCTURE_CHANGED" or self._k_matrix is None:
|
|
163
|
+
self._k_matrix = self._build_matrix(
|
|
164
|
+
k_values, arrays.indices, arrays.indptr, arrays.shape, arrays.fmt
|
|
165
|
+
)
|
|
166
|
+
self._m_matrix = self._build_matrix(
|
|
167
|
+
m_values, arrays.indices, arrays.indptr, arrays.shape, arrays.fmt
|
|
168
|
+
)
|
|
169
|
+
elif status == "COEFFICIENTS_CHANGED":
|
|
170
|
+
self._k_matrix = self._update_matrix(self._k_matrix, k_values)
|
|
171
|
+
self._m_matrix = self._update_matrix(self._m_matrix, m_values)
|
|
172
|
+
return self._k_matrix, self._m_matrix
|
|
173
|
+
|
|
174
|
+
# -- copy ------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
def copy(self) -> BaseOpenSeesSolver:
|
|
177
|
+
return copy.copy(self)
|
|
178
|
+
|
|
179
|
+
def __copy__(self) -> BaseOpenSeesSolver:
|
|
180
|
+
return type(self)(**self._params)
|
|
181
|
+
|
|
182
|
+
def __deepcopy__(self, memo: dict[int, Any]) -> BaseOpenSeesSolver:
|
|
183
|
+
return self.__copy__()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class LinearSolver(BaseOpenSeesSolver, ABC):
|
|
187
|
+
"""Base class for linear system solvers (``Ax = b``).
|
|
188
|
+
|
|
189
|
+
Instances are created by backend factories such as :func:`openseespy_solvers.scipy.cg`.
|
|
190
|
+
OpenSees calls :meth:`solve` with sparse buffer memoryviews; the solution is
|
|
191
|
+
written in place to the ``x`` buffer.
|
|
192
|
+
|
|
193
|
+
Attributes
|
|
194
|
+
----------
|
|
195
|
+
A : sparse matrix or None
|
|
196
|
+
Cached system matrix from the last :meth:`solve` or :meth:`formAp` call.
|
|
197
|
+
b : ndarray
|
|
198
|
+
Right-hand side from the last :meth:`solve` call.
|
|
199
|
+
x : ndarray
|
|
200
|
+
Last solution vector.
|
|
201
|
+
stats : LinearSolverStats
|
|
202
|
+
Runtime statistics updated after each :meth:`solve`.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
stats: LinearSolverStats
|
|
206
|
+
|
|
207
|
+
def __init__(
|
|
208
|
+
self,
|
|
209
|
+
*,
|
|
210
|
+
scheme: str = "CSR",
|
|
211
|
+
writable: str | list[str] = "none",
|
|
212
|
+
debug: bool = False,
|
|
213
|
+
preconditioner: Any = None,
|
|
214
|
+
dtype: Any = np.float64,
|
|
215
|
+
) -> None:
|
|
216
|
+
super().__init__(scheme=scheme, debug=debug, dtype=dtype)
|
|
217
|
+
self.writable = _normalize_writable(writable)
|
|
218
|
+
self._preconditioner = preconditioner
|
|
219
|
+
self._preconditioner_cached: Any | None = None
|
|
220
|
+
self.stats = LinearSolverStats()
|
|
221
|
+
self._A: Any | None = None
|
|
222
|
+
self._b: Any | None = None
|
|
223
|
+
self._x: Any | None = None
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def A(self) -> Any | None:
|
|
227
|
+
return self._A
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def b(self) -> Any | None:
|
|
231
|
+
return self._b
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def x(self) -> Any | None:
|
|
235
|
+
return self._x
|
|
236
|
+
|
|
237
|
+
def _resolve_preconditioner(self, A: Any, matrix_status: str) -> Any | None:
|
|
238
|
+
M = self._preconditioner
|
|
239
|
+
if M is None:
|
|
240
|
+
return None
|
|
241
|
+
if self._is_sparse(M) or self._is_linear_operator(M):
|
|
242
|
+
return M
|
|
243
|
+
if callable(M):
|
|
244
|
+
if matrix_status == "UNCHANGED" and self._preconditioner_cached is not None:
|
|
245
|
+
return self._preconditioner_cached
|
|
246
|
+
self._preconditioner_cached = M(A)
|
|
247
|
+
return self._preconditioner_cached
|
|
248
|
+
return M
|
|
249
|
+
|
|
250
|
+
def solve(self, **kwargs: Any) -> int:
|
|
251
|
+
"""Solve ``Ax = b`` using buffers supplied by OpenSees.
|
|
252
|
+
|
|
253
|
+
This method is called by OpenSeesPy; application code normally does not
|
|
254
|
+
invoke it directly.
|
|
255
|
+
|
|
256
|
+
Parameters
|
|
257
|
+
----------
|
|
258
|
+
**kwargs
|
|
259
|
+
OpenSees ``PythonSparse`` buffers, including ``values``, ``rhs``,
|
|
260
|
+
``x``, ``num_eqn``, ``nnz``, ``matrix_status``, and
|
|
261
|
+
``storage_scheme``.
|
|
262
|
+
|
|
263
|
+
Returns
|
|
264
|
+
-------
|
|
265
|
+
info : int
|
|
266
|
+
``0`` if the solve succeeded; a negative value otherwise. When
|
|
267
|
+
``debug=True``, failures raise the underlying exception instead.
|
|
268
|
+
"""
|
|
269
|
+
try:
|
|
270
|
+
matrix_status = kwargs["matrix_status"]
|
|
271
|
+
num_eqn = int(kwargs["num_eqn"])
|
|
272
|
+
if "rhs" not in kwargs or "x" not in kwargs:
|
|
273
|
+
raise InvalidOpenSeesDataError("Linear solve requires rhs and x buffers")
|
|
274
|
+
|
|
275
|
+
A = self._current_matrix(kwargs)
|
|
276
|
+
rhs = np.frombuffer(kwargs["rhs"], dtype=OPENSEES_BUFFER_DTYPE, count=num_eqn)
|
|
277
|
+
x_buf = np.frombuffer(kwargs["x"], dtype=OPENSEES_BUFFER_DTYPE, count=num_eqn)
|
|
278
|
+
|
|
279
|
+
self._A = A
|
|
280
|
+
b = self._to_device(np.asarray(rhs, dtype=self._compute_dtype))
|
|
281
|
+
self._b = b
|
|
282
|
+
|
|
283
|
+
M = self._resolve_preconditioner(A, matrix_status)
|
|
284
|
+
start = time.perf_counter()
|
|
285
|
+
result, info, num_iter = self._solve_system(A, b, M, matrix_status)
|
|
286
|
+
elapsed = time.perf_counter() - start
|
|
287
|
+
|
|
288
|
+
self._write_opensees_buffer(x_buf, result)
|
|
289
|
+
self._x = result if self._on_device else x_buf
|
|
290
|
+
|
|
291
|
+
residual = None
|
|
292
|
+
if num_eqn > 0:
|
|
293
|
+
ax = np.asarray(self._to_host(self._matvec(A, result)), dtype=OPENSEES_BUFFER_DTYPE)
|
|
294
|
+
r = rhs - ax
|
|
295
|
+
norm_b = float(np.linalg.norm(rhs))
|
|
296
|
+
residual = (
|
|
297
|
+
float(np.linalg.norm(r) / norm_b) if norm_b > 0 else float(np.linalg.norm(r))
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
self.stats.num_solves += 1
|
|
301
|
+
self.stats.last_solve_time = elapsed
|
|
302
|
+
self.stats.last_info = info
|
|
303
|
+
self.stats.last_num_iterations = num_iter
|
|
304
|
+
self.stats.last_residual_norm = residual
|
|
305
|
+
self.stats.last_error = None
|
|
306
|
+
return 0 if info == 0 else -abs(int(info))
|
|
307
|
+
except Exception as exc:
|
|
308
|
+
self.stats.last_error = exc
|
|
309
|
+
if self.debug:
|
|
310
|
+
raise
|
|
311
|
+
return -1
|
|
312
|
+
|
|
313
|
+
def formAp(self, **kwargs: Any) -> int:
|
|
314
|
+
try:
|
|
315
|
+
num_eqn = int(kwargs["num_eqn"])
|
|
316
|
+
if "p" not in kwargs or "Ap" not in kwargs:
|
|
317
|
+
raise InvalidOpenSeesDataError("formAp requires p and Ap buffers")
|
|
318
|
+
|
|
319
|
+
A = self._current_matrix(kwargs)
|
|
320
|
+
self._A = A
|
|
321
|
+
p = np.frombuffer(kwargs["p"], dtype=OPENSEES_BUFFER_DTYPE, count=num_eqn)
|
|
322
|
+
ap_buf = np.frombuffer(kwargs["Ap"], dtype=OPENSEES_BUFFER_DTYPE, count=num_eqn)
|
|
323
|
+
|
|
324
|
+
result = self._matvec(A, self._to_device(np.asarray(p, dtype=self._compute_dtype)))
|
|
325
|
+
self._write_opensees_buffer(ap_buf, result)
|
|
326
|
+
return 0
|
|
327
|
+
except Exception as exc:
|
|
328
|
+
self.stats.last_error = exc
|
|
329
|
+
if self.debug:
|
|
330
|
+
raise
|
|
331
|
+
return -1
|
|
332
|
+
|
|
333
|
+
@abstractmethod
|
|
334
|
+
def _is_sparse(self, obj: Any) -> bool:
|
|
335
|
+
"""Return True if *obj* is a sparse matrix for this backend."""
|
|
336
|
+
|
|
337
|
+
@abstractmethod
|
|
338
|
+
def _is_linear_operator(self, obj: Any) -> bool:
|
|
339
|
+
"""Return True if *obj* is a LinearOperator for this backend."""
|
|
340
|
+
|
|
341
|
+
@abstractmethod
|
|
342
|
+
def _solve_system(
|
|
343
|
+
self,
|
|
344
|
+
A: Any,
|
|
345
|
+
b: Any,
|
|
346
|
+
M: Any | None,
|
|
347
|
+
matrix_status: str,
|
|
348
|
+
) -> tuple[Any, int, int | None]:
|
|
349
|
+
"""Return (solution, info, num_iterations)."""
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
class EigenSolver(BaseOpenSeesSolver, ABC):
|
|
353
|
+
"""Base class for generalized eigenvalue solvers (``K phi = lambda M phi``)."""
|
|
354
|
+
|
|
355
|
+
stats: EigenSolverStats
|
|
356
|
+
|
|
357
|
+
def __init__(
|
|
358
|
+
self, *, scheme: str = "CSR", debug: bool = False, dtype: Any = np.float64
|
|
359
|
+
) -> None:
|
|
360
|
+
super().__init__(scheme=scheme, debug=debug, dtype=dtype)
|
|
361
|
+
self.stats = EigenSolverStats()
|
|
362
|
+
self._K: Any | None = None
|
|
363
|
+
self._M: Any | None = None
|
|
364
|
+
|
|
365
|
+
@property
|
|
366
|
+
def K(self) -> Any | None:
|
|
367
|
+
return self._K
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def M(self) -> Any | None:
|
|
371
|
+
return self._M
|
|
372
|
+
|
|
373
|
+
def solve(self, **kwargs: Any) -> None:
|
|
374
|
+
num_modes = int(kwargs["num_modes"])
|
|
375
|
+
num_eqn = int(kwargs["num_eqn"])
|
|
376
|
+
find_smallest = bool(kwargs["find_smallest"])
|
|
377
|
+
|
|
378
|
+
eigenvalues_buf = np.frombuffer(
|
|
379
|
+
kwargs["eigenvalues"], dtype=OPENSEES_BUFFER_DTYPE, count=num_modes
|
|
380
|
+
)
|
|
381
|
+
eigenvectors_buf = np.frombuffer(
|
|
382
|
+
kwargs["eigenvectors"], dtype=OPENSEES_BUFFER_DTYPE, count=num_modes * num_eqn
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
K, M = self._current_eigen_matrices(kwargs)
|
|
386
|
+
self._K = K
|
|
387
|
+
self._M = M
|
|
388
|
+
|
|
389
|
+
start = time.perf_counter()
|
|
390
|
+
try:
|
|
391
|
+
eigvals, eigvecs = self._solve_eigen(
|
|
392
|
+
K, M, num_modes=num_modes, find_smallest=find_smallest
|
|
393
|
+
)
|
|
394
|
+
elapsed = time.perf_counter() - start
|
|
395
|
+
|
|
396
|
+
eigvals_host = np.asarray(self._to_host(eigvals), dtype=OPENSEES_BUFFER_DTYPE)
|
|
397
|
+
eigvecs_host = np.asarray(self._to_host(eigvecs), dtype=OPENSEES_BUFFER_DTYPE)
|
|
398
|
+
|
|
399
|
+
order = np.argsort(eigvals_host)
|
|
400
|
+
if not find_smallest:
|
|
401
|
+
order = order[::-1]
|
|
402
|
+
eigvals_host = eigvals_host[order]
|
|
403
|
+
eigvecs_host = eigvecs_host[:, order]
|
|
404
|
+
|
|
405
|
+
eigenvalues_buf[:] = eigvals_host[:num_modes]
|
|
406
|
+
eigenvectors_buf[:] = eigvecs_host[:, :num_modes].T.reshape(-1)
|
|
407
|
+
|
|
408
|
+
self.stats.num_solves += 1
|
|
409
|
+
self.stats.last_solve_time = elapsed
|
|
410
|
+
self.stats.last_num_modes = num_modes
|
|
411
|
+
self.stats.last_info = 0
|
|
412
|
+
self.stats.last_eigenvalues = eigvals_host[:num_modes].copy()
|
|
413
|
+
self.stats.last_error = None
|
|
414
|
+
except Exception as exc:
|
|
415
|
+
self.stats.last_error = exc
|
|
416
|
+
self.stats.last_info = -1
|
|
417
|
+
raise
|
|
418
|
+
|
|
419
|
+
@abstractmethod
|
|
420
|
+
def _solve_eigen(
|
|
421
|
+
self,
|
|
422
|
+
K: Any,
|
|
423
|
+
M: Any,
|
|
424
|
+
*,
|
|
425
|
+
num_modes: int,
|
|
426
|
+
find_smallest: bool,
|
|
427
|
+
) -> tuple[Any, Any]:
|
|
428
|
+
"""Return (eigenvalues, eigenvectors) with eigenvectors column-major."""
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Shared NumPy-style docstring fragments for public factory functions."""
|
|
2
|
+
|
|
3
|
+
_OPENSEES_LINEAR = """
|
|
4
|
+
scheme : {'CSR', 'CSC'}, optional
|
|
5
|
+
Sparse storage scheme for :meth:`~openseespy_solvers._base.BaseOpenSeesSolver.to_openseespy`.
|
|
6
|
+
Default is ``'CSR'``.
|
|
7
|
+
writable : str or list of str, optional
|
|
8
|
+
Writable buffers declared to OpenSees. Default is ``'none'``.
|
|
9
|
+
debug : bool, optional
|
|
10
|
+
If ``True``, exceptions raised during :meth:`~openseespy_solvers._base.LinearSolver.solve`
|
|
11
|
+
or :meth:`~openseespy_solvers._base.LinearSolver.formAp` are re-raised. Otherwise a
|
|
12
|
+
negative status code is returned. Default is ``False``.
|
|
13
|
+
dtype : dtype or str, optional
|
|
14
|
+
Floating-point precision for the numerical solve (``float32`` or ``float64``).
|
|
15
|
+
OpenSees buffer I/O remains ``float64``; values are cast at the boundary.
|
|
16
|
+
Default is ``float64``.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
_OPENSEES_EIGEN = """
|
|
20
|
+
scheme : {'CSR', 'CSC'}, optional
|
|
21
|
+
Sparse storage scheme for :meth:`~openseespy_solvers._base.BaseOpenSeesSolver.to_openseespy`.
|
|
22
|
+
Default is ``'CSR'``.
|
|
23
|
+
debug : bool, optional
|
|
24
|
+
If ``True``, exceptions raised during
|
|
25
|
+
:meth:`~openseespy_solvers._base.EigenSolver.solve` are re-raised. Default is ``False``.
|
|
26
|
+
dtype : dtype or str, optional
|
|
27
|
+
Floating-point precision for the eigen solve (``float32`` or ``float64``).
|
|
28
|
+
OpenSees buffer I/O remains ``float64``. Default is ``float64``.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
_LINEAR_RETURNS = """
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
solver : LinearSolver
|
|
35
|
+
Configured linear solver. Pass ``solver.to_openseespy()`` to
|
|
36
|
+
``ops.system('PythonSparse', ...)``.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
_EIGEN_RETURNS = """
|
|
40
|
+
Returns
|
|
41
|
+
-------
|
|
42
|
+
solver : EigenSolver
|
|
43
|
+
Configured eigen solver. Pass ``solver.to_openseespy()`` to
|
|
44
|
+
``ops.eigen('PythonSparse', num_modes, ...)``.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
_LINEAR_NOTES = """
|
|
48
|
+
Notes
|
|
49
|
+
-----
|
|
50
|
+
OpenSees assembles the sparse system matrix and right-hand side and calls
|
|
51
|
+
:meth:`~openseespy_solvers._base.LinearSolver.solve` with buffer memoryviews.
|
|
52
|
+
The solution is written in place to the ``x`` buffer. After each solve,
|
|
53
|
+
``solver.A``, ``solver.b``, and ``solver.x`` refer to the cached matrix and
|
|
54
|
+
vectors from the last call.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
_EIGEN_NOTES = """
|
|
58
|
+
Notes
|
|
59
|
+
-----
|
|
60
|
+
OpenSees assembles ``K`` and ``M`` and calls
|
|
61
|
+
:meth:`~openseespy_solvers._base.EigenSolver.solve`. Eigenvalues and
|
|
62
|
+
eigenvectors are written in place to the output buffers. After each solve,
|
|
63
|
+
``solver.K`` and ``solver.M`` refer to the cached matrices from the last call.
|
|
64
|
+
"""
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Compute precision for solver numerics (OpenSees buffers remain float64)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from openseespy_solvers.exceptions import UnsupportedComputeDtypeError
|
|
10
|
+
|
|
11
|
+
# OpenSees PythonSparse memoryviews are double precision.
|
|
12
|
+
OPENSEES_BUFFER_DTYPE = np.dtype(np.float64)
|
|
13
|
+
|
|
14
|
+
_ALLOWED_COMPUTE_DTYPES = frozenset({np.dtype(np.float32), np.dtype(np.float64)})
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def resolve_compute_dtype(dtype: Any = np.float64) -> np.dtype:
|
|
18
|
+
"""Normalize ``dtype`` to ``float32`` or ``float64`` for internal solves.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
dtype : dtype or str, optional
|
|
23
|
+
``numpy.float32``, ``numpy.float64``, ``'float32'``, ``'f32'``, etc.
|
|
24
|
+
Default is ``float64``.
|
|
25
|
+
|
|
26
|
+
Returns
|
|
27
|
+
-------
|
|
28
|
+
numpy.dtype
|
|
29
|
+
Resolved compute dtype.
|
|
30
|
+
|
|
31
|
+
Raises
|
|
32
|
+
------
|
|
33
|
+
UnsupportedComputeDtypeError
|
|
34
|
+
If *dtype* is not single- or double-precision float.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
resolved = np.dtype(dtype)
|
|
38
|
+
except TypeError as exc:
|
|
39
|
+
raise UnsupportedComputeDtypeError(f"Invalid dtype {dtype!r}") from exc
|
|
40
|
+
if resolved not in _ALLOWED_COMPUTE_DTYPES:
|
|
41
|
+
raise UnsupportedComputeDtypeError(
|
|
42
|
+
f"Compute dtype must be float32 or float64, got {resolved!r}. "
|
|
43
|
+
"OpenSees buffers are always float64; lower precision applies inside the solver."
|
|
44
|
+
)
|
|
45
|
+
return resolved
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Apply an inner direct solver's cached factorization to a vector."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from openseespy_solvers._base import LinearSolver
|
|
10
|
+
from openseespy_solvers._sparse import _host_array, csr_linear_kwargs_from_matrix
|
|
11
|
+
from openseespy_solvers.exceptions import SolverConvergenceError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def apply_inner_factorization(
|
|
15
|
+
inner: LinearSolver,
|
|
16
|
+
matrix: Any,
|
|
17
|
+
vec: Any,
|
|
18
|
+
*,
|
|
19
|
+
refactor: bool,
|
|
20
|
+
on_device: bool,
|
|
21
|
+
structure_changed: bool = False,
|
|
22
|
+
) -> Any:
|
|
23
|
+
"""Apply ``inner`` direct solver's factorization of ``matrix`` to ``vec``.
|
|
24
|
+
|
|
25
|
+
When ``refactor`` is ``True``, the inner solver (re)factorizes ``matrix`` and
|
|
26
|
+
applies the factorization. When ``False``, the cached factorization is reused
|
|
27
|
+
(``matrix_status='UNCHANGED'``) even if ``matrix`` coefficients were updated
|
|
28
|
+
elsewhere.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
inner : LinearSolver
|
|
33
|
+
Direct solver whose factorization is applied.
|
|
34
|
+
matrix : sparse matrix
|
|
35
|
+
System matrix in the inner solver's backend format.
|
|
36
|
+
vec : array
|
|
37
|
+
Right-hand side vector (host or device).
|
|
38
|
+
refactor : bool
|
|
39
|
+
If ``True``, refresh the factorization before applying it.
|
|
40
|
+
on_device : bool
|
|
41
|
+
If ``True``, call ``inner._solve_system`` without a host round-trip.
|
|
42
|
+
If ``False``, marshal through OpenSees-style buffers and ``inner.solve``.
|
|
43
|
+
structure_changed : bool, optional
|
|
44
|
+
When ``refactor`` is ``True``, use ``STRUCTURE_CHANGED`` instead of
|
|
45
|
+
``COEFFICIENTS_CHANGED``.
|
|
46
|
+
|
|
47
|
+
Returns
|
|
48
|
+
-------
|
|
49
|
+
array
|
|
50
|
+
Solution vector on the inner solver's compute device.
|
|
51
|
+
|
|
52
|
+
Raises
|
|
53
|
+
------
|
|
54
|
+
SolverConvergenceError
|
|
55
|
+
If the inner solve reports failure.
|
|
56
|
+
"""
|
|
57
|
+
if refactor:
|
|
58
|
+
matrix_status = "STRUCTURE_CHANGED" if structure_changed else "COEFFICIENTS_CHANGED"
|
|
59
|
+
else:
|
|
60
|
+
matrix_status = "UNCHANGED"
|
|
61
|
+
|
|
62
|
+
if on_device:
|
|
63
|
+
rhs = inner._to_device(np.asarray(_host_array(vec), dtype=inner._compute_dtype))
|
|
64
|
+
result, info, _ = inner._solve_system(matrix, rhs, None, matrix_status)
|
|
65
|
+
if info != 0:
|
|
66
|
+
raise SolverConvergenceError(
|
|
67
|
+
f"Inner direct solve failed with info={info} (matrix_status={matrix_status!r})"
|
|
68
|
+
)
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
n = matrix.shape[0]
|
|
72
|
+
rhs_host = np.asarray(_host_array(vec), dtype=np.float64).ravel()
|
|
73
|
+
x_host = np.zeros(n, dtype=np.float64)
|
|
74
|
+
lin_kwargs = csr_linear_kwargs_from_matrix(
|
|
75
|
+
matrix,
|
|
76
|
+
rhs_host,
|
|
77
|
+
matrix_status=matrix_status,
|
|
78
|
+
x=x_host,
|
|
79
|
+
)
|
|
80
|
+
info = inner.solve(**lin_kwargs)
|
|
81
|
+
if info != 0:
|
|
82
|
+
raise SolverConvergenceError(
|
|
83
|
+
f"Inner direct solve failed with info={info} (matrix_status={matrix_status!r})"
|
|
84
|
+
)
|
|
85
|
+
solution = np.frombuffer(lin_kwargs["x"], dtype=np.float64, count=n).copy()
|
|
86
|
+
return inner._to_device(solution)
|