emerge 0.4.7__py3-none-any.whl → 0.4.9__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 -14
- emerge/_emerge/__init__.py +42 -0
- emerge/_emerge/bc.py +197 -0
- emerge/_emerge/coord.py +119 -0
- emerge/_emerge/cs.py +523 -0
- emerge/_emerge/dataset.py +36 -0
- emerge/_emerge/elements/__init__.py +19 -0
- emerge/_emerge/elements/femdata.py +212 -0
- emerge/_emerge/elements/index_interp.py +64 -0
- emerge/_emerge/elements/legrange2.py +172 -0
- emerge/_emerge/elements/ned2_interp.py +645 -0
- emerge/_emerge/elements/nedelec2.py +140 -0
- emerge/_emerge/elements/nedleg2.py +217 -0
- emerge/_emerge/geo/__init__.py +24 -0
- emerge/_emerge/geo/horn.py +107 -0
- emerge/_emerge/geo/modeler.py +449 -0
- emerge/_emerge/geo/operations.py +254 -0
- emerge/_emerge/geo/pcb.py +1244 -0
- emerge/_emerge/geo/pcb_tools/calculator.py +28 -0
- emerge/_emerge/geo/pcb_tools/macro.py +79 -0
- emerge/_emerge/geo/pmlbox.py +204 -0
- emerge/_emerge/geo/polybased.py +529 -0
- emerge/_emerge/geo/shapes.py +427 -0
- emerge/_emerge/geo/step.py +77 -0
- emerge/_emerge/geo2d.py +86 -0
- emerge/_emerge/geometry.py +510 -0
- emerge/_emerge/howto.py +214 -0
- emerge/_emerge/logsettings.py +5 -0
- emerge/_emerge/material.py +118 -0
- emerge/_emerge/mesh3d.py +730 -0
- emerge/_emerge/mesher.py +339 -0
- emerge/_emerge/mth/common_functions.py +33 -0
- emerge/_emerge/mth/integrals.py +71 -0
- emerge/_emerge/mth/optimized.py +357 -0
- emerge/_emerge/periodic.py +263 -0
- emerge/_emerge/physics/__init__.py +0 -0
- emerge/_emerge/physics/microwave/__init__.py +1 -0
- emerge/_emerge/physics/microwave/adaptive_freq.py +279 -0
- emerge/_emerge/physics/microwave/assembly/assembler.py +569 -0
- emerge/_emerge/physics/microwave/assembly/curlcurl.py +448 -0
- emerge/_emerge/physics/microwave/assembly/generalized_eigen.py +426 -0
- emerge/_emerge/physics/microwave/assembly/robinbc.py +433 -0
- emerge/_emerge/physics/microwave/microwave_3d.py +1150 -0
- emerge/_emerge/physics/microwave/microwave_bc.py +915 -0
- emerge/_emerge/physics/microwave/microwave_data.py +1148 -0
- emerge/_emerge/physics/microwave/periodic.py +82 -0
- emerge/_emerge/physics/microwave/port_functions.py +53 -0
- emerge/_emerge/physics/microwave/sc.py +175 -0
- emerge/_emerge/physics/microwave/simjob.py +147 -0
- emerge/_emerge/physics/microwave/sparam.py +138 -0
- emerge/_emerge/physics/microwave/touchstone.py +140 -0
- emerge/_emerge/plot/__init__.py +0 -0
- emerge/_emerge/plot/display.py +394 -0
- emerge/_emerge/plot/grapher.py +93 -0
- emerge/_emerge/plot/matplotlib/mpldisplay.py +264 -0
- emerge/_emerge/plot/pyvista/__init__.py +1 -0
- emerge/_emerge/plot/pyvista/display.py +931 -0
- emerge/_emerge/plot/pyvista/display_settings.py +24 -0
- emerge/_emerge/plot/simple_plots.py +551 -0
- emerge/_emerge/plot.py +225 -0
- emerge/_emerge/projects/__init__.py +0 -0
- emerge/_emerge/projects/_gen_base.txt +32 -0
- emerge/_emerge/projects/_load_base.txt +24 -0
- emerge/_emerge/projects/generate_project.py +40 -0
- emerge/_emerge/selection.py +596 -0
- emerge/_emerge/simmodel.py +444 -0
- emerge/_emerge/simulation_data.py +411 -0
- emerge/_emerge/solver.py +993 -0
- emerge/_emerge/system.py +54 -0
- emerge/cli.py +19 -0
- emerge/lib.py +1 -1
- emerge/plot.py +1 -1
- {emerge-0.4.7.dist-info → emerge-0.4.9.dist-info}/METADATA +7 -6
- emerge-0.4.9.dist-info/RECORD +78 -0
- emerge-0.4.9.dist-info/entry_points.txt +2 -0
- emerge-0.4.7.dist-info/RECORD +0 -9
- emerge-0.4.7.dist-info/entry_points.txt +0 -2
- {emerge-0.4.7.dist-info → emerge-0.4.9.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,444 @@
|
|
|
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
|
+
from __future__ import annotations
|
|
19
|
+
from .mesher import Mesher, unpack_lists
|
|
20
|
+
from .geometry import GeoObject, _GEOMANAGER
|
|
21
|
+
from .geo.modeler import Modeler
|
|
22
|
+
from .physics.microwave.microwave_3d import Microwave3D
|
|
23
|
+
from .mesh3d import Mesh3D
|
|
24
|
+
from .selection import Selector, FaceSelection, Selection
|
|
25
|
+
from .logsettings import logger_format
|
|
26
|
+
from .plot.display import BaseDisplay
|
|
27
|
+
from .plot.pyvista import PVDisplay
|
|
28
|
+
from .dataset import SimulationDataset
|
|
29
|
+
from .periodic import PeriodicCell
|
|
30
|
+
from .bc import BoundaryCondition
|
|
31
|
+
from typing import Literal, Type, Generator, Any
|
|
32
|
+
from loguru import logger
|
|
33
|
+
import numpy as np
|
|
34
|
+
import sys
|
|
35
|
+
import gmsh
|
|
36
|
+
import joblib
|
|
37
|
+
import os
|
|
38
|
+
import inspect
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from atexit import register
|
|
41
|
+
import signal
|
|
42
|
+
|
|
43
|
+
_GMSH_ERROR_TEXT = """
|
|
44
|
+
--------------------------
|
|
45
|
+
Known problems/solutions:
|
|
46
|
+
(1) - PLC Error: A segment and a facet intersect at point
|
|
47
|
+
This can be caused when approximating thin curved volumes. Try to decrease the mesh size for that region.
|
|
48
|
+
--------------------------
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
class SimulationError(Exception):
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class Simulation3D:
|
|
56
|
+
|
|
57
|
+
def __init__(self,
|
|
58
|
+
modelname: str,
|
|
59
|
+
loglevel: Literal['DEBUG','INFO','WARNING','ERROR'] = 'INFO',
|
|
60
|
+
load_file: bool = False,
|
|
61
|
+
save_file: bool = False,
|
|
62
|
+
logfile: bool = False,
|
|
63
|
+
path_suffix: str = ".EMResults"):
|
|
64
|
+
"""Generate a Simulation3D class object.
|
|
65
|
+
|
|
66
|
+
As a minimum a file name should be provided. Additionally you may provide it with any
|
|
67
|
+
class that inherits from BaseDisplay. This will then be used for geometry displaying.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
modelname (str): The name of the simulation model. This will be used for filenames and path names when saving data.
|
|
71
|
+
loglevel ("DEBUG","INFO","WARNING","ERROR", optional): The loglevel to use for loguru. Defaults to 'INFO'.
|
|
72
|
+
load_file (bool, optional): If the simulatio model should be loaded from a file. Defaults to False.
|
|
73
|
+
save_file (bool, optional): if the simulation file should be stored to a file. Defaults to False.
|
|
74
|
+
logfile (bool, optional): If a file should be created that contains the entire log of the simulation. Defaults to False.
|
|
75
|
+
path_suffix (str, optional): The suffix that will be added to the results directory. Defaults to ".EMResults".
|
|
76
|
+
"""
|
|
77
|
+
caller_file = Path(inspect.stack()[1].filename).resolve()
|
|
78
|
+
base_path = caller_file.parent
|
|
79
|
+
|
|
80
|
+
self.modelname = modelname
|
|
81
|
+
self.modelpath = base_path / (modelname.lower()+path_suffix)
|
|
82
|
+
self.mesher: Mesher = Mesher()
|
|
83
|
+
self.modeler: Modeler = Modeler()
|
|
84
|
+
|
|
85
|
+
self.mesh: Mesh3D = Mesh3D(self.mesher)
|
|
86
|
+
self.select: Selector = Selector()
|
|
87
|
+
self.display: PVDisplay = None
|
|
88
|
+
self.set_loglevel(loglevel)
|
|
89
|
+
|
|
90
|
+
## STATES
|
|
91
|
+
self.__active: bool = False
|
|
92
|
+
self._defined_geometries: bool = False
|
|
93
|
+
self._cell: PeriodicCell = None
|
|
94
|
+
|
|
95
|
+
self.display = PVDisplay(self.mesh)
|
|
96
|
+
if logfile:
|
|
97
|
+
self.set_logfile(logfile)
|
|
98
|
+
|
|
99
|
+
self.save_file: bool = save_file
|
|
100
|
+
self.load_file: bool = load_file
|
|
101
|
+
|
|
102
|
+
self.data: SimulationDataset = SimulationDataset()
|
|
103
|
+
|
|
104
|
+
## Physics
|
|
105
|
+
self.mw: Microwave3D = Microwave3D(self.mesher, self.data.mw)
|
|
106
|
+
|
|
107
|
+
self._initialize_simulation()
|
|
108
|
+
|
|
109
|
+
self._update_data()
|
|
110
|
+
|
|
111
|
+
def __setitem__(self, name: str, value: Any) -> None:
|
|
112
|
+
"""Store data in the current data container"""
|
|
113
|
+
self.data.sim[name] = value
|
|
114
|
+
|
|
115
|
+
def __getitem__(self, name: str) -> Any:
|
|
116
|
+
"""Get the data from the current data container"""
|
|
117
|
+
return self.data.sim[name]
|
|
118
|
+
|
|
119
|
+
def _update_data(self) -> None:
|
|
120
|
+
"""Writes the stored physics data to each phyics class insatnce"""
|
|
121
|
+
self.mw.data = self.data.mw
|
|
122
|
+
|
|
123
|
+
def all_geometries(self) -> list[GeoObject]:
|
|
124
|
+
"""Returns all geometries stored in the simulation file."""
|
|
125
|
+
return [obj for obj in self.sim.default.values() if isinstance(obj, GeoObject)]
|
|
126
|
+
|
|
127
|
+
def all_bcs(self) -> list[BoundaryCondition]:
|
|
128
|
+
"""Returns all boundary condition objects stored in the simulation file"""
|
|
129
|
+
return [obj for obj in self.sim.default.values() if isinstance(obj, BoundaryCondition)]
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def passed_geometries(self) -> list[GeoObject]:
|
|
133
|
+
""""""
|
|
134
|
+
return self.data.sim['geometries']
|
|
135
|
+
|
|
136
|
+
def set_mesh(self, mesh: Mesh3D) -> None:
|
|
137
|
+
"""Set the current model mesh to a given mesh."""
|
|
138
|
+
self.mesh = mesh
|
|
139
|
+
self.mw.mesh = mesh
|
|
140
|
+
self.mesher.mesh = mesh
|
|
141
|
+
self.display._mesh = mesh
|
|
142
|
+
|
|
143
|
+
def save(self) -> None:
|
|
144
|
+
"""Saves the current model in the provided project directory."""
|
|
145
|
+
# Ensure directory exists
|
|
146
|
+
if not self.modelpath.exists():
|
|
147
|
+
self.modelpath.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
logger.info(f"Created directory: {self.modelpath}")
|
|
149
|
+
|
|
150
|
+
# Save mesh
|
|
151
|
+
mesh_path = self.modelpath / 'mesh.msh'
|
|
152
|
+
brep_path = self.modelpath / 'model.brep'
|
|
153
|
+
|
|
154
|
+
gmsh.option.setNumber('Mesh.SaveParametric', 1)
|
|
155
|
+
gmsh.option.setNumber('Mesh.SaveAll', 1)
|
|
156
|
+
gmsh.model.geo.synchronize()
|
|
157
|
+
gmsh.model.occ.synchronize()
|
|
158
|
+
|
|
159
|
+
gmsh.write(str(mesh_path))
|
|
160
|
+
gmsh.write(str(brep_path))
|
|
161
|
+
logger.info(f"Saved mesh to: {mesh_path}")
|
|
162
|
+
|
|
163
|
+
# Pack and save data
|
|
164
|
+
dataset = dict(simdata=self.data, mesh=self.mesh)
|
|
165
|
+
data_path = self.modelpath / 'simdata.emerge'
|
|
166
|
+
joblib.dump(dataset, str(data_path))
|
|
167
|
+
logger.info(f"Saved simulation data to: {data_path}")
|
|
168
|
+
|
|
169
|
+
def load(self) -> None:
|
|
170
|
+
"""Loads the model from the project directory."""
|
|
171
|
+
mesh_path = self.modelpath / 'mesh.msh'
|
|
172
|
+
brep_path = self.modelpath / 'model.brep'
|
|
173
|
+
data_path = self.modelpath / 'simdata.emerge'
|
|
174
|
+
|
|
175
|
+
if not mesh_path.exists() or not data_path.exists():
|
|
176
|
+
raise FileNotFoundError("Missing required mesh or data file.")
|
|
177
|
+
|
|
178
|
+
# Load mesh
|
|
179
|
+
gmsh.open(str(brep_path))
|
|
180
|
+
gmsh.merge(str(mesh_path))
|
|
181
|
+
gmsh.model.geo.synchronize()
|
|
182
|
+
gmsh.model.occ.synchronize()
|
|
183
|
+
logger.info(f"Loaded mesh from: {mesh_path}")
|
|
184
|
+
#self.mesh.update([])
|
|
185
|
+
|
|
186
|
+
# Load data
|
|
187
|
+
datapack = joblib.load(str(data_path))
|
|
188
|
+
self.data = datapack['simdata']
|
|
189
|
+
self.set_mesh(datapack['mesh'])
|
|
190
|
+
logger.info(f"Loaded simulation data from: {data_path}")
|
|
191
|
+
|
|
192
|
+
def load_data(self, key: str) -> Any:
|
|
193
|
+
return self.save_data[key]
|
|
194
|
+
|
|
195
|
+
def set_loglevel(self, loglevel: Literal['DEBUG','INFO','WARNING','ERROR']) -> None:
|
|
196
|
+
"""Set the loglevel for loguru.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
loglevel ('DEBUG','INFO','WARNING','ERROR'): The loglevel
|
|
200
|
+
"""
|
|
201
|
+
handler = {"sink": sys.stdout, "level": loglevel, "format": logger_format}
|
|
202
|
+
logger.configure(handlers=[handler])
|
|
203
|
+
|
|
204
|
+
def set_logfile(self) -> None:
|
|
205
|
+
"""Adds a file output for the logger."""
|
|
206
|
+
logger.add(str(self.modelpath / 'logging.log'), mode='w', level='DEBUG', format=logger_format, colorize=False, backtrace=True, diagnose=True)
|
|
207
|
+
|
|
208
|
+
def view(self,
|
|
209
|
+
selections: list[Selection] = None,
|
|
210
|
+
use_gmsh: bool = False,
|
|
211
|
+
volume_opacity: float = 0.1,
|
|
212
|
+
surface_opacity: float = 1,
|
|
213
|
+
show_edges: bool = True) -> None:
|
|
214
|
+
"""View the current geometry in either the BaseDisplay object (PVDisplay only) or
|
|
215
|
+
the GMSH viewer.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
selections (list[Selection], optional): Additional selections to highlight. Defaults to None.
|
|
219
|
+
use_gmsh (bool, optional): Whether to use the GMSH display. Defaults to False.
|
|
220
|
+
opacity (float, optional): The global opacity of all objects.. Defaults to None.
|
|
221
|
+
show_edges (bool, optional): Whether to show the geometry edges. Defaults to None.
|
|
222
|
+
"""
|
|
223
|
+
if not (self.display is not None and self.mesh.defined) or use_gmsh:
|
|
224
|
+
gmsh.model.occ.synchronize()
|
|
225
|
+
gmsh.fltk.run()
|
|
226
|
+
return
|
|
227
|
+
try:
|
|
228
|
+
for obj in self.data.sim['geometries']:
|
|
229
|
+
if obj.dim==2:
|
|
230
|
+
opacity=surface_opacity
|
|
231
|
+
elif obj.dim==3:
|
|
232
|
+
opacity=volume_opacity
|
|
233
|
+
self.display.add_object(obj, show_edges=show_edges, opacity=opacity)
|
|
234
|
+
if selections:
|
|
235
|
+
[self.display.add_object(sel, color='red', opacity=0.7) for sel in selections]
|
|
236
|
+
self.display.show()
|
|
237
|
+
return
|
|
238
|
+
except NotImplementedError as e:
|
|
239
|
+
logger.warning('The provided BaseDisplay class does not support object display. Please make' \
|
|
240
|
+
'sure that this method is properly implemented.')
|
|
241
|
+
|
|
242
|
+
def set_periodic_cell(self, cell: PeriodicCell, excluded_faces: list[FaceSelection] = None):
|
|
243
|
+
"""Sets the periodic cell information based on the PeriodicCell class object"""
|
|
244
|
+
self.mw.bc._cell = cell
|
|
245
|
+
self._cell = cell
|
|
246
|
+
|
|
247
|
+
def define_geometry(self, *geometries: list[GeoObject]) -> None:
|
|
248
|
+
"""Provide the physics engine with the geometries that are contained and ought to be included
|
|
249
|
+
in the simulation. Please make sure to include all geometries. Its currently unclear how the
|
|
250
|
+
system behaves if only a part of all geometries are included.
|
|
251
|
+
|
|
252
|
+
"""
|
|
253
|
+
if not geometries:
|
|
254
|
+
geometries = _GEOMANAGER.all_geometries()
|
|
255
|
+
else:
|
|
256
|
+
geometries = unpack_lists(geometries + tuple([item for item in self.data.sim.default.values() if isinstance(item, GeoObject)]))
|
|
257
|
+
self.data.sim['geometries'] = geometries
|
|
258
|
+
self.mesher.submit_objects(geometries)
|
|
259
|
+
self._defined_geometries = True
|
|
260
|
+
self.display._facetags = [dt[1] for dt in gmsh.model.get_entities(2)]
|
|
261
|
+
# Set the cell periodicity in GMSH
|
|
262
|
+
if self._cell is not None:
|
|
263
|
+
self.mesher.set_periodic_cell(self._cell)
|
|
264
|
+
|
|
265
|
+
self.mw._initialize_bcs()
|
|
266
|
+
|
|
267
|
+
def generate_mesh(self):
|
|
268
|
+
"""Generate the mesh.
|
|
269
|
+
This can only be done after define_geometry(...) is called and if frequencies are defined.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
name (str, optional): The mesh file name. Defaults to "meshname.msh".
|
|
273
|
+
|
|
274
|
+
Raises:
|
|
275
|
+
ValueError: ValueError if no frequencies are defined.
|
|
276
|
+
"""
|
|
277
|
+
if not self._defined_geometries:
|
|
278
|
+
self.define_geometry()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# Check if frequencies are defined: TODO: Replace with a more generic check
|
|
283
|
+
if self.mw.frequencies is None:
|
|
284
|
+
raise ValueError('No frequencies defined for the simulation. Please set frequencies before generating the mesh.')
|
|
285
|
+
|
|
286
|
+
gmsh.model.occ.synchronize()
|
|
287
|
+
|
|
288
|
+
# Set the mesh size
|
|
289
|
+
self.mesher.set_mesh_size(self.mw.get_discretizer(), self.mw.resolution)
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
gmsh.model.mesh.generate(3)
|
|
293
|
+
except Exception:
|
|
294
|
+
logger.error('GMSH Mesh error detected.')
|
|
295
|
+
print(_GMSH_ERROR_TEXT)
|
|
296
|
+
raise
|
|
297
|
+
|
|
298
|
+
self.mesh.update(self.mesher._get_periodic_bcs())
|
|
299
|
+
gmsh.model.occ.synchronize()
|
|
300
|
+
self.set_mesh(self.mesh)
|
|
301
|
+
|
|
302
|
+
def get_boundary(self, face: FaceSelection = None, tags: list[int] = None) -> tuple[np.ndarray, np.ndarray]:
|
|
303
|
+
''' Return boundary data.
|
|
304
|
+
|
|
305
|
+
Parameters
|
|
306
|
+
----------
|
|
307
|
+
obj: GeoObject
|
|
308
|
+
tags: list[int]
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
----------
|
|
312
|
+
nodes: np.ndarray
|
|
313
|
+
triangles: np.ndarray
|
|
314
|
+
'''
|
|
315
|
+
if tags is None:
|
|
316
|
+
tags = face.tags
|
|
317
|
+
tri_ids = self.mesh.get_triangles(tags)
|
|
318
|
+
return self.mesh.nodes, self.mesh.tris[:,tri_ids]
|
|
319
|
+
|
|
320
|
+
def parameter_sweep(self, clear_mesh: bool = True, **parameters: np.ndarray) -> Generator[tuple[float,...], None, None]:
|
|
321
|
+
"""Executes a parameteric sweep iteration.
|
|
322
|
+
|
|
323
|
+
The first argument clear_mesh determines if the mesh should be cleared and rebuild in between sweeps. This is usually needed
|
|
324
|
+
except when you change only port-properties or material properties. The parameters of the sweep can be provided as a set of
|
|
325
|
+
keyword arguments. As an example if you defined the axes: width=np.linspace(...) and height=np.linspace(...). You can
|
|
326
|
+
add them as arguments using .parameter_sweep(True, width=width, height=height).
|
|
327
|
+
|
|
328
|
+
The rest of the simulation commands should be inside the iteration scope
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
clear_mesh (bool, optional): Wether to clear the mesh in between sweeps. Defaults to True.
|
|
332
|
+
|
|
333
|
+
Yields:
|
|
334
|
+
Generator[tuple[float,...], None, None]: The parameters provided
|
|
335
|
+
|
|
336
|
+
Example:
|
|
337
|
+
>>> for W, H in model.parameter_sweep(True, width=widths, height=heights):
|
|
338
|
+
>>> // build simulation
|
|
339
|
+
>>> data = model.frequency_domain()
|
|
340
|
+
>>> // Extract the data
|
|
341
|
+
>>> widths, heights, frequencies, S21 = data.ax('width','height','freq').S(2,1)
|
|
342
|
+
"""
|
|
343
|
+
paramlist = sorted(list(parameters.keys()))
|
|
344
|
+
dims = np.meshgrid(*[parameters[key] for key in paramlist], indexing='ij')
|
|
345
|
+
dims_flat = [dim.flatten() for dim in dims]
|
|
346
|
+
self.mw.cache_matrices = False
|
|
347
|
+
for i_iter in range(dims_flat[0].shape[0]):
|
|
348
|
+
if clear_mesh:
|
|
349
|
+
logger.info('Cleaning up mesh.')
|
|
350
|
+
gmsh.clear()
|
|
351
|
+
mesh = Mesh3D(self.mesher)
|
|
352
|
+
_GEOMANAGER.reset(self.modelname)
|
|
353
|
+
self.set_mesh(mesh)
|
|
354
|
+
self.mw.reset()
|
|
355
|
+
|
|
356
|
+
params = {key: dim[i_iter] for key,dim in zip(paramlist, dims_flat)}
|
|
357
|
+
self.mw._params = params
|
|
358
|
+
self.data.sim.new(**params)
|
|
359
|
+
|
|
360
|
+
logger.info(f'Iterating: {params}')
|
|
361
|
+
if len(dims_flat)==1:
|
|
362
|
+
yield dims_flat[0][i_iter]
|
|
363
|
+
else:
|
|
364
|
+
yield (dim[i_iter] for dim in dims_flat)
|
|
365
|
+
self.mw.cache_matrices = True
|
|
366
|
+
|
|
367
|
+
def __enter__(self) -> Simulation3D:
|
|
368
|
+
"""This method is depricated with the new atexit system. It still exists for backwards compatibility.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
Simulation3D: the Simulation3D object
|
|
372
|
+
"""
|
|
373
|
+
return self
|
|
374
|
+
|
|
375
|
+
def __exit__(self, type, value, tb):
|
|
376
|
+
"""This method no longer does something. It only serves as backwards compatibility."""
|
|
377
|
+
self._exit_gmsh()
|
|
378
|
+
return False
|
|
379
|
+
|
|
380
|
+
def _install_signal_handlers(self):
|
|
381
|
+
# on SIGINT (Ctrl-C) or SIGTERM, call our exit routine
|
|
382
|
+
for sig in (signal.SIGINT, signal.SIGTERM):
|
|
383
|
+
signal.signal(sig, self._handle_signal)
|
|
384
|
+
|
|
385
|
+
def _handle_signal(self, signum, frame):
|
|
386
|
+
"""
|
|
387
|
+
Signal handler: do our cleanup, then re-raise
|
|
388
|
+
the default handler so that exit code / traceback
|
|
389
|
+
is as expected.
|
|
390
|
+
"""
|
|
391
|
+
try:
|
|
392
|
+
# run your atexit-style cleanup
|
|
393
|
+
self._exit_gmsh()
|
|
394
|
+
except Exception:
|
|
395
|
+
# log but don’t block shutdown
|
|
396
|
+
logger.exception("Error during signal cleanup")
|
|
397
|
+
finally:
|
|
398
|
+
# restore default handler and re‐send the signal
|
|
399
|
+
signal.signal(signum, signal.SIG_DFL)
|
|
400
|
+
os.kill(os.getpid(), signum)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _initialize_simulation(self):
|
|
404
|
+
"""Initializes the Simulation data and GMSH API with proper shutdown routines.
|
|
405
|
+
"""
|
|
406
|
+
_GEOMANAGER.sign_in(self.modelname)
|
|
407
|
+
|
|
408
|
+
# If GMSH is not yet initialized (Two simulation in a file)
|
|
409
|
+
if gmsh.isInitialized() == 0:
|
|
410
|
+
logger.debug('Initializing GMSH')
|
|
411
|
+
gmsh.initialize()
|
|
412
|
+
|
|
413
|
+
# Set an exit handler for Ctrl+C cases
|
|
414
|
+
self._install_signal_handlers()
|
|
415
|
+
|
|
416
|
+
# Restier the Exit GMSH function on proper program abortion
|
|
417
|
+
register(self._exit_gmsh)
|
|
418
|
+
|
|
419
|
+
# Create a new GMSH model or load it
|
|
420
|
+
if not self.load_file:
|
|
421
|
+
gmsh.model.add(self.modelname)
|
|
422
|
+
self.data: SimulationDataset = SimulationDataset()
|
|
423
|
+
else:
|
|
424
|
+
self.load()
|
|
425
|
+
|
|
426
|
+
# Set the Simulation state to active
|
|
427
|
+
self.__active = True
|
|
428
|
+
return self
|
|
429
|
+
|
|
430
|
+
def _exit_gmsh(self):
|
|
431
|
+
# If the simulation object state is still active (GMSH is running)
|
|
432
|
+
if not self.__active:
|
|
433
|
+
return
|
|
434
|
+
logger.debug('Exiting program')
|
|
435
|
+
# Save the file first
|
|
436
|
+
if self.save_file:
|
|
437
|
+
self.save()
|
|
438
|
+
# Finalize GMSH
|
|
439
|
+
gmsh.finalize()
|
|
440
|
+
logger.debug('GMSH Shut down successful')
|
|
441
|
+
# set the state to active
|
|
442
|
+
self.__active = False
|
|
443
|
+
|
|
444
|
+
|