Skip to content

Reference

Facade

A single object that owns the whole reservoir-computing workflow — build, warmup, algebraic fit, autoregressive forecast — with numpy in and numpy out. ESN is the estimator-shaped entry point for users who want fit().forecast() without wiring the pytorch_symbolic graph themselves; its .model attribute exposes the underlying ESNModel the moment you outgrow it.

import numpy as np
from resdag import ESN

series = np.cumsum(np.random.randn(2000, 3), axis=0)   # (time, features)
esn = ESN(reservoir_size=300, spectral_radius=0.9).fit(series)
prediction = esn.forecast(horizon=200)                 # numpy (200, 3)
full_model = esn.model                                 # the composable ESNModel

See Start · The mental model for how fit/forecast map onto ResDAG's warmup + ESNTrainer.fit / rollout phases.

facade

High-level, opinionated ESN facade — train + forecast in ~10 lines.

This module provides :class:ESN, a single object that owns the end-to-end reservoir-computing workflow. It hides the symbolic-graph plumbing (:func:~resdag.models.classic_esn), data slicing, and the :class:~resdag.training.ESNTrainer ↔ :meth:~resdag.core.ESNModel.forecast hand-off behind two chainable methods::

from resdag import ESN

esn = ESN(reservoir_size=400, spectral_radius=0.9).fit(series)
prediction = esn.forecast(horizon=500)

It is the friendly front door, not a replacement for the composable pytorch_symbolic path. The underlying :class:~resdag.core.ESNModel is always reachable via :attr:ESN.model, so advanced users can drop down to the full building-block API at any time without rebuilding anything.

See Also

resdag.models.classic_esn : The builder this facade wraps. resdag.training.ESNTrainer : The trainer this facade drives. resdag.core.ESNModel.forecast : The autoregressive forecaster this facade calls.

ESN

ESN(reservoir_size: int = 500, *, spectral_radius: float = 0.9, leak_rate: float = 1.0, washout: int = 100, alpha: float = 1e-06, topology: TopologySpec = None, feedback_initializer: InitializerSpec = None, activation: str = 'tanh', bias: bool = True, readout_bias: bool = True, seed: int | None = None, device: device | str | None = None, dtype: dtype | None = None, **reservoir_kwargs: Any)

Opinionated, end-to-end Echo State Network — fit().forecast().

A thin, friendly wrapper that owns the whole reservoir-computing workflow so a researcher can train on a time series and roll out an autoregressive forecast of a chaotic system in roughly ten lines, without touching the pytorch_symbolic graph, data slicing, or the trainer/forecast hand-off by hand.

Hyperparameters are stored at construction; the underlying :class:~resdag.core.ESNModel is built lazily on the first :meth:fit call (once the feature dimension is known from the data). :meth:fit slices off the first washout steps for state synchronisation, builds the one-step-ahead training target internally, and drives :class:~resdag.training.ESNTrainer. :meth:forecast resets the reservoir, re-synchronises on the tail of the training series, and runs :meth:~resdag.core.ESNModel.forecast.

The wrapper is deliberately non-hiding: the composed model is exposed as :attr:model, so the full building-block API (custom transforms, multiple readouts, :meth:~resdag.core.ESNModel.save_full, ...) remains available.

PARAMETER DESCRIPTION
reservoir_size

Number of reservoir units.

TYPE: int DEFAULT: 500

spectral_radius

Target spectral radius of the recurrent weights (memory / stability).

TYPE: float DEFAULT: 0.9

leak_rate

Leaky-integration rate (1.0 = no leak, the standard ESN).

TYPE: float DEFAULT: 1.0

washout

Number of initial steps used to synchronise the reservoir (the "warmup" window). These steps teacher-force the state and are not used as training targets. Also the length of the re-synchronisation window used at the start of :meth:forecast.

TYPE: int DEFAULT: 100

alpha

Ridge-regression (L2) regularisation strength for the readout.

TYPE: float DEFAULT: 1e-6

topology

Recurrent-weight topology. Accepts a registry name ("erdos_renyi"), a (name, params) tuple, a pre-built :class:~resdag.init.topology.TopologyInitializer, or None for the :func:~resdag.models.classic_esn default.

TYPE: TopologySpec DEFAULT: None

feedback_initializer

Initializer for the feedback (input) weights. Same three-way spec as topology.

TYPE: InitializerSpec DEFAULT: None

activation

Reservoir activation: "tanh", "relu", "sigmoid" or "identity".

TYPE: str DEFAULT: "tanh"

bias

Whether the reservoir uses a bias term.

TYPE: bool DEFAULT: True

readout_bias

Whether the readout uses a bias term.

TYPE: bool DEFAULT: True

seed

If given, seeds torch before the model is built so the random reservoir weights are reproducible.

TYPE: int DEFAULT: None

device

Device the model and data live on. If None, the device of the series passed to the first :meth:fit is adopted (CPU for numpy input), so a CUDA tensor "just works" without passing device explicitly.

TYPE: device or str DEFAULT: None

dtype

Floating dtype for the model and data. If None, a floating-point series keeps its own dtype (a float64 series is not silently downcast); an integer/bool series is promoted to torch.get_default_dtype().

TYPE: dtype DEFAULT: None

**reservoir_kwargs

Extra keyword arguments forwarded to :class:~resdag.layers.ESNLayer via :func:~resdag.models.classic_esn.

TYPE: Any DEFAULT: {}

ATTRIBUTE DESCRIPTION
model

The composed model, available after :meth:fit. None beforehand.

TYPE: ESNModel or None

feature_size

Number of features D inferred from the training series.

TYPE: int or None

readout_name

Name of the internal readout layer (matches the trainer's targets key).

TYPE: str

Examples:

Train and forecast a chaotic system in ~10 lines:

>>> import numpy as np
>>> from resdag import ESN
>>> rng = np.random.default_rng(0)
>>> series = np.cumsum(rng.standard_normal((2000, 3)), axis=0)  # (T, D)
>>> esn = ESN(reservoir_size=300, spectral_radius=0.9, washout=100)
>>> prediction = esn.fit(series).forecast(horizon=200)
>>> prediction.shape  # numpy in -> numpy out, shape (horizon, D)
(200, 3)

Drop down to the composed model for advanced use:

>>> esn.model.summary()
>>> states = esn.model.get_reservoir_states()
See Also

resdag.models.classic_esn : Lower-level builder this facade composes. resdag.training.ESNTrainer : Trainer this facade drives. resdag.core.ESNModel.forecast : Autoregressive forecaster this facade calls.

Notes
  • Stateful reset. :meth:forecast always resets the reservoir and re-synchronises on the training tail, so back-to-back forecasts are independent and reproducible — no manual reset_reservoirs() needed.
  • Readout name. The internal readout is named consistently and the trainer is fed a matching {name: target} dict, so the "readout name must match the targets key" footgun is handled for you.
  • Slot-0 forecast. The first output dimension equals the feedback dimension by construction, so :meth:forecast is genuinely autoregressive from its first emitted step.
Source code in src/resdag/facade.py
def __init__(
    self,
    reservoir_size: int = 500,
    *,
    spectral_radius: float = 0.9,
    leak_rate: float = 1.0,
    washout: int = 100,
    alpha: float = 1e-6,
    topology: TopologySpec = None,
    feedback_initializer: InitializerSpec = None,
    activation: str = "tanh",
    bias: bool = True,
    readout_bias: bool = True,
    seed: int | None = None,
    device: torch.device | str | None = None,
    dtype: torch.dtype | None = None,
    **reservoir_kwargs: Any,
) -> None:
    if reservoir_size < 1:
        raise ValueError(f"reservoir_size must be >= 1, got {reservoir_size}")
    if washout < 1:
        raise ValueError(f"washout must be >= 1, got {washout}")

    self.reservoir_size = reservoir_size
    self.spectral_radius = spectral_radius
    self.leak_rate = leak_rate
    self.washout = washout
    self.alpha = alpha
    self.topology = topology
    self.feedback_initializer = feedback_initializer
    self.activation = activation
    self.bias = bias
    self.readout_bias = readout_bias
    self.seed = seed
    self.device = torch.device(device) if device is not None else None
    self.dtype = dtype
    self.reservoir_kwargs = reservoir_kwargs

    # Fixed internal readout name so the trainer's targets key always
    # matches — the caller never has to reason about it.
    self.readout_name = "output"

    # Built lazily on first fit(), once the feature dimension is known.
    self.model: ESNModel | None = None
    self.feature_size: int | None = None

    # Remembered across fit -> forecast: the synchronisation tail and the
    # original input type, so forecast() can re-sync the state and return
    # the same array type the caller fitted with.
    self._sync_tail: torch.Tensor | None = None
    self._returns_numpy: bool = False

fit

fit(series: Any) -> 'ESN'

Build the model (if needed) and fit the readout on series.

The series is split into a washout synchronisation prefix and a training window; the one-step-ahead target is built internally (x[t] -> x[t + 1]) and :class:~resdag.training.ESNTrainer fits the readout in a single forward pass.

PARAMETER DESCRIPTION
series

Time series of shape (time, features) or (batch, time, features). A 2-D array is treated as a single batch. Must contain at least washout + 2 timesteps (one washout window plus one input/target pair). numpy input is converted to torch and the original type is remembered so :meth:forecast returns numpy as well.

TYPE: Tensor or ndarray

RETURNS DESCRIPTION
ESN

self, to allow ESN(...).fit(series).forecast(...) chaining.

RAISES DESCRIPTION
ValueError

If series is not 2-D/3-D, or is too short for the configured washout, or its feature dimension changes between successive :meth:fit calls.

Examples:

>>> esn = ESN(reservoir_size=200).fit(series)
Source code in src/resdag/facade.py
def fit(self, series: Any) -> "ESN":
    """Build the model (if needed) and fit the readout on ``series``.

    The series is split into a ``washout`` synchronisation prefix and a
    training window; the one-step-ahead target is built internally
    (``x[t] -> x[t + 1]``) and :class:`~resdag.training.ESNTrainer` fits the
    readout in a single forward pass.

    Parameters
    ----------
    series : torch.Tensor or numpy.ndarray
        Time series of shape ``(time, features)`` or
        ``(batch, time, features)``.  A 2-D array is treated as a single
        batch.  Must contain at least ``washout + 2`` timesteps (one
        washout window plus one input/target pair).  numpy input is
        converted to torch and the original type is remembered so
        :meth:`forecast` returns numpy as well.

    Returns
    -------
    ESN
        ``self``, to allow ``ESN(...).fit(series).forecast(...)`` chaining.

    Raises
    ------
    ValueError
        If ``series`` is not 2-D/3-D, or is too short for the configured
        ``washout``, or its feature dimension changes between successive
        :meth:`fit` calls.

    Examples
    --------
    >>> esn = ESN(reservoir_size=200).fit(series)   # doctest: +SKIP
    """
    data, returns_numpy = self._to_3d_tensor(series)
    self._returns_numpy = returns_numpy

    batch, timesteps, feature_size = data.shape
    # Need: washout warmup + at least one (input, shifted-target) pair.
    if timesteps < self.washout + 2:
        raise ValueError(
            f"series has {timesteps} timesteps but at least washout + 2 = "
            f"{self.washout + 2} are required (a {self.washout}-step washout "
            f"plus one input/target step). Lower `washout` or pass a longer series."
        )

    # Lazily build the model on the first fit; reuse it on refits as long as
    # the feature dimension is unchanged (refitting overwrites the readout).
    if self.model is None:
        self.feature_size = feature_size
        self.model = self._build_model(feature_size)
    elif feature_size != self.feature_size:
        raise ValueError(
            f"This ESN was fit on {self.feature_size}-feature data; got "
            f"{feature_size}-feature data. Create a new ESN for a different "
            f"feature dimension."
        )

    # Split into warmup + autoregressive (input, one-step-ahead target).
    #   warmup : [0,             washout)
    #   train  : [washout,       T - 1)   -> predicts...
    #   target : [washout + 1,   T)
    warmup = data[:, : self.washout, :]
    train = data[:, self.washout : timesteps - 1, :]
    target = data[:, self.washout + 1 :, :]

    trainer = ESNTrainer(self.model)
    trainer.fit(
        warmup_inputs=(warmup,),
        train_inputs=(train,),
        targets={self.readout_name: target},
    )

    # Remember the tail of the *training* series so forecast() can
    # re-synchronise the reservoir from a known, teacher-forced window.
    self._sync_tail = data[:, timesteps - self.washout :, :].clone()
    return self

forecast

forecast(horizon: int, *, return_warmup: bool = False) -> Any

Roll out an autoregressive forecast continuing the fitted series.

Resets the reservoir, re-synchronises it on the final washout steps of the training series, then autoregressively generates horizon steps via :meth:~resdag.core.ESNModel.forecast. Because the state is always reset first, successive calls are independent.

PARAMETER DESCRIPTION
horizon

Number of future steps to generate. Must be >= 1.

TYPE: int

return_warmup

If True, prepend the (teacher-forced) re-synchronisation outputs to the result, giving (washout + horizon, features) per batch.

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
Tensor or ndarray

Forecast of shape (horizon, features) for a single-batch fit, or (batch, horizon, features) when fit on batched data. The array type matches what was passed to :meth:fit (numpy in → numpy out).

RAISES DESCRIPTION
RuntimeError

If called before :meth:fit.

ValueError

If horizon < 1.

Examples:

>>> prediction = esn.fit(series).forecast(horizon=500)
Source code in src/resdag/facade.py
def forecast(
    self,
    horizon: int,
    *,
    return_warmup: bool = False,
) -> Any:
    """Roll out an autoregressive forecast continuing the fitted series.

    Resets the reservoir, re-synchronises it on the final ``washout`` steps
    of the training series, then autoregressively generates ``horizon``
    steps via :meth:`~resdag.core.ESNModel.forecast`.  Because the state is
    always reset first, successive calls are independent.

    Parameters
    ----------
    horizon : int
        Number of future steps to generate. Must be ``>= 1``.
    return_warmup : bool, default=False
        If ``True``, prepend the (teacher-forced) re-synchronisation outputs
        to the result, giving ``(washout + horizon, features)`` per batch.

    Returns
    -------
    torch.Tensor or numpy.ndarray
        Forecast of shape ``(horizon, features)`` for a single-batch fit, or
        ``(batch, horizon, features)`` when fit on batched data.  The array
        type matches what was passed to :meth:`fit` (numpy in → numpy out).

    Raises
    ------
    RuntimeError
        If called before :meth:`fit`.
    ValueError
        If ``horizon < 1``.

    Examples
    --------
    >>> prediction = esn.fit(series).forecast(horizon=500)   # doctest: +SKIP
    """
    if self.model is None or self._sync_tail is None:
        raise RuntimeError("forecast() called before fit(). Call fit(series) first.")
    if horizon < 1:
        raise ValueError(f"horizon must be a positive integer, got {horizon}")

    # forecast() resets the reservoir internally (reset=True), so two
    # successive forecasts are independent — no manual reset needed.
    prediction = self.model.forecast(
        self._sync_tail,
        horizon=horizon,
        return_warmup=return_warmup,
    )
    # classic_esn is single-output, so forecast returns a single tensor.
    assert isinstance(prediction, torch.Tensor)
    return self._from_tensor(prediction)