"""Class representing time in time dependent calculations.
User level function of the Quantarhei package. To be used as:
>>> import quantarhei as qr
>>> time = qr.TimeAxis()
The `TimeAxis` class stands in a close relation to `FrequencyAxis`
class of the `quantarhei` package. `FrequencyAxis` represents the
frequencies one obtains in the Fast Fourier transform of a function of
the `TimeAxis`. By default, `TimeAxis` is of the type `upper-half` which
means that by specifying the `start`, `length` and `step` we
represent the upper half of the interval `<start-length*step,
start+(length-1)*step>`. The Fourier transform of a time dependent
object defined on the `TimeAxis` will then have twice as many points as
the `TimeAxis`. This is usefull when the time dependent object has some
special symmetries. One example is the so-called quantum bath correlation
function which fulfills the relation (in LaTeX)
C(-t) = C^{*}(t)
Examples
--------
Default TimeAxis is of the 'upper-half' type
>>> ta = TimeAxis(0.0,100,0.1)
>>> ta.atype
'upper-half'
It is defined between the `start` and `start+(length-1)*step`
>>> '%.3f' % ta.min
'0.000'
>>> '%.3f' % ta.max
'9.900'
However, when we ask for the corresponding FrequencyAxis, we get an
array of frequencies which is twice as long and contains `2*length`
points
>>> wa = ta.get_FrequencyAxis()
>>> print(wa.length)
200
The frequency step is therefore twice shorter than one would normally
expect.
>>> print(wa.step == wa.step)
True
>>> import numpy
>>> print(numpy.allclose(wa.step,2.0*numpy.pi/(0.1*200)))
True
This definition of the `TimeAxis` is used in conjunction with
the symmetries of the corelation functions, lineshape functions
and response functions.
In some case you want the 'complete' type of the TimeAxis.
>>> ta = TimeAxis(0.0,100,1.0,atype="complete")
>>> ta.atype
'complete'
In this case the relation of the `TimeAxis` and the `FrequencyAxis` is
more straightforward. Now, the `TimeAxis` represents the interval on
which a time dependent object is defined completely. The number of
points in the corresponding `FrequencyAxis` is the same as in the
`TimeAxis` and the frequency step is as expected from a normal FFT.
>>> wa = ta.get_FrequencyAxis()
>>> print(wa.length)
100
>>> print(wa.step == wa.step)
True
>>> import numpy
>>> print(numpy.allclose(wa.step,2.0*numpy.pi/(1.0*100)))
True
No other types than `complete` and `upper-half` are defined at the moment.
The following with throw an Exception
>>> ta = TimeAxis(0.0,100,1.0,atype="lower-half")
Traceback (most recent call last):
...
quantarhei.exceptions.QuantarheiError: Unknown time axis type
Relation between TimeAxis and FrequencyAxis
-------------------------------------------
Complete TimeAxis and even number of points
>>> ta = TimeAxis(0.0,10,0.1,atype="complete")
>>> times = [0.0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9]
>>> print(numpy.allclose(ta.data,times))
True
>>> wa = ta.get_FrequencyAxis()
>>> freques = 2.0*numpy.pi*numpy.fft.fftshift(numpy.fft.fftfreq(10,0.1))
>>> print(numpy.allclose(wa.data,freques))
True
>>> print(numpy.allclose(wa.step,freques[1]-freques[0]))
True
>>> tb = wa.get_TimeAxis()
>>> print(numpy.allclose(tb.data,times))
True
>>> wb = tb.get_FrequencyAxis()
>>> print(numpy.allclose(wb.data,freques))
True
Complete TimeAxis and odd number of points
>>> ta = TimeAxis(0.0,11,0.1,atype="complete")
>>> times = [0.0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0]
>>> print(numpy.allclose(ta.data,times))
True
>>> wa = ta.get_FrequencyAxis()
>>> freques = 2.0*numpy.pi*numpy.fft.fftshift(numpy.fft.fftfreq(11,0.1))
>>> print(numpy.allclose(wa.data,freques))
True
>>> print(numpy.allclose(wa.step,freques[1]-freques[0]))
True
>>> tb = wa.get_TimeAxis()
>>> print(numpy.allclose(tb.data,times))
True
>>> wb = tb.get_FrequencyAxis()
>>> print(numpy.allclose(wb.data,freques))
True
Upper-half TimeAxis and even number of points
>>> ta = TimeAxis(0.0,10,0.1)
>>> print(ta.atype=="upper-half")
True
>>> times = [0.0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9]
>>> print(numpy.allclose(ta.data,times))
True
>>> wa = ta.get_FrequencyAxis()
>>> freques = 2.0*numpy.pi*numpy.fft.fftshift(numpy.fft.fftfreq(20,0.1))
>>> print(numpy.allclose(wa.data,freques))
True
>>> print(numpy.allclose(wa.step,freques[1]-freques[0]))
True
>>> tb = wa.get_TimeAxis()
>>> print(numpy.allclose(tb.data,times))
True
>>> wb = tb.get_FrequencyAxis()
>>> print(numpy.allclose(wb.data,freques))
True
Upper-half TimeAxis and odd number of points
>>> ta = TimeAxis(0.0,11,0.1)
>>> times = [0.0,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1.0]
>>> print(numpy.allclose(ta.data,times))
True
>>> wa = ta.get_FrequencyAxis()
>>> freques = 2.0*numpy.pi*numpy.fft.fftshift(numpy.fft.fftfreq(2*11,0.1))
>>> print(numpy.allclose(wa.data,freques))
True
>>> print(numpy.allclose(wa.step,freques[1]-freques[0]))
True
>>> tb = wa.get_TimeAxis()
>>> print(numpy.allclose(tb.data,times))
True
>>> wb = tb.get_FrequencyAxis()
>>> print(numpy.allclose(wb.data,freques))
True
Class Details
-------------
"""
from __future__ import annotations
import numpy
from ..exceptions import QuantarheiError
from .frequency import FrequencyAxis
from .managers import energy_units
from .valueaxis import ValueAxis
[docs]
class TimeDependent:
_has_cutoff_time: bool = False
cutoff_time: float | None = None
[docs]
def set_cutoff_time(self, time: float) -> None:
self.cutoff_time = time
self._has_cutoff_time = True
[docs]
class TimeAxis(ValueAxis):
"""Linear time grid for time-dependent calculations.
Closely related to :class:`FrequencyAxis`: the FFT of a function defined
on a ``TimeAxis`` lives on the corresponding ``FrequencyAxis``. The
default type ``'upper-half'`` exploits conjugation symmetry of bath
correlation functions; ``'complete'`` gives a straightforward full-range
grid.
Parameters
----------
start : float
First value of the time axis (in internal units, i.e. fs).
length : int
Number of time points.
step : float
Spacing between consecutive time points (fs).
atype : str, optional
Axis type. Must be ``'upper-half'`` (default) or ``'complete'``.
frequency_start : float, optional
Offset applied to the first frequency of the corresponding
:class:`FrequencyAxis`. Default is ``0.0``.
Raises
------
Exception
If ``atype`` is not ``'upper-half'`` or ``'complete'``.
"""
def __init__(
self,
start: float = 0.0,
length: int = 1,
step: float = 1.0,
atype: str = "upper-half",
frequency_start: float = 0.0,
) -> None:
ValueAxis.__init__(self, start=start, length=length, step=step)
self.frequency_start = frequency_start
self.allowed_atypes = ["upper-half", "complete"]
if atype in self.allowed_atypes:
self.atype = atype
else:
raise QuantarheiError("Unknown time axis type")
def __repr__(self) -> str:
return f"TimeAxis(start={self.start}, length={self.length}, step={self.step})"
[docs]
def shift_to_zero(self) -> None:
"""Shifts the values so that the first one is zero"""
if self.start != self.data[0]:
raise QuantarheiError("Inconsistent data")
if self.start > 0.0:
self.data[:] = self.data[:] - self.start
self.start = 0.0
[docs]
def get_FrequencyAxis(self) -> FrequencyAxis:
"""Returns corresponding FrequencyAxis object"""
if self.atype == "complete":
frequencies = numpy.fft.fftshift(
(2.0 * numpy.pi) * numpy.fft.fftfreq(self.length, self.step)
)
step = frequencies[1] - frequencies[0]
start = frequencies[0] + self.frequency_start
nosteps = len(frequencies)
time_start = self.data[self.length // 2]
elif self.atype == "upper-half":
frequencies = numpy.fft.fftshift(
(2.0 * numpy.pi) * numpy.fft.fftfreq(2 * self.length, self.step)
)
start = frequencies[0] + self.frequency_start
step = frequencies[1] - frequencies[0]
nosteps = len(frequencies)
time_start = self.min
else:
raise QuantarheiError("Unknown time axis type")
# this creation has to be protected from units management
with energy_units("int"):
faxis = FrequencyAxis(
start, nosteps, step, atype=self.atype, time_start=time_start
)
return faxis
if __name__ == "__main__":
import doctest
doctest.testmod()