Skip to content

Commit

Permalink
🔧 Fixed bug and added warning when a lumped element is placed outside…
Browse files Browse the repository at this point in the history
… the Simulation.
  • Loading branch information
dmarek-flex committed Feb 18, 2025
1 parent 35edc0b commit 1eb68bb
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 1 deletion.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Changed plot_3d iframe url to tidy3d production environment.
- `num_freqs` is now set to 3 by default for the `PlaneWave`, `GaussianBeam`, and `AnalyticGaussianBeam` sources, which makes the injection more accurate in broadband cases.
- Nonlinear models `KerrNonlinearity` and `TwoPhotonAbsorption` now default to using the physical real fields instead of complex fields.
- Added warning when a lumped element is not completely within the simulation bounds, since now lumped elements will only have an effect on the `Simulation` when they are completely within the simulation bounds.

### Fixed
- Make gauge selection for non-converged modes more robust.
Expand Down
55 changes: 55 additions & 0 deletions tests/test_components/test_simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -1303,6 +1303,61 @@ def test_sim_structure_extent(box_size, log_level):
)


def test_warn_lumped_elements_outside_sim_bounds():
"""Test that warning is emitted for lumped elements that are not entirely contained within simulation bounds."""

sim_center = (0, 0, 0)
sim_size = (2, 2, 2)
src = td.UniformCurrentSource(
source_time=td.GaussianPulse(freq0=10e9, fwidth=8e9),
size=(0, 0, 0),
polarization="Ex",
)

# Lumped element fully contained - should work
resistor_in = td.LumpedResistor(
size=(0.5, 1, 0),
center=(0, 0, 0),
voltage_axis=1,
resistance=50,
name="resistor_inside",
)
with AssertLogLevel("INFO"):
sim_good = td.Simulation(
size=sim_size,
center=sim_center,
sources=[src],
run_time=1e-12,
lumped_elements=[resistor_in],
boundary_spec=td.BoundarySpec.all_sides(boundary=td.Periodic()),
)
assert len(sim_good.volumetric_structures) == 1

# Lumped element outside - should emit warning and not be added
resistor_out = td.LumpedResistor(
size=(0.5, 1, 0),
center=(0, 2, 0),
voltage_axis=1,
resistance=50,
name="resistor_outside",
)
with AssertLogLevel("WARNING"):
sim_bad = sim_good.updated_copy(lumped_elements=[resistor_out])
assert len(sim_bad.volumetric_structures) == 0

# Lumped element extends to boundary and is not strictly inside simulation
resistor_edge = td.LumpedResistor(
size=(0.5, 1, 0),
center=(0, 0.5, 0),
voltage_axis=1,
resistance=50,
name="resistor_edge",
)
with AssertLogLevel("WARNING"):
_ = sim_good.updated_copy(lumped_elements=[resistor_edge])
assert len(sim_bad.volumetric_structures) == 0


@pytest.mark.parametrize(
"box_length,absorb_type,log_level",
[
Expand Down
39 changes: 39 additions & 0 deletions tidy3d/components/geometry/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,45 @@ def intersects(

return True

def contains(
self, other: Geometry, strict_inequality: Tuple[bool, bool, bool] = [False, False, False]
) -> bool:
"""Returns ``True`` if the `.bounds` of ``other`` are contained within the
`.bounds` of ``self``.
Parameters
----------
other : :class:`Geometry`
Geometry to check containment 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 will be considered as contained. If ``True``, ``other``'s
bounds must be strictly within the bounds of ``self``.
Returns
-------
bool
Whether the rectangular bounding box of ``other`` is contained within the bounding
box of ``self``.
"""

self_bmin, self_bmax = self.bounds
other_bmin, other_bmax = other.bounds

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 greater than self's minimim coordinate?
in_minus = omin > smin if strict else omin >= smin
# are all of other's maximum coordinates less than self's maximum coordinate?
in_plus = omax < smax if strict else omax <= smax

# if either failed, return False
if not all((in_minus, in_plus)):
return False

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
12 changes: 12 additions & 0 deletions tidy3d/components/lumped_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,18 @@ def to_geometry(self, grid: Grid = None) -> ClipOperation:
annulus = ClipOperation(operation="difference", geometry_a=disk_out, geometry_b=disk_in)
return annulus

@cached_property
def geometry(self) -> ClipOperation:
"""Alias for ``to_geometry`` that ignores the grid and allows :class:`CoaxialLumpedResistor`
to behave like a :class:.`Structure`.
Returns
-------
ClipOperation
The annulus describing the coaxial lumped resistor.
"""
return self.to_geometry()


class NetworkConversions(Tidy3dBaseModel):
"""Helper functionality for directly computing complex conductivity and permittivities using
Expand Down
8 changes: 7 additions & 1 deletion tidy3d/components/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
from .subpixel_spec import SubpixelSpec
from .types import TYPE_TAG_STR, Ax, Axis, FreqBound, InterpMethod, Literal, Symmetry, annotate_type
from .validators import (
assert_objects_contained_in_sim_bounds,
assert_objects_in_sim_bounds,
validate_mode_objects_symmetry,
validate_mode_plane_radius,
Expand Down Expand Up @@ -1397,8 +1398,10 @@ def snap_to_grid(geom: Geometry, axis: Axis) -> Geometry:

# Convert lumped elements into structures
lumped_structures = []
strict_ineq = 3 * [True]
for lumped_element in self.lumped_elements:
lumped_structures += lumped_element.to_structures(self.grid)
if self.geometry.contains(lumped_element.geometry, strict_inequality=strict_ineq):
lumped_structures += lumped_element.to_structures(self.grid)

# Begin volumetric structures grid
all_structures = list(self.static_structures) + lumped_structures
Expand Down Expand Up @@ -2441,6 +2444,9 @@ def _validate_auto_grid_wavelength(cls, val, values):
return val

_sources_in_bounds = assert_objects_in_sim_bounds("sources", strict_inequality=True)
_lumped_elements_in_bounds = assert_objects_contained_in_sim_bounds(
"lumped_elements", error=False, strict_inequality=True
)
_mode_sources_symmetries = validate_mode_objects_symmetry("sources")
_mode_monitors_symmetries = validate_mode_objects_symmetry("monitors")

Expand Down
34 changes: 34 additions & 0 deletions tidy3d/components/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,40 @@ def objects_in_sim_bounds(cls, val, values):
return objects_in_sim_bounds


def assert_objects_contained_in_sim_bounds(
field_name: str, error: bool = True, strict_inequality: bool = False
):
"""Makes sure all objects in field are completely inside the simulation bounds."""

@pydantic.validator(field_name, allow_reuse=True, always=True)
@skip_if_fields_missing(["center", "size"])
def objects_contained_in_sim_bounds(cls, val, values):
"""check for containment of each structure with simulation bounds."""
sim_center = values.get("center")
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]

with log as consolidated_logger:
for position_index, geometric_object in enumerate(val):
if not sim_box.contains(geometric_object.geometry, strict_inequality=strict_ineq):
message = (
f"'simulation.{field_name}[{position_index}]' "
"is not completely inside the simulation domain."
)
custom_loc = [field_name, position_index]

if error:
raise SetupError(message)
consolidated_logger.warning(message, custom_loc=custom_loc)

return val

return objects_contained_in_sim_bounds


def enforce_monitor_fields_present():
"""Make sure all of the fields in the monitor are present in the corresponding data."""

Expand Down

0 comments on commit 1eb68bb

Please sign in to comment.