Skip to content

Commit 3037d0f

Browse files
fix(Medium2D): fix subdivision when merged geometry is a polygon with holes
1 parent 20a7f36 commit 3037d0f

File tree

4 files changed

+97
-10
lines changed

4 files changed

+97
-10
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
8686
- Fixed DRC parsing for quoted categories in klayout plugin.
8787
- Removed mode solver warnings about evaluating permittivity of a `Medium2D`.
8888
- Maximum number of grid points in an EME simulation is now based solely on transverse grid points. Maximum number of EME cells is unchanged.
89+
- Fixed handling of polygons with holes (interiors) in `subdivide()` function. The function now properly converts polygons with interiors into `PolySlab` geometries using subtraction operations with `GeometryGroup`.
8990

9091
### Removed
9192
- Removed deprecated `use_complex_fields` parameter from `TwoPhotonAbsorption` and `KerrNonlinearity`. Parameters `beta` and `n2` are now real-valued only, as is `n0` if specified.

tests/test_components/test_geometry.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,6 +1158,71 @@ def test_subdivide():
11581158
subdivisions = subdivide(geom=overlapping_boxes, structures=[background_structure, box_sliver])
11591159

11601160

1161+
def test_subdivide_geometry_group_with_polygon_holes():
1162+
"""Test that unionized geometry containing a hole works correctly."""
1163+
mm = 1000.0
1164+
1165+
# Create four boxes arranged to form a cross pattern with a square hole in the middle
1166+
box_a = td.Box.from_bounds((-10 * mm, -10 * mm, 0 * mm), (-5 * mm, 5 * mm, 0 * mm))
1167+
box_b = td.Box.from_bounds((-10 * mm, 5 * mm, 0 * mm), (5 * mm, 10 * mm, 0 * mm))
1168+
box_c = td.Box.from_bounds((5 * mm, -5 * mm, 0 * mm), (10 * mm, 10 * mm, 0 * mm))
1169+
box_d = td.Box.from_bounds((-5 * mm, -10 * mm, 0 * mm), (10 * mm, -5 * mm, 0 * mm))
1170+
1171+
geom_group = td.GeometryGroup(geometries=(box_a, box_b, box_c, box_d))
1172+
geom_structures_group = [td.Structure(geometry=geom_group, medium=td.PEC2D)]
1173+
1174+
feed_pin_bottom = -2 * mm
1175+
feed_pin_top = 0 * mm
1176+
feed_pin_center = 0.5 * (feed_pin_top + feed_pin_bottom)
1177+
feed_pin_length = feed_pin_top - feed_pin_bottom
1178+
feed_center_x = -7.5 * mm
1179+
feed_center_y = -7.5 * mm
1180+
rfeed = 1.0 * mm
1181+
1182+
feed_pin = td.Structure(
1183+
geometry=td.Cylinder(
1184+
center=(feed_center_x, feed_center_y, feed_pin_center),
1185+
radius=rfeed,
1186+
length=feed_pin_length,
1187+
axis=2,
1188+
),
1189+
medium=td.PECMedium(),
1190+
)
1191+
1192+
structures_list = [feed_pin, *geom_structures_group]
1193+
1194+
freq = 1500 * 1e6
1195+
dl = (td.C_0 / freq) / 300.0
1196+
1197+
mesh_overrides = [
1198+
td.MeshOverrideStructure(
1199+
geometry=td.Box(
1200+
center=(0, 0, 0 * mm),
1201+
size=(20 * mm, 20 * mm, 6 * mm),
1202+
),
1203+
dl=[dl, dl, dl],
1204+
)
1205+
]
1206+
1207+
sim = td.Simulation(
1208+
size=[100 * mm, 100 * mm, 30 * mm],
1209+
grid_spec=td.GridSpec.auto(
1210+
min_steps_per_wvl=20,
1211+
wavelength=td.C_0 / freq,
1212+
override_structures=mesh_overrides,
1213+
),
1214+
structures=structures_list,
1215+
run_time=1e-13,
1216+
)
1217+
1218+
contains_difference_operation = False
1219+
for structure in sim._finalized.structures:
1220+
geo = structure.geometry
1221+
if isinstance(geo, td.ClipOperation) and geo.operation == "difference":
1222+
contains_difference_operation = True
1223+
assert contains_difference_operation
1224+
1225+
11611226
@pytest.mark.parametrize("snap_location", [SnapLocation.Boundary, SnapLocation.Center])
11621227
@pytest.mark.parametrize(
11631228
"snap_behavior",

tidy3d/components/geometry/base.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import functools
66
import pathlib
77
from abc import ABC, abstractmethod
8-
from collections.abc import Iterable
8+
from collections.abc import Iterable, Sequence
99
from os import PathLike
1010
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
1111

@@ -721,11 +721,14 @@ def evaluate_inf_shape(shape: Shapely) -> Shapely:
721721
if not any(np.isinf(b) for b in shape.bounds):
722722
return shape
723723

724+
def _processed_coords(coords: Sequence[tuple[Any, ...]]) -> list[tuple[float, ...]]:
725+
evaluated = Geometry._evaluate_inf(np.array(coords))
726+
return [tuple(point) for point in evaluated.tolist()]
727+
724728
if shape.geom_type == "Polygon":
725-
return shapely.Polygon(
726-
Geometry._evaluate_inf(np.array(shape.exterior.coords)),
727-
[Geometry._evaluate_inf(np.array(g.coords)) for g in shape.interiors],
728-
)
729+
shell = _processed_coords(shape.exterior.coords)
730+
holes = [_processed_coords(g.coords) for g in shape.interiors]
731+
return shapely.Polygon(shell, holes)
729732
if shape.geom_type in {"Point", "LineString", "LinearRing"}:
730733
return shape.__class__(Geometry._evaluate_inf(np.array(shape.coords)))
731734
if shape.geom_type in {

tidy3d/components/geometry/utils_2d.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import numpy as np
88
import shapely
99

10-
from tidy3d.components.geometry.base import Box, ClipOperation, Geometry
10+
from tidy3d.components.geometry.base import Box, ClipOperation, Geometry, GeometryGroup
1111
from tidy3d.components.geometry.polyslab import _MIN_POLYGON_AREA, PolySlab
1212
from tidy3d.components.grid.grid import Grid
1313
from tidy3d.components.scene import Scene
@@ -122,10 +122,28 @@ def subdivide(
122122
123123
"""
124124

125-
def shapely_to_polyslab(polygon: shapely.Polygon, axis: Axis, center: float) -> PolySlab:
126-
xx, yy = polygon.exterior.coords.xy
127-
vertices = list(zip(xx, yy))
128-
return PolySlab(slab_bounds=(center, center), vertices=vertices, axis=axis)
125+
def shapely_to_polyslab(polygon: shapely.Polygon, axis: Axis, center: float) -> Geometry:
126+
def ring_vertices(ring: shapely.LinearRing) -> list[tuple[float, float]]:
127+
xx, yy = ring.coords.xy
128+
return list(zip(xx, yy))
129+
130+
polyslab = PolySlab(
131+
slab_bounds=(center, center),
132+
vertices=ring_vertices(polygon.exterior),
133+
axis=axis,
134+
)
135+
if len(polygon.interiors) == 0:
136+
return polyslab
137+
138+
interiors = [
139+
PolySlab(
140+
slab_bounds=(center, center),
141+
vertices=ring_vertices(interior),
142+
axis=axis,
143+
)
144+
for interior in polygon.interiors
145+
]
146+
return polyslab - GeometryGroup(geometries=interiors)
129147

130148
def to_multipolygon(shapely_geometry: Shapely) -> shapely.MultiPolygon:
131149
return shapely.MultiPolygon(ClipOperation.to_polygon_list(shapely_geometry))

0 commit comments

Comments
 (0)