emerge 1.0.2__py3-none-any.whl → 1.0.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 +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 +2 -1
- emerge/_emerge/geo/polybased.py +26 -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 +15 -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 +101 -37
- emerge/_emerge/simmodel.py +75 -21
- emerge/_emerge/simulation_data.py +22 -4
- emerge/_emerge/solver.py +78 -51
- {emerge-1.0.2.dist-info → emerge-1.0.4.dist-info}/METADATA +2 -3
- {emerge-1.0.2.dist-info → emerge-1.0.4.dist-info}/RECORD +30 -28
- {emerge-1.0.2.dist-info → emerge-1.0.4.dist-info}/WHEEL +0 -0
- {emerge-1.0.2.dist-info → emerge-1.0.4.dist-info}/entry_points.txt +0 -0
- {emerge-1.0.2.dist-info → emerge-1.0.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
|
@@ -213,11 +217,13 @@ class _AnimObject:
|
|
|
213
217
|
field: np.ndarray,
|
|
214
218
|
T: Callable,
|
|
215
219
|
grid: pv.Grid,
|
|
220
|
+
filtered_grid: pv.Grid,
|
|
216
221
|
actor: pv.Actor,
|
|
217
222
|
on_update: Callable):
|
|
218
223
|
self.field: np.ndarray = field
|
|
219
224
|
self.T: Callable = T
|
|
220
225
|
self.grid: pv.Grid = grid
|
|
226
|
+
self.fgrid: pv.Grid = filtered_grid
|
|
221
227
|
self.actor: pv.Actor = actor
|
|
222
228
|
self.on_update: Callable = on_update
|
|
223
229
|
|
|
@@ -250,8 +256,20 @@ class PVDisplay(BaseDisplay):
|
|
|
250
256
|
|
|
251
257
|
self._ctr: int = 0
|
|
252
258
|
|
|
259
|
+
self._cbar_args: dict = {}
|
|
260
|
+
self._cbar_lim: tuple[float, float] | None = None
|
|
253
261
|
self.camera_position = (1, -1, 1) # +X, +Z, -Y
|
|
254
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
|
+
|
|
255
273
|
def _wire_close_events(self):
|
|
256
274
|
self._closed = False
|
|
257
275
|
|
|
@@ -376,6 +394,7 @@ class PVDisplay(BaseDisplay):
|
|
|
376
394
|
>>> display.animate().surf(...)
|
|
377
395
|
>>> display.show()
|
|
378
396
|
"""
|
|
397
|
+
print('If you closed the animation without using (Q) press Ctrl+C to kill the process.')
|
|
379
398
|
self._Nsteps = Nsteps
|
|
380
399
|
self._fps = fps
|
|
381
400
|
self._do_animate = True
|
|
@@ -571,7 +590,6 @@ class PVDisplay(BaseDisplay):
|
|
|
571
590
|
else:
|
|
572
591
|
F = np.real(F.T)
|
|
573
592
|
Fnorm = np.sqrt(Fx.real**2 + Fy.real**2 + Fz.real**2).T
|
|
574
|
-
|
|
575
593
|
if XYZ is not None:
|
|
576
594
|
grid = pv.StructuredGrid(X,Y,Z)
|
|
577
595
|
self.add_surf(X,Y,Z,Fnorm, _fieldname = 'portfield')
|
|
@@ -586,7 +604,7 @@ class PVDisplay(BaseDisplay):
|
|
|
586
604
|
z: np.ndarray,
|
|
587
605
|
field: np.ndarray,
|
|
588
606
|
scale: Literal['lin','log','symlog'] = 'lin',
|
|
589
|
-
cmap: cmap_names =
|
|
607
|
+
cmap: cmap_names | None = None,
|
|
590
608
|
clim: tuple[float, float] | None = None,
|
|
591
609
|
opacity: float = 1.0,
|
|
592
610
|
symmetrize: bool = False,
|
|
@@ -611,8 +629,7 @@ class PVDisplay(BaseDisplay):
|
|
|
611
629
|
|
|
612
630
|
grid = pv.StructuredGrid(x,y,z)
|
|
613
631
|
field_flat = field.flatten(order='F')
|
|
614
|
-
|
|
615
|
-
|
|
632
|
+
|
|
616
633
|
if scale=='log':
|
|
617
634
|
T = lambda x: np.log10(np.abs(x+1e-12))
|
|
618
635
|
elif scale=='symlog':
|
|
@@ -621,35 +638,49 @@ class PVDisplay(BaseDisplay):
|
|
|
621
638
|
T = lambda x: x
|
|
622
639
|
|
|
623
640
|
static_field = T(np.real(field_flat))
|
|
641
|
+
|
|
624
642
|
if _fieldname is None:
|
|
625
643
|
name = 'anim'+str(self._ctr)
|
|
626
644
|
else:
|
|
627
645
|
name = _fieldname
|
|
628
646
|
self._ctr += 1
|
|
647
|
+
|
|
629
648
|
grid[name] = static_field
|
|
630
649
|
|
|
631
650
|
grid_no_nan = grid.threshold(scalars=name)
|
|
632
|
-
|
|
651
|
+
|
|
652
|
+
default_cmap = EMERGE_AMP
|
|
633
653
|
# Determine color limits
|
|
634
654
|
if clim is None:
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
+
|
|
638
662
|
if symmetrize:
|
|
639
663
|
lim = max(abs(clim[0]), abs(clim[1]))
|
|
640
664
|
clim = (-lim, lim)
|
|
641
|
-
|
|
665
|
+
default_cmap = EMERGE_WAVE
|
|
666
|
+
|
|
667
|
+
if cmap is None:
|
|
668
|
+
cmap = default_cmap
|
|
669
|
+
|
|
642
670
|
kwargs = setdefault(kwargs, cmap=cmap, clim=clim, opacity=opacity, pickable=False, multi_colors=True)
|
|
643
|
-
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)
|
|
644
672
|
|
|
645
673
|
|
|
646
674
|
if self._do_animate:
|
|
647
675
|
def on_update(obj: _AnimObject, phi: complex):
|
|
648
676
|
field_anim = obj.T(np.real(obj.field * phi))
|
|
649
677
|
obj.grid[name] = field_anim
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
678
|
+
obj.fgrid[name] = obj.grid.threshold(scalars=name)[name]
|
|
679
|
+
#obj.fgrid replace with thresholded scalar data.
|
|
680
|
+
self._objs.append(_AnimObject(field_flat, T, grid, grid_no_nan, actor, on_update))
|
|
681
|
+
|
|
682
|
+
self._reset_cbar()
|
|
683
|
+
|
|
653
684
|
def add_title(self, title: str) -> None:
|
|
654
685
|
"""Adds a title
|
|
655
686
|
|
|
@@ -682,6 +713,7 @@ class PVDisplay(BaseDisplay):
|
|
|
682
713
|
dx: np.ndarray, dy: np.ndarray, dz: np.ndarray,
|
|
683
714
|
scale: float = 1,
|
|
684
715
|
color: tuple[float, float, float] | None = None,
|
|
716
|
+
cmap: cmap_names | None = None,
|
|
685
717
|
scalemode: Literal['lin','log'] = 'lin'):
|
|
686
718
|
"""Add a quiver plot to the display
|
|
687
719
|
|
|
@@ -704,6 +736,8 @@ class PVDisplay(BaseDisplay):
|
|
|
704
736
|
|
|
705
737
|
ids = np.invert(np.isnan(dx))
|
|
706
738
|
|
|
739
|
+
if cmap is None:
|
|
740
|
+
cmap = EMERGE_AMP
|
|
707
741
|
x, y, z, dx, dy, dz = x[ids], y[ids], z[ids], dx[ids], dy[ids], dz[ids]
|
|
708
742
|
|
|
709
743
|
dmin = _min_distance(x,y,z)
|
|
@@ -722,8 +756,8 @@ class PVDisplay(BaseDisplay):
|
|
|
722
756
|
if color is not None:
|
|
723
757
|
kwargs['color'] = color
|
|
724
758
|
|
|
725
|
-
pl = self._plot.add_arrows(Coo, Vec, scalars=None, clim=None, cmap=
|
|
726
|
-
|
|
759
|
+
pl = self._plot.add_arrows(Coo, Vec, scalars=None, clim=None, cmap=cmap, **kwargs)
|
|
760
|
+
self._reset_cbar()
|
|
727
761
|
|
|
728
762
|
def add_contour(self,
|
|
729
763
|
X: np.ndarray,
|
|
@@ -731,8 +765,11 @@ class PVDisplay(BaseDisplay):
|
|
|
731
765
|
Z: np.ndarray,
|
|
732
766
|
V: np.ndarray,
|
|
733
767
|
Nlevels: int = 5,
|
|
768
|
+
scale: Literal['lin','log','symlog'] = 'lin',
|
|
734
769
|
symmetrize: bool = True,
|
|
735
|
-
|
|
770
|
+
clim: tuple[float, float] | None = None,
|
|
771
|
+
cmap: cmap_names | None = None,
|
|
772
|
+
opacity: float = 0.25):
|
|
736
773
|
"""Adds a 3D volumetric contourplot based on a 3D grid of X,Y,Z and field values
|
|
737
774
|
|
|
738
775
|
|
|
@@ -746,27 +783,54 @@ class PVDisplay(BaseDisplay):
|
|
|
746
783
|
cmap (str, optional): The color map. Defaults to 'viridis'.
|
|
747
784
|
"""
|
|
748
785
|
Vf = V.flatten()
|
|
786
|
+
Vf = np.nan_to_num(Vf)
|
|
749
787
|
vmin = np.min(np.real(Vf))
|
|
750
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
|
+
|
|
751
799
|
if symmetrize:
|
|
752
|
-
level =
|
|
800
|
+
level = np.max(np.abs(Vf))
|
|
753
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
|
+
|
|
754
814
|
grid = pv.StructuredGrid(X,Y,Z)
|
|
755
815
|
field = V.flatten(order='F')
|
|
756
|
-
grid['anim'] = np.real(field)
|
|
816
|
+
grid['anim'] = T(np.real(field))
|
|
817
|
+
|
|
757
818
|
levels = list(np.linspace(vmin, vmax, Nlevels))
|
|
758
819
|
contour = grid.contour(isosurfaces=levels)
|
|
759
|
-
|
|
760
|
-
|
|
820
|
+
|
|
821
|
+
actor = self._plot.add_mesh(contour, opacity=opacity, cmap=cmap, clim=clim, pickable=False, scalar_bar_args=self._cbar_args)
|
|
822
|
+
|
|
761
823
|
if self._do_animate:
|
|
762
824
|
def on_update(obj: _AnimObject, phi: complex):
|
|
763
|
-
new_vals = np.real(obj.field * phi)
|
|
825
|
+
new_vals = obj.T(np.real(obj.field * phi))
|
|
764
826
|
obj.grid['anim'] = new_vals
|
|
765
827
|
new_contour = obj.grid.contour(isosurfaces=levels)
|
|
766
828
|
obj.actor.GetMapper().SetInputData(new_contour) # type: ignore
|
|
829
|
+
|
|
830
|
+
self._objs.append(_AnimObject(field, T, grid, None, actor, on_update)) # type: ignore
|
|
767
831
|
|
|
768
|
-
|
|
769
|
-
|
|
832
|
+
self._reset_cbar()
|
|
833
|
+
|
|
770
834
|
def _add_aux_items(self) -> None:
|
|
771
835
|
saved_camera = {
|
|
772
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,17 @@ 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
|
|
233
244
|
############################################################
|
|
234
245
|
# PUBLIC FUNCTIONS #
|
|
235
246
|
############################################################
|
|
@@ -393,9 +404,9 @@ class Simulation:
|
|
|
393
404
|
# Pack and save data
|
|
394
405
|
dataset = dict(simdata=self.data, mesh=self.mesh)
|
|
395
406
|
data_path = self.modelpath / 'simdata.emerge'
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
407
|
+
|
|
408
|
+
joblib.dump(dataset, str(data_path))
|
|
409
|
+
|
|
399
410
|
if self._cache_run:
|
|
400
411
|
cachepath = self.modelpath / 'pylines.txt'
|
|
401
412
|
with open(str(cachepath), 'w') as f_out:
|
|
@@ -418,13 +429,12 @@ class Simulation:
|
|
|
418
429
|
gmsh.model.geo.synchronize()
|
|
419
430
|
gmsh.model.occ.synchronize()
|
|
420
431
|
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)
|
|
432
|
+
|
|
433
|
+
datapack = joblib.load(str(data_path))
|
|
434
|
+
|
|
426
435
|
self.data = datapack['simdata']
|
|
427
|
-
self.
|
|
436
|
+
self.activate(0)
|
|
437
|
+
|
|
428
438
|
logger.info(f"Loaded simulation data from: {data_path}")
|
|
429
439
|
|
|
430
440
|
def set_loglevel(self, loglevel: Literal['DEBUG','INFO','WARNING','ERROR']) -> None:
|
|
@@ -433,12 +443,14 @@ class Simulation:
|
|
|
433
443
|
Args:
|
|
434
444
|
loglevel ('DEBUG','INFO','WARNING','ERROR'): The loglevel
|
|
435
445
|
"""
|
|
446
|
+
logger.trace(f'Setting loglevel to {loglevel}')
|
|
436
447
|
LOG_CONTROLLER.set_std_loglevel(loglevel)
|
|
437
448
|
if loglevel not in ('TRACE','DEBUG'):
|
|
438
449
|
gmsh.option.setNumber("General.Terminal", 0)
|
|
439
450
|
|
|
440
451
|
def set_write_log(self) -> None:
|
|
441
452
|
"""Adds a file output for the logger."""
|
|
453
|
+
logger.trace(f'Writing log to path = {self.modelpath}')
|
|
442
454
|
LOG_CONTROLLER.set_write_file(self.modelpath)
|
|
443
455
|
|
|
444
456
|
def view(self,
|
|
@@ -471,17 +483,28 @@ class Simulation:
|
|
|
471
483
|
|
|
472
484
|
return None
|
|
473
485
|
|
|
474
|
-
def set_periodic_cell(self, cell: PeriodicCell
|
|
486
|
+
def set_periodic_cell(self, cell: PeriodicCell):
|
|
475
487
|
"""Set the given periodic cell object as the simulations peridicity.
|
|
476
488
|
|
|
477
489
|
Args:
|
|
478
490
|
cell (PeriodicCell): The PeriodicCell class
|
|
479
|
-
excluded_faces (list[FaceSelection], optional): Faces to exclude from the periodic boundary condition. Defaults to None.
|
|
480
491
|
"""
|
|
492
|
+
logger.trace(f'Setting {cell} as periodic cell object')
|
|
481
493
|
self.mw.bc._cell = cell
|
|
482
494
|
self._cell = cell
|
|
483
|
-
self._cell.included_faces = included_faces
|
|
484
495
|
|
|
496
|
+
def set_resolution(self, resolution: float) -> Simulation:
|
|
497
|
+
"""Sets the discretization resolution in the various physics interfaces.
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
Args:
|
|
501
|
+
resolution (float): The resolution as a float. Lower resolution is a finer mesh
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
Simulation: _description_
|
|
505
|
+
"""
|
|
506
|
+
self.mw.set_resolution(resolution)
|
|
507
|
+
|
|
485
508
|
def commit_geometry(self, *geometries: GeoObject | list[GeoObject]) -> None:
|
|
486
509
|
"""Finalizes and locks the current geometry state of the simulation.
|
|
487
510
|
|
|
@@ -489,12 +512,14 @@ class Simulation:
|
|
|
489
512
|
|
|
490
513
|
"""
|
|
491
514
|
geometries_parsed: Any = None
|
|
515
|
+
logger.trace('Committing final geometry.')
|
|
492
516
|
if not geometries:
|
|
493
517
|
geometries_parsed = _GEOMANAGER.all_geometries()
|
|
494
518
|
else:
|
|
495
519
|
geometries_parsed = unpack_lists(geometries + tuple([item for item in self.data.sim.default.values() if isinstance(item, GeoObject)]))
|
|
520
|
+
logger.trace(f'Parsed geometries = {geometries_parsed}')
|
|
521
|
+
self._save_geometries()
|
|
496
522
|
|
|
497
|
-
self.data.sim['geos'] = {geo.name: geo for geo in geometries_parsed}
|
|
498
523
|
self.mesher.submit_objects(geometries_parsed)
|
|
499
524
|
self._defined_geometries = True
|
|
500
525
|
self.display._facetags = [dt[1] for dt in gmsh.model.get_entities(2)]
|
|
@@ -507,6 +532,19 @@ class Simulation:
|
|
|
507
532
|
"""
|
|
508
533
|
return _GEOMANAGER.all_geometries()
|
|
509
534
|
|
|
535
|
+
def activate(self, _indx: int | None = None, **variables) -> Simulation:
|
|
536
|
+
"""Searches for the permutaions of parameter sweep variables and sets the current geometry to the provided set."""
|
|
537
|
+
if _indx is not None:
|
|
538
|
+
dataset = self.data.sim.index(_indx)
|
|
539
|
+
else:
|
|
540
|
+
dataset = self.data.sim.find(**variables)
|
|
541
|
+
|
|
542
|
+
variables = ', '.join([f'{key}={value}' for key,value in dataset.vars.items()])
|
|
543
|
+
logger.info(f'Activated entry with variables: {variables}')
|
|
544
|
+
_GEOMANAGER.set_geometries(dataset['geos'])
|
|
545
|
+
self._set_mesh(dataset['mesh'])
|
|
546
|
+
return self
|
|
547
|
+
|
|
510
548
|
def generate_mesh(self) -> None:
|
|
511
549
|
"""Generate the mesh.
|
|
512
550
|
This can only be done after commit_geometry(...) is called and if frequencies are defined.
|
|
@@ -517,13 +555,15 @@ class Simulation:
|
|
|
517
555
|
Raises:
|
|
518
556
|
ValueError: ValueError if no frequencies are defined.
|
|
519
557
|
"""
|
|
558
|
+
logger.trace('Starting mesh generation phase.')
|
|
520
559
|
if not self._defined_geometries:
|
|
521
560
|
self.commit_geometry()
|
|
522
561
|
|
|
562
|
+
logger.trace(' (1) Installing periodic boundaries in mesher.')
|
|
523
563
|
# Set the cell periodicity in GMSH
|
|
524
564
|
if self._cell is not None:
|
|
525
565
|
self.mesher.set_periodic_cell(self._cell)
|
|
526
|
-
|
|
566
|
+
|
|
527
567
|
self.mw._initialize_bcs(_GEOMANAGER.get_surfaces())
|
|
528
568
|
|
|
529
569
|
# Check if frequencies are defined: TODO: Replace with a more generic check
|
|
@@ -533,25 +573,26 @@ class Simulation:
|
|
|
533
573
|
gmsh.model.occ.synchronize()
|
|
534
574
|
|
|
535
575
|
# Set the mesh size
|
|
536
|
-
self.mesher.
|
|
576
|
+
self.mesher._configure_mesh_size(self.mw.get_discretizer(), self.mw.resolution)
|
|
537
577
|
|
|
578
|
+
logger.trace(' (2) Calling GMSH mesher')
|
|
538
579
|
try:
|
|
539
580
|
gmsh.logger.start()
|
|
540
581
|
gmsh.model.mesh.generate(3)
|
|
541
582
|
logs = gmsh.logger.get()
|
|
542
583
|
gmsh.logger.stop()
|
|
543
584
|
for log in logs:
|
|
544
|
-
logger.trace('[GMSH] '+log)
|
|
585
|
+
logger.trace('[GMSH] ' + log)
|
|
545
586
|
except Exception:
|
|
546
587
|
logger.error('GMSH Mesh error detected.')
|
|
547
588
|
print(_GMSH_ERROR_TEXT)
|
|
548
589
|
raise
|
|
549
|
-
|
|
590
|
+
logger.info('GMSH Meshing complete!')
|
|
550
591
|
self.mesh._pre_update(self.mesher._get_periodic_bcs())
|
|
551
592
|
self.mesh.exterior_face_tags = self.mesher.domain_boundary_face_tags
|
|
552
593
|
gmsh.model.occ.synchronize()
|
|
553
|
-
|
|
554
594
|
self._set_mesh(self.mesh)
|
|
595
|
+
logger.trace(' (3) Mesh routine complete')
|
|
555
596
|
|
|
556
597
|
def parameter_sweep(self, clear_mesh: bool = True, **parameters: np.ndarray) -> Generator[tuple[float,...], None, None]:
|
|
557
598
|
"""Executes a parameteric sweep iteration.
|
|
@@ -579,9 +620,13 @@ class Simulation:
|
|
|
579
620
|
paramlist = sorted(list(parameters.keys()))
|
|
580
621
|
dims = np.meshgrid(*[parameters[key] for key in paramlist], indexing='ij')
|
|
581
622
|
dims_flat = [dim.flatten() for dim in dims]
|
|
623
|
+
|
|
582
624
|
self.mw.cache_matrices = False
|
|
625
|
+
logger.trace('Starting parameter sweep.')
|
|
626
|
+
|
|
583
627
|
for i_iter in range(dims_flat[0].shape[0]):
|
|
584
|
-
|
|
628
|
+
|
|
629
|
+
if clear_mesh and i_iter > 0:
|
|
585
630
|
logger.info('Cleaning up mesh.')
|
|
586
631
|
gmsh.clear()
|
|
587
632
|
mesh = Mesh3D(self.mesher)
|
|
@@ -592,12 +637,19 @@ class Simulation:
|
|
|
592
637
|
params = {key: dim[i_iter] for key,dim in zip(paramlist, dims_flat)}
|
|
593
638
|
self.mw._params = params
|
|
594
639
|
self.data.sim.new(**params)
|
|
595
|
-
|
|
640
|
+
|
|
596
641
|
logger.info(f'Iterating: {params}')
|
|
597
642
|
if len(dims_flat)==1:
|
|
598
643
|
yield dims_flat[0][i_iter]
|
|
599
644
|
else:
|
|
600
645
|
yield (dim[i_iter] for dim in dims_flat) # type: ignore
|
|
646
|
+
|
|
647
|
+
if not clear_mesh:
|
|
648
|
+
self._save_geometries()
|
|
649
|
+
|
|
650
|
+
if not clear_mesh:
|
|
651
|
+
self._save_geometries()
|
|
652
|
+
|
|
601
653
|
self.mw.cache_matrices = True
|
|
602
654
|
|
|
603
655
|
def export(self, filename: str):
|
|
@@ -613,6 +665,7 @@ class Simulation:
|
|
|
613
665
|
Args:
|
|
614
666
|
filename (str): The filename
|
|
615
667
|
"""
|
|
668
|
+
logger.trace(f'Writing geometry to {filename}')
|
|
616
669
|
gmsh.write(filename)
|
|
617
670
|
|
|
618
671
|
def set_solver(self, solver: EMSolver | Solver):
|
|
@@ -622,6 +675,7 @@ class Simulation:
|
|
|
622
675
|
Args:
|
|
623
676
|
solver (EMSolver | Solver): The solver objects
|
|
624
677
|
"""
|
|
678
|
+
logger.trace(f'Setting solver to {solver}')
|
|
625
679
|
self.mw.solveroutine.set_solver(solver)
|
|
626
680
|
|
|
627
681
|
############################################################
|
|
@@ -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
|