use ndarray::Array1;
use crate::inference::layer_transport::{ChartTopology, FittedTransport};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CircleTransportClass {
Shift,
Reflect,
Mixing,
}
#[derive(Debug, Clone)]
pub struct CircleTransportReport {
pub layer_from: usize,
pub layer_to: usize,
pub n_samples: usize,
pub winding: i8,
pub phase: f64,
pub defect: f64,
pub resultant_shift: f64,
pub resultant_reflect: f64,
pub class: CircleTransportClass,
}
impl CircleTransportReport {
pub fn phase_degrees(&self) -> f64 {
self.phase * 180.0 / std::f64::consts::PI
}
}
pub fn classify_circle_transport(
theta_in: &[f64],
theta_out: &[f64],
layer_from: usize,
layer_to: usize,
) -> Result<CircleTransportReport, String> {
if theta_in.len() != theta_out.len() {
return Err("classify_circle_transport: length mismatch".to_string());
}
let n = theta_in.len();
if n < 4 {
return Err(format!(
"classify_circle_transport: need at least 4 samples, got {n}"
));
}
let nf = n as f64;
let (mut cp, mut sp, mut cm, mut sm) = (0.0_f64, 0.0_f64, 0.0_f64, 0.0_f64);
for (&a, &b) in theta_in.iter().zip(theta_out.iter()) {
let d = b - a;
cp += d.cos();
sp += d.sin();
let s = b + a;
cm += s.cos();
sm += s.sin();
}
let r_shift = (cp * cp + sp * sp).sqrt() / nf;
let r_reflect = (cm * cm + sm * sm).sqrt() / nf;
let (winding, phase, best, other) = if r_shift >= r_reflect {
(1i8, sp.atan2(cp), r_shift, r_reflect)
} else {
(-1i8, sm.atan2(cm), r_reflect, r_shift)
};
let defect = 1.0 - best;
let sep = 2.0 / nf.sqrt();
let class = if best - other <= sep {
CircleTransportClass::Mixing
} else if winding == 1 {
CircleTransportClass::Shift
} else {
CircleTransportClass::Reflect
};
Ok(CircleTransportReport {
layer_from,
layer_to,
n_samples: n,
winding,
phase,
defect,
resultant_shift: r_shift,
resultant_reflect: r_reflect,
class,
})
}
pub fn classify_circle_transport_fit(
fit: &FittedTransport,
from: ChartTopology,
to: ChartTopology,
layer_from: usize,
layer_to: usize,
grid: usize,
) -> Option<CircleTransportReport> {
if !matches!(from, ChartTopology::Circle) || !matches!(to, ChartTopology::Circle) {
return None;
}
let g = grid.max(4);
let theta_in: Vec<f64> = (0..g)
.map(|k| std::f64::consts::TAU * (k as f64) / (g as f64))
.collect();
let out = fit.eval(Array1::from_vec(theta_in.clone()).view()).ok()?;
let theta_out: Vec<f64> = out.to_vec();
classify_circle_transport(&theta_in, &theta_out, layer_from, layer_to).ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn lcg(seed: &mut u64) -> f64 {
*seed = seed
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
((*seed >> 11) as f64) / ((1u64 << 53) as f64)
}
#[test]
fn recovers_shift_phase_and_low_defect() {
let mut s = 3u64;
let phi = 0.9_f64;
let (mut a, mut b) = (Vec::new(), Vec::new());
for _ in 0..512 {
let th = std::f64::consts::TAU * lcg(&mut s) - std::f64::consts::PI;
a.push(th);
b.push(th + phi + 0.01 * (lcg(&mut s) - 0.5));
}
let r = classify_circle_transport(&a, &b, 17, 18).unwrap();
assert_eq!(r.class, CircleTransportClass::Shift);
assert_eq!(r.winding, 1);
assert!((r.phase - phi).abs() < 0.01);
assert!(r.defect < 1e-3);
}
#[test]
fn recovers_reflection() {
let mut s = 5u64;
let phi = -1.3_f64;
let (mut a, mut b) = (Vec::new(), Vec::new());
for _ in 0..512 {
let th = std::f64::consts::TAU * lcg(&mut s) - std::f64::consts::PI;
a.push(th);
b.push(-th + phi);
}
let r = classify_circle_transport(&a, &b, 0, 1).unwrap();
assert_eq!(r.class, CircleTransportClass::Reflect);
assert_eq!(r.winding, -1);
let mut dphi = r.phase - phi;
while dphi > std::f64::consts::PI {
dphi -= std::f64::consts::TAU;
}
while dphi < -std::f64::consts::PI {
dphi += std::f64::consts::TAU;
}
assert!(dphi.abs() < 1e-9);
assert!(r.defect < 1e-12);
}
#[test]
fn scrambled_map_reports_mixing_defect() {
let mut s = 9u64;
let (mut a, mut b) = (Vec::new(), Vec::new());
for _ in 0..512 {
a.push(std::f64::consts::TAU * lcg(&mut s) - std::f64::consts::PI);
b.push(std::f64::consts::TAU * lcg(&mut s) - std::f64::consts::PI);
}
let r = classify_circle_transport(&a, &b, 0, 2).unwrap();
assert!(r.defect > 0.8);
assert_eq!(r.class, CircleTransportClass::Mixing);
}
}