Skip to content

Commit

Permalink
Validate sources and monitors that are exactly at the simulation doma…
Browse files Browse the repository at this point in the history
…in bounds
  • Loading branch information
momchil-flex committed Jun 4, 2024
1 parent f2d5ceb commit 7998818
Show file tree
Hide file tree
Showing 8 changed files with 75 additions and 16 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions tests/test_components/test_eme.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
Expand Down
4 changes: 2 additions & 2 deletions tests/test_components/test_heat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
36 changes: 35 additions & 1 deletion tests/test_components/test_simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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],
)
2 changes: 1 addition & 1 deletion tidy3d/components/base_sim/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
26 changes: 19 additions & 7 deletions tidy3d/components/geometry/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion tidy3d/components/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
13 changes: 9 additions & 4 deletions tidy3d/components/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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]

Expand Down

0 comments on commit 7998818

Please sign in to comment.