Skip to content

Commit 8d4670b

Browse files
feat(rf): FXC-2053 convenience features for lumped port setup
1 parent e8663e6 commit 8d4670b

File tree

7 files changed

+523
-2
lines changed

7 files changed

+523
-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
@@ -3861,3 +3861,49 @@ def test_validate_microwave_mode_spec():
38613861
sim = sim.updated_copy(
38623862
monitors=[mode_mon],
38633863
)
3864+
3865+
3866+
def test_padded_copy():
3867+
"""Test that padding layers are added along simulation boundaries."""
3868+
grid_spec = td.GridSpec.auto(wavelength=1.0)
3869+
3870+
sim = td.Simulation(
3871+
size=(5, 5, 5),
3872+
grid_spec=grid_spec,
3873+
structures=[
3874+
td.Structure(geometry=td.Box(size=(10, 13, 7)), medium=td.Medium(permittivity=2.0))
3875+
],
3876+
lumped_elements=[],
3877+
run_time=1e-12,
3878+
)
3879+
3880+
padded_sim = sim.padded_copy(x=(4, 10), y=(1, 2))
3881+
assert np.allclose(np.array(padded_sim.size), np.array([19, 8, 5]))
3882+
assert np.allclose(np.array(padded_sim.center), np.array([3, 0.5, 0]))
3883+
3884+
with pytest.raises(ValueError):
3885+
padded_sim = sim.padded_copy(x=(1, -2), z=(-2, 0))
3886+
with pytest.raises(ValueError):
3887+
padded_sim = sim.padded_copy(x=(1))
3888+
3889+
3890+
def test_uniformly_padded_copy():
3891+
"""Test that padding layers are uniformly added along simulation boundaries."""
3892+
grid_spec = td.GridSpec.auto(wavelength=1.0)
3893+
3894+
sim = td.Simulation(
3895+
size=(5, 5, 5),
3896+
grid_spec=grid_spec,
3897+
structures=[
3898+
td.Structure(geometry=td.Box(size=(3, 2, 4)), medium=td.Medium(permittivity=2.0))
3899+
],
3900+
lumped_elements=[],
3901+
run_time=1e-12,
3902+
)
3903+
3904+
padded_sim = sim.uniformly_padded_copy(padding=5)
3905+
assert np.allclose(np.array(padded_sim.size), np.array([15, 15, 15]))
3906+
assert np.allclose(np.array(padded_sim.center), np.array([0, 0, 0]))
3907+
3908+
with pytest.raises(ValueError):
3909+
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
@@ -700,3 +700,48 @@ def make_patch_antenna_modeler(padding: tuple[float, float, float] = (0.25, 0.25
700700
)
701701

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

tests/test_plugins/smatrix/test_terminal_component_modeler.py

Lines changed: 86 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,
@@ -559,6 +560,90 @@ def test_validate_port_voltage_axis():
559560
LumpedPort(center=(0, 0, 0), size=(0, 1, 2), voltage_axis=0, impedance=50)
560561

561562

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

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

0 commit comments

Comments
 (0)