Source code for tidy3d.components.microwave.impedance_calculator

"""Class for computing characteristic impedance of transmission lines."""

from __future__ import annotations

from typing import Optional, Union

import numpy as np
import pydantic.v1 as pd

from tidy3d.components.data.data_array import (
    CurrentIntegralResultType,
    ImpedanceResultType,
    VoltageIntegralResultType,
    _make_current_data_array,
    _make_impedance_data_array,
    _make_voltage_data_array,
)
from tidy3d.components.data.monitor_data import FieldTimeData
from tidy3d.components.microwave.base import MicrowaveBaseModel
from tidy3d.components.microwave.path_integrals.integrals.base import (
    AxisAlignedPathIntegral,
    IntegrableMonitorDataType,
)
from tidy3d.components.microwave.path_integrals.integrals.current import (
    AxisAlignedCurrentIntegral,
    CompositeCurrentIntegral,
    Custom2DCurrentIntegral,
)
from tidy3d.components.microwave.path_integrals.integrals.voltage import (
    AxisAlignedVoltageIntegral,
    Custom2DVoltageIntegral,
)
from tidy3d.components.monitor import ModeMonitor, ModeSolverMonitor
from tidy3d.exceptions import ValidationError

VoltageIntegralType = Union[AxisAlignedVoltageIntegral, Custom2DVoltageIntegral]
CurrentIntegralType = Union[
    AxisAlignedCurrentIntegral, Custom2DCurrentIntegral, CompositeCurrentIntegral
]


[docs] class ImpedanceCalculator(MicrowaveBaseModel): """Tool for computing the characteristic impedance of a transmission line. Example ------- Create a calculator with both voltage and current path integrals defined. >>> v_int = AxisAlignedVoltageIntegral( ... center=(0, 0, 0), ... size=(0, 0, 2), ... sign="+", ... extrapolate_to_endpoints=True, ... snap_path_to_grid=True, ... ) >>> i_int = AxisAlignedCurrentIntegral( ... center=(0, 0, 0), ... size=(1, 1, 0), ... sign="+", ... extrapolate_to_endpoints=True, ... snap_contour_to_grid=True, ... ) >>> calc = ImpedanceCalculator(voltage_integral=v_int, current_integral=i_int) You can also define only one of the integrals. At least one is required. >>> _ = ImpedanceCalculator(voltage_integral=v_int) """ voltage_integral: Optional[VoltageIntegralType] = pd.Field( None, title="Voltage Integral", description="Definition of path integral for computing voltage.", ) current_integral: Optional[CurrentIntegralType] = pd.Field( None, title="Current Integral", description="Definition of contour integral for computing current.", )
[docs] def compute_impedance( self, em_field: IntegrableMonitorDataType, return_voltage_and_current=False ) -> Union[ ImpedanceResultType, tuple[ImpedanceResultType, VoltageIntegralResultType, CurrentIntegralResultType], ]: """Compute impedance for the supplied ``em_field`` using ``voltage_integral`` and ``current_integral``. If only a single integral has been defined, impedance is computed using the total flux in ``em_field``. Parameters ---------- em_field : :class:`.IntegrableMonitorDataType` The electromagnetic field data that will be used for computing the characteristic impedance. return_voltage_and_current: bool When ``True``, returns additional :class:`.IntegralResultType` that represent the voltage and current associated with the supplied fields. Returns ------- :class:`.IntegralResultType` or tuple[VoltageIntegralResultType, CurrentIntegralResultType, ImpedanceResultType] If ``return_voltage_and_current=False``, single result of impedance computation over remaining dimensions (frequency, time, mode indices). If ``return_voltage_and_current=True``, tuple of (impedance, voltage, current). """ AxisAlignedPathIntegral._check_monitor_data_supported(em_field=em_field) voltage = None current = None # If both voltage and current integrals have been defined then impedance is computed directly if self.voltage_integral is not None: voltage = self.voltage_integral.compute_voltage(em_field) if self.current_integral is not None: current = self.current_integral.compute_current(em_field) # If only one of the integrals has been provided, then the computation falls back to using # total power (flux) with Ohm's law to compute the missing quantity. The input field should # cover an area large enough to render the flux computation accurate. If the input field is # a time signal, then it is real and flux corresponds to the instantaneous power. Otherwise # the input field is in frequency domain, where flux indicates the time-averaged power # 0.5*Re(V*conj(I)). # We explicitly take the real part, in case Bloch BCs were used in the simulation. flux_sign = 1.0 # Determine flux sign if isinstance(em_field.monitor, ModeSolverMonitor): flux_sign = 1 if em_field.monitor.direction == "+" else -1 if isinstance(em_field.monitor, ModeMonitor): flux_sign = 1 if em_field.monitor.store_fields_direction == "+" else -1 if self.voltage_integral is None: flux = flux_sign * em_field.complex_flux if isinstance(em_field, FieldTimeData): impedance = flux / np.real(current) ** 2 else: impedance = 2 * flux / (current * np.conj(current)) elif self.current_integral is None: flux = flux_sign * em_field.complex_flux if isinstance(em_field, FieldTimeData): impedance = np.real(voltage) ** 2 / flux else: impedance = (voltage * np.conj(voltage)) / (2 * np.conj(flux)) else: if isinstance(em_field, FieldTimeData): impedance = np.real(voltage) / np.real(current) else: impedance = voltage / current impedance = _make_impedance_data_array(impedance) if return_voltage_and_current: if voltage is None: voltage = _make_voltage_data_array(impedance * current) if current is None: current = _make_current_data_array(voltage / impedance) return (impedance, voltage, current) return impedance
[docs] @pd.validator("current_integral", always=True) def check_voltage_or_current(cls, val, values): """Raise validation error if both ``voltage_integral`` and ``current_integral`` are not provided.""" if not values.get("voltage_integral") and not val: raise ValidationError( "At least one of 'voltage_integral' or 'current_integral' must be provided." ) return val