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.
- emerge/__init__.py +2 -2
- emerge/_emerge/geo/__init__.py +1 -1
- emerge/_emerge/geo/pcb.py +145 -66
- emerge/_emerge/geo/pcb_tools/dxf.py +360 -0
- emerge/_emerge/geo/polybased.py +21 -15
- emerge/_emerge/geo/shapes.py +31 -16
- emerge/_emerge/geometry.py +120 -21
- emerge/_emerge/mesh3d.py +39 -12
- emerge/_emerge/periodic.py +19 -17
- emerge/_emerge/physics/microwave/microwave_bc.py +17 -4
- emerge/_emerge/physics/microwave/microwave_data.py +3 -0
- emerge/_emerge/plot/pyvista/display.py +9 -1
- emerge/_emerge/plot/simple_plots.py +4 -1
- emerge/_emerge/selection.py +10 -8
- emerge/_emerge/simmodel.py +67 -33
- emerge/_emerge/solver.py +9 -2
- emerge/beta/dxf.py +1 -0
- emerge/lib.py +3 -0
- emerge/materials/__init__.py +1 -0
- emerge/materials/isola.py +294 -0
- emerge/materials/rogers.py +58 -0
- {emerge-1.0.0.dist-info → emerge-1.0.1.dist-info}/METADATA +3 -1
- {emerge-1.0.0.dist-info → emerge-1.0.1.dist-info}/RECORD +26 -21
- {emerge-1.0.0.dist-info → emerge-1.0.1.dist-info}/WHEEL +0 -0
- {emerge-1.0.0.dist-info → emerge-1.0.1.dist-info}/entry_points.txt +0 -0
- {emerge-1.0.0.dist-info → emerge-1.0.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
+
|
emerge/_emerge/geo/polybased.py
CHANGED
|
@@ -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
|
-
|
|
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,])
|
emerge/_emerge/geo/shapes.py
CHANGED
|
@@ -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)
|