Source code for opinf.ddt._interpolation

# ddt/_interpolation.py
"""Time derivative estimators based on interpolation."""

__all__ = [
    "InterpDerivativeEstimator",
]


import types
import warnings
import numpy as np
import scipy.interpolate as interp

from .. import errors
from ._base import DerivativeEstimatorTemplate


[docs] class InterpDerivativeEstimator(DerivativeEstimatorTemplate): r"""Time derivative estimator based on interpolation. For a set of (compressed) snapshots :math:`\Qhat = [~\qhat_0~~\cdots~~\qhat_{k-1}~] \in \RR^{r \times k}`, this class forms one-dimensional differentiable interpolants for each of the :math:`r` rows of :math:`\Qhat`. Parameters ---------- time_domain : (k,) ndarray Time domain of the snapshot data. InterpolatorClass : str or scipy.interpolate class Class for performing the interpolation. Must obey the following syntax: >>> intrp = InterpolatorClass(time_domain, states, **options) >>> new_states = intrp(new_time_domain, 0) >>> state_ddts = intrp(new_time_domain, 1) The following strings are also accepted. * ``"cubic"`` (default): use :class:`scipy.interpolate.CubicSpline`. * ``"akima"``: use :class:`scipy.interpolate.Akima1DInterpolator`. This is a local interpolation method and is more resitant to outliers than :class:`scipy.interpolate.CubicSpline`. However, it is not recommended if the time points are not uniformly spaced. * ``"pchip"``: use :class:`scipy.interpolate.PchipInterpolator`. The interpolator preserves monotonicity in the interpolation data and does not overshoot if the data is not smooth. new_time_domain : (k',) ndarray or None If given, evaluate the interpolator at these points to generate new state snapshots and corresponding time derivatives. If input snapshots are also given, interpolate them as well and evaluate the interpolant at these new points. The `new_time_domain` should lie within the range of the original `time_domain`; a warning is raised if extrapolation is requested. options : dict Keyword arguments for the constructor of the :attr:`InterpolatorClass`. """ _interpolators = types.MappingProxyType( { "akima": interp.Akima1DInterpolator, "cubic": interp.CubicSpline, "pchip": interp.PchipInterpolator, } ) # Constructor ------------------------------------------------------------- def __init__( self, time_domain: np.ndarray, InterpolatorClass="cubic", new_time_domain: np.ndarray = None, **options, ): """Set the time domain, InterpolatorClass, and options.""" DerivativeEstimatorTemplate.__init__(self, time_domain) # Set the InterpolatorClass. if not isinstance(InterpolatorClass, type): if InterpolatorClass not in self._interpolators: options = ", ".join([repr(k) for k in self._interpolators]) raise ValueError( f"invalid InterpolatorClass '{InterpolatorClass}', " f"options: {options}" ) InterpolatorClass = self._interpolators[InterpolatorClass] options["axis"] = 1 self.__IC = InterpolatorClass # Set the new_time_domain. if (t2 := new_time_domain) is not None: if not isinstance(t2, np.ndarray) or t2.ndim != 1: raise TypeError( "new_time_domain must be a one-dimensional array or None" ) t1 = time_domain if t2.min() < t1.min() or t2.max() > t1.max(): warnings.warn( "new_time_domain extrapolates beyond time_domain", errors.OpInfWarning, ) self.__t2 = new_time_domain # Set the interpolator constructor options. self.__options = types.MappingProxyType(options) # Properties -------------------------------------------------------------- def __str__(self): """String representation: class name, time domain.""" head = DerivativeEstimatorTemplate.__str__(self) options = ", ".join( [ f"{key}={repr(value)}" for key, value in self.options.items() if key != "axis" ] ) tail = [ f"InterpolatorClass: {self.InterpolatorClass.__name__}({options})" ] if (t2 := self.new_time_domain) is not None: tail.append( f"new_time_domain: {t2.size} entries " f"in [{t2.min()}, {t2.max()}]" ) return f"{head}\n " + "\n ".join(tail) @property def InterpolatorClass(self): """One-dimensional differentiable interpolator class.""" return self.__IC @property def new_time_domain(self): """Time domain at which to evaluate the interpolator and its first derivative. """ return self.__t2 @property def options(self): """Keyword arguments for the constructor of the :attr:`InterpolatorClass`. """ return self.__options # Main routine ------------------------------------------------------------
[docs] def estimate(self, states, inputs=None): """Estimate the first time derivatives of the states. Parameters ---------- states : (r, k) ndarray State snapshots, either full or (preferably) reduced. inputs : (m, k) ndarray or None Inputs corresponding to the state snapshots, if applicable. Returns ------- _states : (r, k') ndarray State snapshots or state estimates at the :attr:`new_time_domain`. ddts : (r, k') ndarray First time derivatives corresponding to ``_states``. _inputs : (m, k') ndarray or None Inputs corresponding to ``_states``, if applicable. **Only returned** if ``inputs`` is provided. """ states, inputs = self._check_dimensions(states, inputs) statespline = self.InterpolatorClass( self.time_domain, states, **self.options, ) t = self.time_domain if (t2 := self.new_time_domain) is not None: states = statespline(t2) if inputs is not None: inputspline = self.InterpolatorClass( t, inputs, **self.options, ) inputs = inputspline(t2) t = self.new_time_domain ddts = statespline(t, 1) if inputs is None: return states, ddts return states, ddts, inputs