Skip to main content

ballistics_engine/
bc_table.rs

1// BC Correction Table - 5D Lookup Table with Linear Interpolation
2//
3// This module provides offline BC corrections by loading a precomputed table
4// of correction factors. The table is indexed by:
5//   - BC value (published BC)
6//   - BC type (G1 or G7)
7//   - Bullet mass (grains)
8//   - Bullet length (inches)
9//   - Velocity (fps)
10//
11// Binary file format (BCCR):
12//   Header (64 bytes):
13//     - Magic: 4 bytes ('BCCR')
14//     - Version: 4 bytes (uint32)
15//     - Flags: 4 bytes (uint32)
16//     - num_bc: 4 bytes (uint32)
17//     - num_mass: 4 bytes (uint32)
18//     - num_length: 4 bytes (uint32)
19//     - num_velocity: 4 bytes (uint32)
20//     - num_bc_types: 4 bytes (uint32)
21//     - timestamp: 8 bytes (uint64)
22//     - checksum: 4 bytes (uint32, CRC32)
23//     - reserved: 16 bytes
24//   Bin definitions:
25//     - BC bins: num_bc * 4 bytes (float32)
26//     - Mass bins: num_mass * 4 bytes (float32)
27//     - Length bins: num_length * 4 bytes (float32)
28//     - Velocity bins: num_velocity * 4 bytes (float32)
29//   Data section:
30//     - Correction factors: total_cells * 4 bytes (float32)
31//     - Layout: [bc_type][bc][mass][length][velocity]
32
33use std::fs::File;
34use std::io::{BufReader, Read};
35use std::path::Path;
36
37/// BC correction table with 5D interpolation
38#[derive(Debug)]
39pub struct BcCorrectionTable {
40    /// Table data: [bc_type][bc][mass][length][velocity]
41    data: Vec<f32>,
42    /// BC bin values
43    bc_bins: Vec<f32>,
44    /// Mass bin values (grains)
45    mass_bins: Vec<f32>,
46    /// Length bin values (inches)
47    length_bins: Vec<f32>,
48    /// Velocity bin values (fps)
49    velocity_bins: Vec<f32>,
50    /// Number of BC types (typically 2: G1, G7)
51    num_types: usize,
52    /// Table version
53    version: u32,
54}
55
56/// Error type for BC table operations
57#[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    /// Load a BC correction table from a binary file
88    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        // Read header (64 bytes)
93        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        // Skip reserved bytes
114        let mut reserved = [0u8; 16];
115        reader.read_exact(&mut reserved)?;
116
117        // Validate dimensions
118        if num_bc == 0 || num_mass == 0 || num_length == 0 || num_velocity == 0 || num_types == 0 {
119            return Err(BcTableError::InvalidDimensions);
120        }
121
122        // Read bin definitions
123        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        // Read data section
129        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    /// Look up a BC correction factor with 5D linear interpolation
144    ///
145    /// # Arguments
146    /// * `bc` - Published BC value
147    /// * `bc_type` - "G1" or "G7"
148    /// * `mass` - Bullet mass in grains
149    /// * `length` - Bullet length in inches
150    /// * `velocity` - Current velocity in fps
151    ///
152    /// # Returns
153    /// Correction factor (multiply published BC by this value)
154    pub fn lookup(&self, bc: f64, bc_type: &str, mass: f64, length: f64, velocity: f64) -> f64 {
155        // Get type index (0 = G1, 1 = G7)
156        let type_idx = if bc_type.to_uppercase() == "G1" { 0 } else { 1 };
157
158        // Find interpolation indices and weights for each dimension
159        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        // 4D linear interpolation (type is discrete, not interpolated)
165        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                        // Calculate weight for this corner
172                        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                        // Get clamped indices
178                        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                        // Calculate flat index
184                        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    /// Get the BC correction factor for a given bullet and velocity
195    /// This is a convenience method that estimates bullet length from mass and caliber
196    ///
197    /// # Arguments
198    /// * `bc` - Published BC value
199    /// * `bc_type` - "G1" or "G7"
200    /// * `mass_grains` - Bullet mass in grains
201    /// * `caliber_inches` - Bullet caliber in inches
202    /// * `velocity_fps` - Current velocity in fps
203    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        // Estimate bullet length from caliber (typical rifle bullets are 3-4 calibers long)
212        let estimated_length = caliber_inches * 3.5;
213        self.lookup(bc, bc_type, mass_grains, estimated_length, velocity_fps)
214    }
215
216    /// Find interpolation index and weight for a value in bins
217    fn interp_idx(&self, value: f32, bins: &[f32]) -> (usize, f64) {
218        if bins.is_empty() {
219            return (0, 0.0);
220        }
221
222        // Handle out of range
223        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        // Binary search for interval
231        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        // Calculate interpolation weight
237        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    /// Calculate flat array index from 5D indices
249    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    /// Get table version
263    pub fn version(&self) -> u32 {
264        self.version
265    }
266
267    /// Get total number of cells in the table
268    pub fn total_cells(&self) -> usize {
269        self.data.len()
270    }
271
272    /// Get table dimensions as a string
273    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
285// Helper functions for reading binary data
286
287fn 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        // Test middle of range
328        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        // Test at bin boundary
333        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        // Test below range
351        let (idx, weight) = table.interp_idx(0.05, &table.bc_bins);
352        assert_eq!(idx, 0);
353        assert_eq!(weight, 0.0);
354
355        // Test above range
356        let (idx, weight) = table.interp_idx(0.6, &table.bc_bins);
357        assert_eq!(idx, 3); // len - 2
358        assert_eq!(weight, 1.0);
359    }
360}