memory-esn 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- memory_esn-0.1.0/CITATION.cff +35 -0
- memory_esn-0.1.0/LICENSE +21 -0
- memory_esn-0.1.0/MANIFEST.in +20 -0
- memory_esn-0.1.0/PKG-INFO +304 -0
- memory_esn-0.1.0/README.md +258 -0
- memory_esn-0.1.0/examples/quickstart.py +100 -0
- memory_esn-0.1.0/memory_esn/__init__.py +56 -0
- memory_esn-0.1.0/memory_esn/_speedups/__init__.py +35 -0
- memory_esn-0.1.0/memory_esn/_speedups/_fallback.py +206 -0
- memory_esn-0.1.0/memory_esn/_speedups/_loader.py +87 -0
- memory_esn-0.1.0/memory_esn/_speedups/esn_reservoir_optimized.c +17439 -0
- memory_esn-0.1.0/memory_esn/_speedups/esn_reservoir_optimized.pyx +360 -0
- memory_esn-0.1.0/memory_esn/_speedups/fractional_diff.c +33213 -0
- memory_esn-0.1.0/memory_esn/_speedups/fractional_diff.pyx +321 -0
- memory_esn-0.1.0/memory_esn/_speedups/modwt_fast.c +31621 -0
- memory_esn-0.1.0/memory_esn/_speedups/modwt_fast.pyx +159 -0
- memory_esn-0.1.0/memory_esn/base.py +306 -0
- memory_esn-0.1.0/memory_esn/dataset.py +230 -0
- memory_esn-0.1.0/memory_esn/double.py +110 -0
- memory_esn-0.1.0/memory_esn/fractional.py +217 -0
- memory_esn-0.1.0/memory_esn/multi.py +315 -0
- memory_esn-0.1.0/memory_esn/wavelet.py +390 -0
- memory_esn-0.1.0/memory_esn/weights.py +202 -0
- memory_esn-0.1.0/memory_esn.egg-info/PKG-INFO +304 -0
- memory_esn-0.1.0/memory_esn.egg-info/SOURCES.txt +29 -0
- memory_esn-0.1.0/memory_esn.egg-info/dependency_links.txt +1 -0
- memory_esn-0.1.0/memory_esn.egg-info/requires.txt +18 -0
- memory_esn-0.1.0/memory_esn.egg-info/top_level.txt +1 -0
- memory_esn-0.1.0/pyproject.toml +89 -0
- memory_esn-0.1.0/setup.cfg +4 -0
- memory_esn-0.1.0/setup.py +70 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use memory-esn in your research, please cite it."
|
|
3
|
+
title: "memory-esn: Long-Memory Echo State Networks for Time-Series Forecasting"
|
|
4
|
+
abstract: >-
|
|
5
|
+
A reservoir-computing library that augments the Echo State Network with a
|
|
6
|
+
dedicated memory reservoir for long-range dependence, via a fractional-difference
|
|
7
|
+
filter (fESN) or a wavelet-smoothed fractional filter (wESN).
|
|
8
|
+
type: software
|
|
9
|
+
version: "0.1.0"
|
|
10
|
+
license: MIT
|
|
11
|
+
repository-code: "https://github.com/yuvrajiro/memory-esn"
|
|
12
|
+
keywords:
|
|
13
|
+
- echo state network
|
|
14
|
+
- reservoir computing
|
|
15
|
+
- long memory
|
|
16
|
+
- fractional differencing
|
|
17
|
+
- wavelet
|
|
18
|
+
- time series forecasting
|
|
19
|
+
authors:
|
|
20
|
+
- given-names: Rahul
|
|
21
|
+
family-names: Goswami
|
|
22
|
+
email: rahul.goswami@iitg.ac.in
|
|
23
|
+
affiliation: "Indian Institute of Technology Guwahati"
|
|
24
|
+
- given-names: Shinjini
|
|
25
|
+
family-names: Paul
|
|
26
|
+
email: s.paul@math.leidenuniv.nl
|
|
27
|
+
affiliation: "Leiden University"
|
|
28
|
+
- given-names: Palash
|
|
29
|
+
family-names: Ghosh
|
|
30
|
+
email: palash.ghosh@iitg.ac.in
|
|
31
|
+
affiliation: "Indian Institute of Technology Guwahati"
|
|
32
|
+
- given-names: Tanujit
|
|
33
|
+
family-names: Chakraborty
|
|
34
|
+
email: tanujit.chakraborty@sorbonne.ae
|
|
35
|
+
affiliation: "Sorbonne University Abu Dhabi"
|
memory_esn-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rahul Goswami, Shinjini Paul, Palash Ghosh, Tanujit Chakraborty
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
include README.md
|
|
2
|
+
include LICENSE
|
|
3
|
+
include CITATION.cff
|
|
4
|
+
include setup.py
|
|
5
|
+
include pyproject.toml
|
|
6
|
+
|
|
7
|
+
# Cython sources are needed to build from an sdist.
|
|
8
|
+
recursive-include memory_esn *.pyx *.pxd
|
|
9
|
+
|
|
10
|
+
# a couple of runnable examples are handy to ship
|
|
11
|
+
include examples/quickstart.py
|
|
12
|
+
|
|
13
|
+
# never ship platform-specific build artifacts or heavy dev material
|
|
14
|
+
global-exclude *.pyd *.so *.pyc *.pyo
|
|
15
|
+
global-exclude __pycache__/*
|
|
16
|
+
prune garbage
|
|
17
|
+
prune docs
|
|
18
|
+
prune benchmarks
|
|
19
|
+
prune build
|
|
20
|
+
prune tests
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: memory-esn
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Long-Memory Echo State Networks (fESN, wESN) for time-series forecasting
|
|
5
|
+
Author-email: Rahul Goswami <rahul.goswami@iitg.ac.in>, Shinjini Paul <s.paul@math.leidenuniv.nl>, Palash Ghosh <palash.ghosh@iitg.ac.in>, Tanujit Chakraborty <tanujit.chakraborty@sorbonne.ae>
|
|
6
|
+
Maintainer-email: Rahul Goswami <rahul.goswami@iitg.ac.in>
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/yuvrajiro/memory-esn
|
|
9
|
+
Project-URL: Repository, https://github.com/yuvrajiro/memory-esn
|
|
10
|
+
Project-URL: Documentation, https://github.com/yuvrajiro/memory-esn#readme
|
|
11
|
+
Project-URL: Issues, https://github.com/yuvrajiro/memory-esn/issues
|
|
12
|
+
Keywords: echo state network,reservoir computing,time series,forecasting,long memory,fractional differencing,wavelet,modwt
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Cython
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering
|
|
25
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: numpy>=1.23
|
|
30
|
+
Requires-Dist: scipy>=1.9
|
|
31
|
+
Requires-Dist: scikit-learn>=1.0
|
|
32
|
+
Requires-Dist: joblib>=1.0
|
|
33
|
+
Requires-Dist: PyWavelets>=1.1
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
36
|
+
Requires-Dist: Cython>=3.0; extra == "dev"
|
|
37
|
+
Provides-Extra: docs
|
|
38
|
+
Requires-Dist: sphinx>=7.2; extra == "docs"
|
|
39
|
+
Requires-Dist: pydata-sphinx-theme>=0.15; extra == "docs"
|
|
40
|
+
Requires-Dist: myst-nb>=1.0; extra == "docs"
|
|
41
|
+
Requires-Dist: sphinx-design>=0.5; extra == "docs"
|
|
42
|
+
Requires-Dist: sphinx-copybutton>=0.5; extra == "docs"
|
|
43
|
+
Requires-Dist: matplotlib>=3.6; extra == "docs"
|
|
44
|
+
Requires-Dist: ipykernel>=6; extra == "docs"
|
|
45
|
+
Dynamic: license-file
|
|
46
|
+
|
|
47
|
+
<div align="center">
|
|
48
|
+
<img src="https://raw.githubusercontent.com/yuvrajiro/memory-esn/refs/heads/main/docs/_static/banner.svg" alt="memory-esn Logo" width="600" />
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# 🧠 memory-esn: Long-Memory Echo State Networks
|
|
53
|
+
**Reservoir Computing for Time-Series Forecasting with Long-Range Dependence** 📈✨
|
|
54
|
+
|
|
55
|
+
[](https://pypi.org/project/memory-esn/)
|
|
56
|
+
[](https://python.org)
|
|
57
|
+
[](https://numpy.org)
|
|
58
|
+
[](https://cython.org)
|
|
59
|
+
[](https://scikit-learn.org)
|
|
60
|
+
[](https://github.com/yuvrajiro/memory-esn#readme)
|
|
61
|
+
[](LICENSE)
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
**Long-Memory Echo State Networks for Time-Series Forecasting**
|
|
65
|
+
|
|
66
|
+
A reservoir-computing library that augments the classic Echo State Network with a
|
|
67
|
+
dedicated **memory reservoir** for long-range dependence. Two memory mechanisms are
|
|
68
|
+
provided — a **fractional-difference filter** (fESN) and a **wavelet-smoothed
|
|
69
|
+
fractional filter** (wESN) — while keeping all internal weights fixed after random
|
|
70
|
+
initialization and training only a linear ridge readout. BLAS-backed **Cython**
|
|
71
|
+
kernels make it fast; pure-NumPy fallbacks keep it portable.
|
|
72
|
+
|
|
73
|
+
> The **vanilla** reservoir captures short-term nonlinear dynamics (exponential
|
|
74
|
+
> forgetting); the **memory** reservoir sustains long-term dynamics (polynomially
|
|
75
|
+
> decaying weights). Their states are concatenated and mapped to the output.
|
|
76
|
+
|
|
77
|
+
## Class hierarchy
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
PersistenceMixin save() / load() (joblib)
|
|
81
|
+
│
|
|
82
|
+
BaseESN single reservoir + ridge readout
|
|
83
|
+
│
|
|
84
|
+
MultiESN N reservoirs, one input each (composes BaseESN)
|
|
85
|
+
└── DoubleReservoirESN fixed to 2 reservoirs; fit([X1, X2], y)
|
|
86
|
+
├── fESN memory reservoir <- ((1-B)^d - 1) u(t)
|
|
87
|
+
└── wESN memory reservoir <- ((1-B)^d - 1) MODWT_smooth(u(t))
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Reservoir 1 (vanilla) always sees the **raw** series; reservoir 2 (memory) sees a
|
|
91
|
+
pure-past, long-memory filter of it. `fESN`/`wESN` take a single series and build the
|
|
92
|
+
memory input internally; `DoubleReservoirESN` and `MultiESN` take the inputs explicitly.
|
|
93
|
+
|
|
94
|
+
Plus `TimeSeriesDataset` — sliding-window train/val/test splitting with leakage-free
|
|
95
|
+
scaling (`none` / `minmax` / `standard` / `log`).
|
|
96
|
+
|
|
97
|
+
## Defaults follow the paper
|
|
98
|
+
|
|
99
|
+
Out of the box the models match the paper's construction: **uniform** weight
|
|
100
|
+
initialization, reservoirs sparsified to **90%** zeros then rescaled to a spectral
|
|
101
|
+
radius `< 1`, and a memory filter `f(t) = ((1-B)^d - 1) u(t) = Σ_{k≥1} ω_k u(t-k)`
|
|
102
|
+
(standard fractional differencing with the present term removed, so the memory
|
|
103
|
+
reservoir is driven purely by the past). Optional isotropic state noise
|
|
104
|
+
`η(t) ~ N(0, σ² I)` is available via `noise=σ`.
|
|
105
|
+
|
|
106
|
+
## Install
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
pip install -e . # installs deps; builds Cython kernels if a
|
|
110
|
+
# C compiler is available
|
|
111
|
+
# or build the fast kernels in place:
|
|
112
|
+
python setup.py build_ext --inplace
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The compiled kernels are **optional** — pure-NumPy fallbacks are used automatically
|
|
116
|
+
when they are not present (with a one-time warning). Check which is active:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
import memory_esn as m
|
|
120
|
+
m.USING_CYTHON_RESERVOIR, m.USING_CYTHON_FRACDIFF, m.USING_CYTHON_MODWT
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Dependencies:** numpy, scipy, scikit-learn, joblib, PyWavelets.
|
|
124
|
+
|
|
125
|
+
## Quickstart
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
import numpy as np
|
|
129
|
+
from memory_esn import BaseESN, fESN
|
|
130
|
+
|
|
131
|
+
X = np.cumsum(np.random.randn(1000, 3), axis=0) * 0.1
|
|
132
|
+
y = np.sin(X[:, :1])
|
|
133
|
+
Xtr, Xte, ytr, yte = X[:800], X[800:], y[:800], y[800:]
|
|
134
|
+
|
|
135
|
+
# Single reservoir
|
|
136
|
+
esn = BaseESN(n_reservoir=200, spectral_radius=0.95, random_state=0)
|
|
137
|
+
esn.fit(Xtr, ytr, washout=100)
|
|
138
|
+
y_pred = esn.predict(Xte)
|
|
139
|
+
|
|
140
|
+
# Fractional ESN (raw + fractionally-differenced branch)
|
|
141
|
+
fesn = fESN(n_reservoir=(200, 150), d=0.5, K=100, random_state=0)
|
|
142
|
+
fesn.fit(Xtr, ytr, washout=100)
|
|
143
|
+
y_pred = fesn.predict(Xte, continuation=True) # continuation keeps output length
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
See [`examples/quickstart.py`](examples/quickstart.py) for one example per class.
|
|
147
|
+
|
|
148
|
+
## Wavelet options (wESN)
|
|
149
|
+
|
|
150
|
+
`wESN` decomposes each input feature with a **MODWT** (undecimated, shift-invariant)
|
|
151
|
+
and feeds the selected component(s) to reservoir 2. The wavelet is validated at
|
|
152
|
+
construction:
|
|
153
|
+
|
|
154
|
+
- **Discrete wavelets** (usable): `haar`, `db1..db38`, `sym2..sym20`, `coif1..coif17`,
|
|
155
|
+
`bior*`, `rbio*`, `dmey` — 106 in total (`pywt.wavelist(kind='discrete')`).
|
|
156
|
+
- **Continuous wavelets** (`mexh`, `morl`, `gaus*`, `cmor`, …) raise
|
|
157
|
+
`NotImplementedError` — support is **planned for a future release**.
|
|
158
|
+
|
|
159
|
+
Pick which decomposition components form reservoir 2's input via `wavelet_components`:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from memory_esn import wESN
|
|
163
|
+
|
|
164
|
+
# default: level-J smooth (approximation) trend — one component
|
|
165
|
+
wESN(wavelet='db4', wavelet_level=2) # components = [A2]
|
|
166
|
+
|
|
167
|
+
# a single detail level
|
|
168
|
+
wESN(wavelet='db4', wavelet_level=3, wavelet_components=2) # components = [D2]
|
|
169
|
+
|
|
170
|
+
# MULTIVARIATE: several levels stacked -> reservoir 2 gets multiple channels
|
|
171
|
+
wESN(wavelet='db4', wavelet_level=3,
|
|
172
|
+
wavelet_components=[1, 2, 'smooth']) # components = [D1, D2, A3]
|
|
173
|
+
|
|
174
|
+
# shortcuts
|
|
175
|
+
wESN(wavelet='haar', wavelet_level=2, wavelet_components='all') # D1, D2, A2
|
|
176
|
+
wESN(wavelet='haar', wavelet_level=2, wavelet_components='details') # D1, D2
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
`wavelet_level` is the decomposition depth `J`; detail levels are `1..J` and `'smooth'`
|
|
180
|
+
is the level-`J` approximation. Selecting *n* components on an *F*-feature series gives
|
|
181
|
+
reservoir 2 an `F × n`-channel (multivariate) input.
|
|
182
|
+
|
|
183
|
+
**MODWT normalization** — `wavelet_norm='modwt'` (default) uses the standard
|
|
184
|
+
Percival–Walden rescaling (Haar smooth filter `[1/2, 1/2]`); `wavelet_norm='classic'`
|
|
185
|
+
uses the DWT orthonormal filters (`[1/√2, 1/√2]`). The two differ only by a per-level
|
|
186
|
+
constant absorbed by the random memory-input weights, so they are model-equivalent.
|
|
187
|
+
|
|
188
|
+
## Multivariate fESN (multiple differencing orders)
|
|
189
|
+
|
|
190
|
+
`fESN` has the analogous knob: `d` accepts a single order (default, univariate)
|
|
191
|
+
or a **list** of orders that are stacked as channels.
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from memory_esn import fESN
|
|
195
|
+
|
|
196
|
+
fESN(d=0.5) # univariate (default)
|
|
197
|
+
fESN(d=[0.3, 0.5, 0.8]) # multivariate: 3 orders stacked
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Selecting *n* orders on an *F*-feature series gives reservoir 2 an `F × n`-channel input.
|
|
201
|
+
Defaults for both classes are univariate: fESN `d=0.5`, wESN `wavelet_components='smooth'`.
|
|
202
|
+
|
|
203
|
+
`wESN` also accepts a **list `d`**, which *cross-products* with the wavelet
|
|
204
|
+
components: `n_features × n_components × n_d` channels. For example
|
|
205
|
+
`wESN(wavelet_components=[1, 2, 'smooth'], d=[0.3, 0.5])` on a 2-feature series
|
|
206
|
+
→ `2 × 3 × 2 = 12` channels into reservoir 2.
|
|
207
|
+
|
|
208
|
+
## Weight initialization & reservoir options
|
|
209
|
+
|
|
210
|
+
Every reservoir weight is drawn from a configurable distribution (`memory_esn.weights`):
|
|
211
|
+
|
|
212
|
+
| parameter | controls | options | default |
|
|
213
|
+
|---|---|---|---|
|
|
214
|
+
| `input_init` | input weights `W_in` | `uniform`, `gaussian`, `bernoulli`, `laplace` | `uniform` |
|
|
215
|
+
| `reservoir_init` | reservoir weights `W` | same | `uniform` |
|
|
216
|
+
| `bias_init` | neuron bias | same | `uniform` |
|
|
217
|
+
| `input_scaling` | scale of `W_in` (= `U[-ψ, ψ]` for uniform) | float | `0.5` |
|
|
218
|
+
| `spectral_radius` | `W` rescaled to this ρ | float `<1` | `0.9` |
|
|
219
|
+
| `sparsity` | fraction of `W` set to zero (φ) | `[0,1)` | `0.9` |
|
|
220
|
+
| `bias_scaling` | scale of bias (0 = none) | float | `0.0` |
|
|
221
|
+
| `noise` | std σ of state noise `η(t)~N(0,σ²I)` | float ≥ 0 | `0.0` |
|
|
222
|
+
|
|
223
|
+
In `MultiESN` each is broadcastable — a scalar applies to all reservoirs, a list of
|
|
224
|
+
length `n_reservoirs` sets them per reservoir. In the two-reservoir models a scalar or
|
|
225
|
+
a `(vanilla, memory)` pair is accepted. `random_state` follows the same rule: a single
|
|
226
|
+
int derives distinct per-reservoir seeds (reproducible but not identical); a list sets
|
|
227
|
+
them explicitly.
|
|
228
|
+
|
|
229
|
+
## Paper ↔ code notation
|
|
230
|
+
|
|
231
|
+
| paper | meaning | code |
|
|
232
|
+
|---|---|---|
|
|
233
|
+
| `p`, `q` | vanilla / memory reservoir sizes | `n_reservoir=(p, q)` |
|
|
234
|
+
| `ρ_x`, `ρ_m` | spectral radii | `spectral_radius=(ρ_x, ρ_m)` |
|
|
235
|
+
| `φ_x`, `φ_m` | sparsification proportion | `sparsity=(φ_x, φ_m)` |
|
|
236
|
+
| `ψ_x`, `ψ_m` | input scalings | `input_scaling=(ψ_x, ψ_m)` |
|
|
237
|
+
| `σ_x`, `σ_m` | state-noise std | `noise=(σ_x, σ_m)` |
|
|
238
|
+
| `d` | fractional-differencing order | `d` |
|
|
239
|
+
| `K` | filter truncation (lags) | `K` |
|
|
240
|
+
| `ζ` | washout ratio `T₀/T` | `washout=int(ζ*T)` |
|
|
241
|
+
| `H` | forecast horizon | number of columns of `y` |
|
|
242
|
+
|
|
243
|
+
The per-horizon readouts `W_out^(h)` are fitted jointly as a multi-output ridge on a
|
|
244
|
+
single design matrix `Φ = [x; m; u; f] ∈ ℝ^{p+q+2}`, with `RidgeCV` selecting `λ` by
|
|
245
|
+
efficient leave-one-out — matching the paper's shared-Cholesky LOOCV.
|
|
246
|
+
|
|
247
|
+
## Notes on alignment (fESN / wESN)
|
|
248
|
+
|
|
249
|
+
The fractional-difference filter needs `K` past samples. With `skip_initial=True`
|
|
250
|
+
(default) and `continuation=False`, predictions are `K` steps shorter than the
|
|
251
|
+
input. With `continuation=True`, the model prepends stored history so the output
|
|
252
|
+
matches the input length — this is the normal mode for streaming / iterative
|
|
253
|
+
forecasting. `wESN.predict` uses `continuation=True` by default (it also re-smooths
|
|
254
|
+
using training history to avoid wavelet boundary effects).
|
|
255
|
+
|
|
256
|
+
## Speed-up kernels
|
|
257
|
+
|
|
258
|
+
`memory_esn/_speedups/` holds three Cython extensions (with NumPy fallbacks):
|
|
259
|
+
|
|
260
|
+
| kernel | what it does |
|
|
261
|
+
|---|---|
|
|
262
|
+
| `esn_reservoir_optimized` | reservoir state update via BLAS `dgemv`, `nogil`, 12 activations |
|
|
263
|
+
| `fractional_diff` | binomial fractional-difference weights + windowed filtering |
|
|
264
|
+
| `modwt_fast` | maximal-overlap DWT (smoothing) via circular convolution |
|
|
265
|
+
|
|
266
|
+
## Tests
|
|
267
|
+
|
|
268
|
+
```bash
|
|
269
|
+
pip install pytest
|
|
270
|
+
python -m pytest tests/ # run from the project root
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
The suite includes Cython-vs-NumPy numerical-equivalence checks, so the fast and
|
|
274
|
+
fallback paths are guaranteed to agree.
|
|
275
|
+
|
|
276
|
+
## Repository layout
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
memory_esn/ the package (import as `memory_esn`)
|
|
280
|
+
base.py BaseESN, PersistenceMixin
|
|
281
|
+
multi.py MultiESN
|
|
282
|
+
double.py DoubleReservoirESN
|
|
283
|
+
fractional.py fESN (alias FractionalESN)
|
|
284
|
+
wavelet.py wESN (alias WaveletESN)
|
|
285
|
+
dataset.py TimeSeriesDataset
|
|
286
|
+
_speedups/ Cython kernels + NumPy fallbacks
|
|
287
|
+
examples/quickstart.py
|
|
288
|
+
tests/test_package.py
|
|
289
|
+
setup.py builds the Cython extensions
|
|
290
|
+
garbage/ the previous, pre-refactor files (archived)
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Authors
|
|
294
|
+
|
|
295
|
+
- [**Rahul Goswami**](https://www.statml.in) | [**Shinjini Paul**](https://www.universiteitleiden.nl/en/staffmembers/shinjini-paul#tab-1) | [**Palash Ghosh**](https://sites.google.com/site/palashghoshstat/home) | [**Tanujit Chakraborty**](https://www.ctanujit.org/)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
## Citation
|
|
299
|
+
|
|
300
|
+
If you use memory-esn in your research, please cite it (see [`CITATION.cff`](CITATION.cff)).
|
|
301
|
+
|
|
302
|
+
## License
|
|
303
|
+
|
|
304
|
+
MIT — see [`LICENSE`](LICENSE).
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/yuvrajiro/memory-esn/refs/heads/main/docs/_static/banner.svg" alt="memory-esn Logo" width="600" />
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# 🧠 memory-esn: Long-Memory Echo State Networks
|
|
7
|
+
**Reservoir Computing for Time-Series Forecasting with Long-Range Dependence** 📈✨
|
|
8
|
+
|
|
9
|
+
[](https://pypi.org/project/memory-esn/)
|
|
10
|
+
[](https://python.org)
|
|
11
|
+
[](https://numpy.org)
|
|
12
|
+
[](https://cython.org)
|
|
13
|
+
[](https://scikit-learn.org)
|
|
14
|
+
[](https://github.com/yuvrajiro/memory-esn#readme)
|
|
15
|
+
[](LICENSE)
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
**Long-Memory Echo State Networks for Time-Series Forecasting**
|
|
19
|
+
|
|
20
|
+
A reservoir-computing library that augments the classic Echo State Network with a
|
|
21
|
+
dedicated **memory reservoir** for long-range dependence. Two memory mechanisms are
|
|
22
|
+
provided — a **fractional-difference filter** (fESN) and a **wavelet-smoothed
|
|
23
|
+
fractional filter** (wESN) — while keeping all internal weights fixed after random
|
|
24
|
+
initialization and training only a linear ridge readout. BLAS-backed **Cython**
|
|
25
|
+
kernels make it fast; pure-NumPy fallbacks keep it portable.
|
|
26
|
+
|
|
27
|
+
> The **vanilla** reservoir captures short-term nonlinear dynamics (exponential
|
|
28
|
+
> forgetting); the **memory** reservoir sustains long-term dynamics (polynomially
|
|
29
|
+
> decaying weights). Their states are concatenated and mapped to the output.
|
|
30
|
+
|
|
31
|
+
## Class hierarchy
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
PersistenceMixin save() / load() (joblib)
|
|
35
|
+
│
|
|
36
|
+
BaseESN single reservoir + ridge readout
|
|
37
|
+
│
|
|
38
|
+
MultiESN N reservoirs, one input each (composes BaseESN)
|
|
39
|
+
└── DoubleReservoirESN fixed to 2 reservoirs; fit([X1, X2], y)
|
|
40
|
+
├── fESN memory reservoir <- ((1-B)^d - 1) u(t)
|
|
41
|
+
└── wESN memory reservoir <- ((1-B)^d - 1) MODWT_smooth(u(t))
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Reservoir 1 (vanilla) always sees the **raw** series; reservoir 2 (memory) sees a
|
|
45
|
+
pure-past, long-memory filter of it. `fESN`/`wESN` take a single series and build the
|
|
46
|
+
memory input internally; `DoubleReservoirESN` and `MultiESN` take the inputs explicitly.
|
|
47
|
+
|
|
48
|
+
Plus `TimeSeriesDataset` — sliding-window train/val/test splitting with leakage-free
|
|
49
|
+
scaling (`none` / `minmax` / `standard` / `log`).
|
|
50
|
+
|
|
51
|
+
## Defaults follow the paper
|
|
52
|
+
|
|
53
|
+
Out of the box the models match the paper's construction: **uniform** weight
|
|
54
|
+
initialization, reservoirs sparsified to **90%** zeros then rescaled to a spectral
|
|
55
|
+
radius `< 1`, and a memory filter `f(t) = ((1-B)^d - 1) u(t) = Σ_{k≥1} ω_k u(t-k)`
|
|
56
|
+
(standard fractional differencing with the present term removed, so the memory
|
|
57
|
+
reservoir is driven purely by the past). Optional isotropic state noise
|
|
58
|
+
`η(t) ~ N(0, σ² I)` is available via `noise=σ`.
|
|
59
|
+
|
|
60
|
+
## Install
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install -e . # installs deps; builds Cython kernels if a
|
|
64
|
+
# C compiler is available
|
|
65
|
+
# or build the fast kernels in place:
|
|
66
|
+
python setup.py build_ext --inplace
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The compiled kernels are **optional** — pure-NumPy fallbacks are used automatically
|
|
70
|
+
when they are not present (with a one-time warning). Check which is active:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import memory_esn as m
|
|
74
|
+
m.USING_CYTHON_RESERVOIR, m.USING_CYTHON_FRACDIFF, m.USING_CYTHON_MODWT
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Dependencies:** numpy, scipy, scikit-learn, joblib, PyWavelets.
|
|
78
|
+
|
|
79
|
+
## Quickstart
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
import numpy as np
|
|
83
|
+
from memory_esn import BaseESN, fESN
|
|
84
|
+
|
|
85
|
+
X = np.cumsum(np.random.randn(1000, 3), axis=0) * 0.1
|
|
86
|
+
y = np.sin(X[:, :1])
|
|
87
|
+
Xtr, Xte, ytr, yte = X[:800], X[800:], y[:800], y[800:]
|
|
88
|
+
|
|
89
|
+
# Single reservoir
|
|
90
|
+
esn = BaseESN(n_reservoir=200, spectral_radius=0.95, random_state=0)
|
|
91
|
+
esn.fit(Xtr, ytr, washout=100)
|
|
92
|
+
y_pred = esn.predict(Xte)
|
|
93
|
+
|
|
94
|
+
# Fractional ESN (raw + fractionally-differenced branch)
|
|
95
|
+
fesn = fESN(n_reservoir=(200, 150), d=0.5, K=100, random_state=0)
|
|
96
|
+
fesn.fit(Xtr, ytr, washout=100)
|
|
97
|
+
y_pred = fesn.predict(Xte, continuation=True) # continuation keeps output length
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
See [`examples/quickstart.py`](examples/quickstart.py) for one example per class.
|
|
101
|
+
|
|
102
|
+
## Wavelet options (wESN)
|
|
103
|
+
|
|
104
|
+
`wESN` decomposes each input feature with a **MODWT** (undecimated, shift-invariant)
|
|
105
|
+
and feeds the selected component(s) to reservoir 2. The wavelet is validated at
|
|
106
|
+
construction:
|
|
107
|
+
|
|
108
|
+
- **Discrete wavelets** (usable): `haar`, `db1..db38`, `sym2..sym20`, `coif1..coif17`,
|
|
109
|
+
`bior*`, `rbio*`, `dmey` — 106 in total (`pywt.wavelist(kind='discrete')`).
|
|
110
|
+
- **Continuous wavelets** (`mexh`, `morl`, `gaus*`, `cmor`, …) raise
|
|
111
|
+
`NotImplementedError` — support is **planned for a future release**.
|
|
112
|
+
|
|
113
|
+
Pick which decomposition components form reservoir 2's input via `wavelet_components`:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from memory_esn import wESN
|
|
117
|
+
|
|
118
|
+
# default: level-J smooth (approximation) trend — one component
|
|
119
|
+
wESN(wavelet='db4', wavelet_level=2) # components = [A2]
|
|
120
|
+
|
|
121
|
+
# a single detail level
|
|
122
|
+
wESN(wavelet='db4', wavelet_level=3, wavelet_components=2) # components = [D2]
|
|
123
|
+
|
|
124
|
+
# MULTIVARIATE: several levels stacked -> reservoir 2 gets multiple channels
|
|
125
|
+
wESN(wavelet='db4', wavelet_level=3,
|
|
126
|
+
wavelet_components=[1, 2, 'smooth']) # components = [D1, D2, A3]
|
|
127
|
+
|
|
128
|
+
# shortcuts
|
|
129
|
+
wESN(wavelet='haar', wavelet_level=2, wavelet_components='all') # D1, D2, A2
|
|
130
|
+
wESN(wavelet='haar', wavelet_level=2, wavelet_components='details') # D1, D2
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`wavelet_level` is the decomposition depth `J`; detail levels are `1..J` and `'smooth'`
|
|
134
|
+
is the level-`J` approximation. Selecting *n* components on an *F*-feature series gives
|
|
135
|
+
reservoir 2 an `F × n`-channel (multivariate) input.
|
|
136
|
+
|
|
137
|
+
**MODWT normalization** — `wavelet_norm='modwt'` (default) uses the standard
|
|
138
|
+
Percival–Walden rescaling (Haar smooth filter `[1/2, 1/2]`); `wavelet_norm='classic'`
|
|
139
|
+
uses the DWT orthonormal filters (`[1/√2, 1/√2]`). The two differ only by a per-level
|
|
140
|
+
constant absorbed by the random memory-input weights, so they are model-equivalent.
|
|
141
|
+
|
|
142
|
+
## Multivariate fESN (multiple differencing orders)
|
|
143
|
+
|
|
144
|
+
`fESN` has the analogous knob: `d` accepts a single order (default, univariate)
|
|
145
|
+
or a **list** of orders that are stacked as channels.
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
from memory_esn import fESN
|
|
149
|
+
|
|
150
|
+
fESN(d=0.5) # univariate (default)
|
|
151
|
+
fESN(d=[0.3, 0.5, 0.8]) # multivariate: 3 orders stacked
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Selecting *n* orders on an *F*-feature series gives reservoir 2 an `F × n`-channel input.
|
|
155
|
+
Defaults for both classes are univariate: fESN `d=0.5`, wESN `wavelet_components='smooth'`.
|
|
156
|
+
|
|
157
|
+
`wESN` also accepts a **list `d`**, which *cross-products* with the wavelet
|
|
158
|
+
components: `n_features × n_components × n_d` channels. For example
|
|
159
|
+
`wESN(wavelet_components=[1, 2, 'smooth'], d=[0.3, 0.5])` on a 2-feature series
|
|
160
|
+
→ `2 × 3 × 2 = 12` channels into reservoir 2.
|
|
161
|
+
|
|
162
|
+
## Weight initialization & reservoir options
|
|
163
|
+
|
|
164
|
+
Every reservoir weight is drawn from a configurable distribution (`memory_esn.weights`):
|
|
165
|
+
|
|
166
|
+
| parameter | controls | options | default |
|
|
167
|
+
|---|---|---|---|
|
|
168
|
+
| `input_init` | input weights `W_in` | `uniform`, `gaussian`, `bernoulli`, `laplace` | `uniform` |
|
|
169
|
+
| `reservoir_init` | reservoir weights `W` | same | `uniform` |
|
|
170
|
+
| `bias_init` | neuron bias | same | `uniform` |
|
|
171
|
+
| `input_scaling` | scale of `W_in` (= `U[-ψ, ψ]` for uniform) | float | `0.5` |
|
|
172
|
+
| `spectral_radius` | `W` rescaled to this ρ | float `<1` | `0.9` |
|
|
173
|
+
| `sparsity` | fraction of `W` set to zero (φ) | `[0,1)` | `0.9` |
|
|
174
|
+
| `bias_scaling` | scale of bias (0 = none) | float | `0.0` |
|
|
175
|
+
| `noise` | std σ of state noise `η(t)~N(0,σ²I)` | float ≥ 0 | `0.0` |
|
|
176
|
+
|
|
177
|
+
In `MultiESN` each is broadcastable — a scalar applies to all reservoirs, a list of
|
|
178
|
+
length `n_reservoirs` sets them per reservoir. In the two-reservoir models a scalar or
|
|
179
|
+
a `(vanilla, memory)` pair is accepted. `random_state` follows the same rule: a single
|
|
180
|
+
int derives distinct per-reservoir seeds (reproducible but not identical); a list sets
|
|
181
|
+
them explicitly.
|
|
182
|
+
|
|
183
|
+
## Paper ↔ code notation
|
|
184
|
+
|
|
185
|
+
| paper | meaning | code |
|
|
186
|
+
|---|---|---|
|
|
187
|
+
| `p`, `q` | vanilla / memory reservoir sizes | `n_reservoir=(p, q)` |
|
|
188
|
+
| `ρ_x`, `ρ_m` | spectral radii | `spectral_radius=(ρ_x, ρ_m)` |
|
|
189
|
+
| `φ_x`, `φ_m` | sparsification proportion | `sparsity=(φ_x, φ_m)` |
|
|
190
|
+
| `ψ_x`, `ψ_m` | input scalings | `input_scaling=(ψ_x, ψ_m)` |
|
|
191
|
+
| `σ_x`, `σ_m` | state-noise std | `noise=(σ_x, σ_m)` |
|
|
192
|
+
| `d` | fractional-differencing order | `d` |
|
|
193
|
+
| `K` | filter truncation (lags) | `K` |
|
|
194
|
+
| `ζ` | washout ratio `T₀/T` | `washout=int(ζ*T)` |
|
|
195
|
+
| `H` | forecast horizon | number of columns of `y` |
|
|
196
|
+
|
|
197
|
+
The per-horizon readouts `W_out^(h)` are fitted jointly as a multi-output ridge on a
|
|
198
|
+
single design matrix `Φ = [x; m; u; f] ∈ ℝ^{p+q+2}`, with `RidgeCV` selecting `λ` by
|
|
199
|
+
efficient leave-one-out — matching the paper's shared-Cholesky LOOCV.
|
|
200
|
+
|
|
201
|
+
## Notes on alignment (fESN / wESN)
|
|
202
|
+
|
|
203
|
+
The fractional-difference filter needs `K` past samples. With `skip_initial=True`
|
|
204
|
+
(default) and `continuation=False`, predictions are `K` steps shorter than the
|
|
205
|
+
input. With `continuation=True`, the model prepends stored history so the output
|
|
206
|
+
matches the input length — this is the normal mode for streaming / iterative
|
|
207
|
+
forecasting. `wESN.predict` uses `continuation=True` by default (it also re-smooths
|
|
208
|
+
using training history to avoid wavelet boundary effects).
|
|
209
|
+
|
|
210
|
+
## Speed-up kernels
|
|
211
|
+
|
|
212
|
+
`memory_esn/_speedups/` holds three Cython extensions (with NumPy fallbacks):
|
|
213
|
+
|
|
214
|
+
| kernel | what it does |
|
|
215
|
+
|---|---|
|
|
216
|
+
| `esn_reservoir_optimized` | reservoir state update via BLAS `dgemv`, `nogil`, 12 activations |
|
|
217
|
+
| `fractional_diff` | binomial fractional-difference weights + windowed filtering |
|
|
218
|
+
| `modwt_fast` | maximal-overlap DWT (smoothing) via circular convolution |
|
|
219
|
+
|
|
220
|
+
## Tests
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
pip install pytest
|
|
224
|
+
python -m pytest tests/ # run from the project root
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
The suite includes Cython-vs-NumPy numerical-equivalence checks, so the fast and
|
|
228
|
+
fallback paths are guaranteed to agree.
|
|
229
|
+
|
|
230
|
+
## Repository layout
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
memory_esn/ the package (import as `memory_esn`)
|
|
234
|
+
base.py BaseESN, PersistenceMixin
|
|
235
|
+
multi.py MultiESN
|
|
236
|
+
double.py DoubleReservoirESN
|
|
237
|
+
fractional.py fESN (alias FractionalESN)
|
|
238
|
+
wavelet.py wESN (alias WaveletESN)
|
|
239
|
+
dataset.py TimeSeriesDataset
|
|
240
|
+
_speedups/ Cython kernels + NumPy fallbacks
|
|
241
|
+
examples/quickstart.py
|
|
242
|
+
tests/test_package.py
|
|
243
|
+
setup.py builds the Cython extensions
|
|
244
|
+
garbage/ the previous, pre-refactor files (archived)
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Authors
|
|
248
|
+
|
|
249
|
+
- [**Rahul Goswami**](https://www.statml.in) | [**Shinjini Paul**](https://www.universiteitleiden.nl/en/staffmembers/shinjini-paul#tab-1) | [**Palash Ghosh**](https://sites.google.com/site/palashghoshstat/home) | [**Tanujit Chakraborty**](https://www.ctanujit.org/)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
## Citation
|
|
253
|
+
|
|
254
|
+
If you use memory-esn in your research, please cite it (see [`CITATION.cff`](CITATION.cff)).
|
|
255
|
+
|
|
256
|
+
## License
|
|
257
|
+
|
|
258
|
+
MIT — see [`LICENSE`](LICENSE).
|