use crate::rotation::Rotation;
use crate::tables::{level, quantize_coord, LEVELS_F32};
use crate::TurboQuantError;
pub const META_BYTES: usize = 8;
pub struct Turbo4Query {
pub blob: Vec<u8>,
pub rotated: Vec<f32>,
pub norm_sq: f32,
}
pub struct Turbo4Codec {
dim: usize,
rotation: Rotation,
}
impl Turbo4Codec {
pub fn new(dim: usize, rotation_seed: u64) -> Result<Self, TurboQuantError> {
if dim < 2 || dim % 2 != 0 {
return Err(TurboQuantError::InvalidDimension(dim));
}
Ok(Self {
dim,
rotation: Rotation::new(dim, rotation_seed),
})
}
#[inline]
pub fn dim(&self) -> usize {
self.dim
}
#[inline]
pub fn code_len(&self) -> usize {
self.dim / 2 + META_BYTES
}
#[inline]
pub fn query_len(&self) -> usize {
self.dim + META_BYTES
}
pub fn encode(&self, v: &[f32]) -> Result<Vec<u8>, TurboQuantError> {
if v.len() != self.dim {
return Err(TurboQuantError::DimensionMismatch {
expected: self.dim,
actual: v.len(),
});
}
let rotated = self.rotation.apply(v);
let norm_sq: f32 = rotated.iter().map(|x| x * x).sum();
let alpha = (norm_sq / self.dim as f32).sqrt();
Ok(self.encode_rotated(&rotated, alpha))
}
pub fn encode_dual(&self, v: &[f32]) -> Result<(Vec<u8>, Vec<u8>), TurboQuantError> {
if v.len() != self.dim {
return Err(TurboQuantError::DimensionMismatch {
expected: self.dim,
actual: v.len(),
});
}
let rotated = self.rotation.apply(v);
let norm_sq: f32 = rotated.iter().map(|x| x * x).sum();
let alpha = (norm_sq / self.dim as f32).sqrt();
let bits = crate::bits1::encode_bits(&rotated, alpha);
let turbo4 = self.encode_rotated(&rotated, alpha);
Ok((turbo4, bits))
}
fn encode_rotated(&self, rotated: &[f32], alpha: f32) -> Vec<u8> {
let inv = if alpha > 0.0 { 1.0 / alpha } else { 0.0 };
let half = self.dim / 2;
let mut blob = vec![0u8; self.code_len()];
let mut s = 0.0f32;
for i in 0..half {
let c_lo = quantize_coord(rotated[i] * inv);
let c_hi = quantize_coord(rotated[i + half] * inv);
s += level(c_lo) * level(c_lo) + level(c_hi) * level(c_hi);
blob[i] = c_lo | (c_hi << 4);
}
blob[half..half + 4].copy_from_slice(&alpha.to_le_bytes());
blob[half + 4..half + 8].copy_from_slice(&s.to_le_bytes());
blob
}
pub fn encode_query(&self, q: &[f32]) -> Result<Turbo4Query, TurboQuantError> {
if q.len() != self.dim {
return Err(TurboQuantError::DimensionMismatch {
expected: self.dim,
actual: q.len(),
});
}
let rotated = self.rotation.apply(q);
let norm_sq: f32 = rotated.iter().map(|x| x * x).sum();
let qmax = rotated.iter().fold(0.0f32, |m, x| m.max(x.abs()));
let qscale = if qmax > 0.0 { qmax / 127.0 } else { 0.0 };
let inv = if qscale > 0.0 { 1.0 / qscale } else { 0.0 };
let mut blob = vec![0u8; self.query_len()];
for (i, &x) in rotated.iter().enumerate() {
blob[i] = ((x * inv).round() as i8) as u8;
}
blob[self.dim..self.dim + 4].copy_from_slice(&qscale.to_le_bytes());
blob[self.dim + 4..self.dim + 8].copy_from_slice(&norm_sq.to_le_bytes());
Ok(Turbo4Query {
blob,
rotated,
norm_sq,
})
}
pub fn decode_rotated(&self, blob: &[u8]) -> Vec<f32> {
let (nibbles, alpha, _) = split_code(blob, self.dim);
let half = self.dim / 2;
let mut out = vec![0.0f32; self.dim];
for i in 0..half {
out[i] = LEVELS_F32[(nibbles[i] & 0x0F) as usize] * alpha;
out[i + half] = LEVELS_F32[(nibbles[i] >> 4) as usize] * alpha;
}
out
}
pub fn decode(&self, blob: &[u8]) -> Vec<f32> {
self.rotation.apply_inverse(&self.decode_rotated(blob))
}
}
#[inline]
pub fn split_code(blob: &[u8], dim: usize) -> (&[u8], f32, f32) {
let half = dim / 2;
assert_eq!(dim % 2, 0, "Turbo4 dimensions must be even");
assert_eq!(blob.len(), half + META_BYTES, "invalid Turbo4 code length");
let alpha = f32::from_le_bytes(blob[half..half + 4].try_into().unwrap());
let s = f32::from_le_bytes(blob[half + 4..half + 8].try_into().unwrap());
(&blob[..half], alpha, s)
}
#[inline]
pub fn split_query(blob: &[u8], dim: usize) -> (&[u8], f32, f32) {
assert_eq!(dim % 2, 0, "Turbo4 dimensions must be even");
assert_eq!(blob.len(), dim + META_BYTES, "invalid Turbo4 query length");
let qscale = f32::from_le_bytes(blob[dim..dim + 4].try_into().unwrap());
let norm_sq = f32::from_le_bytes(blob[dim + 4..dim + 8].try_into().unwrap());
(&blob[..dim], qscale, norm_sq)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rotation::SplitMix64;
fn gauss_vec(dim: usize, seed: u64) -> Vec<f32> {
let mut rng = SplitMix64(seed);
let mut out = Vec::with_capacity(dim);
while out.len() < dim {
let u1 = (rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64;
let u2 = (rng.next_u64() >> 11) as f64 / (1u64 << 53) as f64;
let r = (-2.0 * u1.max(1e-12).ln()).sqrt();
let (s, c) = (2.0 * std::f64::consts::PI * u2).sin_cos();
out.push((r * c) as f32);
if out.len() < dim {
out.push((r * s) as f32);
}
}
out
}
#[test]
fn code_len_is_8x_compression() {
let codec = Turbo4Codec::new(1536, 42).unwrap();
assert_eq!(codec.code_len(), 768 + 8);
assert!(1536.0 * 4.0 / codec.code_len() as f32 > 7.5);
}
#[test]
fn rejects_odd_or_tiny_dims() {
assert!(Turbo4Codec::new(3, 42).is_err());
assert!(Turbo4Codec::new(0, 42).is_err());
assert!(Turbo4Codec::new(128, 42).is_ok());
}
#[test]
fn roundtrip_error_is_bounded() {
let dim = 256;
let codec = Turbo4Codec::new(dim, 42).unwrap();
let v = gauss_vec(dim, 3);
let blob = codec.encode(&v).unwrap();
let back = codec.decode(&blob);
let norm_sq: f32 = v.iter().map(|x| x * x).sum();
let err_sq: f32 = v.iter().zip(&back).map(|(a, b)| (a - b) * (a - b)).sum();
assert!(
err_sq / norm_sq < 0.05,
"relative sq error {}",
err_sq / norm_sq
);
}
#[test]
fn zero_vector_is_safe() {
let codec = Turbo4Codec::new(64, 42).unwrap();
let blob = codec.encode(&vec![0.0; 64]).unwrap();
let (_, alpha, _) = split_code(&blob, 64);
assert_eq!(alpha, 0.0);
assert!(codec.decode(&blob).iter().all(|&x| x == 0.0));
let q = codec.encode_query(&vec![0.0; 64]).unwrap();
assert_eq!(q.norm_sq, 0.0);
}
#[test]
fn blob_lengths_are_disjoint() {
for dim in [2usize, 64, 384, 1536] {
let codec = Turbo4Codec::new(dim, 1).unwrap();
assert_ne!(codec.code_len(), codec.query_len());
}
}
#[test]
fn encode_dual_matches_single_encoders() {
let dim = 128;
let codec = Turbo4Codec::new(dim, 42).unwrap();
let v = gauss_vec(dim, 17);
let (t4, bits) = codec.encode_dual(&v).unwrap();
assert_eq!(t4, codec.encode(&v).unwrap());
assert_eq!(bits.len(), crate::bits1::code1_len(dim));
}
#[test]
fn encoding_is_deterministic() {
let dim = 384;
let v = gauss_vec(dim, 9);
let c1 = Turbo4Codec::new(dim, 42).unwrap();
let c2 = Turbo4Codec::new(dim, 42).unwrap();
assert_eq!(c1.encode(&v).unwrap(), c2.encode(&v).unwrap());
}
}