emerge 1.0.3__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 +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 +75 -21
- emerge/_emerge/simulation_data.py +22 -4
- emerge/_emerge/solver.py +63 -32
- {emerge-1.0.3.dist-info → emerge-1.0.4.dist-info}/METADATA +2 -3
- {emerge-1.0.3.dist-info → emerge-1.0.4.dist-info}/RECORD +30 -28
- {emerge-1.0.3.dist-info → emerge-1.0.4.dist-info}/WHEEL +0 -0
- {emerge-1.0.3.dist-info → emerge-1.0.4.dist-info}/entry_points.txt +0 -0
- {emerge-1.0.3.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
|
|
@@ -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,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
|