1use crate::{
4 calculate_zero_angle_with_conditions, run_monte_carlo_with_direction_std_dev,
5 AtmosphericConditions, BallisticInputs, DragModel, MonteCarloParams, TrajectorySolver,
6 WindConditions,
7};
8use std::os::raw::{c_char, c_double, c_int};
9use std::ptr;
10
11pub const MIN_FFI_STEP_SIZE_MS: c_double = 0.1;
16
17pub const MAX_FFI_DRAG_TABLE_LEN: c_int = 4096;
25
26#[repr(C)]
29pub struct FFIBallisticInputs {
30 pub muzzle_velocity: c_double, pub muzzle_angle: c_double, pub bc_value: c_double, pub bullet_mass: c_double, pub bullet_diameter: c_double, pub bc_type: c_int, pub sight_height: c_double, pub target_distance: c_double, pub temperature: c_double, pub twist_rate: c_double, pub is_twist_right: c_int, pub shooting_angle: c_double, pub altitude: c_double, pub latitude: c_double, pub azimuth_angle: c_double, pub use_rk4: c_int, pub use_adaptive_rk45: c_int, pub enable_wind_shear: c_int, pub enable_trajectory_sampling: c_int, pub sample_interval: c_double, pub enable_pitch_damping: c_int, pub enable_precession_nutation: c_int, pub enable_spin_drift: c_int, pub enable_magnus: c_int, pub enable_coriolis: c_int, pub shot_azimuth: c_double,
59 pub cant_angle: c_double,
63 pub zero_poi_vertical: c_double,
68 pub zero_poi_horizontal: c_double,
72 pub sight_offset_lateral: c_double,
81 }
87
88#[repr(C)]
89pub struct FFIWindConditions {
90 pub speed: c_double, pub direction: c_double,
99 pub vertical_speed: c_double,
102}
103
104#[repr(C)]
105pub struct FFIAtmosphericConditions {
106 pub temperature: c_double, pub pressure: c_double, pub humidity: c_double, pub altitude: c_double, }
111
112#[repr(C)]
113pub struct FFITrajectorySample {
114 pub distance: c_double, pub time: c_double, pub velocity_mps: c_double, pub energy_joules: c_double, pub drop_meters: c_double, pub windage_meters: c_double, pub mach: c_double, pub spin_rate_rps: c_double, }
123
124#[repr(C)]
125pub struct FFITrajectoryPoint {
126 pub time: c_double,
127 pub position_x: c_double,
128 pub position_y: c_double,
129 pub position_z: c_double,
130 pub velocity_magnitude: c_double,
131 pub kinetic_energy: c_double,
132}
133
134#[repr(C)]
135pub struct FFITrajectoryResult {
136 pub max_range: c_double,
137 pub max_height: c_double,
138 pub time_of_flight: c_double,
139 pub impact_velocity: c_double,
140 pub impact_energy: c_double,
141 pub points: *mut FFITrajectoryPoint,
142 pub point_count: c_int,
143 pub sampled_points: *mut FFITrajectorySample,
144 pub sampled_point_count: c_int,
145 pub min_pitch_damping: c_double, pub transonic_mach: c_double, pub final_pitch_angle: c_double, pub final_yaw_angle: c_double, pub max_yaw_angle: c_double, pub max_precession_angle: c_double, }
152
153#[repr(C)]
155pub struct FFIMonteCarloParams {
156 pub num_simulations: c_int,
157 pub velocity_std_dev: c_double,
158 pub angle_std_dev: c_double,
159 pub bc_std_dev: c_double,
160 pub wind_speed_std_dev: c_double,
161 pub target_distance: c_double, pub base_wind_speed: c_double, pub base_wind_direction: c_double, pub azimuth_std_dev: c_double, }
166
167#[repr(C)]
169pub struct FFIMonteCarloResults {
170 pub ranges: *mut c_double,
171 pub impact_velocities: *mut c_double,
172 pub impact_positions_x: *mut c_double,
173 pub impact_positions_y: *mut c_double,
176 pub impact_positions_z: *mut c_double,
177 pub num_results: c_int,
178 pub mean_range: c_double,
179 pub std_dev_range: c_double,
180 pub mean_impact_velocity: c_double,
181 pub std_dev_impact_velocity: c_double,
182 pub hit_probability: c_double, }
184
185#[allow(clippy::field_reassign_with_default)] fn convert_inputs(inputs: &FFIBallisticInputs) -> BallisticInputs {
188 let mut ballistic_inputs = BallisticInputs::default();
189
190 ballistic_inputs.muzzle_velocity = inputs.muzzle_velocity;
191 ballistic_inputs.muzzle_angle = inputs.muzzle_angle;
192 ballistic_inputs.azimuth_angle = inputs.azimuth_angle;
193 ballistic_inputs.shot_azimuth = inputs.shot_azimuth;
194 ballistic_inputs.cant_angle = inputs.cant_angle;
195 ballistic_inputs.zero_poi_vertical_m = inputs.zero_poi_vertical;
196 ballistic_inputs.zero_poi_horizontal_m = inputs.zero_poi_horizontal;
197 ballistic_inputs.sight_offset_lateral_m = inputs.sight_offset_lateral;
198 ballistic_inputs.use_rk4 = inputs.use_rk4 != 0;
199 ballistic_inputs.use_adaptive_rk45 = inputs.use_adaptive_rk45 != 0;
200 ballistic_inputs.bc_value = inputs.bc_value;
201 ballistic_inputs.bullet_mass = inputs.bullet_mass;
202 ballistic_inputs.bullet_diameter = inputs.bullet_diameter;
203 ballistic_inputs.bc_type = match inputs.bc_type {
204 1 => DragModel::G7,
205 2 => DragModel::G2,
206 3 => DragModel::G5,
207 4 => DragModel::G6,
208 5 => DragModel::G8,
209 6 => DragModel::GI,
210 7 => DragModel::GS,
211 8 => DragModel::RA4,
214 _ => DragModel::G1,
215 };
216 ballistic_inputs.sight_height = inputs.sight_height;
217 ballistic_inputs.target_distance = inputs.target_distance;
218 ballistic_inputs.temperature = inputs.temperature;
219 ballistic_inputs.twist_rate = inputs.twist_rate;
220 ballistic_inputs.is_twist_right = inputs.is_twist_right != 0;
221 ballistic_inputs.shooting_angle = inputs.shooting_angle;
222 ballistic_inputs.altitude = inputs.altitude;
223
224 if !inputs.latitude.is_nan() {
225 ballistic_inputs.latitude = Some(inputs.latitude);
226 }
227
228 ballistic_inputs.caliber_inches = inputs.bullet_diameter / 0.0254;
230 ballistic_inputs.weight_grains = inputs.bullet_mass / crate::constants::GRAINS_TO_KG;
231 ballistic_inputs.bullet_length = {
234 let est = crate::stability::estimate_bullet_length_m(
235 ballistic_inputs.bullet_diameter,
236 ballistic_inputs.bullet_mass,
237 );
238 if est > 0.0 {
239 est
240 } else {
241 ballistic_inputs.bullet_diameter * 4.5
242 }
243 };
244
245 ballistic_inputs.enable_wind_shear = inputs.enable_wind_shear != 0;
247 ballistic_inputs.enable_trajectory_sampling = inputs.enable_trajectory_sampling != 0;
248 ballistic_inputs.sample_interval = inputs.sample_interval;
249 ballistic_inputs.enable_pitch_damping = inputs.enable_pitch_damping != 0;
250 ballistic_inputs.enable_precession_nutation = inputs.enable_precession_nutation != 0;
251 ballistic_inputs.use_enhanced_spin_drift = inputs.enable_spin_drift != 0;
252 ballistic_inputs.enable_advanced_effects =
253 inputs.enable_magnus != 0 || inputs.enable_coriolis != 0;
254 ballistic_inputs.enable_magnus = inputs.enable_magnus != 0;
256 ballistic_inputs.enable_coriolis = inputs.enable_coriolis != 0;
257
258 ballistic_inputs
259}
260
261unsafe fn drag_table_from_raw(
275 mach: *const c_double,
276 cd: *const c_double,
277 len: c_int,
278) -> Result<crate::drag::DragTable, ()> {
279 if mach.is_null() || cd.is_null() || !(2..=MAX_FFI_DRAG_TABLE_LEN).contains(&len) {
280 return Err(());
281 }
282 let len = len as usize;
283 let mach = unsafe { std::slice::from_raw_parts(mach, len) }.to_vec();
284 let cd = unsafe { std::slice::from_raw_parts(cd, len) }.to_vec();
285 crate::drag::DragTable::try_new(mach, cd).map_err(|_| ())
286}
287
288unsafe fn calculate_trajectory_impl(
294 inputs: *const FFIBallisticInputs,
295 wind: *const FFIWindConditions,
296 atmosphere: *const FFIAtmosphericConditions,
297 max_range: c_double,
298 step_size: c_double,
299 custom_drag_table: Option<crate::drag::DragTable>,
300 cd_scale: c_double,
301) -> *mut FFITrajectoryResult {
302 if inputs.is_null() {
303 return ptr::null_mut();
304 }
305 if !step_size.is_finite() || step_size < MIN_FFI_STEP_SIZE_MS {
306 return ptr::null_mut();
307 }
308
309 let inputs = unsafe { &*inputs };
310 let mut ballistic_inputs = convert_inputs(inputs);
311 ballistic_inputs.custom_drag_table = custom_drag_table;
312 ballistic_inputs.cd_scale = cd_scale;
313 let twist_rate_in = ballistic_inputs.twist_rate;
314
315 let wind_conditions = if wind.is_null() {
316 WindConditions::default()
317 } else {
318 let wind = unsafe { &*wind };
319 WindConditions {
320 speed: wind.speed,
321 direction: wind.direction,
322 vertical_speed: wind.vertical_speed,
323 }
324 };
325
326 let atmospheric_conditions = if atmosphere.is_null() {
327 AtmosphericConditions::default()
328 } else {
329 let atmo = unsafe { &*atmosphere };
330 AtmosphericConditions {
331 temperature: atmo.temperature,
332 pressure: atmo.pressure,
333 humidity: atmo.humidity,
334 altitude: atmo.altitude,
335 }
336 };
337
338 let (sample_temp_c, sample_pressure_hpa) = crate::atmosphere::resolve_station_conditions(
340 atmospheric_conditions.temperature,
341 atmospheric_conditions.pressure,
342 atmospheric_conditions.altitude,
343 );
344 let (_, sample_speed_of_sound) = crate::atmosphere::calculate_atmosphere(
345 atmospheric_conditions.altitude,
346 Some(sample_temp_c),
347 Some(sample_pressure_hpa),
348 atmospheric_conditions.humidity,
349 );
350
351 let mut solver =
352 TrajectorySolver::new(ballistic_inputs, wind_conditions, atmospheric_conditions);
353
354 solver.set_max_range(max_range);
356 solver.set_time_step(step_size / 1000.0); match solver.solve() {
359 Ok(result) => {
360 let point_count = result.points.len();
362 let points = if point_count > 0 {
363 let mut ffi_points = Vec::with_capacity(point_count);
364 for point in result.points.iter() {
365 ffi_points.push(FFITrajectoryPoint {
366 time: point.time,
367 position_x: point.position[0],
368 position_y: point.position[1],
369 position_z: point.position[2],
370 velocity_magnitude: point.velocity_magnitude,
371 kinetic_energy: point.kinetic_energy,
372 });
373 }
374 let points_ptr = ffi_points.as_mut_ptr();
375 std::mem::forget(ffi_points); points_ptr
377 } else {
378 ptr::null_mut()
379 };
380
381 let (sampled_points, sampled_point_count) =
383 if let Some(ref samples) = result.sampled_points {
384 let mut ffi_samples = Vec::with_capacity(samples.len());
385 for sample in samples {
386 ffi_samples.push(FFITrajectorySample {
387 distance: sample.distance_m,
388 time: sample.time_s,
389 velocity_mps: sample.velocity_mps,
390 energy_joules: sample.energy_j,
391 drop_meters: sample.drop_m,
392 windage_meters: sample.wind_drift_m,
393 mach: if sample_speed_of_sound > 0.0 {
394 sample.velocity_mps / sample_speed_of_sound
395 } else {
396 0.0
397 },
398 spin_rate_rps: if twist_rate_in > 0.0 {
399 sample.velocity_mps / (twist_rate_in * 0.0254)
400 } else {
401 0.0
402 },
403 });
404 }
405 let count = ffi_samples.len() as c_int;
406 let samples_ptr = ffi_samples.as_mut_ptr();
407 std::mem::forget(ffi_samples);
408 (samples_ptr, count)
409 } else {
410 (ptr::null_mut(), 0)
411 };
412
413 let (final_pitch, final_yaw, max_yaw, max_prec) =
415 if let Some(ref angular) = result.angular_state {
416 (
417 angular.pitch_angle,
418 angular.yaw_angle,
419 result.max_yaw_angle.unwrap_or(f64::NAN),
420 result.max_precession_angle.unwrap_or(f64::NAN),
421 )
422 } else {
423 (f64::NAN, f64::NAN, f64::NAN, f64::NAN)
424 };
425
426 let ffi_result = Box::new(FFITrajectoryResult {
428 max_range: result.max_range,
429 max_height: result.max_height,
430 time_of_flight: result.time_of_flight,
431 impact_velocity: result.impact_velocity,
432 impact_energy: result.impact_energy,
433 points,
434 point_count: point_count as c_int,
435 sampled_points,
436 sampled_point_count,
437 min_pitch_damping: result.min_pitch_damping.unwrap_or(f64::NAN),
438 transonic_mach: result.transonic_mach.unwrap_or(f64::NAN),
439 final_pitch_angle: final_pitch,
440 final_yaw_angle: final_yaw,
441 max_yaw_angle: max_yaw,
442 max_precession_angle: max_prec,
443 });
444
445 Box::into_raw(ffi_result)
446 }
447 Err(_) => ptr::null_mut(),
448 }
449}
450
451#[no_mangle]
470pub unsafe extern "C" fn ballistics_calculate_trajectory(
471 inputs: *const FFIBallisticInputs,
472 wind: *const FFIWindConditions,
473 atmosphere: *const FFIAtmosphericConditions,
474 max_range: c_double,
475 step_size: c_double,
476) -> *mut FFITrajectoryResult {
477 unsafe { calculate_trajectory_impl(inputs, wind, atmosphere, max_range, step_size, None, 1.0) }
478}
479
480#[no_mangle]
501pub unsafe extern "C" fn ballistics_calculate_trajectory_with_drag_table(
502 inputs: *const FFIBallisticInputs,
503 wind: *const FFIWindConditions,
504 atmosphere: *const FFIAtmosphericConditions,
505 max_range: c_double,
506 step_size: c_double,
507 drag_mach: *const c_double,
508 drag_cd: *const c_double,
509 drag_table_len: c_int,
510) -> *mut FFITrajectoryResult {
511 let table = match unsafe { drag_table_from_raw(drag_mach, drag_cd, drag_table_len) } {
512 Ok(t) => t,
513 Err(()) => return ptr::null_mut(),
514 };
515 unsafe {
516 calculate_trajectory_impl(
517 inputs, wind, atmosphere, max_range, step_size, Some(table), 1.0,
518 )
519 }
520}
521
522#[no_mangle]
539pub unsafe extern "C" fn ballistics_calculate_trajectory_with_drag_table_scaled(
540 inputs: *const FFIBallisticInputs,
541 wind: *const FFIWindConditions,
542 atmosphere: *const FFIAtmosphericConditions,
543 max_range: c_double,
544 step_size: c_double,
545 drag_mach: *const c_double,
546 drag_cd: *const c_double,
547 drag_table_len: c_int,
548 cd_scale: c_double,
549) -> *mut FFITrajectoryResult {
550 if !cd_scale.is_finite() || cd_scale <= 0.0 {
551 return ptr::null_mut();
552 }
553 let table = match unsafe { drag_table_from_raw(drag_mach, drag_cd, drag_table_len) } {
554 Ok(t) => t,
555 Err(()) => return ptr::null_mut(),
556 };
557 unsafe {
558 calculate_trajectory_impl(
559 inputs, wind, atmosphere, max_range, step_size, Some(table), cd_scale,
560 )
561 }
562}
563
564#[no_mangle]
573pub unsafe extern "C" fn ballistics_free_trajectory_result(result: *mut FFITrajectoryResult) {
574 if !result.is_null() {
575 unsafe {
576 let result = Box::from_raw(result);
577 if !result.points.is_null() && result.point_count > 0 {
578 let points = Vec::from_raw_parts(
579 result.points,
580 result.point_count as usize,
581 result.point_count as usize,
582 );
583 drop(points);
584 }
585 if !result.sampled_points.is_null() && result.sampled_point_count > 0 {
586 let samples = Vec::from_raw_parts(
587 result.sampled_points,
588 result.sampled_point_count as usize,
589 result.sampled_point_count as usize,
590 );
591 drop(samples);
592 }
593 drop(result);
594 }
595 }
596}
597
598unsafe fn calculate_zero_angle_impl(
605 inputs: *const FFIBallisticInputs,
606 wind: *const FFIWindConditions,
607 atmosphere: *const FFIAtmosphericConditions,
608 zero_distance: c_double,
609 custom_drag_table: Option<crate::drag::DragTable>,
610 cd_scale: c_double,
611) -> c_double {
612 if inputs.is_null() {
613 return f64::NAN;
614 }
615
616 let inputs = unsafe { &*inputs };
617 let mut ballistic_inputs = convert_inputs(inputs);
618 ballistic_inputs.custom_drag_table = custom_drag_table;
619 ballistic_inputs.cd_scale = cd_scale;
620
621 let wind_conditions = if wind.is_null() {
622 WindConditions::default()
623 } else {
624 let wind = unsafe { &*wind };
625 WindConditions {
626 speed: wind.speed,
627 direction: wind.direction,
628 vertical_speed: wind.vertical_speed,
629 }
630 };
631
632 let atmospheric_conditions = if atmosphere.is_null() {
633 AtmosphericConditions::default()
634 } else {
635 let atmo = unsafe { &*atmosphere };
636 AtmosphericConditions {
637 temperature: atmo.temperature,
638 pressure: atmo.pressure,
639 humidity: atmo.humidity,
640 altitude: atmo.altitude,
641 }
642 };
643
644 let target_height = ballistic_inputs.sight_height;
647
648 calculate_zero_angle_with_conditions(
649 ballistic_inputs,
650 zero_distance,
651 target_height,
652 wind_conditions,
653 atmospheric_conditions,
654 )
655 .unwrap_or(f64::NAN)
656}
657
658#[no_mangle]
668pub unsafe extern "C" fn ballistics_calculate_zero_angle(
669 inputs: *const FFIBallisticInputs,
670 wind: *const FFIWindConditions,
671 atmosphere: *const FFIAtmosphericConditions,
672 zero_distance: c_double,
673) -> c_double {
674 unsafe { calculate_zero_angle_impl(inputs, wind, atmosphere, zero_distance, None, 1.0) }
675}
676
677#[no_mangle]
699pub unsafe extern "C" fn ballistics_calculate_zero_angle_with_drag_table(
700 inputs: *const FFIBallisticInputs,
701 wind: *const FFIWindConditions,
702 atmosphere: *const FFIAtmosphericConditions,
703 zero_distance: c_double,
704 drag_mach: *const c_double,
705 drag_cd: *const c_double,
706 drag_table_len: c_int,
707) -> c_double {
708 let table = match unsafe { drag_table_from_raw(drag_mach, drag_cd, drag_table_len) } {
709 Ok(t) => t,
710 Err(()) => return f64::NAN,
711 };
712 unsafe {
713 calculate_zero_angle_impl(inputs, wind, atmosphere, zero_distance, Some(table), 1.0)
714 }
715}
716
717#[no_mangle]
735pub unsafe extern "C" fn ballistics_calculate_zero_angle_with_drag_table_scaled(
736 inputs: *const FFIBallisticInputs,
737 wind: *const FFIWindConditions,
738 atmosphere: *const FFIAtmosphericConditions,
739 zero_distance: c_double,
740 drag_mach: *const c_double,
741 drag_cd: *const c_double,
742 drag_table_len: c_int,
743 cd_scale: c_double,
744) -> c_double {
745 if !cd_scale.is_finite() || cd_scale <= 0.0 {
746 return f64::NAN;
747 }
748 let table = match unsafe { drag_table_from_raw(drag_mach, drag_cd, drag_table_len) } {
749 Ok(t) => t,
750 Err(()) => return f64::NAN,
751 };
752 unsafe {
753 calculate_zero_angle_impl(
754 inputs,
755 wind,
756 atmosphere,
757 zero_distance,
758 Some(table),
759 cd_scale,
760 )
761 }
762}
763
764#[no_mangle]
766#[allow(clippy::field_reassign_with_default)] pub extern "C" fn ballistics_quick_trajectory(
768 muzzle_velocity: c_double,
769 bc: c_double,
770 sight_height: c_double,
771 zero_distance: c_double,
772 target_distance: c_double,
773) -> c_double {
774 let mut inputs = BallisticInputs::default();
778 inputs.muzzle_velocity = muzzle_velocity;
779 inputs.bc_value = bc;
780 inputs.sight_height = sight_height;
781 inputs.target_distance = target_distance;
782
783 let wind = WindConditions::default();
784 let atmo = AtmosphericConditions::default();
785
786 let zero_angle = match calculate_zero_angle_with_conditions(
788 inputs.clone(),
789 zero_distance,
790 sight_height,
791 wind.clone(),
792 atmo.clone(),
793 ) {
794 Ok(angle) => angle,
795 Err(_) => return f64::NAN,
796 };
797
798 inputs.muzzle_angle = zero_angle;
800
801 let mut solver = TrajectorySolver::new(inputs, wind, atmo);
802 solver.set_max_range(target_distance * 1.1);
803
804 match solver.solve() {
805 Ok(result) => {
806 for point in result.points {
808 if point.position[0] >= target_distance {
809 return sight_height - point.position[1];
810 }
811 }
812 f64::NAN
813 }
814 Err(_) => f64::NAN,
815 }
816}
817
818#[no_mangle]
830pub unsafe extern "C" fn ballistics_monte_carlo(
831 inputs: *const FFIBallisticInputs,
832 atmosphere: *const FFIAtmosphericConditions,
833 params: *const FFIMonteCarloParams,
834) -> *mut FFIMonteCarloResults {
835 unsafe { ballistics_monte_carlo_impl(inputs, atmosphere, params, 0.0) }
836}
837
838#[no_mangle]
848pub unsafe extern "C" fn ballistics_monte_carlo_with_direction_std_dev(
849 inputs: *const FFIBallisticInputs,
850 atmosphere: *const FFIAtmosphericConditions,
851 params: *const FFIMonteCarloParams,
852 wind_direction_std_dev: c_double,
853) -> *mut FFIMonteCarloResults {
854 unsafe { ballistics_monte_carlo_impl(inputs, atmosphere, params, wind_direction_std_dev) }
855}
856
857unsafe fn ballistics_monte_carlo_impl(
858 inputs: *const FFIBallisticInputs,
859 atmosphere: *const FFIAtmosphericConditions,
860 params: *const FFIMonteCarloParams,
861 wind_direction_std_dev: f64,
862) -> *mut FFIMonteCarloResults {
863 if inputs.is_null() || params.is_null() {
864 return ptr::null_mut();
865 }
866
867 let inputs = unsafe { &*inputs };
868 let params = unsafe { &*params };
869
870 const MAX_SIMULATIONS: c_int = 1_000_000;
876 if params.num_simulations <= 0 || params.num_simulations > MAX_SIMULATIONS {
877 return ptr::null_mut();
878 }
879
880 let mut ballistic_inputs = convert_inputs(inputs);
882 ballistic_inputs.muzzle_height = 1.5;
883 ballistic_inputs.ground_threshold = 0.0;
884 if !atmosphere.is_null() {
885 let atmo = unsafe { &*atmosphere };
886 ballistic_inputs.temperature = atmo.temperature;
887 ballistic_inputs.pressure = atmo.pressure;
888 ballistic_inputs.humidity = (atmo.humidity / 100.0).clamp(0.0, 1.0);
889 ballistic_inputs.altitude = atmo.altitude;
890 }
891
892 let mc_params = MonteCarloParams {
894 num_simulations: params.num_simulations as usize,
895 velocity_std_dev: params.velocity_std_dev,
896 angle_std_dev: params.angle_std_dev,
897 bc_std_dev: params.bc_std_dev,
898 wind_speed_std_dev: params.wind_speed_std_dev,
899 target_distance: if params.target_distance.is_nan() {
900 None
901 } else {
902 Some(params.target_distance)
903 },
904 base_wind_speed: params.base_wind_speed,
905 base_wind_direction: params.base_wind_direction,
906 azimuth_std_dev: params.azimuth_std_dev,
907 };
908
909 match run_monte_carlo_with_direction_std_dev(
911 ballistic_inputs,
912 mc_params,
913 wind_direction_std_dev,
914 ) {
915 Ok(results) => {
916 let num_results = results.ranges.len() as c_int;
917
918 let mean_range: f64 = results.ranges.iter().sum::<f64>() / num_results as f64;
920 let variance_range: f64 = results
921 .ranges
922 .iter()
923 .map(|r| (r - mean_range).powi(2))
924 .sum::<f64>()
925 / num_results as f64;
926 let std_dev_range = variance_range.sqrt();
927
928 let mean_velocity: f64 =
929 results.impact_velocities.iter().sum::<f64>() / num_results as f64;
930 let variance_velocity: f64 = results
931 .impact_velocities
932 .iter()
933 .map(|v| (v - mean_velocity).powi(2))
934 .sum::<f64>()
935 / num_results as f64;
936 let std_dev_velocity = variance_velocity.sqrt();
937
938 let hit_probability = if params.target_distance.is_nan() {
944 0.0
945 } else {
946 results.hit_probability(crate::DEFAULT_HIT_RADIUS_M)
947 };
948
949 let ranges_ptr = unsafe {
951 let ptr = std::alloc::alloc(
952 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
953 ) as *mut c_double;
954 for (i, &range) in results.ranges.iter().enumerate() {
955 *ptr.add(i) = range;
956 }
957 ptr
958 };
959
960 let velocities_ptr = unsafe {
961 let ptr = std::alloc::alloc(
962 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
963 ) as *mut c_double;
964 for (i, &vel) in results.impact_velocities.iter().enumerate() {
965 *ptr.add(i) = vel;
966 }
967 ptr
968 };
969
970 let pos_x_ptr = unsafe {
971 let ptr = std::alloc::alloc(
972 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
973 ) as *mut c_double;
974 for (i, pos) in results.impact_positions.iter().enumerate() {
975 *ptr.add(i) = pos.x;
976 }
977 ptr
978 };
979
980 let pos_y_ptr = unsafe {
981 let ptr = std::alloc::alloc(
982 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
983 ) as *mut c_double;
984 for (i, pos) in results.impact_positions.iter().enumerate() {
985 *ptr.add(i) = pos.y;
986 }
987 ptr
988 };
989
990 let pos_z_ptr = unsafe {
991 let ptr = std::alloc::alloc(
992 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
993 ) as *mut c_double;
994 for (i, pos) in results.impact_positions.iter().enumerate() {
995 *ptr.add(i) = pos.z;
996 }
997 ptr
998 };
999
1000 let result = Box::new(FFIMonteCarloResults {
1002 ranges: ranges_ptr,
1003 impact_velocities: velocities_ptr,
1004 impact_positions_x: pos_x_ptr,
1005 impact_positions_y: pos_y_ptr,
1006 impact_positions_z: pos_z_ptr,
1007 num_results,
1008 mean_range,
1009 std_dev_range,
1010 mean_impact_velocity: mean_velocity,
1011 std_dev_impact_velocity: std_dev_velocity,
1012 hit_probability,
1013 });
1014
1015 Box::into_raw(result)
1016 }
1017 Err(_) => ptr::null_mut(),
1018 }
1019}
1020
1021#[no_mangle]
1030pub unsafe extern "C" fn ballistics_free_monte_carlo_results(results: *mut FFIMonteCarloResults) {
1031 if results.is_null() {
1032 return;
1033 }
1034
1035 unsafe {
1036 let results = Box::from_raw(results);
1037 let num = results.num_results as usize;
1038
1039 if !results.ranges.is_null() {
1041 std::alloc::dealloc(
1042 results.ranges as *mut u8,
1043 std::alloc::Layout::array::<c_double>(num).unwrap(),
1044 );
1045 }
1046
1047 if !results.impact_velocities.is_null() {
1048 std::alloc::dealloc(
1049 results.impact_velocities as *mut u8,
1050 std::alloc::Layout::array::<c_double>(num).unwrap(),
1051 );
1052 }
1053
1054 if !results.impact_positions_x.is_null() {
1055 std::alloc::dealloc(
1056 results.impact_positions_x as *mut u8,
1057 std::alloc::Layout::array::<c_double>(num).unwrap(),
1058 );
1059 }
1060
1061 if !results.impact_positions_y.is_null() {
1062 std::alloc::dealloc(
1063 results.impact_positions_y as *mut u8,
1064 std::alloc::Layout::array::<c_double>(num).unwrap(),
1065 );
1066 }
1067
1068 if !results.impact_positions_z.is_null() {
1069 std::alloc::dealloc(
1070 results.impact_positions_z as *mut u8,
1071 std::alloc::Layout::array::<c_double>(num).unwrap(),
1072 );
1073 }
1074
1075 }
1077}
1078
1079pub const FFI_BC_REFERENCE_ICAO: c_int = 0;
1085pub const FFI_BC_REFERENCE_ARMY_STANDARD_METRO: c_int = 1;
1086
1087#[no_mangle]
1106pub extern "C" fn ballistics_bc_for_reference_standard(
1107 bc: c_double,
1108 reference_standard: c_int,
1109) -> c_double {
1110 if reference_standard == FFI_BC_REFERENCE_ARMY_STANDARD_METRO {
1111 bc * crate::constants::ASM_TO_ICAO_BC
1112 } else {
1113 bc
1114 }
1115}
1116
1117#[no_mangle]
1135pub extern "C" fn ballistics_reduce_qnh_pressure(
1136 qnh_hpa: c_double,
1137 altitude_m: c_double,
1138) -> c_double {
1139 if !qnh_hpa.is_finite() || !altitude_m.is_finite() {
1140 return qnh_hpa;
1141 }
1142 crate::atmosphere::reduce_qnh_to_station_pressure(qnh_hpa, altitude_m)
1143}
1144
1145pub const FFI_NO_EXPLICIT_TEMPERATURE: c_double = f64::NAN;
1150
1151#[no_mangle]
1171pub extern "C" fn ballistics_density_altitude_temperature_c(
1172 density_altitude_m: c_double,
1173 explicit_temperature_c: c_double,
1174) -> c_double {
1175 if !density_altitude_m.is_finite() {
1176 return f64::NAN;
1177 }
1178 let explicit = (!explicit_temperature_c.is_nan()).then_some(explicit_temperature_c);
1179 crate::atmosphere::resolve_atmosphere_for_density_altitude(density_altitude_m, explicit).1
1180}
1181
1182#[no_mangle]
1185pub extern "C" fn ballistics_density_altitude_pressure_hpa(
1186 density_altitude_m: c_double,
1187 explicit_temperature_c: c_double,
1188) -> c_double {
1189 if !density_altitude_m.is_finite() {
1190 return f64::NAN;
1191 }
1192 let explicit = (!explicit_temperature_c.is_nan()).then_some(explicit_temperature_c);
1193 crate::atmosphere::resolve_atmosphere_for_density_altitude(density_altitude_m, explicit).2
1194}
1195
1196#[no_mangle]
1201pub extern "C" fn ballistics_density_altitude_altitude_m(
1202 density_altitude_m: c_double,
1203 explicit_temperature_c: c_double,
1204) -> c_double {
1205 if !density_altitude_m.is_finite() {
1206 return f64::NAN;
1207 }
1208 let explicit = (!explicit_temperature_c.is_nan()).then_some(explicit_temperature_c);
1209 crate::atmosphere::resolve_atmosphere_for_density_altitude(density_altitude_m, explicit).0
1210}
1211
1212pub const MAX_FFI_RETICLE_MARKS: c_int = crate::reticle::MAX_RETICLE_MARKS as c_int;
1220
1221pub const FFI_RETICLE_FIRST_FOCAL_PLANE: c_int = 0;
1224pub const FFI_RETICLE_SECOND_FOCAL_PLANE: c_int = 1;
1227
1228pub const FFI_RETICLE_OK: c_int = 0;
1230pub const FFI_RETICLE_ERR_INVALID_ARGUMENT: c_int = -1;
1232pub const FFI_RETICLE_ERR_MAGNIFICATION: c_int = -2;
1234pub const FFI_RETICLE_ERR_REFERENCE_MAGNIFICATION: c_int = -3;
1236pub const FFI_RETICLE_ERR_NON_FINITE: c_int = -4;
1238
1239#[repr(C)]
1246pub struct FFIReticleHold {
1247 pub down_mil: c_double,
1249 pub right_mil: c_double,
1251 pub nearest_mark: c_int,
1254 pub nearest_mark_distance_mil: c_double,
1257 pub off_reticle: c_int,
1260 pub mark_scale: c_double,
1263}
1264
1265#[no_mangle]
1284pub unsafe extern "C" fn ballistics_hold_point_in_reticle(
1285 drop_mil: c_double,
1286 wind_mil: c_double,
1287 magnification: c_double,
1288 marks: *const c_double,
1289 marks_len: c_int,
1290 focal_plane: c_int,
1291 reference_magnification: c_double,
1292 out: *mut FFIReticleHold,
1293) -> c_int {
1294 use crate::reticle::{
1295 hold_point_in_reticle, FocalPlane, MarkKind, ReticleDescription, ReticleError, ReticleMark,
1296 };
1297
1298 if marks.is_null() || out.is_null() || !(1..=MAX_FFI_RETICLE_MARKS).contains(&marks_len) {
1299 return FFI_RETICLE_ERR_INVALID_ARGUMENT;
1300 }
1301 let count = marks_len as usize;
1302 let flat = unsafe { std::slice::from_raw_parts(marks, count * 2) };
1304
1305 let description = ReticleDescription {
1306 name: String::new(),
1307 focal_plane: if focal_plane == FFI_RETICLE_SECOND_FOCAL_PLANE {
1308 FocalPlane::Second
1309 } else {
1310 FocalPlane::First
1311 },
1312 reference_magnification,
1313 marks: flat
1314 .chunks_exact(2)
1315 .map(|pair| ReticleMark::new(pair[0], pair[1], MarkKind::Dot))
1316 .collect(),
1317 };
1318
1319 let hold = match hold_point_in_reticle(drop_mil, wind_mil, magnification, &description) {
1320 Ok(hold) => hold,
1321 Err(ReticleError::NonPositiveMagnification { .. }) => return FFI_RETICLE_ERR_MAGNIFICATION,
1322 Err(ReticleError::NonPositiveReferenceMagnification { .. }) => {
1323 return FFI_RETICLE_ERR_REFERENCE_MAGNIFICATION
1324 }
1325 Err(ReticleError::NonFiniteMark { .. }) | Err(ReticleError::NonFiniteHold { .. }) => {
1326 return FFI_RETICLE_ERR_NON_FINITE
1327 }
1328 Err(_) => return FFI_RETICLE_ERR_INVALID_ARGUMENT,
1329 };
1330
1331 unsafe {
1332 *out = FFIReticleHold {
1333 down_mil: hold.down_mil,
1334 right_mil: hold.right_mil,
1335 nearest_mark: hold.nearest_mark.map_or(-1, |index| index as c_int),
1336 nearest_mark_distance_mil: hold.nearest_mark_distance_mil,
1337 off_reticle: c_int::from(hold.off_reticle),
1338 mark_scale: hold.mark_scale,
1339 };
1340 }
1341 FFI_RETICLE_OK
1342}
1343
1344#[no_mangle]
1346pub extern "C" fn ballistics_get_version() -> *const c_char {
1347 concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr() as *const c_char
1351}
1352
1353#[cfg(test)]
1354mod tests {
1355 use super::*;
1356
1357 fn valid_trajectory_inputs() -> FFIBallisticInputs {
1358 FFIBallisticInputs {
1359 muzzle_velocity: 800.0,
1360 muzzle_angle: 0.0,
1361 bc_value: 0.5,
1362 bullet_mass: 0.01,
1363 bullet_diameter: 0.00762,
1364 bc_type: 0,
1365 sight_height: 0.05,
1366 target_distance: 1.0,
1367 temperature: 15.0,
1368 twist_rate: 12.0,
1369 is_twist_right: 1,
1370 shooting_angle: 0.0,
1371 altitude: 0.0,
1372 latitude: f64::NAN,
1373 azimuth_angle: 0.0,
1374 use_rk4: 1,
1375 use_adaptive_rk45: 0,
1376 enable_wind_shear: 0,
1377 enable_trajectory_sampling: 0,
1378 sample_interval: 10.0,
1379 enable_pitch_damping: 0,
1380 enable_precession_nutation: 0,
1381 enable_spin_drift: 0,
1382 enable_magnus: 0,
1383 enable_coriolis: 0,
1384 shot_azimuth: 0.0,
1385 cant_angle: 0.0,
1386 zero_poi_vertical: 0.0,
1387 zero_poi_horizontal: 0.0,
1388 sight_offset_lateral: 0.0,
1389 }
1390 }
1391
1392 #[allow(dead_code)]
1393 #[repr(C)]
1394 struct LegacyFFIMonteCarloParams {
1395 num_simulations: c_int,
1396 velocity_std_dev: c_double,
1397 angle_std_dev: c_double,
1398 bc_std_dev: c_double,
1399 wind_speed_std_dev: c_double,
1400 target_distance: c_double,
1401 base_wind_speed: c_double,
1402 base_wind_direction: c_double,
1403 azimuth_std_dev: c_double,
1404 }
1405
1406 #[test]
1407 fn monte_carlo_params_legacy_abi_size_is_unchanged() {
1408 assert_eq!(
1409 std::mem::size_of::<FFIMonteCarloParams>(),
1410 std::mem::size_of::<LegacyFFIMonteCarloParams>()
1411 );
1412 assert_eq!(
1413 std::mem::align_of::<FFIMonteCarloParams>(),
1414 std::mem::align_of::<LegacyFFIMonteCarloParams>()
1415 );
1416 }
1417
1418 #[test]
1421 fn reticle_addition_does_not_disturb_existing_layouts() {
1422 assert_eq!(
1423 std::mem::size_of::<FFIMonteCarloParams>(),
1424 std::mem::size_of::<LegacyFFIMonteCarloParams>()
1425 );
1426 assert_eq!(std::mem::align_of::<FFIReticleHold>(), 8);
1428 }
1429
1430 fn zeroed_hold() -> FFIReticleHold {
1431 FFIReticleHold {
1432 down_mil: 0.0,
1433 right_mil: 0.0,
1434 nearest_mark: -99,
1435 nearest_mark_distance_mil: -1.0,
1436 off_reticle: -1,
1437 mark_scale: -1.0,
1438 }
1439 }
1440
1441 #[test]
1442 fn ffi_hold_point_matches_the_rust_api_on_both_focal_planes() {
1443 let marks: [c_double; 8] = [0.0, 0.0, 2.0, 0.0, 4.0, 0.0, 2.0, 1.0];
1445 let mut out = zeroed_hold();
1446
1447 let code = unsafe {
1449 ballistics_hold_point_in_reticle(
1450 4.0,
1451 0.0,
1452 6.0,
1453 marks.as_ptr(),
1454 4,
1455 FFI_RETICLE_FIRST_FOCAL_PLANE,
1456 0.0,
1457 &mut out,
1458 )
1459 };
1460 assert_eq!(code, FFI_RETICLE_OK);
1461 assert_eq!(out.down_mil, 4.0);
1462 assert_eq!(out.nearest_mark, 2);
1463 assert_eq!(out.nearest_mark_distance_mil, 0.0);
1464 assert_eq!(out.mark_scale, 1.0);
1465 assert_eq!(out.off_reticle, 0);
1466
1467 let mut out = zeroed_hold();
1469 let code = unsafe {
1470 ballistics_hold_point_in_reticle(
1471 4.0,
1472 0.0,
1473 5.0,
1474 marks.as_ptr(),
1475 4,
1476 FFI_RETICLE_SECOND_FOCAL_PLANE,
1477 10.0,
1478 &mut out,
1479 )
1480 };
1481 assert_eq!(code, FFI_RETICLE_OK);
1482 assert_eq!(out.nearest_mark, 1);
1483 assert_eq!(out.nearest_mark_distance_mil, 0.0);
1484 assert_eq!(out.mark_scale, 2.0);
1485 }
1486
1487 #[test]
1490 fn ffi_hold_point_bounds_check_marks_len_before_reading() {
1491 let marks: [c_double; 4] = [0.0, 0.0, 2.0, 0.0];
1492 let mut out = zeroed_hold();
1493 let call = |len: c_int, ptr: *const c_double, out: &mut FFIReticleHold| unsafe {
1494 ballistics_hold_point_in_reticle(
1495 1.0,
1496 0.0,
1497 10.0,
1498 ptr,
1499 len,
1500 FFI_RETICLE_FIRST_FOCAL_PLANE,
1501 0.0,
1502 out,
1503 )
1504 };
1505
1506 assert_eq!(call(0, marks.as_ptr(), &mut out), FFI_RETICLE_ERR_INVALID_ARGUMENT);
1507 assert_eq!(call(-1, marks.as_ptr(), &mut out), FFI_RETICLE_ERR_INVALID_ARGUMENT);
1508 assert_eq!(
1509 call(MAX_FFI_RETICLE_MARKS + 1, marks.as_ptr(), &mut out),
1510 FFI_RETICLE_ERR_INVALID_ARGUMENT
1511 );
1512 assert_eq!(call(c_int::MAX, marks.as_ptr(), &mut out), FFI_RETICLE_ERR_INVALID_ARGUMENT);
1513 assert_eq!(call(2, std::ptr::null(), &mut out), FFI_RETICLE_ERR_INVALID_ARGUMENT);
1514 assert_eq!(out.nearest_mark, -99);
1516
1517 assert_eq!(
1519 unsafe {
1520 ballistics_hold_point_in_reticle(
1521 1.0,
1522 0.0,
1523 10.0,
1524 marks.as_ptr(),
1525 2,
1526 FFI_RETICLE_FIRST_FOCAL_PLANE,
1527 0.0,
1528 std::ptr::null_mut(),
1529 )
1530 },
1531 FFI_RETICLE_ERR_INVALID_ARGUMENT
1532 );
1533 }
1534
1535 #[test]
1536 fn ffi_hold_point_maps_each_error_class_to_its_own_code() {
1537 let marks: [c_double; 4] = [0.0, 0.0, 2.0, 0.0];
1538 let bad_marks: [c_double; 4] = [0.0, 0.0, f64::NAN, 0.0];
1539 let mut out = zeroed_hold();
1540 let call = |drop: c_double, mag: c_double, plane: c_int, ref_mag: c_double,
1541 m: &[c_double], out: &mut FFIReticleHold| unsafe {
1542 ballistics_hold_point_in_reticle(
1543 drop,
1544 0.0,
1545 mag,
1546 m.as_ptr(),
1547 (m.len() / 2) as c_int,
1548 plane,
1549 ref_mag,
1550 out,
1551 )
1552 };
1553
1554 assert_eq!(
1555 call(1.0, 0.0, FFI_RETICLE_FIRST_FOCAL_PLANE, 0.0, &marks, &mut out),
1556 FFI_RETICLE_ERR_MAGNIFICATION
1557 );
1558 assert_eq!(
1559 call(1.0, 10.0, FFI_RETICLE_SECOND_FOCAL_PLANE, 0.0, &marks, &mut out),
1560 FFI_RETICLE_ERR_REFERENCE_MAGNIFICATION
1561 );
1562 assert_eq!(
1563 call(f64::NAN, 10.0, FFI_RETICLE_FIRST_FOCAL_PLANE, 0.0, &marks, &mut out),
1564 FFI_RETICLE_ERR_NON_FINITE
1565 );
1566 assert_eq!(
1567 call(1.0, 10.0, FFI_RETICLE_FIRST_FOCAL_PLANE, 0.0, &bad_marks, &mut out),
1568 FFI_RETICLE_ERR_NON_FINITE
1569 );
1570 assert_eq!(out.nearest_mark, -99, "out stays untouched on every error");
1571 }
1572
1573 #[test]
1578 fn bc_type_8_maps_to_ra4() {
1579 let mut inputs = valid_trajectory_inputs();
1580 inputs.bc_type = 8;
1581 assert_eq!(convert_inputs(&inputs).bc_type, DragModel::RA4);
1582
1583 let expected = [
1585 (0, DragModel::G1),
1586 (1, DragModel::G7),
1587 (2, DragModel::G2),
1588 (3, DragModel::G5),
1589 (4, DragModel::G6),
1590 (5, DragModel::G8),
1591 (6, DragModel::GI),
1592 (7, DragModel::GS),
1593 ];
1594 for (code, model) in expected {
1595 let mut inputs = valid_trajectory_inputs();
1596 inputs.bc_type = code;
1597 assert_eq!(convert_inputs(&inputs).bc_type, model, "code {code}");
1598 }
1599
1600 for code in [9, 42, -1] {
1602 let mut inputs = valid_trajectory_inputs();
1603 inputs.bc_type = code;
1604 assert_eq!(convert_inputs(&inputs).bc_type, DragModel::G1, "code {code}");
1605 }
1606 }
1607
1608 #[test]
1609 fn null_pointer_contracts_return_sentinels_and_free_safely() {
1610 unsafe {
1611 assert!(ballistics_calculate_trajectory(
1612 std::ptr::null(),
1613 std::ptr::null(),
1614 std::ptr::null(),
1615 1_000.0,
1616 1.0,
1617 )
1618 .is_null());
1619 assert!(ballistics_calculate_zero_angle(
1620 std::ptr::null(),
1621 std::ptr::null(),
1622 std::ptr::null(),
1623 100.0,
1624 )
1625 .is_nan());
1626 assert!(ballistics_calculate_trajectory_with_drag_table(
1627 std::ptr::null(),
1628 std::ptr::null(),
1629 std::ptr::null(),
1630 1_000.0,
1631 1.0,
1632 DECK_MACH.as_ptr(),
1633 DECK_CD_LOW.as_ptr(),
1634 DECK_MACH.len() as c_int,
1635 )
1636 .is_null());
1637 assert!(ballistics_calculate_zero_angle_with_drag_table(
1638 std::ptr::null(),
1639 std::ptr::null(),
1640 std::ptr::null(),
1641 100.0,
1642 DECK_MACH.as_ptr(),
1643 DECK_CD_LOW.as_ptr(),
1644 DECK_MACH.len() as c_int,
1645 )
1646 .is_nan());
1647 assert!(
1648 ballistics_monte_carlo(std::ptr::null(), std::ptr::null(), std::ptr::null(),)
1649 .is_null()
1650 );
1651 assert!(ballistics_monte_carlo_with_direction_std_dev(
1652 std::ptr::null(),
1653 std::ptr::null(),
1654 std::ptr::null(),
1655 0.1,
1656 )
1657 .is_null());
1658
1659 ballistics_free_trajectory_result(std::ptr::null_mut());
1660 ballistics_free_monte_carlo_results(std::ptr::null_mut());
1661 }
1662 }
1663
1664 #[test]
1665 fn mba1283_ffi_enforces_step_floor_for_every_solver_mode() {
1666 for (mode, use_rk4, use_adaptive_rk45) in [("Euler", 0, 0), ("RK4", 1, 0), ("RK45", 1, 1)] {
1667 for step_size in [
1668 f64::NAN,
1669 f64::INFINITY,
1670 f64::NEG_INFINITY,
1671 -1.0,
1672 -0.0,
1673 0.0,
1674 0.001,
1675 MIN_FFI_STEP_SIZE_MS - 0.001,
1676 ] {
1677 let mut inputs = valid_trajectory_inputs();
1678 inputs.use_rk4 = use_rk4;
1679 inputs.use_adaptive_rk45 = use_adaptive_rk45;
1680 let result = unsafe {
1681 ballistics_calculate_trajectory(
1682 &inputs,
1683 std::ptr::null(),
1684 std::ptr::null(),
1685 0.01,
1686 step_size,
1687 )
1688 };
1689 assert!(
1690 result.is_null(),
1691 "{mode} step_size={step_size:?} bypassed the FFI floor"
1692 );
1693 }
1694
1695 let mut inputs = valid_trajectory_inputs();
1696 inputs.use_rk4 = use_rk4;
1697 inputs.use_adaptive_rk45 = use_adaptive_rk45;
1698 let result = unsafe {
1699 ballistics_calculate_trajectory(
1700 &inputs,
1701 std::ptr::null(),
1702 std::ptr::null(),
1703 0.01,
1704 MIN_FFI_STEP_SIZE_MS,
1705 )
1706 };
1707 assert!(
1708 !result.is_null(),
1709 "the documented minimum step must remain usable in {mode}"
1710 );
1711 unsafe {
1712 assert!((*result).point_count >= 0);
1713 assert!((*result).point_count as usize <= crate::MAX_TRAJECTORY_POINTS);
1714 ballistics_free_trajectory_result(result);
1715 }
1716 }
1717 }
1718
1719 const DECK_MACH: [f64; 4] = [0.5, 1.0, 2.0, 3.0];
1721 const DECK_CD_LOW: [f64; 4] = [0.05, 0.08, 0.06, 0.05];
1723
1724 #[test]
1725 fn trajectory_with_drag_table_applies_the_deck() {
1726 let inputs = valid_trajectory_inputs();
1727 unsafe {
1728 let plain = ballistics_calculate_trajectory(
1729 &inputs,
1730 std::ptr::null(),
1731 std::ptr::null(),
1732 300.0,
1733 1.0,
1734 );
1735 let decked = ballistics_calculate_trajectory_with_drag_table(
1736 &inputs,
1737 std::ptr::null(),
1738 std::ptr::null(),
1739 300.0,
1740 1.0,
1741 DECK_MACH.as_ptr(),
1742 DECK_CD_LOW.as_ptr(),
1743 DECK_MACH.len() as c_int,
1744 );
1745 assert!(!plain.is_null() && !decked.is_null());
1746 assert!(
1748 (*decked).impact_velocity > (*plain).impact_velocity + 1.0,
1749 "deck did not change the solve: plain={} decked={}",
1750 (*plain).impact_velocity,
1751 (*decked).impact_velocity
1752 );
1753 ballistics_free_trajectory_result(plain);
1754 ballistics_free_trajectory_result(decked);
1755 }
1756 }
1757
1758 #[test]
1759 fn trajectory_with_drag_table_rejects_invalid_decks() {
1760 let inputs = valid_trajectory_inputs();
1761 let descending = [3.0, 2.0, 1.0, 0.5];
1762 let negative_cd = [0.05, -0.08, 0.06, 0.05];
1763 unsafe {
1764 assert!(ballistics_calculate_trajectory_with_drag_table(
1766 &inputs,
1767 std::ptr::null(),
1768 std::ptr::null(),
1769 300.0,
1770 1.0,
1771 std::ptr::null(),
1772 DECK_CD_LOW.as_ptr(),
1773 4,
1774 )
1775 .is_null());
1776 assert!(ballistics_calculate_trajectory_with_drag_table(
1777 &inputs,
1778 std::ptr::null(),
1779 std::ptr::null(),
1780 300.0,
1781 1.0,
1782 DECK_MACH.as_ptr(),
1783 std::ptr::null(),
1784 4,
1785 )
1786 .is_null());
1787 assert!(ballistics_calculate_trajectory_with_drag_table(
1789 &inputs,
1790 std::ptr::null(),
1791 std::ptr::null(),
1792 300.0,
1793 1.0,
1794 DECK_MACH.as_ptr(),
1795 DECK_CD_LOW.as_ptr(),
1796 1,
1797 )
1798 .is_null());
1799 assert!(ballistics_calculate_trajectory_with_drag_table(
1801 &inputs,
1802 std::ptr::null(),
1803 std::ptr::null(),
1804 300.0,
1805 1.0,
1806 descending.as_ptr(),
1807 DECK_CD_LOW.as_ptr(),
1808 4,
1809 )
1810 .is_null());
1811 assert!(ballistics_calculate_trajectory_with_drag_table(
1813 &inputs,
1814 std::ptr::null(),
1815 std::ptr::null(),
1816 300.0,
1817 1.0,
1818 DECK_MACH.as_ptr(),
1819 negative_cd.as_ptr(),
1820 4,
1821 )
1822 .is_null());
1823 assert!(ballistics_calculate_trajectory_with_drag_table(
1825 std::ptr::null(),
1826 std::ptr::null(),
1827 std::ptr::null(),
1828 300.0,
1829 1.0,
1830 DECK_MACH.as_ptr(),
1831 DECK_CD_LOW.as_ptr(),
1832 4,
1833 )
1834 .is_null());
1835 }
1836 }
1837
1838 #[test]
1839 fn zero_angle_with_drag_table_applies_the_deck() {
1840 let inputs = valid_trajectory_inputs();
1842 unsafe {
1843 let plain =
1844 ballistics_calculate_zero_angle(&inputs, std::ptr::null(), std::ptr::null(), 100.0);
1845 let decked = ballistics_calculate_zero_angle_with_drag_table(
1846 &inputs,
1847 std::ptr::null(),
1848 std::ptr::null(),
1849 100.0,
1850 DECK_MACH.as_ptr(),
1851 DECK_CD_LOW.as_ptr(),
1852 DECK_MACH.len() as c_int,
1853 );
1854 assert!(plain.is_finite() && decked.is_finite());
1855 assert!(
1858 (plain - decked).abs() > 1e-6,
1859 "deck did not change the zero: plain={plain} decked={decked}"
1860 );
1861 }
1862 }
1863
1864 #[test]
1865 fn zero_angle_with_drag_table_rejects_invalid_decks() {
1866 let inputs = valid_trajectory_inputs();
1867 let descending = [3.0, 2.0, 1.0, 0.5];
1868 unsafe {
1869 assert!(ballistics_calculate_zero_angle_with_drag_table(
1870 &inputs,
1871 std::ptr::null(),
1872 std::ptr::null(),
1873 100.0,
1874 std::ptr::null(),
1875 DECK_CD_LOW.as_ptr(),
1876 4,
1877 )
1878 .is_nan());
1879 assert!(ballistics_calculate_zero_angle_with_drag_table(
1880 &inputs,
1881 std::ptr::null(),
1882 std::ptr::null(),
1883 100.0,
1884 DECK_MACH.as_ptr(),
1885 DECK_CD_LOW.as_ptr(),
1886 0,
1887 )
1888 .is_nan());
1889 assert!(ballistics_calculate_zero_angle_with_drag_table(
1890 &inputs,
1891 std::ptr::null(),
1892 std::ptr::null(),
1893 100.0,
1894 descending.as_ptr(),
1895 DECK_CD_LOW.as_ptr(),
1896 4,
1897 )
1898 .is_nan());
1899 assert!(ballistics_calculate_zero_angle_with_drag_table(
1901 std::ptr::null(),
1902 std::ptr::null(),
1903 std::ptr::null(),
1904 100.0,
1905 DECK_MACH.as_ptr(),
1906 DECK_CD_LOW.as_ptr(),
1907 4,
1908 )
1909 .is_nan());
1910 }
1911 }
1912
1913 #[test]
1914 fn zero_then_fly_with_same_deck_is_consistent() {
1915 let mut inputs = valid_trajectory_inputs();
1919 unsafe {
1920 let angle = ballistics_calculate_zero_angle_with_drag_table(
1921 &inputs,
1922 std::ptr::null(),
1923 std::ptr::null(),
1924 100.0,
1925 DECK_MACH.as_ptr(),
1926 DECK_CD_LOW.as_ptr(),
1927 DECK_MACH.len() as c_int,
1928 );
1929 assert!(angle.is_finite());
1930 inputs.muzzle_angle = angle;
1931 let result = ballistics_calculate_trajectory_with_drag_table(
1932 &inputs,
1933 std::ptr::null(),
1934 std::ptr::null(),
1935 150.0,
1936 1.0,
1937 DECK_MACH.as_ptr(),
1938 DECK_CD_LOW.as_ptr(),
1939 DECK_MACH.len() as c_int,
1940 );
1941 assert!(!result.is_null());
1942 let zero_distance = 100.0;
1946 let pts = std::slice::from_raw_parts((*result).points, (*result).point_count as usize);
1947 let bracket = pts
1948 .windows(2)
1949 .find(|w| w[0].position_x <= zero_distance && w[1].position_x >= zero_distance)
1950 .expect("trajectory brackets the zero distance");
1951 let (lo, hi) = (&bracket[0], &bracket[1]);
1952 let y_at_zero = if hi.position_x > lo.position_x {
1953 let t = (zero_distance - lo.position_x) / (hi.position_x - lo.position_x);
1954 lo.position_y + t * (hi.position_y - lo.position_y)
1955 } else {
1956 lo.position_y
1957 };
1958 assert!(
1959 (y_at_zero - inputs.sight_height).abs() < 0.002,
1960 "zeroed flight missed the line of sight at 100 m: y={} (sight_height={})",
1961 y_at_zero,
1962 inputs.sight_height
1963 );
1964 ballistics_free_trajectory_result(result);
1965 }
1966 }
1967
1968 #[test]
1969 fn drag_table_len_above_cap_is_rejected() {
1970 let n = (MAX_FFI_DRAG_TABLE_LEN + 1) as usize;
1973 let mach: Vec<f64> = (0..n).map(|i| 0.01 + i as f64 * 0.001).collect();
1974 let cd: Vec<f64> = vec![0.3; n];
1975 let inputs = valid_trajectory_inputs();
1976 unsafe {
1977 let r = ballistics_calculate_trajectory_with_drag_table(
1978 &inputs,
1979 std::ptr::null(),
1980 std::ptr::null(),
1981 300.0,
1982 1.0,
1983 mach.as_ptr(),
1984 cd.as_ptr(),
1985 n as c_int,
1986 );
1987 assert!(r.is_null(), "len {n} must be rejected by the cap");
1988 }
1989 }
1990
1991 #[test]
1992 fn drag_table_len_at_cap_is_accepted() {
1993 let n = MAX_FFI_DRAG_TABLE_LEN as usize;
1994 let mach: Vec<f64> = (0..n).map(|i| 0.01 + i as f64 * 0.001).collect();
1995 let cd: Vec<f64> = vec![0.3; n];
1996 let inputs = valid_trajectory_inputs();
1997 unsafe {
1998 let r = ballistics_calculate_trajectory_with_drag_table(
1999 &inputs,
2000 std::ptr::null(),
2001 std::ptr::null(),
2002 300.0,
2003 1.0,
2004 mach.as_ptr(),
2005 cd.as_ptr(),
2006 n as c_int,
2007 );
2008 assert!(!r.is_null(), "len == cap must be accepted");
2009 ballistics_free_trajectory_result(r);
2010 }
2011 }
2012
2013 #[test]
2014 fn ffi_cant_angle_deflects_laterally() {
2015 let mut level = valid_trajectory_inputs();
2016 level.muzzle_angle = 0.003;
2017 let mut canted = valid_trajectory_inputs();
2018 canted.muzzle_angle = 0.003;
2019 canted.cant_angle = 10f64.to_radians();
2020 unsafe {
2021 let a = ballistics_calculate_trajectory(&level, std::ptr::null(), std::ptr::null(), 400.0, 1.0);
2022 let b = ballistics_calculate_trajectory(&canted, std::ptr::null(), std::ptr::null(), 400.0, 1.0);
2023 assert!(!a.is_null() && !b.is_null());
2024 let za = std::slice::from_raw_parts((*a).points, (*a).point_count as usize).last().unwrap().position_z;
2025 let zb = std::slice::from_raw_parts((*b).points, (*b).point_count as usize).last().unwrap().position_z;
2026 assert!(zb > za + 0.005, "FFI cant must deflect right: level={za} canted={zb}");
2027 ballistics_free_trajectory_result(a);
2028 ballistics_free_trajectory_result(b);
2029 }
2030 }
2031
2032 #[test]
2033 fn ffi_vertical_wind_raises_trajectory() {
2034 let inputs = valid_trajectory_inputs();
2035 let no_wind = FFIWindConditions {
2036 speed: 0.0,
2037 direction: 0.0,
2038 vertical_speed: 0.0,
2039 };
2040 let updraft = FFIWindConditions {
2041 speed: 0.0,
2042 direction: 0.0,
2043 vertical_speed: 5.0,
2044 };
2045 unsafe {
2046 let a = ballistics_calculate_trajectory(&inputs, &no_wind, std::ptr::null(), 400.0, 1.0);
2047 let b = ballistics_calculate_trajectory(&inputs, &updraft, std::ptr::null(), 400.0, 1.0);
2048 assert!(!a.is_null() && !b.is_null());
2049 let ya = std::slice::from_raw_parts((*a).points, (*a).point_count as usize).last().unwrap().position_y;
2050 let yb = std::slice::from_raw_parts((*b).points, (*b).point_count as usize).last().unwrap().position_y;
2051 assert!(yb > ya + 0.01, "FFI updraft must raise the trajectory: no_wind={ya} updraft={yb}");
2052 ballistics_free_trajectory_result(a);
2053 ballistics_free_trajectory_result(b);
2054 }
2055 }
2056
2057 #[test]
2060 fn trajectory_scaled_at_one_matches_unscaled_export() {
2061 let inputs = valid_trajectory_inputs();
2062 unsafe {
2063 let unscaled = ballistics_calculate_trajectory_with_drag_table(
2064 &inputs,
2065 std::ptr::null(),
2066 std::ptr::null(),
2067 300.0,
2068 1.0,
2069 DECK_MACH.as_ptr(),
2070 DECK_CD_LOW.as_ptr(),
2071 DECK_MACH.len() as c_int,
2072 );
2073 let scaled = ballistics_calculate_trajectory_with_drag_table_scaled(
2074 &inputs,
2075 std::ptr::null(),
2076 std::ptr::null(),
2077 300.0,
2078 1.0,
2079 DECK_MACH.as_ptr(),
2080 DECK_CD_LOW.as_ptr(),
2081 DECK_MACH.len() as c_int,
2082 1.0,
2083 );
2084 assert!(!unscaled.is_null() && !scaled.is_null());
2085 assert_eq!(
2086 (*unscaled).impact_velocity.to_bits(),
2087 (*scaled).impact_velocity.to_bits(),
2088 "cd_scale=1.0 must be byte-identical to the unscaled export: unscaled={} scaled={}",
2089 (*unscaled).impact_velocity,
2090 (*scaled).impact_velocity
2091 );
2092 ballistics_free_trajectory_result(unscaled);
2093 ballistics_free_trajectory_result(scaled);
2094 }
2095 }
2096
2097 #[test]
2098 fn trajectory_scaled_at_1_10_lowers_impact_velocity() {
2099 let inputs = valid_trajectory_inputs();
2100 unsafe {
2101 let baseline = ballistics_calculate_trajectory_with_drag_table_scaled(
2102 &inputs,
2103 std::ptr::null(),
2104 std::ptr::null(),
2105 300.0,
2106 1.0,
2107 DECK_MACH.as_ptr(),
2108 DECK_CD_LOW.as_ptr(),
2109 DECK_MACH.len() as c_int,
2110 1.0,
2111 );
2112 let scaled_up = ballistics_calculate_trajectory_with_drag_table_scaled(
2113 &inputs,
2114 std::ptr::null(),
2115 std::ptr::null(),
2116 300.0,
2117 1.0,
2118 DECK_MACH.as_ptr(),
2119 DECK_CD_LOW.as_ptr(),
2120 DECK_MACH.len() as c_int,
2121 1.10,
2122 );
2123 assert!(!baseline.is_null() && !scaled_up.is_null());
2124 assert!(
2125 (*scaled_up).impact_velocity < (*baseline).impact_velocity,
2126 "cd_scale=1.10 must increase drag -> lower impact velocity: base={} scaled={}",
2127 (*baseline).impact_velocity,
2128 (*scaled_up).impact_velocity
2129 );
2130 ballistics_free_trajectory_result(baseline);
2131 ballistics_free_trajectory_result(scaled_up);
2132 }
2133 }
2134
2135 #[test]
2136 fn trajectory_scaled_rejects_invalid_cd_scale() {
2137 let inputs = valid_trajectory_inputs();
2138 unsafe {
2139 for bad in [0.0, -1.0, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
2140 let r = ballistics_calculate_trajectory_with_drag_table_scaled(
2141 &inputs,
2142 std::ptr::null(),
2143 std::ptr::null(),
2144 300.0,
2145 1.0,
2146 DECK_MACH.as_ptr(),
2147 DECK_CD_LOW.as_ptr(),
2148 DECK_MACH.len() as c_int,
2149 bad,
2150 );
2151 assert!(r.is_null(), "cd_scale={bad} must be rejected (null sentinel)");
2152 }
2153 }
2154 }
2155
2156 #[test]
2157 fn zero_angle_scaled_at_one_matches_unscaled_export() {
2158 let inputs = valid_trajectory_inputs();
2159 unsafe {
2160 let unscaled = ballistics_calculate_zero_angle_with_drag_table(
2161 &inputs,
2162 std::ptr::null(),
2163 std::ptr::null(),
2164 100.0,
2165 DECK_MACH.as_ptr(),
2166 DECK_CD_LOW.as_ptr(),
2167 DECK_MACH.len() as c_int,
2168 );
2169 let scaled = ballistics_calculate_zero_angle_with_drag_table_scaled(
2170 &inputs,
2171 std::ptr::null(),
2172 std::ptr::null(),
2173 100.0,
2174 DECK_MACH.as_ptr(),
2175 DECK_CD_LOW.as_ptr(),
2176 DECK_MACH.len() as c_int,
2177 1.0,
2178 );
2179 assert!(unscaled.is_finite() && scaled.is_finite());
2180 assert_eq!(
2181 unscaled.to_bits(),
2182 scaled.to_bits(),
2183 "cd_scale=1.0 must be byte-identical to the unscaled export: unscaled={unscaled} scaled={scaled}"
2184 );
2185 }
2186 }
2187
2188 #[test]
2189 fn zero_angle_scaled_at_1_10_differs_from_baseline() {
2190 let inputs = valid_trajectory_inputs();
2191 unsafe {
2192 let baseline = ballistics_calculate_zero_angle_with_drag_table_scaled(
2193 &inputs,
2194 std::ptr::null(),
2195 std::ptr::null(),
2196 100.0,
2197 DECK_MACH.as_ptr(),
2198 DECK_CD_LOW.as_ptr(),
2199 DECK_MACH.len() as c_int,
2200 1.0,
2201 );
2202 let scaled_up = ballistics_calculate_zero_angle_with_drag_table_scaled(
2203 &inputs,
2204 std::ptr::null(),
2205 std::ptr::null(),
2206 100.0,
2207 DECK_MACH.as_ptr(),
2208 DECK_CD_LOW.as_ptr(),
2209 DECK_MACH.len() as c_int,
2210 1.10,
2211 );
2212 assert!(baseline.is_finite() && scaled_up.is_finite());
2213 assert!(
2215 scaled_up > baseline,
2216 "cd_scale=1.10 must need a larger zero angle: base={baseline} scaled={scaled_up}"
2217 );
2218 }
2219 }
2220
2221 #[test]
2222 fn zero_angle_scaled_rejects_invalid_cd_scale() {
2223 let inputs = valid_trajectory_inputs();
2224 unsafe {
2225 for bad in [0.0, -1.0, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
2226 let angle = ballistics_calculate_zero_angle_with_drag_table_scaled(
2227 &inputs,
2228 std::ptr::null(),
2229 std::ptr::null(),
2230 100.0,
2231 DECK_MACH.as_ptr(),
2232 DECK_CD_LOW.as_ptr(),
2233 DECK_MACH.len() as c_int,
2234 bad,
2235 );
2236 assert!(angle.is_nan(), "cd_scale={bad} must be rejected (NaN sentinel)");
2237 }
2238 }
2239 }
2240
2241 #[test]
2245 fn legacy_drag_table_exports_unaffected_by_cd_scale_plumbing() {
2246 let inputs = valid_trajectory_inputs();
2247 unsafe {
2248 let a = ballistics_calculate_trajectory_with_drag_table(
2249 &inputs,
2250 std::ptr::null(),
2251 std::ptr::null(),
2252 300.0,
2253 1.0,
2254 DECK_MACH.as_ptr(),
2255 DECK_CD_LOW.as_ptr(),
2256 DECK_MACH.len() as c_int,
2257 );
2258 let b = ballistics_calculate_trajectory_with_drag_table(
2259 &inputs,
2260 std::ptr::null(),
2261 std::ptr::null(),
2262 300.0,
2263 1.0,
2264 DECK_MACH.as_ptr(),
2265 DECK_CD_LOW.as_ptr(),
2266 DECK_MACH.len() as c_int,
2267 );
2268 assert!(!a.is_null() && !b.is_null());
2269 assert_eq!((*a).impact_velocity.to_bits(), (*b).impact_velocity.to_bits());
2270 ballistics_free_trajectory_result(a);
2271 ballistics_free_trajectory_result(b);
2272
2273 let za = ballistics_calculate_zero_angle_with_drag_table(
2274 &inputs,
2275 std::ptr::null(),
2276 std::ptr::null(),
2277 100.0,
2278 DECK_MACH.as_ptr(),
2279 DECK_CD_LOW.as_ptr(),
2280 DECK_MACH.len() as c_int,
2281 );
2282 let zb = ballistics_calculate_zero_angle_with_drag_table(
2283 &inputs,
2284 std::ptr::null(),
2285 std::ptr::null(),
2286 100.0,
2287 DECK_MACH.as_ptr(),
2288 DECK_CD_LOW.as_ptr(),
2289 DECK_MACH.len() as c_int,
2290 );
2291 assert!(za.is_finite() && zb.is_finite());
2292 assert_eq!(za.to_bits(), zb.to_bits());
2293 }
2294 }
2295
2296 #[test]
2299 fn bc_for_reference_standard_icao_is_a_byte_identical_no_op() {
2300 let bc = 0.475;
2301 assert_eq!(
2302 ballistics_bc_for_reference_standard(bc, FFI_BC_REFERENCE_ICAO).to_bits(),
2303 bc.to_bits()
2304 );
2305 }
2306
2307 #[test]
2308 fn bc_for_reference_standard_unrecognized_value_falls_back_to_icao() {
2309 let bc = 0.475;
2312 assert_eq!(
2313 ballistics_bc_for_reference_standard(bc, 99).to_bits(),
2314 bc.to_bits()
2315 );
2316 }
2317
2318 #[test]
2319 fn bc_for_reference_standard_army_standard_metro_applies_the_documented_ratio() {
2320 let bc = 0.475;
2321 let converted =
2322 ballistics_bc_for_reference_standard(bc, FFI_BC_REFERENCE_ARMY_STANDARD_METRO);
2323 assert_eq!(converted, bc * crate::constants::ASM_TO_ICAO_BC);
2324 assert!(converted < bc);
2326 }
2327
2328 #[test]
2331 fn reduce_qnh_pressure_matches_the_library_function_and_lowers_pressure() {
2332 let reduced = ballistics_reduce_qnh_pressure(1030.0, 1500.0);
2333 assert_eq!(
2334 reduced,
2335 crate::atmosphere::reduce_qnh_to_station_pressure(1030.0, 1500.0)
2336 );
2337 assert!(reduced < 1030.0);
2338 }
2339
2340 #[test]
2341 fn reduce_qnh_pressure_passes_through_non_finite_inputs() {
2342 assert!(ballistics_reduce_qnh_pressure(f64::NAN, 1500.0).is_nan());
2343 assert_eq!(ballistics_reduce_qnh_pressure(1030.0, f64::INFINITY), 1030.0);
2344 }
2345
2346 #[test]
2354 fn ffi_trajectory_uses_the_reduced_pressure_not_the_raw_qnh() {
2355 let inputs = valid_trajectory_inputs();
2356 let altitude_m = 1500.0;
2357 let qnh_hpa = 1030.0;
2358 let reduced = ballistics_reduce_qnh_pressure(qnh_hpa, altitude_m);
2359 assert!(reduced < qnh_hpa);
2360
2361 let atmo_reduced = FFIAtmosphericConditions {
2362 temperature: 15.0,
2363 pressure: reduced,
2364 humidity: 50.0,
2365 altitude: altitude_m,
2366 };
2367 let atmo_raw_qnh = FFIAtmosphericConditions {
2368 temperature: 15.0,
2369 pressure: qnh_hpa,
2370 humidity: 50.0,
2371 altitude: altitude_m,
2372 };
2373
2374 unsafe {
2375 let a = ballistics_calculate_trajectory(
2376 &inputs,
2377 std::ptr::null(),
2378 &atmo_reduced,
2379 400.0,
2380 1.0,
2381 );
2382 let b = ballistics_calculate_trajectory(
2383 &inputs,
2384 std::ptr::null(),
2385 &atmo_raw_qnh,
2386 400.0,
2387 1.0,
2388 );
2389 assert!(!a.is_null() && !b.is_null());
2390 let drop_a = std::slice::from_raw_parts((*a).points, (*a).point_count as usize)
2391 .last()
2392 .unwrap()
2393 .position_y;
2394 let drop_b = std::slice::from_raw_parts((*b).points, (*b).point_count as usize)
2395 .last()
2396 .unwrap()
2397 .position_y;
2398 assert!(
2399 (drop_a - drop_b).abs() > 1e-6,
2400 "reduced vs. raw-QNH pressure must produce materially different trajectories: \
2401 {drop_a} vs {drop_b}"
2402 );
2403 ballistics_free_trajectory_result(a);
2404 ballistics_free_trajectory_result(b);
2405 }
2406 }
2407
2408 #[test]
2412 fn ffi_monte_carlo_uses_the_reduced_pressure_not_the_raw_qnh() {
2413 let inputs = valid_trajectory_inputs();
2414 let altitude_m = 1500.0;
2415 let qnh_hpa = 1030.0;
2416 let reduced = ballistics_reduce_qnh_pressure(qnh_hpa, altitude_m);
2417
2418 let atmo_reduced = FFIAtmosphericConditions {
2419 temperature: 15.0,
2420 pressure: reduced,
2421 humidity: 50.0,
2422 altitude: altitude_m,
2423 };
2424 let atmo_raw_qnh = FFIAtmosphericConditions {
2425 temperature: 15.0,
2426 pressure: qnh_hpa,
2427 humidity: 50.0,
2428 altitude: altitude_m,
2429 };
2430 let params = FFIMonteCarloParams {
2431 num_simulations: 200,
2432 velocity_std_dev: 1.0,
2433 angle_std_dev: 0.0,
2434 bc_std_dev: 0.0,
2435 wind_speed_std_dev: 0.0,
2436 target_distance: f64::NAN,
2437 base_wind_speed: 0.0,
2438 base_wind_direction: 0.0,
2439 azimuth_std_dev: 0.0,
2440 };
2441
2442 unsafe {
2443 let a = ballistics_monte_carlo(&inputs, &atmo_reduced, ¶ms);
2444 let b = ballistics_monte_carlo(&inputs, &atmo_raw_qnh, ¶ms);
2445 assert!(!a.is_null() && !b.is_null());
2446 assert!(
2447 ((*a).mean_range - (*b).mean_range).abs() > 0.5,
2448 "reduced vs. raw-QNH pressure must change MC mean range materially: \
2449 {} vs {}",
2450 (*a).mean_range,
2451 (*b).mean_range
2452 );
2453 ballistics_free_monte_carlo_results(a);
2454 ballistics_free_monte_carlo_results(b);
2455 }
2456 }
2457
2458 #[test]
2461 fn density_altitude_ffi_exports_match_the_library_function() {
2462 let da_m = 1000.0 * 0.3048;
2463 let expected = crate::atmosphere::resolve_atmosphere_for_density_altitude(da_m, None);
2464 assert_eq!(
2465 ballistics_density_altitude_altitude_m(da_m, FFI_NO_EXPLICIT_TEMPERATURE),
2466 expected.0
2467 );
2468 assert_eq!(
2469 ballistics_density_altitude_temperature_c(da_m, FFI_NO_EXPLICIT_TEMPERATURE),
2470 expected.1
2471 );
2472 assert_eq!(
2473 ballistics_density_altitude_pressure_hpa(da_m, FFI_NO_EXPLICIT_TEMPERATURE),
2474 expected.2
2475 );
2476
2477 assert!((ballistics_density_altitude_altitude_m(da_m, FFI_NO_EXPLICIT_TEMPERATURE) - da_m).abs() < 1e-6);
2479 }
2480
2481 #[test]
2482 fn density_altitude_ffi_explicit_temperature_is_honored_exactly() {
2483 let da_m = 500.0;
2484 let explicit_temp_c = 30.0;
2485 let expected =
2486 crate::atmosphere::resolve_atmosphere_for_density_altitude(da_m, Some(explicit_temp_c));
2487 assert_eq!(
2488 ballistics_density_altitude_temperature_c(da_m, explicit_temp_c),
2489 explicit_temp_c
2490 );
2491 assert_eq!(
2492 ballistics_density_altitude_temperature_c(da_m, explicit_temp_c),
2493 expected.1
2494 );
2495 assert_eq!(
2496 ballistics_density_altitude_pressure_hpa(da_m, explicit_temp_c),
2497 expected.2
2498 );
2499 assert_eq!(
2500 ballistics_density_altitude_altitude_m(da_m, explicit_temp_c),
2501 expected.0
2502 );
2503 }
2504
2505 #[test]
2506 fn density_altitude_ffi_non_finite_input_returns_nan() {
2507 assert!(
2508 ballistics_density_altitude_temperature_c(f64::INFINITY, FFI_NO_EXPLICIT_TEMPERATURE)
2509 .is_nan()
2510 );
2511 assert!(
2512 ballistics_density_altitude_pressure_hpa(f64::NAN, FFI_NO_EXPLICIT_TEMPERATURE).is_nan()
2513 );
2514 assert!(
2515 ballistics_density_altitude_altitude_m(f64::NEG_INFINITY, FFI_NO_EXPLICIT_TEMPERATURE)
2516 .is_nan()
2517 );
2518 }
2519
2520 #[test]
2525 fn ffi_trajectory_uses_the_density_altitude_derived_station_values() {
2526 let inputs = valid_trajectory_inputs();
2527 let da_m = 3000.0 * 0.3048; let altitude_m =
2529 ballistics_density_altitude_altitude_m(da_m, FFI_NO_EXPLICIT_TEMPERATURE);
2530 let temperature_c =
2531 ballistics_density_altitude_temperature_c(da_m, FFI_NO_EXPLICIT_TEMPERATURE);
2532 let pressure_hpa =
2533 ballistics_density_altitude_pressure_hpa(da_m, FFI_NO_EXPLICIT_TEMPERATURE);
2534
2535 let atmo_da = FFIAtmosphericConditions {
2536 temperature: temperature_c,
2537 pressure: pressure_hpa,
2538 humidity: 50.0,
2539 altitude: altitude_m,
2540 };
2541 let atmo_sea_level = FFIAtmosphericConditions {
2542 temperature: 15.0,
2543 pressure: 1013.25,
2544 humidity: 50.0,
2545 altitude: 0.0,
2546 };
2547
2548 unsafe {
2549 let a = ballistics_calculate_trajectory(
2550 &inputs,
2551 std::ptr::null(),
2552 &atmo_da,
2553 400.0,
2554 1.0,
2555 );
2556 let b = ballistics_calculate_trajectory(
2557 &inputs,
2558 std::ptr::null(),
2559 &atmo_sea_level,
2560 400.0,
2561 1.0,
2562 );
2563 assert!(!a.is_null() && !b.is_null());
2564 let drop_a = std::slice::from_raw_parts((*a).points, (*a).point_count as usize)
2565 .last()
2566 .unwrap()
2567 .position_y;
2568 let drop_b = std::slice::from_raw_parts((*b).points, (*b).point_count as usize)
2569 .last()
2570 .unwrap()
2571 .position_y;
2572 assert!(
2573 (drop_a - drop_b).abs() > 1e-6,
2574 "density-altitude-derived vs sea-level atmosphere must produce materially \
2575 different trajectories: {drop_a} vs {drop_b}"
2576 );
2577 ballistics_free_trajectory_result(a);
2578 ballistics_free_trajectory_result(b);
2579 }
2580 }
2581}