Source code for opinf.ddt._finite_difference

# ddt/_finite_difference.py
"""Finite-difference schemes for estimating snapshot time derivatives."""

__all__ = [
    "fwd1",
    "fwd2",
    "fwd3",
    "fwd4",
    "fwd5",
    "fwd6",
    "bwd1",
    "bwd2",
    "bwd3",
    "bwd4",
    "bwd5",
    "bwd6",
    "ctr2",
    "ctr4",
    "ctr6",
    "ord2",
    "ord4",
    "ord6",
    "ddt_uniform",
    "ddt_nonuniform",
    "ddt",
    "UniformFiniteDifferencer",
    "NonuniformFiniteDifferencer",
]

import types
import warnings
import numpy as np

from .. import errors
from ._base import DerivativeEstimatorTemplate


def _finite_difference(states, coeffs, mode, inputs=None):
    r"""Compute the first time derivative with a finite difference scheme.

    Parameters
    ----------
    states : (r, k) ndarray
        State snapshots: ``states[:, j]`` is the state at time :math:`t_j`.
        It is assumed that the states are uniformly spaced in time, i.e.,
        :math:`t_{j+1} - t_j` is the same for all :math:`j`.
    coeffs : (order + 1,) ndarray
        Finite difference coefficients. The convergence order of the
        estimation is ``len(coeffs) - 1``; for example, four points are
        used to obtain a third-order time derivative estimate.
    mode : str
        Indicator for if this is a forward, backward, or central difference.
        **Options:**

        * ``'fwd'``: forward difference.
        * ``'bwd'``: backward difference.
        * ``'ctr'``: central difference.
    inputs : (m, k) or (k,) ndarray or None
        Inputs corresponding to the states, if applicable.

    Returns
    -------
    _states : (r, k - order) ndarray
        State snapshots, excluding the last ``len(coeffs) - 1`` snapshots.
    ddts : (r, k - order) ndarray
        Time derivative estimates corresponding to the state snapshots.
    _inputs : (m, k - order) ndarray or None
        Inputs corresponding to ``_states``, if applicable.
        **Only returned** if ``inputs`` is provided.
    """
    r, k = states.shape
    order = len(coeffs) - 1

    if mode == "fwd":
        cols = slice(0, -order)
    elif mode == "bwd":
        cols = slice(order, None)
    elif mode == "ctr":
        margin = order // 2
        cols = slice(margin, -margin)
    else:
        raise ValueError(f"invalid finite difference mode '{mode}'")

    ddts = np.zeros((r, k - order))
    for j, coeff in enumerate(coeffs):
        if coeff != 0:
            ddts += coeff * states[:, j : k - order + j]

    if inputs is not None:
        return states[:, cols], ddts, inputs[..., cols]
    return states[:, cols], ddts


# Forward differences =========================================================
[docs] def fwd1(states: np.ndarray, dt: float, inputs=None): r"""First-order forward difference for estimating the first derivative. .. math:: \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_j} \approx \frac{1}{\delta t}(\q(t_{j+1}) - \q(t_j)) where :math:`\delta t = t_{j+1} - t_j` for all :math:`j`. Parameters ---------- states : (r, k) ndarray State snapshots: ``states[:, j]`` is the state at time :math:`t_j`. dt : float Time step between snapshots. inputs : (m, k) or (k,) ndarray or None Inputs corresponding to the states, if applicable. Returns ------- _states : (r, k - 1) ndarray State snapshots, excluding the last snapshot. ddts : (r, k - 1) ndarray Time derivative estimates corresponding to the state snapshots. _inputs : (m, k - 1) or (k - 1,) ndarray or None Inputs corresponding to ``_states``, if applicable. **Only returned** if ``inputs`` is not ``None``. """ coeffs = np.array([-1, 1]) / dt return _finite_difference(states, coeffs, "fwd", inputs)
[docs] def fwd2(states: np.ndarray, dt: float, inputs=None): r"""Second-order forward difference for estimating the first derivative. .. math:: \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_j} \approx \frac{1}{2\delta t}(-3\q(t_j) + 4\q(t_{j+1}) - \q(t_{j+2})) where :math:`\delta t = t_{j+1} - t_j` for all :math:`j`. Parameters ---------- states : (r, k) ndarray State snapshots: ``states[:, j]`` is the state at time :math:`t_j`. dt : float Time step between snapshots. inputs : (m, k) or (k,) ndarray or None Inputs corresponding to the states, if applicable. Returns ------- _states : (r, k - 2) ndarray State snapshots, excluding the last two snapshots. ddts : (r, k - 2) ndarray Time derivative estimates corresponding to the state snapshots. _inputs : (m, k - 2) or (k - 2,) ndarray or None Inputs corresponding to ``_states``, if applicable. **Only returned** if ``inputs`` is not ``None``. """ coeffs = np.array([-3, 4, -1]) / (2 * dt) return _finite_difference(states, coeffs, "fwd", inputs)
[docs] def fwd3(states: np.ndarray, dt: float, inputs=None): r"""Third-order forward difference for estimating the first derivative. .. math:: \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_j} \approx \frac{1}{6\delta t} (-11\q(t_j) + 18\q(t_{j+1}) - 9\q(t_{j+2}) + 2\q(t_{j+3})) where :math:`\delta t = t_{j+1} - t_j` for all :math:`j`. Parameters ---------- states : (r, k) ndarray State snapshots: ``states[:, j]`` is the state at time :math:`t_j`. dt : float Time step between snapshots. inputs : (m, k) or (k,) ndarray or None Inputs corresponding to the states, if applicable. Returns ------- _states : (r, k - 3) ndarray State snapshots, excluding the last three snapshots. ddts : (r, k - 3) ndarray Time derivative estimates corresponding to the state snapshots. _inputs : (m, k - 3) or (k - 3,) ndarray or None Inputs corresponding to ``_states``, if applicable. **Only returned** if ``inputs`` is not ``None``. """ coeffs = np.array([-11, 18, -9, 2]) / (6 * dt) return _finite_difference(states, coeffs, "fwd", inputs)
[docs] def fwd4(states: np.ndarray, dt: float, inputs=None): r"""Fourth-order forward difference for estimating the first derivative. .. math:: \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_j} \approx \frac{1}{12\delta t}(-25\q(t_j) + 48\q(t_{j+1}) - 36\q(t_{j+2}) + 16\q(t_{j+3}) - 3\q(t_{j+4})) where :math:`\delta t = t_{j+1} - t_j` for all :math:`j`. Parameters ---------- states : (r, k) ndarray State snapshots: ``states[:, j]`` is the state at time :math:`t_j`. dt : float Time step between snapshots. inputs : (m, k) or (k,) ndarray or None Inputs corresponding to the states, if applicable. Returns ------- _states : (r, k - 4) ndarray State snapshots, excluding the last four snapshots. ddts : (r, k - 4) ndarray Time derivative estimates corresponding to the state snapshots. _inputs : (m, k - 4) or (k - 4,) ndarray or None Inputs corresponding to ``_states``, if applicable. **Only returned** if ``inputs`` is not ``None``. """ coeffs = np.array([-25, 48, -36, 16, -3]) / (12 * dt) return _finite_difference(states, coeffs, "fwd", inputs)
[docs] def fwd5(states: np.ndarray, dt: float, inputs=None): r"""Fifth-order forward difference for estimating the first derivative. .. math:: \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_j} \approx \frac{1}{60\delta t}(-137\q(t_j) + 300\q(t_{j+1}) - 300\q(t_{j+2}) + 200\q(t_{j+3}) - 75\q(t_{j+4}) + 12\q(t_{j+5})) where :math:`\delta t = t_{j+1} - t_j` for all :math:`j`. Parameters ---------- states : (r, k) ndarray State snapshots: ``states[:, j]`` is the state at time :math:`t_j`. dt : float Time step between snapshots. inputs : (m, k) or (k,) ndarray or None Inputs corresponding to the states, if applicable. Returns ------- _states : (r, k - 5) ndarray State snapshots, excluding the last five snapshots. ddts : (r, k - 5) ndarray Time derivative estimates corresponding to the state snapshots. _inputs : (m, k - 5) or (k - 5,) ndarray or None Inputs corresponding to ``_states``, if applicable. **Only returned** if ``inputs`` is not ``None``. """ coeffs = np.array([-137, 300, -300, 200, -75, 12]) / (60 * dt) return _finite_difference(states, coeffs, "fwd", inputs)
[docs] def fwd6(states: np.ndarray, dt: float, inputs=None): r"""Sixth-order forward difference for estimating the first derivative. .. math:: \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_j} \approx \frac{1}{60\delta t}( -147\q(t_j) + 360\q(t_{j+1}) - 450\q(t_{j+2}) + 400\q(t_{j+3}) - 225\q(t_{j+4}) + 72\q(t_{j+5}) - 10\q(t_{j+6})) where :math:`\delta t = t_{j+1} - t_j` for all :math:`j`. Parameters ---------- states : (r, k) ndarray State snapshots: ``states[:, j]`` is the state at time :math:`t_j`. dt : float Time step between snapshots. inputs : (m, k) or (k,) ndarray or None Inputs corresponding to the states, if applicable. Returns ------- _states : (r, k - 6) ndarray State snapshots, excluding the last six snapshots. ddts : (r, k - 6) ndarray Time derivative estimates corresponding to the state snapshots. _inputs : (m, k - 6) or (k - 6,) ndarray or None Inputs corresponding to ``_states``, if applicable. **Only returned** if ``inputs`` is not ``None``. """ coeffs = np.array([-147, 360, -450, 400, -225, 72, -10]) / (60 * dt) return _finite_difference(states, coeffs, "fwd", inputs)
# Backwards differences =======================================================
[docs] def bwd1(states: np.ndarray, dt: float, inputs=None): r"""First-order backward difference for estimating the first derivative. .. math:: \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_j} \approx \frac{1}{\delta t}(\q(t_j) - \q(t_{j-1})) where :math:`\delta t = t_{j+1} - t_j` for all :math:`j`. Parameters ---------- states : (r, k) ndarray State snapshots: ``states[:, j]`` is the state at time :math:`t_j`. dt : float Time step between snapshots. inputs : (m, k) or (k,) ndarray or None Inputs corresponding to the states, if applicable. Returns ------- _states : (r, k - 1) ndarray State snapshots, excluding the first snapshot. ddts : (r, k - 1) ndarray Time derivative estimates corresponding to the state snapshots. _inputs : (m, k - 1) or (k - 1,) ndarray or None Inputs corresponding to ``_states``, if applicable. **Only returned** if ``inputs`` is not ``None``. """ coeffs = np.array([-1, 1]) / dt return _finite_difference(states, coeffs, "bwd", inputs)
[docs] def bwd2(states: np.ndarray, dt: float, inputs=None): r"""Second-order backward difference for estimating the first derivative. .. math:: \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_j} \approx \frac{1}{2\delta t}(3\q(t_j) - 4\q(t_{j-1}) + \q(t_{j-2})) where :math:`\delta t = t_{j+1} - t_j` for all :math:`j`. Parameters ---------- states : (r, k) ndarray State snapshots: ``states[:, j]`` is the state at time :math:`t_j`. dt : float Time step between snapshots. inputs : (m, k) or (k,) ndarray or None Inputs corresponding to the states, if applicable. Returns ------- _states : (r, k - 2) ndarray State snapshots, excluding the first two snapshots. ddts : (r, k - 2) ndarray Time derivative estimates corresponding to the state snapshots. _inputs : (m, k - 2) or (k - 2,) ndarray or None Inputs corresponding to ``_states``, if applicable. **Only returned** if ``inputs`` is not ``None``. """ coeffs = np.array([1, -4, 3]) / (2 * dt) return _finite_difference(states, coeffs, "bwd", inputs)
[docs] def bwd3(states: np.ndarray, dt: float, inputs=None): r"""Third-order backward difference for estimating the first derivative. .. math:: \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_j} \approx \frac{1}{6\delta t} (11\q(t_j) - 18\q(t_{j-1}) + 9\q(t_{j-2}) - 2\q(t_{j-3})) where :math:`\delta t = t_{j+1} - t_j` for all :math:`j`. Parameters ---------- states : (r, k) ndarray State snapshots: ``states[:, j]`` is the state at time :math:`t_j`. dt : float Time step between snapshots. inputs : (m, k) or (k,) ndarray or None Inputs corresponding to the states, if applicable. Returns ------- _states : (r, k - 3) ndarray State snapshots, excluding the first three snapshots. ddts : (r, k - 3) ndarray Time derivative estimates corresponding to the state snapshots. _inputs : (m, k - 3) or (k - 3,) ndarray or None Inputs corresponding to ``_states``, if applicable. **Only returned** if ``inputs`` is not ``None``. """ coeffs = np.array([-2, 9, -18, 11]) / (6 * dt) return _finite_difference(states, coeffs, "bwd", inputs)
[docs] def bwd4(states: np.ndarray, dt: float, inputs=None): r"""Fourth-order backward difference for estimating the first derivative. .. math:: \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_j} \approx \frac{1}{12\delta t}(25\q(t_j) - 48\q(t_{j-1}) + 36\q(t_{j-2}) - 16\q(t_{j-3}) + 3\q(t_{j-4})) where :math:`\delta t = t_{j+1} - t_j` for all :math:`j`. Parameters ---------- states : (r, k) ndarray State snapshots: ``states[:, j]`` is the state at time :math:`t_j`. dt : float Time step between snapshots. inputs : (m, k) or (k,) ndarray or None Inputs corresponding to the states, if applicable. Returns ------- _states : (r, k - 4) ndarray State snapshots, excluding the first four snapshots. ddts : (r, k - 4) ndarray Time derivative estimates corresponding to the state snapshots. _inputs : (m, k - 4) or (k - 4,) ndarray or None Inputs corresponding to ``_states``, if applicable. **Only returned** if ``inputs`` is not ``None``. """ coeffs = np.array([3, -16, 36, -48, 25]) / (12 * dt) return _finite_difference(states, coeffs, "bwd", inputs)
[docs] def bwd5(states: np.ndarray, dt: float, inputs=None): r"""Fifth-order backward difference for estimating the first derivative. .. math:: \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_j} \approx \frac{1}{60\delta t}(137\q(t_j) - 300\q(t_{j-1}) + 300\q(t_{j-2}) - 200\q(t_{j-3}) + 75\q(t_{j-4}) - 12\q(t_{j-5})) where :math:`\delta t = t_{j+1} - t_j` for all :math:`j`. Parameters ---------- states : (r, k) ndarray State snapshots: ``states[:, j]`` is the state at time :math:`t_j`. dt : float Time step between snapshots. inputs : (m, k) or (k,) ndarray or None Inputs corresponding to the states, if applicable. Returns ------- _states : (r, k - 5) ndarray State snapshots, excluding the first five snapshots. ddts : (r, k - 5) ndarray Time derivative estimates corresponding to the state snapshots. _inputs : (m, k - 5) or (k - 5,) ndarray or None Inputs corresponding to ``_states``, if applicable. **Only returned** if ``inputs`` is not ``None``. """ coeffs = np.array([-12, 75, -200, 300, -300, 137]) / (60 * dt) return _finite_difference(states, coeffs, "bwd", inputs)
[docs] def bwd6(states: np.ndarray, dt: float, inputs=None): r"""Sixth-order backward difference for estimating the first derivative. .. math:: \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_j} \approx \frac{1}{60\delta t}( 147\q(t_j) - 360\q(t_{j-1}) + 450\q(t_{j-2}) - 400\q(t_{j-3}) + 225\q(t_{j-4}) - 72\q(t_{j-5}) + 10\q(t_{j-6}) ) where :math:`\delta t = t_{j+1} - t_j` for all :math:`j`. Parameters ---------- states : (r, k) ndarray State snapshots: ``states[:, j]`` is the state at time :math:`t_j`. dt : float Time step between snapshots. inputs : (m, k) or (k,) ndarray or None Inputs corresponding to the states, if applicable. Returns ------- _states : (r, k - 6) ndarray State snapshots, excluding the first six snapshots. ddts : (r, k - 6) ndarray Time derivative estimates corresponding to the state snapshots. _inputs : (m, k - 6) or (k - 6,) ndarray or None Inputs corresponding to ``_states``, if applicable. **Only returned** if ``inputs`` is not ``None``. """ coeffs = np.array([10, -72, 225, -400, 450, -360, 147]) / (60 * dt) return _finite_difference(states, coeffs, "bwd", inputs)
# Central differences ========================================================
[docs] def ctr2(states: np.ndarray, dt: float, inputs=None): r"""Second-order central difference for estimating the first derivative. .. math:: \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_j} \approx \frac{1}{2\delta t}(\q(t_{j+1}) - \q(t_{j-1})) where :math:`\delta t = t_{j+1} - t_j` for all :math:`j`. Parameters ---------- states : (r, k) ndarray State snapshots: ``states[:, j]`` is the state at time :math:`t_j`. dt : float Time step between snapshots. inputs : (m, k) or (k,) ndarray or None Inputs corresponding to the states, if applicable. Returns ------- _states : (r, k - 2) ndarray State snapshots, excluding the first and last snapshots. ddts : (r, k - 2) ndarray Time derivative estimates corresponding to the state snapshots. _inputs : (m, k - 2) or (k - 2,) ndarray or None Inputs corresponding to ``_states``, if applicable. **Only returned** if ``inputs`` is not ``None``. """ coeffs = np.array([-1, 0, 1]) / (2 * dt) return _finite_difference(states, coeffs, "ctr", inputs)
[docs] def ctr4(states: np.ndarray, dt: float, inputs=None): r"""Fourth-order central difference for estimating the first derivative. .. math:: \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_j} \approx \frac{1}{12\delta t} (\q(t_{j-2}) - 8\q(t_{j-1}) + 8\q(t_{j+1}) - \q(t_{j+2})) where :math:`\delta t = t_{j+1} - t_j` for all :math:`j`. Parameters ---------- states : (r, k) ndarray State snapshots: ``states[:, j]`` is the state at time :math:`t_j`. dt : float Time step between snapshots. inputs : (m, k) or (k,) ndarray or None Inputs corresponding to the states, if applicable. Returns ------- _states : (r, k - 4) ndarray State snapshots, excluding the first two and last two snapshots. ddts : (r, k - 4) ndarray Time derivative estimates corresponding to the state snapshots. _inputs : (m, k - 4) or (k - 4,) ndarray or None Inputs corresponding to ``_states``, if applicable. **Only returned** if ``inputs`` is not ``None``. """ coeffs = np.array([1, -8, 0, 8, -1]) / (12 * dt) return _finite_difference(states, coeffs, "ctr", inputs)
[docs] def ctr6(states: np.ndarray, dt: float, inputs=None): r"""Sixth-order central difference for estimating the first derivative. .. math:: \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_j} \approx \frac{1}{60\delta t} (-\q(t_{j-3}) + 9\q(t_{j-2}) - 45\q(t_{j-1}) + 45\q(t_{j+1})) - 9\q(t_{j+2}) + \q(t_{j+3}) where :math:`\delta t = t_{j+1} - t_j` for all :math:`j`. Parameters ---------- states : (r, k) ndarray State snapshots: ``states[:, j]`` is the state at time :math:`t_j`. dt : float Time step between snapshots. inputs : (m, k) or (k,) ndarray or None Inputs corresponding to the states, if applicable. Returns ------- _states : (r, k - 6) ndarray State snapshots, excluding the first three and last three snapshots. ddts : (r, k - 6) ndarray Time derivative estimates corresponding to the state snapshots. _inputs : (m, k - 6) or (k - 6,) ndarray or None Inputs corresponding to ``_states``, if applicable. **Only returned** if ``inputs`` is not ``None``. """ coeffs = np.array([-1, 9, -45, 0, 45, -9, 1]) / (60 * dt) return _finite_difference(states, coeffs, "ctr", inputs)
# Mixed differences =========================================================== def _mixed_differences(states, dt, order, inputs): """Mixed finite differences on a uniform time domain.""" ddts = ddt_uniform(states, dt, order=order) if inputs is not None: return states, ddts, inputs return states, ddts
[docs] def ord2(states: np.ndarray, dt: float, inputs=None): r"""Second-order forward, central, and backward differences for estimating the first derivative. Central differences are used where possible; forward differences are used for the first point and backward differences are used for the last point: .. math:: \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_0} &\approx \frac{1}{2\delta t}(-3\q(t_0) + 4\q(t_{1}) - \q(t_{2})), \\ \\ \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_j} &\approx \frac{1}{2\delta t}(-\q(t_{j-1}) + \q(t_{j+1})), \quad j = 2,\ldots,k-2, \\ \\ \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_{k-1}} &\approx \frac{1}{2\delta t}(\q(t_{k-3}) - 4\q(t_{k-2}) + 3\q(t_{k-1})) where :math:`\delta t = t_{j+1} - t_j` for all :math:`j`. Parameters ---------- states : (r, k) ndarray State snapshots: ``states[:, j]`` is the state at time :math:`t_j`. dt : float Time step between snapshots. inputs : (m, k) or (k,) ndarray or None Inputs corresponding to the states, if applicable. Returns ------- states : (r, k) ndarray State snapshots. ddts : (r, k) ndarray Time derivative estimates corresponding to the state snapshots. inputs : (m, k) or (k,) ndarray or None Inputs corresponding to ``_states``, if applicable. **Only returned** if ``inputs`` is not ``None``. """ return _mixed_differences(states, dt, 2, inputs)
[docs] def ord4(states: np.ndarray, dt: float, inputs=None): r"""Fourth-order forward, central, and backward differences for estimating the first derivative. Central differences are used where possible; forward differences are used for the first two points and backward differences are used for the last two points: .. math:: \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_0} &\approx \frac{1}{12\delta t}(-25\q(t_0) + 48\q(t_{1}) - 36\q(t_{2}) + 16\q(t_{3}) - 3\q(t_{4})), \\ \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_1} &\approx \frac{1}{12\delta t}(-3\q(t_0) - 10\q(t_{1}) + 18\q(t_{2}) - 6\q(t_{3}) + \q(t_{4})), \\ \\ \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_j} &\approx \frac{1}{12\delta t} (\q(t_{j-2}) - 8\q(t_{j-1}) + 8\q(t_{j+1}) - \q(t_{j+2})), \quad j = 2,\ldots,k-3, \\ \\ \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_{k-2}} &\approx \frac{1}{12\delta t}(-\q(t_{k-5}) + 6\q(t_{k-4}) - 18\q(t_{k-3}) + 10\q(t_{k-2}) + 3\q(t_{k-1})), \\ \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_{k-1}} &\approx \frac{1}{12\delta t}(3\q(t_{k-5}) - 16\q(t_{k-4}) + 36\q(t_{k-3}) - 48\q(t_{k-2}) + 25\q(t_{k-1})), where :math:`\delta t = t_{j+1} - t_j` for all :math:`j`. Parameters ---------- states : (r, k) ndarray State snapshots: ``states[:, j]`` is the state at time :math:`t_j`. dt : float Time step between snapshots. inputs : (m, k) or (k,) ndarray or None Inputs corresponding to the states, if applicable. Returns ------- states : (r, k) ndarray State snapshots. ddts : (r, k) ndarray Time derivative estimates corresponding to the state snapshots. inputs : (m, k) or (k,) ndarray or None Inputs corresponding to ``_states``, if applicable. **Only returned** if ``inputs`` is not ``None``. """ return _mixed_differences(states, dt, 4, inputs)
[docs] def ord6(states: np.ndarray, dt: float, inputs=None): r"""Sixth-order forward, central, and backward differences for estimating the first derivative. Central differences are used where possible; forward differences are used for the first three points and backward differences are used for the last three points: .. math:: \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_0} &\approx \frac{1}{60\delta t}( -147\q(t_0) + 360\q(t_{1}) - 450\q(t_{2}) + 400\q(t_{3}) - 225\q(t_{4}) + 72\q(t_{5}) - 10\q(t_{6})), \\ \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_1} &\approx \frac{1}{60\delta t}( -10\q(t_0) - 77\q(t_{1}) + 150\q(t_{2}) - 100\q(t_{3}) + 50\q(t_{4}) - 15\q(t_{5}) + 2\q(t_{6})), \\ \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_2} &\approx \frac{1}{60\delta t}( 2\q(t_0) - 24\q(t_{1}) - 35\q(t_{2}) + 80\q(t_{3}) - 30\q(t_{4}) + 8\q(t_{5}) - \q(t_{6})), \\ \\ \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_j} &\approx \frac{1}{60\delta t} (-\q(t_{j-3}) + 9\q(t_{j-2}) - 45\q(t_{j-1}) + 45\q(t_{j+1})) - 9\q(t_{j+2}) + \q(t_{j+3}), \quad j = 3, \ldots, k - 4, \\ \\ \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_{k-3}} &\approx \frac{1}{60\delta t}( \q(t_{k-7}) - 8\q(t_{k-6}) + 30\q(t_{k-5}) - 80\q(t_{k-4}) + 35\q(t_{k-3}) + 24\q(t_{k-2}) - 2\q(t_{k-1})), \\ \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_{k-2}} &\approx \frac{1}{60\delta t}( -2\q(t_{k-7}) + 15\q(t_{k-6}) - 50\q(t_{k-5}) + 100\q(t_{k-4}) - 150\q(t_{k-3}) + 77\q(t_{k-2}) + 10\q(t_{k-1})), \\ \frac{\textup{d}}{\textup{d}t}\q(t)\bigg|_{t = t_{k-1}} &\approx \frac{1}{60\delta t}( 10\q(t_{k-7}) - 72\q(t_{k-6}) + 225\q(t_{k-5}) - 400\q(t_{k-4}) + 450\q(t_{k-3}) - 360\q(t_{k-2}) + 147\q(t_{k-1})), where :math:`\delta t = t_{j+1} - t_j` for all :math:`j`. Parameters ---------- states : (r, k) ndarray State snapshots: ``states[:, j]`` is the state at time :math:`t_j`. dt : float Time step between snapshots. inputs : (m, k) or (k,) ndarray or None Inputs corresponding to the states, if applicable. Returns ------- states : (r, k) ndarray State snapshots. ddts : (r, k) ndarray Time derivative estimates corresponding to the state snapshots. inputs : (m, k) or (k,) ndarray or None Inputs corresponding to ``_states``, if applicable. **Only returned** if ``inputs`` is not ``None``. """ return _mixed_differences(states, dt, 6, inputs)
# Class interface ============================================================
[docs] class UniformFiniteDifferencer(DerivativeEstimatorTemplate): r"""Time derivative estimation with finite differences for state snapshots spaced uniformly in time. Parameters ---------- time_domain : (k,) ndarray Time domain corresponding to the snapshot data. This class requires uniformly spaced time domains, see :class:`NonuniformFiniteDifferencer` for non-uniform domains. scheme : str or callable Finite difference scheme to use. **Options:** * ``'fwd1'``: first-order forward differences, see :func:`fwd1`. * ``'fwd2'``: second-order forward differences, see :func:`fwd2`. * ``'fwd3'``: third-order forward differences, see :func:`fwd3`. * ``'fwd4'``: fourth-order forward differences, see :func:`fwd4`. * ``'fwd5'``: fifth-order forward differences, see :func:`fwd5`. * ``'fwd6'``: sixth-order forward differences, see :func:`fwd6`. * ``'bwd1'``: first-order backward differences, see :func:`bwd1`. * ``'bwd2'``: second-order backward differences, see :func:`bwd2`. * ``'bwd3'``: third-order backward differences, see :func:`bwd3`. * ``'bwd4'``: fourth-order backward differences, see :func:`bwd4`. * ``'bwd5'``: fifth-order backward differences, see :func:`bwd5`. * ``'bwd6'``: sixth-order backward differences, see :func:`bwd6`. * ``'ctr2'``: second-order backward differences, see :func:`ctr2`. * ``'ctr4'``: fourth-order backward differences, see :func:`ctr4`. * ``'ctr6'``: sixth-order backward differences, see :func:`ctr6`. * ``'ord2'``: second-order differences, see :func:`ord2`. * ``'ord4'``: fourth-order differences, see :func:`ord4`. * ``'ord6'``: sixth-order differences, see :func:`ord6`. If ``scheme`` is a callable function, its signature must match the following syntax. .. code-block:: python _states, ddts = scheme(states, dt) _states, ddts, _inputs = scheme(states, dt, inputs) Here ``dt`` is a positive float, the uniform time step. Each output should have the same number of columns. """ _schemes = types.MappingProxyType( { "fwd1": fwd1, "fwd2": fwd2, "fwd3": fwd3, "fwd4": fwd4, "fwd5": fwd5, "fwd6": fwd6, "bwd1": bwd1, "bwd2": bwd2, "bwd3": bwd3, "bwd4": bwd4, "bwd5": bwd5, "bwd6": bwd6, "ctr2": ctr2, "ctr4": ctr4, "ctr6": ctr6, "ord2": ord2, "ord4": ord4, "ord6": ord6, } ) def __init__(self, time_domain, scheme="ord4"): """Store the time domain and set the finite difference scheme.""" DerivativeEstimatorTemplate.__init__(self, time_domain) # Check for uniform spacing. diffs = np.diff(time_domain) if not np.allclose(diffs, diffs[0]): raise ValueError("time domain must be uniformly spaced") # Set the finite difference scheme. if not callable(scheme): if scheme not in self._schemes: raise ValueError( f"invalid finite difference scheme '{scheme}'" ) scheme = self._schemes[scheme] self.__scheme = scheme # Properties -------------------------------------------------------------- @property def dt(self): """Time step.""" t = self.time_domain return t[1] - t[0] @property def scheme(self): """Finite difference engine.""" return self.__scheme def __str__(self): return "\n ".join( [ DerivativeEstimatorTemplate.__str__(self), f"dt: {self.dt:.4e}", f"scheme: {self.scheme.__name__}()", ] ) # Main routine ------------------------------------------------------------
[docs] def estimate(self, states, inputs=None): r"""Estimate the first time derivatives of the states using finite differences. The stencil and order of the method are determined by the ``scheme`` attribute. 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 Subset of the state snapshots. 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, False) return self.scheme(states, self.dt, inputs)
[docs] class NonuniformFiniteDifferencer(DerivativeEstimatorTemplate): """Time derivative estimation with finite differences for state snapshots that are **not** spaced uniformly in time. This class essentially wraps :func:`numpy.gradient()`, which uses second-order finite differences. Parameters ---------- time_domain : (k,) ndarray Time domain corresponding to the snapshot data. This class is for time domains that are not uniformly spaced, see :class:`UniformFiniteDifferencer` for uniformly spaced time domains. """ def __init__(self, time_domain): """Set the time domain.""" DerivativeEstimatorTemplate.__init__(self, time_domain) # Warn if time_domain in not uniform. if np.allclose(diffs := np.diff(time_domain), diffs[0]): warnings.warn( "time_domain is uniformly spaced, consider using " "UniformFiniteDifferencer", errors.OpInfWarning, ) def __str__(self): """String representation: class name, time domain.""" return "\n ".join( [ DerivativeEstimatorTemplate.__str__(self), "scheme: np.gradient(edge_order=2)", ] ) # Main routine ------------------------------------------------------------
[docs] def estimate(self, states, inputs=None): r"""Estimate the first time derivatives of the states using second-order finite differences. 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. 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) # Do the computation. ddts = np.gradient(states, self.time_domain, edge_order=2, axis=-1) if inputs is not None: return states, ddts, inputs return states, ddts
# Old API =====================================================================
[docs] def ddt_uniform(states, dt, order=2): """Forward, central, and backward differences for estimating the first derivative. Parameters ---------- states : (r, k) ndarray State snapshots: ``states[:, j]`` is the state at time :math:`t_j`. dt : float Time step between snapshots. order : int {2, 4, 6} The order of the derivative approximation. Returns ------- ddts : (r, k) ndarray Time derivative estimates corresponding to the state snapshots. """ # Check dimensions and input types. if states.ndim != 2: raise errors.DimensionalityError("states must be two-dimensional") if not np.isscalar(dt): raise TypeError("time step dt must be a scalar (e.g., float)") ddts = np.empty_like(states) if order == 2: # TODO: Does this next line do what the rest does? # return np.gradient(states, dt, edge_order=2, axis=1) coeffs0 = np.array([-3, 4, -1]) / (2 * dt) # Forward differences on the front. ddts[:, 0] = states[:, :3] @ coeffs0 # Central differences on the interior. _, ddts[:, 1:-1] = ctr2(states, dt, None) # Backward differences on the end. ddts[:, -1] = states[:, -3:] @ -coeffs0[::-1] elif order == 4: coeffs0 = np.array([-25, 48, -36, 16, -3]) / (12 * dt) coeffs1 = np.array([-3, -10, 18, -6, 1]) / (12 * dt) # Forward differences on the front. ddts[:, 0] = states[:, :5] @ coeffs0 ddts[:, 1] = states[:, :5] @ coeffs1 # Central differences on interior. _, ddts[:, 2:-2] = ctr4(states, dt, None) # Backward differences on the end. ddts[:, -2] = states[:, -5:] @ -coeffs1[::-1] ddts[:, -1] = states[:, -5:] @ -coeffs0[::-1] elif order == 6: coeffs0 = np.array([-147, 360, -450, 400, -225, 72, -10]) / (60 * dt) coeffs1 = np.array([-10, -77, 150, -100, 50, -15, 2]) / (60 * dt) coeffs2 = np.array([2, -24, -35, 80, -30, 8, -1]) / (60 * dt) # Forward differences on the front. ddts[:, 0] = states[:, :7] @ coeffs0 ddts[:, 1] = states[:, :7] @ coeffs1 ddts[:, 2] = states[:, :7] @ coeffs2 # Central differences on interior. _, ddts[:, 3:-3] = ctr6(states, dt, None) # Backward differences on the end. ddts[:, -3] = states[:, -7:] @ -coeffs2[::-1] ddts[:, -2] = states[:, -7:] @ -coeffs1[::-1] ddts[:, -1] = states[:, -7:] @ -coeffs0[::-1] else: raise NotImplementedError( f"invalid order '{order}'; " "valid options: {2, 4, 6}" ) return ddts
[docs] def ddt_nonuniform(states, t): """Second-order finite differences for estimating the first derivative. Parameters ---------- states : (r, k) ndarray State snapshots: ``states[:, j]`` is the state at time :math:`t_j`. t : (k,) ndarray Time domain corresponding to the state snapshots. Returns ------- ddts : (r, k) ndarray Time derivative estimates corresponding to the state snapshots. """ # Check dimensions. if states.ndim != 2: raise errors.DimensionalityError("states must be two-dimensional") if t.ndim != 1: raise errors.DimensionalityError("time t must be one-dimensional") if states.shape[-1] != t.shape[0]: raise errors.DimensionalityError("states not aligned with time t") # Compute the derivative with a second-order difference scheme. return np.gradient(states, t, edge_order=2, axis=-1)
[docs] def ddt(states, *args, **kwargs): """Finite differences for estimating the first derivative. This is a convenience function that calls either :func:`ddt_uniform()` or :func:`ddt_nonuniform()`, depending on the arguments. Parameters ---------- states : (r, k) ndarray State snapshots: ``states[:, j]`` is the state at time :math:`t_j`. dt : float Time step between snapshots (in the case of a time domain with uniform spacing). order : int {2, 4, 6} (optional) The order of the derivative approximation. t : (k,) ndarray Time domain corresponding to the state snapshots. May or may not be uniformly spaced. Returns ------- ddts : (r, k) ndarray Time derivative estimates corresponding to the state snapshots. """ n_args = len(args) # Number of other positional args. n_kwargs = len(kwargs) # Number of keyword args. n_total = n_args + n_kwargs # Total number of other args. if n_total == 0: raise TypeError("at least one other argument required (dt or t)") elif n_total == 1: # There is only one other argument. if n_kwargs == 1: # It is a keyword argument. arg_name = list(kwargs.keys())[0] if arg_name == "dt": func = ddt_uniform elif arg_name == "t": func = ddt_nonuniform elif arg_name == "order": raise TypeError( "keyword argument 'order' requires float " "argument dt" ) else: raise TypeError( f"ddt() got unexpected keyword argument '{arg_name}'" ) elif n_args == 1: # It is a positional argument. arg = args[0] if isinstance(arg, float): # arg = dt. func = ddt_uniform elif isinstance(arg, np.ndarray): # arg = t. func = ddt_nonuniform else: raise TypeError(f"invalid argument type '{type(arg)}'") elif n_total == 2: # There are two other arguments: dt, order. func = ddt_uniform else: raise TypeError( "ddt() takes 2 or 3 positional arguments " f"but {n_total+1} were given" ) return func(states, *args, **kwargs)
# Helper functions ============================================================ def _fdcoeffs(s): # pragma: no cover r"""Vandermonde solve for finite difference coefficients. Parameters ---------- s : (p,) ndarray Finite difference stencil. For example, ``s=[-2, -1, 0, 1]`` means we want coefficients :math:`c_{-2},c_{-1},c_{0},c_{1}` such that :math:`f'(x) \approx c_{-2} f(x - 2) + c_{-1} f(x - 1) + c_{0} f(x) + c_{1} f(x + 1)`. Returns ------- (p,) ndarray Finite difference coefficients (the :math:`c_{j}`'s). """ V = np.vander(s, increasing=True).T e = np.zeros_like(s) e[1] = 1 return np.linalg.solve(V, e) def _fdcoeffs2(s): # pragma: no cover r"""Stable solve for finite difference coefficients (LeVeque). Parameters ---------- s : (p,) ndarray Finite difference stencil. For example, ``[-2, -1, 0, 1]`` means get coefficients :math:`c_{-2},c_{-1},c_{0},c_{1}` such that :math:`f'(x) \approx c_{-2} f(x - 2) + c_{-1} f(x - 1) + c_{0} f(x) + c_{1} f(x + 1)`. Returns ------- (p,) ndarray Finite difference coefficients (the :math:`c_{j}`'s). """ pass