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.

@@ -0,0 +1,360 @@
1
+
2
+ import re
3
+ from typing import Any
4
+ from ..pcb import PCB, RouteException
5
+ from ...material import Material, PEC
6
+ from ...cs import GCS, CoordinateSystem
7
+ from loguru import logger
8
+ from math import hypot
9
+
10
+ try:
11
+ import ezdxf
12
+ from ezdxf.recover import readfile as recover_readfile
13
+ from ezdxf.path import make_path, from_hatch
14
+ except ImportError as e:
15
+ logger.error('Cannot find the required ezdxf library. Install using: pip install ezdxf')
16
+ raise e
17
+
18
+ INSUNITS_TO_NAME = {
19
+ 0: "unitless",
20
+ 1: "inch",
21
+ 2: "foot",
22
+ 3: "mile",
23
+ 4: "millimeter",
24
+ 5: "centimeter",
25
+ 6: "meter",
26
+ 7: "kilometer",
27
+ 8: "microinch",
28
+ 9: "mil", # thousandth of an inch
29
+ 10: "yard",
30
+ 11: "angstrom",
31
+ 12: "nanometer",
32
+ 13: "micrometer",
33
+ 14: "decimeter",
34
+ 15: "decameter",
35
+ 16: "hectometer",
36
+ 17: "gigameter",
37
+ 18: "astronomical unit",
38
+ 19: "light year",
39
+ 20: "parsec",
40
+ }
41
+
42
+ # scale factor: drawing units -> millimeters
43
+ INSUNITS_TO_MM = {
44
+ 0: 1.0, # unitless; treat as mm by convention
45
+ 1: 25.4,
46
+ 2: 304.8,
47
+ 3: 1609344.0,
48
+ 4: 1.0,
49
+ 5: 10.0,
50
+ 6: 1000.0,
51
+ 7: 1_000_000.0,
52
+ 8: 25.4e-6,
53
+ 9: 0.0254,
54
+ 10: 914.4,
55
+ 11: 1e-7,
56
+ 12: 1e-6,
57
+ 13: 1e-3,
58
+ 14: 100.0,
59
+ 15: 10_000.0,
60
+ 16: 100_000.0,
61
+ 17: 1e12,
62
+ 18: 1.495978707e14, # AU in mm
63
+ 19: 9.460730472e18, # ly in mm
64
+ 20: 3.085677581e19, # pc in mm
65
+ }
66
+
67
+ def cluster_values(values, tol):
68
+ """Return [(center, count), ...] clustering sorted values by tolerance."""
69
+ if not values:
70
+ return []
71
+ values = sorted(values)
72
+ clusters = [[values[0]]]
73
+ for v in values[1:]:
74
+ if abs(v - clusters[-1][-1]) <= tol:
75
+ clusters[-1].append(v)
76
+ else:
77
+ clusters.append([v])
78
+ out = []
79
+ for c in clusters:
80
+ out.append((sum(c)/len(c), len(c)))
81
+ return out
82
+
83
+ STACKUP_TOKENS = [
84
+ r"fr-?4", r"rogers ?\d+", r"core", r"prepreg", r"dielectric",
85
+ r"cu(?:\W|$)|copper", r"\b1\.?6\s*mm\b", r"\b0\.?8\s*mm\b",
86
+ r"\b[12]\s*oz\b", r"\b35\s*µ?m\b", r"\b70\s*µ?m\b",
87
+ r"stack[- ]?up", r"layer\s*\d+", r"pcb", r"thickness",
88
+ ]
89
+
90
+ def inspect_pcb_from_dxf(
91
+ filename: str,
92
+ flatten_tol: float = 0.25, # curve flattening tolerance (drawing units)
93
+ z_tol: float | None = None, # cluster tolerance for Z (drawing units); default auto
94
+ layer_filter: str | None = None,
95
+ ):
96
+ """
97
+ Returns a dict with:
98
+ {
99
+ 'units': {'code': int, 'name': str, 'to_mm': float},
100
+ 'z_levels': [{'z': float, 'count': int}], # in drawing units
101
+ 'thickness_units': 'same as units',
102
+ 'thickness': float, # in drawing units
103
+ 'thickness_mm': float, # convenience
104
+ 'notes': {'materials': [...], 'raw_text_hits': [...]}
105
+ }
106
+ """
107
+ doc, auditor = recover_readfile(filename)
108
+ msp = doc.modelspace()
109
+
110
+ # Units
111
+ code = int(doc.header.get("$INSUNITS", 0))
112
+ unit_name = INSUNITS_TO_NAME.get(code, f"unknown({code})")
113
+ to_mm = INSUNITS_TO_MM.get(code, 1.0)
114
+
115
+ # Gather geometry Zs (WCS), also consider entity.dxf.elevation as fallback
116
+ z_values = []
117
+
118
+ def on_layer(e) -> bool:
119
+ return layer_filter is None or e.dxf.layer == layer_filter
120
+
121
+ for e in msp:
122
+ if not on_layer(e):
123
+ continue
124
+ dxtype = e.dxftype()
125
+ paths = []
126
+ try:
127
+ if dxtype in ("HATCH", "MPOLYGON"):
128
+ paths = list(from_hatch(e))
129
+ else:
130
+ paths = [make_path(e)]
131
+ except TypeError:
132
+ # skip unsupported entity types
133
+ pass
134
+ except Exception:
135
+ pass
136
+
137
+ for p in paths:
138
+ subs = p.sub_paths() if getattr(p, "has_sub_paths", False) and p.has_sub_paths else [p]
139
+ for sp in subs:
140
+ try:
141
+ verts = list(sp.flattening(distance=flatten_tol))
142
+ except Exception:
143
+ continue
144
+ for v in verts:
145
+ # v may be Vec2 or Vec3; handle both
146
+ z = getattr(v, "z", None)
147
+ if z is None:
148
+ # fallback to entity elevation if available
149
+ z = float(getattr(e.dxf, "elevation", 0.0))
150
+ z_values.append(float(z))
151
+
152
+ # Entities with explicit THICKNESS (rare but possible)
153
+ thick = getattr(e.dxf, "thickness", None)
154
+ if thick not in (None, 0):
155
+ # If an entity is extruded, its "top" would be at elevation+thickness along normal.
156
+ # We can't reliably map OCS normal here; just record the magnitude as a hint.
157
+ pass
158
+
159
+ # Cluster Zs
160
+ auto_tol = (1e-6 if to_mm >= 1.0 else 1e-6 / to_mm) # ~1 nm in mm space → tiny in most units
161
+ ztol = z_tol if z_tol is not None else auto_tol
162
+ z_clusters = cluster_values(z_values, tol=ztol)
163
+ z_levels = [{"z": z, "count": n} for z, n in z_clusters]
164
+
165
+ # Thickness from geometry (only meaningful if multiple distinct Zs)
166
+ if z_values:
167
+ zmin, zmax = min(z_values), max(z_values)
168
+ thickness = zmax - zmin
169
+ else:
170
+ zmin = zmax = thickness = 0.0
171
+
172
+ # Parse material/thickness hints from TEXT/MTEXT
173
+ material_hits = set()
174
+ raw_text_hits = []
175
+ token_re = re.compile("|".join(STACKUP_TOKENS), re.IGNORECASE)
176
+
177
+ for e in msp.query("TEXT MTEXT"):
178
+ try:
179
+ text = e.dxf.text if e.dxftype() == "TEXT" else e.text
180
+ except Exception:
181
+ continue
182
+ if not text:
183
+ continue
184
+ if token_re.search(text):
185
+ raw_text_hits.append(text.strip())
186
+ # simple keyword extraction
187
+ for kw in ["FR4", "Rogers", "core", "prepreg", "copper", "stackup", "thickness", "oz", "µm", "um", "mm"]:
188
+ if re.search(kw, text, re.IGNORECASE):
189
+ material_hits.add(kw.upper())
190
+
191
+ return {
192
+ "units": {"code": code, "name": unit_name, "to_mm": to_mm},
193
+ "z_levels": z_levels,
194
+ "thickness_units": unit_name,
195
+ "thickness": thickness,
196
+ "thickness_mm": thickness * to_mm,
197
+ "notes": {
198
+ "materials": sorted(material_hits),
199
+ "raw_text_hits": raw_text_hits[:50], # cap to keep output sane
200
+ "comment": (
201
+ "Single Z level detected; geometry alone cannot determine PCB thickness."
202
+ if len(z_levels) <= 1 else
203
+ "Thickness estimated from min/max Z across all geometry."
204
+ )
205
+ },
206
+ }
207
+
208
+ def path_items_with_semantics(e, p, distance=0.25):
209
+ """Yield dicts describing each sub-path with geometry + semantics we can infer."""
210
+ subs = p.sub_paths() if getattr(p, "has_sub_paths", False) and p.has_sub_paths else [p]
211
+ for sp in subs:
212
+ verts = list(sp.flattening(distance=distance))
213
+ if len(verts) < 2:
214
+ continue
215
+
216
+ # geometry in XY (keep Z if you like)
217
+ pts = [(v.x, v.y) for v in verts]
218
+ is_closed = pts[0] == pts[-1]
219
+
220
+ item = {
221
+ "layer": e.dxf.layer,
222
+ "entity": e.dxftype(),
223
+ "handle": e.dxf.handle,
224
+ "is_closed": is_closed,
225
+ "points": pts if is_closed else None, # polygon ring for closed shapes
226
+ "start": None,
227
+ "end": None,
228
+ "length": None, # centerline length for open paths
229
+ "width": None, # LWPOLYLINE width if available
230
+ }
231
+
232
+ if not is_closed:
233
+ item["start"] = pts[0]
234
+ item["end"] = pts[-1]
235
+ # polyline centerline length
236
+ item["length"] = sum(
237
+ hypot(x2 - x1, y2 - y1) for (x1, y1), (x2, y2) in zip(pts, pts[1:])
238
+ )
239
+
240
+ # Preserve potential trace width info from LWPOLYLINE
241
+ if e.dxftype() == "LWPOLYLINE":
242
+ cw = getattr(e.dxf, "const_width", None)
243
+ if cw not in (None, 0):
244
+ item["width"] = float(cw)
245
+ else:
246
+ # per-vertex widths (rare but possible)
247
+ try:
248
+ item["widths"] = [(v[0], v[1]) for v in zip(e.get_start_widths(), e.get_end_widths())]
249
+ except Exception:
250
+ pass
251
+
252
+ yield item
253
+
254
+ def extract_polygons_with_meta(
255
+ filename: str,
256
+ layer: str | None = None,
257
+ distance: float = 0.25,
258
+ skip_open: bool = True,
259
+ ) -> dict[str, Any]:
260
+ """
261
+ Returns a list of dicts:
262
+ {
263
+ 'ring': [(x, y), ...], # closed ring
264
+ 'layer': 'LayerName',
265
+ 'z': 12.34, # representative height (median Z of ring)
266
+ 'entity': 'LWPOLYLINE' | ...,
267
+ 'handle': 'ABCD'
268
+ }
269
+ """
270
+ doc, auditor = recover_readfile(filename)
271
+ msp = doc.modelspace()
272
+
273
+ def on_layer(e) -> bool:
274
+ return layer is None or e.dxf.layer == layer
275
+
276
+ items = []
277
+
278
+ for e in msp:
279
+ if not on_layer(e):
280
+ continue
281
+
282
+ dxtype = e.dxftype()
283
+ paths = []
284
+ try:
285
+ if dxtype in ("HATCH", "MPOLYGON"):
286
+ paths = list(from_hatch(e))
287
+ else:
288
+ paths = [make_path(e)]
289
+ except TypeError:
290
+ continue
291
+ except Exception:
292
+ continue
293
+
294
+ for p in paths:
295
+ subs = p.sub_paths() if p.has_sub_paths else [p]
296
+ for sp in subs:
297
+
298
+ verts = list(sp.flattening(distance=distance)) # Vec3 in WCS
299
+ if len(verts) < 2:
300
+ continue
301
+
302
+ # ensure not closed
303
+ if (verts[0].x, verts[0].y) == (verts[-1].x, verts[-1].y):
304
+ verts = verts[:-1]
305
+
306
+ # ring in XY (your original target), but also compute a Z for reference
307
+ ring_xy = [(v.x, v.y) for v in verts]
308
+ zs = sorted(v.z for v in verts)
309
+ z = zs[len(zs)//2] if zs else float(getattr(e.dxf, "elevation", 0.0)) # median Z
310
+
311
+ items.append({
312
+ "ring": ring_xy,
313
+ "layer": e.dxf.layer,
314
+ "z": float(z),
315
+ "entity": dxtype,
316
+ "handle": e.dxf.handle,
317
+ })
318
+
319
+ return items
320
+
321
+
322
+ def import_dxf(filename: str,
323
+ material: Material,
324
+ thickness: float | None = None,
325
+ unit: float | None = None,
326
+ cs: CoordinateSystem | None = GCS,
327
+ trace_material: Material = PEC) -> PCB:
328
+ polies = extract_polygons_with_meta(filename)
329
+ prop = inspect_pcb_from_dxf('LP5G_Chb3.dxf')
330
+
331
+ if prop['units']['name'] == 'unitless':
332
+ if unit is None:
333
+ raise RouteException(f'Cannot generate PCB because the unit is not found in the DXF file or provided in the import_dxf function.')
334
+ pcb_unit = unit
335
+ else:
336
+ pcb_unit = 0.001 * prop['units']['to_mm']
337
+
338
+ if prop['thickness']==0.0:
339
+ if thickness is None:
340
+ raise RouteException(f'Cannot generate PCB because no thickness is found int he DXF file and none is provided in the import_dxf function.')
341
+ pcb_thickness = thickness
342
+ else:
343
+ pcb_thickness = 0.001 * prop['thickness_mm'] / pcb_unit
344
+
345
+ if cs is None:
346
+ cs = GCS
347
+
348
+ zs = sorted(list(set([pol['z'] for pol in polies])))
349
+ pcb = PCB(pcb_thickness, pcb_unit, cs, material=material, trace_material=trace_material, _zs=zs)
350
+
351
+ for poly in polies:
352
+ xs, ys = zip(*poly['ring'])
353
+ z = poly['z']
354
+ zs.append(z)
355
+ xs = [x for x in xs]
356
+ ys = [y for y in ys]
357
+
358
+ pcb.add_poly(xs, ys, z=z, name=poly['handle'])
359
+ return pcb
360
+
@@ -219,8 +219,9 @@ class GeoPrism(GeoVolume):
219
219
  volume_tag: int,
220
220
  front_tag: int | None = None,
221
221
  side_tags: list[int] | None = None,
222
- _axis: Axis | None = None):
223
- super().__init__(volume_tag)
222
+ _axis: Axis | None = None,
223
+ name: str | None = None):
224
+ super().__init__(volume_tag, name=name)
224
225
 
225
226
 
226
227
 
@@ -420,7 +421,7 @@ class XYPolygon:
420
421
  wiretag = gmsh.model.occ.add_wire(lines)
421
422
  return ptags, lines, wiretag
422
423
 
423
- def _finalize(self, cs: CoordinateSystem) -> GeoPolygon:
424
+ def _finalize(self, cs: CoordinateSystem, name: str | None = 'GeoPolygon') -> GeoPolygon:
424
425
  """Turns the XYPolygon object into a GeoPolygon that is embedded in 3D space.
425
426
 
426
427
  The polygon will be placed in the XY-plane of the provided coordinate center.
@@ -433,12 +434,12 @@ class XYPolygon:
433
434
  """
434
435
  ptags, lines, wiretag = self._make_wire(cs)
435
436
  surftag = gmsh.model.occ.add_plane_surface([wiretag,])
436
- poly = GeoPolygon([surftag,])
437
+ poly = GeoPolygon([surftag,], name=name)
437
438
  poly.points = ptags
438
439
  poly.lines = lines
439
440
  return poly
440
441
 
441
- def extrude(self, length: float, cs: CoordinateSystem | None = None) -> GeoPrism:
442
+ def extrude(self, length: float, cs: CoordinateSystem | None = None, name: str = 'Extrusion') -> GeoPrism:
442
443
  """Extrues the polygon along the Z-axis.
443
444
  The z-coordinates go from z1 to z2 (in meters). Then the extrusion
444
445
  is either provided by a maximum dz distance (in meters) or a number
@@ -458,9 +459,9 @@ class XYPolygon:
458
459
  volume = gmsh.model.occ.extrude(poly_fin.dimtags, zax[0], zax[1], zax[2])
459
460
  tags = [t for d,t in volume if d==3]
460
461
  surftags = [t for d,t in volume if d==2]
461
- return GeoPrism(tags, surftags[0], surftags)
462
+ return GeoPrism(tags, surftags[0], surftags, name=name)
462
463
 
463
- def geo(self, cs: CoordinateSystem | None = None) -> GeoPolygon:
464
+ def geo(self, cs: CoordinateSystem | None = None, name: str = 'GeoPolygon') -> GeoPolygon:
464
465
  """Returns a GeoPolygon object for the current polygon.
465
466
 
466
467
  Args:
@@ -475,7 +476,7 @@ class XYPolygon:
475
476
  cs = GCS
476
477
  return self._finalize(cs)
477
478
 
478
- def revolve(self, cs: CoordinateSystem, origin: tuple[float, float, float], axis: tuple[float, float,float], angle: float = 360.0) -> GeoPrism:
479
+ def revolve(self, cs: CoordinateSystem, origin: tuple[float, float, float], axis: tuple[float, float,float], angle: float = 360.0, name: str = 'Revolution') -> GeoPrism:
479
480
  """Applies a revolution to the XYPolygon along the provided rotation ais
480
481
 
481
482
  Args:
@@ -496,7 +497,7 @@ class XYPolygon:
496
497
 
497
498
  tags = [t for d,t in volume if d==3]
498
499
  poly_fin.remove()
499
- return GeoPrism(tags, _axis=axis)
500
+ return GeoPrism(tags, _axis=axis, name=name)
500
501
 
501
502
  @staticmethod
502
503
  def circle(radius: float,
@@ -588,7 +589,7 @@ class XYPolygon:
588
589
  self.extend(xs, ys)
589
590
  return self
590
591
 
591
- def connect(self, other: XYPolygon) -> GeoVolume:
592
+ def connect(self, other: XYPolygon, name: str = 'Connection') -> GeoVolume:
592
593
  """Connect two XYPolygons with a defined coordinate system
593
594
 
594
595
  The coordinate system must be defined before this function can be used. To add a coordinate systme without
@@ -610,17 +611,19 @@ class XYPolygon:
610
611
  o1 = np.array(self._cs.in_global_cs(*self.center, 0)).flatten()
611
612
  o2 = np.array(other._cs.in_global_cs(*other.center, 0)).flatten()
612
613
  dts = gmsh.model.occ.addThruSections([w1, w2], True, parametrization="IsoParametric")
613
- vol = GeoVolume([t for d,t in dts if d==3])
614
+ vol = GeoVolume([t for d,t in dts if d==3], name=name)
614
615
 
615
616
  vol._add_face_pointer('front',o1, self._cs.zax.np)
616
617
  vol._add_face_pointer('back', o2, other._cs.zax.np)
617
618
  return vol
618
619
 
619
620
  class Disc(GeoSurface):
621
+ _default_name: str = 'Disc'
620
622
 
621
623
  def __init__(self, origin: tuple[float, float, float],
622
624
  radius: float,
623
- axis: tuple[float, float, float] = (0,0,1.0)):
625
+ axis: tuple[float, float, float] = (0,0,1.0),
626
+ name: str | None = None):
624
627
  """Creates a circular Disc surface.
625
628
 
626
629
  Args:
@@ -629,10 +632,12 @@ class Disc(GeoSurface):
629
632
  axis (tuple[float, float, float], optional): The disc normal axis. Defaults to (0,0,1.0).
630
633
  """
631
634
  disc = gmsh.model.occ.addDisk(*origin, radius, radius, zAxis=axis)
632
- super().__init__(disc)
635
+ super().__init__(disc, name=name)
633
636
 
634
637
 
635
638
  class Curve(GeoEdge):
639
+ _default_name: str = 'Curve'
640
+
636
641
  def __init__(self,
637
642
  xpts: np.ndarray,
638
643
  ypts: np.ndarray,
@@ -640,7 +645,8 @@ class Curve(GeoEdge):
640
645
  degree: int = 3,
641
646
  weights: list[float] | None = None,
642
647
  knots: list[float] | None = None,
643
- ctype: Literal['Spline','BSpline','Bezier'] = 'Spline'):
648
+ ctype: Literal['Spline','BSpline','Bezier'] = 'Spline',
649
+ name: str | None = None):
644
650
  """Generate a Spline/Bspline or Bezier curve based on a series of points
645
651
 
646
652
  This calls the different curve features in OpenCASCADE.
@@ -678,7 +684,7 @@ class Curve(GeoEdge):
678
684
 
679
685
  tags = gmsh.model.occ.addWire([tags,])
680
686
  gmsh.model.occ.remove([(0,tag) for tag in points])
681
- super().__init__(tags)
687
+ super().__init__(tags, name=name)
682
688
 
683
689
  gmsh.model.occ.synchronize()
684
690
  p1 = gmsh.model.getValue(self.dim, self.tags[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)