polydat_nodes/sampling/
lut.rs1use polydat::ast::{CompiledU64Op, NodeMeta, PolydatNode, Port, PortType, Slot, Value};
16
17pub struct LutF64 {
21 lut: Vec<f64>,
24}
25
26impl LutF64 {
27 pub fn from_fn(f: impl Fn(f64) -> f64, resolution: usize) -> Self {
33 assert!(resolution > 0, "resolution must be positive");
34 let mut lut = Vec::with_capacity(resolution + 1);
35 for i in 0..=resolution {
36 let p = i as f64 / resolution as f64;
37 lut.push(f(p));
38 }
39 Self::sanitize(&mut lut);
41 Self { lut }
42 }
43
44 pub fn from_values(values: &[f64]) -> Self {
46 assert!(values.len() >= 2, "LUT must have at least 2 entries");
47 let mut lut = values.to_vec();
48 Self::sanitize(&mut lut);
49 Self { lut }
50 }
51
52 fn sanitize(lut: &mut [f64]) {
54 let mut last_finite = 0.0;
56 let mut found_first = false;
57 for v in lut.iter_mut() {
58 if v.is_finite() {
59 last_finite = *v;
60 found_first = true;
61 } else if found_first {
62 *v = last_finite;
63 }
64 }
65 let mut last_finite = 0.0;
67 for v in lut.iter_mut().rev() {
68 if v.is_finite() {
69 last_finite = *v;
70 } else {
71 *v = last_finite;
72 }
73 }
74 }
75
76 #[inline]
80 pub fn sample(&self, u: f64) -> f64 {
81 let u = u.clamp(0.0, 1.0);
82 let n = (self.lut.len() - 1) as f64;
83 let pos = u * n;
84 let idx = (pos as usize).min(self.lut.len() - 2);
85 let frac = pos - idx as f64;
86 self.lut[idx] * (1.0 - frac) + self.lut[idx + 1] * frac
87 }
88
89 pub fn len(&self) -> usize {
91 self.lut.len()
92 }
93
94 pub fn is_empty(&self) -> bool {
96 self.lut.is_empty()
97 }
98
99 pub fn as_ptr(&self) -> *const f64 {
101 self.lut.as_ptr()
102 }
103
104 pub fn resolution(&self) -> usize {
106 self.lut.len() - 1
107 }
108}
109
110pub struct LutSample {
131 meta: NodeMeta,
132 table: LutF64,
133}
134
135impl LutSample {
136 pub fn new(table: LutF64) -> Self {
138 Self {
139 meta: NodeMeta {
140 name: "lut_sample".into(),
141 outs: vec![Port::new("output", PortType::F64)],
142 ins: vec![Slot::Wire(Port::new("input", PortType::F64))],
143 },
144 table,
145 }
146 }
147}
148
149impl PolydatNode for LutSample {
150 fn meta(&self) -> &NodeMeta {
151 &self.meta
152 }
153
154 fn eval(&self, inputs: &[Value], outputs: &mut [Value]) {
155 outputs[0] = Value::F64(self.table.sample(inputs[0].as_f64()));
156 }
157
158 fn compiled_u64(&self) -> Option<CompiledU64Op> {
159 let lut_addr = self.table.lut.as_ptr() as usize;
163 let lut_len = self.table.lut.len();
164 Some(Box::new(move |inputs, outputs| {
165 let u = f64::from_bits(inputs[0]).clamp(0.0, 1.0);
166 let n = (lut_len - 1) as f64;
167 let pos = u * n;
168 let idx = (pos as usize).min(lut_len - 2);
169 let frac = pos - idx as f64;
170 let result = unsafe {
171 let ptr = lut_addr as *const f64;
172 let a = *ptr.add(idx);
173 let b = *ptr.add(idx + 1);
174 a * (1.0 - frac) + b * frac
175 };
176 outputs[0] = result.to_bits();
177 }))
178 }
179
180 fn jit_constants(&self) -> Vec<u64> {
181 vec![self.table.lut.as_ptr() as u64, self.table.lut.len() as u64]
182 }
183}
184
185impl polydat::derive_support::PolydatSetup for LutF64 {}
190
191fn parse_empirical_lut(spec: &str) -> LutF64 {
194 let mut values: Vec<f64> = spec
195 .split([' ', ',', ';'])
196 .filter(|s| !s.trim().is_empty())
197 .map(|s| {
198 s.trim()
199 .parse::<f64>()
200 .expect("invalid empirical data point")
201 })
202 .collect();
203 assert!(
204 values.len() >= 2,
205 "empirical distribution needs at least 2 data points"
206 );
207 values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
208 LutF64::from_values(&values)
209}
210
211fn dist_empirical_jit_constants(node: &DistEmpirical) -> Vec<u64> {
214 vec![node.table.as_ptr() as u64, node.table.len() as u64]
215}
216
217#[polydat::polydat_node(category = Probability, jit_constants = dist_empirical_jit_constants)]
222fn dist_empirical(
223 input: f64,
224 spec: polydat::derive_support::Const<&str>,
225 #[poly_const(parse_empirical_lut, from = spec)] table: &LutF64,
226) -> f64 {
227 table.sample(input)
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn lut_identity() {
236 let table = LutF64::from_fn(|p| p, 100);
237 assert!((table.sample(0.0) - 0.0).abs() < 1e-10);
238 assert!((table.sample(0.5) - 0.5).abs() < 0.01);
239 assert!((table.sample(1.0) - 1.0).abs() < 1e-10);
240 }
241
242 #[test]
243 fn lut_quadratic() {
244 let table = LutF64::from_fn(|p| p * p, 1000);
245 assert!((table.sample(0.5) - 0.25).abs() < 0.001);
246 assert!((table.sample(0.0) - 0.0).abs() < 1e-10);
247 assert!((table.sample(1.0) - 1.0).abs() < 0.001);
248 }
249
250 #[test]
251 fn lut_clamps_input() {
252 let table = LutF64::from_fn(|p| p * 10.0, 100);
253 assert!((table.sample(-0.5) - 0.0).abs() < 1e-10);
255 assert!((table.sample(1.5) - 10.0).abs() < 1e-10);
257 }
258
259 #[test]
260 fn lut_sanitizes_infinities() {
261 let table = LutF64::from_fn(
262 |p| {
263 if !(0.01..=0.99).contains(&p) {
264 f64::INFINITY
265 } else {
266 p
267 }
268 },
269 100,
270 );
271 assert!(table.sample(0.0).is_finite());
273 assert!(table.sample(1.0).is_finite());
274 }
275
276 #[test]
277 fn lut_from_values() {
278 let table = LutF64::from_values(&[0.0, 5.0, 10.0]);
279 assert!((table.sample(0.0) - 0.0).abs() < 1e-10);
280 assert!((table.sample(0.5) - 5.0).abs() < 1e-10);
281 assert!((table.sample(1.0) - 10.0).abs() < 1e-10);
282 assert!((table.sample(0.25) - 2.5).abs() < 1e-10);
284 }
285
286 #[test]
287 fn lut_node_eval() {
288 let table = LutF64::from_fn(|p| p * 100.0, 1000);
289 let node = LutSample::new(table);
290 let mut out = [Value::None];
291 node.eval(&[Value::F64(0.5)], &mut out);
292 assert!((out[0].as_f64() - 50.0).abs() < 0.1);
293 }
294}