1use std::fs::File;
34use std::io::{BufReader, Read};
35use std::path::Path;
36
37#[derive(Debug)]
39pub struct BcCorrectionTable {
40 data: Vec<f32>,
42 bc_bins: Vec<f32>,
44 mass_bins: Vec<f32>,
46 length_bins: Vec<f32>,
48 velocity_bins: Vec<f32>,
50 num_types: usize,
52 version: u32,
54}
55
56#[derive(Debug)]
58pub enum BcTableError {
59 IoError(std::io::Error),
60 InvalidMagic,
61 UnsupportedVersion(u32),
62 ChecksumMismatch,
63 InvalidDimensions,
64}
65
66impl std::fmt::Display for BcTableError {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 match self {
69 BcTableError::IoError(e) => write!(f, "IO error: {}", e),
70 BcTableError::InvalidMagic => write!(f, "Invalid file magic (expected 'BCCR')"),
71 BcTableError::UnsupportedVersion(v) => write!(f, "Unsupported table version: {}", v),
72 BcTableError::ChecksumMismatch => write!(f, "Data checksum mismatch"),
73 BcTableError::InvalidDimensions => write!(f, "Invalid table dimensions"),
74 }
75 }
76}
77
78impl std::error::Error for BcTableError {}
79
80impl From<std::io::Error> for BcTableError {
81 fn from(e: std::io::Error) -> Self {
82 BcTableError::IoError(e)
83 }
84}
85
86impl BcCorrectionTable {
87 pub fn load<P: AsRef<Path>>(path: P) -> Result<Self, BcTableError> {
89 let file = File::open(path)?;
90 let mut reader = BufReader::new(file);
91
92 let mut magic = [0u8; 4];
94 reader.read_exact(&mut magic)?;
95 if &magic != b"BCCR" {
96 return Err(BcTableError::InvalidMagic);
97 }
98
99 let version = read_u32(&mut reader)?;
100 if version != 1 {
101 return Err(BcTableError::UnsupportedVersion(version));
102 }
103
104 let _flags = read_u32(&mut reader)?;
105 let num_bc = read_u32(&mut reader)? as usize;
106 let num_mass = read_u32(&mut reader)? as usize;
107 let num_length = read_u32(&mut reader)? as usize;
108 let num_velocity = read_u32(&mut reader)? as usize;
109 let num_types = read_u32(&mut reader)? as usize;
110 let _timestamp = read_u64(&mut reader)?;
111 let stored_checksum = read_u32(&mut reader)?;
112
113 let mut reserved = [0u8; 16];
115 reader.read_exact(&mut reserved)?;
116
117 if num_bc == 0 || num_mass == 0 || num_length == 0 || num_velocity == 0 || num_types == 0 {
119 return Err(BcTableError::InvalidDimensions);
120 }
121
122 let bc_bins = read_f32_array(&mut reader, num_bc)?;
124 let mass_bins = read_f32_array(&mut reader, num_mass)?;
125 let length_bins = read_f32_array(&mut reader, num_length)?;
126 let velocity_bins = read_f32_array(&mut reader, num_velocity)?;
127
128 const MAX_TOTAL_CELLS: usize = 64_000_000; let total_cells = num_types
134 .checked_mul(num_bc)
135 .and_then(|x| x.checked_mul(num_mass))
136 .and_then(|x| x.checked_mul(num_length))
137 .and_then(|x| x.checked_mul(num_velocity))
138 .filter(|&n| n <= MAX_TOTAL_CELLS)
139 .ok_or(BcTableError::InvalidDimensions)?;
140 let data = read_f32_array(&mut reader, total_cells)?;
141
142 if stored_checksum != 0 {
148 let mut data_bytes = Vec::with_capacity(data.len() * 4);
149 for &v in &data {
150 data_bytes.extend_from_slice(&v.to_le_bytes());
151 }
152 let calculated = crate::bc_table_5d::crc32_ieee(&data_bytes);
153 if calculated != stored_checksum {
154 return Err(BcTableError::ChecksumMismatch);
155 }
156 }
157
158 Ok(BcCorrectionTable {
159 data,
160 bc_bins,
161 mass_bins,
162 length_bins,
163 velocity_bins,
164 num_types,
165 version,
166 })
167 }
168
169 pub fn lookup(&self, bc: f64, bc_type: &str, mass: f64, length: f64, velocity: f64) -> f64 {
181 let type_idx = (if bc_type.to_uppercase() == "G1" { 0 } else { 1 })
185 .min(self.num_types.saturating_sub(1));
186
187 let (bc_idx, bc_weight) = self.interp_idx(bc as f32, &self.bc_bins);
189 let (mass_idx, mass_weight) = self.interp_idx(mass as f32, &self.mass_bins);
190 let (length_idx, length_weight) = self.interp_idx(length as f32, &self.length_bins);
191 let (vel_idx, vel_weight) = self.interp_idx(velocity as f32, &self.velocity_bins);
192
193 let mut result = 0.0f64;
195
196 for di in 0..2 {
197 for dj in 0..2 {
198 for dk in 0..2 {
199 for dl in 0..2 {
200 let w = (if di == 0 { 1.0 - bc_weight } else { bc_weight })
202 * (if dj == 0 {
203 1.0 - mass_weight
204 } else {
205 mass_weight
206 })
207 * (if dk == 0 {
208 1.0 - length_weight
209 } else {
210 length_weight
211 })
212 * (if dl == 0 {
213 1.0 - vel_weight
214 } else {
215 vel_weight
216 });
217
218 let i = (bc_idx + di).min(self.bc_bins.len() - 1);
220 let j = (mass_idx + dj).min(self.mass_bins.len() - 1);
221 let k = (length_idx + dk).min(self.length_bins.len() - 1);
222 let l = (vel_idx + dl).min(self.velocity_bins.len() - 1);
223
224 let idx = self.flat_index(type_idx, i, j, k, l);
226 result += w * self.data.get(idx).copied().unwrap_or(0.0) as f64;
230 }
231 }
232 }
233 }
234
235 result
236 }
237
238 pub fn lookup_with_caliber(
248 &self,
249 bc: f64,
250 bc_type: &str,
251 mass_grains: f64,
252 caliber_inches: f64,
253 velocity_fps: f64,
254 ) -> f64 {
255 let est_m = crate::stability::estimate_bullet_length_m(
259 caliber_inches * 0.0254,
260 mass_grains * crate::constants::GRAINS_TO_KG,
261 );
262 let estimated_length = if est_m > 0.0 {
263 est_m / 0.0254
264 } else {
265 caliber_inches * 3.5
266 };
267 self.lookup(bc, bc_type, mass_grains, estimated_length, velocity_fps)
268 }
269
270 fn interp_idx(&self, value: f32, bins: &[f32]) -> (usize, f64) {
272 if bins.len() < 2 || value.is_nan() {
273 return (0, 0.0);
274 }
275
276 if value <= bins[0] {
278 return (0, 0.0);
279 }
280 if value >= bins[bins.len() - 1] {
281 return (bins.len().saturating_sub(2), 1.0);
282 }
283
284 let last_interval = bins.len().saturating_sub(2);
286 let idx = match bins.binary_search_by(|probe| {
287 probe
288 .partial_cmp(&value)
289 .unwrap_or(std::cmp::Ordering::Equal)
290 }) {
291 Ok(i) => i.saturating_sub(1).min(last_interval),
292 Err(i) => i.saturating_sub(1).min(last_interval),
293 };
294
295 let low = bins[idx];
297 let high = bins[idx + 1];
298 let weight = if high > low {
299 ((value - low) / (high - low)) as f64
300 } else {
301 0.0
302 };
303
304 (idx, weight)
305 }
306
307 fn flat_index(
309 &self,
310 type_idx: usize,
311 bc_idx: usize,
312 mass_idx: usize,
313 length_idx: usize,
314 vel_idx: usize,
315 ) -> usize {
316 let n_bc = self.bc_bins.len();
317 let n_mass = self.mass_bins.len();
318 let n_length = self.length_bins.len();
319 let n_velocity = self.velocity_bins.len();
320
321 type_idx * (n_bc * n_mass * n_length * n_velocity)
322 + bc_idx * (n_mass * n_length * n_velocity)
323 + mass_idx * (n_length * n_velocity)
324 + length_idx * n_velocity
325 + vel_idx
326 }
327
328 pub fn version(&self) -> u32 {
330 self.version
331 }
332
333 pub fn total_cells(&self) -> usize {
335 self.data.len()
336 }
337
338 pub fn dimensions_str(&self) -> String {
340 format!(
341 "{}x{}x{}x{}x{}",
342 self.bc_bins.len(),
343 self.mass_bins.len(),
344 self.length_bins.len(),
345 self.velocity_bins.len(),
346 self.num_types
347 )
348 }
349}
350
351fn read_u32<R: Read>(reader: &mut R) -> Result<u32, std::io::Error> {
354 let mut buf = [0u8; 4];
355 reader.read_exact(&mut buf)?;
356 Ok(u32::from_le_bytes(buf))
357}
358
359fn read_u64<R: Read>(reader: &mut R) -> Result<u64, std::io::Error> {
360 let mut buf = [0u8; 8];
361 reader.read_exact(&mut buf)?;
362 Ok(u64::from_le_bytes(buf))
363}
364
365#[allow(unknown_lints, clippy::chunks_exact_to_as_chunks)]
372fn read_f32_array<R: Read>(reader: &mut R, count: usize) -> Result<Vec<f32>, std::io::Error> {
373 const MAX_ELEMS: usize = 64_000_000; if count > MAX_ELEMS {
377 return Err(std::io::Error::new(
378 std::io::ErrorKind::InvalidData,
379 "f32 array length too large",
380 ));
381 }
382 let byte_len = count.checked_mul(4).ok_or_else(|| {
383 std::io::Error::new(std::io::ErrorKind::InvalidData, "f32 array length overflow")
384 })?;
385 let mut data = vec![0f32; count];
386 let mut buf = vec![0u8; byte_len];
387 reader.read_exact(&mut buf)?;
388
389 for (i, chunk) in buf.chunks_exact(4).enumerate() {
390 data[i] = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
391 }
392
393 Ok(data)
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 fn create_single_cell_table() -> BcCorrectionTable {
401 BcCorrectionTable {
402 data: vec![0.875],
403 bc_bins: vec![0.4],
404 mass_bins: vec![168.0],
405 length_bins: vec![1.2],
406 velocity_bins: vec![2500.0],
407 num_types: 1,
408 version: 1,
409 }
410 }
411
412 #[test]
413 fn test_interp_idx_in_range() {
414 let table = BcCorrectionTable {
415 data: vec![1.0; 100],
416 bc_bins: vec![0.1, 0.2, 0.3, 0.4, 0.5],
417 mass_bins: vec![100.0, 150.0, 200.0],
418 length_bins: vec![1.0, 1.2, 1.4],
419 velocity_bins: vec![3000.0, 2500.0, 2000.0],
420 num_types: 2,
421 version: 1,
422 };
423
424 let (idx, weight) = table.interp_idx(0.25, &table.bc_bins);
426 assert_eq!(idx, 1);
427 assert!((weight - 0.5).abs() < 0.01);
428
429 let (idx, weight) = table.interp_idx(0.2, &table.bc_bins);
431 assert_eq!(idx, 0);
432 assert!((weight - 1.0).abs() < 0.01);
433 }
434
435 #[test]
436 fn test_interp_idx_out_of_range() {
437 let table = BcCorrectionTable {
438 data: vec![1.0; 100],
439 bc_bins: vec![0.1, 0.2, 0.3, 0.4, 0.5],
440 mass_bins: vec![100.0, 150.0, 200.0],
441 length_bins: vec![1.0, 1.2, 1.4],
442 velocity_bins: vec![3000.0, 2500.0, 2000.0],
443 num_types: 2,
444 version: 1,
445 };
446
447 let (idx, weight) = table.interp_idx(0.05, &table.bc_bins);
449 assert_eq!(idx, 0);
450 assert_eq!(weight, 0.0);
451
452 let (idx, weight) = table.interp_idx(0.6, &table.bc_bins);
454 assert_eq!(idx, 3); assert_eq!(weight, 1.0);
456 }
457
458 #[test]
459 fn test_interp_idx_nan_defaults_to_first_bin() {
460 let table = create_single_cell_table();
461 let bins = [0.1, 0.2, 0.3];
462
463 assert_eq!(table.interp_idx(f32::NAN, &bins), (0, 0.0));
464 assert_eq!(table.interp_idx(f32::NEG_INFINITY, &bins), (0, 0.0));
465 assert_eq!(table.interp_idx(f32::INFINITY, &bins), (1, 1.0));
466 }
467
468 #[test]
469 fn test_lookup_nan_with_single_bin_axes_uses_only_cell() {
470 let table = create_single_cell_table();
471
472 assert_eq!(table.lookup(f64::NAN, "G1", 168.0, 1.2, 2500.0), 0.875);
473 }
474}