Skip to content

Commit b86eaec

Browse files
committed
feat(rf): Add pseudo_symmetric option for s_param_def
1 parent e3c3930 commit b86eaec

File tree

8 files changed

+183
-56
lines changed

8 files changed

+183
-56
lines changed

CHANGELOG.md

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

1010
### Added
11+
- Added `pseudo_symmetric` option for `s_param_def` in `TerminalComponentModeler` which applies a scaling factor that ensures the S-matrix is symmetric.
1112

1213
### Changed
1314

schemas/TerminalComponentModeler.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18303,7 +18303,8 @@
1830318303
"default": "pseudo",
1830418304
"enum": [
1830518305
"power",
18306-
"pseudo"
18306+
"pseudo",
18307+
"pseudo_symmetric"
1830718308
],
1830818309
"type": "string"
1830918310
},

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 pseudo_symmetric 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 pseudo_symmetric: 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 pseudo_symmetric 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="pseudo_symmetric")
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):
@@ -1427,24 +1453,42 @@ def test_internal_construct_smatrix_with_port_vi(monkeypatch):
14271453
]
14281454
)
14291455
Z0 = np.array(
1456+
[
1457+
12.843105732941 + 15.394208173652j,
1458+
28.567192048123 + 9.1023847562915j,
1459+
31.209457618234 + 3.8475102934671j,
1460+
]
1461+
)
1462+
Z01 = np.array(
14301463
[
14311464
18.725191534567 + 12.672421364213j,
14321465
34.038884625562 + 7.8654410284980j,
14331466
35.725175635077 + 4.5490999181327j,
14341467
]
14351468
)
1469+
Z02 = np.array(
1470+
[
1471+
24.156839210485 + 10.234195827361j,
1472+
41.892301567293 + 6.7812039451120j,
1473+
29.451276384019 + 5.1298475620183j,
1474+
]
1475+
)
14361476
# Break the reference impedance symmetry
1437-
Zref = np.column_stack((0.5 * Z0, 2 * Z0))
1477+
Zref = np.column_stack((Z01, Z02))
14381478
# Calculate analytical S matrices for power and pseudo wave formulations
14391479
S_pseudo = calc_transmission_line_S_matrix_pseudo(Z0, Zref[:, 0], Zref[:, 1], gamma, length)
1480+
S_pseudo_symmetric = calc_transmission_line_S_matrix_pseudo(
1481+
Z0, Zref[:, 0], Zref[:, 1], gamma, length, symmetric=True
1482+
)
14401483
S_power = calc_transmission_line_S_matrix_power(Z0, Zref[:, 0], Zref[:, 1], gamma, length)
1441-
1484+
Zref3 = Zref[:, :, np.newaxis]
14421485
# Calculate A and B matrices where A is diagonal and B = S @ A
1486+
14431487
A = np.tile(np.eye(2), (len(freqs), 1, 1)) # Identity matrix for each frequency
14441488
B = S_pseudo @ A
14451489
# Now get Voltages and Currents at each port due to excitations from each port
1446-
Vscale = np.abs(Zref[:, :, np.newaxis]) / np.sqrt(np.real(Zref[:, :, np.newaxis]))
1447-
Iscale = Vscale / Zref[:, :, np.newaxis]
1490+
Vscale = np.abs(Zref3) / np.sqrt(np.real(Zref3))
1491+
Iscale = Vscale / Zref3
14481492
voltages = Vscale * (A + B) # (f x port_out x port_in)
14491493
currents = Iscale * (A - B) # (f x port_out x port_in)
14501494

@@ -1497,7 +1541,6 @@ def mock_port_impedances(modeler_data):
14971541
)
14981542

14991543
# Test the _internal_construct_smatrix method
1500-
S_computed = modeler_data.smatrix().data.values
15011544

15021545
def check_S_matrix(S_computed, S_expected, tol=1e-12):
15031546
# Check that S-matrix has correct shape
@@ -1519,12 +1562,21 @@ def check_S_matrix(S_computed, S_expected, tol=1e-12):
15191562
)
15201563

15211564
# Check pseudo wave S matrix
1565+
S_computed = modeler_data.smatrix().data.values
15221566
check_S_matrix(S_computed, S_pseudo)
15231567

15241568
# Check power wave S matrix
15251569
S_computed = modeler_data.smatrix(s_param_def="power").data.values
15261570
check_S_matrix(S_computed, S_power)
15271571

1572+
# Check pseudo symmetric wave S matrix
1573+
S_computed = modeler_data.smatrix(s_param_def="pseudo_symmetric").data.values
1574+
check_S_matrix(S_computed, S_pseudo_symmetric)
1575+
1576+
# Check that invalid s_param_def raises ValueError
1577+
with pytest.raises(ValueError, match="Unsupported S-parameter definition"):
1578+
modeler_data.smatrix(s_param_def="invalid")
1579+
15281580

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

154176
ports: tuple[TerminalPortType, ...] = pd.Field(
@@ -201,7 +223,7 @@ class TerminalComponentModeler(AbstractComponentModeler, MicrowaveBaseModel):
201223
s_param_def: SParamDef = pd.Field(
202224
"pseudo",
203225
title="Scattering Parameter Definition",
204-
description="Whether to compute scattering parameters using the 'pseudo' or 'power' wave definitions.",
226+
description="Wave definition: 'pseudo', 'power', or 'pseudo_symmetric'. Default is 'pseudo'.",
205227
)
206228

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

0 commit comments

Comments
 (0)