emerge 1.0.0__py3-none-any.whl → 1.0.2__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 +7 -8
- emerge/_emerge/elements/femdata.py +4 -3
- emerge/_emerge/elements/nedelec2.py +8 -4
- emerge/_emerge/elements/nedleg2.py +6 -2
- emerge/_emerge/geo/__init__.py +1 -1
- emerge/_emerge/geo/pcb.py +149 -66
- emerge/_emerge/geo/pcb_tools/dxf.py +361 -0
- emerge/_emerge/geo/polybased.py +23 -74
- emerge/_emerge/geo/shapes.py +31 -16
- emerge/_emerge/geometry.py +120 -21
- emerge/_emerge/mesh3d.py +62 -43
- emerge/_emerge/{_cache_check.py → mth/_cache_check.py} +2 -2
- emerge/_emerge/mth/optimized.py +69 -3
- emerge/_emerge/periodic.py +19 -17
- emerge/_emerge/physics/microwave/__init__.py +0 -1
- emerge/_emerge/physics/microwave/assembly/assembler.py +27 -5
- emerge/_emerge/physics/microwave/assembly/generalized_eigen_hb.py +2 -3
- emerge/_emerge/physics/microwave/assembly/periodicbc.py +0 -1
- emerge/_emerge/physics/microwave/assembly/robin_abc_order2.py +375 -0
- emerge/_emerge/physics/microwave/assembly/robinbc.py +37 -38
- emerge/_emerge/physics/microwave/microwave_3d.py +11 -19
- emerge/_emerge/physics/microwave/microwave_bc.py +38 -21
- emerge/_emerge/physics/microwave/microwave_data.py +3 -26
- emerge/_emerge/physics/microwave/port_functions.py +4 -4
- emerge/_emerge/plot/pyvista/display.py +13 -2
- emerge/_emerge/plot/simple_plots.py +4 -1
- emerge/_emerge/selection.py +12 -9
- emerge/_emerge/simmodel.py +68 -34
- emerge/_emerge/solver.py +28 -16
- emerge/beta/dxf.py +1 -0
- emerge/lib.py +1 -0
- emerge/materials/__init__.py +1 -0
- emerge/materials/isola.py +294 -0
- emerge/materials/rogers.py +58 -0
- {emerge-1.0.0.dist-info → emerge-1.0.2.dist-info}/METADATA +18 -4
- {emerge-1.0.0.dist-info → emerge-1.0.2.dist-info}/RECORD +39 -33
- {emerge-1.0.0.dist-info → emerge-1.0.2.dist-info}/WHEEL +0 -0
- {emerge-1.0.0.dist-info → emerge-1.0.2.dist-info}/entry_points.txt +0 -0
- {emerge-1.0.0.dist-info → emerge-1.0.2.dist-info}/licenses/LICENSE +0 -0
emerge/_emerge/geo/shapes.py
CHANGED
|
@@ -50,14 +50,15 @@ class Box(GeoVolume):
|
|
|
50
50
|
alignment (Alignment, optional): Which point of the box is placed at the position.
|
|
51
51
|
Defaults to Alignment.CORNER.
|
|
52
52
|
"""
|
|
53
|
-
|
|
53
|
+
_default_name: str = 'Box'
|
|
54
54
|
def __init__(self,
|
|
55
55
|
width: float,
|
|
56
56
|
depth: float,
|
|
57
57
|
height: float,
|
|
58
58
|
position: tuple = (0,0,0),
|
|
59
59
|
alignment: Alignment = Alignment.CORNER,
|
|
60
|
-
cs: CoordinateSystem = GCS
|
|
60
|
+
cs: CoordinateSystem = GCS,
|
|
61
|
+
name: str | None = None):
|
|
61
62
|
"""Creates a box volume object.
|
|
62
63
|
Specify the alignment of the box with the provided position. The options are CORNER (default)
|
|
63
64
|
for the front-left-bottom node of the box or CENTER for the center of the box.
|
|
@@ -76,7 +77,7 @@ class Box(GeoVolume):
|
|
|
76
77
|
x,y,z = position
|
|
77
78
|
|
|
78
79
|
tag = gmsh.model.occ.addBox(x,y,z,width,depth,height)
|
|
79
|
-
super().__init__(tag)
|
|
80
|
+
super().__init__(tag, name=name)
|
|
80
81
|
|
|
81
82
|
self.center = (x+width/2, y+depth/2, z+height/2)
|
|
82
83
|
self.width = width
|
|
@@ -116,6 +117,7 @@ class Sphere(GeoVolume):
|
|
|
116
117
|
radius (float): The sphere radius
|
|
117
118
|
position (tuple, optional): The center position. Defaults to (0,0,0).
|
|
118
119
|
"""
|
|
120
|
+
_default_name: str = 'Sphere'
|
|
119
121
|
def __init__(self,
|
|
120
122
|
radius: float,
|
|
121
123
|
position: tuple = (0,0,0)):
|
|
@@ -142,11 +144,13 @@ class XYPlate(GeoSurface):
|
|
|
142
144
|
position (tuple, optional): The position of the alignment node. Defaults to (0,0,0).
|
|
143
145
|
alignment (Alignment, optional): Which node to align to. Defaults to Alignment.CORNER.
|
|
144
146
|
"""
|
|
147
|
+
_default_name: str = 'XYPlate'
|
|
145
148
|
def __init__(self,
|
|
146
149
|
width: float,
|
|
147
150
|
depth: float,
|
|
148
151
|
position: tuple = (0,0,0),
|
|
149
|
-
alignment: Alignment = Alignment.CORNER
|
|
152
|
+
alignment: Alignment = Alignment.CORNER,
|
|
153
|
+
name: str | None = None):
|
|
150
154
|
"""Generates and XY-plane oriented plate
|
|
151
155
|
|
|
152
156
|
Specify the alignment of the plate with the provided position. The options are CORNER (default)
|
|
@@ -158,7 +162,7 @@ class XYPlate(GeoSurface):
|
|
|
158
162
|
position (tuple, optional): The position of the alignment node. Defaults to (0,0,0).
|
|
159
163
|
alignment (Alignment, optional): Which node to align to. Defaults to Alignment.CORNER.
|
|
160
164
|
"""
|
|
161
|
-
super().__init__([])
|
|
165
|
+
super().__init__([], name=name)
|
|
162
166
|
if alignment is Alignment.CENTER:
|
|
163
167
|
position = (position[0]-width/2, position[1]-depth/2, position[2])
|
|
164
168
|
|
|
@@ -180,10 +184,12 @@ class Plate(GeoSurface):
|
|
|
180
184
|
u (tuple[float, float, float]): The u-axis of the plate
|
|
181
185
|
v (tuple[float, float, float]): The v-axis of the plate
|
|
182
186
|
"""
|
|
187
|
+
_default_name: str = 'Plate'
|
|
183
188
|
def __init__(self,
|
|
184
189
|
origin: tuple[float, float, float],
|
|
185
190
|
u: tuple[float, float, float],
|
|
186
|
-
v: tuple[float, float, float]
|
|
191
|
+
v: tuple[float, float, float],
|
|
192
|
+
name: str | None = None):
|
|
187
193
|
"""A generalized 2D rectangular plate in XYZ-space.
|
|
188
194
|
|
|
189
195
|
The plate is specified by an origin (o) in meters coordinate plus two vectors (u,v) in meters
|
|
@@ -215,7 +221,7 @@ class Plate(GeoSurface):
|
|
|
215
221
|
tag_wire = gmsh.model.occ.addWire([tagl1,tagl2, tagl3, tagl4])
|
|
216
222
|
|
|
217
223
|
tags: list[int] = [gmsh.model.occ.addPlaneSurface([tag_wire,]),]
|
|
218
|
-
super().__init__(tags)
|
|
224
|
+
super().__init__(tags, name=name)
|
|
219
225
|
|
|
220
226
|
|
|
221
227
|
class Cylinder(GeoVolume):
|
|
@@ -235,11 +241,13 @@ class Cylinder(GeoVolume):
|
|
|
235
241
|
cs (CoordinateSystem, optional): The coordinate system. Defaults to GCS.
|
|
236
242
|
Nsections (int, optional): The number of sections. Defaults to None.
|
|
237
243
|
"""
|
|
244
|
+
_default_name: str = 'Cylinder'
|
|
238
245
|
def __init__(self,
|
|
239
246
|
radius: float,
|
|
240
247
|
height: float,
|
|
241
248
|
cs: CoordinateSystem = GCS,
|
|
242
|
-
Nsections: int | None = None
|
|
249
|
+
Nsections: int | None = None,
|
|
250
|
+
name: str | None = None):
|
|
243
251
|
"""Generates a Cylinder object in 3D space.
|
|
244
252
|
The cylinder will always be placed in the origin of the provided CoordinateSystem.
|
|
245
253
|
The bottom cylinder plane is always placed in the XY-plane. The length of the cylinder is
|
|
@@ -263,12 +271,12 @@ class Cylinder(GeoVolume):
|
|
|
263
271
|
cyl = XYPolygon.circle(radius, Nsections=Nsections).extrude(height, cs)
|
|
264
272
|
cyl._exists = False
|
|
265
273
|
self._face_pointers = cyl._face_pointers
|
|
266
|
-
super().__init__(cyl.tags)
|
|
274
|
+
super().__init__(cyl.tags, name=name)
|
|
267
275
|
else:
|
|
268
276
|
cyl = gmsh.model.occ.addCylinder(cs.origin[0], cs.origin[1], cs.origin[2],
|
|
269
277
|
height*ax[0], height*ax[1], height*ax[2],
|
|
270
278
|
radius)
|
|
271
|
-
super().__init__(cyl)
|
|
279
|
+
super().__init__(cyl, name=name)
|
|
272
280
|
self._add_face_pointer('front', cs.origin, -cs.zax.np)
|
|
273
281
|
self._add_face_pointer('back', cs.origin+height*cs.zax.np, cs.zax.np)
|
|
274
282
|
self._add_face_pointer('bottom', cs.origin, -cs.zax.np)
|
|
@@ -312,12 +320,14 @@ class CoaxCylinder(GeoVolume):
|
|
|
312
320
|
cs (CoordinateSystem, optional): The coordinate system. Defaults to GCS.
|
|
313
321
|
Nsections (int, optional): The number of sections. Defaults to None.
|
|
314
322
|
"""
|
|
323
|
+
_default_name: str = 'CoaxCylinder'
|
|
315
324
|
def __init__(self,
|
|
316
325
|
rout: float,
|
|
317
326
|
rin: float,
|
|
318
327
|
height: float,
|
|
319
328
|
cs: CoordinateSystem = GCS,
|
|
320
|
-
Nsections: int | None = None
|
|
329
|
+
Nsections: int | None = None,
|
|
330
|
+
name: str | None = None):
|
|
321
331
|
"""Generates a Coaxial cylinder object in 3D space.
|
|
322
332
|
The coaxial cylinder will always be placed in the origin of the provided CoordinateSystem.
|
|
323
333
|
The bottom coax plane is always placed in the XY-plane. The lenth of the coax is
|
|
@@ -347,7 +357,7 @@ class CoaxCylinder(GeoVolume):
|
|
|
347
357
|
self.cyl_out._exists = False
|
|
348
358
|
cyltags, _ = gmsh.model.occ.cut(self.cyl_out.dimtags, self.cyl_in.dimtags)
|
|
349
359
|
|
|
350
|
-
super().__init__([dt[1] for dt in cyltags])
|
|
360
|
+
super().__init__([dt[1] for dt in cyltags], name=name)
|
|
351
361
|
|
|
352
362
|
self._add_face_pointer('front', cs.origin, -cs.zax.np)
|
|
353
363
|
self._add_face_pointer('back', cs.origin+height*cs.zax.np, cs.zax.np)
|
|
@@ -374,10 +384,12 @@ class CoaxCylinder(GeoVolume):
|
|
|
374
384
|
|
|
375
385
|
class HalfSphere(GeoVolume):
|
|
376
386
|
"""A half sphere volume."""
|
|
387
|
+
_default_name: str = 'HalfSphere'
|
|
377
388
|
def __init__(self,
|
|
378
389
|
radius: float,
|
|
379
390
|
position: tuple = (0,0,0),
|
|
380
|
-
direction: tuple = (1,0,0)
|
|
391
|
+
direction: tuple = (1,0,0),
|
|
392
|
+
name: str | None = None):
|
|
381
393
|
|
|
382
394
|
sphere = Sphere(radius, position=position)
|
|
383
395
|
cx, cy, cz = position
|
|
@@ -393,7 +405,7 @@ class HalfSphere(GeoVolume):
|
|
|
393
405
|
sphere._exists = False
|
|
394
406
|
box._exists = False
|
|
395
407
|
|
|
396
|
-
super().__init__([dt[1] for dt in dimtags])
|
|
408
|
+
super().__init__([dt[1] for dt in dimtags], name=name)
|
|
397
409
|
|
|
398
410
|
self._add_face_pointer('front',np.array(position), np.array(direction))
|
|
399
411
|
self._add_face_pointer('back',np.array(position), np.array(direction))
|
|
@@ -528,10 +540,13 @@ class Cone(GeoVolume):
|
|
|
528
540
|
r1 (float): _description_
|
|
529
541
|
r2 (float): _description_
|
|
530
542
|
"""
|
|
543
|
+
_default_name: str = 'Cone'
|
|
544
|
+
|
|
531
545
|
def __init__(self, p0: tuple[float, float, float],
|
|
532
546
|
direction: tuple[float, float, float],
|
|
533
547
|
r1: float,
|
|
534
|
-
r2: float
|
|
548
|
+
r2: float,
|
|
549
|
+
name: str | None = None):
|
|
535
550
|
"""Constructis a cone that starts at position p0 and is aimed in the given direction.
|
|
536
551
|
r1 is the start radius and r2 the end radius. The magnitude of direction determines its length.
|
|
537
552
|
|
|
@@ -542,7 +557,7 @@ class Cone(GeoVolume):
|
|
|
542
557
|
r2 (float): _description_
|
|
543
558
|
"""
|
|
544
559
|
tag = gmsh.model.occ.add_cone(*p0, *direction, r1, r2)
|
|
545
|
-
super().__init__(tag)
|
|
560
|
+
super().__init__(tag, name=name)
|
|
546
561
|
|
|
547
562
|
p0 = np.array(p0)
|
|
548
563
|
ds = np.array(direction)
|
emerge/_emerge/geometry.py
CHANGED
|
@@ -33,6 +33,7 @@ def _map_tags(tags: list[int], mapping: dict[int, list[int]]):
|
|
|
33
33
|
|
|
34
34
|
def _bbcenter(x1, y1, z1, x2, y2, z2):
|
|
35
35
|
return np.array([(x1+x2)/2, (y1+y2)/2, (z1+z2)/2])
|
|
36
|
+
|
|
36
37
|
FaceNames = Literal['back','front','left','right','top','bottom']
|
|
37
38
|
|
|
38
39
|
class _KEY_GENERATOR:
|
|
@@ -47,8 +48,9 @@ class _KEY_GENERATOR:
|
|
|
47
48
|
class _GeometryManager:
|
|
48
49
|
|
|
49
50
|
def __init__(self):
|
|
50
|
-
self.geometry_list: dict[str,
|
|
51
|
+
self.geometry_list: dict[str, dict[str, GeoObject]] = dict()
|
|
51
52
|
self.active: str = ''
|
|
53
|
+
self.geometry_names: dict[str, set[str]] = dict()
|
|
52
54
|
|
|
53
55
|
def get_surfaces(self) -> list[GeoSurface]:
|
|
54
56
|
return [geo for geo in self.all_geometries() if geo.dim==2]
|
|
@@ -56,23 +58,40 @@ class _GeometryManager:
|
|
|
56
58
|
def all_geometries(self, model: str | None = None) -> list[GeoObject]:
|
|
57
59
|
if model is None:
|
|
58
60
|
model = self.active
|
|
59
|
-
return [geo for geo in self.geometry_list[model] if geo._exists]
|
|
61
|
+
return [geo for geo in self.geometry_list[model].values() if geo._exists]
|
|
60
62
|
|
|
63
|
+
def all_names(self, model: str | None = None) -> set[str]:
|
|
64
|
+
if model is None:
|
|
65
|
+
model = self.active
|
|
66
|
+
return self.geometry_names[model]
|
|
67
|
+
|
|
68
|
+
def get_name(self, suggestion: str, model: str | None = None) -> str:
|
|
69
|
+
names = self.all_names(model)
|
|
70
|
+
if suggestion not in names:
|
|
71
|
+
return suggestion
|
|
72
|
+
for i in range(1_000_000):
|
|
73
|
+
if f'{suggestion}_{i}' not in names:
|
|
74
|
+
return f'{suggestion}_{i}'
|
|
75
|
+
raise RuntimeError('Cannot generate a unique name.')
|
|
76
|
+
|
|
61
77
|
def submit_geometry(self, geo: GeoObject, model: str | None = None) -> None:
|
|
62
78
|
if model is None:
|
|
63
79
|
model = self.active
|
|
64
|
-
self.geometry_list[model].
|
|
80
|
+
self.geometry_list[model][geo.name] = geo
|
|
81
|
+
self.geometry_names[model].add(geo.name)
|
|
65
82
|
|
|
66
83
|
def sign_in(self, modelname: str) -> None:
|
|
67
84
|
# if modelname not in self.geometry_list:
|
|
68
85
|
# self.geometry_list[modelname] = []
|
|
69
86
|
if modelname is self.geometry_list:
|
|
70
87
|
logger.warning(f'{modelname} already exist, Geometries will be reset.')
|
|
71
|
-
self.geometry_list[modelname] =
|
|
88
|
+
self.geometry_list[modelname] = dict()
|
|
89
|
+
self.geometry_names[modelname] = set()
|
|
72
90
|
self.active = modelname
|
|
73
91
|
|
|
74
92
|
def reset(self, modelname: str) -> None:
|
|
75
|
-
self.geometry_list[modelname] =
|
|
93
|
+
self.geometry_list[modelname] = dict()
|
|
94
|
+
self.geometry_names[modelname] = set()
|
|
76
95
|
|
|
77
96
|
def lowest_priority(self) -> int:
|
|
78
97
|
return min([geo._priority for geo in self.all_geometries()])
|
|
@@ -219,7 +238,8 @@ class GeoObject:
|
|
|
219
238
|
"""A generalization of any OpenCASCADE entity described by a dimension and a set of tags.
|
|
220
239
|
"""
|
|
221
240
|
dim: int = -1
|
|
222
|
-
|
|
241
|
+
_default_name: str = 'GeoObject'
|
|
242
|
+
def __init__(self, tags: list[int] | None = None, name: str | None = None):
|
|
223
243
|
if tags is None:
|
|
224
244
|
tags = []
|
|
225
245
|
self.old_tags: list[int] = []
|
|
@@ -238,18 +258,63 @@ class GeoObject:
|
|
|
238
258
|
self._priority: int = 10
|
|
239
259
|
|
|
240
260
|
self._exists: bool = True
|
|
261
|
+
|
|
262
|
+
self.give_name(name)
|
|
241
263
|
_GEOMANAGER.submit_geometry(self)
|
|
242
264
|
|
|
265
|
+
def _store(self, name: str, data: Any) -> None:
|
|
266
|
+
"""Store a property as auxilliary data under a given name
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
name (str): Name field
|
|
270
|
+
data (Any): Data to store
|
|
271
|
+
"""
|
|
272
|
+
self._aux_data[name] = data
|
|
273
|
+
|
|
274
|
+
def _load(self, name: str) -> Any | None:
|
|
275
|
+
"""Load data with a given name. If it doesn't exist, it returns None
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
name (str): The property to retreive
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
Any | None: The property
|
|
282
|
+
"""
|
|
283
|
+
return self._aux_data.get(name, None)
|
|
284
|
+
|
|
285
|
+
def give_name(self, name: str | None = None) -> GeoObject:
|
|
286
|
+
"""Assign a name to this object
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
name (str | None, optional): The name for the object. Defaults to None.
|
|
290
|
+
"""
|
|
291
|
+
if name is None:
|
|
292
|
+
name = self._default_name
|
|
293
|
+
self.name: str = _GEOMANAGER.get_name(name)
|
|
294
|
+
return self
|
|
295
|
+
|
|
243
296
|
@property
|
|
244
297
|
def color_rgb(self) -> tuple[float, float, float]:
|
|
298
|
+
"""The color of the object in RGB float tuple
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
tuple[float, float, float]: The color
|
|
302
|
+
"""
|
|
245
303
|
return self.material.color_rgb
|
|
246
304
|
|
|
247
305
|
@property
|
|
248
306
|
def opacity(self) -> float:
|
|
307
|
+
"""The opacity of the object
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
float: The opacity
|
|
311
|
+
"""
|
|
249
312
|
return self.material.opacity
|
|
250
313
|
|
|
251
314
|
@property
|
|
252
315
|
def _metal(self) -> bool:
|
|
316
|
+
"""If the material should be rendered as metal
|
|
317
|
+
"""
|
|
253
318
|
return self.material._metal
|
|
254
319
|
|
|
255
320
|
@property
|
|
@@ -266,6 +331,14 @@ class GeoObject:
|
|
|
266
331
|
|
|
267
332
|
@staticmethod
|
|
268
333
|
def merged(objects: list[GeoPoint | GeoEdge | GeoSurface | GeoVolume | GeoObject]) -> list[GeoPoint | GeoEdge | GeoSurface | GeoVolume | GeoObject] | GeoPoint | GeoEdge | GeoSurface | GeoVolume | GeoObject:
|
|
334
|
+
"""Create a GeoObject by merging an iterable of GeoObjects
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
objects (list[GeoPoint | GeoEdge | GeoSurface | GeoVolume | GeoObject]): A list of geo objects
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
GeoPoint | GeoEdge | GeoSurface | GeoVolume | GeoObject: The resultant object
|
|
341
|
+
"""
|
|
269
342
|
dim = objects[0].dim
|
|
270
343
|
tags = []
|
|
271
344
|
out: GeoObject | None = None
|
|
@@ -291,6 +364,17 @@ class GeoObject:
|
|
|
291
364
|
origin: np.ndarray | None = None,
|
|
292
365
|
normal: np.ndarray | None = None,
|
|
293
366
|
tag: int | None = None):
|
|
367
|
+
"""Adds a face identifier (face pointer) to this object
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
name (str): The name for the face
|
|
371
|
+
origin (np.ndarray | None, optional): A point on the object. Defaults to None.
|
|
372
|
+
normal (np.ndarray | None, optional): The normal of the face. Defaults to None.
|
|
373
|
+
tag (int | None, optional): The tace tag used to extract the origin and normal. Defaults to None.
|
|
374
|
+
|
|
375
|
+
Raises:
|
|
376
|
+
ValueError: _description_
|
|
377
|
+
"""
|
|
294
378
|
if tag is not None:
|
|
295
379
|
o = gmsh.model.occ.get_center_of_mass(2, tag)
|
|
296
380
|
n = gmsh.model.get_normal(tag, (0,0))
|
|
@@ -302,6 +386,7 @@ class GeoObject:
|
|
|
302
386
|
raise ValueError('Eitehr a tag or an origin + normal must be provided!')
|
|
303
387
|
|
|
304
388
|
def make_copy(self) -> GeoObject:
|
|
389
|
+
""" Copies this object and returns a new object (also in GMSH)"""
|
|
305
390
|
new_dimtags = gmsh.model.occ.copy(self.dimtags)
|
|
306
391
|
new_obj = GeoObject.from_dimtags(new_dimtags)
|
|
307
392
|
new_obj.material = self.material
|
|
@@ -319,6 +404,11 @@ class GeoObject:
|
|
|
319
404
|
return new_obj
|
|
320
405
|
|
|
321
406
|
def replace_tags(self, tagmap: dict[int, list[int]]):
|
|
407
|
+
"""Replaces the GMSH tags assigned to this objects
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
tagmap (dict[int, list[int]]): A map that shows which tag is mapped to which set of new tags.
|
|
411
|
+
"""
|
|
322
412
|
self.old_tags = self.tags
|
|
323
413
|
newtags = []
|
|
324
414
|
for tag in self.tags:
|
|
@@ -454,7 +544,9 @@ class GeoObject:
|
|
|
454
544
|
self._priority = _GEOMANAGER.highest_priority()+10
|
|
455
545
|
return self
|
|
456
546
|
|
|
457
|
-
def boundary(self, exclude: tuple[FaceNames,...] | None = None,
|
|
547
|
+
def boundary(self, exclude: tuple[FaceNames,...] | None = None,
|
|
548
|
+
tags: list[int] | None = None,
|
|
549
|
+
tool: GeoObject | None = None) -> FaceSelection:
|
|
458
550
|
"""Returns the complete set of boundary faces.
|
|
459
551
|
|
|
460
552
|
If implemented, it is possible to exclude a set of faces based on their name
|
|
@@ -471,7 +563,7 @@ class GeoObject:
|
|
|
471
563
|
|
|
472
564
|
|
|
473
565
|
for name in exclude:
|
|
474
|
-
tags.extend(self.face(name).tags)
|
|
566
|
+
tags.extend(self.face(name, tool=tool).tags)
|
|
475
567
|
dimtags = gmsh.model.get_boundary(self.dimtags, True, False)
|
|
476
568
|
return FaceSelection([t for d,t in dimtags if t not in tags])
|
|
477
569
|
|
|
@@ -535,8 +627,10 @@ class GeoVolume(GeoObject):
|
|
|
535
627
|
'''GeoVolume is an interface to the GMSH CAD kernel. It does not represent EMerge
|
|
536
628
|
specific geometry data.'''
|
|
537
629
|
dim = 3
|
|
538
|
-
|
|
539
|
-
|
|
630
|
+
_default_name: str = 'GeoVolume'
|
|
631
|
+
def __init__(self, tag: int | Iterable[int], name: str | None = None):
|
|
632
|
+
super().__init__(name=name)
|
|
633
|
+
|
|
540
634
|
self.tags: list[int] = []
|
|
541
635
|
if isinstance(tag, Iterable):
|
|
542
636
|
self.tags = list(tag)
|
|
@@ -549,13 +643,14 @@ class GeoVolume(GeoObject):
|
|
|
549
643
|
|
|
550
644
|
class GeoPoint(GeoObject):
|
|
551
645
|
dim = 0
|
|
552
|
-
|
|
646
|
+
_default_name: str = 'GeoPoint'
|
|
647
|
+
|
|
553
648
|
@property
|
|
554
649
|
def selection(self) -> PointSelection:
|
|
555
650
|
return PointSelection(self.tags)
|
|
556
651
|
|
|
557
|
-
def __init__(self, tag: int | list[int]):
|
|
558
|
-
super().__init__()
|
|
652
|
+
def __init__(self, tag: int | list[int], name: str | None = None):
|
|
653
|
+
super().__init__(name=name)
|
|
559
654
|
|
|
560
655
|
self.tags: list[int] = []
|
|
561
656
|
if isinstance(tag, Iterable):
|
|
@@ -565,13 +660,14 @@ class GeoPoint(GeoObject):
|
|
|
565
660
|
|
|
566
661
|
class GeoEdge(GeoObject):
|
|
567
662
|
dim = 1
|
|
568
|
-
|
|
663
|
+
_default_name: str = 'GeoEdge'
|
|
664
|
+
|
|
569
665
|
@property
|
|
570
666
|
def selection(self) -> EdgeSelection:
|
|
571
667
|
return EdgeSelection(self.tags)
|
|
572
668
|
|
|
573
|
-
def __init__(self, tag: int | list[int]):
|
|
574
|
-
super().__init__()
|
|
669
|
+
def __init__(self, tag: int | list[int], name: str | None = None):
|
|
670
|
+
super().__init__(name=name)
|
|
575
671
|
self.tags: list[int] = []
|
|
576
672
|
if isinstance(tag, Iterable):
|
|
577
673
|
self.tags = list(tag)
|
|
@@ -583,13 +679,14 @@ class GeoSurface(GeoObject):
|
|
|
583
679
|
'''GeoVolume is an interface to the GMSH CAD kernel. It does not reprsent Emerge
|
|
584
680
|
specific geometry data.'''
|
|
585
681
|
dim = 2
|
|
586
|
-
|
|
682
|
+
_default_name: str = 'GeoSurface'
|
|
683
|
+
|
|
587
684
|
@property
|
|
588
685
|
def selection(self) -> FaceSelection:
|
|
589
686
|
return FaceSelection(self.tags)
|
|
590
687
|
|
|
591
|
-
def __init__(self, tag: int | list[int]):
|
|
592
|
-
super().__init__()
|
|
688
|
+
def __init__(self, tag: int | list[int], name: str | None = None):
|
|
689
|
+
super().__init__(name=name)
|
|
593
690
|
self.tags: list[int] = []
|
|
594
691
|
if isinstance(tag, Iterable):
|
|
595
692
|
self.tags = list(tag)
|
|
@@ -597,10 +694,12 @@ class GeoSurface(GeoObject):
|
|
|
597
694
|
self.tags = [tag,]
|
|
598
695
|
|
|
599
696
|
class GeoPolygon(GeoSurface):
|
|
697
|
+
_default_name: str = 'GeoPolygon'
|
|
600
698
|
|
|
601
699
|
def __init__(self,
|
|
602
|
-
tags: list[int]
|
|
603
|
-
|
|
700
|
+
tags: list[int],
|
|
701
|
+
name: str | None = None):
|
|
702
|
+
super().__init__(tags, name=name)
|
|
604
703
|
self.points: list[int] = []
|
|
605
704
|
self.lines: list[int] = []
|
|
606
705
|
|
emerge/_emerge/mesh3d.py
CHANGED
|
@@ -18,25 +18,14 @@
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
import gmsh # type: ignore
|
|
20
20
|
import numpy as np
|
|
21
|
-
from numba import njit, f8 # type: ignore
|
|
22
21
|
from .mesher import Mesher
|
|
23
22
|
from typing import Union, List, Tuple, Callable, Any
|
|
24
23
|
from collections import defaultdict
|
|
25
24
|
from .geometry import GeoVolume
|
|
26
|
-
from .mth.optimized import outward_normal
|
|
27
25
|
from loguru import logger
|
|
28
|
-
from functools import cache
|
|
29
26
|
from .bc import Periodic
|
|
30
|
-
from .mth.pairing import pair_coordinates
|
|
31
27
|
from .material import Material
|
|
32
28
|
|
|
33
|
-
@njit(f8(f8[:], f8[:], f8[:]), cache=True, nogil=True)
|
|
34
|
-
def area(x1: np.ndarray, x2: np.ndarray, x3: np.ndarray):
|
|
35
|
-
e1 = x2 - x1
|
|
36
|
-
e2 = x3 - x1
|
|
37
|
-
av = np.array([e1[1]*e2[2] - e1[2]*e2[1], e1[2]*e2[0] - e1[0]*e2[2], e1[0]*e2[1] - e1[1]*e2[0]])
|
|
38
|
-
return np.sqrt(av[0]**2 + av[1]**2 + av[2]**2)/2
|
|
39
|
-
|
|
40
29
|
def shortest_distance(point_cloud):
|
|
41
30
|
"""
|
|
42
31
|
Compute the shortest distance between any two points in a 3D point cloud.
|
|
@@ -122,7 +111,6 @@ class Mesh3D(Mesh):
|
|
|
122
111
|
self.inv_tets: dict = dict()
|
|
123
112
|
|
|
124
113
|
# Mappings
|
|
125
|
-
|
|
126
114
|
self.tet_to_edge: np.ndarray = np.array([])
|
|
127
115
|
self.tet_to_edge_sign: np.ndarray = np.array([])
|
|
128
116
|
self.tet_to_tri: np.ndarray = np.array([])
|
|
@@ -133,7 +121,6 @@ class Mesh3D(Mesh):
|
|
|
133
121
|
self.node_to_edge: defaultdict | dict = defaultdict()
|
|
134
122
|
|
|
135
123
|
# Physics mappings
|
|
136
|
-
|
|
137
124
|
self.tet_to_field: np.ndarray = np.array([])
|
|
138
125
|
self.edge_to_field: np.ndarray = np.array([])
|
|
139
126
|
self.tri_to_field: np.ndarray = np.array([])
|
|
@@ -149,6 +136,10 @@ class Mesh3D(Mesh):
|
|
|
149
136
|
self.vtag_to_tet: dict[int, list[int]] = dict()
|
|
150
137
|
self.etag_to_edge: dict[int, list[int]] = dict()
|
|
151
138
|
|
|
139
|
+
## Dervied
|
|
140
|
+
self.dimtag_to_center: dict[tuple[int, int], tuple[float, float, float]] = dict()
|
|
141
|
+
self.dimtag_to_edges: dict[tuple[int, int], np.ndarray] = dict()
|
|
142
|
+
|
|
152
143
|
self.exterior_face_tags: list[int] = []
|
|
153
144
|
|
|
154
145
|
@property
|
|
@@ -170,7 +161,7 @@ class Mesh3D(Mesh):
|
|
|
170
161
|
def n_nodes(self) -> int:
|
|
171
162
|
'''Return the number of nodes'''
|
|
172
163
|
return self.nodes.shape[1]
|
|
173
|
-
|
|
164
|
+
|
|
174
165
|
def get_edge(self, i1: int, i2: int, skip: bool = False) -> int:
|
|
175
166
|
'''Return the edge index given the two node indices'''
|
|
176
167
|
if i1==i2:
|
|
@@ -202,21 +193,6 @@ class Mesh3D(Mesh):
|
|
|
202
193
|
if output is None:
|
|
203
194
|
raise ValueError(f'There is no tetrahedron with indices {i1}, {i2}, {i3}, {i4}')
|
|
204
195
|
return output
|
|
205
|
-
|
|
206
|
-
def boundary_triangles(self, dimtags: list[tuple[int, int]] | None = None) -> np.ndarray:
|
|
207
|
-
if dimtags is None:
|
|
208
|
-
outputtags = []
|
|
209
|
-
for tags in self.ftag_to_tri.values():
|
|
210
|
-
outputtags.extend(tags)
|
|
211
|
-
return np.array(outputtags)
|
|
212
|
-
else:
|
|
213
|
-
dts = []
|
|
214
|
-
for dimtag in dimtags:
|
|
215
|
-
if dimtag[0]==2:
|
|
216
|
-
dts.append(dimtag)
|
|
217
|
-
elif dimtag[0]==3:
|
|
218
|
-
dts.extend(gmsh.model.get_boundary(dimtags))
|
|
219
|
-
return self.get_triangles([tag[1] for tag in dts])
|
|
220
196
|
|
|
221
197
|
|
|
222
198
|
def get_tetrahedra(self, vol_tags: Union[int, list[int]]) -> np.ndarray:
|
|
@@ -242,7 +218,7 @@ class Mesh3D(Mesh):
|
|
|
242
218
|
|
|
243
219
|
return np.array(indices)
|
|
244
220
|
|
|
245
|
-
def
|
|
221
|
+
def _domain_edge(self, dimtag: tuple[int,int]) -> np.ndarray:
|
|
246
222
|
"""Returns a np.ndarray of all edge indices corresponding to a set of dimension tags.
|
|
247
223
|
|
|
248
224
|
Args:
|
|
@@ -252,21 +228,39 @@ class Mesh3D(Mesh):
|
|
|
252
228
|
np.ndarray: The list of mesh edge element indices.
|
|
253
229
|
"""
|
|
254
230
|
dimtags_edge = []
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
231
|
+
d,t = dimtag
|
|
232
|
+
if d==0:
|
|
233
|
+
return np.ndarray([], dtype=np.int64)
|
|
234
|
+
if d==1:
|
|
235
|
+
dimtags_edge.append((1,t))
|
|
236
|
+
if d==2:
|
|
237
|
+
dimtags_edge.extend(gmsh.model.getBoundary([(d,t),], False, False))
|
|
238
|
+
if d==3:
|
|
239
|
+
dts = gmsh.model.getBoundary([(d,t),], False, False)
|
|
240
|
+
dimtags_edge.extend(gmsh.model.getBoundary(dts, False, False))
|
|
241
|
+
|
|
264
242
|
edge_ids = []
|
|
265
243
|
for tag in dimtags_edge:
|
|
266
244
|
edge_ids.extend(self.etag_to_edge[tag[1]])
|
|
267
245
|
edge_ids = np.array(edge_ids)
|
|
268
246
|
return edge_ids
|
|
247
|
+
|
|
248
|
+
def domain_edges(self, dimtags: list[tuple[int,int]]) -> np.ndarray:
|
|
249
|
+
"""Returns a np.ndarray of all edge indices corresponding to a set of dimension tags.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
dimtags (list[tuple[int,int]]): A list of dimtags.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
np.ndarray: The list of mesh edge element indices.
|
|
256
|
+
"""
|
|
269
257
|
|
|
258
|
+
edge_ids = []
|
|
259
|
+
for dt in dimtags:
|
|
260
|
+
edge_ids.extend(self.dimtag_to_edges[dt])
|
|
261
|
+
edge_ids = np.array(edge_ids)
|
|
262
|
+
return edge_ids
|
|
263
|
+
|
|
270
264
|
def get_face_tets(self, *taglist: list[int]) -> np.ndarray:
|
|
271
265
|
''' Return a list of a tetrahedrons that share a node with any of the nodes in the provided face.'''
|
|
272
266
|
nodes: set = set()
|
|
@@ -297,7 +291,16 @@ class Mesh3D(Mesh):
|
|
|
297
291
|
return np.array(sorted(list(set(edges))))
|
|
298
292
|
|
|
299
293
|
|
|
300
|
-
def
|
|
294
|
+
def _pre_update(self, periodic_bcs: list[Periodic] | None = None):
|
|
295
|
+
"""Builds the mesh data properties
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
periodic_bcs (list[Periodic] | None, optional): A list of periodic boundary conditions. Defaults to None.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
None: None
|
|
302
|
+
"""
|
|
303
|
+
from .mth.optimized import area
|
|
301
304
|
|
|
302
305
|
logger.trace('Generating mesh data.')
|
|
303
306
|
if periodic_bcs is None:
|
|
@@ -337,7 +340,7 @@ class Mesh3D(Mesh):
|
|
|
337
340
|
|
|
338
341
|
for bc in periodic_bcs:
|
|
339
342
|
logger.trace(f'reassigning ordered node numbers for periodic boundary {bc}')
|
|
340
|
-
nodemap, ids1, ids2 = self.
|
|
343
|
+
nodemap, ids1, ids2 = self._pre_derive_node_map(bc)
|
|
341
344
|
nodemap = {int(a): int(b) for a,b in nodemap.items()}
|
|
342
345
|
self.nodes[:,ids2] = self.nodes[:,ids1]
|
|
343
346
|
for itet in range(self.tets.shape[1]):
|
|
@@ -487,12 +490,24 @@ class Mesh3D(Mesh):
|
|
|
487
490
|
self.vtag_to_tet[t] = [self.get_tet(node_tags[0,i], node_tags[1,i], node_tags[2,i], node_tags[3,i]) for i in range(node_tags.shape[1])]
|
|
488
491
|
|
|
489
492
|
self.defined = True
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
############################################################
|
|
496
|
+
# GMSH CACHE #
|
|
497
|
+
############################################################
|
|
498
|
+
|
|
499
|
+
for dim in (0,1,2,3):
|
|
500
|
+
dts= gmsh.model.get_entities(dim)
|
|
501
|
+
for dt in dts:
|
|
502
|
+
self.dimtag_to_center[dt] = gmsh.model.occ.get_center_of_mass(*dt)
|
|
503
|
+
self.dimtag_to_edges[dt] = self._domain_edge(dt)
|
|
504
|
+
|
|
490
505
|
logger.trace('Done analyzing mesh.')
|
|
491
506
|
|
|
492
507
|
|
|
493
508
|
## Higher order functions
|
|
494
509
|
|
|
495
|
-
def
|
|
510
|
+
def _pre_derive_node_map(self, bc: Periodic) -> tuple[dict[int, int], np.ndarray, np.ndarray]:
|
|
496
511
|
"""Computes an old to new node index mapping that preserves global sorting
|
|
497
512
|
|
|
498
513
|
Since basis function field direction is based on the order of indices in tetrahedron
|
|
@@ -507,6 +522,8 @@ class Mesh3D(Mesh):
|
|
|
507
522
|
tuple[dict[int, int], np.ndarray, np.ndarray]: The node index mapping and the node index arrays
|
|
508
523
|
"""
|
|
509
524
|
|
|
525
|
+
from .mth.pairing import pair_coordinates
|
|
526
|
+
|
|
510
527
|
node_ids_1 = []
|
|
511
528
|
node_ids_2 = []
|
|
512
529
|
|
|
@@ -539,7 +556,7 @@ class Mesh3D(Mesh):
|
|
|
539
556
|
return conv_map, np.array(node_ids_2_unsorted), np.array(node_ids_2_sorted)
|
|
540
557
|
|
|
541
558
|
|
|
542
|
-
def
|
|
559
|
+
def _get_material_assignment(self, volumes: list[GeoVolume]) -> list[Material]:
|
|
543
560
|
'''Retrieve the material properties of the geometry'''
|
|
544
561
|
#arry = np.zeros((3,3,self.n_tets,), dtype=np.complex128)
|
|
545
562
|
for vol in volumes:
|
|
@@ -791,6 +808,8 @@ class SurfaceMesh(Mesh):
|
|
|
791
808
|
def update(self) -> None:
|
|
792
809
|
## First Edges
|
|
793
810
|
|
|
811
|
+
from .mth.optimized import outward_normal, area
|
|
812
|
+
|
|
794
813
|
edges = set()
|
|
795
814
|
for i in range(self.n_tris):
|
|
796
815
|
i1, i2, i3 = self.tris[:,i]
|