use crate::common::{cast_positive, cast_scalar};
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) -> Result<Self> {
Ok(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: cast_scalar(0.9)?,
velocity: None,
grad_norm_ema: T::zero(),
ema_decay: cast_scalar(0.999)?,
})
}
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) -> Result<T> {
if self.warmup_steps == 0 || self.step_count >= self.warmup_steps {
return Ok(T::one());
}
let step_t: T = cast_scalar(self.step_count + 1)?;
let warmup_t: T = cast_positive(self.warmup_steps, "warmup_steps")?;
Ok(step_t / warmup_t)
}
fn resolution_factor(&self) -> Result<T> {
if !self.progressive_resolution || self.max_resolution_phases <= 1 {
return Ok(T::one());
}
let phase_t: T = cast_scalar(self.resolution_phase + 1)?;
let max_t: T = cast_positive(self.max_resolution_phases, "max_resolution_phases")?;
Ok(phase_t / max_t)
}
fn apply_spatial_scaling(&self, gradients: &Array1<T>) -> Result<Array1<T>> {
let epsilon: T = cast_scalar(1e-12)?;
if (self.spatial_lr_scale - T::one()).abs() < epsilon {
return Ok(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;
}
Ok(scaled)
}
fn apply_channel_normalization(&self, gradients: &Array1<T>) -> Result<Array1<T>> {
if !self.channel_normalization {
return Ok(gradients.clone());
}
let len = gradients.len();
let num_channels: usize = 4; let chunk_size = if len >= num_channels {
len / num_channels
} else {
return Ok(gradients.clone());
};
let epsilon: T = cast_scalar(1e-8)?;
let vec_norm =
|v: &Array1<T>| -> T { v.iter().fold(T::zero(), |acc, &g| acc + g * g).sqrt() };
let orig_norm = vec_norm(gradients);
if orig_norm <= epsilon {
return Ok(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()
};
if channel_norm > epsilon {
for i in start..end {
normalised[i] = normalised[i] / channel_norm;
}
}
}
let new_norm = vec_norm(&normalised);
if new_norm > epsilon {
let scale = orig_norm / new_norm;
normalised.mapv_inplace(|v| v * scale);
}
Ok(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).expect("optimizer");
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)
.expect("optimizer")
.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)
.expect("optimizer")
.with_spatial_lr_scale(2.0)
.with_momentum(0.0);
let mut opt_base = CVOptimizer::new(0.01_f64)
.expect("optimizer")
.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)
.expect("optimizer")
.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)
.expect("optimizer")
.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)
.expect("optimizer")
.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_channel_normalization_converges() {
let mut opt = CVOptimizer::new(0.1_f64)
.expect("optimizer")
.with_channel_normalization(true)
.with_momentum(0.0);
let mut x = Array1::from_vec(vec![1.0_f64; 8]);
for _ in 0..300 {
let grad = x.mapv(|v| 2.0 * v);
x = opt.step(&x, &grad).expect("step should succeed");
}
let final_norm = l2_norm(&x);
assert!(
final_norm < 1e-3,
"channel-normalized optimizer must converge, got norm {final_norm}"
);
}
#[test]
fn test_cv_optimizer_state_info() {
let mut opt = CVOptimizer::new(0.05_f64).expect("optimizer");
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).expect("optimizer");
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");
}
}