Processes
A process turns a distribution into a time series: a value that evolves
over n_steps with serial structure, so that path-dependent quantities
built downstream — running totals, drawdowns, worst-of-day maxima,
threshold excursions — have non-trivial distributions that independent
draws could never produce. Every process is itself a
Distribution over whole paths: its
log_prob scores an observed (batch, n_steps) path (so it calibrates
from data like any distribution), and its sample draws paths by
reparameterization (so gradients flow to every parameter).
Autoregressive structure
Autoregressive is the core building block:
It defines recursion structure only — no probability formula of its own.
The per-step density is exactly the innovation
Distribution’s log_prob at the affine
residual, and sampling is the innovation’s quantile applied to per-step
uniforms. Swap the innovation family and the process becomes heavy-tailed,
skewed, or bimodal with no other change:
import prophys as prp
# Gaussian innovations: an Ornstein–Uhlenbeck-like mean-reverting series.
price = prp.Autoregressive(init=50.0, mean=50.0, phi=0.9,
innovation=prp.Gaussian(0.0, 3.0), n_steps=24)
# A two-regime (bimodal) step distribution — usable as an innovation
# precisely because a Mixture's quantile is derived from its CDF.
regime = prp.Mixture([prp.Gaussian(-4.0, 1.0), prp.Gaussian(4.0, 1.0)],
logits=[0.0, 0.0])
jumpy = prp.Autoregressive(0.0, 0.0, 0.7, innovation=regime, n_steps=24)
RandomWalk is the phi = 1 boundary (pure accumulation
of innovations), and ornstein_uhlenbeck() is a factory that
picks the phi and innovation scale giving the exact discretization of a
continuous-time OU process on a regular grid.
Circular state: the geometry of a value space
The autoregressive recursion touches its state’s geometry in exactly two
places: it differences the state from a reference (to scale by phi),
and it places an updated value back into the state space. On the real
line both are ordinary arithmetic; on a circle — a wind or wave
direction, a compass heading, a phase, a time-of-day — the difference must
be the shortest signed arc and the placement must wrap into range.
A Geometry captures exactly those two operations.
LinearGeometry (the default) is the real line;
Circular is the circle of a given period. Passing a
geometry is all it takes to make the recursion correct for angular state —
circular_autoregressive() is the convenience factory:
# Wind direction in degrees, reverting to a prevailing 250°, with a
# bimodal backing/veering step. Reversion follows the short arc across
# the 0/360 seam, and each state stays in [0, 360).
direction = prp.circular_autoregressive(
init=250.0, mean=250.0, phi=0.85,
innovation=prp.Mixture([prp.Gaussian(-6.0, 4.0), prp.Gaussian(6.0, 4.0)],
logits=[0.0, 0.0]),
n_steps=24, period=360.0,
)
Without the circular geometry a linear recursion near the seam reverts the long way around (a state at 359° pulled toward a mean of 1° would swing almost all the way round the circle) and reports spurious ramps. The innovation itself always lives in the Euclidean difference space, so any real-valued innovation distribution composes with either geometry.
The same geometry makes step-differencing seam-safe.
time_diff() takes an optional geometry, so a directional
first difference from 350° to 010° reads as +20, not -340:
from prophys.processes import Circular
shift = prp.time_diff(direction_path, geometry=Circular(360.0))
Coupling processes
JointProcess runs several autoregressive processes with
instantaneously coupled innovations — a Gaussian copula on the per-step
innovation residuals — so their dependence survives Monte-Carlo
marginalization. A wind model, for instance, couples a mean-reverting
speed process with a circular direction process so that gusty hours and
shifting hours co-occur:
speed = prp.ornstein_uhlenbeck(init=8.0, mean=8.0, theta=0.5, sigma=2.5, n_steps=24)
wind = prp.JointProcess([speed, direction], corr_raw=[0.35])
Because the coupling acts on the (Euclidean) innovations, a circular direction process joins a linear speed process with no special handling. See Example: Runway Crosswind & Wind-Shift Operability for this exact model driving an operability decision.
General recurrences
When the step map is richer than an affine update — a battery state of
charge, an inventory, a wear accumulator —
Recurrence scans an arbitrary differentiable step
function over driver paths, carrying state between steps, still fully
reparameterized and differentiable.