pyMOTO 1.2.1__py3-none-any.whl → 1.4.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.
- {pyMOTO-1.2.1.dist-info → pyMOTO-1.4.0.dist-info}/METADATA +7 -8
- pyMOTO-1.4.0.dist-info/RECORD +29 -0
- {pyMOTO-1.2.1.dist-info → pyMOTO-1.4.0.dist-info}/WHEEL +1 -1
- pymoto/__init__.py +19 -13
- pymoto/common/domain.py +75 -0
- pymoto/common/dyadcarrier.py +33 -4
- pymoto/common/mma.py +83 -53
- pymoto/core_objects.py +117 -113
- pymoto/modules/aggregation.py +209 -0
- pymoto/modules/assembly.py +202 -41
- pymoto/modules/complex.py +3 -3
- pymoto/modules/filter.py +171 -24
- pymoto/modules/generic.py +12 -1
- pymoto/modules/io.py +22 -11
- pymoto/modules/linalg.py +24 -118
- pymoto/modules/scaling.py +4 -4
- pymoto/routines.py +32 -15
- pymoto/solvers/__init__.py +14 -0
- pymoto/solvers/auto_determine.py +108 -0
- pymoto/{common/solvers_dense.py → solvers/dense.py} +90 -70
- pymoto/solvers/iterative.py +361 -0
- pymoto/solvers/matrix_checks.py +56 -0
- pymoto/solvers/solvers.py +253 -0
- pymoto/{common/solvers_sparse.py → solvers/sparse.py} +41 -29
- pyMOTO-1.2.1.dist-info/RECORD +0 -24
- pymoto/common/solvers.py +0 -236
- {pyMOTO-1.2.1.dist-info → pyMOTO-1.4.0.dist-info}/LICENSE +0 -0
- {pyMOTO-1.2.1.dist-info → pyMOTO-1.4.0.dist-info}/top_level.txt +0 -0
- {pyMOTO-1.2.1.dist-info → pyMOTO-1.4.0.dist-info}/zip-safe +0 -0
pymoto/modules/linalg.py
CHANGED
@@ -7,10 +7,9 @@ import scipy.linalg as spla # Dense matrix solvers
|
|
7
7
|
import scipy.sparse as sps
|
8
8
|
import scipy.sparse.linalg as spsla
|
9
9
|
|
10
|
-
from pymoto import Signal, Module, DyadCarrier
|
11
|
-
from pymoto import
|
12
|
-
from pymoto import
|
13
|
-
from pymoto import matrix_is_symmetric, matrix_is_hermitian, matrix_is_diagonal
|
10
|
+
from pymoto import Signal, Module, DyadCarrier
|
11
|
+
from pymoto.solvers import auto_determine_solver
|
12
|
+
from pymoto.solvers import matrix_is_hermitian, LDAWrapper
|
14
13
|
|
15
14
|
|
16
15
|
class StaticCondensation(Module):
|
@@ -172,7 +171,7 @@ class SystemOfEquations(Module):
|
|
172
171
|
adjoint_load += self.Afp * dgdb[self.p, ...]
|
173
172
|
|
174
173
|
lam = np.zeros_like(self.x)
|
175
|
-
lamf = -1.0 * self.module_LinSolve.solver.
|
174
|
+
lamf = -1.0 * self.module_LinSolve.solver.solve(adjoint_load, trans='T')
|
176
175
|
lam[self.f, ...] = lamf
|
177
176
|
|
178
177
|
if dgdb is not None:
|
@@ -219,105 +218,6 @@ class Inverse(Module):
|
|
219
218
|
return dA if np.iscomplexobj(A) else np.real(dA)
|
220
219
|
|
221
220
|
|
222
|
-
# flake8: noqa: C901
|
223
|
-
def auto_determine_solver(A, isdiagonal=None, islowertriangular=None, isuppertriangular=None,
|
224
|
-
ishermitian=None, issymmetric=None, ispositivedefinite=None):
|
225
|
-
"""
|
226
|
-
Uses parts of Matlab's scheme https://nl.mathworks.com/help/matlab/ref/mldivide.html
|
227
|
-
:param A: The matrix
|
228
|
-
:param isdiagonal: Manual override for diagonal matrix
|
229
|
-
:param islowertriangular: Override for lower triangular matrix
|
230
|
-
:param isuppertriangular: Override for upper triangular matrix
|
231
|
-
:param ishermitian: Override for hermitian matrix (prevents check)
|
232
|
-
:param issymmetric: Override for symmetric matrix (prevents check). Is the same as hermitian for a real matrix
|
233
|
-
:param ispositivedefinite: Manual override for positive definiteness
|
234
|
-
:return: LinearSolver which should be 'best' for the matrix
|
235
|
-
"""
|
236
|
-
issparse = sps.issparse(A) # Check if the matrix is sparse
|
237
|
-
issquare = A.shape[0] == A.shape[1] # Check if the matrix is square
|
238
|
-
|
239
|
-
if not issquare:
|
240
|
-
if issparse:
|
241
|
-
sps.SparseEfficiencyWarning("Only a dense version of QR solver is available") # TODO
|
242
|
-
return SolverDenseQR()
|
243
|
-
|
244
|
-
# l_bw, u_bw = spla.bandwidth(A) # TODO Get bandwidth (implemented in scipy version > 1.8.0)
|
245
|
-
|
246
|
-
if isdiagonal is None: # Check if matrix is diagonal
|
247
|
-
# TODO: This could be improved to check other sparse matrix types as well
|
248
|
-
isdiagonal = matrix_is_diagonal(A)
|
249
|
-
if isdiagonal:
|
250
|
-
return SolverDiagonal()
|
251
|
-
|
252
|
-
# Check if the matrix is triangular
|
253
|
-
# TODO Currently only for dense matrices
|
254
|
-
if islowertriangular is None: # Check if matrix is lower triangular
|
255
|
-
islowertriangular = False if issparse else np.allclose(A, np.tril(A))
|
256
|
-
if islowertriangular:
|
257
|
-
warnings.WarningMessage("Lower triangular solver not implemented",
|
258
|
-
UserWarning, getframeinfo(currentframe()).filename, getframeinfo(currentframe()).lineno)
|
259
|
-
|
260
|
-
if isuppertriangular is None: # Check if matrix is upper triangular
|
261
|
-
isuppertriangular = False if issparse else np.allclose(A, np.triu(A))
|
262
|
-
if isuppertriangular:
|
263
|
-
warnings.WarningMessage("Upper triangular solver not implemented",
|
264
|
-
UserWarning, getframeinfo(currentframe()).filename, getframeinfo(currentframe()).lineno)
|
265
|
-
|
266
|
-
ispermutedtriangular = False
|
267
|
-
if ispermutedtriangular:
|
268
|
-
warnings.WarningMessage("Permuted triangular solver not implemented",
|
269
|
-
UserWarning, getframeinfo(currentframe()).filename, getframeinfo(currentframe()).lineno)
|
270
|
-
|
271
|
-
# Check if the matrix is complex-valued
|
272
|
-
iscomplex = np.iscomplexobj(A)
|
273
|
-
if iscomplex:
|
274
|
-
# Detect if the matrix is hermitian and/or symmetric
|
275
|
-
if ishermitian is None:
|
276
|
-
ishermitian = matrix_is_hermitian(A)
|
277
|
-
if issymmetric is None:
|
278
|
-
issymmetric = matrix_is_symmetric(A)
|
279
|
-
else:
|
280
|
-
if ishermitian is None and issymmetric is None:
|
281
|
-
# Detect if the matrix is symmetric
|
282
|
-
issymmetric = matrix_is_symmetric(A)
|
283
|
-
ishermitian = issymmetric
|
284
|
-
elif ishermitian is not None and issymmetric is not None:
|
285
|
-
assert ishermitian == issymmetric, "For real-valued matrices, symmetry and hermitian must be equal"
|
286
|
-
elif ishermitian is None:
|
287
|
-
ishermitian = issymmetric
|
288
|
-
elif issymmetric is None:
|
289
|
-
issymmetric = ishermitian
|
290
|
-
|
291
|
-
if issparse:
|
292
|
-
# Prefer Intel Pardiso solver as it can solve any matrix TODO: Check for complex matrix
|
293
|
-
if SolverSparsePardiso.defined and not iscomplex:
|
294
|
-
# TODO check for positive definiteness? np.all(A.diagonal() > 0) or np.all(A.diagonal() < 0)
|
295
|
-
return SolverSparsePardiso(symmetric=issymmetric, hermitian=ishermitian, positive_definite=ispositivedefinite)
|
296
|
-
|
297
|
-
if ishermitian:
|
298
|
-
# Check if diagonal is all positive or all negative -> Cholesky
|
299
|
-
if np.all(A.diagonal() > 0) or np.all(A.diagonal() < 0): # TODO what about the complex case?
|
300
|
-
if SolverSparseCholeskyScikit.defined:
|
301
|
-
return SolverSparseCholeskyScikit()
|
302
|
-
if SolverSparseCholeskyCVXOPT.defined:
|
303
|
-
return SolverSparseCholeskyCVXOPT()
|
304
|
-
|
305
|
-
return SolverSparseLU() # Default to LU, which should be possible for any non-singular square matrix
|
306
|
-
|
307
|
-
else: # Dense branch
|
308
|
-
if ishermitian:
|
309
|
-
# Check if diagonal is all positive or all negative
|
310
|
-
if np.all(A.diagonal() > 0) or np.all(A.diagonal() < 0):
|
311
|
-
return SolverDenseCholesky()
|
312
|
-
else:
|
313
|
-
return SolverDenseLDL(hermitian=ishermitian)
|
314
|
-
elif issymmetric:
|
315
|
-
return SolverDenseLDL(hermitian=ishermitian)
|
316
|
-
else:
|
317
|
-
# TODO: Detect if the matrix is Hessenberg
|
318
|
-
return SolverDenseLU()
|
319
|
-
|
320
|
-
|
321
221
|
class LinSolve(Module):
|
322
222
|
r""" Solves linear system of equations :math:`\mathbf{A}\mathbf{x}=\mathbf{b}`
|
323
223
|
|
@@ -347,6 +247,7 @@ class LinSolve(Module):
|
|
347
247
|
self.ishermitian = hermitian
|
348
248
|
self.issymmetric = symmetric
|
349
249
|
self.solver = solver
|
250
|
+
self.u = None # Solution storage
|
350
251
|
|
351
252
|
def _response(self, mat, rhs):
|
352
253
|
# Do some detections on the matrix type
|
@@ -365,19 +266,23 @@ class LinSolve(Module):
|
|
365
266
|
if self.solver is None:
|
366
267
|
self.solver = auto_determine_solver(mat, ishermitian=self.ishermitian)
|
367
268
|
if not isinstance(self.solver, LDAWrapper) and self.use_lda_solver:
|
368
|
-
|
269
|
+
lda_kwargs = dict(hermitian=self.ishermitian, symmetric=self.issymmetric)
|
270
|
+
if hasattr(self.solver, 'tol'):
|
271
|
+
lda_kwargs['tol'] = self.solver.tol * 2
|
272
|
+
self.solver = LDAWrapper(self.solver, **lda_kwargs)
|
369
273
|
|
370
274
|
# Update solver with new matrix
|
371
275
|
self.solver.update(mat)
|
372
276
|
|
373
277
|
# Solution
|
374
|
-
self.u = self.solver.solve(rhs)
|
278
|
+
self.u = self.solver.solve(rhs, x0=self.u)
|
375
279
|
|
376
280
|
return self.u
|
377
281
|
|
378
282
|
def _sensitivity(self, dfdv):
|
379
283
|
mat, rhs = [s.state for s in self.sig_in]
|
380
|
-
lam = self.solver.
|
284
|
+
# lam = self.solver.solve(dfdv.conj(), trans='H').conj()
|
285
|
+
lam = self.solver.solve(dfdv, trans='T')
|
381
286
|
|
382
287
|
if self.issparse:
|
383
288
|
if self.u.ndim > 1:
|
@@ -431,13 +336,16 @@ class EigenSolve(Module):
|
|
431
336
|
nmodes: Number of modes to calculate (only for sparse matrices, default = ``0``)
|
432
337
|
sigma: Shift value for the eigenvalue problem (only for sparse matrices). Eigenvalues around the shift are
|
433
338
|
calculated first (default = ``0.0``)
|
339
|
+
mode: Mode of the eigensolver (see documentation of `scipy.sparse.linalg.eigsh` for more info)
|
434
340
|
"""
|
435
|
-
def _prepare(self, sorting_func=lambda W, Q: np.argsort(W), hermitian=None, nmodes=None, sigma=None):
|
341
|
+
def _prepare(self, sorting_func=lambda W, Q: np.argsort(W), hermitian=None, nmodes=None, sigma=None, mode='normal'):
|
436
342
|
self.sorting_fn = sorting_func
|
437
343
|
self.is_hermitian = hermitian
|
438
344
|
self.nmodes = nmodes
|
439
345
|
self.sigma = sigma
|
346
|
+
self.mode = mode
|
440
347
|
self.Ainv = None
|
348
|
+
self.do_solve = False
|
441
349
|
|
442
350
|
def _response(self, A, *args):
|
443
351
|
B = args[0] if len(args) > 0 else None
|
@@ -462,13 +370,6 @@ class EigenSolve(Module):
|
|
462
370
|
qi *= np.sign(np.real(qi[np.argmax(abs(qi) > 0)])) # Set first value positive for orientation
|
463
371
|
Bqi = qi if B is None else B@qi
|
464
372
|
qi /= np.sqrt(qi@Bqi) # Normalize
|
465
|
-
|
466
|
-
eigvec_nrm = abs(qi@(qi if B is None else B@qi) - 1.0)
|
467
|
-
if eigvec_nrm > 1e-5:
|
468
|
-
warnings.warn(f"Eigenvector {i} normalization error large: |v^T{'B'if len(args)>0 else ''}v|-1 = {eigvec_nrm}")
|
469
|
-
resi_nrm = np.linalg.norm(A@qi - wi*(qi if B is None else B@qi)) / np.linalg.norm(A@qi)
|
470
|
-
if resi_nrm > 1e-5:
|
471
|
-
warnings.warn(f"Eigenvalue {i} residual large: |Av - λ{'B'if len(args)>0 else ''}v|/|Av| = {resi_nrm}")
|
472
373
|
return W, Q
|
473
374
|
|
474
375
|
def _sensitivity(self, dW, dQ):
|
@@ -505,12 +406,17 @@ class EigenSolve(Module):
|
|
505
406
|
# Use shift-and-invert, so make inverse operator
|
506
407
|
if self.Ainv is None:
|
507
408
|
self.Ainv = auto_determine_solver(mat_shifted, ishermitian=self.is_hermitian)
|
508
|
-
|
409
|
+
self.do_solve = True
|
410
|
+
if self.sigma != 0:
|
411
|
+
self.do_solve = True
|
412
|
+
if self.do_solve:
|
413
|
+
self.Ainv.update(mat_shifted)
|
509
414
|
|
510
|
-
AinvOp = spsla.LinearOperator(mat_shifted.shape, matvec=self.Ainv.solve,
|
415
|
+
AinvOp = spsla.LinearOperator(mat_shifted.shape, matvec=self.Ainv.solve,
|
416
|
+
rmatvec=lambda b: self.Ainv.solve(b, trans='H'))
|
511
417
|
|
512
418
|
if self.is_hermitian:
|
513
|
-
return spsla.eigsh(A, M=B, k=self.nmodes, OPinv=AinvOp, sigma=self.sigma)
|
419
|
+
return spsla.eigsh(A, M=B, k=self.nmodes, OPinv=AinvOp, sigma=self.sigma, mode=self.mode)
|
514
420
|
else:
|
515
421
|
# TODO
|
516
422
|
raise NotImplementedError('Non-Hermitian sparse matrix not supported')
|
pymoto/modules/scaling.py
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
from pymoto import Module
|
2
|
-
|
2
|
+
import numpy as np
|
3
3
|
|
4
4
|
class Scaling(Module):
|
5
5
|
r""" Scales (scalar) input for different response functions in optimization (objective / constraints).
|
6
6
|
This is useful, for instance, for MMA where the objective must be scaled in a certain way for good convergence.
|
7
7
|
|
8
|
-
Objective scaling (`minval` and `maxval` are both undefined):
|
9
|
-
:math:`y^{(i)} = s \frac{x^{(i)}}{x^{(0)}}`
|
8
|
+
Objective scaling using absolute value or vector norm (`minval` and `maxval` are both undefined):
|
9
|
+
:math:`y^{(i)} = s \frac{x^{(i)}}{||x^{(0)}||}`
|
10
10
|
|
11
11
|
For the constraints, the negative null-form convention is used, which means the constraint is :math:`y(x) \leq 0`.
|
12
12
|
|
@@ -39,7 +39,7 @@ class Scaling(Module):
|
|
39
39
|
|
40
40
|
def _response(self, x):
|
41
41
|
if not hasattr(self, 'sf'):
|
42
|
-
self.sf = self.scaling/x
|
42
|
+
self.sf = self.scaling/np.linalg.norm(x)
|
43
43
|
if self.minval is not None:
|
44
44
|
g = 1 - x/self.minval
|
45
45
|
elif self.maxval is not None:
|
pymoto/routines.py
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
+
import warnings
|
1
2
|
import numpy as np
|
2
3
|
from .utils import _parse_to_list, _concatenate_to_array
|
3
4
|
from .core_objects import Signal, SignalSlice, Module, Network
|
4
5
|
from .common.mma import MMA
|
5
6
|
from typing import List, Iterable, Union, Callable
|
7
|
+
from scipy.sparse import issparse
|
6
8
|
|
7
9
|
|
8
10
|
def _has_signal_overlap(sig1: List[Signal], sig2: List[Signal]):
|
@@ -20,8 +22,8 @@ def _has_signal_overlap(sig1: List[Signal], sig2: List[Signal]):
|
|
20
22
|
# flake8: noqa: C901
|
21
23
|
def finite_difference(blk: Module, fromsig: Union[Signal, Iterable[Signal]] = None,
|
22
24
|
tosig: Union[Signal, Iterable[Signal]] = None,
|
23
|
-
dx: float = 1e-8, tol: float = 1e-5, random: bool = True,
|
24
|
-
test_fn: Callable = None, keep_zero_structure=True, verbose=True):
|
25
|
+
dx: float = 1e-8, relative_dx: bool = False, tol: float = 1e-5, random: bool = True,
|
26
|
+
use_df: list = None, test_fn: Callable = None, keep_zero_structure=True, verbose=True):
|
25
27
|
""" Performs a finite difference check on the given Module or Network
|
26
28
|
|
27
29
|
Args:
|
@@ -31,6 +33,7 @@ def finite_difference(blk: Module, fromsig: Union[Signal, Iterable[Signal]] = No
|
|
31
33
|
|
32
34
|
Keyword Args:
|
33
35
|
dx: Perturbation size
|
36
|
+
relative_dx: Use a relative perturbation size or not
|
34
37
|
tol: Tolerance
|
35
38
|
random: Randomize sensitivity data
|
36
39
|
use_df: Give pre-defined sensitivity data
|
@@ -83,15 +86,15 @@ def finite_difference(blk: Module, fromsig: Union[Signal, Iterable[Signal]] = No
|
|
83
86
|
blk.response()
|
84
87
|
|
85
88
|
print("Outputs:")
|
86
|
-
if
|
89
|
+
if verbose:
|
90
|
+
[print("{}\t{} = {}".format(i, s.tag, s.state)) for i, s in enumerate(outps)]
|
91
|
+
else:
|
87
92
|
print(", ".join([s.tag for s in outps]))
|
88
93
|
|
89
94
|
# Get analytical response and sensitivities, by looping over all outputs
|
90
95
|
for Iout, Sout in enumerate(outps):
|
91
96
|
# Obtain the output state
|
92
97
|
output = Sout.state
|
93
|
-
if verbose:
|
94
|
-
print("{}\t{} = {}".format(Iout, Sout.tag, output))
|
95
98
|
|
96
99
|
# Store the output value
|
97
100
|
f0[Iout] = (output.copy() if hasattr(output, "copy") else output)
|
@@ -153,11 +156,13 @@ def finite_difference(blk: Module, fromsig: Union[Signal, Iterable[Signal]] = No
|
|
153
156
|
if x0 == 0 and keep_zero_structure:
|
154
157
|
it.iternext()
|
155
158
|
continue
|
156
|
-
|
159
|
+
sf = np.abs(x0) if (relative_dx and np.abs(x0)!=0) else 1.0 # Scale factor
|
160
|
+
it[0] += dx*sf
|
157
161
|
Sin.state = x
|
158
162
|
else:
|
159
163
|
x0 = it[0].item()
|
160
|
-
|
164
|
+
sf = np.abs(x0) if (relative_dx and np.abs(x0) != 0) else 1.0
|
165
|
+
Sin.state = x0 + dx*sf
|
161
166
|
|
162
167
|
# Calculate perturbed solution
|
163
168
|
blk.response()
|
@@ -168,7 +173,10 @@ def finite_difference(blk: Module, fromsig: Union[Signal, Iterable[Signal]] = No
|
|
168
173
|
fp = Sout.state
|
169
174
|
|
170
175
|
# Finite difference sensitivity
|
171
|
-
|
176
|
+
if issparse(fp):
|
177
|
+
df = (fp.toarray() - f0[Iout].toarray()) / (dx * sf)
|
178
|
+
else:
|
179
|
+
df = (fp - f0[Iout])/(dx*sf)
|
172
180
|
|
173
181
|
dgdx_fd = np.real(np.sum(df*df_an[Iout]))
|
174
182
|
|
@@ -180,7 +188,7 @@ def finite_difference(blk: Module, fromsig: Union[Signal, Iterable[Signal]] = No
|
|
180
188
|
else:
|
181
189
|
dgdx_an = 0.0
|
182
190
|
|
183
|
-
if abs(dgdx_an)
|
191
|
+
if abs(dgdx_an) == 0:
|
184
192
|
error = abs(dgdx_fd - dgdx_an)
|
185
193
|
else:
|
186
194
|
error = abs(dgdx_fd - dgdx_an)/max(abs(dgdx_fd), abs(dgdx_an))
|
@@ -207,9 +215,9 @@ def finite_difference(blk: Module, fromsig: Union[Signal, Iterable[Signal]] = No
|
|
207
215
|
if np.iscomplexobj(x0):
|
208
216
|
# Do the perturbation
|
209
217
|
if is_iterable:
|
210
|
-
it[0] += dx*1j
|
218
|
+
it[0] += dx*1j*sf
|
211
219
|
else:
|
212
|
-
Sin.state = x0 + dx*1j
|
220
|
+
Sin.state = x0 + dx*1j*sf
|
213
221
|
|
214
222
|
# Calculate perturbed solution
|
215
223
|
blk.response()
|
@@ -220,7 +228,10 @@ def finite_difference(blk: Module, fromsig: Union[Signal, Iterable[Signal]] = No
|
|
220
228
|
fp = Sout.state
|
221
229
|
|
222
230
|
# Finite difference sensitivity
|
223
|
-
|
231
|
+
if issparse(fp):
|
232
|
+
df = (fp.toarray() - f0[Iout].toarray()) / (dx * 1j * sf)
|
233
|
+
else:
|
234
|
+
df = (fp - f0[Iout])/(dx*1j*sf)
|
224
235
|
dgdx_fd = np.imag(np.sum(df*df_an[Iout]))
|
225
236
|
|
226
237
|
if dx_an[Iout][Iin] is not None:
|
@@ -332,8 +343,9 @@ def minimize_oc(function, variables, objective: Signal,
|
|
332
343
|
function.sensitivity()
|
333
344
|
dfdx, _ = _concatenate_to_array(obtain_sensitivities(variables))
|
334
345
|
maxdfdx = max(dfdx)
|
335
|
-
if maxdfdx >
|
336
|
-
|
346
|
+
if maxdfdx > 1e-15:
|
347
|
+
warnings.warn(f"OC only works for negative sensitivities: max(dfdx) = {maxdfdx}. Clipping positive values.")
|
348
|
+
dfdx = np.minimum(dfdx, 0)
|
337
349
|
|
338
350
|
# Do OC update
|
339
351
|
l1, l2 = l1init, l2init
|
@@ -371,7 +383,12 @@ def minimize_mma(function, variables, responses, **kwargs):
|
|
371
383
|
move: Move limit on relative variable change per iteration
|
372
384
|
xmin: Minimum design variable (can be a vector)
|
373
385
|
xmax: Maximum design variable (can be a vector)
|
374
|
-
verbosity:
|
386
|
+
verbosity: Level of information to print
|
387
|
+
0 - No prints
|
388
|
+
1 - Only convergence message
|
389
|
+
2 - Convergence and iteration info (default)
|
390
|
+
3 - Additional info on variables
|
391
|
+
4 - Additional info on sensitivity information
|
375
392
|
|
376
393
|
"""
|
377
394
|
# Save initial state
|
@@ -0,0 +1,14 @@
|
|
1
|
+
from .solvers import LinearSolver, LDAWrapper
|
2
|
+
from .matrix_checks import matrix_is_complex, matrix_is_diagonal, matrix_is_symmetric, matrix_is_hermitian
|
3
|
+
from .dense import SolverDiagonal, SolverDenseQR, SolverDenseLU, SolverDenseCholesky, SolverDenseLDL
|
4
|
+
from .sparse import SolverSparsePardiso, SolverSparseLU, SolverSparseCholeskyScikit, SolverSparseCholeskyCVXOPT
|
5
|
+
from .iterative import Preconditioner, CG, DampedJacobi, SOR, ILU, GeometricMultigrid
|
6
|
+
from .auto_determine import auto_determine_solver
|
7
|
+
|
8
|
+
__all__ = ['matrix_is_complex', 'matrix_is_diagonal', 'matrix_is_symmetric', 'matrix_is_hermitian',
|
9
|
+
'LinearSolver', 'LDAWrapper',
|
10
|
+
'SolverDiagonal', 'SolverDenseQR', 'SolverDenseLU', 'SolverDenseCholesky', 'SolverDenseLDL',
|
11
|
+
'SolverSparsePardiso', 'SolverSparseLU', 'SolverSparseCholeskyScikit', 'SolverSparseCholeskyCVXOPT',
|
12
|
+
'Preconditioner', 'CG', 'DampedJacobi', 'SOR', 'ILU', 'GeometricMultigrid',
|
13
|
+
'auto_determine_solver',
|
14
|
+
]
|
@@ -0,0 +1,108 @@
|
|
1
|
+
import warnings
|
2
|
+
import scipy.sparse as sps
|
3
|
+
from inspect import currentframe, getframeinfo
|
4
|
+
|
5
|
+
from .dense import *
|
6
|
+
from .sparse import *
|
7
|
+
from .matrix_checks import *
|
8
|
+
|
9
|
+
|
10
|
+
# flake8: noqa: C901
|
11
|
+
def auto_determine_solver(A, isdiagonal=None, islowertriangular=None, isuppertriangular=None,
|
12
|
+
ishermitian=None, issymmetric=None, ispositivedefinite=None):
|
13
|
+
"""
|
14
|
+
Uses parts of Matlab's scheme https://nl.mathworks.com/help/matlab/ref/mldivide.html
|
15
|
+
:param A: The matrix
|
16
|
+
:param isdiagonal: Manual override for diagonal matrix
|
17
|
+
:param islowertriangular: Override for lower triangular matrix
|
18
|
+
:param isuppertriangular: Override for upper triangular matrix
|
19
|
+
:param ishermitian: Override for hermitian matrix (prevents check)
|
20
|
+
:param issymmetric: Override for symmetric matrix (prevents check). Is the same as hermitian for a real matrix
|
21
|
+
:param ispositivedefinite: Manual override for positive definiteness
|
22
|
+
:return: LinearSolver which should be 'best' for the matrix
|
23
|
+
"""
|
24
|
+
issparse = sps.issparse(A) # Check if the matrix is sparse
|
25
|
+
issquare = A.shape[0] == A.shape[1] # Check if the matrix is square
|
26
|
+
|
27
|
+
if not issquare:
|
28
|
+
if issparse:
|
29
|
+
sps.SparseEfficiencyWarning("Only a dense version of QR solver is available") # TODO
|
30
|
+
return SolverDenseQR()
|
31
|
+
|
32
|
+
# l_bw, u_bw = spla.bandwidth(A) # TODO Get bandwidth (implemented in scipy version > 1.8.0)
|
33
|
+
|
34
|
+
if isdiagonal is None: # Check if matrix is diagonal
|
35
|
+
# TODO: This could be improved to check other sparse matrix types as well
|
36
|
+
isdiagonal = matrix_is_diagonal(A)
|
37
|
+
if isdiagonal:
|
38
|
+
return SolverDiagonal()
|
39
|
+
|
40
|
+
# Check if the matrix is triangular
|
41
|
+
# TODO Currently only for dense matrices
|
42
|
+
if islowertriangular is None: # Check if matrix is lower triangular
|
43
|
+
islowertriangular = False if issparse else np.allclose(A, np.tril(A))
|
44
|
+
if islowertriangular:
|
45
|
+
warnings.WarningMessage("Lower triangular solver not implemented",
|
46
|
+
UserWarning, getframeinfo(currentframe()).filename, getframeinfo(currentframe()).lineno)
|
47
|
+
|
48
|
+
if isuppertriangular is None: # Check if matrix is upper triangular
|
49
|
+
isuppertriangular = False if issparse else np.allclose(A, np.triu(A))
|
50
|
+
if isuppertriangular:
|
51
|
+
warnings.WarningMessage("Upper triangular solver not implemented",
|
52
|
+
UserWarning, getframeinfo(currentframe()).filename, getframeinfo(currentframe()).lineno)
|
53
|
+
|
54
|
+
ispermutedtriangular = False
|
55
|
+
if ispermutedtriangular:
|
56
|
+
warnings.WarningMessage("Permuted triangular solver not implemented",
|
57
|
+
UserWarning, getframeinfo(currentframe()).filename, getframeinfo(currentframe()).lineno)
|
58
|
+
|
59
|
+
# Check if the matrix is complex-valued
|
60
|
+
iscomplex = np.iscomplexobj(A)
|
61
|
+
if iscomplex:
|
62
|
+
# Detect if the matrix is hermitian and/or symmetric
|
63
|
+
if ishermitian is None:
|
64
|
+
ishermitian = matrix_is_hermitian(A)
|
65
|
+
if issymmetric is None:
|
66
|
+
issymmetric = matrix_is_symmetric(A)
|
67
|
+
else:
|
68
|
+
if ishermitian is None and issymmetric is None:
|
69
|
+
# Detect if the matrix is symmetric
|
70
|
+
issymmetric = matrix_is_symmetric(A)
|
71
|
+
ishermitian = issymmetric
|
72
|
+
elif ishermitian is not None and issymmetric is not None:
|
73
|
+
assert ishermitian == issymmetric, "For real-valued matrices, symmetry and hermitian must be equal"
|
74
|
+
elif ishermitian is None:
|
75
|
+
ishermitian = issymmetric
|
76
|
+
elif issymmetric is None:
|
77
|
+
issymmetric = ishermitian
|
78
|
+
|
79
|
+
if issparse:
|
80
|
+
# Prefer Intel Pardiso solver as it can solve any matrix TODO: Check for complex matrix
|
81
|
+
if SolverSparsePardiso.defined and not iscomplex:
|
82
|
+
# TODO check for positive definiteness? np.all(A.diagonal() > 0) or np.all(A.diagonal() < 0)
|
83
|
+
return SolverSparsePardiso(symmetric=issymmetric, hermitian=ishermitian, positive_definite=ispositivedefinite)
|
84
|
+
|
85
|
+
if ishermitian:
|
86
|
+
# Check if diagonal is all positive or all negative -> Cholesky
|
87
|
+
if ispositivedefinite is None:
|
88
|
+
ispositivedefinite = np.all(A.diagonal() > 0) or np.all(A.diagonal() < 0)
|
89
|
+
if ispositivedefinite: # TODO what about the complex case?
|
90
|
+
if SolverSparseCholeskyScikit.defined:
|
91
|
+
return SolverSparseCholeskyScikit()
|
92
|
+
if SolverSparseCholeskyCVXOPT.defined:
|
93
|
+
return SolverSparseCholeskyCVXOPT()
|
94
|
+
|
95
|
+
return SolverSparseLU() # Default to LU, which should be possible for any non-singular square matrix
|
96
|
+
|
97
|
+
else: # Dense branch
|
98
|
+
if ishermitian:
|
99
|
+
# Check if diagonal is all positive or all negative
|
100
|
+
if np.all(A.diagonal() > 0) or np.all(A.diagonal() < 0):
|
101
|
+
return SolverDenseCholesky()
|
102
|
+
else:
|
103
|
+
return SolverDenseLDL(hermitian=ishermitian)
|
104
|
+
elif issymmetric:
|
105
|
+
return SolverDenseLDL(hermitian=ishermitian)
|
106
|
+
else:
|
107
|
+
# TODO: Detect if the matrix is Hessenberg
|
108
|
+
return SolverDenseLU()
|