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.

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 +2 -1
  7. emerge/_emerge/geo/polybased.py +26 -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 +15 -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 +101 -37
  23. emerge/_emerge/simmodel.py +75 -21
  24. emerge/_emerge/simulation_data.py +22 -4
  25. emerge/_emerge/solver.py +78 -51
  26. {emerge-1.0.2.dist-info → emerge-1.0.4.dist-info}/METADATA +2 -3
  27. {emerge-1.0.2.dist-info → emerge-1.0.4.dist-info}/RECORD +30 -28
  28. {emerge-1.0.2.dist-info → emerge-1.0.4.dist-info}/WHEEL +0 -0
  29. {emerge-1.0.2.dist-info → emerge-1.0.4.dist-info}/entry_points.txt +0 -0
  30. {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
- # 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
@@ -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 = 'viridis',
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
- fmin = np.nanmin(static_field)
636
- fmax = np.nanmax(static_field)
637
- 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
+
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
- self._objs.append(_AnimObject(field_flat, T, grid_no_nan, actor, on_update))
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=None, **kwargs)
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
- cmap: str = 'viridis'):
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 = max(np.abs(vmin),np.abs(vmax))
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
- actor = self._plot.add_mesh(contour, opacity=0.25, cmap=cmap, pickable=False)
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
- self._objs.append(_AnimObject(field, lambda x: x, grid, actor, on_update)) # type: ignore
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,
@@ -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
- with open(str(data_path), "wb") as f_out:
397
- cloudpickle.dump(dataset, f_out)
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
- #self.mesh.update([])
422
-
423
- # Load data
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._set_mesh(datapack['mesh'])
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, included_faces: FaceSelection | None = None):
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.set_mesh_size(self.mw.get_discretizer(), self.mw.resolution)
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
- if clear_mesh:
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
- 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