Skip to content

Commit b184600

Browse files
feat (rf): FXC-2053 convenience features for lumped port setup
1 parent 2884777 commit b184600

File tree

7 files changed

+493
-2
lines changed

7 files changed

+493
-2
lines changed

tests/test_components/test_geometry.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,20 @@ def test_box_from_bounds():
298298
assert b.center == (0, 0, 0)
299299

300300

301+
def test_box_padded_copy():
302+
"""Test that padding layers are added along box boundaries."""
303+
box = td.Box(size=(3, 2, 4))
304+
padded_box = box.padded_copy(x=(4, 10), y=(1, 2))
305+
assert np.allclose(np.array(padded_box.size), np.array([17, 5, 4]))
306+
assert np.allclose(np.array(padded_box.center), np.array([3, 0.5, 0]))
307+
308+
# ensure errors are raised if padding format is invalid.
309+
with pytest.raises(ValueError):
310+
padded_box = box.padded_copy(x=(1, -2), z=(-2, 0))
311+
with pytest.raises(ValueError):
312+
padded_box = box.padded_copy(x=(1))
313+
314+
301315
def test_polyslab_center_axis():
302316
"""Test the handling of center_axis in a polyslab having (-td.inf, td.inf) bounds."""
303317
ps = POLYSLAB.copy(update={"slab_bounds": (-td.inf, td.inf)})

tests/test_components/test_simulation.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3840,3 +3840,49 @@ def test_validate_microwave_mode_spec_generation():
38403840
# check that validation error is caught
38413841
with pytest.raises(SetupError):
38423842
sim._validate_microwave_mode_specs()
3843+
3844+
3845+
def test_padded_copy():
3846+
"""Test that padding layers are added along simulation boundaries."""
3847+
grid_spec = td.GridSpec.auto(wavelength=1.0)
3848+
3849+
sim = td.Simulation(
3850+
size=(5, 5, 5),
3851+
grid_spec=grid_spec,
3852+
structures=[
3853+
td.Structure(geometry=td.Box(size=(10, 13, 7)), medium=td.Medium(permittivity=2.0))
3854+
],
3855+
lumped_elements=[],
3856+
run_time=1e-12,
3857+
)
3858+
3859+
padded_sim = sim.padded_copy(x=(4, 10), y=(1, 2))
3860+
assert np.allclose(np.array(padded_sim.size), np.array([19, 8, 5]))
3861+
assert np.allclose(np.array(padded_sim.center), np.array([3, 0.5, 0]))
3862+
3863+
with pytest.raises(ValueError):
3864+
padded_sim = sim.padded_copy(x=(1, -2), z=(-2, 0))
3865+
with pytest.raises(ValueError):
3866+
padded_sim = sim.padded_copy(x=(1))
3867+
3868+
3869+
def test_uniformly_padded_copy():
3870+
"""Test that padding layers are uniformly added along simulation boundaries."""
3871+
grid_spec = td.GridSpec.auto(wavelength=1.0)
3872+
3873+
sim = td.Simulation(
3874+
size=(5, 5, 5),
3875+
grid_spec=grid_spec,
3876+
structures=[
3877+
td.Structure(geometry=td.Box(size=(3, 2, 4)), medium=td.Medium(permittivity=2.0))
3878+
],
3879+
lumped_elements=[],
3880+
run_time=1e-12,
3881+
)
3882+
3883+
padded_sim = sim.uniformly_padded_copy(padding=5)
3884+
assert np.allclose(np.array(padded_sim.size), np.array([15, 15, 15]))
3885+
assert np.allclose(np.array(padded_sim.center), np.array([0, 0, 0]))
3886+
3887+
with pytest.raises(ValueError):
3888+
padded_sim = sim.uniformly_padded_copy(padding=-1)

tests/test_plugins/smatrix/terminal_component_modeler_def.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,3 +696,48 @@ def make_patch_antenna_modeler(padding: tuple[float, float, float] = (0.25, 0.25
696696
)
697697

698698
return modeler
699+
700+
701+
def make_basic_filter_terminals():
702+
# Frequency
703+
(f_min, f_max) = (0.1e9, 8e9)
704+
705+
# Materials
706+
med_Cu = td.LossyMetalMedium(conductivity=60, frequency_range=(f_min, f_max))
707+
708+
# Geometry and Structure
709+
mm = 1000 # Conversion mm to micron
710+
H = 0.8 * mm # Substrate thickness
711+
T = 0.035 * mm # Metal thickness
712+
WL = 0.5 * mm
713+
WC = 4 * mm
714+
LC = 5.3 * mm
715+
LL1 = 5.8 * mm
716+
LL2 = 1.2 * mm
717+
LL3 = 11.1 * mm
718+
Lsub = LL1 + WL + LL3
719+
Wsub = 2 * (LC + WL + LL2)
720+
721+
geom_C = td.Box.from_bounds(rmin=(-WC / 2, 0, 0), rmax=(WC / 2, LC, T))
722+
geom_L2 = td.Box.from_bounds(rmin=(-WL / 2, -LL2 - WL, 0), rmax=(WL / 2, 0, T))
723+
geom_L1 = td.Box.from_bounds(rmin=(-WL / 2 - LL1, -LL2 - WL, 0), rmax=(-WL / 2, -LL2, T))
724+
geom_L3 = td.Box.from_bounds(rmin=(WL / 2, -LL2 - WL, 0), rmax=(WL / 2 + LL3, -LL2, T))
725+
726+
geom_resonator_basic = td.GeometryGroup(geometries=[geom_C, geom_L1, geom_L2, geom_L3])
727+
728+
x0, y0, z0 = geom_resonator_basic.bounding_box.center # center (x,y) with circuit
729+
geom_gnd = td.Box(center=(x0, y0, -H - T / 2), size=(Lsub, Wsub, T))
730+
731+
str_gnd = td.Structure(geometry=geom_gnd, medium=med_Cu)
732+
str_resonator_basic = td.Structure(geometry=geom_resonator_basic, medium=med_Cu)
733+
734+
# add second signal trace to test lateral_coord
735+
geom_sign = td.Box.from_bounds(
736+
rmin=(-WL / 2 - LL1, -2 * LL2 - WL, 0), rmax=(WL / 2 + LL3, -2 * LL2, T)
737+
)
738+
geom_resonator_modified = td.GeometryGroup(
739+
geometries=[geom_C, geom_L1, geom_L2, geom_L3, geom_sign]
740+
)
741+
str_resonator_modified = td.Structure(geometry=geom_resonator_modified, medium=med_Cu)
742+
743+
return (str_gnd, str_resonator_basic, str_resonator_modified)

tests/test_plugins/smatrix/test_terminal_component_modeler.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from tidy3d import SimulationDataMap
1515
from tidy3d.components.boundary import BroadbandModeABCSpec
1616
from tidy3d.components.data.data_array import FreqDataArray
17-
from tidy3d.exceptions import SetupError, Tidy3dError, Tidy3dKeyError
17+
from tidy3d.exceptions import SetupError, Tidy3dError, Tidy3dKeyError, ValidationError
1818
from tidy3d.plugins.smatrix import (
1919
CoaxialLumpedPort,
2020
LumpedPort,
@@ -31,6 +31,7 @@
3131

3232
from ...utils import run_emulated
3333
from .terminal_component_modeler_def import (
34+
make_basic_filter_terminals,
3435
make_coaxial_component_modeler,
3536
make_component_modeler,
3637
make_differential_stripline_modeler,
@@ -557,6 +558,80 @@ def test_validate_port_voltage_axis():
557558
LumpedPort(center=(0, 0, 0), size=(0, 1, 2), voltage_axis=0, impedance=50)
558559

559560

561+
def test_lumped_port_from_structures():
562+
"""Test automatic lumped port setup between two terminal structures."""
563+
564+
# set up terminals of a basic filter
565+
(str_gnd, str_resonator_basic, str_resonator_modified) = make_basic_filter_terminals()
566+
567+
# Geometry and Structure
568+
mm = 1000 # Conversion mm to micron
569+
WL = 0.5 * mm
570+
LL1 = 5.8 * mm
571+
LL2 = 1.2 * mm
572+
573+
# define basic parameters for automatic lumped port setup (except for signal terminal)
574+
lp_options = {
575+
"ground_terminal": str_gnd,
576+
"lateral_coord": -1450,
577+
"voltage_axis": 2,
578+
"impedance": 50,
579+
}
580+
581+
# ensure that value error is triggered if signal and ground terminals are not specified
582+
with pytest.raises(ValueError):
583+
LP0 = LumpedPort.from_structures(x=-WL / 2 - LL1, name="LP1", **lp_options)
584+
585+
# make sure the Lumped port is set correctly
586+
lp_options["signal_terminal"] = str_resonator_basic
587+
LP1 = LumpedPort.from_structures(x=-WL / 2 - LL1, name="LP1", **lp_options)
588+
assert LP1.voltage_axis == lp_options["voltage_axis"]
589+
assert np.isclose(LP1.impedance, lp_options["impedance"])
590+
591+
# make sure that `port_width` does not cause port geometry exceed overlap of terminals
592+
lp_options["port_width"] = 1000
593+
with pytest.raises(ValueError):
594+
LP2 = LumpedPort.from_structures(x=-WL / 2 - LL1, name="LP1", **lp_options)
595+
596+
lp_options["port_width"] = WL / 3
597+
LP2 = LumpedPort.from_structures(x=-WL / 2 - LL1, name="LP2", **lp_options)
598+
assert np.isclose(LP2.size[1], WL / 3)
599+
600+
# specify lateral coordinate to select appropriate signal terminal to resolve ambiguity
601+
lp_options["signal_terminal"] = str_resonator_modified
602+
lp_options["lateral_coord"] = -2 * LL2 - WL / 2
603+
lp_options["port_width"] = None
604+
LP3 = LumpedPort.from_structures(x=-WL / 2 - LL1, name="LP3", **lp_options)
605+
assert np.isclose(LP3.center[1], -2 * LL2 - WL / 2)
606+
607+
# test that an error is raised when port plane does not intersect any signal terminals
608+
with pytest.raises(ValueError):
609+
LP3 = LumpedPort.from_structures(y=8 * mm, name="LP3", **lp_options)
610+
611+
# ensure that error is raised
612+
with pytest.raises(ValueError):
613+
lp_options["voltage_axis"] = 0
614+
LP3 = LumpedPort.from_structures(x=-WL / 2 - LL1, name="LP3", **lp_options)
615+
616+
# test port width with lateral coords
617+
lp_options["port_width"] = WL / 3
618+
lp_options["voltage_axis"] = 2
619+
LP4 = LumpedPort.from_structures(x=-WL / 2 - LL1, name="LP4", **lp_options)
620+
assert np.isclose(LP4.size[1], lp_options["port_width"])
621+
622+
# test port width with lateral coords
623+
lp_options["lateral_coord"] = None
624+
with pytest.raises(ValidationError):
625+
LP5 = LumpedPort.from_structures(x=-WL / 2 - LL1, name="LP5", **lp_options)
626+
627+
# ensure that validation error is raised when terminal medium is not PEC or lossy metal
628+
str_gnd_new = str_gnd.updated_copy(medium=td.Medium(conductivity=1e2))
629+
lp_options["lateral_coord"] = -2 * LL2 - WL / 2
630+
lp_options["ground_terminal"] = str_gnd_new
631+
with pytest.raises(ValidationError):
632+
LP6 = LumpedPort.from_structures(x=-WL / 2 - LL1, name="LP6", **lp_options)
633+
634+
560635
@pytest.mark.parametrize("snap_center", [None, 0.1])
561636
def test_converting_port_to_simulation_objects(snap_center):
562637
"""Test that the LumpedPort can be converted into monitors and source without the grid present."""

tidy3d/components/geometry/base.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2170,6 +2170,53 @@ def intersections_with(self, other):
21702170
shapely_box = Geometry.evaluate_inf_shape(shapely_box)
21712171
return [Geometry.evaluate_inf_shape(shape) & shapely_box for shape in shapes_plane]
21722172

2173+
def padded_copy(
2174+
self,
2175+
x: Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None,
2176+
y: Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None,
2177+
z: Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None,
2178+
) -> Box:
2179+
"""Created a padded copy of a :class:`Box` instance.
2180+
2181+
Parameters
2182+
----------
2183+
x : Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None
2184+
Padding sizes at the left and right boundaries of the box along x-axis.
2185+
y : Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None
2186+
Padding sizes at the left and right boundaries of the box along y-axis.
2187+
z : Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None
2188+
Padding sizes at the left and right boundaries of the box along z-axis.
2189+
2190+
Returns
2191+
-------
2192+
Box
2193+
Padded instance of :class:`Box`.
2194+
"""
2195+
2196+
# Validate that padding values are non-negative
2197+
for axis_name, axis_padding in zip(("x", "y", "z"), (x, y, z)):
2198+
if axis_padding is not None:
2199+
if not isinstance(axis_padding, (tuple, list)) or len(axis_padding) != 2:
2200+
raise ValueError(f"Padding for {axis_name}-axis must be a tuple of two values.")
2201+
if any(p < 0 for p in axis_padding):
2202+
raise ValueError(
2203+
f"Padding values for {axis_name}-axis must be non-negative. Got {axis_padding}."
2204+
)
2205+
2206+
rmin, rmax = self.bounds
2207+
2208+
def bound_array(arrs, idx):
2209+
return np.array([(a[idx] if a is not None else 0) for a in arrs])
2210+
2211+
# parse padding sizes for simulation
2212+
drmin = bound_array((x, y, z), 0)
2213+
drmax = bound_array((x, y, z), 1)
2214+
2215+
rmin = np.array(rmin) - drmin
2216+
rmax = np.array(rmax) + drmax
2217+
2218+
return Box.from_bounds(rmin=rmin, rmax=rmax)
2219+
21732220
@cached_property
21742221
def bounds(self) -> Bound:
21752222
"""Returns bounding box min and max coordinates.

tidy3d/components/simulation.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5834,3 +5834,50 @@ def from_scene(cls, scene: Scene, **kwargs: Any) -> Simulation:
58345834
)
58355835

58365836
_boundaries_for_zero_dims = validate_boundaries_for_zero_dims()
5837+
5838+
def padded_copy(
5839+
self,
5840+
x: Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None,
5841+
y: Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None,
5842+
z: Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None,
5843+
) -> Simulation:
5844+
"""Created a copy of simulation with padded simulation domain.
5845+
5846+
Parameters
5847+
----------
5848+
x : Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None
5849+
Padding sizes at the left and right boundaries of the simulation along x-axis.
5850+
y : Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None
5851+
Padding sizes at the left and right boundaries of the simulation along y-axis.
5852+
z : Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None
5853+
Padding sizes at the left and right boundaries of the simulation along z-axis.
5854+
5855+
Returns
5856+
-------
5857+
Simulation
5858+
Simulation with padded simulation domain.
5859+
"""
5860+
# get simulation bounding box and pad it
5861+
box = Box(center=self.center, size=self.size)
5862+
padded_box = box.padded_copy(x, y, z)
5863+
5864+
return self.updated_copy(size=padded_box.size, center=padded_box.center)
5865+
5866+
def uniformly_padded_copy(self, padding: pydantic.NonNegativeFloat) -> Simulation:
5867+
"""Create copy of simulation with uniformly padded simulation domain.
5868+
5869+
Parameters
5870+
----------
5871+
padding : pydantic.NonNegativeFloat
5872+
Padding size applied uniformly at all simulation boundaries.
5873+
5874+
Returns
5875+
-------
5876+
Simulation
5877+
Simulation with uniformly padded simulation domain.
5878+
"""
5879+
if padding < 0:
5880+
raise ValueError(f"Padding must be non-negative. Got {padding}.")
5881+
5882+
padding_tuple = (padding, padding)
5883+
return self.padded_copy(x=padding_tuple, y=padding_tuple, z=padding_tuple)

0 commit comments

Comments
 (0)