emerge 1.1.0__py3-none-any.whl → 1.1.1__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 +2 -1
- emerge/_emerge/const.py +2 -1
- emerge/_emerge/elements/ned2_interp.py +122 -42
- emerge/_emerge/geo/__init__.py +1 -1
- emerge/_emerge/geo/operations.py +20 -0
- emerge/_emerge/geo/pcb.py +34 -16
- emerge/_emerge/geo/shapes.py +1 -0
- emerge/_emerge/geo/step.py +177 -41
- emerge/_emerge/geometry.py +166 -19
- emerge/_emerge/material.py +2 -0
- emerge/_emerge/mesh3d.py +1 -3
- emerge/_emerge/mesher.py +33 -9
- emerge/_emerge/mth/common_functions.py +1 -1
- emerge/_emerge/mth/optimized.py +2 -2
- emerge/_emerge/physics/microwave/adaptive_mesh.py +437 -95
- emerge/_emerge/physics/microwave/assembly/assembler.py +9 -1
- emerge/_emerge/physics/microwave/microwave_3d.py +2 -2
- emerge/_emerge/physics/microwave/microwave_bc.py +1 -0
- emerge/_emerge/physics/microwave/microwave_data.py +94 -5
- emerge/_emerge/plot/pyvista/display.py +10 -7
- emerge/_emerge/selection.py +17 -2
- emerge/_emerge/simmodel.py +76 -25
- emerge/_emerge/solver.py +3 -3
- {emerge-1.1.0.dist-info → emerge-1.1.1.dist-info}/METADATA +1 -1
- {emerge-1.1.0.dist-info → emerge-1.1.1.dist-info}/RECORD +28 -28
- {emerge-1.1.0.dist-info → emerge-1.1.1.dist-info}/WHEEL +0 -0
- {emerge-1.1.0.dist-info → emerge-1.1.1.dist-info}/entry_points.txt +0 -0
- {emerge-1.1.0.dist-info → emerge-1.1.1.dist-info}/licenses/LICENSE +0 -0
emerge/_emerge/geo/step.py
CHANGED
|
@@ -1,11 +1,70 @@
|
|
|
1
1
|
import gmsh
|
|
2
2
|
from ..geometry import GeoPoint, GeoEdge, GeoVolume, GeoSurface, GeoObject
|
|
3
|
+
from ..selection import FaceSelection
|
|
4
|
+
from .shapes import Box
|
|
5
|
+
from .operations import unite
|
|
6
|
+
|
|
3
7
|
from pathlib import Path
|
|
4
8
|
import numpy as np
|
|
9
|
+
from typing import Callable
|
|
5
10
|
|
|
6
|
-
|
|
11
|
+
def _select_num(num1: float | None, num2: float | None) -> float:
|
|
12
|
+
if num1 is None and num2 is None:
|
|
13
|
+
return 0.0
|
|
14
|
+
if isinstance(num1, float) and num2 is None:
|
|
15
|
+
return num1
|
|
16
|
+
if num1 is None and isinstance(num2, float):
|
|
17
|
+
return num2
|
|
18
|
+
return max(num1, num2)
|
|
19
|
+
|
|
20
|
+
class _FaceSliceSelector:
|
|
21
|
+
|
|
22
|
+
def __init__(self, face_numbers: list[int], selector: Callable):
|
|
23
|
+
self.numbers: list[str] = face_numbers
|
|
24
|
+
self.selector: Callable = selector
|
|
25
|
+
|
|
26
|
+
def __getitem__(self, slice) -> FaceSelection:
|
|
27
|
+
nums = self.numbers.__getitem__(slice)
|
|
28
|
+
if isinstance(nums, int):
|
|
29
|
+
nums = [nums,]
|
|
30
|
+
return self.selector(*nums)
|
|
31
|
+
|
|
32
|
+
class StepVolume(GeoVolume):
|
|
33
|
+
"""The StepVoume class extens the EMerge GeoVolume class to add easier
|
|
34
|
+
face selection functionalities based on numbers as face names are not
|
|
35
|
+
imported currently
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
GeoVolume (_type_): _description_
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
_type_: _description_
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def _face_numbers(self) -> list[int]:
|
|
47
|
+
return sorted([int(name[4:]) for name in self._face_pointers.keys()])
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def face_slice(self) -> _FaceSliceSelector:
|
|
51
|
+
return _FaceSliceSelector(self._face_numbers, self.faces)
|
|
52
|
+
|
|
53
|
+
def faces(self, *numbers: int) -> FaceSelection:
|
|
54
|
+
"""Select a set of faces by number
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
FaceSelection: _description_
|
|
58
|
+
"""
|
|
59
|
+
names = [f'Face{num}' for num in numbers]
|
|
60
|
+
return super().faces(*names)
|
|
61
|
+
|
|
7
62
|
|
|
8
|
-
|
|
63
|
+
class STEPItems:
|
|
64
|
+
"""STEPItems imports geometries form a STEP file and exposes them to the user.
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
def __init__(self, name: str, filename: str, unit: float = 1.0):
|
|
9
68
|
"""Imports the provided STEP file.
|
|
10
69
|
Specify the unit in case of scaling issues where mm units are not taken into consideration.
|
|
11
70
|
|
|
@@ -16,62 +75,139 @@ class STEPItems:
|
|
|
16
75
|
Raises:
|
|
17
76
|
FileNotFoundError: If a file does not exist
|
|
18
77
|
"""
|
|
78
|
+
self.name: str = name
|
|
79
|
+
|
|
19
80
|
stl_path = Path(filename)
|
|
20
|
-
gmsh.option.setNumber("Geometry.OCCScaling",
|
|
21
|
-
gmsh.option.setNumber("Geometry.OCCImportLabels",
|
|
81
|
+
gmsh.option.setNumber("Geometry.OCCScaling", unit)
|
|
82
|
+
gmsh.option.setNumber("Geometry.OCCImportLabels", 1)
|
|
22
83
|
|
|
23
84
|
if not stl_path.exists:
|
|
24
85
|
raise FileNotFoundError(f'File with name {stl_path} does not exist.')
|
|
25
86
|
|
|
26
87
|
dimtags = gmsh.model.occ.import_shapes(filename, format='step')
|
|
27
|
-
|
|
28
|
-
gmsh.model.occ.affine_transform(dimtags, np.array([unit, 0, 0, 0,
|
|
29
|
-
0, unit, 0, 0,
|
|
30
|
-
0, 0, unit, 0,
|
|
31
|
-
0, 0, 0, 1]))
|
|
32
|
-
#dimtags = gmsh.model.occ.heal_shapes(dimtags, tolerance=1e-6)
|
|
33
88
|
|
|
34
|
-
self.points:
|
|
35
|
-
self.edges:
|
|
36
|
-
self.surfaces:
|
|
37
|
-
self.volumes:
|
|
38
|
-
|
|
89
|
+
self.points: list[GeoPoint] = []
|
|
90
|
+
self.edges: list[GeoEdge] = []
|
|
91
|
+
self.surfaces: list[GeoSurface] = []
|
|
92
|
+
self.volumes: list[GeoVolume] = []
|
|
93
|
+
|
|
39
94
|
i = 0
|
|
40
95
|
for dim, tag in dimtags:
|
|
41
|
-
name = gmsh.model.getPhysicalName(dim, tag)
|
|
96
|
+
name = gmsh.model.getPhysicalName(dim, tag) #for now, this doesn't actually ever work.
|
|
42
97
|
if name == '':
|
|
43
98
|
name = f'Obj{i}'
|
|
44
99
|
i+=1
|
|
45
100
|
if dim == 0:
|
|
46
|
-
self.points
|
|
101
|
+
self.points.append(GeoPoint(tag, name=f'{self.name}_{name}'))
|
|
47
102
|
elif dim == 1:
|
|
48
|
-
self.edges
|
|
103
|
+
self.edges.append(GeoEdge(tag, name=f'{self.name}_{name}'))
|
|
49
104
|
elif dim == 2:
|
|
50
|
-
self.surfaces
|
|
105
|
+
self.surfaces.append(GeoSurface(tag, name=f'{self.name}_{name}'))
|
|
51
106
|
elif dim == 3:
|
|
52
|
-
self.volumes
|
|
53
|
-
|
|
107
|
+
self.volumes.append(StepVolume(tag, name=f'{self.name}_{name}'))
|
|
108
|
+
|
|
109
|
+
gmsh.model.occ.synchronize()
|
|
110
|
+
|
|
54
111
|
@property
|
|
55
|
-
def
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
yield self.surfaces
|
|
59
|
-
yield self.volumes
|
|
60
|
-
|
|
112
|
+
def dictionary(self) -> dict[str, GeoObject]:
|
|
113
|
+
return {obj.name: obj for obj in self.objects}
|
|
114
|
+
|
|
61
115
|
@property
|
|
62
116
|
def objects(self) -> tuple[GeoObject,...]:
|
|
63
|
-
objects
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
117
|
+
"""Returns a list of all objects in the STEP file
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
tuple[GeoObject,...]: _description_
|
|
121
|
+
"""
|
|
122
|
+
return tuple(self.points+self.edges+self.surfaces+self.volumes)
|
|
123
|
+
|
|
124
|
+
def __getitem__(self, name: str) -> GeoObject | None:
|
|
125
|
+
return self.dictionary.get(name, None)
|
|
126
|
+
|
|
127
|
+
def as_volume(self) -> StepVolume:
|
|
128
|
+
"""Returns the 3D volumetric part of the STEP file as a single geometry
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
StepVolume: The resultant StepVolume(GeoVolume) object.
|
|
132
|
+
"""
|
|
133
|
+
if len(self.volumes)==1:
|
|
134
|
+
return self.volumes[0]
|
|
135
|
+
return unite(*self.volumes)._auto_face_tag()
|
|
136
|
+
|
|
137
|
+
def as_surface(self) -> GeoSurface:
|
|
138
|
+
"""Returns the 2D surface part of the STEP file as a single geometry
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
GeoSurface: The resultant GeoSurface object
|
|
143
|
+
"""
|
|
144
|
+
if len(self.surfaces)==1:
|
|
145
|
+
return self.surfaces[0]
|
|
146
|
+
return unite(*self.surfaces)
|
|
147
|
+
|
|
148
|
+
def as_edge(self) -> GeoEdge:
|
|
149
|
+
"""Returns the 1D Edge part of the STEP file as a single geometry
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
GeoEdge: The resultant GeoEdge object
|
|
153
|
+
"""
|
|
154
|
+
if len(self.edges)==1:
|
|
155
|
+
return self.edges[1]
|
|
156
|
+
return unite(*self.edges)
|
|
67
157
|
|
|
68
|
-
def
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
158
|
+
def as_point(self) -> GeoPoint:
|
|
159
|
+
"""Returns the 0D Point part of the STEP file as a single geometry
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
GeoPoint: The resultant GeoPoint object.
|
|
163
|
+
"""
|
|
164
|
+
if len(self.points)==1:
|
|
165
|
+
return self.points[0]
|
|
166
|
+
return unite(*self.points)
|
|
167
|
+
|
|
168
|
+
def enclose(self,
|
|
169
|
+
margin: float = None,
|
|
170
|
+
x_margins: tuple[float, float] = (None, None),
|
|
171
|
+
y_margins: tuple[float, float] = (None, None),
|
|
172
|
+
z_margins: tuple[float, float] = (None, None)) -> Box:
|
|
173
|
+
"""Create an enclosing bounding box for the step model.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
margin (float, optional): _description_. Defaults to 0.
|
|
177
|
+
x_margins (tuple[float, float], optional): _description_. Defaults to (0., 0.).
|
|
178
|
+
y_margins (tuple[float, float], optional): _description_. Defaults to (0., 0.).
|
|
179
|
+
z_margins (tuple[float, float], optional): _description_. Defaults to (0., 0.).
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
Box: _description_
|
|
183
|
+
"""
|
|
184
|
+
xminm = _select_num(margin, x_margins[0])
|
|
185
|
+
xmaxm = _select_num(margin, x_margins[1])
|
|
186
|
+
yminm = _select_num(margin, y_margins[0])
|
|
187
|
+
ymaxm = _select_num(margin, y_margins[1])
|
|
188
|
+
zminm = _select_num(margin, z_margins[0])
|
|
189
|
+
zmaxm = _select_num(margin, z_margins[1])
|
|
190
|
+
|
|
191
|
+
xmin = 1000000
|
|
192
|
+
xmax = -1000000
|
|
193
|
+
ymin = 1000000
|
|
194
|
+
ymax = -1000000
|
|
195
|
+
zmin = 1000000
|
|
196
|
+
zmax = -1000000
|
|
197
|
+
|
|
198
|
+
for obj in self.objects:
|
|
199
|
+
for dim, tag in obj.dimtags:
|
|
200
|
+
x1, y1, z1, x2, y2, z2 = gmsh.model.occ.getBoundingBox(dim, tag)
|
|
201
|
+
xmin = min(xmin, x1)
|
|
202
|
+
xmax = max(xmax, x2)
|
|
203
|
+
ymin = min(ymin, y1)
|
|
204
|
+
ymax = max(ymax, y2)
|
|
205
|
+
zmin = min(zmin, z1)
|
|
206
|
+
zmax = max(zmax, z2)
|
|
207
|
+
|
|
208
|
+
width = xmax-xmin + xminm + xmaxm
|
|
209
|
+
depth = ymax-ymin + yminm + ymaxm
|
|
210
|
+
height = zmax -zmin + zminm + zmaxm
|
|
211
|
+
|
|
212
|
+
return Box(width, depth, height, (xmin-xminm, ymin-yminm, zmin-zminm)).background()
|
|
213
|
+
|
emerge/_emerge/geometry.py
CHANGED
|
@@ -18,13 +18,12 @@
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
import gmsh # type: ignore
|
|
20
20
|
from .material import Material, AIR
|
|
21
|
-
from .selection import FaceSelection, DomainSelection, EdgeSelection, PointSelection, Selection
|
|
21
|
+
from .selection import FaceSelection, DomainSelection, EdgeSelection, PointSelection, Selection, SelectionError
|
|
22
22
|
from loguru import logger
|
|
23
|
-
from typing import Literal, Any, Iterable,
|
|
23
|
+
from typing import Literal, Any, Iterable, Callable
|
|
24
24
|
import numpy as np
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
|
|
28
27
|
def _map_tags(tags: list[int], mapping: dict[int, list[int]]):
|
|
29
28
|
new_tags = []
|
|
30
29
|
for tag in tags:
|
|
@@ -34,6 +33,7 @@ def _map_tags(tags: list[int], mapping: dict[int, list[int]]):
|
|
|
34
33
|
def _bbcenter(x1, y1, z1, x2, y2, z2):
|
|
35
34
|
return np.array([(x1+x2)/2, (y1+y2)/2, (z1+z2)/2])
|
|
36
35
|
|
|
36
|
+
|
|
37
37
|
FaceNames = Literal['back','front','left','right','top','bottom']
|
|
38
38
|
|
|
39
39
|
class _KEY_GENERATOR:
|
|
@@ -232,6 +232,8 @@ class _FacePointer:
|
|
|
232
232
|
def copy(self) -> _FacePointer:
|
|
233
233
|
return _FacePointer(self.o, self.n)
|
|
234
234
|
|
|
235
|
+
def __eq__(self, other: _FacePointer) -> bool:
|
|
236
|
+
return (np.linalg.norm(self.o - other.o) + np.linalg.norm(self.n - other.n)) < 1e-6
|
|
235
237
|
|
|
236
238
|
_GENERATOR = _KEY_GENERATOR()
|
|
237
239
|
_GEOMANAGER = _GeometryManager()
|
|
@@ -241,6 +243,7 @@ class GeoObject:
|
|
|
241
243
|
"""
|
|
242
244
|
dim: int = -1
|
|
243
245
|
_default_name: str = 'GeoObject'
|
|
246
|
+
|
|
244
247
|
def __init__(self, tags: list[int] | None = None, name: str | None = None):
|
|
245
248
|
if tags is None:
|
|
246
249
|
tags = []
|
|
@@ -263,7 +266,24 @@ class GeoObject:
|
|
|
263
266
|
|
|
264
267
|
self.give_name(name)
|
|
265
268
|
_GEOMANAGER.submit_geometry(self)
|
|
266
|
-
|
|
269
|
+
self._fill_face_pointers()
|
|
270
|
+
|
|
271
|
+
def _fill_face_pointers(self) -> None:
|
|
272
|
+
""" Fills the list of all face pointers of this object
|
|
273
|
+
"""
|
|
274
|
+
current = list(self._face_pointers.values())
|
|
275
|
+
ctr = 0
|
|
276
|
+
gmsh.model.occ.synchronize()
|
|
277
|
+
for dim, tag in gmsh.model.get_boundary(self.dimtags, True, False):
|
|
278
|
+
if dim != 2:
|
|
279
|
+
continue
|
|
280
|
+
o = gmsh.model.occ.get_center_of_mass(2, tag)
|
|
281
|
+
n = gmsh.model.get_normal(tag, (0,0))
|
|
282
|
+
fp = _FacePointer(o, n)
|
|
283
|
+
if fp not in current:
|
|
284
|
+
self._face_pointers[f'Face{ctr}'] = fp
|
|
285
|
+
ctr += 1
|
|
286
|
+
|
|
267
287
|
def _store(self, name: str, data: Any) -> None:
|
|
268
288
|
"""Store a property as auxilliary data under a given name
|
|
269
289
|
|
|
@@ -361,6 +381,19 @@ class GeoObject:
|
|
|
361
381
|
def _data(self, *labels) -> tuple[Any | None, ...]:
|
|
362
382
|
return tuple([self._aux_data[lab] for lab in labels])
|
|
363
383
|
|
|
384
|
+
def _replace_pointer(self, name: str, face_pointer: _FacePointer) -> None:
|
|
385
|
+
"""Will be used to replace face pointers so only one unique one exists.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
name (str): _description_
|
|
389
|
+
face_pointer (_FacePointer): _description_
|
|
390
|
+
"""
|
|
391
|
+
for key, fp in self._face_pointers.items():
|
|
392
|
+
if fp == face_pointer:
|
|
393
|
+
self._face_pointers.pop(key)
|
|
394
|
+
break
|
|
395
|
+
self._face_pointers[name] = face_pointer
|
|
396
|
+
|
|
364
397
|
def _add_face_pointer(self,
|
|
365
398
|
name: str,
|
|
366
399
|
origin: np.ndarray | None = None,
|
|
@@ -378,15 +411,16 @@ class GeoObject:
|
|
|
378
411
|
ValueError: _description_
|
|
379
412
|
"""
|
|
380
413
|
if tag is not None:
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
414
|
+
origin = gmsh.model.occ.get_center_of_mass(2, tag)
|
|
415
|
+
normal = gmsh.model.get_normal(tag, (0,0))
|
|
416
|
+
|
|
417
|
+
if origin is None or normal is None:
|
|
418
|
+
raise ValueError('Eitehr a tag or an origin + normal must be provided!')
|
|
419
|
+
fp = _FacePointer(origin, normal)
|
|
420
|
+
self._face_pointers[name] = fp
|
|
421
|
+
#self._replace_pointer(name, fp) <-- Will be added in later versions
|
|
422
|
+
|
|
423
|
+
|
|
390
424
|
def make_copy(self) -> GeoObject:
|
|
391
425
|
""" Copies this object and returns a new object (also in GMSH)"""
|
|
392
426
|
new_dimtags = gmsh.model.occ.copy(self.dimtags)
|
|
@@ -452,7 +486,7 @@ class GeoObject:
|
|
|
452
486
|
def _face_tags(self, name: FaceNames, tool: GeoObject | None = None) -> list[int]:
|
|
453
487
|
names = self._all_pointer_names
|
|
454
488
|
if name not in names:
|
|
455
|
-
raise ValueError(f'The face {name} does not exist in {self}')
|
|
489
|
+
raise ValueError(f'The face {name} does not exist in {self}. Only {list(self._face_pointers.keys())}')
|
|
456
490
|
|
|
457
491
|
gmsh.model.occ.synchronize()
|
|
458
492
|
dimtags = gmsh.model.get_boundary(self.dimtags, True, False)
|
|
@@ -467,6 +501,7 @@ class GeoObject:
|
|
|
467
501
|
logger.info(f'Selected face {tags}.')
|
|
468
502
|
return tags
|
|
469
503
|
|
|
504
|
+
|
|
470
505
|
def set_material(self, material: Material) -> GeoObject:
|
|
471
506
|
self.material = material
|
|
472
507
|
return self
|
|
@@ -563,6 +598,9 @@ class GeoObject:
|
|
|
563
598
|
Returns:
|
|
564
599
|
FaceSelection: The selected faces
|
|
565
600
|
"""
|
|
601
|
+
# if not self._exists:
|
|
602
|
+
# raise SelectionError('Cannot select faces from an object that no longer exists.')
|
|
603
|
+
|
|
566
604
|
if isinstance(exclude, str):
|
|
567
605
|
exclude = (exclude,)
|
|
568
606
|
|
|
@@ -572,11 +610,13 @@ class GeoObject:
|
|
|
572
610
|
if tags is None:
|
|
573
611
|
tags = []
|
|
574
612
|
|
|
575
|
-
|
|
576
613
|
for name in exclude:
|
|
577
614
|
tags.extend(self.face(name, tool=tool).tags)
|
|
578
615
|
dimtags = gmsh.model.get_boundary(self.dimtags, True, False)
|
|
579
|
-
|
|
616
|
+
selname = 'Boundary'
|
|
617
|
+
if exclude:
|
|
618
|
+
selname = selname + 'Except[' + ','.join(exclude) + ']'
|
|
619
|
+
return FaceSelection([t for d,t in dimtags if t not in tags])._named(selname)
|
|
580
620
|
|
|
581
621
|
def face(self, name: FaceNames = None, tool: GeoObject | None = None, no: FaceNames = None) -> FaceSelection:
|
|
582
622
|
"""Returns the FaceSelection for a given face name.
|
|
@@ -596,8 +636,16 @@ class GeoObject:
|
|
|
596
636
|
if no is not None:
|
|
597
637
|
return self.boundary(exclude=no)
|
|
598
638
|
|
|
599
|
-
return FaceSelection(self._face_tags(name, tool))
|
|
639
|
+
return FaceSelection(self._face_tags(name, tool))._named(name)
|
|
640
|
+
|
|
641
|
+
def all_faces(self) -> list[FaceSelection]:
|
|
642
|
+
"""Returns a list of all face selections of this object
|
|
600
643
|
|
|
644
|
+
Returns:
|
|
645
|
+
list[FaceSelection]: A list of all face selections
|
|
646
|
+
"""
|
|
647
|
+
return [self.face(name) for name in self._face_pointers]
|
|
648
|
+
|
|
601
649
|
def faces(self, *names: FaceNames, tool: GeoObject | None = None) -> FaceSelection:
|
|
602
650
|
"""Returns the FaceSelection for a given face names.
|
|
603
651
|
|
|
@@ -613,7 +661,7 @@ class GeoObject:
|
|
|
613
661
|
tags = []
|
|
614
662
|
for name in names:
|
|
615
663
|
tags.extend(self._face_tags(name, tool))
|
|
616
|
-
return FaceSelection(tags)
|
|
664
|
+
return FaceSelection(tags)._named('Faces[' + ','.join(names) + ']')
|
|
617
665
|
|
|
618
666
|
def hide(self) -> GeoObject:
|
|
619
667
|
"""Hides the object from views
|
|
@@ -658,7 +706,22 @@ class GeoObject:
|
|
|
658
706
|
def remove(self) -> None:
|
|
659
707
|
self._exists = False
|
|
660
708
|
gmsh.model.occ.remove(self.dimtags, True)
|
|
661
|
-
|
|
709
|
+
|
|
710
|
+
def extract(self, tags: int | list[int]) -> GeoObject:
|
|
711
|
+
"""Returns a new GeoObject of the same dimensional type that isolates a set of given tags
|
|
712
|
+
|
|
713
|
+
Args:
|
|
714
|
+
tags (list[int]): A list of GMSH tags
|
|
715
|
+
|
|
716
|
+
Returns:
|
|
717
|
+
GeoObject: _description_
|
|
718
|
+
"""
|
|
719
|
+
if isinstance(tags, int):
|
|
720
|
+
tags = [tags,]
|
|
721
|
+
self.tags = [t for t in self.tags if t not in tags]
|
|
722
|
+
dts = [(self.dim, t) for t in tags]
|
|
723
|
+
return GeoObject.from_dimtags(dts)
|
|
724
|
+
|
|
662
725
|
class GeoVolume(GeoObject):
|
|
663
726
|
'''GeoVolume is an interface to the GMSH CAD kernel. It does not represent EMerge
|
|
664
727
|
specific geometry data.'''
|
|
@@ -675,10 +738,94 @@ class GeoVolume(GeoObject):
|
|
|
675
738
|
else:
|
|
676
739
|
self.tags = [tag,]
|
|
677
740
|
|
|
741
|
+
self._fill_face_pointers()
|
|
742
|
+
self._autoname()
|
|
743
|
+
|
|
678
744
|
@property
|
|
679
745
|
def selection(self) -> DomainSelection:
|
|
680
746
|
return DomainSelection(self.tags)
|
|
681
747
|
|
|
748
|
+
def _auto_face_tag(self) -> GeoVolume:
|
|
749
|
+
self._face_pointers = dict()
|
|
750
|
+
self._tools = dict()
|
|
751
|
+
logger.trace('Automatically assigning face pointers.')
|
|
752
|
+
ctr = 0
|
|
753
|
+
for tag in self.tags:
|
|
754
|
+
loops = gmsh.model.occ.getSurfaceLoops(tag)
|
|
755
|
+
for loopcluster in loops[1]:
|
|
756
|
+
for st in loopcluster:
|
|
757
|
+
self._add_face_pointer(f'Face{ctr}', tag=st)
|
|
758
|
+
ctr += 1
|
|
759
|
+
return self
|
|
760
|
+
|
|
761
|
+
def exterior_faces(self, base_object: GeoObject) -> FaceSelection:
|
|
762
|
+
"""Select the exterior faces of an object based on the face
|
|
763
|
+
pointers of an original base object. For example
|
|
764
|
+
|
|
765
|
+
Example:
|
|
766
|
+
>>> cheese = em.geo.Box(...)
|
|
767
|
+
>>> hole = em.geo.Box(...)
|
|
768
|
+
>>> holed_cheese = em.geo.subtract(cheese, hole)
|
|
769
|
+
>>> holed_cheese.exterior_faces(cheese)
|
|
770
|
+
|
|
771
|
+
Args:
|
|
772
|
+
base_object (GeoObject): The object to base the selection on.
|
|
773
|
+
|
|
774
|
+
Returns:
|
|
775
|
+
FaceSelection: The resultant face selection
|
|
776
|
+
"""
|
|
777
|
+
dimtags = gmsh.model.get_boundary(self.dimtags, True, False)
|
|
778
|
+
|
|
779
|
+
normals = [gmsh.model.get_normal(t, [0,0]) for d,t, in dimtags]
|
|
780
|
+
origins = [gmsh.model.occ.get_center_of_mass(d, t) for d,t in dimtags]
|
|
781
|
+
|
|
782
|
+
tool_tags = []
|
|
783
|
+
for key, tool in self._tools.items():
|
|
784
|
+
if key == base_object._key:
|
|
785
|
+
continue
|
|
786
|
+
for name in tool.keys():
|
|
787
|
+
tags = tool[name].find(dimtags, origins, normals)
|
|
788
|
+
tool_tags.extend(tags)
|
|
789
|
+
tags = [dt[1] for dt in dimtags if dt[1] not in tool_tags]
|
|
790
|
+
return FaceSelection(tags)
|
|
791
|
+
|
|
792
|
+
def _find_fp(self, condition: Callable):
|
|
793
|
+
for key, fp in self._face_pointers.items():
|
|
794
|
+
if condition(fp):
|
|
795
|
+
return key, fp
|
|
796
|
+
return None, None
|
|
797
|
+
|
|
798
|
+
def _rename_fp(self, new_name: str, condition: Callable):
|
|
799
|
+
name, fp = self._find_fp(condition)
|
|
800
|
+
if fp is not None:
|
|
801
|
+
self._face_pointers.pop(name)
|
|
802
|
+
self._face_pointers[new_name] = fp
|
|
803
|
+
|
|
804
|
+
def _autoname(self) -> None:
|
|
805
|
+
if len(self._face_pointers)==0:
|
|
806
|
+
return
|
|
807
|
+
|
|
808
|
+
xs = []
|
|
809
|
+
ys = []
|
|
810
|
+
zs = []
|
|
811
|
+
for fp in self._face_pointers.values():
|
|
812
|
+
xs.append(fp.o[0])
|
|
813
|
+
ys.append(fp.o[1])
|
|
814
|
+
zs.append(fp.o[2])
|
|
815
|
+
minx = min(xs)
|
|
816
|
+
maxx = max(xs)
|
|
817
|
+
miny = min(ys)
|
|
818
|
+
maxy = max(ys)
|
|
819
|
+
minz = min(zs)
|
|
820
|
+
maxz = max(zs)
|
|
821
|
+
|
|
822
|
+
self._rename_fp('-x', lambda fp: (fp.o[0]==minx) and np.abs(np.dot(fp.n, np.array([1,0,0])))>0.999)
|
|
823
|
+
self._rename_fp('+x', lambda fp: (fp.o[0]==maxx) and np.abs(np.dot(fp.n, np.array([1,0,0])))>0.999)
|
|
824
|
+
self._rename_fp('-y', lambda fp: (fp.o[1]==miny) and np.abs(np.dot(fp.n, np.array([0,1,0])))>0.999)
|
|
825
|
+
self._rename_fp('+y', lambda fp: (fp.o[1]==maxy) and np.abs(np.dot(fp.n, np.array([0,1,0])))>0.999)
|
|
826
|
+
self._rename_fp('-z', lambda fp: (fp.o[2]==minz) and np.abs(np.dot(fp.n, np.array([0,0,1])))>0.999)
|
|
827
|
+
self._rename_fp('+z', lambda fp: (fp.o[2]==maxz) and np.abs(np.dot(fp.n, np.array([0,0,1])))>0.999)
|
|
828
|
+
|
|
682
829
|
class GeoPoint(GeoObject):
|
|
683
830
|
dim = 0
|
|
684
831
|
_default_name: str = 'GeoPoint'
|
emerge/_emerge/material.py
CHANGED
|
@@ -244,6 +244,7 @@ class FreqCoordDependent(MatProperty):
|
|
|
244
244
|
if scalar is not None:
|
|
245
245
|
def _func(f, x, y, z) -> np.ndarray:
|
|
246
246
|
return np.eye(3)[:, :, None] * scalar(f,x,y,z)[None, None, :]
|
|
247
|
+
|
|
247
248
|
if vector is not None:
|
|
248
249
|
def _func(f,x, y, z) -> np.ndarray:
|
|
249
250
|
N = x.shape[0]
|
|
@@ -251,6 +252,7 @@ class FreqCoordDependent(MatProperty):
|
|
|
251
252
|
idx = np.arange(3)
|
|
252
253
|
out[idx, idx, :] = vector(f,x,y,z)
|
|
253
254
|
return out
|
|
255
|
+
|
|
254
256
|
if matrix is not None:
|
|
255
257
|
_func = matrix
|
|
256
258
|
|
emerge/_emerge/mesh3d.py
CHANGED
|
@@ -420,7 +420,7 @@ class Mesh3D(Mesh):
|
|
|
420
420
|
self.tet_to_edge_sign = np.zeros((6, self.tets.shape[1]), dtype=int) + _MISSING_ID
|
|
421
421
|
self.tet_to_tri = np.zeros((4, self.tets.shape[1]), dtype=int) + _MISSING_ID
|
|
422
422
|
self.tet_to_tri_sign = np.zeros((4, self.tets.shape[1]), dtype=int) + _MISSING_ID
|
|
423
|
-
|
|
423
|
+
|
|
424
424
|
tri_to_tet = defaultdict(list)
|
|
425
425
|
for itet in range(self.tets.shape[1]):
|
|
426
426
|
edge_ids = [self.get_edge(self.tets[i-1,itet],self.tets[j-1,itet]) for i,j in zip([1, 1, 1, 2, 4, 3], [2, 3, 4, 3, 2, 4])]
|
|
@@ -503,7 +503,6 @@ class Mesh3D(Mesh):
|
|
|
503
503
|
continue
|
|
504
504
|
self.etag_to_edge[t] = [int(self.edge_t2i.get(tag,None)) for tag in edge_tags[0] if tag in self.edge_t2i]
|
|
505
505
|
|
|
506
|
-
|
|
507
506
|
## Tag bindings
|
|
508
507
|
logger.trace('Constructing geometry to mesh mappings.')
|
|
509
508
|
face_dimtags = gmsh.model.get_entities(2)
|
|
@@ -512,7 +511,6 @@ class Mesh3D(Mesh):
|
|
|
512
511
|
node_tags = [self.n_t2i[int(t)] for t in node_tags[0]]
|
|
513
512
|
self.ftag_to_node[t] = node_tags
|
|
514
513
|
node_tags = np.squeeze(np.array(node_tags)).reshape(-1,3).T
|
|
515
|
-
|
|
516
514
|
self.ftag_to_tri[t] = [self.get_tri(node_tags[0,i], node_tags[1,i], node_tags[2,i]) for i in range(node_tags.shape[1])]
|
|
517
515
|
self.ftag_to_edge[t] = sorted(list(np.unique(self.tri_to_edge[:,self.ftag_to_tri[t]].flatten())))
|
|
518
516
|
|
emerge/_emerge/mesher.py
CHANGED
|
@@ -75,9 +75,13 @@ class Mesher:
|
|
|
75
75
|
self.objects: list[GeoObject] = []
|
|
76
76
|
self.size_definitions: list[tuple[int, float]] = []
|
|
77
77
|
self.mesh_fields: list[int] = []
|
|
78
|
+
|
|
78
79
|
self._amr_fields: list[int] = []
|
|
79
80
|
self._amr_coords: np.ndarray = None
|
|
80
81
|
self._amr_sizes: np.ndarray = None
|
|
82
|
+
self._amr_ratios: np.ndrray = None
|
|
83
|
+
self._amr_new: np.ndarray = None
|
|
84
|
+
|
|
81
85
|
self.min_size: float = None
|
|
82
86
|
self.max_size: float = None
|
|
83
87
|
self.periodic_cell: PeriodicCell = None
|
|
@@ -152,7 +156,6 @@ class Mesher:
|
|
|
152
156
|
3: dict()}
|
|
153
157
|
if len(objects) > 0: # type: ignore
|
|
154
158
|
dimtags, output_mapping = gmsh.model.occ.fragment(final_dimtags, embedding_dimtags)
|
|
155
|
-
|
|
156
159
|
for domain, mapping in zip(final_dimtags + embedding_dimtags, output_mapping):
|
|
157
160
|
tag_mapping[domain[0]][domain[1]] = [o[1] for o in mapping]
|
|
158
161
|
for dom in objects: # type: ignore
|
|
@@ -289,7 +292,7 @@ class Mesher:
|
|
|
289
292
|
gmsh.model.mesh.setSizeFromBoundary(dimtag[0], dimtag[1], 0)
|
|
290
293
|
|
|
291
294
|
|
|
292
|
-
def add_refinement_points(self, coords: np.ndarray, sizes: np.ndarray
|
|
295
|
+
def add_refinement_points(self, coords: np.ndarray, sizes: np.ndarray, ratios: np.ndarray):
|
|
293
296
|
if self._amr_coords is None:
|
|
294
297
|
self._amr_coords = coords
|
|
295
298
|
else:
|
|
@@ -299,25 +302,46 @@ class Mesher:
|
|
|
299
302
|
self._amr_sizes = sizes
|
|
300
303
|
else:
|
|
301
304
|
self._amr_sizes = np.hstack((self._amr_sizes, sizes))
|
|
302
|
-
|
|
305
|
+
|
|
306
|
+
if self._amr_ratios is None:
|
|
307
|
+
self._amr_ratios = ratios
|
|
308
|
+
else:
|
|
309
|
+
self._amr_ratios = np.hstack((self._amr_ratios, ratios))
|
|
310
|
+
|
|
311
|
+
if self._amr_new is None:
|
|
312
|
+
self._amr_new = np.ones_like(sizes)
|
|
313
|
+
else:
|
|
314
|
+
self._amr_new = np.hstack((0.0*self._amr_new, np.ones_like(sizes)))
|
|
315
|
+
|
|
316
|
+
|
|
303
317
|
def set_refinement_function(self,
|
|
304
|
-
refinement: float,
|
|
305
318
|
gr: float = 1.5,
|
|
306
319
|
_qf: float = 1.0):
|
|
307
320
|
xs = self._amr_coords[0,:]
|
|
308
321
|
ys = self._amr_coords[1,:]
|
|
309
322
|
zs = self._amr_coords[2,:]
|
|
310
|
-
newsize =
|
|
323
|
+
newsize = self._amr_ratios*self._amr_sizes
|
|
311
324
|
A = newsize/gr
|
|
312
325
|
B = (1-gr)/gr
|
|
313
326
|
from numba import njit, i8, f8
|
|
314
327
|
|
|
315
328
|
@njit(f8(i8,i8,f8,f8,f8,f8), nogil=True, fastmath=True, parallel=False)
|
|
316
329
|
def func(dim, tag, x, y, z, lc):
|
|
317
|
-
sizes = np.maximum(newsize, A - B * _qf*np.sqrt((x-xs)**2 + (y-ys)**2 + (z-zs)**2))
|
|
330
|
+
sizes = np.maximum(newsize, A - B * _qf*np.clip(np.sqrt((x-xs)**2 + (y-ys)**2 + (z-zs)**2) - newsize/3, a_min=0, a_max=None))
|
|
318
331
|
return min(lc, float(np.min(sizes)))
|
|
319
|
-
gmsh.model.mesh.setSizeCallback(func)
|
|
320
332
|
|
|
333
|
+
gmsh.model.mesh.setSizeCallback(func)
|
|
334
|
+
|
|
335
|
+
def refine_finer(self, factor: float):
|
|
336
|
+
newids = self._amr_new==1
|
|
337
|
+
self._amr_ratios[newids] = self._amr_ratios[newids]*(0.85**factor)
|
|
338
|
+
return self._amr_ratios[newids][0]
|
|
339
|
+
|
|
340
|
+
def refine_coarser(self, factor: float):
|
|
341
|
+
newids = self._amr_new==1
|
|
342
|
+
self._amr_ratios[newids] = self._amr_ratios[newids]/(0.85**factor)
|
|
343
|
+
return self._amr_ratios[newids][0]
|
|
344
|
+
|
|
321
345
|
def add_refinement_point(self,
|
|
322
346
|
coordinate: np.ndarray,
|
|
323
347
|
refinement: float,
|
|
@@ -389,7 +413,7 @@ class Mesher:
|
|
|
389
413
|
if obj.dim==2:
|
|
390
414
|
logger.warning('Forwarding to set_face_size')
|
|
391
415
|
self.set_face_size(obj, size)
|
|
392
|
-
logger.debug(f'Setting size {size*1000:.3f}
|
|
416
|
+
logger.debug(f'Setting size {size*1000:.3f}mm for object {obj}')
|
|
393
417
|
self._set_size_in_domain(obj.tags, size)
|
|
394
418
|
|
|
395
419
|
def set_face_size(self, obj: GeoSurface | Selection, size: float):
|
|
@@ -405,7 +429,7 @@ class Mesher:
|
|
|
405
429
|
logger.warning('Forwarding to set_domain_size')
|
|
406
430
|
self.set_face_size(obj, size)
|
|
407
431
|
|
|
408
|
-
logger.debug(f'Setting size {size*1000:.3f}
|
|
432
|
+
logger.debug(f'Setting size {size*1000:.3f}mm for face {obj}')
|
|
409
433
|
self._set_size_on_face(obj.tags, size)
|
|
410
434
|
|
|
411
435
|
def set_size(self, obj: GeoObject, size: float) -> None:
|