Skip to main content

ipfrs_semantic/quantization/
scalar.rs

1//! Scalar (INT8/UINT8) vector quantization.
2
3use ipfrs_core::{Error, Result};
4use serde::{Deserialize, Serialize};
5
6/// Scalar quantization configuration
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct ScalarQuantizerConfig {
9    /// Number of bits for quantization (8 for int8/uint8)
10    pub bits: u8,
11    /// Whether to use signed quantization (int8 vs uint8)
12    pub signed: bool,
13    /// Per-dimension min values
14    pub min_values: Vec<f32>,
15    /// Per-dimension max values
16    pub max_values: Vec<f32>,
17}
18
19/// Scalar quantizer for vector compression
20///
21/// Quantizes floating-point vectors to int8 or uint8, achieving 4x compression
22/// with typically < 5% accuracy loss.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ScalarQuantizer {
25    /// Quantization configuration
26    config: ScalarQuantizerConfig,
27    /// Vector dimension
28    dimension: usize,
29    /// Whether the quantizer has been trained
30    trained: bool,
31}
32
33impl ScalarQuantizer {
34    /// Create a new scalar quantizer for the given dimension
35    ///
36    /// # Arguments
37    /// * `dimension` - Vector dimension
38    /// * `signed` - Use int8 (true) or uint8 (false)
39    pub fn new(dimension: usize, signed: bool) -> Self {
40        Self {
41            config: ScalarQuantizerConfig {
42                bits: 8,
43                signed,
44                min_values: vec![f32::MAX; dimension],
45                max_values: vec![f32::MIN; dimension],
46            },
47            dimension,
48            trained: false,
49        }
50    }
51
52    /// Create a quantizer with uint8 (unsigned) quantization
53    pub fn uint8(dimension: usize) -> Self {
54        Self::new(dimension, false)
55    }
56
57    /// Create a quantizer with int8 (signed) quantization
58    pub fn int8(dimension: usize) -> Self {
59        Self::new(dimension, true)
60    }
61
62    /// Train the quantizer on a set of vectors
63    ///
64    /// Learns min/max values for each dimension to establish
65    /// the quantization range.
66    ///
67    /// # Arguments
68    /// * `vectors` - Training vectors
69    pub fn train(&mut self, vectors: &[Vec<f32>]) -> Result<()> {
70        if vectors.is_empty() {
71            return Err(Error::InvalidInput(
72                "Cannot train on empty vector set".to_string(),
73            ));
74        }
75
76        // Validate dimensions
77        for (i, vec) in vectors.iter().enumerate() {
78            if vec.len() != self.dimension {
79                return Err(Error::InvalidInput(format!(
80                    "Vector {} has dimension {}, expected {}",
81                    i,
82                    vec.len(),
83                    self.dimension
84                )));
85            }
86        }
87
88        // Reset min/max values
89        self.config.min_values = vec![f32::MAX; self.dimension];
90        self.config.max_values = vec![f32::MIN; self.dimension];
91
92        // Compute per-dimension min/max
93        for vec in vectors {
94            for (i, &val) in vec.iter().enumerate() {
95                if val < self.config.min_values[i] {
96                    self.config.min_values[i] = val;
97                }
98                if val > self.config.max_values[i] {
99                    self.config.max_values[i] = val;
100                }
101            }
102        }
103
104        // Add small margin to avoid edge cases
105        for i in 0..self.dimension {
106            let range = self.config.max_values[i] - self.config.min_values[i];
107            if range < 1e-6 {
108                // Constant dimension, set arbitrary range
109                self.config.min_values[i] -= 0.5;
110                self.config.max_values[i] += 0.5;
111            } else {
112                let margin = range * 0.01;
113                self.config.min_values[i] -= margin;
114                self.config.max_values[i] += margin;
115            }
116        }
117
118        self.trained = true;
119        Ok(())
120    }
121
122    /// Train on a single vector (incremental training)
123    pub fn train_incremental(&mut self, vector: &[f32]) -> Result<()> {
124        if vector.len() != self.dimension {
125            return Err(Error::InvalidInput(format!(
126                "Vector has dimension {}, expected {}",
127                vector.len(),
128                self.dimension
129            )));
130        }
131
132        for (i, &val) in vector.iter().enumerate() {
133            if val < self.config.min_values[i] {
134                self.config.min_values[i] = val;
135            }
136            if val > self.config.max_values[i] {
137                self.config.max_values[i] = val;
138            }
139        }
140
141        self.trained = true;
142        Ok(())
143    }
144
145    /// Check if the quantizer has been trained
146    pub fn is_trained(&self) -> bool {
147        self.trained
148    }
149
150    /// Quantize a vector to uint8
151    pub fn quantize(&self, vector: &[f32]) -> Result<QuantizedVector> {
152        if !self.trained {
153            return Err(Error::InvalidInput(
154                "Quantizer must be trained before use".to_string(),
155            ));
156        }
157
158        if vector.len() != self.dimension {
159            return Err(Error::InvalidInput(format!(
160                "Vector has dimension {}, expected {}",
161                vector.len(),
162                self.dimension
163            )));
164        }
165
166        let mut quantized = Vec::with_capacity(self.dimension);
167
168        for (i, &val) in vector.iter().enumerate() {
169            let min = self.config.min_values[i];
170            let max = self.config.max_values[i];
171            let range = max - min;
172
173            // Normalize to [0, 1]
174            let normalized = if range > 1e-6 {
175                ((val - min) / range).clamp(0.0, 1.0)
176            } else {
177                0.5
178            };
179
180            // Quantize
181            let q = if self.config.signed {
182                // int8: [-128, 127]
183                ((normalized * 255.0 - 128.0).round() as i8) as u8
184            } else {
185                // uint8: [0, 255]
186                (normalized * 255.0).round() as u8
187            };
188
189            quantized.push(q);
190        }
191
192        Ok(QuantizedVector {
193            data: quantized,
194            signed: self.config.signed,
195        })
196    }
197
198    /// Dequantize a vector back to f32
199    pub fn dequantize(&self, quantized: &QuantizedVector) -> Result<Vec<f32>> {
200        if !self.trained {
201            return Err(Error::InvalidInput(
202                "Quantizer must be trained before use".to_string(),
203            ));
204        }
205
206        if quantized.data.len() != self.dimension {
207            return Err(Error::InvalidInput(format!(
208                "Quantized vector has dimension {}, expected {}",
209                quantized.data.len(),
210                self.dimension
211            )));
212        }
213
214        let mut result = Vec::with_capacity(self.dimension);
215
216        for (i, &q) in quantized.data.iter().enumerate() {
217            let min = self.config.min_values[i];
218            let max = self.config.max_values[i];
219            let range = max - min;
220
221            // Dequantize
222            let normalized = if self.config.signed {
223                // int8: [-128, 127] -> [0, 1]
224                ((q as i8) as f32 + 128.0) / 255.0
225            } else {
226                // uint8: [0, 255] -> [0, 1]
227                q as f32 / 255.0
228            };
229
230            let val = min + normalized * range;
231            result.push(val);
232        }
233
234        Ok(result)
235    }
236
237    /// Compute L2 distance between two quantized vectors (approximate)
238    ///
239    /// Uses integer arithmetic for fast computation
240    pub fn distance_l2_quantized(&self, a: &QuantizedVector, b: &QuantizedVector) -> Result<f32> {
241        if a.data.len() != b.data.len() {
242            return Err(Error::InvalidInput(
243                "Vectors must have same dimension".to_string(),
244            ));
245        }
246
247        let mut sum_sq: i64 = 0;
248
249        for (qa, qb) in a.data.iter().zip(b.data.iter()) {
250            let diff = if a.signed {
251                (*qa as i8 as i64) - (*qb as i8 as i64)
252            } else {
253                (*qa as i64) - (*qb as i64)
254            };
255            sum_sq += diff * diff;
256        }
257
258        // Scale back to approximate original distance
259        // This is an approximation since we lose per-dimension scaling info
260        Ok((sum_sq as f32).sqrt() / 255.0)
261    }
262
263    /// Compute dot product between two quantized vectors (approximate)
264    pub fn dot_product_quantized(&self, a: &QuantizedVector, b: &QuantizedVector) -> Result<f32> {
265        if a.data.len() != b.data.len() {
266            return Err(Error::InvalidInput(
267                "Vectors must have same dimension".to_string(),
268            ));
269        }
270
271        let mut sum: i64 = 0;
272
273        for (qa, qb) in a.data.iter().zip(b.data.iter()) {
274            if a.signed {
275                sum += (*qa as i8 as i64) * (*qb as i8 as i64);
276            } else {
277                sum += (*qa as i64) * (*qb as i64);
278            }
279        }
280
281        // Normalize
282        Ok(sum as f32 / (255.0 * 255.0))
283    }
284
285    /// Get the dimension
286    pub fn dimension(&self) -> usize {
287        self.dimension
288    }
289
290    /// Get compression ratio (always 4x for 8-bit quantization)
291    pub fn compression_ratio(&self) -> f32 {
292        4.0 // f32 (4 bytes) -> u8 (1 byte)
293    }
294
295    /// Get memory usage estimate for a given number of vectors
296    pub fn memory_estimate(&self, num_vectors: usize) -> usize {
297        // Per-vector: dimension bytes
298        // Plus overhead: 2 * dimension * 4 bytes for min/max values
299        num_vectors * self.dimension + 2 * self.dimension * 4
300    }
301}
302
303/// Quantized vector storage
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub struct QuantizedVector {
306    /// Quantized data (uint8 storage)
307    pub data: Vec<u8>,
308    /// Whether values are signed
309    pub signed: bool,
310}
311
312impl QuantizedVector {
313    /// Create a new quantized vector
314    pub fn new(data: Vec<u8>, signed: bool) -> Self {
315        Self { data, signed }
316    }
317
318    /// Get the dimension
319    pub fn dimension(&self) -> usize {
320        self.data.len()
321    }
322
323    /// Get the raw bytes
324    pub fn as_bytes(&self) -> &[u8] {
325        &self.data
326    }
327
328    /// Get memory size in bytes
329    pub fn size_bytes(&self) -> usize {
330        self.data.len()
331    }
332}