1use crate::bounds::{checked_mul, decoded_len, encode_u32_len, u32_to_usize};
23use crate::error::CodecError;
24
25pub fn encode(data: &[f32], dims: usize, vectors: usize) -> Result<Vec<u8>, CodecError> {
30 let (dims_header, vectors_header) = validate_shape(data.len(), dims, vectors)?;
31 if data.is_empty() || dims == 0 {
32 return Ok(build_header(dims_header, vectors_header, 0, &[]));
33 }
34
35 let mut transformed = Vec::with_capacity(data.len());
37 for v in 0..vectors {
38 let start = v * dims;
39 let vec_data = &data[start..start + dims];
40 let spherical = cartesian_to_spherical(vec_data);
41 transformed.extend_from_slice(&spherical);
42 }
43
44 let raw_bytes: Vec<u8> = transformed.iter().flat_map(|f| f.to_le_bytes()).collect();
46 let compressed = crate::lz4::encode(&raw_bytes)?;
47
48 Ok(build_header(
49 dims_header,
50 vectors_header,
51 1, &compressed,
53 ))
54}
55
56pub fn encode_raw(data: &[f32], dims: usize, vectors: usize) -> Result<Vec<u8>, CodecError> {
61 let (dims_header, vectors_header) = validate_shape(data.len(), dims, vectors)?;
62 let raw_bytes: Vec<u8> = data.iter().flat_map(|f| f.to_le_bytes()).collect();
63 let compressed = crate::lz4::encode(&raw_bytes)?;
64
65 Ok(build_header(dims_header, vectors_header, 0, &compressed))
66}
67
68fn validate_shape(
69 actual_elements: usize,
70 dims: usize,
71 vectors: usize,
72) -> Result<(u32, u32), CodecError> {
73 if dims == 0 && vectors != 0 {
74 return Err(CodecError::Corrupt {
75 detail: "zero-dimensional spherical data must have zero vectors".to_owned(),
76 });
77 }
78
79 let expected_elements = checked_mul(vectors, dims, "spherical vector element count")?;
80 if actual_elements != expected_elements {
81 return Err(CodecError::Corrupt {
82 detail: format!(
83 "expected {expected_elements} elements ({vectors} vectors × {dims} dims), got {actual_elements}"
84 ),
85 });
86 }
87
88 let dims_header = encode_u32_len(dims, "spherical dimensions")?;
89 let vectors_header = encode_u32_len(vectors, "spherical vector count")?;
90 Ok((dims_header, vectors_header))
91}
92
93pub fn decode(data: &[u8]) -> Result<(Vec<f32>, usize, usize), CodecError> {
95 if data.len() < 9 {
96 return Err(CodecError::Truncated {
97 expected: 9,
98 actual: data.len(),
99 });
100 }
101
102 let dims = u32_to_usize(
103 u32::from_le_bytes([data[0], data[1], data[2], data[3]]),
104 "spherical dimensions",
105 )?;
106 let vectors = u32_to_usize(
107 u32::from_le_bytes([data[4], data[5], data[6], data[7]]),
108 "spherical vector count",
109 )?;
110 let transform = data[8];
111 if transform > 1 {
112 return Err(CodecError::Corrupt {
113 detail: format!("unknown spherical transform type {transform}"),
114 });
115 }
116
117 if dims == 0 && vectors != 0 {
118 return Err(CodecError::Corrupt {
119 detail: "zero-dimensional spherical data declares nonzero vectors".to_owned(),
120 });
121 }
122 if vectors == 0 || dims == 0 {
123 return Ok((Vec::new(), dims, 0));
124 }
125
126 let elements = checked_mul(vectors, dims, "spherical vector element count")?;
127 let expected_bytes = checked_mul(elements, size_of::<f32>(), "spherical decoded bytes")?;
128 decoded_len(expected_bytes, "spherical")?;
129
130 let compressed = &data[9..];
131 let raw_bytes = crate::lz4::decode(compressed).map_err(|e| CodecError::DecompressFailed {
132 detail: format!("spherical lz4: {e}"),
133 })?;
134
135 if raw_bytes.len() != expected_bytes {
136 return Err(CodecError::Corrupt {
137 detail: format!("expected {expected_bytes} bytes, got {}", raw_bytes.len()),
138 });
139 }
140
141 let floats: Vec<f32> = raw_bytes
142 .as_chunks::<4>()
143 .0
144 .iter()
145 .map(|c| f32::from_le_bytes(*c))
146 .collect();
147
148 if transform == 0 {
149 return Ok((floats, dims, vectors));
151 }
152
153 let mut result = Vec::with_capacity(floats.len());
155 for v in 0..vectors {
156 let start = v * dims;
157 let spherical = &floats[start..start + dims];
158 let cartesian = spherical_to_cartesian(spherical);
159 result.extend_from_slice(&cartesian);
160 }
161
162 Ok((result, dims, vectors))
163}
164
165pub fn normalization_ratio(data: &[f32], dims: usize) -> f64 {
169 if data.is_empty() || dims == 0 {
170 return 0.0;
171 }
172 let vectors = data.len() / dims;
173 let mut normalized = 0usize;
174
175 for v in 0..vectors {
176 let start = v * dims;
177 let norm: f32 = data[start..start + dims]
178 .iter()
179 .map(|x| x * x)
180 .sum::<f32>()
181 .sqrt();
182 if (0.99..=1.01).contains(&norm) {
183 normalized += 1;
184 }
185 }
186
187 normalized as f64 / vectors as f64
188}
189
190fn cartesian_to_spherical(cart: &[f32]) -> Vec<f32> {
202 let n = cart.len();
203 if n == 0 {
204 return Vec::new();
205 }
206 if n == 1 {
207 return vec![cart[0]];
208 }
209
210 let mut spherical = Vec::with_capacity(n);
211
212 let r: f32 = cart.iter().map(|x| x * x).sum::<f32>().sqrt();
214 spherical.push(r);
215
216 for i in 0..n - 1 {
218 let sum_sq: f32 = cart[i..].iter().map(|x| x * x).sum::<f32>();
219 let denom = sum_sq.sqrt();
220 if denom < 1e-30 {
221 spherical.push(0.0);
222 } else if i < n - 2 {
223 spherical.push((cart[i] / denom).acos());
224 } else {
225 spherical.push(cart[n - 1].atan2(cart[n - 2]));
227 }
228 }
229
230 spherical
231}
232
233fn spherical_to_cartesian(sph: &[f32]) -> Vec<f32> {
235 let n = sph.len();
236 if n == 0 {
237 return Vec::new();
238 }
239 if n == 1 {
240 return vec![sph[0]];
241 }
242
243 let r = sph[0];
244 let angles = &sph[1..];
245 let dims = n; let mut cart = Vec::with_capacity(dims);
248
249 for i in 0..dims - 1 {
250 let mut val = r;
251 for a in &angles[..i] {
252 val *= a.sin();
253 }
254 if i < dims - 2 {
255 val *= angles[i].cos();
256 } else {
257 val *= angles[dims - 2].sin();
259 }
260 cart.push(val);
261 }
262
263 let mut val = r;
265 for angle in &angles[..dims - 2] {
266 val *= angle.sin();
267 }
268 val *= angles[dims - 2].cos();
269 cart.push(val);
270
271 cart
272}
273
274fn build_header(dims: u32, vectors: u32, transform: u8, compressed: &[u8]) -> Vec<u8> {
275 let mut out = Vec::with_capacity(9 + compressed.len());
276 out.extend_from_slice(&dims.to_le_bytes());
277 out.extend_from_slice(&vectors.to_le_bytes());
278 out.push(transform);
279 out.extend_from_slice(compressed);
280 out
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 fn make_normalized_vectors(n: usize, dims: usize) -> Vec<f32> {
288 let mut data = Vec::with_capacity(n * dims);
289 for i in 0..n {
290 let mut vec: Vec<f32> = (0..dims)
291 .map(|d| ((i * dims + d) as f32 * 0.1).sin())
292 .collect();
293 let norm: f32 = vec.iter().map(|x| x * x).sum::<f32>().sqrt();
294 if norm > 0.0 {
295 for v in &mut vec {
296 *v /= norm;
297 }
298 }
299 data.extend_from_slice(&vec);
300 }
301 data
302 }
303
304 #[test]
305 fn empty_roundtrip() {
306 let encoded = encode(&[], 32, 0).unwrap();
307 let (decoded, dims, vectors) = decode(&encoded).unwrap();
308 assert!(decoded.is_empty());
309 assert_eq!(vectors, 0);
310 assert_eq!(dims, 32);
311 }
312
313 #[test]
314 fn normalized_roundtrip() {
315 let data = make_normalized_vectors(100, 32);
316 let encoded = encode(&data, 32, 100).unwrap();
317 let (decoded, dims, vectors) = decode(&encoded).unwrap();
318
319 assert_eq!(dims, 32);
320 assert_eq!(vectors, 100);
321 assert_eq!(decoded.len(), data.len());
322
323 let max_error: f32 = data
327 .iter()
328 .zip(decoded.iter())
329 .map(|(a, b)| (a - b).abs())
330 .fold(0.0f32, f32::max);
331 assert!(
332 max_error < 0.1,
333 "max reconstruction error {max_error} exceeds threshold"
334 );
335 }
336
337 #[test]
338 fn raw_roundtrip() {
339 let data: Vec<f32> = (0..320).map(|i| i as f32 * 0.01).collect();
340 let encoded = encode_raw(&data, 32, 10).unwrap();
341 let (decoded, dims, vectors) = decode(&encoded).unwrap();
342 assert_eq!(dims, 32);
343 assert_eq!(vectors, 10);
344 for (a, b) in data.iter().zip(decoded.iter()) {
345 assert_eq!(a.to_bits(), b.to_bits());
346 }
347 }
348
349 #[test]
350 fn compression_ratio() {
351 let data = make_normalized_vectors(1000, 128);
352 let encoded = encode(&data, 128, 1000).unwrap();
353 let raw_size = data.len() * 4;
354 let ratio = raw_size as f64 / encoded.len() as f64;
355 assert!(ratio > 0.9, "should not expand >10%, got {ratio:.2}x");
358 }
359
360 #[test]
361 fn normalization_check() {
362 let normalized = make_normalized_vectors(100, 32);
363 assert!(normalization_ratio(&normalized, 32) > 0.95);
364
365 let raw: Vec<f32> = (0..3200).map(|i| i as f32).collect();
366 assert!(normalization_ratio(&raw, 32) < 0.1);
367 }
368
369 #[test]
370 fn truncated_error() {
371 assert!(decode(&[]).is_err());
372 assert!(decode(&[0; 5]).is_err());
373 }
374
375 #[test]
376 fn hostile_dimensions_are_rejected_before_size_arithmetic_or_decompression() {
377 let mut encoded = encode_raw(&[1.0], 1, 1).expect("fixture must encode");
378 encoded[..4].copy_from_slice(&u32::MAX.to_le_bytes());
379 encoded[4..8].copy_from_slice(&u32::MAX.to_le_bytes());
380
381 let error = decode(&encoded).expect_err("hostile dimensions must fail");
382 assert!(matches!(
383 error,
384 CodecError::Corrupt { .. } | CodecError::ResourceLimit { .. }
385 ));
386 }
387
388 #[test]
389 fn encoders_reject_overflowed_or_mismatched_shapes() {
390 assert!(encode(&[1.0], usize::MAX, 2).is_err());
391 assert!(encode_raw(&[1.0], usize::MAX, 2).is_err());
392 assert!(encode_raw(&[1.0, 2.0], 1, 1).is_err());
393 assert!(encode(&[], 0, 1).is_err());
394 assert!(encode_raw(&[], 0, 1).is_err());
395 }
396
397 #[test]
398 fn zero_dimension_zero_vector_shape_roundtrips_canonically() {
399 let encoded = encode(&[], 0, 0).expect("canonical empty shape must encode");
400 let (decoded, dims, vectors) = decode(&encoded).expect("canonical empty shape must decode");
401 assert!(decoded.is_empty());
402 assert_eq!((dims, vectors), (0, 0));
403 }
404
405 #[test]
406 fn decoder_rejects_zero_dimensions_with_nonzero_vectors() {
407 let mut encoded = [0u8; 9];
408 encoded[4..8].copy_from_slice(&1u32.to_le_bytes());
409 let error = decode(&encoded).expect_err("noncanonical empty shape must fail");
410 assert!(matches!(error, CodecError::Corrupt { .. }));
411 }
412}