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.

Files changed (30) hide show
  1. emerge/__init__.py +7 -3
  2. emerge/_emerge/elements/femdata.py +5 -1
  3. emerge/_emerge/elements/ned2_interp.py +73 -30
  4. emerge/_emerge/elements/nedelec2.py +1 -0
  5. emerge/_emerge/emerge_update.py +63 -0
  6. emerge/_emerge/geo/operations.py +6 -3
  7. emerge/_emerge/geo/polybased.py +37 -5
  8. emerge/_emerge/geometry.py +5 -0
  9. emerge/_emerge/logsettings.py +26 -1
  10. emerge/_emerge/material.py +29 -8
  11. emerge/_emerge/mesh3d.py +16 -13
  12. emerge/_emerge/mesher.py +70 -3
  13. emerge/_emerge/physics/microwave/assembly/assembler.py +5 -4
  14. emerge/_emerge/physics/microwave/assembly/curlcurl.py +0 -1
  15. emerge/_emerge/physics/microwave/assembly/generalized_eigen.py +1 -2
  16. emerge/_emerge/physics/microwave/assembly/generalized_eigen_hb.py +1 -1
  17. emerge/_emerge/physics/microwave/assembly/robin_abc_order2.py +0 -1
  18. emerge/_emerge/physics/microwave/microwave_3d.py +37 -16
  19. emerge/_emerge/physics/microwave/microwave_bc.py +6 -4
  20. emerge/_emerge/physics/microwave/microwave_data.py +14 -11
  21. emerge/_emerge/plot/pyvista/cmap_maker.py +70 -0
  22. emerge/_emerge/plot/pyvista/display.py +93 -36
  23. emerge/_emerge/simmodel.py +77 -21
  24. emerge/_emerge/simulation_data.py +22 -4
  25. emerge/_emerge/solver.py +67 -32
  26. {emerge-1.0.3.dist-info → emerge-1.0.5.dist-info}/METADATA +2 -3
  27. {emerge-1.0.3.dist-info → emerge-1.0.5.dist-info}/RECORD +30 -28
  28. {emerge-1.0.3.dist-info → emerge-1.0.5.dist-info}/WHEEL +0 -0
  29. {emerge-1.0.3.dist-info → emerge-1.0.5.dist-info}/entry_points.txt +0 -0
  30. {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
- # build a linear grid of data‐values (not strictly needed for pure colormap)
79
- vmin, vmax = mesh['values'].min(), mesh['values'].max()
80
- mapping = np.linspace(vmin, vmax, N)
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
- # prepare output
83
- newcolors = np.empty((N, 4))
86
+ # # prepare output
87
+ # newcolors = np.empty((N, 4))
84
88
 
85
- # normalized positions of control points: start, middle, end
86
- control_pos = np.array([0.0, 0.25, 0.5, 0.75, 1]) * (vmax - vmin) + vmin
87
- # stack control colors
88
- controls = np.vstack([col1, col2, col3, col4, col5])
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
- # interp each RGBA channel independently
91
- for chan in range(4):
92
- newcolors[:, chan] = np.interp(mapping, control_pos, controls[:, chan])
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
- return ListedColormap(newcolors)
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 = 'viridis',
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
- fmin = np.nanmin(static_field)
641
- fmax = np.nanmax(static_field)
642
- clim = (fmin, fmax)
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=None, **kwargs)
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
- cmap: str = 'viridis'):
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 = max(np.abs(vmin),np.abs(vmax))
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
- actor = self._plot.add_mesh(contour, opacity=0.25, cmap=cmap, pickable=False)
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
- self._objs.append(_AnimObject(field, lambda x: x, grid, actor, on_update)) # type: ignore
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,
@@ -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
- with open(str(data_path), "wb") as f_out:
397
- cloudpickle.dump(dataset, f_out)
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
- #self.mesh.update([])
422
-
423
- # Load data
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._set_mesh(datapack['mesh'])
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, included_faces: FaceSelection | None = None):
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.set_mesh_size(self.mw.get_discretizer(), self.mw.resolution)
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
- if clear_mesh:
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
- return self.get_entry(sorted(output, key=lambda x:x[1])[0][0])
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