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.
@@ -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
@@ -2,7 +2,8 @@ import warnings
2
2
  import numpy as np
3
3
  import scipy.sparse as sps
4
4
  from scipy.sparse import SparseEfficiencyWarning
5
- from .solvers import matrix_is_hermitian, matrix_is_complex, matrix_is_symmetric, LinearSolver
5
+ from .matrix_checks import matrix_is_hermitian, matrix_is_complex, matrix_is_symmetric
6
+ from .solvers import LinearSolver
6
7
 
7
8
  # ------------------------------------ Pardiso Solver -----------------------------------
8
9
  try:
@@ -126,26 +127,31 @@ class SolverSparsePardiso(LinearSolver):
126
127
 
127
128
  self._pardiso_solver.factorize(A)
128
129
 
129
- def solve(self, b):
130
+ def solve(self, b, x0=None, trans='N'):
130
131
  """ solve Ax=b for x
131
132
 
132
133
  Args:
133
134
  A (scipy.sparse.csr.csr_matrix): sparse square CSR matrix , CSC matrix also possible
134
135
  b (numpy.ndarray): right-hand side(s), b.shape[0] needs to be the same as A.shape[0]
136
+ x0 (unused)
137
+ trans (optional): Indicate which system to solve (Normal, Transposed, or Hermitian transposed)
135
138
 
136
139
  Returns:
137
140
  Solution of the system of linear equations, same shape as input b
138
141
  """
139
- return self._pardiso_solver.solve(self.A, b)
140
-
141
- def adjoint(self, b):
142
- # Cannot use _pardiso_solver.solve because it changes flag 12 internally
143
- iparm_prev = self._pardiso_solver.get_iparm(12)
144
- self._pardiso_solver.set_iparm(12, int(not iparm_prev)) # Adjoint solver (transpose)
145
- b = self._pardiso_solver._check_b(self.A, b)
146
- x = self._pardiso_solver._call_pardiso(self.A, b)
147
- self._pardiso_solver.set_iparm(12, iparm_prev) # Revert back to normal solver
148
- return x
142
+ if trans == 'N':
143
+ return self._pardiso_solver.solve(self.A, b)
144
+ elif trans == 'T' or trans == 'H':
145
+ # T and H are the same, because only real matrix is supported
146
+ # Cannot use _pardiso_solver.solve because it changes flag 12 internally
147
+ iparm_prev = self._pardiso_solver.get_iparm(12)
148
+ self._pardiso_solver.set_iparm(12, int(not iparm_prev)) # Adjoint solver (transpose)
149
+ b = self._pardiso_solver._check_b(self.A, b)
150
+ x = self._pardiso_solver._call_pardiso(self.A, b)
151
+ self._pardiso_solver.set_iparm(12, iparm_prev) # Revert back to normal solver
152
+ return x
153
+ else:
154
+ raise TypeError("Only N, T, or H transposition is possible")
149
155
 
150
156
  def _print_iparm(self):
151
157
  """ Print all iparm settings to console """
@@ -219,17 +225,16 @@ class SolverSparseLU(LinearSolver):
219
225
  self.inv = splu(A)
220
226
  return self
221
227
 
222
- def solve(self, rhs):
228
+ def solve(self, rhs, x0=None, trans='N'):
223
229
  r""" Solves the linear system of equations :math:`\mathbf{A} \mathbf{x} = \mathbf{b}` by forward and backward
224
230
  substitution of :math:`\mathbf{x} = \mathbf{U}^{-1}\mathbf{L}^{-1}\mathbf{b}`.
225
- """
226
- return self.inv.solve(rhs)
227
231
 
228
- def adjoint(self, rhs):
229
- r""" Solves the linear system of equations :math:`\mathbf{A}^\text{H}\mathbf{x} = \mathbf{b}` by forward and
230
- backward substitution of :math:`\mathbf{x} = \mathbf{L}^{-\text{H}}\mathbf{U}^{-\text{H}}\mathbf{b}`.
232
+ Adjoint system solves the linear system of equations :math:`\mathbf{A}^\text{H}\mathbf{x} = \mathbf{b}` by
233
+ forward and backward substitution of :math:`\mathbf{x} = \mathbf{L}^{-\text{H}}\mathbf{U}^{-\text{H}}\mathbf{b}`
231
234
  """
232
- return self.inv.solve(rhs, trans=('H' if self.iscomplex else 'T'))
235
+ if trans not in ['N', 'T', 'H']:
236
+ raise TypeError("Only N, T, or H transposition is possible")
237
+ return self.inv.solve(rhs, trans=trans)
233
238
 
234
239
 
235
240
  # ------------------------------------ Cholesky Solver scikit-sparse -----------------------------------
@@ -281,7 +286,7 @@ class SolverSparseCholeskyScikit(LinearSolver):
281
286
 
282
287
  return self
283
288
 
284
- def solve(self, rhs):
289
+ def solve(self, rhs, x0=None, trans='N'):
285
290
  r""" Solves the linear system of equations :math:`\mathbf{A} \mathbf{x} = \mathbf{b}` by forward and backward
286
291
  substitution of :math:`\mathbf{x} = \mathbf{L}^{-\text{H}}\mathbf{L}^{-1}\mathbf{b}` in case of an
287
292
  Hermitian matrix.
@@ -290,10 +295,12 @@ class SolverSparseCholeskyScikit(LinearSolver):
290
295
  :math:`\mathbf{A}` and ``K`` is the number of right-hand sides.
291
296
 
292
297
  """
293
- return self.inv(rhs)
294
-
295
- def adjoint(self, rhs):
296
- return self.solve(rhs)
298
+ if trans not in ['N', 'T', 'H']:
299
+ raise TypeError("Only N, T, or H transposition is possible")
300
+ if trans == 'T':
301
+ return self.inv(rhs.conj()).conj()
302
+ else:
303
+ return self.inv(rhs)
297
304
 
298
305
 
299
306
  # ------------------------------------ Cholesky Solver cvxopt -----------------------------------
@@ -344,15 +351,20 @@ class SolverSparseCholeskyCVXOPT(LinearSolver):
344
351
 
345
352
  return self
346
353
 
347
- def solve(self, rhs):
354
+ def solve(self, rhs, x0=None, trans='N'):
348
355
  r""" Solves the linear system of equations :math:`\mathbf{A} \mathbf{x} = \mathbf{b}` by forward and backward
349
356
  substitution of :math:`\mathbf{x} = \mathbf{L}^{-\text{H}}\mathbf{L}^{-1}\mathbf{b}`. """
357
+ if trans not in ['N', 'T', 'H']:
358
+ raise TypeError("Only N, T, or H transposition is possible")
350
359
  if rhs.dtype != self._dtype:
351
360
  warnings.warn(f"{type(self).__name__}: Type warning: rhs value type ({rhs.dtype}) is converted to {self._dtype}")
352
- B = cvxopt.matrix(rhs.astype(self._dtype))
361
+ if trans == 'T':
362
+ B = cvxopt.matrix(rhs.conj().astype(self._dtype))
363
+ else:
364
+ B = cvxopt.matrix(rhs.astype(self._dtype))
353
365
  nrhs = 1 if rhs.ndim == 1 else rhs.shape[1]
366
+
354
367
  cvxopt.cholmod.solve(self.inv, B, nrhs=nrhs)
355
- return np.array(B).flatten() if nrhs == 1 else np.array(B)
356
368
 
357
- def adjoint(self, rhs):
358
- return self.solve(rhs)
369
+ x = np.array(B).flatten() if rhs.ndim == 1 else np.array(B)
370
+ return x.conj() if trans == 'T' else x
@@ -1,24 +0,0 @@
1
- pymoto/__init__.py,sha256=V3OzqL_4NDy7lzIv_8tvu6s-8qS4bUwczd6XoH75M3s,2196
2
- pymoto/core_objects.py,sha256=9TjGunvGVwa-LqM2tmE1XlWnqHDscZLeqbKFBZ7ZroU,25111
3
- pymoto/routines.py,sha256=pMJlEFXa413XbqvbJuw3bZTNGQJ4Al-BRdAy_Es_M2g,14360
4
- pymoto/utils.py,sha256=YJ-PNLJLc12Yx6TYCrEechS2aaBRx0o4mTM1soeeyz0,1122
5
- pymoto/common/domain.py,sha256=_VWgm0sjMDsan_GiKnwmpJqZcuksgDU6UTdZYMTSi98,15153
6
- pymoto/common/dyadcarrier.py,sha256=VwLJnOq1omfMX2udG6DMHOkD3AsIB05LTpDY7veYXcc,17136
7
- pymoto/common/mma.py,sha256=W1Z0h5f3a9BP8nFIlCdahDCIHT4XrcDcDyE6Y1Brq3k,23318
8
- pymoto/common/solvers.py,sha256=U7XNMSyHhp0fiZ8ASo1guUb-CHGygik7A4lfLOnh07c,8316
9
- pymoto/common/solvers_dense.py,sha256=vuBUp3y4qJLwmsXbFQ_tEb-7LqqCEemFunTn1z_Qu0U,9901
10
- pymoto/common/solvers_sparse.py,sha256=QVbGTwGtbhOqRUyg2gHmY-K5haiPbskGA6uj_g-dKz8,15776
11
- pymoto/modules/assembly.py,sha256=i0zwigijmsJRR3aHZXSIzlDcYMbCWxKW4fe3NqjW8ew,11540
12
- pymoto/modules/autodiff.py,sha256=WAfoAOHBSozf7jbr9gQz9Vw4a_2G9wGJxLMMqUQP0Co,1684
13
- pymoto/modules/complex.py,sha256=vwzqRo5W319mVf_RqbB7LpYe7jXruVxa3ZV560Iq39k,4421
14
- pymoto/modules/filter.py,sha256=8A-dmWSFEqFyQcutjFv__pfgAwszCVZeZgLxuG9hi0g,18840
15
- pymoto/modules/generic.py,sha256=27EuDMfUtWkkwEqkfbHMCRlHkt6wcV40aUQKfhL2xKI,9783
16
- pymoto/modules/io.py,sha256=4k5S-YQHKhw_HwmqOoYQWFEzdcL5nMJ5fVD2FJFqpFg,10532
17
- pymoto/modules/linalg.py,sha256=j8bZfjo5Il_vbdEHr2BhWB3e7OssYVovieoN54zygx8,24266
18
- pymoto/modules/scaling.py,sha256=hK3sfCoAoseabjqdn5VXe6aGA_fV-MRmMtiv4uIg_I4,2252
19
- pyMOTO-1.2.1.dist-info/LICENSE,sha256=ZXMC2Txpzs-dBwz9Me4_1rQCSVl4P1B27MomNi43F30,1072
20
- pyMOTO-1.2.1.dist-info/METADATA,sha256=YKmcOzqRFlAObECUODa5oZgcIG9r7GRhjn86x12_8fk,4907
21
- pyMOTO-1.2.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
22
- pyMOTO-1.2.1.dist-info/top_level.txt,sha256=EdvAUSmFMaiqhuEZW8jxANMiK-LdPtlmDWL6SfmCdUU,7
23
- pyMOTO-1.2.1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
24
- pyMOTO-1.2.1.dist-info/RECORD,,
pymoto/common/solvers.py DELETED
@@ -1,236 +0,0 @@
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_complex(A):
16
- """ Checks if the matrix is complex """
17
- if is_cvxopt_spmatrix(A):
18
- return A.typecode == 'z'
19
- else:
20
- return np.iscomplexobj(A)
21
-
22
-
23
- def matrix_is_diagonal(A):
24
- """ Checks if the matrix is diagonal"""
25
- if sps.issparse(A):
26
- if isinstance(A, sps.dia_matrix):
27
- return len(A.offsets) == 1 and A.offsets[0] == 0
28
- else:
29
- return np.allclose((A - sps.spdiags(A.diagonal(), 0, *A.shape)).data, 0.0)
30
- elif is_cvxopt_spmatrix(A):
31
- return max(abs(A.I - A.J)) == 0
32
- else:
33
- return np.allclose(A, np.diag(np.diag(A)))
34
-
35
-
36
- def matrix_is_symmetric(A):
37
- """ Checks whether a matrix is numerically symmetric """
38
- if sps.issparse(A):
39
- return np.allclose((A-A.T).data, 0)
40
- elif is_cvxopt_spmatrix(A):
41
- return np.isclose(max(abs(A-A.T)), 0.0)
42
- else:
43
- return np.allclose(A, A.T)
44
-
45
-
46
- def matrix_is_hermitian(A):
47
- """ Checks whether a matrix is numerically Hermitian """
48
- if matrix_is_complex(A):
49
- if sps.issparse(A):
50
- return np.allclose((A-A.T.conj()).data, 0)
51
- elif is_cvxopt_spmatrix(A):
52
- return np.isclose(max(abs(A-A.ctrans())), 0.0)
53
- else:
54
- return np.allclose(A, A.T.conj())
55
- else:
56
- return matrix_is_symmetric(A)
57
-
58
-
59
- class LinearSolver:
60
- """ Base class of all linear solvers
61
-
62
- Keyword Args:
63
- A (matrix): Optionally provide a matrix, which is used in :method:`update` right away.
64
-
65
- Attributes:
66
- defined (bool): Flag if the solver is able to run, e.g. false if some dependent library is not available
67
- """
68
-
69
- defined = True
70
- _err_msg = ""
71
-
72
- def __init__(self, A=None):
73
- if A is not None:
74
- self.update(A)
75
-
76
- def update(self, A):
77
- """ Updates with a new matrix of the same structure
78
-
79
- Args:
80
- A (matrix): The new matrix of size ``(N, N)``
81
-
82
- Returns:
83
- self
84
- """
85
- raise NotImplementedError(f"Solver not implemented {self._err_msg}")
86
-
87
- def solve(self, rhs):
88
- r""" Solves the linear system of equations :math:`\mathbf{A} \mathbf{x} = \mathbf{b}`
89
-
90
- Args:
91
- rhs: Right hand side :math:`\mathbf{b}` of shape ``(N)`` or ``(N, K)`` for multiple right-hand-sides
92
-
93
- Returns:
94
- Solution vector :math:`\mathbf{x}` of same shape as :math:`\mathbf{b}`
95
- """
96
- raise NotImplementedError(f"Solver not implemented {self._err_msg}")
97
-
98
- def adjoint(self, rhs):
99
- r""" Solves the adjoint linear system of equations
100
-
101
- The system of equations is :math:`\mathbf{A}^\text{H} \mathbf{x} = \mathbf{b}` (conjugate transpose) in case of
102
- complex matrix or :math:`\mathbf{A}^\text{T} \mathbf{x} = \mathbf{b}` for a real-valued matrix.
103
-
104
- Args:
105
- rhs: Right hand side :math:`\mathbf{b}` of shape ``(N)`` or ``(N, K)`` for multiple right-hand-sides
106
-
107
- Returns:
108
- Solution vector :math:`\mathbf{x}` of same shape as :math:`\mathbf{b}`
109
- """
110
- raise NotImplementedError(f"Solver not implemented {self._err_msg}")
111
-
112
- @staticmethod
113
- def residual(A, x, b):
114
- r""" Calculates the (relative) residual of the linear system of equations
115
-
116
- The residual is calculated as
117
- :math:`r = \frac{\left| \mathbf{A} \mathbf{x} - \mathbf{b} \right|}{\left| \mathbf{b} \right|}`
118
-
119
- Args:
120
- A: The matrix
121
- x: Solution vector
122
- b: Right-hand side
123
-
124
- Returns:
125
- Residual value
126
- """
127
- return np.linalg.norm(A@x - b) / np.linalg.norm(b)
128
-
129
-
130
- class LDAWrapper(LinearSolver):
131
- r""" Linear dependency aware solver (LDAS)
132
-
133
- This solver uses previous solutions of the system :math:`\mathbf{A} \mathbf{x} = \mathbf{b}` to reduce computational
134
- effort. In case the solution :math:`\mathbf{x}` is linearly dependent on the previous solutions, the solution
135
- will be nearly free of cost.
136
-
137
- Args:
138
- solver: The internal solver to be used
139
- tol (optional): Residual tolerance above which the internal solver is used to add a new solution vector.
140
- A (optional): The matrix :math:`\mathbf{A}`
141
-
142
- References:
143
-
144
- Koppen, S., van der Kolk, M., van den Boom, S., & Langelaar, M. (2022).
145
- Efficient computation of states and sensitivities for compound structural optimisation problems using a Linear Dependency Aware Solver (LDAS).
146
- Structural and Multidisciplinary Optimization, 65(9), 273.
147
- DOI: 10.1007/s00158-022-03378-8
148
- """
149
- def __init__(self, solver: LinearSolver, tol=1e-8, A=None, hermitian=False, symmetric=False):
150
- self.solver = solver
151
- self.tol = tol
152
- self.x_stored = []
153
- self.b_stored = []
154
- self.xadj_stored = []
155
- self.badj_stored = []
156
- self.A = None
157
- self._did_solve = False # For debugging purposes
158
- self._last_rtol = 0.
159
- self.hermitian = hermitian
160
- self.symmetric = symmetric
161
- super().__init__(A)
162
-
163
- def update(self, A):
164
- """ Clear the internal stored solution vectors and update the internal ``solver`` """
165
- self.A = A
166
- self.x_stored.clear()
167
- self.b_stored.clear()
168
- self.xadj_stored.clear()
169
- self.badj_stored.clear()
170
- self.solver.update(A)
171
-
172
- def _do_solve_1rhs(self, A, rhs, x_data, b_data, solve_fn):
173
- rhs_loc = rhs.copy()
174
- sol = 0
175
-
176
- # Check linear dependencies in the rhs using modified Gram-Schmidt
177
- for (x, b) in zip(x_data, b_data):
178
- alpha = rhs_loc.conj() @ b / (b.conj() @ b)
179
- rhs_loc -= alpha * b
180
- sol += alpha * x
181
-
182
- # Check tolerance
183
- self._last_rtol = 1.0 if len(x_data) == 0 else self.residual(A, sol, rhs)
184
-
185
- if self._last_rtol > self.tol:
186
- # Calculate a new solution
187
- xnew = solve_fn(rhs_loc)
188
- x_data.append(xnew)
189
- b_data.append(rhs_loc)
190
- sol += xnew
191
- self._did_solve = True
192
- else:
193
- self._did_solve = False
194
-
195
- return sol
196
-
197
- def _solve_1x(self, b):
198
- return self._do_solve_1rhs(self.A, b, self.x_stored, self.b_stored, self.solver.solve)
199
-
200
- def _adjoint_1x(self, b):
201
- return self._do_solve_1rhs(self.A.conj().T, b, self.xadj_stored, self.badj_stored, self.solver.adjoint)
202
-
203
- def solve(self, rhs):
204
- r""" Solves the linear system of equations :math:`\mathbf{A} \mathbf{x} = \mathbf{b}` by performing a modified
205
- Gram-Schmidt over the previously calculated solutions :math:`\mathbf{U}` and corresponding right-hand-sides
206
- :math:`\mathbf{F}`. This is used to construct an approximate solution
207
- :math:`\tilde{\mathbf{x}} = \sum_k \alpha_k \mathbf{u}_k` in the subspace of :math:`\mathbf{U}`.
208
- If the residual of :math:`\mathbf{A} \tilde{\mathbf{x}} = \mathbf{b}` is above the tolerance, a new solution
209
- :math:`\mathbf{u}_{k+1}` will be added to the database such that
210
- :math:`\mathbf{x} = \tilde{\mathbf{x}}+\mathbf{u}_{k+1}` is the solution to the system
211
- :math:`\mathbf{A} \mathbf{x} = \mathbf{b}`.
212
-
213
- The right-hand-side :math:`\mathbf{b}` can be of size ``(N)`` or ``(N, K)``, where ``N`` is the size of matrix
214
- :math:`\mathbf{A}` and ``K`` is the number of right-hand sides.
215
- """
216
- if rhs.ndim == 1:
217
- return self._solve_1x(rhs)
218
- else: # Multiple rhs
219
- sol = []
220
- for i in range(rhs.shape[-1]):
221
- sol.append(self._solve_1x(rhs[..., i]))
222
- return np.stack(sol, axis=-1)
223
-
224
- def adjoint(self, rhs):
225
- if self.hermitian:
226
- return self.solve(rhs)
227
- elif self.symmetric:
228
- return self.solve(rhs.conj()).conj()
229
- else:
230
- if rhs.ndim == 1:
231
- return self._adjoint_1x(rhs)
232
- else: # Multiple rhs
233
- sol = []
234
- for i in range(rhs.shape[-1]):
235
- sol.append(self._adjoint_1x(rhs[..., i]))
236
- return np.stack(sol, axis=-1)