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/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 = sps.issparse(mat) # Check if it is a sparse matrix
255
- self.iscomplex = np.iscomplexobj(mat) # Check if it is a complex-valued matrix
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 * 2
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 = sps.issparse(A) and (B is None or sps.issparse(B))
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
- qi /= np.sqrt(qi@Bqi) # Normalize
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
- # TODO
422
- raise NotImplementedError('Non-Hermitian sparse matrix not supported')
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
- if np.isrealobj(A):
464
- dA += DyadCarrier([np.real(dA_u), -np.imag(dA_u)], [np.real(qi), np.imag(qi)])
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
- if np.isrealobj(B):
471
- dB -= DyadCarrier([np.real(dB_u), -np.imag(dB_u)], [np.real(qi), np.imag(qi)])
472
- else:
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
- minval: Maximum value :math:`x_\text{max}` for negative-null-form constraint
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.orig_signal
13
+ s1 = s1.base
14
14
  for s2 in sig2:
15
15
  while isinstance(s2, SignalSlice):
16
- s2 = s2.orig_signal
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)
@@ -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',
@@ -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 = sps.issparse(A) # Check if the matrix is sparse
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:
@@ -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 None
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 % 50 == 0: # Explicit restart
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 = {np.linalg.norm(r, axis=0) / np.linalg.norm(b, axis=0)}")
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 residual {tval}')
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 residuals {np.linalg.norm(r, axis=0) / np.linalg.norm(b, axis=0)}")
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
@@ -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 sps.issparse(A):
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 sps.issparse(A):
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 sps.issparse(A):
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
- # Check linear dependencies in the rhs using modified Gram-Schmidt
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 = np.ones(rhs_loc.shape[-1]) if len(x_data) == 0 else self.residual(A, sol, rhs if rhs.ndim > 1 else rhs.reshape(-1, 1))
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
- self.residual(A, xnew, rhs_loc[..., self._did_solve])
186
- sol[..., self._did_solve] += xnew
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[..., i]
192
- badd = A @ xadd
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:
@@ -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,,