From 40e517860335ab7937522acf74c8b2987e1213a3 Mon Sep 17 00:00:00 2001 From: Weiliang Jin Date: Fri, 17 Jan 2025 13:59:09 -0800 Subject: [PATCH] Allow MeshOverrideStructure to unshadow structures in overlapping region --- CHANGELOG.md | 5 +-- tests/test_components/test_meshgenerate.py | 38 ++++++++++++++++++++++ tidy3d/components/grid/mesher.py | 35 +++++++++++++------- tidy3d/components/structure.py | 17 +++++++++- 4 files changed, 81 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20e95c814..2a2346a25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `VisualizationSpec` that allows `Medium` instances to specify color and transparency plotting attributes that override default ones. - `reduce_simulation` argument added to all web functions to allow automatically reducing structures only to the simulation domain, including truncating data in custom media, thus reducing the simulation upload size. Currently only implemented for mode solver simulation types. +- Support for quasi-uniform grid specifications via `QuasiUniformGrid` that subclasses from `GridSpec1d`. The grids are almost uniform, but can adjust locally to the edge of structure bounding boxes, and snapping points. +- New field `min_steps_per_sim_size` in `AutoGrid` that sets minimal number of grid steps per longest edge length of simulation domain. +- New field `shadow` in `MeshOverrideStructure` that sets grid size in overlapping region according to structure list or minimal grid size. ### Changed - `ModeMonitor` and `ModeSolverMonitor` now use the default `td.ModeSpec()` with `num_modes=1` when `mode_spec` is not provided. @@ -17,8 +20,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - NumPy 2.1 compatibility issue where `numpy.float64` values passed to xarray interpolation would raise TypeError. -- Support for quasi-uniform grid specifications via `QuasiUniformGrid` that subclasses from `GridSpec1d`. The grids are almost uniform, but can adjust locally to the edge of structure bounding boxes, and snapping points. -- New field `min_steps_per_sim_size` in `AutoGrid` that sets minimal number of grid steps per longest edge length of simulation domain. - Validation error in inverse design plugin when simulation has no sources by adding a source existence check before validating pixel size. - System-dependent floating-point precision issue in EMEGrid validation. - Fixed magnitude of gradient computation in `CustomMedium` by accounting properly for full volume element when permittivity data is defined over less dimensions than the medium. diff --git a/tests/test_components/test_meshgenerate.py b/tests/test_components/test_meshgenerate.py index 590cc608d..8d7e0d942 100644 --- a/tests/test_components/test_meshgenerate.py +++ b/tests/test_components/test_meshgenerate.py @@ -817,3 +817,41 @@ def test_override_are_box(): assert isinstance( override_not_box.geometry, td.Box ), "Sphere override structure was not converted to Box" + + +def test_override_unshadowed(): + """Test that when an override structure completely covers a structure of smaller grid size, it overrides + the grid size with `shadow=True`; but not with `shadow=False`. + """ + + sim = td.Simulation( + size=(3, 3, 6), + grid_spec=td.GridSpec.auto(wavelength=WAVELENGTH), + run_time=1e-13, + structures=[ + BOX1, + ], + ) + + # override structure of same geometry and shadow = True will override even it has larger grid size + override_structure = td.MeshOverrideStructure( + geometry=BOX1.geometry, + dl=[ + 0.2, + ] + * 3, + shadow=True, + ) + sim_shadow = sim.updated_copy( + grid_spec=td.GridSpec.auto(wavelength=WAVELENGTH, override_structures=[override_structure]) + ) + sizes = sim_shadow.grid.sizes.to_list[2] + assert sizes[len(sizes) // 2] > 0.19 + + # now shadow = False + override_structure = override_structure.updated_copy(shadow=False) + sim_unshadow = sim.updated_copy( + grid_spec=td.GridSpec.auto(wavelength=WAVELENGTH, override_structures=[override_structure]) + ) + sizes = sim_unshadow.grid.sizes.to_list[2] + assert sizes[len(sizes) // 2] < 0.1 diff --git a/tidy3d/components/grid/mesher.py b/tidy3d/components/grid/mesher.py index 2015fc4aa..cc6bcd8ab 100644 --- a/tidy3d/components/grid/mesher.py +++ b/tidy3d/components/grid/mesher.py @@ -224,6 +224,12 @@ def parse_structures( structures_effective ) + # no containment check for those structures + skip_containment = [ + isinstance(structure, MeshOverrideStructure) and not structure.shadow + for structure in structures_ordered + ] + # Required maximum steps in every structure structure_steps = self.structure_steps( structures_ordered, wavelength, min_steps_per_wvl, dl_min, dl_max, axis @@ -271,13 +277,16 @@ def parse_structures( if bbox is None: # Structure has been removed because it is completely contained continue - bbox_2d = self.make_shapely_box(bbox) - # List of structure indexes that may intersect the current structure in 2D - try: - query_inds = tree.query_items(bbox_2d) - except AttributeError: - query_inds = tree.query(bbox_2d) + query_inds = [] + if not skip_containment[str_ind]: + bbox_2d = self.make_shapely_box(bbox) + + # List of structure indexes that may intersect the current structure in 2D + try: + query_inds = tree.query_items(bbox_2d) + except AttributeError: + query_inds = tree.query(bbox_2d) # Remove all lower structures that the current structure completely contains inds_lower = [ @@ -289,11 +298,15 @@ def parse_structures( struct_bbox[inds_lower[ind]] = None # List of structure bboxes that contain the current structure in 2D - inds_upper = [ind for ind in query_inds if ind > str_ind] - query_bbox = [ - struct_bbox[ind] for ind in inds_upper if struct_bbox[ind] is not None - ] - bbox_contained_2d = self.contained_2d(bbox, query_bbox) + bbox_contained_2d = [] + if not skip_containment[str_ind]: + inds_upper = [ind for ind in query_inds if ind > str_ind] + query_bbox = [ + struct_bbox[ind] + for ind in inds_upper + if struct_bbox[ind] is not None and not skip_containment[ind] + ] + bbox_contained_2d = self.contained_2d(bbox, query_bbox) # Handle insertion of the current structure bounds in the intervals # The intervals list is modified in-place diff --git a/tidy3d/components/structure.py b/tidy3d/components/structure.py index 685aecd7f..440eba5d8 100644 --- a/tidy3d/components/structure.py +++ b/tidy3d/components/structure.py @@ -665,13 +665,21 @@ class MeshOverrideStructure(AbstractStructure): enforce: bool = pydantic.Field( False, - title="Enforce grid size", + title="Enforce Grid Size", description="If ``True``, enforce the grid size setup inside the structure " "even if the structure is inside a structure of smaller grid size. In the intersection " "region of multiple structures of ``enforce=True``, grid size is decided by " "the last added structure of ``enforce=True``.", ) + shadow: bool = pydantic.Field( + True, + title="Grid Size Choice In Structure Overlapping Region", + description="In structure intersection region, grid size is decided by the latter added " + "structure in the structure list when ``shadow=True``; or the structure of smaller grid size " + "when ``shadow=False``.", + ) + @pydantic.validator("geometry") def _box_only(cls, val): """Ensure this is a box.""" @@ -684,5 +692,12 @@ def _box_only(cls, val): return val.bounding_box return val + @pydantic.validator("shadow") + def _unshadowed_cannot_be_enforced(cls, val, values): + """Unshadowed structure cannot be enforced.""" + if not val and values["enforce"]: + raise SetupError("A structure cannot be simultaneously enforced and unshadowed.") + return val + StructureType = Union[Structure, MeshOverrideStructure]