Source code for newtonprop.interface

"""Recommended interface for a stateful Newton propagator

Typically, an efficient propagator will rely on low-level data structures.
Since conversion between high- and low-level structures in every call of
:func:`newtonprop.propagator.step` would be wasteful, it is better to use a
stateful propagator that only does the conversion once, holds the required
state, and converts back only on demand.

While a stateful propagator could be achieved through a wrapper function and
closures, a more straightforward implementation holds the necessary state in a
class. To encourage a consistent interface, you should subclass
:class:`NewtonPropagatorBase` for this purpose.
"""
from abc import ABCMeta, abstractmethod

import numpy as np

from .propagator import _step, _exp, _expmi, _expi


__all__ = ["NewtonPropagatorBase"]


[docs]class NewtonPropagatorBase(metaclass=ABCMeta): """Abstract base class for a stateful interface for using the Newton propagator This must be sub-classed before it can be used. Args: data: data that underlies the abstract operator $A$. The propagator will use this together with a time parameter `t` to construct the argument `A` for :func:`newtonprop.propagator.step`. func (callable or str): The scalar version of the operator-function $f$. Must take one complex argument and return a complex value. If given as a string, the corresponding callable will be looked up in :attr:`funcs`. m_max (int): Maximal Krylov dimension. Initializes the corresponding attribute. maxrestart (int): Maximum number of Newton restarts. Initializes the corresponding attribute. tol (float): Desired precision of the result. Initializes the corresponding attribute. Subclasses will customize the behavior mostly by overriding a number of private methods, outlined below. These methods may act on the following (private) attributes: .. py:attribute:: _data Internal data that may be used to calculate $A$. Obtained from `data` via :meth:`_convert_data_to_internal`. .. py:attribute:: _v0 The state set by :meth:`set_initial_state`, in its original format .. py:attribute:: _v The current state, in an internal format. The internal format is determined by :meth:`_convert_state_to_internal`. **Private methods:** .. automethod:: _check .. automethod:: _convert_state_to_internal .. automethod:: _convert_state_from_internal .. automethod:: _convert_data_to_internal .. automethod:: _inner .. automethod:: _norm .. automethod:: _zero .. automethod:: _A **Public methods, attributes, and properties:** .. py:attribute:: func The scalar version of the operator-function $f$, as a callable .. py:attribute:: m_max Maximal Krylov dimension (int) .. py:attribute:: maxrestart Maximum number of Newton restarts (int) .. py:attribute:: tol Desired precision of the result (float) """ #: Registry of `func` names (class attribute) #: #: Cf. the `func` argument of :func:`newtonprop.propagator.step`. You may #: extend this with your own function names. funcs = {"exp": _exp, "expmi": _expmi, "expi": _expi} def __init__(self, data, func="exp", m_max=10, maxrestart=100, tol=1e-12): if isinstance(func, str): func = self.funcs[func] self._data = self._convert_data_to_internal(data) self.func = func self._v0 = None self._v = None self.m_max = m_max self.maxrestart = maxrestart self.tol = tol
[docs] def set_initial_state(self, v): """Set the initial state for the propagation.""" # Note: `self._v` does not need to be the same as the input `v`. In # general, you want to convert `v` to a more efficient internal format # and store that. self._v0 = v self._v = self._convert_state_to_internal(v) self._check()
@property def state(self): """The current state (read-only) After a call to :meth:`set_initial_state`, this is the state set by that call. After a call to :meth:`step`, it is the result of that propagation step. """ res = self._convert_state_from_internal(self._v) assert isinstance(res, self._v0.__class__) return res
[docs] def step(self, t, dt): r"""Perform a single propagation step Args: t (float): time value at which to evaluate the abstract operator $A(t)$ dt (float): length of the time step Construct an abstract operator $A(t)$ based on the `data` the propagator was initialized with, and calculate .. math:: v_\text{out} = f(A(t)\,dt) \, v_\text{in} where $v_\text{in}$ is the current :attr:`state`, and $f$ is the function specified by `func` during initialization. The state $v_\text{out}$ becomes $v_\text{in}$ for the next call to :meth:`step`. The time parameter `t` should generally be the midpoint of the interval covered by $dt$. The results of the propagation step may be read from :attr:`state`. The :meth:`step` method does not return anything. """ A = self._A(t) self._v = _step( A, self._v, dt, self.func, m_max=self.m_max, maxrestart=self.maxrestart, tol=self.tol, inner=self._inner, norm=self._norm, zero=self._zero, )
# subclasses should customize the behavior primarily by overriding the # following private methods:
[docs] def _check(self): # pragma: nocover """Check completeness and consistency of attributes After :meth:`set_initial_state`, check that all attributes are defined and are consistent. If not, raise an :exc:`AttributeError`. """ try: N = np.prod(self._v.shape) if self.m_max >= N: raise ValueError( "m_max must be smaller than the system dimension" ) except AttributeError: pass
[docs] def _convert_state_to_internal(self, v): # pragma: nocover """Convert the initial state `v` into a more efficient internal format. This is used when setting :attr:`_v` from the `v` given in :meth:`set_initial_state`. By default, the state is passed on unchanged. """ return v
[docs] def _convert_state_from_internal(self, v): # pragma: nocover """Convert `v` in internal format of :attr:`_v` to original format If :attr:`_v` uses an internal format different from the `v` in :meth:`set_initial_state`, this method must be overridden to convert :attr:`_v` back to the original format. It is used by :attr:`state` for this purpose. By default, the internal state is passed on unchanged. """ return v
[docs] @staticmethod def _convert_data_to_internal(data): # pragma: nocover """Convert `data` to :attr:`_data` Optional conversion of the `data` passed to the initializer to a more efficient internal format. By default, the `data` is stored unchanged. """ return data
[docs] @abstractmethod def _inner(self, a, b): # pragma: nocover """Inner product between two states `a`, `b` in the format of :attr:`_v` Must return a complex number. This is an abstract method, and thus must be implemented by any subclass of :class:`NewtonPropagatorBase`. An example implementation is:: def _inner(self, a, b): return numpy.vdot(a, b) """ pass
[docs] @abstractmethod def _norm(self, a): # pragma: nocover """Norm of a state `a` in the format of :attr:`_v` Must return a complex number. This is an abstract method, and thus must be implemented by any subclass of :class:`NewtonPropagatorBase`. An example implementation is:: def _norm(self, a): return numpy.linalg.norm(a) """ pass
[docs] @abstractmethod def _zero(self, a): # pragma: nocover """A new zero-state compatible with the format of :attr:`_v` Must allocate and return a new state in the same internal format as :attr:`_v`, but containing zeros, so that the state can set in-place. This is an abstract method, and thus must be implemented by any subclass of :class:`NewtonPropagatorBase`. An example implementation is:: def _zero(self, a): return numpy.zeros(shape=a.shape, dtype=a.dtype) """ pass
[docs] @abstractmethod def _A(self, t): # pragma: nocover """Callable ``A(v)`` encoding the abstract operator $A(t)$. Must return a callable ``A(v)`` that takes a state `v` in the internal format of :attr:`_v` and returns a state in the same format. This is an abstract method, and thus must be implemented by any subclass of :class:`NewtonPropagatorBase`. Assuming ``self._data`` contains a nested list ``[L0, [L1, eps]]`` where ``L0`` and ``L1`` are numpy matrices containing a superoperator, and ``eps`` is a callable with argument `t` for a time-dependent control, an example implementation is:: def _A(self, t): Lmatrix = ( self._data[0] + self._data[1][0] + self._data[1][1](t)) def L(v): return Lmatrix @ v return L .. Note:: This method does not calculate ``A(v)`` directly, but returns a callable that does. This is to take into account time-dependent operators, which :func:`newtonprop.propagator.step` does not know anything about. """ pass