Skip to content

Reference

Utilities

Output grid

The single shared builder of the uniform, endpoint-inclusive output time grid ([t0, t0+dt, …, tf]) that every integrator samples its results onto.

make_output_grid

make_output_grid(
    t0: float, tf: float, dt: float
) -> ndarray

Build a uniform output grid from t0 to tf (inclusive).

The grid is arange(t0, tf, dt) with tf appended when the last sample would otherwise fall short of it — so the final time is always sampled exactly, regardless of whether dt divides tf - t0.

The endpoint tolerance

tf is appended when t_arr is empty or its last sample sits below tf - 1e-12. The small absolute slack matters: when dt divides the window cleanly, the last arange sample lands on tf only up to floating-point error — typically a fraction of a ULP below it. Comparing against the bare tf would then see t_arr[-1] < tf and append a second, sub-ULP-spaced "endpoint", giving a spurious final segment of width ~1e-16. The 1e-12 slack treats any sample already within that band of tf as being the endpoint, so a cleanly-dividing window yields no duplicate tail. It is an absolute tolerance (not relative) by deliberate design: the integration windows here are O(1)–O(1e3) in time units and dt is rarely below ~1e-6, so 1e-12 is far smaller than any meaningful step yet comfortably larger than arange round-off — a relative tolerance would buy nothing and complicate the contract. (A pathological window with dt itself near 1e-12 is already rejected upstream as physically meaningless.)

This is the one chokepoint every flow family (ODE / DDE / SDE) and the engine run layer build their grid through, so it is also where the two silent-footgun horizons are caught early with a domain message: a non-positive dt (which used to surface as a bare ZeroDivisionError from this helper) and a window that does not run forward in time (which used to yield a one-sample garbage trajectory).

PARAMETER DESCRIPTION
t0

Start and end of the window.

TYPE: float

tf

Start and end of the window.

TYPE: float

dt

Output sampling interval. Must be strictly positive.

TYPE: float

RETURNS DESCRIPTION
ndarray

The output times, with t_arr[0] == t0 and t_arr[-1] == tf.

RAISES DESCRIPTION
InvalidParameterError

If dt is not strictly positive, or if tf is not strictly after t0 (an empty / backwards window). Both subclass :class:ValueError, so except ValueError still catches them.

Examples:

>>> make_output_grid(0.0, 1.0, 0.5)
array([0. , 0.5, 1. ])
Source code in src/tsdynamics/utils/grids.py
def make_output_grid(t0: float, tf: float, dt: float) -> np.ndarray:
    """Build a uniform output grid from ``t0`` to ``tf`` (inclusive).

    The grid is ``arange(t0, tf, dt)`` with ``tf`` appended when the last
    sample would otherwise fall short of it — so the final time is always
    sampled exactly, regardless of whether ``dt`` divides ``tf - t0``.

    The endpoint tolerance
    ----------------------
    ``tf`` is appended when ``t_arr`` is empty *or* its last sample sits below
    ``tf - 1e-12``.  The small absolute slack matters: when ``dt`` divides the
    window cleanly, the last ``arange`` sample lands on ``tf`` only up to
    floating-point error — typically a fraction of a ULP *below* it.  Comparing
    against the bare ``tf`` would then see ``t_arr[-1] < tf`` and append a second,
    sub-ULP-spaced "endpoint", giving a spurious final segment of width ~1e-16.
    The ``1e-12`` slack treats any sample already within that band of ``tf`` as
    *being* the endpoint, so a cleanly-dividing window yields no duplicate tail.
    It is an **absolute** tolerance (not relative) by deliberate design: the
    integration windows here are O(1)–O(1e3) in time units and ``dt`` is rarely
    below ~1e-6, so 1e-12 is far smaller than any meaningful step yet comfortably
    larger than ``arange`` round-off — a relative tolerance would buy nothing and
    complicate the contract.  (A pathological window with ``dt`` itself near
    1e-12 is already rejected upstream as physically meaningless.)

    This is the one chokepoint every flow family (ODE / DDE / SDE) and the
    engine run layer build their grid through, so it is also where the two
    silent-footgun horizons are caught early with a domain message: a
    non-positive ``dt`` (which used to surface as a bare ``ZeroDivisionError``
    from this helper) and a window that does not run forward in time (which used
    to yield a one-sample garbage trajectory).

    Parameters
    ----------
    t0, tf : float
        Start and end of the window.
    dt : float
        Output sampling interval.  Must be strictly positive.

    Returns
    -------
    ndarray
        The output times, with ``t_arr[0] == t0`` and ``t_arr[-1] == tf``.

    Raises
    ------
    tsdynamics.errors.InvalidParameterError
        If ``dt`` is not strictly positive, or if ``tf`` is not strictly after
        ``t0`` (an empty / backwards window).  Both subclass :class:`ValueError`,
        so ``except ValueError`` still catches them.

    Examples
    --------
    >>> make_output_grid(0.0, 1.0, 0.5)
    array([0. , 0.5, 1. ])
    """
    from tsdynamics.errors import invalid_value

    if not dt > 0:
        raise invalid_value("dt", dt, rule="must be > 0 (the output sampling interval)")
    if not tf > t0:
        raise invalid_value(
            "final_time",
            tf,
            rule=f"must run forward in time (be > the start time {t0!r})",
            hint="check the sign and that final_time exceeds t0",
        )
    t_arr = np.arange(t0, tf, dt)
    if t_arr.size == 0 or t_arr[-1] < tf - 1e-12:
        t_arr = np.append(t_arr, tf)
    return t_arr