File size: 3,706 Bytes
5e84645
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
//! Composite + data energy — the `render` composite loop and `data_energy` from Python.
//!
//! `E_data = (1/l0)·Σ_p ‖Render(P)[p] − I[p]‖²`.
//!
//! **Summation.** Python computes `np.sum(r*r, axis=2)` (an exact 3-term per-pixel sum) and then
//! `.sum()` over the H×W plane, where numpy uses *pairwise* summation. Reproducing numpy's exact
//! pairwise blocking would be brittle, so the per-pixel grouping is mirrored exactly and the outer
//! reduction uses **Neumaier compensated summation** instead. Both are accurate to ~1 ulp of the
//! true sum, so they agree to ~1e-16 relative — five orders of magnitude inside the 1e-9 gate,
//! and *closer to the true value* than a naive sequential sum would be.

use crate::model::Vectors;

/// Neumaier (improved Kahan) compensated summation.
#[derive(Default, Clone, Copy)]
pub struct Neumaier {
    sum: f64,
    c: f64,
}

impl Neumaier {
    #[inline]
    pub fn add(&mut self, x: f64) {
        let t = self.sum + x;
        if self.sum.abs() >= x.abs() {
            self.c += (self.sum - t) + x;
        } else {
            self.c += (x - t) + self.sum;
        }
        self.sum = t;
    }

    #[inline]
    pub fn total(&self) -> f64 {
        self.sum + self.c
    }
}

pub fn sum_compensated(xs: impl Iterator<Item = f64>) -> f64 {
    let mut s = Neumaier::default();
    for x in xs {
        s.add(x);
    }
    s.total()
}

/// The composite loop from `render`: `img = bg + Σ_R cov_R·(c_R − bg)`, then clipped to [0,1].
/// Regions are visited in face order, matching Python's dict iteration.
pub fn compose(v: &Vectors, cov: &[(i64, Vec<f64>)]) -> Vec<f64> {
    let bg = v.background;
    let n = v.width * v.height;
    let mut img = vec![bg; n * 3];
    let colors = v.colors01();
    for (label, cvr) in cov {
        let c = colors
            .get(label)
            .unwrap_or_else(|| panic!("region {label} has no exported color"));
        let (d0, d1, d2) = (c[0] - bg, c[1] - bg, c[2] - bg);
        for p in 0..n {
            let a = cvr[p];
            img[p * 3] += a * d0;
            img[p * 3 + 1] += a * d1;
            img[p * 3 + 2] += a * d2;
        }
    }
    for x in img.iter_mut() {
        *x = x.clamp(0.0, 1.0);
    }
    img
}

/// `E_data`, normalized by `l0`.
pub fn e_data(img: &[f64], target: &[f64], l0: f64) -> f64 {
    let n = img.len() / 3;
    let per_pixel = (0..n).map(|p| {
        // exact 3-term group, mirroring np.sum(r*r, axis=2)
        let d0 = img[p * 3] - target[p * 3];
        let d1 = img[p * 3 + 1] - target[p * 3 + 1];
        let d2 = img[p * 3 + 2] - target[p * 3 + 2];
        d0 * d0 + d1 * d1 + d2 * d2
    });
    sum_compensated(per_pixel) / l0
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn neumaier_beats_naive_on_a_hostile_sequence() {
        // 1.0 followed by many tiny values: naive f64 loses all of them.
        let mut xs = vec![1.0_f64];
        xs.extend(std::iter::repeat(1e-17).take(1000));
        let naive = xs.iter().fold(0.0_f64, |a, b| a + b);
        let comp = sum_compensated(xs.iter().copied());
        assert_eq!(naive, 1.0, "naive summation should lose the tail");
        assert!(comp > 1.0, "compensated summation should keep it, got {comp}");
    }

    #[test]
    fn energy_is_zero_for_a_perfect_match() {
        let img = vec![0.25, 0.5, 0.75, 0.1, 0.2, 0.3];
        let e = e_data(&img, &img, 2.0);
        assert_eq!(e, 0.0);
    }

    #[test]
    fn energy_scales_by_inverse_l0() {
        let img = vec![1.0, 1.0, 1.0];
        let tgt = vec![0.0, 0.0, 0.0];
        assert!((e_data(&img, &tgt, 1.0) - 3.0).abs() < 1e-15);
        assert!((e_data(&img, &tgt, 3.0) - 1.0).abs() < 1e-15);
    }
}