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)]
151pub struct FFITrajectoryPoint {
152 pub time: c_double,
153 pub position_x: c_double,
155 pub position_y: c_double,
158 pub position_z: c_double,
161 pub velocity_magnitude: c_double,
162 pub kinetic_energy: c_double,
163}
164
165#[repr(C)]
166pub struct FFITrajectoryResult {
167 pub max_range: c_double,
168 pub max_height: c_double,
169 pub time_of_flight: c_double,
170 pub impact_velocity: c_double,
171 pub impact_energy: c_double,
172 pub points: *mut FFITrajectoryPoint,
173 pub point_count: c_int,
174 pub sampled_points: *mut FFITrajectorySample,
175 pub sampled_point_count: c_int,
176 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, }
183
184#[repr(C)]
186pub struct FFIMonteCarloParams {
187 pub num_simulations: c_int,
188 pub velocity_std_dev: c_double,
189 pub angle_std_dev: c_double,
190 pub bc_std_dev: c_double,
191 pub wind_speed_std_dev: c_double,
192 pub target_distance: c_double, pub base_wind_speed: c_double, pub base_wind_direction: c_double, pub azimuth_std_dev: c_double, }
197
198#[repr(C)]
235pub struct FFIMonteCarloResults {
236 pub ranges: *mut c_double,
237 pub impact_velocities: *mut c_double,
238 pub impact_positions_x: *mut c_double,
241 pub impact_positions_y: *mut c_double,
246 pub impact_positions_z: *mut c_double,
249 pub num_results: c_int,
250 pub mean_range: c_double,
251 pub std_dev_range: c_double,
252 pub mean_impact_velocity: c_double,
253 pub std_dev_impact_velocity: c_double,
254 pub hit_probability: c_double, }
256
257#[allow(clippy::field_reassign_with_default)] fn convert_inputs(inputs: &FFIBallisticInputs) -> BallisticInputs {
260 let mut ballistic_inputs = BallisticInputs::default();
261
262 ballistic_inputs.muzzle_velocity = inputs.muzzle_velocity;
263 ballistic_inputs.muzzle_angle = inputs.muzzle_angle;
264 ballistic_inputs.azimuth_angle = inputs.azimuth_angle;
265 ballistic_inputs.shot_azimuth = inputs.shot_azimuth;
266 ballistic_inputs.cant_angle = inputs.cant_angle;
267 ballistic_inputs.zero_poi_vertical_m = inputs.zero_poi_vertical;
268 ballistic_inputs.zero_poi_horizontal_m = inputs.zero_poi_horizontal;
269 ballistic_inputs.sight_offset_lateral_m = inputs.sight_offset_lateral;
270 ballistic_inputs.use_rk4 = inputs.use_rk4 != 0;
271 ballistic_inputs.use_adaptive_rk45 = inputs.use_adaptive_rk45 != 0;
272 ballistic_inputs.bc_value = inputs.bc_value;
273 ballistic_inputs.bullet_mass = inputs.bullet_mass;
274 ballistic_inputs.bullet_diameter = inputs.bullet_diameter;
275 ballistic_inputs.bc_type = match inputs.bc_type {
276 1 => DragModel::G7,
277 2 => DragModel::G2,
278 3 => DragModel::G5,
279 4 => DragModel::G6,
280 5 => DragModel::G8,
281 6 => DragModel::GI,
282 7 => DragModel::GS,
283 8 => DragModel::RA4,
286 _ => DragModel::G1,
287 };
288 ballistic_inputs.sight_height = inputs.sight_height;
289 ballistic_inputs.target_distance = inputs.target_distance;
290 ballistic_inputs.temperature = inputs.temperature;
291 ballistic_inputs.twist_rate = inputs.twist_rate;
292 ballistic_inputs.is_twist_right = inputs.is_twist_right != 0;
293 ballistic_inputs.shooting_angle = inputs.shooting_angle;
294 ballistic_inputs.altitude = inputs.altitude;
295
296 if !inputs.latitude.is_nan() {
297 ballistic_inputs.latitude = Some(inputs.latitude);
298 }
299
300 ballistic_inputs.caliber_inches = inputs.bullet_diameter / 0.0254;
302 ballistic_inputs.weight_grains = inputs.bullet_mass / crate::constants::GRAINS_TO_KG;
303 ballistic_inputs.bullet_length = {
306 let est = crate::stability::estimate_bullet_length_m(
307 ballistic_inputs.bullet_diameter,
308 ballistic_inputs.bullet_mass,
309 );
310 if est > 0.0 {
311 est
312 } else {
313 ballistic_inputs.bullet_diameter * 4.5
314 }
315 };
316
317 ballistic_inputs.enable_wind_shear = inputs.enable_wind_shear != 0;
319 if ballistic_inputs.enable_wind_shear {
328 ballistic_inputs.wind_shear_model = "power_law".to_string();
329 }
330 ballistic_inputs.enable_trajectory_sampling = inputs.enable_trajectory_sampling != 0;
331 ballistic_inputs.sample_interval = inputs.sample_interval;
332 ballistic_inputs.enable_pitch_damping = inputs.enable_pitch_damping != 0;
333 ballistic_inputs.enable_precession_nutation = inputs.enable_precession_nutation != 0;
334 ballistic_inputs.use_enhanced_spin_drift = inputs.enable_spin_drift != 0;
335 ballistic_inputs.enable_advanced_effects =
336 inputs.enable_magnus != 0 || inputs.enable_coriolis != 0;
337 ballistic_inputs.enable_magnus = inputs.enable_magnus != 0;
339 ballistic_inputs.enable_coriolis = inputs.enable_coriolis != 0;
340
341 ballistic_inputs
342}
343
344unsafe fn drag_table_from_raw(
358 mach: *const c_double,
359 cd: *const c_double,
360 len: c_int,
361) -> Result<crate::drag::DragTable, ()> {
362 if mach.is_null() || cd.is_null() || !(2..=MAX_FFI_DRAG_TABLE_LEN).contains(&len) {
363 return Err(());
364 }
365 let len = len as usize;
366 let mach = unsafe { std::slice::from_raw_parts(mach, len) }.to_vec();
367 let cd = unsafe { std::slice::from_raw_parts(cd, len) }.to_vec();
368 crate::drag::DragTable::try_new(mach, cd).map_err(|_| ())
369}
370
371unsafe fn calculate_trajectory_impl(
377 inputs: *const FFIBallisticInputs,
378 wind: *const FFIWindConditions,
379 atmosphere: *const FFIAtmosphericConditions,
380 max_range: c_double,
381 step_size: c_double,
382 custom_drag_table: Option<crate::drag::DragTable>,
383 cd_scale: c_double,
384) -> *mut FFITrajectoryResult {
385 if inputs.is_null() {
386 return ptr::null_mut();
387 }
388 if !step_size.is_finite() || step_size < MIN_FFI_STEP_SIZE_MS {
389 return ptr::null_mut();
390 }
391
392 let inputs = unsafe { &*inputs };
393 let mut ballistic_inputs = convert_inputs(inputs);
394 ballistic_inputs.custom_drag_table = custom_drag_table;
395 ballistic_inputs.cd_scale = cd_scale;
396 let twist_rate_in = ballistic_inputs.twist_rate;
397
398 let wind_conditions = if wind.is_null() {
399 WindConditions::default()
400 } else {
401 let wind = unsafe { &*wind };
402 WindConditions {
403 speed: wind.speed,
404 direction: wind.direction,
405 vertical_speed: wind.vertical_speed,
406 }
407 };
408
409 let atmospheric_conditions = if atmosphere.is_null() {
410 AtmosphericConditions::default()
411 } else {
412 let atmo = unsafe { &*atmosphere };
413 AtmosphericConditions {
414 temperature: atmo.temperature,
415 pressure: atmo.pressure,
416 humidity: atmo.humidity,
417 altitude: atmo.altitude,
418 }
419 };
420
421 let (sample_temp_c, sample_pressure_hpa) = crate::atmosphere::resolve_station_conditions(
423 atmospheric_conditions.temperature,
424 atmospheric_conditions.pressure,
425 atmospheric_conditions.altitude,
426 );
427 let (_, sample_speed_of_sound) = crate::atmosphere::calculate_atmosphere(
428 atmospheric_conditions.altitude,
429 Some(sample_temp_c),
430 Some(sample_pressure_hpa),
431 atmospheric_conditions.humidity,
432 );
433
434 let mut solver =
435 TrajectorySolver::new(ballistic_inputs, wind_conditions, atmospheric_conditions);
436
437 solver.set_max_range(max_range);
439 solver.set_time_step(step_size / 1000.0); match solver.solve() {
442 Ok(result) => {
443 let point_count = result.points.len();
445 let points = if point_count > 0 {
446 let mut ffi_points = Vec::with_capacity(point_count);
447 for point in result.points.iter() {
448 ffi_points.push(FFITrajectoryPoint {
449 time: point.time,
450 position_x: point.position[0],
451 position_y: point.position[1],
452 position_z: point.position[2],
453 velocity_magnitude: point.velocity_magnitude,
454 kinetic_energy: point.kinetic_energy,
455 });
456 }
457 let points_ptr = ffi_points.as_mut_ptr();
458 std::mem::forget(ffi_points); points_ptr
460 } else {
461 ptr::null_mut()
462 };
463
464 let (sampled_points, sampled_point_count) =
466 if let Some(ref samples) = result.sampled_points {
467 let mut ffi_samples = Vec::with_capacity(samples.len());
468 for sample in samples {
469 ffi_samples.push(FFITrajectorySample {
470 distance: sample.distance_m,
471 time: sample.time_s,
472 velocity_mps: sample.velocity_mps,
473 energy_joules: sample.energy_j,
474 drop_meters: sample.drop_m,
475 windage_meters: sample.wind_drift_m,
476 mach: if sample_speed_of_sound > 0.0 {
477 sample.velocity_mps / sample_speed_of_sound
478 } else {
479 0.0
480 },
481 spin_rate_rps: if twist_rate_in > 0.0 {
482 sample.velocity_mps / (twist_rate_in * 0.0254)
483 } else {
484 0.0
485 },
486 });
487 }
488 let count = ffi_samples.len() as c_int;
489 let samples_ptr = ffi_samples.as_mut_ptr();
490 std::mem::forget(ffi_samples);
491 (samples_ptr, count)
492 } else {
493 (ptr::null_mut(), 0)
494 };
495
496 let (final_pitch, final_yaw, max_yaw, max_prec) =
498 if let Some(ref angular) = result.angular_state {
499 (
500 angular.pitch_angle,
501 angular.yaw_angle,
502 result.max_yaw_angle.unwrap_or(f64::NAN),
503 result.max_precession_angle.unwrap_or(f64::NAN),
504 )
505 } else {
506 (f64::NAN, f64::NAN, f64::NAN, f64::NAN)
507 };
508
509 let ffi_result = Box::new(FFITrajectoryResult {
511 max_range: result.max_range,
512 max_height: result.max_height,
513 time_of_flight: result.time_of_flight,
514 impact_velocity: result.impact_velocity,
515 impact_energy: result.impact_energy,
516 points,
517 point_count: point_count as c_int,
518 sampled_points,
519 sampled_point_count,
520 min_pitch_damping: result.min_pitch_damping.unwrap_or(f64::NAN),
521 transonic_mach: result.transonic_mach.unwrap_or(f64::NAN),
522 final_pitch_angle: final_pitch,
523 final_yaw_angle: final_yaw,
524 max_yaw_angle: max_yaw,
525 max_precession_angle: max_prec,
526 });
527
528 Box::into_raw(ffi_result)
529 }
530 Err(_) => ptr::null_mut(),
531 }
532}
533
534#[no_mangle]
553pub unsafe extern "C" fn ballistics_calculate_trajectory(
554 inputs: *const FFIBallisticInputs,
555 wind: *const FFIWindConditions,
556 atmosphere: *const FFIAtmosphericConditions,
557 max_range: c_double,
558 step_size: c_double,
559) -> *mut FFITrajectoryResult {
560 unsafe { calculate_trajectory_impl(inputs, wind, atmosphere, max_range, step_size, None, 1.0) }
561}
562
563#[no_mangle]
584pub unsafe extern "C" fn ballistics_calculate_trajectory_with_drag_table(
585 inputs: *const FFIBallisticInputs,
586 wind: *const FFIWindConditions,
587 atmosphere: *const FFIAtmosphericConditions,
588 max_range: c_double,
589 step_size: c_double,
590 drag_mach: *const c_double,
591 drag_cd: *const c_double,
592 drag_table_len: c_int,
593) -> *mut FFITrajectoryResult {
594 let table = match unsafe { drag_table_from_raw(drag_mach, drag_cd, drag_table_len) } {
595 Ok(t) => t,
596 Err(()) => return ptr::null_mut(),
597 };
598 unsafe {
599 calculate_trajectory_impl(
600 inputs, wind, atmosphere, max_range, step_size, Some(table), 1.0,
601 )
602 }
603}
604
605#[no_mangle]
622pub unsafe extern "C" fn ballistics_calculate_trajectory_with_drag_table_scaled(
623 inputs: *const FFIBallisticInputs,
624 wind: *const FFIWindConditions,
625 atmosphere: *const FFIAtmosphericConditions,
626 max_range: c_double,
627 step_size: c_double,
628 drag_mach: *const c_double,
629 drag_cd: *const c_double,
630 drag_table_len: c_int,
631 cd_scale: c_double,
632) -> *mut FFITrajectoryResult {
633 if !cd_scale.is_finite() || cd_scale <= 0.0 {
634 return ptr::null_mut();
635 }
636 let table = match unsafe { drag_table_from_raw(drag_mach, drag_cd, drag_table_len) } {
637 Ok(t) => t,
638 Err(()) => return ptr::null_mut(),
639 };
640 unsafe {
641 calculate_trajectory_impl(
642 inputs, wind, atmosphere, max_range, step_size, Some(table), cd_scale,
643 )
644 }
645}
646
647#[no_mangle]
656pub unsafe extern "C" fn ballistics_free_trajectory_result(result: *mut FFITrajectoryResult) {
657 if !result.is_null() {
658 unsafe {
659 let result = Box::from_raw(result);
660 if !result.points.is_null() && result.point_count > 0 {
661 let points = Vec::from_raw_parts(
662 result.points,
663 result.point_count as usize,
664 result.point_count as usize,
665 );
666 drop(points);
667 }
668 if !result.sampled_points.is_null() && result.sampled_point_count > 0 {
669 let samples = Vec::from_raw_parts(
670 result.sampled_points,
671 result.sampled_point_count as usize,
672 result.sampled_point_count as usize,
673 );
674 drop(samples);
675 }
676 drop(result);
677 }
678 }
679}
680
681unsafe fn calculate_zero_angle_impl(
688 inputs: *const FFIBallisticInputs,
689 wind: *const FFIWindConditions,
690 atmosphere: *const FFIAtmosphericConditions,
691 zero_distance: c_double,
692 custom_drag_table: Option<crate::drag::DragTable>,
693 cd_scale: c_double,
694) -> c_double {
695 if inputs.is_null() {
696 return f64::NAN;
697 }
698
699 let inputs = unsafe { &*inputs };
700 let mut ballistic_inputs = convert_inputs(inputs);
701 ballistic_inputs.custom_drag_table = custom_drag_table;
702 ballistic_inputs.cd_scale = cd_scale;
703
704 let wind_conditions = if wind.is_null() {
705 WindConditions::default()
706 } else {
707 let wind = unsafe { &*wind };
708 WindConditions {
709 speed: wind.speed,
710 direction: wind.direction,
711 vertical_speed: wind.vertical_speed,
712 }
713 };
714
715 let atmospheric_conditions = if atmosphere.is_null() {
716 AtmosphericConditions::default()
717 } else {
718 let atmo = unsafe { &*atmosphere };
719 AtmosphericConditions {
720 temperature: atmo.temperature,
721 pressure: atmo.pressure,
722 humidity: atmo.humidity,
723 altitude: atmo.altitude,
724 }
725 };
726
727 let target_height = ballistic_inputs.sight_height;
730
731 calculate_zero_angle_with_conditions(
732 ballistic_inputs,
733 zero_distance,
734 target_height,
735 wind_conditions,
736 atmospheric_conditions,
737 )
738 .unwrap_or(f64::NAN)
739}
740
741#[no_mangle]
751pub unsafe extern "C" fn ballistics_calculate_zero_angle(
752 inputs: *const FFIBallisticInputs,
753 wind: *const FFIWindConditions,
754 atmosphere: *const FFIAtmosphericConditions,
755 zero_distance: c_double,
756) -> c_double {
757 unsafe { calculate_zero_angle_impl(inputs, wind, atmosphere, zero_distance, None, 1.0) }
758}
759
760#[no_mangle]
782pub unsafe extern "C" fn ballistics_calculate_zero_angle_with_drag_table(
783 inputs: *const FFIBallisticInputs,
784 wind: *const FFIWindConditions,
785 atmosphere: *const FFIAtmosphericConditions,
786 zero_distance: c_double,
787 drag_mach: *const c_double,
788 drag_cd: *const c_double,
789 drag_table_len: c_int,
790) -> c_double {
791 let table = match unsafe { drag_table_from_raw(drag_mach, drag_cd, drag_table_len) } {
792 Ok(t) => t,
793 Err(()) => return f64::NAN,
794 };
795 unsafe {
796 calculate_zero_angle_impl(inputs, wind, atmosphere, zero_distance, Some(table), 1.0)
797 }
798}
799
800#[no_mangle]
818pub unsafe extern "C" fn ballistics_calculate_zero_angle_with_drag_table_scaled(
819 inputs: *const FFIBallisticInputs,
820 wind: *const FFIWindConditions,
821 atmosphere: *const FFIAtmosphericConditions,
822 zero_distance: c_double,
823 drag_mach: *const c_double,
824 drag_cd: *const c_double,
825 drag_table_len: c_int,
826 cd_scale: c_double,
827) -> c_double {
828 if !cd_scale.is_finite() || cd_scale <= 0.0 {
829 return f64::NAN;
830 }
831 let table = match unsafe { drag_table_from_raw(drag_mach, drag_cd, drag_table_len) } {
832 Ok(t) => t,
833 Err(()) => return f64::NAN,
834 };
835 unsafe {
836 calculate_zero_angle_impl(
837 inputs,
838 wind,
839 atmosphere,
840 zero_distance,
841 Some(table),
842 cd_scale,
843 )
844 }
845}
846
847#[no_mangle]
849#[allow(clippy::field_reassign_with_default)] pub extern "C" fn ballistics_quick_trajectory(
851 muzzle_velocity: c_double,
852 bc: c_double,
853 sight_height: c_double,
854 zero_distance: c_double,
855 target_distance: c_double,
856) -> c_double {
857 let mut inputs = BallisticInputs::default();
861 inputs.muzzle_velocity = muzzle_velocity;
862 inputs.bc_value = bc;
863 inputs.sight_height = sight_height;
864 inputs.target_distance = target_distance;
865
866 let wind = WindConditions::default();
867 let atmo = AtmosphericConditions::default();
868
869 let zero_angle = match calculate_zero_angle_with_conditions(
871 inputs.clone(),
872 zero_distance,
873 sight_height,
874 wind.clone(),
875 atmo.clone(),
876 ) {
877 Ok(angle) => angle,
878 Err(_) => return f64::NAN,
879 };
880
881 inputs.muzzle_angle = zero_angle;
883
884 let mut solver = TrajectorySolver::new(inputs, wind, atmo);
885 solver.set_max_range(target_distance * 1.1);
886
887 match solver.solve() {
888 Ok(result) => {
889 for point in result.points {
891 if point.position[0] >= target_distance {
892 return sight_height - point.position[1];
893 }
894 }
895 f64::NAN
896 }
897 Err(_) => f64::NAN,
898 }
899}
900
901#[no_mangle]
913pub unsafe extern "C" fn ballistics_monte_carlo(
914 inputs: *const FFIBallisticInputs,
915 atmosphere: *const FFIAtmosphericConditions,
916 params: *const FFIMonteCarloParams,
917) -> *mut FFIMonteCarloResults {
918 unsafe { ballistics_monte_carlo_impl(inputs, atmosphere, params, 0.0) }
919}
920
921#[no_mangle]
931pub unsafe extern "C" fn ballistics_monte_carlo_with_direction_std_dev(
932 inputs: *const FFIBallisticInputs,
933 atmosphere: *const FFIAtmosphericConditions,
934 params: *const FFIMonteCarloParams,
935 wind_direction_std_dev: c_double,
936) -> *mut FFIMonteCarloResults {
937 unsafe { ballistics_monte_carlo_impl(inputs, atmosphere, params, wind_direction_std_dev) }
938}
939
940unsafe fn ballistics_monte_carlo_impl(
941 inputs: *const FFIBallisticInputs,
942 atmosphere: *const FFIAtmosphericConditions,
943 params: *const FFIMonteCarloParams,
944 wind_direction_std_dev: f64,
945) -> *mut FFIMonteCarloResults {
946 if inputs.is_null() || params.is_null() {
947 return ptr::null_mut();
948 }
949
950 let inputs = unsafe { &*inputs };
951 let params = unsafe { &*params };
952
953 const MAX_SIMULATIONS: c_int = 1_000_000;
959 if params.num_simulations <= 0 || params.num_simulations > MAX_SIMULATIONS {
960 return ptr::null_mut();
961 }
962
963 let mut ballistic_inputs = convert_inputs(inputs);
965 ballistic_inputs.muzzle_height = 1.5;
966 ballistic_inputs.ground_threshold = 0.0;
967 if !atmosphere.is_null() {
968 let atmo = unsafe { &*atmosphere };
969 ballistic_inputs.temperature = atmo.temperature;
970 ballistic_inputs.pressure = atmo.pressure;
971 ballistic_inputs.humidity = (atmo.humidity / 100.0).clamp(0.0, 1.0);
972 ballistic_inputs.altitude = atmo.altitude;
973 }
974
975 let mc_params = MonteCarloParams {
977 num_simulations: params.num_simulations as usize,
978 velocity_std_dev: params.velocity_std_dev,
979 angle_std_dev: params.angle_std_dev,
980 bc_std_dev: params.bc_std_dev,
981 wind_speed_std_dev: params.wind_speed_std_dev,
982 target_distance: if params.target_distance.is_nan() {
983 None
984 } else {
985 Some(params.target_distance)
986 },
987 base_wind_speed: params.base_wind_speed,
988 base_wind_direction: params.base_wind_direction,
989 azimuth_std_dev: params.azimuth_std_dev,
990 };
991
992 match run_monte_carlo_with_direction_std_dev(
994 ballistic_inputs,
995 mc_params,
996 wind_direction_std_dev,
997 ) {
998 Ok(results) => {
999 let num_results = results.ranges.len() as c_int;
1000
1001 let mean_range: f64 = results.ranges.iter().sum::<f64>() / num_results as f64;
1003 let variance_range: f64 = results
1004 .ranges
1005 .iter()
1006 .map(|r| (r - mean_range).powi(2))
1007 .sum::<f64>()
1008 / num_results as f64;
1009 let std_dev_range = variance_range.sqrt();
1010
1011 let mean_velocity: f64 =
1012 results.impact_velocities.iter().sum::<f64>() / num_results as f64;
1013 let variance_velocity: f64 = results
1014 .impact_velocities
1015 .iter()
1016 .map(|v| (v - mean_velocity).powi(2))
1017 .sum::<f64>()
1018 / num_results as f64;
1019 let std_dev_velocity = variance_velocity.sqrt();
1020
1021 let hit_probability = if params.target_distance.is_nan() {
1027 0.0
1028 } else {
1029 results.hit_probability(crate::DEFAULT_HIT_RADIUS_M)
1030 };
1031
1032 let ranges_ptr = unsafe {
1034 let ptr = std::alloc::alloc(
1035 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
1036 ) as *mut c_double;
1037 for (i, &range) in results.ranges.iter().enumerate() {
1038 *ptr.add(i) = range;
1039 }
1040 ptr
1041 };
1042
1043 let velocities_ptr = unsafe {
1044 let ptr = std::alloc::alloc(
1045 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
1046 ) as *mut c_double;
1047 for (i, &vel) in results.impact_velocities.iter().enumerate() {
1048 *ptr.add(i) = vel;
1049 }
1050 ptr
1051 };
1052
1053 let pos_x_ptr = unsafe {
1054 let ptr = std::alloc::alloc(
1055 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
1056 ) as *mut c_double;
1057 for (i, pos) in results.impact_positions.iter().enumerate() {
1058 *ptr.add(i) = pos.x;
1059 }
1060 ptr
1061 };
1062
1063 let pos_y_ptr = unsafe {
1064 let ptr = std::alloc::alloc(
1065 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
1066 ) as *mut c_double;
1067 for (i, pos) in results.impact_positions.iter().enumerate() {
1068 *ptr.add(i) = pos.y;
1069 }
1070 ptr
1071 };
1072
1073 let pos_z_ptr = unsafe {
1074 let ptr = std::alloc::alloc(
1075 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
1076 ) as *mut c_double;
1077 for (i, pos) in results.impact_positions.iter().enumerate() {
1078 *ptr.add(i) = pos.z;
1079 }
1080 ptr
1081 };
1082
1083 let result = Box::new(FFIMonteCarloResults {
1085 ranges: ranges_ptr,
1086 impact_velocities: velocities_ptr,
1087 impact_positions_x: pos_x_ptr,
1088 impact_positions_y: pos_y_ptr,
1089 impact_positions_z: pos_z_ptr,
1090 num_results,
1091 mean_range,
1092 std_dev_range,
1093 mean_impact_velocity: mean_velocity,
1094 std_dev_impact_velocity: std_dev_velocity,
1095 hit_probability,
1096 });
1097
1098 Box::into_raw(result)
1099 }
1100 Err(_) => ptr::null_mut(),
1101 }
1102}
1103
1104#[no_mangle]
1113pub unsafe extern "C" fn ballistics_free_monte_carlo_results(results: *mut FFIMonteCarloResults) {
1114 if results.is_null() {
1115 return;
1116 }
1117
1118 unsafe {
1119 let results = Box::from_raw(results);
1120 let num = results.num_results as usize;
1121
1122 if !results.ranges.is_null() {
1124 std::alloc::dealloc(
1125 results.ranges as *mut u8,
1126 std::alloc::Layout::array::<c_double>(num).unwrap(),
1127 );
1128 }
1129
1130 if !results.impact_velocities.is_null() {
1131 std::alloc::dealloc(
1132 results.impact_velocities as *mut u8,
1133 std::alloc::Layout::array::<c_double>(num).unwrap(),
1134 );
1135 }
1136
1137 if !results.impact_positions_x.is_null() {
1138 std::alloc::dealloc(
1139 results.impact_positions_x as *mut u8,
1140 std::alloc::Layout::array::<c_double>(num).unwrap(),
1141 );
1142 }
1143
1144 if !results.impact_positions_y.is_null() {
1145 std::alloc::dealloc(
1146 results.impact_positions_y as *mut u8,
1147 std::alloc::Layout::array::<c_double>(num).unwrap(),
1148 );
1149 }
1150
1151 if !results.impact_positions_z.is_null() {
1152 std::alloc::dealloc(
1153 results.impact_positions_z as *mut u8,
1154 std::alloc::Layout::array::<c_double>(num).unwrap(),
1155 );
1156 }
1157
1158 }
1160}
1161
1162pub const FFI_BC_REFERENCE_ICAO: c_int = 0;
1168pub const FFI_BC_REFERENCE_ARMY_STANDARD_METRO: c_int = 1;
1169
1170#[no_mangle]
1189pub extern "C" fn ballistics_bc_for_reference_standard(
1190 bc: c_double,
1191 reference_standard: c_int,
1192) -> c_double {
1193 if reference_standard == FFI_BC_REFERENCE_ARMY_STANDARD_METRO {
1194 bc * crate::constants::ASM_TO_ICAO_BC
1195 } else {
1196 bc
1197 }
1198}
1199
1200#[no_mangle]
1218pub extern "C" fn ballistics_reduce_qnh_pressure(
1219 qnh_hpa: c_double,
1220 altitude_m: c_double,
1221) -> c_double {
1222 if !qnh_hpa.is_finite() || !altitude_m.is_finite() {
1223 return qnh_hpa;
1224 }
1225 crate::atmosphere::reduce_qnh_to_station_pressure(qnh_hpa, altitude_m)
1226}
1227
1228pub const FFI_NO_EXPLICIT_TEMPERATURE: c_double = f64::NAN;
1233
1234#[no_mangle]
1254pub extern "C" fn ballistics_density_altitude_temperature_c(
1255 density_altitude_m: c_double,
1256 explicit_temperature_c: c_double,
1257) -> c_double {
1258 if !density_altitude_m.is_finite() {
1259 return f64::NAN;
1260 }
1261 let explicit = (!explicit_temperature_c.is_nan()).then_some(explicit_temperature_c);
1262 crate::atmosphere::resolve_atmosphere_for_density_altitude(density_altitude_m, explicit).1
1263}
1264
1265#[no_mangle]
1268pub extern "C" fn ballistics_density_altitude_pressure_hpa(
1269 density_altitude_m: c_double,
1270 explicit_temperature_c: c_double,
1271) -> c_double {
1272 if !density_altitude_m.is_finite() {
1273 return f64::NAN;
1274 }
1275 let explicit = (!explicit_temperature_c.is_nan()).then_some(explicit_temperature_c);
1276 crate::atmosphere::resolve_atmosphere_for_density_altitude(density_altitude_m, explicit).2
1277}
1278
1279#[no_mangle]
1284pub extern "C" fn ballistics_density_altitude_altitude_m(
1285 density_altitude_m: c_double,
1286 explicit_temperature_c: c_double,
1287) -> c_double {
1288 if !density_altitude_m.is_finite() {
1289 return f64::NAN;
1290 }
1291 let explicit = (!explicit_temperature_c.is_nan()).then_some(explicit_temperature_c);
1292 crate::atmosphere::resolve_atmosphere_for_density_altitude(density_altitude_m, explicit).0
1293}
1294
1295pub const MAX_FFI_RETICLE_MARKS: c_int = crate::reticle::MAX_RETICLE_MARKS as c_int;
1303
1304pub const FFI_RETICLE_FIRST_FOCAL_PLANE: c_int = 0;
1307pub const FFI_RETICLE_SECOND_FOCAL_PLANE: c_int = 1;
1310
1311pub const FFI_RETICLE_OK: c_int = 0;
1313pub const FFI_RETICLE_ERR_INVALID_ARGUMENT: c_int = -1;
1315pub const FFI_RETICLE_ERR_MAGNIFICATION: c_int = -2;
1317pub const FFI_RETICLE_ERR_REFERENCE_MAGNIFICATION: c_int = -3;
1319pub const FFI_RETICLE_ERR_NON_FINITE: c_int = -4;
1321
1322#[repr(C)]
1329pub struct FFIReticleHold {
1330 pub down_mil: c_double,
1332 pub right_mil: c_double,
1334 pub nearest_mark: c_int,
1337 pub nearest_mark_distance_mil: c_double,
1340 pub off_reticle: c_int,
1343 pub mark_scale: c_double,
1346}
1347
1348#[no_mangle]
1367#[allow(unknown_lints, clippy::chunks_exact_to_as_chunks)]
1374pub unsafe extern "C" fn ballistics_hold_point_in_reticle(
1375 drop_mil: c_double,
1376 wind_mil: c_double,
1377 magnification: c_double,
1378 marks: *const c_double,
1379 marks_len: c_int,
1380 focal_plane: c_int,
1381 reference_magnification: c_double,
1382 out: *mut FFIReticleHold,
1383) -> c_int {
1384 use crate::reticle::{
1385 hold_point_in_reticle, FocalPlane, MarkKind, ReticleDescription, ReticleError, ReticleMark,
1386 };
1387
1388 if marks.is_null() || out.is_null() || !(1..=MAX_FFI_RETICLE_MARKS).contains(&marks_len) {
1389 return FFI_RETICLE_ERR_INVALID_ARGUMENT;
1390 }
1391 let count = marks_len as usize;
1392 let flat = unsafe { std::slice::from_raw_parts(marks, count * 2) };
1394
1395 let description = ReticleDescription {
1396 name: String::new(),
1397 focal_plane: if focal_plane == FFI_RETICLE_SECOND_FOCAL_PLANE {
1398 FocalPlane::Second
1399 } else {
1400 FocalPlane::First
1401 },
1402 reference_magnification,
1403 marks: flat
1404 .chunks_exact(2)
1405 .map(|pair| ReticleMark::new(pair[0], pair[1], MarkKind::Dot))
1406 .collect(),
1407 };
1408
1409 let hold = match hold_point_in_reticle(drop_mil, wind_mil, magnification, &description) {
1410 Ok(hold) => hold,
1411 Err(ReticleError::NonPositiveMagnification { .. }) => return FFI_RETICLE_ERR_MAGNIFICATION,
1412 Err(ReticleError::NonPositiveReferenceMagnification { .. }) => {
1413 return FFI_RETICLE_ERR_REFERENCE_MAGNIFICATION
1414 }
1415 Err(ReticleError::NonFiniteMark { .. }) | Err(ReticleError::NonFiniteHold { .. }) => {
1416 return FFI_RETICLE_ERR_NON_FINITE
1417 }
1418 Err(_) => return FFI_RETICLE_ERR_INVALID_ARGUMENT,
1419 };
1420
1421 unsafe {
1422 *out = FFIReticleHold {
1423 down_mil: hold.down_mil,
1424 right_mil: hold.right_mil,
1425 nearest_mark: hold.nearest_mark.map_or(-1, |index| index as c_int),
1426 nearest_mark_distance_mil: hold.nearest_mark_distance_mil,
1427 off_reticle: c_int::from(hold.off_reticle),
1428 mark_scale: hold.mark_scale,
1429 };
1430 }
1431 FFI_RETICLE_OK
1432}
1433
1434#[no_mangle]
1436pub extern "C" fn ballistics_get_version() -> *const c_char {
1437 concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr() as *const c_char
1441}
1442
1443#[cfg(test)]
1444mod tests {
1445 use super::*;
1446
1447 fn valid_trajectory_inputs() -> FFIBallisticInputs {
1448 FFIBallisticInputs {
1449 muzzle_velocity: 800.0,
1450 muzzle_angle: 0.0,
1451 bc_value: 0.5,
1452 bullet_mass: 0.01,
1453 bullet_diameter: 0.00762,
1454 bc_type: 0,
1455 sight_height: 0.05,
1456 target_distance: 1.0,
1457 temperature: 15.0,
1458 twist_rate: 12.0,
1459 is_twist_right: 1,
1460 shooting_angle: 0.0,
1461 altitude: 0.0,
1462 latitude: f64::NAN,
1463 azimuth_angle: 0.0,
1464 use_rk4: 1,
1465 use_adaptive_rk45: 0,
1466 enable_wind_shear: 0,
1467 enable_trajectory_sampling: 0,
1468 sample_interval: 10.0,
1469 enable_pitch_damping: 0,
1470 enable_precession_nutation: 0,
1471 enable_spin_drift: 0,
1472 enable_magnus: 0,
1473 enable_coriolis: 0,
1474 shot_azimuth: 0.0,
1475 cant_angle: 0.0,
1476 zero_poi_vertical: 0.0,
1477 zero_poi_horizontal: 0.0,
1478 sight_offset_lateral: 0.0,
1479 }
1480 }
1481
1482 #[allow(dead_code)]
1483 #[repr(C)]
1484 struct LegacyFFIMonteCarloParams {
1485 num_simulations: c_int,
1486 velocity_std_dev: c_double,
1487 angle_std_dev: c_double,
1488 bc_std_dev: c_double,
1489 wind_speed_std_dev: c_double,
1490 target_distance: c_double,
1491 base_wind_speed: c_double,
1492 base_wind_direction: c_double,
1493 azimuth_std_dev: c_double,
1494 }
1495
1496 #[test]
1497 fn monte_carlo_params_legacy_abi_size_is_unchanged() {
1498 assert_eq!(
1499 std::mem::size_of::<FFIMonteCarloParams>(),
1500 std::mem::size_of::<LegacyFFIMonteCarloParams>()
1501 );
1502 assert_eq!(
1503 std::mem::align_of::<FFIMonteCarloParams>(),
1504 std::mem::align_of::<LegacyFFIMonteCarloParams>()
1505 );
1506 }
1507
1508 #[test]
1511 fn reticle_addition_does_not_disturb_existing_layouts() {
1512 assert_eq!(
1513 std::mem::size_of::<FFIMonteCarloParams>(),
1514 std::mem::size_of::<LegacyFFIMonteCarloParams>()
1515 );
1516 assert_eq!(std::mem::align_of::<FFIReticleHold>(), 8);
1518 }
1519
1520 #[test]
1531 fn asking_for_shear_over_the_c_abi_names_a_profile_the_fast_path_keeps() {
1532 let mut inputs = valid_trajectory_inputs();
1533 inputs.enable_wind_shear = 1;
1534
1535 let converted = convert_inputs(&inputs);
1536
1537 assert!(converted.enable_wind_shear);
1538 assert_ne!(
1539 crate::wind_shear::boundary_layer_model_from_name(&converted.wind_shear_model),
1540 crate::wind_shear::WindShearModel::None,
1541 "the fast-integrate path would drop this model and leave the flag inert"
1542 );
1543 assert_eq!(converted.wind_shear_model, "power_law");
1547 }
1548
1549 #[test]
1557 fn shear_moves_the_monte_carlo_result_it_used_to_leave_untouched() {
1558 let mut lofted = valid_trajectory_inputs();
1559 lofted.muzzle_angle = 0.15; let mut sheared = valid_trajectory_inputs();
1564 sheared.muzzle_angle = lofted.muzzle_angle;
1565 sheared.enable_wind_shear = 1;
1566
1567 let atmo = FFIAtmosphericConditions {
1568 temperature: 15.0,
1569 pressure: 1013.25,
1570 humidity: 50.0,
1571 altitude: 0.0,
1572 };
1573 let params = FFIMonteCarloParams {
1574 num_simulations: 4,
1575 velocity_std_dev: 0.0,
1576 angle_std_dev: 0.0,
1577 bc_std_dev: 0.0,
1578 wind_speed_std_dev: 0.0,
1579 target_distance: 2500.0,
1580 base_wind_speed: 10.0,
1581 base_wind_direction: std::f64::consts::FRAC_PI_2,
1583 azimuth_std_dev: 0.0,
1584 };
1585
1586 unsafe {
1587 let off = ballistics_monte_carlo(&lofted, &atmo, ¶ms);
1588 let on = ballistics_monte_carlo(&sheared, &atmo, ¶ms);
1589 assert!(!off.is_null() && !on.is_null());
1590 assert!((*off).num_results > 0 && (*on).num_results > 0);
1591
1592 let range_off = (*off).mean_range;
1596 let range_on = (*on).mean_range;
1597
1598 assert!(
1599 (range_off - range_on).abs() > 1.0e-2,
1600 "the shear flag must change the Monte Carlo result; it was inert until \
1601 convert_inputs named a profile: off={range_off} on={range_on}"
1602 );
1603
1604 ballistics_free_monte_carlo_results(off);
1605 ballistics_free_monte_carlo_results(on);
1606 }
1607 }
1608
1609 #[test]
1611 fn leaving_shear_off_over_the_c_abi_names_no_profile() {
1612 let mut inputs = valid_trajectory_inputs();
1613 inputs.enable_wind_shear = 0;
1614
1615 let converted = convert_inputs(&inputs);
1616
1617 assert!(!converted.enable_wind_shear);
1618 assert_eq!(
1619 crate::wind_shear::boundary_layer_model_from_name(&converted.wind_shear_model),
1620 crate::wind_shear::WindShearModel::None
1621 );
1622 }
1623
1624 fn zeroed_hold() -> FFIReticleHold {
1625 FFIReticleHold {
1626 down_mil: 0.0,
1627 right_mil: 0.0,
1628 nearest_mark: -99,
1629 nearest_mark_distance_mil: -1.0,
1630 off_reticle: -1,
1631 mark_scale: -1.0,
1632 }
1633 }
1634
1635 #[test]
1636 fn ffi_hold_point_matches_the_rust_api_on_both_focal_planes() {
1637 let marks: [c_double; 8] = [0.0, 0.0, 2.0, 0.0, 4.0, 0.0, 2.0, 1.0];
1639 let mut out = zeroed_hold();
1640
1641 let code = unsafe {
1643 ballistics_hold_point_in_reticle(
1644 4.0,
1645 0.0,
1646 6.0,
1647 marks.as_ptr(),
1648 4,
1649 FFI_RETICLE_FIRST_FOCAL_PLANE,
1650 0.0,
1651 &mut out,
1652 )
1653 };
1654 assert_eq!(code, FFI_RETICLE_OK);
1655 assert_eq!(out.down_mil, 4.0);
1656 assert_eq!(out.nearest_mark, 2);
1657 assert_eq!(out.nearest_mark_distance_mil, 0.0);
1658 assert_eq!(out.mark_scale, 1.0);
1659 assert_eq!(out.off_reticle, 0);
1660
1661 let mut out = zeroed_hold();
1663 let code = unsafe {
1664 ballistics_hold_point_in_reticle(
1665 4.0,
1666 0.0,
1667 5.0,
1668 marks.as_ptr(),
1669 4,
1670 FFI_RETICLE_SECOND_FOCAL_PLANE,
1671 10.0,
1672 &mut out,
1673 )
1674 };
1675 assert_eq!(code, FFI_RETICLE_OK);
1676 assert_eq!(out.nearest_mark, 1);
1677 assert_eq!(out.nearest_mark_distance_mil, 0.0);
1678 assert_eq!(out.mark_scale, 2.0);
1679 }
1680
1681 #[test]
1684 fn ffi_hold_point_bounds_check_marks_len_before_reading() {
1685 let marks: [c_double; 4] = [0.0, 0.0, 2.0, 0.0];
1686 let mut out = zeroed_hold();
1687 let call = |len: c_int, ptr: *const c_double, out: &mut FFIReticleHold| unsafe {
1688 ballistics_hold_point_in_reticle(
1689 1.0,
1690 0.0,
1691 10.0,
1692 ptr,
1693 len,
1694 FFI_RETICLE_FIRST_FOCAL_PLANE,
1695 0.0,
1696 out,
1697 )
1698 };
1699
1700 assert_eq!(call(0, marks.as_ptr(), &mut out), FFI_RETICLE_ERR_INVALID_ARGUMENT);
1701 assert_eq!(call(-1, marks.as_ptr(), &mut out), FFI_RETICLE_ERR_INVALID_ARGUMENT);
1702 assert_eq!(
1703 call(MAX_FFI_RETICLE_MARKS + 1, marks.as_ptr(), &mut out),
1704 FFI_RETICLE_ERR_INVALID_ARGUMENT
1705 );
1706 assert_eq!(call(c_int::MAX, marks.as_ptr(), &mut out), FFI_RETICLE_ERR_INVALID_ARGUMENT);
1707 assert_eq!(call(2, std::ptr::null(), &mut out), FFI_RETICLE_ERR_INVALID_ARGUMENT);
1708 assert_eq!(out.nearest_mark, -99);
1710
1711 assert_eq!(
1713 unsafe {
1714 ballistics_hold_point_in_reticle(
1715 1.0,
1716 0.0,
1717 10.0,
1718 marks.as_ptr(),
1719 2,
1720 FFI_RETICLE_FIRST_FOCAL_PLANE,
1721 0.0,
1722 std::ptr::null_mut(),
1723 )
1724 },
1725 FFI_RETICLE_ERR_INVALID_ARGUMENT
1726 );
1727 }
1728
1729 #[test]
1730 fn ffi_hold_point_maps_each_error_class_to_its_own_code() {
1731 let marks: [c_double; 4] = [0.0, 0.0, 2.0, 0.0];
1732 let bad_marks: [c_double; 4] = [0.0, 0.0, f64::NAN, 0.0];
1733 let mut out = zeroed_hold();
1734 let call = |drop: c_double, mag: c_double, plane: c_int, ref_mag: c_double,
1735 m: &[c_double], out: &mut FFIReticleHold| unsafe {
1736 ballistics_hold_point_in_reticle(
1737 drop,
1738 0.0,
1739 mag,
1740 m.as_ptr(),
1741 (m.len() / 2) as c_int,
1742 plane,
1743 ref_mag,
1744 out,
1745 )
1746 };
1747
1748 assert_eq!(
1749 call(1.0, 0.0, FFI_RETICLE_FIRST_FOCAL_PLANE, 0.0, &marks, &mut out),
1750 FFI_RETICLE_ERR_MAGNIFICATION
1751 );
1752 assert_eq!(
1753 call(1.0, 10.0, FFI_RETICLE_SECOND_FOCAL_PLANE, 0.0, &marks, &mut out),
1754 FFI_RETICLE_ERR_REFERENCE_MAGNIFICATION
1755 );
1756 assert_eq!(
1757 call(f64::NAN, 10.0, FFI_RETICLE_FIRST_FOCAL_PLANE, 0.0, &marks, &mut out),
1758 FFI_RETICLE_ERR_NON_FINITE
1759 );
1760 assert_eq!(
1761 call(1.0, 10.0, FFI_RETICLE_FIRST_FOCAL_PLANE, 0.0, &bad_marks, &mut out),
1762 FFI_RETICLE_ERR_NON_FINITE
1763 );
1764 assert_eq!(out.nearest_mark, -99, "out stays untouched on every error");
1765 }
1766
1767 #[test]
1772 fn bc_type_8_maps_to_ra4() {
1773 let mut inputs = valid_trajectory_inputs();
1774 inputs.bc_type = 8;
1775 assert_eq!(convert_inputs(&inputs).bc_type, DragModel::RA4);
1776
1777 let expected = [
1779 (0, DragModel::G1),
1780 (1, DragModel::G7),
1781 (2, DragModel::G2),
1782 (3, DragModel::G5),
1783 (4, DragModel::G6),
1784 (5, DragModel::G8),
1785 (6, DragModel::GI),
1786 (7, DragModel::GS),
1787 ];
1788 for (code, model) in expected {
1789 let mut inputs = valid_trajectory_inputs();
1790 inputs.bc_type = code;
1791 assert_eq!(convert_inputs(&inputs).bc_type, model, "code {code}");
1792 }
1793
1794 for code in [9, 42, -1] {
1796 let mut inputs = valid_trajectory_inputs();
1797 inputs.bc_type = code;
1798 assert_eq!(convert_inputs(&inputs).bc_type, DragModel::G1, "code {code}");
1799 }
1800 }
1801
1802 #[test]
1803 fn null_pointer_contracts_return_sentinels_and_free_safely() {
1804 unsafe {
1805 assert!(ballistics_calculate_trajectory(
1806 std::ptr::null(),
1807 std::ptr::null(),
1808 std::ptr::null(),
1809 1_000.0,
1810 1.0,
1811 )
1812 .is_null());
1813 assert!(ballistics_calculate_zero_angle(
1814 std::ptr::null(),
1815 std::ptr::null(),
1816 std::ptr::null(),
1817 100.0,
1818 )
1819 .is_nan());
1820 assert!(ballistics_calculate_trajectory_with_drag_table(
1821 std::ptr::null(),
1822 std::ptr::null(),
1823 std::ptr::null(),
1824 1_000.0,
1825 1.0,
1826 DECK_MACH.as_ptr(),
1827 DECK_CD_LOW.as_ptr(),
1828 DECK_MACH.len() as c_int,
1829 )
1830 .is_null());
1831 assert!(ballistics_calculate_zero_angle_with_drag_table(
1832 std::ptr::null(),
1833 std::ptr::null(),
1834 std::ptr::null(),
1835 100.0,
1836 DECK_MACH.as_ptr(),
1837 DECK_CD_LOW.as_ptr(),
1838 DECK_MACH.len() as c_int,
1839 )
1840 .is_nan());
1841 assert!(
1842 ballistics_monte_carlo(std::ptr::null(), std::ptr::null(), std::ptr::null(),)
1843 .is_null()
1844 );
1845 assert!(ballistics_monte_carlo_with_direction_std_dev(
1846 std::ptr::null(),
1847 std::ptr::null(),
1848 std::ptr::null(),
1849 0.1,
1850 )
1851 .is_null());
1852
1853 ballistics_free_trajectory_result(std::ptr::null_mut());
1854 ballistics_free_monte_carlo_results(std::ptr::null_mut());
1855 }
1856 }
1857
1858 #[test]
1859 fn mba1283_ffi_enforces_step_floor_for_every_solver_mode() {
1860 for (mode, use_rk4, use_adaptive_rk45) in [("Euler", 0, 0), ("RK4", 1, 0), ("RK45", 1, 1)] {
1861 for step_size in [
1862 f64::NAN,
1863 f64::INFINITY,
1864 f64::NEG_INFINITY,
1865 -1.0,
1866 -0.0,
1867 0.0,
1868 0.001,
1869 MIN_FFI_STEP_SIZE_MS - 0.001,
1870 ] {
1871 let mut inputs = valid_trajectory_inputs();
1872 inputs.use_rk4 = use_rk4;
1873 inputs.use_adaptive_rk45 = use_adaptive_rk45;
1874 let result = unsafe {
1875 ballistics_calculate_trajectory(
1876 &inputs,
1877 std::ptr::null(),
1878 std::ptr::null(),
1879 0.01,
1880 step_size,
1881 )
1882 };
1883 assert!(
1884 result.is_null(),
1885 "{mode} step_size={step_size:?} bypassed the FFI floor"
1886 );
1887 }
1888
1889 let mut inputs = valid_trajectory_inputs();
1890 inputs.use_rk4 = use_rk4;
1891 inputs.use_adaptive_rk45 = use_adaptive_rk45;
1892 let result = unsafe {
1893 ballistics_calculate_trajectory(
1894 &inputs,
1895 std::ptr::null(),
1896 std::ptr::null(),
1897 0.01,
1898 MIN_FFI_STEP_SIZE_MS,
1899 )
1900 };
1901 assert!(
1902 !result.is_null(),
1903 "the documented minimum step must remain usable in {mode}"
1904 );
1905 unsafe {
1906 assert!((*result).point_count >= 0);
1907 assert!((*result).point_count as usize <= crate::MAX_TRAJECTORY_POINTS);
1908 ballistics_free_trajectory_result(result);
1909 }
1910 }
1911 }
1912
1913 const DECK_MACH: [f64; 4] = [0.5, 1.0, 2.0, 3.0];
1915 const DECK_CD_LOW: [f64; 4] = [0.05, 0.08, 0.06, 0.05];
1917
1918 #[test]
1919 fn trajectory_with_drag_table_applies_the_deck() {
1920 let inputs = valid_trajectory_inputs();
1921 unsafe {
1922 let plain = ballistics_calculate_trajectory(
1923 &inputs,
1924 std::ptr::null(),
1925 std::ptr::null(),
1926 300.0,
1927 1.0,
1928 );
1929 let decked = ballistics_calculate_trajectory_with_drag_table(
1930 &inputs,
1931 std::ptr::null(),
1932 std::ptr::null(),
1933 300.0,
1934 1.0,
1935 DECK_MACH.as_ptr(),
1936 DECK_CD_LOW.as_ptr(),
1937 DECK_MACH.len() as c_int,
1938 );
1939 assert!(!plain.is_null() && !decked.is_null());
1940 assert!(
1942 (*decked).impact_velocity > (*plain).impact_velocity + 1.0,
1943 "deck did not change the solve: plain={} decked={}",
1944 (*plain).impact_velocity,
1945 (*decked).impact_velocity
1946 );
1947 ballistics_free_trajectory_result(plain);
1948 ballistics_free_trajectory_result(decked);
1949 }
1950 }
1951
1952 #[test]
1953 fn trajectory_with_drag_table_rejects_invalid_decks() {
1954 let inputs = valid_trajectory_inputs();
1955 let descending = [3.0, 2.0, 1.0, 0.5];
1956 let negative_cd = [0.05, -0.08, 0.06, 0.05];
1957 unsafe {
1958 assert!(ballistics_calculate_trajectory_with_drag_table(
1960 &inputs,
1961 std::ptr::null(),
1962 std::ptr::null(),
1963 300.0,
1964 1.0,
1965 std::ptr::null(),
1966 DECK_CD_LOW.as_ptr(),
1967 4,
1968 )
1969 .is_null());
1970 assert!(ballistics_calculate_trajectory_with_drag_table(
1971 &inputs,
1972 std::ptr::null(),
1973 std::ptr::null(),
1974 300.0,
1975 1.0,
1976 DECK_MACH.as_ptr(),
1977 std::ptr::null(),
1978 4,
1979 )
1980 .is_null());
1981 assert!(ballistics_calculate_trajectory_with_drag_table(
1983 &inputs,
1984 std::ptr::null(),
1985 std::ptr::null(),
1986 300.0,
1987 1.0,
1988 DECK_MACH.as_ptr(),
1989 DECK_CD_LOW.as_ptr(),
1990 1,
1991 )
1992 .is_null());
1993 assert!(ballistics_calculate_trajectory_with_drag_table(
1995 &inputs,
1996 std::ptr::null(),
1997 std::ptr::null(),
1998 300.0,
1999 1.0,
2000 descending.as_ptr(),
2001 DECK_CD_LOW.as_ptr(),
2002 4,
2003 )
2004 .is_null());
2005 assert!(ballistics_calculate_trajectory_with_drag_table(
2007 &inputs,
2008 std::ptr::null(),
2009 std::ptr::null(),
2010 300.0,
2011 1.0,
2012 DECK_MACH.as_ptr(),
2013 negative_cd.as_ptr(),
2014 4,
2015 )
2016 .is_null());
2017 assert!(ballistics_calculate_trajectory_with_drag_table(
2019 std::ptr::null(),
2020 std::ptr::null(),
2021 std::ptr::null(),
2022 300.0,
2023 1.0,
2024 DECK_MACH.as_ptr(),
2025 DECK_CD_LOW.as_ptr(),
2026 4,
2027 )
2028 .is_null());
2029 }
2030 }
2031
2032 #[test]
2033 fn zero_angle_with_drag_table_applies_the_deck() {
2034 let inputs = valid_trajectory_inputs();
2036 unsafe {
2037 let plain =
2038 ballistics_calculate_zero_angle(&inputs, std::ptr::null(), std::ptr::null(), 100.0);
2039 let decked = ballistics_calculate_zero_angle_with_drag_table(
2040 &inputs,
2041 std::ptr::null(),
2042 std::ptr::null(),
2043 100.0,
2044 DECK_MACH.as_ptr(),
2045 DECK_CD_LOW.as_ptr(),
2046 DECK_MACH.len() as c_int,
2047 );
2048 assert!(plain.is_finite() && decked.is_finite());
2049 assert!(
2052 (plain - decked).abs() > 1e-6,
2053 "deck did not change the zero: plain={plain} decked={decked}"
2054 );
2055 }
2056 }
2057
2058 #[test]
2059 fn zero_angle_with_drag_table_rejects_invalid_decks() {
2060 let inputs = valid_trajectory_inputs();
2061 let descending = [3.0, 2.0, 1.0, 0.5];
2062 unsafe {
2063 assert!(ballistics_calculate_zero_angle_with_drag_table(
2064 &inputs,
2065 std::ptr::null(),
2066 std::ptr::null(),
2067 100.0,
2068 std::ptr::null(),
2069 DECK_CD_LOW.as_ptr(),
2070 4,
2071 )
2072 .is_nan());
2073 assert!(ballistics_calculate_zero_angle_with_drag_table(
2074 &inputs,
2075 std::ptr::null(),
2076 std::ptr::null(),
2077 100.0,
2078 DECK_MACH.as_ptr(),
2079 DECK_CD_LOW.as_ptr(),
2080 0,
2081 )
2082 .is_nan());
2083 assert!(ballistics_calculate_zero_angle_with_drag_table(
2084 &inputs,
2085 std::ptr::null(),
2086 std::ptr::null(),
2087 100.0,
2088 descending.as_ptr(),
2089 DECK_CD_LOW.as_ptr(),
2090 4,
2091 )
2092 .is_nan());
2093 assert!(ballistics_calculate_zero_angle_with_drag_table(
2095 std::ptr::null(),
2096 std::ptr::null(),
2097 std::ptr::null(),
2098 100.0,
2099 DECK_MACH.as_ptr(),
2100 DECK_CD_LOW.as_ptr(),
2101 4,
2102 )
2103 .is_nan());
2104 }
2105 }
2106
2107 #[test]
2108 fn zero_then_fly_with_same_deck_is_consistent() {
2109 let mut inputs = valid_trajectory_inputs();
2113 unsafe {
2114 let angle = ballistics_calculate_zero_angle_with_drag_table(
2115 &inputs,
2116 std::ptr::null(),
2117 std::ptr::null(),
2118 100.0,
2119 DECK_MACH.as_ptr(),
2120 DECK_CD_LOW.as_ptr(),
2121 DECK_MACH.len() as c_int,
2122 );
2123 assert!(angle.is_finite());
2124 inputs.muzzle_angle = angle;
2125 let result = ballistics_calculate_trajectory_with_drag_table(
2126 &inputs,
2127 std::ptr::null(),
2128 std::ptr::null(),
2129 150.0,
2130 1.0,
2131 DECK_MACH.as_ptr(),
2132 DECK_CD_LOW.as_ptr(),
2133 DECK_MACH.len() as c_int,
2134 );
2135 assert!(!result.is_null());
2136 let zero_distance = 100.0;
2140 let pts = std::slice::from_raw_parts((*result).points, (*result).point_count as usize);
2141 let bracket = pts
2142 .windows(2)
2143 .find(|w| w[0].position_x <= zero_distance && w[1].position_x >= zero_distance)
2144 .expect("trajectory brackets the zero distance");
2145 let (lo, hi) = (&bracket[0], &bracket[1]);
2146 let y_at_zero = if hi.position_x > lo.position_x {
2147 let t = (zero_distance - lo.position_x) / (hi.position_x - lo.position_x);
2148 lo.position_y + t * (hi.position_y - lo.position_y)
2149 } else {
2150 lo.position_y
2151 };
2152 assert!(
2153 (y_at_zero - inputs.sight_height).abs() < 0.002,
2154 "zeroed flight missed the line of sight at 100 m: y={} (sight_height={})",
2155 y_at_zero,
2156 inputs.sight_height
2157 );
2158 ballistics_free_trajectory_result(result);
2159 }
2160 }
2161
2162 #[test]
2163 fn drag_table_len_above_cap_is_rejected() {
2164 let n = (MAX_FFI_DRAG_TABLE_LEN + 1) as usize;
2167 let mach: Vec<f64> = (0..n).map(|i| 0.01 + i as f64 * 0.001).collect();
2168 let cd: Vec<f64> = vec![0.3; n];
2169 let inputs = valid_trajectory_inputs();
2170 unsafe {
2171 let r = ballistics_calculate_trajectory_with_drag_table(
2172 &inputs,
2173 std::ptr::null(),
2174 std::ptr::null(),
2175 300.0,
2176 1.0,
2177 mach.as_ptr(),
2178 cd.as_ptr(),
2179 n as c_int,
2180 );
2181 assert!(r.is_null(), "len {n} must be rejected by the cap");
2182 }
2183 }
2184
2185 #[test]
2186 fn drag_table_len_at_cap_is_accepted() {
2187 let n = MAX_FFI_DRAG_TABLE_LEN as usize;
2188 let mach: Vec<f64> = (0..n).map(|i| 0.01 + i as f64 * 0.001).collect();
2189 let cd: Vec<f64> = vec![0.3; n];
2190 let inputs = valid_trajectory_inputs();
2191 unsafe {
2192 let r = ballistics_calculate_trajectory_with_drag_table(
2193 &inputs,
2194 std::ptr::null(),
2195 std::ptr::null(),
2196 300.0,
2197 1.0,
2198 mach.as_ptr(),
2199 cd.as_ptr(),
2200 n as c_int,
2201 );
2202 assert!(!r.is_null(), "len == cap must be accepted");
2203 ballistics_free_trajectory_result(r);
2204 }
2205 }
2206
2207 #[test]
2208 fn ffi_cant_angle_deflects_laterally() {
2209 let mut level = valid_trajectory_inputs();
2210 level.muzzle_angle = 0.003;
2211 let mut canted = valid_trajectory_inputs();
2212 canted.muzzle_angle = 0.003;
2213 canted.cant_angle = 10f64.to_radians();
2214 unsafe {
2215 let a = ballistics_calculate_trajectory(&level, std::ptr::null(), std::ptr::null(), 400.0, 1.0);
2216 let b = ballistics_calculate_trajectory(&canted, std::ptr::null(), std::ptr::null(), 400.0, 1.0);
2217 assert!(!a.is_null() && !b.is_null());
2218 let za = std::slice::from_raw_parts((*a).points, (*a).point_count as usize).last().unwrap().position_z;
2219 let zb = std::slice::from_raw_parts((*b).points, (*b).point_count as usize).last().unwrap().position_z;
2220 assert!(zb > za + 0.005, "FFI cant must deflect right: level={za} canted={zb}");
2221 ballistics_free_trajectory_result(a);
2222 ballistics_free_trajectory_result(b);
2223 }
2224 }
2225
2226 #[test]
2227 fn ffi_vertical_wind_raises_trajectory() {
2228 let inputs = valid_trajectory_inputs();
2229 let no_wind = FFIWindConditions {
2230 speed: 0.0,
2231 direction: 0.0,
2232 vertical_speed: 0.0,
2233 };
2234 let updraft = FFIWindConditions {
2235 speed: 0.0,
2236 direction: 0.0,
2237 vertical_speed: 5.0,
2238 };
2239 unsafe {
2240 let a = ballistics_calculate_trajectory(&inputs, &no_wind, std::ptr::null(), 400.0, 1.0);
2241 let b = ballistics_calculate_trajectory(&inputs, &updraft, std::ptr::null(), 400.0, 1.0);
2242 assert!(!a.is_null() && !b.is_null());
2243 let ya = std::slice::from_raw_parts((*a).points, (*a).point_count as usize).last().unwrap().position_y;
2244 let yb = std::slice::from_raw_parts((*b).points, (*b).point_count as usize).last().unwrap().position_y;
2245 assert!(yb > ya + 0.01, "FFI updraft must raise the trajectory: no_wind={ya} updraft={yb}");
2246 ballistics_free_trajectory_result(a);
2247 ballistics_free_trajectory_result(b);
2248 }
2249 }
2250
2251 #[test]
2254 fn trajectory_scaled_at_one_matches_unscaled_export() {
2255 let inputs = valid_trajectory_inputs();
2256 unsafe {
2257 let unscaled = ballistics_calculate_trajectory_with_drag_table(
2258 &inputs,
2259 std::ptr::null(),
2260 std::ptr::null(),
2261 300.0,
2262 1.0,
2263 DECK_MACH.as_ptr(),
2264 DECK_CD_LOW.as_ptr(),
2265 DECK_MACH.len() as c_int,
2266 );
2267 let scaled = ballistics_calculate_trajectory_with_drag_table_scaled(
2268 &inputs,
2269 std::ptr::null(),
2270 std::ptr::null(),
2271 300.0,
2272 1.0,
2273 DECK_MACH.as_ptr(),
2274 DECK_CD_LOW.as_ptr(),
2275 DECK_MACH.len() as c_int,
2276 1.0,
2277 );
2278 assert!(!unscaled.is_null() && !scaled.is_null());
2279 assert_eq!(
2280 (*unscaled).impact_velocity.to_bits(),
2281 (*scaled).impact_velocity.to_bits(),
2282 "cd_scale=1.0 must be byte-identical to the unscaled export: unscaled={} scaled={}",
2283 (*unscaled).impact_velocity,
2284 (*scaled).impact_velocity
2285 );
2286 ballistics_free_trajectory_result(unscaled);
2287 ballistics_free_trajectory_result(scaled);
2288 }
2289 }
2290
2291 #[test]
2292 fn trajectory_scaled_at_1_10_lowers_impact_velocity() {
2293 let inputs = valid_trajectory_inputs();
2294 unsafe {
2295 let baseline = ballistics_calculate_trajectory_with_drag_table_scaled(
2296 &inputs,
2297 std::ptr::null(),
2298 std::ptr::null(),
2299 300.0,
2300 1.0,
2301 DECK_MACH.as_ptr(),
2302 DECK_CD_LOW.as_ptr(),
2303 DECK_MACH.len() as c_int,
2304 1.0,
2305 );
2306 let scaled_up = ballistics_calculate_trajectory_with_drag_table_scaled(
2307 &inputs,
2308 std::ptr::null(),
2309 std::ptr::null(),
2310 300.0,
2311 1.0,
2312 DECK_MACH.as_ptr(),
2313 DECK_CD_LOW.as_ptr(),
2314 DECK_MACH.len() as c_int,
2315 1.10,
2316 );
2317 assert!(!baseline.is_null() && !scaled_up.is_null());
2318 assert!(
2319 (*scaled_up).impact_velocity < (*baseline).impact_velocity,
2320 "cd_scale=1.10 must increase drag -> lower impact velocity: base={} scaled={}",
2321 (*baseline).impact_velocity,
2322 (*scaled_up).impact_velocity
2323 );
2324 ballistics_free_trajectory_result(baseline);
2325 ballistics_free_trajectory_result(scaled_up);
2326 }
2327 }
2328
2329 #[test]
2330 fn trajectory_scaled_rejects_invalid_cd_scale() {
2331 let inputs = valid_trajectory_inputs();
2332 unsafe {
2333 for bad in [0.0, -1.0, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
2334 let r = ballistics_calculate_trajectory_with_drag_table_scaled(
2335 &inputs,
2336 std::ptr::null(),
2337 std::ptr::null(),
2338 300.0,
2339 1.0,
2340 DECK_MACH.as_ptr(),
2341 DECK_CD_LOW.as_ptr(),
2342 DECK_MACH.len() as c_int,
2343 bad,
2344 );
2345 assert!(r.is_null(), "cd_scale={bad} must be rejected (null sentinel)");
2346 }
2347 }
2348 }
2349
2350 #[test]
2351 fn zero_angle_scaled_at_one_matches_unscaled_export() {
2352 let inputs = valid_trajectory_inputs();
2353 unsafe {
2354 let unscaled = ballistics_calculate_zero_angle_with_drag_table(
2355 &inputs,
2356 std::ptr::null(),
2357 std::ptr::null(),
2358 100.0,
2359 DECK_MACH.as_ptr(),
2360 DECK_CD_LOW.as_ptr(),
2361 DECK_MACH.len() as c_int,
2362 );
2363 let scaled = ballistics_calculate_zero_angle_with_drag_table_scaled(
2364 &inputs,
2365 std::ptr::null(),
2366 std::ptr::null(),
2367 100.0,
2368 DECK_MACH.as_ptr(),
2369 DECK_CD_LOW.as_ptr(),
2370 DECK_MACH.len() as c_int,
2371 1.0,
2372 );
2373 assert!(unscaled.is_finite() && scaled.is_finite());
2374 assert_eq!(
2375 unscaled.to_bits(),
2376 scaled.to_bits(),
2377 "cd_scale=1.0 must be byte-identical to the unscaled export: unscaled={unscaled} scaled={scaled}"
2378 );
2379 }
2380 }
2381
2382 #[test]
2383 fn zero_angle_scaled_at_1_10_differs_from_baseline() {
2384 let inputs = valid_trajectory_inputs();
2385 unsafe {
2386 let baseline = ballistics_calculate_zero_angle_with_drag_table_scaled(
2387 &inputs,
2388 std::ptr::null(),
2389 std::ptr::null(),
2390 100.0,
2391 DECK_MACH.as_ptr(),
2392 DECK_CD_LOW.as_ptr(),
2393 DECK_MACH.len() as c_int,
2394 1.0,
2395 );
2396 let scaled_up = ballistics_calculate_zero_angle_with_drag_table_scaled(
2397 &inputs,
2398 std::ptr::null(),
2399 std::ptr::null(),
2400 100.0,
2401 DECK_MACH.as_ptr(),
2402 DECK_CD_LOW.as_ptr(),
2403 DECK_MACH.len() as c_int,
2404 1.10,
2405 );
2406 assert!(baseline.is_finite() && scaled_up.is_finite());
2407 assert!(
2409 scaled_up > baseline,
2410 "cd_scale=1.10 must need a larger zero angle: base={baseline} scaled={scaled_up}"
2411 );
2412 }
2413 }
2414
2415 #[test]
2416 fn zero_angle_scaled_rejects_invalid_cd_scale() {
2417 let inputs = valid_trajectory_inputs();
2418 unsafe {
2419 for bad in [0.0, -1.0, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
2420 let angle = ballistics_calculate_zero_angle_with_drag_table_scaled(
2421 &inputs,
2422 std::ptr::null(),
2423 std::ptr::null(),
2424 100.0,
2425 DECK_MACH.as_ptr(),
2426 DECK_CD_LOW.as_ptr(),
2427 DECK_MACH.len() as c_int,
2428 bad,
2429 );
2430 assert!(angle.is_nan(), "cd_scale={bad} must be rejected (NaN sentinel)");
2431 }
2432 }
2433 }
2434
2435 #[test]
2439 fn legacy_drag_table_exports_unaffected_by_cd_scale_plumbing() {
2440 let inputs = valid_trajectory_inputs();
2441 unsafe {
2442 let a = ballistics_calculate_trajectory_with_drag_table(
2443 &inputs,
2444 std::ptr::null(),
2445 std::ptr::null(),
2446 300.0,
2447 1.0,
2448 DECK_MACH.as_ptr(),
2449 DECK_CD_LOW.as_ptr(),
2450 DECK_MACH.len() as c_int,
2451 );
2452 let b = ballistics_calculate_trajectory_with_drag_table(
2453 &inputs,
2454 std::ptr::null(),
2455 std::ptr::null(),
2456 300.0,
2457 1.0,
2458 DECK_MACH.as_ptr(),
2459 DECK_CD_LOW.as_ptr(),
2460 DECK_MACH.len() as c_int,
2461 );
2462 assert!(!a.is_null() && !b.is_null());
2463 assert_eq!((*a).impact_velocity.to_bits(), (*b).impact_velocity.to_bits());
2464 ballistics_free_trajectory_result(a);
2465 ballistics_free_trajectory_result(b);
2466
2467 let za = ballistics_calculate_zero_angle_with_drag_table(
2468 &inputs,
2469 std::ptr::null(),
2470 std::ptr::null(),
2471 100.0,
2472 DECK_MACH.as_ptr(),
2473 DECK_CD_LOW.as_ptr(),
2474 DECK_MACH.len() as c_int,
2475 );
2476 let zb = ballistics_calculate_zero_angle_with_drag_table(
2477 &inputs,
2478 std::ptr::null(),
2479 std::ptr::null(),
2480 100.0,
2481 DECK_MACH.as_ptr(),
2482 DECK_CD_LOW.as_ptr(),
2483 DECK_MACH.len() as c_int,
2484 );
2485 assert!(za.is_finite() && zb.is_finite());
2486 assert_eq!(za.to_bits(), zb.to_bits());
2487 }
2488 }
2489
2490 #[test]
2493 fn bc_for_reference_standard_icao_is_a_byte_identical_no_op() {
2494 let bc = 0.475;
2495 assert_eq!(
2496 ballistics_bc_for_reference_standard(bc, FFI_BC_REFERENCE_ICAO).to_bits(),
2497 bc.to_bits()
2498 );
2499 }
2500
2501 #[test]
2502 fn bc_for_reference_standard_unrecognized_value_falls_back_to_icao() {
2503 let bc = 0.475;
2506 assert_eq!(
2507 ballistics_bc_for_reference_standard(bc, 99).to_bits(),
2508 bc.to_bits()
2509 );
2510 }
2511
2512 #[test]
2513 fn bc_for_reference_standard_army_standard_metro_applies_the_documented_ratio() {
2514 let bc = 0.475;
2515 let converted =
2516 ballistics_bc_for_reference_standard(bc, FFI_BC_REFERENCE_ARMY_STANDARD_METRO);
2517 assert_eq!(converted, bc * crate::constants::ASM_TO_ICAO_BC);
2518 assert!(converted < bc);
2520 }
2521
2522 #[test]
2525 fn reduce_qnh_pressure_matches_the_library_function_and_lowers_pressure() {
2526 let reduced = ballistics_reduce_qnh_pressure(1030.0, 1500.0);
2527 assert_eq!(
2528 reduced,
2529 crate::atmosphere::reduce_qnh_to_station_pressure(1030.0, 1500.0)
2530 );
2531 assert!(reduced < 1030.0);
2532 }
2533
2534 #[test]
2535 fn reduce_qnh_pressure_passes_through_non_finite_inputs() {
2536 assert!(ballistics_reduce_qnh_pressure(f64::NAN, 1500.0).is_nan());
2537 assert_eq!(ballistics_reduce_qnh_pressure(1030.0, f64::INFINITY), 1030.0);
2538 }
2539
2540 #[test]
2548 fn ffi_trajectory_uses_the_reduced_pressure_not_the_raw_qnh() {
2549 let inputs = valid_trajectory_inputs();
2550 let altitude_m = 1500.0;
2551 let qnh_hpa = 1030.0;
2552 let reduced = ballistics_reduce_qnh_pressure(qnh_hpa, altitude_m);
2553 assert!(reduced < qnh_hpa);
2554
2555 let atmo_reduced = FFIAtmosphericConditions {
2556 temperature: 15.0,
2557 pressure: reduced,
2558 humidity: 50.0,
2559 altitude: altitude_m,
2560 };
2561 let atmo_raw_qnh = FFIAtmosphericConditions {
2562 temperature: 15.0,
2563 pressure: qnh_hpa,
2564 humidity: 50.0,
2565 altitude: altitude_m,
2566 };
2567
2568 unsafe {
2569 let a = ballistics_calculate_trajectory(
2570 &inputs,
2571 std::ptr::null(),
2572 &atmo_reduced,
2573 400.0,
2574 1.0,
2575 );
2576 let b = ballistics_calculate_trajectory(
2577 &inputs,
2578 std::ptr::null(),
2579 &atmo_raw_qnh,
2580 400.0,
2581 1.0,
2582 );
2583 assert!(!a.is_null() && !b.is_null());
2584 let drop_a = std::slice::from_raw_parts((*a).points, (*a).point_count as usize)
2585 .last()
2586 .unwrap()
2587 .position_y;
2588 let drop_b = std::slice::from_raw_parts((*b).points, (*b).point_count as usize)
2589 .last()
2590 .unwrap()
2591 .position_y;
2592 assert!(
2593 (drop_a - drop_b).abs() > 1e-6,
2594 "reduced vs. raw-QNH pressure must produce materially different trajectories: \
2595 {drop_a} vs {drop_b}"
2596 );
2597 ballistics_free_trajectory_result(a);
2598 ballistics_free_trajectory_result(b);
2599 }
2600 }
2601
2602 #[test]
2606 fn ffi_monte_carlo_uses_the_reduced_pressure_not_the_raw_qnh() {
2607 let inputs = valid_trajectory_inputs();
2608 let altitude_m = 1500.0;
2609 let qnh_hpa = 1030.0;
2610 let reduced = ballistics_reduce_qnh_pressure(qnh_hpa, altitude_m);
2611
2612 let atmo_reduced = FFIAtmosphericConditions {
2613 temperature: 15.0,
2614 pressure: reduced,
2615 humidity: 50.0,
2616 altitude: altitude_m,
2617 };
2618 let atmo_raw_qnh = FFIAtmosphericConditions {
2619 temperature: 15.0,
2620 pressure: qnh_hpa,
2621 humidity: 50.0,
2622 altitude: altitude_m,
2623 };
2624 let params = FFIMonteCarloParams {
2625 num_simulations: 200,
2626 velocity_std_dev: 1.0,
2627 angle_std_dev: 0.0,
2628 bc_std_dev: 0.0,
2629 wind_speed_std_dev: 0.0,
2630 target_distance: f64::NAN,
2631 base_wind_speed: 0.0,
2632 base_wind_direction: 0.0,
2633 azimuth_std_dev: 0.0,
2634 };
2635
2636 unsafe {
2637 let a = ballistics_monte_carlo(&inputs, &atmo_reduced, ¶ms);
2638 let b = ballistics_monte_carlo(&inputs, &atmo_raw_qnh, ¶ms);
2639 assert!(!a.is_null() && !b.is_null());
2640 assert!(
2641 ((*a).mean_range - (*b).mean_range).abs() > 0.5,
2642 "reduced vs. raw-QNH pressure must change MC mean range materially: \
2643 {} vs {}",
2644 (*a).mean_range,
2645 (*b).mean_range
2646 );
2647 ballistics_free_monte_carlo_results(a);
2648 ballistics_free_monte_carlo_results(b);
2649 }
2650 }
2651
2652 #[test]
2655 fn density_altitude_ffi_exports_match_the_library_function() {
2656 let da_m = 1000.0 * 0.3048;
2657 let expected = crate::atmosphere::resolve_atmosphere_for_density_altitude(da_m, None);
2658 assert_eq!(
2659 ballistics_density_altitude_altitude_m(da_m, FFI_NO_EXPLICIT_TEMPERATURE),
2660 expected.0
2661 );
2662 assert_eq!(
2663 ballistics_density_altitude_temperature_c(da_m, FFI_NO_EXPLICIT_TEMPERATURE),
2664 expected.1
2665 );
2666 assert_eq!(
2667 ballistics_density_altitude_pressure_hpa(da_m, FFI_NO_EXPLICIT_TEMPERATURE),
2668 expected.2
2669 );
2670
2671 assert!((ballistics_density_altitude_altitude_m(da_m, FFI_NO_EXPLICIT_TEMPERATURE) - da_m).abs() < 1e-6);
2673 }
2674
2675 #[test]
2676 fn density_altitude_ffi_explicit_temperature_is_honored_exactly() {
2677 let da_m = 500.0;
2678 let explicit_temp_c = 30.0;
2679 let expected =
2680 crate::atmosphere::resolve_atmosphere_for_density_altitude(da_m, Some(explicit_temp_c));
2681 assert_eq!(
2682 ballistics_density_altitude_temperature_c(da_m, explicit_temp_c),
2683 explicit_temp_c
2684 );
2685 assert_eq!(
2686 ballistics_density_altitude_temperature_c(da_m, explicit_temp_c),
2687 expected.1
2688 );
2689 assert_eq!(
2690 ballistics_density_altitude_pressure_hpa(da_m, explicit_temp_c),
2691 expected.2
2692 );
2693 assert_eq!(
2694 ballistics_density_altitude_altitude_m(da_m, explicit_temp_c),
2695 expected.0
2696 );
2697 }
2698
2699 #[test]
2700 fn density_altitude_ffi_non_finite_input_returns_nan() {
2701 assert!(
2702 ballistics_density_altitude_temperature_c(f64::INFINITY, FFI_NO_EXPLICIT_TEMPERATURE)
2703 .is_nan()
2704 );
2705 assert!(
2706 ballistics_density_altitude_pressure_hpa(f64::NAN, FFI_NO_EXPLICIT_TEMPERATURE).is_nan()
2707 );
2708 assert!(
2709 ballistics_density_altitude_altitude_m(f64::NEG_INFINITY, FFI_NO_EXPLICIT_TEMPERATURE)
2710 .is_nan()
2711 );
2712 }
2713
2714 #[test]
2719 fn ffi_trajectory_uses_the_density_altitude_derived_station_values() {
2720 let inputs = valid_trajectory_inputs();
2721 let da_m = 3000.0 * 0.3048; let altitude_m =
2723 ballistics_density_altitude_altitude_m(da_m, FFI_NO_EXPLICIT_TEMPERATURE);
2724 let temperature_c =
2725 ballistics_density_altitude_temperature_c(da_m, FFI_NO_EXPLICIT_TEMPERATURE);
2726 let pressure_hpa =
2727 ballistics_density_altitude_pressure_hpa(da_m, FFI_NO_EXPLICIT_TEMPERATURE);
2728
2729 let atmo_da = FFIAtmosphericConditions {
2730 temperature: temperature_c,
2731 pressure: pressure_hpa,
2732 humidity: 50.0,
2733 altitude: altitude_m,
2734 };
2735 let atmo_sea_level = FFIAtmosphericConditions {
2736 temperature: 15.0,
2737 pressure: 1013.25,
2738 humidity: 50.0,
2739 altitude: 0.0,
2740 };
2741
2742 unsafe {
2743 let a = ballistics_calculate_trajectory(
2744 &inputs,
2745 std::ptr::null(),
2746 &atmo_da,
2747 400.0,
2748 1.0,
2749 );
2750 let b = ballistics_calculate_trajectory(
2751 &inputs,
2752 std::ptr::null(),
2753 &atmo_sea_level,
2754 400.0,
2755 1.0,
2756 );
2757 assert!(!a.is_null() && !b.is_null());
2758 let drop_a = std::slice::from_raw_parts((*a).points, (*a).point_count as usize)
2759 .last()
2760 .unwrap()
2761 .position_y;
2762 let drop_b = std::slice::from_raw_parts((*b).points, (*b).point_count as usize)
2763 .last()
2764 .unwrap()
2765 .position_y;
2766 assert!(
2767 (drop_a - drop_b).abs() > 1e-6,
2768 "density-altitude-derived vs sea-level atmosphere must produce materially \
2769 different trajectories: {drop_a} vs {drop_b}"
2770 );
2771 ballistics_free_trajectory_result(a);
2772 ballistics_free_trajectory_result(b);
2773 }
2774 }
2775}