use crate::node::{CompiledU64Op, GkNode, NodeMeta, Port, PortType, Slot, Value};
pub struct LutF64 {
lut: Vec<f64>,
}
impl LutF64 {
pub fn from_fn(f: impl Fn(f64) -> f64, resolution: usize) -> Self {
assert!(resolution > 0, "resolution must be positive");
let mut lut = Vec::with_capacity(resolution + 1);
for i in 0..=resolution {
let p = i as f64 / resolution as f64;
lut.push(f(p));
}
Self::sanitize(&mut lut);
Self { lut }
}
pub fn from_values(values: &[f64]) -> Self {
assert!(values.len() >= 2, "LUT must have at least 2 entries");
let mut lut = values.to_vec();
Self::sanitize(&mut lut);
Self { lut }
}
fn sanitize(lut: &mut [f64]) {
let mut last_finite = 0.0;
let mut found_first = false;
for v in lut.iter_mut() {
if v.is_finite() {
last_finite = *v;
found_first = true;
} else if found_first {
*v = last_finite;
}
}
let mut last_finite = 0.0;
for v in lut.iter_mut().rev() {
if v.is_finite() {
last_finite = *v;
} else {
*v = last_finite;
}
}
}
#[inline]
pub fn sample(&self, u: f64) -> f64 {
let u = u.clamp(0.0, 1.0);
let n = (self.lut.len() - 1) as f64;
let pos = u * n;
let idx = (pos as usize).min(self.lut.len() - 2);
let frac = pos - idx as f64;
self.lut[idx] * (1.0 - frac) + self.lut[idx + 1] * frac
}
pub fn len(&self) -> usize {
self.lut.len()
}
pub fn as_ptr(&self) -> *const f64 {
self.lut.as_ptr()
}
pub fn resolution(&self) -> usize {
self.lut.len() - 1
}
}
pub struct LutSample {
meta: NodeMeta,
table: LutF64,
}
impl LutSample {
pub fn new(table: LutF64) -> Self {
Self {
meta: NodeMeta {
name: "lut_sample".into(),
outs: vec![Port::new("output", PortType::F64)],
ins: vec![Slot::Wire(Port::new("input", PortType::F64))],
},
table,
}
}
}
impl GkNode for LutSample {
fn meta(&self) -> &NodeMeta {
&self.meta
}
fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
outputs[0] = Value::F64(self.table.sample(inputs[0].as_f64()));
}
fn compiled_u64(&self) -> Option<CompiledU64Op> {
let lut_addr = self.table.lut.as_ptr() as usize;
let lut_len = self.table.lut.len();
Some(Box::new(move |inputs, outputs| {
let u = f64::from_bits(inputs[0]).clamp(0.0, 1.0);
let n = (lut_len - 1) as f64;
let pos = u * n;
let idx = (pos as usize).min(lut_len - 2);
let frac = pos - idx as f64;
let result = unsafe {
let ptr = lut_addr as *const f64;
let a = *ptr.add(idx);
let b = *ptr.add(idx + 1);
a * (1.0 - frac) + b * frac
};
outputs[0] = result.to_bits();
}))
}
fn jit_constants(&self) -> Vec<u64> {
vec![self.table.lut.as_ptr() as u64, self.table.lut.len() as u64]
}
}
pub struct EmpiricalSample {
meta: NodeMeta,
table: LutF64,
}
impl EmpiricalSample {
pub fn from_spec(spec: &str) -> Self {
let mut values: Vec<f64> = spec.split([' ', ',', ';'])
.filter(|s| !s.trim().is_empty())
.map(|s| s.trim().parse::<f64>().expect("invalid empirical data point"))
.collect();
assert!(values.len() >= 2, "empirical distribution needs at least 2 data points");
values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let table = LutF64::from_values(&values);
Self {
meta: NodeMeta {
name: "dist_empirical".into(),
outs: vec![Port::new("output", PortType::F64)],
ins: vec![Slot::Wire(Port::new("input", PortType::F64))],
},
table,
}
}
}
impl GkNode for EmpiricalSample {
fn meta(&self) -> &NodeMeta { &self.meta }
fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
outputs[0] = Value::F64(self.table.sample(inputs[0].as_f64()));
}
fn compiled_u64(&self) -> Option<CompiledU64Op> {
let lut_addr = self.table.as_ptr() as usize;
let lut_len = self.table.len();
Some(Box::new(move |inputs, outputs| {
let u = f64::from_bits(inputs[0]).clamp(0.0, 1.0);
let n = (lut_len - 1) as f64;
let pos = u * n;
let idx = (pos as usize).min(lut_len - 2);
let frac = pos - idx as f64;
let result = unsafe {
let ptr = lut_addr as *const f64;
let a = *ptr.add(idx);
let b = *ptr.add(idx + 1);
a * (1.0 - frac) + b * frac
};
outputs[0] = result.to_bits();
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lut_identity() {
let table = LutF64::from_fn(|p| p, 100);
assert!((table.sample(0.0) - 0.0).abs() < 1e-10);
assert!((table.sample(0.5) - 0.5).abs() < 0.01);
assert!((table.sample(1.0) - 1.0).abs() < 1e-10);
}
#[test]
fn lut_quadratic() {
let table = LutF64::from_fn(|p| p * p, 1000);
assert!((table.sample(0.5) - 0.25).abs() < 0.001);
assert!((table.sample(0.0) - 0.0).abs() < 1e-10);
assert!((table.sample(1.0) - 1.0).abs() < 0.001);
}
#[test]
fn lut_clamps_input() {
let table = LutF64::from_fn(|p| p * 10.0, 100);
assert!((table.sample(-0.5) - 0.0).abs() < 1e-10);
assert!((table.sample(1.5) - 10.0).abs() < 1e-10);
}
#[test]
fn lut_sanitizes_infinities() {
let table = LutF64::from_fn(
|p| {
if p < 0.01 || p > 0.99 {
f64::INFINITY
} else {
p
}
},
100,
);
assert!(table.sample(0.0).is_finite());
assert!(table.sample(1.0).is_finite());
}
#[test]
fn lut_from_values() {
let table = LutF64::from_values(&[0.0, 5.0, 10.0]);
assert!((table.sample(0.0) - 0.0).abs() < 1e-10);
assert!((table.sample(0.5) - 5.0).abs() < 1e-10);
assert!((table.sample(1.0) - 10.0).abs() < 1e-10);
assert!((table.sample(0.25) - 2.5).abs() < 1e-10);
}
#[test]
fn lut_node_eval() {
let table = LutF64::from_fn(|p| p * 100.0, 1000);
let node = LutSample::new(table);
let mut out = [Value::None];
node.eval(&[Value::F64(0.5)], &mut out);
assert!((out[0].as_f64() - 50.0).abs() < 0.1);
}
}