diff --git a/CHANGELOG.md b/CHANGELOG.md index 49194b9e30..cfdadf5d05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/tests/test_plugins/smatrix/terminal_component_modeler_def.py b/tests/test_plugins/smatrix/terminal_component_modeler_def.py index 609699c637..fece3de50b 100644 --- a/tests/test_plugins/smatrix/terminal_component_modeler_def.py +++ b/tests/test_plugins/smatrix/terminal_component_modeler_def.py @@ -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, ] diff --git a/tests/test_plugins/smatrix/test_terminal_component_modeler.py b/tests/test_plugins/smatrix/test_terminal_component_modeler.py index c5f7e00e87..e7ca3b3f00 100644 --- a/tests/test_plugins/smatrix/test_terminal_component_modeler.py +++ b/tests/test_plugins/smatrix/test_terminal_component_modeler.py @@ -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) @@ -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 diff --git a/tidy3d/plugins/smatrix/analysis/antenna.py b/tidy3d/plugins/smatrix/analysis/antenna.py index 83bbaf1f0f..bc1701bd4d 100644 --- a/tidy3d/plugins/smatrix/analysis/antenna.py +++ b/tidy3d/plugins/smatrix/analysis/antenna.py @@ -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. @@ -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 ------- @@ -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(): @@ -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 = ( diff --git a/tidy3d/plugins/smatrix/component_modelers/base.py b/tidy3d/plugins/smatrix/component_modelers/base.py index 3d13f8646a..9b3e8d47e8 100644 --- a/tidy3d/plugins/smatrix/component_modelers/base.py +++ b/tidy3d/plugins/smatrix/component_modelers/base.py @@ -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": diff --git a/tidy3d/plugins/smatrix/data/terminal.py b/tidy3d/plugins/smatrix/data/terminal.py index b0d19d5670..5599db383b 100644 --- a/tidy3d/plugins/smatrix/data/terminal.py +++ b/tidy3d/plugins/smatrix/data/terminal.py @@ -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, @@ -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. @@ -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`.