From f06eecccdb80bbbd050f53f7196961c7b0b758d9 Mon Sep 17 00:00:00 2001 From: Weiliang Jin Date: Thu, 10 Apr 2025 15:04:38 -0700 Subject: [PATCH 1/3] New field priority in Structure and structure_priority_mode in Scene --- CHANGELOG.md | 2 + tests/test_components/test_scene.py | 45 ++++++++++++++ tidy3d/components/grid/grid_spec.py | 25 +++++--- tidy3d/components/lumped_element.py | 2 + tidy3d/components/scene.py | 40 ++++++++++--- tidy3d/components/simulation.py | 21 ++++--- tidy3d/components/structure.py | 59 ++++++++++++++++++- .../components/tcad/simulation/heat_charge.py | 4 +- tidy3d/components/types.py | 1 + .../plugins/adjoint/components/simulation.py | 4 +- 10 files changed, 174 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index af3df42deb..5cea030718 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Performance enhancement for adjoint gradient calculations by optimizing field interpolation. +### Added +- `priority` field in `Structure` and `MeshOverrideStructure` for setting the behavior in structure overlapping region. When its value is `None`, the priority is automatically decided based on the material property and simulation's `structure_priority_mode`. ## [2.8.2] - 2025-04-09 diff --git a/tests/test_components/test_scene.py b/tests/test_components/test_scene.py index 5e3f6fc893..68738427b9 100644 --- a/tests/test_components/test_scene.py +++ b/tests/test_components/test_scene.py @@ -290,3 +290,48 @@ def test_max_geometry_validation(): ] with pytest.raises(pd.ValidationError, match=f" {MAX_GEOMETRY_COUNT + 2} "): _ = td.Scene(structures=not_fine) + + +def test_structure_manual_priority(): + """make sure structure is properly orderd based on the priority settings.""" + + box = td.Structure( + geometry=td.Box(size=(1, 1, 1), center=(0, 0, 0)), + medium=td.Medium(permittivity=2.0), + ) + structures = [] + priorities = [2, 4, -1, -4, 0] + for priority in priorities: + structures.append(box.updated_copy(priority=priority)) + scene = td.Scene( + structures=structures, + ) + + sorted_priorities = [s.priority for s in scene.sorted_structures] + assert all(np.diff(sorted_priorities) >= 0) + + +def test_structure_automatic_priority(): + """make sure metallic structure has the highest priority in `conductor` mode.""" + + box = td.Structure( + geometry=td.Box(size=(1, 1, 1), center=(0, 0, 0)), + medium=td.Medium(permittivity=2.0), + ) + box_pec = box.updated_copy(medium=td.PEC) + box_lossymetal = box.updated_copy( + medium=td.LossyMetalMedium(conductivity=1.0, frequency_range=(1e14, 2e14)) + ) + structures = [box_pec, box_lossymetal, box] + scene = td.Scene( + structures=structures, + structure_priority_mode="equal", + ) + + # in equal mode, the order is preserved + scene.sorted_structures == structures + + # conductor mode + scene = scene.updated_copy(structure_priority_mode="conductor") + assert scene.sorted_structures[-1].medium == td.PEC + assert isinstance(scene.sorted_structures[-2].medium, td.LossyMetalMedium) diff --git a/tidy3d/components/grid/grid_spec.py b/tidy3d/components/grid/grid_spec.py index 2a5b9ff5e6..65c9f24ea4 100644 --- a/tidy3d/components/grid/grid_spec.py +++ b/tidy3d/components/grid/grid_spec.py @@ -22,6 +22,7 @@ Axis, Coordinate, CoordinateOptional, + PriorityMode, Symmetry, annotate_type, ) @@ -940,6 +941,7 @@ def override_structure( dl=dl_list, shadow=False, drop_outside_sim=drop_outside_sim, + priority=-1, ) @@ -1397,6 +1399,7 @@ def _override_structures_along_axis( dl=self._unpop_axis(ax_coord=dl, plane_coord=None), shadow=False, drop_outside_sim=self.refinement_inside_sim_only, + priority=-1, ) ) @@ -1729,10 +1732,11 @@ def all_override_structures( wavelength: pd.PositiveFloat, sim_size: Tuple[float, 3], lumped_elements: List[LumpedElementType], + structure_priority_mode: PriorityMode = "equal", internal_override_structures: List[MeshOverrideStructure] = None, ) -> List[StructureType]: - """Internal and external mesh override structures. External override structures take higher priority. - So far, internal override structures all come from `layer_refinement_specs`. + """Internal and external mesh override structures sorted based on their priority. By default, + the priority of internal override structures is -1, and 0 for external ones. Parameters ---------- @@ -1744,22 +1748,23 @@ def all_override_structures( Simulation domain size. lumped_elements : List[LumpedElementType] List of lumped elements. + structure_priority_mode : PriorityMode + Structure priority setting. internal_override_structures : List[MeshOverrideStructure] If `None`, recomputes internal override structures. Returns ------- List[StructureType] - List of override structures. + List of sorted override structures. """ if internal_override_structures is None: - return ( - self.internal_override_structures(structures, wavelength, sim_size, lumped_elements) - + self.external_override_structures + internal_override_structures = self.internal_override_structures( + structures, wavelength, sim_size, lumped_elements ) - - return internal_override_structures + self.external_override_structures + all_structures = internal_override_structures + self.external_override_structures + return Structure._sort_structures(all_structures, structure_priority_mode) def _min_vacuum_dl_in_autogrid(self, wavelength: float, sim_size: Tuple[float, 3]) -> float: """Compute grid step size in vacuum for Autogrd. If AutoGrid is applied along more than 1 dimension, @@ -1829,6 +1834,7 @@ def make_grid( lumped_elements: List[LumpedElementType] = (), internal_override_structures: List[MeshOverrideStructure] = None, internal_snapping_points: List[CoordinateOptional] = None, + structure_priority_mode: PriorityMode = "equal", ) -> Grid: """Make the entire simulation grid based on some simulation parameters. @@ -1850,6 +1856,8 @@ def make_grid( If `None`, recomputes internal override structures. internal_snapping_points : List[CoordinateOptional] If `None`, recomputes internal snapping points. + structure_priority_mode : PriorityMode + Structure priority setting. Returns ------- @@ -1912,6 +1920,7 @@ def make_grid( wavelength, sim_size, lumped_elements, + structure_priority_mode, internal_override_structures, ) diff --git a/tidy3d/components/lumped_element.py b/tidy3d/components/lumped_element.py index 45da334a25..0ea4fa405e 100644 --- a/tidy3d/components/lumped_element.py +++ b/tidy3d/components/lumped_element.py @@ -193,6 +193,7 @@ def to_mesh_overrides(self) -> list[MeshOverrideStructure]: geometry=Box(center=self.center, size=override_size), dl=(dl, dl, dl), shadow=False, + priority=-1, ) ] @@ -398,6 +399,7 @@ def to_mesh_overrides(self) -> list[MeshOverrideStructure]: geometry=Box(center=self.center, size=override_size), dl=override_dl, shadow=False, + priority=-1, ) ] diff --git a/tidy3d/components/scene.py b/tidy3d/components/scene.py index cbde1408dd..f66b6f9d4c 100644 --- a/tidy3d/components/scene.py +++ b/tidy3d/components/scene.py @@ -54,6 +54,7 @@ InterpMethod, LengthUnit, PermittivityComponent, + PriorityMode, Shapely, Size, ) @@ -105,8 +106,20 @@ class Scene(Tidy3dBaseModel): (), title="Structures", description="Tuple of structures present in scene. " - "Note: Structures defined later in this list override the " - "simulation material properties in regions of spatial overlap.", + "Note: In regions of spatial overlap between structures, " + "material properties are dictated by structure of higher priority. " + "The priority for structure of `priority=None` is set automatically " + "based on `structure_priority_mode`. For structures of equal priority, " + "the structure added later to the structure list takes precedence.", + ) + + structure_priority_mode: PriorityMode = pd.Field( + "equal", + title="Structure Priority Setting", + description="This field only affects structures of `priority=None`. " + "If `equal`, the priority of those structures is set to 0; if `conductor`, " + "the priority of structures made of `LossyMetalMedium` is set to 90, " + "`PECMedium` to 100, and others to 0.", ) plot_length_units: Optional[LengthUnit] = pd.Field( @@ -244,6 +257,17 @@ def medium_map(self) -> Dict[StructureMediumType, pd.NonNegativeInt]: return {medium: index for index, medium in enumerate(self.mediums)} + @cached_property + def sorted_structures(self) -> List[Structure]: + """Returns a list of sorted structures based on their priority.In the sorted list, + latter added structures take higher priority. + + Returns + ------- + List[:class:`.Structure`] + """ + return Structure._sort_structures(self.structures, self.structure_priority_mode) + @cached_property def background_structure(self) -> Structure: """Returns structure representing the background of the :class:`.Scene`.""" @@ -253,7 +277,7 @@ def background_structure(self) -> Structure: @cached_property def all_structures(self) -> List[Structure]: """List of all structures in the simulation including the background.""" - return [self.background_structure] + list(self.structures) + return [self.background_structure] + list(self.sorted_structures) @staticmethod def intersecting_media( @@ -442,7 +466,7 @@ def plot_structures( """ medium_shapes = self._get_structures_2dbox( - structures=self.to_static().structures, x=x, y=y, z=z, hlim=hlim, vlim=vlim + structures=self.to_static().sorted_structures, x=x, y=y, z=z, hlim=hlim, vlim=vlim ) medium_map = self.medium_map for medium, shape in medium_shapes: @@ -888,7 +912,7 @@ def plot_structures_property( The supplied or created matplotlib axes. """ - structures = self.structures + structures = self.sorted_structures # alpha is None just means plot without any transparency if alpha is None: @@ -1459,7 +1483,7 @@ def plot_structures_heat_charge_property( The supplied or created matplotlib axes. """ - structures = self.structures + structures = self.sorted_structures # alpha is None just means plot without any transparency if alpha is None: @@ -1737,7 +1761,7 @@ def perturbed_mediums_copy( """ scene_dict = self.dict() - structures = self.structures + structures = self.sorted_structures array_dict = { "temperature": temperature, "electron_density": electron_density, @@ -1788,7 +1812,7 @@ def doping_bounds(self): acceptors_lims = [1e50, -1e50] donors_lims = [1e50, -1e50] - for struct in [self.background_structure] + list(self.structures): + for struct in [self.background_structure] + list(self.sorted_structures): if isinstance(struct.medium.charge, SemiconductorMedium): electric_spec = struct.medium.charge for doping, limits in zip( diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index 941ceaa9c6..0792e6048a 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -922,10 +922,16 @@ def plot_grid( plot_params[0] = plot_params[0].include_kwargs(edgecolor=kwargs["colors_internal"]) if self.grid_spec.auto_grid_used: + # Internal and external override structures are visualized with different colors, + # so let's not sort them together. all_override_structures = [ - self.internal_override_structures, - self.grid_spec.external_override_structures, + Structure._sort_structures(structures, self.scene.structure_priority_mode) + for structures in [ + self.internal_override_structures, + self.grid_spec.external_override_structures, + ] ] + for structures, plot_param in zip(all_override_structures, plot_params): for structure in structures: bounds = list(zip(*structure.geometry.bounds)) @@ -1140,6 +1146,7 @@ def grid(self) -> Grid: lumped_elements=self.lumped_elements, internal_snapping_points=self.internal_snapping_points, internal_override_structures=self.internal_override_structures, + structure_priority_mode=self.scene.structure_priority_mode, ) # This would AutoGrid the in-plane directions of the 2D materials @@ -1149,7 +1156,7 @@ def grid(self) -> Grid: @cached_property def static_structures(self) -> list[Structure]: """Structures in simulation with all autograd tracers removed.""" - return [structure.to_static() for structure in self.structures] + return [structure.to_static() for structure in self.scene.sorted_structures] @cached_property def num_cells(self) -> int: @@ -1450,7 +1457,7 @@ def _volumetric_structures_grid(self, grid: Grid) -> Tuple[Structure]: not any(isinstance(medium, Medium2D) for medium in self.scene.mediums) and not self.lumped_elements ): - return self.structures + return self.scene.sorted_structures def get_dls(geom: Geometry, axis: Axis, num_dls: int) -> List[float]: """Get grid size around the 2D material.""" @@ -4431,7 +4438,7 @@ def to_gdstk( clip = gdstk.rectangle(bmin, bmax) polygons = [] - for structure in self.structures: + for structure in self.scene.sorted_structures: gds_layer, gds_dtype = gds_layer_dtype_map.get(structure.medium, (0, 0)) for polygon in structure.to_gdstk( x=x, @@ -4494,7 +4501,7 @@ def to_gdspy( clip = gdspy.Rectangle(bmin, bmax) polygons = [] - for structure in self.structures: + for structure in self.scene.sorted_structures: gds_layer, gds_dtype = gds_layer_dtype_map.get(structure.medium, (0, 0)) for polygon in structure.to_gdspy( x=x, @@ -4976,7 +4983,7 @@ def custom_datasets(self) -> List[Dataset]: ] datasets_geometry = [] - for struct in self.structures: + for struct in self.scene.sorted_structures: for geometry in traverse_geometries(struct.geometry): if isinstance(geometry, TriangleMesh): datasets_geometry += [geometry.mesh_dataset] diff --git a/tidy3d/components/structure.py b/tidy3d/components/structure.py index 9cf8724dd8..5ab6304738 100644 --- a/tidy3d/components/structure.py +++ b/tidy3d/components/structure.py @@ -4,7 +4,8 @@ import pathlib from collections import defaultdict -from typing import Optional, Tuple, Union +from functools import cmp_to_key +from typing import List, Optional, Tuple, Union import autograd.numpy as anp import numpy as np @@ -24,9 +25,9 @@ from .geometry.utils import GeometryType, validate_no_transformed_polyslabs from .grid.grid import Coords from .material.types import StructureMediumType -from .medium import AbstractCustomMedium, CustomMedium, Medium, Medium2D +from .medium import AbstractCustomMedium, CustomMedium, LossyMetalMedium, Medium, Medium2D from .monitor import FieldMonitor, PermittivityMonitor -from .types import TYPE_TAG_STR, Ax, Axis +from .types import TYPE_TAG_STR, Ax, Axis, PriorityMode from .validators import validate_name_str from .viz import add_ax_if_none, equal_aspect @@ -75,6 +76,16 @@ class AbstractStructure(Tidy3dBaseModel): "``Simulation`` by default to compute the shape derivatives.", ) + priority: int = pydantic.Field( + None, + title="Priority", + description="Priority of the structure applied in structure overlapping region. " + "The material property in the overlapping region is dictated by the structure " + "of higher priority. For structures of equal priority, " + "the structure added later to the structure list takes precedence. When `priority` is None, " + "the value is automatically assigned based on `structure_priority_mode` in the `Simulation`.", + ) + @pydantic.root_validator(skip_on_failure=True) def _handle_background_mediums(cls, values): """Handle background medium combinations, including deprecation.""" @@ -110,6 +121,27 @@ def _transformed_slanted_polyslabs_not_allowed(cls, val): validate_no_transformed_polyslabs(val) return val + def _priority(self, priority_mode: PriorityMode) -> int: + """Priority of this structure. The priority value is set automatically based on `priority_modes, + if its original value is `None`. + """ + if self.priority is not None: + return self.priority + return 0 + + @staticmethod + def _sort_structures( + structures: List[StructureType], structure_priority_mode: PriorityMode + ) -> List[StructureType]: + """Sort structure lists based on their priority values in ascending order.""" + + def structure_comparator(struct1, struct2): + return struct1._priority(structure_priority_mode) - struct2._priority( + structure_priority_mode + ) + + return sorted(structures, key=cmp_to_key(structure_comparator)) + @property def viz_spec(self): return None @@ -189,6 +221,20 @@ class Structure(AbstractStructure): discriminator=TYPE_TAG_STR, ) + def _priority(self, priority_mode: PriorityMode) -> int: + """Priority of this structure. The priority value is set automatically based on `priority_modes, + if its original value is `None`. + """ + if self.priority is not None: + return self.priority + + if priority_mode == "conductor": + if self.medium.is_pec: + return 100 + if isinstance(self.medium, LossyMetalMedium): + return 90 + return 0 + @property def viz_spec(self): return self.medium.viz_spec @@ -670,6 +716,13 @@ class MeshOverrideStructure(AbstractStructure): units=MICROMETER, ) + priority: int = pydantic.Field( + 0, + title="Priority", + description="Priority of the structure applied in mesh override structure overlapping region. " + "The priority of internal override structures is ``-1``.", + ) + enforce: bool = pydantic.Field( False, title="Enforce Grid Size", diff --git a/tidy3d/components/tcad/simulation/heat_charge.py b/tidy3d/components/tcad/simulation/heat_charge.py index 73cc219d94..e3cdd053ae 100644 --- a/tidy3d/components/tcad/simulation/heat_charge.py +++ b/tidy3d/components/tcad/simulation/heat_charge.py @@ -1042,7 +1042,7 @@ def plot_boundaries( # get structure list structures = [self.simulation_structure] - structures += list(self.structures) + structures += list(self.scene.sorted_structures) # construct slicing plane axis, position = Box.parse_xyz_kwargs(x=x, y=y, z=z) @@ -1432,7 +1432,7 @@ def plot_sources( """ # background can't have source, so no need to add background structure - structures = self.structures + structures = self.scene.sorted_structures # alpha is None just means plot without any transparency if alpha is None: diff --git a/tidy3d/components/types.py b/tidy3d/components/types.py index 4f4c807be4..3e76c7e929 100644 --- a/tidy3d/components/types.py +++ b/tidy3d/components/types.py @@ -200,6 +200,7 @@ def __modify_schema__(cls, field_schema): ClipOperationType = Literal["union", "intersection", "difference", "symmetric_difference"] BoxSurface = Literal["x-", "x+", "y-", "y+", "z-", "z+"] LengthUnit = Literal["nm", "μm", "um", "mm", "cm", "m"] +PriorityMode = Literal["equal", "conductor"] """ medium """ diff --git a/tidy3d/plugins/adjoint/components/simulation.py b/tidy3d/plugins/adjoint/components/simulation.py index 93088536d4..ba3097d0f2 100644 --- a/tidy3d/plugins/adjoint/components/simulation.py +++ b/tidy3d/plugins/adjoint/components/simulation.py @@ -419,7 +419,9 @@ def to_simulation(self) -> Tuple[Simulation, JaxInfo]: sim = Simulation.parse_obj(sim_dict) # put all structures and monitors in one list - all_structures = list(self.structures) + [js.to_structure() for js in self.input_structures] + all_structures = list(self.scene.sorted_structures) + [ + js.to_structure() for js in self.input_structures + ] all_monitors = ( list(self.monitors) + list(self.output_monitors) From 44fc8251e2dba61a8c04219f8b5ca411d845e3fe Mon Sep 17 00:00:00 2001 From: Weiliang Jin Date: Tue, 15 Apr 2025 09:44:54 -0700 Subject: [PATCH 2/3] revision --- tidy3d/components/scene.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tidy3d/components/scene.py b/tidy3d/components/scene.py index f66b6f9d4c..2bab914230 100644 --- a/tidy3d/components/scene.py +++ b/tidy3d/components/scene.py @@ -277,7 +277,7 @@ def background_structure(self) -> Structure: @cached_property def all_structures(self) -> List[Structure]: """List of all structures in the simulation including the background.""" - return [self.background_structure] + list(self.sorted_structures) + return [self.background_structure] + self.sorted_structures @staticmethod def intersecting_media( @@ -1812,7 +1812,7 @@ def doping_bounds(self): acceptors_lims = [1e50, -1e50] donors_lims = [1e50, -1e50] - for struct in [self.background_structure] + list(self.sorted_structures): + for struct in self.all_structures: if isinstance(struct.medium.charge, SemiconductorMedium): electric_spec = struct.medium.charge for doping, limits in zip( From 43891a80824ec8e5fcf16a99b171cf97d2279b16 Mon Sep 17 00:00:00 2001 From: Weiliang Jin Date: Tue, 15 Apr 2025 10:02:15 -0700 Subject: [PATCH 3/3] fix simulation --- tidy3d/components/base_sim/simulation.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/tidy3d/components/base_sim/simulation.py b/tidy3d/components/base_sim/simulation.py index 7590b5cc9f..8f15718b94 100644 --- a/tidy3d/components/base_sim/simulation.py +++ b/tidy3d/components/base_sim/simulation.py @@ -16,7 +16,7 @@ from ..medium import Medium, MediumType3D from ..scene import Scene from ..structure import Structure -from ..types import TYPE_TAG_STR, Ax, Axis, Bound, LengthUnit, Symmetry +from ..types import TYPE_TAG_STR, Ax, Axis, Bound, LengthUnit, PriorityMode, Symmetry from ..validators import assert_objects_in_sim_bounds, assert_unique_names from ..viz import ( PlotParams, @@ -115,6 +115,15 @@ class AbstractSimulation(Box, ABC): "include the desired unit specifier in labels.", ) + structure_priority_mode: PriorityMode = pd.Field( + "equal", + title="Structure Priority Setting", + description="This field only affects structures of `priority=None`. " + "If `equal`, the priority of those structures is set to 0; if `conductor`, " + "the priority of structures made of `LossyMetalMedium` is set to 90, " + "`PECMedium` to 100, and others to 0.", + ) + """ Validating setup """ @pd.root_validator(pre=True) @@ -184,7 +193,10 @@ def scene(self) -> Scene: """Scene instance associated with the simulation.""" return Scene( - medium=self.medium, structures=self.structures, plot_length_units=self.plot_length_units + medium=self.medium, + structures=self.structures, + plot_length_units=self.plot_length_units, + structure_priority_mode=self.structure_priority_mode, ) def get_monitor_by_name(self, name: str) -> AbstractMonitor: