Phase two: the common model workflow¶
Status: Done; merged through PR #460. See release verification in the roadmap. This page records current decisions and implementation boundaries. Start with the runnable common model guide or the shorter end-to-end guide.
Agreed behavior¶
Basic ESNs and custom graphs share the same fit, forecast and state workflow.
Unified tune and complete-artifact save commands remain phases three and four.
Layers connect through the existing functional
construction API: hidden = reservoir(input) and output = readout(hidden).
The common model is a PyTorch nn.Module:
model(x)applies scaling learned from training data and returns predictions in the original units.model.graph(x)exposes the raw underlying graph for pipelines that manage their own scaling.- Calls carry reservoir state by default. An explicit reset starts an unrelated sequence. Evaluation mode controls noise and other training behavior without implicitly resetting state.
- One output returns a tensor. Multiple outputs return a dictionary using the names declared when constructing the model.
- Ordinary forward calls retain PyTorch gradients and module hooks. Freezing reservoir weights still permits gradients to upstream modules.
- Scaling statistics are registered buffers, so device/dtype conversions and checkpoints include them. Each input and target has its own preprocessing where their units or dimensions differ.
These decisions apply directly to the new major API. The maintainer chose no legacy wrapper or compatibility layer. Raw graph classes and trainers remain useful public building blocks for advanced workflows.
Approved construction¶
The maintainer approved rd.Model(inputs={...}, outputs={...}). It wraps the
existing symbolic graph in a common module with one registered graph child,
reusing the current graph construction and execution infrastructure.
The following illustrates the implemented interface:
x = rd.reservoir_input(3)
h = reservoir(x)
y = readout(h)
model = rd.Model(inputs={"state": x}, outputs={"prediction": y})
model.fit(train)
prediction = model.forecast(history=observed, horizon=500)
The graph= form wraps an existing factory-built graph. The PyTorch ESN
preset constructs one common workflow without requiring users to wire layers.
Use input_size= or build() before attaching optimizers; an untouched lazy
preset can instead infer width, dtype and device on its first fit.
Each graph is registered once, preserving parameter identities and avoiding duplicate checkpoint entries. See the ESN API changes.
Current workflow¶
rd.Model accepts named symbolic inputs/outputs or an existing graph through
graph=. It fits declared readout heads, returns original-unit forward and
forecast predictions, and supports named targets and explicit recurrent state.
Its state dictionary includes named-port/feedback metadata, fitted scaling and
observed history. Loading validates their consistency before using them.
Declare every algebraically fitted readout as a named output and supply its target. An intermediate head can also feed a downstream reservoir; staged training fits it before collecting that reservoir's features. Unexposed internal fitted heads require the lower-level trainer's explicit targets.
Feedback connects named output/input ports with an explicit graph-step delay.
prediction_lead separately records each head's target distance. Default
self-feedback uses matching lead/delay, and forecasts always start at the first
unseen physical sample. Pending warmup outputs prevent gaps for multi-step
heads. Differing output/input scales are bridged through original units on the
model device. See feedback and timing.
Custom framewise modules can declare supports_framewise=True. Pure temporal
sequence operations can use execution="history": fitting and forecasting
preserve the complete prefix. Fast models retain the optimized existing
backend and compact warmup tail. Delayed rollout queues are reconstructed from
history; reservoir state alone cannot resume such a rollout. See
execution capabilities.
Existing backend families¶
Model(graph=...) now also wraps ParallelESNModel and
CoupledEnsembleESNModel. Small private adapters reuse their existing
execution paths; the raw graph remains the sole registered graph child.
Parallel ports represent full fields, and scaling precedes patch gathering.
Coupled ports represent the shared member inputs and single aggregate output;
each member's terminal readout name is resolved independently during fitting.
State, gradients, training-only scaling, checkpoint metadata and trajectory conversion use the same workflow. Parallel/coupled compiled forecasting is rejected before state changes because those raw forecast paths do not support the compile option. Their explicit-state paths keep their existing support. A reservoir hidden inside a custom coupled aggregator cannot participate in explicit-state or forecast execution; it is rejected before operations run. Ordinary stateful calls remain available.
Non-symbolic checkpoint schemas include patch geometry or ordered member interfaces, the aggregate selector/class, and persistent tensor name/shape signatures. They reject structural mismatches before copying tensors. Custom Python settings still belong to the compatible architecture supplied by the caller. Generic architecture reconstruction remains phase four.
The graph-tooling review records the evaluated standard tools. Functional construction stays in place; FX can capture a surrounding PyTorch pipeline using the documented leaf-module hook.
Trajectory integration¶
The maintainer approved automatic, configurable resampling for uneven time
axes, with uniform sampling as the normal case. A model fitted on a bare
TSDynamics trajectory now returns a trajectory by default. The optional
return_type="tensor" keeps forecasts on the model device; ordinary forward
calls retain the Torch interface.
The trajectory's time axis defines sampling. Default interpolation preserves
both endpoints and sample count; an explicit resample_dt creates an
in-support grid ending at the latest observation. Later trajectory histories
use the fitted interval and the same component names/order. Predictions begin
at the immediate unseen time; optional warmup predictions start at
history[max(prediction_leads.values()):], so every head shares the same
physical timeline. New tensor histories require tensor output because
they carry no absolute clock.
Named trajectory inputs align once on a shared grid inside their common
observed range. The first declared model input supplies the default interval;
resample_dt overrides it. Per-head trajectory targets are physical signals:
an origin at t uses that head's label at t + prediction_lead * dt. Labels
beyond the common observed endpoint are held out before interpolation.
Fitting preserves the full input prefix and uses only jointly valid supervised
rows after washout. Fast models retain a compact tail ending at the common
observed endpoint; temporal models retain the full prefix.
Clock, per-port components and primitive source metadata survive current state-dictionary checkpoints without requiring TSDynamics at load time. Live producing-system objects remain transient. One output returns a trajectory; several return a dictionary with each target's own signal names and one shared future timeline. See the trajectory guide and named trajectories.
The common workflow packages the existing four normalization methods in an
internal fitted PyTorch module. Statistics are learned only by an explicit
fit operation, remain detached from training gradients, and do not update
during inference. Transform and inverse operations preserve upstream gradients.
Tests cover constant channels, refitting, checkpoint restoration, invalid data,
and device/dtype behavior, including native MPS. The module is used internally
by every supported Model family. Named input_transforms and
output_transforms override individual ports with reversible Torch modules,
including Standardize and nn.Identity. Fitting stages independent copies
and preserves the original modules and parameters on successful commit.
See custom preprocessing.
Existing data-preparation defaults
are unchanged. Minmax scaling now divides before multiplying, avoiding an
intermediate overflow for large finite training ranges.
The workflow also guards explicit-state execution against reservoirs hidden inside containers or custom modules. Previously, such a call could change stored state and omit it from the returned state dictionary. The guard checks the entire execution plan before running any operation and provides a remedy: expose each reservoir as a separate functional graph node. Ordinary stateful graph calls and supported pointwise containers remain usable. See state management.
Approved extensions¶
The maintainer approved these choices on 22 October 2026:
- Custom preprocessing uses reversible changes of units that preserve signals. Temporal operations and transforms that add features belong inside the DAG.
- Named TSDynamics inputs align automatically to a common sampling grid within their shared observed range. Resampling never extrapolates beyond observations. Targets use physical timestamps and each head's declared prediction lead.
Both extensions are implemented. Complete integrated and hosted verification passes; see the roadmap's completion record. The new major API uses current version-2 workflow/preset checkpoint metadata. Older common-workflow checkpoints require reconstruction and refitting; no compatibility converter is provided.
The public component recipes cover stable
named seeds, registered initializers, recurrent cells, framewise transforms and
the public ReadoutLayer.solve hook. User modules remain ordinary PyTorch
components.
Tuning remains phase three. Complete model loading and diagram exports remain phase four. They will use the agreed common interface, with separate acceptance tests rather than placeholder methods.