Source code for tidy3d.plugins.smatrix.component_modelers.modal

"""Tool for generating an S matrix automatically from a Tidy3d simulation and modal port definitions."""

from __future__ import annotations

from typing import Any, Optional

import autograd.numpy as np
import pydantic.v1 as pd

from tidy3d.components.base import cached_property
from tidy3d.components.data.sim_data import SimulationData
from tidy3d.components.index import SimulationMap
from tidy3d.components.monitor import ModeMonitor
from tidy3d.components.source.field import ModeSource
from tidy3d.components.source.time import GaussianPulse
from tidy3d.components.types import Ax, Complex
from tidy3d.components.viz import add_ax_if_none, equal_aspect
from tidy3d.plugins.smatrix.ports.modal import Port
from tidy3d.plugins.smatrix.types import Element, MatrixIndex

from .base import FWIDTH_FRAC, AbstractComponentModeler


[docs] class ModalComponentModeler(AbstractComponentModeler): """A tool for modeling devices and computing scattering matrix elements. This class orchestrates the process of running multiple simulations to derive the scattering matrix (S-matrix) of a component. It uses modal sources and monitors defined by a set of ports. See Also -------- **Notebooks** * `Computing the scattering matrix of a device <../../notebooks/SMatrix.html>`_ """ ports: tuple[Port, ...] = pd.Field( (), title="Ports", description="Collection of ports describing the scattering matrix elements. " "For each input mode, one simulation will be run with a modal source.", ) run_only: Optional[tuple[MatrixIndex, ...]] = pd.Field( None, title="Run Only", description="Set of matrix indices that define the simulations to run. " "If ``None``, simulations will be run for all indices in the scattering matrix. " "If a tuple is given, simulations will be run only for the given matrix indices.", ) element_mappings: tuple[tuple[Element, Element, Complex], ...] = pd.Field( (), title="Element Mappings", description="Tuple of S matrix element mappings, each described by a tuple of " "(input_element, output_element, coefficient), where the coefficient is the " "element_mapping coefficient describing the relationship between the input and output " "matrix element. If all elements of a given column of the scattering matrix are defined " "by ``element_mappings``, the simulation corresponding to this column is skipped automatically.", ) @property def base_sim(self): """The base simulation.""" return self.simulation @cached_property def sim_dict(self) -> SimulationMap: """Generates all :class:`.Simulation` objects for the S-matrix calculation. Returns ------- Dict[str, Simulation] A dictionary where keys are task names and values are the corresponding :class:`.Simulation` objects. Each simulation is configured to excite a specific mode at a specific port and includes all necessary monitors. """ sim_dict = {} mode_monitors = [self.to_monitor(port=port) for port in self.ports] for port_name, mode_index in self.matrix_indices_run_sim: port = self.get_port_by_name(port_name=port_name) port_source = self.shift_port(port=port) mode_source = self.to_source(port=port_source, mode_index=mode_index) new_mnts = list(self.simulation.monitors) + mode_monitors sim_copy = self.simulation.copy(update={"sources": [mode_source], "monitors": new_mnts}) task_name = self.get_task_name(port=port, mode_index=mode_index) sim_dict[task_name] = sim_copy return SimulationMap(keys=tuple(sim_dict.keys()), values=tuple(sim_dict.values())) @staticmethod def _construct_matrix_indices_monitor(ports: tuple[Port, ...]) -> tuple[MatrixIndex, ...]: """Construct matrix indices for monitoring from modal ports. Parameters ---------- ports : tuple[Port, ...] Tuple of Port objects. Returns ------- tuple[MatrixIndex, ...] Tuple of (port_name, mode_index) pairs. """ matrix_indices = [] for port in ports: for mode_index in range(port.mode_spec.num_modes): matrix_indices.append((port.name, mode_index)) return tuple(matrix_indices) @cached_property def matrix_indices_monitor(self) -> tuple[MatrixIndex, ...]: """Returns a tuple of all possible matrix indices for monitoring. Each matrix index is a tuple of (port_name, mode_index). Returns ------- Tuple[MatrixIndex, ...] A tuple of all possible matrix indices for the monitoring ports. """ return self._construct_matrix_indices_monitor(self.ports) @cached_property def matrix_indices_source(self) -> tuple[MatrixIndex, ...]: """Tuple of all the source matrix indices, which may be less than the total number of ports.""" return super().matrix_indices_source @cached_property def matrix_indices_run_sim(self) -> tuple[MatrixIndex, ...]: """Tuple of all the matrix indices that will be used to run simulations.""" return super().matrix_indices_run_sim @cached_property def port_names(self) -> tuple[list[str], list[str]]: """Returns lists of port names for inputs and outputs. Returns ------- Tuple[List[str], List[str]] A tuple containing two lists: the first with the names of the output ports, and the second with the names of the input ports. """ def get_port_names(matrix_elements: tuple[str, int]) -> list[str]: """Get the port names from a list of (port name, mode index).""" port_names = [] for port_name, _ in matrix_elements: if port_name not in port_names: port_names.append(port_name) return port_names port_names_in = get_port_names(self.matrix_indices_source) port_names_out = get_port_names(self.matrix_indices_monitor) return port_names_out, port_names_in
[docs] def to_monitor(self, port: Port) -> ModeMonitor: """Creates a mode monitor from a given port. This monitor is used to measure the mode amplitudes at the port. Parameters ---------- port : Port The port to convert into a monitor. Returns ------- ModeMonitor A :class:`.ModeMonitor` configured to match the port's properties. """ return ModeMonitor( center=port.center, size=port.size, freqs=self.freqs, mode_spec=port.mode_spec, name=port.name, )
[docs] def to_source( self, port: Port, mode_index: int, num_freqs: int = 1, **kwargs: Any ) -> list[ModeSource]: """Creates a mode source from a given port. This source is used to excite a specific mode at the port. Parameters ---------- port : Port The port to convert into a source. mode_index : int The index of the mode to excite. num_freqs : int, optional The number of frequency points for the source, by default 1. Returns ------- List[ModeSource] A list containing a single :class:`.ModeSource` configured to excite the specified mode at the port. """ freq0 = np.mean(self.freqs) fdiff = max(self.freqs) - min(self.freqs) fwidth = max(fdiff, freq0 * FWIDTH_FRAC) return ModeSource( center=port.center, size=port.size, source_time=self.custom_source_time if self.custom_source_time is not None else GaussianPulse(freq0=freq0, fwidth=fwidth), mode_spec=port.mode_spec, mode_index=mode_index, direction=port.direction, name=port.name, num_freqs=num_freqs, **kwargs, )
[docs] def shift_port(self, port: Port) -> Port: """Generates a new port shifted slightly in the normal direction. This is to ensure that the source is placed just inside the simulation domain, away from the PML. Parameters ---------- port : Port The port to shift. Returns ------- Port A new :class:`.Port` object with its center shifted. """ shift_value = self._shift_value_signed(port=port) center_shifted = list(port.center) center_shifted[port.size.index(0.0)] += shift_value port_shifted = port.copy(update={"center": center_shifted}) return port_shifted
[docs] @equal_aspect @add_ax_if_none def plot_sim( self, x: Optional[float] = None, y: Optional[float] = None, z: Optional[float] = None, ax: Ax = None, ) -> Ax: """Plots the simulation with all sources added for troubleshooting. This method creates a temporary simulation with all mode sources activated to help visualize the setup. Parameters ---------- x : float, optional The x-coordinate of the cross-section, by default None. y : float, optional The y-coordinate of the cross-section, by default None. z : float, optional The z-coordinate of the cross-section, by default None. ax : Ax, optional The matplotlib axes to plot on, by default None. Returns ------- Ax The matplotlib axes with the plot. """ plot_sources = [] for port_source in self.ports: mode_source_0 = self.to_source(port=port_source, mode_index=0) plot_sources.append(mode_source_0) sim_plot = self.simulation.copy(update={"sources": plot_sources}) return sim_plot.plot(x=x, y=y, z=z, ax=ax)
[docs] @equal_aspect @add_ax_if_none def plot_sim_eps( self, x: Optional[float] = None, y: Optional[float] = None, z: Optional[float] = None, ax: Ax = None, **kwargs: Any, ) -> Ax: """Plots the permittivity of the simulation with all sources. This method is useful for visualizing the device geometry along with the placement of the sources. Parameters ---------- x : float, optional The x-coordinate of the cross-section, by default None. y : float, optional The y-coordinate of the cross-section, by default None. z : float, optional The z-coordinate of the cross-section, by default None. ax : Ax, optional The matplotlib axes to plot on, by default None. **kwargs Additional keyword arguments passed to the plotter. Returns ------- Ax The matplotlib axes with the plot. """ plot_sources = [] for port_source in self.ports: mode_source_0 = self.to_source(port=port_source, mode_index=0) plot_sources.append(mode_source_0) sim_plot = self.simulation.copy(update={"sources": plot_sources}) return sim_plot.plot_eps(x=x, y=y, z=z, ax=ax, **kwargs)
def _normalization_factor(self, port_source: Port, sim_data: SimulationData) -> complex: """Computes the normalization amplitude for the input mode. This is used to normalize the S-matrix elements. Parameters ---------- port_source : Port The port that was excited. sim_data : SimulationData The data from the simulation run. Returns ------- complex The complex amplitude of the input mode. """ port_monitor_data = sim_data[port_source.name] mode_index = sim_data.simulation.sources[0].mode_index normalize_amps = port_monitor_data.amps.sel( f=np.array(self.freqs), direction=port_source.direction, mode_index=mode_index, ) return normalize_amps.values @cached_property def max_mode_index(self) -> tuple[int, int]: """Returns the maximum mode indices for the in and out ports. Returns ------- Tuple[int, int] A tuple containing the maximum mode index for the output ports and the maximum mode index for the input ports. """ def get_max_mode_indices(matrix_elements: tuple[str, int]) -> int: """Get the maximum mode index for a list of (port name, mode index).""" return max(mode_index for _, mode_index in matrix_elements) max_mode_index_out = get_max_mode_indices(self.matrix_indices_monitor) max_mode_index_in = get_max_mode_indices(self.matrix_indices_source) return max_mode_index_out, max_mode_index_in
[docs] def task_name_from_index(self, matrix_index: MatrixIndex) -> str: """Compute task name for a given (port_name, mode_index) without constructing simulations.""" port_name, mode_index = matrix_index port = self.get_port_by_name(port_name=port_name) return self.get_task_name(port=port, mode_index=mode_index)