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.
- emerge/__init__.py +7 -8
- emerge/_emerge/elements/femdata.py +4 -3
- emerge/_emerge/elements/nedelec2.py +8 -4
- emerge/_emerge/elements/nedleg2.py +6 -2
- emerge/_emerge/geo/__init__.py +1 -1
- emerge/_emerge/geo/pcb.py +149 -66
- emerge/_emerge/geo/pcb_tools/dxf.py +361 -0
- emerge/_emerge/geo/polybased.py +23 -74
- emerge/_emerge/geo/shapes.py +31 -16
- emerge/_emerge/geometry.py +120 -21
- emerge/_emerge/mesh3d.py +62 -43
- emerge/_emerge/{_cache_check.py → mth/_cache_check.py} +2 -2
- emerge/_emerge/mth/optimized.py +69 -3
- emerge/_emerge/periodic.py +19 -17
- emerge/_emerge/physics/microwave/__init__.py +0 -1
- emerge/_emerge/physics/microwave/assembly/assembler.py +27 -5
- emerge/_emerge/physics/microwave/assembly/generalized_eigen_hb.py +2 -3
- emerge/_emerge/physics/microwave/assembly/periodicbc.py +0 -1
- emerge/_emerge/physics/microwave/assembly/robin_abc_order2.py +375 -0
- emerge/_emerge/physics/microwave/assembly/robinbc.py +37 -38
- emerge/_emerge/physics/microwave/microwave_3d.py +11 -19
- emerge/_emerge/physics/microwave/microwave_bc.py +38 -21
- emerge/_emerge/physics/microwave/microwave_data.py +3 -26
- emerge/_emerge/physics/microwave/port_functions.py +4 -4
- emerge/_emerge/plot/pyvista/display.py +13 -2
- emerge/_emerge/plot/simple_plots.py +4 -1
- emerge/_emerge/selection.py +12 -9
- emerge/_emerge/simmodel.py +68 -34
- emerge/_emerge/solver.py +28 -16
- emerge/beta/dxf.py +1 -0
- emerge/lib.py +1 -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.2.dist-info}/METADATA +18 -4
- {emerge-1.0.0.dist-info → emerge-1.0.2.dist-info}/RECORD +39 -33
- {emerge-1.0.0.dist-info → emerge-1.0.2.dist-info}/WHEEL +0 -0
- {emerge-1.0.0.dist-info → emerge-1.0.2.dist-info}/entry_points.txt +0 -0
- {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
|
+
|
emerge/_emerge/geo/polybased.py
CHANGED
|
@@ -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
|
-
|
|
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,])
|