Skip to content

Commit ddbe4ae

Browse files
formatting
1 parent c3138c0 commit ddbe4ae

File tree

3 files changed

+117
-40
lines changed

3 files changed

+117
-40
lines changed

tests/test_components/test_layerrefinement.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,8 @@ def count_grids_within_layer(sim_t):
285285
)
286286

287287

288-
def test_corner_refinement_outside_domain():
288+
@pytest.mark.parametrize("gap_meshing_iters", [0, 1])
289+
def test_corner_refinement_outside_domain(gap_meshing_iters):
289290
"""Test the behavior of corner refinement if corners are outside the simulation domain."""
290291

291292
# CPW waveguides that goes through the simulation domain along x-axis, so that the corners
@@ -313,6 +314,7 @@ def test_corner_refinement_outside_domain():
313314
axis=2,
314315
corner_refinement=td.GridRefinement(refinement_factor=5),
315316
refinement_inside_sim_only=True,
317+
gap_meshing_iters=gap_meshing_iters,
316318
)
317319

318320
sim = td.Simulation(
@@ -333,11 +335,12 @@ def count_grids_within_gap(sim_t):
333335
return len(y)
334336

335337
# just 2 grids sampling the gap
336-
assert count_grids_within_gap(sim) == 2
338+
assert count_grids_within_gap(sim) == gap_meshing_iters + 2
337339

338340
# 2) refined if corners outside simulation domain is accounted for.
339341
layer = layer.updated_copy(refinement_inside_sim_only=False)
340342
sim = sim.updated_copy(grid_spec=td.GridSpec.auto(wavelength=1, layer_refinement_specs=[layer]))
343+
341344
assert count_grids_within_gap(sim) > 2
342345

343346

tidy3d/components/grid/grid_spec.py

+106-36
Original file line numberDiff line numberDiff line change
@@ -1553,7 +1553,9 @@ def _override_structures_along_axis(
15531553
override_structures += refinement_structures
15541554
return override_structures
15551555

1556-
def _find_vertical_intersections(self, grid_x_coords, grid_y_coords, poly_vertices) -> Tuple[List[Tuple[int, int]], List[float]]:
1556+
def _find_vertical_intersections(
1557+
self, grid_x_coords, grid_y_coords, poly_vertices
1558+
) -> Tuple[List[Tuple[int, int]], List[float]]:
15571559
"""Detect intersection points of single polygon and vertical grid lines."""
15581560

15591561
# indices of cells that contain intersection with grid lines (left edge of a cell)
@@ -1599,7 +1601,9 @@ def _find_vertical_intersections(self, grid_x_coords, grid_y_coords, poly_vertic
15991601
# however, some of the vertical lines might be crossed
16001602
# outside of computational domain
16011603
# so we need to see which ones are actually inside along y axis
1602-
inds_inside_grid = np.logical_and(intersections_y >= y[0], intersections_y <= y[-1])
1604+
inds_inside_grid = np.logical_and(
1605+
intersections_y >= grid_y_coords[0], intersections_y <= grid_y_coords[-1]
1606+
)
16031607

16041608
intersections_y = intersections_y[inds_inside_grid]
16051609

@@ -1615,60 +1619,94 @@ def _find_vertical_intersections(self, grid_x_coords, grid_y_coords, poly_vertic
16151619

16161620
# find local dy, that is, the distance between the intersection point
16171621
# and the bottom edge of the cell
1618-
dy = (intersections_y - grid_y_coords[cell_js]) / (grid_y_coords[cell_js + 1] - grid_y_coords[cell_js])
1622+
dy = (intersections_y - grid_y_coords[cell_js]) / (
1623+
grid_y_coords[cell_js + 1] - grid_y_coords[cell_js]
1624+
)
16191625

16201626
# record info
16211627
cells_ij.append(np.transpose([cell_is, cell_js]))
16221628
cells_dy.append(dy)
16231629

16241630
if len(cells_ij) > 0:
16251631
cells_ij = np.concatenate(cells_ij)
1626-
1627-
if len(cells_dy) > 0:
16281632
cells_dy = np.concatenate(cells_dy)
16291633

1634+
# Filter from re-entering subcell features. That is, we discard any consecutive
1635+
# intersections if they are crossing the same edge. This happens, for example,
1636+
# when a tiny feature pokes through an edge. This helps not to set dl_min
1637+
# to a very low value, and take into account only actual gaps and strips.
1638+
1639+
# To do that we use the fact that intersection points are recorded and stored
1640+
# in the order as they appear along the border of the polygon.
1641+
1642+
# first we calculate linearized indices of edges (cells) they cross
16301643
linear_index = cells_ij[:, 0] * len(grid_y_coords) + cells_ij[:, 1]
16311644

1645+
# then look at the differences with next and previous neighbors
16321646
fwd_diff = linear_index - np.roll(linear_index, -1)
1633-
bwd_diff = linear_index - np.roll(linear_index, 1)
1647+
bwd_diff = np.roll(fwd_diff, 1)
16341648

1649+
# an intersection point is not a part of a "re-entering subcell feature"
1650+
# if it doesn't cross the same edges as its neighbors
16351651
valid = np.logical_and(fwd_diff != 0, bwd_diff != 0)
16361652

16371653
cells_dy = cells_dy[valid]
16381654
cells_ij = cells_ij[valid]
16391655

1640-
# duplicate crossings that are too close to cell boundaries
1641-
close_zero = cells_dy < GAP_MESHING_TOL
1642-
close_one = (1.0 - cells_dy) < GAP_MESHING_TOL
1643-
1644-
duplicate_ij_zero = cells_ij[close_zero]
1645-
duplicate_ij_one = cells_ij[close_one]
1646-
1647-
cells_ij = np.concatenate([cells_ij, duplicate_ij_zero - np.array([0, 1]), duplicate_ij_one + np.array([0, 1])])
1648-
cells_dy = np.concatenate([cells_dy, (1.0 - GAP_MESHING_TOL / 10) * np.ones(len(duplicate_ij_zero)), GAP_MESHING_TOL / 10 * np.ones(len(duplicate_ij_one))])
1656+
# Now we are duplicating intersection points very close to cell boundaries
1657+
# to corresponding adjacent cells. Basically, if we have a line crossing
1658+
# very close to a grid node, we consider that it crosses edges on both sides
1659+
# from that node. That is, this serves as a tolerance allowance.
1660+
# Note that duplicated intersections and their originals will be snapped to
1661+
# cell boundaries during quantization later.
1662+
close_to_zero = cells_dy < GAP_MESHING_TOL
1663+
close_to_one = (1.0 - cells_dy) < GAP_MESHING_TOL
1664+
1665+
points_to_duplicate_near_zero = cells_ij[close_to_zero]
1666+
points_to_duplicate_near_one = cells_ij[close_to_one]
1667+
1668+
cells_ij = np.concatenate(
1669+
[
1670+
cells_ij,
1671+
points_to_duplicate_near_zero - np.array([0, 1]),
1672+
points_to_duplicate_near_one + np.array([0, 1]),
1673+
]
1674+
)
1675+
cells_dy = np.concatenate(
1676+
[
1677+
cells_dy,
1678+
np.ones(len(points_to_duplicate_near_zero)),
1679+
np.zeros(len(points_to_duplicate_near_one)),
1680+
]
1681+
)
16491682

16501683
return cells_ij, cells_dy
16511684

16521685
def _process_poly(
1653-
self, grid_x_coords, grid_y_coords, poly_vertices
1686+
self, grid_x_coords, grid_y_coords, poly_vertices
16541687
) -> Tuple[List[Tuple[int, int]], List[float], List[Tuple[int, int]], List[float]]:
16551688
"""Detect intersection points of single polygon and grid lines."""
16561689

16571690
# find cells that contain intersections of vertical grid lines
16581691
# and relative locations of those intersections (along y axis)
1659-
v_cells_ij, v_cells_dy = self._find_vertical_intersections(grid_x_coords, grid_y_coords, poly_vertices)
1692+
v_cells_ij, v_cells_dy = self._find_vertical_intersections(
1693+
grid_x_coords, grid_y_coords, poly_vertices
1694+
)
16601695

16611696
# find cells that contain intersections of horizontal grid lines
16621697
# and relative locations of those intersections (along x axis)
16631698
# reuse the same command but flip dimensions
1664-
h_cells_ij, h_cells_dx = self._find_vertical_intersections(grid_y_coords, grid_x_coords, np.flip(poly_vertices, axis=1))
1699+
h_cells_ij, h_cells_dx = self._find_vertical_intersections(
1700+
grid_y_coords, grid_x_coords, np.flip(poly_vertices, axis=1)
1701+
)
16651702
if len(h_cells_ij) > 0:
1703+
# flip dimensions back
16661704
h_cells_ij = np.roll(h_cells_ij, axis=1, shift=1)
16671705

16681706
return v_cells_ij, v_cells_dy, h_cells_ij, h_cells_dx
16691707

16701708
def _process_slice(
1671-
self, x, y, merged_geos
1709+
self, x, y, merged_geos
16721710
) -> Tuple[List[Tuple[int, int]], List[float], List[Tuple[int, int]], List[float]]:
16731711
"""Detect intersection points of geometries boundaries and grid lines."""
16741712

@@ -1685,6 +1723,8 @@ def _process_slice(
16851723
# loop over all shapes
16861724
for mat, shapes in merged_geos:
16871725
if not mat.is_pec:
1726+
# note that we expect LossyMetal's converted into PEC in merged_geos
1727+
# that is why we are not checking for that separately
16881728
continue
16891729
polygon_list = ClipOperation.to_polygon_list(shapes)
16901730
for poly in polygon_list:
@@ -1726,12 +1766,12 @@ def _process_slice(
17261766
h_cells_dx = np.concatenate(h_cells_dx)
17271767

17281768
return v_cells_ij, v_cells_dy, h_cells_ij, h_cells_dx
1729-
1769+
17301770
def _generate_horizontal_snapping_lines(
1731-
self, grid_y_coords, intersected_cells_ij, relative_vert_disp
1771+
self, grid_y_coords, intersected_cells_ij, relative_vert_disp
17321772
) -> Tuple[List[CoordinateOptional], float]:
17331773
"""Convert a list of intersections of vertical grid lines, given as coordinates of cells
1734-
and relative vertical displacement inside each cell, into locations of snapping lines that
1774+
and relative vertical displacement inside each cell, into locations of snapping lines that
17351775
resolve thin gaps and strips.
17361776
"""
17371777
min_gap_width = inf
@@ -1740,9 +1780,11 @@ def _generate_horizontal_snapping_lines(
17401780
if len(intersected_cells_ij) > 0:
17411781
# quantize intersection locations
17421782
relative_vert_disp = np.round(relative_vert_disp / GAP_MESHING_TOL).astype(int)
1743-
cell_linear_inds = intersected_cells_ij[:, 0] * len(grid_y_coords) + intersected_cells_ij[:, 1]
1783+
cell_linear_inds = (
1784+
intersected_cells_ij[:, 0] * len(grid_y_coords) + intersected_cells_ij[:, 1]
1785+
)
17441786
cell_linear_inds_and_disps = np.transpose([cell_linear_inds, relative_vert_disp])
1745-
# remove duplicates
1787+
# remove duplicates
17461788
cell_linear_inds_and_disps_unique = np.unique(cell_linear_inds_and_disps, axis=0)
17471789

17481790
# count intersections of vertical grid lines in each cell
@@ -1767,31 +1809,47 @@ def _generate_horizontal_snapping_lines(
17671809
j_selection = cell_linear_inds_unique_j == ind_j
17681810
# and number intersections in each of them
17691811
counts_j = counts[j_selection]
1770-
1812+
17711813
# find cell with max intersections
17721814
max_count_el = np.argmax(counts_j)
17731815
max_count = counts_j[max_count_el]
17741816
if max_count > 1:
17751817
# get its linear index
17761818
target_cell_linear_ind = cell_linear_inds_unique[j_selection][max_count_el]
17771819
# look up relative positions of intersections in that cells
1778-
target_disps = np.sort(cell_linear_inds_and_disps_unique[cell_linear_inds_and_disps_unique[:, 0] == target_cell_linear_ind, 1])
1820+
target_disps = np.sort(
1821+
cell_linear_inds_and_disps_unique[
1822+
cell_linear_inds_and_disps_unique[:, 0] == target_cell_linear_ind, 1
1823+
]
1824+
)
17791825

17801826
# place a snapping line between any two neighboring intersections (in relative units)
1781-
relative_snap_lines_pos = 0.5 * (target_disps[1:] + target_disps[:-1]) * GAP_MESHING_TOL
1827+
relative_snap_lines_pos = (
1828+
0.5 * (target_disps[1:] + target_disps[:-1]) * GAP_MESHING_TOL
1829+
)
17821830
# convert relative positions to absolute ones
1783-
snapping_lines_y += [grid_y_coords[ind_j] + rel_pos * (grid_y_coords[ind_j + 1] - grid_y_coords[ind_j]) for rel_pos in relative_snap_lines_pos]
1831+
snapping_lines_y += [
1832+
grid_y_coords[ind_j]
1833+
+ rel_pos * (grid_y_coords[ind_j + 1] - grid_y_coords[ind_j])
1834+
for rel_pos in relative_snap_lines_pos
1835+
]
17841836

17851837
# compute minimal gap/strip width
1786-
min_gap_width_current = np.min(target_disps[1:] - target_disps[:-1]) * GAP_MESHING_TOL
1787-
min_gap_width = min(min_gap_width, min_gap_width_current * (grid_y_coords[ind_j + 1] - grid_y_coords[ind_j]))
1838+
min_gap_width_current = (
1839+
np.min(target_disps[1:] - target_disps[:-1]) * GAP_MESHING_TOL
1840+
)
1841+
min_gap_width = min(
1842+
min_gap_width,
1843+
min_gap_width_current * (grid_y_coords[ind_j + 1] - grid_y_coords[ind_j]),
1844+
)
17881845

17891846
return snapping_lines_y, min_gap_width
17901847

17911848
def _resolve_gaps(self, structures: List, grid: Grid) -> Tuple[List[CoordinateOptional], float]:
17921849
"""Detect underresolved gaps and place snapping lines in them. Also return the detected minimal gap width."""
17931850

17941851
# get merged pec structures on plane
1852+
# note that we expect this function to also convert all LossyMetal's into PEC
17951853
plane_slice = CornerFinderSpec._merged_pec_on_plane(
17961854
coord=self.center_axis, normal_axis=self.axis, structure_list=structures
17971855
)
@@ -1810,16 +1868,28 @@ def _resolve_gaps(self, structures: List, grid: Grid) -> Tuple[List[CoordinateOp
18101868
v_cells_ij, v_cells_dy, h_cells_ij, h_cells_dx = self._process_slice(x, y, plane_slice)
18111869

18121870
# generate horizontal snapping lines
1813-
snapping_lines_y, min_gap_width_along_y = self._generate_horizontal_snapping_lines(y, v_cells_ij, v_cells_dy)
1871+
snapping_lines_y, min_gap_width_along_y = self._generate_horizontal_snapping_lines(
1872+
y, v_cells_ij, v_cells_dy
1873+
)
1874+
detected_gap_width = min_gap_width_along_y
18141875

18151876
# generate vertical snapping lines
1816-
snapping_lines_x, min_gap_width_along_x = self._generate_horizontal_snapping_lines(x, np.roll(h_cells_ij, shift=1, axis=1), h_cells_dx)
1877+
if len(h_cells_ij) > 0: # check, otherwise np.roll fails
1878+
snapping_lines_x, min_gap_width_along_x = self._generate_horizontal_snapping_lines(
1879+
x, np.roll(h_cells_ij, shift=1, axis=1), h_cells_dx
1880+
)
18171881

1818-
detected_gap_width = min(min_gap_width_along_y, min_gap_width_along_x)
1882+
detected_gap_width = min(detected_gap_width, min_gap_width_along_x)
1883+
else:
1884+
snapping_lines_x = []
18191885

18201886
# convert snapping lines' coordinates into 3d coordinates
1821-
snapping_lines_y_3d = [Box.unpop_axis(Y, (None, None), axis=tan_dims[1]) for Y in snapping_lines_y]
1822-
snapping_lines_x_3d = [Box.unpop_axis(X, (None, None), axis=tan_dims[0]) for X in snapping_lines_x]
1887+
snapping_lines_y_3d = [
1888+
Box.unpop_axis(Y, (None, None), axis=tan_dims[1]) for Y in snapping_lines_y
1889+
]
1890+
snapping_lines_x_3d = [
1891+
Box.unpop_axis(X, (None, None), axis=tan_dims[0]) for X in snapping_lines_x
1892+
]
18231893

18241894
return snapping_lines_x_3d + snapping_lines_y_3d, detected_gap_width
18251895

@@ -2405,7 +2475,7 @@ def _make_grid_one_iteration(
24052475
If `None`, recomputes internal snapping points.
24062476
dl_min_from_gaps : pd.PositiveFloat
24072477
Minimal grid size computed based on autodetected gaps.
2408-
2478+
24092479
24102480
Returns
24112481
-------

tidy3d/components/simulation.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -944,7 +944,12 @@ def plot_grid(
944944

945945
# Plot snapping points
946946
for points, plot_param in zip(
947-
[self.internal_snapping_points, self.grid_spec.snapping_points, self._gap_meshing_snapping_lines], plot_params
947+
[
948+
self.internal_snapping_points,
949+
self.grid_spec.snapping_points,
950+
self._gap_meshing_snapping_lines,
951+
],
952+
plot_params,
948953
):
949954
for point in points:
950955
_, (x_point, y_point) = Geometry.pop_axis(point, axis=axis)
@@ -1161,7 +1166,6 @@ def grid(self) -> Grid:
11611166
# This would AutoGrid the in-plane directions of the 2D materials
11621167
# return self._grid_corrections_2dmaterials(grid)
11631168
return grid
1164-
11651169

11661170
@cached_property
11671171
def _gap_meshing_snapping_lines(self) -> List[CoordinateOptional]:

0 commit comments

Comments
 (0)