emerge 0.4.7__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 (78) hide show
  1. emerge/__init__.py +14 -14
  2. emerge/_emerge/__init__.py +42 -0
  3. emerge/_emerge/bc.py +197 -0
  4. emerge/_emerge/coord.py +119 -0
  5. emerge/_emerge/cs.py +523 -0
  6. emerge/_emerge/dataset.py +36 -0
  7. emerge/_emerge/elements/__init__.py +19 -0
  8. emerge/_emerge/elements/femdata.py +212 -0
  9. emerge/_emerge/elements/index_interp.py +64 -0
  10. emerge/_emerge/elements/legrange2.py +172 -0
  11. emerge/_emerge/elements/ned2_interp.py +645 -0
  12. emerge/_emerge/elements/nedelec2.py +140 -0
  13. emerge/_emerge/elements/nedleg2.py +217 -0
  14. emerge/_emerge/geo/__init__.py +24 -0
  15. emerge/_emerge/geo/horn.py +107 -0
  16. emerge/_emerge/geo/modeler.py +449 -0
  17. emerge/_emerge/geo/operations.py +254 -0
  18. emerge/_emerge/geo/pcb.py +1244 -0
  19. emerge/_emerge/geo/pcb_tools/calculator.py +28 -0
  20. emerge/_emerge/geo/pcb_tools/macro.py +79 -0
  21. emerge/_emerge/geo/pmlbox.py +204 -0
  22. emerge/_emerge/geo/polybased.py +529 -0
  23. emerge/_emerge/geo/shapes.py +427 -0
  24. emerge/_emerge/geo/step.py +77 -0
  25. emerge/_emerge/geo2d.py +86 -0
  26. emerge/_emerge/geometry.py +510 -0
  27. emerge/_emerge/howto.py +214 -0
  28. emerge/_emerge/logsettings.py +5 -0
  29. emerge/_emerge/material.py +118 -0
  30. emerge/_emerge/mesh3d.py +730 -0
  31. emerge/_emerge/mesher.py +339 -0
  32. emerge/_emerge/mth/common_functions.py +33 -0
  33. emerge/_emerge/mth/integrals.py +71 -0
  34. emerge/_emerge/mth/optimized.py +357 -0
  35. emerge/_emerge/periodic.py +263 -0
  36. emerge/_emerge/physics/__init__.py +0 -0
  37. emerge/_emerge/physics/microwave/__init__.py +1 -0
  38. emerge/_emerge/physics/microwave/adaptive_freq.py +279 -0
  39. emerge/_emerge/physics/microwave/assembly/assembler.py +569 -0
  40. emerge/_emerge/physics/microwave/assembly/curlcurl.py +448 -0
  41. emerge/_emerge/physics/microwave/assembly/generalized_eigen.py +426 -0
  42. emerge/_emerge/physics/microwave/assembly/robinbc.py +433 -0
  43. emerge/_emerge/physics/microwave/microwave_3d.py +1150 -0
  44. emerge/_emerge/physics/microwave/microwave_bc.py +915 -0
  45. emerge/_emerge/physics/microwave/microwave_data.py +1148 -0
  46. emerge/_emerge/physics/microwave/periodic.py +82 -0
  47. emerge/_emerge/physics/microwave/port_functions.py +53 -0
  48. emerge/_emerge/physics/microwave/sc.py +175 -0
  49. emerge/_emerge/physics/microwave/simjob.py +147 -0
  50. emerge/_emerge/physics/microwave/sparam.py +138 -0
  51. emerge/_emerge/physics/microwave/touchstone.py +140 -0
  52. emerge/_emerge/plot/__init__.py +0 -0
  53. emerge/_emerge/plot/display.py +394 -0
  54. emerge/_emerge/plot/grapher.py +93 -0
  55. emerge/_emerge/plot/matplotlib/mpldisplay.py +264 -0
  56. emerge/_emerge/plot/pyvista/__init__.py +1 -0
  57. emerge/_emerge/plot/pyvista/display.py +931 -0
  58. emerge/_emerge/plot/pyvista/display_settings.py +24 -0
  59. emerge/_emerge/plot/simple_plots.py +551 -0
  60. emerge/_emerge/plot.py +225 -0
  61. emerge/_emerge/projects/__init__.py +0 -0
  62. emerge/_emerge/projects/_gen_base.txt +32 -0
  63. emerge/_emerge/projects/_load_base.txt +24 -0
  64. emerge/_emerge/projects/generate_project.py +40 -0
  65. emerge/_emerge/selection.py +596 -0
  66. emerge/_emerge/simmodel.py +444 -0
  67. emerge/_emerge/simulation_data.py +411 -0
  68. emerge/_emerge/solver.py +993 -0
  69. emerge/_emerge/system.py +54 -0
  70. emerge/cli.py +19 -0
  71. emerge/lib.py +1 -1
  72. emerge/plot.py +1 -1
  73. {emerge-0.4.7.dist-info → emerge-0.4.8.dist-info}/METADATA +1 -1
  74. emerge-0.4.8.dist-info/RECORD +78 -0
  75. emerge-0.4.8.dist-info/entry_points.txt +2 -0
  76. emerge-0.4.7.dist-info/RECORD +0 -9
  77. emerge-0.4.7.dist-info/entry_points.txt +0 -2
  78. {emerge-0.4.7.dist-info → emerge-0.4.8.dist-info}/WHEEL +0 -0
@@ -0,0 +1,411 @@
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
+ import numpy as np
20
+ from loguru import logger
21
+ from typing import TypeVar, Generic, Any, List, Union, Dict
22
+ from collections import defaultdict
23
+
24
+ T = TypeVar("T")
25
+ M = TypeVar("M")
26
+
27
+ def assemble_nd_data(
28
+ data_in: List[Union[float, np.ndarray]],
29
+ vars_in: List[Dict[str, float]],
30
+ axes: Dict[str, List[float]],
31
+ ) -> np.ndarray:
32
+ """
33
+ Assemble a flat list of data entries into an N-dimensional array
34
+ based on provided axis definitions.
35
+
36
+ Parameters
37
+ ----------
38
+ data_in : List[float or np.ndarray]
39
+ Flat list of data entries. Scalars are treated as shape ().
40
+ vars_in : List[Dict[str, float]]
41
+ List of dictionaries mapping variable names to their value for each data entry.
42
+ Length must match data_in.
43
+ axes : Dict[str, List[float]]
44
+ Mapping from variable name to list of allowed values along that axis.
45
+ If 'freq' is a key, its axis will be placed last.
46
+
47
+ Returns
48
+ -------
49
+ np.ndarray
50
+ An array of shape (*axis_lengths, *shp_out), where axis_lengths correspond
51
+ to lengths of each axis in `axes` (with 'freq' last if present) and
52
+ shp_out is the shape of the first ndarray in data_in or () if all scalars.
53
+ """
54
+ # Determine axis ordering
55
+ axis_names = sorted(list(axes.keys()))
56
+ if 'freq' in axis_names:
57
+ axis_names.append(axis_names.pop(axis_names.index('freq')))
58
+ axis_lengths = [len(axes[name]) for name in axis_names]
59
+
60
+ # Determine output shape suffix
61
+ # Assume all array entries share the same shape
62
+ first = next((d for d in data_in if isinstance(d, np.ndarray)), None)
63
+ shp_out = first.shape if isinstance(first, np.ndarray) else ()
64
+
65
+ # Initialize output array
66
+ out_shape = tuple(axis_lengths) + shp_out
67
+ out = np.empty(out_shape, dtype=first.dtype if isinstance(first, np.ndarray) else float)
68
+
69
+ # Fill in data
70
+ for entry, var_map in zip(data_in, vars_in):
71
+ # build index for each axis
72
+ idx = tuple(
73
+ axes[name].index(var_map[name])
74
+ for name in axis_names
75
+ )
76
+ if isinstance(entry, np.ndarray):
77
+ out[idx + tuple(slice(None) for _ in shp_out)] = entry
78
+ else:
79
+ out[idx] = entry
80
+
81
+ return out
82
+
83
+ def generate_ndim(
84
+ outer_data: dict[str, list[float]],
85
+ inner_data: list[float],
86
+ outer_labels: tuple[str, ...]
87
+ ) -> np.ndarray:
88
+ """
89
+ Generates an N-dimensional grid of values from flattened data, and returns each axis array plus the grid.
90
+
91
+ Parameters
92
+ ----------
93
+ outer_data : dict of {label: flat list of coordinates}
94
+ Each key corresponds to one axis label, and the list contains coordinate values for each point.
95
+ inner_data : list of float
96
+ Flattened list of data values corresponding to each set of coordinates.
97
+ outer_labels : tuple of str
98
+ Order of axes (keys of outer_data) which defines the dimension order in the output array.
99
+
100
+ Returns
101
+ -------
102
+ *axes : np.ndarray
103
+ One 1D array for each axis, containing the sorted unique coordinates for that dimension,
104
+ in the order specified by outer_labels.
105
+ grid : np.ndarray
106
+ N-dimensional array of shape (n1, n2, ..., nN), where ni is the number of unique
107
+ values along the i-th axis. Missing points are filled with np.nan.
108
+ """
109
+ # Convert inner data to numpy array
110
+ values = np.asarray(inner_data)
111
+
112
+ # Determine unique sorted coordinates for each axis
113
+ axes = [np.unique(np.asarray(outer_data[label])) for label in outer_labels]
114
+ grid_shape = tuple(axis.size for axis in axes)
115
+
116
+ # Initialize grid with NaNs
117
+ grid = np.full(grid_shape, np.nan, dtype=values.dtype)
118
+
119
+ # Build coordinate arrays for each axis
120
+ coords = [np.asarray(outer_data[label]) for label in outer_labels]
121
+
122
+ # Map coordinates to indices in the grid for each axis
123
+ idxs = [np.searchsorted(axes[i], coords[i]) for i in range(len(axes))]
124
+
125
+ # Assign values into the grid
126
+ grid[tuple(idxs)] = values
127
+
128
+ # Return each axis array followed by the grid
129
+ return (*axes, grid)
130
+
131
+
132
+ class DataEntry:
133
+
134
+ def __init__(self, vars: dict[str, float]):
135
+ self.vars: dict[str, float] = vars
136
+ self.data: dict[str, Any] = dict()
137
+
138
+ def print(self) -> None:
139
+ """Print the content of the DataEntry object"""
140
+ for key, value in self.data.items():
141
+ print(f' {key} = {value}')
142
+
143
+ def values(self) -> list[Any]:
144
+ """ Return all values stored in the DataEntry"""
145
+ return self.data.values()
146
+
147
+ def keys(self) -> list[str]:
148
+ """ Return all names of data stored in the DataEntry"""
149
+ return list(self.data.keys())
150
+
151
+ def items(self) -> list[tuple[str, Any]]:
152
+ """ Returns a list of all key: value pairs of the DataEntry."""
153
+
154
+ def __eq__(self, other: dict[str, float]) -> bool:
155
+ allkeys = set(list(self.vars.keys()) + list(other.keys()))
156
+ return all(self.vars[key]==other[key] for key in allkeys)
157
+
158
+ def _dist(self, other: dict[str, float]) -> bool:
159
+ return sum([(abs(self.vars.get(key,1e20)-other[key])/other[key]) for key in other.keys()])
160
+
161
+ def __getitem__(self, key) -> Any:
162
+ return self.data[key]
163
+
164
+ def __setitem__(self, key: str, value: Any) -> None:
165
+ self.data[key] = value
166
+
167
+
168
+ class DataContainer:
169
+ """The DataContainer class is a generalized class to store data for a set of parameter sweeps"""
170
+ def __init__(self):
171
+ self.entries: list[DataEntry] = []
172
+ self.stock: DataEntry = DataEntry({})
173
+
174
+
175
+ def print(self) -> None:
176
+ """ Print an overview of all data in the DataContainer"""
177
+ self.stock.print()
178
+ for entry in self.entries:
179
+ entry.print()
180
+
181
+ def new(self, **vars: float) -> DataEntry:
182
+ """Create a new entry in the DataContainer for the given value setting"""
183
+ entry = DataEntry(vars)
184
+ self.entries.append(entry)
185
+ return entry
186
+
187
+ @property
188
+ def last(self) -> DataEntry:
189
+ """Returns the last added entry"""
190
+ return self.entries[-1]
191
+
192
+ @property
193
+ def default(self) -> DataEntry:
194
+ """Returns the default DataEntry which is either the last from the parameter sweep or the general one in case of no parameter sweep."""
195
+ if not self.entries:
196
+ return self.stock
197
+ else:
198
+ return self.last
199
+
200
+ def select(self, **vars: float) -> DataEntry | None:
201
+ """Returns the data entry corresponding to the provided parametric sweep set"""
202
+ for entry in self.entries:
203
+ if entry==vars:
204
+ return entry
205
+ return None
206
+
207
+ def find(self, **vars: float) -> DataEntry:
208
+ """Returns the DataEntry closest to the provided parametric sweep setting."""
209
+ return sorted([(entry, entry._dist(vars)) for entry in self.entries], key=lambda x: x[1])[0][0]
210
+
211
+ def __getitem__(self, key: str) -> DataEntry:
212
+ """Returns the requested item from the default DataEntry"""
213
+ return self.default[key]
214
+
215
+ def __setitem__(self, key: str, value: Any) -> None:
216
+ """Writes a value to the requested default DataEntry"""
217
+ self.default[key] = value
218
+
219
+
220
+ class BaseDataset(Generic[T,M]):
221
+ def __init__(self, datatype: T, matrixtype: M, scalar: bool):
222
+ self._datatype: type[T] = datatype
223
+ self._matrixtype: type[M] = matrixtype
224
+ self._variables: list[dict[str, float]] = []
225
+ self._data_entries: list[T] = []
226
+ self._scalar: bool = scalar
227
+
228
+ self._gritted: bool = None
229
+ self._axes: dict[str, np.ndarray] = None
230
+ self._ax_ids: dict[str, int] = None
231
+ self._ids: np.ndarray = None
232
+ self._gridobj: M = None
233
+
234
+ self._data: dict[str, Any] = dict()
235
+
236
+ def __getitem__(self, index: int) -> T:
237
+ return self._data_entries[index]
238
+
239
+ @property
240
+ def _fields(self) -> list[str]:
241
+ return self._datatype._fields
242
+
243
+ @property
244
+ def _copy(self) -> list[str]:
245
+ return self._datatype._copy
246
+
247
+ def store(self, key: str, value: Any) -> None:
248
+ """Stores a variable with some value in the provided key.
249
+ Make sure that all values passed are picklable.
250
+
251
+ Args:
252
+ key (str): The name for the data entry
253
+ value (Any): The value of the data entry
254
+ """
255
+ self._data[key] = value
256
+
257
+ def load(self, key: str) -> Any | None:
258
+ """Returns the data entry for a given key
259
+
260
+ Args:
261
+ key (str): The name of the data entry
262
+
263
+ Returns:
264
+ Any: The value of the data entry
265
+ """
266
+ return self._data.get(key, None)
267
+
268
+ def get_entry(self, index: int) -> T:
269
+ """Returns the physics dataset for the given index
270
+
271
+ Args:
272
+ index (int): The index of the solution
273
+
274
+ Returns:
275
+ T: The Physics dataset
276
+ """
277
+ return self._data_entries[index]
278
+
279
+ def select(self, **variables: float) -> T:
280
+ """Returns the first physics dataset that satisfies the variable assignemnt.
281
+
282
+ Returns:
283
+ T: The physics dataset
284
+ """
285
+ index = next(
286
+ i
287
+ for i, var_map in enumerate(self._variables)
288
+ if all(var_map.get(k) == v for k, v in variables.items())
289
+ )
290
+ return self.get_entry(index)
291
+
292
+ def filter(self, **variables: float) -> list[T]:
293
+ """Returns a list of all physics datasets that are valid for the given variable assignment
294
+
295
+ Returns:
296
+ list[T]: A list of all matching datasets
297
+ """
298
+ output = []
299
+ for i, var_map in enumerate(self._variables):
300
+ if all(var_map.get(k) == v for k, v in variables.items()):
301
+ output.append(self.get_entry(i))
302
+ return output
303
+
304
+ def find(self, **variables: float) -> T:
305
+ """Returns the physics dataset that is closest to the constraint given by the variables.
306
+
307
+ Returns:
308
+ T: The physics dataset.
309
+ """
310
+ output = []
311
+ for i, var_map in enumerate(self._variables):
312
+ error = sum([abs(var_map.get(k, 1e30) - v) for k, v in variables.items()])
313
+ output.append((i,error))
314
+ return self.get_entry(sorted(output, key=lambda x:x[1])[0][0])
315
+
316
+ def axis(self, name: str) -> np.ndarray:
317
+ """Returns a sorted list of all variables for the given name
318
+
319
+ Args:
320
+ name (str): The name of the variable axis
321
+
322
+ Returns:
323
+ np.ndarray: A sorted list of all unique values.
324
+ """
325
+ return np.sort(np.unique(np.array([var[name] for var in self._variables])))
326
+
327
+ def new(self, **vars: float) -> T:
328
+ """Creates a new dataset
329
+
330
+ Returns:
331
+ T: The physics dataset object
332
+ """
333
+ self._variables.append(vars)
334
+ new_entry = self._datatype()
335
+ self._data_entries.append(new_entry)
336
+ return new_entry
337
+
338
+ def _grid_axes(self) -> None:
339
+ """This method attepmts to create a gritted version of the scalar dataset
340
+
341
+ Returns:
342
+ None
343
+ """
344
+ logger.debug('Attempting to grid simulation data')
345
+ variables = defaultdict(set)
346
+ for var in self._variables:
347
+ for key, value in var.items():
348
+ variables[key].add(value)
349
+ N_entries = len(self._variables)
350
+ N_prod = 1
351
+ N_dim = len(variables)
352
+ for key, val_list in variables.items():
353
+ N_prod *= len(val_list)
354
+
355
+ if N_entries == N_prod:
356
+ logger.debug('Multi-dimensional grid found!')
357
+ self._gritted = True
358
+ else:
359
+ logger.debug('Multi-dimensional grid not found')
360
+ self._gritted = False
361
+ return False
362
+
363
+ self._axes = dict()
364
+ self._ax_ids = dict()
365
+ revax = dict()
366
+ i = 0
367
+ for key, val_set in variables.items():
368
+ self._axes[key] = np.sort(np.array(list(val_set)))
369
+ self._ax_ids[key] = i
370
+ revax[i] = key
371
+ i += 1
372
+
373
+ axlist = []
374
+
375
+ for idim in range(N_dim):
376
+ axlist.append(self._axes[revax[idim]])
377
+
378
+ indices = np.arange(N_entries)
379
+ Ndimlist = [len(dim) for dim in axlist]
380
+ self._ids = indices.reshape(Ndimlist)
381
+
382
+ obj = self._matrixtype()
383
+
384
+ axes_list = {key: list(self._axes[key]) for key in self._axes.keys()}
385
+ for field in self._fields:
386
+ data_field = [self._data_entries[i].__dict__[field] for i in range(N_entries)]
387
+ data_set = assemble_nd_data(data_field, self._variables, axes_list)
388
+ obj.__setattr__(field, data_set)
389
+ for copyfield in self._copy:
390
+
391
+ obj.__setattr__(copyfield, self._data_entries[0].__dict__[copyfield])
392
+ self._gridobj = obj
393
+
394
+ return True
395
+
396
+ @property
397
+ def grid(self) -> M:
398
+ """Returns the gridded version of the scalar dataset.
399
+
400
+ Raises:
401
+ ValueError: _description_
402
+
403
+ Returns:
404
+ M: The gritted physics dataset
405
+ """
406
+ if self._gritted is None:
407
+ self._grid_axes()
408
+ if self._gritted is False:
409
+ logger.error('The dataset cannot be cast to a structured grid.')
410
+ raise ValueError('Data not in regular grid')
411
+ return self._gridobj