emerge 0.4.7__py3-none-any.whl → 0.4.9__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.

Files changed (78) hide show
  1. emerge/__init__.py +14 -14
  2. emerge/_emerge/__init__.py +42 -0
  3. emerge/_emerge/bc.py +197 -0
  4. emerge/_emerge/coord.py +119 -0
  5. emerge/_emerge/cs.py +523 -0
  6. emerge/_emerge/dataset.py +36 -0
  7. emerge/_emerge/elements/__init__.py +19 -0
  8. emerge/_emerge/elements/femdata.py +212 -0
  9. emerge/_emerge/elements/index_interp.py +64 -0
  10. emerge/_emerge/elements/legrange2.py +172 -0
  11. emerge/_emerge/elements/ned2_interp.py +645 -0
  12. emerge/_emerge/elements/nedelec2.py +140 -0
  13. emerge/_emerge/elements/nedleg2.py +217 -0
  14. emerge/_emerge/geo/__init__.py +24 -0
  15. emerge/_emerge/geo/horn.py +107 -0
  16. emerge/_emerge/geo/modeler.py +449 -0
  17. emerge/_emerge/geo/operations.py +254 -0
  18. emerge/_emerge/geo/pcb.py +1244 -0
  19. emerge/_emerge/geo/pcb_tools/calculator.py +28 -0
  20. emerge/_emerge/geo/pcb_tools/macro.py +79 -0
  21. emerge/_emerge/geo/pmlbox.py +204 -0
  22. emerge/_emerge/geo/polybased.py +529 -0
  23. emerge/_emerge/geo/shapes.py +427 -0
  24. emerge/_emerge/geo/step.py +77 -0
  25. emerge/_emerge/geo2d.py +86 -0
  26. emerge/_emerge/geometry.py +510 -0
  27. emerge/_emerge/howto.py +214 -0
  28. emerge/_emerge/logsettings.py +5 -0
  29. emerge/_emerge/material.py +118 -0
  30. emerge/_emerge/mesh3d.py +730 -0
  31. emerge/_emerge/mesher.py +339 -0
  32. emerge/_emerge/mth/common_functions.py +33 -0
  33. emerge/_emerge/mth/integrals.py +71 -0
  34. emerge/_emerge/mth/optimized.py +357 -0
  35. emerge/_emerge/periodic.py +263 -0
  36. emerge/_emerge/physics/__init__.py +0 -0
  37. emerge/_emerge/physics/microwave/__init__.py +1 -0
  38. emerge/_emerge/physics/microwave/adaptive_freq.py +279 -0
  39. emerge/_emerge/physics/microwave/assembly/assembler.py +569 -0
  40. emerge/_emerge/physics/microwave/assembly/curlcurl.py +448 -0
  41. emerge/_emerge/physics/microwave/assembly/generalized_eigen.py +426 -0
  42. emerge/_emerge/physics/microwave/assembly/robinbc.py +433 -0
  43. emerge/_emerge/physics/microwave/microwave_3d.py +1150 -0
  44. emerge/_emerge/physics/microwave/microwave_bc.py +915 -0
  45. emerge/_emerge/physics/microwave/microwave_data.py +1148 -0
  46. emerge/_emerge/physics/microwave/periodic.py +82 -0
  47. emerge/_emerge/physics/microwave/port_functions.py +53 -0
  48. emerge/_emerge/physics/microwave/sc.py +175 -0
  49. emerge/_emerge/physics/microwave/simjob.py +147 -0
  50. emerge/_emerge/physics/microwave/sparam.py +138 -0
  51. emerge/_emerge/physics/microwave/touchstone.py +140 -0
  52. emerge/_emerge/plot/__init__.py +0 -0
  53. emerge/_emerge/plot/display.py +394 -0
  54. emerge/_emerge/plot/grapher.py +93 -0
  55. emerge/_emerge/plot/matplotlib/mpldisplay.py +264 -0
  56. emerge/_emerge/plot/pyvista/__init__.py +1 -0
  57. emerge/_emerge/plot/pyvista/display.py +931 -0
  58. emerge/_emerge/plot/pyvista/display_settings.py +24 -0
  59. emerge/_emerge/plot/simple_plots.py +551 -0
  60. emerge/_emerge/plot.py +225 -0
  61. emerge/_emerge/projects/__init__.py +0 -0
  62. emerge/_emerge/projects/_gen_base.txt +32 -0
  63. emerge/_emerge/projects/_load_base.txt +24 -0
  64. emerge/_emerge/projects/generate_project.py +40 -0
  65. emerge/_emerge/selection.py +596 -0
  66. emerge/_emerge/simmodel.py +444 -0
  67. emerge/_emerge/simulation_data.py +411 -0
  68. emerge/_emerge/solver.py +993 -0
  69. emerge/_emerge/system.py +54 -0
  70. emerge/cli.py +19 -0
  71. emerge/lib.py +1 -1
  72. emerge/plot.py +1 -1
  73. {emerge-0.4.7.dist-info → emerge-0.4.9.dist-info}/METADATA +7 -6
  74. emerge-0.4.9.dist-info/RECORD +78 -0
  75. emerge-0.4.9.dist-info/entry_points.txt +2 -0
  76. emerge-0.4.7.dist-info/RECORD +0 -9
  77. emerge-0.4.7.dist-info/entry_points.txt +0 -2
  78. {emerge-0.4.7.dist-info → emerge-0.4.9.dist-info}/WHEEL +0 -0
@@ -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()