emerge 0.5.2__py3-none-any.whl → 0.5.4__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 +2 -2
- emerge/_emerge/__init__.py +1 -26
- emerge/_emerge/_cache_check.py +46 -0
- emerge/_emerge/bc.py +3 -12
- emerge/_emerge/const.py +5 -0
- emerge/_emerge/elements/nedleg2.py +2 -2
- emerge/_emerge/geo/pcb.py +110 -13
- emerge/_emerge/geo/pcb_tools/calculator.py +2 -2
- emerge/_emerge/geometry.py +1 -1
- emerge/_emerge/logsettings.py +29 -13
- emerge/_emerge/material.py +4 -0
- emerge/_emerge/mesh3d.py +9 -9
- emerge/_emerge/mth/integrals.py +1 -1
- emerge/_emerge/mth/pairing.py +1 -2
- emerge/_emerge/periodic.py +1 -1
- emerge/_emerge/physics/microwave/adaptive_freq.py +1 -5
- emerge/_emerge/physics/microwave/assembly/assembler.py +62 -39
- emerge/_emerge/physics/microwave/assembly/curlcurl.py +1 -8
- emerge/_emerge/physics/microwave/microwave_3d.py +33 -26
- emerge/_emerge/physics/microwave/microwave_bc.py +97 -27
- emerge/_emerge/physics/microwave/microwave_data.py +3 -5
- emerge/_emerge/physics/microwave/sc.py +26 -26
- emerge/_emerge/physics/microwave/simjob.py +8 -3
- emerge/_emerge/selection.py +1 -1
- emerge/_emerge/simmodel.py +12 -9
- emerge/_emerge/simulation_data.py +5 -1
- emerge/_emerge/solve_interfaces/cudss_interface.py +238 -0
- emerge/_emerge/solver.py +285 -107
- emerge/cli.py +1 -1
- emerge/lib.py +54 -40
- {emerge-0.5.2.dist-info → emerge-0.5.4.dist-info}/METADATA +15 -8
- {emerge-0.5.2.dist-info → emerge-0.5.4.dist-info}/RECORD +35 -32
- {emerge-0.5.2.dist-info → emerge-0.5.4.dist-info}/licenses/LICENSE +39 -0
- {emerge-0.5.2.dist-info → emerge-0.5.4.dist-info}/WHEEL +0 -0
- {emerge-0.5.2.dist-info → emerge-0.5.4.dist-info}/entry_points.txt +0 -0
emerge/__init__.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""A Python based FEM solver.
|
|
2
|
-
Copyright (C) 2025
|
|
2
|
+
Copyright (C) 2025 Robert Fennis
|
|
3
3
|
|
|
4
4
|
This program is free software; you can redistribute it and/or
|
|
5
5
|
modify it under the terms of the GNU General Public License
|
|
@@ -36,7 +36,7 @@ os.environ["NUMEXPR_NUM_THREADS"] = NTHREADS
|
|
|
36
36
|
############################################################
|
|
37
37
|
# IMPORT MODULES #
|
|
38
38
|
############################################################
|
|
39
|
-
|
|
39
|
+
from ._emerge import _cache_check
|
|
40
40
|
from ._emerge.logsettings import LOG_CONTROLLER
|
|
41
41
|
from loguru import logger
|
|
42
42
|
|
emerge/_emerge/__init__.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# """A Python based FEM solver.
|
|
2
|
-
# Copyright (C) 2025
|
|
2
|
+
# Copyright (C) 2025 Robert Fennis
|
|
3
3
|
|
|
4
4
|
# This program is free software; you can redistribute it and/or
|
|
5
5
|
# modify it under the terms of the GNU General Public License
|
|
@@ -15,28 +15,3 @@
|
|
|
15
15
|
# along with this program; if not, see
|
|
16
16
|
# <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
|
-
# """
|
|
19
|
-
# import os
|
|
20
|
-
|
|
21
|
-
# os.environ["OMP_NUM_THREADS"] = "1"
|
|
22
|
-
# os.environ["MKL_NUM_THREADS"] = "1"
|
|
23
|
-
# os.environ["OPENBLAS_NUM_THREADS"] = "1"
|
|
24
|
-
|
|
25
|
-
# from loguru import logger
|
|
26
|
-
# from .logsettings import logger_format
|
|
27
|
-
# import sys
|
|
28
|
-
|
|
29
|
-
# logger.remove()
|
|
30
|
-
# logger.add(sys.stderr, format=logger_format)
|
|
31
|
-
|
|
32
|
-
# logger.debug('Importing modules')
|
|
33
|
-
# from _emerge.simmodel import Simulation3D
|
|
34
|
-
# from _emerge.material import Material, FR4, AIR, VACUUM, COPPER
|
|
35
|
-
# import bc
|
|
36
|
-
# from _emerge.solver import superlu_info, SolverBicgstab, SolverGMRES, SolveRoutine, ReverseCuthillMckee, Sorter, SolverPardiso, SolverUMFPACK
|
|
37
|
-
# from _emerge.cs import CoordinateSystem, Plane, Axis, XAX, YAX, ZAX, XYPLANE, XZPLANE, YZPLANE, YXPLANE, ZXPLANE, ZYPLANE
|
|
38
|
-
# from _emerge.coord import Line
|
|
39
|
-
# import geo
|
|
40
|
-
# from _emerge.selection import Selection, FaceSelection, DomainSelection, EdgeSelection
|
|
41
|
-
# from _emerge.mth.common_functions import norm
|
|
42
|
-
# logger.debug('Importing complete!')
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
from numba.core import event, types
|
|
19
|
+
from numba import njit
|
|
20
|
+
|
|
21
|
+
_COMPILE_MESSAGE = """
|
|
22
|
+
[ EMERGE ]
|
|
23
|
+
⚠ Numba is compiling optimized code; this may take a few minutes.
|
|
24
|
+
• Additional functions may be compiled on-the-fly.
|
|
25
|
+
• Compilation happens only once—subsequent runs load from cache.
|
|
26
|
+
Please wait…"""
|
|
27
|
+
|
|
28
|
+
@njit(cache=True)
|
|
29
|
+
def _donothing(a):
|
|
30
|
+
return a
|
|
31
|
+
|
|
32
|
+
class Notify(event.Listener):
|
|
33
|
+
def on_start(self, ev):
|
|
34
|
+
f = ev.data['dispatcher']
|
|
35
|
+
sig = ev.data['args']
|
|
36
|
+
if f is _donothing: # limit to the function you care about
|
|
37
|
+
sig = ev.data['args']
|
|
38
|
+
print(_COMPILE_MESSAGE)
|
|
39
|
+
|
|
40
|
+
def on_end(self, ev): # unused here
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# install listener only for this block:
|
|
45
|
+
with event.install_listener("numba:compile", Notify()):
|
|
46
|
+
_donothing(0)
|
emerge/_emerge/bc.py
CHANGED
|
@@ -64,17 +64,8 @@ class BoundaryCondition:
|
|
|
64
64
|
return self.dimension.value
|
|
65
65
|
|
|
66
66
|
def __repr__(self) -> str:
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
elif self.dimension is BCDimension.EDGE:
|
|
70
|
-
return f'{type(self).__name__}{self.tags}'
|
|
71
|
-
elif self.dimension is BCDimension.NODE:
|
|
72
|
-
return f'{type(self).__name__}{self.tags}'
|
|
73
|
-
elif self.dimension is BCDimension.FACE:
|
|
74
|
-
return f'{type(self).__name__}{self.tags}'
|
|
75
|
-
elif self.dimension is BCDimension.DOMAIN:
|
|
76
|
-
return f'{type(self).__name__}{self.tags}'
|
|
77
|
-
|
|
67
|
+
return f'{type(self).__name__}{self.tags}'
|
|
68
|
+
|
|
78
69
|
def __str__(self) -> str:
|
|
79
70
|
return self.__repr__()
|
|
80
71
|
|
|
@@ -183,7 +174,7 @@ class BoundaryConditionSet:
|
|
|
183
174
|
Returns:
|
|
184
175
|
list[BoundaryCondition]: The list of boundary conditions
|
|
185
176
|
"""
|
|
186
|
-
return [item for item in self.
|
|
177
|
+
return [item for item in self.boundary_conditions if isinstance(item, bctype)]
|
|
187
178
|
|
|
188
179
|
def reset(self) -> None:
|
|
189
180
|
"""Resets the boundary conditions that are defined
|
emerge/_emerge/const.py
ADDED
|
@@ -22,7 +22,7 @@ from .femdata import FEMBasis
|
|
|
22
22
|
from .ned2_interp import ned2_tri_interp_full, ned2_tri_interp_curl
|
|
23
23
|
from ..mth.optimized import matinv
|
|
24
24
|
from ..cs import CoordinateSystem
|
|
25
|
-
|
|
25
|
+
from ..const import MU0, C0
|
|
26
26
|
|
|
27
27
|
## TODO: TEMPORARY SOLUTION FIX THIS
|
|
28
28
|
|
|
@@ -158,7 +158,7 @@ class NedelecLegrange2(FEMBasis):
|
|
|
158
158
|
|
|
159
159
|
def interpolate_Hf(self, field: np.ndarray, k0: float, ur: np.ndarray, beta: float) -> FieldFunctionClass:
|
|
160
160
|
'''Generates the Interpolation function as a function object for a given coordiante basis and origin.'''
|
|
161
|
-
constant = 1j / ((k0*
|
|
161
|
+
constant = 1j / ((k0*C0)*MU0)
|
|
162
162
|
urinv = np.zeros_like(ur)
|
|
163
163
|
|
|
164
164
|
for i in range(ur.shape[2]):
|
emerge/_emerge/geo/pcb.py
CHANGED
|
@@ -17,9 +17,6 @@
|
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
-
import numpy as np
|
|
21
|
-
import gmsh
|
|
22
|
-
|
|
23
20
|
from ..cs import CoordinateSystem, GCS, Axis
|
|
24
21
|
from ..geometry import GeoPolygon, GeoVolume, GeoSurface
|
|
25
22
|
from ..material import Material, AIR, COPPER
|
|
@@ -28,11 +25,14 @@ from .polybased import XYPolygon
|
|
|
28
25
|
from .operations import change_coordinate_system
|
|
29
26
|
from .pcb_tools.macro import parse_macro
|
|
30
27
|
from .pcb_tools.calculator import PCBCalculator
|
|
28
|
+
|
|
29
|
+
import numpy as np
|
|
31
30
|
from loguru import logger
|
|
32
31
|
from typing import Literal, Callable, overload
|
|
33
32
|
from dataclasses import dataclass
|
|
34
|
-
import math
|
|
35
33
|
|
|
34
|
+
import math
|
|
35
|
+
import gmsh
|
|
36
36
|
|
|
37
37
|
############################################################
|
|
38
38
|
# EXCEPTIONS #
|
|
@@ -63,7 +63,15 @@ def normalize(vector: np.ndarray) -> np.ndarray:
|
|
|
63
63
|
return vector
|
|
64
64
|
return vector / norm
|
|
65
65
|
|
|
66
|
-
def _rot_mat(angle):
|
|
66
|
+
def _rot_mat(angle: float) -> np.ndarray:
|
|
67
|
+
"""Returns a 2D rotation matrix given an angle in degrees
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
angle (float): The angle in degrees
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
np.ndarray: The rotation matrix
|
|
74
|
+
"""
|
|
67
75
|
ang = -angle * np.pi/180
|
|
68
76
|
return np.array([[np.cos(ang), -np.sin(ang)], [np.sin(ang), np.cos(ang)]])
|
|
69
77
|
|
|
@@ -265,6 +273,66 @@ class StripTurn(RouteElement):
|
|
|
265
273
|
else:
|
|
266
274
|
raise RouteException(f'Trying to route a StripTurn with an unknown corner type: {self.corner_type}')
|
|
267
275
|
|
|
276
|
+
class StripCurve(StripTurn):
|
|
277
|
+
def __init__(self,
|
|
278
|
+
x: float,
|
|
279
|
+
y: float,
|
|
280
|
+
width: float,
|
|
281
|
+
direction: tuple[float, float],
|
|
282
|
+
angle: float,
|
|
283
|
+
radius: float,
|
|
284
|
+
dang: float = 10.0):
|
|
285
|
+
self.xold: float = x
|
|
286
|
+
self.yold: float = y
|
|
287
|
+
self.width: float = width
|
|
288
|
+
self.old_direction: np.ndarray = normalize(np.array(direction))
|
|
289
|
+
self.direction: np.ndarray = _rot_mat(angle) @ self.old_direction
|
|
290
|
+
self.angle: float = angle
|
|
291
|
+
self.radius: float = radius
|
|
292
|
+
self.dirright: np.ndarray = np.array([self.old_direction[1], -self.old_direction[0]])
|
|
293
|
+
self.dang: float = dang
|
|
294
|
+
|
|
295
|
+
angd = abs(angle*np.pi/180)
|
|
296
|
+
self.start = np.array([x,y])
|
|
297
|
+
self.circ_origin = self.start + radius * np.sign(angle) * self.dirright
|
|
298
|
+
|
|
299
|
+
self._xhat = -self.dirright * np.sign(angle)
|
|
300
|
+
self._yhat = self.old_direction
|
|
301
|
+
|
|
302
|
+
self.end = self.circ_origin + radius*(self._xhat*np.cos(angd)+self._yhat*np.sin(angd))
|
|
303
|
+
self.x, self.y = self.end
|
|
304
|
+
|
|
305
|
+
def __str__(self) -> str:
|
|
306
|
+
return f'StripCurve[{self.x},{self.y},w={self.width},d=({self.direction})]'
|
|
307
|
+
|
|
308
|
+
@property
|
|
309
|
+
def right(self) -> list[tuple[float, float]]:
|
|
310
|
+
points: list[tuple[float, float]] = []
|
|
311
|
+
Npts = int(np.ceil(abs(self.angle/self.dang)))
|
|
312
|
+
R = self.radius-np.sign(self.angle)*self.width/2
|
|
313
|
+
print(R, self.circ_origin, self._xhat, self._yhat)
|
|
314
|
+
for i in range(Npts):
|
|
315
|
+
ang = abs((i+1)/Npts * self.angle * np.pi/180)
|
|
316
|
+
pnew = self.circ_origin + R*(self._xhat*np.cos(ang)+self._yhat*np.sin(ang))
|
|
317
|
+
points.append((pnew[0], pnew[1]))
|
|
318
|
+
|
|
319
|
+
return points
|
|
320
|
+
|
|
321
|
+
@property
|
|
322
|
+
def left(self) -> list[tuple[float, float]]:
|
|
323
|
+
points: list[tuple[float, float]] = []
|
|
324
|
+
|
|
325
|
+
Npts = int(np.ceil(abs(self.angle/self.dang)))
|
|
326
|
+
R = self.radius+np.sign(self.angle)*self.width/2
|
|
327
|
+
print(R, self.circ_origin, self._xhat, self._yhat)
|
|
328
|
+
for i in range(Npts):
|
|
329
|
+
ang = abs((i+1)/Npts * self.angle * np.pi/180)
|
|
330
|
+
pnew = self.circ_origin + R*(self._xhat*np.cos(ang)+self._yhat*np.sin(ang))
|
|
331
|
+
points.append((pnew[0], pnew[1]))
|
|
332
|
+
|
|
333
|
+
return points[::-1]
|
|
334
|
+
|
|
335
|
+
|
|
268
336
|
############################################################
|
|
269
337
|
# THE STRIP PATH CLASS #
|
|
270
338
|
############################################################
|
|
@@ -405,7 +473,9 @@ class StripPath:
|
|
|
405
473
|
"""
|
|
406
474
|
x, y = self.end.x, self.end.y
|
|
407
475
|
dx, dy = self.end.direction
|
|
408
|
-
|
|
476
|
+
if abs(angle) <= 20:
|
|
477
|
+
corner_type = 'square'
|
|
478
|
+
logger.warning('Small turn detected, defaulting to rectangular corners because chamfers would add to much detail.')
|
|
409
479
|
if width is not None:
|
|
410
480
|
if width != self.end.width:
|
|
411
481
|
self._add_element(StripLine(x, y, width, (dx, dy)))
|
|
@@ -413,6 +483,37 @@ class StripPath:
|
|
|
413
483
|
width=self.end.width
|
|
414
484
|
self._add_element(StripTurn(x, y, width, (dx, dy), angle, corner_type))
|
|
415
485
|
return self
|
|
486
|
+
|
|
487
|
+
def curve(self, angle: float, radius: float,
|
|
488
|
+
width: float | None = None,
|
|
489
|
+
corner_type: Literal['champher','square'] = 'champher',
|
|
490
|
+
dang: float = 10) -> StripPath:
|
|
491
|
+
"""Adds a bend to the strip path.
|
|
492
|
+
|
|
493
|
+
The angle is specified in degrees. The width of the turn will be the same as the last segment.
|
|
494
|
+
optionally, a different width may be provided.
|
|
495
|
+
By default, all corners will be cut using the "champher" type. Other options are not yet provided.
|
|
496
|
+
|
|
497
|
+
Args:
|
|
498
|
+
angle (float): The turning angle
|
|
499
|
+
width (float, optional): The stripline width. Defaults to None.
|
|
500
|
+
corner_type (str, optional): The corner type. Defaults to 'champher'.
|
|
501
|
+
|
|
502
|
+
Returns:
|
|
503
|
+
StripPath: The current StripPath object
|
|
504
|
+
"""
|
|
505
|
+
if angle == 0:
|
|
506
|
+
logger.trace('Zero angle turn, passing action')
|
|
507
|
+
return self
|
|
508
|
+
x, y = self.end.x, self.end.y
|
|
509
|
+
dx, dy = self.end.direction
|
|
510
|
+
if width is not None:
|
|
511
|
+
if width != self.end.width:
|
|
512
|
+
self._add_element(StripLine(x, y, width, (dx, dy)))
|
|
513
|
+
else:
|
|
514
|
+
width=self.end.width
|
|
515
|
+
self._add_element(StripCurve(x, y, width, (dx, dy), angle, radius, dang=dang))
|
|
516
|
+
return self
|
|
416
517
|
|
|
417
518
|
def store(self, name: str) -> StripPath:
|
|
418
519
|
""" Store the current x,y coordinate labeled in the PCB object.
|
|
@@ -495,7 +596,6 @@ class StripPath:
|
|
|
495
596
|
self.pcb._lumped_element(poly, impedance_function, width, length)
|
|
496
597
|
return self.pcb.new(x+dx*length, y+dy*length, self.end.width, self.end.direction, self.z)
|
|
497
598
|
|
|
498
|
-
|
|
499
599
|
def cut(self) -> StripPath:
|
|
500
600
|
"""Split the current path in N new paths given by a new departure direction
|
|
501
601
|
|
|
@@ -637,7 +737,7 @@ class StripPath:
|
|
|
637
737
|
def to(self, dest: tuple[float, float],
|
|
638
738
|
arrival_dir: tuple[float, float] | None = None,
|
|
639
739
|
arrival_margin: float | None= None,
|
|
640
|
-
angle_step: float = 90):
|
|
740
|
+
angle_step: float = 90) -> StripPath:
|
|
641
741
|
"""
|
|
642
742
|
Extend the path from current end point to dest (x, y).
|
|
643
743
|
Optionally ensure arrival in arrival_dir after a straight segment of arrival_margin.
|
|
@@ -743,9 +843,8 @@ class StripPath:
|
|
|
743
843
|
back_ang = math.degrees(math.atan2(cross2, dot2))
|
|
744
844
|
|
|
745
845
|
backoff = math.tan(abs(back_ang)*np.pi/360)*self.end.width/2
|
|
746
|
-
self.straight(s - backoff)
|
|
747
|
-
|
|
748
846
|
|
|
847
|
+
self.straight(s - backoff)
|
|
749
848
|
self.turn(-back_ang)
|
|
750
849
|
|
|
751
850
|
x0 = self.end.x
|
|
@@ -753,7 +852,6 @@ class StripPath:
|
|
|
753
852
|
D = math.hypot(tx-x0, ty-y0)
|
|
754
853
|
# 4) Final straight into destination by arrival_margin + t
|
|
755
854
|
self.straight(D)
|
|
756
|
-
|
|
757
855
|
|
|
758
856
|
return self
|
|
759
857
|
|
|
@@ -801,7 +899,7 @@ class StripPath:
|
|
|
801
899
|
return self.path[element_nr]
|
|
802
900
|
|
|
803
901
|
############################################################
|
|
804
|
-
# PCB DESIGN CLASS
|
|
902
|
+
# PCB DESIGN CLASS #
|
|
805
903
|
############################################################
|
|
806
904
|
|
|
807
905
|
class PCB:
|
|
@@ -1270,7 +1368,6 @@ class PCB:
|
|
|
1270
1368
|
polys.material = COPPER
|
|
1271
1369
|
return polys
|
|
1272
1370
|
|
|
1273
|
-
|
|
1274
1371
|
############################################################
|
|
1275
1372
|
# DEPRICATED #
|
|
1276
1373
|
############################################################
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
from ...material import Material
|
|
3
|
+
from ...const import Z0 as n0
|
|
3
4
|
|
|
4
5
|
PI = np.pi
|
|
5
6
|
TAU = 2*PI
|
|
6
|
-
n0 = 376.73
|
|
7
7
|
|
|
8
8
|
def microstrip_z0(W: float, th: float, er: float):
|
|
9
9
|
u = W/th
|
|
@@ -14,7 +14,7 @@ def microstrip_z0(W: float, th: float, er: float):
|
|
|
14
14
|
|
|
15
15
|
class PCBCalculator:
|
|
16
16
|
|
|
17
|
-
def __init__(self, thickness: float, layers: np.
|
|
17
|
+
def __init__(self, thickness: float, layers: np.ndarray, material: Material, unit: float):
|
|
18
18
|
self.th = thickness
|
|
19
19
|
self.layers = layers
|
|
20
20
|
self.mat = material
|
emerge/_emerge/geometry.py
CHANGED
|
@@ -299,7 +299,7 @@ class GeoObject:
|
|
|
299
299
|
for tag in self.tags:
|
|
300
300
|
newtags.extend(tagmap.get(tag, [tag,]))
|
|
301
301
|
self.tags = newtags
|
|
302
|
-
logger.debug(f'Replaced {self.old_tags}
|
|
302
|
+
logger.debug(f'{self} Replaced {self.old_tags} -> {self.tags}')
|
|
303
303
|
|
|
304
304
|
def update_tags(self, tag_mapping: dict[int,dict]) -> GeoObject:
|
|
305
305
|
''' Update the tag definition of a GeoObject after fragementation.'''
|
emerge/_emerge/logsettings.py
CHANGED
|
@@ -1,34 +1,52 @@
|
|
|
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
|
+
|
|
1
18
|
from loguru import logger
|
|
2
19
|
import sys
|
|
3
20
|
from typing import Literal
|
|
4
21
|
from enum import Enum
|
|
5
22
|
from pathlib import Path
|
|
6
23
|
import os
|
|
7
|
-
|
|
24
|
+
import gmsh
|
|
8
25
|
|
|
9
26
|
############################################################
|
|
10
27
|
# FORMATS #
|
|
11
28
|
############################################################
|
|
12
29
|
|
|
13
30
|
TRACE_FORMAT = (
|
|
14
|
-
"{time: YY/MM/DD
|
|
15
|
-
"
|
|
31
|
+
"{time:ddd YY/MM/DD HH:mm:ss.SSSS} {level:<7} {thread.id:<15} {line:>4}: "
|
|
32
|
+
"{message}"
|
|
16
33
|
)
|
|
34
|
+
|
|
17
35
|
DEBUG_FORMAT = (
|
|
18
|
-
"<green>{elapsed}</green>
|
|
19
|
-
"
|
|
36
|
+
"<green>{elapsed}</green> <level>{level:<7}</level>: "
|
|
37
|
+
"<level>{message}</level>"
|
|
20
38
|
)
|
|
21
39
|
INFO_FORMAT = (
|
|
22
|
-
"<green>{elapsed}</green>
|
|
23
|
-
"
|
|
40
|
+
"<green>{elapsed}</green> <level>{level:<7}</level>: "
|
|
41
|
+
"<level>{message}</level>"
|
|
24
42
|
)
|
|
25
43
|
WARNING_FORMAT = (
|
|
26
|
-
"<green>{elapsed}</green>
|
|
27
|
-
"
|
|
44
|
+
"<green>{elapsed}</green> <level>{level:<7}</level>: "
|
|
45
|
+
"<level>{message}</level>"
|
|
28
46
|
)
|
|
29
47
|
ERROR_FORMAT = (
|
|
30
|
-
"<green>{elapsed}</green>
|
|
31
|
-
"
|
|
48
|
+
"<green>{elapsed}</green> <level>{level:<7}</level>: "
|
|
49
|
+
"<level>{message}</level>"
|
|
32
50
|
)
|
|
33
51
|
FORMAT_DICT = {
|
|
34
52
|
'TRACE': TRACE_FORMAT,
|
|
@@ -71,10 +89,8 @@ class LogController:
|
|
|
71
89
|
logger.configure(handlers=[handler]) # type: ignore
|
|
72
90
|
self.level = loglevel
|
|
73
91
|
os.environ["EMERGE_STD_LOGLEVEL"] = loglevel
|
|
74
|
-
|
|
75
92
|
|
|
76
93
|
def set_write_file(self, path: Path, loglevel: str = 'TRACE'):
|
|
77
|
-
|
|
78
94
|
handler_id = logger.add(str(path / 'logging.log'), mode='w', level=loglevel, format=FORMAT_DICT.get(loglevel, INFO_FORMAT), colorize=False, backtrace=True, diagnose=True)
|
|
79
95
|
self.file_handlers.append(handler_id)
|
|
80
96
|
self.file_level = loglevel
|
emerge/_emerge/material.py
CHANGED
|
@@ -45,6 +45,10 @@ class Material:
|
|
|
45
45
|
hex_str = self.color.lstrip('#')
|
|
46
46
|
self._color_rgb = tuple(int(hex_str[i:i+2], 16)/255.0 for i in (0, 2, 4))
|
|
47
47
|
|
|
48
|
+
@property
|
|
49
|
+
def sigma(self) -> float:
|
|
50
|
+
return self.cond
|
|
51
|
+
|
|
48
52
|
@property
|
|
49
53
|
def color_rgb(self) -> tuple[float,float,float]:
|
|
50
54
|
return self._color_rgb
|
emerge/_emerge/mesh3d.py
CHANGED
|
@@ -455,15 +455,11 @@ class Mesh3D(Mesh):
|
|
|
455
455
|
Returns:
|
|
456
456
|
tuple[dict[int, int], np.ndarray, np.ndarray]: The node index mapping and the node index arrays
|
|
457
457
|
"""
|
|
458
|
-
|
|
459
|
-
def gen_key(coord, mult):
|
|
460
|
-
return tuple([int(round(c*mult)) for c in coord])
|
|
461
458
|
|
|
462
|
-
ftag_to_node = dict()
|
|
463
|
-
face_dimtags = gmsh.model.get_entities(2)
|
|
464
|
-
|
|
465
459
|
node_ids_1 = []
|
|
466
460
|
node_ids_2 = []
|
|
461
|
+
|
|
462
|
+
face_dimtags = gmsh.model.get_entities(2)
|
|
467
463
|
|
|
468
464
|
for d,t in face_dimtags:
|
|
469
465
|
domain_tag, f_tags, node_tags = gmsh.model.mesh.get_elements(2, t)
|
|
@@ -472,19 +468,23 @@ class Mesh3D(Mesh):
|
|
|
472
468
|
node_ids_1.extend(node_tags)
|
|
473
469
|
if t in bc.face2.tags:
|
|
474
470
|
node_ids_2.extend(node_tags)
|
|
475
|
-
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
node_ids_1 = sorted(list(set(node_ids_1)))
|
|
474
|
+
node_ids_2 = sorted(list(set(node_ids_2)))
|
|
476
475
|
|
|
477
476
|
all_node_ids = np.unique(np.array(node_ids_1 + node_ids_2))
|
|
478
477
|
dsmin = shortest_distance(self.nodes[:,all_node_ids])
|
|
479
478
|
|
|
480
|
-
node_ids_1_arry = np.
|
|
481
|
-
node_ids_2_arry = np.
|
|
479
|
+
node_ids_1_arry = np.array(node_ids_1)
|
|
480
|
+
node_ids_2_arry = np.array(node_ids_2)
|
|
482
481
|
dv = np.array(bc.dv)
|
|
483
482
|
|
|
484
483
|
nodemap = pair_coordinates(self.nodes, node_ids_1_arry, node_ids_2_arry, dv, dsmin/2)
|
|
485
484
|
node_ids_2_unsorted = [nodemap[i] for i in sorted(node_ids_1)]
|
|
486
485
|
node_ids_2_sorted = sorted(node_ids_2_unsorted)
|
|
487
486
|
conv_map = {i1: i2 for i1, i2 in zip(node_ids_2_unsorted, node_ids_2_sorted)}
|
|
487
|
+
|
|
488
488
|
return conv_map, np.array(node_ids_2_unsorted), np.array(node_ids_2_sorted)
|
|
489
489
|
|
|
490
490
|
|
emerge/_emerge/mth/integrals.py
CHANGED
|
@@ -60,7 +60,7 @@ def _fast_integral_f(nodes, triangles, constants, DPTs, field_values):
|
|
|
60
60
|
def surface_integral(nodes: np.ndarray,
|
|
61
61
|
triangles: np.ndarray,
|
|
62
62
|
function: Callable,
|
|
63
|
-
constants: np.ndarray = None,
|
|
63
|
+
constants: np.ndarray | None = None,
|
|
64
64
|
gq_order: int = 4) -> complex:
|
|
65
65
|
"""Computes the surface integral of a scalar-field
|
|
66
66
|
|
emerge/_emerge/mth/pairing.py
CHANGED
|
@@ -47,7 +47,6 @@ def link_coords(coords: np.ndarray, ids1: np.ndarray, ids2: np.ndarray, disp: np
|
|
|
47
47
|
for i1 in range(N):
|
|
48
48
|
ictr = 0
|
|
49
49
|
c1 = coords[:,ids1[i1]]
|
|
50
|
-
|
|
51
50
|
for i2 in range(id_start, N):
|
|
52
51
|
if available[i2] == 0:
|
|
53
52
|
continue
|
|
@@ -91,7 +90,7 @@ def pair_coordinates(coords: np.ndarray, ids1: np.ndarray, ids2: np.ndarray, dis
|
|
|
91
90
|
ids2_c_sorted = sorted(ids2, key= lambda x: tuple(coords[:,x]-disp))
|
|
92
91
|
|
|
93
92
|
mapping = link_coords(coords, np.array(ids1_c_sorted), np.array(ids2_c_sorted), disp, dsmax)
|
|
94
|
-
|
|
93
|
+
|
|
95
94
|
mapping = {i: j for i,j in zip(mapping[0,:], mapping[1,:])}
|
|
96
95
|
|
|
97
96
|
return mapping
|
emerge/_emerge/periodic.py
CHANGED
|
@@ -31,7 +31,7 @@ def _pair_selection(f1: Selection, f2: Selection, translation: tuple[float, floa
|
|
|
31
31
|
f2s = []
|
|
32
32
|
for t1, c1 in zip(f1.tags, c1s):
|
|
33
33
|
for t2, c2 in zip(f2.tags, c2s):
|
|
34
|
-
if np.linalg.norm((c1 + ds)-c2) < 1e-
|
|
34
|
+
if np.linalg.norm((c1 + ds)-c2) < 1e-8:
|
|
35
35
|
f1s.append(Selection([t1,]))
|
|
36
36
|
f2s.append(Selection([t2,]))
|
|
37
37
|
return f1s, f2s
|
|
@@ -108,10 +108,6 @@ def vectfit_step(f: np.ndarray, s: np.ndarray, poles: np.ndarray) -> np.ndarray:
|
|
|
108
108
|
b = np.concatenate((b.real, b.imag))
|
|
109
109
|
x, residuals, rnk, s = np.linalg.lstsq(A, b, rcond=-1)
|
|
110
110
|
|
|
111
|
-
residues = x[:N]
|
|
112
|
-
d = x[N]
|
|
113
|
-
h = x[N+1]
|
|
114
|
-
|
|
115
111
|
# We only want the "tilde" part in (A.4)
|
|
116
112
|
x = x[-N:]
|
|
117
113
|
|
|
@@ -197,7 +193,7 @@ def vectfit_auto(f: np.ndarray,
|
|
|
197
193
|
w = s.imag
|
|
198
194
|
pole_locs = np.linspace(w[0], w[-1], n_poles+2)[1:-1]
|
|
199
195
|
lr = loss_ratio
|
|
200
|
-
|
|
196
|
+
poles = np.concatenate([[p*(-lr + 1j), p*(-lr - 1j)] for p in pole_locs])
|
|
201
197
|
|
|
202
198
|
if inc_real:
|
|
203
199
|
poles = np.concatenate((poles, [1]))
|