Skip to content

Commit 1f0982e

Browse files
committed
add tunable corner-angle splitting for bspline meshing
1 parent a31c930 commit 1f0982e

8 files changed

Lines changed: 287 additions & 41 deletions

File tree

nbs/palace_dc.ipynb

Lines changed: 27 additions & 34 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/gsim/palace/base.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ def _build_mesh_config(
345345
curve_fit_layers: list[str] | None = None,
346346
curve_fit_tolerance_um: float | None = None,
347347
curve_fit_min_points: int | None = None,
348+
curve_fit_corner_angle_deg: float | None = None,
348349
high_order_elements: bool | None = None,
349350
high_order_order: int | None = None,
350351
high_order_optimize: bool | None = None,
@@ -427,6 +428,9 @@ def _build_mesh_config(
427428
mesh_config.curve_fit_layers = list(existing_config.curve_fit_layers)
428429
mesh_config.curve_fit_tolerance_um = existing_config.curve_fit_tolerance_um
429430
mesh_config.curve_fit_min_points = existing_config.curve_fit_min_points
431+
mesh_config.curve_fit_corner_angle_deg = (
432+
existing_config.curve_fit_corner_angle_deg
433+
)
430434
mesh_config.high_order_elements = existing_config.high_order_elements
431435
mesh_config.high_order_order = existing_config.high_order_order
432436
mesh_config.high_order_optimize = existing_config.high_order_optimize
@@ -462,6 +466,8 @@ def _build_mesh_config(
462466
mesh_config.curve_fit_tolerance_um = curve_fit_tolerance_um
463467
if curve_fit_min_points is not None:
464468
mesh_config.curve_fit_min_points = curve_fit_min_points
469+
if curve_fit_corner_angle_deg is not None:
470+
mesh_config.curve_fit_corner_angle_deg = curve_fit_corner_angle_deg
465471
if high_order_elements is not None:
466472
mesh_config.high_order_elements = high_order_elements
467473
if high_order_order is not None:
@@ -930,6 +936,7 @@ def _generate_mesh_internal(
930936
curve_fit_layers=mesh_config.curve_fit_layers,
931937
curve_fit_tolerance_um=mesh_config.curve_fit_tolerance_um,
932938
curve_fit_min_points=mesh_config.curve_fit_min_points,
939+
curve_fit_corner_angle_deg=mesh_config.curve_fit_corner_angle_deg,
933940
high_order_elements=mesh_config.high_order_elements,
934941
high_order_order=mesh_config.high_order_order,
935942
high_order_optimize=mesh_config.high_order_optimize,
@@ -979,6 +986,7 @@ def preview(
979986
curve_fit_layers: list[str] | None = None,
980987
curve_fit_tolerance_um: float | None = None,
981988
curve_fit_min_points: int | None = None,
989+
curve_fit_corner_angle_deg: float | None = None,
982990
high_order_elements: bool | None = None,
983991
high_order_order: int | None = None,
984992
high_order_optimize: bool | None = None,
@@ -1006,6 +1014,8 @@ def preview(
10061014
curve_fit_layers: Layer names where spline fitting is applied.
10071015
curve_fit_tolerance_um: Point merge tolerance before fitting.
10081016
curve_fit_min_points: Min contour points required to fit curves.
1017+
curve_fit_corner_angle_deg: Turn-angle threshold used to split
1018+
sharp corners from smooth curve-fit segments.
10091019
high_order_elements: Enable high-order geometric mesh elements.
10101020
high_order_order: Polynomial order for high-order elements.
10111021
high_order_optimize: Run gmsh high-order optimization after meshing.
@@ -1040,6 +1050,7 @@ def preview(
10401050
curve_fit_layers=curve_fit_layers,
10411051
curve_fit_tolerance_um=curve_fit_tolerance_um,
10421052
curve_fit_min_points=curve_fit_min_points,
1053+
curve_fit_corner_angle_deg=curve_fit_corner_angle_deg,
10431054
high_order_elements=high_order_elements,
10441055
high_order_order=high_order_order,
10451056
high_order_optimize=high_order_optimize,
@@ -1076,6 +1087,7 @@ def preview(
10761087
curve_fit_layers=mesh_config.curve_fit_layers,
10771088
curve_fit_tolerance_um=mesh_config.curve_fit_tolerance_um,
10781089
curve_fit_min_points=mesh_config.curve_fit_min_points,
1090+
curve_fit_corner_angle_deg=mesh_config.curve_fit_corner_angle_deg,
10791091
high_order_elements=mesh_config.high_order_elements,
10801092
high_order_order=mesh_config.high_order_order,
10811093
high_order_optimize=mesh_config.high_order_optimize,
@@ -1106,6 +1118,7 @@ def mesh(
11061118
curve_fit_layers: list[str] | None = None,
11071119
curve_fit_tolerance_um: float | None = None,
11081120
curve_fit_min_points: int | None = None,
1121+
curve_fit_corner_angle_deg: float | None = None,
11091122
high_order_elements: bool | None = None,
11101123
high_order_order: int | None = None,
11111124
high_order_optimize: bool | None = None,
@@ -1139,6 +1152,8 @@ def mesh(
11391152
curve_fit_layers: Layer names where spline fitting is applied.
11401153
curve_fit_tolerance_um: Point merge tolerance before fitting.
11411154
curve_fit_min_points: Min contour points required to fit curves.
1155+
curve_fit_corner_angle_deg: Turn-angle threshold used to split
1156+
sharp corners from smooth curve-fit segments.
11421157
high_order_elements: Enable high-order geometric mesh elements.
11431158
high_order_order: Polynomial order for high-order elements.
11441159
high_order_optimize: Run gmsh high-order optimization after meshing.
@@ -1179,6 +1194,7 @@ def mesh(
11791194
curve_fit_layers=curve_fit_layers,
11801195
curve_fit_tolerance_um=curve_fit_tolerance_um,
11811196
curve_fit_min_points=curve_fit_min_points,
1197+
curve_fit_corner_angle_deg=curve_fit_corner_angle_deg,
11821198
high_order_elements=high_order_elements,
11831199
high_order_order=high_order_order,
11841200
high_order_optimize=high_order_optimize,

src/gsim/palace/mesh/generator.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import math
77
from dataclasses import dataclass, field
88
from pathlib import Path
9-
from typing import TYPE_CHECKING
9+
from typing import TYPE_CHECKING, Literal
1010

1111
import gmsh
1212

@@ -220,10 +220,11 @@ def generate_mesh(
220220
pec_blocks: list[PECBlockConfig] | None = None,
221221
absorbing_boundary: bool = True,
222222
merge_via_distance: float = 2.0,
223-
curve_fit_mode: str = "line",
223+
curve_fit_mode: Literal["line", "spline", "bspline"] = "line",
224224
curve_fit_layers: list[str] | None = None,
225225
curve_fit_tolerance_um: float = 0.0,
226226
curve_fit_min_points: int = 8,
227+
curve_fit_corner_angle_deg: float = 45.0,
227228
high_order_elements: bool = False,
228229
high_order_order: int = 2,
229230
high_order_optimize: bool = True,
@@ -256,6 +257,8 @@ def generate_mesh(
256257
curve_fit_layers: Layer names where curve fitting is applied
257258
curve_fit_tolerance_um: Point merge tolerance before curve fitting
258259
curve_fit_min_points: Min contour points required for curve fitting
260+
curve_fit_corner_angle_deg: Turn-angle threshold used to identify
261+
sharp corners during curve fitting segmentation
259262
high_order_elements: Enable high-order geometric mesh elements
260263
high_order_order: Polynomial order for high-order elements
261264
high_order_optimize: Run gmsh high-order optimization after meshing
@@ -324,6 +327,7 @@ def generate_mesh(
324327
curve_fit_layers=curve_fit_layers,
325328
curve_fit_tolerance_um=curve_fit_tolerance_um,
326329
curve_fit_min_points=curve_fit_min_points,
330+
curve_fit_corner_angle_deg=curve_fit_corner_angle_deg,
327331
)
328332

329333
all_dielectric_tags = {

src/gsim/palace/mesh/geometry.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,7 @@ def add_patterned_dielectrics(
542542
curve_fit_layers: list[str] | None = None,
543543
curve_fit_tolerance_um: float = 0.0,
544544
curve_fit_min_points: int = 8,
545+
curve_fit_corner_angle_deg: float = 45.0,
545546
) -> dict[str, list[int]]:
546547
"""Add patterned dielectric volumes from stack dielectric layers.
547548
@@ -559,6 +560,8 @@ def add_patterned_dielectrics(
559560
curve_fit_layers: Layer names where spline/bspline fitting is allowed.
560561
curve_fit_tolerance_um: Point merge tolerance before curve fitting.
561562
curve_fit_min_points: Minimum contour points to attempt curve fitting.
563+
curve_fit_corner_angle_deg: Turn-angle threshold for corner detection
564+
during spline/bspline segmentation.
562565
563566
Returns:
564567
Dict mapping dielectric layer name -> list of volume tags.
@@ -603,6 +606,7 @@ def add_patterned_dielectrics(
603606
loop_mode=surface_loop_mode,
604607
fit_tolerance_um=curve_fit_tolerance_um,
605608
min_points_for_curve_fit=curve_fit_min_points,
609+
corner_turn_threshold_deg=curve_fit_corner_angle_deg,
606610
)
607611
if surfacetag is not None:
608612
surfaces.append(surfacetag)

src/gsim/palace/mesh/gmsh_utils.py

Lines changed: 95 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44

55
import logging
66
import math
7+
from itertools import pairwise
78

89
import gmsh
910
import numpy as np
1011

1112
logger = logging.getLogger(__name__)
1213

14+
_CORNER_TURN_THRESHOLD_DEG = 45.0
15+
1316
# ---------------------------------------------------------------------------
1417
# Minimalistic meshwell-style entity + boolean pipeline
1518
# ---------------------------------------------------------------------------
@@ -304,6 +307,7 @@ def _create_wire_loop(
304307
meshseed: float = 0,
305308
loop_mode: str = "line",
306309
point_merge_tol: float = 0.0,
310+
corner_turn_threshold_deg: float = _CORNER_TURN_THRESHOLD_DEG,
307311
) -> int | None:
308312
"""Create a closed curve loop from polygon vertices.
309313
@@ -322,25 +326,105 @@ def _create_wire_loop(
322326
if math.hypot(x - px, y - py) > tol:
323327
cleaned.append((x, y))
324328

325-
if len(cleaned) >= 2:
326-
if math.hypot(cleaned[0][0] - cleaned[-1][0], cleaned[0][1] - cleaned[-1][1]) <= tol:
327-
cleaned.pop()
329+
if len(cleaned) >= 2 and (
330+
math.hypot(cleaned[0][0] - cleaned[-1][0], cleaned[0][1] - cleaned[-1][1])
331+
<= tol
332+
):
333+
cleaned.pop()
328334

329335
if len(cleaned) < 3:
330336
return None
331337

332338
verts = [kernel.addPoint(x, y, z, meshseed, -1) for x, y in cleaned]
333339

340+
turn_threshold_deg = max(0.0, min(float(corner_turn_threshold_deg), 180.0))
341+
342+
def _turn_angle_deg(
343+
prev_pt: tuple[float, float],
344+
curr_pt: tuple[float, float],
345+
next_pt: tuple[float, float],
346+
) -> float | None:
347+
ax = curr_pt[0] - prev_pt[0]
348+
ay = curr_pt[1] - prev_pt[1]
349+
bx = next_pt[0] - curr_pt[0]
350+
by = next_pt[1] - curr_pt[1]
351+
norm_a = math.hypot(ax, ay)
352+
norm_b = math.hypot(bx, by)
353+
if norm_a <= tol or norm_b <= tol:
354+
return None
355+
cos_theta = (ax * bx + ay * by) / (norm_a * norm_b)
356+
cos_theta = max(-1.0, min(1.0, cos_theta))
357+
return math.degrees(math.acos(cos_theta))
358+
359+
def _corner_indices(points: list[tuple[float, float]]) -> list[int]:
360+
if len(points) < 3:
361+
return []
362+
corners: list[int] = []
363+
for idx in range(len(points)):
364+
prev_pt = points[(idx - 1) % len(points)]
365+
curr_pt = points[idx]
366+
next_pt = points[(idx + 1) % len(points)]
367+
turn_angle = _turn_angle_deg(prev_pt, curr_pt, next_pt)
368+
if turn_angle is not None and turn_angle >= turn_threshold_deg:
369+
corners.append(idx)
370+
return corners
371+
372+
def _corner_to_corner_segments(
373+
num_points: int,
374+
corners: list[int],
375+
) -> list[list[int]]:
376+
unique = sorted(set(corners))
377+
if num_points < 3 or len(unique) < 2:
378+
return []
379+
380+
segments: list[list[int]] = []
381+
for idx, start in enumerate(unique):
382+
end = unique[(idx + 1) % len(unique)]
383+
segment = [start]
384+
cursor = (start + 1) % num_points
385+
while cursor != end:
386+
segment.append(cursor)
387+
cursor = (cursor + 1) % num_points
388+
segment.append(end)
389+
segments.append(segment)
390+
return segments
391+
334392
if loop_mode in {"spline", "bspline"} and len(verts) >= 4:
393+
corners = _corner_indices(cleaned)
394+
if len(corners) >= 2:
395+
try:
396+
segmented_curves: list[int] = []
397+
for segment in _corner_to_corner_segments(len(verts), corners):
398+
segment_pts = [verts[i] for i in segment]
399+
if len(segment_pts) >= 4:
400+
if loop_mode == "bspline":
401+
curve = kernel.addBSpline(segment_pts, -1)
402+
else:
403+
curve = kernel.addSpline(segment_pts, -1)
404+
segmented_curves.append(curve)
405+
else:
406+
for p1, p2 in pairwise(segment_pts):
407+
segmented_curves.append(kernel.addLine(p1, p2, -1))
408+
409+
if len(segmented_curves) >= 3:
410+
return kernel.addCurveLoop(segmented_curves, tag=-1)
411+
except Exception:
412+
logger.debug(
413+
"Failed segmented %s loop, falling back to full-curve loop",
414+
loop_mode,
415+
)
416+
335417
try:
336-
curve_pts = verts + [verts[0]]
418+
curve_pts = [*verts, verts[0]]
337419
if loop_mode == "bspline":
338420
curve = kernel.addBSpline(curve_pts, -1)
339421
else:
340422
curve = kernel.addSpline(curve_pts, -1)
341423
return kernel.addCurveLoop([curve], tag=-1)
342424
except Exception:
343-
logger.debug("Failed to create %s loop, falling back to line loop", loop_mode)
425+
logger.debug(
426+
"Failed to create %s loop, falling back to line loop", loop_mode
427+
)
344428

345429
lines = []
346430
for v in range(len(verts)):
@@ -364,6 +448,7 @@ def create_polygon_surface(
364448
loop_mode: str = "line",
365449
fit_tolerance_um: float = 0.0,
366450
min_points_for_curve_fit: int = 8,
451+
corner_turn_threshold_deg: float = _CORNER_TURN_THRESHOLD_DEG,
367452
) -> int | None:
368453
"""Create a planar surface from polygon vertices at z height.
369454
@@ -382,6 +467,8 @@ def create_polygon_surface(
382467
fit_tolerance_um: Point merge tolerance before loop creation
383468
min_points_for_curve_fit: Minimum contour points required to attempt
384469
spline/bspline loop creation
470+
corner_turn_threshold_deg: Turn-angle threshold used to detect corners
471+
when segmenting spline/bspline loops
385472
386473
Returns:
387474
Surface tag, or None if polygon is invalid
@@ -401,6 +488,7 @@ def create_polygon_surface(
401488
meshseed,
402489
loop_mode=effective_mode,
403490
point_merge_tol=fit_tolerance_um,
491+
corner_turn_threshold_deg=corner_turn_threshold_deg,
404492
)
405493
if exterior_loop is None and effective_mode != "line":
406494
exterior_loop = _create_wire_loop(
@@ -411,6 +499,7 @@ def create_polygon_surface(
411499
meshseed,
412500
loop_mode="line",
413501
point_merge_tol=fit_tolerance_um,
502+
corner_turn_threshold_deg=corner_turn_threshold_deg,
414503
)
415504
if exterior_loop is None:
416505
return None
@@ -432,6 +521,7 @@ def create_polygon_surface(
432521
meshseed,
433522
loop_mode="line",
434523
point_merge_tol=fit_tolerance_um,
524+
corner_turn_threshold_deg=corner_turn_threshold_deg,
435525
)
436526
if hloop is not None:
437527
hsurf = kernel.addPlaneSurface([hloop], tag=-1)

src/gsim/palace/models/mesh.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class MeshConfig(BaseModel):
5353
curve_fit_layers: list[str] = Field(default_factory=lambda: ["core", "core2"])
5454
curve_fit_tolerance_um: float = Field(default=0.0, ge=0)
5555
curve_fit_min_points: int = Field(default=8, ge=3)
56+
curve_fit_corner_angle_deg: float = Field(default=45.0, gt=0, lt=180)
5657
high_order_elements: bool = False
5758
high_order_order: int = Field(default=2, ge=2, le=6)
5859
high_order_optimize: bool = True

0 commit comments

Comments
 (0)