use crate::domain_optimizers::{l2_norm, AdvancedOptimizer, OptimizerStateInfo};
use crate::error::{OptimError, Result};
use scirs2_core::ndarray::Array1;
use scirs2_core::numeric::Float;
use std::fmt::Debug;
#[derive(Debug, Clone)]
pub struct CVOptimizer<T: Float + Debug + Send + Sync + 'static> {
base_lr: T,
current_lr: T,
spatial_lr_scale: T,
channel_normalization: bool,
progressive_resolution: bool,
resolution_phase: usize,
max_resolution_phases: usize,
warmup_steps: usize,
step_count: usize,
momentum: T,
velocity: Option<Array1<T>>,
grad_norm_ema: T,
ema_decay: T,
}
impl<T: Float + Debug + Send + Sync + 'static> CVOptimizer<T> {
pub fn new(base_lr: T) -> Self {
Self {
base_lr,
current_lr: base_lr,
spatial_lr_scale: T::one(),
channel_normalization: false,
progressive_resolution: false,
resolution_phase: 0,
max_resolution_phases: 1,
warmup_steps: 0,
step_count: 0,
momentum: T::from(0.9).expect("0.9 should convert to T"),
velocity: None,
grad_norm_ema: T::zero(),
ema_decay: T::from(0.999).expect("0.999 should convert to T"),
}
}
pub fn with_spatial_lr_scale(mut self, scale: T) -> Self {
self.spatial_lr_scale = scale;
self
}
pub fn with_channel_normalization(mut self, enable: bool) -> Self {
self.channel_normalization = enable;
self
}
pub fn with_progressive_resolution(mut self, enable: bool, max_phases: usize) -> Self {
self.progressive_resolution = enable;
self.max_resolution_phases = if max_phases == 0 { 1 } else { max_phases };
self
}
pub fn with_warmup(mut self, steps: usize) -> Self {
self.warmup_steps = steps;
self
}
pub fn with_momentum(mut self, momentum: T) -> Self {
self.momentum = momentum;
self
}
pub fn advance_resolution_phase(&mut self) -> Result<()> {
if !self.progressive_resolution {
return Err(OptimError::InvalidConfig(
"Progressive resolution is not enabled".to_string(),
));
}
if self.resolution_phase + 1 >= self.max_resolution_phases {
return Err(OptimError::InvalidState(format!(
"Already at maximum resolution phase {}",
self.resolution_phase
)));
}
self.resolution_phase += 1;
Ok(())
}
fn warmup_factor(&self) -> T {
if self.warmup_steps == 0 || self.step_count >= self.warmup_steps {
return T::one();
}
let step_t = T::from(self.step_count + 1).expect("step should convert");
let warmup_t = T::from(self.warmup_steps).expect("warmup should convert");
step_t / warmup_t
}
fn resolution_factor(&self) -> T {
if !self.progressive_resolution || self.max_resolution_phases <= 1 {
return T::one();
}
let phase_t = T::from(self.resolution_phase + 1).expect("phase should convert");
let max_t = T::from(self.max_resolution_phases).expect("max_phases should convert");
phase_t / max_t
}
fn apply_spatial_scaling(&self, gradients: &Array1<T>) -> Array1<T> {
if (self.spatial_lr_scale - T::one()).abs() < T::from(1e-12).expect("epsilon convert") {
return gradients.clone();
}
let len = gradients.len();
let spatial_end = len / 2;
let mut scaled = gradients.clone();
for i in 0..spatial_end {
scaled[i] = scaled[i] * self.spatial_lr_scale;
}
scaled
}
fn apply_channel_normalization(&self, gradients: &Array1<T>) -> Array1<T> {
if !self.channel_normalization {
return gradients.clone();
}
let len = gradients.len();
let num_channels: usize = 4; let chunk_size = if len >= num_channels {
len / num_channels
} else {
return gradients.clone();
};
let mut normalised = gradients.clone();
for ch in 0..num_channels {
let start = ch * chunk_size;
let end = if ch == num_channels - 1 {
len
} else {
start + chunk_size
};
let channel_norm = {
let mut sum_sq = T::zero();
for i in start..end {
sum_sq = sum_sq + normalised[i] * normalised[i];
}
sum_sq.sqrt()
};
let epsilon = T::from(1e-8).expect("epsilon convert");
if channel_norm > epsilon {
for i in start..end {
normalised[i] = normalised[i] / (channel_norm + epsilon);
}
}
}
normalised
}
}
impl<T: Float + Debug + Send + Sync + 'static> AdvancedOptimizer<T> for CVOptimizer<T> {
fn step(&mut self, params: &Array1<T>, gradients: &Array1<T>) -> Result<Array1<T>> {
if params.len() != gradients.len() {
return Err(OptimError::InvalidConfig(format!(
"Parameter length {} != gradient length {}",
params.len(),
gradients.len()
)));
}
if params.is_empty() {
return Err(OptimError::InsufficientData(
"Empty parameter array".to_string(),
));
}
let warmup = self.warmup_factor();
let grad = self.apply_spatial_scaling(gradients);
let grad = self.apply_channel_normalization(&grad);
let norm = l2_norm(&grad);
self.grad_norm_ema =
self.ema_decay * self.grad_norm_ema + (T::one() - self.ema_decay) * norm;
let res_factor = self.resolution_factor();
let effective_lr = self.base_lr * warmup * res_factor;
self.current_lr = effective_lr;
let velocity = match self.velocity.take() {
Some(v) if v.len() == params.len() => v,
_ => Array1::zeros(params.len()),
};
let velocity =
velocity.mapv(|v| v * self.momentum) + &grad.mapv(|g| g * (T::one() - self.momentum));
let updated = params - &velocity.mapv(|v| v * effective_lr);
self.velocity = Some(velocity);
self.step_count += 1;
Ok(updated)
}
fn get_learning_rate(&self) -> T {
self.current_lr
}
fn set_learning_rate(&mut self, lr: T) {
self.base_lr = lr;
self.current_lr = lr;
}
fn name(&self) -> &str {
"CVOptimizer"
}
fn get_state(&self) -> OptimizerStateInfo<T> {
OptimizerStateInfo {
step_count: self.step_count,
current_lr: self.current_lr,
grad_norm_ema: self.grad_norm_ema,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array1;
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
#[test]
fn test_cv_optimizer_basic_step() {
let mut opt = CVOptimizer::new(0.01_f64);
let params = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]);
let grads = Array1::from_vec(vec![0.1, 0.2, 0.3, 0.4]);
let updated = opt.step(¶ms, &grads).expect("step should succeed");
for i in 0..4 {
assert!(updated[i] < params[i], "param {} should decrease", i);
}
assert_eq!(opt.step_count, 1);
}
#[test]
fn test_cv_optimizer_warmup() {
let mut opt = CVOptimizer::new(0.1_f64).with_warmup(10);
let params = Array1::from_vec(vec![1.0, 1.0, 1.0, 1.0]);
let grads = Array1::from_vec(vec![1.0, 1.0, 1.0, 1.0]);
let u1 = opt.step(¶ms, &grads).expect("step should succeed");
let u2 = opt.step(¶ms, &grads).expect("step should succeed");
let diff1 = (¶ms - &u1).mapv(|x| x.abs()).sum();
let diff2 = (¶ms - &u2).mapv(|x| x.abs()).sum();
assert!(
diff2 > diff1,
"warmup should increase update magnitude over time"
);
}
#[test]
fn test_cv_optimizer_spatial_scaling() {
let mut opt_scaled = CVOptimizer::new(0.01_f64)
.with_spatial_lr_scale(2.0)
.with_momentum(0.0);
let mut opt_base = CVOptimizer::new(0.01_f64).with_momentum(0.0);
let params = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]);
let grads = Array1::from_vec(vec![0.1, 0.1, 0.1, 0.1]);
let u_scaled = opt_scaled
.step(¶ms, &grads)
.expect("step should succeed");
let u_base = opt_base.step(¶ms, &grads).expect("step should succeed");
let delta_scaled_spatial = (params[0] - u_scaled[0]).abs();
let delta_base_spatial = (params[0] - u_base[0]).abs();
assert!(
delta_scaled_spatial > delta_base_spatial,
"spatial scaling should amplify spatial updates"
);
}
#[test]
fn test_cv_optimizer_progressive_resolution() {
let mut opt = CVOptimizer::new(0.1_f64).with_progressive_resolution(true, 4);
let params = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]);
let grads = Array1::from_vec(vec![0.5, 0.5, 0.5, 0.5]);
let _u0 = opt.step(¶ms, &grads).expect("step should succeed");
let lr0 = opt.get_learning_rate();
opt.advance_resolution_phase()
.expect("advance should succeed");
let _u1 = opt.step(¶ms, &grads).expect("step should succeed");
let lr1 = opt.get_learning_rate();
assert!(lr1 > lr0, "LR should increase with resolution phase");
}
#[test]
fn test_cv_optimizer_advance_phase_error() {
let mut opt = CVOptimizer::new(0.01_f64).with_progressive_resolution(true, 2);
opt.advance_resolution_phase().expect("first advance ok");
let result = opt.advance_resolution_phase();
assert!(result.is_err(), "should error when at max phase");
}
#[test]
fn test_cv_optimizer_channel_normalization() {
let mut opt = CVOptimizer::new(0.01_f64)
.with_channel_normalization(true)
.with_momentum(0.0);
let params = Array1::from_vec(vec![1.0; 8]);
let grads = Array1::from_vec(vec![100.0, 100.0, 0.01, 0.01, 0.01, 0.01, 0.01, 0.01]);
let updated = opt.step(¶ms, &grads).expect("step should succeed");
let diff_ch0 = (params[0] - updated[0]).abs();
let diff_ch1 = (params[2] - updated[2]).abs();
let ratio = diff_ch0 / (diff_ch1 + 1e-15);
assert!(
ratio < 100.0,
"channel normalization should balance updates, ratio={}",
ratio
);
}
#[test]
fn test_cv_optimizer_state_info() {
let mut opt = CVOptimizer::new(0.05_f64);
let params = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0]);
let grads = Array1::from_vec(vec![0.1, 0.2, 0.3, 0.4]);
let _ = opt.step(¶ms, &grads).expect("step should succeed");
let state = opt.get_state();
assert_eq!(state.step_count, 1);
assert!(approx_eq(state.current_lr, 0.05, 1e-9));
assert!(state.grad_norm_ema > 0.0);
}
#[test]
fn test_cv_optimizer_dimension_mismatch() {
let mut opt = CVOptimizer::new(0.01_f64);
let params = Array1::from_vec(vec![1.0, 2.0]);
let grads = Array1::from_vec(vec![0.1, 0.2, 0.3]);
let result = opt.step(¶ms, &grads);
assert!(result.is_err(), "should error on dimension mismatch");
}
}