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.
Files changed (31) hide show
  1. memory_esn-0.1.0/CITATION.cff +35 -0
  2. memory_esn-0.1.0/LICENSE +21 -0
  3. memory_esn-0.1.0/MANIFEST.in +20 -0
  4. memory_esn-0.1.0/PKG-INFO +304 -0
  5. memory_esn-0.1.0/README.md +258 -0
  6. memory_esn-0.1.0/examples/quickstart.py +100 -0
  7. memory_esn-0.1.0/memory_esn/__init__.py +56 -0
  8. memory_esn-0.1.0/memory_esn/_speedups/__init__.py +35 -0
  9. memory_esn-0.1.0/memory_esn/_speedups/_fallback.py +206 -0
  10. memory_esn-0.1.0/memory_esn/_speedups/_loader.py +87 -0
  11. memory_esn-0.1.0/memory_esn/_speedups/esn_reservoir_optimized.c +17439 -0
  12. memory_esn-0.1.0/memory_esn/_speedups/esn_reservoir_optimized.pyx +360 -0
  13. memory_esn-0.1.0/memory_esn/_speedups/fractional_diff.c +33213 -0
  14. memory_esn-0.1.0/memory_esn/_speedups/fractional_diff.pyx +321 -0
  15. memory_esn-0.1.0/memory_esn/_speedups/modwt_fast.c +31621 -0
  16. memory_esn-0.1.0/memory_esn/_speedups/modwt_fast.pyx +159 -0
  17. memory_esn-0.1.0/memory_esn/base.py +306 -0
  18. memory_esn-0.1.0/memory_esn/dataset.py +230 -0
  19. memory_esn-0.1.0/memory_esn/double.py +110 -0
  20. memory_esn-0.1.0/memory_esn/fractional.py +217 -0
  21. memory_esn-0.1.0/memory_esn/multi.py +315 -0
  22. memory_esn-0.1.0/memory_esn/wavelet.py +390 -0
  23. memory_esn-0.1.0/memory_esn/weights.py +202 -0
  24. memory_esn-0.1.0/memory_esn.egg-info/PKG-INFO +304 -0
  25. memory_esn-0.1.0/memory_esn.egg-info/SOURCES.txt +29 -0
  26. memory_esn-0.1.0/memory_esn.egg-info/dependency_links.txt +1 -0
  27. memory_esn-0.1.0/memory_esn.egg-info/requires.txt +18 -0
  28. memory_esn-0.1.0/memory_esn.egg-info/top_level.txt +1 -0
  29. memory_esn-0.1.0/pyproject.toml +89 -0
  30. memory_esn-0.1.0/setup.cfg +4 -0
  31. 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"
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/memory-esn?logo=pypi&logoColor=white)](https://pypi.org/project/memory-esn/)
56
+ [![Python](https://img.shields.io/badge/Python-3.9%2B-blue?logo=python&logoColor=white)](https://python.org)
57
+ [![NumPy](https://img.shields.io/badge/NumPy-powered-013243?logo=numpy&logoColor=white)](https://numpy.org)
58
+ [![Cython](https://img.shields.io/badge/Cython-accelerated-ffd43b?logo=cython&logoColor=black)](https://cython.org)
59
+ [![scikit-learn](https://img.shields.io/badge/scikit--learn-ridge%20readout-f7931e?logo=scikitlearn&logoColor=white)](https://scikit-learn.org)
60
+ [![Docs](https://img.shields.io/badge/docs-online-6d28d9)](https://github.com/yuvrajiro/memory-esn#readme)
61
+ [![License](https://img.shields.io/badge/License-MIT-green.svg)](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
+ [![PyPI](https://img.shields.io/pypi/v/memory-esn?logo=pypi&logoColor=white)](https://pypi.org/project/memory-esn/)
10
+ [![Python](https://img.shields.io/badge/Python-3.9%2B-blue?logo=python&logoColor=white)](https://python.org)
11
+ [![NumPy](https://img.shields.io/badge/NumPy-powered-013243?logo=numpy&logoColor=white)](https://numpy.org)
12
+ [![Cython](https://img.shields.io/badge/Cython-accelerated-ffd43b?logo=cython&logoColor=black)](https://cython.org)
13
+ [![scikit-learn](https://img.shields.io/badge/scikit--learn-ridge%20readout-f7931e?logo=scikitlearn&logoColor=white)](https://scikit-learn.org)
14
+ [![Docs](https://img.shields.io/badge/docs-online-6d28d9)](https://github.com/yuvrajiro/memory-esn#readme)
15
+ [![License](https://img.shields.io/badge/License-MIT-green.svg)](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).