""" 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)