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.

Files changed (39) hide show
  1. emerge/__init__.py +7 -8
  2. emerge/_emerge/elements/femdata.py +4 -3
  3. emerge/_emerge/elements/nedelec2.py +8 -4
  4. emerge/_emerge/elements/nedleg2.py +6 -2
  5. emerge/_emerge/geo/__init__.py +1 -1
  6. emerge/_emerge/geo/pcb.py +149 -66
  7. emerge/_emerge/geo/pcb_tools/dxf.py +361 -0
  8. emerge/_emerge/geo/polybased.py +23 -74
  9. emerge/_emerge/geo/shapes.py +31 -16
  10. emerge/_emerge/geometry.py +120 -21
  11. emerge/_emerge/mesh3d.py +62 -43
  12. emerge/_emerge/{_cache_check.py → mth/_cache_check.py} +2 -2
  13. emerge/_emerge/mth/optimized.py +69 -3
  14. emerge/_emerge/periodic.py +19 -17
  15. emerge/_emerge/physics/microwave/__init__.py +0 -1
  16. emerge/_emerge/physics/microwave/assembly/assembler.py +27 -5
  17. emerge/_emerge/physics/microwave/assembly/generalized_eigen_hb.py +2 -3
  18. emerge/_emerge/physics/microwave/assembly/periodicbc.py +0 -1
  19. emerge/_emerge/physics/microwave/assembly/robin_abc_order2.py +375 -0
  20. emerge/_emerge/physics/microwave/assembly/robinbc.py +37 -38
  21. emerge/_emerge/physics/microwave/microwave_3d.py +11 -19
  22. emerge/_emerge/physics/microwave/microwave_bc.py +38 -21
  23. emerge/_emerge/physics/microwave/microwave_data.py +3 -26
  24. emerge/_emerge/physics/microwave/port_functions.py +4 -4
  25. emerge/_emerge/plot/pyvista/display.py +13 -2
  26. emerge/_emerge/plot/simple_plots.py +4 -1
  27. emerge/_emerge/selection.py +12 -9
  28. emerge/_emerge/simmodel.py +68 -34
  29. emerge/_emerge/solver.py +28 -16
  30. emerge/beta/dxf.py +1 -0
  31. emerge/lib.py +1 -0
  32. emerge/materials/__init__.py +1 -0
  33. emerge/materials/isola.py +294 -0
  34. emerge/materials/rogers.py +58 -0
  35. {emerge-1.0.0.dist-info → emerge-1.0.2.dist-info}/METADATA +18 -4
  36. {emerge-1.0.0.dist-info → emerge-1.0.2.dist-info}/RECORD +39 -33
  37. {emerge-1.0.0.dist-info → emerge-1.0.2.dist-info}/WHEEL +0 -0
  38. {emerge-1.0.0.dist-info → emerge-1.0.2.dist-info}/entry_points.txt +0 -0
  39. {emerge-1.0.0.dist-info → emerge-1.0.2.dist-info}/licenses/LICENSE +0 -0
@@ -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)
@@ -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, list[GeoObject]] = dict()
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].append(geo)
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
- def __init__(self, tags: list[int] | None = None):
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, tags: list[int] | None = None) -> FaceSelection:
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
- def __init__(self, tag: int | Iterable[int]):
539
- super().__init__()
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
- super().__init__(tags)
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 domain_edges(self, dimtags: list[tuple[int,int]]) -> np.ndarray:
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
- for (d,t) in dimtags:
256
- if d==1:
257
- dimtags_edge.append(t)
258
- if d==2:
259
- dimtags_edge.extend(gmsh.model.getBoundary([(d,t),], False, False))
260
- if d==3:
261
- dts = gmsh.model.getBoundary([(d,t),], False, False)
262
- dimtags_edge.extend(gmsh.model.getBoundary(dts, False, False))
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 update(self, periodic_bcs: list[Periodic] | None = None):
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._derive_node_map(bc)
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 _derive_node_map(self, bc: Periodic) -> tuple[dict[int, int], np.ndarray, np.ndarray]:
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 retreive(self, volumes: list[GeoVolume]) -> list[Material]:
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]