Skip to main content

diskann_tools/utils/
random_data_generator.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6use std::io::{BufWriter, Write};
7
8use byteorder::{LittleEndian, WriteBytesExt};
9use diskann_providers::storage::StorageWriteProvider;
10use diskann_utils::{io::Metadata, sampling::random::WithApproximateNorm};
11use diskann_vector::Half;
12
13use crate::utils::{CMDResult, CMDToolError, DataType};
14
15type WriteVectorMethodType<T> = Box<dyn Fn(&mut BufWriter<T>, &Vec<f32>) -> CMDResult<bool>>;
16
17/**
18Generate random points around a sphere with the specified radius and write them to a file
19
20When data_type is int8 or uint8 radius must be <= 127.0
21 */
22#[allow(clippy::panic)]
23pub fn write_random_data<StorageProvider: StorageWriteProvider>(
24    storage_provider: &StorageProvider,
25    output_file: &str,
26    data_type: DataType,
27    number_of_dimensions: usize,
28    number_of_vectors: u64,
29    radius: f32,
30) -> CMDResult<()> {
31    if (data_type == DataType::Int8 || data_type == DataType::Uint8)
32        && (radius > 127.0 || radius <= 0.0)
33    {
34        return Err(CMDToolError {
35            details:
36            "Error: for int8/uint8 datatypes, radius (L2 norm) cannot be greater than 127 and less than or equal to 0"
37                .to_string(),
38        });
39    }
40
41    let file = storage_provider.create_for_write(output_file)?;
42    let writer = BufWriter::new(file);
43
44    write_random_data_writer(
45        writer,
46        data_type,
47        number_of_dimensions,
48        number_of_vectors,
49        radius,
50    )
51}
52
53/**
54Generate random points around a sphere with the specified radius and write them to a file
55
56When data_type is int8 or uint8 radius must be <= 127.0
57*/
58#[allow(clippy::panic)]
59fn write_random_data_writer<T: Sized + Write>(
60    mut writer: BufWriter<T>,
61    data_type: DataType,
62    number_of_dimensions: usize,
63    number_of_vectors: u64,
64    radius: f32,
65) -> CMDResult<()> {
66    if (data_type == DataType::Int8 || data_type == DataType::Uint8)
67        && (radius > 127.0 || radius <= 0.0)
68    {
69        return Err(CMDToolError {
70            details:
71                "Error: for int8/uint8 datatypes, radius (L2 norm) cannot be greater than 127 and less than or equal to 0"
72                    .to_string(),
73        });
74    }
75
76    Metadata::new(number_of_vectors, number_of_dimensions)?.write(&mut writer)?;
77
78    let block_size = 131072;
79    let nblks = u64::div_ceil(number_of_vectors, block_size);
80    println!("# blks: {}", nblks);
81
82    for i in 0..nblks {
83        let cblk_size = std::cmp::min(number_of_vectors - i * block_size, block_size);
84
85        // Each data has special code to write it out.  These methods convert the random data
86        // from the input vector into the specific datatype and writes it out to the data file.
87        let write_method: WriteVectorMethodType<T> = match data_type {
88            DataType::Float => Box::new(
89                |writer: &mut BufWriter<T>, vector: &Vec<f32>| -> CMDResult<bool> {
90                    let mut found_nonzero = false;
91                    for value in vector {
92                        writer.write_f32::<LittleEndian>(*value)?;
93                        found_nonzero = found_nonzero || ((*value != 0f32) && value.is_finite());
94                    }
95                    Ok(found_nonzero)
96                },
97            ),
98            DataType::Uint8 => Box::new(
99                |writer: &mut BufWriter<T>, vector: &Vec<f32>| -> CMDResult<bool> {
100                    let mut found_nonempty = false;
101                    // Since u8 is unsigned, add 128 to ensure non-negative before
102                    // rounding and casting
103                    for value in vector.iter().map(|&item| (item + 128.0).round() as u8) {
104                        writer.write_u8(value)?;
105
106                        // Since we add 128 to the random number to prevent negative values,
107                        // 'empty' is a vector where all indices hold 128u8.
108                        found_nonempty = found_nonempty || (value != 128u8);
109                    }
110                    Ok(found_nonempty)
111                },
112            ),
113            DataType::Int8 => Box::new(
114                |writer: &mut BufWriter<T>, vector: &Vec<f32>| -> CMDResult<bool> {
115                    let mut found_nonzero = false;
116                    for value in vector.iter().map(|&item| item.round() as i8) {
117                        writer.write_i8(value)?;
118                        found_nonzero = found_nonzero || (value != 0i8);
119                    }
120                    Ok(found_nonzero)
121                },
122            ),
123            DataType::Fp16 => Box::new(
124                |writer: &mut BufWriter<T>, vector: &Vec<f32>| -> CMDResult<bool> {
125                    let mut found_nonzero = false;
126                    for value in vector.iter().map(|&item| Half::from_f32(item)) {
127                        let mut buf = [0; 2];
128                        buf.clone_from_slice(value.to_le_bytes().as_slice());
129                        writer.write_all(&buf)?;
130                        found_nonzero =
131                            found_nonzero || (value != Half::from_f32(0.0) && value.is_finite());
132                    }
133                    Ok(found_nonzero)
134                },
135            ),
136        };
137
138        // Propagate errors if there are any
139        write_random_vector_block(
140            write_method,
141            &mut writer,
142            number_of_dimensions,
143            cblk_size,
144            radius,
145        )?;
146    }
147
148    // writer flushes the inner file object as part of it's flush.  File object moved
149    // to writer scope so we cannot manually call flush on it here.
150    writer.flush()?;
151
152    Ok(())
153}
154
155/**
156Writes random vectors to the specified writer.  Function generates random floats.  It is the
157responsibility of the "write_method" method argument to convert the random floats into other
158datatypes.
159
160NOTE: This generates random points on a sphere that has the specified radius
161*/
162fn write_random_vector_block<
163    F: Sized + Write,
164    T: FnMut(&mut BufWriter<F>, &Vec<f32>) -> CMDResult<bool>,
165>(
166    mut write_method: T,
167    writer: &mut BufWriter<F>,
168    number_of_dimensions: usize,
169    number_of_points: u64,
170    radius: f32,
171) -> CMDResult<()> {
172    let mut found_nonzero = false;
173
174    let mut rng = diskann_providers::utils::create_rnd_from_seed(0);
175    for _ in 0..number_of_points {
176        let vector = f32::with_approximate_norm(number_of_dimensions, radius, &mut rng);
177        // Check for non-zero after casting to final numeric types.  Do not short-circuit
178        // evaluate to ensure we always write the data.
179        found_nonzero |= write_method(writer, &vector)?;
180    }
181
182    if found_nonzero {
183        Ok(())
184    } else {
185        Err(CMDToolError {
186            details: format!(
187                "Generated all-zero vectors with radius {}. Try increasing radius",
188                radius
189            ),
190        })
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use diskann_providers::storage::VirtualStorageProvider;
197    use rstest::rstest;
198
199    use super::*;
200
201    /// The small dataset size for testing the library.
202    /// A prime number is used to avoid any accidental patterns in the data.
203    #[cfg(not(miri))]
204    const TEST_DATASET_SIZE_SMALL: u64 = 101;
205
206    /// The recommended number of dimensions for testing the library.
207    /// A prime number is used to avoid any accidental patterns in the data.
208    /// When "memory aligned" the dimensions become 64 (8*8). Setting to non-aligned value to ensure aligning works.
209    #[cfg(not(miri))]
210    const TEST_NUM_DIMENSIONS_RECOMMENDED: usize = 59;
211
212    /// The small dataset size for testing the library.
213    /// A prime number is used to avoid any accidental patterns in the data.
214    #[cfg(miri)]
215    const TEST_DATASET_SIZE_SMALL: u64 = 3;
216
217    /// The recommended number of dimensions for testing the library.
218    /// A prime number is used to avoid any accidental patterns in the data.
219    /// When "memory aligned" the dimensions become 16 (8*2). Setting to non-aligned value to ensure aligning works.
220    #[cfg(miri)]
221    const TEST_NUM_DIMENSIONS_RECOMMENDED: usize = 13;
222
223    #[rstest]
224    fn random_data_write_success(
225        #[values(DataType::Float, DataType::Uint8, DataType::Int8)] data_type: DataType,
226        #[values(100.0, 127.0)] norm: f32,
227    ) {
228        let random_data_path = "/mydatafile.bin";
229        let num_dimensions = TEST_NUM_DIMENSIONS_RECOMMENDED;
230
231        let storage_provider = VirtualStorageProvider::new_overlay(".");
232        let result = write_random_data(
233            &storage_provider,
234            random_data_path,
235            data_type,
236            num_dimensions,
237            10000,
238            norm,
239        );
240
241        assert!(result.is_ok(), "write_random_data should succeed");
242        assert!(
243            storage_provider.exists(random_data_path),
244            "Random data file should exist"
245        );
246    }
247
248    /// Very low values of "radius" cause the random data to all be zero.
249    /// Ensure that an appropriate error is returned when invalid radius is used.
250    #[rstest]
251    #[case(DataType::Float, 0.0)]
252    #[case(DataType::Int8, 0.0)]
253    #[case(DataType::Int8, 0.1)]
254    #[case(DataType::Int8, 1.0)]
255    #[case(DataType::Uint8, 0.0)]
256    #[case(DataType::Uint8, 0.1)]
257    #[case(DataType::Uint8, 1.0)]
258    fn random_data_write_too_low_norm(#[case] data_type: DataType, #[case] radius: f32) {
259        let random_data_path = "/mydatafile.bin";
260        let num_dimensions = TEST_NUM_DIMENSIONS_RECOMMENDED;
261
262        let expected = if (data_type == DataType::Int8 || data_type == DataType::Uint8)
263            && radius <= 0.0
264        {
265            Err(CMDToolError {
266                details:
267                    "Error: for int8/uint8 datatypes, radius (L2 norm) cannot be greater than 127 and less than or equal to 0"
268                        .to_string(),
269            })
270        } else {
271            Err(CMDToolError {
272                details: format!(
273                    "Generated all-zero vectors with radius {}. Try increasing radius",
274                    radius
275                ),
276            })
277        };
278
279        let storage_provider = VirtualStorageProvider::new_overlay(".");
280        let result = write_random_data(
281            &storage_provider,
282            random_data_path,
283            data_type,
284            num_dimensions,
285            TEST_DATASET_SIZE_SMALL,
286            radius,
287        );
288
289        assert_eq!(expected, result);
290    }
291
292    #[test]
293    fn test_fp16_data_type() {
294        let random_data_path = "/fp16_data.bin";
295        let num_dimensions = TEST_NUM_DIMENSIONS_RECOMMENDED;
296
297        let storage_provider = VirtualStorageProvider::new_overlay(".");
298        let result = write_random_data(
299            &storage_provider,
300            random_data_path,
301            DataType::Fp16,
302            num_dimensions,
303            100,
304            50.0,
305        );
306
307        assert!(result.is_ok(), "write_random_data with Fp16 should succeed");
308        assert!(storage_provider.exists(random_data_path));
309    }
310
311    #[test]
312    fn test_invalid_radius_for_int8() {
313        let random_data_path = "/invalid_int8.bin";
314        let storage_provider = VirtualStorageProvider::new_overlay(".");
315
316        let expected = Err(CMDToolError {
317            details:
318                "Error: for int8/uint8 datatypes, radius (L2 norm) cannot be greater than 127 and less than or equal to 0"
319                    .to_string(),
320        });
321        let result = write_random_data(
322            &storage_provider,
323            random_data_path,
324            DataType::Int8,
325            10,
326            100,
327            128.0,
328        );
329
330        assert_eq!(expected, result);
331    }
332
333    #[test]
334    fn test_invalid_radius_for_uint8() {
335        let random_data_path = "/invalid_uint8.bin";
336        let storage_provider = VirtualStorageProvider::new_overlay(".");
337
338        let expected = Err(CMDToolError {
339            details:
340                "Error: for int8/uint8 datatypes, radius (L2 norm) cannot be greater than 127 and less than or equal to 0"
341                    .to_string(),
342        });
343        let result = write_random_data(
344            &storage_provider,
345            random_data_path,
346            DataType::Uint8,
347            10,
348            100,
349            150.0,
350        );
351
352        assert_eq!(expected, result);
353    }
354
355    #[test]
356    fn test_small_dataset() {
357        let random_data_path = "/small_data.bin";
358        let storage_provider = VirtualStorageProvider::new_overlay(".");
359
360        // Test with very small dataset
361        let result = write_random_data(
362            &storage_provider,
363            random_data_path,
364            DataType::Float,
365            5,
366            10,
367            100.0,
368        );
369
370        assert!(result.is_ok());
371        assert!(storage_provider.exists(random_data_path));
372    }
373
374    #[test]
375    fn test_large_block_size() {
376        let random_data_path = "/large_blocks.bin";
377        let storage_provider = VirtualStorageProvider::new_overlay(".");
378
379        // Test with more than one block
380        let result = write_random_data(
381            &storage_provider,
382            random_data_path,
383            DataType::Float,
384            10,
385            200000, // More than block_size (131072)
386            100.0,
387        );
388
389        assert!(result.is_ok());
390        assert!(storage_provider.exists(random_data_path));
391    }
392}