Skip to main content

ruvector_core/encoding/
codecs.rs

1//! In-core implementations of the [`VectorCodec`] plane: Fp32, Fp16, Int8,
2//! and the Turbo4 adapter over `ruvector-turboquant` (ADR-297 §1).
3//!
4//! Fp32/Fp16/Int8 score by decoding — they are the verification / hot tiers,
5//! where fidelity matters more than per-candidate cost. Turbo4 scores
6//! directly on packed codes (never reconstructs), as required by ADR-296.
7
8use super::{metric_distance, CodecKind, EncodedQuery, VectorCodec};
9use crate::error::{Result, RuvectorError};
10use crate::types::DistanceMetric;
11use ruvector_turboquant as tq;
12
13fn check_dim(expected: usize, actual: usize) -> Result<()> {
14    if expected != actual {
15        return Err(RuvectorError::DimensionMismatch { expected, actual });
16    }
17    Ok(())
18}
19
20fn check_blob(expected: usize, actual: usize) -> Result<()> {
21    if expected != actual {
22        return Err(RuvectorError::InvalidInput(format!(
23            "encoded blob length {actual} != expected {expected}"
24        )));
25    }
26    Ok(())
27}
28
29/// Query that scores by decoding the candidate blob to f32 first.
30struct DecodingQuery {
31    q: Vec<f32>,
32    metric: DistanceMetric,
33    decode: fn(&[u8]) -> Vec<f32>,
34}
35
36impl EncodedQuery for DecodingQuery {
37    fn distance_to(&self, blob: &[u8]) -> f32 {
38        let v = (self.decode)(blob);
39        metric_distance(self.metric, &self.q, &v)
40    }
41}
42
43// ---------------------------------------------------------------------------
44// Fp32
45// ---------------------------------------------------------------------------
46
47/// Identity codec: 4 bytes/dim, exact. The verification anchor of the plane.
48pub struct Fp32Codec {
49    dim: usize,
50    metric: DistanceMetric,
51}
52
53impl Fp32Codec {
54    pub fn new(dim: usize, metric: DistanceMetric) -> Self {
55        Self { dim, metric }
56    }
57}
58
59fn fp32_decode(blob: &[u8]) -> Vec<f32> {
60    blob.chunks_exact(4)
61        .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
62        .collect()
63}
64
65impl VectorCodec for Fp32Codec {
66    fn kind(&self) -> CodecKind {
67        CodecKind::Fp32
68    }
69    fn dim(&self) -> usize {
70        self.dim
71    }
72    fn encoded_len(&self) -> usize {
73        self.dim * 4
74    }
75    fn encode(&self, v: &[f32]) -> Result<Vec<u8>> {
76        check_dim(self.dim, v.len())?;
77        Ok(v.iter().flat_map(|x| x.to_le_bytes()).collect())
78    }
79    fn decode(&self, blob: &[u8]) -> Result<Vec<f32>> {
80        check_blob(self.encoded_len(), blob.len())?;
81        Ok(fp32_decode(blob))
82    }
83    fn distance(&self, a: &[u8], b: &[u8]) -> Result<f32> {
84        check_blob(self.encoded_len(), a.len())?;
85        check_blob(self.encoded_len(), b.len())?;
86        Ok(metric_distance(
87            self.metric,
88            &fp32_decode(a),
89            &fp32_decode(b),
90        ))
91    }
92    fn make_query(&self, q: &[f32]) -> Result<Box<dyn EncodedQuery>> {
93        check_dim(self.dim, q.len())?;
94        Ok(Box::new(DecodingQuery {
95            q: q.to_vec(),
96            metric: self.metric,
97            decode: fp32_decode,
98        }))
99    }
100}
101
102// ---------------------------------------------------------------------------
103// Fp16
104// ---------------------------------------------------------------------------
105
106/// IEEE 754 binary16: 2 bytes/dim. Hot-tier codec (~1e-3 relative error).
107/// Conversion is implemented in-crate (no `half` dependency) with
108/// round-to-nearest-even, matching hardware semantics.
109pub struct Fp16Codec {
110    dim: usize,
111    metric: DistanceMetric,
112}
113
114impl Fp16Codec {
115    pub fn new(dim: usize, metric: DistanceMetric) -> Self {
116        Self { dim, metric }
117    }
118}
119
120/// f32 → binary16 bits, round-to-nearest-even; overflow → ±inf.
121pub fn f32_to_f16_bits(x: f32) -> u16 {
122    let bits = x.to_bits();
123    let sign = ((bits >> 16) & 0x8000) as u16;
124    let exp = ((bits >> 23) & 0xFF) as i32;
125    let mant = bits & 0x007F_FFFF;
126    if exp == 255 {
127        // Inf / NaN (preserve NaN-ness with a set mantissa bit).
128        return sign | 0x7C00 | if mant != 0 { 0x0200 } else { 0 };
129    }
130    let unbiased = exp - 127;
131    if unbiased > 15 {
132        return sign | 0x7C00; // overflow → inf
133    }
134    if unbiased >= -14 {
135        // Normal half.
136        let m16 = (mant >> 13) as u16;
137        let h = sign | (((unbiased + 15) as u16) << 10) | m16;
138        let round = mant & 0x1FFF;
139        // RNE; a carry out of the mantissa correctly increments the exponent.
140        if round > 0x1000 || (round == 0x1000 && (m16 & 1) == 1) {
141            h + 1
142        } else {
143            h
144        }
145    } else if unbiased >= -24 {
146        // Subnormal half.
147        let shift = (13 + (-14 - unbiased)) as u32;
148        let full = mant | 0x0080_0000;
149        let m16 = (full >> shift) as u16;
150        let rem = full & ((1u32 << shift) - 1);
151        let half = 1u32 << (shift - 1);
152        let h = sign | m16;
153        if rem > half || (rem == half && (m16 & 1) == 1) {
154            h + 1
155        } else {
156            h
157        }
158    } else {
159        sign // underflow → ±0
160    }
161}
162
163/// binary16 bits → f32 (exact).
164pub fn f16_bits_to_f32(h: u16) -> f32 {
165    let sign = ((h & 0x8000) as u32) << 16;
166    let exp = ((h >> 10) & 0x1F) as u32;
167    let mant = (h & 0x03FF) as u32;
168    let bits = if exp == 0 {
169        if mant == 0 {
170            sign
171        } else {
172            // Subnormal: renormalize.
173            let mut e = 113u32; // 127 - 15 + 1
174            let mut m = mant;
175            while m & 0x0400 == 0 {
176                m <<= 1;
177                e -= 1;
178            }
179            sign | (e << 23) | ((m & 0x03FF) << 13)
180        }
181    } else if exp == 31 {
182        sign | 0x7F80_0000 | (mant << 13)
183    } else {
184        sign | ((exp + 112) << 23) | (mant << 13)
185    };
186    f32::from_bits(bits)
187}
188
189fn fp16_decode(blob: &[u8]) -> Vec<f32> {
190    blob.chunks_exact(2)
191        .map(|c| f16_bits_to_f32(u16::from_le_bytes(c.try_into().unwrap())))
192        .collect()
193}
194
195impl VectorCodec for Fp16Codec {
196    fn kind(&self) -> CodecKind {
197        CodecKind::Fp16
198    }
199    fn dim(&self) -> usize {
200        self.dim
201    }
202    fn encoded_len(&self) -> usize {
203        self.dim * 2
204    }
205    fn encode(&self, v: &[f32]) -> Result<Vec<u8>> {
206        check_dim(self.dim, v.len())?;
207        Ok(v.iter()
208            .flat_map(|x| f32_to_f16_bits(*x).to_le_bytes())
209            .collect())
210    }
211    fn decode(&self, blob: &[u8]) -> Result<Vec<f32>> {
212        check_blob(self.encoded_len(), blob.len())?;
213        Ok(fp16_decode(blob))
214    }
215    fn distance(&self, a: &[u8], b: &[u8]) -> Result<f32> {
216        check_blob(self.encoded_len(), a.len())?;
217        check_blob(self.encoded_len(), b.len())?;
218        Ok(metric_distance(
219            self.metric,
220            &fp16_decode(a),
221            &fp16_decode(b),
222        ))
223    }
224    fn make_query(&self, q: &[f32]) -> Result<Box<dyn EncodedQuery>> {
225        check_dim(self.dim, q.len())?;
226        Ok(Box::new(DecodingQuery {
227            q: q.to_vec(),
228            metric: self.metric,
229            decode: fp16_decode,
230        }))
231    }
232}
233
234// ---------------------------------------------------------------------------
235// Int8
236// ---------------------------------------------------------------------------
237
238/// Per-vector min/scale uniform int8: `[min f32 | scale f32 | D bytes]`.
239/// Same scheme as the legacy `ScalarQuantized`, but blob-oriented so the
240/// plane can move it around like any other representation.
241pub struct Int8Codec {
242    dim: usize,
243    metric: DistanceMetric,
244}
245
246impl Int8Codec {
247    pub fn new(dim: usize, metric: DistanceMetric) -> Self {
248        Self { dim, metric }
249    }
250}
251
252fn int8_decode(blob: &[u8]) -> Vec<f32> {
253    let min = f32::from_le_bytes(blob[0..4].try_into().unwrap());
254    let scale = f32::from_le_bytes(blob[4..8].try_into().unwrap());
255    blob[8..].iter().map(|&u| min + u as f32 * scale).collect()
256}
257
258impl VectorCodec for Int8Codec {
259    fn kind(&self) -> CodecKind {
260        CodecKind::Int8
261    }
262    fn dim(&self) -> usize {
263        self.dim
264    }
265    fn encoded_len(&self) -> usize {
266        self.dim + 8
267    }
268    fn encode(&self, v: &[f32]) -> Result<Vec<u8>> {
269        check_dim(self.dim, v.len())?;
270        let min = v.iter().copied().fold(f32::INFINITY, f32::min);
271        let max = v.iter().copied().fold(f32::NEG_INFINITY, f32::max);
272        let scale = if (max - min).abs() < f32::EPSILON {
273            1.0
274        } else {
275            (max - min) / 255.0
276        };
277        let mut blob = Vec::with_capacity(self.encoded_len());
278        blob.extend_from_slice(&min.to_le_bytes());
279        blob.extend_from_slice(&scale.to_le_bytes());
280        blob.extend(
281            v.iter()
282                .map(|&x| ((x - min) / scale).round().clamp(0.0, 255.0) as u8),
283        );
284        Ok(blob)
285    }
286    fn decode(&self, blob: &[u8]) -> Result<Vec<f32>> {
287        check_blob(self.encoded_len(), blob.len())?;
288        Ok(int8_decode(blob))
289    }
290    fn distance(&self, a: &[u8], b: &[u8]) -> Result<f32> {
291        check_blob(self.encoded_len(), a.len())?;
292        check_blob(self.encoded_len(), b.len())?;
293        Ok(metric_distance(
294            self.metric,
295            &int8_decode(a),
296            &int8_decode(b),
297        ))
298    }
299    fn make_query(&self, q: &[f32]) -> Result<Box<dyn EncodedQuery>> {
300        check_dim(self.dim, q.len())?;
301        Ok(Box::new(DecodingQuery {
302            q: q.to_vec(),
303            metric: self.metric,
304            decode: int8_decode,
305        }))
306    }
307}
308
309// ---------------------------------------------------------------------------
310// Turbo4 adapter
311// ---------------------------------------------------------------------------
312
313fn to_turbo_metric(metric: DistanceMetric) -> Result<tq::Metric> {
314    match metric {
315        DistanceMetric::Euclidean => Ok(tq::Metric::Euclidean),
316        DistanceMetric::Cosine => Ok(tq::Metric::Cosine),
317        DistanceMetric::DotProduct => Ok(tq::Metric::DotProduct),
318        DistanceMetric::Manhattan => Err(RuvectorError::InvalidParameter(
319            "Turbo4 does not support the Manhattan metric".into(),
320        )),
321    }
322}
323
324/// [`VectorCodec`] adapter over `ruvector_turboquant::Turbo4Codec` — scores
325/// on packed codes, never reconstructs (ADR-296).
326pub struct Turbo4PlaneCodec {
327    inner: tq::Turbo4Codec,
328    metric: tq::Metric,
329}
330
331impl Turbo4PlaneCodec {
332    pub fn new(dim: usize, metric: DistanceMetric, rotation_seed: u64) -> Result<Self> {
333        let metric = to_turbo_metric(metric)?;
334        let inner = tq::Turbo4Codec::new(dim, rotation_seed)
335            .map_err(|e| RuvectorError::InvalidParameter(e.to_string()))?;
336        Ok(Self { inner, metric })
337    }
338}
339
340struct Turbo4PlaneQuery {
341    query: tq::Turbo4Query,
342    metric: tq::Metric,
343    dim: usize,
344}
345
346impl EncodedQuery for Turbo4PlaneQuery {
347    fn distance_to(&self, blob: &[u8]) -> f32 {
348        tq::rescore(self.metric, &self.query, blob, self.dim)
349    }
350}
351
352impl VectorCodec for Turbo4PlaneCodec {
353    fn kind(&self) -> CodecKind {
354        CodecKind::Turbo4
355    }
356    fn dim(&self) -> usize {
357        self.inner.dim()
358    }
359    fn encoded_len(&self) -> usize {
360        self.inner.code_len()
361    }
362    fn encode(&self, v: &[f32]) -> Result<Vec<u8>> {
363        self.inner
364            .encode(v)
365            .map_err(|e| RuvectorError::InvalidInput(e.to_string()))
366    }
367    fn decode(&self, blob: &[u8]) -> Result<Vec<f32>> {
368        check_blob(self.encoded_len(), blob.len())?;
369        Ok(self.inner.decode(blob))
370    }
371    fn distance(&self, a: &[u8], b: &[u8]) -> Result<f32> {
372        check_blob(self.encoded_len(), a.len())?;
373        check_blob(self.encoded_len(), b.len())?;
374        Ok(tq::symmetric_distance(self.metric, a, b, self.inner.dim()))
375    }
376    fn make_query(&self, q: &[f32]) -> Result<Box<dyn EncodedQuery>> {
377        let query = self
378            .inner
379            .encode_query(q)
380            .map_err(|e| RuvectorError::InvalidInput(e.to_string()))?;
381        Ok(Box::new(Turbo4PlaneQuery {
382            query,
383            metric: self.metric,
384            dim: self.inner.dim(),
385        }))
386    }
387}
388
389#[cfg(test)]
390mod tests {
391    use super::super::{codec_for, CodecKind};
392    use super::*;
393
394    /// Decorrelated deterministic vectors (SplitMix64 per (seed, i)).
395    fn test_vec(dim: usize, seed: u64) -> Vec<f32> {
396        (0..dim as u64)
397            .map(|i| {
398                let mut z = (seed << 32 | i).wrapping_add(0x9E3779B97F4A7C15);
399                z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
400                z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
401                ((z >> 40) as f32 / (1u64 << 24) as f32) * 2.0 - 1.0
402            })
403            .collect()
404    }
405
406    #[test]
407    fn f16_conversion_known_values() {
408        for (x, bits) in [
409            (0.0f32, 0x0000u16),
410            (1.0, 0x3C00),
411            (-2.0, 0xC000),
412            (65504.0, 0x7BFF), // max finite half
413            (0.5, 0x3800),
414        ] {
415            assert_eq!(f32_to_f16_bits(x), bits, "encode {x}");
416            assert_eq!(f16_bits_to_f32(bits), x, "decode {x}");
417        }
418        // Overflow → inf; NaN stays NaN; subnormals roundtrip.
419        assert_eq!(f32_to_f16_bits(1e30), 0x7C00);
420        assert!(f16_bits_to_f32(f32_to_f16_bits(f32::NAN)).is_nan());
421        let sub = 3.0e-6f32;
422        let rt = f16_bits_to_f32(f32_to_f16_bits(sub));
423        assert!((rt - sub).abs() / sub < 0.05, "subnormal {sub} -> {rt}");
424    }
425
426    #[test]
427    fn f16_roundtrip_relative_error() {
428        for &x in &[1.0f32, -1.5, 0.123, 999.25, -0.0004883, 3.1415926] {
429            let rt = f16_bits_to_f32(f32_to_f16_bits(x));
430            assert!(
431                (rt - x).abs() / x.abs() < 1e-3,
432                "roundtrip {x} -> {rt} exceeds half precision"
433            );
434        }
435    }
436
437    #[test]
438    fn all_codecs_roundtrip_and_score() {
439        let dim = 64;
440        let a = test_vec(dim, 1);
441        let b = test_vec(dim, 7);
442        let exact = metric_distance(DistanceMetric::Euclidean, &a, &b);
443
444        for kind in [
445            CodecKind::Fp32,
446            CodecKind::Fp16,
447            CodecKind::Int8,
448            CodecKind::Turbo4,
449        ] {
450            let codec = codec_for(kind, dim, DistanceMetric::Euclidean, 42).unwrap();
451            let ca = codec.encode(&a).unwrap();
452            let cb = codec.encode(&b).unwrap();
453            assert_eq!(ca.len(), codec.encoded_len(), "{kind:?} blob length");
454
455            // decode error bounded per codec class
456            let da = codec.decode(&ca).unwrap();
457            let err: f32 = a.iter().zip(&da).map(|(x, y)| (x - y).abs()).sum::<f32>() / dim as f32;
458            let tol = match kind {
459                CodecKind::Fp32 => 1e-9,
460                CodecKind::Fp16 => 1e-3,
461                CodecKind::Int8 => 0.01,
462                _ => 0.2,
463            };
464            assert!(err <= tol, "{kind:?} mean decode error {err} > {tol}");
465
466            // symmetric + asymmetric agree with exact within codec tolerance
467            let sym = codec.distance(&ca, &cb).unwrap();
468            let asym = codec.make_query(&a).unwrap().distance_to(&cb);
469            let dtol = match kind {
470                CodecKind::Fp32 => 1e-4,
471                CodecKind::Fp16 => 1e-2,
472                CodecKind::Int8 => 0.05,
473                _ => 0.30,
474            };
475            assert!(
476                (sym - exact).abs() <= dtol * exact.max(1.0),
477                "{kind:?} symmetric {sym} vs exact {exact}"
478            );
479            assert!(
480                (asym - exact).abs() <= dtol * exact.max(1.0),
481                "{kind:?} asymmetric {asym} vs exact {exact}"
482            );
483        }
484    }
485
486    #[test]
487    fn turbo4_plane_matches_direct_codec() {
488        let dim = 128;
489        let v = test_vec(dim, 3);
490        let plane = codec_for(CodecKind::Turbo4, dim, DistanceMetric::Cosine, 42).unwrap();
491        let direct = tq::Turbo4Codec::new(dim, 42).unwrap();
492        assert_eq!(plane.encode(&v).unwrap(), direct.encode(&v).unwrap());
493    }
494
495    #[test]
496    fn manhattan_rejected_only_for_turbo4() {
497        assert!(codec_for(CodecKind::Turbo4, 64, DistanceMetric::Manhattan, 42).is_err());
498        assert!(codec_for(CodecKind::Fp16, 64, DistanceMetric::Manhattan, 42).is_ok());
499    }
500}