use crate::error::{SpatialError, SpatialResult};
use scirs2_core::ndarray::Array1;
use scirs2_core::random::{Rng, RngExt};
use std::f64::consts::PI;
#[derive(Debug, Clone)]
pub struct ClassicalAdaptation {
adaptation_rate: f64,
quantum_noise_level: f64,
}
impl ClassicalAdaptation {
pub fn new(adaptation_rate: f64, quantum_noise_level: f64) -> Self {
Self {
adaptation_rate,
quantum_noise_level,
}
}
pub fn adaptation_rate(&self) -> f64 {
self.adaptation_rate
}
pub fn quantum_noise_level(&self) -> f64 {
self.quantum_noise_level
}
pub fn adapt(&self, params: &Array1<f64>) -> Array1<f64> {
let sigma = self.adaptation_rate * (1.0 + self.quantum_noise_level).sqrt();
let mut rng = scirs2_core::random::rng();
let mut result = params.clone();
for val in result.iter_mut() {
let noise = gaussian_noise(&mut rng, sigma);
*val += noise;
}
result
}
pub fn anneal(&self, energy: f64, temperature: f64) -> f64 {
if temperature <= 0.0 {
if energy <= 0.0 {
1.0
} else {
0.0
}
} else {
let transverse_field = 1.0 + self.quantum_noise_level;
let exponent = -energy / (temperature * transverse_field);
exponent.exp().clamp(0.0, 1.0)
}
}
pub fn decohere(
&self,
amplitudes: &Array1<f64>,
elapsed_time: f64,
) -> SpatialResult<Array1<f64>> {
if elapsed_time < 0.0 {
return Err(SpatialError::InvalidInput(
"elapsed_time must be non-negative".to_string(),
));
}
let decoherence_rate = self.adaptation_rate * self.quantum_noise_level + 1e-12;
let t2 = 1.0 / decoherence_rate;
let decay = (-elapsed_time / t2).exp();
Ok(amplitudes.mapv(|a| a * decay))
}
pub fn smooth_landscape(&self, landscape: &Array1<f64>) -> SpatialResult<Array1<f64>> {
let n = landscape.len();
if n == 0 {
return Err(SpatialError::InvalidInput(
"landscape must be non-empty".to_string(),
));
}
let sigma = (self.adaptation_rate * (n as f64).sqrt()).max(0.5);
let half_width = (3.0 * sigma).ceil() as usize;
let kernel_len = 2 * half_width + 1;
let mut kernel = vec![0.0f64; kernel_len];
let mut kernel_sum = 0.0f64;
for (k, kval) in kernel.iter_mut().enumerate() {
let offset = k as f64 - half_width as f64;
let g = (-0.5 * (offset / sigma).powi(2)).exp() / (sigma * (2.0 * PI).sqrt());
*kval = g;
kernel_sum += g;
}
if kernel_sum > 1e-12 {
for kval in kernel.iter_mut() {
*kval /= kernel_sum;
}
}
let mut smoothed = Array1::<f64>::zeros(n);
for i in 0..n {
let mut acc = 0.0f64;
for (k, &kval) in kernel.iter().enumerate() {
let offset = k as isize - half_width as isize;
let idx = (i as isize + offset).clamp(0, n as isize - 1) as usize;
acc += kval * landscape[idx];
}
smoothed[i] = acc;
}
Ok(smoothed)
}
}
fn gaussian_noise(rng: &mut impl Rng, sigma: f64) -> f64 {
if sigma.abs() < 1e-15 {
return 0.0;
}
let u1: f64 = rng.random_range(1e-10_f64..1.0_f64);
let u2: f64 = rng.random_range(0.0_f64..1.0_f64);
sigma * (-2.0 * u1.ln()).sqrt() * (2.0 * PI * u2).cos()
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array1;
#[test]
fn test_adapt_output_shape_and_finite() {
let adapter = ClassicalAdaptation::new(0.1, 0.05);
let params = Array1::from_vec(vec![1.0, 2.0, -1.0, 0.5]);
let adapted = adapter.adapt(¶ms);
assert_eq!(adapted.len(), params.len());
for &v in adapted.iter() {
assert!(v.is_finite(), "adapted value must be finite");
}
}
#[test]
fn test_anneal_probability_bounds() {
let adapter = ClassicalAdaptation::new(0.1, 0.05);
let p_uphill = adapter.anneal(10.0, 1.0);
assert!(p_uphill > 0.0 && p_uphill < 1.0);
let p_down = adapter.anneal(-5.0, 1.0);
assert!(p_down >= 1.0 - 1e-9, "downhill acceptance should be ≥ 1");
let p_cold_up = adapter.anneal(1.0, 0.0);
assert_eq!(p_cold_up, 0.0);
let p_cold_down = adapter.anneal(-1.0, 0.0);
assert_eq!(p_cold_down, 1.0);
}
#[test]
fn test_decohere_decay() {
let adapter = ClassicalAdaptation::new(1.0, 1.0);
let amps = Array1::from_vec(vec![1.0, 0.5, -0.5]);
let t0 = adapter.decohere(&s, 0.0).expect("t=0 is valid");
for (&a, &b) in amps.iter().zip(t0.iter()) {
assert!((a - b).abs() < 1e-12);
}
let t1 = adapter.decohere(&s, 1.0).expect("t=1 is valid");
for (&a, &b) in amps.iter().zip(t1.iter()) {
assert!(b.abs() < a.abs() + 1e-12);
}
assert!(adapter.decohere(&s, -1.0).is_err());
}
#[test]
fn test_smooth_landscape_energy_preservation() {
let adapter = ClassicalAdaptation::new(0.5, 0.1);
let landscape = Array1::from_vec(vec![0.0, 1.0, 5.0, 1.0, 0.0, 2.0, 0.0]);
let smoothed = adapter
.smooth_landscape(&landscape)
.expect("smoothing should succeed");
assert_eq!(smoothed.len(), landscape.len());
for &v in smoothed.iter() {
assert!(v.is_finite());
}
let max_idx = smoothed
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap_or(0);
assert!(
max_idx <= 4,
"smoothed peak should remain near original spike, got index {max_idx}"
);
let empty = Array1::<f64>::zeros(0);
assert!(adapter.smooth_landscape(&empty).is_err());
}
}