emerge 0.4.10__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of emerge might be problematic. Click here for more details.

@@ -0,0 +1,468 @@
1
+
2
+ # EMerge is an open source Python based FEM EM simulation module.
3
+ # Copyright (C) 2025 Robert Fennis.
4
+
5
+ # This program is free software; you can redistribute it and/or
6
+ # modify it under the terms of the GNU General Public License
7
+ # as published by the Free Software Foundation; either version 2
8
+ # of the License, or (at your option) any later version.
9
+
10
+ # This program is distributed in the hope that it will be useful,
11
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ # GNU General Public License for more details.
14
+
15
+ # You should have received a copy of the GNU General Public License
16
+ # along with this program; if not, see
17
+ # <https://www.gnu.org/licenses/>.
18
+ from __future__ import annotations
19
+ import os
20
+ import sys
21
+ import ctypes
22
+ import re
23
+ import site
24
+ from ctypes.util import find_library
25
+ from enum import Enum
26
+ import numpy as np
27
+ from scipy import sparse
28
+ from pathlib import Path
29
+ from typing import Iterable, Iterator
30
+ import pickle
31
+ from loguru import logger
32
+
33
+ ############################################################
34
+ # ERROR CODES #
35
+ ############################################################
36
+
37
+ PARDISO_ERROR_CODES = {
38
+ 0: "No error.",
39
+ -1: "Input inconsistent.",
40
+ -2: "Not enough memory.",
41
+ -3: "Reordering problem.",
42
+ -4: "Zero pivot, numerical fac. or iterative refinement problem.",
43
+ -5: "Unclassified (internal) error.",
44
+ -6: "Preordering failed (matrix types 11(real and nonsymmetric), 13(complex and nonsymmetric) only).",
45
+ -7: "Diagonal Matrix problem.",
46
+ -8: "32-bit integer overflow problem.",
47
+ -10: "No license file pardiso.lic found.",
48
+ -11: "License is expired.",
49
+ -12: "Wrong username or hostname.",
50
+ -100: "Reached maximum number of Krylov-subspace iteration in iterative solver.",
51
+ -101: "No sufficient convergence in Krylov-subspace iteration within 25 iterations.",
52
+ -102: "Error in Krylov-subspace iteration.",
53
+ -103: "Bread-Down in Krylov-subspace iteration",
54
+ }
55
+
56
+
57
+ ############################################################
58
+ # FINDING THE PARDISO DLL FILES #
59
+ ############################################################
60
+
61
+
62
+ #: Environment variable that overrides automatic searching
63
+ ENV_VAR = "PYPARDISO_MKL_RT"
64
+
65
+
66
+ def _candidate_dirs() -> Iterable[Path]:
67
+ """Return directories in which to look for MKL."""
68
+ # Ordered from most to least likely
69
+ seen: set[Path] = set()
70
+
71
+ for p in ( # likely “local” env first
72
+ Path(sys.prefix),
73
+ Path(getattr(sys, "base_prefix", sys.prefix)),
74
+ Path(site.USER_BASE),
75
+ *(Path(x) for x in os.getenv("LD_LIBRARY_PATH", "").split(":") if x),
76
+ ):
77
+ if p not in seen:
78
+ seen.add(p)
79
+ yield p
80
+
81
+ def _search_mkl() -> Iterator[Path]:
82
+ """Yield candidate MKL library paths, shortest first."""
83
+ pattern = {
84
+ "win32": r"^mkl_rt.*\.dll$",
85
+ "darwin": r"^libmkl_rt(\.\d+)*\.dylib$",
86
+ "linux": r"^libmkl_rt(\.so(\.\d+)*)?$",
87
+ }.get(sys.platform, r"^libmkl_rt")
88
+
89
+ regex = re.compile(pattern, re.IGNORECASE)
90
+
91
+ for base in _candidate_dirs():
92
+ for path in sorted(base.rglob("**/*mkl_rt*"), key=lambda p: len(str(p))):
93
+ if regex.match(path.name):
94
+ yield path
95
+
96
+ def cache_path_result(tag: str, compute_fn, force: bool = False):
97
+ """
98
+ Retrieve a cached Path object or compute it and store it.
99
+
100
+ Parameters
101
+ ----------
102
+ tag : str
103
+ Cache key.
104
+ compute_fn : callable
105
+ Callable that returns a Path.
106
+ force : bool
107
+ If True, bypass and overwrite the cache.
108
+ """
109
+ cache_dir = Path(__file__).parent / "__pycache__"
110
+ cache_dir.mkdir(exist_ok=True)
111
+ cache_file = cache_dir / f"{tag}.pkl"
112
+
113
+ if not force and cache_file.exists():
114
+ with open(cache_file, "rb") as f:
115
+ filename = pickle.load(f)
116
+ print(f"Using cached MKL file: {filename}")
117
+ return filename
118
+
119
+ result = compute_fn()
120
+ with open(cache_file, "wb") as f:
121
+ pickle.dump(result, f)
122
+ return result
123
+
124
+ def search_mkl() -> str:
125
+ """Searches for the file path of the PARDISO MKL executable
126
+
127
+ Returns:
128
+ str: The filepath
129
+ """
130
+ logger.debug('Searching for MKL executable...')
131
+ for candidate in _search_mkl():
132
+ try:
133
+ ctypes.CDLL(candidate)
134
+ except OSError:
135
+ continue # try the next one
136
+ logger.debug(f'Executable found: {candidate}')
137
+ return candidate
138
+
139
+ def load_mkl() -> ctypes.CDLL:
140
+ """Locate and load **mkl_rt**; raise ImportError on failure."""
141
+ # 1 explicit override
142
+ override = os.getenv(ENV_VAR)
143
+ if override:
144
+ try:
145
+ return ctypes.CDLL(override)
146
+ except OSError as e:
147
+ raise ImportError(f"{override!r} could not be loaded: {e}") from None
148
+
149
+ # 2 system utility (cheap)
150
+ lib = find_library("mkl_rt")
151
+ if lib:
152
+ try:
153
+ return ctypes.CDLL(lib)
154
+ except OSError:
155
+ pass
156
+
157
+ # 2 system utility (cheap)
158
+ lib = find_library("mkl_rt.1")
159
+ if lib:
160
+ try:
161
+ return ctypes.CDLL(lib)
162
+ except OSError:
163
+ pass
164
+
165
+ # 3 filesystem walk (expensive, but last resort)
166
+ try:
167
+ filename = cache_path_result('mkl_file',search_mkl)
168
+ return ctypes.CDLL(filename)
169
+ except OSError:
170
+ logger.warning('File name {filename} is no longer valid. Re-executing MKL search.')
171
+ filename = cache_path_result('mkl_file',search_mkl,force=True)
172
+ return ctypes.CDLL(filename)
173
+
174
+ raise ImportError(
175
+ "Shared library *mkl_rt* not found. "
176
+ f"Set the environment variable {ENV_VAR} to its full path if it is in a non-standard location."
177
+ )
178
+
179
+
180
+
181
+ ############################################################
182
+ # ALL C-TYPE DEFINITIONS #
183
+ ############################################################
184
+
185
+
186
+ class MKL_Complex16(ctypes.Structure):
187
+ _fields_ = [("real", ctypes.c_double),
188
+ ("imag", ctypes.c_double)]
189
+
190
+ CINT64 = ctypes.c_int64
191
+ CINT32 = ctypes.c_int32
192
+ CFLOAT32 = ctypes.c_float
193
+ CFLOAT64 = ctypes.c_double
194
+
195
+ CPX16_P = ctypes.POINTER(MKL_Complex16)
196
+ CINT64_P = ctypes.POINTER(CINT64)
197
+ CINT32_P = ctypes.POINTER(CINT32)
198
+ CNONE_P = ctypes.POINTER(None)
199
+ CFLOAT32_P = ctypes.POINTER(CFLOAT32)
200
+ CFLOAT64_P = ctypes.POINTER(CFLOAT64)
201
+ VOID_SIZE = ctypes.sizeof(ctypes.c_void_p)
202
+
203
+ if VOID_SIZE == 8:
204
+ PT_A = CINT64
205
+ PT_B = np.int64
206
+ elif VOID_SIZE == 4:
207
+ PT_A = CINT32
208
+ PT_B = np.int32
209
+
210
+ def c_int(value: int):
211
+ return ctypes.byref(ctypes.c_int32(value))
212
+
213
+ PARDISO_ARG_TYPES = (ctypes.POINTER(PT_A),CINT32_P,CINT32_P,
214
+ CINT32_P,CINT32_P,CINT32_P,CNONE_P,CINT32_P,CINT32_P,
215
+ CINT32_P,CINT32_P,CINT32_P,CINT32_P,CNONE_P,CNONE_P,CINT32_P,
216
+ )
217
+
218
+
219
+ ############################################################
220
+ # PARDISO CONFIGURATIONS #
221
+ ############################################################
222
+
223
+
224
+ class PARDISOMType(Enum):
225
+ REAL_SYM_STRUCT = 1
226
+ REAL_SYM_POSDEF = 2
227
+ REAL_SYM_INDEF = -2
228
+ COMP_SYM_STRUCT = 3
229
+ COMP_HERM_POSDEF = 4
230
+ COMP_HERM_INDEF = -4
231
+ COMP_SYM = 6
232
+ REAL_NONSYM = 11
233
+ COMP_NONSYM = 13
234
+
235
+ class PARDISOPhase(Enum):
236
+ SYMBOLIC_FACTOR = 11
237
+ NUMERIC_FACTOR = 12
238
+ NUMERIC_SOLVE = 33
239
+
240
+
241
+
242
+ ############################################################
243
+ # GENERIC MATRIX CLASS #
244
+ ############################################################
245
+
246
+
247
+ class SolveMatrix:
248
+ def __init__(self, A: sparse.csr_matrix):
249
+ A = A.tocsr()
250
+ if not A.has_sorted_indices:
251
+ A.sort_indices()
252
+
253
+ if (A.getnnz(axis=1) == 0).any() or (A.getnnz(axis=0) == 0).any():
254
+ raise ValueError('Matrix A is singular, because it contains empty rows or columns')
255
+
256
+ if A.dtype in (np.float16, np.float32, np.float64):
257
+ A = A.astype(np.float64)
258
+ elif np.iscomplexobj(A):
259
+ A = A.astype(np.complex128)
260
+ self.mat: sparse.csr_matrix = A
261
+
262
+ self.factorized_symb: bool = False
263
+ self.factorized_num: bool = False
264
+
265
+
266
+ def is_same(self, matrix: SolveMatrix):
267
+ return id(self.mat) is id(matrix.mat)
268
+
269
+ def same_structure(self, matrix: SolveMatrix):
270
+ return np.all(matrix.mat.indices == self.mat.indices) & np.all(matrix.mat.indptr == self.mat.indptr)
271
+
272
+ @property
273
+ def zerovec(self):
274
+ return np.zeros_like((self.mat.shape[0], 1), dtype=self.mat.dtype)
275
+
276
+
277
+ ############################################################
278
+ # THE PARDISO INTERFACE #
279
+ ############################################################
280
+
281
+ class PardisoInterface:
282
+
283
+ def __init__(self):
284
+
285
+ self.libmkl: ctypes.CDLL = load_mkl()
286
+ self._pardiso_interface = self.libmkl.pardiso
287
+
288
+ self._pardiso_interface.restype = None
289
+ self._pardiso_interface.argtypes = PARDISO_ARG_TYPES
290
+
291
+ self.PT = np.zeros(64, dtype=PT_B)
292
+ self.IPARM = np.zeros(64, dtype=np.int32)
293
+ self.PERM = np.zeros(0, dtype=np.int32)
294
+ self.MATRIX_TYPE: PARDISOMType = None
295
+ self.complex: bool = False
296
+ self.mat_structure: np.ndarray = None
297
+ self.message_level: int = 0
298
+
299
+ self.matrix: SolveMatrix = None
300
+ self.factored_matrix: SolveMatrix = None
301
+
302
+ self.configure_solver()
303
+
304
+ def _configure(self, A: sparse.csr_matrix) -> None:
305
+ """Configures the solver for the appropriate data type (float/complex)
306
+
307
+ Args:
308
+ A (sparse.csr_matrix): The sparse matrix to solve
309
+ """
310
+ if np.iscomplexobj(A):
311
+ self.MATRIX_TYPE = PARDISOMType.COMP_SYM_STRUCT
312
+ self.complex = True
313
+ else:
314
+ self.MATRIX_TYPE = PARDISOMType.REAL_SYM_STRUCT
315
+ self.complex = False
316
+
317
+ def _prepare_B(self, b: np.ndarray) -> np.ndarray:
318
+ """Fixes the forcing-vector for the solution process
319
+
320
+ Args:
321
+ b (np.ndarray): The forcing vector in Ax=b
322
+
323
+ Returns:
324
+ np.ndarray: The prepared forcing-vector
325
+ """
326
+ if sparse.issparse(b):
327
+ b = b.todense()
328
+ if np.iscomplexobj(b):
329
+ b = b.astype(np.complex128)
330
+ else:
331
+ b = b.astype(np.float64)
332
+ return b
333
+
334
+ def symbolic(self, A: sparse.csr_matrix) -> int:
335
+ """Calls the Symbollic solve routinge
336
+
337
+ Returns:
338
+ int: The error code
339
+ """
340
+ print("SYMBOLIC FACTORIZATION")
341
+ self._configure(A)
342
+ zerovec = np.zeros_like((A.shape[0], 1), dtype=A.dtype)
343
+ _, error = self._call_solver(A, zerovec, phase=PARDISOPhase.SYMBOLIC_FACTOR)
344
+ return error
345
+
346
+ def numeric(self, A: sparse.csr_matrix) -> int:
347
+ """Calls the Numeric solve routine
348
+
349
+ Returns:
350
+ int: The error code
351
+ """
352
+ print("NUMERIC FACTORIZATION")
353
+ self._configure(A)
354
+ zerovec = np.zeros_like((A.shape[0], 1), dtype=A.dtype)
355
+ _, error = self._call_solver(A, zerovec, phase=PARDISOPhase.NUMERIC_FACTOR)
356
+ return error
357
+
358
+ def solve(self, A: sparse.csr_matrix, b: np.ndarray) -> tuple[np.ndarray, int]:
359
+ """ Solves the linear problem Ax=b with PARDISO
360
+
361
+ Args:
362
+ A (sparse.csr_matrix): The A-matrix
363
+ b (np.ndarray): The b-vector
364
+
365
+ Returns:
366
+ tuple[np.ndarray, int]: The solution vector x, and error code
367
+ """
368
+ self._configure(A)
369
+ b = self._prepare_B(b)
370
+ x, error = self._call_solver(A, b, phase=PARDISOPhase.NUMERIC_SOLVE)
371
+ return x, error
372
+
373
+ def configure_solver(self,
374
+ perm_algo: int = 3,
375
+ nthreads: int = None,
376
+ user_perm: int = 0,
377
+ n_refine_steps: int = 0,
378
+ pivot_pert: int = 13,
379
+ weighted_matching: int = 2):
380
+ """Configures the solver
381
+
382
+ Args:
383
+ perm_algo (int, optional): The permutation algorithm. Defaults to 3.
384
+ nthreads (int, optional): The number of threads (Must be greater than OMP_NUM_THREADS). Defaults to None.
385
+ user_perm (int, optional): 1, if a user permuation is provided (not supported yet). Defaults to 0.
386
+ n_refine_steps (int, optional): Number of refinement steps. Defaults to 0.
387
+ pivot_pert (int, optional): _description_. Defaults to 13.
388
+ weighted_matching (int, optional): weighted matching mode. Defaults to 2.
389
+ """
390
+ if nthreads is None:
391
+ nthreads = int(os.environ.get('OMP_NUM_THREADS'))
392
+
393
+ self.IPARM[1] = perm_algo
394
+ self.IPARM[2] = nthreads
395
+ self.IPARM[4] = user_perm
396
+ self.IPARM[7] = n_refine_steps
397
+ self.IPARM[9] = pivot_pert
398
+ self.IPARM[12] = weighted_matching
399
+
400
+ def _call_solver(self, A: sparse.csr_matrix, b: np.ndarray, phase: PARDISOPhase) -> tuple[np.ndarray, int]:
401
+ """Calls the PARDISO solver on linear problem Ax=b
402
+
403
+ Args:
404
+ A (sparse.csr_matrix): The A-matrix
405
+ b (np.ndarray): The b-vector
406
+ phase (PARDISOPhase): The solution phase
407
+
408
+ Returns:
409
+ tuple[np.ndarray, int]: The solution vector x and error code.
410
+ """
411
+
412
+ # Declare the empty vector
413
+ x = np.zeros_like(b)
414
+ error = ctypes.c_int32(0)
415
+
416
+ # Up the pointers as PARDISO uses [1,...] indexing
417
+ A_index_pointers = A.indptr + 1
418
+ A_indices = A.indices + 1
419
+
420
+ # Define the appropriate data type (complex vs real)
421
+ if self.complex:
422
+ VALUE_P = A.data.ctypes.data_as(CPX16_P)
423
+ RHS_P = b.ctypes.data_as(CPX16_P)
424
+ X_P = x.ctypes.data_as(CPX16_P)
425
+ else:
426
+ VALUE_P = A.data.ctypes.data_as(CFLOAT64_P)
427
+ RHS_P = b.ctypes.data_as(CFLOAT64_P)
428
+ X_P = x.ctypes.data_as(CFLOAT64_P)
429
+
430
+ # Calls the pardiso function
431
+ self._pardiso_interface(
432
+ self.PT.ctypes.data_as(ctypes.POINTER(PT_A)),
433
+ c_int(1),
434
+ c_int(1),
435
+ c_int(self.MATRIX_TYPE.value),
436
+ c_int(phase.value),
437
+ c_int(A.shape[0]),
438
+ VALUE_P,
439
+ A_index_pointers.ctypes.data_as(CINT32_P),
440
+ A_indices.ctypes.data_as(CINT32_P),
441
+ self.PERM.ctypes.data_as(CINT32_P),
442
+ c_int(1),
443
+ self.IPARM.ctypes.data_as(CINT32_P),
444
+ c_int(self.message_level),
445
+ RHS_P,
446
+ X_P,
447
+ ctypes.byref(error))
448
+
449
+ # Returns the solution vector plus error code
450
+ return np.ascontiguousarray(x), error.value
451
+
452
+ def get_error(self, error: int) -> str:
453
+ """Returns the PARDISO error description string given an error number
454
+
455
+ Args:
456
+ error (int): The error number
457
+
458
+ Returns:
459
+ str: A description string
460
+ """
461
+ return PARDISO_ERROR_CODES[error]
462
+
463
+ def clear_memory(self):
464
+ """Clear the memory of this solver plus the PARDISO process.
465
+ """
466
+ self.factorized_A = None
467
+ self.matrix = None
468
+ self.symbolic(sparse.csr_matrix((0,0), dtype=np.complex128), np.zeros(0, dtype=np.complex128))