diff --git a/CHANGELOG.md b/CHANGELOG.md index 01b61fe8ec..0b86cb8520 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/tests/test_components/test_simulation.py b/tests/test_components/test_simulation.py index 2323906f97..53279d8633 100644 --- a/tests/test_components/test_simulation.py +++ b/tests/test_components/test_simulation.py @@ -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] @@ -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 diff --git a/tidy3d/components/eme/simulation.py b/tidy3d/components/eme/simulation.py index 5d4ef1b698..cf04801252 100644 --- a/tidy3d/components/eme/simulation.py +++ b/tidy3d/components/eme/simulation.py @@ -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() diff --git a/tidy3d/components/mode/simulation.py b/tidy3d/components/mode/simulation.py index 1ac69fd9ff..305f5e9b35 100644 --- a/tidy3d/components/mode/simulation.py +++ b/tidy3d/components/mode/simulation.py @@ -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) diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index de2c147904..c172cb7654 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -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] @@ -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) @@ -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): """ @@ -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() @@ -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.") @@ -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'." - )