emerge 1.0.7__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.

Files changed (33) hide show
  1. emerge/__init__.py +15 -3
  2. emerge/_emerge/const.py +2 -1
  3. emerge/_emerge/elements/ned2_interp.py +122 -42
  4. emerge/_emerge/geo/__init__.py +1 -1
  5. emerge/_emerge/geo/operations.py +20 -0
  6. emerge/_emerge/geo/pcb.py +162 -71
  7. emerge/_emerge/geo/shapes.py +12 -7
  8. emerge/_emerge/geo/step.py +177 -41
  9. emerge/_emerge/geometry.py +189 -27
  10. emerge/_emerge/logsettings.py +26 -2
  11. emerge/_emerge/material.py +2 -0
  12. emerge/_emerge/mesh3d.py +6 -8
  13. emerge/_emerge/mesher.py +67 -11
  14. emerge/_emerge/mth/common_functions.py +1 -1
  15. emerge/_emerge/mth/optimized.py +2 -2
  16. emerge/_emerge/physics/microwave/adaptive_mesh.py +549 -116
  17. emerge/_emerge/physics/microwave/assembly/assembler.py +9 -1
  18. emerge/_emerge/physics/microwave/microwave_3d.py +133 -83
  19. emerge/_emerge/physics/microwave/microwave_bc.py +158 -8
  20. emerge/_emerge/physics/microwave/microwave_data.py +94 -5
  21. emerge/_emerge/plot/pyvista/display.py +36 -23
  22. emerge/_emerge/selection.py +17 -2
  23. emerge/_emerge/settings.py +124 -6
  24. emerge/_emerge/simmodel.py +273 -150
  25. emerge/_emerge/simstate.py +106 -0
  26. emerge/_emerge/simulation_data.py +11 -23
  27. emerge/_emerge/solve_interfaces/cudss_interface.py +20 -1
  28. emerge/_emerge/solver.py +4 -4
  29. {emerge-1.0.7.dist-info → emerge-1.1.1.dist-info}/METADATA +7 -3
  30. {emerge-1.0.7.dist-info → emerge-1.1.1.dist-info}/RECORD +33 -32
  31. {emerge-1.0.7.dist-info → emerge-1.1.1.dist-info}/WHEEL +0 -0
  32. {emerge-1.0.7.dist-info → emerge-1.1.1.dist-info}/entry_points.txt +0 -0
  33. {emerge-1.0.7.dist-info → emerge-1.1.1.dist-info}/licenses/LICENSE +0 -0
@@ -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
- class STEPItems:
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
- def __init__(self, filename: str, unit: float = 1.0):
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", 1)
21
- gmsh.option.setNumber("Geometry.OCCImportLabels", 2)
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: dict[str, GeoPoint] = dict()
35
- self.edges: dict[str, GeoEdge] = dict()
36
- self.surfaces: dict[str, GeoSurface] = dict()
37
- self.volumes: dict[str, GeoVolume] = dict()
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[name] = GeoPoint(tag)
101
+ self.points.append(GeoPoint(tag, name=f'{self.name}_{name}'))
47
102
  elif dim == 1:
48
- self.edges[name] = GeoEdge(tag)
103
+ self.edges.append(GeoEdge(tag, name=f'{self.name}_{name}'))
49
104
  elif dim == 2:
50
- self.surfaces[name] = GeoSurface(tag)
105
+ self.surfaces.append(GeoSurface(tag, name=f'{self.name}_{name}'))
51
106
  elif dim == 3:
52
- self.volumes[name] = GeoVolume(tag)
53
-
107
+ self.volumes.append(StepVolume(tag, name=f'{self.name}_{name}'))
108
+
109
+ gmsh.model.occ.synchronize()
110
+
54
111
  @property
55
- def _dicts(self):
56
- yield self.points
57
- yield self.edges
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 = tuple()
64
- for dct in self._dicts:
65
- objects = objects + tuple(dct.values())
66
- return objects
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 __getitem__(self, name: str) -> GeoObject:
69
- if name in self.points:
70
- return self.points[name]
71
- elif name in self.edges:
72
- return self.edges[name]
73
- elif name in self.surfaces:
74
- return self.surfaces[name]
75
- elif name in self.volumes:
76
- return self.volumes[name]
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
+
@@ -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, TypeVar
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:
@@ -88,15 +88,12 @@ class _GeometryManager:
88
88
  def sign_in(self, modelname: str) -> None:
89
89
  # if modelname not in self.geometry_list:
90
90
  # self.geometry_list[modelname] = []
91
- if modelname is self.geometry_list:
92
- logger.warning(f'{modelname} already exist, Geometries will be reset.')
93
91
  self.geometry_list[modelname] = dict()
94
92
  self.geometry_names[modelname] = set()
95
93
  self.active = modelname
96
94
 
97
95
  def reset(self, modelname: str) -> None:
98
- self.geometry_list[modelname] = dict()
99
- self.geometry_names[modelname] = set()
96
+ self.sign_in(modelname)
100
97
 
101
98
  def lowest_priority(self) -> int:
102
99
  return min([geo._priority for geo in self.all_geometries()])
@@ -123,9 +120,9 @@ class _FacePointer:
123
120
  normals: list[np.ndarray]) -> list[int]:
124
121
  tags = []
125
122
  for (d,t), o, n in zip(dimtags, origins, normals):
126
- normdist = np.abs((o-self.o)@self.n)
123
+ normdist = np.abs((o-self.o) @ self.n)
127
124
  dotnorm = np.abs(n@self.n)
128
- if normdist < 1e-3 and dotnorm > 0.99:
125
+ if normdist < 1e-5 and dotnorm > 0.999:
129
126
  tags.append(t)
130
127
  return tags
131
128
 
@@ -235,6 +232,8 @@ class _FacePointer:
235
232
  def copy(self) -> _FacePointer:
236
233
  return _FacePointer(self.o, self.n)
237
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
238
237
 
239
238
  _GENERATOR = _KEY_GENERATOR()
240
239
  _GEOMANAGER = _GeometryManager()
@@ -244,6 +243,7 @@ class GeoObject:
244
243
  """
245
244
  dim: int = -1
246
245
  _default_name: str = 'GeoObject'
246
+
247
247
  def __init__(self, tags: list[int] | None = None, name: str | None = None):
248
248
  if tags is None:
249
249
  tags = []
@@ -257,7 +257,7 @@ class GeoObject:
257
257
  self._embeddings: list[GeoObject] = []
258
258
  self._face_pointers: dict[str, _FacePointer] = dict()
259
259
  self._tools: dict[int, dict[str, _FacePointer]] = dict()
260
-
260
+ self._hidden: bool = False
261
261
  self._key = _GENERATOR.new()
262
262
  self._aux_data: dict[str, Any] = dict()
263
263
  self._priority: int = 10
@@ -266,7 +266,24 @@ class GeoObject:
266
266
 
267
267
  self.give_name(name)
268
268
  _GEOMANAGER.submit_geometry(self)
269
-
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
+
270
287
  def _store(self, name: str, data: Any) -> None:
271
288
  """Store a property as auxilliary data under a given name
272
289
 
@@ -364,6 +381,19 @@ class GeoObject:
364
381
  def _data(self, *labels) -> tuple[Any | None, ...]:
365
382
  return tuple([self._aux_data[lab] for lab in labels])
366
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
+
367
397
  def _add_face_pointer(self,
368
398
  name: str,
369
399
  origin: np.ndarray | None = None,
@@ -381,15 +411,16 @@ class GeoObject:
381
411
  ValueError: _description_
382
412
  """
383
413
  if tag is not None:
384
- o = gmsh.model.occ.get_center_of_mass(2, tag)
385
- n = gmsh.model.get_normal(tag, (0,0))
386
- self._face_pointers[name] = _FacePointer(o, n)
387
- return
388
- if origin is not None and normal is not None:
389
- self._face_pointers[name] = _FacePointer(origin, normal)
390
- return
391
- raise ValueError('Eitehr a tag or an origin + normal must be provided!')
392
-
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
+
393
424
  def make_copy(self) -> GeoObject:
394
425
  """ Copies this object and returns a new object (also in GMSH)"""
395
426
  new_dimtags = gmsh.model.occ.copy(self.dimtags)
@@ -455,7 +486,7 @@ class GeoObject:
455
486
  def _face_tags(self, name: FaceNames, tool: GeoObject | None = None) -> list[int]:
456
487
  names = self._all_pointer_names
457
488
  if name not in names:
458
- 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())}')
459
490
 
460
491
  gmsh.model.occ.synchronize()
461
492
  dimtags = gmsh.model.get_boundary(self.dimtags, True, False)
@@ -470,6 +501,7 @@ class GeoObject:
470
501
  logger.info(f'Selected face {tags}.')
471
502
  return tags
472
503
 
504
+
473
505
  def set_material(self, material: Material) -> GeoObject:
474
506
  self.material = material
475
507
  return self
@@ -566,6 +598,9 @@ class GeoObject:
566
598
  Returns:
567
599
  FaceSelection: The selected faces
568
600
  """
601
+ # if not self._exists:
602
+ # raise SelectionError('Cannot select faces from an object that no longer exists.')
603
+
569
604
  if isinstance(exclude, str):
570
605
  exclude = (exclude,)
571
606
 
@@ -575,11 +610,13 @@ class GeoObject:
575
610
  if tags is None:
576
611
  tags = []
577
612
 
578
-
579
613
  for name in exclude:
580
614
  tags.extend(self.face(name, tool=tool).tags)
581
615
  dimtags = gmsh.model.get_boundary(self.dimtags, True, False)
582
- return FaceSelection([t for d,t in dimtags if t not in tags])
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)
583
620
 
584
621
  def face(self, name: FaceNames = None, tool: GeoObject | None = None, no: FaceNames = None) -> FaceSelection:
585
622
  """Returns the FaceSelection for a given face name.
@@ -599,8 +636,16 @@ class GeoObject:
599
636
  if no is not None:
600
637
  return self.boundary(exclude=no)
601
638
 
602
- return FaceSelection(self._face_tags(name, tool))
639
+ return FaceSelection(self._face_tags(name, tool))._named(name)
603
640
 
641
+ def all_faces(self) -> list[FaceSelection]:
642
+ """Returns a list of all face selections of this object
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
+
604
649
  def faces(self, *names: FaceNames, tool: GeoObject | None = None) -> FaceSelection:
605
650
  """Returns the FaceSelection for a given face names.
606
651
 
@@ -616,8 +661,26 @@ class GeoObject:
616
661
  tags = []
617
662
  for name in names:
618
663
  tags.extend(self._face_tags(name, tool))
619
- return FaceSelection(tags)
620
-
664
+ return FaceSelection(tags)._named('Faces[' + ','.join(names) + ']')
665
+
666
+ def hide(self) -> GeoObject:
667
+ """Hides the object from views
668
+
669
+ Returns:
670
+ GeoObject: _description_
671
+ """
672
+ self._hidden = True
673
+ return self
674
+
675
+ def unhide(self) -> GeoObject:
676
+ """Unhides the object from views
677
+
678
+ Returns:
679
+ GeoObject: _description_
680
+ """
681
+ self._hidden = False
682
+ return self
683
+
621
684
  @property
622
685
  def dimtags(self) -> list[tuple[int, int]]:
623
686
  return [(self.dim, tag) for tag in self.tags]
@@ -643,7 +706,22 @@ class GeoObject:
643
706
  def remove(self) -> None:
644
707
  self._exists = False
645
708
  gmsh.model.occ.remove(self.dimtags, True)
646
-
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
+
647
725
  class GeoVolume(GeoObject):
648
726
  '''GeoVolume is an interface to the GMSH CAD kernel. It does not represent EMerge
649
727
  specific geometry data.'''
@@ -660,10 +738,94 @@ class GeoVolume(GeoObject):
660
738
  else:
661
739
  self.tags = [tag,]
662
740
 
741
+ self._fill_face_pointers()
742
+ self._autoname()
743
+
663
744
  @property
664
745
  def selection(self) -> DomainSelection:
665
746
  return DomainSelection(self.tags)
666
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
+
667
829
  class GeoPoint(GeoObject):
668
830
  dim = 0
669
831
  _default_name: str = 'GeoPoint'
@@ -17,7 +17,7 @@
17
17
 
18
18
  from loguru import logger
19
19
  import sys
20
- from typing import Literal
20
+ from typing import Literal, Generator
21
21
  from pathlib import Path
22
22
  import os
23
23
  from collections import deque
@@ -132,4 +132,28 @@ class LogController:
132
132
  self.file_level = loglevel
133
133
  os.environ["EMERGE_FILE_LOGLEVEL"] = loglevel
134
134
 
135
- LOG_CONTROLLER = LogController()
135
+ class DebugCollector:
136
+ """The DebugController is used by EMerge to collect heuristic
137
+ warnings for detections of things that might be causing problems but aren't
138
+ guaranteed to cause them. These logs will be printed at the end of a simulation
139
+ to ensure that users are aware of them if they abort simulations.
140
+
141
+ """
142
+ def __init__(self):
143
+ self.reports: list[str] = []
144
+
145
+ @property
146
+ def any_warnings(self) -> bool:
147
+ return len(self.reports)>0
148
+
149
+ def add_report(self, message: str):
150
+ self.reports.append(message)
151
+
152
+ def all_reports(self) -> Generator[tuple[int, str], None, None]:
153
+
154
+ for i, message in enumerate(self.reports):
155
+ yield i+1, message
156
+
157
+
158
+ LOG_CONTROLLER = LogController()
159
+ DEBUG_COLLECTOR = DebugCollector()
@@ -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