Skip to content

Commit 183e3aa

Browse files
WIP: local changes before rebase
1 parent d7b443a commit 183e3aa

File tree

4 files changed

+151
-45
lines changed

4 files changed

+151
-45
lines changed

CHANGELOG.md

Lines changed: 2 additions & 2 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

1415
### Changed
@@ -21,8 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2122

2223
## [v2.10.0rc2] - 2025-10-01
2324

24-
### Added
25-
- Added S-parameter de-embedding to `TerminalComponentModelerData`, enabling recalculation with shifted reference planes.
25+
### Added
2626
- New `MediumMonitor` that returns both permittivity and permeability profiles.
2727
- 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.
2828
- Enabled lazy loading of data via `web.load(..., lazy=True)`. When used, this returns a lightweight proxy object holding a reference to the data. On first access to any field or method, the proxy transparently loads the full object (same as with the default lazy=False).

tests/test_plugins/smatrix/test_terminal_component_modeler.py

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
TerminalPortDataArray,
3030
WavePort,
3131
)
32+
from tidy3d.plugins.smatrix.data.data_array import PortNameDataArray
3233
from tidy3d.plugins.smatrix.ports.base_lumped import AbstractLumpedPort
3334
from tidy3d.plugins.smatrix.utils import s_to_z, validate_square_matrix
3435

@@ -1421,29 +1422,57 @@ def test_S_parameter_deembedding(monkeypatch, tmp_path):
14211422
xy_grid = td.UniformGrid(dl=0.1 * 1e3)
14221423
grid_spec = td.GridSpec(grid_x=xy_grid, grid_y=xy_grid, grid_z=z_grid)
14231424
modeler = make_coaxial_component_modeler(port_types=(WavePort, WavePort), grid_spec=grid_spec)
1424-
port0_idx = modeler.network_index(modeler.ports[0])
1425-
port1_idx = modeler.network_index(modeler.ports[1])
1426-
modeler_run1 = modeler.updated_copy(run_only=(port0_idx,))
14271425

14281426
# Make sure the smatrix and impedance calculations work for reduced simulations
14291427
modeler_data = run_component_modeler(monkeypatch, modeler)
14301428
s_matrix = modeler_data.smatrix()
14311429

1432-
# test for invalid dimensions in port shifts
1433-
port_shifts = np.array([0])
1434-
with pytest.raises(ValueError):
1435-
S_dmb = modeler_data.change_port_reference_planes(smatrix=s_matrix, port_shifts=port_shifts)
1436-
1437-
port_shifts = np.zeros((3, 1))
1438-
with pytest.raises(ValueError):
1439-
S_dmb = modeler_data.change_port_reference_planes(smatrix=s_matrix, port_shifts=port_shifts)
1430+
# set up port shifts
1431+
port_names = [port.name for port in modeler.ports]
1432+
coords = {"port": port_names}
1433+
shift_vec = [0, 0]
1434+
port_shifts = PortNameDataArray(data=shift_vec, coords=coords)
14401435

1441-
# ensure matrices are identical if there is no shift in reference planes
1442-
port_shifts = np.array([0, 0])
1436+
# make sure that de-embedded S-matrices are identical to the original one if reference planes are not shifted
14431437
S_dmb = modeler_data.change_port_reference_planes(smatrix=s_matrix, port_shifts=port_shifts)
1438+
S_dmb_shortcut = modeler_data.smatrix_deembedded(port_shifts=port_shifts)
14441439
assert np.allclose(S_dmb.data.values, s_matrix.data.values)
1440+
assert np.allclose(S_dmb_shortcut.data.values, s_matrix.data.values)
14451441

14461442
# make sure S-parameters are different if reference planes are moved
1447-
port_shifts = np.array([-2, 10])
1443+
port_shifts = PortNameDataArray(data=[-100, 200], coords=coords)
14481444
S_dmb = modeler_data.change_port_reference_planes(smatrix=s_matrix, port_shifts=port_shifts)
1445+
S_dmb_shortcut = modeler_data.smatrix_deembedded(port_shifts=port_shifts)
14491446
assert not np.allclose(S_dmb.data.values, s_matrix.data.values)
1447+
assert np.allclose(S_dmb.data.values, S_dmb_shortcut.data.values)
1448+
1449+
# test if `.smatrix_deembedded()` raises a `ValueError` when at least one port to be shifted is not defined in TCM
1450+
port_shifts_wrong = PortNameDataArray(data=[10, -10], coords={"port": ["wave_1", "LP_wave_2"]})
1451+
1452+
with pytest.raises(ValueError):
1453+
S_dmb = modeler_data.smatrix_deembedded(port_shifts=port_shifts_wrong)
1454+
1455+
# set up a new TCM with a mixture of `WavePort` and `CoaxialLumpedPort`
1456+
modeler_LP = make_coaxial_component_modeler(
1457+
port_types=(WavePort, CoaxialLumpedPort), grid_spec=grid_spec
1458+
)
1459+
modeler_data_LP = run_component_modeler(monkeypatch, modeler_LP)
1460+
1461+
# test if `.smatrix_deembedded()` raises a `ValueError` when one tries to de-embed a lumped port
1462+
port_shifts_LP = PortNameDataArray(data=[10, -10], coords={"port": ["wave_1", "coax_2"]})
1463+
with pytest.raises(ValueError):
1464+
S_dmb = modeler_data_LP.smatrix_deembedded(port_shifts=port_shifts_LP)
1465+
1466+
# update port shifts so that a reference plane is shifted only for `WavePort` port
1467+
port_shifts_LP = PortNameDataArray(data=[100], coords={"port": ["wave_1"]})
1468+
1469+
# get a new S-matrix
1470+
s_matrix_LP = modeler_data_LP.smatrix()
1471+
1472+
# de-embed S-matrix
1473+
S_dmb = modeler_data_LP.change_port_reference_planes(
1474+
smatrix=s_matrix_LP, port_shifts=port_shifts_LP
1475+
)
1476+
S_dmb_shortcut = modeler_data_LP.smatrix_deembedded(port_shifts=port_shifts_LP)
1477+
assert not np.allclose(S_dmb.data.values, s_matrix_LP.data.values)
1478+
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
@@ -84,3 +84,19 @@ def _warn_rf_license(cls, values):
8484
log_once=True,
8585
)
8686
return values
87+
88+
89+
class PortNameDataArray(DataArray):
90+
"""Port shifts Data Array.
91+
92+
Example
93+
-------
94+
>>> import numpy as np
95+
>>> port_names = ["port1", "port2"]
96+
>>> coords = dict(port_name=port_names)
97+
>>> data = (1 + 1j) * np.random.random((2,))
98+
>>> port_data = PortNameDataArray(data, coords=coords)
99+
"""
100+
101+
__slots__ = ()
102+
_dims = "port_name"

tidy3d/plugins/smatrix/data/terminal.py

Lines changed: 90 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import numpy as np
88
import pydantic.v1 as pd
99

10-
from tidy3d import ModeData
1110
from tidy3d.components.base import Tidy3dBaseModel, cached_property
1211
from tidy3d.components.data.data_array import FreqDataArray
1312
from tidy3d.components.data.monitor_data import MonitorData
@@ -17,8 +16,12 @@
1716
from tidy3d.log import log
1817
from tidy3d.plugins.smatrix.component_modelers.terminal import TerminalComponentModeler
1918
from tidy3d.plugins.smatrix.data.base import AbstractComponentModelerData
20-
from tidy3d.plugins.smatrix.data.data_array import PortDataArray, TerminalPortDataArray
21-
from tidy3d.plugins.smatrix.ports.types import TerminalPortType
19+
from tidy3d.plugins.smatrix.data.data_array import (
20+
PortDataArray,
21+
PortNameDataArray,
22+
TerminalPortDataArray,
23+
)
24+
from tidy3d.plugins.smatrix.ports.types import LumpedPortType, TerminalPortType
2225
from tidy3d.plugins.smatrix.types import SParamDef
2326
from tidy3d.plugins.smatrix.utils import (
2427
ab_to_s,
@@ -120,17 +123,20 @@ def smatrix(
120123
return smatrix_data
121124

122125
def change_port_reference_planes(
123-
self, smatrix: MicrowaveSMatrixData, port_shifts: np.ndarray
126+
self, smatrix: MicrowaveSMatrixData, port_shifts: PortNameDataArray = None
124127
) -> MicrowaveSMatrixData:
125128
"""
126-
Performs S-parameter de-embedding by shifting reference planes `port_shifts` um.
129+
Performs S-parameter de-embedding by shifting reference planes ``port_shifts`` um.
127130
128131
Parameters
129132
----------
130133
smatrix : :class:`.MicrowaveSMatrixData`
131134
S-parameters before reference planes are shifted.
132-
port_shifts : np.ndarray
135+
port_shifts : :class:`.PortNameDataArray`
133136
Numpy array of shifts of wave ports' reference planes.
137+
The sign of a port shift reflects direction with respect to the axis normal to a ``WavePort`` plane:
138+
E.g.: ``port_shift[0]==-a`` defines a shift in the first ``WavePort`` by
139+
``a`` um in direction opposite to the positive axis direction (the axis normal to the port plane).
134140
135141
Returns
136142
-------
@@ -143,41 +149,96 @@ def change_port_reference_planes(
143149
S_new = np.zeros_like(S_matrix, dtype=complex)
144150
N_freq, N_ports, _ = S_matrix.shape
145151

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 `WavePort`'s 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-
157152
# pre-allocate memory for effective propagation constants
158-
kvecs = np.zeros((N_ports, N_freq), dtype=complex)
153+
kvecs = np.zeros((N_freq, N_ports), dtype=complex)
154+
shifts_vec = np.zeros(N_ports)
155+
directions_vec = np.ones(N_ports)
156+
157+
modes_data = []
158+
mode_indices = []
159+
port_idxs = []
159160

160-
# extract mode data
161+
# extract raw data
161162
key = self.data.keys_tuple[0]
162163
data = self.data[key].data
163-
modes_data = tuple(mode_data for mode_data in data if isinstance(mode_data, ModeData))
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+
mode_indices.append(port.mode_index)
198+
port_idxs.append(idx)
199+
200+
# Collect corresponding mode_data
201+
modes_data.append(mode_map[port._mode_monitor_name])
202+
203+
# Convert modes_data to tuple (same as original)
204+
modes_data = tuple(modes_data)
205+
206+
# flatten port shift vector
207+
shifts_vec = np.ravel(shifts_vec)
208+
directions_vec = np.ravel(directions_vec)
209+
210+
# Collect relevant propagation constants and frequencies
211+
n_complex_new, freqs = zip(
212+
*(
213+
(
214+
np.squeeze(mode_data.n_complex.sel(mode_index=mode_indices[i]).data),
215+
np.squeeze(mode_data.n_complex.f),
216+
)
217+
for i, mode_data in enumerate(modes_data)
218+
)
219+
)
164220

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()
221+
# Convert to stacked arrays
222+
n_complex_new = np.stack(n_complex_new, axis=1)
223+
freqs = np.stack(freqs, axis=1)
169224

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
225+
# construct transformation matrix P_inv
226+
kvecs[:, port_idxs] = 2 * np.pi * freqs * n_complex_new / C_0
227+
phase = -kvecs * shifts_vec * directions_vec
228+
P_inv = np.exp(1j * phase)
229+
230+
# de-embed S-parameters: S_new = P_inv @ S_matrix @ P_inv
231+
S_new = S_matrix * P_inv[:, :, np.newaxis] * P_inv[:, np.newaxis, :]
175232

176233
# create a new Port Data Array
177234
smat_data = TerminalPortDataArray(S_new, coords=smatrix.data.coords)
178235

179236
return smatrix.updated_copy(data=smat_data)
180237

238+
def smatrix_deembedded(self, port_shifts: np.ndarray = None) -> MicrowaveSMatrixData:
239+
"""Interface function returns de-embedded S-parameter matrix."""
240+
return self.change_port_reference_planes(self.smatrix(), port_shifts=port_shifts)
241+
181242
@pd.root_validator(pre=False)
182243
def _warn_rf_license(cls, values):
183244
log.warning(

0 commit comments

Comments
 (0)