Skip to content

Commit 10836f7

Browse files
Merge pull request #323 from SciML/fm/ngrc
feat: add next generation reservoir computing
2 parents cacdf74 + 238c84b commit 10836f7

File tree

16 files changed

+519
-33
lines changed

16 files changed

+519
-33
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "ReservoirComputing"
22
uuid = "7c2d2b1e-3dd4-11ea-355a-8f6a8116e294"
33
authors = ["Francesco Martinuzzi"]
4-
version = "0.12.3"
4+
version = "0.12.4"
55

66
[deps]
77
ArrayInterface = "4fba245c-0d91-5ea0-9b3e-6abc04ee57a9"

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ reservoir computing models. More specifically the software offers:
4242
+ Deep echo state networks `DeepESN`
4343
+ Echo state networks with delayed states `DelayESN`
4444
+ Hybrid echo state networks `HybridESN`
45+
+ Next generation reservoir computing `NGRC`
4546
- 15+ reservoir initializers and 5+ input layer initializers
4647
- 5+ reservoir states modification algorithms
4748
- Sparse matrix computation through

docs/pages.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pages = [
33
"Tutorials" => Any[
44
"Building a model from scratch" => "tutorials/scratch.md",
55
"Chaos forecasting with an ESN" => "tutorials/lorenz_basic.md",
6+
"Fitting a Next Generation Reservoir Computer" => "tutorials/ngrc.md",
67
#"Using Different Training Methods" => "esn_tutorials/different_training.md",
78
"Deep Echo State Networks" => "tutorials/deep_esn.md",
89
#"Hybrid Echo State Networks" => "tutorials/hybrid.md",

docs/src/api/models.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@
99
HybridESN
1010
```
1111

12+
## Next generation reservoir computing
13+
14+
```@docs
15+
NGRC
16+
```
17+
18+
```@docs
19+
polynomial_monomials
20+
```
21+
1222
### Utilities
1323

1424
```@docs

docs/src/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ or `dev` the package.
8282
+ Deep echo state networks [`DeepESN`](@ref)
8383
+ Echo state networks with delayed states [`DelayESN`](@ref)
8484
+ Hybrid echo state networks [`HybridESN`](@ref)
85+
+ Next generation reservoir computing [`NGRC`](@ref)
8586
- 15+ reservoir initializers and 5+ input layer initializers
8687
- 5+ reservoir states modification algorithms
8788
- Sparse matrix computation through

docs/src/refs.bib

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,3 +355,17 @@ @article{Fleddermann2025
355355
year = {2025},
356356
month = may
357357
}
358+
359+
@article{Gauthier2021,
360+
title = {Next generation reservoir computing},
361+
volume = {12},
362+
ISSN = {2041-1723},
363+
url = {http://dx.doi.org/10.1038/s41467-021-25801-2},
364+
DOI = {10.1038/s41467-021-25801-2},
365+
number = {1},
366+
journal = {Nature Communications},
367+
publisher = {Springer Science and Business Media LLC},
368+
author = {Gauthier, Daniel J. and Bollt, Erik and Griffith, Aaron and Barbosa, Wendson A. S.},
369+
year = {2021},
370+
month = sep
371+
}

docs/src/tutorials/ngrc.md

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# Next Generation Reservoir Computing
2+
3+
This tutorial shows how to use next generation reservoir computing [NGRC](@ref)
4+
in ReservoirComputing.jl to model the chaotic Lorenz system.
5+
6+
NGRC works differently compared to traditional reservoir computing. In NGRC
7+
the reservoir is replaced with:
8+
- A delay embedding of the input
9+
- A nonlinear feature map
10+
The model is finally trained through ridge regression, like a normal RC.
11+
12+
13+
In this tutorial we will :
14+
- simulate the Lorenz system,
15+
- build an NGRC model with delayed inputs and polynomial features, following the
16+
[original paper](https://doi.org/10.1038/s41467-021-25801-2),
17+
- train it on one-step increments,
18+
- roll it out generatively and compare with the true trajectory.
19+
20+
## 1. Setup and imports
21+
22+
First we need to load the necessary packages. We are going to use the following:
23+
24+
```@example ngrc
25+
using OrdinaryDiffEq
26+
using Random
27+
using ReservoirComputing
28+
using Plots
29+
using Statistics
30+
```
31+
32+
## 2. Define Lorenz system and generate data
33+
34+
We define the Lorenz system and integrate it to generate a long trajectory:
35+
36+
```@example ngrc
37+
function lorenz!(du, u, p, t)
38+
σ, ρ, β = p
39+
du[1] = σ * (u[2] - u[1])
40+
du[2] = u[1] * (ρ - u[3]) - u[2]
41+
du[3] = u[1] * u[2] - β * u[3]
42+
end
43+
44+
prob = ODEProblem(
45+
lorenz!,
46+
Float32[1.0, 0.0, 0.0],
47+
(0.0, 200.0),
48+
(10.0f0, 28.0f0, 8/3f0),
49+
)
50+
51+
data = Array(solve(prob, ABM54(); dt = 0.025)) # size: (3, T)
52+
```
53+
54+
We then split the time series into training and testing segments:
55+
56+
```@example ngrc
57+
shift = 300
58+
train_len = 500
59+
predict_len = 900
60+
61+
input_data = data[:, shift:(shift + train_len - 1)]
62+
target_data = data[:, (shift + 1):(shift + train_len)]
63+
test_data = data[:, (shift + train_len):(shift + train_len + predict_len - 1)]
64+
```
65+
66+
## 3. Normalization
67+
68+
It is good practice to normalize the data, especially for polynomial features:
69+
70+
```@example ngrc
71+
in_mean = mean(input_data; dims = 2)
72+
in_std = std(input_data; dims = 2)
73+
74+
train_norm_x = (input_data .- in_mean) ./ in_std
75+
train_norm_y = (target_data .- in_mean) ./ in_std
76+
test_norm_x = (test_data .- in_mean) ./ in_std
77+
78+
# We train an increment (residual) model: Δy = y_{t+1} − y_t
79+
train_delta_y = train_norm_y .- train_norm_x
80+
```
81+
82+
## 4. Build the NGRC model
83+
84+
Now that we have the data we can start building the model.
85+
Following the approach of the paper we first define two feature functions:
86+
87+
- a constant feature
88+
- a second order polynomial monomial
89+
90+
```@example ngrc
91+
const_feature = x -> Float32[1.0]
92+
poly_feature = x -> polynomial_monomials(x; degrees = 1:2)
93+
```
94+
95+
Finally, we can construct the NGRC model.
96+
97+
We set the following:
98+
99+
```@example ngrc
100+
in_dims = 3
101+
out_dims = 3
102+
num_delays = 1
103+
```
104+
105+
With `in_dims=3` and `num_delays=1` the delayed input length is 6.
106+
Adding the polinomial of degrees 1 and 2 will put give us 21 more. Finally, the constant
107+
term adds 1 more feature. In total we have 28 features.
108+
109+
We can pass the number of features to `ro_dims` to initialize the [`LinearReadout`](@ref)
110+
with the correct dimensions. However, unless one is planning to fry run the model without training,
111+
the [`train`](@ref) function will take care to adjust the dimensions.
112+
113+
Now we build the NGRC:
114+
115+
```@example ngrc
116+
rng = MersenneTwister(0)
117+
118+
ngrc = NGRC(in_dims, out_dims; num_delays = num_delays, stride = 1, features = (const_feature, poly_feature),
119+
include_input = false, # we already encode everything in the features
120+
ro_dims = 28,
121+
readout_activation = identity)
122+
123+
ps, st = setup(rng, ngrc)
124+
```
125+
126+
At this point, `ngrc` is a fully specified model with:
127+
- a [`DelayLayer`](@ref) that builds a 6-dimensional delayed vector from the 3D input,
128+
- a [`NonlinearFeaturesLayer`](@ref) that maps that vector to 28 polynomial features,
129+
- a [`LinearReadout`](@ref) (28 => 3).
130+
131+
## 5. Training the NGRC readout
132+
133+
We now train the linear readout using ridge regression on the increment `train_delta_y`:
134+
135+
```@example ngrc
136+
ps, st = train!(ngrc, train_norm_x, train_delta_y, ps, st;
137+
train_method = StandardRidge(2.5e-6))
138+
```
139+
140+
where [`StandardRidge`](@ref) is the ridge regression provided natively by ReservoirComputing.jl.
141+
142+
## 6. Generative prediction
143+
144+
We now perform generative prediction on the increments to obtain the predicted time series:
145+
146+
```@example ngrc
147+
single_step = copy(test_norm_x[:, 1]) # normalized initial condition
148+
traj_norm = similar(test_norm_x, 3, predict_len)
149+
150+
for step in 1:predict_len
151+
global st
152+
delta_step, st = ngrc(single_step, ps, st)
153+
single_step .= single_step .+ delta_step # increment update in normalized space
154+
traj_norm[:, step] .= single_step
155+
end
156+
```
157+
158+
Finally, we unscale back to the original coordinates:
159+
160+
```@example ngrc
161+
traj = traj_norm .* in_std .+ in_mean # size: (3, predict_len)
162+
```
163+
164+
## 7. Visualization
165+
166+
We can now compare the predicted trajectory with the true Lorenz data on the test segment:
167+
168+
```@example ngrc
169+
plot(transpose(test_data)[:, 1], transpose(test_data)[:, 2], transpose(test_data)[:, 3]; label="actual");
170+
plot!(transpose(traj)[:, 1], transpose(traj)[:, 2], transpose(traj)[:, 3]; label="predicted")
171+
```

src/ReservoirComputing.jl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ include("models/esn.jl")
4040
include("models/esn_deep.jl")
4141
include("models/esn_delay.jl")
4242
include("models/esn_hybrid.jl")
43+
include("models/ngrc.jl")
4344
#extensions
4445
include("extensions/reca.jl")
4546

@@ -57,9 +58,10 @@ export block_diagonal, chaotic_init, cycle_jumps, delay_line, delayline_backward
5758
selfloop_forwardconnection, simple_cycle, true_doublecycle
5859
export add_jumps!, backward_connection!, delay_line!, reverse_simple_cycle!,
5960
scale_radius!, self_loop!, simple_cycle!
60-
export train, train!, predict, resetcarry!
61+
export train, train!, predict, resetcarry!, polynomial_monomials
6162
export ESN, DeepESN, DelayESN, HybridESN
62-
#reca
63+
export NGRC
64+
#ext
6365
export RECACell, RECA
6466
export RandomMapping, RandomMaps
6567

0 commit comments

Comments
 (0)