ORC¶
Continuous-Time Reservoir Computing¶
Standard ESNs operate in discrete time — the reservoir state updates via a map at each timestep. A continuous-time ESN (CESN) instead evolves the reservoir state as an ODE:
$$\frac{d\mathbf{r}}{dt} = \tau\left(-\mathbf{r} + \tanh\left(W_r \mathbf{r} + W_{in} \mathbf{u}(t) + \mathbf{b}\right)\right)$$
where $\tau$ is a time constant controlling how quickly the reservoir responds to input. During training, the input $\mathbf{u}(t)$ is interpolated continuously (cubic Hermite) and the ODE is solved with an adaptive solver via Diffrax.
This formulation is useful when the underlying dynamics are naturally continuous, when data is irregularly sampled, or when you want explicit control over integration accuracy.
import jax
import jax.numpy as jnp
import diffrax
import equinox as eqx
from jaxtyping import Array, Float
jax.config.update("jax_enable_x64", True)
import orc
import orc.utils.visualization as vis
Data¶
tN = 100.0
dt = 0.02
test_perc = 0.2
U, t = orc.data.lorenz63(tN=tN, dt=dt)
vis.plot_time_series(U, t)
# train test split
split_idx = int((1 - test_perc) * U.shape[0])
U_train = U[:split_idx]
U_test = U[split_idx:]
t_train = t[:split_idx]
t_test = jnp.arange(U_test.shape[0]) * dt
print(f"Train shape: {U_train.shape}, Test shape: {U_test.shape}")
Train shape: (4000, 3), Test shape: (1000, 3)
Train a Continuous ESN¶
We create a CESNForecaster with time_const=40.0. The time constant $\tau$ sets the reservoir's intrinsic timescale relative to the data — larger values produce slower, more stable dynamics. Choosing $\tau$ appropriately for your system's timescale is important for good performance.
By default, CESNForecaster uses a 5th-order Runge-Kutta solver (Tsit5) with adaptive step-size control (PIDController). This balances accuracy and computational cost automatically.
esn = orc.forecaster.CESNForecaster(data_dim=U_train.shape[1], res_dim=1000, time_const=40.0)
esn, R = orc.forecaster.train_CESNForecaster(model=esn, train_seq=U_train, t_train=t_train)
U_pred = esn.forecast(ts=t_test, res_state=R[-1])
vis.plot_time_series(
[U_test, U_pred],
t_test,
line_formats=["-", "r--"],
time_series_labels=["True", "Predicted"],
)
Adjust ODE Solver and Step-Size Controller¶
Since CESNForecaster delegates integration to Diffrax, the user can swap in any compatible solver or step-size controller. Here we switch to a fixed-step Euler method (diffrax.Euler + diffrax.ConstantStepSize).
solver = diffrax.Euler()
stepsize_controller = diffrax.ConstantStepSize()
esn = orc.forecaster.CESNForecaster(data_dim=U_train.shape[1], res_dim=400, time_const=50.0, solver=solver,stepsize_controller=stepsize_controller)
esn, R = orc.forecaster.train_CESNForecaster(model=esn, train_seq=U_train, t_train=t_train)
U_pred = esn.forecast(ts=t_test, res_state=R[-1])
vis.plot_time_series(
[U_test, U_pred],
t_test,
line_formats=["-", "r--"],
time_series_labels=["True", "Predicted"],
)
Forecast from Initial Conditions¶
When forecasting a new trajectory (not continuing from the training data), the reservoir must first be "spun up" — driven with a short segment of real data to synchronize its internal state with the system's current dynamics. Without spinup, the reservoir starts from a zero state that doesn't reflect the true system, leading to poor initial predictions.
### Some new data
tN = 20.0
dt = 0.02
eps = 50 # amount of spinup data
U, t = orc.data.lorenz63(tN=tN, dt=dt, u0 = [10,-1,10])
U_spinup = U[:eps]
U_test2 = U[eps:]
t_test2 = jnp.arange(U_test2.shape[0]) * dt
vis.plot_time_series(U_test2, t_test2)
U_pred2 = esn.forecast_from_IC(ts=t_test2, spinup_data=U_spinup)
vis.plot_time_series(
[U_test2, U_pred2],
t_test2,
line_formats=["-", "r--"],
time_series_labels=["True", "Predicted"],
)