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 :class:`~prophys.distributions.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 -------------------------- :class:`~prophys.Autoregressive` is the core building block: .. math:: x_t = \mathrm{mean} + \phi\,(x_{t-1} - \mathrm{mean}) + \varepsilon_t, \qquad \varepsilon_t \sim \text{innovation}. It defines *recursion structure only* — no probability formula of its own. The per-step density is exactly the innovation :class:`~prophys.distributions.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: .. code-block:: python 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) :class:`~prophys.RandomWalk` is the ``phi = 1`` boundary (pure accumulation of innovations), and :func:`~prophys.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 :class:`~prophys.Geometry` captures exactly those two operations. :class:`~prophys.LinearGeometry` (the default) is the real line; :class:`~prophys.Circular` is the circle of a given period. Passing a geometry is all it takes to make the recursion correct for angular state — :func:`~prophys.circular_autoregressive` is the convenience factory: .. code-block:: python # 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. :func:`~prophys.time_diff` takes an optional ``geometry``, so a directional first difference from 350° to 010° reads as ``+20``, not ``-340``: .. code-block:: python from prophys.processes import Circular shift = prp.time_diff(direction_path, geometry=Circular(360.0)) Coupling processes ------------------- :class:`~prophys.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: .. code-block:: python 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 :doc:`examples/runway_crosswind` 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 — :class:`~prophys.Recurrence` scans an arbitrary differentiable step function over driver paths, carrying state between steps, still fully reparameterized and differentiable.