pyMOTO 1.3.0__py3-none-any.whl → 1.5.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.
@@ -0,0 +1,361 @@
1
+ import warnings
2
+ import time
3
+ import numpy as np
4
+ import scipy.sparse as sps
5
+ from scipy.sparse.linalg import splu, spilu
6
+ from .solvers import LinearSolver
7
+ from .auto_determine import auto_determine_solver
8
+ from pymoto import DomainDefinition
9
+
10
+
11
+ class Preconditioner(LinearSolver):
12
+ """ Abstract base class for preconditioners to inexact solvers """
13
+ def update(self, A):
14
+ pass
15
+
16
+ def solve(self, rhs, x0=None, trans='N'):
17
+ return rhs.copy()
18
+
19
+
20
+ class DampedJacobi(Preconditioner):
21
+ r""" Damped Jacobi preconditioner
22
+ :math:`M = \frac{1}{\omega} D`
23
+ Args:
24
+ A (optional): The matrix
25
+ w (optional): Weight factor :math:`0 < \omega \leq 1`
26
+ """
27
+ def __init__(self, A=None, w=1.0):
28
+ assert 0 < w <= 1, 'w must be between 0 and 1'
29
+ self.w = w
30
+ self.D = None
31
+ super().__init__(A)
32
+
33
+ def update(self, A):
34
+ self.D = A.diagonal()
35
+
36
+ def solve(self, rhs, x0=None, trans='N'):
37
+ if trans == 'N' or trans == 'T':
38
+ return self.w * (rhs.T/self.D).T
39
+ elif trans == 'H':
40
+ return self.w * (rhs.T/self.D.conj()).T
41
+ else:
42
+ raise TypeError("Only N, T, or H transposition is possible")
43
+
44
+
45
+ class SOR(Preconditioner):
46
+ r""" Successive over-relaxation preconditioner
47
+ The matrix :math:`A = L + D + U` is split into a lower triangular, diagonal, and upper triangular part.
48
+ :math:`M = \left(\frac{D}{\omega} + L\right) \frac{\omega D^{-1}}{2-\omega} \left(\frac{D}{\omega} + U\right)`
49
+
50
+ Args:
51
+ A (optional): The matrix
52
+ w (optional): Weight factor :math:`0 < \omega < 2`
53
+ """
54
+ def __init__(self, A=None, w=1.0):
55
+ assert 0 < w < 2, 'w must be between 0 and 2'
56
+ self.w = w
57
+ self.L = None
58
+ self.U = None
59
+ self.Dw = None
60
+ super().__init__(A)
61
+
62
+ def update(self, A):
63
+ diag = A.diagonal()
64
+ diagw = sps.diags(diag)/self.w
65
+ self.L = splu(sps.tril(A, k=-1) + diagw) # Lower triangular part including diagonal
66
+ self.U = splu(sps.triu(A, k=1) + diagw)
67
+
68
+ self.Dw = diag * (2 - self.w) / self.w
69
+
70
+ def solve(self, rhs, x0=None, trans='N'):
71
+ if trans == 'N':
72
+ # M = (D/w + L) wD^-1 / (2-w) (D/w + U)
73
+ # from scipy.sparse.linalg import spsolve_triangular
74
+ # u1 = spsolve_triangular(self.L, rhs, lower=True, overwrite_A=False) # Solve triangular is still very slow :(
75
+ u1 = self.L.solve(rhs)
76
+ u1 *= self.Dw[:, None]
77
+ # u2 = spsolve_triangular(self.U, u1, lower=False, overwrite_A=False, overwrite_b=True)
78
+ u2 = self.U.solve(u1)
79
+ return u2
80
+ elif trans == 'T':
81
+ u1 = self.U.solve(rhs, trans='T')
82
+ u1 *= self.Dw[:, None]
83
+ u2 = self.L.solve(u1, trans='T')
84
+ return u2
85
+ elif trans == 'H':
86
+ u1 = self.U.solve(rhs, trans='H')
87
+ u1 *= self.Dw[:, None].conj()
88
+ u2 = self.L.solve(u1, trans='H')
89
+ return u2
90
+ else:
91
+ raise TypeError("Only N, T, or H transposition is possible")
92
+
93
+
94
+ class ILU(Preconditioner):
95
+ """ Incomplete LU factorization
96
+
97
+ Args:
98
+ A (optional): The matrix
99
+ **kwargs (optional): Keyword arguments passed to `scipy.sparse.linalg.spilu`
100
+ """
101
+ def __init__(self, A=None, **kwargs):
102
+ self.kwargs = kwargs
103
+ self.ilu = None
104
+ super().__init__(A)
105
+
106
+ def update(self, A):
107
+ self.ilu = spilu(A, **self.kwargs)
108
+
109
+ def solve(self, rhs, x0=None, trans='N'):
110
+ return self.ilu.solve(rhs, trans=trans)
111
+
112
+
113
+ class GeometricMultigrid(Preconditioner):
114
+ """ Geometric multigrid preconditioner
115
+
116
+ Args:
117
+ domain: The `DomainDefinition` used for the geometry
118
+ A (optional): The matrix
119
+ inner_level (optional): Inner solver for the coarse grid, for instance, a direct solver or another MG level.
120
+ The default is an automatically determined direct solver.
121
+ smoother (optional): Smoother to use to smooth the residual and solution before and after coarse level.
122
+ The default is `DampedJacobi(w=0.5)`.
123
+ smooth_steps (optional): Number of smoothing steps to execute
124
+ """
125
+ _available_cycles = ['v', 'w']
126
+
127
+ def __init__(self, domain: DomainDefinition, A=None, cycle='V', inner_level=None, smoother=None, smooth_steps=5):
128
+ assert domain.nelx % 2 == 0 and domain.nely % 2 == 0 and domain.nelz % 2 == 0, \
129
+ f"Domain sizes {domain.nelx, domain.nely, domain.nelz} must be divisible by 2"
130
+ self.domain = domain
131
+ self.A = A
132
+ assert cycle.lower() in self._available_cycles, f"Cycle ({cycle}) is not available. Options are {self._available_cycles}"
133
+ self.cycle = cycle
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
136
+ self.smooth_steps = smooth_steps
137
+ self.R = None
138
+ self.sub_domain = DomainDefinition(domain.nelx // 2, domain.nely // 2, domain.nelz // 2,
139
+ domain.unitx * 2, domain.unity * 2, domain.unitz * 2)
140
+
141
+ super().__init__(A)
142
+
143
+ def update(self, A):
144
+ if self.R is None:
145
+ self.setup_interpolation(A)
146
+ self.A = A
147
+ self.smoother.update(A)
148
+ Ac = self.R.T @ A @ self.R
149
+ if self.inner_level is None:
150
+ self.inner_level = auto_determine_solver(Ac)
151
+ self.inner_level.update(Ac)
152
+
153
+ def setup_interpolation(self, A):
154
+ assert A.shape[0] % self.domain.nnodes == 0
155
+ ndof = int(A.shape[0] / self.domain.nnodes) # Number of dofs per node
156
+
157
+ w = np.ones((3, 3, 3))*0.125
158
+ w[1, :, :] = 0.25
159
+ w[:, 1, :] = 0.25
160
+ w[:, :, 1] = 0.25
161
+ w[1, 1, :] = 0.5
162
+ w[1, :, 1] = 0.5
163
+ w[:, 1, 1] = 0.5
164
+ w[1, 1, 1] = 1.0
165
+
166
+ rows = []
167
+ cols = []
168
+ vals = []
169
+ for i in [-1, 0, 1]:
170
+ imin, imax = max(-i, 0), min(self.sub_domain.nelx + 1 - i, self.sub_domain.nelx + 1)
171
+ ix = np.arange(imin, imax)
172
+ for j in [-1, 0, 1]:
173
+ jmin, jmax = max(-j, 0), min(self.sub_domain.nely + 1 - j, self.sub_domain.nely + 1)
174
+ iy = np.arange(jmin, jmax)
175
+ for k in ([-1, 0, 1] if self.domain.dim == 3 else [0]):
176
+ # Coarse node cartesian indices
177
+ kmin, kmax = max(-k, 0), min(self.sub_domain.nelz + 1 - k, self.sub_domain.nelz + 1)
178
+ iz = np.arange(kmin, kmax)
179
+ # Coarse node numbers
180
+ nod_c = self.sub_domain.get_nodenumber(*np.meshgrid(ix, iy, iz, indexing='ij')).flatten()
181
+ # Fine node numbers with offset
182
+ ixc, iyc, izc = ix * 2 + i, iy * 2 + j, iz * 2 + k
183
+ nod_f = self.domain.get_nodenumber(*np.meshgrid(ixc, iyc, izc, indexing='ij')).flatten()
184
+ for d in range(ndof):
185
+ rows.append(nod_f * ndof + d)
186
+ cols.append(nod_c * ndof + d)
187
+ vals.append(np.ones_like(rows[-1], dtype=w.dtype) * w[1+i, 1+j, 1+k])
188
+
189
+ rows = np.concatenate(rows)
190
+ cols = np.concatenate(cols)
191
+ vals = np.concatenate(vals)
192
+ nfine = ndof * self.domain.nnodes
193
+ ncoarse = ndof * self.sub_domain.nnodes
194
+ self.R = sps.coo_matrix((vals, (rows, cols)), shape=(nfine, ncoarse))
195
+ self.R = type(A)(self.R) # Convert to correct matrix type
196
+
197
+ def solve(self, rhs, x0=None, trans='N'):
198
+ if trans == 'N':
199
+ A = self.A
200
+ elif trans == 'T':
201
+ A = self.A.T
202
+ elif trans == 'H':
203
+ A = self.A.conj().T
204
+ else:
205
+ raise TypeError("Only N, T, or H transposition is possible")
206
+
207
+ # Pre-smoothing
208
+ if x0 is None:
209
+ u_f = self.smoother.solve(rhs, trans=trans)
210
+ else:
211
+ r = rhs - self.A @ x0
212
+ u_f = x0 + self.smoother.solve(r, trans=trans)
213
+ for i in range(self.smooth_steps-1):
214
+ r = rhs - self.A @ u_f
215
+ u_f += self.smoother.solve(r, trans=trans)
216
+
217
+ r = rhs - A @ u_f
218
+ # Restrict residual to coarse level
219
+ r_c = self.R.T @ r
220
+
221
+ # Solve at coarse level
222
+ u_c = self.inner_level.solve(r_c)
223
+
224
+ # Interpolate and correct
225
+ u_f += self.R @ u_c
226
+
227
+ # Post-smoothing
228
+ for i in range(self.smooth_steps):
229
+ r = rhs - self.A @ u_f
230
+ u_f += self.smoother.solve(r, trans=trans)
231
+ return u_f
232
+
233
+
234
+ def orth(u, normalize=True, zero_rtol=1e-15):
235
+ """ Create orthogonal basis from a set of vectors
236
+
237
+ Args:
238
+ u: Set of vectors of size (#dof, #vectors)
239
+ normalize: Also normalize the basis vectors
240
+ zero_rtol: Relative tolerance for detection of zero vectors (in case of a rank-deficient basis)
241
+
242
+ Returns:
243
+ v: Orthogonal basis vectors (#dof, #non-zero-vectors)
244
+ """
245
+ if u.ndim == 1:
246
+ return u
247
+ elif u.ndim > 2:
248
+ raise TypeError("Only valid for 1D or 2D matrix")
249
+
250
+ def dot(a, b): # Define inner product
251
+ return a @ b.conj()
252
+
253
+ orth_vecs = []
254
+ for i in range(u.shape[-1]):
255
+ vi = np.copy(u[..., i])
256
+ beta_i = dot(vi, vi)
257
+ for vj in orth_vecs:
258
+ alpha_ij = dot(vi, vj)
259
+ alpha_jj = 1.0 if normalize else dot(vj, vj)
260
+ vi -= vj * alpha_ij / alpha_jj
261
+ beta_i_new = dot(vi, vi)
262
+ if beta_i_new / beta_i < zero_rtol: # Detect zero vector
263
+ continue
264
+ if normalize:
265
+ vi /= np.sqrt(beta_i_new)
266
+ orth_vecs.append(vi)
267
+ return np.stack(orth_vecs, axis=-1)
268
+
269
+
270
+ class CG(LinearSolver):
271
+ """ Preconditioned conjugate gradient method
272
+ Works for positive-definite self-adjoint matrices (:math:`A=A^H`)
273
+
274
+ References:
275
+ Ji & Li (2017), A breakdown-free BCG method. DOI 10.1007/s10543-016-0631-z
276
+ https://www.cs.odu.edu/~yaohang/portfolio/BIT2017.pdf
277
+ Shewchuck (1994), Introduction to CG method without the agonzing pain.
278
+ https://www.cs.cmu.edu/~quake-papers/painless-conjugate-gradient.pdf
279
+
280
+ Args:
281
+ A: The matrix
282
+ preconditioner: Preconditioner to use
283
+ tol: Convergence tolerance
284
+ maxit: Maximum number of iterations
285
+ restart: Restart every Nth iteration
286
+ verbosity: Log level
287
+ """
288
+ def __init__(self, A=None, preconditioner=Preconditioner(), tol=1e-7, maxit=10000, restart=50, verbosity=0):
289
+ self.preconditioner = preconditioner
290
+ self.A = A
291
+ self.tol = tol
292
+ self.maxit = maxit
293
+ self.restart = restart
294
+ self.verbosity = verbosity
295
+ super().__init__(A)
296
+
297
+ def update(self, A):
298
+ tstart = time.perf_counter()
299
+ self.A = A
300
+ self.preconditioner.update(A)
301
+ if self.verbosity >= 1:
302
+ print(f"Preconditioner set up in {np.round(time.perf_counter() - tstart,3)}s")
303
+
304
+ def solve(self, rhs, x0=None, trans='N'):
305
+ if trans == 'N':
306
+ A = self.A
307
+ elif trans == 'T':
308
+ A = self.A.T
309
+ elif trans == 'H':
310
+ A = self.A.conj().T
311
+ else:
312
+ raise TypeError("Only N, T, or H transposition is possible")
313
+
314
+ tstart = time.perf_counter()
315
+ if rhs.ndim == 1:
316
+ b = rhs.reshape((rhs.size, 1))
317
+ else:
318
+ b = rhs
319
+ x = np.zeros_like(rhs, dtype=np.result_type(rhs, A)) if x0 is None else x0.copy()
320
+ if x.ndim == 1:
321
+ x = x.reshape((x.size, 1))
322
+
323
+ r = b - A@x
324
+ z = self.preconditioner.solve(r, trans=trans)
325
+ 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
+
329
+ for i in range(self.maxit):
330
+ q = A @ p
331
+ pq = p.conj().T @ q
332
+ pq_inv = np.linalg.inv(pq)
333
+ alpha = pq_inv @ (p.conj().T @ r)
334
+
335
+ x += p @ alpha
336
+ if i % 50 == 0: # Explicit restart
337
+ r = b - A@x
338
+ else:
339
+ r -= q @ alpha
340
+
341
+ 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:
346
+ break
347
+
348
+ z = self.preconditioner.solve(r, trans=trans)
349
+
350
+ beta = -pq_inv @ (q.conj().T @ z)
351
+ p = orth(z + p@beta, normalize=False)
352
+
353
+ if tval > self.tol:
354
+ warnings.warn(f'Maximum iterations ({self.maxit}) reached, with final residual {tval}')
355
+ 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)}")
357
+
358
+ if rhs.ndim == 1:
359
+ return x.flatten()
360
+ else:
361
+ return x
@@ -0,0 +1,60 @@
1
+ import numpy as np
2
+ import scipy.sparse as sps
3
+ try:
4
+ import cvxopt
5
+ _has_cvxopt = True
6
+ except ImportError:
7
+ _has_cvxopt = False
8
+
9
+
10
+ def is_cvxopt_spmatrix(A):
11
+ """ Checks if the argument is a cvxopt sparse matrix """
12
+ return isinstance(A, cvxopt.spmatrix) if _has_cvxopt else False
13
+
14
+
15
+ def matrix_is_sparse(A):
16
+ return sps.issparse(A)
17
+
18
+
19
+ def matrix_is_complex(A):
20
+ """ Checks if the matrix is complex """
21
+ if is_cvxopt_spmatrix(A):
22
+ return A.typecode == 'z'
23
+ else:
24
+ return np.iscomplexobj(A)
25
+
26
+
27
+ def matrix_is_diagonal(A):
28
+ """ Checks if the matrix is diagonal"""
29
+ if matrix_is_sparse(A):
30
+ if isinstance(A, sps.dia_matrix):
31
+ return len(A.offsets) == 1 and A.offsets[0] == 0
32
+ else:
33
+ return np.allclose((A - sps.spdiags(A.diagonal(), 0, *A.shape)).data, 0.0)
34
+ elif is_cvxopt_spmatrix(A):
35
+ return max(abs(A.I - A.J)) == 0
36
+ else:
37
+ return np.allclose(A, np.diag(np.diag(A)))
38
+
39
+
40
+ def matrix_is_symmetric(A):
41
+ """ Checks whether a matrix is numerically symmetric """
42
+ if matrix_is_sparse(A):
43
+ return np.allclose((A-A.T).data, 0)
44
+ elif is_cvxopt_spmatrix(A):
45
+ return np.isclose(max(abs(A-A.T)), 0.0)
46
+ else:
47
+ return np.allclose(A, A.T)
48
+
49
+
50
+ def matrix_is_hermitian(A):
51
+ """ Checks whether a matrix is numerically Hermitian """
52
+ if matrix_is_complex(A):
53
+ if matrix_is_sparse(A):
54
+ return np.allclose((A-A.T.conj()).data, 0)
55
+ elif is_cvxopt_spmatrix(A):
56
+ return np.isclose(max(abs(A-A.ctrans())), 0.0)
57
+ else:
58
+ return np.allclose(A, A.T.conj())
59
+ else:
60
+ return matrix_is_symmetric(A)
@@ -0,0 +1,253 @@
1
+ import warnings
2
+ import numpy as np
3
+ from .matrix_checks import matrix_is_hermitian, matrix_is_symmetric, matrix_is_complex
4
+
5
+
6
+ class LinearSolver:
7
+ """ Base class of all linear solvers
8
+
9
+ Keyword Args:
10
+ A (matrix): Optionally provide a matrix, which is used in :method:`update` right away.
11
+
12
+ Attributes:
13
+ defined (bool): Flag if the solver is able to run, e.g. false if some dependent library is not available
14
+ """
15
+
16
+ defined = True
17
+ _err_msg = ""
18
+
19
+ def __init__(self, A=None):
20
+ if A is not None:
21
+ self.update(A)
22
+
23
+ def update(self, A):
24
+ """ Updates with a new matrix of the same structure
25
+
26
+ Args:
27
+ A (matrix): The new matrix of size ``(N, N)``
28
+
29
+ Returns:
30
+ self
31
+ """
32
+ raise NotImplementedError(f"Solver not implemented {self._err_msg}")
33
+
34
+ def solve(self, rhs, x0=None, trans='N'):
35
+ r""" Solves the linear system of equations :math:`\mathbf{A} \mathbf{x} = \mathbf{b}`
36
+
37
+ Args:
38
+ rhs: Right hand side :math:`\mathbf{b}` of shape ``(N)`` or ``(N, K)`` for multiple right-hand-sides
39
+ x0 (optional): Initial guess for the solution
40
+ trans (optional): Option to transpose matrix
41
+ 'N': A @ x == rhs (default) Normal matrix
42
+ 'T': A^T @ x == rhs Transposed matrix
43
+ 'H': A^H @ x == rhs Hermitian transposed matrix (conjugate transposed)
44
+
45
+ Returns:
46
+ Solution vector :math:`\mathbf{x}` of same shape as :math:`\mathbf{b}`
47
+ """
48
+ raise NotImplementedError(f"Solver not implemented {self._err_msg}")
49
+
50
+ @staticmethod
51
+ def residual(A, x, b, trans='N'):
52
+ r""" Calculates the (relative) residual of the linear system of equations
53
+
54
+ The residual is calculated as
55
+ :math:`r = \frac{\left| \mathbf{A} \mathbf{x} - \mathbf{b} \right|}{\left| \mathbf{b} \right|}`
56
+
57
+ Args:
58
+ A: The matrix
59
+ x: Solution vector
60
+ b: Right-hand side
61
+ trans (optional): Matrix tranformation (`N` is normal, `T` is transposed, `H` is hermitian transposed)
62
+
63
+ Returns:
64
+ Residual value
65
+ """
66
+ assert x.shape == b.shape
67
+ if trans == 'N':
68
+ mat = A
69
+ elif trans == 'T':
70
+ mat = A.T
71
+ elif trans == 'H':
72
+ mat = A.conj().T
73
+ else:
74
+ raise TypeError("Only N, T, or H transposition is possible")
75
+ return np.linalg.norm(mat@x - b, axis=0) / np.linalg.norm(b, axis=0)
76
+
77
+
78
+ class LDAWrapper(LinearSolver):
79
+ r""" Linear dependency aware solver (LDAS)
80
+
81
+ This solver uses previous solutions of the system :math:`\mathbf{A} \mathbf{x} = \mathbf{b}` to reduce computational
82
+ effort. In case the solution :math:`\mathbf{x}` is linearly dependent on the previous solutions, the solution
83
+ will be nearly free of cost.
84
+
85
+ Args:
86
+ solver: The internal solver to be used
87
+ tol (optional): Residual tolerance above which the internal solver is used to add a new solution vector.
88
+ A (optional): The matrix :math:`\mathbf{A}`
89
+ symmetric (optional): Flag to indicate a symmetric matrix :math:`A=A^T`
90
+ hermitian (optional): Flag to indicate a Hermitian matrix :math:`A=A^H`
91
+
92
+ References:
93
+ Koppen, S., van der Kolk, M., van den Boom, S., & Langelaar, M. (2022).
94
+ Efficient computation of states and sensitivities for compound structural optimisation problems using a Linear Dependency Aware Solver (LDAS).
95
+ Structural and Multidisciplinary Optimization, 65(9), 273.
96
+ DOI: 10.1007/s00158-022-03378-8
97
+ """
98
+ def __init__(self, solver: LinearSolver, tol=1e-7, A=None, symmetric=None, hermitian=None):
99
+ self.solver = solver
100
+ self.tol = tol
101
+ # Storage for solution vectors (solutions of A x = b)
102
+ self.x_stored = []
103
+ self.b_stored = []
104
+ # Storage for adjoint solution vectors (solutions of A^H x = b)
105
+ self.xadj_stored = []
106
+ self.badj_stored = []
107
+ self.A = None
108
+ self._did_solve = False # For debugging purposes
109
+ self._last_rtol = 0.
110
+ self.hermitian = hermitian
111
+ self.symmetric = symmetric
112
+ self.complex = None
113
+ super().__init__(A)
114
+
115
+ def update(self, A):
116
+ """ Clear the internal stored solution vectors and update the internal ``solver`` """
117
+ if self.symmetric is None:
118
+ self.symmetric = matrix_is_symmetric(A)
119
+
120
+ if self.hermitian is None:
121
+ if not matrix_is_complex(A):
122
+ self.hermitian = self.symmetric
123
+ self.hermitian = matrix_is_hermitian(A)
124
+
125
+ self.A = A
126
+ self.x_stored.clear()
127
+ self.b_stored.clear()
128
+ self.xadj_stored.clear()
129
+ self.badj_stored.clear()
130
+ self.solver.update(A)
131
+
132
+ def _do_solve_1rhs(self, A, rhs, x_data, b_data, solve_fn, x0=None):
133
+ dtype = np.result_type(A, rhs)
134
+ if rhs.ndim == 1:
135
+ rhs_loc = np.zeros((rhs.size, 1), dtype=dtype)
136
+ rhs_loc[:, 0] = rhs
137
+ else:
138
+ rhs_loc = np.zeros_like(rhs, dtype=dtype)
139
+ rhs_loc[:] = rhs
140
+ sol = np.zeros_like(rhs_loc, dtype=dtype)
141
+
142
+ # Check linear dependencies in the rhs using modified Gram-Schmidt
143
+ for (x, b) in zip(x_data, b_data):
144
+ 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)
147
+
148
+ rem_rhs = alpha * b[:, None]
149
+ add_sol = alpha * x[:, None]
150
+
151
+ if np.iscomplexobj(rem_rhs) and not np.iscomplexobj(rhs_loc):
152
+ if np.linalg.norm(np.imag(rem_rhs)) < 1e-10*np.linalg.norm(np.real(rem_rhs)):
153
+ rem_rhs = np.real(rem_rhs)
154
+ else:
155
+ warnings.warn('LDAS: Complex vector cannot be subtracted from real rhs')
156
+ continue
157
+
158
+ if np.iscomplexobj(add_sol) and not np.iscomplexobj(sol):
159
+ if np.linalg.norm(np.imag(add_sol)) < 1e-10*np.linalg.norm(np.real(add_sol)):
160
+ add_sol = np.real(add_sol)
161
+ else:
162
+ warnings.warn('LDAS: Complex vector cannot be added to real solution')
163
+ continue
164
+
165
+ rhs_loc -= rem_rhs
166
+ sol += add_sol
167
+
168
+ # 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))
170
+ self._did_solve = self._last_rtol > self.tol
171
+ if np.any(self._did_solve):
172
+ if x0 is not None:
173
+ if x0.ndim == 1:
174
+ x0_loc = x0.reshape(-1, 1).copy()
175
+ else:
176
+ x0_loc = x0[..., self._did_solve].copy()
177
+ for x in x_data:
178
+ beta = x0_loc.T @ x.conj() / (x.conj() @ x)
179
+ x0_loc -= beta * x
180
+ else:
181
+ x0_loc = None
182
+
183
+ # Calculate a new solution
184
+ 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
187
+
188
+ # Add to database
189
+ for i in range(xnew.shape[-1]):
190
+ # Remove all previous components that are already in the database (orthogonalize)
191
+ xadd = xnew[..., i]
192
+ badd = A @ xadd
193
+ for x, b in zip(x_data, b_data):
194
+ beta = badd @ b.conj() / (b.conj() @ b)
195
+ badd -= beta * b
196
+ xadd -= beta * x
197
+ bnrm = np.linalg.norm(badd)
198
+ if not np.isfinite(bnrm) or bnrm == 0:
199
+ continue
200
+ badd /= bnrm
201
+ xadd /= bnrm
202
+ x_data.append(xadd)
203
+ b_data.append(badd)
204
+
205
+ if rhs.ndim == 1:
206
+ return sol.flatten()
207
+ else:
208
+ return sol
209
+
210
+ def solve(self, rhs, x0=None, trans='N'):
211
+ r""" Solves the linear system of equations :math:`\mathbf{A} \mathbf{x} = \mathbf{b}` by performing a modified
212
+ Gram-Schmidt over the previously calculated solutions :math:`\mathbf{U}` and corresponding right-hand-sides
213
+ :math:`\mathbf{F}`. This is used to construct an approximate solution
214
+ :math:`\tilde{\mathbf{x}} = \sum_k \alpha_k \mathbf{u}_k` in the subspace of :math:`\mathbf{U}`.
215
+ If the residual of :math:`\mathbf{A} \tilde{\mathbf{x}} = \mathbf{b}` is above the tolerance, a new solution
216
+ :math:`\mathbf{u}_{k+1}` will be added to the database such that
217
+ :math:`\mathbf{x} = \tilde{\mathbf{x}}+\mathbf{u}_{k+1}` is the solution to the system
218
+ :math:`\mathbf{A} \mathbf{x} = \mathbf{b}`.
219
+
220
+ The right-hand-side :math:`\mathbf{b}` can be of size ``(N)`` or ``(N, K)``, where ``N`` is the size of matrix
221
+ :math:`\mathbf{A}` and ``K`` is the number of right-hand sides.
222
+ """
223
+ ''' Required Symm. matrix Herm. matrix Any matrix (uses adjoint storage)
224
+ A^T = A A^H = A
225
+ A x = b
226
+ A^T x = b A x = b A x^* = b^* A^H x^* = b^*
227
+ A^H x = b A x^* = b^* A x = b A^H x = b
228
+
229
+ For symmetric or Hermitian matrices, only the normal storage is required. For any other matrix, the `T` and
230
+ `H` mode will require the adjoint storage space.
231
+ '''
232
+ if trans not in ['N', 'T', 'H']:
233
+ raise TypeError("Only N, T, or H transposition is possible")
234
+
235
+ # Use adjoint storage?
236
+ adjoint_mode = trans != 'N' and not (self.symmetric or self.hermitian)
237
+ # Use conjugation?
238
+ conj_mode = self.symmetric and trans == 'H' or not self.symmetric and trans == 'T'
239
+
240
+ storage = 'H' if adjoint_mode else 'N'
241
+ rhs = rhs.conj() if conj_mode else rhs
242
+ if storage == 'N': # Use the normal storage
243
+ ret = self._do_solve_1rhs(self.A, rhs,
244
+ self.x_stored, self.b_stored,
245
+ lambda b, x_init: self.solver.solve(b, trans='N', x0=x_init),
246
+ x0=x0)
247
+ elif storage == 'H': # Use adjoint storage
248
+ ret = self._do_solve_1rhs(self.A.conj().T, rhs,
249
+ self.xadj_stored, self.badj_stored,
250
+ lambda b, x_init: self.solver.solve(b, trans='H', x0=x_init),
251
+ x0=x0)
252
+
253
+ return ret.conj() if conj_mode else ret