Skip to content

Commit d0f1fc3

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

File tree

4 files changed

+352
-2
lines changed

4 files changed

+352
-2
lines changed

tests/test_plugins/smatrix/terminal_component_modeler_def.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,3 +474,48 @@ def make_differential_stripline_modeler():
474474
)
475475

476476
return tcm
477+
478+
479+
def make_basic_filter_terminals():
480+
# Frequency
481+
(f_min, f_max) = (0.1e9, 8e9)
482+
483+
# Materials
484+
med_Cu = td.LossyMetalMedium(conductivity=60, frequency_range=(f_min, f_max))
485+
486+
# Geometry and Structure
487+
mm = 1000 # Conversion mm to micron
488+
H = 0.8 * mm # Substrate thickness
489+
T = 0.035 * mm # Metal thickness
490+
WL = 0.5 * mm
491+
WC = 4 * mm
492+
LC = 5.3 * mm
493+
LL1 = 5.8 * mm
494+
LL2 = 1.2 * mm
495+
LL3 = 11.1 * mm
496+
Lsub = LL1 + WL + LL3
497+
Wsub = 2 * (LC + WL + LL2)
498+
499+
geom_C = td.Box.from_bounds(rmin=(-WC / 2, 0, 0), rmax=(WC / 2, LC, T))
500+
geom_L2 = td.Box.from_bounds(rmin=(-WL / 2, -LL2 - WL, 0), rmax=(WL / 2, 0, T))
501+
geom_L1 = td.Box.from_bounds(rmin=(-WL / 2 - LL1, -LL2 - WL, 0), rmax=(-WL / 2, -LL2, T))
502+
geom_L3 = td.Box.from_bounds(rmin=(WL / 2, -LL2 - WL, 0), rmax=(WL / 2 + LL3, -LL2, T))
503+
504+
geom_resonator_basic = td.GeometryGroup(geometries=[geom_C, geom_L1, geom_L2, geom_L3])
505+
506+
x0, y0, z0 = geom_resonator_basic.bounding_box.center # center (x,y) with circuit
507+
geom_gnd = td.Box(center=(x0, y0, -H - T / 2), size=(Lsub, Wsub, T))
508+
509+
str_gnd = td.Structure(geometry=geom_gnd, medium=med_Cu)
510+
str_resonator_basic = td.Structure(geometry=geom_resonator_basic, medium=med_Cu)
511+
512+
# add second signal trace to test lateral_coord
513+
geom_sign = td.Box.from_bounds(
514+
rmin=(-WL / 2 - LL1, -2 * LL2 - WL, 0), rmax=(WL / 2 + LL3, -2 * LL2, T)
515+
)
516+
geom_resonator_modified = td.GeometryGroup(
517+
geometries=[geom_C, geom_L1, geom_L2, geom_L3, geom_sign]
518+
)
519+
str_resonator_modified = td.Structure(geometry=geom_resonator_modified, medium=med_Cu)
520+
521+
return (str_gnd, str_resonator_basic, str_resonator_modified)

tests/test_plugins/smatrix/test_terminal_component_modeler.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
make_coaxial_component_modeler,
3434
make_component_modeler,
3535
make_differential_stripline_modeler,
36+
make_basic_filter_terminals,
3637
)
3738

3839
mm = 1e3
@@ -555,6 +556,54 @@ def test_validate_port_voltage_axis():
555556
LumpedPort(center=(0, 0, 0), size=(0, 1, 2), voltage_axis=0, impedance=50)
556557

557558

559+
def test_lumped_port_from_structures():
560+
"""Test automatic lumped port setup between two terminal structures."""
561+
562+
# set up terminals of a basic filter
563+
(str_gnd, str_resonator_basic, str_resonator_modified) = make_basic_filter_terminals()
564+
565+
# Geometry and Structure
566+
mm = 1000 # Conversion mm to micron
567+
WL = 0.5 * mm
568+
LL1 = 5.8 * mm
569+
LL2 = 1.2 * mm
570+
571+
# define basic parameters for automatic lumped port setup (except for signal terminal)
572+
lp_options = {
573+
"ground_terminal": str_gnd,
574+
"lateral_coord": -1450,
575+
"voltage_axis": 2,
576+
"impedance": 50,
577+
}
578+
579+
# ensure that value error is triggered if signal and ground terminals are not specified
580+
with pytest.raises(ValueError):
581+
LP0 = LumpedPort.from_structures(x=-WL / 2 - LL1, name="LP1", **lp_options)
582+
583+
# make sure the Lumped port is set correctly
584+
lp_options["signal_terminal"] = str_resonator_basic
585+
LP1 = LumpedPort.from_structures(x=-WL / 2 - LL1, name="LP1", **lp_options)
586+
assert LP1.voltage_axis == lp_options["voltage_axis"]
587+
assert np.isclose(LP1.impedance, lp_options["impedance"])
588+
589+
# make sure that `port_width` does not cause port geometry exceed overlap of terminals
590+
lp_options["port_width"] = 1000
591+
with pytest.raises(ValueError):
592+
LP2 = LumpedPort.from_structures(x=-WL / 2 - LL1, name="LP1", **lp_options)
593+
594+
# specify lateral coordinate to select appropriate signal terminal to resolve ambiguity
595+
lp_options["signal_terminal"] = str_resonator_modified
596+
lp_options["lateral_coord"] = -2 * LL2 - WL / 2
597+
lp_options["port_width"] = None
598+
LP3 = LumpedPort.from_structures(x=-WL / 2 - LL1, name="LP3", **lp_options)
599+
assert np.isclose(LP3.center[1], -2 * LL2 - WL / 2)
600+
601+
# test port width with lateral coords
602+
lp_options["port_width"] = WL / 3
603+
LP4 = LumpedPort.from_structures(x=-WL / 2 - LL1, name="LP4", **lp_options)
604+
assert np.isclose(LP4.size[1], lp_options["port_width"])
605+
606+
558607
@pytest.mark.parametrize("snap_center", [None, 0.1])
559608
def test_converting_port_to_simulation_objects(snap_center):
560609
"""Test that the LumpedPort can be converted into monitors and source without the grid present."""

tidy3d/components/simulation.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5830,3 +5830,57 @@ def from_scene(cls, scene: Scene, **kwargs) -> Simulation:
58305830
)
58315831

58325832
_boundaries_for_zero_dims = validate_boundaries_for_zero_dims()
5833+
5834+
def add_padding(
5835+
self,
5836+
x: Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None,
5837+
y: Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None,
5838+
z: Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None,
5839+
) -> Simulation:
5840+
"""Created a copy of simulation with padded simulation domain.
5841+
5842+
Parameters
5843+
----------
5844+
x : Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None
5845+
Padding sizes at the left and right boundaries of the simulation along x-axis.
5846+
y : Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None
5847+
Padding sizes at the left and right boundaries of the simulation along y-axis.
5848+
z : Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None
5849+
Padding sizes at the left and right boundaries of the simulation along z-axis.
5850+
5851+
Returns
5852+
-------
5853+
Simulation
5854+
Simulation with padded simulation domain.
5855+
"""
5856+
box = Box(center=self.center, size=self.size)
5857+
rmin, rmax = box.bounds
5858+
5859+
def bound_array(arrs, idx):
5860+
return np.array([(a[idx] if a is not None else 0) for a in arrs])
5861+
5862+
# parse padding sizes for simulation
5863+
drmin = bound_array((x, y, z), 0)
5864+
drmax = bound_array((x, y, z), 1)
5865+
5866+
rmin = np.array(rmin) - drmin
5867+
rmax = np.array(rmax) + drmax
5868+
new_box = Box.from_bounds(rmin=rmin, rmax=rmax)
5869+
5870+
return self.updated_copy(size=new_box.size, center=new_box.center)
5871+
5872+
def add_uniform_padding(self, padding: pydantic.NonNegativeFloat) -> Simulation:
5873+
"""Create copy of simulation with uniformly padded simulation domain.
5874+
5875+
Parameters
5876+
----------
5877+
padding : pydantic.NonNegativeFloat
5878+
Padding size applied uniformly at all simulation boundaries.
5879+
5880+
Returns
5881+
-------
5882+
Simulation
5883+
Simulation with uniformly padded simulation domain.
5884+
"""
5885+
padding_tuple = (padding, padding)
5886+
return self.add_padding(x=padding_tuple, y=padding_tuple, z=padding_tuple)

0 commit comments

Comments
 (0)