Skip to content

Commit 5dddc65

Browse files
More efficient mesh refinement in LayerRefinementSpec for interior-disjoint structures
1 parent 7753b89 commit 5dddc65

File tree

6 files changed

+201
-25
lines changed

6 files changed

+201
-25
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- Added support for `tidy3d-extras`, an optional plugin that enables more accurate local mode solving via subpixel averaging.
1515

1616
### Changed
17+
- `LayerRefinementSpec` defaults to assuming structures made of different materials are interior-disjoint for more efficient mesh generation.
1718

1819
### Fixed
1920
- Stricter validation for `bend_radius` in mode simulations, preventing the bend center from coinciding with the simulation boundary.

tests/test_components/test_layerrefinement.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,62 @@ def count_grids_within_layer(sim_t):
288288
)
289289

290290

291+
def test_grid_spec_with_layers_interior_disjoint():
292+
"""Test the application of layer_specs with interior_disjoint assumption to GridSpec."""
293+
294+
thickness = 1e-3
295+
lumped_elements = []
296+
# a PEC thin layer structure
297+
box1 = td.Box(size=(thickness, 2, 2))
298+
box2 = td.Box(center=(0, -1, 0), size=(thickness, 1, 1))
299+
pec_str = td.Structure(geometry=box1 - box2, medium=td.PEC)
300+
# a dielectric that overlaps with PEC structure, hence breaking interior-disjoint assumption
301+
die_str = td.Structure(
302+
geometry=td.Box(size=(thickness, 0.5, 0.5)),
303+
medium=td.Medium(),
304+
)
305+
# layer spec assuming interior disjoint geometries
306+
layer_disjoint = LayerRefinementSpec.from_structures(
307+
[
308+
pec_str,
309+
],
310+
interior_disjoint_geometries=True,
311+
)
312+
# layer spec for general geometries
313+
layer_general = layer_disjoint.updated_copy(interior_disjoint_geometries=False)
314+
315+
sim = td.Simulation(
316+
size=(4, 4, 4),
317+
grid_spec=td.GridSpec.auto(
318+
min_steps_per_wvl=11, wavelength=1, layer_refinement_specs=[layer_disjoint]
319+
),
320+
boundary_spec=td.BoundarySpec.pml(),
321+
structures=[pec_str],
322+
run_time=1e-12,
323+
)
324+
sim_general = sim.updated_copy(
325+
grid_spec=sim.grid_spec.updated_copy(layer_refinement_specs=[layer_general])
326+
)
327+
328+
# for simulations with interior-disjoint geometries, same corner finder results for both layer settings
329+
def is_equal_snapping_points(plist1, plist2):
330+
if len(plist1) != len(plist2):
331+
return False
332+
for p1, p2 in zip(plist1, plist2):
333+
if p1 != p2:
334+
return False
335+
return True
336+
337+
assert is_equal_snapping_points(
338+
sim.internal_snapping_points, sim_general.internal_snapping_points
339+
)
340+
341+
# for simulations with overlapping geometries, assuming interior-disjoint misses some corners in this simulation
342+
sim = sim.updated_copy(structures=[pec_str, die_str])
343+
sim_general = sim_general.updated_copy(structures=[pec_str, die_str])
344+
assert len(sim_general.internal_snapping_points) > len(sim.internal_snapping_points)
345+
346+
291347
@pytest.mark.parametrize("gap_meshing_iters", [0, 1])
292348
def test_corner_refinement_outside_domain(gap_meshing_iters):
293349
"""Test the behavior of corner refinement if corners are outside the simulation domain."""
@@ -573,6 +629,7 @@ def test_gap_meshing():
573629
corner_finder=None,
574630
gap_meshing_iters=2,
575631
dl_min_from_gap_width=True,
632+
interior_disjoint_geometries=False,
576633
)
577634
],
578635
min_steps_per_wvl=10,

tidy3d/components/geometry/utils.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
from __future__ import annotations
44

5+
from collections import defaultdict
56
from enum import Enum
67
from math import isclose
78
from typing import Any, Optional, Union
89

910
import numpy as np
1011
import pydantic
12+
import shapely
1113

1214
from tidy3d.components.autograd.utils import get_static
1315
from tidy3d.components.base import Tidy3dBaseModel
@@ -45,6 +47,7 @@ def merging_geometries_on_plane(
4547
geometries: list[GeometryType],
4648
plane: Box,
4749
property_list: list[Any],
50+
interior_disjoint_geometries: bool = False,
4851
) -> list[tuple[Any, Shapely]]:
4952
"""Compute list of shapes on plane. Overlaps are removed or merged depending on
5053
provided property_list.
@@ -57,6 +60,8 @@ def merging_geometries_on_plane(
5760
Plane specification.
5861
property_list : List = None
5962
Property value for each structure.
63+
interior_disjoint_geometries: bool = False
64+
If ``True``, geometries of different properties on the plane must not be overlapping.
6065
6166
Returns
6267
-------
@@ -78,6 +83,20 @@ def merging_geometries_on_plane(
7883
for shape in shapes_plane:
7984
shapes.append((prop, shape, shape.bounds))
8085

86+
if interior_disjoint_geometries:
87+
# No need to consider overlapping. We simply group shapes by property, and union_all
88+
# shapes of the same property.
89+
shapes_by_prop = defaultdict(list)
90+
for prop, shape, _ in shapes:
91+
shapes_by_prop[prop].append(shape)
92+
# union shapes of same property
93+
results = []
94+
for prop, shapes in shapes_by_prop.items():
95+
unionized = shapely.union_all(shapes).buffer(0).normalize()
96+
if not unionized.is_empty:
97+
results.append((prop, unionized))
98+
return results
99+
81100
background_shapes = []
82101
for prop, shape, bounds in shapes:
83102
minx, miny, maxx, maxy = bounds

tidy3d/components/grid/corner_finder.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ def _merged_pec_on_plane(
8585
structure_list: list[Structure],
8686
center: tuple[float, float] = [0, 0, 0],
8787
size: tuple[float, float, float] = [inf, inf, inf],
88+
interior_disjoint_geometries: bool = False,
8889
) -> list[tuple[Any, Shapely]]:
8990
"""On a 2D plane specified by axis = `normal_axis` and coordinate `coord`, merge geometries made of PEC.
9091
@@ -100,6 +101,8 @@ def _merged_pec_on_plane(
100101
Center of the 2D plane (coordinate along ``axis`` is ignored)
101102
size : Tuple[float, float, float] = [inf, inf, inf]
102103
Size of the 2D plane (size along ``axis`` is ignored)
104+
interior_disjoint_geometries: bool = False
105+
If ``True``, geometries on the plane must not be overlapping.
103106
104107
Returns
105108
-------
@@ -123,7 +126,9 @@ def _merged_pec_on_plane(
123126
PEC if (mat.is_pec or isinstance(mat, LossyMetalMedium)) else mat for mat in medium_list
124127
]
125128
# merge geometries
126-
merged_geos = merging_geometries_on_plane(geometry_list, plane, medium_list)
129+
merged_geos = merging_geometries_on_plane(
130+
geometry_list, plane, medium_list, interior_disjoint_geometries
131+
)
127132

128133
return merged_geos
129134

@@ -133,6 +138,7 @@ def _corners_and_convexity(
133138
coord: float,
134139
structure_list: list[Structure],
135140
ravel: bool,
141+
interior_disjoint_geometries: bool = False,
136142
) -> tuple[ArrayFloat2D, ArrayFloat1D]:
137143
"""On a 2D plane specified by axis = `normal_axis` and coordinate `coord`, find out corners of merged
138144
geometries made of PEC.
@@ -148,6 +154,8 @@ def _corners_and_convexity(
148154
List of structures present in simulation.
149155
ravel : bool
150156
Whether to put the resulting corners in a single list or per polygon.
157+
interior_disjoint_geometries: bool = False
158+
If ``True``, geometries made of different materials on the plane must not be overlapping.
151159
152160
Returns
153161
-------
@@ -157,7 +165,10 @@ def _corners_and_convexity(
157165

158166
# merge geometries
159167
merged_geos = self._merged_pec_on_plane(
160-
normal_axis=normal_axis, coord=coord, structure_list=structure_list
168+
normal_axis=normal_axis,
169+
coord=coord,
170+
structure_list=structure_list,
171+
interior_disjoint_geometries=interior_disjoint_geometries,
161172
)
162173

163174
# corner finder
@@ -183,18 +194,22 @@ def _corners_and_convexity(
183194
)
184195
corner_list.append(corners_xy)
185196
convexity_list.append(corners_convexity)
197+
return self._ravel_corners_and_convexity(ravel, corner_list, convexity_list)
186198

199+
def _ravel_corners_and_convexity(
200+
self, ravel: bool, corner_list, convexity_list
201+
) -> tuple[ArrayFloat2D, ArrayFloat1D]:
202+
"""Whether to put the resulting corners in a single list or per polygon."""
187203
if ravel and len(corner_list) > 0:
188-
corner_list = np.concatenate(corner_list)
189-
convexity_list = np.concatenate(convexity_list)
190-
204+
return np.concatenate(corner_list), np.concatenate(convexity_list)
191205
return corner_list, convexity_list
192206

193207
def corners(
194208
self,
195209
normal_axis: Axis,
196210
coord: float,
197211
structure_list: list[Structure],
212+
interior_disjoint_geometries: bool = False,
198213
) -> ArrayFloat2D:
199214
"""On a 2D plane specified by axis = `normal_axis` and coordinate `coord`, find out corners of merged
200215
geometries made of `medium`.
@@ -208,15 +223,20 @@ def corners(
208223
Position of plane along the normal axis.
209224
structure_list : List[Structure]
210225
List of structures present in simulation.
211-
226+
interior_disjoint_geometries: bool = False
227+
If ``True``, geometries made of different materials on the plane must not be overlapping.
212228
Returns
213229
-------
214230
ArrayFloat2D
215231
Corner coordinates.
216232
"""
217233

218234
corner_list, _ = self._corners_and_convexity(
219-
normal_axis=normal_axis, coord=coord, structure_list=structure_list, ravel=True
235+
normal_axis=normal_axis,
236+
coord=coord,
237+
structure_list=structure_list,
238+
ravel=True,
239+
interior_disjoint_geometries=interior_disjoint_geometries,
220240
)
221241
return corner_list
222242

0 commit comments

Comments
 (0)