Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Solver error for named 2D materials with inhomogeneous substrates.
- In `Tidy3dBaseModel` the hash (and cached `.json_string`) are now sensitive to changes in `.attrs`.
- More accurate frequency range for ``GaussianPulse`` when DC is removed.
- 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.

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

Expand Down
4 changes: 2 additions & 2 deletions tests/test_plugins/smatrix/terminal_component_modeler_def.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,8 @@ def make_coaxial_simulation(length: Optional[float] = None, grid_spec: td.GridSp
# Make simulation
center_sim = [0, 0, 0]
size_sim = [
2 * Router,
2 * Router,
4 * Router,
4 * Router,
length + 0.5 * wavelength0,
]

Expand Down
16 changes: 14 additions & 2 deletions tests/test_plugins/smatrix/test_terminal_component_modeler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1097,10 +1097,16 @@ def test_antenna_helpers(monkeypatch, tmp_path):
assert isinstance(b, PortDataArray)


def test_antenna_parameters(monkeypatch, tmp_path):
@pytest.mark.parametrize("port_type", ["lumped", "wave"])
def test_antenna_parameters(monkeypatch, port_type):
"""Test basic antenna parameters computation and validation."""
# Setup modeler with radiation monitor
modeler = make_component_modeler(False)
if port_type == "lumped":
modeler: TerminalComponentModeler = make_component_modeler(False)
else:
modeler: TerminalComponentModeler = make_coaxial_component_modeler(
port_types=(WavePort, WavePort)
)
sim = modeler.simulation
theta = np.linspace(0, np.pi, 101)
phi = np.linspace(0, 2 * np.pi, 201)
Expand All @@ -1126,6 +1132,12 @@ def test_antenna_parameters(monkeypatch, tmp_path):

# Run simulation and get antenna parameters
modeler_data = run_component_modeler(monkeypatch, modeler)

# Make sure network index works for single mode / multimode cases
port_1_network_index = modeler.network_index(modeler.ports[0])
port_2_network_index = modeler.network_index(modeler.ports[1], 0)
_ = modeler_data.get_antenna_metrics_data({port_1_network_index: 1.0})
_ = modeler_data.get_antenna_metrics_data({port_2_network_index: None})
antenna_params = modeler_data.get_antenna_metrics_data()

# Test that all essential parameters exist and are correct type
Expand Down
33 changes: 26 additions & 7 deletions tidy3d/plugins/smatrix/analysis/antenna.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
from tidy3d.components.microwave.data.monitor_data import AntennaMetricsData
from tidy3d.plugins.smatrix.data.data_array import PortDataArray
from tidy3d.plugins.smatrix.data.terminal import TerminalComponentModelerData
from tidy3d.plugins.smatrix.ports.wave import WavePort
from tidy3d.plugins.smatrix.types import NetworkIndex


def get_antenna_metrics_data(
terminal_component_modeler_data: TerminalComponentModelerData,
port_amplitudes: Optional[dict[str, complex]] = None,
port_amplitudes: Optional[dict[NetworkIndex, complex]] = None,
monitor_name: Optional[str] = None,
) -> AntennaMetricsData:
"""Calculate antenna parameters using superposition of fields from multiple port excitations.
Expand All @@ -20,17 +22,27 @@ def get_antenna_metrics_data(
for a superposition of port excitations, which can be used to analyze antenna radiation
characteristics.

Note
----
The ``NetworkIndex`` identifies a single excitation in the modeled device, so it represents
a :class:`.LumpedPort` or a single mode from a :class:`.WavePort`. Use the static method
:meth:`.TerminalComponentModeler.network_index` to convert port and optional mode index
into the appropriate ``NetworkIndex`` for use in the ``port_amplitudes`` dictionary.

Parameters
----------
terminal_component_modeler_data: TerminalComponentModelerData
Data associated with a :class:`.TerminalComponentModeler` simulation run.
port_amplitudes : dict[str, complex] = None
Dictionary mapping port names to their desired excitation amplitudes. For each port,
port_amplitudes : dict[NetworkIndex, complex] = None
Dictionary mapping a network index to their desired excitation amplitudes. For each network port,
:math:`\\frac{1}{2}|a|^2` represents the incident power from that port into the system.
If None, uses only the first port without any scaling of the raw simulation data.
If ``None``, uses only the first port without any scaling of the raw simulation data. When
``None`` is passed as a port amplitude, the raw simulation data is used for that port. Note
that in this method ``a`` represents the incident wave amplitude using the power wave definition
in [2].
monitor_name : str = None
Name of the :class:`.DirectivityMonitor` to use for calculating far fields.
If None, uses the first monitor in `radiation_monitors`.
If ``None``, uses the first monitor in ``radiation_monitors``.

Returns
-------
Expand All @@ -40,7 +52,13 @@ def get_antenna_metrics_data(
"""
# Use the first port as default if none specified
if port_amplitudes is None:
port_amplitudes = {terminal_component_modeler_data.modeler.ports[0].name: None}
first_port = terminal_component_modeler_data.modeler.ports[0]
mode_index = None
if isinstance(first_port, WavePort):
mode_index = first_port.mode_index
port_amplitudes = {
terminal_component_modeler_data.modeler.network_index(first_port, mode_index): None
}
# Check port names, and create map from port to amplitude
port_dict = {}
for key in port_amplitudes.keys():
Expand Down Expand Up @@ -71,11 +89,12 @@ def get_antenna_metrics_data(
combined_directivity_data = None
for port, amplitude in port_dict.items():
port_in_index = terminal_component_modeler_data.modeler.network_index(port)
_, mode_index = terminal_component_modeler_data.modeler.network_dict[port_in_index]
if amplitude is not None:
if np.isclose(amplitude, 0.0):
continue
sim_data_port = terminal_component_modeler_data.data[
terminal_component_modeler_data.modeler.get_task_name(port)
terminal_component_modeler_data.modeler.get_task_name(port, mode_index)
]

a, b = (
Expand Down
2 changes: 2 additions & 0 deletions tidy3d/plugins/smatrix/component_modelers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ def get_task_name(
ValueError
If an invalid `format` string is provided.
"""
if isinstance(port, WavePort) and mode_index is None:
mode_index = port.mode_index
if mode_index is not None:
return f"{port.name}@{mode_index}"
if format == "PF":
Expand Down
22 changes: 15 additions & 7 deletions tidy3d/plugins/smatrix/data/terminal.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from tidy3d.plugins.smatrix.data.base import AbstractComponentModelerData
from tidy3d.plugins.smatrix.data.data_array import PortDataArray, TerminalPortDataArray
from tidy3d.plugins.smatrix.ports.types import TerminalPortType
from tidy3d.plugins.smatrix.types import SParamDef
from tidy3d.plugins.smatrix.types import NetworkIndex, SParamDef
from tidy3d.plugins.smatrix.utils import (
ab_to_s,
check_port_impedance_sign,
Expand Down Expand Up @@ -167,7 +167,7 @@ def _monitor_data_at_port_amplitude(

def get_antenna_metrics_data(
self,
port_amplitudes: Optional[dict[str, complex]] = None,
port_amplitudes: Optional[dict[NetworkIndex, complex]] = None,
monitor_name: Optional[str] = None,
) -> AntennaMetricsData:
"""Calculate antenna parameters using superposition of fields from multiple port excitations.
Expand All @@ -176,14 +176,22 @@ def get_antenna_metrics_data(
for a superposition of port excitations, which can be used to analyze antenna radiation
characteristics.

Note
----
The ``NetworkIndex`` identifies a single excitation in the modeled device, so it represents
a :class:`.LumpedPort` or a single mode from a :class:`.WavePort`. Use the static method
:meth:`.TerminalComponentModeler.network_index` to convert port and optional mode index
into the appropriate ``NetworkIndex`` for use in the ``port_amplitudes`` dictionary.

Parameters
----------
port_amplitudes : dict[str, complex]
Dictionary mapping port names to their desired excitation amplitudes. For each port,
port_amplitudes : dict[NetworkIndex, complex] = None
Dictionary mapping a network index to their desired excitation amplitudes. For each network port,
:math:`\\frac{1}{2}|a|^2` represents the incident power from that port into the system.
If None, uses only the first port without any scaling of the raw simulation data. When ``None``
is passed as a port amplitude, the raw simulation data is used for that port. Note that in this method ``a`` represents
the incident wave amplitude using the power wave definition in [2].
If ``None``, uses only the first port without any scaling of the raw simulation data. When
``None`` is passed as a port amplitude, the raw simulation data is used for that port. Note
that in this method ``a`` represents the incident wave amplitude using the power wave definition
in [2].
monitor_name : str
Name of the :class:`.DirectivityMonitor` to use for calculating far fields.
If None, uses the first monitor in `radiation_monitors`.
Expand Down