use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImportanceConfig {
pub access_weight: f64,
pub entropy_weight: f64,
pub structuring_weight: f64,
pub access_half_life_days: f64,
}
impl Default for ImportanceConfig {
fn default() -> Self {
Self {
access_weight: 0.40,
entropy_weight: 0.30,
structuring_weight: 0.30,
access_half_life_days: 30.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum QuantizationLevel {
F32,
SQ8,
SQ4,
SQ4Marked,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ImportanceScore {
pub embedding_id: String,
pub score: f64,
pub level: QuantizationLevel,
pub last_computed: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RequantizationReceipt {
pub embedding_id: String,
pub old_level: QuantizationLevel,
pub new_level: QuantizationLevel,
pub timestamp: String,
}
pub fn compute_importance(
access_frequency: f64,
information_entropy: f64,
structuring_score: f64,
config: &ImportanceConfig,
) -> f64 {
config.access_weight * access_frequency
+ config.entropy_weight * information_entropy
+ config.structuring_weight * structuring_score
}
pub fn decide_quantization(importance: f64) -> QuantizationLevel {
if importance > 0.8 {
QuantizationLevel::F32
} else if importance > 0.5 {
QuantizationLevel::SQ8
} else if importance > 0.2 {
QuantizationLevel::SQ4
} else {
QuantizationLevel::SQ4Marked
}
}
#[cfg(feature = "turbo-quant-codec")]
pub fn level_to_turbo_quant_bits(level: QuantizationLevel) -> Option<u8> {
match level {
QuantizationLevel::F32 => None, QuantizationLevel::SQ8 => Some(8),
QuantizationLevel::SQ4 => Some(4),
QuantizationLevel::SQ4Marked => Some(4),
}
}
#[cfg(feature = "turbo-quant-codec")]
pub fn compress_with_turbo_quant(
embedding: &[f32],
importance: f64,
projections: usize,
seed: u64,
) -> Result<Option<crate::vector_codec::VectorArtifactV1>, crate::MemoryError> {
let level = decide_quantization(importance);
let bits = match level_to_turbo_quant_bits(level) {
Some(b) => b,
None => return Ok(None), };
let codec =
crate::vector_codec::TurboQuantCodec::new(embedding.len(), bits, projections, seed)?;
let artifact = crate::vector_codec::VectorCodec::encode(&codec, embedding)?;
Ok(Some(artifact))
}
#[cfg(feature = "turbo-quant-codec")]
pub fn batch_compress_with_turbo_quant(
embeddings: &[(String, Vec<f32>, f64)], projections: usize,
seed: u64,
) -> Vec<(String, Option<crate::vector_codec::VectorArtifactV1>)> {
embeddings
.iter()
.filter_map(|(id, vec, importance)| {
match compress_with_turbo_quant(vec, *importance, projections, seed) {
Ok(artifact) => Some((id.clone(), artifact)),
Err(e) => {
tracing::warn!("TurboQuant compression failed for {}: {}", id, e);
None
}
}
})
.collect()
}
pub fn re_quantize(
scores: &[ImportanceScore],
_config: &ImportanceConfig,
) -> Vec<RequantizationReceipt> {
let timestamp = "1970-01-01T00:00:00Z".to_string(); scores
.iter()
.filter_map(|s| {
let new_level = decide_quantization(s.score);
if new_level != s.level {
Some(RequantizationReceipt {
embedding_id: s.embedding_id.clone(),
old_level: s.level,
new_level,
timestamp: timestamp.clone(),
})
} else {
None
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn importance_computation_uses_weights() {
let cfg = ImportanceConfig::default();
let score = compute_importance(1.0, 1.0, 1.0, &cfg);
assert!((score - 1.0).abs() < f64::EPSILON);
let score = compute_importance(0.5, 0.0, 0.0, &cfg);
assert!((score - 0.20).abs() < f64::EPSILON);
let score = compute_importance(0.0, 0.5, 0.0, &cfg);
assert!((score - 0.15).abs() < f64::EPSILON);
let score = compute_importance(0.0, 0.0, 0.5, &cfg);
assert!((score - 0.15).abs() < f64::EPSILON);
}
#[test]
fn quantization_decision_boundaries() {
assert_eq!(decide_quantization(0.81), QuantizationLevel::F32);
assert_eq!(decide_quantization(0.8), QuantizationLevel::SQ8); assert_eq!(decide_quantization(0.51), QuantizationLevel::SQ8);
assert_eq!(decide_quantization(0.5), QuantizationLevel::SQ4); assert_eq!(decide_quantization(0.21), QuantizationLevel::SQ4);
assert_eq!(decide_quantization(0.2), QuantizationLevel::SQ4Marked); assert_eq!(decide_quantization(0.1), QuantizationLevel::SQ4Marked);
assert_eq!(decide_quantization(0.0), QuantizationLevel::SQ4Marked);
}
#[test]
fn re_quantize_only_on_change() {
let cfg = ImportanceConfig::default();
let scores = vec![
ImportanceScore {
embedding_id: "e1".to_string(),
score: 0.9, level: QuantizationLevel::SQ8, last_computed: "t".to_string(),
},
ImportanceScore {
embedding_id: "e2".to_string(),
score: 0.4, level: QuantizationLevel::SQ4, last_computed: "t".to_string(),
},
ImportanceScore {
embedding_id: "e3".to_string(),
score: 0.1, level: QuantizationLevel::F32, last_computed: "t".to_string(),
},
];
let receipts = re_quantize(&scores, &cfg);
assert_eq!(receipts.len(), 2);
assert_eq!(receipts[0].embedding_id, "e1");
assert_eq!(receipts[0].old_level, QuantizationLevel::SQ8);
assert_eq!(receipts[0].new_level, QuantizationLevel::F32);
assert_eq!(receipts[1].embedding_id, "e3");
assert_eq!(receipts[1].old_level, QuantizationLevel::F32);
assert_eq!(receipts[1].new_level, QuantizationLevel::SQ4Marked);
}
#[test]
fn re_quantize_empty_inputs() {
let cfg = ImportanceConfig::default();
let receipts = re_quantize(&[], &cfg);
assert!(receipts.is_empty());
}
#[test]
fn config_defaults() {
let cfg = ImportanceConfig::default();
assert!((cfg.access_weight - 0.40).abs() < f64::EPSILON);
assert!((cfg.entropy_weight - 0.30).abs() < f64::EPSILON);
assert!((cfg.structuring_weight - 0.30).abs() < f64::EPSILON);
assert!((cfg.access_half_life_days - 30.0).abs() < f64::EPSILON);
}
}