use std::collections::HashMap;
use std::fs::File;
use std::io::{Read, Write};
use std::path::Path;
use std::f64::consts::PI;
use anyhow::{bail, Context, Result};
use ndarray::{Array1, Array2, ArrayView1, ArrayView2, Axis};
use rand::prelude::*;
use serde::{Deserialize, Serialize};
fn serialize_array2<S>(array: &Array2<f64>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let shape = array.shape();
let data: Vec<f64> = array.iter().cloned().collect();
(&shape[..], data).serialize(serializer)
}
fn deserialize_array2<'de, D>(deserializer: D) -> Result<Array2<f64>, D::Error>
where
D: serde::Deserializer<'de>,
{
let (shape, data): (Vec<usize>, Vec<f64>) = Deserialize::deserialize(deserializer)?;
Array2::from_shape_vec((shape[0], shape[1]), data)
.map_err(serde::de::Error::custom)
}
fn serialize_array2_option<S>(array: &Option<Array2<f64>>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match array {
Some(arr) => {
let shape = arr.shape();
let data: Vec<f64> = arr.iter().cloned().collect();
Some((&shape[..], data)).serialize(serializer)
}
None => None::<(Vec<usize>, Vec<f64>)>.serialize(serializer),
}
}
fn deserialize_array2_option<'de, D>(deserializer: D) -> Result<Option<Array2<f64>>, D::Error>
where
D: serde::Deserializer<'de>,
{
let opt: Option<(Vec<usize>, Vec<f64>)> = Deserialize::deserialize(deserializer)?;
match opt {
Some((shape, data)) => {
let array = Array2::from_shape_vec((shape[0], shape[1]), data)
.map_err(serde::de::Error::custom)?;
Ok(Some(array))
}
None => Ok(None),
}
}
fn serialize_array1<S>(array: &Array1<f64>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let data: Vec<f64> = array.iter().cloned().collect();
data.serialize(serializer)
}
fn deserialize_array1<'de, D>(deserializer: D) -> Result<Array1<f64>, D::Error>
where
D: serde::Deserializer<'de>,
{
let data: Vec<f64> = Deserialize::deserialize(deserializer)?;
Ok(Array1::from_vec(data))
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EchoStateConfig {
pub reservoir_size: usize,
pub input_scaling: f64,
pub spectral_radius: f64,
pub connectivity: f64,
pub ridge_param: f64,
pub leak_rate: f64,
pub seed: Option<u64>,
}
impl Default for EchoStateConfig {
fn default() -> Self {
Self {
reservoir_size: 100,
input_scaling: 1.0,
spectral_radius: 0.95,
connectivity: 0.1,
ridge_param: 1e-8,
leak_rate: 1.0,
seed: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EchoStateNetwork {
config: EchoStateConfig,
#[serde(serialize_with = "serialize_array2", deserialize_with = "deserialize_array2")]
w_in: Array2<f64>,
#[serde(serialize_with = "serialize_array2", deserialize_with = "deserialize_array2")]
w_res: Array2<f64>,
#[serde(serialize_with = "serialize_array2_option", deserialize_with = "deserialize_array2_option")]
w_out: Option<Array2<f64>>,
#[serde(serialize_with = "serialize_array1", deserialize_with = "deserialize_array1")]
state: Array1<f64>,
input_dim: usize,
output_dim: usize,
}
impl EchoStateNetwork {
pub fn new(config: EchoStateConfig, input_dim: usize, output_dim: usize) -> Result<Self> {
if config.spectral_radius >= 1.0 {
bail!("Spectral radius must be < 1.0 for echo state property");
}
let mut rng = if let Some(seed) = config.seed {
StdRng::seed_from_u64(seed)
} else {
StdRng::from_entropy()
};
let w_in = Self::initialize_input_weights(
&mut rng,
config.reservoir_size,
input_dim,
config.input_scaling,
);
let w_res = Self::initialize_reservoir_weights(
&mut rng,
config.reservoir_size,
config.spectral_radius,
config.connectivity,
)?;
let state = Array1::zeros(config.reservoir_size);
Ok(EchoStateNetwork {
config,
w_in,
w_res,
w_out: None,
state,
input_dim,
output_dim,
})
}
fn initialize_input_weights(
rng: &mut StdRng,
reservoir_size: usize,
input_dim: usize,
input_scaling: f64,
) -> Array2<f64> {
let mut w_in = Array2::zeros((reservoir_size, input_dim));
for elem in w_in.iter_mut() {
*elem = if rng.gen::<f64>() < 0.5 { -1.0 } else { 1.0 };
*elem *= input_scaling;
}
w_in
}
fn initialize_reservoir_weights(
rng: &mut StdRng,
reservoir_size: usize,
spectral_radius: f64,
connectivity: f64,
) -> Result<Array2<f64>> {
let mut w_res = Array2::zeros((reservoir_size, reservoir_size));
let num_connections = (reservoir_size * reservoir_size) as f64 * connectivity;
for _ in 0..num_connections as usize {
let i = rng.gen_range(0..reservoir_size);
let j = rng.gen_range(0..reservoir_size);
w_res[[i, j]] = rng.gen_range(-1.0..1.0);
}
let current_spectral_radius = Self::estimate_spectral_radius(&w_res, 100)?;
if current_spectral_radius > 0.0 {
w_res *= spectral_radius / current_spectral_radius;
}
Ok(w_res)
}
fn estimate_spectral_radius(matrix: &Array2<f64>, max_iterations: usize) -> Result<f64> {
let n = matrix.nrows();
if n != matrix.ncols() {
bail!("Matrix must be square for spectral radius calculation");
}
let mut v = Array1::from_vec(vec![1.0; n]);
let mut eigenvalue = 0.0;
for _ in 0..max_iterations {
let new_v = matrix.dot(&v);
eigenvalue = new_v.dot(&v) / v.dot(&v);
let norm = new_v.dot(&new_v).sqrt();
if norm > 0.0 {
v = new_v / norm;
} else {
break;
}
}
Ok(eigenvalue.abs())
}
pub fn update_state(&mut self, input: ArrayView1<f64>) -> Result<ArrayView1<f64>> {
if input.len() != self.input_dim {
bail!(
"Input dimension mismatch: expected {}, got {}",
self.input_dim,
input.len()
);
}
let input_contribution = self.w_in.dot(&input);
let reservoir_contribution = self.w_res.dot(&self.state);
let raw_state = &input_contribution + &reservoir_contribution;
let new_state = raw_state.mapv(|x| x.tanh());
self.state = (1.0 - self.config.leak_rate) * &self.state + self.config.leak_rate * &new_state;
Ok(self.state.view())
}
pub fn reset_state(&mut self) {
self.state.fill(0.0);
}
pub fn train(&mut self, inputs: ArrayView2<f64>, targets: ArrayView2<f64>) -> Result<f64> {
let (seq_len, input_dim) = inputs.dim();
let (target_seq_len, output_dim) = targets.dim();
if seq_len != target_seq_len {
bail!("Input and target sequences must have same length");
}
if input_dim != self.input_dim {
bail!("Input dimension mismatch");
}
if output_dim != self.output_dim {
bail!("Output dimension mismatch");
}
self.reset_state();
let mut states = Array2::zeros((seq_len, self.input_dim + self.config.reservoir_size));
for t in 0..seq_len {
let input = inputs.row(t);
self.update_state(input)?;
states.row_mut(t).slice_mut(ndarray::s![..self.input_dim]).assign(&input);
states.row_mut(t).slice_mut(ndarray::s![self.input_dim..]).assign(&self.state);
}
let xt = states.t();
let xtx = xt.dot(&states);
let ridge_matrix = Array2::<f64>::eye(xtx.nrows()) * self.config.ridge_param;
let xtx_ridge = xtx + ridge_matrix;
let xty = xt.dot(&targets);
let scale = 1.0 / (xtx_ridge.diag().sum() / xtx_ridge.nrows() as f64);
self.w_out = Some(xty.t().to_owned() * scale);
let predictions = self.predict_batch(&states)?;
let mse = Self::mean_squared_error(&predictions, targets);
Ok(mse)
}
pub fn predict_step(&mut self, input: ArrayView1<f64>) -> Result<Array1<f64>> {
if self.w_out.is_none() {
bail!("Network must be trained before prediction");
}
self.update_state(input)?;
let mut augmented_state = Array1::zeros(self.input_dim + self.config.reservoir_size);
augmented_state.slice_mut(ndarray::s![..self.input_dim]).assign(&input);
augmented_state.slice_mut(ndarray::s![self.input_dim..]).assign(&self.state);
let w_out = self.w_out.as_ref().unwrap();
let prediction = w_out.dot(&augmented_state);
Ok(prediction)
}
fn predict_batch(&self, states: &Array2<f64>) -> Result<Array2<f64>> {
if self.w_out.is_none() {
bail!("Network must be trained before prediction");
}
let w_out = self.w_out.as_ref().unwrap();
let predictions = states.dot(&w_out.t());
Ok(predictions)
}
fn mean_squared_error(predictions: &Array2<f64>, targets: ArrayView2<f64>) -> f64 {
let diff = predictions - &targets;
let squared_diff = diff.mapv(|x| x * x);
squared_diff.mean().unwrap_or(f64::INFINITY)
}
pub fn is_trained(&self) -> bool {
self.w_out.is_some()
}
pub fn get_statistics(&self) -> HashMap<String, f64> {
let mut stats = HashMap::new();
stats.insert("reservoir_size".to_string(), self.config.reservoir_size as f64);
stats.insert("input_dim".to_string(), self.input_dim as f64);
stats.insert("output_dim".to_string(), self.output_dim as f64);
stats.insert("spectral_radius".to_string(), self.config.spectral_radius);
stats.insert("connectivity".to_string(), self.config.connectivity);
stats.insert("is_trained".to_string(), if self.is_trained() { 1.0 } else { 0.0 });
stats
}
}
fn main() -> Result<()> {
println!("🧠Standalone Echo State Network Demonstration");
println!("============================================");
let config = EchoStateConfig {
reservoir_size: 50,
input_scaling: 1.0,
spectral_radius: 0.95,
connectivity: 0.1,
ridge_param: 1e-6,
leak_rate: 1.0,
seed: Some(42),
};
println!("📋 Configuration:");
println!(" Reservoir size: {}", config.reservoir_size);
println!(" Spectral radius: {:.3}", config.spectral_radius);
println!(" Connectivity: {:.1}%", config.connectivity * 100.0);
let mut esn = EchoStateNetwork::new(config, 1, 1)?;
println!("\n✅ Echo State Network created");
let n_train = 100;
let mut inputs = Array2::zeros((n_train, 1));
let mut targets = Array2::zeros((n_train, 1));
for i in 0..n_train {
let t = i as f64 * 0.1;
inputs[[i, 0]] = (t * PI / 4.0).sin();
targets[[i, 0]] = ((t + 0.1) * PI / 4.0).sin(); }
println!("📊 Generated {} training samples", n_train);
println!("\n🎯 Training Echo State Network...");
let mse = esn.train(inputs.view(), targets.view())?;
println!(" Training MSE: {:.6}", mse);
println!("\n🔮 Testing predictions...");
for i in 0..5 {
let t = (100 + i) as f64 * 0.1;
let expected = ((t + 0.1) * PI / 4.0).sin();
let test_input = Array1::from_vec(vec![(t * PI / 4.0).sin()]);
let prediction = esn.predict_step(test_input.view())?;
let error = (prediction[0] - expected).abs();
println!(" Step {}: input={:.4}, predicted={:.4}, expected={:.4}, error={:.4}",
i + 1, test_input[0], prediction[0], expected, error);
}
println!("\n📈 Network Statistics:");
let stats = esn.get_statistics();
for (key, value) in stats.iter() {
println!(" {}: {:.3}", key, value);
}
println!("\n🎉 Standalone Echo State Network demonstration completed!");
println!(" ✓ Random reservoir initialization with spectral radius control");
println!(" ✓ Ridge regression for output weight training");
println!(" ✓ Real matrix operations using ndarray");
println!(" ✓ Step-by-step forecasting capability");
println!(" ✓ Mathematical foundations verified");
Ok(())
}