|
4 | 4 | import numpy as np
|
5 | 5 | import pydantic.v1 as pd
|
6 | 6 | import pytest
|
| 7 | +import skrf |
7 | 8 | import xarray as xr
|
8 | 9 |
|
9 | 10 | import tidy3d as td
|
@@ -40,7 +41,7 @@ def run_component_modeler(monkeypatch, modeler: TerminalComponentModeler):
|
40 | 41 | monkeypatch.setattr(
|
41 | 42 | TerminalComponentModeler,
|
42 | 43 | "_compute_F",
|
43 |
| - lambda matrix: 1.0 / (2.0 * np.sqrt(np.abs(matrix) + 1e-4)), |
| 44 | + lambda Z_numpy, S_def: 1.0 / (2.0 * np.sqrt(np.abs(Z_numpy) + 1e-4)), |
44 | 45 | )
|
45 | 46 | monkeypatch.setattr(
|
46 | 47 | TerminalComponentModeler,
|
@@ -77,6 +78,152 @@ def check_lumped_port_components_snapped_correctly(modeler: TerminalComponentMod
|
77 | 78 | assert center_load == center_current_monitor
|
78 | 79 |
|
79 | 80 |
|
| 81 | +def make_t_network_impedance_matrix( |
| 82 | + series_a: complex, series_b: complex, shunt_c: complex |
| 83 | +) -> np.ndarray: |
| 84 | + """Create impedance matrix for T-network with series elements A, B and shunt element C. |
| 85 | +
|
| 86 | + Network topology: |
| 87 | + Port1 ----[A]----+----[B]---- Port2 |
| 88 | + | |
| 89 | + [C] |
| 90 | + | |
| 91 | + GND |
| 92 | + """ |
| 93 | + z11 = series_a + shunt_c |
| 94 | + z21 = shunt_c |
| 95 | + z12 = shunt_c |
| 96 | + z22 = series_b + shunt_c |
| 97 | + return np.array([[z11, z12], [z21, z22]]) |
| 98 | + |
| 99 | + |
| 100 | +def calc_transmission_line_S_matrix_pseudo(Z0, Zref1, Zref2, gamma, length): |
| 101 | + """ |
| 102 | + Calculate complete 2x2 S-parameter matrix for a transmission line |
| 103 | + using pseudo wave definition |
| 104 | +
|
| 105 | + [1] S. Amakawa, "Scattered reflections on scattering parameters—Demystifying complex-referenced |
| 106 | + S parameters—," IEICE Trans. Electron., vol. E99-C, no. 10, pp. 1100-1112, Oct. 2016. |
| 107 | +
|
| 108 | + Parameters: |
| 109 | + ----------- |
| 110 | + Z0 : complex or array-like |
| 111 | + Characteristic impedance (can be frequency-dependent) |
| 112 | + Zref1 : complex or array-like |
| 113 | + Reference impedance at port 1 (can be frequency-dependent) |
| 114 | + Zref2 : complex or array-like |
| 115 | + Reference impedance at port 2 (can be frequency-dependent) |
| 116 | + gamma : complex or array-like |
| 117 | + Propagation constant (can be frequency-dependent) |
| 118 | + length : float |
| 119 | + Length (scalar only) |
| 120 | +
|
| 121 | + Returns: |
| 122 | + -------- |
| 123 | + np.ndarray : |
| 124 | + S-parameter matrix of shape (nfreq, 2, 2) |
| 125 | + """ |
| 126 | + |
| 127 | + # Calculate hyperbolic functions |
| 128 | + tanh_gamma_ell = np.tanh(gamma * length) |
| 129 | + cosh_gamma_ell = np.cosh(gamma * length) |
| 130 | + |
| 131 | + # Common denominator for all S-parameters |
| 132 | + denom = (Z0**2 + Zref1 * Zref2) * tanh_gamma_ell + Z0 * (Zref1 + Zref2) |
| 133 | + |
| 134 | + # Calculate S11 |
| 135 | + numerator_S11 = (Z0**2 - Zref1 * Zref2) * tanh_gamma_ell + Z0 * (Zref2 - Zref1) |
| 136 | + S11 = numerator_S11 / denom |
| 137 | + |
| 138 | + # Calculate S22 |
| 139 | + numerator_S22 = (Z0**2 - Zref1 * Zref2) * tanh_gamma_ell + Z0 * (Zref1 - Zref2) |
| 140 | + S22 = numerator_S22 / denom |
| 141 | + |
| 142 | + # Calculate S21 (transmission from port 1 to port 2) |
| 143 | + numerator_S21 = ( |
| 144 | + np.sqrt(np.real(Zref1) / np.real(Zref2)) * (np.abs(Zref2) / np.abs(Zref1)) * 2 * Z0 * Zref1 |
| 145 | + ) |
| 146 | + S21 = numerator_S21 / (denom * cosh_gamma_ell) |
| 147 | + |
| 148 | + # Calculate S12 (transmission from port 2 to port 1) |
| 149 | + numerator_S12 = ( |
| 150 | + np.sqrt(np.real(Zref2) / np.real(Zref1)) * (np.abs(Zref1) / np.abs(Zref2)) * 2 * Z0 * Zref2 |
| 151 | + ) |
| 152 | + S12 = numerator_S12 / (denom * cosh_gamma_ell) |
| 153 | + |
| 154 | + # Construct the S-parameter matrix (nfreq, 2, 2) |
| 155 | + nfreq = len(np.atleast_1d(S11)) |
| 156 | + S_matrix = np.zeros((nfreq, 2, 2), dtype=complex) |
| 157 | + S_matrix[:, 0, 0] = S11 |
| 158 | + S_matrix[:, 0, 1] = S12 |
| 159 | + S_matrix[:, 1, 0] = S21 |
| 160 | + S_matrix[:, 1, 1] = S22 |
| 161 | + |
| 162 | + return S_matrix |
| 163 | + |
| 164 | + |
| 165 | +def calc_transmission_line_S_matrix_power(Z0, Zref1, Zref2, gamma, length): |
| 166 | + """ |
| 167 | + Calculate complete 2x2 S-parameter matrix for a transmission line |
| 168 | + using power wave definition |
| 169 | +
|
| 170 | + [1] S. Amakawa, "Scattered reflections on scattering parameters—Demystifying complex-referenced |
| 171 | + S parameters—," IEICE Trans. Electron., vol. E99-C, no. 10, pp. 1100-1112, Oct. 2016. |
| 172 | +
|
| 173 | + Parameters: |
| 174 | + ----------- |
| 175 | + Z0 : complex or array-like |
| 176 | + Characteristic impedance (can be frequency-dependent) |
| 177 | + Zref1 : complex or array-like |
| 178 | + Reference impedance at port 1 (can be frequency-dependent) |
| 179 | + Zref2 : complex or array-like |
| 180 | + Reference impedance at port 2 (can be frequency-dependent) |
| 181 | + gamma : complex or array-like |
| 182 | + Propagation constant (can be frequency-dependent) |
| 183 | + length : float |
| 184 | + Length (scalar only) |
| 185 | +
|
| 186 | + Returns: |
| 187 | + -------- |
| 188 | + np.ndarray : |
| 189 | + S-parameter matrix of shape (nfreq, 2, 2) |
| 190 | + """ |
| 191 | + |
| 192 | + # Calculate hyperbolic functions |
| 193 | + tanh_gamma_ell = np.tanh(gamma * length) |
| 194 | + cosh_gamma_ell = np.cosh(gamma * length) |
| 195 | + |
| 196 | + # Complex conjugates |
| 197 | + Zref1_conj = np.conj(Zref1) |
| 198 | + Zref2_conj = np.conj(Zref2) |
| 199 | + |
| 200 | + # Common denominator |
| 201 | + denom = (Z0**2 + Zref1 * Zref2) * tanh_gamma_ell + Z0 * (Zref1 + Zref2) |
| 202 | + |
| 203 | + # S11 with conjugate terms |
| 204 | + numerator_S11 = (Z0**2 - Zref1_conj * Zref2) * tanh_gamma_ell + Z0 * (Zref2 - Zref1_conj) |
| 205 | + S11 = numerator_S11 / denom |
| 206 | + |
| 207 | + # S22 with conjugate terms |
| 208 | + numerator_S22 = (Z0**2 - Zref1 * Zref2_conj) * tanh_gamma_ell + Z0 * (Zref1 - Zref2_conj) |
| 209 | + S22 = numerator_S22 / denom |
| 210 | + |
| 211 | + # S21 and S12 (transmission parameters) |
| 212 | + numerator_trans = 2 * Z0 * np.sqrt(np.real(Zref1) * np.real(Zref2)) |
| 213 | + S21 = numerator_trans / (denom * cosh_gamma_ell) |
| 214 | + S12 = S21 # For reciprocal network |
| 215 | + |
| 216 | + # Construct the S-parameter matrix (nfreq, 2, 2) |
| 217 | + nfreq = len(np.atleast_1d(S11)) |
| 218 | + S_matrix = np.zeros((nfreq, 2, 2), dtype=complex) |
| 219 | + S_matrix[:, 0, 0] = S11 |
| 220 | + S_matrix[:, 0, 1] = S12 |
| 221 | + S_matrix[:, 1, 0] = S21 |
| 222 | + S_matrix[:, 1, 1] = S22 |
| 223 | + |
| 224 | + return S_matrix |
| 225 | + |
| 226 | + |
80 | 227 | def test_validate_no_sources(tmp_path):
|
81 | 228 | modeler = make_component_modeler(planar_pec=True, path_dir=str(tmp_path))
|
82 | 229 | source = td.PointDipole(
|
@@ -156,15 +303,25 @@ def test_run_component_modeler(monkeypatch, tmp_path):
|
156 | 303 |
|
157 | 304 |
|
158 | 305 | def test_s_to_z_component_modeler():
|
159 |
| - # Test case is 2 port T network with reference impedance of 50 Ohm |
| 306 | + """Test conversion of S parameters to impedance matrix, |
| 307 | + for a simple test case of 2 port T network with reference impedance of 50 Ohm |
| 308 | +
|
| 309 | + Network topology: |
| 310 | + Port1 ----[A]----+----[B]---- Port2 |
| 311 | + | |
| 312 | + [C] |
| 313 | + | |
| 314 | + GND |
| 315 | + """ |
160 | 316 | A = 20 + 30j
|
161 | 317 | B = 50 - 15j
|
162 | 318 | C = 60
|
163 | 319 |
|
164 |
| - Z11 = A + C |
165 |
| - Z21 = C |
166 |
| - Z12 = C |
167 |
| - Z22 = B + C |
| 320 | + Z = make_t_network_impedance_matrix(A, B, C) |
| 321 | + Z11 = Z[0, 0] |
| 322 | + Z21 = Z[1, 0] |
| 323 | + Z12 = Z[0, 1] |
| 324 | + Z22 = Z[1, 1] |
168 | 325 |
|
169 | 326 | Z0 = 50.0
|
170 | 327 | # Manual creation of S parameters Pozar Table 4.2
|
@@ -211,6 +368,53 @@ def test_s_to_z_component_modeler():
|
211 | 368 | assert np.isclose(z_matrix_at_f[1, 1], Z22)
|
212 | 369 |
|
213 | 370 |
|
| 371 | +def test_complex_reference_s_to_z_component_modeler(): |
| 372 | + """Test conversion of S parameters to impedance matrix, |
| 373 | + for a test case of 2 port T network with complex reference impedances, which requires |
| 374 | + identifying the precise definition used for S parameters |
| 375 | +
|
| 376 | + Network topology: |
| 377 | + Port1 ----[A]----+----[B]---- Port2 |
| 378 | + | |
| 379 | + [C] |
| 380 | + | |
| 381 | + GND |
| 382 | + """ |
| 383 | + A = np.array([0, 21 + 31j, 60]) |
| 384 | + B = np.array([0, 51 - 16j, 40]) |
| 385 | + C = np.array([50, 61, 0]) |
| 386 | + |
| 387 | + freqs = np.array([1e9, 2e9, 3e9]) |
| 388 | + # Build Z for each frequency with different A, B, C |
| 389 | + Z = np.stack([make_t_network_impedance_matrix(a, b, c) for a, b, c in zip(A, B, C)], axis=0) |
| 390 | + # Choose some port reference impedances |
| 391 | + z0 = np.stack(3 * [np.array([50 + 5j, 60 - 2j])], axis=0) |
| 392 | + |
| 393 | + skrf_S_50ohm = skrf.Network.from_z(z=Z, f=freqs) |
| 394 | + skrf_S_power = skrf.Network.from_z(z=Z, f=freqs, s_def="power", z0=z0) |
| 395 | + skrf_S_pseudo = skrf.Network.from_z(z=Z, f=freqs, s_def="pseudo", z0=z0) |
| 396 | + |
| 397 | + ports = ["port1", "port2"] |
| 398 | + smatrix = TerminalPortDataArray( |
| 399 | + skrf_S_50ohm.s, coords={"f": freqs, "port_out": ports, "port_in": ports} |
| 400 | + ) |
| 401 | + # Test real reference impedance calculations |
| 402 | + z_tidy3d = TerminalComponentModeler.s_to_z(smatrix, reference=50, S_def="power") |
| 403 | + assert np.all(np.isclose(z_tidy3d.values, Z)) |
| 404 | + z_tidy3d = TerminalComponentModeler.s_to_z(smatrix, reference=50, S_def="pseudo") |
| 405 | + assert np.all(np.isclose(z_tidy3d.values, Z)) |
| 406 | + |
| 407 | + # Test complex reference impedance calculations |
| 408 | + z0_tidy3d = PortDataArray(data=z0, coords={"f": freqs, "port": ports}) |
| 409 | + smatrix.values = skrf_S_power.s |
| 410 | + z_tidy3d = TerminalComponentModeler.s_to_z(smatrix, reference=z0_tidy3d, S_def="power") |
| 411 | + assert np.all(np.isclose(z_tidy3d.values, Z)) |
| 412 | + |
| 413 | + smatrix.values = skrf_S_pseudo.s |
| 414 | + z_tidy3d = TerminalComponentModeler.s_to_z(smatrix, reference=z0_tidy3d, S_def="pseudo") |
| 415 | + assert np.all(np.isclose(z_tidy3d.values, Z)) |
| 416 | + |
| 417 | + |
214 | 418 | def test_ab_to_s_component_modeler():
|
215 | 419 | coords = {
|
216 | 420 | "f": np.array([1e8]),
|
@@ -900,3 +1104,109 @@ def test_get_combined_antenna_parameters_data(monkeypatch, tmp_path):
|
900 | 1104 | assert not np.allclose(
|
901 | 1105 | antenna_params.radiation_efficiency, single_port_params.radiation_efficiency
|
902 | 1106 | )
|
| 1107 | + |
| 1108 | + |
| 1109 | +def test_internal_construct_smatrix_with_port_vi(monkeypatch): |
| 1110 | + """Test _internal_construct_smatrix method by monkeypatching compute_port_VI |
| 1111 | + with precomputed voltage and current values and comparing the final S-matrix to expected results. |
| 1112 | + """ |
| 1113 | + # Create a simple 2-port modeler for testing |
| 1114 | + modeler = make_component_modeler(planar_pec=False) |
| 1115 | + freqs = np.array([1e9, 5e9, 10e9]) |
| 1116 | + modeler = modeler.updated_copy(freqs=freqs) |
| 1117 | + |
| 1118 | + # Some test data from a 20 mm lossy microstrip |
| 1119 | + # Data is given using engineering convention exp(jwt) |
| 1120 | + length = 0.02 |
| 1121 | + gamma = np.array( |
| 1122 | + [ |
| 1123 | + 35.845260386378 + 52.964956959149j, |
| 1124 | + 48.283102945750 + 208.91753284900j, |
| 1125 | + 50.594134809653 + 397.12168963974j, |
| 1126 | + ] |
| 1127 | + ) |
| 1128 | + Z0 = np.array( |
| 1129 | + [ |
| 1130 | + 18.725191534567 + 12.672421364213j, |
| 1131 | + 34.038884625562 + 7.8654410284980j, |
| 1132 | + 35.725175635077 + 4.5490999181327j, |
| 1133 | + ] |
| 1134 | + ) |
| 1135 | + # Break the reference impedance symmetry |
| 1136 | + Zref = np.column_stack((0.5 * Z0, 2 * Z0)) |
| 1137 | + # Calculate analytical S matrices for power and pseudo wave formulations |
| 1138 | + S_pseudo = calc_transmission_line_S_matrix_pseudo(Z0, Zref[:, 0], Zref[:, 1], gamma, length) |
| 1139 | + S_power = calc_transmission_line_S_matrix_power(Z0, Zref[:, 0], Zref[:, 1], gamma, length) |
| 1140 | + |
| 1141 | + # Calculate A and B matrices where A is diagonal and B = S @ A |
| 1142 | + A = np.tile(np.eye(2), (len(freqs), 1, 1)) # Identity matrix for each frequency |
| 1143 | + B = S_pseudo @ A |
| 1144 | + # Now get Voltages and Currents at each port due to excitations from each port |
| 1145 | + Vscale = np.abs(Zref[:, :, np.newaxis]) / np.sqrt(np.real(Zref[:, :, np.newaxis])) |
| 1146 | + Iscale = Vscale / Zref[:, :, np.newaxis] |
| 1147 | + voltages = Vscale * (A + B) # (f x port_out x port_in) |
| 1148 | + currents = Iscale * (A - B) # (f x port_out x port_in) |
| 1149 | + |
| 1150 | + port_names = [port.name for port in modeler.ports] |
| 1151 | + |
| 1152 | + # Create mock batch data |
| 1153 | + batch_data = {} |
| 1154 | + for j, port_in in enumerate(modeler.ports): |
| 1155 | + task_name = modeler._task_name(port_in) |
| 1156 | + batch_data[task_name] = {} |
| 1157 | + for i, port_out in enumerate(modeler.ports): |
| 1158 | + # Initialize with zeros - user should replace with actual values |
| 1159 | + batch_data[task_name][port_out.name] = { |
| 1160 | + "voltage": FreqDataArray(voltages[:, i, j], coords={"f": freqs}), |
| 1161 | + "current": FreqDataArray(currents[:, i, j], coords={"f": freqs}), |
| 1162 | + } |
| 1163 | + |
| 1164 | + # Mock the compute_port_VI method |
| 1165 | + def mock_compute_port_vi(port_out, sim_data): |
| 1166 | + """Mock compute_port_VI to return voltage and current from dummy sim_data.""" |
| 1167 | + port_name = port_out.name |
| 1168 | + voltage = sim_data[port_name]["voltage"] |
| 1169 | + current = sim_data[port_name]["current"] |
| 1170 | + return voltage, current |
| 1171 | + |
| 1172 | + # Mock port reference impedances to return constant Z0 |
| 1173 | + def mock_port_impedances(self, batch_data): |
| 1174 | + coords = {"f": np.array(freqs), "port": port_names} |
| 1175 | + return PortDataArray(Zref, coords=coords) |
| 1176 | + |
| 1177 | + # Apply monkeypatches |
| 1178 | + monkeypatch.setattr( |
| 1179 | + TerminalComponentModeler, "compute_port_VI", staticmethod(mock_compute_port_vi) |
| 1180 | + ) |
| 1181 | + monkeypatch.setattr( |
| 1182 | + TerminalComponentModeler, "_port_reference_impedances", mock_port_impedances |
| 1183 | + ) |
| 1184 | + |
| 1185 | + # Test the _internal_construct_smatrix method |
| 1186 | + S_computed = modeler._internal_construct_smatrix(batch_data).values |
| 1187 | + |
| 1188 | + def check_S_matrix(S_computed, S_expected, tol=1e-12): |
| 1189 | + # Check that S-matrix has correct shape |
| 1190 | + assert S_computed.shape == (len(freqs), len(port_names), len(port_names)) |
| 1191 | + |
| 1192 | + # Compare computed S-matrix with analytical values at each frequency |
| 1193 | + for freq_idx in range(len(freqs)): |
| 1194 | + S_computed_at_freq = S_computed[freq_idx, :, :] |
| 1195 | + S_expected_at_freq = S_expected[freq_idx, :, :] |
| 1196 | + max_rel_err = np.max( |
| 1197 | + np.abs((S_computed_at_freq - S_expected_at_freq) / (S_expected_at_freq + 1e-14)) |
| 1198 | + ) |
| 1199 | + assert np.allclose(S_computed_at_freq, S_expected_at_freq, rtol=tol, atol=tol), ( |
| 1200 | + f"S-matrix mismatch at frequency index {freq_idx}\n" |
| 1201 | + f"Expected:\n{S_expected_at_freq}\n" |
| 1202 | + f"Computed:\n{S_computed_at_freq}\n" |
| 1203 | + f"Difference:\n{S_computed_at_freq - S_expected_at_freq}\n" |
| 1204 | + f"Max relative error: {max_rel_err:.2e}" |
| 1205 | + ) |
| 1206 | + |
| 1207 | + # Check pseudo wave S matrix |
| 1208 | + check_S_matrix(S_computed, S_pseudo) |
| 1209 | + |
| 1210 | + # Check power wave S matrix |
| 1211 | + S_computed = modeler._internal_construct_smatrix(batch_data, S_def="power").values |
| 1212 | + check_S_matrix(S_computed, S_power) |
0 commit comments