By ACS Lab, ITMO University · 2025
Official repository for the paper:
A. Kovantsev, R. Vysotskiy (2025). Advanced Reservoir Neural Network Techniques for Chaotic Time Series Prediction. SSRN 5481760.
We present ESN‑F — the Echo State Network (ESN) enhanced with Fourier features and polynomial expansion for forecasting chaotic / nonlinear time series. The approach keeps the reservoir untrained and learns only a ridge readout, while enriching inputs with periodic (sin/cos) and nonlinear bases that improve long‑horizon stability.
Use cases include finance/economics, risk modeling, and other non‑stationary domains.
Long‑horizon forecasting on chaotic / weakly stationary signals is tricky: standard RNNs tend to drift, while purely statistical baselines miss nonlinear structure. EnhancedESN_FAN keeps a random, untrained reservoir for rich dynamics and augments the readout with deterministic Fourier harmonics and polynomial features. The linear readout is trained via ridge regression, keeping training fast, convex, and robust.
git clone https://github.com/CapitalistGeorge/chaotic_library.git
cd chaotic_library
python -m venv .venv
# macOS/Linux
source .venv/bin/activate
# Windows (PowerShell)
# .venv\Scripts\Activate.ps1
python -m pip install -U pip
pip install -e .Planned: after the API is frozen we’ll publish to PyPI, so you can pip install esn-fan (package name TBD). The import path in the examples below assumes a module enhanced_esn_fan.py at the project root; adjust if packaged differently.
import numpy as np
from chaotic_library import EnhancedESN_FAN # adjust import path if packaged differently
# 1) Build training data (shape: [n_timesteps, input_dim])
# Univariate example: input_dim=1 → X is 2D with one column, y is 1D
T = 1200
noise = 0.1 * np.random.randn(T)
signal = np.sin(2*np.pi*np.arange(T)/50) + 0.25*np.sin(2*np.pi*np.arange(T)/7) + noise
X = signal[:-1].reshape(-1, 1) # features are previous value(s)
y = signal[1:] # next-step target
# 2) Initialize Enhanced ESN + FAN features (Fourier + polynomial)
esn = EnhancedESN_FAN(
input_dim=1, # number of input features per timestep (columns of X)
reservoir_size=800,
spectral_radius=0.95,
sparsity=0.1,
ridge_alpha=1e-2,
leaking_rate=0.3,
poly_order=2,
fan_terms=8,
random_state=42,
clip_value=3.0,
)
# 3) Fit and one-step-ahead predictions (teacher forcing / open loop)
esn.fit(X, y)
y_hat = esn.predict(X[:100]) # shape: (100,) for univariate target
print("y_hat shape:", np.asarray(y_hat).shape)# Seed with the last observed input row (shape: [1, input_dim])
seed = X[-1:].copy()
# Produce next 300 steps autoregressively
future = esn.predict(seed, generative_steps=300) # shape: (300,) for univariate
# Concatenate history + forecast for plotting
full = np.concatenate([signal, future.ravel()])import numpy as np
from chaotic_library import EnhancedESN_FAN
# Suppose you have 3 exogenous drivers + the main signal → input_dim=4
n = 2000
main = np.sin(2*np.pi*np.arange(n)/30) + 0.05*np.random.randn(n)
x1 = np.cos(2*np.pi*np.arange(n)/100)
x2 = np.sin(2*np.pi*np.arange(n)/7)
x3 = 0.01*np.arange(n) # slow trend proxy
# Features: use current drivers + lagged main as input; predict next main
X = np.column_stack([main[:-1], x1[:-1], x2[:-1], x3[:-1]]) # shape (n-1, 4)
y = main[1:] # shape (n-1,)
model = EnhancedESN_FAN(
input_dim=4,
reservoir_size=500,
spectral_radius=0.9,
sparsity=0.1,
ridge_alpha=0.1,
leaking_rate=0.4,
poly_order=2,
fan_terms=6,
random_state=7,
)
model.fit(X, y)
pred = model.predict(X[:128]) # one-step predictions (teacher forcing)
gen = model.predict(X[-1:], generative_steps=200) # recursive forecastReservoir state
where:
-
$u_t$ is the input (e.g., components of$X_t$ ) -
$[1; u_t]$ denotes a bias‑augmented input -
$\alpha$ is the leaking rate (leaking_rateparameter)
To satisfy the echo state property (state forgets initial conditions), scale the reservoir so that its spectral radius spectral_radius parameter).
We collect features at time
Notation:
-
$\mathbf{W}$ : reservoir weight matrix -
$\mathbf{W}_{\text{in}}$ : input weight matrix -
$\phi_{\text{poly}}$ : polynomial features -
$\phi_{\text{Fourier}}$ : Fourier features -
$D$ : total feature dimension
-
PolynomialFeatures of degree
$d$ (poly_orderparameter):$[u_t, u_t^2, \dots, u_t^d]$ per input dimension (no extra bias term; bias provided separately). -
Fourier (FAN) features with harmonics
$k=1..K$ (fan_termsparameter): for each input dimension, compute$\sin(2\pi k X)$ and$\cos(2\pi k X)$ . These inject periodic structure explicitly, so the reservoir does not have to "discover" it from scratch.
Only the final linear readout
Columns of StandardScaler).
- Teacher forcing / open loop (default in
predict(X)): one‑step predictions using the provided inputs. - Generative / recursive (
predict(seed, generative_steps=m)): feed back model outputs as inputs to generate future steps. - Hybrid (future option): recursive core with direct corrections for selected horizons.
- Reservoir provides a rich, fading memory of nonlinear histories.
- Fourier layer anchors periodic structure → less burden on the reservoir.
- Polynomial bias stabilizes local trends and offsets.
- Ridge readout tames variance and keeps training convex & fast.
You can compute these to cluster series by predictability and adapt hyperparameters:
- Hurst exponent (H) — persistence (>0.5) vs anti‑persistence (<0.5) vs (=0.5) random walk
- Correlation dimension (D₂) — attractor dimension (Grassberger–Procaccia)
- Max Lyapunov exponent (λₘₐₓ) — sensitivity to initial conditions
- Kolmogorov–Sinai entropy (KSE) — information production rate
- # Prevailing harmonics — count strong spectral peaks (e.g., via periodogram)
Use the cluster to pick reservoir_size, spectral_radius, and fan_terms. For highly chaotic signals (large λₘₐₓ), prefer slightly lower spectral_radius and stronger regularization (ridge_alpha).
NOTATION:
-
$\bar{x}_\tau$ - sample mean on window of length$\tau$ -
$\theta(\cdot)$ - Heaviside step function -
$\rho(i,j)$ - distance in reconstructed phase space (delay embedding optional) -
$x_i' = x_i - x_{i-1}$ (first difference)
where:
Note: In classical R/S analysis,
Definition via entropy-rate upper bound:
where:
| Parameter | Meaning | Typical range / tips |
|---|---|---|
reservoir_size |
Number of reservoir units | 300–2000 |
spectral_radius |
Spectral radius after scaling | 0.7–1.2 with leakage |
sparsity |
Fraction of zeroed connections (mask threshold) | 0.7–0.95 for very sparse reservoirs |
leaking_rate |
Leaky integrator rate | 0.1–0.5 for longer memory |
ridge_alpha |
Ridge regularization strength | 1e−6–1e0 |
poly_order |
Polynomial degree (no bias term) | 1–3 |
fan_terms |
#Fourier harmonics per input dimension | 3–12 |
clip_value |
Clip for scaled inputs in generative mode | 2–5 |
random_state |
Seed | set for reproducibility |
-
State update:
$O(T \cdot N_r \cdot s)$ with sparsity fraction$s$ (dense → $O(T \cdot N_r^2)$) -
Readout training: build
$\mathbf{Z} \in \mathbb{R}^{T \times D}$ ; solve ridge via Cholesky/QR:$\sim O(D^3)$ (usually$D \ll T$ ) -
Memory:
$O(T \cdot D)$ if keeping all features; use chunked/online solvers for very long series
- Fix
random_statefor weights and reservoirs. - Standardize inputs and feature matrix consistently across train/forecast.
- Log: hyperparameters, seeds, and package versions.
- Provide notebooks that mirror experiments and regenerate figures.
| Cluster | ESN‑F | ESN | LGBM | Prophet | SSA |
|---|---|---|---|---|---|
| Good | 3.44 | 3.56 | 3.72 | 6.86 | 18.03 |
| Bad | 5.26 | 5.19 | 5.39 | 8.57 | 20.05 |
In the Bad cluster, ESN‑F beats LGBM by ≥1 pp in 27% of series (LGBM better in 15%; remainder negligible).
| Model | MAPE |
|---|---|
| ESN‑F | 2.56 |
| ESN | 3.19 |
| LGBM | 7.18 |
Chaotic traits of the real‑estate series (for interpretation): Hurst 0.65, Noise 0.99, Corr. dimension 1.33, max Lyapunov 0.01, KSE 1.84, (N_{Fh}=30).
.
├── src/
│ └── chaotic_library/
│ ├── __init__.py # public API (EnhancedESN_FAN, chaos measures, version, etc.)
│ ├── enhanced_esn_fan.py # ESN-FAN implementation
│ └── chaotic_measures.py # Hurst, Lyapunov, entropy, dimensionality, etc.
├── tests/ # unit tests
├── .github/workflows/ # CI (linting, tests)
├── requirements.txt # runtime/dev dependencies
├── pyproject.toml # packaging metadata (build system, project info)
├── README.md
└── LICENSE
- Run linters/formatters before committing (e.g.,
ruff check ./ruff format .). - Add/extend tests in
tests/. - For new feature blocks, include a minimal notebook demo.
- Keep figures reproducible from notebooks where possible.
License: MIT — see LICENSE.
Citation (placeholder): If you use this repository, please cite the corresponding preprint/paper.
@misc{esn_fan_2025,
title = {Enhanced Echo State Network with Fourier Analysis Network (FAN) Features},
author = {Kovantsev, A. and Vysotskiy, R.},
year = {2025},
note = {preprint},
howpublished = {URL: add when available}
}
Q: My recursive (generative) forecast saturates or explodes.
A: Increase ridge_alpha, decrease spectral_radius, and consider a slightly larger clip_value (2–5). Also try lowering leaking_rate for longer memory.
Q: Shapes?
A: X must be 2D: (n_timesteps, input_dim). For univariate, reshape with reshape(-1, 1). y can be 1D (univariate) or 2D (multi‑output).
Q: Scaling consistency between train and predict? A: The model uses internal scalers. Ensure that polynomial and Fourier features at prediction time are computed in a way consistent with training. If you modify the code, apply the same input scaling before feature generation in all paths (teacher forcing and generative).