Skip to content

Commit 7198c7a

Browse files
committed
fix(rf): WavePort mode handling in antenna metrics
1 parent 2f3646a commit 7198c7a

File tree

6 files changed

+60
-18
lines changed

6 files changed

+60
-18
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2424
- Solver error for named 2D materials with inhomogeneous substrates.
2525
- In `Tidy3dBaseModel` the hash (and cached `.json_string`) are now sensitive to changes in `.attrs`.
2626
- More accurate frequency range for ``GaussianPulse`` when DC is removed.
27+
- Bug in `TerminalComponentModelerData.get_antenna_metrics_data()` where `WavePort` mode indices were not properly handled. Improved docstrings and type hints to make the usage clearer.
2728

2829
## [v2.10.0rc2] - 2025-10-01
2930

tests/test_plugins/smatrix/terminal_component_modeler_def.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,8 @@ def make_coaxial_simulation(length: Optional[float] = None, grid_spec: td.GridSp
227227
# Make simulation
228228
center_sim = [0, 0, 0]
229229
size_sim = [
230-
2 * Router,
231-
2 * Router,
230+
4 * Router,
231+
4 * Router,
232232
length + 0.5 * wavelength0,
233233
]
234234

tests/test_plugins/smatrix/test_terminal_component_modeler.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1097,10 +1097,16 @@ def test_antenna_helpers(monkeypatch, tmp_path):
10971097
assert isinstance(b, PortDataArray)
10981098

10991099

1100-
def test_antenna_parameters(monkeypatch, tmp_path):
1100+
@pytest.mark.parametrize("port_type", ["lumped", "wave"])
1101+
def test_antenna_parameters(monkeypatch, port_type):
11011102
"""Test basic antenna parameters computation and validation."""
11021103
# Setup modeler with radiation monitor
1103-
modeler = make_component_modeler(False)
1104+
if port_type == "lumped":
1105+
modeler: TerminalComponentModeler = make_component_modeler(False)
1106+
else:
1107+
modeler: TerminalComponentModeler = make_coaxial_component_modeler(
1108+
port_types=(WavePort, WavePort)
1109+
)
11041110
sim = modeler.simulation
11051111
theta = np.linspace(0, np.pi, 101)
11061112
phi = np.linspace(0, 2 * np.pi, 201)
@@ -1126,6 +1132,12 @@ def test_antenna_parameters(monkeypatch, tmp_path):
11261132

11271133
# Run simulation and get antenna parameters
11281134
modeler_data = run_component_modeler(monkeypatch, modeler)
1135+
1136+
# Make sure network index works for single mode / multimode cases
1137+
port_1_network_index = modeler.network_index(modeler.ports[0])
1138+
port_2_network_index = modeler.network_index(modeler.ports[1], 0)
1139+
_ = modeler_data.get_antenna_metrics_data({port_1_network_index: 1.0})
1140+
_ = modeler_data.get_antenna_metrics_data({port_2_network_index: None})
11291141
antenna_params = modeler_data.get_antenna_metrics_data()
11301142

11311143
# Test that all essential parameters exist and are correct type

tidy3d/plugins/smatrix/analysis/antenna.py

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
from tidy3d.components.microwave.data.monitor_data import AntennaMetricsData
88
from tidy3d.plugins.smatrix.data.data_array import PortDataArray
99
from tidy3d.plugins.smatrix.data.terminal import TerminalComponentModelerData
10+
from tidy3d.plugins.smatrix.ports.wave import WavePort
11+
from tidy3d.plugins.smatrix.types import NetworkIndex
1012

1113

1214
def get_antenna_metrics_data(
1315
terminal_component_modeler_data: TerminalComponentModelerData,
14-
port_amplitudes: Optional[dict[str, complex]] = None,
16+
port_amplitudes: Optional[dict[NetworkIndex, complex]] = None,
1517
monitor_name: Optional[str] = None,
1618
) -> AntennaMetricsData:
1719
"""Calculate antenna parameters using superposition of fields from multiple port excitations.
@@ -20,17 +22,27 @@ def get_antenna_metrics_data(
2022
for a superposition of port excitations, which can be used to analyze antenna radiation
2123
characteristics.
2224
25+
Note
26+
----
27+
The ``NetworkIndex`` identifies a single excitation in the modeled device, so it represents
28+
a :class:`.LumpedPort` or a single mode from a :class:`.WavePort`. Use the static method
29+
:meth:`.TerminalComponentModeler.network_index` to convert port and optional mode index
30+
into the appropriate ``NetworkIndex`` for use in the ``port_amplitudes`` dictionary.
31+
2332
Parameters
2433
----------
2534
terminal_component_modeler_data: TerminalComponentModelerData
2635
Data associated with a :class:`.TerminalComponentModeler` simulation run.
27-
port_amplitudes : dict[str, complex] = None
28-
Dictionary mapping port names to their desired excitation amplitudes. For each port,
36+
port_amplitudes : dict[NetworkIndex, complex] = None
37+
Dictionary mapping a network index to their desired excitation amplitudes. For each network port,
2938
:math:`\\frac{1}{2}|a|^2` represents the incident power from that port into the system.
30-
If None, uses only the first port without any scaling of the raw simulation data.
39+
If ``None``, uses only the first port without any scaling of the raw simulation data. When
40+
``None`` is passed as a port amplitude, the raw simulation data is used for that port. Note
41+
that in this method ``a`` represents the incident wave amplitude using the power wave definition
42+
in [2].
3143
monitor_name : str = None
3244
Name of the :class:`.DirectivityMonitor` to use for calculating far fields.
33-
If None, uses the first monitor in `radiation_monitors`.
45+
If ``None``, uses the first monitor in ``radiation_monitors``.
3446
3547
Returns
3648
-------
@@ -40,7 +52,13 @@ def get_antenna_metrics_data(
4052
"""
4153
# Use the first port as default if none specified
4254
if port_amplitudes is None:
43-
port_amplitudes = {terminal_component_modeler_data.modeler.ports[0].name: None}
55+
first_port = terminal_component_modeler_data.modeler.ports[0]
56+
mode_index = None
57+
if isinstance(first_port, WavePort):
58+
mode_index = first_port.mode_index
59+
port_amplitudes = {
60+
terminal_component_modeler_data.modeler.network_index(first_port, mode_index): None
61+
}
4462
# Check port names, and create map from port to amplitude
4563
port_dict = {}
4664
for key in port_amplitudes.keys():
@@ -71,11 +89,12 @@ def get_antenna_metrics_data(
7189
combined_directivity_data = None
7290
for port, amplitude in port_dict.items():
7391
port_in_index = terminal_component_modeler_data.modeler.network_index(port)
92+
_, mode_index = terminal_component_modeler_data.modeler.network_dict[port_in_index]
7493
if amplitude is not None:
7594
if np.isclose(amplitude, 0.0):
7695
continue
7796
sim_data_port = terminal_component_modeler_data.data[
78-
terminal_component_modeler_data.modeler.get_task_name(port)
97+
terminal_component_modeler_data.modeler.get_task_name(port, mode_index)
7998
]
8099

81100
a, b = (

tidy3d/plugins/smatrix/component_modelers/base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,8 @@ def get_task_name(
179179
ValueError
180180
If an invalid `format` string is provided.
181181
"""
182+
if isinstance(port, WavePort) and mode_index is None:
183+
mode_index = port.mode_index
182184
if mode_index is not None:
183185
return f"{port.name}@{mode_index}"
184186
if format == "PF":

tidy3d/plugins/smatrix/data/terminal.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from tidy3d.plugins.smatrix.data.base import AbstractComponentModelerData
1818
from tidy3d.plugins.smatrix.data.data_array import PortDataArray, TerminalPortDataArray
1919
from tidy3d.plugins.smatrix.ports.types import TerminalPortType
20-
from tidy3d.plugins.smatrix.types import SParamDef
20+
from tidy3d.plugins.smatrix.types import NetworkIndex, SParamDef
2121
from tidy3d.plugins.smatrix.utils import (
2222
ab_to_s,
2323
check_port_impedance_sign,
@@ -167,7 +167,7 @@ def _monitor_data_at_port_amplitude(
167167

168168
def get_antenna_metrics_data(
169169
self,
170-
port_amplitudes: Optional[dict[str, complex]] = None,
170+
port_amplitudes: Optional[dict[NetworkIndex, complex]] = None,
171171
monitor_name: Optional[str] = None,
172172
) -> AntennaMetricsData:
173173
"""Calculate antenna parameters using superposition of fields from multiple port excitations.
@@ -176,14 +176,22 @@ def get_antenna_metrics_data(
176176
for a superposition of port excitations, which can be used to analyze antenna radiation
177177
characteristics.
178178
179+
Note
180+
----
181+
The ``NetworkIndex`` identifies a single excitation in the modeled device, so it represents
182+
a :class:`.LumpedPort` or a single mode from a :class:`.WavePort`. Use the static method
183+
:meth:`.TerminalComponentModeler.network_index` to convert port and optional mode index
184+
into the appropriate ``NetworkIndex`` for use in the ``port_amplitudes`` dictionary.
185+
179186
Parameters
180187
----------
181-
port_amplitudes : dict[str, complex]
182-
Dictionary mapping port names to their desired excitation amplitudes. For each port,
188+
port_amplitudes : dict[NetworkIndex, complex] = None
189+
Dictionary mapping a network index to their desired excitation amplitudes. For each network port,
183190
:math:`\\frac{1}{2}|a|^2` represents the incident power from that port into the system.
184-
If None, uses only the first port without any scaling of the raw simulation data. When ``None``
185-
is passed as a port amplitude, the raw simulation data is used for that port. Note that in this method ``a`` represents
186-
the incident wave amplitude using the power wave definition in [2].
191+
If ``None``, uses only the first port without any scaling of the raw simulation data. When
192+
``None`` is passed as a port amplitude, the raw simulation data is used for that port. Note
193+
that in this method ``a`` represents the incident wave amplitude using the power wave definition
194+
in [2].
187195
monitor_name : str
188196
Name of the :class:`.DirectivityMonitor` to use for calculating far fields.
189197
If None, uses the first monitor in `radiation_monitors`.

0 commit comments

Comments
 (0)