1use std::fs::File;
13use std::io::{Read, Write};
14use std::path::Path;
15
16use crate::config::DistanceMetric;
17use crate::error::KnnError;
18use crate::graph::{KnnGraph, NeighborList};
19
20const MAGIC: &[u8; 4] = b"FKNN";
21const VERSION: u32 = 1;
22
23fn metric_to_u8(metric: DistanceMetric) -> u8 {
24 match metric {
25 DistanceMetric::Euclidean => 0,
26 DistanceMetric::EuclideanSq => 1,
27 DistanceMetric::Cosine => 2,
28 DistanceMetric::Manhattan => 3,
29 }
30}
31
32fn metric_from_u8(v: u8) -> Result<DistanceMetric, KnnError> {
33 match v {
34 0 => Ok(DistanceMetric::Euclidean),
35 1 => Ok(DistanceMetric::EuclideanSq),
36 2 => Ok(DistanceMetric::Cosine),
37 3 => Ok(DistanceMetric::Manhattan),
38 _ => Err(KnnError::Io(format!(
39 "unknown distance metric discriminant {v}"
40 ))),
41 }
42}
43
44fn write_u32<W: Write>(w: &mut W, v: u32) -> Result<(), KnnError> {
45 w.write_all(&v.to_le_bytes())
46 .map_err(|e| KnnError::Io(e.to_string()))
47}
48
49fn write_u64<W: Write>(w: &mut W, v: u64) -> Result<(), KnnError> {
50 w.write_all(&v.to_le_bytes())
51 .map_err(|e| KnnError::Io(e.to_string()))
52}
53
54fn read_exact_arr<R: Read, const N: usize>(r: &mut R) -> Result<[u8; N], KnnError> {
55 let mut buf = [0u8; N];
56 r.read_exact(&mut buf)
57 .map_err(|e| KnnError::Io(e.to_string()))?;
58 Ok(buf)
59}
60
61fn read_u32<R: Read>(r: &mut R) -> Result<u32, KnnError> {
62 Ok(u32::from_le_bytes(read_exact_arr(r)?))
63}
64
65fn read_u64<R: Read>(r: &mut R) -> Result<u64, KnnError> {
66 Ok(u64::from_le_bytes(read_exact_arr(r)?))
67}
68
69pub fn write_knn_graph(path: &Path, graph: &KnnGraph) -> Result<(), KnnError> {
71 if graph.neighbors.len() != graph.n {
72 return Err(KnnError::GraphSizeMismatch {
73 graph_n: graph.n,
74 neighbors_len: graph.neighbors.len(),
75 data_n: graph.n,
76 });
77 }
78 for (i, nbr) in graph.neighbors.iter().enumerate() {
79 if nbr.indices.len() != graph.k || nbr.distances.len() != graph.k {
80 return Err(KnnError::Io(format!(
81 "neighbor list {i} has indices={} distances={} but graph.k={}",
82 nbr.indices.len(),
83 nbr.distances.len(),
84 graph.k
85 )));
86 }
87 }
88
89 let mut file = File::create(path).map_err(|e| KnnError::Io(e.to_string()))?;
90 file.write_all(MAGIC)
91 .map_err(|e| KnnError::Io(e.to_string()))?;
92 write_u32(&mut file, VERSION)?;
93 write_u64(&mut file, graph.n as u64)?;
94 write_u64(&mut file, graph.k as u64)?;
95 file.write_all(&[metric_to_u8(graph.metric)])
96 .map_err(|e| KnnError::Io(e.to_string()))?;
97
98 let prov = graph.provenance.as_deref().unwrap_or("");
99 let prov_bytes = prov.as_bytes();
100 if prov_bytes.len() > u32::MAX as usize {
101 return Err(KnnError::Io(
102 "provenance string exceeds u32 length".to_string(),
103 ));
104 }
105 write_u32(&mut file, prov_bytes.len() as u32)?;
106 file.write_all(prov_bytes)
107 .map_err(|e| KnnError::Io(e.to_string()))?;
108
109 let total = graph
110 .n
111 .checked_mul(graph.k)
112 .ok_or_else(|| KnnError::Io("n*k overflow".to_string()))?;
113 let byte_len = total
114 .checked_mul(4)
115 .ok_or_else(|| KnnError::Io("n*k*4 overflow".to_string()))?;
116
117 let mut idx_bytes = Vec::with_capacity(byte_len);
119 let mut dist_bytes = Vec::with_capacity(byte_len);
120 if cfg!(target_endian = "little") {
121 let mut indices = Vec::with_capacity(total);
122 let mut distances = Vec::with_capacity(total);
123 for nbr in &graph.neighbors {
124 indices.extend_from_slice(&nbr.indices);
125 distances.extend_from_slice(&nbr.distances);
126 }
127 idx_bytes.extend_from_slice(bytemuck::cast_slice::<u32, u8>(&indices));
128 dist_bytes.extend_from_slice(bytemuck::cast_slice::<f32, u8>(&distances));
129 } else {
130 for nbr in &graph.neighbors {
131 for &idx in &nbr.indices {
132 idx_bytes.extend_from_slice(&idx.to_le_bytes());
133 }
134 }
135 for nbr in &graph.neighbors {
136 for &dist in &nbr.distances {
137 dist_bytes.extend_from_slice(&dist.to_le_bytes());
138 }
139 }
140 }
141 file.write_all(&idx_bytes)
142 .map_err(|e| KnnError::Io(e.to_string()))?;
143 file.write_all(&dist_bytes)
144 .map_err(|e| KnnError::Io(e.to_string()))?;
145 file.sync_all().map_err(|e| KnnError::Io(e.to_string()))?;
146 Ok(())
147}
148
149pub fn read_knn_graph(path: &Path) -> Result<KnnGraph, KnnError> {
151 let mut file = File::open(path).map_err(|e| KnnError::Io(e.to_string()))?;
152 let magic = read_exact_arr::<_, 4>(&mut file)?;
153 if &magic != MAGIC {
154 return Err(KnnError::Io(format!(
155 "bad knn.bin magic: expected FKNN, got {:?}",
156 String::from_utf8_lossy(&magic)
157 )));
158 }
159 let version = read_u32(&mut file)?;
160 if version != VERSION {
161 return Err(KnnError::Io(format!(
162 "unsupported knn.bin version {version} (expected {VERSION})"
163 )));
164 }
165 let n = read_u64(&mut file)? as usize;
166 let k = read_u64(&mut file)? as usize;
167 let metric = metric_from_u8(read_exact_arr::<_, 1>(&mut file)?[0])?;
168 let prov_len = read_u32(&mut file)? as usize;
169 let provenance = if prov_len == 0 {
170 None
171 } else {
172 let mut buf = vec![0u8; prov_len];
173 file.read_exact(&mut buf)
174 .map_err(|e| KnnError::Io(e.to_string()))?;
175 let s = String::from_utf8(buf).map_err(|e| KnnError::Io(e.to_string()))?;
176 Some(s)
177 };
178
179 let total = n
180 .checked_mul(k)
181 .ok_or_else(|| KnnError::Io("n*k overflow".to_string()))?;
182 let byte_len = total
183 .checked_mul(4)
184 .ok_or_else(|| KnnError::Io("n*k*4 overflow".to_string()))?;
185
186 let (indices, distances) = if cfg!(target_endian = "little") {
188 let mut indices = vec![0u32; total];
189 file.read_exact(bytemuck::cast_slice_mut(&mut indices))
190 .map_err(|e| KnnError::Io(e.to_string()))?;
191 let mut distances = vec![0f32; total];
192 file.read_exact(bytemuck::cast_slice_mut(&mut distances))
193 .map_err(|e| KnnError::Io(e.to_string()))?;
194 (indices, distances)
195 } else {
196 let mut idx_bytes = vec![0u8; byte_len];
197 file.read_exact(&mut idx_bytes)
198 .map_err(|e| KnnError::Io(e.to_string()))?;
199 let mut dist_bytes = vec![0u8; byte_len];
200 file.read_exact(&mut dist_bytes)
201 .map_err(|e| KnnError::Io(e.to_string()))?;
202 let indices: Vec<u32> = idx_bytes
203 .chunks_exact(4)
204 .map(|c| u32::from_le_bytes(c.try_into().unwrap()))
205 .collect();
206 let distances: Vec<f32> = dist_bytes
207 .chunks_exact(4)
208 .map(|c| f32::from_le_bytes(c.try_into().unwrap()))
209 .collect();
210 (indices, distances)
211 };
212
213 let mut neighbors = Vec::with_capacity(n);
214 for i in 0..n {
215 let start = i * k;
216 let end = start + k;
217 neighbors.push(NeighborList {
218 indices: indices[start..end].to_vec(),
219 distances: distances[start..end].to_vec(),
220 });
221 }
222
223 Ok(KnnGraph {
224 neighbors,
225 n,
226 k,
227 metric,
228 provenance,
229 })
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235 use crate::{KnnMethod, compute_knn};
236 use std::env::temp_dir;
237
238 fn make_grid(n: usize) -> Vec<f32> {
239 (0..n).flat_map(|i| [i as f32, 0.0]).collect()
240 }
241
242 #[test]
243 fn round_trip_preserves_graph() {
244 let data = make_grid(12);
245 let graph = compute_knn(
246 &data,
247 12,
248 2,
249 4,
250 &KnnMethod::Exact,
251 DistanceMetric::Euclidean,
252 )
253 .unwrap();
254 let path = temp_dir().join(format!(
255 "flow-knn-roundtrip-{}.bin",
256 std::process::id()
257 ));
258 write_knn_graph(&path, &graph).unwrap();
259 let loaded = read_knn_graph(&path).unwrap();
260 let _ = std::fs::remove_file(&path);
261
262 assert_eq!(loaded.n, graph.n);
263 assert_eq!(loaded.k, graph.k);
264 assert_eq!(loaded.metric, graph.metric);
265 assert_eq!(loaded.provenance, graph.provenance);
266 for (a, b) in loaded.neighbors.iter().zip(graph.neighbors.iter()) {
267 assert_eq!(a.indices, b.indices);
268 assert_eq!(a.distances, b.distances);
269 }
270 }
271
272 #[test]
273 fn rejects_bad_magic() {
274 let path = temp_dir().join(format!("flow-knn-bad-magic-{}.bin", std::process::id()));
275 std::fs::write(&path, b"XXXX\0\0\0\x01").unwrap();
276 let err = read_knn_graph(&path).unwrap_err();
277 let _ = std::fs::remove_file(&path);
278 assert!(matches!(err, KnnError::Io(_)));
279 assert!(err.to_string().contains("magic"));
280 }
281
282 #[test]
283 fn rejects_bad_version() {
284 let path = temp_dir().join(format!("flow-knn-bad-ver-{}.bin", std::process::id()));
285 let mut bytes = Vec::new();
286 bytes.extend_from_slice(MAGIC);
287 bytes.extend_from_slice(&99u32.to_le_bytes());
288 std::fs::write(&path, &bytes).unwrap();
289 let err = read_knn_graph(&path).unwrap_err();
290 let _ = std::fs::remove_file(&path);
291 assert!(err.to_string().contains("version"));
292 }
293
294 #[test]
295 fn round_trip_preserves_metric_and_provenance() {
296 let data = make_grid(8);
297 let mut graph = compute_knn(
298 &data,
299 8,
300 2,
301 2,
302 &KnnMethod::Exact,
303 DistanceMetric::Manhattan,
304 )
305 .unwrap();
306 graph.provenance = Some("unit-test".to_string());
307 let path = temp_dir().join(format!("flow-knn-metric-{}.bin", std::process::id()));
308 write_knn_graph(&path, &graph).unwrap();
309 let loaded = read_knn_graph(&path).unwrap();
310 let _ = std::fs::remove_file(&path);
311 assert_eq!(loaded.metric, DistanceMetric::Manhattan);
312 assert_eq!(loaded.provenance.as_deref(), Some("unit-test"));
313 }
314}