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

Merged
merged 1 commit into from
May 22, 2025
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Added `eps_lim` keyword argument to `Simulation.plot_eps()` for manual control over the permittivity color limits.
- Added `thickness` parameter to `LossyMetalMedium` for computing surface impedance of a thin conductor.
- `priority` field in `Structure` and `MeshOverrideStructure` for setting the behavior in structure overlapping region. When its value is `None`, the priority is automatically determined based on the material property and simulation's `structure_priority_mode`.

### Changed
- Relaxed bounds checking of path integrals during `WavePort` validation.
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 @@ -307,3 +307,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 (
_warn_unsupported_traced_argument,
assert_objects_in_sim_bounds,
Expand Down Expand Up @@ -119,6 +119,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 @@ -191,7 +200,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
35 changes: 26 additions & 9 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 @@ -959,6 +960,7 @@ def override_structure(
dl=dl_list,
shadow=False,
drop_outside_sim=drop_outside_sim,
priority=-1,
)


Expand Down Expand Up @@ -1518,6 +1520,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 @@ -2293,10 +2296,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 @@ -2308,22 +2312,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 @@ -2398,6 +2403,7 @@ def make_grid(
[None, None],
[None, None],
],
structure_priority_mode: PriorityMode = "equal",
) -> Grid:
"""Make the entire simulation grid based on some simulation parameters.

Expand Down Expand Up @@ -2425,6 +2431,8 @@ def make_grid(
boundary_types : Tuple[Tuple[str, str], Tuple[str, str], Tuple[str, str]] = [[None, None], [None, None], [None, None]]
Type of boundary conditions along each dimension: "pec/pmc", "periodic", or
None for any other. This is relevant only for gap meshing.
structure_priority_mode : PriorityMode
Structure priority setting.

Returns
-------
Expand All @@ -2441,6 +2449,7 @@ def make_grid(
lumped_elements=lumped_elements,
internal_override_structures=internal_override_structures,
internal_snapping_points=internal_snapping_points,
structure_priority_mode=structure_priority_mode,
)

return grid
Expand All @@ -2460,6 +2469,7 @@ def _make_grid_and_snapping_lines(
[None, None],
[None, None],
],
structure_priority_mode: PriorityMode = "equal",
) -> Tuple[Grid, List[CoordinateOptional]]:
"""Make the entire simulation grid based on some simulation parameters.
Also return snappiung point resulted from iterative gap meshing.
Expand Down Expand Up @@ -2488,6 +2498,8 @@ def _make_grid_and_snapping_lines(
boundary_types : Tuple[Tuple[str, str], Tuple[str, str], Tuple[str, str]] = [[None, None], [None, None], [None, None]]
Type of boundary conditions along each dimension: "pec/pmc", "periodic", or
None for any other. This is relevant only for gap meshing.
structure_priority_mode : PriorityMode
Structure priority setting.

Returns
-------
Expand All @@ -2504,6 +2516,7 @@ def _make_grid_and_snapping_lines(
lumped_elements=lumped_elements,
internal_override_structures=internal_override_structures,
internal_snapping_points=internal_snapping_points,
structure_priority_mode=structure_priority_mode,
)

sim_geometry = structures[0].geometry
Expand Down Expand Up @@ -2549,6 +2562,7 @@ def _make_grid_and_snapping_lines(
internal_override_structures=internal_override_structures,
internal_snapping_points=snapping_lines + internal_snapping_points,
dl_min_from_gaps=0.45 * min_gap_width,
structure_priority_mode=structure_priority_mode,
)

same = old_grid == new_grid
Expand All @@ -2575,6 +2589,7 @@ def _make_grid_one_iteration(
internal_override_structures: List[MeshOverrideStructure] = None,
internal_snapping_points: List[CoordinateOptional] = None,
dl_min_from_gaps: pd.PositiveFloat = inf,
structure_priority_mode: PriorityMode = "equal",
) -> Grid:
"""Make the entire simulation grid based on some simulation parameters.

Expand All @@ -2601,7 +2616,8 @@ def _make_grid_one_iteration(
If `None`, recomputes internal snapping points.
dl_min_from_gaps : pd.PositiveFloat
Minimal grid size computed based on autodetected gaps.

structure_priority_mode : PriorityMode
Structure priority setting.

Returns
-------
Expand Down Expand Up @@ -2664,6 +2680,7 @@ def _make_grid_one_iteration(
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 @@ -203,6 +203,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 @@ -408,6 +409,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 @@ -106,8 +107,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 @@ -245,6 +258,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 @@ -254,7 +278,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 @@ -443,7 +467,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 @@ -889,7 +913,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 @@ -1466,7 +1490,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 @@ -1744,7 +1768,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 @@ -1795,7 +1819,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
Loading