Source code for tidy3d.components.time_modulation

"""Defines time modulation to the medium"""

from __future__ import annotations

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

import numpy as np
import pydantic.v1 as pd

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


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)