1pub use polydat::numeric::vector::{
22 add_f32, add_f32_into, check_lens, cosine_f32, dot_f32, dot_scalar, hash_vec_into, l2sq_f32,
23 l2sq_scalar, lid_mle_of, norm_f32_into, scale_f32, scale_f32_into, xxhash3_vec_into,
24};
25
26#[polydat::polydat_node(category = Arithmetic)]
31fn vec_add(a: &[f32], b: &[f32]) -> Vec<f32> {
32 check_lens("vec_add", a.len(), b.len());
33 add_f32(a, b)
34}
35
36#[polydat::polydat_node(category = Arithmetic)]
39fn vec_scale(a: &[f32], k: f64) -> Vec<f32> {
40 scale_f32(a, k as f32)
41}
42
43#[polydat::polydat_node(category = Arithmetic)]
46fn vec_dot(a: &[f32], b: &[f32]) -> f64 {
47 check_lens("vec_dot", a.len(), b.len());
48 dot_f32(a, b) as f64
49}
50
51#[polydat::polydat_node(category = Arithmetic)]
54fn vec_l2(a: &[f32], b: &[f32]) -> f64 {
55 check_lens("vec_l2", a.len(), b.len());
56 (l2sq_f32(a, b) as f64).sqrt()
57}
58
59#[polydat::polydat_node(category = Arithmetic)]
64fn vec_cosine(a: &[f32], b: &[f32]) -> f64 {
65 check_lens("vec_cosine", a.len(), b.len());
66 cosine_f32(a, b)
67}
68
69#[polydat::polydat_node(category = Arithmetic)]
74fn vec_norm(a: &[f32]) -> Vec<f32> {
75 let mut out = Vec::new();
76 norm_f32_into(a, &mut out);
77 out
78}
79
80#[polydat::polydat_node(category = Arithmetic)]
109fn lid_mle(distances: &[f32], k: f64) -> f64 {
110 lid_mle_of(distances, k)
111}
112
113#[polydat::polydat_node(category = Hashing)]
120fn hash_vec(seed: u64, dim: u64) -> Vec<f32> {
121 let mut out = Vec::new();
122 hash_vec_into(seed, dim, &mut out);
123 out
124}
125
126#[polydat::polydat_node(category = Hashing)]
128fn xxhash3_vec(seed: u64, dim: u64) -> Vec<f32> {
129 let mut out = Vec::new();
130 xxhash3_vec_into(seed, dim, &mut out);
131 out
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use polydat::ast::{PolydatNode, SliceArc, Value};
138
139 fn vecv(v: Vec<f32>) -> Value {
140 Value::VecF32(SliceArc::from_vec(v))
141 }
142
143 fn test_vec(n: usize, seed: u64) -> Vec<f32> {
144 (0..n)
145 .map(|i| {
146 let h = xxhash_rust::xxh3::xxh3_64(&(seed ^ i as u64).to_le_bytes());
147 (h as f64 / u64::MAX as f64 * 2.0 - 1.0) as f32
148 })
149 .collect()
150 }
151
152 fn eval2<N: PolydatNode>(node: &N, a: Vec<f32>, b: Vec<f32>) -> Value {
153 let mut out = [Value::None];
154 node.eval(&[vecv(a), vecv(b)], &mut out);
155 out[0].clone()
156 }
157
158 #[test]
159 fn vec_math_matches_scalar_reference() {
160 let a = test_vec(1029, 1);
162 let b = test_vec(1029, 2);
163
164 let dot = eval2(&VecDot::new(), a.clone(), b.clone()).as_f64();
165 let dot_ref = dot_scalar(&a, &b) as f64;
166 assert!((dot - dot_ref).abs() / dot_ref.abs().max(1e-6) < 1e-4);
167
168 let l2 = eval2(&VecL2::new(), a.clone(), b.clone()).as_f64();
169 let l2_ref = (l2sq_scalar(&a, &b) as f64).sqrt();
170 assert!((l2 - l2_ref).abs() / l2_ref.max(1e-6) < 1e-4);
171
172 let sum = eval2(&VecAdd::new(), a.clone(), b.clone());
173 let sum = sum.as_vec_f32();
174 for i in 0..a.len() {
175 assert_eq!(sum[i], a[i] + b[i], "vec_add lane {i}");
176 }
177
178 let cos_self = eval2(&VecCosine::new(), a.clone(), a.clone()).as_f64();
179 assert!((cos_self - 1.0).abs() < 1e-4, "self-cosine = {cos_self}");
180 }
181
182 #[test]
183 fn vec_scale_and_norm() {
184 let a = test_vec(37, 3);
185 let mut out = [Value::None];
186 VecScale::new().eval(&[vecv(a.clone()), Value::F64(2.0)], &mut out);
187 let scaled = out[0].as_vec_f32();
188 for i in 0..a.len() {
189 assert_eq!(scaled[i], a[i] * 2.0, "vec_scale lane {i}");
190 }
191
192 let mut out = [Value::None];
193 VecNorm::new().eval(&[vecv(a.clone())], &mut out);
194 let unit = out[0].as_vec_f32().to_vec();
195 let mag = (dot_scalar(&unit, &unit) as f64).sqrt();
196 assert!((mag - 1.0).abs() < 1e-4, "norm magnitude = {mag}");
197
198 let mut out = [Value::None];
200 VecNorm::new().eval(&[vecv(vec![0.0; 4])], &mut out);
201 assert_eq!(out[0].as_vec_f32(), &[0.0, 0.0, 0.0, 0.0]);
202 }
203
204 #[test]
205 fn lid_mle_matches_closed_form_and_handles_degenerate() {
206 let dists: Vec<f32> = (0..10).map(|j| (j as f32).exp()).collect();
209 let mut out = [Value::None];
210 LidMle::new().eval(&[vecv(dists), Value::F64(10.0)], &mut out);
211 assert!(
212 (out[0].as_f64() - 0.2).abs() < 1e-4,
213 "got {}",
214 out[0].as_f64()
215 );
216
217 let mut out = [Value::None];
219 LidMle::new().eval(&[vecv(vec![1.0]), Value::F64(10.0)], &mut out);
220 assert_eq!(out[0].as_f64(), 0.0);
221
222 let mut out = [Value::None];
224 LidMle::new().eval(&[vecv(vec![0.0, 0.0, 0.0]), Value::F64(3.0)], &mut out);
225 assert_eq!(out[0].as_f64(), 0.0);
226
227 let mut out = [Value::None];
231 let d: Vec<f32> = vec![
232 0.0,
233 std::f32::consts::E,
234 std::f32::consts::E * std::f32::consts::E,
235 ];
236 LidMle::new().eval(&[vecv(d), Value::F64(3.0)], &mut out);
237 assert!(
238 (out[0].as_f64() - 1.0).abs() < 1e-4,
239 "got {}",
240 out[0].as_f64()
241 );
242 }
243
244 #[test]
245 fn hash_vec_is_deterministic_and_seed_sensitive() {
246 let mut out = [Value::None];
247 HashVec::new().eval(&[Value::U64(7), Value::U64(16)], &mut out);
248 let v1 = out[0].as_vec_f32().to_vec();
249 let mut out = [Value::None];
250 HashVec::new().eval(&[Value::U64(7), Value::U64(16)], &mut out);
251 assert_eq!(v1, out[0].as_vec_f32(), "same seed must reproduce");
252 let mut out = [Value::None];
253 HashVec::new().eval(&[Value::U64(8), Value::U64(16)], &mut out);
254 assert_ne!(v1, out[0].as_vec_f32(), "different seed must differ");
255 assert_eq!(v1.len(), 16);
256 assert!(v1.iter().all(|x| (-1.0..1.0).contains(x)));
257 }
258
259 #[test]
267 fn vec_flow_rides_compiled_kernels() {
268 let src = r#"
269 input cycle: u64
270 a := hash_vec(cycle, 37)
271 b := hash_vec(hash(cycle), 37)
272 s := vec_add(a, b)
273 out := vec_dot(s, b)
274 "#;
275 let mut p1 = polydat::dsl::compile_polydat(src).unwrap();
276
277 let asm = polydat::dsl::compile::compile_polydat_to_assembler(src).unwrap();
278 let mut p2 = asm
279 .try_compile_raw()
280 .expect("slice-bearing nodes are P2-eligible via compiled_slot");
281
282 for cycle in [0u64, 7, 0xFEED] {
283 p1.set_inputs(&[cycle]);
284 let want = p1.pull("out").as_f64();
285 let slot = p2.resolve_output("out").unwrap();
286 let got = f64::from_bits(p2.eval_for_slot(&[cycle], slot));
287 assert_eq!(got, want, "P2 vec flow mismatch at cycle={cycle}");
288 }
289
290 #[cfg(feature = "jit")]
291 {
292 let asm = polydat::dsl::compile::compile_polydat_to_assembler(src).unwrap();
293 let mut hy = asm.compile_hybrid().unwrap();
294 for cycle in [0u64, 7, 0xFEED] {
295 p1.set_inputs(&[cycle]);
296 let want = p1.pull("out").as_f64();
297 let slot = hy.resolve_output("out").unwrap();
298 hy.eval(&[cycle]);
299 let got = f64::from_bits(hy.get_slot(slot));
300 assert_eq!(got, want, "hybrid vec flow mismatch at cycle={cycle}");
301 }
302 }
303 }
304
305 #[test]
309 fn vec_scratch_reuses_across_evals() {
310 let src = r#"
311 input cycle: u64
312 a := hash_vec(cycle, 16)
313 out := vec_l2(a, vec_scale(a, 2.0))
314 "#;
315 let mut p1 = polydat::dsl::compile_polydat(src).unwrap();
316 let asm = polydat::dsl::compile::compile_polydat_to_assembler(src).unwrap();
317 let mut p2 = asm.try_compile_raw().expect("P2-eligible");
318 let slot = p2.resolve_output("out").unwrap();
319 for cycle in [1u64, 9, 1, 42, 9, 1] {
322 p1.set_inputs(&[cycle]);
323 let want = p1.pull("out").as_f64();
324 let got = f64::from_bits(p2.eval_for_slot(&[cycle], slot));
325 assert_eq!(got, want, "scratch reuse mismatch at cycle={cycle}");
326 }
327 }
328
329 #[test]
330 fn vec_length_mismatch_panics() {
331 let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
332 let mut out = [Value::None];
333 VecDot::new().eval(&[vecv(vec![1.0]), vecv(vec![1.0, 2.0])], &mut out);
334 }));
335 assert!(r.is_err(), "vec_dot accepted mismatched lengths");
336 }
337}