Skip to content

Structure priority option in structure overlapping region #2336

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
45 changes: 45 additions & 0 deletions tests/test_components/test_scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
16 changes: 14 additions & 2 deletions tidy3d/components/base_sim/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
25 changes: 17 additions & 8 deletions tidy3d/components/grid/grid_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
Axis,
Coordinate,
CoordinateOptional,
PriorityMode,
Symmetry,
annotate_type,
)
Expand Down Expand Up @@ -940,6 +941,7 @@ def override_structure(
dl=dl_list,
shadow=False,
drop_outside_sim=drop_outside_sim,
priority=-1,
)


Expand Down Expand Up @@ -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,
)
)

Expand Down Expand Up @@ -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
----------
Expand All @@ -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,
Expand Down Expand Up @@ -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.

Expand All @@ -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
-------
Expand Down Expand Up @@ -1912,6 +1920,7 @@ def make_grid(
wavelength,
sim_size,
lumped_elements,
structure_priority_mode,
internal_override_structures,
)

Expand Down
2 changes: 2 additions & 0 deletions tidy3d/components/lumped_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
]

Expand Down Expand Up @@ -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,
)
]

Expand Down
40 changes: 32 additions & 8 deletions tidy3d/components/scene.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
InterpMethod,
LengthUnit,
PermittivityComponent,
PriorityMode,
Shapely,
Size,
)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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`."""
Expand All @@ -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] + self.sorted_structures

@staticmethod
def intersecting_media(
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.all_structures:
if isinstance(struct.medium.charge, SemiconductorMedium):
electric_spec = struct.medium.charge
for doping, limits in zip(
Expand Down
21 changes: 14 additions & 7 deletions tidy3d/components/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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]
Expand Down
Loading