Skip to content

Commit 5caee13

Browse files
committed
providing support for both pseudo and power wave formulations
1 parent 6197c30 commit 5caee13

File tree

2 files changed

+113
-45
lines changed

2 files changed

+113
-45
lines changed

tests/test_plugins/smatrix/test_terminal_component_modeler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def run_component_modeler(monkeypatch, modeler: TerminalComponentModeler):
4040
monkeypatch.setattr(
4141
TerminalComponentModeler,
4242
"_compute_F",
43-
lambda matrix: 1.0 / (2.0 * np.sqrt(np.abs(matrix) + 1e-4)),
43+
lambda matrix, wave_type: 1.0 / (2.0 * np.sqrt(np.abs(matrix) + 1e-4)),
4444
)
4545
monkeypatch.setattr(
4646
TerminalComponentModeler,

tidy3d/plugins/smatrix/component_modelers/terminal.py

Lines changed: 112 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from __future__ import annotations
44

5-
from typing import Optional, Union
5+
from typing import Literal, Optional, Union
66

77
import numpy as np
88
import pydantic.v1 as pd
@@ -33,7 +33,17 @@
3333

3434
class TerminalComponentModeler(AbstractComponentModeler):
3535
"""Tool for modeling two-terminal multiport devices and computing port parameters
36-
with lumped and wave ports."""
36+
with lumped and wave ports.
37+
38+
Notes
39+
-----
40+
**References**
41+
42+
[1] R. B. Marks and D. F. Williams, "A general waveguide circuit theory,"
43+
J. Res. Natl. Inst. Stand. Technol., vol. 97, pp. 533, 1992.
44+
45+
[2] D. M. Pozar, Microwave Engineering, 4th ed. Hoboken, NJ, USA: John Wiley & Sons, 2012.
46+
"""
3747

3848
ports: tuple[TerminalPortType, ...] = pd.Field(
3949
(),
@@ -220,7 +230,9 @@ def _internal_construct_smatrix(self, batch_data: BatchData) -> TerminalPortData
220230
# loop through source ports
221231
for port_in in self.ports:
222232
sim_data = batch_data[self._task_name(port=port_in)]
223-
a, b = self.compute_power_wave_amplitudes_at_each_port(port_impedances, sim_data)
233+
a, b = self.compute_wave_amplitudes_at_each_port(
234+
port_impedances, sim_data, wave_type="pseudo"
235+
)
224236
indexer = {"f": a.f, "port_in": port_in.name, "port_out": a.port}
225237
a_matrix.loc[indexer] = a
226238
b_matrix.loc[indexer] = b
@@ -280,10 +292,13 @@ def _check_grid_size_at_wave_ports(simulation: Simulation, ports: list[WavePort]
280292
"for the simulation passed to the 'TerminalComponentModeler'."
281293
)
282294

283-
def compute_power_wave_amplitudes_at_each_port(
284-
self, port_reference_impedances: PortDataArray, sim_data: SimulationData
295+
def compute_wave_amplitudes_at_each_port(
296+
self,
297+
port_reference_impedances: PortDataArray,
298+
sim_data: SimulationData,
299+
wave_type: Literal["pseudo", "power"] = "pseudo",
285300
) -> tuple[PortDataArray, PortDataArray]:
286-
"""Compute the incident and reflected power wave amplitudes at each port.
301+
"""Compute the incident and reflected amplitudes at each port.
287302
The computed amplitudes have not been normalized.
288303
289304
Parameters
@@ -292,11 +307,14 @@ def compute_power_wave_amplitudes_at_each_port(
292307
Reference impedance at each port.
293308
sim_data : :class:`.SimulationData`
294309
Results from the simulation.
310+
wave_type : Literal['pseudo', 'power']
311+
The type of waves computed, either pseudo waves defined by Equation 53 and Equation 54 in [1],
312+
or power waves defined by Equation 4.67 in [2].
295313
296314
Returns
297315
-------
298316
tuple[:class:`.PortDataArray`, :class:`.PortDataArray`]
299-
Incident (a) and reflected (b) power wave amplitudes at each port.
317+
Incident (a) and reflected (b) wave amplitudes at each port.
300318
"""
301319
port_names = [port.name for port in self.ports]
302320
values = np.zeros(
@@ -331,14 +349,41 @@ def compute_power_wave_amplitudes_at_each_port(
331349
V_numpy = np.where(negative_real_Z, -V_numpy, V_numpy)
332350
Z_numpy = np.where(negative_real_Z, -Z_numpy, Z_numpy)
333351

334-
F_numpy = TerminalComponentModeler._compute_F(Z_numpy)
352+
F_numpy = TerminalComponentModeler._compute_F(Z_numpy, wave_type)
353+
354+
b_Zref = Z_numpy
355+
if wave_type == "power":
356+
b_Zref = np.conj(Z_numpy)
335357

358+
# Equations 53 and 54 from [1]
336359
# Equation 4.67 - Pozar - Microwave Engineering 4ed
337360
a.values = F_numpy * (V_numpy + Z_numpy * I_numpy)
338-
b.values = F_numpy * (V_numpy - np.conj(Z_numpy) * I_numpy)
361+
b.values = F_numpy * (V_numpy - b_Zref * I_numpy)
339362

340363
return a, b
341364

365+
def compute_power_wave_amplitudes_at_each_port(
366+
self, port_reference_impedances: PortDataArray, sim_data: SimulationData
367+
) -> tuple[PortDataArray, PortDataArray]:
368+
"""DEPRECATED: Compute the incident and reflected power wave amplitudes at each port.
369+
The computed amplitudes have not been normalized.
370+
371+
Parameters
372+
----------
373+
port_reference_impedances : :class:`.PortDataArray`
374+
Reference impedance at each port.
375+
sim_data : :class:`.SimulationData`
376+
Results from the simulation.
377+
378+
Returns
379+
-------
380+
tuple[:class:`.PortDataArray`, :class:`.PortDataArray`]
381+
Incident (a) and reflected (b) power wave amplitudes at each port.
382+
"""
383+
return self.compute_wave_amplitudes_at_each_port(
384+
port_reference_impedances, sim_data, wave_type="power"
385+
)
386+
342387
@staticmethod
343388
def compute_port_VI(
344389
port_out: TerminalPortType, sim_data: SimulationData
@@ -365,7 +410,7 @@ def compute_port_VI(
365410
def compute_power_wave_amplitudes(
366411
port: Union[LumpedPort, CoaxialLumpedPort], sim_data: SimulationData
367412
) -> tuple[FreqDataArray, FreqDataArray]:
368-
"""Compute the incident and reflected power wave amplitudes at a lumped port.
413+
"""DEPRECATED: Compute the incident and reflected power wave amplitudes at a lumped port.
369414
The computed amplitudes have not been normalized.
370415
371416
Parameters
@@ -388,9 +433,10 @@ def compute_power_wave_amplitudes(
388433

389434
@staticmethod
390435
def compute_power_delivered_by_port(
391-
port: Union[LumpedPort, CoaxialLumpedPort], sim_data: SimulationData
436+
port: Union[LumpedPort, CoaxialLumpedPort],
437+
sim_data: SimulationData,
392438
) -> FreqDataArray:
393-
"""Compute the power delivered to the network by a lumped port.
439+
"""DEPRECATED: Compute the power delivered to the network by a lumped port.
394440
395441
Parameters
396442
----------
@@ -412,7 +458,7 @@ def compute_power_delivered_by_port(
412458
def ab_to_s(
413459
a_matrix: TerminalPortDataArray, b_matrix: TerminalPortDataArray
414460
) -> TerminalPortDataArray:
415-
"""Get the scattering matrix given the power wave matrices."""
461+
"""Get the scattering matrix given the wave amplitude matrices."""
416462
# Ensure dimensions are ordered properly
417463
a_matrix = a_matrix.transpose(*TerminalPortDataArray._dims)
418464
b_matrix = b_matrix.transpose(*TerminalPortDataArray._dims)
@@ -428,44 +474,63 @@ def ab_to_s(
428474

429475
@staticmethod
430476
def s_to_z(
431-
s_matrix: TerminalPortDataArray, reference: Union[complex, PortDataArray]
477+
s_matrix: TerminalPortDataArray,
478+
reference: Union[complex, PortDataArray],
479+
wave_type: Literal["pseudo", "power"] = "pseudo",
432480
) -> DataArray:
433-
"""Get the impedance matrix given the scattering matrix and a reference impedance."""
481+
"""Get the impedance matrix given the scattering matrix and a reference impedance.
482+
483+
Parameters
484+
----------
485+
s_matrix : :class:`.TerminalPortDataArray`
486+
Scattering matrix computed using either the pseudo or power wave formulation.
487+
reference : Union[complex, :class:`.PortDataArray`]
488+
The reference impedance used at each port.
489+
wave_type : Literal['pseudo', 'power']
490+
The type of wave amplitudes used for computing the scattering matrix, either pseudo waves
491+
defined by Equation 53 and Equation 54 in [1] or power waves defined by Equation 4.67 in [2].
492+
"""
434493

435494
# Ensure dimensions are ordered properly
436495
z_matrix = s_matrix.transpose(*TerminalPortDataArray._dims).copy(deep=True)
437496
s_vals = z_matrix.values
438-
eye = np.eye(len(s_matrix.port_out.values), len(s_matrix.port_in.values))
497+
eye = np.eye(len(s_matrix.port_out.values), len(s_matrix.port_in.values))[np.newaxis, :, :]
498+
# Ensure that Zport, F, and Finv act as diagonal matrices when multiplying by left or right
499+
shape_left = (len(s_matrix.f), len(s_matrix.port_out), 1)
500+
shape_right = (len(s_matrix.f), 1, len(s_matrix.port_in))
501+
# Setup the port reference impedance array (scalar)
439502
if isinstance(reference, PortDataArray):
440-
# From Equation 4.68 - Pozar - Microwave Engineering 4ed
441-
# Ensure that Zport, F, and Finv act as diagonal matrices when multiplying by left or right
442-
shape_left = (len(s_matrix.f), len(s_matrix.port_out), 1)
443-
shape_right = (len(s_matrix.f), 1, len(s_matrix.port_in))
444503
Zport = reference.values.reshape(shape_right)
445-
F = TerminalComponentModeler._compute_F(Zport).reshape(shape_right)
504+
F = TerminalComponentModeler._compute_F(Zport, wave_type).reshape(shape_right)
446505
Finv = (1.0 / F).reshape(shape_left)
447-
FinvSF = Finv * s_vals * F
448-
RHS = eye * np.conj(Zport) + FinvSF * Zport
449-
LHS = eye - FinvSF
450-
z_vals = np.matmul(AbstractComponentModeler.inv(LHS), RHS)
451506
else:
452-
# Simpler case when all port impedances are the same
453-
z_vals = (
454-
np.matmul(AbstractComponentModeler.inv(eye - s_vals), (eye + s_vals)) * reference
455-
)
507+
Zport = reference
508+
F = TerminalComponentModeler._compute_F(Zport, wave_type)
509+
Finv = 1.0 / F
510+
# Use conjugfate when S matrix is power-wave based
511+
if wave_type == "power":
512+
Zport_mod = np.conj(Zport)
513+
else:
514+
Zport_mod = Zport
515+
516+
# From equation 74 from [1] for pseudo waves
517+
# From Equation 4.68 - Pozar - Microwave Engineering 4ed for power waves
518+
FinvSF = Finv * s_vals * F
519+
RHS = eye * Zport_mod + FinvSF * Zport
520+
LHS = eye - FinvSF
521+
z_vals = np.linalg.solve(LHS, RHS)
456522

457523
z_matrix.data = z_vals
458524
return z_matrix
459525

460526
@cached_property
461527
def port_reference_impedances(self) -> PortDataArray:
462-
"""The reference impedance used at each port for definining power wave amplitudes.
528+
"""The reference impedance used at each port for definining wave amplitudes.
463529
464530
Note
465531
----
466-
By default, we choose reference impedances to be the conjugate of the load impedance.
467-
For wave ports, this corresponds with the conjugate of the characteristic impedance.
468-
This choice corresponds with Equation 4.64 - Pozar - Microwave Engineering 4ed.
532+
By default, we choose reference impedances to be the load impedance.
533+
For wave ports, this corresponds with the the characteristic impedance.
469534
"""
470535
return self._port_reference_impedances(self.batch_data)
471536

@@ -493,16 +558,19 @@ def _port_reference_impedances(self, batch_data: BatchData) -> PortDataArray:
493558
# LumpedPorts have a constant reference impedance
494559
port_impedances.loc[{"port": port.name}] = np.full(len(self.freqs), port.impedance)
495560

496-
port_impedances = TerminalComponentModeler._set_port_data_array_attributes(
497-
port_impedances.conj()
498-
)
561+
port_impedances = TerminalComponentModeler._set_port_data_array_attributes(port_impedances)
499562
return port_impedances
500563

501564
@staticmethod
502-
def _compute_F(Z_numpy: np.array):
565+
def _compute_F(Z_numpy: np.array, wave_type: Literal["pseudo", "power"] = "pseudo"):
503566
"""Helper to convert port impedance matrix to F, which is used for
504-
computing generalized scattering parameters."""
505-
return 1.0 / (2.0 * np.sqrt(np.real(Z_numpy)))
567+
computing scattering parameters
568+
"""
569+
# Defined in [2] after equation 4.67
570+
if wave_type == "power":
571+
return 1.0 / (2.0 * np.sqrt(np.real(Z_numpy)))
572+
# Equation 75 from [1]
573+
return np.sqrt(np.real(Z_numpy)) / (2.0 * np.abs(Z_numpy))
506574

507575
@cached_property
508576
def _lumped_ports(self) -> list[AbstractLumpedPort]:
@@ -585,7 +653,7 @@ def get_antenna_metrics_data(
585653
) -> AntennaMetricsData:
586654
"""Calculate antenna parameters using superposition of fields from multiple port excitations.
587655
588-
The method computes the radiated far fields and port excitation power wave amplitudes
656+
The method computes the radiated far fields and port excitation wave amplitudes
589657
for a superposition of port excitations, which can be used to analyze antenna radiation
590658
characteristics.
591659
@@ -621,7 +689,7 @@ def get_antenna_metrics_data(
621689
else:
622690
rad_mon = self.get_radiation_monitor_by_name(monitor_name)
623691

624-
# Create data arrays for holding the superposition of all port power wave amplitudes
692+
# Create data arrays for holding the superposition of all port wave amplitudes
625693
f = list(rad_mon.freqs)
626694
coords = {"f": f, "port": port_names}
627695
a_sum = PortDataArray(np.zeros((len(f), len(port_names)), dtype=complex), coords=coords)
@@ -632,8 +700,8 @@ def get_antenna_metrics_data(
632700
sim_data_port = self.batch_data[self._task_name(port=port)]
633701
radiation_data = sim_data_port[rad_mon.name]
634702

635-
a, b = self.compute_power_wave_amplitudes_at_each_port(
636-
self.port_reference_impedances, sim_data_port
703+
a, b = self.compute_wave_amplitudes_at_each_port(
704+
self.port_reference_impedances, sim_data_port, wave_type="pseudo"
637705
)
638706
# Select a possible subset of frequencies
639707
a = a.sel(f=f)
@@ -652,7 +720,7 @@ def get_antenna_metrics_data(
652720
a = scale_factor * a
653721
b = scale_factor * b
654722

655-
# Combine the possibly scaled directivity data and the power wave amplitudes
723+
# Combine the possibly scaled directivity data and the wave amplitudes
656724
if combined_directivity_data is None:
657725
combined_directivity_data = scaled_directivity_data
658726
else:

0 commit comments

Comments
 (0)