Skip to content

Commit 1e179bf

Browse files
committed
feat(rf): Add symmetric_pseudo option for s_param_def
1 parent f7dd912 commit 1e179bf

8 files changed

Lines changed: 183 additions & 56 deletions

File tree

CHANGELOG.md

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

2020
### Changed
2121
- `ModeSortSpec.sort_key` is now required with a default of `"n_eff"` (previously optional with `None` default). `ModeSortSpec.sort_order` is now optional with a default of `None`, which automatically selects the natural order based on `sort_key` and `sort_reference`: ascending when a reference is provided (closest first), otherwise descending for `n_eff` and polarization fractions (higher values first), ascending for `k_eff` and `mode_area` (lower values first).
22+
- Added `symmetric_pseudo` option for `s_param_def` in `TerminalComponentModeler` which applies a scaling factor that ensures the S-matrix is symmetric in reciprocal systems.
2223

2324
### Fixed
2425
- Fixed intermittent "API key not found" errors in parallel job launches by making configuration directory detection race-safe.

schemas/TerminalComponentModeler.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18345,7 +18345,8 @@
1834518345
"default": "pseudo",
1834618346
"enum": [
1834718347
"power",
18348-
"pseudo"
18348+
"pseudo",
18349+
"symmetric_pseudo"
1834918350
],
1835018351
"type": "string"
1835118352
},

tests/test_plugins/smatrix/test_terminal_component_modeler.py

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def make_t_network_impedance_matrix(
121121
return np.array([[z11, z12], [z21, z22]])
122122

123123

124-
def calc_transmission_line_S_matrix_pseudo(Z0, Zref1, Zref2, gamma, length):
124+
def calc_transmission_line_S_matrix_pseudo(Z0, Zref1, Zref2, gamma, length, symmetric=False):
125125
"""
126126
Calculate complete 2x2 S-parameter matrix for a transmission line
127127
using pseudo wave definition
@@ -141,6 +141,9 @@ def calc_transmission_line_S_matrix_pseudo(Z0, Zref1, Zref2, gamma, length):
141141
Propagation constant (can be frequency-dependent)
142142
length : float
143143
Length (scalar only)
144+
symmetric : bool, optional
145+
If True, use symmetric_pseudo scaling (F = 1/(2*sqrt(Z))) which ensures
146+
S12 = S21 for reciprocal networks. Default is False.
144147
145148
Returns:
146149
--------
@@ -163,17 +166,31 @@ def calc_transmission_line_S_matrix_pseudo(Z0, Zref1, Zref2, gamma, length):
163166
numerator_S22 = (Z0**2 - Zref1 * Zref2) * tanh_gamma_ell + Z0 * (Zref1 - Zref2)
164167
S22 = numerator_S22 / denom
165168

166-
# Calculate S21 (transmission from port 1 to port 2)
167-
numerator_S21 = (
168-
np.sqrt(np.real(Zref1) / np.real(Zref2)) * (np.abs(Zref2) / np.abs(Zref1)) * 2 * Z0 * Zref1
169-
)
170-
S21 = numerator_S21 / (denom * cosh_gamma_ell)
169+
# Calculate S12 and S21 (off-diagonal transmission terms)
170+
if symmetric:
171+
# For symmetric_pseudo: F = 1/(2*sqrt(Z)), so F1/F2 = sqrt(Z2/Z1)
172+
# This gives S12 = S21 for reciprocal networks
173+
numerator_S12 = 2 * Z0 * np.sqrt(Zref1 * Zref2)
174+
numerator_S21 = numerator_S12
175+
else:
176+
# For pseudo: F = sqrt(Re(Z))/(2|Z|)
177+
numerator_S12 = (
178+
np.sqrt(np.real(Zref1) / np.real(Zref2))
179+
* (np.abs(Zref2) / np.abs(Zref1))
180+
* 2
181+
* Z0
182+
* Zref1
183+
)
184+
numerator_S21 = (
185+
np.sqrt(np.real(Zref2) / np.real(Zref1))
186+
* (np.abs(Zref1) / np.abs(Zref2))
187+
* 2
188+
* Z0
189+
* Zref2
190+
)
171191

172-
# Calculate S12 (transmission from port 2 to port 1)
173-
numerator_S12 = (
174-
np.sqrt(np.real(Zref2) / np.real(Zref1)) * (np.abs(Zref1) / np.abs(Zref2)) * 2 * Z0 * Zref2
175-
)
176192
S12 = numerator_S12 / (denom * cosh_gamma_ell)
193+
S21 = numerator_S21 / (denom * cosh_gamma_ell)
177194

178195
# Construct the S-parameter matrix (nfreq, 2, 2)
179196
nfreq = len(np.atleast_1d(S11))
@@ -434,6 +451,7 @@ def test_complex_reference_s_to_z_component_modeler():
434451
skrf_S_50ohm = skrf.Network.from_z(z=Z, f=freqs)
435452
skrf_S_power = skrf.Network.from_z(z=Z, f=freqs, s_def="power", z0=z0)
436453
skrf_S_pseudo = skrf.Network.from_z(z=Z, f=freqs, s_def="pseudo", z0=z0)
454+
skrf_S_traveling = skrf.Network.from_z(z=Z, f=freqs, s_def="traveling", z0=z0)
437455

438456
ports = ["port1", "port2"]
439457
smatrix = TerminalPortDataArray(
@@ -454,6 +472,14 @@ def test_complex_reference_s_to_z_component_modeler():
454472
smatrix.values = skrf_S_pseudo.s
455473
z_tidy3d = s_to_z(smatrix, reference=z0_tidy3d, s_param_def="pseudo")
456474
assert np.all(np.isclose(z_tidy3d.values, Z))
475+
# Our symmetric_pseudo name is equivalent to "traveling" definition in scikit-rf
476+
smatrix.values = skrf_S_traveling.s
477+
z_tidy3d = s_to_z(smatrix, reference=z0_tidy3d, s_param_def="symmetric_pseudo")
478+
assert np.all(np.isclose(z_tidy3d.values, Z))
479+
480+
# Check that invalid s_param_def raises ValueError
481+
with pytest.raises(ValueError, match="Unsupported S-parameter definition"):
482+
s_to_z(smatrix, reference=z0_tidy3d, s_param_def="invalid")
457483

458484

459485
def test_data_s_to_z(monkeypatch):
@@ -1450,24 +1476,42 @@ def test_internal_construct_smatrix_with_port_vi(monkeypatch):
14501476
]
14511477
)
14521478
Z0 = np.array(
1479+
[
1480+
12.843105732941 + 15.394208173652j,
1481+
28.567192048123 + 9.1023847562915j,
1482+
31.209457618234 + 3.8475102934671j,
1483+
]
1484+
)
1485+
Z01 = np.array(
14531486
[
14541487
18.725191534567 + 12.672421364213j,
14551488
34.038884625562 + 7.8654410284980j,
14561489
35.725175635077 + 4.5490999181327j,
14571490
]
14581491
)
1492+
Z02 = np.array(
1493+
[
1494+
24.156839210485 + 10.234195827361j,
1495+
41.892301567293 + 6.7812039451120j,
1496+
29.451276384019 + 5.1298475620183j,
1497+
]
1498+
)
14591499
# Break the reference impedance symmetry
1460-
Zref = np.column_stack((0.5 * Z0, 2 * Z0))
1500+
Zref = np.column_stack((Z01, Z02))
14611501
# Calculate analytical S matrices for power and pseudo wave formulations
14621502
S_pseudo = calc_transmission_line_S_matrix_pseudo(Z0, Zref[:, 0], Zref[:, 1], gamma, length)
1503+
S_symmetric_pseudo = calc_transmission_line_S_matrix_pseudo(
1504+
Z0, Zref[:, 0], Zref[:, 1], gamma, length, symmetric=True
1505+
)
14631506
S_power = calc_transmission_line_S_matrix_power(Z0, Zref[:, 0], Zref[:, 1], gamma, length)
1464-
1507+
Zref3 = Zref[:, :, np.newaxis]
14651508
# Calculate A and B matrices where A is diagonal and B = S @ A
1509+
14661510
A = np.tile(np.eye(2), (len(freqs), 1, 1)) # Identity matrix for each frequency
14671511
B = S_pseudo @ A
14681512
# Now get Voltages and Currents at each port due to excitations from each port
1469-
Vscale = np.abs(Zref[:, :, np.newaxis]) / np.sqrt(np.real(Zref[:, :, np.newaxis]))
1470-
Iscale = Vscale / Zref[:, :, np.newaxis]
1513+
Vscale = np.abs(Zref3) / np.sqrt(np.real(Zref3))
1514+
Iscale = Vscale / Zref3
14711515
voltages = Vscale * (A + B) # (f x port_out x port_in)
14721516
currents = Iscale * (A - B) # (f x port_out x port_in)
14731517

@@ -1520,7 +1564,6 @@ def mock_port_impedances(modeler_data):
15201564
)
15211565

15221566
# Test the _internal_construct_smatrix method
1523-
S_computed = modeler_data.smatrix().data.values
15241567

15251568
def check_S_matrix(S_computed, S_expected, tol=1e-12):
15261569
# Check that S-matrix has correct shape
@@ -1542,12 +1585,21 @@ def check_S_matrix(S_computed, S_expected, tol=1e-12):
15421585
)
15431586

15441587
# Check pseudo wave S matrix
1588+
S_computed = modeler_data.smatrix().data.values
15451589
check_S_matrix(S_computed, S_pseudo)
15461590

15471591
# Check power wave S matrix
15481592
S_computed = modeler_data.smatrix(s_param_def="power").data.values
15491593
check_S_matrix(S_computed, S_power)
15501594

1595+
# Check symmetric_pseudo wave S matrix
1596+
S_computed = modeler_data.smatrix(s_param_def="symmetric_pseudo").data.values
1597+
check_S_matrix(S_computed, S_symmetric_pseudo)
1598+
1599+
# Check that invalid s_param_def raises ValueError
1600+
with pytest.raises(ValueError, match="Unsupported S-parameter definition"):
1601+
modeler_data.smatrix(s_param_def="invalid")
1602+
15511603

15521604
def test_wave_port_to_absorber(tmp_path):
15531605
"""Test that wave port absorber can be specified as a boolean, ABCBoundary, or ModeABCBoundary."""

tidy3d/plugins/smatrix/analysis/terminal.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ def terminal_construct_smatrix(
5858
waves at other ports. This simplifies the S-matrix calculation and is
5959
required if not all ports are excited. Default is ``False``.
6060
s_param_def : SParamDef, optional
61-
The definition of S-parameters to use depends whether "pseudo waves"
62-
or "power waves" are calculated. Default is "pseudo".
61+
Wave definition: "pseudo", "power", or "symmetric_pseudo". Default is "pseudo".
62+
See :class:`.TerminalComponentModeler` for details.
6363
6464
Returns
6565
-------
@@ -73,8 +73,15 @@ def terminal_construct_smatrix(
7373

7474
if s_param_def == "pseudo":
7575
a_matrix, b_matrix = modeler_data.port_pseudo_wave_matrices
76-
else:
76+
elif s_param_def == "symmetric_pseudo":
77+
a_matrix, b_matrix = modeler_data.port_symmetric_pseudo_wave_matrices
78+
elif s_param_def == "power":
7779
a_matrix, b_matrix = modeler_data.port_power_wave_matrices
80+
else:
81+
raise ValueError(
82+
f"Unsupported S-parameter definition '{s_param_def}'. "
83+
"Supported values are 'pseudo', 'symmetric_pseudo', and 'power'."
84+
)
7885

7986
# If excitation is assumed ideal, a_matrix is assumed to be diagonal
8087
# and the explicit inverse can be avoided. When only a subset of excitations
@@ -225,10 +232,6 @@ def _compute_wave_amplitudes_from_VI(
225232
specified wave definition. The conversion handles impedance sign consistency and
226233
applies the appropriate normalization based on the chosen S-parameter definition.
227234
228-
The wave amplitudes are computed using:
229-
- Pseudo waves: Equations 53-54 from Marks and Williams [1]
230-
- Power waves: Equation 4.67 from Pozar [2]
231-
232235
Parameters
233236
----------
234237
port_reference_impedances : :class:`.PortDataArray`
@@ -238,8 +241,8 @@ def _compute_wave_amplitudes_from_VI(
238241
port_currents : :class:`.PortDataArray`
239242
Current values at each port with dimensions (f, port).
240243
s_param_def : SParamDef, optional
241-
Wave definition type: "pseudo" for pseudo waves or "power" for power waves.
242-
Defaults to "pseudo".
244+
Wave definition: "pseudo", "power", or "symmetric_pseudo". Default is "pseudo".
245+
See :class:`.TerminalComponentModeler` for details.
243246
244247
Returns
245248
-------
@@ -295,8 +298,8 @@ def compute_wave_amplitudes_at_each_port(
295298
sim_data : :class:`.SimulationData`
296299
Results from a single simulation run.
297300
s_param_def : SParamDef
298-
The type of waves computed, either pseudo waves defined by Equation 53 and
299-
Equation 54 in [1], or power waves defined by Equation 4.67 in [2].
301+
Wave definition: "pseudo", "power", or "symmetric_pseudo".
302+
See :class:`.TerminalComponentModeler` for details.
300303
301304
Returns
302305
-------

tidy3d/plugins/smatrix/component_modelers/terminal.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,13 +141,35 @@ class TerminalComponentModeler(AbstractComponentModeler, MicrowaveBaseModel):
141141
Notes
142142
-----
143143
144+
**S-Parameter Definitions**
145+
146+
The ``s_param_def`` parameter controls which wave definition is used to compute scattering
147+
parameters. Three definitions are supported:
148+
149+
- ``"pseudo"`` (default): Pseudo-waves as defined by Marks and Williams [1]. Uses scaling
150+
factor :math:`F = \\sqrt{\\text{Re}(Z)} / (2|Z|)`. Wave amplitudes are :math:`a = F(V + ZI)`
151+
and :math:`b = F(V - ZI)`.
152+
153+
- ``"power"``: Power waves as defined by Kurokawa [3] and described in Pozar [2]. Uses
154+
scaling factor :math:`F = 1 / (2\\sqrt{\\text{Re}(Z)})`. Wave amplitudes are
155+
:math:`a = F(V + ZI)` and :math:`b = F(V - Z^*I)` where :math:`Z^*` is the complex
156+
conjugate. Ensures :math:`|a|^2 - |b|^2` represents actual power flow.
157+
158+
- ``"symmetric_pseudo"``: Equivalent to pseudo-waves except for the scaling factor. Uses
159+
:math:`F = 1 / (2\\sqrt{Z})` where the square root is complex. This choice of scaling
160+
factor ensures the S-matrix will be symmetric when the simulated device is reciprocal.
161+
162+
144163
**References**
145164
146165
.. [1] R. B. Marks and D. F. Williams, "A general waveguide circuit theory,"
147166
J. Res. Natl. Inst. Stand. Technol., vol. 97, pp. 533, 1992.
148167
149168
.. [2] D. M. Pozar, Microwave Engineering, 4th ed. Hoboken, NJ, USA:
150169
John Wiley & Sons, 2012.
170+
171+
.. [3] K. Kurokawa, "Power Waves and the Scattering Matrix," IEEE Trans.
172+
Microwave Theory Tech., vol. 13, no. 2, pp. 194-202, March 1965.
151173
"""
152174

153175
ports: tuple[TerminalPortType, ...] = pd.Field(
@@ -200,7 +222,7 @@ class TerminalComponentModeler(AbstractComponentModeler, MicrowaveBaseModel):
200222
s_param_def: SParamDef = pd.Field(
201223
"pseudo",
202224
title="Scattering Parameter Definition",
203-
description="Whether to compute scattering parameters using the 'pseudo' or 'power' wave definitions.",
225+
description="Wave definition: 'pseudo', 'power', or 'symmetric_pseudo'. Default is 'pseudo'.",
204226
)
205227

206228
low_freq_smoothing: Optional[ModelerLowFrequencySmoothingSpec] = pd.Field(

0 commit comments

Comments
 (0)