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
106 changes: 106 additions & 0 deletions tests/test_components/test_mode_interp.py
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,112 @@ def test_mode_solver_with_reduce_data_but_small_num_freqs():
assert data.interpolated_copy == data


@pytest.mark.parametrize(
"num_freqs,num_points,reduce_data,expected_stored_len",
[
# No interp_spec (num_points=None)
(10, None, None, 10),
# interp_spec with reduce_data=False
(10, 5, False, 10),
(10, 8, False, 10),
# interp_spec with reduce_data=True and num_points < len(monitor.freqs)
(20, 5, True, 5),
(20, 10, True, 10),
# interp_spec with reduce_data=True and num_points >= len(monitor.freqs)
(10, 10, True, 10),
(10, 15, True, 10),
(5, 10, True, 5),
],
)
@pytest.mark.parametrize("rf", [False, True])
def test_mode_solver_data_stored_freqs(num_freqs, num_points, reduce_data, expected_stored_len, rf):
"""Test that _stored_freqs in ModeSolverData is correct based on interp_spec.

Cases tested:
1. No interp_spec: _stored_freqs matches monitor.freqs
2. interp_spec with reduce_data=False: _stored_freqs matches monitor.freqs
3. interp_spec with reduce_data=True and num_points < len(monitor.freqs):
len(_stored_freqs) == num_points
4. interp_spec with reduce_data=True and num_points >= len(monitor.freqs):
_stored_freqs matches monitor.freqs
"""
from tidy3d.components.data.data_array import ModeIndexDataArray

from ..test_data.test_data_arrays import SIM, make_scalar_mode_field_data_array

freqs = np.linspace(1e14, 2e14, num_freqs)

# Create mode_spec based on parameters
if num_points is None:
# No interp_spec
mode_spec = td.ModeSpec(
num_modes=2,
sort_spec=td.ModeSortSpec(track_freq="central"),
)
else:
# With interp_spec
mode_spec = td.ModeSpec(
num_modes=2,
sort_spec=td.ModeSortSpec(track_freq="central"),
interp_spec=td.ModeInterpSpec.uniform(
num_points=num_points, method="linear", reduce_data=reduce_data
),
)

# Create monitor
monitor = td.ModeSolverMonitor(
center=(0, 0, 0),
size=SIZE_2D,
freqs=freqs,
mode_spec=mode_spec,
name="test_monitor",
colocate=False,
)

# Create n_complex with the expected stored frequencies
mode_indices = np.arange(2)
n_complex_values = (1.5 + 0.1j) * np.ones((expected_stored_len, 2))
n_complex = ModeIndexDataArray(
n_complex_values,
coords={"f": np.linspace(1e14, 2e14, expected_stored_len), "mode_index": mode_indices},
)

# Create ModeSolverData
data = td.ModeSolverData(
monitor=monitor,
Ex=make_scalar_mode_field_data_array("Ex", symmetry=False),
Ey=make_scalar_mode_field_data_array("Ey", symmetry=False),
Ez=make_scalar_mode_field_data_array("Ez", symmetry=False),
Hx=make_scalar_mode_field_data_array("Hx", symmetry=False),
Hy=make_scalar_mode_field_data_array("Hy", symmetry=False),
Hz=make_scalar_mode_field_data_array("Hz", symmetry=False),
n_complex=n_complex,
symmetry=(0, 0, 0),
symmetry_center=(0, 0, 0),
grid_expanded=SIM.discretize_monitor(monitor),
)

if rf:
mode_spec = td.MicrowaveModeSpec(**mode_spec.dict(exclude={"type"}))
monitor = td.MicrowaveModeSolverMonitor(
**monitor.dict(exclude={"type", "mode_spec"}), mode_spec=mode_spec
)
data = td.ModeSolverData(**data.dict(exclude={"type", "monitor"}), monitor=monitor)

# Check _stored_freqs length
assert len(data.monitor._stored_freqs) == expected_stored_len, (
f"Expected _stored_freqs length {expected_stored_len}, "
f"got {len(data.monitor._stored_freqs)}"
)

# Check that monitor.freqs always matches original freqs
assert len(data.monitor.freqs) == num_freqs
assert np.allclose(data.monitor.freqs, freqs)

# Check data shape matches _stored_freqs
assert data.n_complex.shape[0] == expected_stored_len


# ============================================================================
# Monitor Integration Tests (Phase 6)
# ============================================================================
Expand Down
149 changes: 89 additions & 60 deletions tidy3d/components/microwave/data/monitor_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import pydantic.v1 as pd
import xarray as xr
from typing_extensions import Self

from tidy3d.components.data.data_array import FieldProjectionAngleDataArray, FreqDataArray
from tidy3d.components.data.monitor_data import DirectivityData, ModeData, ModeSolverData
Expand Down Expand Up @@ -201,69 +202,27 @@ def realized_gain(self) -> FieldProjectionAngleDataArray:
return partial_G.Gtheta + partial_G.Gphi


class MicrowaveModeData(ModeData, MicrowaveBaseModel):
"""
Data associated with a :class:`.ModeMonitor` for microwave and RF applications: modal amplitudes,
propagation indices, mode profiles, and transmission line data.
class MicrowaveModeDataBase(MicrowaveBaseModel):
"""Base class for microwave mode data that extends standard mode data with RF/microwave features.

Notes
-----
This base class adds microwave-specific functionality to mode data classes, including:

This class extends :class:`.ModeData` with additional microwave-specific data including
characteristic impedance, voltage coefficients, and current coefficients. The data is
stored as `DataArray <https://docs.xarray.dev/en/stable/generated/xarray.DataArray.html>`_
objects using the `xarray <https://docs.xarray.dev/en/stable/index.html>`_ package.
- **Transmission line data**: Characteristic impedance (Z0), voltage coefficients, and
current coefficients for transmission line analysis
- **Enhanced modes_info**: Includes impedance data in the mode properties dataset
- **Group index handling**: Properly filters transmission line data when computing group indices
- **Mode reordering**: Ensures transmission line data tracks with reordered modes

The microwave mode data contains all the information from :class:`.ModeData` plus additional
microwave dataset with impedance calculations performed using voltage and current line integrals
as specified in the :class:`.MicrowaveModeSpec`.
Notes
-----
This is a mixin class that must be combined with mode data classes (:class:`.ModeData` or
:class:`.ModeSolverData`). It uses ``super()`` to call methods on the mixed-in class, extending
their functionality rather than replacing it.

Example
-------
>>> import tidy3d as td
>>> import numpy as np
>>> from tidy3d.components.data.data_array import (
... CurrentFreqModeDataArray,
... ImpedanceFreqModeDataArray,
... ModeAmpsDataArray,
... ModeIndexDataArray,
... VoltageFreqModeDataArray,
... )
>>> from tidy3d.components.microwave.data.dataset import TransmissionLineDataset
>>> direction = ["+", "-"]
>>> f = [1e14, 2e14, 3e14]
>>> mode_index = np.arange(3)
>>> index_coords = dict(f=f, mode_index=mode_index)
>>> index_data = ModeIndexDataArray((1+1j) * np.random.random((3, 3)), coords=index_coords)
>>> amp_coords = dict(direction=direction, f=f, mode_index=mode_index)
>>> amp_data = ModeAmpsDataArray((1+1j) * np.random.random((2, 3, 3)), coords=amp_coords)
>>> impedance_data = ImpedanceFreqModeDataArray(50 * np.ones((3, 3)), coords=index_coords)
>>> voltage_data = VoltageFreqModeDataArray((1+1j) * np.random.random((3, 3)), coords=index_coords)
>>> current_data = CurrentFreqModeDataArray((0.02+0.01j) * np.random.random((3, 3)), coords=index_coords)
>>> tl_data = TransmissionLineDataset(
... Z0=impedance_data,
... voltage_coeffs=voltage_data,
... current_coeffs=current_data
... )
>>> monitor = td.MicrowaveModeMonitor(
... center=(0, 0, 0),
... size=(2, 0, 6),
... freqs=[2e14, 3e14],
... mode_spec=td.MicrowaveModeSpec(num_modes=3, impedance_specs=td.AutoImpedanceSpec()),
... name='microwave_mode',
... )
>>> data = MicrowaveModeData(
... monitor=monitor,
... amps=amp_data,
... n_complex=index_data,
... transmission_line_data=tl_data
... )
The mixin should be placed first in the inheritance list to ensure its method overrides
are used.
"""

monitor: MicrowaveModeMonitor = pd.Field(
..., title="Monitor", description="Mode monitor associated with the data."
)

transmission_line_data: Optional[TransmissionLineDataset] = pd.Field(
None,
title="Transmission Line Data",
Expand All @@ -276,12 +235,14 @@ class MicrowaveModeData(ModeData, MicrowaveBaseModel):
def modes_info(self) -> xr.Dataset:
"""Dataset collecting various properties of the stored modes."""
super_info = super().modes_info

# Add transmission line data if present
if self.transmission_line_data is not None:
super_info["Re(Z0)"] = self.transmission_line_data.Z0.real
super_info["Im(Z0)"] = self.transmission_line_data.Z0.imag
return super_info

def _group_index_post_process(self, frequency_step: float) -> ModeData:
def _group_index_post_process(self, frequency_step: float) -> Self:
"""Calculate group index and remove added frequencies used only for this calculation.

Parameters
Expand All @@ -291,10 +252,12 @@ def _group_index_post_process(self, frequency_step: float) -> ModeData:

Returns
-------
:class:`.ModeData`
Self
Filtered data with calculated group index.
"""
super_data = super()._group_index_post_process(frequency_step)

# Add transmission line data handling if present
if self.transmission_line_data is not None:
_, center_inds, _ = self._group_index_freq_slices()
update_dict = {
Expand All @@ -315,6 +278,8 @@ def _apply_mode_reorder(self, sort_inds_2d):
permutation to apply to the mode_index for that frequency.
"""
main_data_reordered = super()._apply_mode_reorder(sort_inds_2d)

# Add transmission line data handling if present
if self.transmission_line_data is not None:
transmission_line_data_reordered = self.transmission_line_data._apply_mode_reorder(
sort_inds_2d
Expand All @@ -325,7 +290,71 @@ def _apply_mode_reorder(self, sort_inds_2d):
return main_data_reordered


class MicrowaveModeSolverData(ModeSolverData, MicrowaveModeData):
class MicrowaveModeData(MicrowaveModeDataBase, ModeData):
"""
Data associated with a :class:`.ModeMonitor` for microwave and RF applications: modal amplitudes,
propagation indices, mode profiles, and transmission line data.

Notes
-----

This class extends :class:`.ModeData` with additional microwave-specific data including
characteristic impedance, voltage coefficients, and current coefficients. The data is
stored as `DataArray <https://docs.xarray.dev/en/stable/generated/xarray.DataArray.html>`_
objects using the `xarray <https://docs.xarray.dev/en/stable/index.html>`_ package.

The microwave mode data contains all the information from :class:`.ModeData` plus additional
microwave dataset with impedance calculations performed using voltage and current line integrals
as specified in the :class:`.MicrowaveModeSpec`.

Example
-------
>>> import tidy3d as td
>>> import numpy as np
>>> from tidy3d.components.data.data_array import (
... CurrentFreqModeDataArray,
... ImpedanceFreqModeDataArray,
... ModeAmpsDataArray,
... ModeIndexDataArray,
... VoltageFreqModeDataArray,
... )
>>> from tidy3d.components.microwave.data.dataset import TransmissionLineDataset
>>> direction = ["+", "-"]
>>> f = [1e14, 2e14, 3e14]
>>> mode_index = np.arange(3)
>>> index_coords = dict(f=f, mode_index=mode_index)
>>> index_data = ModeIndexDataArray((1+1j) * np.random.random((3, 3)), coords=index_coords)
>>> amp_coords = dict(direction=direction, f=f, mode_index=mode_index)
>>> amp_data = ModeAmpsDataArray((1+1j) * np.random.random((2, 3, 3)), coords=amp_coords)
>>> impedance_data = ImpedanceFreqModeDataArray(50 * np.ones((3, 3)), coords=index_coords)
>>> voltage_data = VoltageFreqModeDataArray((1+1j) * np.random.random((3, 3)), coords=index_coords)
>>> current_data = CurrentFreqModeDataArray((0.02+0.01j) * np.random.random((3, 3)), coords=index_coords)
>>> tl_data = TransmissionLineDataset(
... Z0=impedance_data,
... voltage_coeffs=voltage_data,
... current_coeffs=current_data
... )
>>> monitor = td.MicrowaveModeMonitor(
... center=(0, 0, 0),
... size=(2, 0, 6),
... freqs=[2e14, 3e14],
... mode_spec=td.MicrowaveModeSpec(num_modes=3, impedance_specs=td.AutoImpedanceSpec()),
... name='microwave_mode',
... )
>>> data = MicrowaveModeData(
... monitor=monitor,
... amps=amp_data,
... n_complex=index_data,
... transmission_line_data=tl_data
... )
"""

monitor: MicrowaveModeMonitor = pd.Field(
..., title="Monitor", description="Mode monitor associated with the data."
)


class MicrowaveModeSolverData(MicrowaveModeDataBase, ModeSolverData):
"""
Data associated with a :class:`.ModeSolverMonitor` for microwave and RF applications: scalar components
of E and H fields plus characteristic impedance data.
Expand Down
30 changes: 22 additions & 8 deletions tidy3d/components/microwave/monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,27 @@
from tidy3d.components.monitor import ModeMonitor, ModeSolverMonitor


class MicrowaveModeMonitor(MicrowaveBaseModel, ModeMonitor):
class MicrowaveModeMonitorBase(MicrowaveBaseModel):
"""Base class for microwave mode monitors that use :class:`.MicrowaveModeSpec`.

This mixin provides the ``mode_spec`` field configured for RF and microwave applications,
including characteristic impedance calculations and transmission line analysis.

Notes
-----
This is a mixin class that provides the :class:`.MicrowaveModeSpec` field for mode monitors.
It must be placed first in the inheritance list to ensure its ``mode_spec`` field takes
precedence over the base :class:`.ModeSpec` field from :class:`.AbstractModeMonitor`.
"""

mode_spec: MicrowaveModeSpec = pydantic.Field(
default_factory=MicrowaveModeSpec._default_without_license_warning,
title="Mode Specification",
description="Parameters to feed to mode solver which determine modes measured by monitor.",
)


class MicrowaveModeMonitor(MicrowaveModeMonitorBase, ModeMonitor):
""":class:`Monitor` that records amplitudes from modal decomposition of fields on plane.

Notes
Expand Down Expand Up @@ -47,14 +67,8 @@ class MicrowaveModeMonitor(MicrowaveBaseModel, ModeMonitor):
* `ModalSourcesMonitors <../../notebooks/ModalSourcesMonitors.html>`_
"""

mode_spec: MicrowaveModeSpec = pydantic.Field(
default_factory=MicrowaveModeSpec._default_without_license_warning,
title="Mode Specification",
description="Parameters to feed to mode solver which determine modes measured by monitor.",
)


class MicrowaveModeSolverMonitor(MicrowaveModeMonitor, ModeSolverMonitor):
class MicrowaveModeSolverMonitor(MicrowaveModeMonitorBase, ModeSolverMonitor):
""":class:`Monitor` that stores the mode field profiles returned by the mode solver in the
monitor plane.

Expand Down
Loading