use std::error::Error;
use std::fmt;
use crate::rank::{rank_to_bucket, rank_transform};
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct CompositionSpec {
dim: usize,
buckets: usize,
expected_per_bucket: usize,
}
impl CompositionSpec {
pub fn new(dim: usize, buckets: usize) -> Result<Self, CompositionViolation> {
if dim == 0 {
return Err(CompositionViolation::InvalidSpec("dim must be > 0"));
}
if buckets < 2 {
return Err(CompositionViolation::InvalidSpec("buckets must be >= 2"));
}
if buckets > u8::MAX as usize + 1 {
return Err(CompositionViolation::InvalidSpec(
"buckets must be <= 256 (codes are stored as u8)",
));
}
if !dim.is_multiple_of(buckets) {
return Err(CompositionViolation::NonUniformSpec { dim, buckets });
}
Ok(Self {
dim,
buckets,
expected_per_bucket: dim / buckets,
})
}
pub fn rank_quant(dim: usize, bits: u8) -> Result<Self, CompositionViolation> {
RankQuantSpec::new(dim, bits).map(|spec| spec.composition)
}
pub fn dim(&self) -> usize {
self.dim
}
pub fn buckets(&self) -> usize {
self.buckets
}
pub fn expected_per_bucket(&self) -> usize {
self.expected_per_bucket
}
pub fn histogram(&self, codes: &[u8]) -> Result<Vec<usize>, CompositionViolation> {
if codes.len() != self.dim {
return Err(CompositionViolation::WrongLength {
expected: self.dim,
actual: codes.len(),
});
}
let mut hist = vec![0usize; self.buckets];
for (coordinate, &bucket) in codes.iter().enumerate() {
let bucket = bucket as usize;
if bucket >= self.buckets {
return Err(CompositionViolation::BucketOutOfRange {
coordinate,
bucket,
buckets: self.buckets,
});
}
hist[bucket] += 1;
}
Ok(hist)
}
pub fn validate_codes(&self, codes: &[u8]) -> Result<(), CompositionViolation> {
let hist = self.histogram(codes)?;
for (bucket, &count) in hist.iter().enumerate() {
if count != self.expected_per_bucket {
return Err(CompositionViolation::WrongBucketCount {
bucket,
expected: self.expected_per_bucket,
actual: count,
});
}
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct RankQuantSpec {
bits: u8,
composition: CompositionSpec,
}
impl RankQuantSpec {
pub fn new(dim: usize, bits: u8) -> Result<Self, CompositionViolation> {
if !matches!(bits, 1 | 2 | 4 | 8) {
return Err(CompositionViolation::InvalidBits { bits });
}
if dim > u16::MAX as usize {
return Err(CompositionViolation::DimTooLarge {
dim,
max: u16::MAX as usize,
});
}
let buckets = 1usize << bits;
Ok(Self {
bits,
composition: CompositionSpec::new(dim, buckets)?,
})
}
pub fn bits(&self) -> u8 {
self.bits
}
pub fn composition(&self) -> &CompositionSpec {
&self.composition
}
pub fn into_composition(self) -> CompositionSpec {
self.composition
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct BucketCode {
spec: CompositionSpec,
codes: Vec<u8>,
}
impl BucketCode {
pub fn new(spec: CompositionSpec, codes: Vec<u8>) -> Result<Self, CompositionViolation> {
spec.validate_codes(&codes)?;
Ok(Self { spec, codes })
}
pub fn from_ranks(
dim: usize,
buckets: usize,
ranks: &[usize],
) -> Result<Self, CompositionViolation> {
let spec = CompositionSpec::new(dim, buckets)?;
if ranks.len() != dim {
return Err(CompositionViolation::WrongLength {
expected: dim,
actual: ranks.len(),
});
}
let mut seen = vec![false; dim];
let mut codes = Vec::with_capacity(dim);
for (coordinate, &rank) in ranks.iter().enumerate() {
if rank >= dim {
return Err(CompositionViolation::RankOutOfRange {
coordinate,
rank,
dim,
});
}
if seen[rank] {
return Err(CompositionViolation::DuplicateRank { rank });
}
seen[rank] = true;
codes.push(((rank as u64 * buckets as u64) / dim as u64) as u8);
}
Self::new(spec, codes)
}
pub fn from_vector(dim: usize, bits: u8, vector: &[f32]) -> Result<Self, CompositionViolation> {
let spec = RankQuantSpec::new(dim, bits)?;
if vector.len() != dim {
return Err(CompositionViolation::WrongLength {
expected: dim,
actual: vector.len(),
});
}
if let Some(coordinate) = vector.iter().position(|x| !x.is_finite()) {
return Err(CompositionViolation::NonFiniteValue { coordinate });
}
let ranks = rank_transform(vector);
let codes: Vec<u8> = ranks
.iter()
.map(|&rank| rank_to_bucket(rank, dim, bits))
.collect();
Self::new(spec.into_composition(), codes)
}
pub fn spec(&self) -> &CompositionSpec {
&self.spec
}
pub fn codes(&self) -> &[u8] {
&self.codes
}
pub fn top_bitmap(&self) -> Vec<bool> {
let top = self.spec.buckets - 1;
self.codes
.iter()
.map(|&bucket| bucket as usize == top)
.collect()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum CompositionViolation {
InvalidSpec(&'static str),
InvalidBits {
bits: u8,
},
DimTooLarge {
dim: usize,
max: usize,
},
NonUniformSpec {
dim: usize,
buckets: usize,
},
WrongLength {
expected: usize,
actual: usize,
},
BucketOutOfRange {
coordinate: usize,
bucket: usize,
buckets: usize,
},
WrongBucketCount {
bucket: usize,
expected: usize,
actual: usize,
},
RankOutOfRange {
coordinate: usize,
rank: usize,
dim: usize,
},
DuplicateRank {
rank: usize,
},
NonFiniteValue {
coordinate: usize,
},
}
impl fmt::Display for CompositionViolation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidSpec(message) => write!(f, "{message}"),
Self::InvalidBits { bits } => {
write!(f, "bits {bits} is invalid; expected one of 1, 2, 4, 8")
}
Self::DimTooLarge { dim, max } => write!(f, "dim {dim} exceeds maximum {max}"),
Self::NonUniformSpec { dim, buckets } => {
write!(f, "dim {dim} is not divisible by buckets {buckets}")
}
Self::WrongLength { expected, actual } => {
write!(f, "code length {actual} does not match dim {expected}")
}
Self::BucketOutOfRange {
coordinate,
bucket,
buckets,
} => write!(
f,
"coordinate {coordinate} has bucket {bucket}, expected < {buckets}"
),
Self::WrongBucketCount {
bucket,
expected,
actual,
} => write!(
f,
"bucket {bucket} has {actual} coordinates, expected {expected}"
),
Self::RankOutOfRange {
coordinate,
rank,
dim,
} => write!(
f,
"coordinate {coordinate} has rank {rank}, expected < {dim}"
),
Self::DuplicateRank { rank } => write!(f, "rank {rank} appears more than once"),
Self::NonFiniteValue { coordinate } => {
write!(f, "coordinate {coordinate} is non-finite (NaN or ±Inf)")
}
}
}
}
impl Error for CompositionViolation {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_ranks_builds_uniform_bucket_code() {
let code = BucketCode::from_ranks(8, 4, &[0, 1, 2, 3, 4, 5, 6, 7]).unwrap();
assert_eq!(code.spec().expected_per_bucket(), 2);
assert_eq!(code.codes(), &[0, 0, 1, 1, 2, 2, 3, 3]);
}
#[test]
fn rejects_non_uniform_bucket_counts() {
let spec = CompositionSpec::new(8, 4).unwrap();
let err = BucketCode::new(spec, vec![0, 0, 0, 1, 2, 2, 3, 3]).unwrap_err();
assert_eq!(
err,
CompositionViolation::WrongBucketCount {
bucket: 0,
expected: 2,
actual: 3,
}
);
}
#[test]
fn rejects_duplicate_ranks() {
let err = BucketCode::from_ranks(4, 2, &[0, 1, 1, 3]).unwrap_err();
assert_eq!(err, CompositionViolation::DuplicateRank { rank: 1 });
}
#[test]
fn rankquant_spec_rejects_unsupported_bits_and_large_dims() {
assert_eq!(
RankQuantSpec::new(8, 3).unwrap_err(),
CompositionViolation::InvalidBits { bits: 3 }
);
assert_eq!(
BucketCode::from_vector(8, 3, &[0.0f32; 8]).unwrap_err(),
CompositionViolation::InvalidBits { bits: 3 }
);
assert_eq!(
RankQuantSpec::new(u16::MAX as usize + 1, 2).unwrap_err(),
CompositionViolation::DimTooLarge {
dim: u16::MAX as usize + 1,
max: u16::MAX as usize,
}
);
}
#[test]
fn rankquant_spec_accepts_fixed_composition_bits_8() {
let spec = RankQuantSpec::new(512, 8).unwrap();
assert_eq!(spec.bits(), 8);
assert_eq!(spec.composition().buckets(), 256);
assert_eq!(spec.composition().expected_per_bucket(), 2);
let v: Vec<f32> = (0..512).map(|i| i as f32).collect();
let code = BucketCode::from_vector(512, 8, &v).unwrap();
assert_eq!(code.spec().buckets(), 256);
assert_eq!(code.spec().expected_per_bucket(), 2);
assert_eq!(code.codes()[0], 0);
assert_eq!(code.codes()[511], 255);
}
#[test]
fn rankquant_spec_rejects_non_fixed_composition_bits_8() {
assert_eq!(
RankQuantSpec::new(384, 8).unwrap_err(),
CompositionViolation::NonUniformSpec {
dim: 384,
buckets: 256,
}
);
}
#[test]
fn composition_spec_rejects_more_than_256_buckets() {
assert_eq!(
CompositionSpec::new(512, 257).unwrap_err(),
CompositionViolation::InvalidSpec("buckets must be <= 256 (codes are stored as u8)")
);
assert!(CompositionSpec::new(512, 256).is_ok());
}
#[test]
fn rankquant_spec_rejects_non_divisible_dims() {
assert_eq!(
RankQuantSpec::new(10, 2).unwrap_err(),
CompositionViolation::NonUniformSpec {
dim: 10,
buckets: 4,
}
);
}
#[test]
fn validate_codes_rejects_wrong_length() {
let spec = CompositionSpec::new(8, 4).unwrap();
assert_eq!(
spec.validate_codes(&[0, 0, 1, 1, 2, 2, 3]).unwrap_err(),
CompositionViolation::WrongLength {
expected: 8,
actual: 7,
}
);
}
#[test]
fn validate_codes_rejects_out_of_range_code() {
let spec = CompositionSpec::new(8, 4).unwrap();
assert_eq!(
spec.validate_codes(&[0, 0, 1, 1, 2, 2, 3, 4]).unwrap_err(),
CompositionViolation::BucketOutOfRange {
coordinate: 7,
bucket: 4,
buckets: 4,
}
);
}
#[test]
fn composition_spec_rejects_zero_dim_and_small_buckets() {
assert_eq!(
CompositionSpec::new(0, 4).unwrap_err(),
CompositionViolation::InvalidSpec("dim must be > 0")
);
assert_eq!(
CompositionSpec::new(8, 1).unwrap_err(),
CompositionViolation::InvalidSpec("buckets must be >= 2")
);
}
#[test]
fn rank_quant_helper_matches_rankquant_spec_composition() {
let from_helper = CompositionSpec::rank_quant(16, 2).unwrap();
let from_spec = RankQuantSpec::new(16, 2).unwrap().into_composition();
assert_eq!(from_helper, from_spec);
assert_eq!(from_helper.buckets(), 4);
assert_eq!(from_helper.expected_per_bucket(), 4);
}
#[test]
fn from_ranks_rejects_rank_out_of_range() {
assert_eq!(
BucketCode::from_ranks(4, 2, &[4, 1, 2, 3]).unwrap_err(),
CompositionViolation::RankOutOfRange {
coordinate: 0,
rank: 4,
dim: 4,
}
);
}
#[test]
fn histogram_counts_each_bucket() {
let spec = CompositionSpec::new(8, 4).unwrap();
assert_eq!(
spec.histogram(&[0, 0, 1, 1, 2, 2, 3, 3]).unwrap(),
vec![2, 2, 2, 2]
);
}
#[test]
fn top_bitmap_marks_only_the_top_bucket() {
let code = BucketCode::from_ranks(8, 4, &[0, 1, 2, 3, 4, 5, 6, 7]).unwrap();
assert_eq!(
code.top_bitmap(),
vec![false, false, false, false, false, false, true, true]
);
}
#[test]
fn from_vector_matches_from_ranks_for_sorted_input() {
let v: Vec<f32> = (0..8).map(|i| i as f32).collect();
let code = BucketCode::from_vector(8, 2, &v).unwrap();
assert_eq!(code.codes(), &[0, 0, 1, 1, 2, 2, 3, 3]);
let via_ranks = BucketCode::from_ranks(8, 4, &[0, 1, 2, 3, 4, 5, 6, 7]).unwrap();
assert_eq!(code.codes(), via_ranks.codes());
}
#[test]
fn from_vector_buckets_are_balanced() {
let v = [3.0f32, 1.0, 4.0, 1.5, 5.0, 9.0, 2.0, 6.0];
let code = BucketCode::from_vector(8, 2, &v).unwrap();
assert_eq!(code.spec().validate_codes(code.codes()), Ok(()));
assert_eq!(
code.spec().histogram(code.codes()).unwrap(),
vec![2, 2, 2, 2]
);
}
#[test]
fn from_vector_rejects_wrong_length() {
assert_eq!(
BucketCode::from_vector(8, 2, &[0.0, 1.0, 2.0]).unwrap_err(),
CompositionViolation::WrongLength {
expected: 8,
actual: 3,
}
);
}
#[test]
fn from_vector_rejects_non_finite() {
let v = [0.0f32, 1.0, f32::NAN, 3.0, 4.0, 5.0, 6.0, 7.0];
assert_eq!(
BucketCode::from_vector(8, 2, &v).unwrap_err(),
CompositionViolation::NonFiniteValue { coordinate: 2 }
);
}
#[test]
fn from_vector_rejects_invalid_bits() {
let v: Vec<f32> = (0..8).map(|i| i as f32).collect();
assert_eq!(
BucketCode::from_vector(8, 3, &v).unwrap_err(),
CompositionViolation::InvalidBits { bits: 3 }
);
}
#[test]
fn display_is_stable_for_each_variant() {
let cases = [
CompositionViolation::InvalidSpec("dim must be > 0"),
CompositionViolation::InvalidBits { bits: 3 },
CompositionViolation::DimTooLarge {
dim: 70000,
max: 65535,
},
CompositionViolation::NonUniformSpec {
dim: 10,
buckets: 4,
},
CompositionViolation::WrongLength {
expected: 8,
actual: 7,
},
CompositionViolation::BucketOutOfRange {
coordinate: 7,
bucket: 4,
buckets: 4,
},
CompositionViolation::WrongBucketCount {
bucket: 0,
expected: 2,
actual: 3,
},
CompositionViolation::RankOutOfRange {
coordinate: 0,
rank: 4,
dim: 4,
},
CompositionViolation::DuplicateRank { rank: 1 },
CompositionViolation::NonFiniteValue { coordinate: 2 },
];
for case in cases {
assert!(!case.to_string().is_empty());
}
}
}