Skip to content
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- More robust `Sellmeier` and `Debye` material model, and prevent very large pole parameters in `PoleResidue` material model.
- Bug in `WavePort` when more than one mode is requested in the `ModeSpec`.
- Solver error for named 2D materials with inhomogeneous substrates.

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

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


## [v2.10.0rc1] - 2025-09-11

### Added
Expand Down
4 changes: 3 additions & 1 deletion tests/test_components/test_simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3047,7 +3047,7 @@ def test_2d_material_subdivision():
med_2d = td.Medium2D(ss=conductor, tt=conductor)
plane_size = [0, 1.5 * plane_width, 1.5 * plane_height]
plane_material = td.Structure(
geometry=td.Box(size=plane_size, center=[plane_pos, 0, 0]), medium=med_2d
geometry=td.Box(size=plane_size, center=[plane_pos, 0, 0]), medium=med_2d, name="plane"
)

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

_ = sim_td._finalized

volume = td.Box(center=(plane_pos, 0, 0), size=(0, 2 * plane_width, 2 * plane_height))
eps_centers = sim_td.epsilon(box=volume, freq=freq0, coord_key="Ey")
# Plot should give a smiley face
Expand Down
1 change: 1 addition & 0 deletions tidy3d/components/eme/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,7 @@ def _post_init_validators(self) -> None:

def validate_pre_upload(self) -> None:
"""Validate the fully initialized EME simulation is ok for upload to our servers."""
super().validate_pre_upload()
log.begin_capture()
self._validate_sweep_spec_size()
self._validate_size()
Expand Down
1 change: 1 addition & 0 deletions tidy3d/components/mode/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,7 @@ def plot_pml_mode_plane(
return self._mode_solver.plot_pml(ax=ax)

def validate_pre_upload(self, source_required: bool = False):
super().validate_pre_upload()
self._mode_solver.validate_pre_upload(source_required=source_required)

_boundaries_for_zero_dims = validate_boundaries_for_zero_dims(warn_on_change=False)
191 changes: 101 additions & 90 deletions tidy3d/components/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1805,7 +1805,7 @@ def snap_to_grid(geom: Geometry, axis: Axis) -> Geometry:
# subdivide
subdivided_geometries = subdivide(geometry, background_structures)
# Create and add volumetric equivalents
for subdivided_geometry in subdivided_geometries:
for i, subdivided_geometry in enumerate(subdivided_geometries):
# Snap to the grid and create volumetric equivalent
snapped_geometry = snap_to_grid(subdivided_geometry[0], axis)
snapped_center = get_bounds(snapped_geometry, axis)[0]
Expand All @@ -1819,7 +1819,12 @@ def snap_to_grid(geom: Geometry, axis: Axis) -> Geometry:

new_bounds = (snapped_center, snapped_center)
new_geometry = snapped_geometry._update_from_bounds(bounds=new_bounds, axis=axis)
new_structure = structure.updated_copy(geometry=new_geometry, medium=new_medium)
new_name = structure.name
if new_name:
new_name += f"_SUBDIVIDED[{i}]"
new_structure = structure.updated_copy(
geometry=new_geometry, medium=new_medium, name=new_name
)

new_structures.append(new_structure)

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

def validate_pre_upload(self) -> None:
"""Validate the fully initialized simulation is ok for upload to our servers."""
log.begin_capture()
self._validate_finalized()
log.end_capture(self)

def _make_pec_frame(self, obj: Union[ModeSource, InternalAbsorber]) -> Structure:
"""Make a pec frame around a mode source or an internal absorber. For mode sources,
the frame is added around the injection plane. For internal absorbers, a backing pec
plate is also added on the non-absorbing side.
"""
span_inds = np.array(self.grid.discretize_inds(obj))

coords = self.grid.boundaries.to_list
direction = obj.direction
if isinstance(obj, ModeSource):
axis = obj.injection_axis
length = obj.frame.length
if direction == "+":
span_inds[axis][1] += length - 1
else:
span_inds[axis][0] -= length - 1
else:
axis = obj.size.index(0.0)

box_bounds = [
[
c[beg],
c[end],
]
for c, (beg, end) in zip(coords, span_inds)
]

box = Box.from_bounds(*np.transpose(box_bounds))

surfaces = Box.surfaces(box.size, box.center)
if isinstance(obj, ModeSource):
del surfaces[2 * axis : 2 * axis + 2]
else:
if direction == "-":
del surfaces[2 * axis + 1]
else:
del surfaces[2 * axis]

structure = Structure(
geometry=GeometryGroup(
geometries=surfaces,
),
medium=PECMedium(),
)

return structure

@cached_property
def _modal_plane_frames(self) -> list[Structure]:
"""Return frames to add around mode sources and internal absorbers."""

pec_frames = [
self._make_pec_frame(src)
for src in self.sources
if isinstance(src, ModeSource) and isinstance(src.frame, PECFrame)
]

pec_frames = pec_frames + [
self._make_pec_frame(abc) for abc in self._shifted_internal_absorbers
]

return pec_frames

@cached_property
def _finalized(self) -> Simulation:
"""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."""

modal_frames = self._modal_plane_frames

if len(modal_frames) == 0 and not self._contains_converted_volumetric_structures:
return self

structures = list(self.volumetric_structures) + modal_frames

return self.updated_copy(grid_spec=GridSpec.from_grid(self.grid), structures=structures)

def _validate_finalized(self):
"""Validate that after adding pec frames simulation setup is still valid."""

try:
_ = self._finalized
except Exception:
log.error(
"Simulation fails after requested mode source PEC frames are added. "
"Please inspect '._finalized'."
)


class Simulation(AbstractYeeGridSimulation):
"""
Expand Down Expand Up @@ -4336,6 +4434,7 @@ def validate_pre_upload(self, source_required: bool = True) -> None:
source_required: bool = True
If ``True``, validation will fail in case no sources are found in the simulation.
"""
super().validate_pre_upload()
log.begin_capture()
self._validate_size()
self._validate_monitor_size()
Expand All @@ -4346,7 +4445,6 @@ def validate_pre_upload(self, source_required: bool = True) -> None:
self._warn_time_monitors_outside_run_time()
self._validate_time_monitors_num_steps()
self._validate_freq_monitors_freq_range()
self._validate_finalized()
log.end_capture(self)
if source_required and len(self.sources) == 0:
raise SetupError("No sources in simulation.")
Expand Down Expand Up @@ -5655,90 +5753,3 @@ def from_scene(cls, scene: Scene, **kwargs) -> Simulation:
)

_boundaries_for_zero_dims = validate_boundaries_for_zero_dims()

def _make_pec_frame(self, obj: Union[ModeSource, InternalAbsorber]) -> Structure:
"""Make a pec frame around a mode source or an internal absorber. For mode sources,
the frame is added around the injection plane. For internal absorbers, a backing pec
plate is also added on the non-absorbing side.
"""
span_inds = np.array(self.grid.discretize_inds(obj))

coords = self.grid.boundaries.to_list
direction = obj.direction
if isinstance(obj, ModeSource):
axis = obj.injection_axis
length = obj.frame.length
if direction == "+":
span_inds[axis][1] += length - 1
else:
span_inds[axis][0] -= length - 1
else:
axis = obj.size.index(0.0)

box_bounds = [
[
c[beg],
c[end],
]
for c, (beg, end) in zip(coords, span_inds)
]

box = Box.from_bounds(*np.transpose(box_bounds))

surfaces = Box.surfaces(box.size, box.center)
if isinstance(obj, ModeSource):
del surfaces[2 * axis : 2 * axis + 2]
else:
if direction == "-":
del surfaces[2 * axis + 1]
else:
del surfaces[2 * axis]

structure = Structure(
geometry=GeometryGroup(
geometries=surfaces,
),
medium=PECMedium(),
)

return structure

@cached_property
def _modal_plane_frames(self) -> list[Structure]:
"""Return frames to add around mode sources and internal absorbers."""

pec_frames = [
self._make_pec_frame(src)
for src in self.sources
if isinstance(src, ModeSource) and isinstance(src.frame, PECFrame)
]

pec_frames = pec_frames + [
self._make_pec_frame(abc) for abc in self._shifted_internal_absorbers
]

return pec_frames

@cached_property
def _finalized(self) -> Simulation:
"""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."""

modal_frames = self._modal_plane_frames

if len(modal_frames) == 0 and not self._contains_converted_volumetric_structures:
return self

structures = list(self.volumetric_structures) + modal_frames

return self.updated_copy(grid_spec=GridSpec.from_grid(self.grid), structures=structures)

def _validate_finalized(self):
"""Validate that after adding pec frames simulation setup is still valid."""

try:
_ = self._finalized
except Exception:
log.error(
"Simulation fails after requested mode source PEC frames are added. "
"Please inspect '._finalized'."
)