use scirs2_core::ndarray::{Array1, Array2, Axis, Zip};
use scirs2_linalg::compat::{Eigh, SVD};
use sklears_core::{
error::{Result, SklearsError},
types::Float,
};
use std::collections::VecDeque;
#[derive(Debug, Clone)]
pub struct StreamingConfig {
pub window_size: usize,
pub forgetting_factor: Float,
pub update_frequency: usize,
pub min_samples: usize,
pub detect_drift: bool,
pub drift_threshold: Float,
}
impl Default for StreamingConfig {
fn default() -> Self {
Self {
window_size: 1000,
forgetting_factor: 0.95,
update_frequency: 10,
min_samples: 50,
detect_drift: true,
drift_threshold: 0.1,
}
}
}
#[derive(Debug, Clone)]
pub struct StreamingPCA {
config: StreamingConfig,
n_components: usize,
components: Option<Array2<Float>>,
explained_variance: Option<Array1<Float>>,
mean: Option<Array1<Float>>,
n_samples_seen: usize,
data_buffer: VecDeque<Array1<Float>>,
covariance_matrix: Option<Array2<Float>>,
sum_weights: Float,
previous_components: Option<Array2<Float>>,
drift_detected: bool,
}
impl StreamingPCA {
pub fn new(n_components: usize) -> Self {
Self {
config: StreamingConfig::default(),
n_components,
components: None,
explained_variance: None,
mean: None,
n_samples_seen: 0,
data_buffer: VecDeque::new(),
covariance_matrix: None,
sum_weights: 0.0,
previous_components: None,
drift_detected: false,
}
}
pub fn with_config(mut self, config: StreamingConfig) -> Self {
self.config = config;
self
}
pub fn partial_fit(&mut self, sample: &Array1<Float>) -> Result<()> {
let n_features = sample.len();
if self.mean.is_none() {
self.mean = Some(Array1::zeros(n_features));
self.covariance_matrix = Some(Array2::zeros((n_features, n_features)));
}
self.data_buffer.push_back(sample.clone());
if self.data_buffer.len() > self.config.window_size {
self.data_buffer.pop_front();
}
self.n_samples_seen += 1;
self.update_statistics(sample)?;
if self.n_samples_seen >= self.config.min_samples
&& self
.n_samples_seen
.is_multiple_of(self.config.update_frequency)
{
self.update_decomposition()?;
}
Ok(())
}
fn update_statistics(&mut self, sample: &Array1<Float>) -> Result<()> {
let weight = 1.0 / (self.n_samples_seen as Float);
let alpha = self.config.forgetting_factor;
if let Some(ref mut mean) = self.mean {
let delta = sample - &*mean;
mean.zip_mut_with(&delta, |m, d| *m += d * weight);
if let Some(ref mut cov) = self.covariance_matrix {
let delta_mean = sample - &*mean;
for i in 0..sample.len() {
for j in 0..sample.len() {
cov[[i, j]] =
alpha * cov[[i, j]] + (1.0 - alpha) * delta_mean[i] * delta_mean[j];
}
}
}
}
Ok(())
}
fn update_decomposition(&mut self) -> Result<()> {
if let Some(ref cov) = self.covariance_matrix {
let symmetric_cov = (cov + &cov.t()) / 2.0;
let (eigenvalues, eigenvectors) = symmetric_cov
.eigh(scirs2_linalg::compat::UPLO::Upper)
.map_err(|e| {
SklearsError::InvalidOperation(format!("Eigendecomposition failed: {:?}", e))
})?;
let mut indices: Vec<usize> = (0..eigenvalues.len()).collect();
indices.sort_by(|&i, &j| {
eigenvalues[j]
.partial_cmp(&eigenvalues[i])
.expect("operation should succeed")
});
let n_components = self.n_components.min(eigenvalues.len());
let mut components = Array2::<Float>::zeros((n_components, cov.nrows()));
let mut explained_variance = Array1::<Float>::zeros(n_components);
for (i, &idx) in indices.iter().take(n_components).enumerate() {
components.row_mut(i).assign(&eigenvectors.column(idx));
explained_variance[i] = eigenvalues[idx].max(0.0);
}
if self.config.detect_drift {
self.detect_concept_drift(&components)?;
}
self.components = Some(components);
self.explained_variance = Some(explained_variance);
}
Ok(())
}
fn detect_concept_drift(&mut self, new_components: &Array2<Float>) -> Result<()> {
if let Some(ref prev_components) = self.previous_components {
let similarity = self.compute_subspace_similarity(prev_components, new_components)?;
if similarity < (1.0 - self.config.drift_threshold) {
self.drift_detected = true;
println!("Concept drift detected! Similarity: {similarity:.4}");
} else {
self.drift_detected = false;
}
}
self.previous_components = Some(new_components.clone());
Ok(())
}
fn compute_subspace_similarity(&self, a: &Array2<Float>, b: &Array2<Float>) -> Result<Float> {
let ab = a.dot(&b.t());
let (_u, s, _vt) = ab
.svd(true)
.map_err(|e| SklearsError::InvalidOperation(format!("SVD failed: {:?}", e)))?;
let similarity = s.iter().map(|&x| x.powi(2)).sum::<Float>() / s.len() as Float;
Ok(similarity)
}
pub fn transform(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
if let (Some(ref components), Some(ref mean)) = (&self.components, &self.mean) {
let centered = x - mean;
Ok(centered.dot(&components.t()))
} else {
Err(SklearsError::NotFitted {
operation: "transform".to_string(),
})
}
}
pub fn components(&self) -> Option<&Array2<Float>> {
self.components.as_ref()
}
pub fn explained_variance(&self) -> Option<&Array1<Float>> {
self.explained_variance.as_ref()
}
pub fn is_drift_detected(&self) -> bool {
self.drift_detected
}
pub fn n_samples_seen(&self) -> usize {
self.n_samples_seen
}
pub fn force_update(&mut self) -> Result<()> {
if self.n_samples_seen >= self.config.min_samples {
self.update_decomposition()
} else {
Err(SklearsError::InvalidParameter {
name: "min_samples".to_string(),
reason: "Not enough samples for decomposition update".to_string(),
})
}
}
pub fn reset(&mut self) {
self.components = None;
self.explained_variance = None;
self.mean = None;
self.n_samples_seen = 0;
self.data_buffer.clear();
self.covariance_matrix = None;
self.sum_weights = 0.0;
self.previous_components = None;
self.drift_detected = false;
}
pub fn transform_sample(&self, sample: &Array1<Float>) -> Result<Array1<Float>> {
if let (Some(ref components), Some(ref mean)) = (&self.components, &self.mean) {
let centered = sample - mean;
Ok(components.dot(¢ered))
} else {
Err(SklearsError::NotFitted {
operation: "transform_sample".to_string(),
})
}
}
pub fn reconstruction_error(&self, sample: &Array1<Float>) -> Result<Float> {
if let (Some(ref components), Some(ref mean)) = (&self.components, &self.mean) {
let centered = sample - mean;
let transformed = components.dot(¢ered);
let reconstructed = components.t().dot(&transformed);
let error = (¢ered - &reconstructed)
.mapv(|x| x.powi(2))
.sum()
.sqrt();
Ok(error)
} else {
Err(SklearsError::NotFitted {
operation: "reconstruction_error".to_string(),
})
}
}
}
#[derive(Debug, Clone)]
pub struct StreamingICA {
config: StreamingConfig,
n_components: usize,
components: Option<Array2<Float>>,
mixing_matrix: Option<Array2<Float>>,
mean: Option<Array1<Float>>,
n_samples_seen: usize,
data_buffer: VecDeque<Array1<Float>>,
learning_rate: Float,
previous_components: Option<Array2<Float>>,
drift_detected: bool,
}
impl StreamingICA {
pub fn new(n_components: usize) -> Self {
Self {
config: StreamingConfig::default(),
n_components,
components: None,
mixing_matrix: None,
mean: None,
n_samples_seen: 0,
data_buffer: VecDeque::new(),
learning_rate: 0.01,
previous_components: None,
drift_detected: false,
}
}
pub fn with_config(mut self, config: StreamingConfig) -> Self {
self.config = config;
self
}
pub fn partial_fit(&mut self, sample: &Array1<Float>) -> Result<()> {
let n_features = sample.len();
if self.mean.is_none() {
self.mean = Some(Array1::zeros(n_features));
self.components = Some(Array2::eye(self.n_components));
}
self.data_buffer.push_back(sample.clone());
if self.data_buffer.len() > self.config.window_size {
self.data_buffer.pop_front();
}
self.n_samples_seen += 1;
self.update_mean(sample)?;
if self.n_samples_seen >= self.config.min_samples
&& self
.n_samples_seen
.is_multiple_of(self.config.update_frequency)
{
self.update_decomposition()?;
}
Ok(())
}
fn update_mean(&mut self, sample: &Array1<Float>) -> Result<()> {
let weight = 1.0 / (self.n_samples_seen as Float);
if let Some(ref mut mean) = self.mean {
let delta = sample - &*mean;
mean.zip_mut_with(&delta, |m, d| *m += d * weight);
}
Ok(())
}
fn update_decomposition(&mut self) -> Result<()> {
if let (Some(ref mean), Some(_)) = (&self.mean, &self.components) {
let recent_samples: Vec<_> = self
.data_buffer
.iter()
.rev()
.take(self.config.update_frequency)
.cloned()
.collect();
for sample in recent_samples {
let centered = &sample - mean;
if let Some(ref mut components) = self.components {
let y = components.dot(¢ered);
let g: Array1<Float> = y.mapv(|val| val.tanh());
let g_prime: Array1<Float> = y.mapv(|val| 1.0 - val.tanh().powi(2));
let outer_product = g
.insert_axis(Axis(1))
.dot(¢ered.clone().insert_axis(Axis(0)));
let diagonal = Array2::from_diag(&g_prime);
let gradient = outer_product - diagonal.dot(&*components);
*components += &(gradient * self.learning_rate);
}
}
if let Some(ref mut components) = self.components {
Self::orthogonalize_components_static(components)?;
}
if self.config.detect_drift {
if let Some(ref components) = self.components {
let components_clone = components.clone();
self.detect_concept_drift(&components_clone)?;
}
}
}
Ok(())
}
#[allow(dead_code)]
fn online_ica_update(
&mut self,
components: &mut Array2<Float>,
x: &Array1<Float>,
) -> Result<()> {
let y = components.dot(x);
let g: Array1<Float> = y.mapv(|val| val.tanh());
let g_prime: Array1<Float> = y.mapv(|val| 1.0 - val.tanh().powi(2));
let outer_product = g.insert_axis(Axis(1)).dot(&x.clone().insert_axis(Axis(0)));
let _identity = Array2::<Float>::eye(self.n_components);
let diagonal = Array2::from_diag(&g_prime);
let gradient = outer_product - diagonal.dot(components);
*components += &(gradient * self.learning_rate);
self.orthogonalize_components(components)?;
Ok(())
}
#[allow(dead_code)]
fn orthogonalize_components(&self, components: &mut Array2<Float>) -> Result<()> {
Self::orthogonalize_components_static(components)
}
fn orthogonalize_components_static(components: &mut Array2<Float>) -> Result<()> {
for i in 0..components.nrows() {
let norm = components.row(i).dot(&components.row(i)).sqrt();
if norm > 1e-10 {
components.row_mut(i).mapv_inplace(|x| x / norm);
}
for j in 0..i {
let projection = components.row(i).dot(&components.row(j));
let row_j = components.row(j).to_owned();
Zip::from(components.row_mut(i))
.and(&row_j)
.for_each(|a, &b| *a -= projection * b);
}
}
Ok(())
}
fn detect_concept_drift(&mut self, new_components: &Array2<Float>) -> Result<()> {
if let Some(ref prev_components) = self.previous_components {
let similarity = self.compute_subspace_similarity(prev_components, new_components)?;
if similarity < (1.0 - self.config.drift_threshold) {
self.drift_detected = true;
println!("Concept drift detected in ICA! Similarity: {similarity:.4}");
} else {
self.drift_detected = false;
}
}
self.previous_components = Some(new_components.clone());
Ok(())
}
fn compute_subspace_similarity(&self, a: &Array2<Float>, b: &Array2<Float>) -> Result<Float> {
let ab = a.dot(&b.t());
let (_u, s, _vt) = ab
.svd(true)
.map_err(|e| SklearsError::InvalidOperation(format!("SVD failed: {:?}", e)))?;
let similarity = s.iter().map(|&x| x.powi(2)).sum::<Float>() / s.len() as Float;
Ok(similarity)
}
pub fn transform(&self, x: &Array2<Float>) -> Result<Array2<Float>> {
if let (Some(ref components), Some(ref mean)) = (&self.components, &self.mean) {
let centered = x - mean;
Ok(components.dot(¢ered.t()).t().to_owned())
} else {
Err(SklearsError::NotFitted {
operation: "transform".to_string(),
})
}
}
pub fn components(&self) -> Option<&Array2<Float>> {
self.components.as_ref()
}
pub fn is_drift_detected(&self) -> bool {
self.drift_detected
}
pub fn n_samples_seen(&self) -> usize {
self.n_samples_seen
}
pub fn transform_sample(&self, sample: &Array1<Float>) -> Result<Array1<Float>> {
if let (Some(ref components), Some(ref mean)) = (&self.components, &self.mean) {
let centered = sample - mean;
Ok(components.dot(¢ered))
} else {
Err(SklearsError::NotFitted {
operation: "transform_sample".to_string(),
})
}
}
pub fn mixing_matrix(&self) -> Option<&Array2<Float>> {
self.mixing_matrix.as_ref()
}
pub fn separate_sources(&self, mixed_sample: &Array1<Float>) -> Result<Array1<Float>> {
if let (Some(ref components), Some(ref mean)) = (&self.components, &self.mean) {
let centered = mixed_sample - mean;
Ok(components.dot(¢ered))
} else {
Err(SklearsError::NotFitted {
operation: "separate_sources".to_string(),
})
}
}
pub fn set_learning_rate(&mut self, learning_rate: Float) {
self.learning_rate = learning_rate;
}
pub fn learning_rate(&self) -> Float {
self.learning_rate
}
pub fn force_update(&mut self) -> Result<()> {
if self.n_samples_seen >= self.config.min_samples {
self.update_decomposition()
} else {
Err(SklearsError::InvalidParameter {
name: "min_samples".to_string(),
reason: "Not enough samples for decomposition update".to_string(),
})
}
}
pub fn reset(&mut self) {
self.components = None;
self.mixing_matrix = None;
self.mean = None;
self.n_samples_seen = 0;
self.data_buffer.clear();
self.previous_components = None;
self.drift_detected = false;
}
}
#[derive(Debug, Clone)]
pub struct AdaptiveDecomposition {
#[allow(dead_code)]
config: StreamingConfig,
active_algorithm: String,
streaming_pca: Option<StreamingPCA>,
streaming_ica: Option<StreamingICA>,
reconstruction_errors: VecDeque<Float>,
adaptation_threshold: Float,
}
impl AdaptiveDecomposition {
pub fn new(n_components: usize) -> Self {
Self {
config: StreamingConfig::default(),
active_algorithm: "PCA".to_string(),
streaming_pca: Some(StreamingPCA::new(n_components)),
streaming_ica: Some(StreamingICA::new(n_components)),
reconstruction_errors: VecDeque::new(),
adaptation_threshold: 0.05,
}
}
pub fn partial_fit(&mut self, sample: &Array1<Float>) -> Result<()> {
if let Some(ref mut pca) = self.streaming_pca {
pca.partial_fit(sample)?;
}
if let Some(ref mut ica) = self.streaming_ica {
ica.partial_fit(sample)?;
}
self.evaluate_and_adapt(sample)?;
Ok(())
}
fn evaluate_and_adapt(&mut self, sample: &Array1<Float>) -> Result<()> {
let pca_error = self.compute_reconstruction_error_pca(sample)?;
let ica_error = self.compute_reconstruction_error_ica(sample)?;
self.reconstruction_errors
.push_back(pca_error.min(ica_error));
if self.reconstruction_errors.len() > 100 {
self.reconstruction_errors.pop_front();
}
if pca_error < ica_error - self.adaptation_threshold {
if self.active_algorithm != "PCA" {
println!("Switching to PCA (error: {pca_error:.4})");
self.active_algorithm = "PCA".to_string();
}
} else if ica_error < pca_error - self.adaptation_threshold
&& self.active_algorithm != "ICA"
{
println!("Switching to ICA (error: {ica_error:.4})");
self.active_algorithm = "ICA".to_string();
}
Ok(())
}
fn compute_reconstruction_error_pca(&self, sample: &Array1<Float>) -> Result<Float> {
if let Some(ref pca) = self.streaming_pca {
if let (Some(components), Some(mean)) = (pca.components(), pca.mean.as_ref()) {
let centered = sample - mean;
let transformed = components.dot(¢ered);
let reconstructed = components.t().dot(&transformed) + mean;
let error = (sample - &reconstructed).mapv(|x| x.powi(2)).sum().sqrt();
return Ok(error);
}
}
Ok(Float::INFINITY)
}
fn compute_reconstruction_error_ica(&self, sample: &Array1<Float>) -> Result<Float> {
if let Some(ref ica) = self.streaming_ica {
if let (Some(components), Some(mean)) = (ica.components(), ica.mean.as_ref()) {
let centered = sample - mean;
let transformed = components.dot(¢ered);
let reconstructed = components.t().dot(&transformed) + mean;
let error = (sample - &reconstructed).mapv(|x| x.powi(2)).sum().sqrt();
return Ok(error);
}
}
Ok(Float::INFINITY)
}
pub fn active_algorithm(&self) -> &str {
&self.active_algorithm
}
pub fn average_reconstruction_error(&self) -> Float {
if self.reconstruction_errors.is_empty() {
0.0
} else {
self.reconstruction_errors.iter().sum::<Float>()
/ self.reconstruction_errors.len() as Float
}
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_streaming_pca_basic() {
let mut pca = StreamingPCA::new(2);
for i in 0..100 {
let sample = Array1::from_vec(vec![i as Float, (i * 2) as Float, (i * 3) as Float]);
pca.partial_fit(&sample).expect("sampling should succeed");
}
assert!(pca.n_samples_seen() == 100);
assert!(pca.components().is_some());
}
#[test]
fn test_streaming_ica_basic() {
let mut ica = StreamingICA::new(2);
for i in 0..100 {
let sample = Array1::from_vec(vec![i as Float, (i * 2) as Float]);
ica.partial_fit(&sample).expect("sampling should succeed");
}
assert!(ica.n_samples_seen() == 100);
assert!(ica.components().is_some());
}
#[test]
fn test_adaptive_decomposition() {
let mut adaptive = AdaptiveDecomposition::new(2);
for i in 0..100 {
let sample = Array1::from_vec(vec![i as Float, (i * 2) as Float]);
adaptive
.partial_fit(&sample)
.expect("sampling should succeed");
}
assert!(adaptive.active_algorithm() == "PCA" || adaptive.active_algorithm() == "ICA");
}
}