diff --git a/CHANGELOG.md b/CHANGELOG.md index cab85bac91..02d039f995 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `RectangularWaveguide` supports layered cladding above and below core. - `SubpixelSpec` accepted by `Simulation.subpixel` to select subpixel averaging methods separately for dielectric, metal, and PEC materials. Specifically, added support for conformal mesh methods near PEC structures that can be specified through the field `pec` in the `SubpixelSpec` class. Note: previously, `subpixel=False` was implementing staircasing for every material except PEC. Now, `subpixel=False` implements direct staircasing for all materials. For PEC, the behavior of `subpixel=False` in Tidy3D < 2.7 is now achieved through `subpixel=SubpixelSpec(pec=HeuristicPECStaircasing())`, while `subpixel=True` in Tidy3D < 2.7 is now achieved through `subpixel=SubpixelSpec(pec=Staircasing())`. The default is `subpixel=SubpixelSpec(pec=PECConformal())` for more accurate PEC modelling. +### Changed +- Sources and monitors which are exactly at the simulation domain boundaries will now error. They can still be placed very close to the boundaries, but need to be on the inside of the region. + ### Fixed - `ModeSolver.plot_field` correctly returning the plot axes. - Avoid error if non-positive refractive index used for integration resolution in adjoint. diff --git a/tests/test_components/test_eme.py b/tests/test_components/test_eme.py index 944b5d5753..ded2258951 100644 --- a/tests/test_components/test_eme.py +++ b/tests/test_components/test_eme.py @@ -394,6 +394,11 @@ def test_eme_simulation(log_capture): # noqa: F811 with pytest.raises(SetupError): _ = sim.updated_copy(monitors=[monitor]) + # test monitor at simulation bounds + monitor = sim.monitors[-1].updated_copy(center=[0, 0, -sim.size[2] / 2]) + with pytest.raises(pd.ValidationError): + _ = sim.updated_copy(monitors=[monitor]) + # test boundary and source validation with pytest.raises(SetupError): _ = sim.updated_copy(boundary_spec=td.BoundarySpec.all_sides(td.Periodic())) diff --git a/tests/test_components/test_simulation.py b/tests/test_components/test_simulation.py index 4669d19258..bc08732db1 100644 --- a/tests/test_components/test_simulation.py +++ b/tests/test_components/test_simulation.py @@ -1059,7 +1059,7 @@ def test_proj_monitor_warnings(log_capture): # noqa F811 src = td.PlaneWave( source_time=td.GaussianPulse(freq0=2.5e14, fwidth=1e13), - center=(0, 0, -0.5), + center=(0, 0, -0.4), size=(td.inf, td.inf, 0), direction="+", pol_angle=-1.0, @@ -2906,3 +2906,37 @@ def test_validate_low_num_cells_in_mode_objects(): sim = SIM.updated_copy(monitors=[mode_monitor]) with pytest.raises(SetupError): sim._validate_num_cells_in_mode_objects() + + +def test_validate_sources_monitors_in_bounds(): + pulse = td.GaussianPulse(freq0=200e12, fwidth=20e12) + mode_source = td.ModeSource( + center=(0, -1, 0), + size=(1, 0, 1), + source_time=pulse, + direction="+", + ) + mode_monitor = td.ModeMonitor( + center=(0, 1, 0), + size=(1, 0, 1), + freqs=[1e12], + name="test_in_bounds", + mode_spec=td.ModeSpec(), + ) + + # check that a source at y- simulation domain edge errors + with pytest.raises(pydantic.ValidationError): + sim = td.Simulation( + size=(2, 2, 2), + run_time=1e-12, + grid_spec=td.GridSpec(wavelength=1.0), + sources=[mode_source], + ) + # check that a monitor at y+ simulation domain edge errors + with pytest.raises(pydantic.ValidationError): + sim = td.Simulation( + size=(2, 2, 2), + run_time=1e-12, + grid_spec=td.GridSpec(wavelength=1.0), + monitors=[mode_monitor], + ) diff --git a/tidy3d/components/base_sim/simulation.py b/tidy3d/components/base_sim/simulation.py index 39ad8b9e00..32d76855c2 100644 --- a/tidy3d/components/base_sim/simulation.py +++ b/tidy3d/components/base_sim/simulation.py @@ -114,7 +114,7 @@ class AbstractSimulation(Box, ABC): _unique_structure_names = assert_unique_names("structures") _unique_source_names = assert_unique_names("sources") - _monitors_in_bounds = assert_objects_in_sim_bounds("monitors") + _monitors_in_bounds = assert_objects_in_sim_bounds("monitors", strict_inequality=True) _structures_in_bounds = assert_objects_in_sim_bounds("structures", error=False) @pd.validator("structures", always=True) diff --git a/tidy3d/components/geometry/base.py b/tidy3d/components/geometry/base.py index 0aca47241f..71c9dd5735 100644 --- a/tidy3d/components/geometry/base.py +++ b/tidy3d/components/geometry/base.py @@ -248,13 +248,20 @@ def intersections_2dbox(self, plane: Box) -> List[Shapely]: ) return plane.intersections_with(self) - def intersects(self, other) -> bool: + def intersects( + self, other, strict_inequality: Tuple[bool, bool, bool] = [False, False, False] + ) -> bool: """Returns ``True`` if two :class:`Geometry` have intersecting `.bounds`. Parameters ---------- other : :class:`Geometry` Geometry to check intersection with. + strict_inequality : Tuple[bool, bool, bool] = [False, False, False] + For each dimension, defines whether to include equality in the boundaries comparison. + If ``False``, equality is included, and two geometries that only intersect at their + boundaries will evaluate as ``True``. If ``True``, such geometries will evaluate as + ``False``. Returns ------- @@ -265,14 +272,19 @@ def intersects(self, other) -> bool: self_bmin, self_bmax = self.bounds other_bmin, other_bmax = other.bounds - # are all of other's minimum coordinates less than self's maximum coordinate? - in_minus = all(o <= s for (s, o) in zip(self_bmax, other_bmin)) + for smin, omin, smax, omax, strict in zip( + self_bmin, other_bmin, self_bmax, other_bmax, strict_inequality + ): + # are all of other's minimum coordinates less than self's maximum coordinate? + in_minus = omin < smax if strict else omin <= smax + # are all of other's maximum coordinates greater than self's minimum coordinate? + in_plus = omax > smin if strict else omax >= smin - # are all of other's maximum coordinates greater than self's minimum coordinate? - in_plus = all(o >= s for (s, o) in zip(self_bmin, other_bmax)) + # if either failed, return False + if not all((in_minus, in_plus)): + return False - # for intersection of bounds, both must be true - return in_minus and in_plus + return True def intersects_plane(self, x: float = None, y: float = None, z: float = None) -> bool: """Whether self intersects plane specified by one non-None value of x,y,z. diff --git a/tidy3d/components/simulation.py b/tidy3d/components/simulation.py index 4640b2dc82..4ed7297bcc 100644 --- a/tidy3d/components/simulation.py +++ b/tidy3d/components/simulation.py @@ -1988,7 +1988,7 @@ def _validate_auto_grid_wavelength(cls, val, values): _ = val.wavelength_from_sources(sources=values.get("sources")) return val - _sources_in_bounds = assert_objects_in_sim_bounds("sources") + _sources_in_bounds = assert_objects_in_sim_bounds("sources", strict_inequality=True) _mode_sources_symmetries = validate_mode_objects_symmetry("sources") _mode_monitors_symmetries = validate_mode_objects_symmetry("monitors") diff --git a/tidy3d/components/validators.py b/tidy3d/components/validators.py index 4b163f23cc..cf84199538 100644 --- a/tidy3d/components/validators.py +++ b/tidy3d/components/validators.py @@ -162,7 +162,9 @@ def field_has_unique_names(cls, val, values): return field_has_unique_names -def assert_objects_in_sim_bounds(field_name: str, error: bool = True): +def assert_objects_in_sim_bounds( + field_name: str, error: bool = True, strict_inequality: bool = False +): """Makes sure all objects in field are at least partially inside of simulation bounds.""" @pydantic.validator(field_name, allow_reuse=True, always=True) @@ -173,11 +175,14 @@ def objects_in_sim_bounds(cls, val, values): sim_size = values.get("size") sim_box = Box(size=sim_size, center=sim_center) + # Do a strict check, unless simulation is 0D along a dimension + strict_ineq = [size != 0 and strict_inequality for size in sim_size] + for position_index, geometric_object in enumerate(val): - if not sim_box.intersects(geometric_object.geometry): + if not sim_box.intersects(geometric_object.geometry, strict_inequality=strict_ineq): message = ( - f"'simulation.{field_name}[{position_index}]'" - "is completely outside of simulation domain." + f"'simulation.{field_name}[{position_index}]' " + "is outside of the simulation domain." ) custom_loc = [field_name, position_index]