emerge 1.0.7__py3-none-any.whl → 1.1.0__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 +14 -3
- emerge/_emerge/geo/pcb.py +132 -59
- emerge/_emerge/geo/shapes.py +11 -7
- emerge/_emerge/geometry.py +23 -8
- emerge/_emerge/logsettings.py +26 -2
- emerge/_emerge/mesh3d.py +5 -5
- emerge/_emerge/mesher.py +40 -8
- emerge/_emerge/physics/microwave/adaptive_mesh.py +113 -22
- emerge/_emerge/physics/microwave/microwave_3d.py +131 -81
- emerge/_emerge/physics/microwave/microwave_bc.py +157 -8
- emerge/_emerge/plot/pyvista/display.py +26 -16
- emerge/_emerge/settings.py +124 -6
- emerge/_emerge/simmodel.py +220 -148
- emerge/_emerge/simstate.py +106 -0
- emerge/_emerge/simulation_data.py +11 -23
- emerge/_emerge/solve_interfaces/cudss_interface.py +20 -1
- emerge/_emerge/solver.py +1 -1
- {emerge-1.0.7.dist-info → emerge-1.1.0.dist-info}/METADATA +7 -3
- {emerge-1.0.7.dist-info → emerge-1.1.0.dist-info}/RECORD +22 -21
- {emerge-1.0.7.dist-info → emerge-1.1.0.dist-info}/WHEEL +0 -0
- {emerge-1.0.7.dist-info → emerge-1.1.0.dist-info}/entry_points.txt +0 -0
- {emerge-1.0.7.dist-info → emerge-1.1.0.dist-info}/licenses/LICENSE +0 -0
emerge/_emerge/simmodel.py
CHANGED
|
@@ -16,19 +16,20 @@
|
|
|
16
16
|
# <https://www.gnu.org/licenses/>.
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
|
-
from .mesher import Mesher
|
|
20
|
-
from .geometry import GeoObject
|
|
19
|
+
from .mesher import Mesher
|
|
20
|
+
from .geometry import GeoObject
|
|
21
21
|
from .geo.modeler import Modeler
|
|
22
22
|
from .physics.microwave.microwave_3d import Microwave3D
|
|
23
23
|
from .mesh3d import Mesh3D
|
|
24
|
-
from .selection import Selector,
|
|
25
|
-
from .logsettings import LOG_CONTROLLER
|
|
26
|
-
from .plot.pyvista import PVDisplay
|
|
24
|
+
from .selection import Selector, Selection
|
|
27
25
|
from .dataset import SimulationDataset
|
|
26
|
+
from .logsettings import LOG_CONTROLLER, DEBUG_COLLECTOR
|
|
27
|
+
from .plot.pyvista import PVDisplay
|
|
28
28
|
from .periodic import PeriodicCell
|
|
29
29
|
from .cacherun import get_build_section, get_run_section
|
|
30
30
|
from .settings import DEFAULT_SETTINGS, Settings
|
|
31
31
|
from .solver import EMSolver, Solver
|
|
32
|
+
from .simstate import SimState
|
|
32
33
|
from typing import Literal, Generator, Any
|
|
33
34
|
from loguru import logger
|
|
34
35
|
import numpy as np
|
|
@@ -40,7 +41,7 @@ import joblib
|
|
|
40
41
|
from atexit import register
|
|
41
42
|
import signal
|
|
42
43
|
from .. import __version__
|
|
43
|
-
|
|
44
|
+
from .mesher import Algorithm3D
|
|
44
45
|
############################################################
|
|
45
46
|
# EXCEPTION DEFINITIONS #
|
|
46
47
|
############################################################
|
|
@@ -63,6 +64,7 @@ class VersionError(Exception):
|
|
|
63
64
|
# BASE 3D SIMULATION MODEL #
|
|
64
65
|
############################################################
|
|
65
66
|
|
|
67
|
+
|
|
66
68
|
class Simulation:
|
|
67
69
|
|
|
68
70
|
def __init__(self,
|
|
@@ -94,16 +96,12 @@ class Simulation:
|
|
|
94
96
|
self.mesher: Mesher = Mesher()
|
|
95
97
|
self.modeler: Modeler = Modeler()
|
|
96
98
|
|
|
97
|
-
self.
|
|
99
|
+
self.state: SimState = SimState()
|
|
98
100
|
self.select: Selector = Selector()
|
|
99
|
-
|
|
100
101
|
self.settings: Settings = DEFAULT_SETTINGS
|
|
101
102
|
|
|
102
103
|
## Display
|
|
103
|
-
self.display: PVDisplay = PVDisplay(self.
|
|
104
|
-
|
|
105
|
-
## Dataset
|
|
106
|
-
self.data: SimulationDataset = SimulationDataset()
|
|
104
|
+
self.display: PVDisplay = PVDisplay(self.state)
|
|
107
105
|
|
|
108
106
|
## STATES
|
|
109
107
|
self.__active: bool = False
|
|
@@ -115,7 +113,7 @@ class Simulation:
|
|
|
115
113
|
self._file_lines: str = ''
|
|
116
114
|
|
|
117
115
|
## Physics
|
|
118
|
-
self.mw: Microwave3D = Microwave3D(self.
|
|
116
|
+
self.mw: Microwave3D = Microwave3D(self.state, self.mesher, self.settings)
|
|
119
117
|
|
|
120
118
|
self._initialize_simulation()
|
|
121
119
|
|
|
@@ -125,16 +123,25 @@ class Simulation:
|
|
|
125
123
|
self.set_write_log()
|
|
126
124
|
|
|
127
125
|
LOG_CONTROLLER._flush_log_buffer()
|
|
128
|
-
|
|
129
126
|
LOG_CONTROLLER._sys_info()
|
|
130
|
-
|
|
131
|
-
self.
|
|
132
|
-
|
|
127
|
+
|
|
128
|
+
self.__post_init__()
|
|
133
129
|
|
|
134
130
|
############################################################
|
|
135
131
|
# PRIVATE FUNCTIONS #
|
|
136
132
|
############################################################
|
|
137
133
|
|
|
134
|
+
@property
|
|
135
|
+
def data(self) -> SimulationDataset:
|
|
136
|
+
return self.state.data
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def mesh(self) -> Mesh3D:
|
|
140
|
+
return self.state.mesh
|
|
141
|
+
|
|
142
|
+
def __post_init__(self):
|
|
143
|
+
pass
|
|
144
|
+
|
|
138
145
|
def __setitem__(self, name: str, value: Any) -> None:
|
|
139
146
|
"""Store data in the current data container"""
|
|
140
147
|
self.data.sim[name] = value
|
|
@@ -181,13 +188,13 @@ class Simulation:
|
|
|
181
188
|
def _initialize_simulation(self):
|
|
182
189
|
"""Initializes the Simulation data and GMSH API with proper shutdown routines.
|
|
183
190
|
"""
|
|
184
|
-
|
|
191
|
+
self.state.init(self.modelname)
|
|
185
192
|
|
|
186
193
|
# If GMSH is not yet initialized (Two simulation in a file)
|
|
187
194
|
if gmsh.isInitialized() == 0:
|
|
188
195
|
logger.debug('Initializing GMSH')
|
|
189
|
-
gmsh.initialize()
|
|
190
196
|
|
|
197
|
+
gmsh.initialize()
|
|
191
198
|
# Set an exit handler for Ctrl+C cases
|
|
192
199
|
self._install_signal_handlers()
|
|
193
200
|
|
|
@@ -200,7 +207,6 @@ class Simulation:
|
|
|
200
207
|
# Create a new GMSH model or load it
|
|
201
208
|
if not self.load_file:
|
|
202
209
|
gmsh.model.add(self.modelname)
|
|
203
|
-
self.data: SimulationDataset = SimulationDataset()
|
|
204
210
|
else:
|
|
205
211
|
self.load()
|
|
206
212
|
|
|
@@ -213,6 +219,11 @@ class Simulation:
|
|
|
213
219
|
if not self.__active:
|
|
214
220
|
return
|
|
215
221
|
logger.debug('Exiting program')
|
|
222
|
+
|
|
223
|
+
if DEBUG_COLLECTOR.any_warnings:
|
|
224
|
+
logger.warning('EMerge simulation warnings:')
|
|
225
|
+
for i, report in DEBUG_COLLECTOR.all_reports():
|
|
226
|
+
logger.warning(f'{i}: {report}')
|
|
216
227
|
# Save the file first
|
|
217
228
|
if self.save_file:
|
|
218
229
|
self.save()
|
|
@@ -223,25 +234,8 @@ class Simulation:
|
|
|
223
234
|
logger.debug('GMSH Shut down successful')
|
|
224
235
|
# set the state to active
|
|
225
236
|
self.__active = False
|
|
226
|
-
|
|
227
|
-
def _update_data(self) -> None:
|
|
228
|
-
"""Writes the stored physics data to each phyics class insatnce"""
|
|
229
|
-
self.mw.data = self.data.mw
|
|
230
|
-
|
|
231
|
-
def _set_mesh(self, mesh: Mesh3D) -> None:
|
|
232
|
-
"""Set the current model mesh to a given mesh."""
|
|
233
|
-
logger.trace(f'Setting {mesh} as model mesh')
|
|
234
|
-
self.mesh = mesh
|
|
235
|
-
self.mw.mesh = mesh
|
|
236
|
-
self.display._mesh = mesh
|
|
237
|
+
|
|
237
238
|
|
|
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)
|
|
245
239
|
############################################################
|
|
246
240
|
# PUBLIC FUNCTIONS #
|
|
247
241
|
############################################################
|
|
@@ -382,6 +376,11 @@ class Simulation:
|
|
|
382
376
|
|
|
383
377
|
raise VersionError(msg)
|
|
384
378
|
|
|
379
|
+
def activate(self, _indx: int | None = None, **variables) -> Simulation:
|
|
380
|
+
"""Searches for the permutaions of parameter sweep variables and sets the current geometry to the provided set."""
|
|
381
|
+
self.state.activate(_indx, **variables)
|
|
382
|
+
return self
|
|
383
|
+
|
|
385
384
|
def save(self) -> None:
|
|
386
385
|
"""Saves the current model in the provided project directory."""
|
|
387
386
|
# Ensure directory exists
|
|
@@ -403,7 +402,7 @@ class Simulation:
|
|
|
403
402
|
logger.info(f"Saved mesh to: {mesh_path}")
|
|
404
403
|
|
|
405
404
|
# Pack and save data
|
|
406
|
-
dataset =
|
|
405
|
+
dataset = self.state.get_dataset()
|
|
407
406
|
data_path = self.modelpath / 'simdata.emerge'
|
|
408
407
|
|
|
409
408
|
joblib.dump(dataset, str(data_path))
|
|
@@ -424,17 +423,16 @@ class Simulation:
|
|
|
424
423
|
if not mesh_path.exists() or not data_path.exists():
|
|
425
424
|
raise FileNotFoundError("Missing required mesh or data file.")
|
|
426
425
|
|
|
427
|
-
# Load
|
|
426
|
+
# Load GMSH Mesh (Ideally Id remove)
|
|
428
427
|
gmsh.open(str(brep_path))
|
|
429
428
|
gmsh.merge(str(mesh_path))
|
|
430
429
|
gmsh.model.geo.synchronize()
|
|
431
430
|
gmsh.model.occ.synchronize()
|
|
432
|
-
logger.info(f"Loaded mesh from: {mesh_path}")
|
|
433
431
|
|
|
432
|
+
logger.info(f"Loaded mesh from: {mesh_path}")
|
|
434
433
|
datapack = joblib.load(str(data_path))
|
|
435
|
-
|
|
436
|
-
self.
|
|
437
|
-
self.activate(0)
|
|
434
|
+
self.state.load_dataset(datapack)
|
|
435
|
+
self.state.activate(0)
|
|
438
436
|
|
|
439
437
|
logger.info(f"Loaded simulation data from: {data_path}")
|
|
440
438
|
|
|
@@ -446,7 +444,7 @@ class Simulation:
|
|
|
446
444
|
"""
|
|
447
445
|
logger.trace(f'Setting loglevel to {loglevel}')
|
|
448
446
|
LOG_CONTROLLER.set_std_loglevel(loglevel)
|
|
449
|
-
if loglevel not in ('TRACE'
|
|
447
|
+
if loglevel not in ('TRACE'):
|
|
450
448
|
gmsh.option.setNumber("General.Terminal", 0)
|
|
451
449
|
|
|
452
450
|
def set_write_log(self) -> None:
|
|
@@ -476,7 +474,7 @@ class Simulation:
|
|
|
476
474
|
gmsh.model.occ.synchronize()
|
|
477
475
|
gmsh.fltk.run()
|
|
478
476
|
return
|
|
479
|
-
for geo in
|
|
477
|
+
for geo in self.state.current_geo_state:
|
|
480
478
|
self.display.add_object(geo, mesh=plot_mesh, opacity=opacity, volume_mesh=volume_mesh, label=labels)
|
|
481
479
|
if selections:
|
|
482
480
|
[self.display.add_object(sel, color='red', opacity=0.6, label=labels) for sel in selections]
|
|
@@ -512,17 +510,13 @@ class Simulation:
|
|
|
512
510
|
The geometries may be provided (legacy behavior) but are automatically managed in the background.
|
|
513
511
|
|
|
514
512
|
"""
|
|
515
|
-
geometries_parsed: Any = None
|
|
516
513
|
logger.trace('Committing final geometry.')
|
|
517
|
-
|
|
518
|
-
geometries_parsed = _GEOMANAGER.all_geometries()
|
|
519
|
-
else:
|
|
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}')
|
|
514
|
+
self.state.store_geometry_data()
|
|
522
515
|
|
|
523
|
-
self.
|
|
516
|
+
logger.trace(f'Parsed geometries = {self.state.geos}')
|
|
517
|
+
|
|
518
|
+
self.mesher.submit_objects(self.state.geos)
|
|
524
519
|
|
|
525
|
-
self.mesher.submit_objects(geometries_parsed)
|
|
526
520
|
self._defined_geometries = True
|
|
527
521
|
self.display._facetags = [dt[1] for dt in gmsh.model.get_entities(2)]
|
|
528
522
|
|
|
@@ -532,20 +526,7 @@ class Simulation:
|
|
|
532
526
|
Returns:
|
|
533
527
|
list[GeoObject]: A list of all GeoObjects
|
|
534
528
|
"""
|
|
535
|
-
return
|
|
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
|
|
529
|
+
return self.state.current_geo_state
|
|
549
530
|
|
|
550
531
|
def generate_mesh(self, regenerate: bool = False) -> None:
|
|
551
532
|
"""Generate the mesh.
|
|
@@ -557,8 +538,9 @@ class Simulation:
|
|
|
557
538
|
Raises:
|
|
558
539
|
ValueError: ValueError if no frequencies are defined.
|
|
559
540
|
"""
|
|
541
|
+
logger.info('Starting mesh generation phase.')
|
|
560
542
|
if not regenerate:
|
|
561
|
-
|
|
543
|
+
|
|
562
544
|
if not self._defined_geometries:
|
|
563
545
|
self.commit_geometry()
|
|
564
546
|
|
|
@@ -567,7 +549,7 @@ class Simulation:
|
|
|
567
549
|
if self._cell is not None:
|
|
568
550
|
self.mesher.set_periodic_cell(self._cell)
|
|
569
551
|
|
|
570
|
-
self.mw._initialize_bcs(
|
|
552
|
+
self.mw._initialize_bcs(self.state.manager.get_surfaces())
|
|
571
553
|
|
|
572
554
|
# Check if frequencies are defined: TODO: Replace with a more generic check
|
|
573
555
|
if self.mw.frequencies is None:
|
|
@@ -576,8 +558,18 @@ class Simulation:
|
|
|
576
558
|
gmsh.model.occ.synchronize()
|
|
577
559
|
|
|
578
560
|
# Set the mesh size
|
|
579
|
-
self.mesher._configure_mesh_size(self.mw.get_discretizer(), self.mw.resolution)
|
|
561
|
+
self.mesher._configure_mesh_size(self.mw.get_discretizer(), self.mw.resolution) # This makes no sense to do this here
|
|
580
562
|
|
|
563
|
+
# Validity check
|
|
564
|
+
x1, y1, z1, x2, y2, z2 = gmsh.model.getBoundingBox(-1, -1)
|
|
565
|
+
bb_volume = (x2-x1)*(y2-y1)*(z2-z1)
|
|
566
|
+
wl = 299792458/self.mw.frequencies[-1]
|
|
567
|
+
Nelem = int(5 * bb_volume / (wl**3))
|
|
568
|
+
if Nelem > 100_000 and DEFAULT_SETTINGS.size_check:
|
|
569
|
+
DEBUG_COLLECTOR.add_report(f'An estimated {Nelem} tetrahedra are required for the bounding box of the geometry. This may imply a simulation domain that is very large.' +
|
|
570
|
+
'To disable this message. Set the .size_check parameter in model.settings to False.')
|
|
571
|
+
|
|
572
|
+
raise SimulationError('Simulation requires too many elements.')
|
|
581
573
|
logger.trace(' (2) Calling GMSH mesher')
|
|
582
574
|
try:
|
|
583
575
|
gmsh.logger.start()
|
|
@@ -595,16 +587,7 @@ class Simulation:
|
|
|
595
587
|
self.mesh._pre_update(self.mesher._get_periodic_bcs())
|
|
596
588
|
self.mesh.exterior_face_tags = self.mesher.domain_boundary_face_tags
|
|
597
589
|
gmsh.model.occ.synchronize()
|
|
598
|
-
self._set_mesh(self.mesh)
|
|
599
590
|
logger.trace(' (3) Mesh routine complete')
|
|
600
|
-
|
|
601
|
-
def _reset_mesh(self):
|
|
602
|
-
#gmsh.clear()
|
|
603
|
-
gmsh.model.mesh.clear()
|
|
604
|
-
mesh = Mesh3D(self.mesher)
|
|
605
|
-
|
|
606
|
-
self.mw.reset(False)
|
|
607
|
-
self._set_mesh(mesh)
|
|
608
591
|
|
|
609
592
|
def parameter_sweep(self, clear_mesh: bool = True, **parameters: np.ndarray) -> Generator[tuple[float,...], None, None]:
|
|
610
593
|
"""Executes a parameteric sweep iteration.
|
|
@@ -641,14 +624,13 @@ class Simulation:
|
|
|
641
624
|
if clear_mesh and i_iter > 0:
|
|
642
625
|
logger.info('Cleaning up mesh.')
|
|
643
626
|
gmsh.clear()
|
|
644
|
-
|
|
645
|
-
_GEOMANAGER.reset(self.modelname)
|
|
646
|
-
self._set_mesh(mesh)
|
|
627
|
+
self.state.reset_geostate(self.modelname)
|
|
647
628
|
self.mw.reset()
|
|
629
|
+
|
|
648
630
|
|
|
649
631
|
params = {key: dim[i_iter] for key,dim in zip(paramlist, dims_flat)}
|
|
650
|
-
|
|
651
|
-
self.
|
|
632
|
+
|
|
633
|
+
self.state.set_parameters(params)
|
|
652
634
|
|
|
653
635
|
logger.info(f'Iterating: {params}')
|
|
654
636
|
if len(dims_flat)==1:
|
|
@@ -656,75 +638,16 @@ class Simulation:
|
|
|
656
638
|
else:
|
|
657
639
|
yield (dim[i_iter] for dim in dims_flat) # type: ignore
|
|
658
640
|
|
|
641
|
+
|
|
659
642
|
if not clear_mesh:
|
|
660
|
-
self.
|
|
643
|
+
self.state.store_geometry_data()
|
|
661
644
|
|
|
662
645
|
if not clear_mesh:
|
|
663
|
-
|
|
646
|
+
self.state.store_geometry_data()
|
|
664
647
|
|
|
665
648
|
self.mw.cache_matrices = True
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
def _beta_adaptive_mesh_refinement(self,
|
|
669
|
-
max_steps: int = 6,
|
|
670
|
-
convergence: float = 0.02,
|
|
671
|
-
refinement_percentage: float = 0.1,
|
|
672
|
-
refinement_ratio: float = 0.3,
|
|
673
|
-
growth_rate: float = 3) -> None:
|
|
674
|
-
"""Beta implementation of Adaptive Mesh Refinement
|
|
675
|
-
|
|
676
|
-
Args:
|
|
677
|
-
max_steps (int, optional): _description_. Defaults to 6.
|
|
678
|
-
convergence (float, optional): _description_. Defaults to 0.02.
|
|
679
|
-
refinement_percentage (float, optional): _description_. Defaults to 0.1.
|
|
680
|
-
refinement_ratio (float, optional): _description_. Defaults to 0.3.
|
|
681
|
-
growth_rate (float, optional): _description_. Defaults to 3.
|
|
682
|
-
"""
|
|
683
|
-
from .physics.microwave.adaptive_mesh import select_refinement_indices, reduce_point_set, compute_convergence
|
|
684
|
-
|
|
685
|
-
max_freq = np.max(self.mw.frequencies)
|
|
686
649
|
|
|
687
|
-
regenerate = False
|
|
688
650
|
|
|
689
|
-
Smats = []
|
|
690
|
-
|
|
691
|
-
for step in range(max_steps):
|
|
692
|
-
amr_params = dict(iter_step=step)
|
|
693
|
-
self.mw._params = amr_params
|
|
694
|
-
self.data.sim.new(**amr_params)
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
data = self.mw._run_adaptive_mesh(step, max_freq)
|
|
698
|
-
|
|
699
|
-
field = data.field[-1]
|
|
700
|
-
|
|
701
|
-
Smat_new = data.scalar[-1].Sp
|
|
702
|
-
Smats.append(Smat_new)
|
|
703
|
-
if step > 0:
|
|
704
|
-
S0 = Smats[-2]
|
|
705
|
-
S1 = Smats[-1]
|
|
706
|
-
conv = compute_convergence(S0, S1)
|
|
707
|
-
logger.info(f'Convergence = {conv}')
|
|
708
|
-
if conv < convergence:
|
|
709
|
-
logger.info('Mesh refinement passed!')
|
|
710
|
-
break
|
|
711
|
-
error, lengths = field._solution_quality()
|
|
712
|
-
|
|
713
|
-
idx = select_refinement_indices(error, refinement_percentage)
|
|
714
|
-
|
|
715
|
-
idx = idx[reduce_point_set(self.mw.mesh.centers[:,idx], growth_rate, lengths[idx], refinement_ratio)]
|
|
716
|
-
centers = self.mw.mesh.centers
|
|
717
|
-
|
|
718
|
-
self.mesher._reset_amr_points()
|
|
719
|
-
self._reset_mesh()
|
|
720
|
-
logger.debug(f'Adding {len(idx)} refinement points.')
|
|
721
|
-
for i in idx:
|
|
722
|
-
coord = centers[:,i]
|
|
723
|
-
size = lengths[i]
|
|
724
|
-
self.mesher.add_refinement_point(coord, refinement_ratio, size, growth_rate)
|
|
725
|
-
|
|
726
|
-
self.generate_mesh(True)
|
|
727
|
-
self.view(plot_mesh=True)
|
|
728
651
|
def export(self, filename: str):
|
|
729
652
|
"""Exports the model or mesh depending on the extension.
|
|
730
653
|
|
|
@@ -760,4 +683,153 @@ class Simulation:
|
|
|
760
683
|
"""
|
|
761
684
|
logger.warning('define_geometry() will be derpicated. Use commit_geometry() instead.')
|
|
762
685
|
self.commit_geometry(*args)
|
|
763
|
-
|
|
686
|
+
|
|
687
|
+
class SimulationBeta(Simulation):
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
def __post_init__(self):
|
|
691
|
+
|
|
692
|
+
self.mesher.set_algorithm(Algorithm3D.HXT)
|
|
693
|
+
logger.debug('Setting mesh algorithm to HXT')
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def _reset_mesh(self):
|
|
697
|
+
#gmsh.clear()
|
|
698
|
+
gmsh.model.mesh.clear()
|
|
699
|
+
|
|
700
|
+
self.mw.reset(_reset_bc = False)
|
|
701
|
+
self.state.reset_mesh()
|
|
702
|
+
|
|
703
|
+
def adaptive_mesh_refinement(self,
|
|
704
|
+
max_steps: int = 6,
|
|
705
|
+
min_refined_passes: int = 1,
|
|
706
|
+
convergence: float = 0.02,
|
|
707
|
+
magnitude_convergence: float = 2.0,
|
|
708
|
+
phase_convergence: float = 180,
|
|
709
|
+
refinement_ratio: float = 0.9,
|
|
710
|
+
growth_rate: float = 3,
|
|
711
|
+
minimum_refinement_percentage: float = 20.0,
|
|
712
|
+
error_field_inclusion_percentage: float = 5.0,
|
|
713
|
+
frequency: float = None,
|
|
714
|
+
show_mesh: bool = False) -> SimulationDataset:
|
|
715
|
+
""" A beta-version of adaptive mesh refinement.
|
|
716
|
+
|
|
717
|
+
Convergence Criteria:
|
|
718
|
+
(1): max(abs(S[n]-S[n-1]))
|
|
719
|
+
(2): max(abs(abs(S[n]) - abs(S[n-1])))
|
|
720
|
+
(3): max(angle(S[n]/S[n-1])) * 180/π
|
|
721
|
+
|
|
722
|
+
Args:
|
|
723
|
+
max_steps (int, optional): The maximum number of refinement steps. Defaults to 6.
|
|
724
|
+
min_refined_passes (int, optional): The minimum number of refined passes. Defaults to 1.
|
|
725
|
+
convergence (float, optional): The S-paramerter convergence (1). Defaults to 0.02.
|
|
726
|
+
magnitude_convergence (float, optional): The S-parameter magnitude convergence (2). Defaults to 2.0.
|
|
727
|
+
phase_convergence (float, optional): The S-parameter Phase convergence (3). Defaults to 180.
|
|
728
|
+
refinement_ratio (float, optional): The size reduction of mesh elements by original length. Defaults to 0.75.
|
|
729
|
+
growth_rate (float, optional): The mesh size growth rate. Defaults to 3.0.
|
|
730
|
+
minimum_refinement_percentage (float, optional): The minimum mesh size increase . Defaults to 15.0.
|
|
731
|
+
error_field_inclusion_percentage (float, optional): A percentage of tet elements to be included for refinement. Defaults to 5.0.
|
|
732
|
+
frequency (float, optional): The refinement frequency. Defaults to None.
|
|
733
|
+
show_mesh (bool, optional): If the intermediate meshes should be shown (freezes simulation). Defaults to False
|
|
734
|
+
|
|
735
|
+
Returns:
|
|
736
|
+
SimulationDataset: _description_
|
|
737
|
+
"""
|
|
738
|
+
from .physics.microwave.adaptive_mesh import select_refinement_indices, reduce_point_set, compute_convergence
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
max_freq = np.max(self.mw.frequencies)
|
|
742
|
+
|
|
743
|
+
if frequency is not None:
|
|
744
|
+
max_freq = frequency
|
|
745
|
+
|
|
746
|
+
S_matrices: list[np.ndarray] = []
|
|
747
|
+
|
|
748
|
+
last_n_tets: int = self.mesh.n_tets
|
|
749
|
+
logger.info(f'Initial mesh has {last_n_tets} tetrahedra')
|
|
750
|
+
|
|
751
|
+
passed = 0
|
|
752
|
+
|
|
753
|
+
self.state.stash()
|
|
754
|
+
|
|
755
|
+
for step in range(1,max_steps+1):
|
|
756
|
+
|
|
757
|
+
self.data.sim.new(iter_step=step)
|
|
758
|
+
|
|
759
|
+
data = self.mw._run_adaptive_mesh(step, max_freq)
|
|
760
|
+
|
|
761
|
+
field = data.field[-1]
|
|
762
|
+
|
|
763
|
+
Smat_new = data.scalar[-1].Sp
|
|
764
|
+
S_matrices.append(Smat_new)
|
|
765
|
+
|
|
766
|
+
if step > 1:
|
|
767
|
+
S0 = S_matrices[-2]
|
|
768
|
+
S1 = S_matrices[-1]
|
|
769
|
+
conv_complex, conv_mag, conv_phase = compute_convergence(S0, S1)
|
|
770
|
+
logger.info(f'Pass {step}: Convergence = {conv_complex:.3f}, Mag = {conv_mag:.3f}, Phase = {conv_phase:.1f} deg')
|
|
771
|
+
if conv_complex <= convergence and conv_phase < phase_convergence and conv_mag < magnitude_convergence:
|
|
772
|
+
logger.info(f'Pass {step}: Mesh refinement passed!')
|
|
773
|
+
passed += 1
|
|
774
|
+
else:
|
|
775
|
+
passed = 0
|
|
776
|
+
|
|
777
|
+
if passed >= min_refined_passes:
|
|
778
|
+
logger.info('Adaptive mesh refinement successfull')
|
|
779
|
+
break
|
|
780
|
+
|
|
781
|
+
error, lengths = field._solution_quality()
|
|
782
|
+
|
|
783
|
+
idx = select_refinement_indices(error, error_field_inclusion_percentage/100)
|
|
784
|
+
idx = idx[::-1]
|
|
785
|
+
|
|
786
|
+
self.mesher.add_refinement_points(self.mw.mesh.centers[:,idx], lengths[idx])
|
|
787
|
+
|
|
788
|
+
logger.debug(f'Pass {step}: Adding {len(idx)} new refinement points.')
|
|
789
|
+
|
|
790
|
+
new_ids = reduce_point_set(self.mesher._amr_coords, growth_rate, self.mesher._amr_sizes, refinement_ratio, 0.20)
|
|
791
|
+
|
|
792
|
+
logger.debug(f'Pass {step}: Removing {self.mesher._amr_coords.shape[1] - len(new_ids)} points from {self.mesher._amr_coords.shape[1]} to {len(new_ids)}')
|
|
793
|
+
|
|
794
|
+
self.mesher._amr_coords = self.mesher._amr_coords[:,new_ids]
|
|
795
|
+
self.mesher._amr_sizes = self.mesher._amr_sizes[new_ids]
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
while True:
|
|
799
|
+
|
|
800
|
+
self._reset_mesh()
|
|
801
|
+
|
|
802
|
+
logger.debug(f'Pass {step}: Adding {len(idx)} refinement points.')
|
|
803
|
+
|
|
804
|
+
self.mesher.set_refinement_function(refinement_ratio, growth_rate, 1.0)
|
|
805
|
+
|
|
806
|
+
self.generate_mesh(True)
|
|
807
|
+
|
|
808
|
+
percentage = (self.mesh.n_tets/last_n_tets - 1) * 100
|
|
809
|
+
logger.info(f'Pass {step}: New mesh has {self.mesh.n_tets} (+{percentage:.1f}%) tetrahedra.')
|
|
810
|
+
|
|
811
|
+
if percentage < minimum_refinement_percentage:
|
|
812
|
+
logger.debug('Not enough mesh refinement, decreasing mesh size constraint.')
|
|
813
|
+
refinement_ratio = refinement_ratio * 0.9
|
|
814
|
+
logger.debug(f'New refinement ratio: {refinement_ratio}')
|
|
815
|
+
continue
|
|
816
|
+
|
|
817
|
+
if percentage > 2*minimum_refinement_percentage and refinement_ratio < 0.99:
|
|
818
|
+
logger.debug('Too much mesh refinement, decreasing mesh size constraint.')
|
|
819
|
+
refinement_ratio = refinement_ratio ** (1-np.log(percentage/minimum_refinement_percentage)/4)
|
|
820
|
+
logger.debug(f'New refinement ratio: {refinement_ratio}')
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
last_n_tets = self.mesh.n_tets
|
|
824
|
+
break
|
|
825
|
+
|
|
826
|
+
if show_mesh:
|
|
827
|
+
self.view(plot_mesh=True, volume_mesh=True)
|
|
828
|
+
|
|
829
|
+
if passed < min_refined_passes:
|
|
830
|
+
logger.warning('Adaptive mesh refinement did not converge!')
|
|
831
|
+
|
|
832
|
+
old = self.state.reload()
|
|
833
|
+
return old
|
|
834
|
+
|
|
835
|
+
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# EMerge is an open source Python based FEM EM simulation module.
|
|
2
|
+
# Copyright (C) 2025 Robert Fennis.
|
|
3
|
+
|
|
4
|
+
# This program is free software; you can redistribute it and/or
|
|
5
|
+
# modify it under the terms of the GNU General Public License
|
|
6
|
+
# as published by the Free Software Foundation; either version 2
|
|
7
|
+
# of the License, or (at your option) any later version.
|
|
8
|
+
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program; if not, see
|
|
16
|
+
# <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
from .mesh3d import Mesh3D
|
|
20
|
+
from .geometry import GeoObject, _GeometryManager, _GEOMANAGER
|
|
21
|
+
from .dataset import SimulationDataset
|
|
22
|
+
from loguru import logger
|
|
23
|
+
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SimState:
|
|
28
|
+
|
|
29
|
+
def __init__(self):
|
|
30
|
+
self.mesh: Mesh3D = Mesh3D()
|
|
31
|
+
self.geos: list[GeoObject] = []
|
|
32
|
+
self.data: SimulationDataset = SimulationDataset()
|
|
33
|
+
self.params: dict[str, float] = dict()
|
|
34
|
+
self._stashed: SimulationDataset | None = None
|
|
35
|
+
self.manager: _GeometryManager = _GEOMANAGER
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def current_geo_state(self) -> list[GeoObject]:
|
|
39
|
+
return self.manager.all_geometries()
|
|
40
|
+
|
|
41
|
+
def reset_geostate(self, modelname: str) -> None:
|
|
42
|
+
_GEOMANAGER.reset(modelname)
|
|
43
|
+
self.clear_mesh()
|
|
44
|
+
|
|
45
|
+
def init(self, modelname: str) -> None:
|
|
46
|
+
self.mesh = Mesh3D()
|
|
47
|
+
self.geos = []
|
|
48
|
+
self.reset_geostate(modelname)
|
|
49
|
+
self.init_data()
|
|
50
|
+
|
|
51
|
+
def stash(self) -> None:
|
|
52
|
+
self._stashed = self.data
|
|
53
|
+
self.data = SimulationDataset()
|
|
54
|
+
|
|
55
|
+
def set_parameters(self, parameters: dict[str, float]) -> None:
|
|
56
|
+
self.params = parameters
|
|
57
|
+
|
|
58
|
+
def init_data(self) -> None:
|
|
59
|
+
self.data.sim.new(**self.params)
|
|
60
|
+
|
|
61
|
+
def reload(self) -> SimulationDataset:
|
|
62
|
+
old = self._stashed
|
|
63
|
+
self.data = self._stashed
|
|
64
|
+
self._stashed = None
|
|
65
|
+
return old
|
|
66
|
+
|
|
67
|
+
def reset_mesh(self) -> None:
|
|
68
|
+
self.mesh = Mesh3D()
|
|
69
|
+
|
|
70
|
+
def set_mesh(self, mesh: Mesh3D) -> None:
|
|
71
|
+
self.mesh = mesh
|
|
72
|
+
|
|
73
|
+
def set_geos(self, geos: list[GeoObject]) -> None:
|
|
74
|
+
self.geos = geos
|
|
75
|
+
_GEOMANAGER.set_geometries(geos)
|
|
76
|
+
|
|
77
|
+
def clear_mesh(self) -> None:
|
|
78
|
+
self.mesh = Mesh3D()
|
|
79
|
+
|
|
80
|
+
def store_geometry_data(self) -> None:
|
|
81
|
+
"""Saves the current geometry state to the simulatin dataset
|
|
82
|
+
"""
|
|
83
|
+
logger.trace('Storing geometries in data.sim')
|
|
84
|
+
self.geos = self.current_geo_state
|
|
85
|
+
self.data.sim['geos'] = self.geos
|
|
86
|
+
self.data.sim['mesh'] = self.mesh
|
|
87
|
+
|
|
88
|
+
def get_dataset(self) -> dict[str, Any]:
|
|
89
|
+
return dict(simdata=self.data, mesh=self.mesh)
|
|
90
|
+
|
|
91
|
+
def load_dataset(self, dataset: dict[str, Any]):
|
|
92
|
+
self.data = dataset['simdata']
|
|
93
|
+
self.mesh = dataset['mesh']
|
|
94
|
+
|
|
95
|
+
def activate(self, _indx: int | None = None, **variables):
|
|
96
|
+
"""Searches for the permutaions of parameter sweep variables and sets the current geometry to the provided set."""
|
|
97
|
+
if _indx is not None:
|
|
98
|
+
dataset = self.data.sim.index(_indx)
|
|
99
|
+
else:
|
|
100
|
+
dataset = self.data.sim.find(**variables)
|
|
101
|
+
|
|
102
|
+
variables = ', '.join([f'{key}={value}' for key,value in dataset.vars.items()])
|
|
103
|
+
logger.info(f'Activated entry with variables: {variables}')
|
|
104
|
+
self.set_mesh(dataset['mesh'])
|
|
105
|
+
self.set_geos(dataset['geos'])
|
|
106
|
+
return self
|