1use std::io::{Read, Write};
11
12#[derive(Debug, Clone, PartialEq)]
16pub struct BinaryHeader {
17 pub magic: [u8; 4],
19 pub version: u32,
21 pub n_atoms: u32,
23 pub n_frames: u32,
25 pub dt: f64,
27 pub endian: u8,
29}
30
31pub const HEADER_SIZE: usize = 4 + 4 + 4 + 4 + 8 + 1; pub fn write_binary_header(path: &str, header: &BinaryHeader) -> Result<(), std::io::Error> {
36 let mut file = std::fs::File::create(path)?;
37 file.write_all(&header.magic)?;
38 file.write_all(&header.version.to_le_bytes())?;
39 file.write_all(&header.n_atoms.to_le_bytes())?;
40 file.write_all(&header.n_frames.to_le_bytes())?;
41 file.write_all(&f64_to_bytes_le(header.dt))?;
42 file.write_all(&[header.endian])?;
43 Ok(())
44}
45
46pub fn read_binary_header(path: &str) -> Result<BinaryHeader, std::io::Error> {
48 let mut file = std::fs::File::open(path)?;
49 let mut magic = [0u8; 4];
50 file.read_exact(&mut magic)?;
51 let mut buf4 = [0u8; 4];
52 file.read_exact(&mut buf4)?;
53 let version = u32::from_le_bytes(buf4);
54 file.read_exact(&mut buf4)?;
55 let n_atoms = u32::from_le_bytes(buf4);
56 file.read_exact(&mut buf4)?;
57 let n_frames = u32::from_le_bytes(buf4);
58 let mut buf8 = [0u8; 8];
59 file.read_exact(&mut buf8)?;
60 let dt = f64_from_bytes_le(buf8);
61 let mut endian_buf = [0u8; 1];
62 file.read_exact(&mut endian_buf)?;
63 Ok(BinaryHeader {
64 magic,
65 version,
66 n_atoms,
67 n_frames,
68 dt,
69 endian: endian_buf[0],
70 })
71}
72
73pub fn write_frame_binary(
79 file: &mut std::fs::File,
80 positions: &[[f32; 3]],
81) -> Result<(), std::io::Error> {
82 for pos in positions {
83 file.write_all(&f32_to_bytes_le(pos[0]))?;
84 file.write_all(&f32_to_bytes_le(pos[1]))?;
85 file.write_all(&f32_to_bytes_le(pos[2]))?;
86 }
87 Ok(())
88}
89
90pub fn read_frame_binary(
92 file: &mut std::fs::File,
93 n_atoms: usize,
94) -> Result<Vec<[f32; 3]>, std::io::Error> {
95 let mut positions = Vec::with_capacity(n_atoms);
96 let mut buf4 = [0u8; 4];
97 for _ in 0..n_atoms {
98 file.read_exact(&mut buf4)?;
99 let x = f32_from_bytes_le(buf4);
100 file.read_exact(&mut buf4)?;
101 let y = f32_from_bytes_le(buf4);
102 file.read_exact(&mut buf4)?;
103 let z = f32_from_bytes_le(buf4);
104 positions.push([x, y, z]);
105 }
106 Ok(positions)
107}
108
109pub fn f64_to_bytes_le(val: f64) -> [u8; 8] {
113 val.to_bits().to_le_bytes()
114}
115
116pub fn f64_from_bytes_le(bytes: [u8; 8]) -> f64 {
118 f64::from_bits(u64::from_le_bytes(bytes))
119}
120
121pub fn f32_to_bytes_le(val: f32) -> [u8; 4] {
123 val.to_bits().to_le_bytes()
124}
125
126pub fn f32_from_bytes_le(bytes: [u8; 4]) -> f32 {
128 f32::from_bits(u32::from_le_bytes(bytes))
129}
130
131pub fn compress_positions_quantized(positions: &[[f32; 3]], scale: f32) -> Vec<i16> {
139 if scale == 0.0 {
140 return vec![0i16; positions.len() * 3];
141 }
142 let inv = 1.0 / scale;
143 let mut out = Vec::with_capacity(positions.len() * 3);
144 for pos in positions {
145 for &c in pos {
146 let q = (c * inv).round().clamp(i16::MIN as f32, i16::MAX as f32) as i16;
147 out.push(q);
148 }
149 }
150 out
151}
152
153pub fn decompress_positions_quantized(data: &[i16], scale: f32) -> Vec<[f32; 3]> {
157 let n = data.len() / 3;
158 let mut out = Vec::with_capacity(n);
159 for i in 0..n {
160 let x = data[i * 3] as f32 * scale;
161 let y = data[i * 3 + 1] as f32 * scale;
162 let z = data[i * 3 + 2] as f32 * scale;
163 out.push([x, y, z]);
164 }
165 out
166}
167
168#[cfg(test)]
171mod tests {
172 use super::*;
173
174 fn sample_header() -> BinaryHeader {
175 BinaryHeader {
176 magic: *b"OXIP",
177 version: 1,
178 n_atoms: 100,
179 n_frames: 50,
180 dt: 0.002,
181 endian: 0,
182 }
183 }
184
185 #[test]
186 fn test_f64_roundtrip() {
187 let val = std::f64::consts::PI;
188 let bytes = f64_to_bytes_le(val);
189 let back = f64_from_bytes_le(bytes);
190 assert!((back - val).abs() < 1e-15);
191 }
192
193 #[test]
194 fn test_f64_zero() {
195 let bytes = f64_to_bytes_le(0.0);
196 let back = f64_from_bytes_le(bytes);
197 assert!((back).abs() < 1e-15);
198 }
199
200 #[test]
201 fn test_f64_negative() {
202 let val = -1.23456789e-10;
203 let bytes = f64_to_bytes_le(val);
204 let back = f64_from_bytes_le(bytes);
205 assert!((back - val).abs() < 1e-20);
206 }
207
208 #[test]
209 fn test_f32_roundtrip() {
210 let val = 3.15625_f32;
211 let bytes = f32_to_bytes_le(val);
212 let back = f32_from_bytes_le(bytes);
213 assert!((back - val).abs() < 1e-6);
214 }
215
216 #[test]
217 fn test_f32_negative() {
218 let val = -0.001_f32;
219 let bytes = f32_to_bytes_le(val);
220 let back = f32_from_bytes_le(bytes);
221 assert!((back - val).abs() < 1e-7);
222 }
223
224 #[test]
225 fn test_write_read_header_roundtrip() {
226 let path = "/tmp/test_oxiphysics_header.bin";
227 let hdr = sample_header();
228 write_binary_header(path, &hdr).unwrap();
229 let hdr2 = read_binary_header(path).unwrap();
230 assert_eq!(hdr2.magic, *b"OXIP");
231 assert_eq!(hdr2.version, 1);
232 assert_eq!(hdr2.n_atoms, 100);
233 assert_eq!(hdr2.n_frames, 50);
234 assert!((hdr2.dt - 0.002).abs() < 1e-12);
235 assert_eq!(hdr2.endian, 0);
236 }
237
238 #[test]
239 fn test_header_magic() {
240 let path = "/tmp/test_oxiphysics_header_magic.bin";
241 let hdr = BinaryHeader {
242 magic: *b"TEST",
243 ..sample_header()
244 };
245 write_binary_header(path, &hdr).unwrap();
246 let hdr2 = read_binary_header(path).unwrap();
247 assert_eq!(hdr2.magic, *b"TEST");
248 }
249
250 #[test]
251 fn test_header_size() {
252 assert_eq!(HEADER_SIZE, 25);
253 }
254
255 #[test]
256 fn test_write_read_frame_roundtrip() {
257 let path = "/tmp/test_oxiphysics_frame.bin";
258 let positions = vec![[1.0_f32, 2.0, 3.0], [4.0, 5.0, 6.0], [-1.0, 0.0, 0.5]];
259 {
260 let mut file = std::fs::File::create(path).unwrap();
261 write_frame_binary(&mut file, &positions).unwrap();
262 }
263 {
264 let mut file = std::fs::File::open(path).unwrap();
265 let back = read_frame_binary(&mut file, 3).unwrap();
266 assert_eq!(back.len(), 3);
267 assert!((back[0][0] - 1.0).abs() < 1e-6);
268 assert!((back[2][2] - 0.5).abs() < 1e-6);
269 }
270 }
271
272 #[test]
273 fn test_write_read_frame_single_atom() {
274 let path = "/tmp/test_oxiphysics_frame_single.bin";
275 let positions = vec![[0.1_f32, 0.2, 0.3]];
276 {
277 let mut file = std::fs::File::create(path).unwrap();
278 write_frame_binary(&mut file, &positions).unwrap();
279 }
280 {
281 let mut file = std::fs::File::open(path).unwrap();
282 let back = read_frame_binary(&mut file, 1).unwrap();
283 assert!((back[0][1] - 0.2).abs() < 1e-6);
284 }
285 }
286
287 #[test]
288 fn test_write_read_empty_frame() {
289 let path = "/tmp/test_oxiphysics_frame_empty.bin";
290 {
291 let mut file = std::fs::File::create(path).unwrap();
292 write_frame_binary(&mut file, &[]).unwrap();
293 }
294 {
295 let mut file = std::fs::File::open(path).unwrap();
296 let back = read_frame_binary(&mut file, 0).unwrap();
297 assert!(back.is_empty());
298 }
299 }
300
301 #[test]
302 fn test_compress_decompress_positions() {
303 let positions = vec![[1.0_f32, 2.0, -1.5], [0.5, 0.0, 3.0]];
304 let scale = 0.001;
305 let compressed = compress_positions_quantized(&positions, scale);
306 let decompressed = decompress_positions_quantized(&compressed, scale);
307 assert_eq!(decompressed.len(), 2);
308 assert!((decompressed[0][0] - 1.0).abs() < scale * 2.0);
309 assert!((decompressed[1][2] - 3.0).abs() < scale * 2.0);
310 }
311
312 #[test]
313 fn test_compress_empty() {
314 let compressed = compress_positions_quantized(&[], 0.001);
315 assert!(compressed.is_empty());
316 }
317
318 #[test]
319 fn test_decompress_empty() {
320 let decompressed = decompress_positions_quantized(&[], 0.001);
321 assert!(decompressed.is_empty());
322 }
323
324 #[test]
325 fn test_compress_zero_scale() {
326 let positions = vec![[1.0_f32, 2.0, 3.0]];
328 let compressed = compress_positions_quantized(&positions, 0.0);
329 assert_eq!(compressed, vec![0i16, 0, 0]);
330 }
331
332 #[test]
333 fn test_compress_large_positive() {
334 let positions = vec![[1e10_f32, 0.0, 0.0]];
336 let compressed = compress_positions_quantized(&positions, 0.001);
337 assert_eq!(compressed[0], i16::MAX);
338 }
339
340 #[test]
341 fn test_compress_large_negative() {
342 let positions = vec![[-1e10_f32, 0.0, 0.0]];
343 let compressed = compress_positions_quantized(&positions, 0.001);
344 assert_eq!(compressed[0], i16::MIN);
345 }
346
347 #[test]
348 fn test_compress_count() {
349 let positions = vec![[0.0_f32; 3]; 5];
350 let compressed = compress_positions_quantized(&positions, 0.01);
351 assert_eq!(compressed.len(), 15);
352 }
353
354 #[test]
355 fn test_decompress_count() {
356 let data = vec![0i16; 12];
357 let out = decompress_positions_quantized(&data, 0.01);
358 assert_eq!(out.len(), 4);
359 }
360
361 #[test]
362 fn test_multiple_frames_sequential() {
363 let path = "/tmp/test_oxiphysics_multiframe.bin";
364 let frame1 = vec![[1.0_f32, 0.0, 0.0]];
365 let frame2 = vec![[2.0_f32, 0.0, 0.0]];
366 {
367 let mut file = std::fs::File::create(path).unwrap();
368 write_frame_binary(&mut file, &frame1).unwrap();
369 write_frame_binary(&mut file, &frame2).unwrap();
370 }
371 {
372 let mut file = std::fs::File::open(path).unwrap();
373 let f1 = read_frame_binary(&mut file, 1).unwrap();
374 let f2 = read_frame_binary(&mut file, 1).unwrap();
375 assert!((f1[0][0] - 1.0).abs() < 1e-6);
376 assert!((f2[0][0] - 2.0).abs() < 1e-6);
377 }
378 }
379
380 #[test]
381 fn test_header_dt_precision() {
382 let path = "/tmp/test_oxiphysics_header_dt.bin";
383 let hdr = BinaryHeader {
384 dt: 1.23456789012345e-4,
385 ..sample_header()
386 };
387 write_binary_header(path, &hdr).unwrap();
388 let hdr2 = read_binary_header(path).unwrap();
389 assert!((hdr2.dt - 1.23456789012345e-4).abs() < 1e-18);
390 }
391
392 #[test]
393 fn test_header_large_n_atoms() {
394 let path = "/tmp/test_oxiphysics_header_large.bin";
395 let hdr = BinaryHeader {
396 n_atoms: 1_000_000,
397 ..sample_header()
398 };
399 write_binary_header(path, &hdr).unwrap();
400 let hdr2 = read_binary_header(path).unwrap();
401 assert_eq!(hdr2.n_atoms, 1_000_000);
402 }
403
404 #[test]
405 fn test_quantize_negative_values() {
406 let positions = vec![[-0.5_f32, -1.0, -2.0]];
407 let scale = 0.001;
408 let compressed = compress_positions_quantized(&positions, scale);
409 let back = decompress_positions_quantized(&compressed, scale);
410 assert!((back[0][0] - (-0.5)).abs() < scale * 2.0);
411 }
412
413 #[test]
414 fn test_f32_bytes_all_zeros() {
415 let bytes = f32_to_bytes_le(0.0);
416 assert_eq!(bytes, [0, 0, 0, 0]);
417 }
418
419 #[test]
420 fn test_f64_bytes_all_zeros() {
421 let bytes = f64_to_bytes_le(0.0);
422 assert_eq!(bytes, [0, 0, 0, 0, 0, 0, 0, 0]);
423 }
424}