Skip to content

Commit 7889fe6

Browse files
committed
Fix subdivision for named 2D materials
1 parent fbeeb61 commit 7889fe6

File tree

5 files changed

+108
-91
lines changed

5 files changed

+108
-91
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717
### Fixed
1818
- More robust `Sellmeier` and `Debye` material model, and prevent very large pole parameters in `PoleResidue` material model.
1919
- Bug in `WavePort` when more than one mode is requested in the `ModeSpec`.
20+
- Solver error for named 2D materials with inhomogeneous substrates.
2021

2122
## [v2.10.0rc2] - 2025-10-01
2223

@@ -38,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3839
- Prevent autograd adjoint simulations from reusing out-of-range `normalize_index` values by defaulting their normalization to the first adjoint source when needed.
3940
- Subtasks validation errors from `web.upload(ComponentModeler)` previously were not being propagated to users, and hung without response.
4041

42+
4143
## [v2.10.0rc1] - 2025-09-11
4244

4345
### Added

tests/test_components/test_simulation.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3047,7 +3047,7 @@ def test_2d_material_subdivision():
30473047
med_2d = td.Medium2D(ss=conductor, tt=conductor)
30483048
plane_size = [0, 1.5 * plane_width, 1.5 * plane_height]
30493049
plane_material = td.Structure(
3050-
geometry=td.Box(size=plane_size, center=[plane_pos, 0, 0]), medium=med_2d
3050+
geometry=td.Box(size=plane_size, center=[plane_pos, 0, 0]), medium=med_2d, name="plane"
30513051
)
30523052

30533053
structures = [face, left_top, right_top, bottom, plane_material]
@@ -3064,6 +3064,8 @@ def test_2d_material_subdivision():
30643064
run_time=1e-12,
30653065
)
30663066

3067+
_ = sim_td._finalized
3068+
30673069
volume = td.Box(center=(plane_pos, 0, 0), size=(0, 2 * plane_width, 2 * plane_height))
30683070
eps_centers = sim_td.epsilon(box=volume, freq=freq0, coord_key="Ey")
30693071
# Plot should give a smiley face

tidy3d/components/eme/simulation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,7 @@ def _post_init_validators(self) -> None:
594594

595595
def validate_pre_upload(self) -> None:
596596
"""Validate the fully initialized EME simulation is ok for upload to our servers."""
597+
super().validate_pre_upload()
597598
log.begin_capture()
598599
self._validate_sweep_spec_size()
599600
self._validate_size()

tidy3d/components/mode/simulation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,7 @@ def plot_pml_mode_plane(
612612
return self._mode_solver.plot_pml(ax=ax)
613613

614614
def validate_pre_upload(self, source_required: bool = False):
615+
super().validate_pre_upload()
615616
self._mode_solver.validate_pre_upload(source_required=source_required)
616617

617618
_boundaries_for_zero_dims = validate_boundaries_for_zero_dims(warn_on_change=False)

tidy3d/components/simulation.py

Lines changed: 101 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -1805,7 +1805,7 @@ def snap_to_grid(geom: Geometry, axis: Axis) -> Geometry:
18051805
# subdivide
18061806
subdivided_geometries = subdivide(geometry, background_structures)
18071807
# Create and add volumetric equivalents
1808-
for subdivided_geometry in subdivided_geometries:
1808+
for i, subdivided_geometry in enumerate(subdivided_geometries):
18091809
# Snap to the grid and create volumetric equivalent
18101810
snapped_geometry = snap_to_grid(subdivided_geometry[0], axis)
18111811
snapped_center = get_bounds(snapped_geometry, axis)[0]
@@ -1819,7 +1819,12 @@ def snap_to_grid(geom: Geometry, axis: Axis) -> Geometry:
18191819

18201820
new_bounds = (snapped_center, snapped_center)
18211821
new_geometry = snapped_geometry._update_from_bounds(bounds=new_bounds, axis=axis)
1822-
new_structure = structure.updated_copy(geometry=new_geometry, medium=new_medium)
1822+
new_name = structure.name
1823+
if new_name:
1824+
new_name += f"_SUBDIVIDED[{i}]"
1825+
new_structure = structure.updated_copy(
1826+
geometry=new_geometry, medium=new_medium, name=new_name
1827+
)
18231828

18241829
new_structures.append(new_structure)
18251830

@@ -2131,6 +2136,99 @@ def _invalidate_solver_cache(self) -> None:
21312136
"""Clear cached attributes that become stale when subpixel changes."""
21322137
self._cached_properties.pop("_mode_solver", None)
21332138

2139+
def validate_pre_upload(self) -> None:
2140+
"""Validate the fully initialized simulation is ok for upload to our servers."""
2141+
log.begin_capture()
2142+
self._validate_finalized()
2143+
log.end_capture(self)
2144+
2145+
def _make_pec_frame(self, obj: Union[ModeSource, InternalAbsorber]) -> Structure:
2146+
"""Make a pec frame around a mode source or an internal absorber. For mode sources,
2147+
the frame is added around the injection plane. For internal absorbers, a backing pec
2148+
plate is also added on the non-absorbing side.
2149+
"""
2150+
span_inds = np.array(self.grid.discretize_inds(obj))
2151+
2152+
coords = self.grid.boundaries.to_list
2153+
direction = obj.direction
2154+
if isinstance(obj, ModeSource):
2155+
axis = obj.injection_axis
2156+
length = obj.frame.length
2157+
if direction == "+":
2158+
span_inds[axis][1] += length - 1
2159+
else:
2160+
span_inds[axis][0] -= length - 1
2161+
else:
2162+
axis = obj.size.index(0.0)
2163+
2164+
box_bounds = [
2165+
[
2166+
c[beg],
2167+
c[end],
2168+
]
2169+
for c, (beg, end) in zip(coords, span_inds)
2170+
]
2171+
2172+
box = Box.from_bounds(*np.transpose(box_bounds))
2173+
2174+
surfaces = Box.surfaces(box.size, box.center)
2175+
if isinstance(obj, ModeSource):
2176+
del surfaces[2 * axis : 2 * axis + 2]
2177+
else:
2178+
if direction == "-":
2179+
del surfaces[2 * axis + 1]
2180+
else:
2181+
del surfaces[2 * axis]
2182+
2183+
structure = Structure(
2184+
geometry=GeometryGroup(
2185+
geometries=surfaces,
2186+
),
2187+
medium=PECMedium(),
2188+
)
2189+
2190+
return structure
2191+
2192+
@cached_property
2193+
def _modal_plane_frames(self) -> list[Structure]:
2194+
"""Return frames to add around mode sources and internal absorbers."""
2195+
2196+
pec_frames = [
2197+
self._make_pec_frame(src)
2198+
for src in self.sources
2199+
if isinstance(src, ModeSource) and isinstance(src.frame, PECFrame)
2200+
]
2201+
2202+
pec_frames = pec_frames + [
2203+
self._make_pec_frame(abc) for abc in self._shifted_internal_absorbers
2204+
]
2205+
2206+
return pec_frames
2207+
2208+
@cached_property
2209+
def _finalized(self) -> Simulation:
2210+
"""Return the finalized version of the simulation setup. That is, including automatic frames around mode sources and internal absorbers, and 2d strutures converted into volumetric analogues."""
2211+
2212+
modal_frames = self._modal_plane_frames
2213+
2214+
if len(modal_frames) == 0 and not self._contains_converted_volumetric_structures:
2215+
return self
2216+
2217+
structures = list(self.volumetric_structures) + modal_frames
2218+
2219+
return self.updated_copy(grid_spec=GridSpec.from_grid(self.grid), structures=structures)
2220+
2221+
def _validate_finalized(self):
2222+
"""Validate that after adding pec frames simulation setup is still valid."""
2223+
2224+
try:
2225+
_ = self._finalized
2226+
except Exception:
2227+
log.error(
2228+
"Simulation fails after requested mode source PEC frames are added. "
2229+
"Please inspect '._finalized'."
2230+
)
2231+
21342232

21352233
class Simulation(AbstractYeeGridSimulation):
21362234
"""
@@ -4336,6 +4434,7 @@ def validate_pre_upload(self, source_required: bool = True) -> None:
43364434
source_required: bool = True
43374435
If ``True``, validation will fail in case no sources are found in the simulation.
43384436
"""
4437+
super().validate_pre_upload()
43394438
log.begin_capture()
43404439
self._validate_size()
43414440
self._validate_monitor_size()
@@ -4346,7 +4445,6 @@ def validate_pre_upload(self, source_required: bool = True) -> None:
43464445
self._warn_time_monitors_outside_run_time()
43474446
self._validate_time_monitors_num_steps()
43484447
self._validate_freq_monitors_freq_range()
4349-
self._validate_finalized()
43504448
log.end_capture(self)
43514449
if source_required and len(self.sources) == 0:
43524450
raise SetupError("No sources in simulation.")
@@ -5655,90 +5753,3 @@ def from_scene(cls, scene: Scene, **kwargs) -> Simulation:
56555753
)
56565754

56575755
_boundaries_for_zero_dims = validate_boundaries_for_zero_dims()
5658-
5659-
def _make_pec_frame(self, obj: Union[ModeSource, InternalAbsorber]) -> Structure:
5660-
"""Make a pec frame around a mode source or an internal absorber. For mode sources,
5661-
the frame is added around the injection plane. For internal absorbers, a backing pec
5662-
plate is also added on the non-absorbing side.
5663-
"""
5664-
span_inds = np.array(self.grid.discretize_inds(obj))
5665-
5666-
coords = self.grid.boundaries.to_list
5667-
direction = obj.direction
5668-
if isinstance(obj, ModeSource):
5669-
axis = obj.injection_axis
5670-
length = obj.frame.length
5671-
if direction == "+":
5672-
span_inds[axis][1] += length - 1
5673-
else:
5674-
span_inds[axis][0] -= length - 1
5675-
else:
5676-
axis = obj.size.index(0.0)
5677-
5678-
box_bounds = [
5679-
[
5680-
c[beg],
5681-
c[end],
5682-
]
5683-
for c, (beg, end) in zip(coords, span_inds)
5684-
]
5685-
5686-
box = Box.from_bounds(*np.transpose(box_bounds))
5687-
5688-
surfaces = Box.surfaces(box.size, box.center)
5689-
if isinstance(obj, ModeSource):
5690-
del surfaces[2 * axis : 2 * axis + 2]
5691-
else:
5692-
if direction == "-":
5693-
del surfaces[2 * axis + 1]
5694-
else:
5695-
del surfaces[2 * axis]
5696-
5697-
structure = Structure(
5698-
geometry=GeometryGroup(
5699-
geometries=surfaces,
5700-
),
5701-
medium=PECMedium(),
5702-
)
5703-
5704-
return structure
5705-
5706-
@cached_property
5707-
def _modal_plane_frames(self) -> list[Structure]:
5708-
"""Return frames to add around mode sources and internal absorbers."""
5709-
5710-
pec_frames = [
5711-
self._make_pec_frame(src)
5712-
for src in self.sources
5713-
if isinstance(src, ModeSource) and isinstance(src.frame, PECFrame)
5714-
]
5715-
5716-
pec_frames = pec_frames + [
5717-
self._make_pec_frame(abc) for abc in self._shifted_internal_absorbers
5718-
]
5719-
5720-
return pec_frames
5721-
5722-
@cached_property
5723-
def _finalized(self) -> Simulation:
5724-
"""Return the finalized version of the simulation setup. That is, including automatic frames around mode sources and internal absorbers, and 2d strutures converted into volumetric analogues."""
5725-
5726-
modal_frames = self._modal_plane_frames
5727-
5728-
if len(modal_frames) == 0 and not self._contains_converted_volumetric_structures:
5729-
return self
5730-
5731-
structures = list(self.volumetric_structures) + modal_frames
5732-
5733-
return self.updated_copy(grid_spec=GridSpec.from_grid(self.grid), structures=structures)
5734-
5735-
def _validate_finalized(self):
5736-
"""Validate that after adding pec frames simulation setup is still valid."""
5737-
5738-
try:
5739-
_ = self._finalized
5740-
except Exception:
5741-
log.error(
5742-
"Simulation fails after requested mode source PEC frames are added. "
5743-
"Please inspect '._finalized'."
5744-
)

0 commit comments

Comments
 (0)