Skip to content

Commit c18c1f0

Browse files
feat: add S-parameter de-embedding support to TerminalComponentModelerData
1 parent 70af873 commit c18c1f0

File tree

3 files changed

+97
-0
lines changed

3 files changed

+97
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
## [Unreleased]
1010

1111
### Added
12+
- Added S-parameter de-embedding to `TerminalComponentModelerData`, enabling recalculation with shifted reference planes.
1213
- New `MediumMonitor` that returns both permittivity and permeability profiles.
1314
- Task names are now optional when using `run(sim)` or `Job`. When running multiple jobs (via `run_async` or `Batch`), you can also provide simulations as a list without specifying task names. The previous dictionary-based format with explicit task names is still supported.
1415
- Added support for `tidy3d-extras`, an optional plugin that enables more accurate local mode solving via subpixel averaging.

tests/test_plugins/smatrix/test_terminal_component_modeler.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1392,3 +1392,38 @@ def test_wave_port_to_absorber(tmp_path):
13921392
sim = list(modeler.sim_dict.values())[0]
13931393
absorber = sim.internal_absorbers[0]
13941394
assert absorber.boundary_spec == custom_boundary_spec
1395+
1396+
1397+
def test_S_parameter_deembedding(monkeypatch, tmp_path):
1398+
"""Test S-parameter de-embedding."""
1399+
1400+
z_grid = td.UniformGrid(dl=1 * 1e3)
1401+
xy_grid = td.UniformGrid(dl=0.1 * 1e3)
1402+
grid_spec = td.GridSpec(grid_x=xy_grid, grid_y=xy_grid, grid_z=z_grid)
1403+
modeler = make_coaxial_component_modeler(port_types=(WavePort, WavePort), grid_spec=grid_spec)
1404+
port0_idx = modeler.network_index(modeler.ports[0])
1405+
port1_idx = modeler.network_index(modeler.ports[1])
1406+
modeler_run1 = modeler.updated_copy(run_only=(port0_idx,))
1407+
1408+
# Make sure the smatrix and impedance calculations work for reduced simulations
1409+
modeler_data = run_component_modeler(monkeypatch, modeler)
1410+
s_matrix = modeler_data.smatrix()
1411+
1412+
# test for invalid dimensions in port shifts
1413+
port_shifts = np.array([0])
1414+
with pytest.raises(ValueError):
1415+
S_dmb = modeler_data.change_port_reference_planes(smatrix=s_matrix, port_shifts=port_shifts)
1416+
1417+
port_shifts = np.zeros((3, 1))
1418+
with pytest.raises(ValueError):
1419+
S_dmb = modeler_data.change_port_reference_planes(smatrix=s_matrix, port_shifts=port_shifts)
1420+
1421+
# ensure matrices are identical if there is no shift in reference planes
1422+
port_shifts = np.array([0, 0])
1423+
S_dmb = modeler_data.change_port_reference_planes(smatrix=s_matrix, port_shifts=port_shifts)
1424+
assert np.allclose(S_dmb.data.values, s_matrix.data.values)
1425+
1426+
# make sure S-parameters are different if reference planes are moved
1427+
port_shifts = np.array([-2, 10])
1428+
S_dmb = modeler_data.change_port_reference_planes(smatrix=s_matrix, port_shifts=port_shifts)
1429+
assert not np.allclose(S_dmb.data.values, s_matrix.data.values)

tidy3d/plugins/smatrix/data/terminal.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
import numpy as np
88
import pydantic.v1 as pd
99

10+
from tidy3d import ModeData
1011
from tidy3d.components.base import Tidy3dBaseModel, cached_property
1112
from tidy3d.components.data.data_array import FreqDataArray
1213
from tidy3d.components.data.monitor_data import MonitorData
1314
from tidy3d.components.data.sim_data import SimulationData
1415
from tidy3d.components.microwave.data.monitor_data import AntennaMetricsData
16+
from tidy3d.constants import C_0
1517
from tidy3d.log import log
1618
from tidy3d.plugins.smatrix.component_modelers.terminal import TerminalComponentModeler
1719
from tidy3d.plugins.smatrix.data.base import AbstractComponentModelerData
@@ -117,6 +119,65 @@ def smatrix(
117119
)
118120
return smatrix_data
119121

122+
def change_port_reference_planes(
123+
self, smatrix: MicrowaveSMatrixData, port_shifts: np.array
124+
) -> MicrowaveSMatrixData:
125+
"""
126+
Performs S-parameter de-embedding by shifting reference planes `port_shifts` um.
127+
128+
Parameters
129+
----------
130+
smatrix: :class:`.MicrowaveSMatrixData`
131+
S-parameters before reference planes are shifted.
132+
port_shifts: np.array
133+
Numpy array of shifts of wave ports' reference planes.
134+
135+
Returns
136+
-------
137+
:class:`MicrowaveSMatrixData`
138+
De-embedded S-parameters with respect to updated reference frames.
139+
"""
140+
141+
# get s-parameters with respect to current `WavePort` locations
142+
S_matrix = smatrix.data.values
143+
S_new = np.zeros_like(S_matrix, dtype=complex)
144+
N_freq, N_ports, _ = S_matrix.shape
145+
146+
if len(port_shifts) != N_ports:
147+
raise ValueError(
148+
"A vector of WavePort reference plane shifts has to match a total number of Waveports in a simulation."
149+
f"The expected length was {N_ports}, while a vector of {len(port_shifts)} was provided."
150+
)
151+
152+
# extract `ModeSource` directions to ensure correct sign is used
153+
port_shifts = np.ravel(port_shifts)
154+
directions = np.array([1 if port.direction == "+" else -1 for port in self.modeler.ports])
155+
directions = np.ravel(directions)
156+
157+
# pre-allocate memory for effective propagation constants
158+
kvecs = np.zeros((N_ports, N_freq), dtype=complex)
159+
160+
# extract mode data
161+
key = self.data.keys_tuple[0]
162+
data = self.data[key].data
163+
modes_data = tuple(mode_data for mode_data in data if isinstance(mode_data, ModeData))
164+
165+
# infer propagation constants from modal data
166+
for i, mode_data in enumerate(modes_data):
167+
n_complex = mode_data.n_complex
168+
kvecs[i, :] = (2 * np.pi * n_complex.f * n_complex / C_0).squeeze()
169+
170+
# updated/de-embed S-parameters with respect to shifted reference planes
171+
for i in range(N_freq):
172+
phase = kvecs[:, i] * port_shifts * directions
173+
P_inv = np.diag(np.exp(-1j * phase))
174+
S_new[i, :, :] = P_inv @ S_matrix[i, :, :] @ P_inv
175+
176+
# create a new Port Data Array
177+
smat_data = TerminalPortDataArray(S_new, coords=smatrix.data.coords)
178+
179+
return smatrix.updated_copy(data=smat_data)
180+
120181
@pd.root_validator(pre=False)
121182
def _warn_rf_license(cls, values):
122183
log.warning(

0 commit comments

Comments
 (0)