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 =
185 (if bc_type.to_uppercase() == "G1" { 0 } else { 1 }).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 { 1.0 - mass_weight } else { mass_weight })
203 * (if dk == 0 { 1.0 - length_weight } else { length_weight })
204 * (if dl == 0 { 1.0 - vel_weight } else { vel_weight });
205
206 let i = (bc_idx + di).min(self.bc_bins.len() - 1);
208 let j = (mass_idx + dj).min(self.mass_bins.len() - 1);
209 let k = (length_idx + dk).min(self.length_bins.len() - 1);
210 let l = (vel_idx + dl).min(self.velocity_bins.len() - 1);
211
212 let idx = self.flat_index(type_idx, i, j, k, l);
214 result += w * self.data.get(idx).copied().unwrap_or(0.0) as f64;
218 }
219 }
220 }
221 }
222
223 result
224 }
225
226 pub fn lookup_with_caliber(
236 &self,
237 bc: f64,
238 bc_type: &str,
239 mass_grains: f64,
240 caliber_inches: f64,
241 velocity_fps: f64,
242 ) -> f64 {
243 let estimated_length = caliber_inches * 3.5;
245 self.lookup(bc, bc_type, mass_grains, estimated_length, velocity_fps)
246 }
247
248 fn interp_idx(&self, value: f32, bins: &[f32]) -> (usize, f64) {
250 if bins.is_empty() {
251 return (0, 0.0);
252 }
253
254 if value <= bins[0] {
256 return (0, 0.0);
257 }
258 if value >= bins[bins.len() - 1] {
259 return (bins.len().saturating_sub(2), 1.0);
260 }
261
262 let idx = match bins
264 .binary_search_by(|probe| probe.partial_cmp(&value).unwrap_or(std::cmp::Ordering::Equal))
265 {
266 Ok(i) => i.saturating_sub(1).min(bins.len() - 2),
267 Err(i) => i.saturating_sub(1).min(bins.len() - 2),
268 };
269
270 let low = bins[idx];
272 let high = bins[idx + 1];
273 let weight = if high > low {
274 ((value - low) / (high - low)) as f64
275 } else {
276 0.0
277 };
278
279 (idx, weight)
280 }
281
282 fn flat_index(&self, type_idx: usize, bc_idx: usize, mass_idx: usize, length_idx: usize, vel_idx: usize) -> usize {
284 let n_bc = self.bc_bins.len();
285 let n_mass = self.mass_bins.len();
286 let n_length = self.length_bins.len();
287 let n_velocity = self.velocity_bins.len();
288
289 type_idx * (n_bc * n_mass * n_length * n_velocity)
290 + bc_idx * (n_mass * n_length * n_velocity)
291 + mass_idx * (n_length * n_velocity)
292 + length_idx * n_velocity
293 + vel_idx
294 }
295
296 pub fn version(&self) -> u32 {
298 self.version
299 }
300
301 pub fn total_cells(&self) -> usize {
303 self.data.len()
304 }
305
306 pub fn dimensions_str(&self) -> String {
308 format!(
309 "{}x{}x{}x{}x{}",
310 self.bc_bins.len(),
311 self.mass_bins.len(),
312 self.length_bins.len(),
313 self.velocity_bins.len(),
314 self.num_types
315 )
316 }
317}
318
319fn read_u32<R: Read>(reader: &mut R) -> Result<u32, std::io::Error> {
322 let mut buf = [0u8; 4];
323 reader.read_exact(&mut buf)?;
324 Ok(u32::from_le_bytes(buf))
325}
326
327fn read_u64<R: Read>(reader: &mut R) -> Result<u64, std::io::Error> {
328 let mut buf = [0u8; 8];
329 reader.read_exact(&mut buf)?;
330 Ok(u64::from_le_bytes(buf))
331}
332
333fn read_f32_array<R: Read>(reader: &mut R, count: usize) -> Result<Vec<f32>, std::io::Error> {
334 const MAX_ELEMS: usize = 64_000_000; if count > MAX_ELEMS {
338 return Err(std::io::Error::new(
339 std::io::ErrorKind::InvalidData,
340 "f32 array length too large",
341 ));
342 }
343 let byte_len = count.checked_mul(4).ok_or_else(|| {
344 std::io::Error::new(std::io::ErrorKind::InvalidData, "f32 array length overflow")
345 })?;
346 let mut data = vec![0f32; count];
347 let mut buf = vec![0u8; byte_len];
348 reader.read_exact(&mut buf)?;
349
350 for (i, chunk) in buf.chunks_exact(4).enumerate() {
351 data[i] = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
352 }
353
354 Ok(data)
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 #[test]
362 fn test_interp_idx_in_range() {
363 let table = BcCorrectionTable {
364 data: vec![1.0; 100],
365 bc_bins: vec![0.1, 0.2, 0.3, 0.4, 0.5],
366 mass_bins: vec![100.0, 150.0, 200.0],
367 length_bins: vec![1.0, 1.2, 1.4],
368 velocity_bins: vec![3000.0, 2500.0, 2000.0],
369 num_types: 2,
370 version: 1,
371 };
372
373 let (idx, weight) = table.interp_idx(0.25, &table.bc_bins);
375 assert_eq!(idx, 1);
376 assert!((weight - 0.5).abs() < 0.01);
377
378 let (idx, weight) = table.interp_idx(0.2, &table.bc_bins);
380 assert_eq!(idx, 0);
381 assert!((weight - 1.0).abs() < 0.01);
382 }
383
384 #[test]
385 fn test_interp_idx_out_of_range() {
386 let table = BcCorrectionTable {
387 data: vec![1.0; 100],
388 bc_bins: vec![0.1, 0.2, 0.3, 0.4, 0.5],
389 mass_bins: vec![100.0, 150.0, 200.0],
390 length_bins: vec![1.0, 1.2, 1.4],
391 velocity_bins: vec![3000.0, 2500.0, 2000.0],
392 num_types: 2,
393 version: 1,
394 };
395
396 let (idx, weight) = table.interp_idx(0.05, &table.bc_bins);
398 assert_eq!(idx, 0);
399 assert_eq!(weight, 0.0);
400
401 let (idx, weight) = table.interp_idx(0.6, &table.bc_bins);
403 assert_eq!(idx, 3); assert_eq!(weight, 1.0);
405 }
406}