emerge 1.0.0__py3-none-any.whl → 1.0.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.

@@ -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
@@ -122,7 +122,6 @@ class Mesh3D(Mesh):
122
122
  self.inv_tets: dict = dict()
123
123
 
124
124
  # Mappings
125
-
126
125
  self.tet_to_edge: np.ndarray = np.array([])
127
126
  self.tet_to_edge_sign: np.ndarray = np.array([])
128
127
  self.tet_to_tri: np.ndarray = np.array([])
@@ -133,7 +132,6 @@ class Mesh3D(Mesh):
133
132
  self.node_to_edge: defaultdict | dict = defaultdict()
134
133
 
135
134
  # Physics mappings
136
-
137
135
  self.tet_to_field: np.ndarray = np.array([])
138
136
  self.edge_to_field: np.ndarray = np.array([])
139
137
  self.tri_to_field: np.ndarray = np.array([])
@@ -149,6 +147,10 @@ class Mesh3D(Mesh):
149
147
  self.vtag_to_tet: dict[int, list[int]] = dict()
150
148
  self.etag_to_edge: dict[int, list[int]] = dict()
151
149
 
150
+ ## Dervied
151
+ self.dimtag_to_center: dict[tuple[int, int], tuple[float, float, float]] = dict()
152
+ self.dimtag_to_edges: dict[tuple[int, int], np.ndarray] = dict()
153
+
152
154
  self.exterior_face_tags: list[int] = []
153
155
 
154
156
  @property
@@ -242,7 +244,7 @@ class Mesh3D(Mesh):
242
244
 
243
245
  return np.array(indices)
244
246
 
245
- def domain_edges(self, dimtags: list[tuple[int,int]]) -> np.ndarray:
247
+ def _domain_edge(self, dimtag: tuple[int,int]) -> np.ndarray:
246
248
  """Returns a np.ndarray of all edge indices corresponding to a set of dimension tags.
247
249
 
248
250
  Args:
@@ -252,21 +254,39 @@ class Mesh3D(Mesh):
252
254
  np.ndarray: The list of mesh edge element indices.
253
255
  """
254
256
  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
-
257
+ d,t = dimtag
258
+ if d==0:
259
+ return np.ndarray([], dtype=np.int64)
260
+ if d==1:
261
+ dimtags_edge.append((1,t))
262
+ if d==2:
263
+ dimtags_edge.extend(gmsh.model.getBoundary([(d,t),], False, False))
264
+ if d==3:
265
+ dts = gmsh.model.getBoundary([(d,t),], False, False)
266
+ dimtags_edge.extend(gmsh.model.getBoundary(dts, False, False))
267
+
264
268
  edge_ids = []
265
269
  for tag in dimtags_edge:
266
270
  edge_ids.extend(self.etag_to_edge[tag[1]])
267
271
  edge_ids = np.array(edge_ids)
268
272
  return edge_ids
273
+
274
+ def domain_edges(self, dimtags: list[tuple[int,int]]) -> np.ndarray:
275
+ """Returns a np.ndarray of all edge indices corresponding to a set of dimension tags.
276
+
277
+ Args:
278
+ dimtags (list[tuple[int,int]]): A list of dimtags.
279
+
280
+ Returns:
281
+ np.ndarray: The list of mesh edge element indices.
282
+ """
269
283
 
284
+ edge_ids = []
285
+ for dt in dimtags:
286
+ edge_ids.extend(self.dimtag_to_edges[dt])
287
+ edge_ids = np.array(edge_ids)
288
+ return edge_ids
289
+
270
290
  def get_face_tets(self, *taglist: list[int]) -> np.ndarray:
271
291
  ''' Return a list of a tetrahedrons that share a node with any of the nodes in the provided face.'''
272
292
  nodes: set = set()
@@ -487,6 +507,13 @@ class Mesh3D(Mesh):
487
507
  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
508
 
489
509
  self.defined = True
510
+
511
+ for dim in (0,1,2,3):
512
+ dts= gmsh.model.get_entities(dim)
513
+ for dt in dts:
514
+ self.dimtag_to_center[dt] = gmsh.model.occ.get_center_of_mass(*dt)
515
+ self.dimtag_to_edges[dt] = self._domain_edge(dt)
516
+
490
517
  logger.trace('Done analyzing mesh.')
491
518
 
492
519
 
@@ -16,7 +16,7 @@
16
16
  # <https://www.gnu.org/licenses/>.
17
17
 
18
18
  from .cs import Axis, _parse_axis, GCS, _parse_vector
19
- from .selection import SELECTOR_OBJ, Selection
19
+ from .selection import SELECTOR_OBJ, Selection, FaceSelection
20
20
  from .geo import GeoPrism, XYPolygon, Alignment, XYPlate
21
21
  from .bc import BoundaryCondition
22
22
  from typing import Generator
@@ -50,8 +50,8 @@ def _pair_selection(f1: Selection,
50
50
  for t1, c1 in zip(f1.tags, c1s):
51
51
  for t2, c2 in zip(f2.tags, c2s):
52
52
  if np.linalg.norm((c1 + ds)-c2) < 1e-8:
53
- f1s.append(Selection([t1,]))
54
- f2s.append(Selection([t2,]))
53
+ f1s.append(FaceSelection([t1,]))
54
+ f2s.append(FaceSelection([t2,]))
55
55
  return f1s, f2s
56
56
 
57
57
 
@@ -69,7 +69,7 @@ class PeriodicCell:
69
69
 
70
70
  self.origins: list[tuple[float, float, float]] = [_parse_vector(origin) for origin in origins] # type: ignore
71
71
  self.vectors: list[Axis] = [_parse_axis(vec) for vec in vectors]
72
- self.excluded_faces: Selection | None = None
72
+ self.included_faces: Selection | None = None
73
73
  self._bcs: list[Periodic] = []
74
74
  self._ports: list[BoundaryCondition] = []
75
75
 
@@ -105,9 +105,11 @@ class PeriodicCell:
105
105
  for f1, f2, a in self.cell_data():
106
106
  f1_new = f1
107
107
  f2_new = f2
108
- if self.excluded_faces is not None:
109
- f1_new = f1 - self.excluded_faces # type: ignore
110
- f2_new = f2 - self.excluded_faces # type: ignore
108
+ if self.included_faces is not None:
109
+ f1_new = f1 & self.included_faces # type: ignore
110
+ f2_new = f2 & self.included_faces # type: ignore
111
+ if len(f1_new.tags)==0:
112
+ continue
111
113
  bcs.append(Periodic(f1_new, f2_new, tuple(a)))
112
114
  self._bcs = bcs
113
115
  return bcs
@@ -191,16 +193,16 @@ class RectCell(PeriodicCell):
191
193
  return XYPlate(self.width, self.height, position=(0,0,z), alignment=Alignment.CENTER)
192
194
 
193
195
  def cell_data(self):
194
- f1s = SELECTOR_OBJ.inplane(*self.fleft[0], *self.fleft[1])
195
- f2s = SELECTOR_OBJ.inplane(*self.fright[0], *self.fright[1])
196
+ f1s = SELECTOR_OBJ.inplane(*self.fleft[0], self.fleft[1])
197
+ f2s = SELECTOR_OBJ.inplane(*self.fright[0], self.fright[1])
196
198
  vec = (self.fright[0][0]-self.fleft[0][0],
197
199
  self.fright[0][1]-self.fleft[0][1],
198
200
  self.fright[0][2]-self.fleft[0][2])
199
201
  for f1, f2 in zip(*_pair_selection(f1s, f2s, vec)):
200
202
  yield f1, f2, vec
201
203
 
202
- f1s = SELECTOR_OBJ.inplane(*self.fbot[0], *self.fbot[1])
203
- f2s = SELECTOR_OBJ.inplane(*self.ftop[0], *self.ftop[1])
204
+ f1s = SELECTOR_OBJ.inplane(*self.fbot[0], self.fbot[1])
205
+ f2s = SELECTOR_OBJ.inplane(*self.ftop[0], self.ftop[1])
204
206
  vec = (self.ftop[0][0]-self.fbot[0][0],
205
207
  self.ftop[0][1]-self.fbot[0][1],
206
208
  self.ftop[0][2]-self.fbot[0][2])
@@ -273,9 +275,9 @@ class HexCell(PeriodicCell):
273
275
  o = self.o1[:-1]
274
276
  n = self.f11[1][:-1]
275
277
  w = nrm(self.p2-self.p1)/2
276
- f1s = SELECTOR_OBJ.inplane(*self.f11[0], *self.f11[1])\
278
+ f1s = SELECTOR_OBJ.inplane(*self.f11[0], self.f11[1])\
277
279
  .exclude(lambda x, y, z: (nrm(np.array([x,y])-o)>w) or (abs((np.array([x,y])-o) @ n ) > 1e-6))
278
- f2s = SELECTOR_OBJ.inplane(*self.f12[0], *self.f12[1])\
280
+ f2s = SELECTOR_OBJ.inplane(*self.f12[0], self.f12[1])\
279
281
  .exclude(lambda x, y, z: (nrm(np.array([x,y])+o)>w) or (abs((np.array([x,y])+o) @ n ) > 1e-6))
280
282
  vec = - (self.p1 + self.p2)
281
283
 
@@ -285,9 +287,9 @@ class HexCell(PeriodicCell):
285
287
  o = self.o2[:-1]
286
288
  n = self.f21[1][:-1]
287
289
  w = nrm(self.p3-self.p2)/2
288
- f1s = SELECTOR_OBJ.inplane(*self.f21[0], *self.f21[1])\
290
+ f1s = SELECTOR_OBJ.inplane(*self.f21[0], self.f21[1])\
289
291
  .exclude(lambda x, y, z: (nrm(np.array([x,y])-o)>w) or (abs((np.array([x,y])-o) @ n ) > 1e-6))
290
- f2s = SELECTOR_OBJ.inplane(*self.f22[0], *self.f22[1])\
292
+ f2s = SELECTOR_OBJ.inplane(*self.f22[0], self.f22[1])\
291
293
  .exclude(lambda x, y, z: (nrm(np.array([x,y])+o)>w) or (abs((np.array([x,y])+o) @ n ) > 1e-6))
292
294
  vec = - (self.p2 + self.p3)
293
295
  for f1, f2 in zip(*_pair_selection(f1s, f2s, vec)): # type: ignore
@@ -296,9 +298,9 @@ class HexCell(PeriodicCell):
296
298
  o = self.o3[:-1]
297
299
  n = self.f31[1][:-1]
298
300
  w = nrm(-self.p1-self.p3)/2
299
- f1s = SELECTOR_OBJ.inplane(*self.f31[0], *self.f31[1])\
301
+ f1s = SELECTOR_OBJ.inplane(*self.f31[0], self.f31[1])\
300
302
  .exclude(lambda x, y, z: (nrm(np.array([x,y])-o)>w) or (abs((np.array([x,y])-o) @ n ) > 1e-6))
301
- f2s = SELECTOR_OBJ.inplane(*self.f32[0], *self.f32[1])\
303
+ f2s = SELECTOR_OBJ.inplane(*self.f32[0], self.f32[1])\
302
304
  .exclude(lambda x, y, z: (nrm(np.array([x,y])+o)>w) or (abs((np.array([x,y])+o) @ n ) > 1e-6))
303
305
  vec = - (self.p3 - self.p1)
304
306
  for f1, f2 in zip(*_pair_selection(f1s, f2s, vec)): # type: ignore
@@ -1002,6 +1002,7 @@ class SurfaceImpedance(RobinBC):
1002
1002
  material: Material | None = None,
1003
1003
  surface_conductance: float | None = None,
1004
1004
  surface_roughness: float = 0,
1005
+ thickness: float | None = None,
1005
1006
  sr_model: Literal['Hammerstad-Jensen'] = 'Hammerstad-Jensen',
1006
1007
  ):
1007
1008
  """Generates a SurfaceImpedance bounary condition.
@@ -1021,7 +1022,8 @@ class SurfaceImpedance(RobinBC):
1021
1022
  material (Material | None, optional): The matrial to assign. Defaults to None.
1022
1023
  surface_conductance (float | None, optional): The specific bulk conductivity to use. Defaults to None.
1023
1024
  surface_roughness (float, optional): The surface roughness. Defaults to 0.
1024
- sr_model (Literal[&#39;Hammerstad, optional): The surface roughness model. Defaults to 'Hammerstad-Jensen'.
1025
+ thickness (float | None, optional): The layer thickness. Defaults to None
1026
+ sr_model (Literal["Hammerstad-Jensen", optional): The surface roughness model. Defaults to 'Hammerstad-Jensen'.
1025
1027
  """
1026
1028
  super().__init__(face)
1027
1029
 
@@ -1029,7 +1031,11 @@ class SurfaceImpedance(RobinBC):
1029
1031
  self._mur: float | complex = 1.0
1030
1032
  self._epsr: float | complex = 1.0
1031
1033
  self.sigma: float = 0.0
1034
+ self.thickness: float | None = thickness
1032
1035
 
1036
+ if isinstance(face, GeoObject) and thickness is None:
1037
+ self.thickness = face._load('thickness')
1038
+
1033
1039
  if material is not None:
1034
1040
  self.sigma = material.cond.scalar(1e9)
1035
1041
  self._mur = material.ur
@@ -1067,10 +1073,17 @@ class SurfaceImpedance(RobinBC):
1067
1073
  sigma = self.sigma
1068
1074
  mur = self._material.ur.scalar(f0)
1069
1075
  er = self._material.er.scalar(f0)
1070
-
1076
+ eps = EPS0*er
1077
+ mu = MU0*mur
1071
1078
  rho = 1/sigma
1072
- d_skin = (2*rho/(w0*MU0*mur) * ((1+(w0*EPS0*er*rho)**2)**0.5 + rho*w0*EPS0*er))**0.5
1073
- R = rho/d_skin
1079
+ d_skin = (2*rho/(w0*mu) * ((1+(w0*eps*rho)**2)**0.5 + rho*w0*eps))**0.5
1080
+ logger.debug(f'Computed skin depth δ={d_skin*1e6:.2}μm')
1081
+ R = (1+1j)*rho/d_skin
1082
+ if self.thickness is not None:
1083
+ eps_c = eps - 1j * sigma / w0
1084
+ gamma_m = 1j * w0 * np.sqrt(mu*eps_c)
1085
+ R = R / np.tanh(gamma_m * self.thickness)
1086
+ logger.debug(f'Impedance scaler due to thickness: {1/ np.tanh(gamma_m * self.thickness) :.4f}')
1074
1087
  if self._sr_model=='Hammerstad-Jensen' and self._sr > 0.0:
1075
1088
  R = R * (1 + 2/np.pi * np.arctan(1.4*(self._sr/d_skin)**2))
1076
1089
  return 1j*k0*Z0/R
@@ -1137,6 +1137,9 @@ class MWScalarNdim:
1137
1137
  self._portmap: dict[int, float|int] = dict()
1138
1138
  self._portnumbers: list[int | float] = []
1139
1139
 
1140
+ def dense_f(self, N: int) -> np.ndarray:
1141
+ return np.linspace(np.min(self.freq), np.max(self.freq), N)
1142
+
1140
1143
  def S(self, i1: int, i2: int) -> np.ndarray:
1141
1144
  return self.Sp[...,self._portmap[i1], self._portmap[i2]]
1142
1145
 
@@ -435,7 +435,7 @@ class PVDisplay(BaseDisplay):
435
435
  return None
436
436
 
437
437
  ## OBLIGATORY METHODS
438
- def add_object(self, obj: GeoObject | Selection, mesh: bool = False, volume_mesh: bool = True, *args, **kwargs):
438
+ def add_object(self, obj: GeoObject | Selection, mesh: bool = False, volume_mesh: bool = True, label: bool = False, *args, **kwargs):
439
439
 
440
440
  show_edges = False
441
441
  opacity = obj.opacity
@@ -486,6 +486,14 @@ class PVDisplay(BaseDisplay):
486
486
 
487
487
  self._plot.add_mesh(self._volume_edges(_select(obj)), color='#000000', line_width=2, show_edges=True)
488
488
 
489
+ if isinstance(obj, GeoObject) and label:
490
+ points = []
491
+ labels = []
492
+ for dt in obj.dimtags:
493
+ points.append(self._mesh.dimtag_to_center[dt])
494
+ labels.append(obj.name)
495
+ self._plot.add_point_labels(points, labels, shape_color='white')
496
+
489
497
  def add_objects(self, *objects, **kwargs) -> None:
490
498
  """Add a series of objects provided as a list of arguments
491
499
  """
@@ -317,6 +317,7 @@ and sparse frequency annotations (e.g., labeled by frequency).
317
317
  colors_list = _broadcast(colors, None, 'colors')
318
318
  lw_list = _broadcast(linewidth, None, 'linewidth')
319
319
  labels_list: Optional[List[Optional[str]]]
320
+
320
321
  if labels is None:
321
322
  labels_list = None
322
323
  else:
@@ -377,7 +378,9 @@ and sparse frequency annotations (e.g., labeled by frequency).
377
378
 
378
379
  # frequency labels (sparse)
379
380
  fi = fs_list[i]
380
- if fi[0] is not None and n_flabels > 0 and len(s) > 0 and len(fi) > 0:
381
+ if fi is None:
382
+ continue
383
+ if n_flabels > 0 and len(s) > 0 and len(fi) > 0:
381
384
  n = min(len(s), len(fi))
382
385
  step = max(1, int(round(n / n_flabels))) if n_flabels > 0 else n # avoid step=0
383
386
  idx = np.arange(0, n, step)
@@ -537,9 +537,8 @@ class Selector:
537
537
  x: float,
538
538
  y: float,
539
539
  z: float,
540
- nx: float,
541
- ny: float,
542
- nz: float,
540
+ normal_axis: Axis | tuple[float, float, float] | None = None,
541
+ plane: Plane | None = None,
543
542
  tolerance: float = 1e-8) -> FaceSelection:
544
543
  """Returns a FaceSelection for all faces that lie in a provided infinite plane
545
544
  specified by an origin plus a plane normal vector.
@@ -548,17 +547,20 @@ class Selector:
548
547
  x (float): The plane origin X-coordinate
549
548
  y (float): The plane origin Y-coordinate
550
549
  z (float): The plane origin Z-coordinate
551
- nx (float): The plane normal X-component
552
- ny (float): The plane normal Y-component
553
- nz (float): The plane normal Z-component
550
+ normal_axis (Axis, tuple): The plane normal vector
554
551
  tolerance (float, optional): An in plane tolerance (displacement and normal dot product). Defaults to 1e-6.
555
552
 
556
553
  Returns:
557
554
  FaceSelection: All faces that lie in the specified plane
558
555
  """
559
556
  orig = np.array([x,y,z])
560
- norm = np.array([nx,ny,nz])
561
- norm = norm/np.linalg.norm(norm)
557
+ if plane is not None:
558
+ norm = plane.normal.np
559
+ elif normal_axis is not None:
560
+ norm = _parse_vector(normal_axis)
561
+ norm = norm/np.linalg.norm(norm)
562
+ else:
563
+ raise RuntimeError('No plane or axis defined for selection.')
562
564
 
563
565
  dimtags = gmsh.model.getEntities(2)
564
566
  coords = [gmsh.model.occ.getCenterOfMass(*tag) for tag in dimtags]