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.
- emerge/__init__.py +54 -0
- emerge/__main__.py +5 -0
- 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 +57 -0
- emerge/plot.py +1 -0
- emerge/pyvista.py +1 -0
- {emerge-0.4.6.dist-info → emerge-0.4.8.dist-info}/METADATA +1 -1
- emerge-0.4.8.dist-info/RECORD +78 -0
- emerge-0.4.8.dist-info/entry_points.txt +2 -0
- emerge-0.4.6.dist-info/RECORD +0 -4
- emerge-0.4.6.dist-info/entry_points.txt +0 -2
- {emerge-0.4.6.dist-info → emerge-0.4.8.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,449 @@
|
|
|
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 typing import Callable, Iterable, Any
|
|
21
|
+
from .shapes import Box, Sphere, Cyllinder, CoaxCyllinder, Alignment
|
|
22
|
+
from ..geometry import GeoVolume, GeoObject, GeoSurface
|
|
23
|
+
from .operations import rotate, mirror, translate, add, subtract, embed
|
|
24
|
+
from numbers import Number
|
|
25
|
+
from functools import reduce
|
|
26
|
+
from operator import mul
|
|
27
|
+
from ..cs import CoordinateSystem, GCS
|
|
28
|
+
from .polybased import XYPolygon
|
|
29
|
+
|
|
30
|
+
def get_flat_index(indices, shape):
|
|
31
|
+
flat_index = 0
|
|
32
|
+
for i, idx in enumerate(indices):
|
|
33
|
+
stride = reduce(mul, shape[i+1:], 1)
|
|
34
|
+
flat_index += idx * stride
|
|
35
|
+
return flat_index
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Series:
|
|
39
|
+
|
|
40
|
+
def __init__(self, values: Iterable[Number]):
|
|
41
|
+
self.values: list[Number] = list(values)
|
|
42
|
+
self.N: int = len(self.values)
|
|
43
|
+
|
|
44
|
+
def __len__(self) -> int:
|
|
45
|
+
return self.N
|
|
46
|
+
|
|
47
|
+
def __getitem__(self, index: int):
|
|
48
|
+
return self.values[index]
|
|
49
|
+
|
|
50
|
+
def get_count(args) -> int:
|
|
51
|
+
N = 1
|
|
52
|
+
for arg in args:
|
|
53
|
+
if isinstance(arg,Series):
|
|
54
|
+
N = max(N, arg.N)
|
|
55
|
+
return N
|
|
56
|
+
elif isinstance(arg, Iterable):
|
|
57
|
+
return get_count(arg)
|
|
58
|
+
return 1
|
|
59
|
+
|
|
60
|
+
def get_item(value, n: int):
|
|
61
|
+
if isinstance(value, Number):
|
|
62
|
+
return value
|
|
63
|
+
elif isinstance(value, Series):
|
|
64
|
+
return value[n]
|
|
65
|
+
elif isinstance(value, Iterable):
|
|
66
|
+
typ = type(value)
|
|
67
|
+
return typ([get_item(v,n) for v in value])
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
def unpack(*args):
|
|
71
|
+
output = []
|
|
72
|
+
N = get_count(args)
|
|
73
|
+
return tuple(zip(*[[get_item(a,n) for a in args] for n in range(N)]))
|
|
74
|
+
|
|
75
|
+
ObjectTransformer = Callable[[GeoObject,], GeoObject]
|
|
76
|
+
|
|
77
|
+
class PartialFunction:
|
|
78
|
+
|
|
79
|
+
def __init__(self,
|
|
80
|
+
function: Callable,
|
|
81
|
+
identity: bool = False,
|
|
82
|
+
permute: bool = False,
|
|
83
|
+
**kwargs):
|
|
84
|
+
self.f: Callable = function
|
|
85
|
+
self.kwargs: dict[str, float | Series] = kwargs
|
|
86
|
+
self.identity: bool = identity
|
|
87
|
+
self.permute: bool = permute
|
|
88
|
+
self.N: int = 1
|
|
89
|
+
self.kwargset: list[dict] = None
|
|
90
|
+
|
|
91
|
+
for key, value in self.kwargs.items():
|
|
92
|
+
if not isinstance(value, Series):
|
|
93
|
+
continue
|
|
94
|
+
self.N = max(self.N, value.N)
|
|
95
|
+
|
|
96
|
+
# Include the identity operation
|
|
97
|
+
if self.identity:
|
|
98
|
+
self.N = self.N + 1
|
|
99
|
+
|
|
100
|
+
def _compile(self) -> None:
|
|
101
|
+
kwargset = []
|
|
102
|
+
N = self.N
|
|
103
|
+
|
|
104
|
+
if self.identity:
|
|
105
|
+
kwargset.append(None)
|
|
106
|
+
N = N - 1
|
|
107
|
+
|
|
108
|
+
for n in range(N):
|
|
109
|
+
kwargs = dict()
|
|
110
|
+
for key,value in self.kwargs.items():
|
|
111
|
+
if not isinstance(value, Series):
|
|
112
|
+
kwargs[key] = value
|
|
113
|
+
else:
|
|
114
|
+
kwargs[key] = value[n]
|
|
115
|
+
kwargset.append(kwargs)
|
|
116
|
+
self.kwargset = kwargset
|
|
117
|
+
|
|
118
|
+
def __call__(self, objects: list[GeoObject], index: int) -> list[GeoObject]:
|
|
119
|
+
|
|
120
|
+
kwargs = self.kwargset[index]
|
|
121
|
+
if kwargs is None:
|
|
122
|
+
return objects
|
|
123
|
+
|
|
124
|
+
objects_out = []
|
|
125
|
+
for obj in objects:
|
|
126
|
+
objects_out.append(self.f(obj, **kwargs))
|
|
127
|
+
return objects_out
|
|
128
|
+
|
|
129
|
+
class NDimContainer:
|
|
130
|
+
|
|
131
|
+
def __init__(self):
|
|
132
|
+
self.items: list= []
|
|
133
|
+
self.dimensions: list[tuple[int,int]] = []
|
|
134
|
+
self.expanded_dims: list[tuple[int,int,int]] = []
|
|
135
|
+
self.map: np.ndarray = []
|
|
136
|
+
self.Ncopies: int = 1
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def Ntot(self) -> int:
|
|
140
|
+
"""The Total number of different objects this NDimContainer acts on.
|
|
141
|
+
"""
|
|
142
|
+
n = self.Ncopies
|
|
143
|
+
for i,N in self.dimensions:
|
|
144
|
+
n = n*N
|
|
145
|
+
|
|
146
|
+
return n
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def Nop(self) -> int:
|
|
150
|
+
"""The number of operations corresponding to this NDimContainer
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
int: The number of operations this container can support.
|
|
154
|
+
"""
|
|
155
|
+
n = 1
|
|
156
|
+
for i,N in self.dimensions:
|
|
157
|
+
n = n*N
|
|
158
|
+
|
|
159
|
+
return n
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def dimtup(self) -> tuple[int]:
|
|
163
|
+
"""A dimension tuple containing the dimensions of the NDimContainer
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
tuple[int]: A tuple of dimension sizes to be used in np.zeros(shape)
|
|
167
|
+
"""
|
|
168
|
+
dimtup = tuple([dim[1] for dim in self.dimensions])
|
|
169
|
+
if self.Ncopies:
|
|
170
|
+
dimtup = dimtup + (self.Ncopies,)
|
|
171
|
+
return dimtup
|
|
172
|
+
|
|
173
|
+
def _init(self) -> None:
|
|
174
|
+
"""Initialization function to set the index map and expanded dimension list.
|
|
175
|
+
"""
|
|
176
|
+
i = 0
|
|
177
|
+
for i,(ncopies,N) in enumerate(self.dimensions):
|
|
178
|
+
for n in range(ncopies):
|
|
179
|
+
self.expanded_dims.append((i,n,N))
|
|
180
|
+
|
|
181
|
+
if self.Ncopies > 1:
|
|
182
|
+
self.expanded_dims.append((i+1, (1, self.Ncopies)))
|
|
183
|
+
|
|
184
|
+
self.map = np.arange(self.Ntot).reshape(self.dimtup)
|
|
185
|
+
|
|
186
|
+
def add_dim(self, N: int, same_axis: bool = False) -> None:
|
|
187
|
+
"""Aads a new dimension to iterate over.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
N (int): The size of the dimension (number of differnt items contained)
|
|
191
|
+
same_axis (bool, optional): Defines that the next iteration dimension is actually parallel
|
|
192
|
+
to the last existing dimension. In this case N must be equal to the size of the last defined dimension. Defaults to False.
|
|
193
|
+
|
|
194
|
+
Raises:
|
|
195
|
+
ValueError: An error if the provided dimension is not correct.
|
|
196
|
+
"""
|
|
197
|
+
if same_axis:
|
|
198
|
+
if N != self.dimensions[-1][1]:
|
|
199
|
+
raise ValueError('Trying to add a dimension with the same size as previous but the sizes are different.' +
|
|
200
|
+
f'The provided size is {N} but the last dimensions is {self.dimensions[-1][1]}')
|
|
201
|
+
self.dimensions[-1] = (self.dimensions[-1][0]+1, N)
|
|
202
|
+
else:
|
|
203
|
+
self.dimensions.append((1,N))
|
|
204
|
+
|
|
205
|
+
def set_copies(self, N: int) -> None:
|
|
206
|
+
"""Define how many original copies of the new object will be created.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
N (int): The number of copies.
|
|
210
|
+
"""
|
|
211
|
+
self.Ncopies = N
|
|
212
|
+
|
|
213
|
+
def get(self,
|
|
214
|
+
dim_ex_dim: int,
|
|
215
|
+
number: int) -> list:
|
|
216
|
+
dimindex, niter, Nvariations = self.expanded_dims[dim_ex_dim]
|
|
217
|
+
slclist = [slice(None) for _ in self.dimensions]
|
|
218
|
+
slclist[dimindex] = number
|
|
219
|
+
return list(self.map[tuple(slclist)].flatten())
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
class Modeler:
|
|
223
|
+
|
|
224
|
+
def __init__(self):
|
|
225
|
+
## Function importing
|
|
226
|
+
self.rotate = rotate
|
|
227
|
+
self.add = add
|
|
228
|
+
self.translate = translate
|
|
229
|
+
self.remove = subtract
|
|
230
|
+
self.subtract = subtract
|
|
231
|
+
self.embed = embed
|
|
232
|
+
self.mirror = mirror
|
|
233
|
+
|
|
234
|
+
self.pre_transforms: list[PartialFunction] = []
|
|
235
|
+
self.post_transforms: list[PartialFunction] = []
|
|
236
|
+
self.last_object: GeoObject = None
|
|
237
|
+
self.ndimcont: NDimContainer = NDimContainer()
|
|
238
|
+
self._and: bool = False
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def AND(self) -> Modeler:
|
|
242
|
+
"""This method chain property ensures that the following transformation will be merged
|
|
243
|
+
with the previous one. For example, a rotation plus a translation with AND in between
|
|
244
|
+
both containing three operations will merge their operations so that each single value
|
|
245
|
+
is paired up with the next.
|
|
246
|
+
|
|
247
|
+
For example, if you wish to create a three boxes that are rotated in increments of 90
|
|
248
|
+
degrees and traslated by 1, 2 and 3 meters respectively (90deg, 1m) (180deg, 2m) and (270deg, 3m)
|
|
249
|
+
you would do:
|
|
250
|
+
|
|
251
|
+
>>> m.rotated(...,..., m.Series(90,180,270)).AND.translated(m.Series(1,2,3),...)
|
|
252
|
+
|
|
253
|
+
If you would not use AND, the operations are permuted resulting in 9 boxes.
|
|
254
|
+
"""
|
|
255
|
+
self._and = True
|
|
256
|
+
return self
|
|
257
|
+
|
|
258
|
+
def series(self, *values: Number) -> Series:
|
|
259
|
+
"""Returns a Series object to represent a series of values instead of a single value.
|
|
260
|
+
|
|
261
|
+
A series of values can be used at any argument that accepts Series objects. If such a
|
|
262
|
+
variable is provided as a series, the modeler will execute multiple iterations of the
|
|
263
|
+
object or transformation for each value in the series.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Series: The generated Series object.
|
|
267
|
+
|
|
268
|
+
Example:
|
|
269
|
+
>>> modeler.box(modeler.Series(1,2,3), 2, 3)
|
|
270
|
+
|
|
271
|
+
"""
|
|
272
|
+
return Series(values)
|
|
273
|
+
|
|
274
|
+
def nvars(self) -> int:
|
|
275
|
+
return self.ndimcont.Nop
|
|
276
|
+
|
|
277
|
+
def _merge_object(self, objs: list[GeoObject]) -> GeoObject:
|
|
278
|
+
"""Merges a list of GeoObjects into a single GeoObject.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
objs (list[GeoObject]): A list of GMSH objects
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
GeoObject: A single GeoObject
|
|
285
|
+
"""
|
|
286
|
+
tags = []
|
|
287
|
+
for obj in objs:
|
|
288
|
+
tags.extend(obj.tags)
|
|
289
|
+
gmshobj = GeoObject.from_dimtags(objs[0].dim, tags)
|
|
290
|
+
for obj in objs:
|
|
291
|
+
gmshobj._take_tools(obj)
|
|
292
|
+
return gmshobj
|
|
293
|
+
|
|
294
|
+
def _clean(self) -> None:
|
|
295
|
+
self.pre_transforms = []
|
|
296
|
+
self._and = False
|
|
297
|
+
self._combine = False
|
|
298
|
+
|
|
299
|
+
def _add_dim(self, N: int) -> None:
|
|
300
|
+
self.ndimcont.add_dim(N, self._and)
|
|
301
|
+
self._and = False
|
|
302
|
+
|
|
303
|
+
def _add_function(self, function: PartialFunction):
|
|
304
|
+
self._add_dim(function.N)
|
|
305
|
+
self.pre_transforms.append(function)
|
|
306
|
+
|
|
307
|
+
def _apply_presteps(self, objects: list[GeoObject]) -> list[GeoObject]:
|
|
308
|
+
self.ndimcont._init()
|
|
309
|
+
for func in self.pre_transforms:
|
|
310
|
+
func._compile()
|
|
311
|
+
if not self.pre_transforms:
|
|
312
|
+
return objects
|
|
313
|
+
|
|
314
|
+
for i, function in enumerate(self.pre_transforms):
|
|
315
|
+
for n in range(function.N):
|
|
316
|
+
ids = self.ndimcont.get(i,n)
|
|
317
|
+
sel = [objects[i] for i in ids]
|
|
318
|
+
objects_out = function(sel, n)
|
|
319
|
+
for j, obj in zip(ids, objects_out):
|
|
320
|
+
objects[j] = obj
|
|
321
|
+
|
|
322
|
+
self._clean()
|
|
323
|
+
return objects
|
|
324
|
+
|
|
325
|
+
def rotated(self,
|
|
326
|
+
c0: tuple[float, float, float],
|
|
327
|
+
ax: tuple[float, float, float],
|
|
328
|
+
angle: float | Series,
|
|
329
|
+
) -> Modeler:
|
|
330
|
+
"""Adds a rotation task to the geometry builder.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
c0 (tuple[float, float, float]): The origin at which the rotation axis is placed.
|
|
334
|
+
ax (tuple[float, float, float]): The Rotation axis to be used
|
|
335
|
+
angle (float | Series): The angles or series of angles provided in degrees.
|
|
336
|
+
|
|
337
|
+
Returns:
|
|
338
|
+
Modeler: The same modeler instance
|
|
339
|
+
"""
|
|
340
|
+
function = PartialFunction(rotate, c0=c0, ax=ax, angle=angle)
|
|
341
|
+
self._add_function(function)
|
|
342
|
+
return self
|
|
343
|
+
|
|
344
|
+
def translated(self,
|
|
345
|
+
dx: float | Series,
|
|
346
|
+
dy: float | Series,
|
|
347
|
+
dz: float | Series) -> Modeler:
|
|
348
|
+
"""Adds a translation task to the geometry builder pipeline.
|
|
349
|
+
The dx, dy, dz may either be a single displacement coordinate or a series of them in the
|
|
350
|
+
shape of a series object.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
dx (float | Series): The x-displacement
|
|
354
|
+
dy (float | Series): The y-displacement
|
|
355
|
+
dz (float | Series): The z-displacement
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
Modeler: The Modeler object
|
|
359
|
+
"""
|
|
360
|
+
function = PartialFunction(translate, dx=dx, dy=dy, dz=dz)
|
|
361
|
+
self._add_function(function)
|
|
362
|
+
return self
|
|
363
|
+
|
|
364
|
+
def mirrored(self,
|
|
365
|
+
origin: tuple[float, float, float],
|
|
366
|
+
direction: tuple[float, float, float],
|
|
367
|
+
keep_original: bool = True) -> Modeler:
|
|
368
|
+
"""Adds a mirror transformation to the geometry builder pipeline.
|
|
369
|
+
|
|
370
|
+
Args:
|
|
371
|
+
origin (tuple[float, float, float]): The origin of the mirror axis plane
|
|
372
|
+
direction (tuple[float, float, float]): The mirror direction which is normal to the plane of reflection.
|
|
373
|
+
keep_original (bool, optional): Whether to keep the original object. Defaults to True.
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
Modeler: The Modeler object
|
|
377
|
+
"""
|
|
378
|
+
function = PartialFunction(mirror,
|
|
379
|
+
identity=keep_original,
|
|
380
|
+
origin=origin,
|
|
381
|
+
direction=direction)
|
|
382
|
+
self._add_function(function)
|
|
383
|
+
return self
|
|
384
|
+
|
|
385
|
+
def cyllinder(self,
|
|
386
|
+
radius: float | Series,
|
|
387
|
+
height: float | Series,
|
|
388
|
+
position: tuple[float | Series, float | Series, float | Series],
|
|
389
|
+
cs: CoordinateSystem = GCS,
|
|
390
|
+
merge: bool = False,
|
|
391
|
+
NPoly: int = False):
|
|
392
|
+
|
|
393
|
+
N_objects = self.nvars()
|
|
394
|
+
Rs, Hs, Ps = unpack(radius, height, position)
|
|
395
|
+
N = len(Rs)
|
|
396
|
+
cyls = []
|
|
397
|
+
|
|
398
|
+
for _ in range(N_objects):
|
|
399
|
+
for r,h,p in zip(Rs, Hs, Ps):
|
|
400
|
+
cs2 = cs.displace(p[0], p[1], p[2])
|
|
401
|
+
if NPoly:
|
|
402
|
+
cyl = XYPolygon.circle(r, Nsections=NPoly).extrude(h, cs2)
|
|
403
|
+
else:
|
|
404
|
+
cyl = Cyllinder(r,h, cs2)
|
|
405
|
+
cyls.append(cyl)
|
|
406
|
+
|
|
407
|
+
self.ndimcont.set_copies(N)
|
|
408
|
+
self._apply_presteps(cyls)
|
|
409
|
+
|
|
410
|
+
if merge:
|
|
411
|
+
cyls = self._merge_object(cyls)
|
|
412
|
+
|
|
413
|
+
return cyls
|
|
414
|
+
|
|
415
|
+
def box(self, width: float | Series,
|
|
416
|
+
depth: float | Series,
|
|
417
|
+
height: float | Series,
|
|
418
|
+
position: tuple[float | Series, float | Series, float | Series],
|
|
419
|
+
alignment: Alignment = Alignment.CORNER,
|
|
420
|
+
merge: bool = False) -> list[GeoVolume] | GeoVolume:
|
|
421
|
+
"""Create a box object which will be transformed by the transformation pipeline.
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
width (float): The box's width (X direction)
|
|
425
|
+
depth (float): The box's depth (Y direction)
|
|
426
|
+
height (float): The box's height (Z direction)
|
|
427
|
+
position (tuple[float, float, float]): The position of the box object.
|
|
428
|
+
alignment (Alignment, optional): Where to alight the box. Defaults to Alignment.CORNER.
|
|
429
|
+
merge (bool): Whether to merge the final result into a single GeoVolume object.
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
list[GeoVolume]: A list of GeoVolume objects for each box.
|
|
433
|
+
"""
|
|
434
|
+
N_objects = self.nvars()
|
|
435
|
+
Ws, Ds, Hs, Ps = unpack(width, depth, height, position)
|
|
436
|
+
N = len(Ws)
|
|
437
|
+
boxes = []
|
|
438
|
+
|
|
439
|
+
for _ in range(N_objects):
|
|
440
|
+
for w, d, h, p in zip(Ws, Ds, Hs, Ps):
|
|
441
|
+
box = Box(w,d,h, position=p, alignment=alignment)
|
|
442
|
+
boxes.append(box)
|
|
443
|
+
self.ndimcont.set_copies(N)
|
|
444
|
+
self._apply_presteps(boxes)
|
|
445
|
+
|
|
446
|
+
if merge:
|
|
447
|
+
boxes = self._merge_object(boxes)
|
|
448
|
+
|
|
449
|
+
return boxes
|
|
@@ -0,0 +1,254 @@
|
|
|
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 typing import TypeVar
|
|
19
|
+
from ..geometry import GeoSurface, GeoVolume
|
|
20
|
+
from ..cs import CoordinateSystem, GCS
|
|
21
|
+
import gmsh
|
|
22
|
+
import numpy as np
|
|
23
|
+
|
|
24
|
+
T = TypeVar('T', GeoSurface, GeoVolume)
|
|
25
|
+
|
|
26
|
+
def _gen_mapping(obj_in, obj_out) -> dict:
|
|
27
|
+
tag_mapping: dict[int, dict] = {0: dict(),
|
|
28
|
+
1: dict(),
|
|
29
|
+
2: dict(),
|
|
30
|
+
3: dict()}
|
|
31
|
+
for domain, mapping in zip(obj_in, obj_out):
|
|
32
|
+
tag_mapping[domain[0]][domain[1]] = [o[1] for o in mapping]
|
|
33
|
+
return tag_mapping
|
|
34
|
+
|
|
35
|
+
def add(main: T, tool: T,
|
|
36
|
+
remove_object: bool = True,
|
|
37
|
+
remove_tool: bool = True) -> T:
|
|
38
|
+
''' Adds two GMSH objects together, returning a new object that is the union of the two.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
main : GeoSurface | GeoVolume
|
|
43
|
+
tool : GeoSurface | GeoVolume
|
|
44
|
+
remove_object : bool, optional
|
|
45
|
+
If True, the main object will be removed from the model after the operation. Default is True.
|
|
46
|
+
remove_tool : bool, optional
|
|
47
|
+
If True, the tool object will be removed from the model after the operation. Default is True.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
GeoSurface | GeoVolume
|
|
52
|
+
A new object that is the union of the main and tool objects.
|
|
53
|
+
'''
|
|
54
|
+
out_dim_tags, out_dim_tags_map = gmsh.model.occ.fuse(main.dimtags, tool.dimtags, removeObject=remove_object, removeTool=remove_tool)
|
|
55
|
+
if out_dim_tags[0][0] == 3:
|
|
56
|
+
output = GeoVolume([dt[1] for dt in out_dim_tags])._take_tools(tool,main)
|
|
57
|
+
elif out_dim_tags[0][0] == 2:
|
|
58
|
+
output = GeoSurface([dt[1] for dt in out_dim_tags])._take_tools(tool,main)
|
|
59
|
+
if remove_object:
|
|
60
|
+
main._exists = False
|
|
61
|
+
if remove_tool:
|
|
62
|
+
tool._exists = False
|
|
63
|
+
return output
|
|
64
|
+
|
|
65
|
+
def remove(main: T, tool: T,
|
|
66
|
+
remove_object: bool = True,
|
|
67
|
+
remove_tool: bool = True) -> T:
|
|
68
|
+
''' Subtractes a tool object GMSH from the main object, returning a new object that is the difference of the two.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
main : GeoSurface | GeoVolume
|
|
73
|
+
tool : GeoSurface | GeoVolume
|
|
74
|
+
remove_object : bool, optional
|
|
75
|
+
If True, the main object will be removed from the model after the operation. Default is True.
|
|
76
|
+
remove_tool : bool, optional
|
|
77
|
+
If True, the tool object will be removed from the model after the operation. Default is True.
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
GeoSurface | GeoVolume
|
|
82
|
+
A new object that is the difference of the main and tool objects.
|
|
83
|
+
'''
|
|
84
|
+
out_dim_tags, out_dim_tags_map = gmsh.model.occ.cut(main.dimtags, tool.dimtags, removeObject=remove_object, removeTool=remove_tool)
|
|
85
|
+
if out_dim_tags[0][0] == 3:
|
|
86
|
+
output = GeoVolume([dt[1] for dt in out_dim_tags])._take_tools(tool,main)
|
|
87
|
+
elif out_dim_tags[0][0] == 2:
|
|
88
|
+
output = GeoSurface([dt[1] for dt in out_dim_tags])._take_tools(tool,main)
|
|
89
|
+
if remove_object:
|
|
90
|
+
main._exists = False
|
|
91
|
+
if remove_tool:
|
|
92
|
+
tool._exists = False
|
|
93
|
+
return output
|
|
94
|
+
|
|
95
|
+
subtract = remove
|
|
96
|
+
|
|
97
|
+
def intersect(main: T, tool: T,
|
|
98
|
+
remove_object: bool = True,
|
|
99
|
+
remove_tool: bool = True) -> T:
|
|
100
|
+
''' Intersection of a tool object GMSH with the main object, returning a new object that is the intersection of the two.
|
|
101
|
+
|
|
102
|
+
Parameters
|
|
103
|
+
----------
|
|
104
|
+
main : GeoSurface | GeoVolume
|
|
105
|
+
tool : GeoSurface | GeoVolume
|
|
106
|
+
remove_object : bool, optional
|
|
107
|
+
If True, the main object will be removed from the model after the operation. Default is True.
|
|
108
|
+
remove_tool : bool, optional
|
|
109
|
+
If True, the tool object will be removed from the model after the operation. Default is True.
|
|
110
|
+
|
|
111
|
+
Returns
|
|
112
|
+
-------
|
|
113
|
+
GeoSurface | GeoVolume
|
|
114
|
+
A new object that is the difference of the main and tool objects.
|
|
115
|
+
'''
|
|
116
|
+
out_dim_tags, out_dim_tags_map = gmsh.model.occ.intersect(main.dimtags, tool.dimtags, removeObject=remove_object, removeTool=remove_tool)
|
|
117
|
+
if out_dim_tags[0][0] == 3:
|
|
118
|
+
output = GeoVolume([dt[1] for dt in out_dim_tags])._take_tools(tool,main)
|
|
119
|
+
elif out_dim_tags[0][0] == 2:
|
|
120
|
+
output = GeoSurface([dt[1] for dt in out_dim_tags])._take_tools(tool,main)
|
|
121
|
+
if remove_object:
|
|
122
|
+
main._exists = False
|
|
123
|
+
if remove_tool:
|
|
124
|
+
tool._exists = False
|
|
125
|
+
return output
|
|
126
|
+
|
|
127
|
+
def embed(main: GeoVolume, other: GeoSurface) -> None:
|
|
128
|
+
''' Embeds a surface into a volume in the GMSH model.
|
|
129
|
+
Parameters
|
|
130
|
+
----------
|
|
131
|
+
main : GeoVolume
|
|
132
|
+
The volume into which the surface will be embedded.
|
|
133
|
+
other : GeoSurface
|
|
134
|
+
The surface to be embedded into the volume.
|
|
135
|
+
|
|
136
|
+
Returns
|
|
137
|
+
-------
|
|
138
|
+
None
|
|
139
|
+
'''
|
|
140
|
+
gmsh.model.geo.synchronize()
|
|
141
|
+
gmsh.model.mesh.embed(other.dim, [other.tag,], main.dim, main.tags)
|
|
142
|
+
|
|
143
|
+
def rotate(main: GeoVolume,
|
|
144
|
+
c0: tuple[float, float, float],
|
|
145
|
+
ax: tuple[float, float, float],
|
|
146
|
+
angle: float,
|
|
147
|
+
degree=True) -> GeoVolume:
|
|
148
|
+
"""Rotates a GeoVolume object around an axist defined at a coordinate.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
main (GeoVolume): The object to rotate
|
|
152
|
+
c0 (tuple[float, float, float]): The point of origin for the rotation axis
|
|
153
|
+
ax (tuple[float, float, float]): A vector defining the rotation axis
|
|
154
|
+
angle (float): The angle in degrees (if degree is True)
|
|
155
|
+
degree (bool, optional): Whether to interpret the angle in degrees. Defaults to True.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
GeoVolume: The rotated GeoVolume object.
|
|
159
|
+
"""
|
|
160
|
+
if degree:
|
|
161
|
+
angle = angle * np.pi/180
|
|
162
|
+
gmsh.model.occ.rotate(main.dimtags, *c0, *ax, -angle)
|
|
163
|
+
|
|
164
|
+
# Rotate the facepointers
|
|
165
|
+
for fp in main._all_pointers:
|
|
166
|
+
fp.rotate(c0, ax, angle)
|
|
167
|
+
return main
|
|
168
|
+
|
|
169
|
+
def translate(main: GeoVolume,
|
|
170
|
+
dx: float = 0,
|
|
171
|
+
dy: float = 0,
|
|
172
|
+
dz: float = 0) -> GeoVolume:
|
|
173
|
+
"""Translates the GeoVolume object along a given displacement
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
main (GeoVolume): The object to translate
|
|
177
|
+
dx (float, optional): The X-displacement in meters. Defaults to 0.
|
|
178
|
+
dy (float, optional): The Y-displacement in meters. Defaults to 0.
|
|
179
|
+
dz (float, optional): The Z-displacement in meters. Defaults to 0.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
GeoVolume: The translated object
|
|
183
|
+
"""
|
|
184
|
+
gmsh.model.occ.translate(main.dimtags, dx, dy, dz)
|
|
185
|
+
|
|
186
|
+
# Rotate the facepointers
|
|
187
|
+
for fp in main._all_pointers:
|
|
188
|
+
fp.translate(dx, dy, dz)
|
|
189
|
+
|
|
190
|
+
return main
|
|
191
|
+
|
|
192
|
+
def mirror(main: GeoVolume,
|
|
193
|
+
origin: tuple[float, float, float] = (0.0, 0.0, 0.0),
|
|
194
|
+
direction: tuple[float, float, float] = (1.0, 0.0, 0.0),
|
|
195
|
+
make_copy: bool = True) -> GeoVolume:
|
|
196
|
+
"""Mirrors a GeoVolume object along a miror plane defined by a direction originating at a point
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
main (GeoVolume): The object to mirror
|
|
200
|
+
origin (tuple[float, float, float], optional): The point of origin in meters. Defaults to (0.0, 0.0, 0.0).
|
|
201
|
+
direction (tuple[float, float, float], optional): The normal axis defining the plane of reflection. Defaults to (1.0, 0.0, 0.0).
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
GeoVolume: The mirrored GeoVolume object
|
|
205
|
+
"""
|
|
206
|
+
a, b, c = direction
|
|
207
|
+
x0, y0, z0 = origin
|
|
208
|
+
d = -(a*x0 + b*y0 + c*z0)
|
|
209
|
+
if (a==0) and (b==0) and (c==0):
|
|
210
|
+
return main
|
|
211
|
+
mirror_obj = main
|
|
212
|
+
if make_copy:
|
|
213
|
+
new_obj = main.make_copy()
|
|
214
|
+
gmsh.model.occ.mirror(new_obj.dimtags, a,b,c,d)
|
|
215
|
+
mirror_obj = new_obj
|
|
216
|
+
else:
|
|
217
|
+
gmsh.model.occ.mirror(main.dimtags, a,b,c,d)
|
|
218
|
+
|
|
219
|
+
for fp in mirror_obj._all_pointers:
|
|
220
|
+
fp.mirror(origin, direction)
|
|
221
|
+
return mirror_obj
|
|
222
|
+
|
|
223
|
+
def change_coordinate_system(main: GeoVolume,
|
|
224
|
+
new_cs: CoordinateSystem = GCS,
|
|
225
|
+
old_cs: CoordinateSystem = GCS):
|
|
226
|
+
"""Moves the GeoVolume object from a current coordinate system to a new one.
|
|
227
|
+
|
|
228
|
+
The old and new coordinate system by default are the global coordinate system.
|
|
229
|
+
Thus only one needs to be provided to transform to and from these coordinate systems.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
main (GeoVolume): The object to transform
|
|
233
|
+
new_cs (CoordinateSystem): The new coordinate system. Defaults to GCS
|
|
234
|
+
old_cs (CoordinateSystem, optional): The old coordinate system. Defaults to GCS.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
_type_: _description_
|
|
238
|
+
"""
|
|
239
|
+
if new_cs._is_global and old_cs._is_global:
|
|
240
|
+
return main
|
|
241
|
+
|
|
242
|
+
M1 = old_cs.affine_to_global()
|
|
243
|
+
M2 = new_cs.affine_from_global()
|
|
244
|
+
# Transform to the global coordinate system.
|
|
245
|
+
if not old_cs._is_global:
|
|
246
|
+
gmsh.model.occ.affine_transform(main.dimtags, M1.flatten()[:12])
|
|
247
|
+
# Transform to a new coordinate system.
|
|
248
|
+
if not new_cs._is_global:
|
|
249
|
+
gmsh.model.occ.affineTransform(main.dimtags, M2.flatten()[:12])
|
|
250
|
+
|
|
251
|
+
for fp in main._all_pointers:
|
|
252
|
+
fp.affine_transform(M1)
|
|
253
|
+
fp.affine_transform(M2)
|
|
254
|
+
return main
|