emerge 0.5.2__py3-none-any.whl → 0.5.3__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/_emerge/solver.py CHANGED
@@ -17,44 +17,126 @@
17
17
 
18
18
 
19
19
  from __future__ import annotations
20
- from scipy.sparse import lil_matrix, csc_matrix, csr_matrix # type: ignore
20
+ from scipy.sparse import csr_matrix # type: ignore
21
21
  from scipy.sparse.csgraph import reverse_cuthill_mckee # type: ignore
22
22
  from scipy.sparse.linalg import bicgstab, gmres, gcrotmk, eigs, splu # type: ignore
23
23
  from scipy.linalg import eig # type: ignore
24
24
  from scipy import sparse # type: ignore
25
- from dataclasses import dataclass
25
+ from dataclasses import dataclass, field
26
26
  import numpy as np
27
27
  from loguru import logger
28
28
  import platform
29
29
  import time
30
- from typing import Literal
30
+ from typing import Literal, Callable
31
31
  from enum import Enum
32
32
 
33
33
  _PARDISO_AVAILABLE = False
34
34
  _UMFPACK_AVAILABLE = False
35
+ _CUDSS_AVAILABLE = False
35
36
 
36
37
  """ Check if the PC runs on a non-ARM architechture
37
38
  If so, attempt to import PyPardiso (if its installed)
38
39
  """
39
40
 
41
+
42
+ ############################################################
43
+ # PARDISO #
44
+ ############################################################
45
+
40
46
  if 'arm' not in platform.processor():
41
47
  try:
42
48
  from .solve_interfaces.pardiso_interface import PardisoInterface
43
49
  _PARDISO_AVAILABLE = True
44
- except ModuleNotFoundError as e:
50
+ except ModuleNotFoundError:
45
51
  logger.info('Pardiso not found, defaulting to SuperLU')
52
+
53
+
54
+ ############################################################
55
+ # UMFPACK #
56
+ ############################################################
57
+
58
+
46
59
  try:
47
60
  import scikits.umfpack as um # type: ignore
48
61
  _UMFPACK_AVAILABLE = True
49
- except ModuleNotFoundError as e:
62
+ except ModuleNotFoundError:
50
63
  logger.debug('UMFPACK not found, defaulting to SuperLU')
51
64
 
52
65
 
66
+ ############################################################
67
+ # CUDSS #
68
+ ############################################################
69
+
70
+ try:
71
+ from .solve_interfaces.cudss_interface import CuDSSInterface
72
+ _CUDSS_AVAILABLE = True
73
+ except ModuleNotFoundError:
74
+ pass
75
+
76
+ ############################################################
77
+ # SOLVE REPORT #
78
+ ############################################################
79
+
80
+ @dataclass
81
+ class SolveReport:
82
+ simtime: float = -1.0
83
+ jobid: int = -1
84
+ ndof: int = -1
85
+ nnz: int = -1
86
+ ndof_solve: int = -1
87
+ nnz_solve: int = -1
88
+ exit_code: int = 0
89
+ solver: str = 'None'
90
+ sorter: str = 'None'
91
+ precon: str = 'None'
92
+ aux: dict[str, str] = field(default_factory=dict)
93
+
94
+ def add(self, **kwargs: str):
95
+ for key, value in kwargs.items():
96
+ self.aux[key] = str(value)
97
+
98
+ def pretty_print(self, print_cal: Callable | None = None):
99
+ if print_cal is None:
100
+ print_cal = print
101
+ # Set column widths
102
+ col1_width = 22 # Wider key column
103
+ col2_width = 40 # Value column
104
+ total_width = col1_width + col2_width + 5 # +5 for borders/padding
105
+
106
+ def row(key, val):
107
+ val_str = f"{val:.4f}" if isinstance(val, float) else str(val)
108
+ print_cal(f"| {key:<{col1_width}} | {val_str:<{col2_width}} |") # ty: ignore
109
+
110
+ border = "+" + "-" * (col1_width + 2) + "+" + "-" * (col2_width + 2) + "+"
111
+
112
+ print_cal(border)
113
+ print_cal(f"| {'FEM Solve Report':^{total_width - 2}} |")
114
+ print_cal(border)
115
+ row("Solver", self.solver)
116
+ row("Sorter", self.sorter)
117
+ row("Preconditioner", self.precon)
118
+ row("Job ID", self.jobid)
119
+ row("Sim Time (s)", self.simtime)
120
+ row("DOFs (Total)", self.ndof)
121
+ row("NNZ (Total)", self.nnz)
122
+ row("DOFs (Solve)", self.ndof_solve)
123
+ row("NNZ (Solve)", self.nnz_solve)
124
+ row("Exit Code", self.exit_code)
125
+ print_cal(border)
126
+
127
+ if self.aux:
128
+ print_cal(f"| {'Additional Info':^{total_width - 2}} |")
129
+ print_cal(border)
130
+ for k, v in self.aux.items():
131
+ row(str(k), v)
132
+ print_cal(border)
133
+
53
134
  ############################################################
54
135
  # EIGENMODE FILTER ROUTINE #
55
136
  ############################################################
56
137
 
57
- def filter_real_modes(eigvals, eigvecs, k0, ermax, urmax, sign):
138
+ def filter_real_modes(eigvals: np.ndarray, eigvecs: np.ndarray,
139
+ k0: float, ermax: complex, urmax: complex, sign: float) -> tuple[np.ndarray, np.ndarray]:
58
140
  """
59
141
  Given arrays of eigenvalues `eigvals` and eigenvectors `eigvecs` (cols of shape (N,)),
60
142
  and a free‐space wavenumber k0, return only those eigenpairs whose eigenvalue can
@@ -174,7 +256,7 @@ class Sorter:
174
256
  def __str__(self) -> str:
175
257
  return f'{self.__class__.__name__}'
176
258
 
177
- def sort(self, A: lil_matrix, b: np.ndarray, reuse_sorting: bool = False) -> tuple[lil_matrix, np.ndarray]:
259
+ def sort(self, A: csr_matrix, b: np.ndarray, reuse_sorting: bool = False) -> tuple[csr_matrix, np.ndarray]:
178
260
  return A,b
179
261
 
180
262
  def unsort(self, x: np.ndarray) -> np.ndarray:
@@ -190,8 +272,8 @@ class Preconditioner:
190
272
  def __str__(self) -> str:
191
273
  return f'{self.__class__.__name__}'
192
274
 
193
- def init(self, A: lil_matrix, b: np.ndarray) -> None:
194
- pass
275
+ def init(self, A: csr_matrix, b: np.ndarray) -> None:
276
+ raise NotImplementedError('')
195
277
 
196
278
  class Solver:
197
279
  """ A generic class representing a solver for the problem Ax=b
@@ -204,13 +286,22 @@ class Solver:
204
286
  """
205
287
  real_only: bool = False
206
288
  req_sorter: bool = False
289
+
207
290
  def __init__(self):
208
291
  self.own_preconditioner: bool = False
209
292
 
210
293
  def __str__(self) -> str:
211
294
  return f'{self.__class__.__name__}'
212
295
 
213
- def solve(self, A: lil_matrix, b: np.ndarray, precon: Preconditioner, reuse_factorization: bool = False, id: int = -1) -> tuple[np.ndarray, int]:
296
+ def duplicate(self) -> Solver:
297
+ return self.__class__()
298
+
299
+ def set_options(self, pivoting_threshold: float | None = None) -> None:
300
+ """Write generic simulation options to the solver object.
301
+ Options may be ignored depending on the type of solver used."""
302
+ pass
303
+
304
+ def solve(self, A: csr_matrix, b: np.ndarray, precon: Preconditioner, reuse_factorization: bool = False, id: int = -1) -> tuple[np.ndarray, SolveReport]:
214
305
  raise NotImplementedError("This classes Ax=B solver method is not implemented.")
215
306
 
216
307
  def reset(self) -> None:
@@ -228,13 +319,17 @@ class EigSolver:
228
319
  """
229
320
  real_only: bool = False
230
321
  req_sorter: bool = False
322
+
231
323
  def __init__(self):
232
324
  self.own_preconditioner: bool = False
233
325
 
234
326
  def __str__(self) -> str:
235
327
  return f'{self.__class__.__name__}'
236
328
 
237
- def eig(self, A: lil_matrix | csr_matrix, B: lil_matrix | csr_matrix, nmodes: int = 6, target_k0: float = 0.0, which: str = 'LM', sign: float = 1.):
329
+ def duplicate(self) -> Solver:
330
+ return self.__class__()
331
+
332
+ def eig(self, A: csr_matrix | csr_matrix, B: csr_matrix | csr_matrix, nmodes: int = 6, target_k0: float = 0.0, which: str = 'LM', sign: float = 1.):
238
333
  raise NotImplementedError("This classes eigenmdoe solver method is not implemented.")
239
334
 
240
335
  def reset(self) -> None:
@@ -247,16 +342,6 @@ class EigSolver:
247
342
  # SORTERS #
248
343
  ############################################################
249
344
 
250
- @dataclass
251
- class SolveReport:
252
- solver: str
253
- sorter: str
254
- precon: str
255
- simtime: float
256
- ndof: int
257
- nnz: int
258
- code: int = 0
259
-
260
345
 
261
346
  class ReverseCuthillMckee(Sorter):
262
347
  """ Implements the Reverse Cuthill-Mckee sorting."""
@@ -294,8 +379,8 @@ class ILUPrecon(Preconditioner):
294
379
 
295
380
  def init(self, A, b):
296
381
  logger.info("Generating ILU Preconditioner")
297
- 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)
298
- self.M = sparse.linalg.LinearOperator(A.shape, self.ilu.solve)
382
+ 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) # ty: ignore
383
+ self.M = sparse.linalg.LinearOperator(A.shape, self.ilu.solve) # ty: ignore
299
384
 
300
385
 
301
386
  ############################################################
@@ -316,22 +401,21 @@ class SolverBicgstab(Solver):
316
401
  convergence = np.linalg.norm((self.A @ xk - self.b))
317
402
  logger.info(f'Iteration {convergence:.4f}')
318
403
 
319
- def solve(self, A, b, precon, reuse_factorization: bool = False, id: int = -1):
320
- logger.info(f'Calling BiCGStab. ID={id}')
404
+ def solve(self, A, b, precon, reuse_factorization: bool = False, id: int = -1) -> tuple[np.ndarray, SolveReport]:
405
+ logger.info(f'[ID={id}] Calling BiCGStab.')
321
406
  self.A = A
322
407
  self.b = b
323
408
  if precon.M is not None:
324
409
  x, info = bicgstab(A, b, M=precon.M, atol=self.atol, callback=self.callback)
325
410
  else:
326
411
  x, info = bicgstab(A, b, atol=self.atol, callback=self.callback)
327
- return x, info
412
+ return x, SolveReport(solver=str(self), exit_code=info)
328
413
 
329
414
  class SolverGCROTMK(Solver):
330
415
  """ Implements the GCRO-T(m,k) Iterative solver. """
331
416
  def __init__(self):
332
417
  super().__init__()
333
418
  self.atol = 1e-5
334
-
335
419
  self.A: np.ndarray = None
336
420
  self.b: np.ndarray = None
337
421
 
@@ -339,15 +423,15 @@ class SolverGCROTMK(Solver):
339
423
  convergence = np.linalg.norm((self.A @ xk - self.b))
340
424
  logger.info(f'Iteration {convergence:.4f}')
341
425
 
342
- def solve(self, A: lil_matrix, b: np.ndarray, precon: Preconditioner, reuse_factorization: bool = False, id: int = -1):
343
- logger.info(f'Calling GCRO-T(m,k) algorithm. ID={id}')
426
+ def solve(self, A: csr_matrix, b: np.ndarray, precon: Preconditioner, reuse_factorization: bool = False, id: int = -1) -> tuple[np.ndarray, SolveReport]:
427
+ logger.info(f'[ID={id}] Calling GCRO-T(m,k) algorithm')
344
428
  self.A = A
345
429
  self.b = b
346
430
  if precon.M is not None:
347
431
  x, info = gcrotmk(A, b, M=precon.M, atol=self.atol, callback=self.callback)
348
432
  else:
349
433
  x, info = gcrotmk(A, b, atol=self.atol, callback=self.callback)
350
- return x, info
434
+ return x, SolveReport(solver=str(self), exit_code=info)
351
435
 
352
436
  class SolverGMRES(Solver):
353
437
  """ Implements the GMRES solver. """
@@ -365,15 +449,15 @@ class SolverGMRES(Solver):
365
449
  #convergence = np.linalg.norm((self.A @ xk - self.b))
366
450
  logger.info(f'Iteration {norm:.4f}')
367
451
 
368
- def solve(self, A, b, precon, reuse_factorization: bool = False, id: int = -1):
369
- logger.info(f'Calling GMRES Function. ID={id}')
452
+ def solve(self, A, b, precon, reuse_factorization: bool = False, id: int = -1) -> tuple[np.ndarray, SolveReport]:
453
+ logger.info(f'[ID={id}] Calling GMRES Function.')
370
454
  self.A = A
371
455
  self.b = b
372
456
  if precon.M is not None:
373
457
  x, info = gmres(A, b, M=precon.M, atol=self.atol, callback=self.callback, callback_type='pr_norm')
374
458
  else:
375
459
  x, info = gmres(A, b, atol=self.atol, callback=self.callback, restart=500, callback_type='pr_norm')
376
- return x, info
460
+ return x, SolveReport(solver=str(self), exit_code=info)
377
461
 
378
462
 
379
463
  ############################################################
@@ -385,6 +469,7 @@ class SolverSuperLU(Solver):
385
469
  """ Implements Scipi's direct SuperLU solver."""
386
470
  req_sorter: bool = False
387
471
  real_only: bool = False
472
+
388
473
  def __init__(self):
389
474
  super().__init__()
390
475
  self.atol = 1e-5
@@ -392,16 +477,29 @@ class SolverSuperLU(Solver):
392
477
  self.A: np.ndarray = None
393
478
  self.b: np.ndarray = None
394
479
  self.options: dict[str, str] = dict(SymmetricMode=True, Equil=False, IterRefine='SINGLE')
480
+ self._pivoting_threshold: float = 0.001
395
481
  self.lu = None
396
-
397
- def solve(self, A, b, precon, reuse_factorization: bool = False, id: int = -1):
398
- logger.info(f'Calling SuperLU Solver, ID={id}')
482
+
483
+ def duplicate(self) -> Solver:
484
+ new_solver = self.__class__()
485
+ new_solver._pivoting_threshold = self._pivoting_threshold
486
+ return new_solver
487
+
488
+ def set_options(self,
489
+ pivoting_threshold: float | None = None) -> None:
490
+ if pivoting_threshold is not None:
491
+ self._pivoting_threshold = pivoting_threshold
492
+
493
+ def solve(self, A, b, precon, reuse_factorization: bool = False, id: int = -1) -> tuple[np.ndarray, SolveReport]:
494
+ logger.info(f'[ID={id}] Calling SuperLU Solver.')
399
495
  self.single = True
400
496
  if not reuse_factorization:
401
- self.lu = splu(A, permc_spec='MMD_AT_PLUS_A', relax=0, diag_pivot_thresh=0.001, options=self.options)
497
+ self.lu = splu(A, permc_spec='MMD_AT_PLUS_A', relax=0, diag_pivot_thresh=self._pivoting_threshold, options=self.options)
402
498
  x = self.lu.solve(b)
403
-
404
- return x, 0
499
+ aux = {
500
+ "pivoting threshold": str(self._pivoting_threshold)
501
+ }
502
+ return x, SolveReport(solver=str(self), exit_code=0, aux=aux)
405
503
 
406
504
  class SolverUMFPACK(Solver):
407
505
  """ Implements the UMFPACK Sparse SP solver."""
@@ -413,35 +511,52 @@ class SolverUMFPACK(Solver):
413
511
  self.A: np.ndarray = None
414
512
  self.b: np.ndarray = None
415
513
  self.umfpack: um.UmfpackContext = um.UmfpackContext('zl')
416
- self.umfpack.control[um.UMFPACK_PRL] = 0
417
- self.umfpack.control[um.UMFPACK_IRSTEP] = 2
418
- self.umfpack.control[um.UMFPACK_STRATEGY] = um.UMFPACK_STRATEGY_SYMMETRIC
419
- self.umfpack.control[um.UMFPACK_ORDERING] = 3
420
- self.umfpack.control[um.UMFPACK_PIVOT_TOLERANCE] = 0.001
421
- self.umfpack.control[um.UMFPACK_SYM_PIVOT_TOLERANCE] = 0.001
422
- self.umfpack.control[um.UMFPACK_BLOCK_SIZE] = 64
423
- self.umfpack.control[um.UMFPACK_FIXQ] = -1
514
+ self.umfpack.control[um.UMFPACK_PRL] = 0 # ty: ignore
515
+ self.umfpack.control[um.UMFPACK_IRSTEP] = 2 # ty: ignore
516
+ self.umfpack.control[um.UMFPACK_STRATEGY] = um.UMFPACK_STRATEGY_SYMMETRIC # ty: ignore
517
+ self.umfpack.control[um.UMFPACK_ORDERING] = 3 # ty: ignore
518
+ self.umfpack.control[um.UMFPACK_PIVOT_TOLERANCE] = 0.001 # ty: ignore
519
+ self.umfpack.control[um.UMFPACK_SYM_PIVOT_TOLERANCE] = 0.001 # ty: ignore
520
+ self.umfpack.control[um.UMFPACK_BLOCK_SIZE] = 64 # ty: ignore
521
+ self.umfpack.control[um.UMFPACK_FIXQ] = -1 # ty: ignore
522
+
523
+ # SETTINGS
524
+ self._pivoting_threshold: float = 0.001
424
525
 
425
526
  self.fact_symb: bool = False
426
527
 
427
528
  def reset(self) -> None:
428
529
  self.fact_symb = False
429
-
430
- def solve(self, A, b, precon, reuse_factorization: bool = False, id: int = -1):
431
- logger.info(f'Calling UMFPACK Solver. ID={id}')
530
+
531
+ def set_options(self,
532
+ pivoting_threshold: float | None = None) -> None:
533
+ if pivoting_threshold is not None:
534
+ self.umfpack.control[um.UMFPACK_PIVOT_TOLERANCE] = pivoting_threshold # ty: ignore
535
+ self.umfpack.control[um.UMFPACK_SYM_PIVOT_TOLERANCE] = pivoting_threshold # ty: ignore
536
+ self._pivoting_threshold = pivoting_threshold
537
+
538
+ def duplicate(self) -> Solver:
539
+ new_solver = self.__class__()
540
+ new_solver.set_options(pivoting_threshold = self._pivoting_threshold)
541
+ return new_solver
542
+
543
+ def solve(self, A, b, precon, reuse_factorization: bool = False, id: int = -1) -> tuple[np.ndarray, SolveReport]:
544
+ logger.info(f'[ID={id}] Calling UMFPACK Solver.')
432
545
  A.indptr = A.indptr.astype(np.int64)
433
546
  A.indices = A.indices.astype(np.int64)
434
547
  if self.fact_symb is False:
435
- logger.debug('Executing symbollic factorization.')
548
+ logger.debug(f'[ID={id}] Executing symbollic factorization.')
436
549
  self.umfpack.symbolic(A)
437
- #self.up.report_symbolic()
438
550
  self.fact_symb = True
439
551
  if not reuse_factorization:
440
552
  #logger.debug('Executing numeric factorization.')
441
553
  self.umfpack.numeric(A)
442
554
  self.A = A
443
- x = self.umfpack.solve(um.UMFPACK_A, self.A, b, autoTranspose = False )
444
- return x, 0
555
+ x = self.umfpack.solve(um.UMFPACK_A, self.A, b, autoTranspose = False ) # ty: ignore
556
+ aux = {
557
+ "Pivoting Threshold": str(self._pivoting_threshold),
558
+ }
559
+ return x, SolveReport(solver=str(self), exit_code=0, aux=aux)
445
560
 
446
561
  class SolverPardiso(Solver):
447
562
  """ Implements the PARDISO solver through PyPardiso. """
@@ -454,11 +569,11 @@ class SolverPardiso(Solver):
454
569
  self.fact_symb: bool = False
455
570
  self.A: np.ndarray = None
456
571
  self.b: np.ndarray = None
457
-
458
- def solve(self, A, b, precon, reuse_factorization: bool = False, id: int = -1):
459
- logger.info(f'Calling Pardiso Solver. ID={id}')
572
+
573
+ def solve(self, A, b, precon, reuse_factorization: bool = False, id: int = -1) -> tuple[np.ndarray, SolveReport]:
574
+ logger.info(f'[ID={id}] Calling Pardiso Solver')
460
575
  if self.fact_symb is False:
461
- logger.debug('Executing symbollic factorization.')
576
+ logger.debug(f'[ID={id}] Executing symbollic factorization.')
462
577
  self.solver.symbolic(A)
463
578
  self.fact_symb = True
464
579
  if not reuse_factorization:
@@ -466,12 +581,42 @@ class SolverPardiso(Solver):
466
581
  self.A = A
467
582
  x, error = self.solver.solve(A, b)
468
583
  if error != 0:
469
- logger.error(f'Terminated with error code {error}')
584
+ logger.error(f'[ID={id}] Terminated with error code {error}')
470
585
  logger.error(self.solver.get_error(error))
471
- raise SimulationError(f'PARDISO Terminated with error code {error}')
472
- return x, error
586
+ raise SimulationError(f'[ID={id}] PARDISO Terminated with error code {error}')
587
+ aux = {}
588
+ return x, SolveReport(solver=str(self), exit_code=error, aux=aux)
473
589
 
474
590
 
591
+ class CuDSSSolver(Solver):
592
+ real_only = False
593
+ def __init__(self):
594
+ self._cudss = CuDSSInterface()
595
+ self.fact_symb: bool = False
596
+ self.fact_numb: bool = False
597
+ self._cudss._PRES = 2
598
+
599
+ def reset(self) -> None:
600
+ self.fact_symb = False
601
+ self.fact_numb = False
602
+
603
+ def solve(self, A, b, precon, reuse_factorization: bool = False, id: int = -1):
604
+ logger.info(f'[{id}] Calling cuDSS Solver')
605
+
606
+ if self.fact_symb is False:
607
+ logger.debug('Executing symbollic factorization')
608
+ x = self._cudss.from_symbolic(A,b)
609
+ self.fact_symb = True
610
+ return x, 0
611
+ else:
612
+ if reuse_factorization:
613
+ x = self._cudss.from_solve(b)
614
+ return x, 0
615
+ else:
616
+ x = self._cudss.from_numeric(A,b)
617
+ return x, 0
618
+
619
+
475
620
  ############################################################
476
621
  # DIRECT EIGENMODE SOLVERS #
477
622
  ############################################################
@@ -482,12 +627,12 @@ class SolverLAPACK(EigSolver):
482
627
  super().__init__()
483
628
 
484
629
  def eig(self,
485
- A: lil_matrix | csr_matrix,
486
- B: lil_matrix | csr_matrix,
630
+ A: csr_matrix | csr_matrix,
631
+ B: csr_matrix | csr_matrix,
487
632
  nmodes: int = 6,
488
- target_k0: float | None = 0,
633
+ target_k0: float = 0,
489
634
  which: str = 'LM',
490
- sign: float = 1.0):
635
+ sign: float = 1.0) -> tuple[np.ndarray, np.ndarray]:
491
636
  """
492
637
  Dense solver for A x = λ B x with A = Aᴴ, B = Bᴴ (B may be indefinite).
493
638
 
@@ -521,12 +666,12 @@ class SolverARPACK(EigSolver):
521
666
  super().__init__()
522
667
 
523
668
  def eig(self,
524
- A: lil_matrix | csr_matrix,
525
- B: lil_matrix | csr_matrix,
669
+ A: csr_matrix | csr_matrix,
670
+ B: csr_matrix | csr_matrix,
526
671
  nmodes: int = 6,
527
672
  target_k0: float = 0,
528
673
  which: str = 'LM',
529
- sign: float = 1.0):
674
+ sign: float = 1.0) -> tuple[np.ndarray, np.ndarray]:
530
675
  logger.info(f'Searching around β = {target_k0:.2f} rad/m')
531
676
  sigma = sign*(target_k0**2)
532
677
  eigen_values, eigen_modes = eigs(A, k=nmodes, M=B, sigma=sigma, which=which)
@@ -544,12 +689,13 @@ class SmartARPACK_BMA(EigSolver):
544
689
  self.energy_limit: float = 1e-4
545
690
 
546
691
  def eig(self,
547
- A: lil_matrix | csr_matrix,
548
- B: lil_matrix | csr_matrix,
692
+ A: csr_matrix | csr_matrix,
693
+ B: csr_matrix | csr_matrix,
549
694
  nmodes: int = 6,
550
695
  target_k0: float = 0,
551
696
  which: str = 'LM',
552
- sign: float = 1.):
697
+ sign: float = 1.) -> tuple[np.ndarray, np.ndarray]:
698
+
553
699
  logger.info(f'Searching around β = {target_k0:.2f} rad/m')
554
700
  qs = np.geomspace(1, self.search_range, self.symmetric_steps)
555
701
  tot_eigen_values = []
@@ -593,12 +739,12 @@ class SmartARPACK(EigSolver):
593
739
  self.energy_limit: float = 1e-4
594
740
 
595
741
  def eig(self,
596
- A: lil_matrix | csr_matrix,
597
- B: lil_matrix | csr_matrix,
742
+ A: csr_matrix | csr_matrix,
743
+ B: csr_matrix | csr_matrix,
598
744
  nmodes: int = 6,
599
745
  target_k0: float = 0,
600
746
  which: str = 'LM',
601
- sign: float = 1.):
747
+ sign: float = 1.) -> tuple[np.ndarray, np.ndarray]:
602
748
  logger.info(f'Searching around β = {target_k0:.2f} rad/m')
603
749
  qs = np.geomspace(1, self.search_range, self.symmetric_steps)
604
750
  tot_eigen_values = []
@@ -647,18 +793,19 @@ class EMSolver(Enum):
647
793
  ARPACK = 5
648
794
  SMART_ARPACK = 6
649
795
  SMART_ARPACK_BMA = 7
796
+ CUDSS = 8
650
797
 
651
- def get_solver(self) -> Solver | EigSolver:
798
+ def get_solver(self) -> Solver | EigSolver | None:
652
799
  if self==EMSolver.SUPERLU:
653
800
  return SolverSuperLU()
654
801
  elif self==EMSolver.UMFPACK:
655
802
  if _UMFPACK_AVAILABLE is False:
656
- return SolverSuperLU()
803
+ return None
657
804
  else:
658
805
  return SolverUMFPACK()
659
806
  elif self==EMSolver.PARDISO:
660
807
  if _PARDISO_AVAILABLE is False:
661
- return SolverSuperLU()
808
+ return None
662
809
  else:
663
810
  return SolverPardiso()
664
811
  elif self==EMSolver.LAPACK:
@@ -669,7 +816,12 @@ class EMSolver(Enum):
669
816
  return SmartARPACK()
670
817
  elif self==EMSolver.SMART_ARPACK_BMA:
671
818
  return SmartARPACK_BMA()
672
-
819
+ elif self==EMSolver.CUDSS:
820
+ if _CUDSS_AVAILABLE is False:
821
+ return None
822
+ else:
823
+ return CuDSSSolver()
824
+ raise ValueError(f'An unsupported Enum case has been reached: {self}')
673
825
 
674
826
  ############################################################
675
827
  # SOLVE ROUTINE #
@@ -687,6 +839,7 @@ class SolveRoutine:
687
839
  self.sorter: Sorter = ReverseCuthillMckee()
688
840
  self.precon: Preconditioner = ILUPrecon()
689
841
  self.solvers: dict[EMSolver, Solver | EigSolver] = {slv: slv.get_solver() for slv in EMSolver}
842
+ self.solvers = {key: solver for key, solver in self.solvers.items() if solver is not None}
690
843
 
691
844
  self.parallel: Literal['SI','MT','MP'] = 'SI'
692
845
  self.smart_search: bool = False
@@ -697,6 +850,7 @@ class SolveRoutine:
697
850
  self.use_preconditioner: bool = False
698
851
  self.use_direct: bool = True
699
852
 
853
+
700
854
  def __str__(self) -> str:
701
855
  return 'SolveRoutine()'
702
856
 
@@ -753,6 +907,8 @@ class SolveRoutine:
753
907
  new_routine.parallel = self.parallel
754
908
  new_routine.smart_search = self.smart_search
755
909
  new_routine.forced_solver = self.forced_solver
910
+ for tpe, solver in self.solvers.items():
911
+ new_routine.solvers[tpe] = solver.duplicate()
756
912
  return new_routine
757
913
 
758
914
  def set_solver(self, *solvers: EMSolver | EigSolver | Solver) -> None:
@@ -779,8 +935,9 @@ class SolveRoutine:
779
935
  else:
780
936
  self.disabled_solver.append(solver.__class__)
781
937
 
782
- def configure(self,
783
- parallel: Literal['SI','MT','MP'] = 'SI', smart_search: bool = False) -> SolveRoutine:
938
+ def _configure_routine(self,
939
+ parallel: Literal['SI','MT','MP'] = 'SI',
940
+ smart_search: bool = False) -> SolveRoutine:
784
941
  """Configure the solver with the given settings
785
942
 
786
943
  Args:
@@ -798,6 +955,20 @@ class SolveRoutine:
798
955
  self.smart_search = smart_search
799
956
  return self
800
957
 
958
+ def configure(self,
959
+ pivoting_threshold: float | None = None) -> None:
960
+ """Sets general user configurations for all solvers.
961
+
962
+ Args:
963
+ pivoting_threshold (float | None, optional):
964
+ The diagonal pivoting threshold used in direct solvers. Standard values are 0.001.
965
+ In simulations with a very low surface impedance (such as with copper walls) a much
966
+ lower pivoting threshold is desired.
967
+ """
968
+ for solver in self.solvers.values():
969
+ if isinstance(solver, Solver):
970
+ solver.set_options(pivoting_threshold=pivoting_threshold)
971
+
801
972
  def reset(self) -> None:
802
973
  """Reset all solver states"""
803
974
  for solver in self.solvers.values():
@@ -808,7 +979,7 @@ class SolveRoutine:
808
979
  self.forced_solver = []
809
980
  self.disabled_solver = []
810
981
 
811
- def _get_solver(self, A: lil_matrix, b: np.ndarray) -> Solver:
982
+ def _get_solver(self, A: csr_matrix, b: np.ndarray) -> Solver:
812
983
  """Returns the relevant Solver object given a certain matrix and source vector
813
984
 
814
985
  This is the default implementation for the SolveRoutine Class.
@@ -829,7 +1000,7 @@ class SolveRoutine:
829
1000
  return self.pick_solver(A,b)
830
1001
 
831
1002
 
832
- def pick_solver(self, A: lil_matrix, b: np.ndarray) -> Solver:
1003
+ def pick_solver(self, A: csr_matrix, b: np.ndarray) -> Solver:
833
1004
  """Returns the relevant Solver object given a certain matrix and source vector
834
1005
 
835
1006
  This is the default implementation for the SolveRoutine Class.
@@ -844,7 +1015,7 @@ class SolveRoutine:
844
1015
  """
845
1016
  return self._try_solver(EMSolver.SUPERLU)
846
1017
 
847
- def _get_eig_solver(self, A: lil_matrix, b: lil_matrix, direct: bool | None = None) -> EigSolver:
1018
+ def _get_eig_solver(self, A: csr_matrix, b: csr_matrix, direct: bool | None = None) -> EigSolver:
848
1019
  """Returns the relevant eigenmode Solver object given a certain matrix and source vector
849
1020
 
850
1021
  This is the default implementation for the SolveRoutine Class.
@@ -866,7 +1037,7 @@ class SolveRoutine:
866
1037
  else:
867
1038
  return self.solvers[EMSolver.SMART_ARPACK] # type: ignore
868
1039
 
869
- def _get_eig_solver_bma(self, A: lil_matrix, b: lil_matrix, direct: bool | None = None) -> EigSolver:
1040
+ def _get_eig_solver_bma(self, A: csr_matrix, b: csr_matrix, direct: bool | None = None) -> EigSolver:
870
1041
  """Returns the relevant eigenmode Solver object given a certain matrix and source vector
871
1042
 
872
1043
  This is the default implementation for the SolveRoutine Class.
@@ -889,7 +1060,7 @@ class SolveRoutine:
889
1060
  else:
890
1061
  return self.solvers[EMSolver.ARPACK] # type: ignore
891
1062
 
892
- def solve(self, A: lil_matrix | csc_matrix,
1063
+ def solve(self, A: csr_matrix | csr_matrix,
893
1064
  b: np.ndarray,
894
1065
  solve_ids: np.ndarray,
895
1066
  reuse: bool = False,
@@ -900,7 +1071,7 @@ class SolveRoutine:
900
1071
  The solve routine will go through the required steps defined in the routine to tackle the problme.
901
1072
 
902
1073
  Args:
903
- A (np.ndarray | lil_matrix | csc_matrix): The (Sparse) matrix
1074
+ A (np.ndarray | csr_matrix | csr_matrix): The (Sparse) matrix
904
1075
  b (np.ndarray): The source vector
905
1076
  solve_ids (np.ndarray): A vector of ids for which to solve the problem. For EM problems this
906
1077
  implies all non-PEC degrees of freedom.
@@ -909,7 +1080,7 @@ class SolveRoutine:
909
1080
  Returns:
910
1081
  np.ndarray: The resultant solution.
911
1082
  """
912
- solver = self._get_solver(A, b)
1083
+ solver: Solver = self._get_solver(A, b)
913
1084
 
914
1085
  NF = A.shape[0]
915
1086
  NS = solve_ids.shape[0]
@@ -920,10 +1091,10 @@ class SolveRoutine:
920
1091
  bsel = b[solve_ids]
921
1092
  nnz = Asel.nnz
922
1093
 
923
- logger.debug(f' Removed {NF-NS} prescribed DOFs ({NS:,} left, {nnz:,} non-zero)')
1094
+ logger.debug(f'[ID={id}] Removed {NF-NS} prescribed DOFs ({NS:,} left, {nnz:,}≠0)')
924
1095
 
925
1096
  if solver.real_only:
926
- logger.debug(' Converting to real matrix')
1097
+ logger.debug(f'[ID={id}] Converting to real matrix')
927
1098
  Asel, bsel = complex_to_real_block(Asel, bsel)
928
1099
 
929
1100
  # SORT
@@ -943,11 +1114,11 @@ class SolveRoutine:
943
1114
 
944
1115
  start = time.time()
945
1116
 
946
- x_solved, code = solver.solve(Asorted, bsorted, self.precon, reuse_factorization=reuse, id=id)
1117
+ x_solved, report = solver.solve(Asorted, bsorted, self.precon, reuse_factorization=reuse, id=id)
947
1118
  end = time.time()
948
1119
  simtime = end-start
949
- logger.info(f'Time taken: {simtime:.3f} seconds')
950
- logger.debug(f' O(N²) performance = {(NS**2)/((end-start+1e-6)*1e6):.3f} MDoF/s')
1120
+ logger.info(f'[ID={id}] Elapsed time taken: {simtime:.3f} seconds')
1121
+ logger.debug(f'[ID={id}] O(N²) performance = {(NS**2)/((end-start+1e-6)*1e6):.3f} MDoF/s')
951
1122
 
952
1123
  if self.use_sorter and solver.req_sorter:
953
1124
  x = self.sorter.unsort(x_solved)
@@ -955,20 +1126,27 @@ class SolveRoutine:
955
1126
  x = x_solved
956
1127
 
957
1128
  if solver.real_only:
958
- logger.debug(' Converting back to complex matrix')
1129
+ logger.debug(f'[ID={id}] Converting back to complex matrix')
959
1130
  x = real_to_complex_block(x)
960
1131
 
961
1132
  solution = np.zeros((NF,), dtype=np.complex128)
962
1133
 
963
1134
  solution[solve_ids] = x
964
1135
 
965
- logger.debug('Solver complete!')
966
- if code:
967
- logger.debug(' Solver code: {code}')
968
- return solution, SolveReport(str(solver), sorter, precon, simtime, NS, A.nnz, code)
1136
+ logger.debug(f'[ID={id}] Solver complete!')
1137
+ report.jobid = id
1138
+ report.sorter = str(sorter)
1139
+ report.simtime = simtime
1140
+ report.nnz = A.nnz
1141
+ report.ndof = b.shape[0]
1142
+ report.nnz_solve = Asorted.nnz
1143
+ report.ndof_solve = bsorted.shape[0]
1144
+ report.precon = precon
1145
+
1146
+ return solution, report
969
1147
 
970
1148
  def eig_boundary(self,
971
- A: lil_matrix | csr_matrix,
1149
+ A: csr_matrix | csr_matrix,
972
1150
  B: np.ndarray,
973
1151
  solve_ids: np.ndarray,
974
1152
  nmodes: int = 6,
@@ -981,8 +1159,8 @@ class SolveRoutine:
981
1159
  For generalized eigenvalue problems of boundary mode analysis studies, the equation is: Ae = -β²Be
982
1160
 
983
1161
  Args:
984
- A (csc_matrix): The Stiffness matrix
985
- B (csc_matrix): The mass matrix
1162
+ A (csr_matrix): The Stiffness matrix
1163
+ B (csr_matrix): The mass matrix
986
1164
  solve_ids (np.ndarray): The free nodes (non PEC)
987
1165
  nmodes (int): The number of modes to solve for. Defaults to 6
988
1166
  direct (bool): If the direct solver should be used (always). Defaults to False
@@ -1000,7 +1178,7 @@ class SolveRoutine:
1000
1178
  NF = A.shape[0]
1001
1179
  NS = solve_ids.shape[0]
1002
1180
 
1003
- logger.debug(f' Removing {NF-NS} prescribed DOFs ({NS} left)')
1181
+ logger.debug(f'Removing {NF-NS} prescribed DOFs ({NS} left)')
1004
1182
 
1005
1183
  Asel = A[np.ix_(solve_ids, solve_ids)]
1006
1184
  Bsel = B[np.ix_(solve_ids, solve_ids)]
@@ -1010,10 +1188,10 @@ class SolveRoutine:
1010
1188
  end = time.time()
1011
1189
 
1012
1190
  simtime = end-start
1013
- return eigen_values, eigen_modes, SolveReport(str(solver), 'None', 'None', simtime, NS, A.nnz, int(simtime))
1191
+ return eigen_values, eigen_modes, SolveReport(ndof=A.shape[0], nnz=A.nnz, ndof_solve=Asel.shape[0], nnz_solve=Asel.nnz, simtime=simtime, solver=str(solver), sorter='None', precon='None')
1014
1192
 
1015
1193
  def eig(self,
1016
- A: lil_matrix | csr_matrix,
1194
+ A: csr_matrix | csr_matrix,
1017
1195
  B: np.ndarray,
1018
1196
  solve_ids: np.ndarray,
1019
1197
  nmodes: int = 6,
@@ -1024,8 +1202,8 @@ class SolveRoutine:
1024
1202
  Find the eigenmodes for the system Ax = λBx for a boundary mode problem
1025
1203
 
1026
1204
  Args:
1027
- A (csc_matrix): The Stiffness matrix
1028
- B (csc_matrix): The mass matrix
1205
+ A (csr_matrix): The Stiffness matrix
1206
+ B (csr_matrix): The mass matrix
1029
1207
  solve_ids (np.ndarray): The free nodes (non PEC)
1030
1208
  nmodes (int): The number of modes to solve for. Defaults to 6
1031
1209
  direct (bool): If the direct solver should be used (always). Defaults to False
@@ -1040,7 +1218,7 @@ class SolveRoutine:
1040
1218
  NF = A.shape[0]
1041
1219
  NS = solve_ids.shape[0]
1042
1220
 
1043
- logger.debug(f' Removing {NF-NS} prescribed DOFs ({NS} left)')
1221
+ logger.debug(f'Removing {NF-NS} prescribed DOFs ({NS} left)')
1044
1222
 
1045
1223
  Asel = A[np.ix_(solve_ids, solve_ids)]
1046
1224
  Bsel = B[np.ix_(solve_ids, solve_ids)]
@@ -1055,7 +1233,7 @@ class SolveRoutine:
1055
1233
  for i in range(Nsols):
1056
1234
  sols[solve_ids,i] = eigen_modes[:,i]
1057
1235
 
1058
- return eigen_values, sols, SolveReport(str(solver), 'None', 'None', simtime, NS, A.nnz, int(simtime))
1236
+ return eigen_values, sols, SolveReport(ndof=A.shape[0], nnz=A.nnz, ndof_solve=Asel.shape[0], nnz_solve=Asel.nnz, simtime=simtime, solver=str(solver), sorter='None', precon='None')
1059
1237
 
1060
1238
 
1061
1239
  class AutomaticRoutine(SolveRoutine):