Skip to content

Commit dab6854

Browse files
committed
🔧 Fixed bug and added warning when a lumped element is placed outside the Simulation.
1 parent 2bbec11 commit dab6854

File tree

6 files changed

+148
-1
lines changed

6 files changed

+148
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2222
- Changed plot_3d iframe url to tidy3d production environment.
2323
- `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.
2424
- Nonlinear models `KerrNonlinearity` and `TwoPhotonAbsorption` now default to using the physical real fields instead of complex fields.
25+
- 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.
2526

2627
### Fixed
2728
- Make gauge selection for non-converged modes more robust.

tests/test_components/test_simulation.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,6 +1303,61 @@ def test_sim_structure_extent(box_size, log_level):
13031303
)
13041304

13051305

1306+
def test_warn_lumped_elements_outside_sim_bounds():
1307+
"""Test that warning is emitted for lumped elements that are not entirely contained within simulation bounds."""
1308+
1309+
sim_center = (0, 0, 0)
1310+
sim_size = (2, 2, 2)
1311+
src = td.UniformCurrentSource(
1312+
source_time=td.GaussianPulse(freq0=10e9, fwidth=8e9),
1313+
size=(0, 0, 0),
1314+
polarization="Ex",
1315+
)
1316+
1317+
# Lumped element fully contained - should work
1318+
resistor_in = td.LumpedResistor(
1319+
size=(0.5, 1, 0),
1320+
center=(0, 0, 0),
1321+
voltage_axis=1,
1322+
resistance=50,
1323+
name="resistor_inside",
1324+
)
1325+
with AssertLogLevel("INFO"):
1326+
sim_good = td.Simulation(
1327+
size=sim_size,
1328+
center=sim_center,
1329+
sources=[src],
1330+
run_time=1e-12,
1331+
lumped_elements=[resistor_in],
1332+
boundary_spec=td.BoundarySpec.all_sides(boundary=td.Periodic()),
1333+
)
1334+
assert len(sim_good.volumetric_structures) == 1
1335+
1336+
# Lumped element outside - should emit warning and not be added
1337+
resistor_out = td.LumpedResistor(
1338+
size=(0.5, 1, 0),
1339+
center=(0, 2, 0),
1340+
voltage_axis=1,
1341+
resistance=50,
1342+
name="resistor_outside",
1343+
)
1344+
with AssertLogLevel("WARNING"):
1345+
sim_bad = sim_good.updated_copy(lumped_elements=[resistor_out])
1346+
assert len(sim_bad.volumetric_structures) == 0
1347+
1348+
# Lumped element extends to boundary and is not strictly inside simulation
1349+
resistor_edge = td.LumpedResistor(
1350+
size=(0.5, 1, 0),
1351+
center=(0, 0.5, 0),
1352+
voltage_axis=1,
1353+
resistance=50,
1354+
name="resistor_edge",
1355+
)
1356+
with AssertLogLevel("WARNING"):
1357+
_ = sim_good.updated_copy(lumped_elements=[resistor_edge])
1358+
assert len(sim_bad.volumetric_structures) == 0
1359+
1360+
13061361
@pytest.mark.parametrize(
13071362
"box_length,absorb_type,log_level",
13081363
[

tidy3d/components/geometry/base.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,45 @@ def intersects(
317317

318318
return True
319319

320+
def contains(
321+
self, other: Geometry, strict_inequality: Tuple[bool, bool, bool] = [False, False, False]
322+
) -> bool:
323+
"""Returns ``True`` if the `.bounds` of ``other`` are contained within the
324+
`.bounds` of ``self``.
325+
326+
Parameters
327+
----------
328+
other : :class:`Geometry`
329+
Geometry to check containment with.
330+
strict_inequality : Tuple[bool, bool, bool] = [False, False, False]
331+
For each dimension, defines whether to include equality in the boundaries comparison.
332+
If ``False``, equality will be considered as contained. If ``True``, ``other``'s
333+
bounds must be strictly within the bounds of ``self``.
334+
335+
Returns
336+
-------
337+
bool
338+
Whether the rectangular bounding box of ``other`` is contained within the bounding
339+
box of ``self``.
340+
"""
341+
342+
self_bmin, self_bmax = self.bounds
343+
other_bmin, other_bmax = other.bounds
344+
345+
for smin, omin, smax, omax, strict in zip(
346+
self_bmin, other_bmin, self_bmax, other_bmax, strict_inequality
347+
):
348+
# are all of other's minimum coordinates greater than self's minimim coordinate?
349+
in_minus = omin > smin if strict else omin >= smin
350+
# are all of other's maximum coordinates less than self's maximum coordinate?
351+
in_plus = omax < smax if strict else omax <= smax
352+
353+
# if either failed, return False
354+
if not all((in_minus, in_plus)):
355+
return False
356+
357+
return True
358+
320359
def intersects_plane(self, x: float = None, y: float = None, z: float = None) -> bool:
321360
"""Whether self intersects plane specified by one non-None value of x,y,z.
322361

tidy3d/components/lumped_element.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,18 @@ def to_geometry(self, grid: Grid = None) -> ClipOperation:
449449
annulus = ClipOperation(operation="difference", geometry_a=disk_out, geometry_b=disk_in)
450450
return annulus
451451

452+
@cached_property
453+
def geometry(self) -> ClipOperation:
454+
"""Alias for ``to_geometry`` that ignores the grid and allows :class:`CoaxialLumpedResistor`
455+
to behave like a :class:.`Structure`.
456+
457+
Returns
458+
-------
459+
ClipOperation
460+
The annulus describing the coaxial lumped resistor.
461+
"""
462+
return self.to_geometry()
463+
452464

453465
class NetworkConversions(Tidy3dBaseModel):
454466
"""Helper functionality for directly computing complex conductivity and permittivities using

tidy3d/components/simulation.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
annotate_type,
111111
)
112112
from .validators import (
113+
assert_objects_contained_in_sim_bounds,
113114
assert_objects_in_sim_bounds,
114115
validate_mode_objects_symmetry,
115116
validate_mode_plane_radius,
@@ -1432,8 +1433,10 @@ def snap_to_grid(geom: Geometry, axis: Axis) -> Geometry:
14321433

14331434
# Convert lumped elements into structures
14341435
lumped_structures = []
1436+
strict_ineq = 3 * [True]
14351437
for lumped_element in self.lumped_elements:
1436-
lumped_structures += lumped_element.to_structures(self.grid)
1438+
if self.geometry.contains(lumped_element.geometry, strict_inequality=strict_ineq):
1439+
lumped_structures += lumped_element.to_structures(self.grid)
14371440

14381441
# Begin volumetric structures grid
14391442
all_structures = list(self.static_structures) + lumped_structures
@@ -2476,6 +2479,9 @@ def _validate_auto_grid_wavelength(cls, val, values):
24762479
return val
24772480

24782481
_sources_in_bounds = assert_objects_in_sim_bounds("sources", strict_inequality=True)
2482+
_lumped_elements_in_bounds = assert_objects_contained_in_sim_bounds(
2483+
"lumped_elements", error=False, strict_inequality=True
2484+
)
24792485
_mode_sources_symmetries = validate_mode_objects_symmetry("sources")
24802486
_mode_monitors_symmetries = validate_mode_objects_symmetry("monitors")
24812487

tidy3d/components/validators.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,40 @@ def objects_in_sim_bounds(cls, val, values):
211211
return objects_in_sim_bounds
212212

213213

214+
def assert_objects_contained_in_sim_bounds(
215+
field_name: str, error: bool = True, strict_inequality: bool = False
216+
):
217+
"""Makes sure all objects in field are completely inside the simulation bounds."""
218+
219+
@pydantic.validator(field_name, allow_reuse=True, always=True)
220+
@skip_if_fields_missing(["center", "size"])
221+
def objects_contained_in_sim_bounds(cls, val, values):
222+
"""check for containment of each structure with simulation bounds."""
223+
sim_center = values.get("center")
224+
sim_size = values.get("size")
225+
sim_box = Box(size=sim_size, center=sim_center)
226+
227+
# Do a strict check, unless simulation is 0D along a dimension
228+
strict_ineq = [size != 0 and strict_inequality for size in sim_size]
229+
230+
with log as consolidated_logger:
231+
for position_index, geometric_object in enumerate(val):
232+
if not sim_box.contains(geometric_object.geometry, strict_inequality=strict_ineq):
233+
message = (
234+
f"'simulation.{field_name}[{position_index}]' "
235+
"is not completely inside the simulation domain."
236+
)
237+
custom_loc = [field_name, position_index]
238+
239+
if error:
240+
raise SetupError(message)
241+
consolidated_logger.warning(message, custom_loc=custom_loc)
242+
243+
return val
244+
245+
return objects_contained_in_sim_bounds
246+
247+
214248
def enforce_monitor_fields_present():
215249
"""Make sure all of the fields in the monitor are present in the corresponding data."""
216250

0 commit comments

Comments
 (0)