Skip to content

Commit 60bee1f

Browse files
Changes so far
1 parent 55cb5d4 commit 60bee1f

File tree

3 files changed

+196
-110
lines changed

3 files changed

+196
-110
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
from __future__ import annotations
2+
3+
from typing import Optional
4+
5+
import numpy as np
6+
7+
from tidy3d.components.microwave.data.monitor_data import AntennaMetricsData
8+
from tidy3d.plugins.smatrix.analysis.terminal import (
9+
compute_power_wave_amplitudes_at_each_port,
10+
)
11+
from tidy3d.plugins.smatrix.data.data_array import PortDataArray
12+
from tidy3d.plugins.smatrix.data.terminal import TerminalComponentModelerData
13+
14+
15+
def get_antenna_metrics_data(
16+
terminal_component_modeler_data: TerminalComponentModelerData,
17+
port_amplitudes: Optional[dict[str, complex]] = None,
18+
monitor_name: Optional[str] = None,
19+
) -> AntennaMetricsData:
20+
"""Calculate antenna parameters using superposition of fields from multiple port excitations.
21+
22+
The method computes the radiated far fields and port excitation power wave amplitudes
23+
for a superposition of port excitations, which can be used to analyze antenna radiation
24+
characteristics.
25+
26+
Parameters
27+
----------
28+
terminal_component_modeler_data: TerminalComponentModelerData
29+
Data associated with a :class:`TerminalComponentModeler` simulation run.
30+
port_amplitudes : dict[str, complex] = None
31+
Dictionary mapping port names to their desired excitation amplitudes. For each port,
32+
:math:`\\frac{1}{2}|a|^2` represents the incident power from that port into the system.
33+
If None, uses only the first port without any scaling of the raw simulation data.
34+
monitor_name : str = None
35+
Name of the :class:`.DirectivityMonitor` to use for calculating far fields.
36+
If None, uses the first monitor in `radiation_monitors`.
37+
38+
Returns
39+
-------
40+
:class:`.AntennaMetricsData`
41+
Container with antenna parameters including directivity, gain, and radiation efficiency,
42+
computed from the superposition of fields from all excited ports.
43+
"""
44+
# Use the first port as default if none specified
45+
if port_amplitudes is None:
46+
port_amplitudes = {terminal_component_modeler_data.modeler.ports[0].name: None}
47+
port_names = [port.name for port in terminal_component_modeler_data.modeler.ports]
48+
# Check port names, and create map from port to amplitude
49+
port_dict = {}
50+
for key in port_amplitudes.keys():
51+
port = terminal_component_modeler_data.modeler.get_port_by_name(port_name=key)
52+
port_dict[port] = port_amplitudes[key]
53+
# Get the radiation monitor, use first as default
54+
# if none specified
55+
if monitor_name is None:
56+
rad_mon = terminal_component_modeler_data.modeler.radiation_monitors[0]
57+
else:
58+
rad_mon = terminal_component_modeler_data.modeler.get_radiation_monitor_by_name(
59+
monitor_name
60+
)
61+
62+
# Create data arrays for holding the superposition of all port power wave amplitudes
63+
f = list(rad_mon.freqs)
64+
coords = {"f": f, "port": port_names}
65+
a_sum = PortDataArray(np.zeros((len(f), len(port_names)), dtype=complex), coords=coords)
66+
b_sum = a_sum.copy()
67+
# Retrieve associated simulation data
68+
combined_directivity_data = None
69+
for port, amplitude in port_dict.items():
70+
sim_data_port = terminal_component_modeler_data.data.data[port]
71+
radiation_data = sim_data_port[rad_mon.name]
72+
73+
a, b = compute_power_wave_amplitudes_at_each_port(
74+
modeler=terminal_component_modeler_data.modeler,
75+
port_reference_impedances=terminal_component_modeler_data.modeler.port_reference_impedances,
76+
sim_data=sim_data_port,
77+
)
78+
# Select a possible subset of frequencies
79+
a = a.sel(f=f)
80+
b = b.sel(f=f)
81+
a_raw = a.sel(port=port.name)
82+
83+
if amplitude is None:
84+
# No scaling performed when amplitude is None
85+
scaled_directivity_data = sim_data_port[rad_mon.name]
86+
scale_factor = 1.0
87+
else:
88+
scaled_directivity_data = (
89+
terminal_component_modeler_data._monitor_data_at_port_amplitude(
90+
port, sim_data_port, radiation_data, amplitude
91+
)
92+
)
93+
scale_factor = amplitude / a_raw
94+
a = scale_factor * a
95+
b = scale_factor * b
96+
97+
# Combine the possibly scaled directivity data and the power wave amplitudes
98+
if combined_directivity_data is None:
99+
combined_directivity_data = scaled_directivity_data
100+
else:
101+
combined_directivity_data = combined_directivity_data + scaled_directivity_data
102+
a_sum += a
103+
b_sum += b
104+
105+
# Compute and add power measures to results
106+
power_incident = np.real(0.5 * a_sum * np.conj(a_sum)).sum(dim="port")
107+
power_reflected = np.real(0.5 * b_sum * np.conj(b_sum)).sum(dim="port")
108+
return AntennaMetricsData.from_directivity_data(
109+
combined_directivity_data, power_incident, power_reflected
110+
)

tidy3d/plugins/smatrix/analysis/terminal.py

Lines changed: 32 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,22 @@
1717

1818
def terminal_construct_smatrix(modeler_data: TerminalComponentModelerData) -> TerminalPortDataArray:
1919
"""
20-
Constructs the scattering matrix (S-matrix) from raw simulation data.
21-
22-
This function iterates through each port, treating it as an input source once.
23-
For each input source, it calculates the resulting incident (a) and reflected (b)
24-
power wave amplitudes at all ports. These amplitudes are compiled into matrices,
25-
which are then used to compute the final S-matrix.
26-
27-
Parameters
28-
----------
29-
modeler_data
30-
31-
Returns
32-
-------
33-
TerminalPortDataArray
34-
The computed S-matrix as a data array with dimensions for frequency,
35-
output port, and input port.
20+
Constructs the scattering matrix (S-matrix) from raw simulation data stored in a :class:`TerminalComponentModelerData`
21+
22+
This function iterates through each port excitation simulation. For each run,
23+
it calculates the resulting incident ('a') and reflected ('b') power wave
24+
amplitudes at all ports. These amplitudes are compiled into matrices,
25+
which are then used to compute the final S-matrix using the formula
26+
:math:`S = b a^{-1}`.
27+
28+
Args:
29+
modeler_data: Data object containing the modeler definition and the raw
30+
results from each port simulation run.
31+
32+
Returns:
33+
TerminalPortDataArray
34+
The computed S-matrix as a data array with dimensions for frequency,
35+
output port, and input port.
3636
"""
3737

3838
port_names = [port.name for port in modeler_data.modeler.ports]
@@ -67,28 +67,22 @@ def terminal_construct_smatrix(modeler_data: TerminalComponentModelerData) -> Te
6767

6868

6969
def port_reference_impedances(modeler_data: TerminalComponentModelerData) -> PortDataArray:
70-
"""
71-
Calculates the reference impedance for each port across all frequencies.
72-
73-
This function determines the characteristic impedance for every port in the simulation.
74-
It handles two types of ports differently:
75-
- `WavePort`: The impedance is frequency-dependent and is computed from the modal
76-
properties stored in the simulation data.
77-
- Other ports (e.g., `LumpedPort`): The impedance is a constant value defined by the
78-
user.
79-
80-
Parameters
81-
----------
82-
modeler : TerminalComponentModeler
83-
The modeler setup defining the ports.
84-
batch_data : BatchData, optional
85-
Pre-computed simulation results, required for `WavePort` impedance calculation.
86-
If None, the batch data from the simulation object is used.
87-
88-
Returns
89-
-------
90-
PortDataArray
91-
A data array containing the complex impedance for each port at each frequency.
70+
"""Calculates the reference impedance for each port across all frequencies.
71+
72+
This function determines the characteristic impedance for every port defined
73+
in the modeler. It handles two types of ports differently: for a
74+
:class:`.WavePort`, the impedance is frequency-dependent and computed from
75+
modal properties, while for other types like :class:`.LumpedPort`, the
76+
impedance is a user-defined constant value.
77+
78+
Args:
79+
modeler_data: Data object containing the modeler definition and the raw
80+
simulation data needed for :class:`.WavePort` impedance calculations.
81+
82+
Returns:
83+
TerminalComponentModelerData
84+
A data array containing the complex impedance for each port at each
85+
frequency.
9286
"""
9387
port_names = [port.name for port in modeler_data.modeler.ports]
9488

tidy3d/plugins/smatrix/data/terminal.py

Lines changed: 54 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,13 @@ class MicrowaveSMatrixData(Tidy3dBaseModel):
3434
port_reference_impedances: Optional[PortDataArray] = pd.Field(
3535
None,
3636
title="Port Reference Impedances",
37-
description="Reference impedance for each port used in the S-parameter calculation. "
38-
"This is optional and may not be present if not specified or computed.",
37+
description="Reference impedance for each port used in the S-parameter calculation. This is optional and may not be present if not specified or computed.",
3938
)
4039

4140
data: TerminalPortDataArray = pd.Field(
4241
...,
4342
title="S-Matrix Data",
44-
description="An array containing the computed S-matrix of the device. The data is organized "
45-
"by terminal ports, representing the scattering parameters between them.",
43+
description="An array containing the computed S-matrix of the device. The data is organized by terminal ports, representing the scattering parameters between them.",
4644
)
4745

4846

@@ -51,8 +49,17 @@ class TerminalComponentModelerData(Tidy3dBaseModel):
5149
Data associated with a :class:`TerminalComponentModeler` simulation run.
5250
5351
This class serves as a data container for the results of a component modeler simulation,
54-
holding the original simulation definition, the resulting S-matrix or raw port data,
55-
and the solver log.
52+
with the original simulation definition, and port simulation data, and the solver log.
53+
54+
See Also
55+
--------
56+
:func:`tidy3d.plugins.smatrix.utils.ab_to_s`
57+
:func:`tidy3d.plugins.smatrix.utils.check_port_impedance_sign`
58+
:func:`tidy3d.plugins.smatrix.utils.compute_F`
59+
:func:`tidy3d.plugins.smatrix.utils.compute_port_VI`
60+
:func:`tidy3d.plugins.smatrix.utils.compute_power_delivered_by_port`
61+
:func:`tidy3d.plugins.smatrix.utils.compute_power_wave_amplitudes`
62+
:func:`tidy3d.plugins.smatrix.utils.s_to_z`
5663
"""
5764

5865
modeler: TerminalComponentModeler = pd.Field(
@@ -140,84 +147,59 @@ def get_antenna_metrics_data(
140147
Container with antenna parameters including directivity, gain, and radiation efficiency,
141148
computed from the superposition of fields from all excited ports.
142149
"""
143-
from tidy3d.plugins.smatrix.analysis.terminal import (
144-
compute_power_wave_amplitudes_at_each_port,
145-
)
150+
from tidy3d.plugins.smatrix.analysis.antenna import get_antenna_metrics_data
146151

147-
# Use the first port as default if none specified
148-
if port_amplitudes is None:
149-
port_amplitudes = {self.modeler.ports[0].name: None}
150-
port_names = [port.name for port in self.modeler.ports]
151-
# Check port names, and create map from port to amplitude
152-
port_dict = {}
153-
for key in port_amplitudes.keys():
154-
port = self.modeler.get_port_by_name(port_name=key)
155-
port_dict[port] = port_amplitudes[key]
156-
# Get the radiation monitor, use first as default
157-
# if none specified
158-
if monitor_name is None:
159-
rad_mon = self.modeler.radiation_monitors[0]
160-
else:
161-
rad_mon = self.modeler.get_radiation_monitor_by_name(monitor_name)
162-
163-
# Create data arrays for holding the superposition of all port power wave amplitudes
164-
f = list(rad_mon.freqs)
165-
coords = {"f": f, "port": port_names}
166-
a_sum = PortDataArray(np.zeros((len(f), len(port_names)), dtype=complex), coords=coords)
167-
b_sum = a_sum.copy()
168-
# Retrieve associated simulation data
169-
combined_directivity_data = None
170-
for port, amplitude in port_dict.items():
171-
sim_data_port = self.data.data[port]
172-
radiation_data = sim_data_port[rad_mon.name]
173-
174-
a, b = compute_power_wave_amplitudes_at_each_port(
175-
modeler=self.modeler,
176-
port_reference_impedances=self.modeler.port_reference_impedances,
177-
sim_data=sim_data_port,
178-
)
179-
# Select a possible subset of frequencies
180-
a = a.sel(f=f)
181-
b = b.sel(f=f)
182-
a_raw = a.sel(port=port.name)
183-
184-
if amplitude is None:
185-
# No scaling performed when amplitude is None
186-
scaled_directivity_data = sim_data_port[rad_mon.name]
187-
scale_factor = 1.0
188-
else:
189-
scaled_directivity_data = self._monitor_data_at_port_amplitude(
190-
port, sim_data_port, radiation_data, amplitude
191-
)
192-
scale_factor = amplitude / a_raw
193-
a = scale_factor * a
194-
b = scale_factor * b
195-
196-
# Combine the possibly scaled directivity data and the power wave amplitudes
197-
if combined_directivity_data is None:
198-
combined_directivity_data = scaled_directivity_data
199-
else:
200-
combined_directivity_data = combined_directivity_data + scaled_directivity_data
201-
a_sum += a
202-
b_sum += b
203-
204-
# Compute and add power measures to results
205-
power_incident = np.real(0.5 * a_sum * np.conj(a_sum)).sum(dim="port")
206-
power_reflected = np.real(0.5 * b_sum * np.conj(b_sum)).sum(dim="port")
207-
return AntennaMetricsData.from_directivity_data(
208-
combined_directivity_data, power_incident, power_reflected
152+
antenna_metrics_data = get_antenna_metrics_data(
153+
terminal_component_modeler_data=self,
154+
port_amplitudes=port_amplitudes,
155+
monitor_name=monitor_name,
209156
)
157+
return antenna_metrics_data
210158

211159
@cached_property
212160
def port_reference_impedances(self) -> PortDataArray:
213-
"""The reference impedance used at each port for definining power wave amplitudes."""
161+
"""Calculates the reference impedance for each port across all frequencies.
162+
163+
This function determines the characteristic impedance for every port defined
164+
in the modeler. It handles two types of ports differently: for a
165+
:class:`.WavePort`, the impedance is frequency-dependent and computed from
166+
modal properties, while for other types like :class:`.LumpedPort`, the
167+
impedance is a user-defined constant value.
168+
169+
170+
Returns:
171+
A data array containing the complex impedance for each port at each
172+
frequency.
173+
"""
214174
from tidy3d.plugins.smatrix.analysis.terminal import port_reference_impedances
215175

216176
return port_reference_impedances(self.modeler)
217177

218178
def compute_power_wave_amplitudes_at_each_port(
219179
self, port_reference_impedances: PortDataArray, sim_data: SimulationData
220180
) -> tuple[PortDataArray, PortDataArray]:
181+
"""Computes power waves 'a' and 'b' at all ports from one simulation.
182+
183+
This function converts raw voltage (V) and current (I) at each port into
184+
incident (a) and reflected (b) power wave amplitudes. The conversion uses
185+
the formulas:
186+
:math:`a = F (V + ZI)`
187+
:math:`b = F (V - Z^*I)`
188+
where :math:`Z` is the port impedance and :math:`F` is a normalization
189+
factor. A sanity check ensures the real part of the reference impedance
190+
is positive, flipping signs of V and Z if needed for physical consistency.
191+
192+
Args:
193+
modeler: The modeler setup defining the ports.
194+
port_reference_impedances: The characteristic impedance for each port
195+
at each frequency.
196+
sim_data: The raw results (fields, currents, etc.) from a single
197+
simulation run with one port excited.
198+
199+
Returns:
200+
A tuple containing two :class:`.PortDataArray` objects: the incident
201+
power wave amplitudes 'a' and the reflected power wave amplitudes 'b'.
202+
"""
221203
from tidy3d.plugins.smatrix.analysis.terminal import (
222204
compute_power_wave_amplitudes_at_each_port,
223205
)

0 commit comments

Comments
 (0)