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 _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 let total_cells = num_types * num_bc * num_mass * num_length * num_velocity;
130 let data = read_f32_array(&mut reader, total_cells)?;
131
132 Ok(BcCorrectionTable {
133 data,
134 bc_bins,
135 mass_bins,
136 length_bins,
137 velocity_bins,
138 num_types,
139 version,
140 })
141 }
142
143 pub fn lookup(&self, bc: f64, bc_type: &str, mass: f64, length: f64, velocity: f64) -> f64 {
155 let type_idx = if bc_type.to_uppercase() == "G1" { 0 } else { 1 };
157
158 let (bc_idx, bc_weight) = self.interp_idx(bc as f32, &self.bc_bins);
160 let (mass_idx, mass_weight) = self.interp_idx(mass as f32, &self.mass_bins);
161 let (length_idx, length_weight) = self.interp_idx(length as f32, &self.length_bins);
162 let (vel_idx, vel_weight) = self.interp_idx(velocity as f32, &self.velocity_bins);
163
164 let mut result = 0.0f64;
166
167 for di in 0..2 {
168 for dj in 0..2 {
169 for dk in 0..2 {
170 for dl in 0..2 {
171 let w = (if di == 0 { 1.0 - bc_weight } else { bc_weight })
173 * (if dj == 0 { 1.0 - mass_weight } else { mass_weight })
174 * (if dk == 0 { 1.0 - length_weight } else { length_weight })
175 * (if dl == 0 { 1.0 - vel_weight } else { vel_weight });
176
177 let i = (bc_idx + di).min(self.bc_bins.len() - 1);
179 let j = (mass_idx + dj).min(self.mass_bins.len() - 1);
180 let k = (length_idx + dk).min(self.length_bins.len() - 1);
181 let l = (vel_idx + dl).min(self.velocity_bins.len() - 1);
182
183 let idx = self.flat_index(type_idx, i, j, k, l);
185 result += w * self.data[idx] as f64;
186 }
187 }
188 }
189 }
190
191 result
192 }
193
194 pub fn lookup_with_caliber(
204 &self,
205 bc: f64,
206 bc_type: &str,
207 mass_grains: f64,
208 caliber_inches: f64,
209 velocity_fps: f64,
210 ) -> f64 {
211 let estimated_length = caliber_inches * 3.5;
213 self.lookup(bc, bc_type, mass_grains, estimated_length, velocity_fps)
214 }
215
216 fn interp_idx(&self, value: f32, bins: &[f32]) -> (usize, f64) {
218 if bins.is_empty() {
219 return (0, 0.0);
220 }
221
222 if value <= bins[0] {
224 return (0, 0.0);
225 }
226 if value >= bins[bins.len() - 1] {
227 return (bins.len().saturating_sub(2), 1.0);
228 }
229
230 let idx = match bins.binary_search_by(|probe| probe.partial_cmp(&value).unwrap()) {
232 Ok(i) => i.saturating_sub(1).min(bins.len() - 2),
233 Err(i) => i.saturating_sub(1).min(bins.len() - 2),
234 };
235
236 let low = bins[idx];
238 let high = bins[idx + 1];
239 let weight = if high > low {
240 ((value - low) / (high - low)) as f64
241 } else {
242 0.0
243 };
244
245 (idx, weight)
246 }
247
248 fn flat_index(&self, type_idx: usize, bc_idx: usize, mass_idx: usize, length_idx: usize, vel_idx: usize) -> usize {
250 let n_bc = self.bc_bins.len();
251 let n_mass = self.mass_bins.len();
252 let n_length = self.length_bins.len();
253 let n_velocity = self.velocity_bins.len();
254
255 type_idx * (n_bc * n_mass * n_length * n_velocity)
256 + bc_idx * (n_mass * n_length * n_velocity)
257 + mass_idx * (n_length * n_velocity)
258 + length_idx * n_velocity
259 + vel_idx
260 }
261
262 pub fn version(&self) -> u32 {
264 self.version
265 }
266
267 pub fn total_cells(&self) -> usize {
269 self.data.len()
270 }
271
272 pub fn dimensions_str(&self) -> String {
274 format!(
275 "{}x{}x{}x{}x{}",
276 self.bc_bins.len(),
277 self.mass_bins.len(),
278 self.length_bins.len(),
279 self.velocity_bins.len(),
280 self.num_types
281 )
282 }
283}
284
285fn read_u32<R: Read>(reader: &mut R) -> Result<u32, std::io::Error> {
288 let mut buf = [0u8; 4];
289 reader.read_exact(&mut buf)?;
290 Ok(u32::from_le_bytes(buf))
291}
292
293fn read_u64<R: Read>(reader: &mut R) -> Result<u64, std::io::Error> {
294 let mut buf = [0u8; 8];
295 reader.read_exact(&mut buf)?;
296 Ok(u64::from_le_bytes(buf))
297}
298
299fn read_f32_array<R: Read>(reader: &mut R, count: usize) -> Result<Vec<f32>, std::io::Error> {
300 let mut data = vec![0f32; count];
301 let mut buf = vec![0u8; count * 4];
302 reader.read_exact(&mut buf)?;
303
304 for (i, chunk) in buf.chunks_exact(4).enumerate() {
305 data[i] = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
306 }
307
308 Ok(data)
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 #[test]
316 fn test_interp_idx_in_range() {
317 let table = BcCorrectionTable {
318 data: vec![1.0; 100],
319 bc_bins: vec![0.1, 0.2, 0.3, 0.4, 0.5],
320 mass_bins: vec![100.0, 150.0, 200.0],
321 length_bins: vec![1.0, 1.2, 1.4],
322 velocity_bins: vec![3000.0, 2500.0, 2000.0],
323 num_types: 2,
324 version: 1,
325 };
326
327 let (idx, weight) = table.interp_idx(0.25, &table.bc_bins);
329 assert_eq!(idx, 1);
330 assert!((weight - 0.5).abs() < 0.01);
331
332 let (idx, weight) = table.interp_idx(0.2, &table.bc_bins);
334 assert_eq!(idx, 0);
335 assert!((weight - 1.0).abs() < 0.01);
336 }
337
338 #[test]
339 fn test_interp_idx_out_of_range() {
340 let table = BcCorrectionTable {
341 data: vec![1.0; 100],
342 bc_bins: vec![0.1, 0.2, 0.3, 0.4, 0.5],
343 mass_bins: vec![100.0, 150.0, 200.0],
344 length_bins: vec![1.0, 1.2, 1.4],
345 velocity_bins: vec![3000.0, 2500.0, 2000.0],
346 num_types: 2,
347 version: 1,
348 };
349
350 let (idx, weight) = table.interp_idx(0.05, &table.bc_bins);
352 assert_eq!(idx, 0);
353 assert_eq!(weight, 0.0);
354
355 let (idx, weight) = table.interp_idx(0.6, &table.bc_bins);
357 assert_eq!(idx, 3); assert_eq!(weight, 1.0);
359 }
360}