use super::*;
use crate::optimizers::{Adam, SGD};
use scirs2_core::ndarray::Ix1;
use std::collections::HashMap;
fn point(features: Vec<f64>) -> StreamingDataPoint<f64> {
StreamingDataPoint {
features: Array1::from_vec(features),
target: Some(Array1::from_vec(vec![1.0])),
timestamp: Instant::now(),
source_id: None,
quality_score: 1.0,
metadata: HashMap::new(),
}
}
fn batch() -> Vec<StreamingDataPoint<f64>> {
vec![
point(vec![1.0, 0.0, 0.5]),
point(vec![0.0, 1.0, -0.5]),
point(vec![0.5, 0.5, 1.0]),
point(vec![-1.0, 0.5, 0.25]),
]
}
fn config() -> StreamingConfig {
let mut config = StreamingConfig::default();
config.buffer_config.initial_size = 4;
config.buffer_config.min_size = 2;
config
}
fn run<O>(mut optimizer: AdaptiveStreamingOptimizer<O, f64, Ix1>) -> Option<Array1<f64>>
where
O: crate::optimizers::Optimizer<f64, Ix1> + Clone,
{
let mut last = None;
for _ in 0..8 {
if let Ok(params) = optimizer.adaptive_step(batch()) {
last = Some(params);
}
}
last
}
#[test]
fn base_optimizer_actually_drives_the_update() {
let adam_result = run(
AdaptiveStreamingOptimizer::new(Adam::new(0.05_f64), config()).expect("construct adam"),
);
let sgd_result =
run(AdaptiveStreamingOptimizer::new(SGD::new(0.05_f64), config()).expect("construct sgd"));
let (adam_params, sgd_params) = match (adam_result, sgd_result) {
(Some(a), Some(s)) => (a, s),
_ => return, };
assert_eq!(adam_params.len(), sgd_params.len());
assert!(
adam_params.iter().all(|v| v.is_finite()),
"Adam-driven parameters must stay finite: {adam_params:?}"
);
let max_difference = adam_params
.iter()
.zip(sgd_params.iter())
.map(|(a, s)| (a - s).abs())
.fold(0.0f64, f64::max);
assert!(
max_difference > 1e-9,
"regression: Adam and SGD produced identical trajectories, so the base \
optimizer is being ignored again (max coordinate difference {max_difference})"
);
}
#[test]
fn mismatched_gradient_dimensionality_is_reported() {
let mut optimizer: AdaptiveStreamingOptimizer<SGD<f64>, f64, Ix1> =
AdaptiveStreamingOptimizer::new(SGD::new(0.05_f64), config()).expect("construct");
for _ in 0..4 {
let _ = optimizer.adaptive_step(batch());
}
if optimizer.parameters.is_none() {
return; }
let narrow: Vec<StreamingDataPoint<f64>> = (0..4).map(|_| point(vec![1.0, 0.0])).collect();
let result = optimizer.perform_optimization_step(&narrow);
assert!(
result.is_err(),
"a parameter/gradient length mismatch must be an honest error, not a silent truncation"
);
}
#[test]
fn recent_adaptations_is_derived_not_stored() {
let optimizer: AdaptiveStreamingOptimizer<SGD<f64>, f64, Ix1> =
AdaptiveStreamingOptimizer::new(SGD::new(0.05_f64), config()).expect("construct");
let stats = optimizer.get_adaptive_stats();
assert_eq!(stats.recent_adaptations, 0);
assert_eq!(stats.adaptations_applied, 0);
assert_eq!(
optimizer.count_adaptations_applied(RECENT_ADAPTATION_WINDOW),
0
);
}
#[test]
fn factory_built_optimizers_can_actually_step() {
let mut optimizer =
crate::streaming::adaptive_streaming::create_default_optimizer::<f64, Ix1>()
.expect("default factory");
for _ in 0..8 {
let _ = optimizer.adaptive_step(batch());
}
let mut configured =
crate::streaming::adaptive_streaming::create_optimizer_with_config::<f64, Ix1>(config())
.expect("configured factory");
for _ in 0..8 {
let _ = configured.adaptive_step(batch());
}
let stats = configured.get_adaptive_stats();
assert!(
stats.total_data_points > 0,
"the factory optimizer never ingested data"
);
assert!(
stats.recent_adaptations <= stats.adaptations_applied,
"the recent-window count cannot exceed the lifetime count"
);
}