Skip to content

Commit 34f2a48

Browse files
dmarek-flexyaugenst-flex
authored andcommitted
added a relaxed version of bounds checking
made dedicated file for bound operations
1 parent c77b6f4 commit 34f2a48

File tree

7 files changed

+113
-29
lines changed

7 files changed

+113
-29
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Added `eps_lim` keyword argument to `Simulation.plot_eps()` for manual control over the permittivity color limits.
1313

14+
### Changed
15+
- Relaxed bounds checking of path integrals during `WavePort` validation.
16+
1417
## [2.8.4] - 2025-05-15
1518

1619
### Added

tests/test_plugins/smatrix/test_terminal_component_modeler.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,21 @@ def test_wave_port_path_integral_validation():
564564
current_integral=custom_current_path,
565565
)
566566

567+
# Test integral path only slightly larger than port bounds
568+
wave_port = WavePort(
569+
center=(0, 10000, 115.00000022351743),
570+
size=(500, 0, 160.00000000000003),
571+
name="wave_port_1",
572+
mode_spec=mode_spec,
573+
direction="+",
574+
voltage_integral=voltage_path.updated_copy(
575+
size=(0, 0, 70.000000298023424), center=(0, 10000, 70.000000298023424)
576+
),
577+
current_integral=None,
578+
)
579+
# Make sure validation would have failed if a strict comparison was used
580+
assert wave_port.bounds[0][2] > wave_port.voltage_integral.bounds[0][2]
581+
567582

568583
def test_wave_port_to_mode_solver(tmp_path):
569584
"""Checks that wave port can be converted to a mode solver."""

tidy3d/components/data/data_array.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
)
2929
from ...exceptions import DataError, FileError
3030
from ..autograd import TidyArrayBox, get_static, interpn, is_tidy_box
31+
from ..geometry.bound_ops import bounds_contains
3132
from ..types import Axis, Bound
3233

3334
# maps the dimension names to their attributes
@@ -627,7 +628,7 @@ def sel_inside(self, bounds: Bound) -> SpatialDataArray:
627628

628629
return sorted_self.isel(x=inds_list[0], y=inds_list[1], z=inds_list[2])
629630

630-
def does_cover(self, bounds: Bound) -> bool:
631+
def does_cover(self, bounds: Bound, rtol: float = 0.0, atol: float = 0.0) -> bool:
631632
"""Check whether data fully covers specified by ``bounds`` spatial region. If data contains
632633
only one point along a given direction, then it is assumed the data is constant along that
633634
direction and coverage is not checked.
@@ -637,6 +638,10 @@ def does_cover(self, bounds: Bound) -> bool:
637638
----------
638639
bounds : Tuple[float, float, float], Tuple[float, float float]
639640
Min and max bounds packaged as ``(minx, miny, minz), (maxx, maxy, maxz)``.
641+
rtol : float = 0.0
642+
Relative tolerance for comparing bounds
643+
atol : float = 0.0
644+
Absolute tolerance for comparing bounds
640645
641646
Returns
642647
-------
@@ -647,12 +652,19 @@ def does_cover(self, bounds: Bound) -> bool:
647652
raise DataError(
648653
"Min and max bounds must be packaged as '(minx, miny, minz), (maxx, maxy, maxz)'."
649654
)
650-
651-
coords = (self.x, self.y, self.z)
652-
return all(
653-
(np.min(coord) <= smin and np.max(coord) >= smax) or len(coord) == 1
654-
for coord, smin, smax in zip(coords, bounds[0], bounds[1])
655-
)
655+
xyz = [self.x, self.y, self.z]
656+
self_min = [0] * 3
657+
self_max = [0] * 3
658+
for dim in range(3):
659+
coords = xyz[dim]
660+
if len(coords) == 1:
661+
self_min[dim] = bounds[0][dim]
662+
self_max[dim] = bounds[1][dim]
663+
else:
664+
self_min[dim] = np.min(coords)
665+
self_max[dim] = np.max(coords)
666+
self_bounds = (tuple(self_min), tuple(self_max))
667+
return bounds_contains(self_bounds, bounds, rtol=rtol, atol=atol)
656668

657669

658670
class SpatialDataArray(AbstractSpatialDataArray):

tidy3d/components/geometry/base.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
polygon_patch,
6060
set_default_labels_and_title,
6161
)
62+
from .bound_ops import bounds_intersection, bounds_union
6263

6364
POLY_GRID_SIZE = 1e-12
6465

@@ -408,20 +409,12 @@ def bounds(self) -> Bound:
408409
@staticmethod
409410
def bounds_intersection(bounds1: Bound, bounds2: Bound) -> Bound:
410411
"""Return the bounds that are the intersection of two bounds."""
411-
rmin1, rmax1 = bounds1
412-
rmin2, rmax2 = bounds2
413-
rmin = tuple(max(v1, v2) for v1, v2 in zip(rmin1, rmin2))
414-
rmax = tuple(min(v1, v2) for v1, v2 in zip(rmax1, rmax2))
415-
return (rmin, rmax)
412+
return bounds_intersection(bounds1, bounds2)
416413

417414
@staticmethod
418415
def bounds_union(bounds1: Bound, bounds2: Bound) -> Bound:
419416
"""Return the bounds that are the union of two bounds."""
420-
rmin1, rmax1 = bounds1
421-
rmin2, rmax2 = bounds2
422-
rmin = tuple(min(v1, v2) for v1, v2 in zip(rmin1, rmin2))
423-
rmax = tuple(max(v1, v2) for v1, v2 in zip(rmax1, rmax2))
424-
return (rmin, rmax)
417+
return bounds_union(bounds1, bounds2)
425418

426419
@cached_property
427420
def bounding_box(self):
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""Geometry operations for bounding box type with minimal imports."""
2+
3+
from math import isclose
4+
5+
from ...constants import fp_eps
6+
from ..types import Bound
7+
8+
9+
def bounds_intersection(bounds1: Bound, bounds2: Bound) -> Bound:
10+
"""Return the bounds that are the intersection of two bounds."""
11+
rmin1, rmax1 = bounds1
12+
rmin2, rmax2 = bounds2
13+
rmin = tuple(max(v1, v2) for v1, v2 in zip(rmin1, rmin2))
14+
rmax = tuple(min(v1, v2) for v1, v2 in zip(rmax1, rmax2))
15+
return (rmin, rmax)
16+
17+
18+
def bounds_union(bounds1: Bound, bounds2: Bound) -> Bound:
19+
"""Return the bounds that are the union of two bounds."""
20+
rmin1, rmax1 = bounds1
21+
rmin2, rmax2 = bounds2
22+
rmin = tuple(min(v1, v2) for v1, v2 in zip(rmin1, rmin2))
23+
rmax = tuple(max(v1, v2) for v1, v2 in zip(rmax1, rmax2))
24+
return (rmin, rmax)
25+
26+
27+
def bounds_contains(
28+
outer_bounds: Bound, inner_bounds: Bound, rtol: float = fp_eps, atol: float = 0.0
29+
) -> bool:
30+
"""Checks whether ``inner_bounds`` is contained within ``outer_bounds`` within specified tolerances.
31+
32+
Parameters
33+
----------
34+
outer_bounds : Bound
35+
The outer bounds to check containment against
36+
inner_bounds : Bound
37+
The inner bounds to check if contained
38+
rtol : float = fp_eps
39+
Relative tolerance for comparing bounds
40+
atol : float = 0.0
41+
Absolute tolerance for comparing bounds
42+
43+
Returns
44+
-------
45+
bool
46+
True if ``inner_bounds`` is contained within ``outer_bounds`` within tolerances
47+
"""
48+
outer_min, outer_max = outer_bounds
49+
inner_min, inner_max = inner_bounds
50+
for dim in range(3):
51+
outer_min_dim = outer_min[dim]
52+
outer_max_dim = outer_max[dim]
53+
inner_min_dim = inner_min[dim]
54+
inner_max_dim = inner_max[dim]
55+
within_min = (
56+
isclose(outer_min_dim, inner_min_dim, rel_tol=rtol, abs_tol=atol)
57+
or outer_min_dim <= inner_min_dim
58+
)
59+
within_max = (
60+
isclose(outer_max_dim, inner_max_dim, rel_tol=rtol, abs_tol=atol)
61+
or outer_max_dim >= inner_max_dim
62+
)
63+
64+
if not within_min or not within_max:
65+
return False
66+
return True

tidy3d/plugins/microwave/path_integrals.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ class AxisAlignedPathIntegral(AbstractAxesRH, Box):
105105
def compute_integral(self, scalar_field: EMScalarFieldType) -> IntegralResultTypes:
106106
"""Computes the defined integral given the input ``scalar_field``."""
107107

108-
if not scalar_field.does_cover(self.bounds):
108+
if not scalar_field.does_cover(self.bounds, fp_eps, np.finfo(np.float32).smallest_normal):
109109
raise DataError("Scalar field does not cover the integration domain.")
110110
coord = "xyz"[self.main_axis]
111111

tidy3d/plugins/smatrix/ports/wave.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@
1010
from ....components.data.monitor_data import ModeData
1111
from ....components.data.sim_data import SimulationData
1212
from ....components.geometry.base import Box
13+
from ....components.geometry.bound_ops import bounds_contains
1314
from ....components.grid.grid import Grid
1415
from ....components.monitor import ModeMonitor
1516
from ....components.simulation import Simulation
1617
from ....components.source.field import ModeSource, ModeSpec
1718
from ....components.source.time import GaussianPulse
18-
from ....components.types import Bound, Direction, FreqArray
19+
from ....components.types import Direction, FreqArray
20+
from ....constants import fp_eps
1921
from ....exceptions import ValidationError
2022
from ...microwave import (
2123
CurrentIntegralTypes,
@@ -183,22 +185,15 @@ def compute_port_impedance(
183185
impedance_array = impedance_calc.compute_impedance(mode_data)
184186
return impedance_array
185187

186-
@staticmethod
187-
def _within_port_bounds(path_bounds: Bound, port_bounds: Bound) -> bool:
188-
"""Helper to check if one bounding box is completely within the other."""
189-
path_min = np.array(path_bounds[0])
190-
path_max = np.array(path_bounds[1])
191-
bound_min = np.array(port_bounds[0])
192-
bound_max = np.array(port_bounds[1])
193-
return (bound_min <= path_min).all() and (bound_max >= path_max).all()
194-
195188
@pd.validator("voltage_integral", "current_integral")
196189
def _validate_path_integrals_within_port(cls, val, values):
197190
"""Raise ``ValidationError`` when the supplied path integrals are not within the port bounds."""
198191
center = values["center"]
199192
size = values["size"]
200193
box = Box(center=center, size=size)
201-
if val and not WavePort._within_port_bounds(val.bounds, box.bounds):
194+
if val and not bounds_contains(
195+
box.bounds, val.bounds, fp_eps, np.finfo(np.float32).smallest_normal
196+
):
202197
raise ValidationError(
203198
f"'{cls.__name__}' must be setup with all path integrals defined within the bounds "
204199
f"of the port. Path bounds are '{val.bounds}', but port bounds are '{box.bounds}'."

0 commit comments

Comments
 (0)