Skip to content

Time-indexed parameters

A parameter subscripted with the configured time axis is supplied as a table. For example, rate[time] takes one value per declared time coordinate, while rate[group,time] takes one table per group. The compiler removes the time dimension at each evaluation and passes the resulting scalar or shaped value to the equations and reaction propensities. The time axis can occupy any position in the parameter array; its coordinate order defines the table order.

The top-level time_interpolation field chooses one policy for all time-indexed parameters in the specification:

Policy Value between adjacent coordinates
linear Linear interpolation; the default when the field is omitted
previous Hold the value at t_i on [t_i, t_{i+1})

Both policies clamp to the nearest endpoint outside the coordinate range. previous is right-continuous: evaluating exactly at a coordinate returns the new table value. For coordinates [0, 1, 2] and values [0, 3, 1], evaluations at 0.5, 1, and 1.5 return [0, 3, 3] with previous and [1.5, 3, 2] with linear.

import numpy as np

from op_system import compile_spec

spec = {
    "kind": "transitions",
    "time_interpolation": "previous",
    "axes": [
        {"name": "group", "coords": ["a"]},
        {"name": "time", "type": "continuous", "coords": [0.0, 1.0, 2.0]},
    ],
    "state": ["A[group]", "B[group]"],
    "transitions": [
        {
            "name": "transfer",
            "from": "A[group]",
            "to": "B[group]",
            "rate": "rate[time]",
            "reactants": [{"state": "A[group]", "order": 1}],
        }
    ],
}
compiled = compile_spec(spec)
table = np.asarray([0.0, 3.0, 1.0])
drift = compiled.eval_fn(1.5, np.asarray([10.0, 0.0]), rate=table)
np.testing.assert_array_equal(drift, [-30.0, 30.0])
assert compiled.meta["forcing_breakpoints"] == (1.0, 2.0)

Use time_axis: day with a declared day axis and subscripts such as rate[day] to rename the time axis. Active time coordinates must be finite, numeric, and strictly increasing. A continuous time axis may explicitly declare a single coordinate; its table is constant for every evaluation time. Other continuous axes retain their requirement for at least two coordinates. Unknown policy values are rejected during normalization.

Flat, PyTree, block, and reaction evaluators use the same policy. They adopt tables into the evolving state's namespace at call time, including JAX under jit and vmap. Runtime table values are not baked into compiled functions.

Metadata for numerical engines

Normalized and compiled metadata expose time_axis, time_interpolation, time_coordinates, and forcing_breakpoints. Coordinates and breakpoints are immutable tuples. The flepimop2 system provider exposes the same four values through system.option(...).

For an active previous table, forcing breakpoints are all coordinates after the first. The first coordinate introduces no change because the first table value also applies before it. A single-coordinate table has no breakpoints. Linear interpolation and specifications without active time-indexed parameters publish an empty breakpoint tuple.

An engine can pass the published forcing boundaries to its breakpoint-aware stochastic solver independently of the observation grid. In op_engine, pure stochastic direct SSA, fixed tau-leaping, and adaptive tau-leaping accept forcing_breakpoints. The flepimop2-op_engine 0.7.0 provider automatically merges the producer schedule with explicitly supplied forcing boundaries. Other consumers must wire these metadata into their solver configuration. The interpolation setting changes table evaluation only. A rate that also uses t directly can still vary between table coordinates. Exact breakpoint SSA and adaptive exact fallback require all external time dependence to be constant between declared changes while the state is unchanged.

Linear tables continue to produce smoothly varying rates. Listing their coordinates as breakpoints does not make frozen-rate SSA exact; those rates require a suitable time-dependent waiting-time method.