Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions tests/test_components/test_geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,20 @@ def test_box_from_bounds():
assert b.center == (0, 0, 0)


def test_box_padded_copy():
"""Test that padding layers are added along box boundaries."""
box = td.Box(size=(3, 2, 4))
padded_box = box.padded_copy(x=(4, 10), y=(1, 2))
assert np.allclose(np.array(padded_box.size), np.array([17, 5, 4]))
assert np.allclose(np.array(padded_box.center), np.array([3, 0.5, 0]))

# ensure errors are raised if padding format is invalid.
with pytest.raises(ValueError):
padded_box = box.padded_copy(x=(1, -2), z=(-2, 0))
with pytest.raises(ValueError):
padded_box = box.padded_copy(x=(1))


def test_polyslab_center_axis():
"""Test the handling of center_axis in a polyslab having (-td.inf, td.inf) bounds."""
ps = POLYSLAB.copy(update={"slab_bounds": (-td.inf, td.inf)})
Expand Down
46 changes: 46 additions & 0 deletions tests/test_components/test_simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -3861,3 +3861,49 @@ def test_validate_microwave_mode_spec():
sim = sim.updated_copy(
monitors=[mode_mon],
)


def test_padded_copy():
"""Test that padding layers are added along simulation boundaries."""
grid_spec = td.GridSpec.auto(wavelength=1.0)

sim = td.Simulation(
size=(5, 5, 5),
grid_spec=grid_spec,
structures=[
td.Structure(geometry=td.Box(size=(10, 13, 7)), medium=td.Medium(permittivity=2.0))
],
lumped_elements=[],
run_time=1e-12,
)

padded_sim = sim.padded_copy(x=(4, 10), y=(1, 2))
assert np.allclose(np.array(padded_sim.size), np.array([19, 8, 5]))
assert np.allclose(np.array(padded_sim.center), np.array([3, 0.5, 0]))

with pytest.raises(ValueError):
padded_sim = sim.padded_copy(x=(1, -2), z=(-2, 0))
with pytest.raises(ValueError):
padded_sim = sim.padded_copy(x=(1))


def test_uniformly_padded_copy():
"""Test that padding layers are uniformly added along simulation boundaries."""
grid_spec = td.GridSpec.auto(wavelength=1.0)

sim = td.Simulation(
size=(5, 5, 5),
grid_spec=grid_spec,
structures=[
td.Structure(geometry=td.Box(size=(3, 2, 4)), medium=td.Medium(permittivity=2.0))
],
lumped_elements=[],
run_time=1e-12,
)

padded_sim = sim.uniformly_padded_copy(padding=5)
assert np.allclose(np.array(padded_sim.size), np.array([15, 15, 15]))
assert np.allclose(np.array(padded_sim.center), np.array([0, 0, 0]))

with pytest.raises(ValueError):
padded_sim = sim.uniformly_padded_copy(padding=-1)
45 changes: 45 additions & 0 deletions tests/test_plugins/smatrix/terminal_component_modeler_def.py
Original file line number Diff line number Diff line change
Expand Up @@ -700,3 +700,48 @@ def make_patch_antenna_modeler(padding: tuple[float, float, float] = (0.25, 0.25
)

return modeler


def make_basic_filter_terminals():
# Frequency
(f_min, f_max) = (0.1e9, 8e9)

# Materials
med_Cu = td.LossyMetalMedium(conductivity=60, frequency_range=(f_min, f_max))

# Geometry and Structure
mm = 1000 # Conversion mm to micron
H = 0.8 * mm # Substrate thickness
T = 0.035 * mm # Metal thickness
WL = 0.5 * mm
WC = 4 * mm
LC = 5.3 * mm
LL1 = 5.8 * mm
LL2 = 1.2 * mm
LL3 = 11.1 * mm
Lsub = LL1 + WL + LL3
Wsub = 2 * (LC + WL + LL2)

geom_C = td.Box.from_bounds(rmin=(-WC / 2, 0, 0), rmax=(WC / 2, LC, T))
geom_L2 = td.Box.from_bounds(rmin=(-WL / 2, -LL2 - WL, 0), rmax=(WL / 2, 0, T))
geom_L1 = td.Box.from_bounds(rmin=(-WL / 2 - LL1, -LL2 - WL, 0), rmax=(-WL / 2, -LL2, T))
geom_L3 = td.Box.from_bounds(rmin=(WL / 2, -LL2 - WL, 0), rmax=(WL / 2 + LL3, -LL2, T))

geom_resonator_basic = td.GeometryGroup(geometries=[geom_C, geom_L1, geom_L2, geom_L3])

x0, y0, z0 = geom_resonator_basic.bounding_box.center # center (x,y) with circuit
geom_gnd = td.Box(center=(x0, y0, -H - T / 2), size=(Lsub, Wsub, T))

str_gnd = td.Structure(geometry=geom_gnd, medium=med_Cu)
str_resonator_basic = td.Structure(geometry=geom_resonator_basic, medium=med_Cu)

# add second signal trace to test lateral_coord
geom_sign = td.Box.from_bounds(
rmin=(-WL / 2 - LL1, -2 * LL2 - WL, 0), rmax=(WL / 2 + LL3, -2 * LL2, T)
)
geom_resonator_modified = td.GeometryGroup(
geometries=[geom_C, geom_L1, geom_L2, geom_L3, geom_sign]
)
str_resonator_modified = td.Structure(geometry=geom_resonator_modified, medium=med_Cu)

return (str_gnd, str_resonator_basic, str_resonator_modified)
87 changes: 86 additions & 1 deletion tests/test_plugins/smatrix/test_terminal_component_modeler.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from tidy3d import SimulationDataMap
from tidy3d.components.boundary import BroadbandModeABCSpec
from tidy3d.components.data.data_array import FreqDataArray
from tidy3d.exceptions import SetupError, Tidy3dError, Tidy3dKeyError
from tidy3d.exceptions import SetupError, Tidy3dError, Tidy3dKeyError, ValidationError
from tidy3d.plugins.smatrix import (
CoaxialLumpedPort,
LumpedPort,
Expand All @@ -31,6 +31,7 @@

from ...utils import run_emulated
from .terminal_component_modeler_def import (
make_basic_filter_terminals,
make_coaxial_component_modeler,
make_component_modeler,
make_differential_stripline_modeler,
Expand Down Expand Up @@ -559,6 +560,90 @@ def test_validate_port_voltage_axis():
LumpedPort(center=(0, 0, 0), size=(0, 1, 2), voltage_axis=0, impedance=50)


def test_lumped_port_from_structures():
"""Test automatic lumped port setup between two terminal structures."""

# set up terminals of a basic filter
(str_gnd, str_resonator_basic, str_resonator_modified) = make_basic_filter_terminals()

# Geometry and Structure
mm = 1000 # Conversion mm to micron
WL = 0.5 * mm
LL1 = 5.8 * mm
LL2 = 1.2 * mm

# define basic parameters for automatic lumped port setup (except for signal terminal)
lp_options = {
"ground_terminal": str_gnd,
"lateral_coord": -1450,
"voltage_axis": 2,
"impedance": 50,
}

# ensure that value error is triggered if signal and ground terminals are not specified
with pytest.raises(ValueError):
LP0 = LumpedPort.from_structures(x=-WL / 2 - LL1, name="LP1", **lp_options)

# make sure the Lumped port is set correctly
lp_options["signal_terminal"] = str_resonator_basic
LP1 = LumpedPort.from_structures(x=-WL / 2 - LL1, name="LP1", **lp_options)
assert LP1.voltage_axis == lp_options["voltage_axis"]
assert np.isclose(LP1.impedance, lp_options["impedance"])

# make sure that `port_width` does not cause port geometry exceed overlap of terminals
lp_options["port_width"] = 1000
with pytest.raises(ValueError):
LP2 = LumpedPort.from_structures(x=-WL / 2 - LL1, name="LP1", **lp_options)

lp_options["port_width"] = WL / 3
LP2 = LumpedPort.from_structures(x=-WL / 2 - LL1, name="LP2", **lp_options)
assert np.isclose(LP2.size[1], WL / 3)

# specify lateral coordinate to select appropriate signal terminal to resolve ambiguity
lp_options["signal_terminal"] = str_resonator_modified
lp_options["lateral_coord"] = -2 * LL2 - WL / 2
lp_options["port_width"] = None
LP3 = LumpedPort.from_structures(x=-WL / 2 - LL1, name="LP3", **lp_options)
assert np.isclose(LP3.center[1], -2 * LL2 - WL / 2)

# test that an error is raised when port plane does not intersect any signal terminals
with pytest.raises(ValueError):
LP3 = LumpedPort.from_structures(y=8 * mm, name="LP3", **lp_options)

# ensure that error is raised
with pytest.raises(ValueError):
lp_options["voltage_axis"] = 0
LP3 = LumpedPort.from_structures(x=-WL / 2 - LL1, name="LP3", **lp_options)

# test port width with lateral coords
lp_options["port_width"] = WL / 3
lp_options["voltage_axis"] = 2
LP4 = LumpedPort.from_structures(x=-WL / 2 - LL1, name="LP4", **lp_options)
assert np.isclose(LP4.size[1], lp_options["port_width"])

# test port width with lateral coords
lp_options["lateral_coord"] = None
with pytest.raises(ValidationError):
LP5 = LumpedPort.from_structures(x=-WL / 2 - LL1, name="LP5", **lp_options)

lp_options["lateral_coord"] = 10 * WL
with pytest.raises(ValidationError):
LP6 = LumpedPort.from_structures(x=-WL / 2 - LL1, name="LP6", **lp_options)

# ensure that validation error is raised when specified port width exceeds terminal overlap in lateral direction.
lp_options["port_width"] = 4 * WL
lp_options["lateral_coord"] = -2 * LL2 - WL / 2
with pytest.raises(ValueError):
LP6 = LumpedPort.from_structures(x=-WL / 2 - LL1, name="LP6", **lp_options)

# ensure that validation error is raised when terminal medium is not PEC or lossy metal
str_gnd_new = str_gnd.updated_copy(medium=td.Medium(conductivity=1e2))
lp_options["lateral_coord"] = -2 * LL2 - WL / 2
lp_options["ground_terminal"] = str_gnd_new
with pytest.raises(ValidationError):
LP7 = LumpedPort.from_structures(x=-WL / 2 - LL1, name="LP7", **lp_options)


@pytest.mark.parametrize("snap_center", [None, 0.1])
def test_converting_port_to_simulation_objects(snap_center):
"""Test that the LumpedPort can be converted into monitors and source without the grid present."""
Expand Down
47 changes: 47 additions & 0 deletions tidy3d/components/geometry/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2170,6 +2170,53 @@ def intersections_with(self, other):
shapely_box = Geometry.evaluate_inf_shape(shapely_box)
return [Geometry.evaluate_inf_shape(shape) & shapely_box for shape in shapes_plane]

def padded_copy(
self,
x: Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None,
y: Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None,
z: Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None,
) -> Box:
"""Created a padded copy of a :class:`Box` instance.

Parameters
----------
x : Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None
Padding sizes at the left and right boundaries of the box along x-axis.
y : Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None
Padding sizes at the left and right boundaries of the box along y-axis.
z : Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None
Padding sizes at the left and right boundaries of the box along z-axis.

Returns
-------
Box
Padded instance of :class:`Box`.
"""

# Validate that padding values are non-negative
for axis_name, axis_padding in zip(("x", "y", "z"), (x, y, z)):
if axis_padding is not None:
if not isinstance(axis_padding, (tuple, list)) or len(axis_padding) != 2:
raise ValueError(f"Padding for {axis_name}-axis must be a tuple of two values.")
if any(p < 0 for p in axis_padding):
raise ValueError(
f"Padding values for {axis_name}-axis must be non-negative. Got {axis_padding}."
)

rmin, rmax = self.bounds

def bound_array(arrs, idx):
return np.array([(a[idx] if a is not None else 0) for a in arrs])

# parse padding sizes for simulation
drmin = bound_array((x, y, z), 0)
drmax = bound_array((x, y, z), 1)

rmin = np.array(rmin) - drmin
rmax = np.array(rmax) + drmax

return Box.from_bounds(rmin=rmin, rmax=rmax)

@cached_property
def bounds(self) -> Bound:
"""Returns bounding box min and max coordinates.
Expand Down
47 changes: 47 additions & 0 deletions tidy3d/components/simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -5816,3 +5816,50 @@ def from_scene(cls, scene: Scene, **kwargs: Any) -> Simulation:
)

_boundaries_for_zero_dims = validate_boundaries_for_zero_dims()

def padded_copy(
self,
x: Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None,
y: Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None,
z: Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None,
) -> Simulation:
"""Created a copy of simulation with padded simulation domain.

Parameters
----------
x : Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None
Padding sizes at the left and right boundaries of the simulation along x-axis.
y : Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None
Padding sizes at the left and right boundaries of the simulation along y-axis.
z : Optional[tuple[pydantic.NonNegativeFloat, pydantic.NonNegativeFloat]] = None
Padding sizes at the left and right boundaries of the simulation along z-axis.

Returns
-------
Simulation
Simulation with padded simulation domain.
"""
# get simulation bounding box and pad it
box = Box(center=self.center, size=self.size)
padded_box = box.padded_copy(x, y, z)

return self.updated_copy(size=padded_box.size, center=padded_box.center)

def uniformly_padded_copy(self, padding: pydantic.NonNegativeFloat) -> Simulation:
"""Create copy of simulation with uniformly padded simulation domain.

Parameters
----------
padding : pydantic.NonNegativeFloat
Padding size applied uniformly at all simulation boundaries.

Returns
-------
Simulation
Simulation with uniformly padded simulation domain.
"""
if padding < 0:
raise ValueError(f"Padding must be non-negative. Got {padding}.")

padding_tuple = (padding, padding)
return self.padded_copy(x=padding_tuple, y=padding_tuple, z=padding_tuple)
Loading