Source code for oqupy.correlations

# Copyright 2022 The TEMPO Collaboration
#
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#
# Unless required by applicable law or agreed to in writing, software
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
"""
Module for environment correlations.
"""

from typing import Callable, Optional, Text
from typing import Any as ArrayLike
import functools

import numpy as np
from scipy import integrate

from oqupy.base_api import BaseAPIClass
from oqupy.config import INTEGRATE_EPSREL, SUBDIV_LIMIT

# --- spectral density classes ------------------------------------------------

[docs]class BaseCorrelations(BaseAPIClass):
"""Base class for environment auto-correlations. """

[docs]    def correlation(
self,
tau: ArrayLike,
epsrel: Optional[float] = INTEGRATE_EPSREL,
subdiv_limit: Optional[int] = SUBDIV_LIMIT) -> ArrayLike:
r"""
Auto-correlation function.

.. math::

C(\tau) = C(t, t-\tau) \
= \langle F(t) F(t-\tau) \rangle_\mathrm{env}

where :math:\tau is the time difference tau and :math:F(t) is the
the environment part of the coupling operator in Heisenberg picture with
respect to the environment Hamiltonian.

Parameters
----------
tau : ndarray
Time difference :math:\tau
epsrel : float
Relative error tolerance.
subdiv_limit: int
Maximal number of interval subdivisions for numerical integration.

Returns
-------
correlation : ndarray
The auto-correlation function :math:C(\tau) at time :math:\tau.
"""
raise NotImplementedError(
"{} has no correlation implementation.".format(type(self).__name__))

[docs]    def correlation_2d_integral(
self,
delta: float,
time_1: float,
time_2: Optional[float] = None,
shape: Optional[Text] = 'square',
epsrel: Optional[float] = INTEGRATE_EPSREL,
subdiv_limit: Optional[int] = SUBDIV_LIMIT) -> complex:
r"""
2D integrals of the correlation function

.. math::

\eta_\mathrm{square} =
\int_{t_1}^{t_1+\Delta} \int_{0}^{\Delta} C(t'-t'') dt'' dt'

\eta_\mathrm{upper-triangle} =
\int_{t_1}^{t_1+\Delta} \int_{0}^{t'-t_1} C(t'-t'') dt'' dt'

\eta_\mathrm{lower-triangle} =
\int_{t_1}^{t_1+\Delta} \int_{t'-t_1}^{\Delta} C(t'-t'') dt'' dt'

\eta_\mathrm{rectangle} =
\int_{t_1}^{t_2} \int_{0}^{\Delta} C(t'-t'') dt'' dt'

for shape either 'square', 'upper-triangle',
'lower-triangle', or 'rectangle'.

Parameters
----------
delta : float
Length of integration intervals.
time_1 : float
Lower bound of integration interval of :math:dt'.
time_2 : float
Upper bound of integration interval of :math:dt' for shape =
'rectangle'.
shape : str (default = 'square')
The shape of the 2D integral. Shapes are: {'square',
'upper-triangle', 'lower-triangle', 'lower-triangle'}
epsrel : float
Relative error tolerance.
subdiv_limit: int
Maximal number of interval subdivisions for numerical integration.

Returns
-------
integral : float
The numerical value for the two dimensional integral
:math:\eta_\mathrm{shape}.
"""
raise NotImplementedError(
"{} has no correlation_2d_integral implementation.".format(
type(self).__name__))

[docs]class CustomCorrelations(BaseCorrelations):
r"""
Encodes a custom auto-correlation function

.. math::

C(\tau) = C(t, t-\tau) = \langle F(t) F(t-\tau) \rangle_\mathrm{env}

with time difference tau :math:\tau and :math:F(t) is the
the environment part of the coupling operator in Heisenberg picture with
respect to the environment Hamiltonian. We assume that :math:C(\tau) = 0
for all :math:\tau > \tau_\mathrm{max}.

Parameters
----------
correlation_function : callable
The correlation function :math:C.
name: str
An optional name for the correlations.
description: str
An optional description of the correlations.
"""

def __init__(
self,
correlation_function: Callable[[float], float],
name: Optional[Text] = None,
description: Optional[Text] = None) -> None:
"""Creates a CustomCorrelations object. """

# check input: j_function
try:
tmp_correlation_function = np.vectorize(correlation_function)
complex(tmp_correlation_function(1.0))
except Exception as e:
raise AssertionError("Correlation function must be vectorizable " \
+ "and must return float.") from e
self.correlation_function = tmp_correlation_function

super().__init__(name, description)

def __str__(self) -> Text:
ret = []
ret.append(super().__str__())
return "".join(ret)

[docs]    def correlation(
self,
tau: ArrayLike,
epsrel: Optional[float] = None,
subdiv_limit: Optional[int] = None) -> ArrayLike:
r"""
Auto-correlation function.

.. math::

C(\tau) = C(t, t-\tau) \
= \langle F(t) F(t-\tau) \rangle_\mathrm{env}

with time difference tau :math:\tau and :math:F(t) is the
the environment part of the coupling operator in Heisenberg picture with
respect to the environment Hamiltonian.

Parameters
----------
tau : ndarray
Time difference :math:\tau
epsrel : float
Relative error tolerance (has no effect here).
subdiv_limit : int
Maximal number of interval subdivisions for numerical integration
(has no effect here).

Returns
-------
correlation : ndarray
The auto-correlation function :math:C(\tau) at time :math:\tau.
"""
return self.correlation_function(tau)

[docs]    @functools.lru_cache(maxsize=2**10, typed=False)
def correlation_2d_integral(
self,
delta: float,
time_1: float,
time_2: Optional[float] = None,
shape: Optional[Text] = 'square',
epsrel: Optional[float] = INTEGRATE_EPSREL,
subdiv_limit: Optional[int] = SUBDIV_LIMIT) -> complex:
r"""
2D integrals of the correlation function

.. math::

\eta_\mathrm{square} =
\int_{t_1}^{t_1+\Delta} \int_{0}^{\Delta} C(t'-t'') dt'' dt'

\eta_\mathrm{upper-triangle} =
\int_{t_1}^{t_1+\Delta} \int_{0}^{t'-t_1} C(t'-t'') dt'' dt'

\eta_\mathrm{lower-triangle} =
\int_{t_1}^{t_1+\Delta} \int_{t'-t_1}^{\Delta} C(t'-t'') dt'' dt'

\eta_\mathrm{rectangle} =
\int_{t_1}^{t_2} \int_{0}^{\Delta} C(t'-t'') dt'' dt'

for shape either 'square', 'upper-triangle',
'lower-triangle', or 'rectangle'.

Parameters
----------
delta : float
Length of integration intervals.
time_1 : float
Lower bound of integration interval of :math:dt'.
time_2 : float
Upper bound of integration interval of :math:dt' for shape =
'rectangle'.
shape : str (default = 'square')
The shape of the 2D integral. Shapes are: {'square',
'upper-triangle', 'lower-triangle', 'lower-triangle'}
epsrel : float
Relative error tolerance.
subdiv_limit: int
Maximal number of interval subdivisions for numerical integration.

Returns
-------
integral : float
The numerical value for the two dimensional integral
:math:\eta_\mathrm{shape}.
"""
c_real = lambda y, x: np.real(self.correlation(x-y))
c_imag = lambda y, x: np.imag(self.correlation(x-y))

if time_2 is None:
time_2 = time_1 + delta
else:
assert shape == 'rectangle', \
"parameter 'time_2' can only be used in conjunction with " \
"'shape' = rectangle !"

lower_boundary = {'square': lambda x: 0.0,
'upper-triangle': lambda x: 0.0,
'lower-triangle': lambda x: x-time_1,
'rectangle': lambda x: 0.0}
upper_boundary = {'square': lambda x: delta,
'upper-triangle': lambda x: x-time_1,
'lower-triangle': lambda x: delta,
'rectangle': lambda x: delta,}

a=time_1,
b=time_2,
gfun=lower_boundary[shape],
hfun=upper_boundary[shape],
epsrel=epsrel)[0]
a=time_1,
b=time_2,
gfun=lower_boundary[shape],
hfun=upper_boundary[shape],
epsrel=epsrel)[0]
return int_real+1.0j*int_imag

# === CORRELATIONS FROM SPECTRAL DENSITIES ====================================

# --- the cutoffs -------------------------------------------------------------

def _hard_cutoff(omega: ArrayLike, omega_c: float) -> ArrayLike:
"""Hard cutoff function."""
return np.heaviside(omega_c - omega, 0)

def _exponential_cutoff(omega: ArrayLike, omega_c: float) -> ArrayLike:
"""Exponential cutoff function."""
return np.exp(-omega/omega_c)

def _gaussian_cutoff(omega: ArrayLike, omega_c: float) -> ArrayLike:
"""Gaussian cutoff function."""
return np.exp(-(omega/omega_c)**2)

# dictionary for the various cutoffs in the form:
#   'cutoff_name': cutoff_function
CUTOFF_DICT = {
'hard':_hard_cutoff,
'exponential':_exponential_cutoff,
'gaussian':_gaussian_cutoff,
}

# --- 2d integrals ------------------------------------------------------------

def _2d_square_integrand_real(
omega: ArrayLike,
delta: float,
time_1: float,
time_2: Optional[float] = None,
temperature: Optional[float] = None) -> ArrayLike:
"""Integrand for real part of square 2D time integral at zero
temperature without J(omega). """
return 1.0/omega**2 * 2 * np.cos(time_1*omega) * (1 - np.cos(delta*omega))

def _2d_square_integrand_real_t(
omega: ArrayLike,
delta: float,
time_1: float,
time_2: Optional[float] = None,
temperature: Optional[float] = None) -> ArrayLike:
"""Integrand for real part of square 2D time integral at finite
temperature without J(omega). """
integrand = 1.0/omega**2 * 2 * np.cos(time_1*omega) \
* (1 - np.cos(delta*omega))
return integrand / np.tanh(omega/(2*temperature))

def _2d_square_integrand_imag(
omega: ArrayLike,
delta: float,
time_1: float,
time_2: Optional[float] = None,
temperature: Optional[float] = None) -> ArrayLike:
"""Integrand for imaginary part of square 2D time integral without
J(omega). """
return -1.0/omega**2 * 2 * np.sin(time_1*omega) * (1 - np.cos(delta*omega))

def _2d_upper_triangle_integrand_real(
omega: ArrayLike,
delta: float,
time_1: float,
time_2: Optional[float] = None,
temperature: Optional[float] = None) -> ArrayLike:
"""Integrand for real part of upper triangle 2D time integral at zero
temperature without J(omega). """
return 1.0/omega**2 * (np.cos(omega*time_1) \
- np.cos(omega*(time_1+delta)) \
- omega * delta * np.sin(omega*time_1))

def _2d_upper_triangle_integrand_real_t(
omega: ArrayLike,
delta: float,
time_1: float,
time_2: Optional[float] = None,
temperature: Optional[float] = None) -> ArrayLike:
"""Integrand for real part of upper triangle 2D time integral at finite
temperature without J(omega). """
return 1.0/omega**2 * (np.cos(omega*time_1) \
- np.cos(omega*(time_1+delta)) \
- omega * delta * np.sin(omega*time_1)) \
/ np.tanh(omega/(2*temperature))

def _2d_upper_triangle_integrand_imag(
omega: ArrayLike,
delta: float,
time_1: float,
time_2: Optional[float] = None,
temperature: Optional[float] = None) -> ArrayLike:
"""Integrand for imaginary part of upper triangle 2D time integral without
J(omega). """
return -1.0/omega**2 * (np.sin(omega*time_1) \
- np.sin(omega*(time_1+delta)) \
+ omega * delta * np.cos(omega*time_1))

def _2d_lower_triangle_integrand_real(
omega: ArrayLike,
delta: float,
time_1: float,
time_2: Optional[float] = None,
temperature: Optional[float] = None) -> ArrayLike:
"""Integrand for real part of lower triangle 2D time integral at zero
temperature without J(omega). """
return 1.0/omega**2 * (np.cos(omega*time_1) \
- np.cos(omega*(time_1-delta)) \
+ omega * delta * np.sin(omega*time_1))

def _2d_lower_triangle_integrand_real_t(
omega: ArrayLike,
delta: float,
time_1: float,
time_2: Optional[float] = None,
temperature: Optional[float] = None) -> ArrayLike:
"""Integrand for real part of lower triangle 2D time integral at finite
temperature without J(omega). """
return 1.0/omega**2 * (np.cos(omega*time_1) \
- np.cos(omega*(time_1-delta)) \
+ omega * delta * np.sin(omega*time_1)) \
/ np.tanh(omega/(2*temperature))

def _2d_lower_triangle_integrand_imag(
omega: ArrayLike,
delta: float,
time_1: float,
time_2: Optional[float] = None,
temperature: Optional[float] = None) -> ArrayLike:
"""Integrand for imaginary part of lower triangle 2D time integral without
J(omega). """
return -1.0/omega**2 * (np.sin(omega*time_1)
- np.sin(omega*(time_1-delta))
- omega * delta * np.cos(omega*time_1))

def _2d_rectangle_integrand_real(
omega: ArrayLike,
delta: float,
time_1: float,
time_2: Optional[float] = None,
temperature: Optional[float] = None) -> ArrayLike:
"""Integrand for real part of rectangle 2D time integral at zero
temperature without J(omega). """
return 1.0/omega**2 * (np.cos((time_2 - delta) * omega)
- np.cos((time_1 - delta) * omega)
- np.cos(time_2 * omega)
+ np.cos(time_1 * omega))

def _2d_rectangle_integrand_real_t(
omega: ArrayLike,
delta: float,
time_1: float,
time_2: Optional[float] = None,
temperature: Optional[float] = None) -> ArrayLike:
"""Integrand for real part of rectangle 2D time integral at finite
temperature without J(omega). """
integrand = 1.0/omega**2 * (np.cos((time_2 - delta) * omega)
- np.cos((time_1 - delta) * omega)
- np.cos(time_2 * omega)
+ np.cos(time_1 * omega))
return integrand / np.tanh(omega/(2*temperature))

def _2d_rectangle_integrand_imag(
omega: ArrayLike,
delta: float,
time_1: float,
time_2: Optional[float] = None,
temperature: Optional[float] = None) -> ArrayLike:
"""Integrand for imaginary part of rectangle 2D time integral without
J(omega). """
return -1.0/omega**2 * (np.sin((time_2 - delta) * omega)
- np.sin((time_1 - delta) * omega)
- np.sin(time_2 * omega)
+ np.sin(time_1 * omega))

# dictionary for the various integrands for the 2d time integral
INTEGRAND_DICT = {
'square': (_2d_square_integrand_real,
_2d_square_integrand_real_t,
_2d_square_integrand_imag),
'upper-triangle': (_2d_upper_triangle_integrand_real,
_2d_upper_triangle_integrand_real_t,
_2d_upper_triangle_integrand_imag),
'lower-triangle': (_2d_lower_triangle_integrand_real,
_2d_lower_triangle_integrand_real_t,
_2d_lower_triangle_integrand_imag),
'rectangle': (_2d_rectangle_integrand_real,
_2d_rectangle_integrand_real_t,
_2d_rectangle_integrand_imag),
}

# --- the spectral density classes --------------------------------------------

[docs]class CustomSD(BaseCorrelations):
r"""
Correlations corresponding to a custom spectral density. The resulting
spectral density is

.. math::

J(\omega) = j(\omega) X(\omega,\omega_c) ,

with j_function :math:j, cutoff :math:\omega_c and a cutoff type
:math:X.

If cutoff_type is

- 'hard' then
:math:X(\omega,\omega_c)=\Theta(\omega_c-\omega), where
:math:\Theta is the Heaviside step function.
- 'exponential' then
:math:X(\omega,\omega_c)=\exp(-\omega/\omega_c).
- 'gaussian' then
:math:X(\omega,\omega_c)=\exp(-\omega^2/\omega_c^2).

Parameters
----------
j_function : callable
The spectral density :math:j without the cutoff.
cutoff : float
The cutoff frequency :math:\omega_c.
cutoff_type : str (default = 'exponential')
The cutoff type. Types are: {'hard', 'exponential',
'gaussian'}
temperature: float
The environment's temperature.
name: str
An optional name for the correlations.
description: str
An optional description of the correlations.
"""

def __init__(
self,
j_function: Callable[[float], float],
cutoff: float,
cutoff_type: Optional[Text] = 'exponential',
temperature: Optional[float] = 0.0,
name: Optional[Text] = None,
description: Optional[Text] = None) -> None:
"""Create a CustomFunctionSD (spectral density) object. """

# check input: j_function
try:
tmp_j_function = np.vectorize(j_function)
float(tmp_j_function(1.0))
except Exception as e:
raise AssertionError("Spectral density must be vectorizable " \
+ "and must return float.") from e
self.j_function = tmp_j_function

# check input: cutoff
try:
tmp_cutoff = float(cutoff)
except Exception as e:
raise AssertionError("Cutoff must be a float.") from e
self.cutoff = tmp_cutoff

# check input: cutoff_type
assert cutoff_type in CUTOFF_DICT, \
"Cutoff type must be one of: {}".format(CUTOFF_DICT.keys())
self.cutoff_type = cutoff_type

# input check for temperature.
try:
tmp_temperature = float(temperature)
except Exception as e:
raise AssertionError("Temperature must be a float.") from e
if tmp_temperature < 0.0:
raise ValueError("Temperature must be >= 0.0 (but is {})".format(
tmp_temperature))
self.temperature = tmp_temperature

self._cutoff_function = \
lambda omega: CUTOFF_DICT[self.cutoff_type](omega, self.cutoff)
self._spectral_density = \
lambda omega: self.j_function(omega) * self._cutoff_function(omega)

super().__init__(name, description)

def __str__(self) -> Text:
ret = []
ret.append(super().__str__())
ret.append("  cutoff        = {} \n".format(self.cutoff))
ret.append("  cutoff_type   = {} \n".format(self.cutoff_type))
ret.append("  temperature   = {} \n".format(self.temperature))

return "".join(ret)

[docs]    def spectral_density(self, omega: ArrayLike) -> ArrayLike:
r"""
The resulting spectral density (including the cutoff).

Parameters
----------
omega : ndarray
The frequency :math:\omega for which we want to know the
spectral density.

Returns
-------
spectral_density : ndarray
The resulting spectral density :math:J(\omega) at the frequency
:math:\omega.
"""
return self._spectral_density(omega)

[docs]    def correlation(
self,
tau: ArrayLike,
epsrel: Optional[float] = INTEGRATE_EPSREL,
subdiv_limit: Optional[int] = SUBDIV_LIMIT) -> ArrayLike:
r"""
Auto-correlation function associated to the spectral density at the
given temperature :math:T

.. math::

C(\tau) = \int_0^{\infty} J(\omega) \
\left[ \cos(\omega \tau) \
\coth\left( \frac{\omega}{2 T}\right) \
- i \sin(\omega \tau) \right] \mathrm{d}\omega .

with time difference tau :math:\tau.

Parameters
----------
tau : ndarray
Time difference :math:\tau
epsrel : float
Relative error tolerance.
subdiv_limit: int
Maximal number of interval subdivisions for numerical integration.

Returns
-------
correlation : ndarray
The auto-correlation function :math:C(\tau) at time :math:\tau.
"""
# real and imaginary part of the integrand
if self.temperature == 0.0:
re_integrand = lambda w: self._spectral_density(w) * np.cos(w*tau)
else:
re_integrand = lambda w: self._spectral_density(w) * np.cos(w*tau) \
/ np.tanh(w/(2.0*self.temperature))
im_integrand = lambda w: -1.0 * self._spectral_density(w) \
* np.sin(w*tau)
# real and imaginary part of the integral
a=0.0,
b=self.cutoff,
epsrel=epsrel,
limit=subdiv_limit)[0]
a=0.0,
b=self.cutoff,
epsrel=epsrel,
limit=subdiv_limit)[0]
if self.cutoff_type != "hard":
a=self.cutoff,
b=np.inf,
epsrel=epsrel,
limit=subdiv_limit)[0]
a=self.cutoff,
b=np.inf,
epsrel=epsrel,
limit=subdiv_limit)[0]
return re_int+1j*im_int

[docs]    @functools.lru_cache(maxsize=2**10, typed=False)
def correlation_2d_integral(
self,
delta: float,
time_1: float,
time_2: Optional[float] = None,
shape: Optional[Text] = 'square',
epsrel: Optional[float] = INTEGRATE_EPSREL,
subdiv_limit: Optional[int] = SUBDIV_LIMIT) -> complex:
r"""
2D integrals of the correlation function

.. math::

\eta_\mathrm{square} =
\int_{t_1}^{t_1+\Delta} \int_{0}^{\Delta} C(t'-t'') dt'' dt'

\eta_\mathrm{upper-triangle} =
\int_{t_1}^{t_1+\Delta} \int_{0}^{t'-t_1} C(t'-t'') dt'' dt'

\eta_\mathrm{lower-triangle} =
\int_{t_1}^{t_1+\Delta} \int_{t'-t_1}^{\Delta} C(t'-t'') dt'' dt'

\eta_\mathrm{rectangle} =
\int_{t_1}^{t_2} \int_{0}^{\Delta} C(t'-t'') dt'' dt'

for shape either 'square', 'upper-triangle',
'lower-triangle', or 'rectangle'.

Parameters
----------
delta : float
Length of integration intervals.
time_1 : float
Lower bound of integration interval of :math:dt'.
time_2 : float
Upper bound of integration interval of :math:dt' for shape =
'rectangle'.
shape : str (default = 'square')
The shape of the 2D integral. Shapes are: {'square',
'upper-triangle', 'lower-triangle', 'lower-triangle'}
epsrel : float
Relative error tolerance.
subdiv_limit: int
Maximal number of interval subdivisions for numerical integration.

Returns
-------
integral : float
The numerical value for the two dimensional integral
:math:\eta_\mathrm{shape}.
"""
# real and imaginary part of the integrand
if self.temperature == 0.0:
re_integrand = lambda w: \
self._spectral_density(w) \
* INTEGRAND_DICT[shape][0](w,
delta,
time_1,
time_2,
self.temperature)
else:
re_integrand = lambda w: \
self._spectral_density(w) \
* INTEGRAND_DICT[shape][1](w,
delta,
time_1,
time_2,
self.temperature)
im_integrand = lambda w: \
self._spectral_density(w) \
* INTEGRAND_DICT[shape][2](w,
delta,
time_1,
time_2,
self.temperature)
# real and imaginary part of the integral
a=0.0,
b=self.cutoff,
epsrel=epsrel,
limit=subdiv_limit)[0]
a=0.0,
b=self.cutoff,
epsrel=epsrel,
limit=subdiv_limit)[0]
if self.cutoff_type != "hard":
a=self.cutoff,
b=np.inf,
epsrel=epsrel,
limit=subdiv_limit)[0]
a=self.cutoff,
b=np.inf,
epsrel=epsrel,
limit=subdiv_limit)[0]

return re_int+1j*im_int

[docs]class PowerLawSD(CustomSD):
r"""
Correlations corresponding to the spectral density of the standard form

.. math::

J(\omega) = 2 \alpha \frac{\omega^\zeta}{\omega_c^{\zeta-1}} \
X(\omega,\omega_c)

with alpha :math:\alpha, zeta :math:\zeta and a cutoff type
:math:X.

If cutoff_type is

- 'hard' then
:math:X(\omega,\omega_c)=\Theta(\omega_c-\omega), where
:math:\Theta is the Heaviside step function.
- 'exponential' then
:math:X(\omega,\omega_c)=\exp(-\omega/\omega_c).
- 'gaussian' then
:math:X(\omega,\omega_c)=\exp(-\omega^2/\omega_c^2).

Parameters
----------
alpha : float
The coupling strength :math:\alpha.
zeta : float
The exponent :math:\zeta (corresponds to the dimensionality of the
environment). The environment is called *ohmic* if :math:\zeta=1,
*superohmic* if :math:\zeta>1 and *subohmic* if :math:\zeta<1
cutoff : float
The cutoff frequency :math:\omega_c.
cutoff_type : str (default = 'exponential')
The cutoff type. Types are: {'hard', 'exponential',
'gaussian'}
temperature: float
The environment's temperature.
name: str
An optional name for the correlations.
description: str
An optional description of the correlations.
"""

def __init__(
self,
alpha: float,
zeta: float,
cutoff: float,
cutoff_type: Text = 'exponential',
temperature: Optional[float] = 0.0,
name: Optional[Text] = None,
description: Optional[Text] = None) -> None:
"""Create a StandardSD (spectral density) object. """

# check input: alpha
try:
tmp_alpha = float(alpha)
except Exception as e:
raise AssertionError("Alpha must be a float.") from e
self.alpha = tmp_alpha

# check input: zeta
try:
tmp_zeta = float(zeta)
except Exception as e:
raise AssertionError("Nu must be a float.") from e
self.zeta = tmp_zeta

# check input: cutoff
try:
tmp_cutoff = float(cutoff)
except Exception as e:
raise AssertionError("Cutoff must be a float.") from e
self.cutoff = tmp_cutoff

# use parent class for all the rest.
j_function = lambda w: 2.0 * self.alpha * w**self.zeta \
* self.cutoff**(1-zeta)

super().__init__(j_function,
cutoff=cutoff,
cutoff_type=cutoff_type,
temperature=temperature,
name=name,
description=description)

def __str__(self) -> Text:
ret = []
ret.append(super().__str__())
ret.append("  alpha         = {} \n".format(self.alpha))
ret.append("  zeta          = {} \n".format(self.zeta))

return "".join(ret)