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
@@ -0,0 +1,361 @@
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
+
329
+ polies = extract_polygons_with_meta(filename)
330
+ prop = inspect_pcb_from_dxf(filename)
331
+
332
+ if prop['units']['name'] == 'unitless':
333
+ if unit is None:
334
+ raise RouteException(f'Cannot generate PCB because the unit is not found in the DXF file or provided in the import_dxf function.')
335
+ pcb_unit = unit
336
+ else:
337
+ pcb_unit = 0.001 * prop['units']['to_mm']
338
+
339
+ if prop['thickness'] == 0.0:
340
+ if thickness is None:
341
+ raise RouteException(f'Cannot generate PCB because no thickness is found int he DXF file and none is provided in the import_dxf function.')
342
+ pcb_thickness = thickness
343
+ else:
344
+ pcb_thickness = 0.001 * prop['thickness_mm'] / pcb_unit
345
+
346
+ if cs is None:
347
+ cs = GCS
348
+
349
+ zs = sorted(list(set([pol['z'] for pol in polies])))
350
+ pcb = PCB(pcb_thickness, pcb_unit, cs, material=material, trace_material=trace_material)
351
+
352
+ for poly in polies:
353
+ xs, ys = zip(*poly['ring'])
354
+ z = poly['z']
355
+ zs.append(z)
356
+ xs = [x for x in xs]
357
+ ys = [y for y in ys]
358
+
359
+ pcb.add_poly(xs, ys, z=z, name=poly['handle'])
360
+ return pcb
361
+
@@ -24,66 +24,7 @@ from typing import Generator, Callable
24
24
  from ..selection import FaceSelection
25
25
  from typing import Literal
26
26
  from functools import reduce
27
- from numba import njit
28
27
 
29
- @njit(cache=True)
30
- def _subsample_coordinates(xs: np.ndarray, ys: np.ndarray, tolerance: float, xmin: float) -> tuple[np.ndarray, np.ndarray]:
31
- """This function takes a set of x and y coordinates in a finely sampled set and returns a reduced
32
- set of numbers that traces the input curve within a provided tolerance.
33
-
34
- Args:
35
- xs (np.ndarray): The set of X-coordinates
36
- ys (np.ndarray): The set of Y-coordinates
37
- tolerance (float): The maximum deviation of the curve in meters
38
- xmin (float): The minimal distance to the next point.
39
-
40
- Returns:
41
- np.ndarray: The output X-coordinates
42
- np.ndarray: The output Y-coordinates
43
- """
44
- N = xs.shape[0]
45
- ids = np.zeros((N,), dtype=np.int32)
46
- store_index = 1
47
- start_index = 0
48
- final_index = 0
49
- for iteration in range(N):
50
- i1 = start_index
51
- done = 0
52
- for i2 in range(i1+1,N):
53
- x_true = xs[i1:i2+1]
54
- y_true = ys[i1:i2+1]
55
-
56
- x_f = np.linspace(xs[i1],xs[i2], i2-i1+1)
57
- y_f = np.linspace(ys[i1],ys[i2], i2-i1+1)
58
- error = np.max(np.sqrt((x_f-x_true)**2 + (y_f-y_true)**2))
59
- ds = np.sqrt((xs[i2]-xs[i1])**2 + (ys[i2]-ys[i1])**2)
60
- # If at the end
61
- if i2==N-1:
62
- ids[store_index] = i2-1
63
- final_index = store_index + 1
64
- done = 1
65
- break
66
- # If not yet past the minimum distance, accumulate more
67
- if ds < xmin:
68
- continue
69
- # If the end is less than a minimum distance
70
- if np.sqrt((ys[-1]-ys[i2])**2 + (xs[-1]-xs[i2])**2) < xmin:
71
- imid = i1 + (N-1-i1)//2
72
- ids[store_index] = imid
73
- ids[store_index+1] = N-1
74
- final_index = store_index + 2
75
- done = 1
76
- break
77
- if error < tolerance:
78
- continue
79
- else:
80
- ids[store_index] = i2-1
81
- start_index = i2
82
- store_index = store_index + 1
83
- break
84
- if done==1:
85
- break
86
- return xs[ids[0:final_index]], ys[ids[0:final_index]]
87
28
 
88
29
  def _discretize_curve(xfunc: Callable, yfunc: Callable,
89
30
  t0: float, t1: float, xmin: float, tol: float=1e-4) -> tuple[np.ndarray, np.ndarray]:
@@ -100,6 +41,8 @@ def _discretize_curve(xfunc: Callable, yfunc: Callable,
100
41
  Returns:
101
42
  tuple[np.ndarray, np.ndarray]: _description_
102
43
  """
44
+ from ..mth.optimized import _subsample_coordinates
45
+
103
46
  td = np.linspace(t0, t1, 10001)
104
47
  xs = xfunc(td)
105
48
  ys = yfunc(td)
@@ -219,8 +162,9 @@ class GeoPrism(GeoVolume):
219
162
  volume_tag: int,
220
163
  front_tag: int | None = None,
221
164
  side_tags: list[int] | None = None,
222
- _axis: Axis | None = None):
223
- super().__init__(volume_tag)
165
+ _axis: Axis | None = None,
166
+ name: str | None = None):
167
+ super().__init__(volume_tag, name=name)
224
168
 
225
169
 
226
170
 
@@ -420,7 +364,7 @@ class XYPolygon:
420
364
  wiretag = gmsh.model.occ.add_wire(lines)
421
365
  return ptags, lines, wiretag
422
366
 
423
- def _finalize(self, cs: CoordinateSystem) -> GeoPolygon:
367
+ def _finalize(self, cs: CoordinateSystem, name: str | None = 'GeoPolygon') -> GeoPolygon:
424
368
  """Turns the XYPolygon object into a GeoPolygon that is embedded in 3D space.
425
369
 
426
370
  The polygon will be placed in the XY-plane of the provided coordinate center.
@@ -433,12 +377,12 @@ class XYPolygon:
433
377
  """
434
378
  ptags, lines, wiretag = self._make_wire(cs)
435
379
  surftag = gmsh.model.occ.add_plane_surface([wiretag,])
436
- poly = GeoPolygon([surftag,])
380
+ poly = GeoPolygon([surftag,], name=name)
437
381
  poly.points = ptags
438
382
  poly.lines = lines
439
383
  return poly
440
384
 
441
- def extrude(self, length: float, cs: CoordinateSystem | None = None) -> GeoPrism:
385
+ def extrude(self, length: float, cs: CoordinateSystem | None = None, name: str = 'Extrusion') -> GeoPrism:
442
386
  """Extrues the polygon along the Z-axis.
443
387
  The z-coordinates go from z1 to z2 (in meters). Then the extrusion
444
388
  is either provided by a maximum dz distance (in meters) or a number
@@ -458,9 +402,9 @@ class XYPolygon:
458
402
  volume = gmsh.model.occ.extrude(poly_fin.dimtags, zax[0], zax[1], zax[2])
459
403
  tags = [t for d,t in volume if d==3]
460
404
  surftags = [t for d,t in volume if d==2]
461
- return GeoPrism(tags, surftags[0], surftags)
405
+ return GeoPrism(tags, surftags[0], surftags, name=name)
462
406
 
463
- def geo(self, cs: CoordinateSystem | None = None) -> GeoPolygon:
407
+ def geo(self, cs: CoordinateSystem | None = None, name: str = 'GeoPolygon') -> GeoPolygon:
464
408
  """Returns a GeoPolygon object for the current polygon.
465
409
 
466
410
  Args:
@@ -475,7 +419,7 @@ class XYPolygon:
475
419
  cs = GCS
476
420
  return self._finalize(cs)
477
421
 
478
- def revolve(self, cs: CoordinateSystem, origin: tuple[float, float, float], axis: tuple[float, float,float], angle: float = 360.0) -> GeoPrism:
422
+ def revolve(self, cs: CoordinateSystem, origin: tuple[float, float, float], axis: tuple[float, float,float], angle: float = 360.0, name: str = 'Revolution') -> GeoPrism:
479
423
  """Applies a revolution to the XYPolygon along the provided rotation ais
480
424
 
481
425
  Args:
@@ -496,7 +440,7 @@ class XYPolygon:
496
440
 
497
441
  tags = [t for d,t in volume if d==3]
498
442
  poly_fin.remove()
499
- return GeoPrism(tags, _axis=axis)
443
+ return GeoPrism(tags, _axis=axis, name=name)
500
444
 
501
445
  @staticmethod
502
446
  def circle(radius: float,
@@ -588,7 +532,7 @@ class XYPolygon:
588
532
  self.extend(xs, ys)
589
533
  return self
590
534
 
591
- def connect(self, other: XYPolygon) -> GeoVolume:
535
+ def connect(self, other: XYPolygon, name: str = 'Connection') -> GeoVolume:
592
536
  """Connect two XYPolygons with a defined coordinate system
593
537
 
594
538
  The coordinate system must be defined before this function can be used. To add a coordinate systme without
@@ -610,17 +554,19 @@ class XYPolygon:
610
554
  o1 = np.array(self._cs.in_global_cs(*self.center, 0)).flatten()
611
555
  o2 = np.array(other._cs.in_global_cs(*other.center, 0)).flatten()
612
556
  dts = gmsh.model.occ.addThruSections([w1, w2], True, parametrization="IsoParametric")
613
- vol = GeoVolume([t for d,t in dts if d==3])
557
+ vol = GeoVolume([t for d,t in dts if d==3], name=name)
614
558
 
615
559
  vol._add_face_pointer('front',o1, self._cs.zax.np)
616
560
  vol._add_face_pointer('back', o2, other._cs.zax.np)
617
561
  return vol
618
562
 
619
563
  class Disc(GeoSurface):
564
+ _default_name: str = 'Disc'
620
565
 
621
566
  def __init__(self, origin: tuple[float, float, float],
622
567
  radius: float,
623
- axis: tuple[float, float, float] = (0,0,1.0)):
568
+ axis: tuple[float, float, float] = (0,0,1.0),
569
+ name: str | None = None):
624
570
  """Creates a circular Disc surface.
625
571
 
626
572
  Args:
@@ -629,10 +575,12 @@ class Disc(GeoSurface):
629
575
  axis (tuple[float, float, float], optional): The disc normal axis. Defaults to (0,0,1.0).
630
576
  """
631
577
  disc = gmsh.model.occ.addDisk(*origin, radius, radius, zAxis=axis)
632
- super().__init__(disc)
578
+ super().__init__(disc, name=name)
633
579
 
634
580
 
635
581
  class Curve(GeoEdge):
582
+ _default_name: str = 'Curve'
583
+
636
584
  def __init__(self,
637
585
  xpts: np.ndarray,
638
586
  ypts: np.ndarray,
@@ -640,7 +588,8 @@ class Curve(GeoEdge):
640
588
  degree: int = 3,
641
589
  weights: list[float] | None = None,
642
590
  knots: list[float] | None = None,
643
- ctype: Literal['Spline','BSpline','Bezier'] = 'Spline'):
591
+ ctype: Literal['Spline','BSpline','Bezier'] = 'Spline',
592
+ name: str | None = None):
644
593
  """Generate a Spline/Bspline or Bezier curve based on a series of points
645
594
 
646
595
  This calls the different curve features in OpenCASCADE.
@@ -678,7 +627,7 @@ class Curve(GeoEdge):
678
627
 
679
628
  tags = gmsh.model.occ.addWire([tags,])
680
629
  gmsh.model.occ.remove([(0,tag) for tag in points])
681
- super().__init__(tags)
630
+ super().__init__(tags, name=name)
682
631
 
683
632
  gmsh.model.occ.synchronize()
684
633
  p1 = gmsh.model.getValue(self.dim, self.tags[0], [0,])