emerge 0.4.6__py3-none-any.whl → 0.4.8__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.
Potentially problematic release.
This version of emerge might be problematic. Click here for more details.
- emerge/__init__.py +54 -0
- emerge/__main__.py +5 -0
- emerge/_emerge/__init__.py +42 -0
- emerge/_emerge/bc.py +197 -0
- emerge/_emerge/coord.py +119 -0
- emerge/_emerge/cs.py +523 -0
- emerge/_emerge/dataset.py +36 -0
- emerge/_emerge/elements/__init__.py +19 -0
- emerge/_emerge/elements/femdata.py +212 -0
- emerge/_emerge/elements/index_interp.py +64 -0
- emerge/_emerge/elements/legrange2.py +172 -0
- emerge/_emerge/elements/ned2_interp.py +645 -0
- emerge/_emerge/elements/nedelec2.py +140 -0
- emerge/_emerge/elements/nedleg2.py +217 -0
- emerge/_emerge/geo/__init__.py +24 -0
- emerge/_emerge/geo/horn.py +107 -0
- emerge/_emerge/geo/modeler.py +449 -0
- emerge/_emerge/geo/operations.py +254 -0
- emerge/_emerge/geo/pcb.py +1244 -0
- emerge/_emerge/geo/pcb_tools/calculator.py +28 -0
- emerge/_emerge/geo/pcb_tools/macro.py +79 -0
- emerge/_emerge/geo/pmlbox.py +204 -0
- emerge/_emerge/geo/polybased.py +529 -0
- emerge/_emerge/geo/shapes.py +427 -0
- emerge/_emerge/geo/step.py +77 -0
- emerge/_emerge/geo2d.py +86 -0
- emerge/_emerge/geometry.py +510 -0
- emerge/_emerge/howto.py +214 -0
- emerge/_emerge/logsettings.py +5 -0
- emerge/_emerge/material.py +118 -0
- emerge/_emerge/mesh3d.py +730 -0
- emerge/_emerge/mesher.py +339 -0
- emerge/_emerge/mth/common_functions.py +33 -0
- emerge/_emerge/mth/integrals.py +71 -0
- emerge/_emerge/mth/optimized.py +357 -0
- emerge/_emerge/periodic.py +263 -0
- emerge/_emerge/physics/__init__.py +0 -0
- emerge/_emerge/physics/microwave/__init__.py +1 -0
- emerge/_emerge/physics/microwave/adaptive_freq.py +279 -0
- emerge/_emerge/physics/microwave/assembly/assembler.py +569 -0
- emerge/_emerge/physics/microwave/assembly/curlcurl.py +448 -0
- emerge/_emerge/physics/microwave/assembly/generalized_eigen.py +426 -0
- emerge/_emerge/physics/microwave/assembly/robinbc.py +433 -0
- emerge/_emerge/physics/microwave/microwave_3d.py +1150 -0
- emerge/_emerge/physics/microwave/microwave_bc.py +915 -0
- emerge/_emerge/physics/microwave/microwave_data.py +1148 -0
- emerge/_emerge/physics/microwave/periodic.py +82 -0
- emerge/_emerge/physics/microwave/port_functions.py +53 -0
- emerge/_emerge/physics/microwave/sc.py +175 -0
- emerge/_emerge/physics/microwave/simjob.py +147 -0
- emerge/_emerge/physics/microwave/sparam.py +138 -0
- emerge/_emerge/physics/microwave/touchstone.py +140 -0
- emerge/_emerge/plot/__init__.py +0 -0
- emerge/_emerge/plot/display.py +394 -0
- emerge/_emerge/plot/grapher.py +93 -0
- emerge/_emerge/plot/matplotlib/mpldisplay.py +264 -0
- emerge/_emerge/plot/pyvista/__init__.py +1 -0
- emerge/_emerge/plot/pyvista/display.py +931 -0
- emerge/_emerge/plot/pyvista/display_settings.py +24 -0
- emerge/_emerge/plot/simple_plots.py +551 -0
- emerge/_emerge/plot.py +225 -0
- emerge/_emerge/projects/__init__.py +0 -0
- emerge/_emerge/projects/_gen_base.txt +32 -0
- emerge/_emerge/projects/_load_base.txt +24 -0
- emerge/_emerge/projects/generate_project.py +40 -0
- emerge/_emerge/selection.py +596 -0
- emerge/_emerge/simmodel.py +444 -0
- emerge/_emerge/simulation_data.py +411 -0
- emerge/_emerge/solver.py +993 -0
- emerge/_emerge/system.py +54 -0
- emerge/cli.py +19 -0
- emerge/lib.py +57 -0
- emerge/plot.py +1 -0
- emerge/pyvista.py +1 -0
- {emerge-0.4.6.dist-info → emerge-0.4.8.dist-info}/METADATA +1 -1
- emerge-0.4.8.dist-info/RECORD +78 -0
- emerge-0.4.8.dist-info/entry_points.txt +2 -0
- emerge-0.4.6.dist-info/RECORD +0 -4
- emerge-0.4.6.dist-info/entry_points.txt +0 -2
- {emerge-0.4.6.dist-info → emerge-0.4.8.dist-info}/WHEEL +0 -0
emerge/_emerge/solver.py
ADDED
|
@@ -0,0 +1,993 @@
|
|
|
1
|
+
# EMerge is an open source Python based FEM EM simulation module.
|
|
2
|
+
# Copyright (C) 2025 Robert Fennis.
|
|
3
|
+
|
|
4
|
+
# This program is free software; you can redistribute it and/or
|
|
5
|
+
# modify it under the terms of the GNU General Public License
|
|
6
|
+
# as published by the Free Software Foundation; either version 2
|
|
7
|
+
# of the License, or (at your option) any later version.
|
|
8
|
+
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, see
|
|
16
|
+
# <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
from scipy.sparse import lil_matrix, csc_matrix
|
|
21
|
+
from scipy.sparse.csgraph import reverse_cuthill_mckee
|
|
22
|
+
from scipy.sparse.linalg import bicgstab, gmres, gcrotmk, eigs, splu
|
|
23
|
+
from scipy.linalg import eig
|
|
24
|
+
from scipy import sparse
|
|
25
|
+
from dataclasses import dataclass
|
|
26
|
+
import numpy as np
|
|
27
|
+
from loguru import logger
|
|
28
|
+
import platform
|
|
29
|
+
import time
|
|
30
|
+
from typing import Literal
|
|
31
|
+
from enum import Enum
|
|
32
|
+
|
|
33
|
+
_PARDISO_AVAILABLE = False
|
|
34
|
+
_UMFPACK_AVAILABLE = False
|
|
35
|
+
_PARDISO_ERROR_CODES = """
|
|
36
|
+
0 | No error.
|
|
37
|
+
-1 | Input inconsistent.
|
|
38
|
+
-2 | Not enough memory.
|
|
39
|
+
-3 | Reordering problem.
|
|
40
|
+
-4 | Zero pivot, numerical fac. or iterative refinement problem.
|
|
41
|
+
-5 | Unclassified (internal) error.
|
|
42
|
+
-6 | Preordering failed (matrix types 11(real and nonsymmetric), 13(complex and nonsymmetric) only).
|
|
43
|
+
-7 | Diagonal Matrix problem.
|
|
44
|
+
-8 | 32-bit integer overflow problem.
|
|
45
|
+
-10 | No license file pardiso.lic found.
|
|
46
|
+
-11 | License is expired.
|
|
47
|
+
-12 | Wrong username or hostname.
|
|
48
|
+
-100 | Reached maximum number of Krylov-subspace iteration in iterative solver.
|
|
49
|
+
-101 | No sufficient convergence in Krylov-subspace iteration within 25 iterations.
|
|
50
|
+
-102 | Error in Krylov-subspace iteration.
|
|
51
|
+
-103 | Bread-Down in Krylov-subspace iteration
|
|
52
|
+
"""
|
|
53
|
+
""" Check if the PC runs on a non-ARM architechture
|
|
54
|
+
If so, attempt to import PyPardiso (if its installed)
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
if 'arm' not in platform.processor():
|
|
58
|
+
try:
|
|
59
|
+
from pypardiso import spsolve as pardiso_solve
|
|
60
|
+
from pypardiso.pardiso_wrapper import PyPardisoError
|
|
61
|
+
_PARDISO_AVAILABLE = True
|
|
62
|
+
except ModuleNotFoundError as e:
|
|
63
|
+
logger.info('Pardiso not found, defaulting to SuperLU')
|
|
64
|
+
try:
|
|
65
|
+
import scikits.umfpack as um
|
|
66
|
+
_UMFPACK_AVAILABLE = True
|
|
67
|
+
except ModuleNotFoundError as e:
|
|
68
|
+
logger.debug('UMFPACK not found, defaulting to SuperLU')
|
|
69
|
+
|
|
70
|
+
def filter_real_modes(eigvals, eigvecs, k0, ermax, urmax, sign):
|
|
71
|
+
"""
|
|
72
|
+
Given arrays of eigenvalues `eigvals` and eigenvectors `eigvecs` (cols of shape (N,)),
|
|
73
|
+
and a free‐space wavenumber k0, return only those eigenpairs whose eigenvalue can
|
|
74
|
+
correspond to a real propagation constant β (i.e. 0 ≤ β² ≤ k0²·ermax·urmax).
|
|
75
|
+
|
|
76
|
+
Assumes that `ermax` and `urmax` are defined in the surrounding scope.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
eigvals : 1D array_like of float
|
|
81
|
+
The generalized eigenvalues (β² candidates).
|
|
82
|
+
eigvecs : 2D array_like, shape (N, M)
|
|
83
|
+
The corresponding eigenvectors, one column per eigenvalue.
|
|
84
|
+
k0 : float
|
|
85
|
+
Free‐space wavenumber.
|
|
86
|
+
|
|
87
|
+
Returns
|
|
88
|
+
-------
|
|
89
|
+
filtered_vals : 1D ndarray
|
|
90
|
+
Subset of `eigvals` satisfying 0 ≤ eigval ≤ k0²·ermax·urmax (within numerical tol).
|
|
91
|
+
filtered_vecs : 2D ndarray
|
|
92
|
+
Columns of `eigvecs` corresponding to `filtered_vals`.
|
|
93
|
+
"""
|
|
94
|
+
minimum = 1
|
|
95
|
+
extremum = (k0**2) * ermax * urmax * 2
|
|
96
|
+
|
|
97
|
+
mask = (sign*eigvals <= extremum) & (sign*eigvals >= minimum)
|
|
98
|
+
filtered_vals = eigvals[mask]
|
|
99
|
+
filtered_vecs = eigvecs[:, mask]
|
|
100
|
+
k0vals = np.sqrt(sign*filtered_vals)
|
|
101
|
+
order = np.argsort(np.abs(k0vals))# ascending distance
|
|
102
|
+
filtered_vals = filtered_vals[order] # reorder eigenvalues
|
|
103
|
+
filtered_vecs = filtered_vecs[:, order]
|
|
104
|
+
return filtered_vals, filtered_vecs
|
|
105
|
+
|
|
106
|
+
def filter_unique_eigenpairs(eigen_values: list[complex], eigen_vectors: list[np.ndarray], tol=-3) -> tuple[list[complex], list[np.ndarray]]:
|
|
107
|
+
"""
|
|
108
|
+
Filters eigenvectors by orthogonality using dot-product tolerance.
|
|
109
|
+
|
|
110
|
+
Parameters:
|
|
111
|
+
eigen_values (np.ndarray): Array of eigenvalues, shape (n,)
|
|
112
|
+
eigen_vectors (np.ndarray): Array of eigenvectors, shape (n, n)
|
|
113
|
+
tol (float): Dot product tolerance for considering vectors orthogonal (default: 1e-5)
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
unique_values (np.ndarray): Filtered eigenvalues
|
|
117
|
+
unique_vectors (np.ndarray): Corresponding orthogonal eigenvectors
|
|
118
|
+
"""
|
|
119
|
+
selected = []
|
|
120
|
+
indices = []
|
|
121
|
+
for i in range(len(eigen_vectors)):
|
|
122
|
+
|
|
123
|
+
vec = eigen_vectors[i]
|
|
124
|
+
vec = vec / np.linalg.norm(vec) # Normalize
|
|
125
|
+
|
|
126
|
+
# Check orthogonality against selected vectors
|
|
127
|
+
if all(10*np.log10(abs(np.dot(vec, sel))) < tol for sel in selected):
|
|
128
|
+
selected.append(vec)
|
|
129
|
+
indices.append(i)
|
|
130
|
+
|
|
131
|
+
unique_values = [eigen_values[i] for i in indices]
|
|
132
|
+
unique_vectors = [eigen_vectors[i] for i in indices]
|
|
133
|
+
|
|
134
|
+
return unique_values, unique_vectors
|
|
135
|
+
|
|
136
|
+
def complex_to_real_block(A, b):
|
|
137
|
+
"""Return (Â, b̂) real-augmented representation of A x = b."""
|
|
138
|
+
A_r = sparse.csr_matrix(A.real)
|
|
139
|
+
A_i = sparse.csr_matrix(A.imag)
|
|
140
|
+
# [ ReA -ImA ]
|
|
141
|
+
# [ ImA ReA ]
|
|
142
|
+
upper = sparse.hstack([A_r, -A_i])
|
|
143
|
+
lower = sparse.hstack([A_i, A_r])
|
|
144
|
+
A_hat = sparse.vstack([upper, lower]).tocsr()
|
|
145
|
+
|
|
146
|
+
b_hat = np.hstack([b.real, b.imag])
|
|
147
|
+
return A_hat, b_hat
|
|
148
|
+
|
|
149
|
+
def real_to_complex_block(x):
|
|
150
|
+
"""Return x = (x_r, x_i) as complex vector."""
|
|
151
|
+
n = x.shape[0] // 2
|
|
152
|
+
x_r = x[:n]
|
|
153
|
+
x_i = x[n:]
|
|
154
|
+
return x_r + 1j * x_i
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class Sorter:
|
|
158
|
+
""" A Generic class that executes a sort on the indices.
|
|
159
|
+
It must implement a sort and unsort method.
|
|
160
|
+
"""
|
|
161
|
+
def __init__(self):
|
|
162
|
+
self.perm = None
|
|
163
|
+
self.inv_perm = None
|
|
164
|
+
|
|
165
|
+
def reset(self) -> str:
|
|
166
|
+
""" Reset the permuation vectors."""
|
|
167
|
+
self.perm = None
|
|
168
|
+
self.inv_perm = None
|
|
169
|
+
|
|
170
|
+
def __str__(self) -> str:
|
|
171
|
+
return f'{self.__class__.__name__}'
|
|
172
|
+
|
|
173
|
+
def sort(self, A: lil_matrix, b: np.ndarray, reuse_sorting: bool = False) -> tuple[lil_matrix, np.ndarray]:
|
|
174
|
+
return A,b
|
|
175
|
+
|
|
176
|
+
def unsort(self, x: np.ndarray) -> np.ndarray:
|
|
177
|
+
return x
|
|
178
|
+
|
|
179
|
+
class Preconditioner:
|
|
180
|
+
"""A Generic class defining a preconditioner as attribute .M based on the
|
|
181
|
+
matrix A and b. This must be generated in the .init(A,b) method.
|
|
182
|
+
"""
|
|
183
|
+
def __init__(self):
|
|
184
|
+
self.M: np.ndarray = None
|
|
185
|
+
|
|
186
|
+
def __str__(self) -> str:
|
|
187
|
+
return f'{self.__class__.__name__}'
|
|
188
|
+
|
|
189
|
+
def init(self, A: lil_matrix, b: np.ndarray) -> None:
|
|
190
|
+
pass
|
|
191
|
+
|
|
192
|
+
class Solver:
|
|
193
|
+
""" A generic class representing a solver for the problem Ax=b
|
|
194
|
+
|
|
195
|
+
A solver class has two class attributes.
|
|
196
|
+
- real_only: defines if the solver can only deal with real numbers. In this case
|
|
197
|
+
the solve routine will automatically provide A and b in real number format.
|
|
198
|
+
- req_sorter: defines if this solver requires the use of a sorter algorithm. By setting
|
|
199
|
+
it to False, the SolveRoutine will not use the default sorting algorithm.
|
|
200
|
+
"""
|
|
201
|
+
real_only: bool = False
|
|
202
|
+
req_sorter: bool = False
|
|
203
|
+
def __init__(self):
|
|
204
|
+
self.own_preconditioner: bool = False
|
|
205
|
+
|
|
206
|
+
def __str__(self) -> str:
|
|
207
|
+
return f'{self.__class__.__name__}'
|
|
208
|
+
|
|
209
|
+
def solve(self, A: lil_matrix, b: np.ndarray, precon: Preconditioner, reuse_factorization: bool = False, id: int = -1) -> tuple[np.ndarray, int]:
|
|
210
|
+
raise NotImplementedError("This classes Ax=B solver method is not implemented.")
|
|
211
|
+
|
|
212
|
+
def reset(self) -> None:
|
|
213
|
+
"""Reset state variables like numeric and symbollic factorizations."""
|
|
214
|
+
pass
|
|
215
|
+
|
|
216
|
+
class EigSolver:
|
|
217
|
+
""" A generic class representing a solver for the eigenvalue problem Ax=λBx
|
|
218
|
+
|
|
219
|
+
A solver class has two class attributes.
|
|
220
|
+
- real_only: defines if the solver can only deal with real numbers. In this case
|
|
221
|
+
the solve routine will automatically provide A and b in real number format.
|
|
222
|
+
- req_sorter: defines if this solver requires the use of a sorter algorithm. By setting
|
|
223
|
+
it to False, the SolveRoutine will not use the default sorting algorithm.
|
|
224
|
+
"""
|
|
225
|
+
real_only: bool = False
|
|
226
|
+
req_sorter: bool = False
|
|
227
|
+
def __init__(self):
|
|
228
|
+
self.own_preconditioner: bool = False
|
|
229
|
+
|
|
230
|
+
def __str__(self) -> str:
|
|
231
|
+
return f'{self.__class__.__name__}'
|
|
232
|
+
|
|
233
|
+
def eig(self, A: lil_matrix, B: np.ndarray, nmodes: int = 6, target_k0: float = None, which: str = 'LM', sign: float = 1.):
|
|
234
|
+
raise NotImplementedError("This classes eigenmdoe solver method is not implemented.")
|
|
235
|
+
|
|
236
|
+
def reset(self) -> None:
|
|
237
|
+
"""Reset state variables like numeric and symbollic factorizations."""
|
|
238
|
+
pass
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
## ----- SORTERS ----------------------------------------------
|
|
242
|
+
@dataclass
|
|
243
|
+
class SolveReport:
|
|
244
|
+
solver: str
|
|
245
|
+
sorter: str
|
|
246
|
+
precon: str
|
|
247
|
+
simtime: float
|
|
248
|
+
ndof: int
|
|
249
|
+
nnz: int
|
|
250
|
+
code: int = None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class ReverseCuthillMckee(Sorter):
|
|
254
|
+
""" Implements the Reverse Cuthill-Mckee sorting."""
|
|
255
|
+
def __init__(self):
|
|
256
|
+
super().__init__()
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def sort(self, A, b, reuse_sorting: bool = False):
|
|
260
|
+
if not reuse_sorting:
|
|
261
|
+
logger.debug('Generating Reverse Cuthill-Mckee sorting.')
|
|
262
|
+
self.perm = reverse_cuthill_mckee(A)
|
|
263
|
+
self.inv_perm = np.argsort(self.perm)
|
|
264
|
+
logger.debug('Applying Reverse Cuthill-Mckee sorting.')
|
|
265
|
+
Asorted = A[self.perm, :][:, self.perm]
|
|
266
|
+
bsorted = b[self.perm]
|
|
267
|
+
return Asorted, bsorted
|
|
268
|
+
|
|
269
|
+
def unsort(self, x: np.ndarray):
|
|
270
|
+
logger.debug('Reversing Reverse Cuthill-Mckee sorting.')
|
|
271
|
+
return x[self.inv_perm]
|
|
272
|
+
|
|
273
|
+
## ----- PRECONS ----------------------------------------------
|
|
274
|
+
|
|
275
|
+
class ILUPrecon(Preconditioner):
|
|
276
|
+
""" Implements the incomplete LU preconditioner on matrix A. """
|
|
277
|
+
def __init__(self):
|
|
278
|
+
super().__init__()
|
|
279
|
+
self.M = None
|
|
280
|
+
self.fill_factor = 10
|
|
281
|
+
self.options: dict[str, str] = dict(SymmetricMode=True)
|
|
282
|
+
|
|
283
|
+
def init(self, A, b):
|
|
284
|
+
logger.info("Generating ILU Preconditioner")
|
|
285
|
+
self.ilu = sparse.linalg.spilu(A, drop_tol=1e-2, fill_factor=self.fill_factor, permc_spec='MMD_AT_PLUS_A', diag_pivot_thresh=0.001, options=self.options)
|
|
286
|
+
self.M = sparse.linalg.LinearOperator(A.shape, self.ilu.solve)
|
|
287
|
+
|
|
288
|
+
## ----- ITERATIVE SOLVERS -------------------------------------
|
|
289
|
+
|
|
290
|
+
class SolverBicgstab(Solver):
|
|
291
|
+
""" Implements the Bi-Conjugate Gradient Stabilized method"""
|
|
292
|
+
def __init__(self):
|
|
293
|
+
super().__init__()
|
|
294
|
+
self.atol = 1e-5
|
|
295
|
+
|
|
296
|
+
self.A: np.ndarray = None
|
|
297
|
+
self.b: np.ndarray = None
|
|
298
|
+
|
|
299
|
+
def callback(self, xk):
|
|
300
|
+
convergence = np.linalg.norm((self.A @ xk - self.b))
|
|
301
|
+
logger.info(f'Iteration {convergence:.4f}')
|
|
302
|
+
|
|
303
|
+
def solve(self, A, b, precon, reuse_factorization: bool = False, id: int = -1):
|
|
304
|
+
logger.info(f'Calling BiCGStab. ID={id}')
|
|
305
|
+
self.A = A
|
|
306
|
+
self.b = b
|
|
307
|
+
if precon.M is not None:
|
|
308
|
+
x, info = bicgstab(A, b, M=precon.M, atol=self.atol, callback=self.callback)
|
|
309
|
+
else:
|
|
310
|
+
x, info = bicgstab(A, b, atol=self.atol, callback=self.callback)
|
|
311
|
+
return x, info
|
|
312
|
+
|
|
313
|
+
class SolverGCROTMK(Solver):
|
|
314
|
+
""" Implements the GCRO-T(m,k) Iterative solver. """
|
|
315
|
+
def __init__(self):
|
|
316
|
+
super().__init__()
|
|
317
|
+
self.atol = 1e-5
|
|
318
|
+
|
|
319
|
+
self.A: np.ndarray = None
|
|
320
|
+
self.b: np.ndarray = None
|
|
321
|
+
|
|
322
|
+
def callback(self, xk):
|
|
323
|
+
convergence = np.linalg.norm((self.A @ xk - self.b))
|
|
324
|
+
logger.info(f'Iteration {convergence:.4f}')
|
|
325
|
+
|
|
326
|
+
def solve(self, A, b, precon, id: int = -1):
|
|
327
|
+
logger.info(f'Calling GCRO-T(m,k) algorithm. ID={id}')
|
|
328
|
+
self.A = A
|
|
329
|
+
self.b = b
|
|
330
|
+
if precon.M is not None:
|
|
331
|
+
x, info = gcrotmk(A, b, M=precon.M, atol=self.atol, callback=self.callback)
|
|
332
|
+
else:
|
|
333
|
+
x, info = gcrotmk(A, b, atol=self.atol, callback=self.callback)
|
|
334
|
+
return x, info
|
|
335
|
+
|
|
336
|
+
class SolverGMRES(Solver):
|
|
337
|
+
""" Implements the GMRES solver. """
|
|
338
|
+
real_only = False
|
|
339
|
+
req_sorter = True
|
|
340
|
+
|
|
341
|
+
def __init__(self):
|
|
342
|
+
super().__init__()
|
|
343
|
+
self.atol = 1e-5
|
|
344
|
+
|
|
345
|
+
self.A: np.ndarray = None
|
|
346
|
+
self.b: np.ndarray = None
|
|
347
|
+
|
|
348
|
+
def callback(self, norm):
|
|
349
|
+
#convergence = np.linalg.norm((self.A @ xk - self.b))
|
|
350
|
+
logger.info(f'Iteration {norm:.4f}')
|
|
351
|
+
|
|
352
|
+
def solve(self, A, b, precon, reuse_factorization: bool = False, id: int = -1):
|
|
353
|
+
logger.info(f'Calling GMRES Function. ID={id}')
|
|
354
|
+
self.A = A
|
|
355
|
+
self.b = b
|
|
356
|
+
if precon.M is not None:
|
|
357
|
+
x, info = gmres(A, b, M=precon.M, atol=self.atol, callback=self.callback, callback_type='pr_norm')
|
|
358
|
+
else:
|
|
359
|
+
x, info = gmres(A, b, atol=self.atol, callback=self.callback, restart=500, callback_type='pr_norm')
|
|
360
|
+
return x, info
|
|
361
|
+
|
|
362
|
+
## ----- DIRECT SOLVERS ----------------------------------------
|
|
363
|
+
|
|
364
|
+
class SolverSuperLU(Solver):
|
|
365
|
+
""" Implements Scipi's direct SuperLU solver."""
|
|
366
|
+
req_sorter: bool = False
|
|
367
|
+
real_only: bool = False
|
|
368
|
+
def __init__(self):
|
|
369
|
+
super().__init__()
|
|
370
|
+
self.atol = 1e-5
|
|
371
|
+
|
|
372
|
+
self.A: np.ndarray = None
|
|
373
|
+
self.b: np.ndarray = None
|
|
374
|
+
self.options: dict[str, str] = dict(SymmetricMode=True, Equil=False, IterRefine='SINGLE')
|
|
375
|
+
self.lu = None
|
|
376
|
+
|
|
377
|
+
def solve(self, A, b, precon, reuse_factorization: bool = False, id: int = -1):
|
|
378
|
+
logger.info(f'Calling SuperLU Solver, ID={id}')
|
|
379
|
+
self.single = True
|
|
380
|
+
if not reuse_factorization:
|
|
381
|
+
self.lu = splu(A, permc_spec='MMD_AT_PLUS_A', relax=0, diag_pivot_thresh=0.001, options=self.options)
|
|
382
|
+
x = self.lu.solve(b)
|
|
383
|
+
|
|
384
|
+
return x, 0
|
|
385
|
+
|
|
386
|
+
class SolverUMFPACK(Solver):
|
|
387
|
+
""" Implements the UMFPACK Sparse SP solver."""
|
|
388
|
+
req_sorter = False
|
|
389
|
+
real_only = False
|
|
390
|
+
|
|
391
|
+
def __init__(self):
|
|
392
|
+
super().__init__()
|
|
393
|
+
self.A: np.ndarray = None
|
|
394
|
+
self.b: np.ndarray = None
|
|
395
|
+
self.up: um.UmfpackContext = um.UmfpackContext('zi')
|
|
396
|
+
self.up.control[um.UMFPACK_PRL] = 0 #less terminal printing
|
|
397
|
+
self.up.control[um.UMFPACK_IRSTEP] = 2
|
|
398
|
+
self.up.control[um.UMFPACK_STRATEGY] = um.UMFPACK_STRATEGY_SYMMETRIC
|
|
399
|
+
self.up.control[um.UMFPACK_ORDERING] = 3
|
|
400
|
+
self.up.control[um.UMFPACK_PIVOT_TOLERANCE] = 0.001
|
|
401
|
+
self.up.control[um.UMFPACK_SYM_PIVOT_TOLERANCE] = 0.001
|
|
402
|
+
self.up.control[um.UMFPACK_BLOCK_SIZE] = 64
|
|
403
|
+
self.up.control[um.UMFPACK_FIXQ] = -1
|
|
404
|
+
|
|
405
|
+
self.fact_symb: bool = False
|
|
406
|
+
|
|
407
|
+
def reset(self) -> None:
|
|
408
|
+
self.fact_symb = False
|
|
409
|
+
|
|
410
|
+
def solve(self, A, b, precon, reuse_factorization: bool = False, id: int = -1):
|
|
411
|
+
logger.info(f'Calling UMFPACK Solver. ID={id}')
|
|
412
|
+
if self.fact_symb is False:
|
|
413
|
+
logger.debug('Executing symbollic factorization.')
|
|
414
|
+
self.up.symbolic(A)
|
|
415
|
+
#self.up.report_symbolic()
|
|
416
|
+
self.fact_symb = True
|
|
417
|
+
if not reuse_factorization:
|
|
418
|
+
#logger.debug('Executing numeric factorization.')
|
|
419
|
+
self.up.numeric(A)
|
|
420
|
+
self.A = A
|
|
421
|
+
x = self.up.solve(um.UMFPACK_A, self.A, b, autoTranspose = False )
|
|
422
|
+
return x, 0
|
|
423
|
+
|
|
424
|
+
class SolverPardiso(Solver):
|
|
425
|
+
""" Implements the PARDISO solver through PyPardiso. """
|
|
426
|
+
real_only: bool = True
|
|
427
|
+
req_sorter: bool = False
|
|
428
|
+
|
|
429
|
+
def __init__(self):
|
|
430
|
+
super().__init__()
|
|
431
|
+
|
|
432
|
+
self.A: np.ndarray = None
|
|
433
|
+
self.b: np.ndarray = None
|
|
434
|
+
|
|
435
|
+
def solve(self, A, b, precon, reuse_factorization: bool = False, id: int = -1):
|
|
436
|
+
logger.info(f'Calling Pardiso Solver. ID={id}')
|
|
437
|
+
self.A = A
|
|
438
|
+
self.b = b
|
|
439
|
+
try:
|
|
440
|
+
x = pardiso_solve(A, b)
|
|
441
|
+
except PyPardisoError as e:
|
|
442
|
+
print('Error Codes:')
|
|
443
|
+
print(_PARDISO_ERROR_CODES)
|
|
444
|
+
return x, 0
|
|
445
|
+
|
|
446
|
+
## ----- DIRECT EIG SOLVERS --------------------------------------
|
|
447
|
+
class SolverLAPACK(EigSolver):
|
|
448
|
+
|
|
449
|
+
def __init__(self):
|
|
450
|
+
super().__init__()
|
|
451
|
+
|
|
452
|
+
def eig(self,
|
|
453
|
+
A: np.ndarray,
|
|
454
|
+
B: np.ndarray,
|
|
455
|
+
nmodes: int = 6,
|
|
456
|
+
target_k0: float = 0,
|
|
457
|
+
which: str = 'LM',
|
|
458
|
+
sign: float = 1.0):
|
|
459
|
+
"""
|
|
460
|
+
Dense solver for A x = λ B x with A = Aᴴ, B = Bᴴ (B may be indefinite).
|
|
461
|
+
|
|
462
|
+
Parameters
|
|
463
|
+
----------
|
|
464
|
+
A, B : (n, n) array_like, complex127/complex64/float64
|
|
465
|
+
k : int or None
|
|
466
|
+
How many eigenpairs to return.
|
|
467
|
+
* None → return all n
|
|
468
|
+
* k>0 → return k pairs with |λ| smallest
|
|
469
|
+
|
|
470
|
+
Returns
|
|
471
|
+
-------
|
|
472
|
+
lam : (m,) real ndarray eigenvalues (m = n or k)
|
|
473
|
+
vecs : (n, m) complex ndarray eigenvectors, B-orthonormal (xiᴴ B xj = δij)
|
|
474
|
+
"""
|
|
475
|
+
logger.debug('Calling LAPACK eig solver')
|
|
476
|
+
lam, vecs = eig(A.toarray(), B.toarray(), overwrite_a=True, overwrite_b=True, check_finite=False)
|
|
477
|
+
lam, vecs = filter_real_modes(lam, vecs, target_k0, 2, 2, sign=sign)
|
|
478
|
+
return lam, vecs
|
|
479
|
+
|
|
480
|
+
## ----- ITER EIG SOLVERS ---------------------------------------
|
|
481
|
+
|
|
482
|
+
class SolverARPACK(EigSolver):
|
|
483
|
+
""" Implements the Scipy ARPACK iterative eigenmode solver."""
|
|
484
|
+
def __init__(self):
|
|
485
|
+
super().__init__()
|
|
486
|
+
|
|
487
|
+
def eig(self,
|
|
488
|
+
A: np.ndarray,
|
|
489
|
+
B: np.ndarray,
|
|
490
|
+
nmodes: int = 6,
|
|
491
|
+
target_k0: float = 0,
|
|
492
|
+
which: str = 'LM',
|
|
493
|
+
sign: float = 1.0):
|
|
494
|
+
logger.info(f'Searching around β = {target_k0:.2f} rad/m')
|
|
495
|
+
sigma = sign*(target_k0**2)
|
|
496
|
+
eigen_values, eigen_modes = eigs(A, k=nmodes, M=B, sigma=sigma, which=which)
|
|
497
|
+
return eigen_values, eigen_modes
|
|
498
|
+
|
|
499
|
+
class SmartARPACK_BMA(EigSolver):
|
|
500
|
+
""" Implements the Scipy ARPACK iterative eigenmode solver with automatic search.
|
|
501
|
+
|
|
502
|
+
The Solver searches in a geometric range around the target wave constant.
|
|
503
|
+
"""
|
|
504
|
+
def __init__(self):
|
|
505
|
+
super().__init__()
|
|
506
|
+
self.symmetric_steps: int = 41
|
|
507
|
+
self.search_range: float = 2.0
|
|
508
|
+
self.energy_limit: float = 1e-4
|
|
509
|
+
|
|
510
|
+
def eig(self,
|
|
511
|
+
A: np.ndarray,
|
|
512
|
+
B: np.ndarray,
|
|
513
|
+
nmodes: int = 6,
|
|
514
|
+
target_k0: float = 0,
|
|
515
|
+
which: str = 'LM',
|
|
516
|
+
sign: float = 1.):
|
|
517
|
+
logger.info(f'Searching around β = {target_k0:.2f} rad/m')
|
|
518
|
+
qs = np.geomspace(1, self.search_range, self.symmetric_steps)
|
|
519
|
+
tot_eigen_values = []
|
|
520
|
+
tot_eigen_modes = []
|
|
521
|
+
energies = []
|
|
522
|
+
for i, q in enumerate(qs):
|
|
523
|
+
# Above target k0
|
|
524
|
+
sigma = sign*((q*target_k0)**2)
|
|
525
|
+
eigen_values, eigen_modes = eigs(A, k=1, M=B, sigma=sigma, which=which)
|
|
526
|
+
energy = np.mean(np.abs(eigen_modes.flatten())**2)
|
|
527
|
+
if energy > self.energy_limit:
|
|
528
|
+
tot_eigen_values.append(eigen_values[0])
|
|
529
|
+
tot_eigen_modes.append(eigen_modes.flatten())
|
|
530
|
+
energies.append(energy)
|
|
531
|
+
if i!=0:
|
|
532
|
+
# Below target k0
|
|
533
|
+
sigma = sign*((target_k0/q)**2)
|
|
534
|
+
eigen_values, eigen_modes = eigs(A, k=1, M=B, sigma=sigma, which=which)
|
|
535
|
+
energy = np.mean(np.abs(eigen_modes.flatten())**2)
|
|
536
|
+
if energy > self.energy_limit:
|
|
537
|
+
tot_eigen_values.append(eigen_values[0])
|
|
538
|
+
tot_eigen_modes.append(eigen_modes.flatten())
|
|
539
|
+
energies.append(energy)
|
|
540
|
+
|
|
541
|
+
#Sort solutions on mode energy
|
|
542
|
+
val, mode, energy = zip(*sorted(zip(tot_eigen_values,tot_eigen_modes,energies), key=lambda x: x[2], reverse=True))
|
|
543
|
+
eigen_values = np.array(val[:nmodes])
|
|
544
|
+
eigen_modes = np.array(mode[:nmodes]).T
|
|
545
|
+
|
|
546
|
+
return eigen_values, eigen_modes
|
|
547
|
+
|
|
548
|
+
class SmartARPACK(EigSolver):
|
|
549
|
+
""" Implements the Scipy ARPACK iterative eigenmode solver with automatic search.
|
|
550
|
+
|
|
551
|
+
The Solver searches in a geometric range around the target wave constant.
|
|
552
|
+
"""
|
|
553
|
+
def __init__(self):
|
|
554
|
+
super().__init__()
|
|
555
|
+
self.symmetric_steps: int = 3
|
|
556
|
+
self.search_range: float = 2.0
|
|
557
|
+
self.energy_limit: float = 1e-4
|
|
558
|
+
|
|
559
|
+
def eig(self,
|
|
560
|
+
A: np.ndarray,
|
|
561
|
+
B: np.ndarray,
|
|
562
|
+
nmodes: int = 6,
|
|
563
|
+
target_k0: float = 0,
|
|
564
|
+
which: str = 'LM',
|
|
565
|
+
sign: float = 1.):
|
|
566
|
+
logger.info(f'Searching around β = {target_k0:.2f} rad/m')
|
|
567
|
+
qs = np.geomspace(1, self.search_range, self.symmetric_steps)
|
|
568
|
+
tot_eigen_values = []
|
|
569
|
+
tot_eigen_modes = []
|
|
570
|
+
for i, q in enumerate(qs):
|
|
571
|
+
# Above target k0
|
|
572
|
+
sigma = sign*((q*target_k0)**2)
|
|
573
|
+
eigen_values, eigen_modes = eigs(A, k=6, M=B, sigma=sigma, which=which)
|
|
574
|
+
for j in range(eigen_values.shape[0]):
|
|
575
|
+
if eigen_values[j]<(sigma/self.search_range):
|
|
576
|
+
continue
|
|
577
|
+
tot_eigen_values.append(eigen_values[j])
|
|
578
|
+
tot_eigen_modes.append(eigen_modes[:,j])
|
|
579
|
+
if i!=0:
|
|
580
|
+
# Below target k0
|
|
581
|
+
sigma = sign*((target_k0/q)**2)
|
|
582
|
+
eigen_values, eigen_modes = eigs(A, k=6, M=B, sigma=sigma, which=which)
|
|
583
|
+
for j in range(eigen_values.shape[0]):
|
|
584
|
+
if eigen_values[j]<(sigma/self.search_range):
|
|
585
|
+
continue
|
|
586
|
+
tot_eigen_values.append(eigen_values[j])
|
|
587
|
+
tot_eigen_modes.append(eigen_modes[:,j])
|
|
588
|
+
tot_eigen_values, tot_eigen_modes = filter_unique_eigenpairs(tot_eigen_values, tot_eigen_modes)
|
|
589
|
+
if len(tot_eigen_values)>nmodes:
|
|
590
|
+
break
|
|
591
|
+
#Sort solutions on mode energy
|
|
592
|
+
val, mode = filter_unique_eigenpairs(np.array(tot_eigen_values), np.array(tot_eigen_modes))
|
|
593
|
+
val, mode = zip(*sorted(zip(val,mode), key=lambda x: x[0], reverse=False))
|
|
594
|
+
eigen_values = np.array(val[:nmodes])
|
|
595
|
+
eigen_modes = np.array(mode[:nmodes]).T
|
|
596
|
+
|
|
597
|
+
return eigen_values, eigen_modes
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
## ----- SOLVE ENUMS ---------------------------------------------
|
|
601
|
+
|
|
602
|
+
class EMSolver(Enum):
|
|
603
|
+
SUPERLU = 1
|
|
604
|
+
UMFPACK = 2
|
|
605
|
+
PARDISO = 3
|
|
606
|
+
LAPACK = 4
|
|
607
|
+
ARPACK = 5
|
|
608
|
+
SMART_ARPACK = 6
|
|
609
|
+
SMART_ARPACK_BMA = 7
|
|
610
|
+
|
|
611
|
+
def get_solver(self) -> Solver:
|
|
612
|
+
if self==EMSolver.SUPERLU:
|
|
613
|
+
return SolverSuperLU()
|
|
614
|
+
elif self==EMSolver.UMFPACK:
|
|
615
|
+
if _UMFPACK_AVAILABLE is False:
|
|
616
|
+
return SolverSuperLU()
|
|
617
|
+
else:
|
|
618
|
+
return SolverUMFPACK()
|
|
619
|
+
elif self==EMSolver.PARDISO:
|
|
620
|
+
if _PARDISO_AVAILABLE is False:
|
|
621
|
+
return SolverSuperLU()
|
|
622
|
+
else:
|
|
623
|
+
return SolverPardiso()
|
|
624
|
+
elif self==EMSolver.LAPACK:
|
|
625
|
+
return SolverLAPACK()
|
|
626
|
+
elif self==EMSolver.ARPACK:
|
|
627
|
+
return SolverARPACK()
|
|
628
|
+
elif self==EMSolver.SMART_ARPACK:
|
|
629
|
+
return SmartARPACK()
|
|
630
|
+
elif self==EMSolver.SMART_ARPACK_BMA:
|
|
631
|
+
return SmartARPACK_BMA()
|
|
632
|
+
|
|
633
|
+
## ----- SOLVE ROUTINES -----------------------------------------
|
|
634
|
+
|
|
635
|
+
class SolveRoutine:
|
|
636
|
+
""" A generic class describing a solve routine.
|
|
637
|
+
A solve routine contains all the relevant sorter preconditioner and solver objects
|
|
638
|
+
and goes through a sequence of steps to solve a linear system or find eigenmodes.
|
|
639
|
+
|
|
640
|
+
"""
|
|
641
|
+
def __init__(self):
|
|
642
|
+
|
|
643
|
+
self.sorter: Sorter = ReverseCuthillMckee()
|
|
644
|
+
self.precon: Preconditioner = ILUPrecon()
|
|
645
|
+
self.solvers: dict[EMSolver, Solver] = {slv: slv.get_solver() for slv in EMSolver}
|
|
646
|
+
|
|
647
|
+
self.parallel: Literal['SI','MT','MP'] = 'SI'
|
|
648
|
+
self.smart_search: bool = False
|
|
649
|
+
self.forced_solver: list[Solver] = []
|
|
650
|
+
|
|
651
|
+
self.use_sorter: bool = False
|
|
652
|
+
self.use_preconditioner: bool = False
|
|
653
|
+
self.use_direct: bool = True
|
|
654
|
+
|
|
655
|
+
def __str__(self) -> str:
|
|
656
|
+
return f'SolveRoutine({self.sorter},{self.precon},{self.iterative_solver}, {self.direct_solver})'
|
|
657
|
+
|
|
658
|
+
def duplicate(self) -> SolveRoutine:
|
|
659
|
+
"""Creates a copy of this SolveRoutine class object.
|
|
660
|
+
|
|
661
|
+
Returns:
|
|
662
|
+
SolveRoutine: The copied version
|
|
663
|
+
"""
|
|
664
|
+
new_routine = self.__class__()
|
|
665
|
+
new_routine.parallel = self.parallel
|
|
666
|
+
new_routine.smart_search = self.smart_search
|
|
667
|
+
new_routine.forced_solver = self.forced_solver
|
|
668
|
+
return new_routine
|
|
669
|
+
|
|
670
|
+
def set_solver(self, *solver: EMSolver | EigSolver | Solver) -> None:
|
|
671
|
+
"""Set a given Solver class instance as the main solver. Solvers will be checked on validity for the given problem.
|
|
672
|
+
|
|
673
|
+
Args:
|
|
674
|
+
solver (EMSolver | Solver): The solver objects
|
|
675
|
+
"""
|
|
676
|
+
if isinstance(solver, EMSolver):
|
|
677
|
+
self.forced_solver.append(solver.get_solver())
|
|
678
|
+
else:
|
|
679
|
+
self.forced_solver.append(solver)
|
|
680
|
+
|
|
681
|
+
def configure(self,
|
|
682
|
+
parallel: Literal['SI','MT','MP'] = 'SI', smart_search: bool = False) -> SolveRoutine:
|
|
683
|
+
"""Configure the solver with the given settings
|
|
684
|
+
|
|
685
|
+
Args:
|
|
686
|
+
parallel (Literal['SI','MT','MP'], optional):
|
|
687
|
+
The solver parallism, Defaults to 'SI'.
|
|
688
|
+
- "SI" = Single threaded
|
|
689
|
+
- "MT" = Multi threaded
|
|
690
|
+
- "MP" = Multi-processing,
|
|
691
|
+
smart_search (bool, optional): Wether to use smart-search solvers for eigenmode problems. Defaults to False.
|
|
692
|
+
|
|
693
|
+
Returns:
|
|
694
|
+
SolveRoutine: The same SolveRoutine object.
|
|
695
|
+
"""
|
|
696
|
+
self.parallel = parallel
|
|
697
|
+
self.smart_search = smart_search
|
|
698
|
+
return self
|
|
699
|
+
|
|
700
|
+
def reset(self) -> None:
|
|
701
|
+
"""Reset all solver states"""
|
|
702
|
+
for solver in self.solvers.values():
|
|
703
|
+
solver.reset()
|
|
704
|
+
self.sorter.reset()
|
|
705
|
+
self.parallel: Literal['SI','MT','MP'] = 'SI'
|
|
706
|
+
self.smart_search: bool = False
|
|
707
|
+
self.forced_solver = []
|
|
708
|
+
|
|
709
|
+
def _get_solver(self, A: lil_matrix, b: np.ndarray) -> Solver:
|
|
710
|
+
"""Returns the relevant Solver object given a certain matrix and source vector
|
|
711
|
+
|
|
712
|
+
This is the default implementation for the SolveRoutine Class.
|
|
713
|
+
|
|
714
|
+
Args:
|
|
715
|
+
A (np.ndarray): The Matrix to solve for
|
|
716
|
+
b (np.ndarray): the vector to solve for
|
|
717
|
+
|
|
718
|
+
Returns:
|
|
719
|
+
Solver: Returns the direct solver
|
|
720
|
+
|
|
721
|
+
"""
|
|
722
|
+
for solver in self.forced_solver:
|
|
723
|
+
if isinstance(solver, Solver):
|
|
724
|
+
return solver
|
|
725
|
+
return self.pick_solver(A,b)
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def pick_solver(self, A: lil_matrix, b: np.ndarray) -> Solver:
|
|
729
|
+
"""Returns the relevant Solver object given a certain matrix and source vector
|
|
730
|
+
|
|
731
|
+
This is the default implementation for the SolveRoutine Class.
|
|
732
|
+
|
|
733
|
+
Args:
|
|
734
|
+
A (np.ndarray): The Matrix to solve for
|
|
735
|
+
b (np.ndarray): the vector to solve for
|
|
736
|
+
|
|
737
|
+
Returns:
|
|
738
|
+
Solver: Returns the direct solver
|
|
739
|
+
|
|
740
|
+
"""
|
|
741
|
+
return self.solvers[EMSolver.SUPERLU]
|
|
742
|
+
|
|
743
|
+
def _get_eig_solver(self, A: lil_matrix, b: lil_matrix, direct: bool = None) -> Solver:
|
|
744
|
+
"""Returns the relevant eigenmode Solver object given a certain matrix and source vector
|
|
745
|
+
|
|
746
|
+
This is the default implementation for the SolveRoutine Class.
|
|
747
|
+
|
|
748
|
+
Args:
|
|
749
|
+
A (np.ndarray): The Matrix to solve for
|
|
750
|
+
b (np.ndarray): the vector to solve for
|
|
751
|
+
direct (bool): If the direct solver should be used.
|
|
752
|
+
|
|
753
|
+
Returns:
|
|
754
|
+
Solver: Returns the solver object
|
|
755
|
+
|
|
756
|
+
"""
|
|
757
|
+
for solver in self.forced_solver:
|
|
758
|
+
if isinstance(solver, EigSolver):
|
|
759
|
+
return solver
|
|
760
|
+
if direct or A.shape[0] < 1000:
|
|
761
|
+
return self.solvers[EMSolver.LAPACK]
|
|
762
|
+
else:
|
|
763
|
+
return self.solvers[EMSolver.SMART_ARPACK]
|
|
764
|
+
|
|
765
|
+
def _get_eig_solver_bma(self, A: lil_matrix, b: lil_matrix, direct: bool = None) -> Solver:
|
|
766
|
+
"""Returns the relevant eigenmode Solver object given a certain matrix and source vector
|
|
767
|
+
|
|
768
|
+
This is the default implementation for the SolveRoutine Class.
|
|
769
|
+
|
|
770
|
+
Args:
|
|
771
|
+
A (np.ndarray): The Matrix to solve for
|
|
772
|
+
b (np.ndarray): the vector to solve for
|
|
773
|
+
direct (bool): If the direct solver should be used.
|
|
774
|
+
|
|
775
|
+
Returns:
|
|
776
|
+
Solver: Returns the solver object
|
|
777
|
+
|
|
778
|
+
"""
|
|
779
|
+
for solver in self.forced_solver:
|
|
780
|
+
if isinstance(solver, EigSolver):
|
|
781
|
+
return solver
|
|
782
|
+
|
|
783
|
+
if direct or A.shape[0] < 1000:
|
|
784
|
+
return self.solvers[EMSolver.LAPACK]
|
|
785
|
+
else:
|
|
786
|
+
return self.solvers[EMSolver.ARPACK]
|
|
787
|
+
|
|
788
|
+
def solve(self, A: np.ndarray | lil_matrix | csc_matrix,
|
|
789
|
+
b: np.ndarray,
|
|
790
|
+
solve_ids: np.ndarray,
|
|
791
|
+
reuse: bool = False,
|
|
792
|
+
id: int = -1) -> tuple[np.ndarray, SolveReport]:
|
|
793
|
+
""" Solve the system of equations defined by Ax=b for x.
|
|
794
|
+
|
|
795
|
+
Solve is the main function call to solve a linear system of equations defined by Ax=b.
|
|
796
|
+
The solve routine will go through the required steps defined in the routine to tackle the problme.
|
|
797
|
+
|
|
798
|
+
Args:
|
|
799
|
+
A (np.ndarray | lil_matrix | csc_matrix): The (Sparse) matrix
|
|
800
|
+
b (np.ndarray): The source vector
|
|
801
|
+
solve_ids (np.ndarray): A vector of ids for which to solve the problem. For EM problems this
|
|
802
|
+
implies all non-PEC degrees of freedom.
|
|
803
|
+
reuse (bool): Whether to reuse the existing factorization if it exists.
|
|
804
|
+
|
|
805
|
+
Returns:
|
|
806
|
+
np.ndarray: The resultant solution.
|
|
807
|
+
"""
|
|
808
|
+
solver = self._get_solver(A, b)
|
|
809
|
+
|
|
810
|
+
NF = A.shape[0]
|
|
811
|
+
NS = solve_ids.shape[0]
|
|
812
|
+
|
|
813
|
+
A = A.tocsc()
|
|
814
|
+
|
|
815
|
+
logger.debug(f' Removing {NF-NS} prescribed DOFs ({NS} left)')
|
|
816
|
+
|
|
817
|
+
Asel = A[np.ix_(solve_ids, solve_ids)]
|
|
818
|
+
bsel = b[solve_ids]
|
|
819
|
+
|
|
820
|
+
if solver.real_only:
|
|
821
|
+
logger.debug(' Converting to real matrix')
|
|
822
|
+
Asel, bsel = complex_to_real_block(Asel, bsel)
|
|
823
|
+
|
|
824
|
+
# SORT
|
|
825
|
+
sorter = 'None'
|
|
826
|
+
if solver.req_sorter and self.use_sorter:
|
|
827
|
+
sorter = str(self.sorter)
|
|
828
|
+
Asorted, bsorted = self.sorter.sort(Asel, bsel, reuse_sorting=reuse)
|
|
829
|
+
else:
|
|
830
|
+
Asorted, bsorted = Asel, bsel
|
|
831
|
+
|
|
832
|
+
# Preconditioner
|
|
833
|
+
precon = 'None'
|
|
834
|
+
if self.use_preconditioner:
|
|
835
|
+
if not self.iterative_solver.own_preconditioner:
|
|
836
|
+
self.precon.init(Asorted, bsorted)
|
|
837
|
+
precon = str(self.precon)
|
|
838
|
+
|
|
839
|
+
start = time.time()
|
|
840
|
+
x_solved, code = solver.solve(Asorted, bsorted, self.precon, reuse_factorization=reuse, id=id)
|
|
841
|
+
end = time.time()
|
|
842
|
+
simtime = end-start
|
|
843
|
+
logger.info(f'Time taken: {simtime:.3f} seconds')
|
|
844
|
+
logger.debug(f' O(N²) performance = {(NS**2)/((end-start+1e-6)*1e6):.3f} MDoF/s')
|
|
845
|
+
|
|
846
|
+
if self.use_sorter and solver.req_sorter:
|
|
847
|
+
x = self.sorter.unsort(x_solved)
|
|
848
|
+
else:
|
|
849
|
+
x = x_solved
|
|
850
|
+
|
|
851
|
+
if solver.real_only:
|
|
852
|
+
logger.debug(' Converting back to complex matrix')
|
|
853
|
+
x = real_to_complex_block(x)
|
|
854
|
+
|
|
855
|
+
solution = np.zeros((NF,), dtype=np.complex128)
|
|
856
|
+
|
|
857
|
+
solution[solve_ids] = x
|
|
858
|
+
|
|
859
|
+
logger.debug('Solver complete!')
|
|
860
|
+
if code:
|
|
861
|
+
logger.debug(' Solver code: {code}')
|
|
862
|
+
return solution, SolveReport(str(solver), sorter, precon, simtime, NS, A.nnz, code)
|
|
863
|
+
|
|
864
|
+
def eig_boundary(self,
|
|
865
|
+
A: np.ndarray | lil_matrix | csc_matrix,
|
|
866
|
+
B: np.ndarray,
|
|
867
|
+
solve_ids: np.ndarray,
|
|
868
|
+
nmodes: int = 6,
|
|
869
|
+
direct: bool = None,
|
|
870
|
+
target_k0: float = None,
|
|
871
|
+
which: str = 'LM',
|
|
872
|
+
sign: float=-1) -> tuple[np.ndarray, np.ndarray, SolveReport]:
|
|
873
|
+
""" Find the eigenmodes for the system Ax = λBx for a boundary mode problem
|
|
874
|
+
|
|
875
|
+
For generalized eigenvalue problems of boundary mode analysis studies, the equation is: Ae = -β²Be
|
|
876
|
+
|
|
877
|
+
Args:
|
|
878
|
+
A (csc_matrix): The Stiffness matrix
|
|
879
|
+
B (csc_matrix): The mass matrix
|
|
880
|
+
solve_ids (np.ndarray): The free nodes (non PEC)
|
|
881
|
+
nmodes (int): The number of modes to solve for. Defaults to 6
|
|
882
|
+
direct (bool): If the direct solver should be used (always). Defaults to False
|
|
883
|
+
target_k0 (float): The k0 value to search around
|
|
884
|
+
which (str): The search method. Defaults to 'LM' (Largest Magnitude)
|
|
885
|
+
sign (float): The sign of the eigenvalue expression. Defaults to -1
|
|
886
|
+
|
|
887
|
+
Returns:
|
|
888
|
+
np.ndarray: The eigen values
|
|
889
|
+
np.ndarray: The eigen vectors
|
|
890
|
+
SolveReport: The solution report
|
|
891
|
+
"""
|
|
892
|
+
solver = self._get_eig_solver_bma(A, B, direct=direct)
|
|
893
|
+
|
|
894
|
+
NF = A.shape[0]
|
|
895
|
+
NS = solve_ids.shape[0]
|
|
896
|
+
|
|
897
|
+
logger.debug(f' Removing {NF-NS} prescribed DOFs ({NS} left)')
|
|
898
|
+
|
|
899
|
+
Asel = A[np.ix_(solve_ids, solve_ids)]
|
|
900
|
+
Bsel = B[np.ix_(solve_ids, solve_ids)]
|
|
901
|
+
|
|
902
|
+
start = time.time()
|
|
903
|
+
eigen_values, eigen_modes = solver.eig(Asel, Bsel, nmodes, target_k0, which, sign=sign)
|
|
904
|
+
end = time.time()
|
|
905
|
+
|
|
906
|
+
simtime = end-start
|
|
907
|
+
return eigen_values, eigen_modes, SolveReport(str(solver), 'None', 'None', simtime, NS, A.nnz, simtime)
|
|
908
|
+
|
|
909
|
+
def eig(self,
|
|
910
|
+
A: np.ndarray | lil_matrix | csc_matrix,
|
|
911
|
+
B: np.ndarray,
|
|
912
|
+
solve_ids: np.ndarray,
|
|
913
|
+
nmodes: int = 6,
|
|
914
|
+
direct: bool = None,
|
|
915
|
+
target_f0: float = None,
|
|
916
|
+
which: str = 'LM') -> tuple[np.ndarray, np.ndarray, SolveReport]:
|
|
917
|
+
"""
|
|
918
|
+
Find the eigenmodes for the system Ax = λBx for a boundary mode problem
|
|
919
|
+
|
|
920
|
+
Args:
|
|
921
|
+
A (csc_matrix): The Stiffness matrix
|
|
922
|
+
B (csc_matrix): The mass matrix
|
|
923
|
+
solve_ids (np.ndarray): The free nodes (non PEC)
|
|
924
|
+
nmodes (int): The number of modes to solve for. Defaults to 6
|
|
925
|
+
direct (bool): If the direct solver should be used (always). Defaults to False
|
|
926
|
+
target_k0 (float): The k0 value to search around
|
|
927
|
+
which (str): The search method. Defaults to 'LM' (Largest Magnitude)
|
|
928
|
+
sign (float): The sign of the eigenvalue expression. Defaults to -1\
|
|
929
|
+
Returns:
|
|
930
|
+
np.ndarray: The resultant solution.
|
|
931
|
+
"""
|
|
932
|
+
solver = self._get_eig_solver(A, B, direct=direct)
|
|
933
|
+
|
|
934
|
+
NF = A.shape[0]
|
|
935
|
+
NS = solve_ids.shape[0]
|
|
936
|
+
|
|
937
|
+
logger.debug(f' Removing {NF-NS} prescribed DOFs ({NS} left)')
|
|
938
|
+
|
|
939
|
+
Asel = A[np.ix_(solve_ids, solve_ids)]
|
|
940
|
+
Bsel = B[np.ix_(solve_ids, solve_ids)]
|
|
941
|
+
|
|
942
|
+
start = time.time()
|
|
943
|
+
eigen_values, eigen_modes = solver.eig(Asel, Bsel, nmodes, target_f0, which, sign=1.0)
|
|
944
|
+
end = time.time()
|
|
945
|
+
simtime = end-start
|
|
946
|
+
|
|
947
|
+
Nsols = eigen_modes.shape[1]
|
|
948
|
+
sols = np.zeros((NF, Nsols), dtype=np.complex128)
|
|
949
|
+
for i in range(Nsols):
|
|
950
|
+
sols[solve_ids,i] = eigen_modes[:,i]
|
|
951
|
+
|
|
952
|
+
return eigen_values, sols, SolveReport(str(solver), 'None', 'None', simtime, NS, A.nnz, simtime)
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
class AutomaticRoutine(SolveRoutine):
|
|
956
|
+
""" Defines the Automatic Routine for EMerge.
|
|
957
|
+
"""
|
|
958
|
+
|
|
959
|
+
def pick_solver(self, A: np.ndarray, b: np.ndarray) -> Solver:
|
|
960
|
+
"""Returns the relevant Solver object given a certain matrix and source vector
|
|
961
|
+
|
|
962
|
+
The current implementation only looks at matrix size to select the best solver. Matrices
|
|
963
|
+
with a large size will use iterative solvers while smaller sizes will use either Pardiso
|
|
964
|
+
for medium sized problems or SPSolve for small ones.
|
|
965
|
+
|
|
966
|
+
Args:
|
|
967
|
+
A (np.ndarray): The Matrix to solve for
|
|
968
|
+
b (np.ndarray): the vector to solve for
|
|
969
|
+
|
|
970
|
+
Returns:
|
|
971
|
+
Solver: A solver object appropriate for solving the problem.
|
|
972
|
+
|
|
973
|
+
"""
|
|
974
|
+
N = b.shape[0]
|
|
975
|
+
if N < 10_000:
|
|
976
|
+
return self.solvers[EMSolver.SUPERLU]
|
|
977
|
+
if self.parallel=='SI':
|
|
978
|
+
if _PARDISO_AVAILABLE:
|
|
979
|
+
return self.solvers[EMSolver.PARDISO]
|
|
980
|
+
elif _UMFPACK_AVAILABLE:
|
|
981
|
+
return self.solvers[EMSolver.UMFPACK]
|
|
982
|
+
else:
|
|
983
|
+
return self.solvers[EMSolver.SUPERLU]
|
|
984
|
+
elif self.parallel=='MP':
|
|
985
|
+
if _UMFPACK_AVAILABLE:
|
|
986
|
+
return self.solvers[EMSolver.UMFPACK]
|
|
987
|
+
else:
|
|
988
|
+
return self.solvers[EMSolver.SUPERLU]
|
|
989
|
+
elif self.parallel=='MT':
|
|
990
|
+
return self.solvers[EMSolver.SUPERLU]
|
|
991
|
+
return self.solvers[EMSolver.SUPERLU]
|
|
992
|
+
|
|
993
|
+
DEFAULT_ROUTINE = AutomaticRoutine()
|