From 7998818fd29c6c7499c4e9d0c208f7314915ce81 Mon Sep 17 00:00:00 2001 From: momchil Date: Mon, 3 Jun 2024 11:52:06 +0200 Subject: [PATCH] Validate sources and monitors that are exactly at the simulation domain bounds --- CHANGELOG.md | 3 ++ tests/test_components/test_eme.py | 5 ++++ tests/test_components/test_heat.py | 4 +-- tests/test_components/test_simulation.py | 36 +++++++++++++++++++++++- tidy3d/components/base_sim/simulation.py | 2 +- tidy3d/components/geometry/base.py | 26 ++++++++++++----- tidy3d/components/simulation.py | 2 +- tidy3d/components/validators.py | 13 ++++++--- 8 files changed, 75 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cab85bac9..02d039f99 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 944b5d575..ded225895 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_heat.py b/tests/test_components/test_heat.py index 08f32be6d..1a01f2159 100644 --- a/tests/test_components/test_heat.py +++ b/tests/test_components/test_heat.py @@ -113,10 +113,10 @@ def make_heat_mnts(): temp_mnt1 = TemperatureMonitor(size=(1.6, 2, 3), name="test") temp_mnt2 = TemperatureMonitor(size=(1.6, 2, 3), name="tet", unstructured=True) temp_mnt3 = TemperatureMonitor( - center=(0, 1, 0), size=(1.6, 0, 3), name="tri", unstructured=True, conformal=True + center=(0, 0.9, 0), size=(1.6, 0, 3), name="tri", unstructured=True, conformal=True ) temp_mnt4 = TemperatureMonitor( - center=(0, 1, 0), size=(1.6, 0, 3), name="empty", unstructured=True, conformal=False + center=(0, 0.9, 0), size=(1.6, 0, 3), name="empty", unstructured=True, conformal=False ) temp_mnt5 = TemperatureMonitor(center=(0, 0.7, 0.8), size=(3, 0, 0), name="line") temp_mnt6 = TemperatureMonitor(center=(0.7, 0.6, 0.8), size=(0, 0, 0), name="point") diff --git a/tests/test_components/test_simulation.py b/tests/test_components/test_simulation.py index 4669d1925..bc08732db 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 39ad8b9e0..32d76855c 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 0aca47241..71c9dd573 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 4640b2dc8..4ed7297bc 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 4b163f23c..cf8419953 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]