Skip to content

Commit 86b3f0d

Browse files
min size feature autodetect
1 parent f87b392 commit 86b3f0d

File tree

4 files changed

+234
-20
lines changed

4 files changed

+234
-20
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- The method `Geometry.reflected` can be used to create a reflected copy of any geometry off a plane. As for other transformations, for efficiency, `reflected` `PolySlab` directly returns an updated `PolySlab` object rather than a `Transformed` object, except when the normal of the plane of reflection has a non-zero component along the slab axis, in which case `Transformed` is still returned.
1414
- Validation check for unit error in grid spacing.
1515
- Validation that when symmetry is imposed along a given axis, the boundary conditions on each side of the axis are identical.
16+
- Fields `convex_resolution`, `concave_resolution`, and `mixed_resolution` in `CornerFinderSpec` can be used to take into account the dimensions of autodetected convex, concave, or mixed geometric features when `dl_min` is automatically inferred during automatic grid generation.
1617

1718
### Changed
1819
- Supplying autograd-traced values to geometric fields (`center`, `size`) of simulations, monitors, and sources now logs a warning and falls back to the static value instead of erroring.

tests/test_components/test_layerrefinement.py

+63
Original file line numberDiff line numberDiff line change
@@ -339,3 +339,66 @@ def count_grids_within_gap(sim_t):
339339
layer = layer.updated_copy(refinement_inside_sim_only=False)
340340
sim = sim.updated_copy(grid_spec=td.GridSpec.auto(wavelength=1, layer_refinement_specs=[layer]))
341341
assert count_grids_within_gap(sim) > 2
342+
343+
344+
def test_dl_min_from_smallest_feature():
345+
structure = td.Structure(
346+
geometry=td.PolySlab(
347+
vertices=[
348+
[0, 0],
349+
[2, 0],
350+
[2, 1],
351+
[1, 1],
352+
[1, 1.1],
353+
[2, 1.1],
354+
[2, 2],
355+
[1, 2],
356+
[1, 2.2],
357+
[0.7, 2.2],
358+
[0.7, 2],
359+
[0, 2],
360+
],
361+
slab_bounds=[-1, 1],
362+
axis=2,
363+
),
364+
medium=td.PECMedium(),
365+
)
366+
367+
# check expected dl_min
368+
layer_spec = td.LayerRefinementSpec(
369+
axis=2,
370+
size=(td.inf, td.inf, 2),
371+
corner_finder=td.CornerFinderSpec(
372+
convex_resolution=10,
373+
),
374+
)
375+
dl_min = layer_spec._dl_min_from_smallest_feature([structure])
376+
assert np.allclose(0.3 / 10, dl_min)
377+
378+
layer_spec = td.LayerRefinementSpec(
379+
axis=2,
380+
size=(td.inf, td.inf, 2),
381+
corner_finder=td.CornerFinderSpec(mixed_resolution=10),
382+
)
383+
dl_min = layer_spec._dl_min_from_smallest_feature([structure])
384+
assert np.allclose(0.2 / 10, dl_min)
385+
386+
layer_spec = td.LayerRefinementSpec(
387+
axis=2,
388+
size=(td.inf, td.inf, 2),
389+
corner_finder=td.CornerFinderSpec(
390+
concave_resolution=10,
391+
),
392+
)
393+
dl_min = layer_spec._dl_min_from_smallest_feature([structure])
394+
assert np.allclose(0.1 / 10, dl_min)
395+
396+
# check grid is generated succesfully
397+
sim = td.Simulation(
398+
size=(5, 5, 5),
399+
structures=[structure],
400+
grid_spec=td.GridSpec.auto(layer_refinement_specs=[layer_spec], wavelength=100 * td.C_0),
401+
run_time=1e-20,
402+
)
403+
404+
_ = sim.grid

tidy3d/components/grid/corner_finder.py

+97-11
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
"""Find corners of structures on a 2D plane."""
22

3-
from typing import List, Literal, Optional
3+
from typing import List, Literal, Optional, Tuple
44

55
import numpy as np
66
import pydantic.v1 as pd
77

88
from ...constants import inf
9-
from ..base import Tidy3dBaseModel
9+
from ..base import Tidy3dBaseModel, cached_property
1010
from ..geometry.base import Box, ClipOperation
1111
from ..geometry.utils import merging_geometries_on_plane
1212
from ..medium import PEC, LossyMetalMedium
1313
from ..structure import Structure
14-
from ..types import ArrayFloat2D, Axis
14+
from ..types import ArrayFloat1D, ArrayFloat2D, Axis
1515

1616
CORNER_ANGLE_THRESOLD = 0.1 * np.pi
1717

@@ -44,12 +44,44 @@ class CornerFinderSpec(Tidy3dBaseModel):
4444
"is below the threshold value based on Douglas-Peucker algorithm, the vertex is disqualified as a corner.",
4545
)
4646

47-
def corners(
47+
concave_resolution: Optional[pd.PositiveInt] = pd.Field(
48+
None,
49+
title="Concave Region Resolution.",
50+
description="Specifies number of steps to use for determining `dl_min` based on concave featues."
51+
"If set to ``None``, then the corresponding `dl_min` reduction is not applied.",
52+
)
53+
54+
convex_resolution: Optional[pd.PositiveInt] = pd.Field(
55+
None,
56+
title="Convex Region Resolution.",
57+
description="Specifies number of steps to use for determining `dl_min` based on convex featues."
58+
"If set to ``None``, then the corresponding `dl_min` reduction is not applied.",
59+
)
60+
61+
mixed_resolution: Optional[pd.PositiveInt] = pd.Field(
62+
None,
63+
title="Mixed Region Resolution.",
64+
description="Specifies number of steps to use for determining `dl_min` based on mixed featues."
65+
"If set to ``None``, then the corresponding `dl_min` reduction is not applied.",
66+
)
67+
68+
@cached_property
69+
def _no_min_dl_override(self):
70+
return all(
71+
(
72+
self.concave_resolution is None,
73+
self.convex_resolution is None,
74+
self.mixed_resolution is None,
75+
)
76+
)
77+
78+
def _corners_and_convexity(
4879
self,
4980
normal_axis: Axis,
5081
coord: float,
5182
structure_list: List[Structure],
52-
) -> ArrayFloat2D:
83+
ravel: bool,
84+
) -> Tuple[ArrayFloat2D, ArrayFloat1D]:
5385
"""On a 2D plane specified by axis = `normal_axis` and coordinate `coord`, find out corners of merged
5486
geometries made of `medium`.
5587
@@ -62,6 +94,8 @@ def corners(
6294
Position of plane along the normal axis.
6395
structure_list : List[Structure]
6496
List of structures present in simulation.
97+
ravel : bool
98+
Whether to put the resulting corners in a single list or per polygon.
6599
66100
Returns
67101
-------
@@ -89,6 +123,7 @@ def corners(
89123

90124
# corner finder
91125
corner_list = []
126+
convexity_list = []
92127
for mat, shapes in merged_geos:
93128
if self.medium != "all" and mat.is_pec != (self.medium == "metal"):
94129
continue
@@ -97,17 +132,59 @@ def corners(
97132
poly = poly.normalize().buffer(0)
98133
if self.distance_threshold is not None:
99134
poly = poly.simplify(self.distance_threshold, preserve_topology=True)
100-
corner_list.append(self._filter_collinear_vertices(list(poly.exterior.coords)))
135+
corners_xy, corners_convexity = self._filter_collinear_vertices(
136+
list(poly.exterior.coords)
137+
)
138+
corner_list.append(corners_xy)
139+
convexity_list.append(corners_convexity)
101140
# in case the polygon has holes
102141
for poly_inner in poly.interiors:
103-
corner_list.append(self._filter_collinear_vertices(list(poly_inner.coords)))
142+
corners_xy, corners_convexity = self._filter_collinear_vertices(
143+
list(poly_inner.coords)
144+
)
145+
corner_list.append(corners_xy)
146+
convexity_list.append(corners_convexity)
104147

105-
if len(corner_list) > 0:
148+
if ravel and len(corner_list) > 0:
106149
corner_list = np.concatenate(corner_list)
150+
convexity_list = np.concatenate(convexity_list)
151+
152+
return corner_list, convexity_list
153+
154+
def corners(
155+
self,
156+
normal_axis: Axis,
157+
coord: float,
158+
structure_list: List[Structure],
159+
) -> ArrayFloat2D:
160+
"""On a 2D plane specified by axis = `normal_axis` and coordinate `coord`, find out corners of merged
161+
geometries made of `medium`.
162+
163+
164+
Parameters
165+
----------
166+
normal_axis : Axis
167+
Axis normal to the 2D plane.
168+
coord : float
169+
Position of plane along the normal axis.
170+
structure_list : List[Structure]
171+
List of structures present in simulation.
172+
173+
Returns
174+
-------
175+
ArrayFloat2D
176+
Corner coordinates.
177+
"""
178+
179+
corner_list, _ = self._corners_and_convexity(
180+
normal_axis=normal_axis, coord=coord, structure_list=structure_list, ravel=True
181+
)
107182
return corner_list
108183

109-
def _filter_collinear_vertices(self, vertices: ArrayFloat2D) -> ArrayFloat2D:
110-
"""Filter collinear vertices of a polygon, and return corners.
184+
def _filter_collinear_vertices(
185+
self, vertices: ArrayFloat2D
186+
) -> Tuple[ArrayFloat2D, ArrayFloat1D]:
187+
"""Filter collinear vertices of a polygon, and return corners locations and their convexity.
111188
112189
Parameters
113190
----------
@@ -119,6 +196,8 @@ def _filter_collinear_vertices(self, vertices: ArrayFloat2D) -> ArrayFloat2D:
119196
-------
120197
ArrayFloat2D
121198
Corner coordinates.
199+
ArrayFloat1D
200+
Convexity of corners: True for outer corners, False for inner corners.
122201
"""
123202

124203
def normalize(v):
@@ -136,5 +215,12 @@ def normalize(v):
136215
inner_product = np.where(inner_product > 1, 1, inner_product)
137216
inner_product = np.where(inner_product < -1, -1, inner_product)
138217
angle = np.arccos(inner_product)
218+
num_vs = len(vs_orig)
219+
cross_product = np.cross(
220+
np.hstack([unit_next, np.zeros((num_vs, 1))]),
221+
np.hstack([unit_previous, np.zeros((num_vs, 1))]),
222+
axis=-1,
223+
)
224+
convexity = cross_product[:, 2] < 0
139225
ind_filter = angle <= np.pi - self.angle_threshold
140-
return vs_orig[ind_filter]
226+
return vs_orig[ind_filter], convexity[ind_filter]

tidy3d/components/grid/grid_spec.py

+73-9
Original file line numberDiff line numberDiff line change
@@ -1262,7 +1262,7 @@ def _unpop_axis(self, ax_coord: float, plane_coord: Any) -> CoordinateOptional:
12621262
"""
12631263
return self.unpop_axis(ax_coord, [plane_coord, plane_coord], self.axis)
12641264

1265-
def suggested_dl_min(self, grid_size_in_vacuum: float) -> float:
1265+
def suggested_dl_min(self, grid_size_in_vacuum: float, structures: List[Structure]) -> float:
12661266
"""Suggested lower bound of grid step size for this layer.
12671267
12681268
Parameters
@@ -1293,6 +1293,12 @@ def suggested_dl_min(self, grid_size_in_vacuum: float) -> float:
12931293
# inplane dimension
12941294
if self.corner_finder is not None and self.corner_refinement is not None:
12951295
dl_min = min(dl_min, self.corner_refinement._grid_size(grid_size_in_vacuum))
1296+
1297+
# min feature size
1298+
if self.corner_finder is not None and not self.corner_finder._no_min_dl_override:
1299+
dl_suggested = self._dl_min_from_smallest_feature(structures)
1300+
dl_min = min(dl_min, dl_suggested)
1301+
12961302
return dl_min
12971303

12981304
def generate_snapping_points(self, structure_list: List[Structure]) -> List[CoordinateOptional]:
@@ -1329,22 +1335,80 @@ def _inplane_inside(self, point: ArrayFloat2D) -> bool:
13291335
)
13301336
return self.inside(point_3d[0], point_3d[1], point_3d[2])
13311337

1332-
def _corners(self, structure_list: List[Structure]) -> List[CoordinateOptional]:
1333-
"""Inplane corners in 3D coordinate."""
1338+
def _corners_and_convexity_2d(
1339+
self, structure_list: List[Structure], ravel: bool
1340+
) -> List[CoordinateOptional]:
1341+
"""Raw inplane corners and their convexity."""
13341342
if self.corner_finder is None:
1335-
return []
1343+
return [], []
13361344

13371345
# filter structures outside the layer
13381346
structures_intersect = structure_list
13391347
if self._is_inplane_bounded:
13401348
structures_intersect = [s for s in structure_list if self.intersects(s.geometry)]
1341-
inplane_points = self.corner_finder.corners(
1342-
self.axis, self.center_axis, structures_intersect
1349+
inplane_points, convexity = self.corner_finder._corners_and_convexity(
1350+
self.axis, self.center_axis, structures_intersect, ravel
13431351
)
13441352

13451353
# filter corners outside the inplane bounds
1346-
if self._is_inplane_bounded:
1347-
inplane_points = [point for point in inplane_points if self._inplane_inside(point)]
1354+
if self._is_inplane_bounded and len(inplane_points) > 0:
1355+
# flatten temporary list of arrays for faster processing
1356+
if not ravel:
1357+
split_inds = np.cumsum([len(pts) for pts in inplane_points])[:-1]
1358+
inplane_points = np.concatenate(inplane_points)
1359+
convexity = np.concatenate(convexity)
1360+
inds = [self._inplane_inside(point) for point in inplane_points]
1361+
inplane_points = inplane_points[inds]
1362+
convexity = convexity[inds]
1363+
if not ravel:
1364+
inplane_points = np.split(inplane_points, split_inds)
1365+
convexity = np.split(convexity, split_inds)
1366+
1367+
return inplane_points, convexity
1368+
1369+
def _dl_min_from_smallest_feature(self, structure_list: List[Structure]):
1370+
"""Calculate `dl_min` suggestion based on smallest feature size."""
1371+
1372+
inplane_points, convexity = self._corners_and_convexity_2d(
1373+
structure_list=structure_list, ravel=False
1374+
)
1375+
1376+
dl_min = inf
1377+
1378+
if self.corner_finder is None or self.corner_finder._no_min_dl_override:
1379+
return dl_min
1380+
1381+
finder = self.corner_finder
1382+
1383+
for points, conv in zip(inplane_points, convexity):
1384+
conv_nei = np.roll(conv, -1)
1385+
lengths = np.linalg.norm(points - np.roll(points, axis=0, shift=-1), axis=-1)
1386+
1387+
if finder.convex_resolution is not None:
1388+
convex_features = np.logical_and(conv, conv_nei)
1389+
if np.any(convex_features):
1390+
min_convex_size = np.min(lengths[convex_features])
1391+
dl_min = min(dl_min, min_convex_size / finder.convex_resolution)
1392+
1393+
if finder.concave_resolution is not None:
1394+
concave_features = np.logical_not(np.logical_or(conv, conv_nei))
1395+
if np.any(concave_features):
1396+
min_concave_size = np.min(lengths[concave_features])
1397+
dl_min = min(dl_min, min_concave_size / finder.concave_resolution)
1398+
1399+
if finder.mixed_resolution is not None:
1400+
mixed_features = np.logical_xor(conv, conv_nei)
1401+
if np.any(mixed_features):
1402+
min_mixed_size = np.min(lengths[mixed_features])
1403+
dl_min = min(dl_min, min_mixed_size / finder.mixed_resolution)
1404+
1405+
return dl_min
1406+
1407+
def _corners(self, structure_list: List[Structure]) -> List[CoordinateOptional]:
1408+
"""Inplane corners in 3D coordinate."""
1409+
inplane_points, _ = self._corners_and_convexity_2d(
1410+
structure_list=structure_list, ravel=True
1411+
)
13481412

13491413
# convert 2d points to 3d
13501414
return [
@@ -1817,7 +1881,7 @@ def _dl_min(
18171881
if self.layer_refinement_used:
18181882
min_vacuum_dl = self._min_vacuum_dl_in_autogrid(wavelength, sim_size)
18191883
for layer in self.layer_refinement_specs:
1820-
min_dl = min(min_dl, layer.suggested_dl_min(min_vacuum_dl))
1884+
min_dl = min(min_dl, layer.suggested_dl_min(min_vacuum_dl, structures))
18211885
# from lumped elements
18221886
for lumped_element in lumped_elements:
18231887
for override_structure in lumped_element.to_mesh_overrides():

0 commit comments

Comments
 (0)