diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c8c98dcf..1a0ff20c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `SimulationData.plot_field` accepts new field componets and values, including the Poynting vector. - `SimulationData.get_poynting_vector` for calculating the 3D Poynting vector at the Yee cell centers. +- Post init validation of Tidy3D components. ### Changed - `export_matlib_to_file` in `material_library` exports material's full name in additional to abbreviation. diff --git a/requirements/basic.txt b/requirements/basic.txt index a2cf43b6d..c62e10573 100644 --- a/requirements/basic.txt +++ b/requirements/basic.txt @@ -8,7 +8,7 @@ h5py>=3.0.0 rich<12.6.0 # note: rich >= 12.6 adds double progressbars matplotlib shapely>=2.0 -pydantic>=1.10.0 +pydantic>=1.10.0,<2.0.0 PyYAML dask toml diff --git a/tests/test_components/test_simulation.py b/tests/test_components/test_simulation.py index 33169eeef..d999ff2c2 100644 --- a/tests/test_components/test_simulation.py +++ b/tests/test_components/test_simulation.py @@ -185,7 +185,7 @@ def _test_monitor_size(): s.validate_pre_upload() -@pytest.mark.parametrize("freq, log_level", [(1.5, "warning"), (2.5, None), (3.5, "warning")]) +@pytest.mark.parametrize("freq, log_level", [(1.5, "warning"), (2.5, "info"), (3.5, "warning")]) def test_monitor_medium_frequency_range(log_capture, freq, log_level): # monitor frequency above or below a given medium's range should throw a warning @@ -209,7 +209,7 @@ def test_monitor_medium_frequency_range(log_capture, freq, log_level): assert_log_level(log_capture, log_level) -@pytest.mark.parametrize("fwidth, log_level", [(0.1, "warning"), (2, None)]) +@pytest.mark.parametrize("fwidth, log_level", [(0.1, "warning"), (2, "info")]) def test_monitor_simulation_frequency_range(log_capture, fwidth, log_level): # monitor frequency outside of the simulation's frequency range should throw a warning @@ -559,7 +559,7 @@ def test_large_grid_size(log_capture, grid_size, log_level): assert_log_level(log_capture, log_level) -@pytest.mark.parametrize("box_size,log_level", [(0.001, None), (9.9, "warning"), (20, None)]) +@pytest.mark.parametrize("box_size,log_level", [(0.001, "info"), (9.9, "warning"), (20, "info")]) def test_sim_structure_gap(log_capture, box_size, log_level): """Make sure the gap between a structure and PML is not too small compared to lambda0.""" medium = td.Medium(permittivity=2) @@ -844,7 +844,7 @@ def test_diffraction_medium(): @pytest.mark.parametrize( "box_size,log_level", [ - ((0.1, 0.1, 0.1), None), + ((0.1, 0.1, 0.1), "info"), ((1, 0.1, 0.1), "warning"), ((0.1, 1, 0.1), "warning"), ((0.1, 0.1, 1), "warning"), @@ -870,6 +870,42 @@ def test_sim_structure_extent(log_capture, box_size, log_level): assert_log_level(log_capture, log_level) +@pytest.mark.parametrize( + "box_length,absorb_type,log_level", + [ + (0, "PML", "info"), + (0.5, "PML", "info"), + (1, "PML", "info"), + (1.5, "PML", "warning"), + (1.5, "absorber", "info"), + (5.0, "PML", "info"), + ], +) +def test_sim_validate_structure_bounds_pml(log_capture, box_length, absorb_type, log_level): + """Make sure we warn if structure bounds are within the PML exactly to simulation edges.""" + + boundary = td.PML() if absorb_type == "PML" else td.Absorber() + + src = td.UniformCurrentSource( + source_time=td.GaussianPulse(freq0=3e14, fwidth=1e13), + size=(0, 0, 0), + polarization="Ex", + ) + box = td.Structure( + geometry=td.Box(size=(box_length, 0.5, 0.5), center=(0, 0, 0)), + medium=td.Medium(permittivity=2), + ) + sim = td.Simulation( + size=(1, 1, 1), + structures=[box], + sources=[src], + run_time=1e-12, + boundary_spec=td.BoundarySpec.all_sides(boundary=boundary), + ) + + assert_log_level(log_capture, log_level) + + def test_num_mediums(): """Make sure we error if too many mediums supplied.""" @@ -1227,7 +1263,6 @@ def test_tfsf_structures_grid(log_capture): ) ], ) - sim.validate_pre_upload() # TFSF box must not intersect a custom medium Nx, Ny, Nz = 10, 9, 8 diff --git a/tidy3d/components/base.py b/tidy3d/components/base.py index 78d296583..10474e137 100644 --- a/tidy3d/components/base.py +++ b/tidy3d/components/base.py @@ -59,6 +59,14 @@ class Tidy3dBaseModel(pydantic.BaseModel): `Pydantic Models `_ """ + def __init__(self, **kwargs): + """Init method, includes post-init validators.""" + super().__init__(**kwargs) + self._post_init_validators() + + def _post_init_validators(self) -> None: + """Call validators taking ``self`` that get run after init, implement in subclasses.""" + def __init_subclass__(cls) -> None: """Things that are done to each of the models.""" diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index 7fe1a20c1..f8ad66bae 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -795,6 +795,79 @@ def _check_normalize_index(cls, val, values): return val + """ Post-init validators """ + + def _post_init_validators(self) -> None: + """Call validators taking z`self` that get run after init.""" + self._validate_no_structures_pml() + self._validate_tfsf_nonuniform_grid() + + def _validate_no_structures_pml(self) -> None: + """Ensure no structures terminate / have bounds inside of PML.""" + + pml_thicks = self.pml_thicknesses + sim_bounds = self.bounds + bound_spec = self.boundary_spec.to_list + for i, structure in enumerate(self.structures): + geo_bounds = structure.geometry.bounds + for sim_bound, geo_bound, pml_thick, bound_dim in zip( + sim_bounds, geo_bounds, pml_thicks, bound_spec + ): + for sim_pos, geo_pos, pml, pm_val, bound_edge in zip( + sim_bound, geo_bound, pml_thick, (-1, 1), bound_dim + ): + sim_pos_pml = sim_pos + pm_val * pml + in_pml_plus = (pm_val > 0) and (sim_pos < geo_pos <= sim_pos_pml) + in_pml_mnus = (pm_val < 0) and (sim_pos > geo_pos >= sim_pos_pml) + if not isinstance(bound_edge, Absorber) and (in_pml_plus or in_pml_mnus): + log.warning( + f"A bound of Simulation.structures[{i}] was detected as being within " + "the simulation PML. We recommend extending structures to infinity or " + "completely outside of the simulation PML to " + "avoid unexpected effects when the structures are not translationally " + "invariant within the PML. Skipping rest of structures." + ) + return + + def _validate_tfsf_nonuniform_grid(self) -> None: + """Warn if the grid is nonuniform along the directions tangential to the injection plane, + inside the TFSF box. + """ + # if the grid is uniform in all directions, there's no need to proceed + if not (self.grid_spec.auto_grid_used or self.grid_spec.custom_grid_used): + return + + for source in self.sources: + if not isinstance(source, TFSF): + continue + + centers = self.grid.centers.to_list + sizes = self.grid.sizes.to_list + tfsf_bounds = source.bounds + _, plane_inds = source.pop_axis([0, 1, 2], axis=source.injection_axis) + grid_list = [self.grid_spec.grid_x, self.grid_spec.grid_y, self.grid_spec.grid_z] + for ind in plane_inds: + grid_type = grid_list[ind] + if isinstance(grid_type, UniformGrid): + continue + + sizes_in_tfsf = [ + size + for size, center in zip(sizes[ind], centers[ind]) + if tfsf_bounds[0][ind] <= center <= tfsf_bounds[1][ind] + ] + + # check if all the grid sizes are sufficiently unequal + if not np.all(np.isclose(sizes_in_tfsf, sizes_in_tfsf[0])): + log.warning( + f"The grid is nonuniform along the '{'xyz'[ind]}' axis, which may lead " + "to sub-optimal cancellation of the incident field in the scattered-field " + "region for the total-field scattered-field (TFSF) source " + f"'{source.name}'. For best results, we recomended ensuring a uniform " + "grid in both directions tangential to the TFSF injection axis, " + f"'{'xyz'[source.injection_axis]}'. " + ) + """ Pre submit validation (before web.upload()) """ def validate_pre_upload(self) -> None: @@ -803,7 +876,6 @@ def validate_pre_upload(self) -> None: self._validate_monitor_size() self._validate_datasets_not_none() self._validate_tfsf_structure_intersections() - self._validate_tfsf_nonuniform_grid() # self._validate_run_time() def _validate_size(self) -> None: @@ -936,45 +1008,6 @@ def _validate_tfsf_structure_intersections(self) -> None: f" '{'xyz'[source.injection_axis]}'." ) - def _validate_tfsf_nonuniform_grid(self) -> None: - """Warn if the grid is nonuniform along the directions tangential to the injection plane, - inside the TFSF box. - """ - # if the grid is uniform in all directions, there's no need to proceed - if not (self.grid_spec.auto_grid_used or self.grid_spec.custom_grid_used): - return - - for source in self.sources: - if not isinstance(source, TFSF): - continue - - centers = self.grid.centers.to_list - sizes = self.grid.sizes.to_list - tfsf_bounds = source.bounds - _, plane_inds = source.pop_axis([0, 1, 2], axis=source.injection_axis) - grid_list = [self.grid_spec.grid_x, self.grid_spec.grid_y, self.grid_spec.grid_z] - for ind in plane_inds: - grid_type = grid_list[ind] - if isinstance(grid_type, UniformGrid): - continue - - sizes_in_tfsf = [ - size - for size, center in zip(sizes[ind], centers[ind]) - if tfsf_bounds[0][ind] <= center <= tfsf_bounds[1][ind] - ] - - # check if all the grid sizes are sufficiently unequal - if not np.all(np.isclose(sizes_in_tfsf, sizes_in_tfsf[0])): - log.warning( - f"The grid is nonuniform along the '{'xyz'[ind]}' axis, which may lead " - "to sub-optimal cancellation of the incident field in the scattered-field " - "region for the total-field scattered-field (TFSF) source " - f"'{source.name}'. For best results, we recomended ensuring a uniform " - "grid in both directions tangential to the TFSF injection axis, " - f"'{'xyz'[source.injection_axis]}'. " - ) - """ Accounting """ @cached_property