Skip to content

Commit f1ebdef

Browse files
dmarek-flexdaquinteroflex
authored andcommitted
improve performance of antenna metrics calculation
1 parent 1686978 commit f1ebdef

File tree

5 files changed

+269
-81
lines changed

5 files changed

+269
-81
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88

9+
## [Unreleased]
10+
11+
### Changed
12+
- Improved performance of antenna metrics calculation by utilizing cached wave amplitude calculations instead of recomputing wave amplitudes for each port excitation in the `TerminalComponentModelerData`.
13+
914
## [v2.10.0rc2] - 2025-10-01
1015

1116
### Added

tests/test_plugins/smatrix/test_terminal_component_modeler.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,11 +1058,12 @@ def test_antenna_helpers(monkeypatch, tmp_path):
10581058

10591059
# Test monitor data normalization with different amplitude types
10601060
a_array = FreqDataArray(np.ones(len(modeler.freqs)), {"f": modeler.freqs})
1061+
a_array_raw = 2.0 * a_array
10611062
normalized_data_array = modeler_data._monitor_data_at_port_amplitude(
1062-
modeler.ports[0], radiation_monitor.name, a_array
1063+
modeler.ports[0], radiation_monitor.name, a_array, a_array_raw
10631064
)
10641065
normalized_data_const = modeler_data._monitor_data_at_port_amplitude(
1065-
modeler.ports[0], radiation_monitor.name, 1.0
1066+
modeler.ports[0], radiation_monitor.name, 1.0, a_array_raw
10661067
)
10671068
assert isinstance(normalized_data_array, td.DirectivityData)
10681069
assert isinstance(normalized_data_const, td.DirectivityData)
@@ -1198,7 +1199,7 @@ def test_run_only_and_element_mappings(monkeypatch, tmp_path):
11981199
xy_grid = td.UniformGrid(dl=0.1 * 1e3)
11991200
grid_spec = td.GridSpec(grid_x=xy_grid, grid_y=xy_grid, grid_z=z_grid)
12001201
modeler = make_coaxial_component_modeler(
1201-
port_types=(CoaxialLumpedPort, WavePort), grid_spec=grid_spec
1202+
port_types=(CoaxialLumpedPort, CoaxialLumpedPort), grid_spec=grid_spec
12021203
)
12031204
port0_idx = modeler.network_index(modeler.ports[0])
12041205
port1_idx = modeler.network_index(modeler.ports[1])

tidy3d/plugins/smatrix/analysis/antenna.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,6 @@
55
import numpy as np
66

77
from tidy3d.components.microwave.data.monitor_data import AntennaMetricsData
8-
from tidy3d.plugins.smatrix.analysis.terminal import (
9-
compute_wave_amplitudes_at_each_port,
10-
)
118
from tidy3d.plugins.smatrix.data.data_array import PortDataArray
129
from tidy3d.plugins.smatrix.data.terminal import TerminalComponentModelerData
1310

@@ -69,26 +66,27 @@ def get_antenna_metrics_data(
6966
coords=coords,
7067
)
7168
b_sum = a_sum.copy()
69+
a_matrix, b_matrix = terminal_component_modeler_data.port_power_wave_matrices
7270
# Retrieve associated simulation data
7371
combined_directivity_data = None
7472
for port, amplitude in port_dict.items():
73+
port_in_index = terminal_component_modeler_data.modeler.network_index(port)
7574
if amplitude is not None:
7675
if np.isclose(amplitude, 0.0):
7776
continue
7877
sim_data_port = terminal_component_modeler_data.data[
7978
terminal_component_modeler_data.modeler.get_task_name(port)
8079
]
8180

82-
a, b = compute_wave_amplitudes_at_each_port(
83-
modeler=terminal_component_modeler_data.modeler,
84-
port_reference_impedances=terminal_component_modeler_data.port_reference_impedances,
85-
sim_data=sim_data_port,
86-
s_param_def="power",
81+
a, b = (
82+
a_matrix.sel(port_in=port_in_index, drop=True),
83+
b_matrix.sel(port_in=port_in_index, drop=True),
8784
)
85+
8886
# Select a possible subset of frequencies
8987
a = a.sel(f=f)
9088
b = b.sel(f=f)
91-
a_raw = a.sel(port=terminal_component_modeler_data.modeler.network_index(port))
89+
a_raw = a.sel(port_out=port_in_index)
9290

9391
if amplitude is None:
9492
# No scaling performed when amplitude is None
@@ -97,7 +95,7 @@ def get_antenna_metrics_data(
9795
else:
9896
scaled_directivity_data = (
9997
terminal_component_modeler_data._monitor_data_at_port_amplitude(
100-
port, rad_mon.name, amplitude
98+
port, rad_mon.name, amplitude, a_raw
10199
)
102100
)
103101
scale_factor = amplitude / a_raw
@@ -109,8 +107,8 @@ def get_antenna_metrics_data(
109107
combined_directivity_data = scaled_directivity_data
110108
else:
111109
combined_directivity_data = combined_directivity_data + scaled_directivity_data
112-
a_sum += a
113-
b_sum += b
110+
a_sum += a.rename({"port_out": "port"})
111+
b_sum += b.rename({"port_out": "port"})
114112

115113
# Compute and add power measures to results
116114
power_incident = np.real(0.5 * a_sum * np.conj(a_sum)).sum(dim="port")

tidy3d/plugins/smatrix/analysis/terminal.py

Lines changed: 96 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -71,33 +71,10 @@ def terminal_construct_smatrix(
7171
source_indices = list(modeler_data.modeler.matrix_indices_source)
7272
run_source_indices = list(modeler_data.modeler.matrix_indices_run_sim)
7373

74-
values = np.zeros(
75-
(len(modeler_data.modeler.freqs), len(monitor_indices), len(source_indices)),
76-
dtype=complex,
77-
)
78-
coords = {
79-
"f": np.array(modeler_data.modeler.freqs),
80-
"port_out": monitor_indices,
81-
"port_in": source_indices,
82-
}
83-
a_matrix = TerminalPortDataArray(values, coords=coords)
84-
b_matrix = a_matrix.copy(deep=True)
85-
86-
# Tabulate the reference impedances at each port and frequency
87-
port_impedances = port_reference_impedances(modeler_data=modeler_data)
88-
89-
for source_index in run_source_indices:
90-
port, mode_index = modeler_data.modeler.network_dict[source_index]
91-
sim_data = modeler_data.data[
92-
modeler_data.modeler.get_task_name(port=port, mode_index=mode_index)
93-
]
94-
a, b = modeler_data.compute_wave_amplitudes_at_each_port(
95-
port_reference_impedances=port_impedances, sim_data=sim_data, s_param_def=s_param_def
96-
)
97-
98-
indexer = {"port_in": source_index}
99-
a_matrix = a_matrix._with_updated_data(data=a.data, coords=indexer)
100-
b_matrix = b_matrix._with_updated_data(data=b.data, coords=indexer)
74+
if s_param_def == "pseudo":
75+
a_matrix, b_matrix = modeler_data.port_pseudo_wave_matrices
76+
else:
77+
a_matrix, b_matrix = modeler_data.port_power_wave_matrices
10178

10279
# If excitation is assumed ideal, a_matrix is assumed to be diagonal
10380
# and the explicit inverse can be avoided. When only a subset of excitations
@@ -109,6 +86,8 @@ def terminal_construct_smatrix(
10986
# Scale each column by the corresponding diagonal entry
11087
s_matrix = b_matrix / a_diag[:, np.newaxis, :]
11188

89+
# Expand the smatrix using user defined mappings
90+
s_matrix_expanded = s_matrix.reindex(port_in=source_indices, fill_value=0.0)
11291
# element can be determined by user-defined mapping
11392
for (row_in, col_in), (row_out, col_out), mult_by in modeler_data.modeler.element_mappings:
11493
coords_from = {
@@ -119,9 +98,9 @@ def terminal_construct_smatrix(
11998
"port_in": col_out,
12099
"port_out": row_out,
121100
}
122-
data = mult_by * s_matrix.loc[coords_from].data
123-
s_matrix = s_matrix._with_updated_data(data=data, coords=coords_to)
124-
return s_matrix
101+
data = mult_by * s_matrix_expanded.loc[coords_from].data
102+
s_matrix_expanded = s_matrix_expanded._with_updated_data(data=data, coords=coords_to)
103+
return s_matrix_expanded
125104

126105

127106
def port_reference_impedances(modeler_data: TerminalComponentModelerData) -> PortDataArray:
@@ -179,32 +158,29 @@ def port_reference_impedances(modeler_data: TerminalComponentModelerData) -> Por
179158
return port_impedances
180159

181160

182-
def compute_wave_amplitudes_at_each_port(
161+
def _compute_port_voltages_currents(
183162
modeler: TerminalComponentModeler,
184-
port_reference_impedances: PortDataArray,
185163
sim_data: SimulationData,
186-
s_param_def: SParamDef = "pseudo",
187164
) -> tuple[PortDataArray, PortDataArray]:
188-
"""Compute the incident and reflected amplitudes at each port.
165+
"""Compute voltage and current values at all ports for a single simulation.
189166
190-
The computed amplitudes have not been normalized.
167+
This function calculates the voltage and current at each monitor port from the
168+
electromagnetic field data in a single simulation result. The voltages and currents
169+
are computed according to the specific port type (e.g., lumped, wave) and are used
170+
as inputs for subsequent wave amplitude calculations.
191171
192172
Parameters
193173
----------
194174
modeler : :class:`.TerminalComponentModeler`
195-
The component modeler defining the ports and simulation settings.
196-
port_reference_impedances : :class:`.PortDataArray`
197-
Reference impedance at each port.
175+
The component modeler containing port definitions and network mapping.
198176
sim_data : :class:`.SimulationData`
199-
Results from a single simulation run.
200-
s_param_def : SParamDef
201-
The type of waves computed, either pseudo waves defined by Equation 53 and
202-
Equation 54 in [1], or power waves defined by Equation 4.67 in [2].
177+
Simulation results containing the electromagnetic field data.
203178
204179
Returns
205180
-------
206181
tuple[:class:`.PortDataArray`, :class:`.PortDataArray`]
207-
Incident (a) and reflected (b) wave amplitudes at each port.
182+
A tuple containing the voltage and current arrays with dimensions (f, port),
183+
where voltages and currents are computed for each frequency and monitor port.
208184
"""
209185
network_indices = list(modeler.matrix_indices_monitor)
210186
values = np.zeros(
@@ -218,18 +194,56 @@ def compute_wave_amplitudes_at_each_port(
218194

219195
V_matrix = PortDataArray(values, coords=coords)
220196
I_matrix = V_matrix.copy(deep=True)
221-
a = V_matrix.copy(deep=True)
222-
b = V_matrix.copy(deep=True)
223197

224198
for network_index in network_indices:
225199
port, mode_index = modeler.network_dict[network_index]
226200
V_out, I_out = compute_port_VI(port, sim_data)
227201
indexer = {"port": network_index}
228202
V_matrix = V_matrix._with_updated_data(data=V_out.data, coords=indexer)
229203
I_matrix = I_matrix._with_updated_data(data=I_out.data, coords=indexer)
204+
return (V_matrix, I_matrix)
205+
206+
207+
def _compute_wave_amplitudes_from_VI(
208+
port_reference_impedances: PortDataArray,
209+
port_voltages: PortDataArray,
210+
port_currents: PortDataArray,
211+
s_param_def: SParamDef = "pseudo",
212+
) -> tuple[PortDataArray, PortDataArray]:
213+
"""Convert port voltages and currents to incident and reflected wave amplitudes.
214+
215+
This function transforms voltage and current data at each port into forward-traveling
216+
(incident, 'a') and backward-traveling (reflected, 'b') wave amplitudes using the
217+
specified wave definition. The conversion handles impedance sign consistency and
218+
applies the appropriate normalization based on the chosen S-parameter definition.
219+
220+
The wave amplitudes are computed using:
221+
- Pseudo waves: Equations 53-54 from Marks and Williams [1]
222+
- Power waves: Equation 4.67 from Pozar [2]
230223
231-
V_numpy = V_matrix.values
232-
I_numpy = I_matrix.values
224+
Parameters
225+
----------
226+
port_reference_impedances : :class:`.PortDataArray`
227+
Reference impedance values for each port with dimensions (f, port).
228+
port_voltages : :class:`.PortDataArray`
229+
Voltage values at each port with dimensions (f, port).
230+
port_currents : :class:`.PortDataArray`
231+
Current values at each port with dimensions (f, port).
232+
s_param_def : SParamDef, optional
233+
Wave definition type: "pseudo" for pseudo waves or "power" for power waves.
234+
Defaults to "pseudo".
235+
236+
Returns
237+
-------
238+
tuple[:class:`.PortDataArray`, :class:`.PortDataArray`]
239+
A tuple containing the incident (a) and reflected (b) wave amplitude arrays,
240+
each with dimensions (f, port) representing the wave amplitudes at each
241+
frequency and port.
242+
"""
243+
a = port_voltages.copy(deep=True)
244+
b = port_currents.copy(deep=True)
245+
V_numpy = port_voltages.values
246+
I_numpy = port_currents.values
233247
Z_numpy = port_reference_impedances.values
234248

235249
# Check to make sure sign is consistent for all impedance values
@@ -254,6 +268,41 @@ def compute_wave_amplitudes_at_each_port(
254268
return a, b
255269

256270

271+
def compute_wave_amplitudes_at_each_port(
272+
modeler: TerminalComponentModeler,
273+
port_reference_impedances: PortDataArray,
274+
sim_data: SimulationData,
275+
s_param_def: SParamDef = "pseudo",
276+
) -> tuple[PortDataArray, PortDataArray]:
277+
"""Compute the incident and reflected amplitudes at each port.
278+
279+
The computed amplitudes have not been normalized.
280+
281+
Parameters
282+
----------
283+
modeler : :class:`.TerminalComponentModeler`
284+
The component modeler defining the ports and simulation settings.
285+
port_reference_impedances : :class:`.PortDataArray`
286+
Reference impedance at each port.
287+
sim_data : :class:`.SimulationData`
288+
Results from a single simulation run.
289+
s_param_def : SParamDef
290+
The type of waves computed, either pseudo waves defined by Equation 53 and
291+
Equation 54 in [1], or power waves defined by Equation 4.67 in [2].
292+
293+
Returns
294+
-------
295+
tuple[:class:`.PortDataArray`, :class:`.PortDataArray`]
296+
Incident (a) and reflected (b) wave amplitudes at each port.
297+
"""
298+
299+
port_voltages, port_currents = _compute_port_voltages_currents(modeler, sim_data)
300+
301+
return _compute_wave_amplitudes_from_VI(
302+
port_reference_impedances, port_voltages, port_currents, s_param_def=s_param_def
303+
)
304+
305+
257306
def compute_power_wave_amplitudes_at_each_port(
258307
modeler: TerminalComponentModeler,
259308
port_reference_impedances: PortDataArray,

0 commit comments

Comments
 (0)