Source code for tidy3d.components.time_modulation

""" Defines time modulation to the medium"""
from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Union
from math import isclose

import pydantic.v1 as pd
import numpy as np

from .base import Tidy3dBaseModel, cached_property, skip_if_fields_missing
from .types import InterpMethod, Bound
from .time import AbstractTimeDependence
from .data.data_array import SpatialDataArray
from .data.validators import validate_no_nans
from ..exceptions import ValidationError
from ..constants import HERTZ, RADIAN


class AbstractTimeModulation(AbstractTimeDependence, ABC):
    """Base class for modulation in time.

    Note
    ----
    This class describes the time dependence part of the separable space-time modulation type
    as shown below,

    .. math::

        amp(r, t) = \\Re[amp\\_time(t) \\cdot amp\\_space(r)]

    """

    @cached_property
    @abstractmethod
    def max_modulation(self) -> float:
        """Estimated maximum modulation amplitude."""


[docs] class ContinuousWaveTimeModulation(AbstractTimeDependence): """Class describing modulation with a harmonic time dependence. Note ---- .. math:: amp\\_time(t) = amplitude \\cdot \\ e^{i \\cdot phase - 2 \\pi i \\cdot freq0 \\cdot t} Note ---- The full space-time modulation is, .. math:: amp(r, t) = \\Re[amp\\_time(t) \\cdot amp\\_space(r)] Example ------- >>> cw = ContinuousWaveTimeModulation(freq0=200e12, amplitude=1, phase=0) """ freq0: pd.PositiveFloat = pd.Field( ..., title="Modulation Frequency", description="Modulation frequency.", units=HERTZ )
[docs] def amp_time(self, time: float) -> complex: """Complex-valued source amplitude as a function of time.""" omega = 2 * np.pi * self.freq0 return self.amplitude * np.exp(-1j * omega * time + 1j * self.phase)
@cached_property def max_modulation(self) -> float: """Estimated maximum modulation amplitude.""" return abs(self.amplitude)
TimeModulationType = Union[ContinuousWaveTimeModulation] class AbstractSpaceModulation(ABC, Tidy3dBaseModel): """Base class for modulation in space. Note ---- This class describes the 2nd term in the full space-time modulation below, .. math:: amp(r, t) = \\Re[amp\\_time(t) \\cdot amp\\_space(r)] """ @cached_property @abstractmethod def max_modulation(self) -> float: """Estimated maximum modulation amplitude."""
[docs] class SpaceModulation(AbstractSpaceModulation): """The modulation profile with a user-supplied spatial distribution of amplitude and phase. Note ---- .. math:: amp\\_space(r) = amplitude(r) \\cdot e^{i \\cdot phase(r)} The full space-time modulation is, .. math:: amp(r, t) = \\Re[amp\\_time(t) \\cdot amp\\_space(r)] Example ------- >>> Nx, Ny, Nz = 10, 9, 8 >>> X = np.linspace(-1, 1, Nx) >>> Y = np.linspace(-1, 1, Ny) >>> Z = np.linspace(-1, 1, Nz) >>> coords = dict(x=X, y=Y, z=Z) >>> amp = SpatialDataArray(np.random.random((Nx, Ny, Nz)), coords=coords) >>> phase = SpatialDataArray(np.random.random((Nx, Ny, Nz)), coords=coords) >>> space = SpaceModulation(amplitude=amp, phase=phase) """ amplitude: Union[float, SpatialDataArray] = pd.Field( 1, title="Amplitude of modulation in space", description="Amplitude of modulation that can vary spatially. " "It takes the unit of whatever is being modulated.", ) phase: Union[float, SpatialDataArray] = pd.Field( 0, title="Phase of modulation in space", description="Phase of modulation that can vary spatially.", units=RADIAN, ) interp_method: InterpMethod = pd.Field( "nearest", title="Interpolation method", description="Method of interpolation to use to obtain values at spatial locations on the Yee grids.", ) _no_nans_amplitude = validate_no_nans("amplitude") _no_nans_phase = validate_no_nans("phase") @pd.validator("amplitude", always=True) def _real_amplitude(cls, val): """Assert that the amplitude is real.""" if np.iscomplexobj(val): raise ValidationError("'amplitude' must be real.") return val @pd.validator("phase", always=True) def _real_phase(cls, val): """Assert that the phase is real.""" if np.iscomplexobj(val): raise ValidationError("'phase' must be real.") return val @cached_property def max_modulation(self) -> float: """Estimated maximum modulation amplitude.""" return np.max(abs(np.array(self.amplitude)))
[docs] def sel_inside(self, bounds: Bound) -> SpaceModulation: """Return a new space modulation that contains the minimal amount data necessary to cover a spatial region defined by ``bounds``. Parameters ---------- bounds : Tuple[float, float, float], Tuple[float, float float] Min and max bounds packaged as ``(minx, miny, minz), (maxx, maxy, maxz)``. Returns ------- SpaceModulation SpaceModulation with reduced data. """ if isinstance(self.amplitude, SpatialDataArray): amp_reduced = self.amplitude.sel_inside(bounds) else: amp_reduced = self.amplitude if isinstance(self.phase, SpatialDataArray): phase_reduced = self.phase.sel_inside(bounds) else: phase_reduced = self.phase return self.updated_copy(amplitude=amp_reduced, phase=phase_reduced)
SpaceModulationType = Union[SpaceModulation]
[docs] class SpaceTimeModulation(Tidy3dBaseModel): """Space-time modulation applied to a medium, adding on top of the time-independent part. Note ---- The space-time modulation must be separable in space and time. e.g. when applied to permittivity, .. math:: \\delta \\epsilon(r, t) = \\Re[amp\\_time(t) \\cdot amp\\_space(r)] """ space_modulation: SpaceModulationType = pd.Field( SpaceModulation(), title="Space modulation", description="Space modulation part from the separable SpaceTimeModulation.", # discriminator=TYPE_TAG_STR, ) time_modulation: TimeModulationType = pd.Field( ..., title="Time modulation", description="Time modulation part from the separable SpaceTimeModulation.", # discriminator=TYPE_TAG_STR, ) @cached_property def max_modulation(self) -> float: """Estimated maximum modulation amplitude.""" return self.time_modulation.max_modulation * self.space_modulation.max_modulation @cached_property def negligible_modulation(self) -> bool: """whether the modulation is weak enough to be regarded as zero.""" # if isclose(np.diff(time_modulation.range), 0) and if isclose(self.max_modulation, 0): return True return False
[docs] def sel_inside(self, bounds: Bound) -> SpaceTimeModulation: """Return a new space-time modulation that contains the minimal amount data necessary to cover a spatial region defined by ``bounds``. Parameters ---------- bounds : Tuple[float, float, float], Tuple[float, float float] Min and max bounds packaged as ``(minx, miny, minz), (maxx, maxy, maxz)``. Returns ------- SpaceTimeModulation SpaceTimeModulation with reduced data. """ return self.updated_copy(space_modulation=self.space_modulation.sel_inside(bounds))
[docs] class ModulationSpec(Tidy3dBaseModel): """Specification adding space-time modulation to the non-dispersive part of medium including relative permittivity at infinite frequency and electric conductivity. """ permittivity: SpaceTimeModulation = pd.Field( None, title="Space-time modulation of relative permittivity", description="Space-time modulation of relative permittivity at infinite frequency " "applied on top of the base permittivity at infinite frequency.", ) conductivity: SpaceTimeModulation = pd.Field( None, title="Space-time modulation of conductivity", description="Space-time modulation of electric conductivity " "applied on top of the base conductivity.", ) @pd.validator("conductivity", always=True) @skip_if_fields_missing(["permittivity"]) def _same_modulation_frequency(cls, val, values): """Assert same time-modulation applied to permittivity and conductivity.""" permittivity = values.get("permittivity") if val is not None and permittivity is not None: if val.time_modulation != permittivity.time_modulation: raise ValidationError( "'permittivity' and 'conductivity' should have the same time modulation." ) return val @cached_property def applied_modulation(self) -> bool: """Check if any modulation has been applied to ``permittivity`` or ``conductivity``.""" return self.permittivity is not None or self.conductivity is not None
[docs] def sel_inside(self, bounds: Bound) -> ModulationSpec: """Return a new modulation specficiation that contains the minimal amount data necessary to cover a spatial region defined by ``bounds``. Parameters ---------- bounds : Tuple[float, float, float], Tuple[float, float float] Min and max bounds packaged as ``(minx, miny, minz), (maxx, maxy, maxz)``. Returns ------- ModulationSpec ModulationSpec with reduced data. """ perm_reduced = None if self.permittivity is not None: perm_reduced = self.permittivity.sel_inside(bounds) cond_reduced = None if self.conductivity is not None: cond_reduced = self.conductivity.sel_inside(bounds) return self.updated_copy(permittivity=perm_reduced, conductivity=cond_reduced)