emerge 0.4.6__py3-none-any.whl → 0.4.8__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 (80) hide show
  1. emerge/__init__.py +54 -0
  2. emerge/__main__.py +5 -0
  3. emerge/_emerge/__init__.py +42 -0
  4. emerge/_emerge/bc.py +197 -0
  5. emerge/_emerge/coord.py +119 -0
  6. emerge/_emerge/cs.py +523 -0
  7. emerge/_emerge/dataset.py +36 -0
  8. emerge/_emerge/elements/__init__.py +19 -0
  9. emerge/_emerge/elements/femdata.py +212 -0
  10. emerge/_emerge/elements/index_interp.py +64 -0
  11. emerge/_emerge/elements/legrange2.py +172 -0
  12. emerge/_emerge/elements/ned2_interp.py +645 -0
  13. emerge/_emerge/elements/nedelec2.py +140 -0
  14. emerge/_emerge/elements/nedleg2.py +217 -0
  15. emerge/_emerge/geo/__init__.py +24 -0
  16. emerge/_emerge/geo/horn.py +107 -0
  17. emerge/_emerge/geo/modeler.py +449 -0
  18. emerge/_emerge/geo/operations.py +254 -0
  19. emerge/_emerge/geo/pcb.py +1244 -0
  20. emerge/_emerge/geo/pcb_tools/calculator.py +28 -0
  21. emerge/_emerge/geo/pcb_tools/macro.py +79 -0
  22. emerge/_emerge/geo/pmlbox.py +204 -0
  23. emerge/_emerge/geo/polybased.py +529 -0
  24. emerge/_emerge/geo/shapes.py +427 -0
  25. emerge/_emerge/geo/step.py +77 -0
  26. emerge/_emerge/geo2d.py +86 -0
  27. emerge/_emerge/geometry.py +510 -0
  28. emerge/_emerge/howto.py +214 -0
  29. emerge/_emerge/logsettings.py +5 -0
  30. emerge/_emerge/material.py +118 -0
  31. emerge/_emerge/mesh3d.py +730 -0
  32. emerge/_emerge/mesher.py +339 -0
  33. emerge/_emerge/mth/common_functions.py +33 -0
  34. emerge/_emerge/mth/integrals.py +71 -0
  35. emerge/_emerge/mth/optimized.py +357 -0
  36. emerge/_emerge/periodic.py +263 -0
  37. emerge/_emerge/physics/__init__.py +0 -0
  38. emerge/_emerge/physics/microwave/__init__.py +1 -0
  39. emerge/_emerge/physics/microwave/adaptive_freq.py +279 -0
  40. emerge/_emerge/physics/microwave/assembly/assembler.py +569 -0
  41. emerge/_emerge/physics/microwave/assembly/curlcurl.py +448 -0
  42. emerge/_emerge/physics/microwave/assembly/generalized_eigen.py +426 -0
  43. emerge/_emerge/physics/microwave/assembly/robinbc.py +433 -0
  44. emerge/_emerge/physics/microwave/microwave_3d.py +1150 -0
  45. emerge/_emerge/physics/microwave/microwave_bc.py +915 -0
  46. emerge/_emerge/physics/microwave/microwave_data.py +1148 -0
  47. emerge/_emerge/physics/microwave/periodic.py +82 -0
  48. emerge/_emerge/physics/microwave/port_functions.py +53 -0
  49. emerge/_emerge/physics/microwave/sc.py +175 -0
  50. emerge/_emerge/physics/microwave/simjob.py +147 -0
  51. emerge/_emerge/physics/microwave/sparam.py +138 -0
  52. emerge/_emerge/physics/microwave/touchstone.py +140 -0
  53. emerge/_emerge/plot/__init__.py +0 -0
  54. emerge/_emerge/plot/display.py +394 -0
  55. emerge/_emerge/plot/grapher.py +93 -0
  56. emerge/_emerge/plot/matplotlib/mpldisplay.py +264 -0
  57. emerge/_emerge/plot/pyvista/__init__.py +1 -0
  58. emerge/_emerge/plot/pyvista/display.py +931 -0
  59. emerge/_emerge/plot/pyvista/display_settings.py +24 -0
  60. emerge/_emerge/plot/simple_plots.py +551 -0
  61. emerge/_emerge/plot.py +225 -0
  62. emerge/_emerge/projects/__init__.py +0 -0
  63. emerge/_emerge/projects/_gen_base.txt +32 -0
  64. emerge/_emerge/projects/_load_base.txt +24 -0
  65. emerge/_emerge/projects/generate_project.py +40 -0
  66. emerge/_emerge/selection.py +596 -0
  67. emerge/_emerge/simmodel.py +444 -0
  68. emerge/_emerge/simulation_data.py +411 -0
  69. emerge/_emerge/solver.py +993 -0
  70. emerge/_emerge/system.py +54 -0
  71. emerge/cli.py +19 -0
  72. emerge/lib.py +57 -0
  73. emerge/plot.py +1 -0
  74. emerge/pyvista.py +1 -0
  75. {emerge-0.4.6.dist-info → emerge-0.4.8.dist-info}/METADATA +1 -1
  76. emerge-0.4.8.dist-info/RECORD +78 -0
  77. emerge-0.4.8.dist-info/entry_points.txt +2 -0
  78. emerge-0.4.6.dist-info/RECORD +0 -4
  79. emerge-0.4.6.dist-info/entry_points.txt +0 -2
  80. {emerge-0.4.6.dist-info → emerge-0.4.8.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
+