ResDAG
Reservoir computing for PyTorch.
Compose reservoir models as DAGs, train readouts with a single algebraic solve, run it all on the GPU.
ResDAG treats reservoir models — echo state networks and beyond — as ordinary PyTorch layers. Reservoirs, readouts, and transforms are nn.Modules wired together with a functional API; training a readout is one teacher-forced pass and one algebraic ridge solve, with no epochs. Models move with .to(device), serialize with state_dict(), and embed in larger PyTorch pipelines, optimizers included.
Installation
pip install resdag # core
pip install "resdag[hpo]" # + Optuna hyperparameter optimization
Python ≥ 3.11, PyTorch ≥ 2.10.
Docs track
main. For unreleased changes, install from source:pip install "git+https://github.com/El3ssar/ResDAG". See the release history for packaged versions.
Try it
A reservoir forecaster on a toy signal, end to end:
import torch
import resdag as rd
t = torch.linspace(0, 60, 3000)
data = torch.sin(t).reshape(1, -1, 1) # (batch, time, features)
warmup, train, target, f_warmup, val = rd.data.prepare_esn_data(
data, warmup_steps=100, train_steps=2000, val_steps=300)
model = rd.models.classic_esn(reservoir_size=300, feedback_size=1, output_size=1)
rd.ESNTrainer(model).fit((warmup,), (train,), targets={"output": target})
prediction = model.forecast(f_warmup, horizon=300) # autoregressive, (1, 300, 1)
The first forecast walkthrough does the same on the Lorenz attractor, with the math explained.
The end-to-end guide covers data preparation, fitting, evaluation, saving and reuse inside a larger PyTorch model. Run its small complete example with uv run python examples/15_end_to_end.py.
Choose and customize the memory
Use leaky ESNs, oscillators, Legendre delay memory, spiking reservoirs, or
wrapped PyTorch GRU/LSTM cells through the same composition API. Select recurrent
weights with recurrent_initializer=; names, parameter tuples, callables and
custom registration all work. Biases, reset states and gated recurrence have
separate hooks. Optional intrinsic plasticity and flow control calibrate a
standard ESN on training inputs before fitting its readout.
Start with recurrent weights, custom registration, the layer catalog, or calibration.
Compose, don't configure
Architectures are DAGs you wire, not options you toggle. Two reservoirs on different timescales, read out together:
from resdag import CGReadoutLayer, Concatenate, ESNModel, reservoir_input
from resdag.layers import ESNLayer
inp = reservoir_input(3)
fast = ESNLayer(64, feedback_size=3, leak_rate=1.0, spectral_radius=0.9)(inp)
slow = ESNLayer(64, feedback_size=3, leak_rate=0.2, spectral_radius=0.9)(inp)
merged = Concatenate()(fast, slow)
model = ESNModel(inp, CGReadoutLayer(128, 3, name="output")(merged))
Branches, feature augmentations, and multiple readout heads compose the same way — one reservoir, squared-state augmentation, two heads:
All heads fit in a single pass, in dependency order. The composition handbook covers the patterns.
Coming from scikit-learn?
The whole fit → predict loop fits in one object. ESN.fit(series) slices the warmup window, builds the one-step-ahead target, and runs the algebraic solve; forecast(horizon=...) re-synchronizes and rolls out — numpy in, numpy out:
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) # (200, 3)
esn.model drops you back into the full composable graph whenever you outgrow the facade. The mental model maps fit/predict onto ResDAG's warmup + ESNTrainer.fit / forecast flow.
Train through it with SGD
A reservoir is an ordinary PyTorch layer, so it drops into a pipeline as a frozen feature extractor and you train any head with a normal optimizer loop. The reservoir has zero trainable parameters, so the optimizer only ever touches the head:
import torch
import torch.nn as nn
from resdag import ReservoirFeatureExtractor
net = nn.Sequential(
ReservoirFeatureExtractor(500, feedback_size=3, spectral_radius=0.9, stateful=False),
nn.Linear(500, 64), nn.Tanh(), nn.Linear(64, n_classes),
)
extractor, head = net[0], net[1:]
opt = torch.optim.Adam(head.parameters(), lr=1e-3) # head only — reservoir is frozen
with torch.no_grad(): # frozen features: compute once
extractor.on_epoch_start()
feats = extractor(sequences)[:, -1] # (batch, 500) last-step summary
for _ in range(300):
loss = nn.functional.cross_entropy(head(feats), labels)
opt.zero_grad(); loss.backward(); opt.step()
This is the pure-PyTorch path: gradient heads, full BPTT through the recurrence (trainable=True), and embedding frozen reservoirs in larger networks. Work · Train covers all three training paths; Work · Scale & deploy shows the frozen-backbone classifier inside a nn.Module pipeline.
For independent minibatches, stateful=False starts from zeros on each call.
The default carries state across calls for streaming. Frozen weights still
allow gradients to flow to an upstream neural encoder. Keep the complete
trained esn.model inside a larger nn.Module when you need its readout and
all DAG branches; reset its reservoirs between independent sequences.
Premade factories accept a readout name or class, plus separate solver options:
model = rd.classic_esn(
reservoir_size=300, feedback_size=3, output_size=3,
readout="svd", readout_alpha=1e-6,
readout_kwargs={"trainable": True, "rcond": 1e-12},
)
This fits algebraically with ESNTrainer and lets an optimizer fine-tune the
same readout afterwards. Available names are ridge, cholesky, cg, svd,
pinv, incremental, rls, and fista. SVD and pseudoinverse fitting operate
directly on the design matrix without allocating a feature-by-feature Gram.
Documentation
| Start | Install, a first trained forecaster, the mental model |
| Build | Layers, readouts, architectures, topologies, initializers |
| Work | Training paths, forecasting with drivers, tuning, GPU |
| Theory | Every equation, stated against the code |
| Reference | The full public API |
Ecosystem
Built on pytorch_symbolic for graph composition. Pairs with TSDynamics, a companion library of dynamical systems — it generates the systems, ResDAG forecasts them.
Contributing
See CONTRIBUTING.md — releases are automated from conventional commits, and most component types are one registry decorator away.
License
MIT — © Daniel Estevez-Moya
Metadata
Release files for resdag 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| resdag-1.1.0.tar.gz | 5.2 MB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| resdag-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 5.8 MB
Release files / resdag-1.1.0.tar.gz
| Download URL | resdag-1.1.0.tar.gz |
|---|---|
| Size | 5.2 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b235c407c0d1c3f89079f664c733859fa3794e82ed989e6e2851539d2361ce58
|
|
BLAKE2b-256 checksum How to use checksums |
c992fb712f4fbcb0e5d1812a2923b7db34335322dfc03ec9dd7ccebb217c0948
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logRelease files / resdag-1.1.0-py3-none-any.whl
| Download URL | resdag-1.1.0-py3-none-any.whl |
|---|---|
| Size | 612.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
12a5cf88c1d0a9762e56ae188c490fb5e21f78e2c44c06269c609cc122f29a60
|
|
BLAKE2b-256 checksum How to use checksums |
20e43f774b8425175d528c99379e5ae846e1c3c3f1608308d86c152bb6f0e5ab
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logSecurity for resdag 1.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for resdag-1.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 21, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.