19
19
from ..medium import AnisotropicMedium , Medium2D , PECMedium
20
20
from ...exceptions import SetupError , ValidationError
21
21
from ...constants import C_0 , fp_eps
22
+ from ...log import log
22
23
23
24
_ROOTS_TOL = 1e-10
24
25
26
+ # Shrink min_step a little so that if e.g. a structure has target dl = 0.1 and a width of 0.1,
27
+ # a grid point will be added on both sides of the structure. Without this factor, the mesher
28
+ # ``is_close`` check will deem that the second point is too close.
29
+ MIN_STEP_SCALE = 0.9999
30
+
25
31
26
32
class Mesher (Tidy3dBaseModel , ABC ):
27
33
"""Abstract class for automatic meshing."""
@@ -122,7 +128,7 @@ def parse_structures(
122
128
structures_ordered , wavelength , min_steps_per_wvl , dl_min , axis
123
129
)
124
130
# Smallest of the maximum steps
125
- min_step = np .amin (structure_steps )
131
+ min_step = MIN_STEP_SCALE * np .amin (structure_steps )
126
132
127
133
# If empty simulation, return
128
134
if len (structures ) == 1 :
@@ -135,40 +141,74 @@ def parse_structures(
135
141
tree = self .bounds_2d_tree (struct_bbox )
136
142
137
143
intervals = {"coords" : list (domain_bounds ), "structs" : [[]]}
138
- # Iterate in reverse order as latter structures override earlier ones. To properly handle
139
- # containment then we need to populate interval coordinates starting from the top.
140
- # If a structure is found to be completely contained, the corresponding ``struct_bbox`` is
141
- # set to ``None``.
142
- for str_ind in range (len (structures_ordered ) - 1 , - 1 , - 1 ):
143
- # 3D and 2D bounding box of current structure
144
- bbox = struct_bbox [str_ind ]
145
- if bbox is None :
146
- # Structure has been removed because it is completely contained
147
- continue
148
- bbox_2d = shapely_box (bbox [0 , 0 ], bbox [0 , 1 ], bbox [1 , 0 ], bbox [1 , 1 ])
149
-
150
- # List of structure indexes that may intersect the current structure in 2D
151
- try :
152
- query_inds = tree .query_items (bbox_2d )
153
- except AttributeError :
154
- query_inds = tree .query (bbox_2d )
155
-
156
- # Remove all lower structures that the current structure completely contains
157
- inds_lower = [
158
- ind for ind in query_inds if ind < str_ind and struct_bbox [ind ] is not None
159
- ]
160
- query_bbox = [struct_bbox [ind ] for ind in inds_lower ]
161
- bbox_contains_inds = self .contains_3d (bbox , query_bbox )
162
- for ind in bbox_contains_inds :
163
- struct_bbox [inds_lower [ind ]] = None
164
-
165
- # List of structure bboxes that contain the current structure in 2D
166
- inds_upper = [ind for ind in query_inds if ind > str_ind ]
167
- query_bbox = [struct_bbox [ind ] for ind in inds_upper if struct_bbox [ind ] is not None ]
168
- bbox_contained_2d = self .contained_2d (bbox , query_bbox )
169
-
170
- # Handle insertion of the current structure bounds in the intervals
171
- intervals = self .insert_bbox (intervals , str_ind , bbox , bbox_contained_2d , min_step )
144
+ """ Build the ``intervals`` dictionary. ``intervals["coords"]`` gets populated based on the
145
+ bounding boxes of all structures in the list (some filtering is done to exclude points that
146
+ will be too close together compared to the absolute lower desired step). At every point, the
147
+ ``"structs"`` list has length one lower than the ``"coords"`` list, and each element is
148
+ another list of all structure indexes that are found in the interval formed by ``coords[i]``
149
+ and ``coords[i + 1]``. This only includes structures that have a physical presence in the
150
+ interval, i.e. it excludes structures that are completely covered by higher-up ones.
151
+
152
+ To build this, we iterate in reverse order as latter structures override earlier ones.
153
+ We also handle containment in the following way (note - we work with bounding boxes only):
154
+ 1. If a structure that is lower in the list than the current structure is found to be
155
+ completely contained in 3D, the corresponding ``struct_bbox`` is immediately set to
156
+ ``None`` and nothing more will be done using that structure.
157
+ 2. If the current structure is covering an interval but there's a higher-up structure that
158
+ contains it in 2D and also covers the same interval, then it will not be added to the
159
+ ``intervals["structs"] list for that interval.
160
+ 3. If the current structure is found to not cover any interval, its bounding box
161
+ is set to ``None``, so that it will not affect structures that lie below it as per point
162
+ 2. A warning is also raised since the structure will have an unpredictable effect on the
163
+ material coefficients used in the simulation.
164
+ """
165
+
166
+ with log :
167
+ for str_ind in range (len (structures_ordered ) - 1 , - 1 , - 1 ):
168
+ # 3D and 2D bounding box of current structure
169
+ bbox = struct_bbox [str_ind ]
170
+ if bbox is None :
171
+ # Structure has been removed because it is completely contained
172
+ continue
173
+ bbox_2d = shapely_box (bbox [0 , 0 ], bbox [0 , 1 ], bbox [1 , 0 ], bbox [1 , 1 ])
174
+
175
+ # List of structure indexes that may intersect the current structure in 2D
176
+ try :
177
+ query_inds = tree .query_items (bbox_2d )
178
+ except AttributeError :
179
+ query_inds = tree .query (bbox_2d )
180
+
181
+ # Remove all lower structures that the current structure completely contains
182
+ inds_lower = [
183
+ ind for ind in query_inds if ind < str_ind and struct_bbox [ind ] is not None
184
+ ]
185
+ query_bbox = [struct_bbox [ind ] for ind in inds_lower ]
186
+ bbox_contains_inds = self .contains_3d (bbox , query_bbox )
187
+ for ind in bbox_contains_inds :
188
+ struct_bbox [inds_lower [ind ]] = None
189
+
190
+ # List of structure bboxes that contain the current structure in 2D
191
+ inds_upper = [ind for ind in query_inds if ind > str_ind ]
192
+ query_bbox = [
193
+ struct_bbox [ind ] for ind in inds_upper if struct_bbox [ind ] is not None
194
+ ]
195
+ bbox_contained_2d = self .contained_2d (bbox , query_bbox )
196
+
197
+ # Handle insertion of the current structure bounds in the intervals
198
+ # The intervals list is modified in-place
199
+ too_small = self .insert_bbox (intervals , str_ind , bbox , bbox_contained_2d , min_step )
200
+ if too_small and (bbox [1 , 2 ] - bbox [0 , 2 ]) > 0 :
201
+ # If the structure is too small (but not 0D), issue a warning
202
+ log .warning (
203
+ f"A structure has a nonzero dimension along axis { 'xyz' [axis ]} , which "
204
+ "is however too small compared to the generated mesh step along that "
205
+ "direction. This could produce unpredictable results. We recommend "
206
+ "increasing the resolution, or adding a mesh override structure to ensure "
207
+ "that all geometries are at least one pixel thick along all dimensions."
208
+ )
209
+ # Also remove this structure from the bbox list so that lower structures
210
+ # will not be affected by it, as it was not added to any interval.
211
+ struct_bbox [str_ind ] = None
172
212
173
213
# Truncate intervals to domain bounds
174
214
coords = np .array (intervals ["coords" ])
@@ -226,17 +266,29 @@ def insert_bbox(
226
266
List of 3D bounding boxes that contain the current structure in 2D.
227
267
min_step : float
228
268
Absolute minimum interval size to impose.
269
+
270
+ Returns
271
+ -------
272
+ structure_too_small : bool
273
+ True if the structure did not span any interval after coordinates were inserted. This
274
+ would happen if the structure size is too small compared to the minimum step.
275
+
276
+ Note
277
+ ----
278
+ This function modifies ``intervals`` in-place.
229
279
"""
230
280
231
281
coords = intervals ["coords" ]
232
282
structs = intervals ["structs" ]
233
283
284
+ min_step_check = MIN_STEP_SCALE * min_step
285
+
234
286
# Left structure bound
235
287
bound_coord = str_bbox [0 , 2 ]
236
288
indsmin = np .nonzero (bound_coord <= coords )[0 ]
237
- indmin = int (indsmin [0 ]) # coordinate is in interval index ``indmin - 1````
238
- is_close_l = self .is_close (bound_coord , coords , indmin - 1 , min_step )
239
- is_close_r = self .is_close (bound_coord , coords , indmin , min_step )
289
+ indmin = int (indsmin [0 ]) # coordinate is in interval index ``indmin - 1``
290
+ is_close_l = self .is_close (bound_coord , coords , indmin - 1 , min_step_check )
291
+ is_close_r = self .is_close (bound_coord , coords , indmin , min_step_check )
240
292
is_contained = self .is_contained (bound_coord , bbox_contained_2d )
241
293
242
294
# Decide on whether coordinate should be inserted or indmin modified
@@ -254,8 +306,8 @@ def insert_bbox(
254
306
bound_coord = str_bbox [1 , 2 ]
255
307
indsmax = np .nonzero (bound_coord >= coords )[0 ]
256
308
indmax = int (indsmax [- 1 ]) # coordinate is in interval index ``indmax``
257
- is_close_l = self .is_close (bound_coord , coords , indmax , min_step )
258
- is_close_r = self .is_close (bound_coord , coords , indmax + 1 , min_step )
309
+ is_close_l = self .is_close (bound_coord , coords , indmax , min_step_check )
310
+ is_close_r = self .is_close (bound_coord , coords , indmax + 1 , min_step_check )
259
311
is_contained = self .is_contained (bound_coord , bbox_contained_2d )
260
312
261
313
# Decide on whether coordinate should be inserted or indmax modified
@@ -278,7 +330,7 @@ def insert_bbox(
278
330
if not self .is_contained (mid_coord , bbox_contained_2d ):
279
331
structs [interval_ind ].append (str_ind )
280
332
281
- return { "coords" : coords , "structs" : structs }
333
+ return indmin >= indmax
282
334
283
335
@staticmethod
284
336
def reorder_structures_enforced_to_end (
@@ -501,7 +553,7 @@ def filter_min_step(
501
553
coords_filter = [interval_coords [0 ]]
502
554
steps_filter = []
503
555
for coord_ind , coord in enumerate (interval_coords [1 :]):
504
- if coord - coords_filter [- 1 ] > min_step :
556
+ if coord - coords_filter [- 1 ] >= min_step :
505
557
coords_filter .append (coord )
506
558
steps_filter .append (max_steps [coord_ind ])
507
559
0 commit comments