ph-color-bake 0.1.0

Host-side generator for auditable ph-color matrices, fixed-point LUTs, and golden vectors
Documentation
//! Transfer-function LUT baking.

/// Baked knot table plus measured max absolute error in UQ0.16 LSBs.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BakedLut<const N: usize> {
    /// Knot samples in UQ0.16.
    pub knots: [u16; N],
    /// Maximum |lut(x) - round(f64(x))| over a dense UQ0.16 grid.
    pub max_err_lsb: u16,
}

/// Valid `InterpLut` lengths.
pub const LUT_LENGTHS: [usize; 4] = [17, 33, 65, 257];

/// Bake `f: [0,1] → [0,1]` into an `N = 2^k+1` knot table.
#[must_use]
pub fn bake_lut<const N: usize>(f: impl Fn(f64) -> f64) -> BakedLut<N> {
    assert!(LUT_LENGTHS.contains(&N), "N must be 17, 33, 65, or 257");
    let mut knots = [0u16; N];
    let den = (N - 1) as f64;
    for (i, slot) in knots.iter_mut().enumerate() {
        let x = i as f64 / den;
        *slot = u16_from_unit(f(x));
    }
    let max_err_lsb = measure_err::<N>(&knots, &f);
    BakedLut { knots, max_err_lsb }
}

pub(crate) fn u16_from_unit(v: f64) -> u16 {
    let v = v.clamp(0.0, 1.0);
    let scaled = v * f64::from(u16::MAX);
    let rounded = if scaled >= 0.0 {
        (scaled + 0.5).floor()
    } else {
        (scaled - 0.5).ceil()
    };
    rounded as u16
}

pub(crate) fn lerp_u16(a: u16, b: u16, t: u16) -> u16 {
    if t == 0 {
        return a;
    }
    if t == u16::MAX {
        return b;
    }
    let den = i64::from(u16::MAX);
    let num = i64::from(a) * (den - i64::from(t)) + i64::from(b) * i64::from(t);
    let quot = num / den;
    let rem = num % den;
    let out = if rem.saturating_mul(2) >= den {
        quot + 1
    } else {
        quot
    };
    out.clamp(0, i64::from(u16::MAX)) as u16
}

pub(crate) fn lookup<const N: usize>(knots: &[u16; N], x: u16) -> u16 {
    let segs = (N - 1) as u64;
    let den = u64::from(u16::MAX);
    let pos = u64::from(x) * segs;
    let idx = (pos / den) as usize;
    if idx >= N - 1 {
        return knots[N - 1];
    }
    let rem = (pos % den) as u16;
    lerp_u16(knots[idx], knots[idx + 1], rem)
}

fn measure_err<const N: usize>(knots: &[u16; N], f: &impl Fn(f64) -> f64) -> u16 {
    let mut max_err = 0u16;
    for x in 0..=u16::MAX {
        let baked = lookup(knots, x);
        let reference = u16_from_unit(f(f64::from(x) / f64::from(u16::MAX)));
        let err = baked.abs_diff(reference);
        if err > max_err {
            max_err = err;
        }
    }
    max_err
}

/// Identity transfer (linear).
#[must_use]
pub fn identity(x: f64) -> f64 {
    x
}

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

    #[test]
    fn identity_lut_has_finite_error() {
        let baked = bake_lut::<17>(identity);
        assert_eq!(baked.knots[0], 0);
        assert_eq!(baked.knots[16], 65535);
        let _ = baked.max_err_lsb;
    }
}