Skip to content

Commit e8258e1

Browse files
feat(rf): add S-parameter de-embedding support to TCM
1 parent 2315f83 commit e8258e1

File tree

4 files changed

+200
-2
lines changed

4 files changed

+200
-2
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
- Added support for `tidy3d-extras`, an optional plugin that enables more accurate local mode solving via subpixel averaging.
1314
- Added support for `symlog` and `log` scale plotting in `Scene.plot_eps()` and `Scene.plot_structures_property()` methods. The `symlog` scale provides linear behavior near zero and logarithmic behavior elsewhere, while 'log' is a base 10 logarithmic scale.
1415
- Added `LowFrequencySmoothingSpec` and `ModelerLowFrequencySmoothingSpec` for automatic smoothing of mode monitor data at low frequencies where DFT sampling is insufficient.

tests/test_plugins/smatrix/test_terminal_component_modeler.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
TerminalPortDataArray,
2525
WavePort,
2626
)
27+
from tidy3d.plugins.smatrix.data.data_array import PortNameDataArray
2728
from tidy3d.plugins.smatrix.ports.base_lumped import AbstractLumpedPort
2829
from tidy3d.plugins.smatrix.utils import s_to_z, validate_square_matrix
2930

@@ -1531,3 +1532,66 @@ def test_low_freq_smoothing_spec_sim_dict():
15311532
modeler = modeler.updated_copy(low_freq_smoothing=None)
15321533
for sim in modeler.sim_dict.values():
15331534
assert sim.low_freq_smoothing is None
1535+
1536+
1537+
def test_S_parameter_deembedding(monkeypatch, tmp_path):
1538+
"""Test S-parameter de-embedding."""
1539+
1540+
z_grid = td.UniformGrid(dl=1 * 1e3)
1541+
xy_grid = td.UniformGrid(dl=0.1 * 1e3)
1542+
grid_spec = td.GridSpec(grid_x=xy_grid, grid_y=xy_grid, grid_z=z_grid)
1543+
modeler = make_coaxial_component_modeler(port_types=(WavePort, WavePort), grid_spec=grid_spec)
1544+
1545+
# Make sure the smatrix and impedance calculations work for reduced simulations
1546+
modeler_data = run_component_modeler(monkeypatch, modeler)
1547+
s_matrix = modeler_data.smatrix()
1548+
1549+
# set up port shifts
1550+
port_names = [port.name for port in modeler.ports]
1551+
coords = {"port": port_names}
1552+
shift_vec = [0, 0]
1553+
port_shifts = PortNameDataArray(data=shift_vec, coords=coords)
1554+
1555+
# make sure that de-embedded S-matrices are identical to the original one if reference planes are not shifted
1556+
S_dmb = modeler_data.change_port_reference_planes(smatrix=s_matrix, port_shifts=port_shifts)
1557+
S_dmb_shortcut = modeler_data.smatrix_deembedded(port_shifts=port_shifts)
1558+
assert np.allclose(S_dmb.data.values, s_matrix.data.values)
1559+
assert np.allclose(S_dmb_shortcut.data.values, s_matrix.data.values)
1560+
1561+
# make sure S-parameters are different if reference planes are moved
1562+
port_shifts = PortNameDataArray(data=[-100, 200], coords=coords)
1563+
S_dmb = modeler_data.change_port_reference_planes(smatrix=s_matrix, port_shifts=port_shifts)
1564+
S_dmb_shortcut = modeler_data.smatrix_deembedded(port_shifts=port_shifts)
1565+
assert not np.allclose(S_dmb.data.values, s_matrix.data.values)
1566+
assert np.allclose(S_dmb.data.values, S_dmb_shortcut.data.values)
1567+
1568+
# test if `.smatrix_deembedded()` raises a `ValueError` when at least one port to be shifted is not defined in TCM
1569+
port_shifts_wrong = PortNameDataArray(data=[10, -10], coords={"port": ["wave_1", "LP_wave_2"]})
1570+
1571+
with pytest.raises(ValueError):
1572+
S_dmb = modeler_data.smatrix_deembedded(port_shifts=port_shifts_wrong)
1573+
1574+
# set up a new TCM with a mixture of `WavePort` and `CoaxialLumpedPort`
1575+
modeler_LP = make_coaxial_component_modeler(
1576+
port_types=(WavePort, CoaxialLumpedPort), grid_spec=grid_spec
1577+
)
1578+
modeler_data_LP = run_component_modeler(monkeypatch, modeler_LP)
1579+
1580+
# test if `.smatrix_deembedded()` raises a `ValueError` when one tries to de-embed a lumped port
1581+
port_shifts_LP = PortNameDataArray(data=[10, -10], coords={"port": ["wave_1", "coax_2"]})
1582+
with pytest.raises(ValueError):
1583+
S_dmb = modeler_data_LP.smatrix_deembedded(port_shifts=port_shifts_LP)
1584+
1585+
# update port shifts so that a reference plane is shifted only for `WavePort` port
1586+
port_shifts_LP = PortNameDataArray(data=[100], coords={"port": ["wave_1"]})
1587+
1588+
# get a new S-matrix
1589+
s_matrix_LP = modeler_data_LP.smatrix()
1590+
1591+
# de-embed S-matrix
1592+
S_dmb = modeler_data_LP.change_port_reference_planes(
1593+
smatrix=s_matrix_LP, port_shifts=port_shifts_LP
1594+
)
1595+
S_dmb_shortcut = modeler_data_LP.smatrix_deembedded(port_shifts=port_shifts_LP)
1596+
assert not np.allclose(S_dmb.data.values, s_matrix_LP.data.values)
1597+
assert np.allclose(S_dmb_shortcut.data.values, S_dmb.data.values)

tidy3d/plugins/smatrix/data/data_array.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,19 @@ class TerminalPortDataArray(DataArray):
6565
__slots__ = ()
6666
_dims = ("f", "port_out", "port_in")
6767
_data_attrs = {"long_name": "terminal-based port matrix element"}
68+
69+
70+
class PortNameDataArray(DataArray):
71+
"""Array of values indexed by port name.
72+
73+
Example
74+
-------
75+
>>> import numpy as np
76+
>>> port_names = ["port1", "port2"]
77+
>>> coords = dict(port_name=port_names)
78+
>>> data = (1 + 1j) * np.random.random((2,))
79+
>>> port_data = PortNameDataArray(data, coords=coords)
80+
"""
81+
82+
__slots__ = ()
83+
_dims = "port_name"

tidy3d/plugins/smatrix/data/terminal.py

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,16 @@
1313
from tidy3d.components.data.sim_data import SimulationData
1414
from tidy3d.components.microwave.base import MicrowaveBaseModel
1515
from tidy3d.components.microwave.data.monitor_data import AntennaMetricsData
16+
from tidy3d.constants import C_0
17+
from tidy3d.log import log
1618
from tidy3d.plugins.smatrix.component_modelers.terminal import TerminalComponentModeler
1719
from tidy3d.plugins.smatrix.data.base import AbstractComponentModelerData
18-
from tidy3d.plugins.smatrix.data.data_array import PortDataArray, TerminalPortDataArray
19-
from tidy3d.plugins.smatrix.ports.types import TerminalPortType
20+
from tidy3d.plugins.smatrix.data.data_array import (
21+
PortDataArray,
22+
PortNameDataArray,
23+
TerminalPortDataArray,
24+
)
25+
from tidy3d.plugins.smatrix.ports.types import LumpedPortType, TerminalPortType
2026
from tidy3d.plugins.smatrix.types import NetworkIndex, SParamDef
2127
from tidy3d.plugins.smatrix.utils import (
2228
ab_to_s,
@@ -117,6 +123,117 @@ def smatrix(
117123
)
118124
return smatrix_data
119125

126+
def change_port_reference_planes(
127+
self, smatrix: MicrowaveSMatrixData, port_shifts: PortNameDataArray = None
128+
) -> MicrowaveSMatrixData:
129+
"""
130+
Performs S-parameter de-embedding by shifting reference planes ``port_shifts`` um.
131+
132+
Parameters
133+
----------
134+
smatrix : :class:`.MicrowaveSMatrixData`
135+
S-parameters before reference planes are shifted.
136+
port_shifts : :class:`.PortNameDataArray`
137+
Data array of shifts of wave ports' reference planes.
138+
The sign of a port shift reflects direction with respect to the axis normal to a ``WavePort`` plane:
139+
E.g.: ``PortNameDataArray(data=-a, coords={"port": "WP1"})`` defines a shift in the first ``WavePort`` by
140+
``a`` um in the direction opposite to the positive axis direction (the axis normal to the port plane).
141+
142+
Returns
143+
-------
144+
:class:`MicrowaveSMatrixData`
145+
De-embedded S-parameters with respect to updated reference frames.
146+
"""
147+
148+
# get s-parameters with respect to current `WavePort` locations
149+
S_matrix = smatrix.data.values
150+
S_new = np.zeros_like(S_matrix, dtype=complex)
151+
N_freq, N_ports, _ = S_matrix.shape
152+
153+
# pre-allocate memory for effective propagation constants
154+
kvecs = np.zeros((N_freq, N_ports), dtype=complex)
155+
shifts_vec = np.zeros(N_ports)
156+
directions_vec = np.ones(N_ports)
157+
158+
port_idxs = []
159+
n_complex_new = []
160+
161+
# extract raw data
162+
key = self.data.keys_tuple[0]
163+
data = self.data[key].data
164+
ports = self.modeler.ports
165+
166+
# get port names and names of ports to be shifted
167+
port_names = [port.name for port in ports]
168+
shift_names = port_shifts.coords["port"].values
169+
170+
# Build a mapping for quick lookup from monitor name to monitor data
171+
mode_map = {mode_data.monitor.name: mode_data for mode_data in data}
172+
173+
# form a numpy vector of port shifts
174+
for shift_name in shift_names:
175+
# ensure that port shifts were defined for valid ports
176+
if shift_name not in port_names:
177+
raise ValueError(
178+
"The specified port could not be found in the simulation! "
179+
f"Please, make sure the port name is from the following list {port_names}"
180+
)
181+
182+
# get index of a shifted port in port_names list
183+
idx = port_names.index(shift_name)
184+
port = ports[idx]
185+
186+
# if de-embedding is requested for lumped port
187+
if isinstance(port, LumpedPortType):
188+
raise ValueError(
189+
"De-embedding currently supports only 'WavePort' instances. "
190+
f"Received type: '{type(port).__name__}'."
191+
)
192+
# alternatively we can send a warning and set `shifts_vector[index]` to 0.
193+
# shifts_vector[index] = 0.0
194+
else:
195+
shifts_vec[idx] = port_shifts.sel(port=shift_name).values
196+
directions_vec[idx] = -1 if port.direction == "-" else 1
197+
port_idxs.append(idx)
198+
199+
# Collect corresponding mode_data
200+
mode_data = mode_map[port._mode_monitor_name]
201+
n_complex = mode_data.n_complex.sel(mode_index=port.mode_index)
202+
n_complex_new.append(np.squeeze(n_complex.data))
203+
204+
# flatten port shift vector
205+
shifts_vec = np.ravel(shifts_vec)
206+
directions_vec = np.ravel(directions_vec)
207+
208+
# Convert to stacked arrays
209+
freqs = np.array(self.modeler.freqs)
210+
n_complex_new = np.array(n_complex_new).T
211+
212+
# construct transformation matrix P_inv
213+
kvecs[:, port_idxs] = 2 * np.pi * freqs[:, np.newaxis] * n_complex_new / C_0
214+
phase = -kvecs * shifts_vec * directions_vec
215+
P_inv = np.exp(1j * phase)
216+
217+
# de-embed S-parameters: S_new = P_inv @ S_matrix @ P_inv
218+
S_new = S_matrix * P_inv[:, :, np.newaxis] * P_inv[:, np.newaxis, :]
219+
220+
# create a new Port Data Array
221+
smat_data = TerminalPortDataArray(S_new, coords=smatrix.data.coords)
222+
223+
return smatrix.updated_copy(data=smat_data)
224+
225+
def smatrix_deembedded(self, port_shifts: np.ndarray = None) -> MicrowaveSMatrixData:
226+
"""Interface function returns de-embedded S-parameter matrix."""
227+
return self.change_port_reference_planes(self.smatrix(), port_shifts=port_shifts)
228+
229+
@pd.root_validator(pre=False)
230+
def _warn_rf_license(cls, values):
231+
log.warning(
232+
"ℹ️ ⚠️ RF simulations are subject to new license requirements in the future. You have instantiated at least one RF-specific component.",
233+
log_once=True,
234+
)
235+
return values
236+
120237
def _monitor_data_at_port_amplitude(
121238
self,
122239
port: TerminalPortType,

0 commit comments

Comments
 (0)