emerge 1.0.3__py3-none-any.whl → 1.0.5__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 +7 -3
- emerge/_emerge/elements/femdata.py +5 -1
- emerge/_emerge/elements/ned2_interp.py +73 -30
- emerge/_emerge/elements/nedelec2.py +1 -0
- emerge/_emerge/emerge_update.py +63 -0
- emerge/_emerge/geo/operations.py +6 -3
- emerge/_emerge/geo/polybased.py +37 -5
- emerge/_emerge/geometry.py +5 -0
- emerge/_emerge/logsettings.py +26 -1
- emerge/_emerge/material.py +29 -8
- emerge/_emerge/mesh3d.py +16 -13
- emerge/_emerge/mesher.py +70 -3
- emerge/_emerge/physics/microwave/assembly/assembler.py +5 -4
- emerge/_emerge/physics/microwave/assembly/curlcurl.py +0 -1
- emerge/_emerge/physics/microwave/assembly/generalized_eigen.py +1 -2
- emerge/_emerge/physics/microwave/assembly/generalized_eigen_hb.py +1 -1
- emerge/_emerge/physics/microwave/assembly/robin_abc_order2.py +0 -1
- emerge/_emerge/physics/microwave/microwave_3d.py +37 -16
- emerge/_emerge/physics/microwave/microwave_bc.py +6 -4
- emerge/_emerge/physics/microwave/microwave_data.py +14 -11
- emerge/_emerge/plot/pyvista/cmap_maker.py +70 -0
- emerge/_emerge/plot/pyvista/display.py +93 -36
- emerge/_emerge/simmodel.py +77 -21
- emerge/_emerge/simulation_data.py +22 -4
- emerge/_emerge/solver.py +67 -32
- {emerge-1.0.3.dist-info → emerge-1.0.5.dist-info}/METADATA +2 -3
- {emerge-1.0.3.dist-info → emerge-1.0.5.dist-info}/RECORD +30 -28
- {emerge-1.0.3.dist-info → emerge-1.0.5.dist-info}/WHEEL +0 -0
- {emerge-1.0.3.dist-info → emerge-1.0.5.dist-info}/entry_points.txt +0 -0
- {emerge-1.0.3.dist-info → emerge-1.0.5.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from typing import Iterable, Sequence, Optional
|
|
2
|
+
import numpy as np
|
|
3
|
+
from matplotlib.colors import LinearSegmentedColormap, to_rgba
|
|
4
|
+
|
|
5
|
+
def make_colormap(
|
|
6
|
+
hex_colors: Sequence[str],
|
|
7
|
+
positions: Optional[Iterable[float]] = None,
|
|
8
|
+
name: str = "custom",
|
|
9
|
+
N: int = 256,
|
|
10
|
+
) -> LinearSegmentedColormap:
|
|
11
|
+
"""
|
|
12
|
+
Create a Matplotlib colormap from hex colors with optional positions.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
hex_colors : sequence of str
|
|
17
|
+
Hex color strings like '#RRGGBB' (or 'RRGGBB'). At least 2 required.
|
|
18
|
+
positions : iterable of float in [0, 1], optional
|
|
19
|
+
Locations of each color along the gradient. If not provided,
|
|
20
|
+
they are evenly spaced via linspace(0, 1, len(hex_colors)).
|
|
21
|
+
If provided, they do not have to be sorted; they will be sorted
|
|
22
|
+
together with the colors. If the first/last position do not
|
|
23
|
+
hit 0 or 1, they’ll be extended by duplicating the end colors.
|
|
24
|
+
name : str
|
|
25
|
+
Name for the colormap.
|
|
26
|
+
N : int
|
|
27
|
+
Resolution (number of lookup entries) of the colormap.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
matplotlib.colors.LinearSegmentedColormap
|
|
32
|
+
"""
|
|
33
|
+
# Normalize hex strings and basic validation
|
|
34
|
+
colors = []
|
|
35
|
+
for c in hex_colors:
|
|
36
|
+
if not isinstance(c, str):
|
|
37
|
+
raise TypeError("All colors must be hex strings.")
|
|
38
|
+
colors.append(c if c.startswith("#") else f"#{c}")
|
|
39
|
+
|
|
40
|
+
if len(colors) < 2:
|
|
41
|
+
raise ValueError("Provide at least two hex colors.")
|
|
42
|
+
|
|
43
|
+
# Build/validate positions
|
|
44
|
+
if positions is None:
|
|
45
|
+
pos = np.linspace(0.0, 1.0, len(colors), dtype=float)
|
|
46
|
+
else:
|
|
47
|
+
pos = np.asarray(list(positions), dtype=float)
|
|
48
|
+
if pos.size != len(colors):
|
|
49
|
+
raise ValueError("`positions` must have the same length as `hex_colors`.")
|
|
50
|
+
if np.any((pos < 0.0) | (pos > 1.0)):
|
|
51
|
+
raise ValueError("All positions must be within [0, 1].")
|
|
52
|
+
|
|
53
|
+
# Sort positions and carry colors along
|
|
54
|
+
order = np.argsort(pos)
|
|
55
|
+
pos = pos[order]
|
|
56
|
+
colors = [colors[i] for i in order]
|
|
57
|
+
|
|
58
|
+
# Ensure coverage of [0, 1] by extending ends if needed
|
|
59
|
+
if pos[0] > 0.0:
|
|
60
|
+
pos = np.r_[0.0, pos]
|
|
61
|
+
colors = [colors[0]] + colors
|
|
62
|
+
if pos[-1] < 1.0:
|
|
63
|
+
pos = np.r_[pos, 1.0]
|
|
64
|
+
colors = colors + [colors[-1]]
|
|
65
|
+
|
|
66
|
+
# Pair positions with RGBA
|
|
67
|
+
segment = list(zip(pos.tolist(), (to_rgba(c) for c in colors)))
|
|
68
|
+
|
|
69
|
+
# Create the colormap
|
|
70
|
+
return LinearSegmentedColormap.from_list(name, segment, N=N)
|
|
@@ -26,6 +26,8 @@ from typing import Iterable, Literal, Callable, Any
|
|
|
26
26
|
from ..display import BaseDisplay
|
|
27
27
|
from .display_settings import PVDisplaySettings
|
|
28
28
|
from matplotlib.colors import ListedColormap
|
|
29
|
+
from .cmap_maker import make_colormap
|
|
30
|
+
|
|
29
31
|
from itertools import cycle
|
|
30
32
|
### Color scale
|
|
31
33
|
|
|
@@ -40,6 +42,8 @@ cmap_names = Literal['bgy','bgyw','kbc','blues','bmw','bmy','kgy','gray','dimgra
|
|
|
40
42
|
'bkr','bky','coolwarm','gwv','bjy','bwy','cwr','colorwheel','isolum','rainbow','fire',
|
|
41
43
|
'cet_fire','gouldian','kbgyw','cwr','CET_CBL1','CET_CBL3','CET_D1A']
|
|
42
44
|
|
|
45
|
+
EMERGE_AMP = make_colormap(["#1F0061","#35188e","#1531ab", "#ff007b", "#ff7c51"], (0.0, 0.2, 0.4, 0.7, 0.9))
|
|
46
|
+
EMERGE_WAVE = make_colormap(["#4ab9ff","#0510B2B8","#3A37466E","#CC0954B9","#ff9036"], (0.0, 0.3, 0.5, 0.7, 1.0))
|
|
43
47
|
|
|
44
48
|
def _gen_c_cycle():
|
|
45
49
|
colors = [
|
|
@@ -74,24 +78,24 @@ class _RunState:
|
|
|
74
78
|
|
|
75
79
|
ANIM_STATE = _RunState()
|
|
76
80
|
|
|
77
|
-
def gen_cmap(mesh, N: int = 256):
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
+
# def gen_cmap(mesh, N: int = 256):
|
|
82
|
+
# # build a linear grid of data‐values (not strictly needed for pure colormap)
|
|
83
|
+
# vmin, vmax = mesh['values'].min(), mesh['values'].max()
|
|
84
|
+
# mapping = np.linspace(vmin, vmax, N)
|
|
81
85
|
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
# # prepare output
|
|
87
|
+
# newcolors = np.empty((N, 4))
|
|
84
88
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
# # normalized positions of control points: start, middle, end
|
|
90
|
+
# control_pos = np.array([0.0, 0.25, 0.5, 0.75, 1]) * (vmax - vmin) + vmin
|
|
91
|
+
# # stack control colors
|
|
92
|
+
# controls = np.vstack([col1, col2, col3, col4, col5])
|
|
89
93
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
# # interp each RGBA channel independently
|
|
95
|
+
# for chan in range(4):
|
|
96
|
+
# newcolors[:, chan] = np.interp(mapping, control_pos, controls[:, chan])
|
|
93
97
|
|
|
94
|
-
|
|
98
|
+
# return ListedColormap(newcolors)
|
|
95
99
|
|
|
96
100
|
def setdefault(options: dict, **kwargs) -> dict:
|
|
97
101
|
"""Shorthand for overwriting non-existent keyword arguments with defaults
|
|
@@ -252,8 +256,20 @@ class PVDisplay(BaseDisplay):
|
|
|
252
256
|
|
|
253
257
|
self._ctr: int = 0
|
|
254
258
|
|
|
259
|
+
self._cbar_args: dict = {}
|
|
260
|
+
self._cbar_lim: tuple[float, float] | None = None
|
|
255
261
|
self.camera_position = (1, -1, 1) # +X, +Z, -Y
|
|
256
262
|
|
|
263
|
+
|
|
264
|
+
def cbar(self, name: str, n_labels: int = 5, interactive: bool = False, clim: tuple[float, float] | None = None ) -> PVDisplay:
|
|
265
|
+
self._cbar_args = dict(title=name, n_labels=n_labels, interactive=interactive)
|
|
266
|
+
self._cbar_lim = clim
|
|
267
|
+
return self
|
|
268
|
+
|
|
269
|
+
def _reset_cbar(self) -> None:
|
|
270
|
+
self._cbar_args: dict = {}
|
|
271
|
+
self._cbar_lim: tuple[float, float] | None = None
|
|
272
|
+
|
|
257
273
|
def _wire_close_events(self):
|
|
258
274
|
self._closed = False
|
|
259
275
|
|
|
@@ -378,6 +394,7 @@ class PVDisplay(BaseDisplay):
|
|
|
378
394
|
>>> display.animate().surf(...)
|
|
379
395
|
>>> display.show()
|
|
380
396
|
"""
|
|
397
|
+
print('If you closed the animation without using (Q) press Ctrl+C to kill the process.')
|
|
381
398
|
self._Nsteps = Nsteps
|
|
382
399
|
self._fps = fps
|
|
383
400
|
self._do_animate = True
|
|
@@ -573,7 +590,6 @@ class PVDisplay(BaseDisplay):
|
|
|
573
590
|
else:
|
|
574
591
|
F = np.real(F.T)
|
|
575
592
|
Fnorm = np.sqrt(Fx.real**2 + Fy.real**2 + Fz.real**2).T
|
|
576
|
-
|
|
577
593
|
if XYZ is not None:
|
|
578
594
|
grid = pv.StructuredGrid(X,Y,Z)
|
|
579
595
|
self.add_surf(X,Y,Z,Fnorm, _fieldname = 'portfield')
|
|
@@ -588,7 +604,7 @@ class PVDisplay(BaseDisplay):
|
|
|
588
604
|
z: np.ndarray,
|
|
589
605
|
field: np.ndarray,
|
|
590
606
|
scale: Literal['lin','log','symlog'] = 'lin',
|
|
591
|
-
cmap: cmap_names =
|
|
607
|
+
cmap: cmap_names | None = None,
|
|
592
608
|
clim: tuple[float, float] | None = None,
|
|
593
609
|
opacity: float = 1.0,
|
|
594
610
|
symmetrize: bool = False,
|
|
@@ -614,8 +630,6 @@ class PVDisplay(BaseDisplay):
|
|
|
614
630
|
grid = pv.StructuredGrid(x,y,z)
|
|
615
631
|
field_flat = field.flatten(order='F')
|
|
616
632
|
|
|
617
|
-
|
|
618
|
-
|
|
619
633
|
if scale=='log':
|
|
620
634
|
T = lambda x: np.log10(np.abs(x+1e-12))
|
|
621
635
|
elif scale=='symlog':
|
|
@@ -624,6 +638,7 @@ class PVDisplay(BaseDisplay):
|
|
|
624
638
|
T = lambda x: x
|
|
625
639
|
|
|
626
640
|
static_field = T(np.real(field_flat))
|
|
641
|
+
|
|
627
642
|
if _fieldname is None:
|
|
628
643
|
name = 'anim'+str(self._ctr)
|
|
629
644
|
else:
|
|
@@ -634,18 +649,26 @@ class PVDisplay(BaseDisplay):
|
|
|
634
649
|
|
|
635
650
|
grid_no_nan = grid.threshold(scalars=name)
|
|
636
651
|
|
|
637
|
-
|
|
652
|
+
default_cmap = EMERGE_AMP
|
|
638
653
|
# Determine color limits
|
|
639
654
|
if clim is None:
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
655
|
+
if self._cbar_lim is not None:
|
|
656
|
+
clim = self._cbar_lim
|
|
657
|
+
else:
|
|
658
|
+
fmin = np.nanmin(static_field)
|
|
659
|
+
fmax = np.nanmax(static_field)
|
|
660
|
+
clim = (fmin, fmax)
|
|
661
|
+
|
|
643
662
|
if symmetrize:
|
|
644
663
|
lim = max(abs(clim[0]), abs(clim[1]))
|
|
645
664
|
clim = (-lim, lim)
|
|
646
|
-
|
|
665
|
+
default_cmap = EMERGE_WAVE
|
|
666
|
+
|
|
667
|
+
if cmap is None:
|
|
668
|
+
cmap = default_cmap
|
|
669
|
+
|
|
647
670
|
kwargs = setdefault(kwargs, cmap=cmap, clim=clim, opacity=opacity, pickable=False, multi_colors=True)
|
|
648
|
-
actor = self._plot.add_mesh(grid_no_nan, scalars=name, **kwargs)
|
|
671
|
+
actor = self._plot.add_mesh(grid_no_nan, scalars=name, scalar_bar_args=self._cbar_args, **kwargs)
|
|
649
672
|
|
|
650
673
|
|
|
651
674
|
if self._do_animate:
|
|
@@ -655,8 +678,9 @@ class PVDisplay(BaseDisplay):
|
|
|
655
678
|
obj.fgrid[name] = obj.grid.threshold(scalars=name)[name]
|
|
656
679
|
#obj.fgrid replace with thresholded scalar data.
|
|
657
680
|
self._objs.append(_AnimObject(field_flat, T, grid, grid_no_nan, actor, on_update))
|
|
658
|
-
|
|
659
|
-
|
|
681
|
+
|
|
682
|
+
self._reset_cbar()
|
|
683
|
+
|
|
660
684
|
def add_title(self, title: str) -> None:
|
|
661
685
|
"""Adds a title
|
|
662
686
|
|
|
@@ -689,6 +713,7 @@ class PVDisplay(BaseDisplay):
|
|
|
689
713
|
dx: np.ndarray, dy: np.ndarray, dz: np.ndarray,
|
|
690
714
|
scale: float = 1,
|
|
691
715
|
color: tuple[float, float, float] | None = None,
|
|
716
|
+
cmap: cmap_names | None = None,
|
|
692
717
|
scalemode: Literal['lin','log'] = 'lin'):
|
|
693
718
|
"""Add a quiver plot to the display
|
|
694
719
|
|
|
@@ -711,6 +736,8 @@ class PVDisplay(BaseDisplay):
|
|
|
711
736
|
|
|
712
737
|
ids = np.invert(np.isnan(dx))
|
|
713
738
|
|
|
739
|
+
if cmap is None:
|
|
740
|
+
cmap = EMERGE_AMP
|
|
714
741
|
x, y, z, dx, dy, dz = x[ids], y[ids], z[ids], dx[ids], dy[ids], dz[ids]
|
|
715
742
|
|
|
716
743
|
dmin = _min_distance(x,y,z)
|
|
@@ -729,8 +756,8 @@ class PVDisplay(BaseDisplay):
|
|
|
729
756
|
if color is not None:
|
|
730
757
|
kwargs['color'] = color
|
|
731
758
|
|
|
732
|
-
pl = self._plot.add_arrows(Coo, Vec, scalars=None, clim=None, cmap=
|
|
733
|
-
|
|
759
|
+
pl = self._plot.add_arrows(Coo, Vec, scalars=None, clim=None, cmap=cmap, **kwargs)
|
|
760
|
+
self._reset_cbar()
|
|
734
761
|
|
|
735
762
|
def add_contour(self,
|
|
736
763
|
X: np.ndarray,
|
|
@@ -738,8 +765,11 @@ class PVDisplay(BaseDisplay):
|
|
|
738
765
|
Z: np.ndarray,
|
|
739
766
|
V: np.ndarray,
|
|
740
767
|
Nlevels: int = 5,
|
|
768
|
+
scale: Literal['lin','log','symlog'] = 'lin',
|
|
741
769
|
symmetrize: bool = True,
|
|
742
|
-
|
|
770
|
+
clim: tuple[float, float] | None = None,
|
|
771
|
+
cmap: cmap_names | None = None,
|
|
772
|
+
opacity: float = 0.25):
|
|
743
773
|
"""Adds a 3D volumetric contourplot based on a 3D grid of X,Y,Z and field values
|
|
744
774
|
|
|
745
775
|
|
|
@@ -753,27 +783,54 @@ class PVDisplay(BaseDisplay):
|
|
|
753
783
|
cmap (str, optional): The color map. Defaults to 'viridis'.
|
|
754
784
|
"""
|
|
755
785
|
Vf = V.flatten()
|
|
786
|
+
Vf = np.nan_to_num(Vf)
|
|
756
787
|
vmin = np.min(np.real(Vf))
|
|
757
788
|
vmax = np.max(np.real(Vf))
|
|
789
|
+
|
|
790
|
+
default_cmap = EMERGE_AMP
|
|
791
|
+
|
|
792
|
+
if scale=='log':
|
|
793
|
+
T = lambda x: np.log10(np.abs(x+1e-12))
|
|
794
|
+
elif scale=='symlog':
|
|
795
|
+
T = lambda x: np.sign(x) * np.log10(1 + np.abs(x*np.log(10)))
|
|
796
|
+
else:
|
|
797
|
+
T = lambda x: x
|
|
798
|
+
|
|
758
799
|
if symmetrize:
|
|
759
|
-
level =
|
|
800
|
+
level = np.max(np.abs(Vf))
|
|
760
801
|
vmin, vmax = (-level, level)
|
|
802
|
+
default_cmap = EMERGE_WAVE
|
|
803
|
+
|
|
804
|
+
if clim is None:
|
|
805
|
+
if self._cbar_lim is not None:
|
|
806
|
+
clim = self._cbar_lim
|
|
807
|
+
vmin, vmax = clim
|
|
808
|
+
else:
|
|
809
|
+
clim = (vmin, vmax)
|
|
810
|
+
|
|
811
|
+
if cmap is None:
|
|
812
|
+
cmap = default_cmap
|
|
813
|
+
|
|
761
814
|
grid = pv.StructuredGrid(X,Y,Z)
|
|
762
815
|
field = V.flatten(order='F')
|
|
763
|
-
grid['anim'] = np.real(field)
|
|
816
|
+
grid['anim'] = T(np.real(field))
|
|
817
|
+
|
|
764
818
|
levels = list(np.linspace(vmin, vmax, Nlevels))
|
|
765
819
|
contour = grid.contour(isosurfaces=levels)
|
|
766
|
-
|
|
767
|
-
|
|
820
|
+
|
|
821
|
+
actor = self._plot.add_mesh(contour, opacity=opacity, cmap=cmap, clim=clim, pickable=False, scalar_bar_args=self._cbar_args)
|
|
822
|
+
|
|
768
823
|
if self._do_animate:
|
|
769
824
|
def on_update(obj: _AnimObject, phi: complex):
|
|
770
|
-
new_vals = np.real(obj.field * phi)
|
|
825
|
+
new_vals = obj.T(np.real(obj.field * phi))
|
|
771
826
|
obj.grid['anim'] = new_vals
|
|
772
827
|
new_contour = obj.grid.contour(isosurfaces=levels)
|
|
773
828
|
obj.actor.GetMapper().SetInputData(new_contour) # type: ignore
|
|
829
|
+
|
|
830
|
+
self._objs.append(_AnimObject(field, T, grid, None, actor, on_update)) # type: ignore
|
|
774
831
|
|
|
775
|
-
|
|
776
|
-
|
|
832
|
+
self._reset_cbar()
|
|
833
|
+
|
|
777
834
|
def _add_aux_items(self) -> None:
|
|
778
835
|
saved_camera = {
|
|
779
836
|
"position": self._plot.camera.position,
|
emerge/_emerge/simmodel.py
CHANGED
|
@@ -33,10 +33,10 @@ from typing import Literal, Generator, Any
|
|
|
33
33
|
from loguru import logger
|
|
34
34
|
import numpy as np
|
|
35
35
|
import gmsh # type: ignore
|
|
36
|
-
import cloudpickle
|
|
37
36
|
import os
|
|
38
37
|
import inspect
|
|
39
38
|
from pathlib import Path
|
|
39
|
+
import joblib
|
|
40
40
|
from atexit import register
|
|
41
41
|
import signal
|
|
42
42
|
from .. import __version__
|
|
@@ -120,10 +120,14 @@ class Simulation:
|
|
|
120
120
|
self._initialize_simulation()
|
|
121
121
|
|
|
122
122
|
self.set_loglevel(loglevel)
|
|
123
|
+
|
|
123
124
|
if write_log:
|
|
124
125
|
self.set_write_log()
|
|
125
126
|
|
|
126
127
|
LOG_CONTROLLER._flush_log_buffer()
|
|
128
|
+
|
|
129
|
+
LOG_CONTROLLER._sys_info()
|
|
130
|
+
|
|
127
131
|
self._update_data()
|
|
128
132
|
|
|
129
133
|
|
|
@@ -226,10 +230,18 @@ class Simulation:
|
|
|
226
230
|
|
|
227
231
|
def _set_mesh(self, mesh: Mesh3D) -> None:
|
|
228
232
|
"""Set the current model mesh to a given mesh."""
|
|
233
|
+
logger.trace(f'Setting {mesh} as model mesh')
|
|
229
234
|
self.mesh = mesh
|
|
230
235
|
self.mw.mesh = mesh
|
|
231
236
|
self.display._mesh = mesh
|
|
232
237
|
|
|
238
|
+
def _save_geometries(self) -> None:
|
|
239
|
+
"""Saves the current geometry state to the simulatin dataset
|
|
240
|
+
"""
|
|
241
|
+
logger.trace('Storing geometries in data.sim')
|
|
242
|
+
self.data.sim['geos'] = {geo.name: geo for geo in _GEOMANAGER.all_geometries()}
|
|
243
|
+
self.data.sim['mesh'] = self.mesh
|
|
244
|
+
self.data.sim.entries.append(self.data.sim.stock)
|
|
233
245
|
############################################################
|
|
234
246
|
# PUBLIC FUNCTIONS #
|
|
235
247
|
############################################################
|
|
@@ -393,9 +405,9 @@ class Simulation:
|
|
|
393
405
|
# Pack and save data
|
|
394
406
|
dataset = dict(simdata=self.data, mesh=self.mesh)
|
|
395
407
|
data_path = self.modelpath / 'simdata.emerge'
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
408
|
+
|
|
409
|
+
joblib.dump(dataset, str(data_path))
|
|
410
|
+
|
|
399
411
|
if self._cache_run:
|
|
400
412
|
cachepath = self.modelpath / 'pylines.txt'
|
|
401
413
|
with open(str(cachepath), 'w') as f_out:
|
|
@@ -418,13 +430,12 @@ class Simulation:
|
|
|
418
430
|
gmsh.model.geo.synchronize()
|
|
419
431
|
gmsh.model.occ.synchronize()
|
|
420
432
|
logger.info(f"Loaded mesh from: {mesh_path}")
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
with open(str(data_path), "rb") as f_in:
|
|
425
|
-
datapack= cloudpickle.load(f_in)
|
|
433
|
+
|
|
434
|
+
datapack = joblib.load(str(data_path))
|
|
435
|
+
|
|
426
436
|
self.data = datapack['simdata']
|
|
427
|
-
self.
|
|
437
|
+
self.activate(0)
|
|
438
|
+
|
|
428
439
|
logger.info(f"Loaded simulation data from: {data_path}")
|
|
429
440
|
|
|
430
441
|
def set_loglevel(self, loglevel: Literal['DEBUG','INFO','WARNING','ERROR']) -> None:
|
|
@@ -433,12 +444,14 @@ class Simulation:
|
|
|
433
444
|
Args:
|
|
434
445
|
loglevel ('DEBUG','INFO','WARNING','ERROR'): The loglevel
|
|
435
446
|
"""
|
|
447
|
+
logger.trace(f'Setting loglevel to {loglevel}')
|
|
436
448
|
LOG_CONTROLLER.set_std_loglevel(loglevel)
|
|
437
449
|
if loglevel not in ('TRACE','DEBUG'):
|
|
438
450
|
gmsh.option.setNumber("General.Terminal", 0)
|
|
439
451
|
|
|
440
452
|
def set_write_log(self) -> None:
|
|
441
453
|
"""Adds a file output for the logger."""
|
|
454
|
+
logger.trace(f'Writing log to path = {self.modelpath}')
|
|
442
455
|
LOG_CONTROLLER.set_write_file(self.modelpath)
|
|
443
456
|
|
|
444
457
|
def view(self,
|
|
@@ -471,17 +484,28 @@ class Simulation:
|
|
|
471
484
|
|
|
472
485
|
return None
|
|
473
486
|
|
|
474
|
-
def set_periodic_cell(self, cell: PeriodicCell
|
|
487
|
+
def set_periodic_cell(self, cell: PeriodicCell):
|
|
475
488
|
"""Set the given periodic cell object as the simulations peridicity.
|
|
476
489
|
|
|
477
490
|
Args:
|
|
478
491
|
cell (PeriodicCell): The PeriodicCell class
|
|
479
|
-
excluded_faces (list[FaceSelection], optional): Faces to exclude from the periodic boundary condition. Defaults to None.
|
|
480
492
|
"""
|
|
493
|
+
logger.trace(f'Setting {cell} as periodic cell object')
|
|
481
494
|
self.mw.bc._cell = cell
|
|
482
495
|
self._cell = cell
|
|
483
|
-
self._cell.included_faces = included_faces
|
|
484
496
|
|
|
497
|
+
def set_resolution(self, resolution: float) -> Simulation:
|
|
498
|
+
"""Sets the discretization resolution in the various physics interfaces.
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
resolution (float): The resolution as a float. Lower resolution is a finer mesh
|
|
503
|
+
|
|
504
|
+
Returns:
|
|
505
|
+
Simulation: _description_
|
|
506
|
+
"""
|
|
507
|
+
self.mw.set_resolution(resolution)
|
|
508
|
+
|
|
485
509
|
def commit_geometry(self, *geometries: GeoObject | list[GeoObject]) -> None:
|
|
486
510
|
"""Finalizes and locks the current geometry state of the simulation.
|
|
487
511
|
|
|
@@ -489,12 +513,15 @@ class Simulation:
|
|
|
489
513
|
|
|
490
514
|
"""
|
|
491
515
|
geometries_parsed: Any = None
|
|
516
|
+
logger.trace('Committing final geometry.')
|
|
492
517
|
if not geometries:
|
|
493
518
|
geometries_parsed = _GEOMANAGER.all_geometries()
|
|
494
519
|
else:
|
|
495
520
|
geometries_parsed = unpack_lists(geometries + tuple([item for item in self.data.sim.default.values() if isinstance(item, GeoObject)]))
|
|
521
|
+
logger.trace(f'Parsed geometries = {geometries_parsed}')
|
|
522
|
+
|
|
523
|
+
self._save_geometries()
|
|
496
524
|
|
|
497
|
-
self.data.sim['geos'] = {geo.name: geo for geo in geometries_parsed}
|
|
498
525
|
self.mesher.submit_objects(geometries_parsed)
|
|
499
526
|
self._defined_geometries = True
|
|
500
527
|
self.display._facetags = [dt[1] for dt in gmsh.model.get_entities(2)]
|
|
@@ -507,6 +534,19 @@ class Simulation:
|
|
|
507
534
|
"""
|
|
508
535
|
return _GEOMANAGER.all_geometries()
|
|
509
536
|
|
|
537
|
+
def activate(self, _indx: int | None = None, **variables) -> Simulation:
|
|
538
|
+
"""Searches for the permutaions of parameter sweep variables and sets the current geometry to the provided set."""
|
|
539
|
+
if _indx is not None:
|
|
540
|
+
dataset = self.data.sim.index(_indx)
|
|
541
|
+
else:
|
|
542
|
+
dataset = self.data.sim.find(**variables)
|
|
543
|
+
|
|
544
|
+
variables = ', '.join([f'{key}={value}' for key,value in dataset.vars.items()])
|
|
545
|
+
logger.info(f'Activated entry with variables: {variables}')
|
|
546
|
+
_GEOMANAGER.set_geometries(dataset['geos'])
|
|
547
|
+
self._set_mesh(dataset['mesh'])
|
|
548
|
+
return self
|
|
549
|
+
|
|
510
550
|
def generate_mesh(self) -> None:
|
|
511
551
|
"""Generate the mesh.
|
|
512
552
|
This can only be done after commit_geometry(...) is called and if frequencies are defined.
|
|
@@ -517,13 +557,15 @@ class Simulation:
|
|
|
517
557
|
Raises:
|
|
518
558
|
ValueError: ValueError if no frequencies are defined.
|
|
519
559
|
"""
|
|
560
|
+
logger.trace('Starting mesh generation phase.')
|
|
520
561
|
if not self._defined_geometries:
|
|
521
562
|
self.commit_geometry()
|
|
522
563
|
|
|
564
|
+
logger.trace(' (1) Installing periodic boundaries in mesher.')
|
|
523
565
|
# Set the cell periodicity in GMSH
|
|
524
566
|
if self._cell is not None:
|
|
525
567
|
self.mesher.set_periodic_cell(self._cell)
|
|
526
|
-
|
|
568
|
+
|
|
527
569
|
self.mw._initialize_bcs(_GEOMANAGER.get_surfaces())
|
|
528
570
|
|
|
529
571
|
# Check if frequencies are defined: TODO: Replace with a more generic check
|
|
@@ -533,25 +575,26 @@ class Simulation:
|
|
|
533
575
|
gmsh.model.occ.synchronize()
|
|
534
576
|
|
|
535
577
|
# Set the mesh size
|
|
536
|
-
self.mesher.
|
|
578
|
+
self.mesher._configure_mesh_size(self.mw.get_discretizer(), self.mw.resolution)
|
|
537
579
|
|
|
580
|
+
logger.trace(' (2) Calling GMSH mesher')
|
|
538
581
|
try:
|
|
539
582
|
gmsh.logger.start()
|
|
540
583
|
gmsh.model.mesh.generate(3)
|
|
541
584
|
logs = gmsh.logger.get()
|
|
542
585
|
gmsh.logger.stop()
|
|
543
586
|
for log in logs:
|
|
544
|
-
logger.trace('[GMSH] '+log)
|
|
587
|
+
logger.trace('[GMSH] ' + log)
|
|
545
588
|
except Exception:
|
|
546
589
|
logger.error('GMSH Mesh error detected.')
|
|
547
590
|
print(_GMSH_ERROR_TEXT)
|
|
548
591
|
raise
|
|
549
|
-
|
|
592
|
+
logger.info('GMSH Meshing complete!')
|
|
550
593
|
self.mesh._pre_update(self.mesher._get_periodic_bcs())
|
|
551
594
|
self.mesh.exterior_face_tags = self.mesher.domain_boundary_face_tags
|
|
552
595
|
gmsh.model.occ.synchronize()
|
|
553
|
-
|
|
554
596
|
self._set_mesh(self.mesh)
|
|
597
|
+
logger.trace(' (3) Mesh routine complete')
|
|
555
598
|
|
|
556
599
|
def parameter_sweep(self, clear_mesh: bool = True, **parameters: np.ndarray) -> Generator[tuple[float,...], None, None]:
|
|
557
600
|
"""Executes a parameteric sweep iteration.
|
|
@@ -579,9 +622,13 @@ class Simulation:
|
|
|
579
622
|
paramlist = sorted(list(parameters.keys()))
|
|
580
623
|
dims = np.meshgrid(*[parameters[key] for key in paramlist], indexing='ij')
|
|
581
624
|
dims_flat = [dim.flatten() for dim in dims]
|
|
625
|
+
|
|
582
626
|
self.mw.cache_matrices = False
|
|
627
|
+
logger.trace('Starting parameter sweep.')
|
|
628
|
+
|
|
583
629
|
for i_iter in range(dims_flat[0].shape[0]):
|
|
584
|
-
|
|
630
|
+
|
|
631
|
+
if clear_mesh and i_iter > 0:
|
|
585
632
|
logger.info('Cleaning up mesh.')
|
|
586
633
|
gmsh.clear()
|
|
587
634
|
mesh = Mesh3D(self.mesher)
|
|
@@ -592,12 +639,19 @@ class Simulation:
|
|
|
592
639
|
params = {key: dim[i_iter] for key,dim in zip(paramlist, dims_flat)}
|
|
593
640
|
self.mw._params = params
|
|
594
641
|
self.data.sim.new(**params)
|
|
595
|
-
|
|
642
|
+
|
|
596
643
|
logger.info(f'Iterating: {params}')
|
|
597
644
|
if len(dims_flat)==1:
|
|
598
645
|
yield dims_flat[0][i_iter]
|
|
599
646
|
else:
|
|
600
647
|
yield (dim[i_iter] for dim in dims_flat) # type: ignore
|
|
648
|
+
|
|
649
|
+
if not clear_mesh:
|
|
650
|
+
self._save_geometries()
|
|
651
|
+
|
|
652
|
+
if not clear_mesh:
|
|
653
|
+
self._save_geometries()
|
|
654
|
+
|
|
601
655
|
self.mw.cache_matrices = True
|
|
602
656
|
|
|
603
657
|
def export(self, filename: str):
|
|
@@ -613,6 +667,7 @@ class Simulation:
|
|
|
613
667
|
Args:
|
|
614
668
|
filename (str): The filename
|
|
615
669
|
"""
|
|
670
|
+
logger.trace(f'Writing geometry to {filename}')
|
|
616
671
|
gmsh.write(filename)
|
|
617
672
|
|
|
618
673
|
def set_solver(self, solver: EMSolver | Solver):
|
|
@@ -622,6 +677,7 @@ class Simulation:
|
|
|
622
677
|
Args:
|
|
623
678
|
solver (EMSolver | Solver): The solver objects
|
|
624
679
|
"""
|
|
680
|
+
logger.trace(f'Setting solver to {solver}')
|
|
625
681
|
self.mw.solveroutine.set_solver(solver)
|
|
626
682
|
|
|
627
683
|
############################################################
|
|
@@ -159,7 +159,7 @@ class DataEntry:
|
|
|
159
159
|
return all(self.vars[key]==other[key] for key in allkeys)
|
|
160
160
|
|
|
161
161
|
def _dist(self, other: dict[str, float]) -> float:
|
|
162
|
-
return sum([(abs(self.vars.get(key,1e20)-other[key])/other[key]) for key in other.keys()])
|
|
162
|
+
return sum([(abs(self.vars.get(key,1e20)-other[key])/(other[key]+1e-12)) for key in other.keys()])
|
|
163
163
|
|
|
164
164
|
def __getitem__(self, key) -> Any:
|
|
165
165
|
return self.data[key]
|
|
@@ -191,6 +191,11 @@ class DataContainer:
|
|
|
191
191
|
for entry in self.entries:
|
|
192
192
|
yield entry.vars, entry.data
|
|
193
193
|
|
|
194
|
+
@property
|
|
195
|
+
def first(self) -> DataEntry:
|
|
196
|
+
"""Returns the first added entry"""
|
|
197
|
+
return self.entries[0]
|
|
198
|
+
|
|
194
199
|
@property
|
|
195
200
|
def last(self) -> DataEntry:
|
|
196
201
|
"""Returns the last added entry"""
|
|
@@ -203,7 +208,11 @@ class DataContainer:
|
|
|
203
208
|
return self.stock
|
|
204
209
|
else:
|
|
205
210
|
return self.last
|
|
206
|
-
|
|
211
|
+
|
|
212
|
+
def index(self, index: int) -> DataEntry:
|
|
213
|
+
"""Returns the last added entry"""
|
|
214
|
+
return self.entries[index]
|
|
215
|
+
|
|
207
216
|
def select(self, **vars: float) -> DataEntry | None:
|
|
208
217
|
"""Returns the data entry corresponding to the provided parametric sweep set"""
|
|
209
218
|
for entry in self.entries:
|
|
@@ -318,7 +327,11 @@ class BaseDataset(Generic[T,M]):
|
|
|
318
327
|
for i, var_map in enumerate(self._variables):
|
|
319
328
|
error = sum([abs(var_map.get(k, 1e30) - v) for k, v in variables.items()])
|
|
320
329
|
output.append((i,error))
|
|
321
|
-
|
|
330
|
+
selection_id = sorted(output, key=lambda x:x[1])[0][0]
|
|
331
|
+
entry = self.get_entry(selection_id)
|
|
332
|
+
variables = ', '.join([f'{key}={value}' for key,value in self._variables[selection_id].items()])
|
|
333
|
+
logger.info(f'Selected entry: {variables}')
|
|
334
|
+
return entry
|
|
322
335
|
|
|
323
336
|
def axis(self, name: str) -> np.ndarray:
|
|
324
337
|
"""Returns a sorted list of all variables for the given name
|
|
@@ -343,7 +356,8 @@ class BaseDataset(Generic[T,M]):
|
|
|
343
356
|
return new_entry
|
|
344
357
|
|
|
345
358
|
def _grid_axes(self) -> bool:
|
|
346
|
-
"""This method attepmts to create a gritted version of the scalar dataset
|
|
359
|
+
"""This method attepmts to create a gritted version of the scalar dataset. It may fail
|
|
360
|
+
if the data in the dataset cannot be cast into a gridded structure.
|
|
347
361
|
|
|
348
362
|
Returns:
|
|
349
363
|
None
|
|
@@ -353,9 +367,11 @@ class BaseDataset(Generic[T,M]):
|
|
|
353
367
|
for var in self._variables:
|
|
354
368
|
for key, value in var.items():
|
|
355
369
|
variables[key].add(value)
|
|
370
|
+
|
|
356
371
|
N_entries = len(self._variables)
|
|
357
372
|
N_prod = 1
|
|
358
373
|
N_dim = len(variables)
|
|
374
|
+
|
|
359
375
|
for key, val_list in variables.items():
|
|
360
376
|
N_prod *= len(val_list)
|
|
361
377
|
|
|
@@ -369,8 +385,10 @@ class BaseDataset(Generic[T,M]):
|
|
|
369
385
|
|
|
370
386
|
self._axes = dict()
|
|
371
387
|
self._ax_ids = dict()
|
|
388
|
+
|
|
372
389
|
revax = dict()
|
|
373
390
|
i = 0
|
|
391
|
+
|
|
374
392
|
for key, val_set in variables.items():
|
|
375
393
|
self._axes[key] = np.sort(np.array(list(val_set)))
|
|
376
394
|
self._ax_ids[key] = i
|