pyMOTO 1.4.0__py3-none-any.whl → 1.5.1__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.4.0.dist-info → pyMOTO-1.5.1.dist-info}/METADATA +2 -2
- pyMOTO-1.5.1.dist-info/RECORD +29 -0
- {pyMOTO-1.4.0.dist-info → pyMOTO-1.5.1.dist-info}/WHEEL +1 -1
- pymoto/__init__.py +5 -5
- pymoto/common/domain.py +3 -1
- pymoto/common/dyadcarrier.py +56 -27
- pymoto/common/mma.py +70 -79
- pymoto/core_objects.py +103 -47
- pymoto/modules/assembly.py +146 -28
- pymoto/modules/io.py +80 -6
- pymoto/modules/linalg.py +84 -23
- pymoto/modules/scaling.py +2 -1
- pymoto/routines.py +13 -2
- pymoto/solvers/__init__.py +2 -2
- pymoto/solvers/auto_determine.py +1 -1
- pymoto/solvers/iterative.py +20 -16
- pymoto/solvers/matrix_checks.py +7 -3
- pymoto/solvers/solvers.py +44 -13
- pymoto/solvers/sparse.py +11 -0
- pyMOTO-1.4.0.dist-info/RECORD +0 -29
- {pyMOTO-1.4.0.dist-info → pyMOTO-1.5.1.dist-info}/LICENSE +0 -0
- {pyMOTO-1.4.0.dist-info → pyMOTO-1.5.1.dist-info}/top_level.txt +0 -0
- {pyMOTO-1.4.0.dist-info → pyMOTO-1.5.1.dist-info}/zip-safe +0 -0
pymoto/modules/linalg.py
CHANGED
@@ -9,7 +9,7 @@ import scipy.sparse.linalg as spsla
|
|
9
9
|
|
10
10
|
from pymoto import Signal, Module, DyadCarrier
|
11
11
|
from pymoto.solvers import auto_determine_solver
|
12
|
-
from pymoto.solvers import matrix_is_hermitian, LDAWrapper
|
12
|
+
from pymoto.solvers import matrix_is_sparse, matrix_is_complex, matrix_is_hermitian, LDAWrapper
|
13
13
|
|
14
14
|
|
15
15
|
class StaticCondensation(Module):
|
@@ -251,8 +251,8 @@ class LinSolve(Module):
|
|
251
251
|
|
252
252
|
def _response(self, mat, rhs):
|
253
253
|
# Do some detections on the matrix type
|
254
|
-
self.issparse =
|
255
|
-
self.iscomplex =
|
254
|
+
self.issparse = matrix_is_sparse(mat) # Check if it is a sparse matrix
|
255
|
+
self.iscomplex = matrix_is_complex(mat) # Check if it is a complex-valued matrix
|
256
256
|
if not self.iscomplex and self.issymmetric is not None:
|
257
257
|
self.ishermitian = self.issymmetric
|
258
258
|
if self.ishermitian is None:
|
@@ -268,7 +268,7 @@ class LinSolve(Module):
|
|
268
268
|
if not isinstance(self.solver, LDAWrapper) and self.use_lda_solver:
|
269
269
|
lda_kwargs = dict(hermitian=self.ishermitian, symmetric=self.issymmetric)
|
270
270
|
if hasattr(self.solver, 'tol'):
|
271
|
-
lda_kwargs['tol'] = self.solver.tol *
|
271
|
+
lda_kwargs['tol'] = self.solver.tol * 5
|
272
272
|
self.solver = LDAWrapper(self.solver, **lda_kwargs)
|
273
273
|
|
274
274
|
# Update solver with new matrix
|
@@ -318,9 +318,6 @@ class EigenSolve(Module):
|
|
318
318
|
Mode tracking algorithms can be implemented by the user by providing the argument ``sorting_func``, which is a
|
319
319
|
function with arguments (``λ``, ``Q``).
|
320
320
|
|
321
|
-
Todo:
|
322
|
-
Support for sparse matrix
|
323
|
-
|
324
321
|
Input Signal(s):
|
325
322
|
- ``A`` (`dense matrix`): The system matrix of size ``(n, n)``
|
326
323
|
- ``B`` (`dense matrix, optional`): Second system matrix (must be positive-definite) of size ``(n, n)``
|
@@ -346,12 +343,14 @@ class EigenSolve(Module):
|
|
346
343
|
self.mode = mode
|
347
344
|
self.Ainv = None
|
348
345
|
self.do_solve = False
|
346
|
+
self.adjoint_solvers_need_update = True
|
349
347
|
|
350
348
|
def _response(self, A, *args):
|
351
349
|
B = args[0] if len(args) > 0 else None
|
352
350
|
if self.is_hermitian is None:
|
353
351
|
self.is_hermitian = (matrix_is_hermitian(A) and (B is None or matrix_is_hermitian(B)))
|
354
|
-
self.is_sparse =
|
352
|
+
self.is_sparse = matrix_is_sparse(A) and (B is None or matrix_is_sparse(B))
|
353
|
+
self.adjoint_solvers_need_update = True
|
355
354
|
|
356
355
|
# Solve the eigenvalue problem
|
357
356
|
if self.is_sparse:
|
@@ -364,12 +363,20 @@ class EigenSolve(Module):
|
|
364
363
|
W = W[isort]
|
365
364
|
Q = Q[:, isort]
|
366
365
|
|
367
|
-
# Normalize the eigenvectors
|
366
|
+
# Normalize the eigenvectors with the following conditions:
|
367
|
+
# 1) Flip sign such that the average (real) value is positive
|
368
|
+
# 2) Normalize the eigenvector v⋅v or v⋅Bv to unity
|
368
369
|
for i in range(W.size):
|
369
370
|
qi, wi = Q[:, i], W[i]
|
370
|
-
qi *= np.sign(np.real(qi[np.argmax(abs(qi) > 0)])) # Set first value positive for orientation
|
371
371
|
Bqi = qi if B is None else B@qi
|
372
|
-
|
372
|
+
|
373
|
+
normval = np.sqrt(qi @ Bqi)
|
374
|
+
sgn = 1 if np.real(np.average(qi)) >= 0 else -1
|
375
|
+
sf = sgn / normval
|
376
|
+
assert np.isfinite(sf)
|
377
|
+
if sf == 0.0:
|
378
|
+
warnings.warn(f"Scaling factor of mode {i} is zero!")
|
379
|
+
qi *= sf
|
373
380
|
return W, Q
|
374
381
|
|
375
382
|
def _sensitivity(self, dW, dQ):
|
@@ -380,7 +387,8 @@ class EigenSolve(Module):
|
|
380
387
|
dA, dB = self._dense_sens(A, B, dW, dQ)
|
381
388
|
else:
|
382
389
|
if dQ is not None:
|
383
|
-
raise NotImplementedError('Sparse eigenvector sensitivities not implemented')
|
390
|
+
# raise NotImplementedError('Sparse eigenvector sensitivities not implemented')
|
391
|
+
dA, dB = self._sparse_eigvec_sens(A, B, dW, dQ)
|
384
392
|
elif dW is not None:
|
385
393
|
dA, dB = self._sparse_eigval_sens(A, B, dW)
|
386
394
|
|
@@ -389,7 +397,6 @@ class EigenSolve(Module):
|
|
389
397
|
elif len(self.sig_in) == 2:
|
390
398
|
return dA, dB
|
391
399
|
|
392
|
-
|
393
400
|
def _sparse_eigs(self, A, B=None):
|
394
401
|
if self.nmodes is None:
|
395
402
|
self.nmodes = 6
|
@@ -418,8 +425,9 @@ class EigenSolve(Module):
|
|
418
425
|
if self.is_hermitian:
|
419
426
|
return spsla.eigsh(A, M=B, k=self.nmodes, OPinv=AinvOp, sigma=self.sigma, mode=self.mode)
|
420
427
|
else:
|
421
|
-
|
422
|
-
|
428
|
+
if self.mode.lower() not in ['normal']:
|
429
|
+
raise NotImplementedError('Only `normal` mode can be selected for non-hermitian matrix')
|
430
|
+
return spsla.eigs(A, M=B, k=self.nmodes, OPinv=AinvOp, sigma=self.sigma)
|
423
431
|
|
424
432
|
def _dense_sens(self, A, B, dW, dQ):
|
425
433
|
""" Calculates all (eigenvector and eigenvalue) sensitivities for dense matrix """
|
@@ -460,16 +468,69 @@ class EigenSolve(Module):
|
|
460
468
|
qi = Q[:, i]
|
461
469
|
qmq = qi@qi if B is None else qi @ (B @ qi)
|
462
470
|
dA_u = (dwi/qmq) * qi
|
463
|
-
|
464
|
-
|
465
|
-
else:
|
466
|
-
dA += DyadCarrier(dA_u, qi)
|
471
|
+
dAi = DyadCarrier(dA_u, qi)
|
472
|
+
dA += np.real(dAi) if np.isrealobj(A) else dAi
|
467
473
|
|
468
474
|
if dB is not None:
|
469
475
|
dB_u = (wi*dwi/qmq) * qi
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
dB -= DyadCarrier(dB_u, qi)
|
476
|
+
dBi = DyadCarrier(dB_u, qi)
|
477
|
+
dB -= np.real(dBi) if np.isrealobj(B) else dBi
|
478
|
+
|
474
479
|
return dA, dB
|
475
480
|
|
481
|
+
def _sparse_eigvec_sens(self, A, B, dW, dQ):
|
482
|
+
""" Calculate eigenvector sensitivities for a sparse eigenvalue problem
|
483
|
+
References:
|
484
|
+
Delissen (2022), Topology optimization for dynamic and controlled systems,
|
485
|
+
doi: https://doi.org/10.4233/uuid:c9ed8f61-efe1-4dc8-bb56-e353546cf247
|
486
|
+
|
487
|
+
Args:
|
488
|
+
A: System matrix
|
489
|
+
B: Mass matrix
|
490
|
+
dW: Adjoint eigenvalue sensitivities
|
491
|
+
dQ: Adjoint eigenvector sensitivities
|
492
|
+
|
493
|
+
Returns:
|
494
|
+
dA: Adjoint system matrix sensitivities
|
495
|
+
dB: Adjoint mass matrix sensitivities
|
496
|
+
"""
|
497
|
+
if dQ is None:
|
498
|
+
return self._sparse_eigval_sens(A, B, dW)
|
499
|
+
W, Q = [s.state for s in self.sig_out]
|
500
|
+
if dW is not None:
|
501
|
+
dA, dB = self._sparse_eigval_sens(A, B, dW)
|
502
|
+
else:
|
503
|
+
dA, dB = DyadCarrier(), None if B is None else DyadCarrier()
|
504
|
+
for i in range(W.size):
|
505
|
+
phi = Q[:, i]
|
506
|
+
dphi = dQ[:, i]
|
507
|
+
if dphi.min() == dphi.max() == 0.0:
|
508
|
+
continue
|
509
|
+
lam = W[i]
|
510
|
+
|
511
|
+
alpha = - phi @ dphi
|
512
|
+
r = dphi + alpha * B.T @ phi
|
513
|
+
|
514
|
+
# Solve particular solution
|
515
|
+
if self.adjoint_solvers_need_update or self.solvers[i] is None:
|
516
|
+
Z = A - lam * B
|
517
|
+
if not hasattr(self, 'solvers'):
|
518
|
+
self.solvers = [None for _ in range(W.size)]
|
519
|
+
if self.solvers[i] is None: # Solver must be able to solve indefinite system
|
520
|
+
self.solvers[i] = auto_determine_solver(Z, ispositivedefinite=False)
|
521
|
+
if self.adjoint_solvers_need_update:
|
522
|
+
self.solvers[i].update(Z)
|
523
|
+
|
524
|
+
vp = self.solvers[i].solve(r, trans='T')
|
525
|
+
|
526
|
+
# Calculate total ajoint by adding homogeneous solution
|
527
|
+
c = - vp @ B @ phi
|
528
|
+
v = vp + c * phi
|
529
|
+
|
530
|
+
# Add to mass and stiffness matrix
|
531
|
+
dAi = - DyadCarrier(v, phi)
|
532
|
+
dA += np.real(dAi) if np.isrealobj(A) else dAi
|
533
|
+
if B is not None:
|
534
|
+
dBi = DyadCarrier(alpha / 2 * phi + lam * v, phi)
|
535
|
+
dB += np.real(dBi) if np.isrealobj(B) else dBi
|
536
|
+
return dA, dB
|
pymoto/modules/scaling.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
from pymoto import Module
|
2
2
|
import numpy as np
|
3
3
|
|
4
|
+
|
4
5
|
class Scaling(Module):
|
5
6
|
r""" Scales (scalar) input for different response functions in optimization (objective / constraints).
|
6
7
|
This is useful, for instance, for MMA where the objective must be scaled in a certain way for good convergence.
|
@@ -25,7 +26,7 @@ class Scaling(Module):
|
|
25
26
|
Keyword Args:
|
26
27
|
scaling: Value :math:`s` to scale with
|
27
28
|
minval: Minimum value :math:`x_\text{min}` for negative-null-form constraint
|
28
|
-
|
29
|
+
maxval: Maximum value :math:`x_\text{max}` for negative-null-form constraint
|
29
30
|
"""
|
30
31
|
def _prepare(self, scaling: float = 100.0, minval: float = None, maxval: float = None):
|
31
32
|
self.minval = minval
|
pymoto/routines.py
CHANGED
@@ -10,10 +10,10 @@ from scipy.sparse import issparse
|
|
10
10
|
def _has_signal_overlap(sig1: List[Signal], sig2: List[Signal]):
|
11
11
|
for s1 in sig1:
|
12
12
|
while isinstance(s1, SignalSlice):
|
13
|
-
s1 = s1.
|
13
|
+
s1 = s1.base
|
14
14
|
for s2 in sig2:
|
15
15
|
while isinstance(s2, SignalSlice):
|
16
|
-
s2 = s2.
|
16
|
+
s2 = s2.base
|
17
17
|
if s1 == s2:
|
18
18
|
return True
|
19
19
|
return False
|
@@ -102,6 +102,10 @@ def finite_difference(blk: Module, fromsig: Union[Signal, Iterable[Signal]] = No
|
|
102
102
|
# Get the output state shape
|
103
103
|
shape = (output.shape if hasattr(output, "shape") else ())
|
104
104
|
|
105
|
+
if output is None:
|
106
|
+
warnings.warn(f"Output {Iout} of {Sout.tag} is None")
|
107
|
+
continue
|
108
|
+
|
105
109
|
# Generate a (random) sensitivity for output signal
|
106
110
|
if use_df is not None:
|
107
111
|
df_an[Iout] = use_df[Iout]
|
@@ -171,6 +175,9 @@ def finite_difference(blk: Module, fromsig: Union[Signal, Iterable[Signal]] = No
|
|
171
175
|
for Iout, Sout in enumerate(outps):
|
172
176
|
# Obtain perturbed response
|
173
177
|
fp = Sout.state
|
178
|
+
if fp is None:
|
179
|
+
warnings.warn(f"Output {Iout} of {Sout.tag} is None")
|
180
|
+
continue
|
174
181
|
|
175
182
|
# Finite difference sensitivity
|
176
183
|
if issparse(fp):
|
@@ -227,6 +234,10 @@ def finite_difference(blk: Module, fromsig: Union[Signal, Iterable[Signal]] = No
|
|
227
234
|
# Obtain perturbed response
|
228
235
|
fp = Sout.state
|
229
236
|
|
237
|
+
if fp is None:
|
238
|
+
warnings.warn(f"Output {Iout} of {Sout.tag} is None")
|
239
|
+
continue
|
240
|
+
|
230
241
|
# Finite difference sensitivity
|
231
242
|
if issparse(fp):
|
232
243
|
df = (fp.toarray() - f0[Iout].toarray()) / (dx * 1j * sf)
|
pymoto/solvers/__init__.py
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
from .solvers import LinearSolver, LDAWrapper
|
2
|
-
from .matrix_checks import matrix_is_complex, matrix_is_diagonal, matrix_is_symmetric, matrix_is_hermitian
|
2
|
+
from .matrix_checks import matrix_is_sparse, matrix_is_complex, matrix_is_diagonal, matrix_is_symmetric, matrix_is_hermitian
|
3
3
|
from .dense import SolverDiagonal, SolverDenseQR, SolverDenseLU, SolverDenseCholesky, SolverDenseLDL
|
4
4
|
from .sparse import SolverSparsePardiso, SolverSparseLU, SolverSparseCholeskyScikit, SolverSparseCholeskyCVXOPT
|
5
5
|
from .iterative import Preconditioner, CG, DampedJacobi, SOR, ILU, GeometricMultigrid
|
6
6
|
from .auto_determine import auto_determine_solver
|
7
7
|
|
8
|
-
__all__ = ['matrix_is_complex', 'matrix_is_diagonal', 'matrix_is_symmetric', 'matrix_is_hermitian',
|
8
|
+
__all__ = ['matrix_is_sparse', 'matrix_is_complex', 'matrix_is_diagonal', 'matrix_is_symmetric', 'matrix_is_hermitian',
|
9
9
|
'LinearSolver', 'LDAWrapper',
|
10
10
|
'SolverDiagonal', 'SolverDenseQR', 'SolverDenseLU', 'SolverDenseCholesky', 'SolverDenseLDL',
|
11
11
|
'SolverSparsePardiso', 'SolverSparseLU', 'SolverSparseCholeskyScikit', 'SolverSparseCholeskyCVXOPT',
|
pymoto/solvers/auto_determine.py
CHANGED
@@ -21,7 +21,7 @@ def auto_determine_solver(A, isdiagonal=None, islowertriangular=None, isuppertri
|
|
21
21
|
:param ispositivedefinite: Manual override for positive definiteness
|
22
22
|
:return: LinearSolver which should be 'best' for the matrix
|
23
23
|
"""
|
24
|
-
issparse =
|
24
|
+
issparse = matrix_is_sparse(A) # Check if the matrix is sparse
|
25
25
|
issquare = A.shape[0] == A.shape[1] # Check if the matrix is square
|
26
26
|
|
27
27
|
if not issquare:
|
pymoto/solvers/iterative.py
CHANGED
@@ -132,7 +132,7 @@ class GeometricMultigrid(Preconditioner):
|
|
132
132
|
assert cycle.lower() in self._available_cycles, f"Cycle ({cycle}) is not available. Options are {self._available_cycles}"
|
133
133
|
self.cycle = cycle
|
134
134
|
self.inner_level = None if inner_level is None else inner_level
|
135
|
-
self.smoother = DampedJacobi(w=0.5) if smoother is None else
|
135
|
+
self.smoother = DampedJacobi(w=0.5) if smoother is None else smoother
|
136
136
|
self.smooth_steps = smooth_steps
|
137
137
|
self.R = None
|
138
138
|
self.sub_domain = DomainDefinition(domain.nelx // 2, domain.nely // 2, domain.nelz // 2,
|
@@ -299,7 +299,7 @@ class CG(LinearSolver):
|
|
299
299
|
self.A = A
|
300
300
|
self.preconditioner.update(A)
|
301
301
|
if self.verbosity >= 1:
|
302
|
-
print(f"Preconditioner set up in {np.round(time.perf_counter() - tstart,3)}s")
|
302
|
+
print(f"CG Preconditioner set up in {np.round(time.perf_counter() - tstart, 3)}s")
|
303
303
|
|
304
304
|
def solve(self, rhs, x0=None, trans='N'):
|
305
305
|
if trans == 'N':
|
@@ -321,10 +321,18 @@ class CG(LinearSolver):
|
|
321
321
|
x = x.reshape((x.size, 1))
|
322
322
|
|
323
323
|
r = b - A@x
|
324
|
+
tval = np.linalg.norm(r, axis=0) / np.linalg.norm(b, axis=0)
|
325
|
+
if self.verbosity >= 2:
|
326
|
+
print(f"CG Initial (max) residual = {tval.max()}")
|
327
|
+
|
328
|
+
if tval.max() <= self.tol:
|
329
|
+
if self.verbosity >= 1:
|
330
|
+
print(f"CG Converged in 0 iterations and {np.round(time.perf_counter() - tstart, 3)}s, with final (max) residual {tval.max()}")
|
331
|
+
|
332
|
+
return x.flatten() if rhs.ndim == 1 else x
|
333
|
+
|
324
334
|
z = self.preconditioner.solve(r, trans=trans)
|
325
335
|
p = orth(z, normalize=True)
|
326
|
-
if self.verbosity >= 2:
|
327
|
-
print(f"Initial residual = {np.linalg.norm(r, axis=0) / np.linalg.norm(b, axis=0)}")
|
328
336
|
|
329
337
|
for i in range(self.maxit):
|
330
338
|
q = A @ p
|
@@ -333,16 +341,15 @@ class CG(LinearSolver):
|
|
333
341
|
alpha = pq_inv @ (p.conj().T @ r)
|
334
342
|
|
335
343
|
x += p @ alpha
|
336
|
-
if i %
|
344
|
+
if i % self.restart == 0: # Explicit restart
|
337
345
|
r = b - A@x
|
338
346
|
else:
|
339
347
|
r -= q @ alpha
|
340
348
|
|
349
|
+
tval = np.linalg.norm(r, axis=0)/np.linalg.norm(b, axis=0)
|
341
350
|
if self.verbosity >= 2:
|
342
|
-
print(f"i = {i}, residuals = {
|
343
|
-
|
344
|
-
tval = np.linalg.norm(r)/np.linalg.norm(b)
|
345
|
-
if tval <= self.tol:
|
351
|
+
print(f"CG i = {i}, residuals = {tval}")
|
352
|
+
if tval.max() <= self.tol:
|
346
353
|
break
|
347
354
|
|
348
355
|
z = self.preconditioner.solve(r, trans=trans)
|
@@ -350,12 +357,9 @@ class CG(LinearSolver):
|
|
350
357
|
beta = -pq_inv @ (q.conj().T @ z)
|
351
358
|
p = orth(z + p@beta, normalize=False)
|
352
359
|
|
353
|
-
if tval > self.tol:
|
354
|
-
warnings.warn(f'Maximum iterations ({self.maxit}) reached, with final
|
360
|
+
if tval.max() > self.tol:
|
361
|
+
warnings.warn(f'CG Maximum iterations ({self.maxit}) reached, with final residuals {tval}')
|
355
362
|
elif self.verbosity >= 1:
|
356
|
-
print(f"Converged in {i} iterations and {np.round(time.perf_counter() - tstart, 3)}s, with final
|
363
|
+
print(f"CG Converged in {i} iterations and {np.round(time.perf_counter() - tstart, 3)}s, with final (max) residual {tval.max()}")
|
357
364
|
|
358
|
-
if rhs.ndim == 1
|
359
|
-
return x.flatten()
|
360
|
-
else:
|
361
|
-
return x
|
365
|
+
return x.flatten() if rhs.ndim == 1 else x
|
pymoto/solvers/matrix_checks.py
CHANGED
@@ -12,6 +12,10 @@ def is_cvxopt_spmatrix(A):
|
|
12
12
|
return isinstance(A, cvxopt.spmatrix) if _has_cvxopt else False
|
13
13
|
|
14
14
|
|
15
|
+
def matrix_is_sparse(A):
|
16
|
+
return sps.issparse(A)
|
17
|
+
|
18
|
+
|
15
19
|
def matrix_is_complex(A):
|
16
20
|
""" Checks if the matrix is complex """
|
17
21
|
if is_cvxopt_spmatrix(A):
|
@@ -22,7 +26,7 @@ def matrix_is_complex(A):
|
|
22
26
|
|
23
27
|
def matrix_is_diagonal(A):
|
24
28
|
""" Checks if the matrix is diagonal"""
|
25
|
-
if
|
29
|
+
if matrix_is_sparse(A):
|
26
30
|
if isinstance(A, sps.dia_matrix):
|
27
31
|
return len(A.offsets) == 1 and A.offsets[0] == 0
|
28
32
|
else:
|
@@ -35,7 +39,7 @@ def matrix_is_diagonal(A):
|
|
35
39
|
|
36
40
|
def matrix_is_symmetric(A):
|
37
41
|
""" Checks whether a matrix is numerically symmetric """
|
38
|
-
if
|
42
|
+
if matrix_is_sparse(A):
|
39
43
|
return np.allclose((A-A.T).data, 0)
|
40
44
|
elif is_cvxopt_spmatrix(A):
|
41
45
|
return np.isclose(max(abs(A-A.T)), 0.0)
|
@@ -46,7 +50,7 @@ def matrix_is_symmetric(A):
|
|
46
50
|
def matrix_is_hermitian(A):
|
47
51
|
""" Checks whether a matrix is numerically Hermitian """
|
48
52
|
if matrix_is_complex(A):
|
49
|
-
if
|
53
|
+
if matrix_is_sparse(A):
|
50
54
|
return np.allclose((A-A.T.conj()).data, 0)
|
51
55
|
elif is_cvxopt_spmatrix(A):
|
52
56
|
return np.isclose(max(abs(A-A.ctrans())), 0.0)
|
pymoto/solvers/solvers.py
CHANGED
@@ -75,6 +75,17 @@ class LinearSolver:
|
|
75
75
|
return np.linalg.norm(mat@x - b, axis=0) / np.linalg.norm(b, axis=0)
|
76
76
|
|
77
77
|
|
78
|
+
def get_diagonal_indices(mat):
|
79
|
+
""" Get the row/column indices for entries in the matrix that are diagonal (has all zeros in its row and column) """
|
80
|
+
assert mat.ndim == 2, "Not a matrix"
|
81
|
+
n = min(*mat.shape) # Matrix size
|
82
|
+
bmat = mat != 0
|
83
|
+
has_diag = bmat.diagonal()
|
84
|
+
nnz_rows = np.array(bmat.sum(axis=0)).flatten()[:n]
|
85
|
+
nnz_cols = np.array(bmat.sum(axis=1)).flatten()[:n]
|
86
|
+
return np.logical_and(has_diag, nnz_rows <= 1, nnz_cols <= 1)
|
87
|
+
|
88
|
+
|
78
89
|
class LDAWrapper(LinearSolver):
|
79
90
|
r""" Linear dependency aware solver (LDAS)
|
80
91
|
|
@@ -105,6 +116,8 @@ class LDAWrapper(LinearSolver):
|
|
105
116
|
self.xadj_stored = []
|
106
117
|
self.badj_stored = []
|
107
118
|
self.A = None
|
119
|
+
self.diagonal_idx = []
|
120
|
+
self.nondiagonal_idx = slice(None)
|
108
121
|
self._did_solve = False # For debugging purposes
|
109
122
|
self._last_rtol = 0.
|
110
123
|
self.hermitian = hermitian
|
@@ -123,6 +136,9 @@ class LDAWrapper(LinearSolver):
|
|
123
136
|
self.hermitian = matrix_is_hermitian(A)
|
124
137
|
|
125
138
|
self.A = A
|
139
|
+
diags = get_diagonal_indices(A)
|
140
|
+
self.diagonal_idx = np.argwhere(diags).flatten()
|
141
|
+
self.nondiagonal_idx = np.argwhere(~diags).flatten()
|
126
142
|
self.x_stored.clear()
|
127
143
|
self.b_stored.clear()
|
128
144
|
self.xadj_stored.clear()
|
@@ -130,6 +146,9 @@ class LDAWrapper(LinearSolver):
|
|
130
146
|
self.solver.update(A)
|
131
147
|
|
132
148
|
def _do_solve_1rhs(self, A, rhs, x_data, b_data, solve_fn, x0=None):
|
149
|
+
isel = self.nondiagonal_idx
|
150
|
+
idia = self.diagonal_idx
|
151
|
+
|
133
152
|
dtype = np.result_type(A, rhs)
|
134
153
|
if rhs.ndim == 1:
|
135
154
|
rhs_loc = np.zeros((rhs.size, 1), dtype=dtype)
|
@@ -139,11 +158,16 @@ class LDAWrapper(LinearSolver):
|
|
139
158
|
rhs_loc[:] = rhs
|
140
159
|
sol = np.zeros_like(rhs_loc, dtype=dtype)
|
141
160
|
|
142
|
-
#
|
161
|
+
# Diagonal part of the matrix
|
162
|
+
sol[idia, ...] = rhs_loc[idia, ...] / A.diagonal()[idia, None]
|
163
|
+
rhs_loc[idia, ...] = 0
|
164
|
+
|
165
|
+
# Non-diagonal part of the matrix
|
166
|
+
# Reconstruct using the database using modified Gram-Schmidt
|
143
167
|
for (x, b) in zip(x_data, b_data):
|
144
168
|
assert x.ndim == b.ndim == 1
|
145
|
-
assert x.size == b.size == rhs_loc.shape[0]
|
146
|
-
alpha = rhs_loc.T @ b.conj() / (b.conj() @ b)
|
169
|
+
# assert x.size == b.size == rhs_loc.shape[0]
|
170
|
+
alpha = rhs_loc[isel, ...].T @ b.conj() / (b.conj() @ b)
|
147
171
|
|
148
172
|
rem_rhs = alpha * b[:, None]
|
149
173
|
add_sol = alpha * x[:, None]
|
@@ -162,34 +186,41 @@ class LDAWrapper(LinearSolver):
|
|
162
186
|
warnings.warn('LDAS: Complex vector cannot be added to real solution')
|
163
187
|
continue
|
164
188
|
|
165
|
-
rhs_loc -= rem_rhs
|
166
|
-
sol += add_sol
|
189
|
+
rhs_loc[isel, ...] -= rem_rhs
|
190
|
+
sol[isel, ...] += add_sol
|
191
|
+
|
192
|
+
assert np.all(rhs_loc[idia, ...] == 0)
|
167
193
|
|
168
194
|
# Check tolerance
|
169
|
-
self._last_rtol =
|
195
|
+
self._last_rtol = self.residual(A, sol, rhs if rhs.ndim > 1 else rhs.reshape(-1, 1))
|
170
196
|
self._did_solve = self._last_rtol > self.tol
|
197
|
+
# If tolerance too large, use the solver for new values
|
171
198
|
if np.any(self._did_solve):
|
172
199
|
if x0 is not None:
|
173
200
|
if x0.ndim == 1:
|
174
201
|
x0_loc = x0.reshape(-1, 1).copy()
|
175
202
|
else:
|
176
203
|
x0_loc = x0[..., self._did_solve].copy()
|
204
|
+
x0_loc[idia, ...] = 0
|
177
205
|
for x in x_data:
|
178
|
-
beta = x0_loc.T @ x.conj() / (x.conj() @ x)
|
179
|
-
x0_loc -= beta * x
|
206
|
+
beta = x0_loc[isel, ...].T @ x.conj() / (x.conj() @ x)
|
207
|
+
x0_loc[isel, ...] -= beta * x
|
180
208
|
else:
|
181
209
|
x0_loc = None
|
182
210
|
|
183
|
-
# Calculate a new solution
|
211
|
+
# Calculate a new solution (for the vectors that have too high residual)
|
184
212
|
xnew = solve_fn(rhs_loc[..., self._did_solve], x0_loc)
|
185
|
-
|
186
|
-
|
213
|
+
|
214
|
+
if isinstance(isel, slice):
|
215
|
+
sol[isel, self._did_solve] += xnew[isel, ...]
|
216
|
+
else:
|
217
|
+
sol[np.ix_(isel, self._did_solve)] += xnew[isel, ...]
|
187
218
|
|
188
219
|
# Add to database
|
189
220
|
for i in range(xnew.shape[-1]):
|
190
221
|
# Remove all previous components that are already in the database (orthogonalize)
|
191
|
-
xadd = xnew[
|
192
|
-
badd = A @
|
222
|
+
xadd = xnew[isel, i]
|
223
|
+
badd = (A @ xnew[..., i])[isel, ...]
|
193
224
|
for x, b in zip(x_data, b_data):
|
194
225
|
beta = badd @ b.conj() / (b.conj() @ b)
|
195
226
|
badd -= beta * b
|
pymoto/solvers/sparse.py
CHANGED
@@ -126,6 +126,7 @@ class SolverSparsePardiso(LinearSolver):
|
|
126
126
|
self._pardiso_solver = PyPardisoSolver(mtype=self._mtype)
|
127
127
|
|
128
128
|
self._pardiso_solver.factorize(A)
|
129
|
+
self._pardiso_solver.set_phase(33)
|
129
130
|
|
130
131
|
def solve(self, b, x0=None, trans='N'):
|
131
132
|
""" solve Ax=b for x
|
@@ -139,6 +140,9 @@ class SolverSparsePardiso(LinearSolver):
|
|
139
140
|
Returns:
|
140
141
|
Solution of the system of linear equations, same shape as input b
|
141
142
|
"""
|
143
|
+
if b.dtype != np.float64: # Only float64 is supported --> this is also done in _check_b, but fails for int8
|
144
|
+
warnings.warn(f"Array b's data type was converted from {b.dtype} to float64")
|
145
|
+
b = b.astype(np.float64)
|
142
146
|
if trans == 'N':
|
143
147
|
return self._pardiso_solver.solve(self.A, b)
|
144
148
|
elif trans == 'T' or trans == 'H':
|
@@ -199,6 +203,13 @@ class SolverSparsePardiso(LinearSolver):
|
|
199
203
|
if v != 0:
|
200
204
|
print(f"{i+1}: {v} ({k})") # i+1 because of 1-based numbering
|
201
205
|
|
206
|
+
def __del__(self):
|
207
|
+
try:
|
208
|
+
self._pardiso_solver.free_memory(everything=True)
|
209
|
+
except ImportError:
|
210
|
+
# To prevent ImportError: sys.meta_path is None, Python is likely shutting down
|
211
|
+
pass
|
212
|
+
|
202
213
|
|
203
214
|
# ------------------------------------ LU Solver -----------------------------------
|
204
215
|
try:
|
pyMOTO-1.4.0.dist-info/RECORD
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
pymoto/__init__.py,sha256=jsqGNDDpd3Tde1w46C5GqQCuAqfgN3faVjReYMQFiLw,1922
|
2
|
-
pymoto/core_objects.py,sha256=mgn5etKHwAOINvu9qqTU89yLs0CDPvxi0anb7q76bNI,24798
|
3
|
-
pymoto/routines.py,sha256=KlP6kgY-817i51Z9EotDclaRnOjrSD4SdymD0Rt7yX0,15186
|
4
|
-
pymoto/utils.py,sha256=YJ-PNLJLc12Yx6TYCrEechS2aaBRx0o4mTM1soeeyz0,1122
|
5
|
-
pymoto/common/domain.py,sha256=QJmdRXzexc1KbdLHNP6X6v5whZWtn0f8b4Rv_wlRBT8,18078
|
6
|
-
pymoto/common/dyadcarrier.py,sha256=b8Ji8einZ4c0lMDuwxd4ZHwRckb2pkuZtAl4Sbg9q4w,18273
|
7
|
-
pymoto/common/mma.py,sha256=sO4PzG2SBeanK82v-Lyw0c29uPmh2q40nXQVt2NCIBU,24838
|
8
|
-
pymoto/modules/aggregation.py,sha256=Oi17hIJ6dic4lOPw16zmjbdC72MjB6XK34H80bnbWAI,7580
|
9
|
-
pymoto/modules/assembly.py,sha256=UpTzp4KGKVcWruEpNarmNRK5lp6CK82QB-7d4Wk2xTg,18138
|
10
|
-
pymoto/modules/autodiff.py,sha256=WAfoAOHBSozf7jbr9gQz9Vw4a_2G9wGJxLMMqUQP0Co,1684
|
11
|
-
pymoto/modules/complex.py,sha256=B_Obk-ABdV66lEudZ5s8o6qG9NsmYlBsX-PbWvbphhc,4429
|
12
|
-
pymoto/modules/filter.py,sha256=6X9FaQMWYZ_TpHVTFiEibzlmAwmSWbydYM93LFrJ0Wo,25490
|
13
|
-
pymoto/modules/generic.py,sha256=YzsGZ8J0oLCORt78Bf2p0v4GuqpWRI77NLoCk7gqidw,10666
|
14
|
-
pymoto/modules/io.py,sha256=huqF2A54Tk0f36WRDLaRR7_CdeahmQc25OfvqqQ2hT4,11012
|
15
|
-
pymoto/modules/linalg.py,sha256=kvfPTblFXGANWzCxEliTMi8qKxgKiejh2zGp_60iL_Q,19358
|
16
|
-
pymoto/modules/scaling.py,sha256=FN6WsqJME-bkJ5nhoKziS_utFCserpRmRDKlJ8F3MEo,2326
|
17
|
-
pymoto/solvers/__init__.py,sha256=CuEJMqnZ2QWgj96n4mB18LWonT4D3cq7FgnubrZsxXg,995
|
18
|
-
pymoto/solvers/auto_determine.py,sha256=Q4TcTT49SYmPLVG0LMmzOx2Tnly--fLXAQVVYjDzGG0,5130
|
19
|
-
pymoto/solvers/dense.py,sha256=9fKPCwNxRKAEk5k1A7fdLrr9ngeVssGlw-sbjWCm4iU,11235
|
20
|
-
pymoto/solvers/iterative.py,sha256=CIxJHjGnCaIjXbtO2NxV60yeDpcCbSD6Bp0xR-7vOf0,12944
|
21
|
-
pymoto/solvers/matrix_checks.py,sha256=gmq7vwFQaXjH3QG-2bW5LcMJF0abTejzw1vEVXCfLPc,1597
|
22
|
-
pymoto/solvers/solvers.py,sha256=RwHjZYYlE3oA0U9k7ukla2gOdmq57rSSJQvHqjaM7JU,10626
|
23
|
-
pymoto/solvers/sparse.py,sha256=VtAdscg7USR8oG_RlS21k64J7VQDa5Rk0ByJ9fzvb_0,16621
|
24
|
-
pyMOTO-1.4.0.dist-info/LICENSE,sha256=ZXMC2Txpzs-dBwz9Me4_1rQCSVl4P1B27MomNi43F30,1072
|
25
|
-
pyMOTO-1.4.0.dist-info/METADATA,sha256=JpafKYREI7mIGlwLQ3v5sFyskvPYPTJBt9u7H-mIhi8,5006
|
26
|
-
pyMOTO-1.4.0.dist-info/WHEEL,sha256=cVxcB9AmuTcXqmwrtPhNK88dr7IR_b6qagTj0UvIEbY,91
|
27
|
-
pyMOTO-1.4.0.dist-info/top_level.txt,sha256=EdvAUSmFMaiqhuEZW8jxANMiK-LdPtlmDWL6SfmCdUU,7
|
28
|
-
pyMOTO-1.4.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
29
|
-
pyMOTO-1.4.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|