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 ballistic_inputs.enable_trajectory_sampling = inputs.enable_trajectory_sampling != 0;
320 ballistic_inputs.sample_interval = inputs.sample_interval;
321 ballistic_inputs.enable_pitch_damping = inputs.enable_pitch_damping != 0;
322 ballistic_inputs.enable_precession_nutation = inputs.enable_precession_nutation != 0;
323 ballistic_inputs.use_enhanced_spin_drift = inputs.enable_spin_drift != 0;
324 ballistic_inputs.enable_advanced_effects =
325 inputs.enable_magnus != 0 || inputs.enable_coriolis != 0;
326 ballistic_inputs.enable_magnus = inputs.enable_magnus != 0;
328 ballistic_inputs.enable_coriolis = inputs.enable_coriolis != 0;
329
330 ballistic_inputs
331}
332
333unsafe fn drag_table_from_raw(
347 mach: *const c_double,
348 cd: *const c_double,
349 len: c_int,
350) -> Result<crate::drag::DragTable, ()> {
351 if mach.is_null() || cd.is_null() || !(2..=MAX_FFI_DRAG_TABLE_LEN).contains(&len) {
352 return Err(());
353 }
354 let len = len as usize;
355 let mach = unsafe { std::slice::from_raw_parts(mach, len) }.to_vec();
356 let cd = unsafe { std::slice::from_raw_parts(cd, len) }.to_vec();
357 crate::drag::DragTable::try_new(mach, cd).map_err(|_| ())
358}
359
360unsafe fn calculate_trajectory_impl(
366 inputs: *const FFIBallisticInputs,
367 wind: *const FFIWindConditions,
368 atmosphere: *const FFIAtmosphericConditions,
369 max_range: c_double,
370 step_size: c_double,
371 custom_drag_table: Option<crate::drag::DragTable>,
372 cd_scale: c_double,
373) -> *mut FFITrajectoryResult {
374 if inputs.is_null() {
375 return ptr::null_mut();
376 }
377 if !step_size.is_finite() || step_size < MIN_FFI_STEP_SIZE_MS {
378 return ptr::null_mut();
379 }
380
381 let inputs = unsafe { &*inputs };
382 let mut ballistic_inputs = convert_inputs(inputs);
383 ballistic_inputs.custom_drag_table = custom_drag_table;
384 ballistic_inputs.cd_scale = cd_scale;
385 let twist_rate_in = ballistic_inputs.twist_rate;
386
387 let wind_conditions = if wind.is_null() {
388 WindConditions::default()
389 } else {
390 let wind = unsafe { &*wind };
391 WindConditions {
392 speed: wind.speed,
393 direction: wind.direction,
394 vertical_speed: wind.vertical_speed,
395 }
396 };
397
398 let atmospheric_conditions = if atmosphere.is_null() {
399 AtmosphericConditions::default()
400 } else {
401 let atmo = unsafe { &*atmosphere };
402 AtmosphericConditions {
403 temperature: atmo.temperature,
404 pressure: atmo.pressure,
405 humidity: atmo.humidity,
406 altitude: atmo.altitude,
407 }
408 };
409
410 let (sample_temp_c, sample_pressure_hpa) = crate::atmosphere::resolve_station_conditions(
412 atmospheric_conditions.temperature,
413 atmospheric_conditions.pressure,
414 atmospheric_conditions.altitude,
415 );
416 let (_, sample_speed_of_sound) = crate::atmosphere::calculate_atmosphere(
417 atmospheric_conditions.altitude,
418 Some(sample_temp_c),
419 Some(sample_pressure_hpa),
420 atmospheric_conditions.humidity,
421 );
422
423 let mut solver =
424 TrajectorySolver::new(ballistic_inputs, wind_conditions, atmospheric_conditions);
425
426 solver.set_max_range(max_range);
428 solver.set_time_step(step_size / 1000.0); match solver.solve() {
431 Ok(result) => {
432 let point_count = result.points.len();
434 let points = if point_count > 0 {
435 let mut ffi_points = Vec::with_capacity(point_count);
436 for point in result.points.iter() {
437 ffi_points.push(FFITrajectoryPoint {
438 time: point.time,
439 position_x: point.position[0],
440 position_y: point.position[1],
441 position_z: point.position[2],
442 velocity_magnitude: point.velocity_magnitude,
443 kinetic_energy: point.kinetic_energy,
444 });
445 }
446 let points_ptr = ffi_points.as_mut_ptr();
447 std::mem::forget(ffi_points); points_ptr
449 } else {
450 ptr::null_mut()
451 };
452
453 let (sampled_points, sampled_point_count) =
455 if let Some(ref samples) = result.sampled_points {
456 let mut ffi_samples = Vec::with_capacity(samples.len());
457 for sample in samples {
458 ffi_samples.push(FFITrajectorySample {
459 distance: sample.distance_m,
460 time: sample.time_s,
461 velocity_mps: sample.velocity_mps,
462 energy_joules: sample.energy_j,
463 drop_meters: sample.drop_m,
464 windage_meters: sample.wind_drift_m,
465 mach: if sample_speed_of_sound > 0.0 {
466 sample.velocity_mps / sample_speed_of_sound
467 } else {
468 0.0
469 },
470 spin_rate_rps: if twist_rate_in > 0.0 {
471 sample.velocity_mps / (twist_rate_in * 0.0254)
472 } else {
473 0.0
474 },
475 });
476 }
477 let count = ffi_samples.len() as c_int;
478 let samples_ptr = ffi_samples.as_mut_ptr();
479 std::mem::forget(ffi_samples);
480 (samples_ptr, count)
481 } else {
482 (ptr::null_mut(), 0)
483 };
484
485 let (final_pitch, final_yaw, max_yaw, max_prec) =
487 if let Some(ref angular) = result.angular_state {
488 (
489 angular.pitch_angle,
490 angular.yaw_angle,
491 result.max_yaw_angle.unwrap_or(f64::NAN),
492 result.max_precession_angle.unwrap_or(f64::NAN),
493 )
494 } else {
495 (f64::NAN, f64::NAN, f64::NAN, f64::NAN)
496 };
497
498 let ffi_result = Box::new(FFITrajectoryResult {
500 max_range: result.max_range,
501 max_height: result.max_height,
502 time_of_flight: result.time_of_flight,
503 impact_velocity: result.impact_velocity,
504 impact_energy: result.impact_energy,
505 points,
506 point_count: point_count as c_int,
507 sampled_points,
508 sampled_point_count,
509 min_pitch_damping: result.min_pitch_damping.unwrap_or(f64::NAN),
510 transonic_mach: result.transonic_mach.unwrap_or(f64::NAN),
511 final_pitch_angle: final_pitch,
512 final_yaw_angle: final_yaw,
513 max_yaw_angle: max_yaw,
514 max_precession_angle: max_prec,
515 });
516
517 Box::into_raw(ffi_result)
518 }
519 Err(_) => ptr::null_mut(),
520 }
521}
522
523#[no_mangle]
542pub unsafe extern "C" fn ballistics_calculate_trajectory(
543 inputs: *const FFIBallisticInputs,
544 wind: *const FFIWindConditions,
545 atmosphere: *const FFIAtmosphericConditions,
546 max_range: c_double,
547 step_size: c_double,
548) -> *mut FFITrajectoryResult {
549 unsafe { calculate_trajectory_impl(inputs, wind, atmosphere, max_range, step_size, None, 1.0) }
550}
551
552#[no_mangle]
573pub unsafe extern "C" fn ballistics_calculate_trajectory_with_drag_table(
574 inputs: *const FFIBallisticInputs,
575 wind: *const FFIWindConditions,
576 atmosphere: *const FFIAtmosphericConditions,
577 max_range: c_double,
578 step_size: c_double,
579 drag_mach: *const c_double,
580 drag_cd: *const c_double,
581 drag_table_len: c_int,
582) -> *mut FFITrajectoryResult {
583 let table = match unsafe { drag_table_from_raw(drag_mach, drag_cd, drag_table_len) } {
584 Ok(t) => t,
585 Err(()) => return ptr::null_mut(),
586 };
587 unsafe {
588 calculate_trajectory_impl(
589 inputs, wind, atmosphere, max_range, step_size, Some(table), 1.0,
590 )
591 }
592}
593
594#[no_mangle]
611pub unsafe extern "C" fn ballistics_calculate_trajectory_with_drag_table_scaled(
612 inputs: *const FFIBallisticInputs,
613 wind: *const FFIWindConditions,
614 atmosphere: *const FFIAtmosphericConditions,
615 max_range: c_double,
616 step_size: c_double,
617 drag_mach: *const c_double,
618 drag_cd: *const c_double,
619 drag_table_len: c_int,
620 cd_scale: c_double,
621) -> *mut FFITrajectoryResult {
622 if !cd_scale.is_finite() || cd_scale <= 0.0 {
623 return ptr::null_mut();
624 }
625 let table = match unsafe { drag_table_from_raw(drag_mach, drag_cd, drag_table_len) } {
626 Ok(t) => t,
627 Err(()) => return ptr::null_mut(),
628 };
629 unsafe {
630 calculate_trajectory_impl(
631 inputs, wind, atmosphere, max_range, step_size, Some(table), cd_scale,
632 )
633 }
634}
635
636#[no_mangle]
645pub unsafe extern "C" fn ballistics_free_trajectory_result(result: *mut FFITrajectoryResult) {
646 if !result.is_null() {
647 unsafe {
648 let result = Box::from_raw(result);
649 if !result.points.is_null() && result.point_count > 0 {
650 let points = Vec::from_raw_parts(
651 result.points,
652 result.point_count as usize,
653 result.point_count as usize,
654 );
655 drop(points);
656 }
657 if !result.sampled_points.is_null() && result.sampled_point_count > 0 {
658 let samples = Vec::from_raw_parts(
659 result.sampled_points,
660 result.sampled_point_count as usize,
661 result.sampled_point_count as usize,
662 );
663 drop(samples);
664 }
665 drop(result);
666 }
667 }
668}
669
670unsafe fn calculate_zero_angle_impl(
677 inputs: *const FFIBallisticInputs,
678 wind: *const FFIWindConditions,
679 atmosphere: *const FFIAtmosphericConditions,
680 zero_distance: c_double,
681 custom_drag_table: Option<crate::drag::DragTable>,
682 cd_scale: c_double,
683) -> c_double {
684 if inputs.is_null() {
685 return f64::NAN;
686 }
687
688 let inputs = unsafe { &*inputs };
689 let mut ballistic_inputs = convert_inputs(inputs);
690 ballistic_inputs.custom_drag_table = custom_drag_table;
691 ballistic_inputs.cd_scale = cd_scale;
692
693 let wind_conditions = if wind.is_null() {
694 WindConditions::default()
695 } else {
696 let wind = unsafe { &*wind };
697 WindConditions {
698 speed: wind.speed,
699 direction: wind.direction,
700 vertical_speed: wind.vertical_speed,
701 }
702 };
703
704 let atmospheric_conditions = if atmosphere.is_null() {
705 AtmosphericConditions::default()
706 } else {
707 let atmo = unsafe { &*atmosphere };
708 AtmosphericConditions {
709 temperature: atmo.temperature,
710 pressure: atmo.pressure,
711 humidity: atmo.humidity,
712 altitude: atmo.altitude,
713 }
714 };
715
716 let target_height = ballistic_inputs.sight_height;
719
720 calculate_zero_angle_with_conditions(
721 ballistic_inputs,
722 zero_distance,
723 target_height,
724 wind_conditions,
725 atmospheric_conditions,
726 )
727 .unwrap_or(f64::NAN)
728}
729
730#[no_mangle]
740pub unsafe extern "C" fn ballistics_calculate_zero_angle(
741 inputs: *const FFIBallisticInputs,
742 wind: *const FFIWindConditions,
743 atmosphere: *const FFIAtmosphericConditions,
744 zero_distance: c_double,
745) -> c_double {
746 unsafe { calculate_zero_angle_impl(inputs, wind, atmosphere, zero_distance, None, 1.0) }
747}
748
749#[no_mangle]
771pub unsafe extern "C" fn ballistics_calculate_zero_angle_with_drag_table(
772 inputs: *const FFIBallisticInputs,
773 wind: *const FFIWindConditions,
774 atmosphere: *const FFIAtmosphericConditions,
775 zero_distance: c_double,
776 drag_mach: *const c_double,
777 drag_cd: *const c_double,
778 drag_table_len: c_int,
779) -> c_double {
780 let table = match unsafe { drag_table_from_raw(drag_mach, drag_cd, drag_table_len) } {
781 Ok(t) => t,
782 Err(()) => return f64::NAN,
783 };
784 unsafe {
785 calculate_zero_angle_impl(inputs, wind, atmosphere, zero_distance, Some(table), 1.0)
786 }
787}
788
789#[no_mangle]
807pub unsafe extern "C" fn ballistics_calculate_zero_angle_with_drag_table_scaled(
808 inputs: *const FFIBallisticInputs,
809 wind: *const FFIWindConditions,
810 atmosphere: *const FFIAtmosphericConditions,
811 zero_distance: c_double,
812 drag_mach: *const c_double,
813 drag_cd: *const c_double,
814 drag_table_len: c_int,
815 cd_scale: c_double,
816) -> c_double {
817 if !cd_scale.is_finite() || cd_scale <= 0.0 {
818 return f64::NAN;
819 }
820 let table = match unsafe { drag_table_from_raw(drag_mach, drag_cd, drag_table_len) } {
821 Ok(t) => t,
822 Err(()) => return f64::NAN,
823 };
824 unsafe {
825 calculate_zero_angle_impl(
826 inputs,
827 wind,
828 atmosphere,
829 zero_distance,
830 Some(table),
831 cd_scale,
832 )
833 }
834}
835
836#[no_mangle]
838#[allow(clippy::field_reassign_with_default)] pub extern "C" fn ballistics_quick_trajectory(
840 muzzle_velocity: c_double,
841 bc: c_double,
842 sight_height: c_double,
843 zero_distance: c_double,
844 target_distance: c_double,
845) -> c_double {
846 let mut inputs = BallisticInputs::default();
850 inputs.muzzle_velocity = muzzle_velocity;
851 inputs.bc_value = bc;
852 inputs.sight_height = sight_height;
853 inputs.target_distance = target_distance;
854
855 let wind = WindConditions::default();
856 let atmo = AtmosphericConditions::default();
857
858 let zero_angle = match calculate_zero_angle_with_conditions(
860 inputs.clone(),
861 zero_distance,
862 sight_height,
863 wind.clone(),
864 atmo.clone(),
865 ) {
866 Ok(angle) => angle,
867 Err(_) => return f64::NAN,
868 };
869
870 inputs.muzzle_angle = zero_angle;
872
873 let mut solver = TrajectorySolver::new(inputs, wind, atmo);
874 solver.set_max_range(target_distance * 1.1);
875
876 match solver.solve() {
877 Ok(result) => {
878 for point in result.points {
880 if point.position[0] >= target_distance {
881 return sight_height - point.position[1];
882 }
883 }
884 f64::NAN
885 }
886 Err(_) => f64::NAN,
887 }
888}
889
890#[no_mangle]
902pub unsafe extern "C" fn ballistics_monte_carlo(
903 inputs: *const FFIBallisticInputs,
904 atmosphere: *const FFIAtmosphericConditions,
905 params: *const FFIMonteCarloParams,
906) -> *mut FFIMonteCarloResults {
907 unsafe { ballistics_monte_carlo_impl(inputs, atmosphere, params, 0.0) }
908}
909
910#[no_mangle]
920pub unsafe extern "C" fn ballistics_monte_carlo_with_direction_std_dev(
921 inputs: *const FFIBallisticInputs,
922 atmosphere: *const FFIAtmosphericConditions,
923 params: *const FFIMonteCarloParams,
924 wind_direction_std_dev: c_double,
925) -> *mut FFIMonteCarloResults {
926 unsafe { ballistics_monte_carlo_impl(inputs, atmosphere, params, wind_direction_std_dev) }
927}
928
929unsafe fn ballistics_monte_carlo_impl(
930 inputs: *const FFIBallisticInputs,
931 atmosphere: *const FFIAtmosphericConditions,
932 params: *const FFIMonteCarloParams,
933 wind_direction_std_dev: f64,
934) -> *mut FFIMonteCarloResults {
935 if inputs.is_null() || params.is_null() {
936 return ptr::null_mut();
937 }
938
939 let inputs = unsafe { &*inputs };
940 let params = unsafe { &*params };
941
942 const MAX_SIMULATIONS: c_int = 1_000_000;
948 if params.num_simulations <= 0 || params.num_simulations > MAX_SIMULATIONS {
949 return ptr::null_mut();
950 }
951
952 let mut ballistic_inputs = convert_inputs(inputs);
954 ballistic_inputs.muzzle_height = 1.5;
955 ballistic_inputs.ground_threshold = 0.0;
956 if !atmosphere.is_null() {
957 let atmo = unsafe { &*atmosphere };
958 ballistic_inputs.temperature = atmo.temperature;
959 ballistic_inputs.pressure = atmo.pressure;
960 ballistic_inputs.humidity = (atmo.humidity / 100.0).clamp(0.0, 1.0);
961 ballistic_inputs.altitude = atmo.altitude;
962 }
963
964 let mc_params = MonteCarloParams {
966 num_simulations: params.num_simulations as usize,
967 velocity_std_dev: params.velocity_std_dev,
968 angle_std_dev: params.angle_std_dev,
969 bc_std_dev: params.bc_std_dev,
970 wind_speed_std_dev: params.wind_speed_std_dev,
971 target_distance: if params.target_distance.is_nan() {
972 None
973 } else {
974 Some(params.target_distance)
975 },
976 base_wind_speed: params.base_wind_speed,
977 base_wind_direction: params.base_wind_direction,
978 azimuth_std_dev: params.azimuth_std_dev,
979 };
980
981 match run_monte_carlo_with_direction_std_dev(
983 ballistic_inputs,
984 mc_params,
985 wind_direction_std_dev,
986 ) {
987 Ok(results) => {
988 let num_results = results.ranges.len() as c_int;
989
990 let mean_range: f64 = results.ranges.iter().sum::<f64>() / num_results as f64;
992 let variance_range: f64 = results
993 .ranges
994 .iter()
995 .map(|r| (r - mean_range).powi(2))
996 .sum::<f64>()
997 / num_results as f64;
998 let std_dev_range = variance_range.sqrt();
999
1000 let mean_velocity: f64 =
1001 results.impact_velocities.iter().sum::<f64>() / num_results as f64;
1002 let variance_velocity: f64 = results
1003 .impact_velocities
1004 .iter()
1005 .map(|v| (v - mean_velocity).powi(2))
1006 .sum::<f64>()
1007 / num_results as f64;
1008 let std_dev_velocity = variance_velocity.sqrt();
1009
1010 let hit_probability = if params.target_distance.is_nan() {
1016 0.0
1017 } else {
1018 results.hit_probability(crate::DEFAULT_HIT_RADIUS_M)
1019 };
1020
1021 let ranges_ptr = unsafe {
1023 let ptr = std::alloc::alloc(
1024 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
1025 ) as *mut c_double;
1026 for (i, &range) in results.ranges.iter().enumerate() {
1027 *ptr.add(i) = range;
1028 }
1029 ptr
1030 };
1031
1032 let velocities_ptr = unsafe {
1033 let ptr = std::alloc::alloc(
1034 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
1035 ) as *mut c_double;
1036 for (i, &vel) in results.impact_velocities.iter().enumerate() {
1037 *ptr.add(i) = vel;
1038 }
1039 ptr
1040 };
1041
1042 let pos_x_ptr = unsafe {
1043 let ptr = std::alloc::alloc(
1044 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
1045 ) as *mut c_double;
1046 for (i, pos) in results.impact_positions.iter().enumerate() {
1047 *ptr.add(i) = pos.x;
1048 }
1049 ptr
1050 };
1051
1052 let pos_y_ptr = unsafe {
1053 let ptr = std::alloc::alloc(
1054 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
1055 ) as *mut c_double;
1056 for (i, pos) in results.impact_positions.iter().enumerate() {
1057 *ptr.add(i) = pos.y;
1058 }
1059 ptr
1060 };
1061
1062 let pos_z_ptr = unsafe {
1063 let ptr = std::alloc::alloc(
1064 std::alloc::Layout::array::<c_double>(num_results as usize).unwrap(),
1065 ) as *mut c_double;
1066 for (i, pos) in results.impact_positions.iter().enumerate() {
1067 *ptr.add(i) = pos.z;
1068 }
1069 ptr
1070 };
1071
1072 let result = Box::new(FFIMonteCarloResults {
1074 ranges: ranges_ptr,
1075 impact_velocities: velocities_ptr,
1076 impact_positions_x: pos_x_ptr,
1077 impact_positions_y: pos_y_ptr,
1078 impact_positions_z: pos_z_ptr,
1079 num_results,
1080 mean_range,
1081 std_dev_range,
1082 mean_impact_velocity: mean_velocity,
1083 std_dev_impact_velocity: std_dev_velocity,
1084 hit_probability,
1085 });
1086
1087 Box::into_raw(result)
1088 }
1089 Err(_) => ptr::null_mut(),
1090 }
1091}
1092
1093#[no_mangle]
1102pub unsafe extern "C" fn ballistics_free_monte_carlo_results(results: *mut FFIMonteCarloResults) {
1103 if results.is_null() {
1104 return;
1105 }
1106
1107 unsafe {
1108 let results = Box::from_raw(results);
1109 let num = results.num_results as usize;
1110
1111 if !results.ranges.is_null() {
1113 std::alloc::dealloc(
1114 results.ranges as *mut u8,
1115 std::alloc::Layout::array::<c_double>(num).unwrap(),
1116 );
1117 }
1118
1119 if !results.impact_velocities.is_null() {
1120 std::alloc::dealloc(
1121 results.impact_velocities as *mut u8,
1122 std::alloc::Layout::array::<c_double>(num).unwrap(),
1123 );
1124 }
1125
1126 if !results.impact_positions_x.is_null() {
1127 std::alloc::dealloc(
1128 results.impact_positions_x as *mut u8,
1129 std::alloc::Layout::array::<c_double>(num).unwrap(),
1130 );
1131 }
1132
1133 if !results.impact_positions_y.is_null() {
1134 std::alloc::dealloc(
1135 results.impact_positions_y as *mut u8,
1136 std::alloc::Layout::array::<c_double>(num).unwrap(),
1137 );
1138 }
1139
1140 if !results.impact_positions_z.is_null() {
1141 std::alloc::dealloc(
1142 results.impact_positions_z as *mut u8,
1143 std::alloc::Layout::array::<c_double>(num).unwrap(),
1144 );
1145 }
1146
1147 }
1149}
1150
1151pub const FFI_BC_REFERENCE_ICAO: c_int = 0;
1157pub const FFI_BC_REFERENCE_ARMY_STANDARD_METRO: c_int = 1;
1158
1159#[no_mangle]
1178pub extern "C" fn ballistics_bc_for_reference_standard(
1179 bc: c_double,
1180 reference_standard: c_int,
1181) -> c_double {
1182 if reference_standard == FFI_BC_REFERENCE_ARMY_STANDARD_METRO {
1183 bc * crate::constants::ASM_TO_ICAO_BC
1184 } else {
1185 bc
1186 }
1187}
1188
1189#[no_mangle]
1207pub extern "C" fn ballistics_reduce_qnh_pressure(
1208 qnh_hpa: c_double,
1209 altitude_m: c_double,
1210) -> c_double {
1211 if !qnh_hpa.is_finite() || !altitude_m.is_finite() {
1212 return qnh_hpa;
1213 }
1214 crate::atmosphere::reduce_qnh_to_station_pressure(qnh_hpa, altitude_m)
1215}
1216
1217pub const FFI_NO_EXPLICIT_TEMPERATURE: c_double = f64::NAN;
1222
1223#[no_mangle]
1243pub extern "C" fn ballistics_density_altitude_temperature_c(
1244 density_altitude_m: c_double,
1245 explicit_temperature_c: c_double,
1246) -> c_double {
1247 if !density_altitude_m.is_finite() {
1248 return f64::NAN;
1249 }
1250 let explicit = (!explicit_temperature_c.is_nan()).then_some(explicit_temperature_c);
1251 crate::atmosphere::resolve_atmosphere_for_density_altitude(density_altitude_m, explicit).1
1252}
1253
1254#[no_mangle]
1257pub extern "C" fn ballistics_density_altitude_pressure_hpa(
1258 density_altitude_m: c_double,
1259 explicit_temperature_c: c_double,
1260) -> c_double {
1261 if !density_altitude_m.is_finite() {
1262 return f64::NAN;
1263 }
1264 let explicit = (!explicit_temperature_c.is_nan()).then_some(explicit_temperature_c);
1265 crate::atmosphere::resolve_atmosphere_for_density_altitude(density_altitude_m, explicit).2
1266}
1267
1268#[no_mangle]
1273pub extern "C" fn ballistics_density_altitude_altitude_m(
1274 density_altitude_m: c_double,
1275 explicit_temperature_c: c_double,
1276) -> c_double {
1277 if !density_altitude_m.is_finite() {
1278 return f64::NAN;
1279 }
1280 let explicit = (!explicit_temperature_c.is_nan()).then_some(explicit_temperature_c);
1281 crate::atmosphere::resolve_atmosphere_for_density_altitude(density_altitude_m, explicit).0
1282}
1283
1284pub const MAX_FFI_RETICLE_MARKS: c_int = crate::reticle::MAX_RETICLE_MARKS as c_int;
1292
1293pub const FFI_RETICLE_FIRST_FOCAL_PLANE: c_int = 0;
1296pub const FFI_RETICLE_SECOND_FOCAL_PLANE: c_int = 1;
1299
1300pub const FFI_RETICLE_OK: c_int = 0;
1302pub const FFI_RETICLE_ERR_INVALID_ARGUMENT: c_int = -1;
1304pub const FFI_RETICLE_ERR_MAGNIFICATION: c_int = -2;
1306pub const FFI_RETICLE_ERR_REFERENCE_MAGNIFICATION: c_int = -3;
1308pub const FFI_RETICLE_ERR_NON_FINITE: c_int = -4;
1310
1311#[repr(C)]
1318pub struct FFIReticleHold {
1319 pub down_mil: c_double,
1321 pub right_mil: c_double,
1323 pub nearest_mark: c_int,
1326 pub nearest_mark_distance_mil: c_double,
1329 pub off_reticle: c_int,
1332 pub mark_scale: c_double,
1335}
1336
1337#[no_mangle]
1356#[allow(unknown_lints, clippy::chunks_exact_to_as_chunks)]
1363pub unsafe extern "C" fn ballistics_hold_point_in_reticle(
1364 drop_mil: c_double,
1365 wind_mil: c_double,
1366 magnification: c_double,
1367 marks: *const c_double,
1368 marks_len: c_int,
1369 focal_plane: c_int,
1370 reference_magnification: c_double,
1371 out: *mut FFIReticleHold,
1372) -> c_int {
1373 use crate::reticle::{
1374 hold_point_in_reticle, FocalPlane, MarkKind, ReticleDescription, ReticleError, ReticleMark,
1375 };
1376
1377 if marks.is_null() || out.is_null() || !(1..=MAX_FFI_RETICLE_MARKS).contains(&marks_len) {
1378 return FFI_RETICLE_ERR_INVALID_ARGUMENT;
1379 }
1380 let count = marks_len as usize;
1381 let flat = unsafe { std::slice::from_raw_parts(marks, count * 2) };
1383
1384 let description = ReticleDescription {
1385 name: String::new(),
1386 focal_plane: if focal_plane == FFI_RETICLE_SECOND_FOCAL_PLANE {
1387 FocalPlane::Second
1388 } else {
1389 FocalPlane::First
1390 },
1391 reference_magnification,
1392 marks: flat
1393 .chunks_exact(2)
1394 .map(|pair| ReticleMark::new(pair[0], pair[1], MarkKind::Dot))
1395 .collect(),
1396 };
1397
1398 let hold = match hold_point_in_reticle(drop_mil, wind_mil, magnification, &description) {
1399 Ok(hold) => hold,
1400 Err(ReticleError::NonPositiveMagnification { .. }) => return FFI_RETICLE_ERR_MAGNIFICATION,
1401 Err(ReticleError::NonPositiveReferenceMagnification { .. }) => {
1402 return FFI_RETICLE_ERR_REFERENCE_MAGNIFICATION
1403 }
1404 Err(ReticleError::NonFiniteMark { .. }) | Err(ReticleError::NonFiniteHold { .. }) => {
1405 return FFI_RETICLE_ERR_NON_FINITE
1406 }
1407 Err(_) => return FFI_RETICLE_ERR_INVALID_ARGUMENT,
1408 };
1409
1410 unsafe {
1411 *out = FFIReticleHold {
1412 down_mil: hold.down_mil,
1413 right_mil: hold.right_mil,
1414 nearest_mark: hold.nearest_mark.map_or(-1, |index| index as c_int),
1415 nearest_mark_distance_mil: hold.nearest_mark_distance_mil,
1416 off_reticle: c_int::from(hold.off_reticle),
1417 mark_scale: hold.mark_scale,
1418 };
1419 }
1420 FFI_RETICLE_OK
1421}
1422
1423#[no_mangle]
1425pub extern "C" fn ballistics_get_version() -> *const c_char {
1426 concat!(env!("CARGO_PKG_VERSION"), "\0").as_ptr() as *const c_char
1430}
1431
1432#[cfg(test)]
1433mod tests {
1434 use super::*;
1435
1436 fn valid_trajectory_inputs() -> FFIBallisticInputs {
1437 FFIBallisticInputs {
1438 muzzle_velocity: 800.0,
1439 muzzle_angle: 0.0,
1440 bc_value: 0.5,
1441 bullet_mass: 0.01,
1442 bullet_diameter: 0.00762,
1443 bc_type: 0,
1444 sight_height: 0.05,
1445 target_distance: 1.0,
1446 temperature: 15.0,
1447 twist_rate: 12.0,
1448 is_twist_right: 1,
1449 shooting_angle: 0.0,
1450 altitude: 0.0,
1451 latitude: f64::NAN,
1452 azimuth_angle: 0.0,
1453 use_rk4: 1,
1454 use_adaptive_rk45: 0,
1455 enable_wind_shear: 0,
1456 enable_trajectory_sampling: 0,
1457 sample_interval: 10.0,
1458 enable_pitch_damping: 0,
1459 enable_precession_nutation: 0,
1460 enable_spin_drift: 0,
1461 enable_magnus: 0,
1462 enable_coriolis: 0,
1463 shot_azimuth: 0.0,
1464 cant_angle: 0.0,
1465 zero_poi_vertical: 0.0,
1466 zero_poi_horizontal: 0.0,
1467 sight_offset_lateral: 0.0,
1468 }
1469 }
1470
1471 #[allow(dead_code)]
1472 #[repr(C)]
1473 struct LegacyFFIMonteCarloParams {
1474 num_simulations: c_int,
1475 velocity_std_dev: c_double,
1476 angle_std_dev: c_double,
1477 bc_std_dev: c_double,
1478 wind_speed_std_dev: c_double,
1479 target_distance: c_double,
1480 base_wind_speed: c_double,
1481 base_wind_direction: c_double,
1482 azimuth_std_dev: c_double,
1483 }
1484
1485 #[test]
1486 fn monte_carlo_params_legacy_abi_size_is_unchanged() {
1487 assert_eq!(
1488 std::mem::size_of::<FFIMonteCarloParams>(),
1489 std::mem::size_of::<LegacyFFIMonteCarloParams>()
1490 );
1491 assert_eq!(
1492 std::mem::align_of::<FFIMonteCarloParams>(),
1493 std::mem::align_of::<LegacyFFIMonteCarloParams>()
1494 );
1495 }
1496
1497 #[test]
1500 fn reticle_addition_does_not_disturb_existing_layouts() {
1501 assert_eq!(
1502 std::mem::size_of::<FFIMonteCarloParams>(),
1503 std::mem::size_of::<LegacyFFIMonteCarloParams>()
1504 );
1505 assert_eq!(std::mem::align_of::<FFIReticleHold>(), 8);
1507 }
1508
1509 fn zeroed_hold() -> FFIReticleHold {
1510 FFIReticleHold {
1511 down_mil: 0.0,
1512 right_mil: 0.0,
1513 nearest_mark: -99,
1514 nearest_mark_distance_mil: -1.0,
1515 off_reticle: -1,
1516 mark_scale: -1.0,
1517 }
1518 }
1519
1520 #[test]
1521 fn ffi_hold_point_matches_the_rust_api_on_both_focal_planes() {
1522 let marks: [c_double; 8] = [0.0, 0.0, 2.0, 0.0, 4.0, 0.0, 2.0, 1.0];
1524 let mut out = zeroed_hold();
1525
1526 let code = unsafe {
1528 ballistics_hold_point_in_reticle(
1529 4.0,
1530 0.0,
1531 6.0,
1532 marks.as_ptr(),
1533 4,
1534 FFI_RETICLE_FIRST_FOCAL_PLANE,
1535 0.0,
1536 &mut out,
1537 )
1538 };
1539 assert_eq!(code, FFI_RETICLE_OK);
1540 assert_eq!(out.down_mil, 4.0);
1541 assert_eq!(out.nearest_mark, 2);
1542 assert_eq!(out.nearest_mark_distance_mil, 0.0);
1543 assert_eq!(out.mark_scale, 1.0);
1544 assert_eq!(out.off_reticle, 0);
1545
1546 let mut out = zeroed_hold();
1548 let code = unsafe {
1549 ballistics_hold_point_in_reticle(
1550 4.0,
1551 0.0,
1552 5.0,
1553 marks.as_ptr(),
1554 4,
1555 FFI_RETICLE_SECOND_FOCAL_PLANE,
1556 10.0,
1557 &mut out,
1558 )
1559 };
1560 assert_eq!(code, FFI_RETICLE_OK);
1561 assert_eq!(out.nearest_mark, 1);
1562 assert_eq!(out.nearest_mark_distance_mil, 0.0);
1563 assert_eq!(out.mark_scale, 2.0);
1564 }
1565
1566 #[test]
1569 fn ffi_hold_point_bounds_check_marks_len_before_reading() {
1570 let marks: [c_double; 4] = [0.0, 0.0, 2.0, 0.0];
1571 let mut out = zeroed_hold();
1572 let call = |len: c_int, ptr: *const c_double, out: &mut FFIReticleHold| unsafe {
1573 ballistics_hold_point_in_reticle(
1574 1.0,
1575 0.0,
1576 10.0,
1577 ptr,
1578 len,
1579 FFI_RETICLE_FIRST_FOCAL_PLANE,
1580 0.0,
1581 out,
1582 )
1583 };
1584
1585 assert_eq!(call(0, marks.as_ptr(), &mut out), FFI_RETICLE_ERR_INVALID_ARGUMENT);
1586 assert_eq!(call(-1, marks.as_ptr(), &mut out), FFI_RETICLE_ERR_INVALID_ARGUMENT);
1587 assert_eq!(
1588 call(MAX_FFI_RETICLE_MARKS + 1, marks.as_ptr(), &mut out),
1589 FFI_RETICLE_ERR_INVALID_ARGUMENT
1590 );
1591 assert_eq!(call(c_int::MAX, marks.as_ptr(), &mut out), FFI_RETICLE_ERR_INVALID_ARGUMENT);
1592 assert_eq!(call(2, std::ptr::null(), &mut out), FFI_RETICLE_ERR_INVALID_ARGUMENT);
1593 assert_eq!(out.nearest_mark, -99);
1595
1596 assert_eq!(
1598 unsafe {
1599 ballistics_hold_point_in_reticle(
1600 1.0,
1601 0.0,
1602 10.0,
1603 marks.as_ptr(),
1604 2,
1605 FFI_RETICLE_FIRST_FOCAL_PLANE,
1606 0.0,
1607 std::ptr::null_mut(),
1608 )
1609 },
1610 FFI_RETICLE_ERR_INVALID_ARGUMENT
1611 );
1612 }
1613
1614 #[test]
1615 fn ffi_hold_point_maps_each_error_class_to_its_own_code() {
1616 let marks: [c_double; 4] = [0.0, 0.0, 2.0, 0.0];
1617 let bad_marks: [c_double; 4] = [0.0, 0.0, f64::NAN, 0.0];
1618 let mut out = zeroed_hold();
1619 let call = |drop: c_double, mag: c_double, plane: c_int, ref_mag: c_double,
1620 m: &[c_double], out: &mut FFIReticleHold| unsafe {
1621 ballistics_hold_point_in_reticle(
1622 drop,
1623 0.0,
1624 mag,
1625 m.as_ptr(),
1626 (m.len() / 2) as c_int,
1627 plane,
1628 ref_mag,
1629 out,
1630 )
1631 };
1632
1633 assert_eq!(
1634 call(1.0, 0.0, FFI_RETICLE_FIRST_FOCAL_PLANE, 0.0, &marks, &mut out),
1635 FFI_RETICLE_ERR_MAGNIFICATION
1636 );
1637 assert_eq!(
1638 call(1.0, 10.0, FFI_RETICLE_SECOND_FOCAL_PLANE, 0.0, &marks, &mut out),
1639 FFI_RETICLE_ERR_REFERENCE_MAGNIFICATION
1640 );
1641 assert_eq!(
1642 call(f64::NAN, 10.0, FFI_RETICLE_FIRST_FOCAL_PLANE, 0.0, &marks, &mut out),
1643 FFI_RETICLE_ERR_NON_FINITE
1644 );
1645 assert_eq!(
1646 call(1.0, 10.0, FFI_RETICLE_FIRST_FOCAL_PLANE, 0.0, &bad_marks, &mut out),
1647 FFI_RETICLE_ERR_NON_FINITE
1648 );
1649 assert_eq!(out.nearest_mark, -99, "out stays untouched on every error");
1650 }
1651
1652 #[test]
1657 fn bc_type_8_maps_to_ra4() {
1658 let mut inputs = valid_trajectory_inputs();
1659 inputs.bc_type = 8;
1660 assert_eq!(convert_inputs(&inputs).bc_type, DragModel::RA4);
1661
1662 let expected = [
1664 (0, DragModel::G1),
1665 (1, DragModel::G7),
1666 (2, DragModel::G2),
1667 (3, DragModel::G5),
1668 (4, DragModel::G6),
1669 (5, DragModel::G8),
1670 (6, DragModel::GI),
1671 (7, DragModel::GS),
1672 ];
1673 for (code, model) in expected {
1674 let mut inputs = valid_trajectory_inputs();
1675 inputs.bc_type = code;
1676 assert_eq!(convert_inputs(&inputs).bc_type, model, "code {code}");
1677 }
1678
1679 for code in [9, 42, -1] {
1681 let mut inputs = valid_trajectory_inputs();
1682 inputs.bc_type = code;
1683 assert_eq!(convert_inputs(&inputs).bc_type, DragModel::G1, "code {code}");
1684 }
1685 }
1686
1687 #[test]
1688 fn null_pointer_contracts_return_sentinels_and_free_safely() {
1689 unsafe {
1690 assert!(ballistics_calculate_trajectory(
1691 std::ptr::null(),
1692 std::ptr::null(),
1693 std::ptr::null(),
1694 1_000.0,
1695 1.0,
1696 )
1697 .is_null());
1698 assert!(ballistics_calculate_zero_angle(
1699 std::ptr::null(),
1700 std::ptr::null(),
1701 std::ptr::null(),
1702 100.0,
1703 )
1704 .is_nan());
1705 assert!(ballistics_calculate_trajectory_with_drag_table(
1706 std::ptr::null(),
1707 std::ptr::null(),
1708 std::ptr::null(),
1709 1_000.0,
1710 1.0,
1711 DECK_MACH.as_ptr(),
1712 DECK_CD_LOW.as_ptr(),
1713 DECK_MACH.len() as c_int,
1714 )
1715 .is_null());
1716 assert!(ballistics_calculate_zero_angle_with_drag_table(
1717 std::ptr::null(),
1718 std::ptr::null(),
1719 std::ptr::null(),
1720 100.0,
1721 DECK_MACH.as_ptr(),
1722 DECK_CD_LOW.as_ptr(),
1723 DECK_MACH.len() as c_int,
1724 )
1725 .is_nan());
1726 assert!(
1727 ballistics_monte_carlo(std::ptr::null(), std::ptr::null(), std::ptr::null(),)
1728 .is_null()
1729 );
1730 assert!(ballistics_monte_carlo_with_direction_std_dev(
1731 std::ptr::null(),
1732 std::ptr::null(),
1733 std::ptr::null(),
1734 0.1,
1735 )
1736 .is_null());
1737
1738 ballistics_free_trajectory_result(std::ptr::null_mut());
1739 ballistics_free_monte_carlo_results(std::ptr::null_mut());
1740 }
1741 }
1742
1743 #[test]
1744 fn mba1283_ffi_enforces_step_floor_for_every_solver_mode() {
1745 for (mode, use_rk4, use_adaptive_rk45) in [("Euler", 0, 0), ("RK4", 1, 0), ("RK45", 1, 1)] {
1746 for step_size in [
1747 f64::NAN,
1748 f64::INFINITY,
1749 f64::NEG_INFINITY,
1750 -1.0,
1751 -0.0,
1752 0.0,
1753 0.001,
1754 MIN_FFI_STEP_SIZE_MS - 0.001,
1755 ] {
1756 let mut inputs = valid_trajectory_inputs();
1757 inputs.use_rk4 = use_rk4;
1758 inputs.use_adaptive_rk45 = use_adaptive_rk45;
1759 let result = unsafe {
1760 ballistics_calculate_trajectory(
1761 &inputs,
1762 std::ptr::null(),
1763 std::ptr::null(),
1764 0.01,
1765 step_size,
1766 )
1767 };
1768 assert!(
1769 result.is_null(),
1770 "{mode} step_size={step_size:?} bypassed the FFI floor"
1771 );
1772 }
1773
1774 let mut inputs = valid_trajectory_inputs();
1775 inputs.use_rk4 = use_rk4;
1776 inputs.use_adaptive_rk45 = use_adaptive_rk45;
1777 let result = unsafe {
1778 ballistics_calculate_trajectory(
1779 &inputs,
1780 std::ptr::null(),
1781 std::ptr::null(),
1782 0.01,
1783 MIN_FFI_STEP_SIZE_MS,
1784 )
1785 };
1786 assert!(
1787 !result.is_null(),
1788 "the documented minimum step must remain usable in {mode}"
1789 );
1790 unsafe {
1791 assert!((*result).point_count >= 0);
1792 assert!((*result).point_count as usize <= crate::MAX_TRAJECTORY_POINTS);
1793 ballistics_free_trajectory_result(result);
1794 }
1795 }
1796 }
1797
1798 const DECK_MACH: [f64; 4] = [0.5, 1.0, 2.0, 3.0];
1800 const DECK_CD_LOW: [f64; 4] = [0.05, 0.08, 0.06, 0.05];
1802
1803 #[test]
1804 fn trajectory_with_drag_table_applies_the_deck() {
1805 let inputs = valid_trajectory_inputs();
1806 unsafe {
1807 let plain = ballistics_calculate_trajectory(
1808 &inputs,
1809 std::ptr::null(),
1810 std::ptr::null(),
1811 300.0,
1812 1.0,
1813 );
1814 let decked = ballistics_calculate_trajectory_with_drag_table(
1815 &inputs,
1816 std::ptr::null(),
1817 std::ptr::null(),
1818 300.0,
1819 1.0,
1820 DECK_MACH.as_ptr(),
1821 DECK_CD_LOW.as_ptr(),
1822 DECK_MACH.len() as c_int,
1823 );
1824 assert!(!plain.is_null() && !decked.is_null());
1825 assert!(
1827 (*decked).impact_velocity > (*plain).impact_velocity + 1.0,
1828 "deck did not change the solve: plain={} decked={}",
1829 (*plain).impact_velocity,
1830 (*decked).impact_velocity
1831 );
1832 ballistics_free_trajectory_result(plain);
1833 ballistics_free_trajectory_result(decked);
1834 }
1835 }
1836
1837 #[test]
1838 fn trajectory_with_drag_table_rejects_invalid_decks() {
1839 let inputs = valid_trajectory_inputs();
1840 let descending = [3.0, 2.0, 1.0, 0.5];
1841 let negative_cd = [0.05, -0.08, 0.06, 0.05];
1842 unsafe {
1843 assert!(ballistics_calculate_trajectory_with_drag_table(
1845 &inputs,
1846 std::ptr::null(),
1847 std::ptr::null(),
1848 300.0,
1849 1.0,
1850 std::ptr::null(),
1851 DECK_CD_LOW.as_ptr(),
1852 4,
1853 )
1854 .is_null());
1855 assert!(ballistics_calculate_trajectory_with_drag_table(
1856 &inputs,
1857 std::ptr::null(),
1858 std::ptr::null(),
1859 300.0,
1860 1.0,
1861 DECK_MACH.as_ptr(),
1862 std::ptr::null(),
1863 4,
1864 )
1865 .is_null());
1866 assert!(ballistics_calculate_trajectory_with_drag_table(
1868 &inputs,
1869 std::ptr::null(),
1870 std::ptr::null(),
1871 300.0,
1872 1.0,
1873 DECK_MACH.as_ptr(),
1874 DECK_CD_LOW.as_ptr(),
1875 1,
1876 )
1877 .is_null());
1878 assert!(ballistics_calculate_trajectory_with_drag_table(
1880 &inputs,
1881 std::ptr::null(),
1882 std::ptr::null(),
1883 300.0,
1884 1.0,
1885 descending.as_ptr(),
1886 DECK_CD_LOW.as_ptr(),
1887 4,
1888 )
1889 .is_null());
1890 assert!(ballistics_calculate_trajectory_with_drag_table(
1892 &inputs,
1893 std::ptr::null(),
1894 std::ptr::null(),
1895 300.0,
1896 1.0,
1897 DECK_MACH.as_ptr(),
1898 negative_cd.as_ptr(),
1899 4,
1900 )
1901 .is_null());
1902 assert!(ballistics_calculate_trajectory_with_drag_table(
1904 std::ptr::null(),
1905 std::ptr::null(),
1906 std::ptr::null(),
1907 300.0,
1908 1.0,
1909 DECK_MACH.as_ptr(),
1910 DECK_CD_LOW.as_ptr(),
1911 4,
1912 )
1913 .is_null());
1914 }
1915 }
1916
1917 #[test]
1918 fn zero_angle_with_drag_table_applies_the_deck() {
1919 let inputs = valid_trajectory_inputs();
1921 unsafe {
1922 let plain =
1923 ballistics_calculate_zero_angle(&inputs, std::ptr::null(), std::ptr::null(), 100.0);
1924 let decked = ballistics_calculate_zero_angle_with_drag_table(
1925 &inputs,
1926 std::ptr::null(),
1927 std::ptr::null(),
1928 100.0,
1929 DECK_MACH.as_ptr(),
1930 DECK_CD_LOW.as_ptr(),
1931 DECK_MACH.len() as c_int,
1932 );
1933 assert!(plain.is_finite() && decked.is_finite());
1934 assert!(
1937 (plain - decked).abs() > 1e-6,
1938 "deck did not change the zero: plain={plain} decked={decked}"
1939 );
1940 }
1941 }
1942
1943 #[test]
1944 fn zero_angle_with_drag_table_rejects_invalid_decks() {
1945 let inputs = valid_trajectory_inputs();
1946 let descending = [3.0, 2.0, 1.0, 0.5];
1947 unsafe {
1948 assert!(ballistics_calculate_zero_angle_with_drag_table(
1949 &inputs,
1950 std::ptr::null(),
1951 std::ptr::null(),
1952 100.0,
1953 std::ptr::null(),
1954 DECK_CD_LOW.as_ptr(),
1955 4,
1956 )
1957 .is_nan());
1958 assert!(ballistics_calculate_zero_angle_with_drag_table(
1959 &inputs,
1960 std::ptr::null(),
1961 std::ptr::null(),
1962 100.0,
1963 DECK_MACH.as_ptr(),
1964 DECK_CD_LOW.as_ptr(),
1965 0,
1966 )
1967 .is_nan());
1968 assert!(ballistics_calculate_zero_angle_with_drag_table(
1969 &inputs,
1970 std::ptr::null(),
1971 std::ptr::null(),
1972 100.0,
1973 descending.as_ptr(),
1974 DECK_CD_LOW.as_ptr(),
1975 4,
1976 )
1977 .is_nan());
1978 assert!(ballistics_calculate_zero_angle_with_drag_table(
1980 std::ptr::null(),
1981 std::ptr::null(),
1982 std::ptr::null(),
1983 100.0,
1984 DECK_MACH.as_ptr(),
1985 DECK_CD_LOW.as_ptr(),
1986 4,
1987 )
1988 .is_nan());
1989 }
1990 }
1991
1992 #[test]
1993 fn zero_then_fly_with_same_deck_is_consistent() {
1994 let mut inputs = valid_trajectory_inputs();
1998 unsafe {
1999 let angle = ballistics_calculate_zero_angle_with_drag_table(
2000 &inputs,
2001 std::ptr::null(),
2002 std::ptr::null(),
2003 100.0,
2004 DECK_MACH.as_ptr(),
2005 DECK_CD_LOW.as_ptr(),
2006 DECK_MACH.len() as c_int,
2007 );
2008 assert!(angle.is_finite());
2009 inputs.muzzle_angle = angle;
2010 let result = ballistics_calculate_trajectory_with_drag_table(
2011 &inputs,
2012 std::ptr::null(),
2013 std::ptr::null(),
2014 150.0,
2015 1.0,
2016 DECK_MACH.as_ptr(),
2017 DECK_CD_LOW.as_ptr(),
2018 DECK_MACH.len() as c_int,
2019 );
2020 assert!(!result.is_null());
2021 let zero_distance = 100.0;
2025 let pts = std::slice::from_raw_parts((*result).points, (*result).point_count as usize);
2026 let bracket = pts
2027 .windows(2)
2028 .find(|w| w[0].position_x <= zero_distance && w[1].position_x >= zero_distance)
2029 .expect("trajectory brackets the zero distance");
2030 let (lo, hi) = (&bracket[0], &bracket[1]);
2031 let y_at_zero = if hi.position_x > lo.position_x {
2032 let t = (zero_distance - lo.position_x) / (hi.position_x - lo.position_x);
2033 lo.position_y + t * (hi.position_y - lo.position_y)
2034 } else {
2035 lo.position_y
2036 };
2037 assert!(
2038 (y_at_zero - inputs.sight_height).abs() < 0.002,
2039 "zeroed flight missed the line of sight at 100 m: y={} (sight_height={})",
2040 y_at_zero,
2041 inputs.sight_height
2042 );
2043 ballistics_free_trajectory_result(result);
2044 }
2045 }
2046
2047 #[test]
2048 fn drag_table_len_above_cap_is_rejected() {
2049 let n = (MAX_FFI_DRAG_TABLE_LEN + 1) as usize;
2052 let mach: Vec<f64> = (0..n).map(|i| 0.01 + i as f64 * 0.001).collect();
2053 let cd: Vec<f64> = vec![0.3; n];
2054 let inputs = valid_trajectory_inputs();
2055 unsafe {
2056 let r = ballistics_calculate_trajectory_with_drag_table(
2057 &inputs,
2058 std::ptr::null(),
2059 std::ptr::null(),
2060 300.0,
2061 1.0,
2062 mach.as_ptr(),
2063 cd.as_ptr(),
2064 n as c_int,
2065 );
2066 assert!(r.is_null(), "len {n} must be rejected by the cap");
2067 }
2068 }
2069
2070 #[test]
2071 fn drag_table_len_at_cap_is_accepted() {
2072 let n = MAX_FFI_DRAG_TABLE_LEN as usize;
2073 let mach: Vec<f64> = (0..n).map(|i| 0.01 + i as f64 * 0.001).collect();
2074 let cd: Vec<f64> = vec![0.3; n];
2075 let inputs = valid_trajectory_inputs();
2076 unsafe {
2077 let r = ballistics_calculate_trajectory_with_drag_table(
2078 &inputs,
2079 std::ptr::null(),
2080 std::ptr::null(),
2081 300.0,
2082 1.0,
2083 mach.as_ptr(),
2084 cd.as_ptr(),
2085 n as c_int,
2086 );
2087 assert!(!r.is_null(), "len == cap must be accepted");
2088 ballistics_free_trajectory_result(r);
2089 }
2090 }
2091
2092 #[test]
2093 fn ffi_cant_angle_deflects_laterally() {
2094 let mut level = valid_trajectory_inputs();
2095 level.muzzle_angle = 0.003;
2096 let mut canted = valid_trajectory_inputs();
2097 canted.muzzle_angle = 0.003;
2098 canted.cant_angle = 10f64.to_radians();
2099 unsafe {
2100 let a = ballistics_calculate_trajectory(&level, std::ptr::null(), std::ptr::null(), 400.0, 1.0);
2101 let b = ballistics_calculate_trajectory(&canted, std::ptr::null(), std::ptr::null(), 400.0, 1.0);
2102 assert!(!a.is_null() && !b.is_null());
2103 let za = std::slice::from_raw_parts((*a).points, (*a).point_count as usize).last().unwrap().position_z;
2104 let zb = std::slice::from_raw_parts((*b).points, (*b).point_count as usize).last().unwrap().position_z;
2105 assert!(zb > za + 0.005, "FFI cant must deflect right: level={za} canted={zb}");
2106 ballistics_free_trajectory_result(a);
2107 ballistics_free_trajectory_result(b);
2108 }
2109 }
2110
2111 #[test]
2112 fn ffi_vertical_wind_raises_trajectory() {
2113 let inputs = valid_trajectory_inputs();
2114 let no_wind = FFIWindConditions {
2115 speed: 0.0,
2116 direction: 0.0,
2117 vertical_speed: 0.0,
2118 };
2119 let updraft = FFIWindConditions {
2120 speed: 0.0,
2121 direction: 0.0,
2122 vertical_speed: 5.0,
2123 };
2124 unsafe {
2125 let a = ballistics_calculate_trajectory(&inputs, &no_wind, std::ptr::null(), 400.0, 1.0);
2126 let b = ballistics_calculate_trajectory(&inputs, &updraft, std::ptr::null(), 400.0, 1.0);
2127 assert!(!a.is_null() && !b.is_null());
2128 let ya = std::slice::from_raw_parts((*a).points, (*a).point_count as usize).last().unwrap().position_y;
2129 let yb = std::slice::from_raw_parts((*b).points, (*b).point_count as usize).last().unwrap().position_y;
2130 assert!(yb > ya + 0.01, "FFI updraft must raise the trajectory: no_wind={ya} updraft={yb}");
2131 ballistics_free_trajectory_result(a);
2132 ballistics_free_trajectory_result(b);
2133 }
2134 }
2135
2136 #[test]
2139 fn trajectory_scaled_at_one_matches_unscaled_export() {
2140 let inputs = valid_trajectory_inputs();
2141 unsafe {
2142 let unscaled = ballistics_calculate_trajectory_with_drag_table(
2143 &inputs,
2144 std::ptr::null(),
2145 std::ptr::null(),
2146 300.0,
2147 1.0,
2148 DECK_MACH.as_ptr(),
2149 DECK_CD_LOW.as_ptr(),
2150 DECK_MACH.len() as c_int,
2151 );
2152 let scaled = ballistics_calculate_trajectory_with_drag_table_scaled(
2153 &inputs,
2154 std::ptr::null(),
2155 std::ptr::null(),
2156 300.0,
2157 1.0,
2158 DECK_MACH.as_ptr(),
2159 DECK_CD_LOW.as_ptr(),
2160 DECK_MACH.len() as c_int,
2161 1.0,
2162 );
2163 assert!(!unscaled.is_null() && !scaled.is_null());
2164 assert_eq!(
2165 (*unscaled).impact_velocity.to_bits(),
2166 (*scaled).impact_velocity.to_bits(),
2167 "cd_scale=1.0 must be byte-identical to the unscaled export: unscaled={} scaled={}",
2168 (*unscaled).impact_velocity,
2169 (*scaled).impact_velocity
2170 );
2171 ballistics_free_trajectory_result(unscaled);
2172 ballistics_free_trajectory_result(scaled);
2173 }
2174 }
2175
2176 #[test]
2177 fn trajectory_scaled_at_1_10_lowers_impact_velocity() {
2178 let inputs = valid_trajectory_inputs();
2179 unsafe {
2180 let baseline = ballistics_calculate_trajectory_with_drag_table_scaled(
2181 &inputs,
2182 std::ptr::null(),
2183 std::ptr::null(),
2184 300.0,
2185 1.0,
2186 DECK_MACH.as_ptr(),
2187 DECK_CD_LOW.as_ptr(),
2188 DECK_MACH.len() as c_int,
2189 1.0,
2190 );
2191 let scaled_up = ballistics_calculate_trajectory_with_drag_table_scaled(
2192 &inputs,
2193 std::ptr::null(),
2194 std::ptr::null(),
2195 300.0,
2196 1.0,
2197 DECK_MACH.as_ptr(),
2198 DECK_CD_LOW.as_ptr(),
2199 DECK_MACH.len() as c_int,
2200 1.10,
2201 );
2202 assert!(!baseline.is_null() && !scaled_up.is_null());
2203 assert!(
2204 (*scaled_up).impact_velocity < (*baseline).impact_velocity,
2205 "cd_scale=1.10 must increase drag -> lower impact velocity: base={} scaled={}",
2206 (*baseline).impact_velocity,
2207 (*scaled_up).impact_velocity
2208 );
2209 ballistics_free_trajectory_result(baseline);
2210 ballistics_free_trajectory_result(scaled_up);
2211 }
2212 }
2213
2214 #[test]
2215 fn trajectory_scaled_rejects_invalid_cd_scale() {
2216 let inputs = valid_trajectory_inputs();
2217 unsafe {
2218 for bad in [0.0, -1.0, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
2219 let r = ballistics_calculate_trajectory_with_drag_table_scaled(
2220 &inputs,
2221 std::ptr::null(),
2222 std::ptr::null(),
2223 300.0,
2224 1.0,
2225 DECK_MACH.as_ptr(),
2226 DECK_CD_LOW.as_ptr(),
2227 DECK_MACH.len() as c_int,
2228 bad,
2229 );
2230 assert!(r.is_null(), "cd_scale={bad} must be rejected (null sentinel)");
2231 }
2232 }
2233 }
2234
2235 #[test]
2236 fn zero_angle_scaled_at_one_matches_unscaled_export() {
2237 let inputs = valid_trajectory_inputs();
2238 unsafe {
2239 let unscaled = ballistics_calculate_zero_angle_with_drag_table(
2240 &inputs,
2241 std::ptr::null(),
2242 std::ptr::null(),
2243 100.0,
2244 DECK_MACH.as_ptr(),
2245 DECK_CD_LOW.as_ptr(),
2246 DECK_MACH.len() as c_int,
2247 );
2248 let scaled = ballistics_calculate_zero_angle_with_drag_table_scaled(
2249 &inputs,
2250 std::ptr::null(),
2251 std::ptr::null(),
2252 100.0,
2253 DECK_MACH.as_ptr(),
2254 DECK_CD_LOW.as_ptr(),
2255 DECK_MACH.len() as c_int,
2256 1.0,
2257 );
2258 assert!(unscaled.is_finite() && scaled.is_finite());
2259 assert_eq!(
2260 unscaled.to_bits(),
2261 scaled.to_bits(),
2262 "cd_scale=1.0 must be byte-identical to the unscaled export: unscaled={unscaled} scaled={scaled}"
2263 );
2264 }
2265 }
2266
2267 #[test]
2268 fn zero_angle_scaled_at_1_10_differs_from_baseline() {
2269 let inputs = valid_trajectory_inputs();
2270 unsafe {
2271 let baseline = ballistics_calculate_zero_angle_with_drag_table_scaled(
2272 &inputs,
2273 std::ptr::null(),
2274 std::ptr::null(),
2275 100.0,
2276 DECK_MACH.as_ptr(),
2277 DECK_CD_LOW.as_ptr(),
2278 DECK_MACH.len() as c_int,
2279 1.0,
2280 );
2281 let scaled_up = ballistics_calculate_zero_angle_with_drag_table_scaled(
2282 &inputs,
2283 std::ptr::null(),
2284 std::ptr::null(),
2285 100.0,
2286 DECK_MACH.as_ptr(),
2287 DECK_CD_LOW.as_ptr(),
2288 DECK_MACH.len() as c_int,
2289 1.10,
2290 );
2291 assert!(baseline.is_finite() && scaled_up.is_finite());
2292 assert!(
2294 scaled_up > baseline,
2295 "cd_scale=1.10 must need a larger zero angle: base={baseline} scaled={scaled_up}"
2296 );
2297 }
2298 }
2299
2300 #[test]
2301 fn zero_angle_scaled_rejects_invalid_cd_scale() {
2302 let inputs = valid_trajectory_inputs();
2303 unsafe {
2304 for bad in [0.0, -1.0, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
2305 let angle = ballistics_calculate_zero_angle_with_drag_table_scaled(
2306 &inputs,
2307 std::ptr::null(),
2308 std::ptr::null(),
2309 100.0,
2310 DECK_MACH.as_ptr(),
2311 DECK_CD_LOW.as_ptr(),
2312 DECK_MACH.len() as c_int,
2313 bad,
2314 );
2315 assert!(angle.is_nan(), "cd_scale={bad} must be rejected (NaN sentinel)");
2316 }
2317 }
2318 }
2319
2320 #[test]
2324 fn legacy_drag_table_exports_unaffected_by_cd_scale_plumbing() {
2325 let inputs = valid_trajectory_inputs();
2326 unsafe {
2327 let a = ballistics_calculate_trajectory_with_drag_table(
2328 &inputs,
2329 std::ptr::null(),
2330 std::ptr::null(),
2331 300.0,
2332 1.0,
2333 DECK_MACH.as_ptr(),
2334 DECK_CD_LOW.as_ptr(),
2335 DECK_MACH.len() as c_int,
2336 );
2337 let b = ballistics_calculate_trajectory_with_drag_table(
2338 &inputs,
2339 std::ptr::null(),
2340 std::ptr::null(),
2341 300.0,
2342 1.0,
2343 DECK_MACH.as_ptr(),
2344 DECK_CD_LOW.as_ptr(),
2345 DECK_MACH.len() as c_int,
2346 );
2347 assert!(!a.is_null() && !b.is_null());
2348 assert_eq!((*a).impact_velocity.to_bits(), (*b).impact_velocity.to_bits());
2349 ballistics_free_trajectory_result(a);
2350 ballistics_free_trajectory_result(b);
2351
2352 let za = ballistics_calculate_zero_angle_with_drag_table(
2353 &inputs,
2354 std::ptr::null(),
2355 std::ptr::null(),
2356 100.0,
2357 DECK_MACH.as_ptr(),
2358 DECK_CD_LOW.as_ptr(),
2359 DECK_MACH.len() as c_int,
2360 );
2361 let zb = ballistics_calculate_zero_angle_with_drag_table(
2362 &inputs,
2363 std::ptr::null(),
2364 std::ptr::null(),
2365 100.0,
2366 DECK_MACH.as_ptr(),
2367 DECK_CD_LOW.as_ptr(),
2368 DECK_MACH.len() as c_int,
2369 );
2370 assert!(za.is_finite() && zb.is_finite());
2371 assert_eq!(za.to_bits(), zb.to_bits());
2372 }
2373 }
2374
2375 #[test]
2378 fn bc_for_reference_standard_icao_is_a_byte_identical_no_op() {
2379 let bc = 0.475;
2380 assert_eq!(
2381 ballistics_bc_for_reference_standard(bc, FFI_BC_REFERENCE_ICAO).to_bits(),
2382 bc.to_bits()
2383 );
2384 }
2385
2386 #[test]
2387 fn bc_for_reference_standard_unrecognized_value_falls_back_to_icao() {
2388 let bc = 0.475;
2391 assert_eq!(
2392 ballistics_bc_for_reference_standard(bc, 99).to_bits(),
2393 bc.to_bits()
2394 );
2395 }
2396
2397 #[test]
2398 fn bc_for_reference_standard_army_standard_metro_applies_the_documented_ratio() {
2399 let bc = 0.475;
2400 let converted =
2401 ballistics_bc_for_reference_standard(bc, FFI_BC_REFERENCE_ARMY_STANDARD_METRO);
2402 assert_eq!(converted, bc * crate::constants::ASM_TO_ICAO_BC);
2403 assert!(converted < bc);
2405 }
2406
2407 #[test]
2410 fn reduce_qnh_pressure_matches_the_library_function_and_lowers_pressure() {
2411 let reduced = ballistics_reduce_qnh_pressure(1030.0, 1500.0);
2412 assert_eq!(
2413 reduced,
2414 crate::atmosphere::reduce_qnh_to_station_pressure(1030.0, 1500.0)
2415 );
2416 assert!(reduced < 1030.0);
2417 }
2418
2419 #[test]
2420 fn reduce_qnh_pressure_passes_through_non_finite_inputs() {
2421 assert!(ballistics_reduce_qnh_pressure(f64::NAN, 1500.0).is_nan());
2422 assert_eq!(ballistics_reduce_qnh_pressure(1030.0, f64::INFINITY), 1030.0);
2423 }
2424
2425 #[test]
2433 fn ffi_trajectory_uses_the_reduced_pressure_not_the_raw_qnh() {
2434 let inputs = valid_trajectory_inputs();
2435 let altitude_m = 1500.0;
2436 let qnh_hpa = 1030.0;
2437 let reduced = ballistics_reduce_qnh_pressure(qnh_hpa, altitude_m);
2438 assert!(reduced < qnh_hpa);
2439
2440 let atmo_reduced = FFIAtmosphericConditions {
2441 temperature: 15.0,
2442 pressure: reduced,
2443 humidity: 50.0,
2444 altitude: altitude_m,
2445 };
2446 let atmo_raw_qnh = FFIAtmosphericConditions {
2447 temperature: 15.0,
2448 pressure: qnh_hpa,
2449 humidity: 50.0,
2450 altitude: altitude_m,
2451 };
2452
2453 unsafe {
2454 let a = ballistics_calculate_trajectory(
2455 &inputs,
2456 std::ptr::null(),
2457 &atmo_reduced,
2458 400.0,
2459 1.0,
2460 );
2461 let b = ballistics_calculate_trajectory(
2462 &inputs,
2463 std::ptr::null(),
2464 &atmo_raw_qnh,
2465 400.0,
2466 1.0,
2467 );
2468 assert!(!a.is_null() && !b.is_null());
2469 let drop_a = std::slice::from_raw_parts((*a).points, (*a).point_count as usize)
2470 .last()
2471 .unwrap()
2472 .position_y;
2473 let drop_b = std::slice::from_raw_parts((*b).points, (*b).point_count as usize)
2474 .last()
2475 .unwrap()
2476 .position_y;
2477 assert!(
2478 (drop_a - drop_b).abs() > 1e-6,
2479 "reduced vs. raw-QNH pressure must produce materially different trajectories: \
2480 {drop_a} vs {drop_b}"
2481 );
2482 ballistics_free_trajectory_result(a);
2483 ballistics_free_trajectory_result(b);
2484 }
2485 }
2486
2487 #[test]
2491 fn ffi_monte_carlo_uses_the_reduced_pressure_not_the_raw_qnh() {
2492 let inputs = valid_trajectory_inputs();
2493 let altitude_m = 1500.0;
2494 let qnh_hpa = 1030.0;
2495 let reduced = ballistics_reduce_qnh_pressure(qnh_hpa, altitude_m);
2496
2497 let atmo_reduced = FFIAtmosphericConditions {
2498 temperature: 15.0,
2499 pressure: reduced,
2500 humidity: 50.0,
2501 altitude: altitude_m,
2502 };
2503 let atmo_raw_qnh = FFIAtmosphericConditions {
2504 temperature: 15.0,
2505 pressure: qnh_hpa,
2506 humidity: 50.0,
2507 altitude: altitude_m,
2508 };
2509 let params = FFIMonteCarloParams {
2510 num_simulations: 200,
2511 velocity_std_dev: 1.0,
2512 angle_std_dev: 0.0,
2513 bc_std_dev: 0.0,
2514 wind_speed_std_dev: 0.0,
2515 target_distance: f64::NAN,
2516 base_wind_speed: 0.0,
2517 base_wind_direction: 0.0,
2518 azimuth_std_dev: 0.0,
2519 };
2520
2521 unsafe {
2522 let a = ballistics_monte_carlo(&inputs, &atmo_reduced, ¶ms);
2523 let b = ballistics_monte_carlo(&inputs, &atmo_raw_qnh, ¶ms);
2524 assert!(!a.is_null() && !b.is_null());
2525 assert!(
2526 ((*a).mean_range - (*b).mean_range).abs() > 0.5,
2527 "reduced vs. raw-QNH pressure must change MC mean range materially: \
2528 {} vs {}",
2529 (*a).mean_range,
2530 (*b).mean_range
2531 );
2532 ballistics_free_monte_carlo_results(a);
2533 ballistics_free_monte_carlo_results(b);
2534 }
2535 }
2536
2537 #[test]
2540 fn density_altitude_ffi_exports_match_the_library_function() {
2541 let da_m = 1000.0 * 0.3048;
2542 let expected = crate::atmosphere::resolve_atmosphere_for_density_altitude(da_m, None);
2543 assert_eq!(
2544 ballistics_density_altitude_altitude_m(da_m, FFI_NO_EXPLICIT_TEMPERATURE),
2545 expected.0
2546 );
2547 assert_eq!(
2548 ballistics_density_altitude_temperature_c(da_m, FFI_NO_EXPLICIT_TEMPERATURE),
2549 expected.1
2550 );
2551 assert_eq!(
2552 ballistics_density_altitude_pressure_hpa(da_m, FFI_NO_EXPLICIT_TEMPERATURE),
2553 expected.2
2554 );
2555
2556 assert!((ballistics_density_altitude_altitude_m(da_m, FFI_NO_EXPLICIT_TEMPERATURE) - da_m).abs() < 1e-6);
2558 }
2559
2560 #[test]
2561 fn density_altitude_ffi_explicit_temperature_is_honored_exactly() {
2562 let da_m = 500.0;
2563 let explicit_temp_c = 30.0;
2564 let expected =
2565 crate::atmosphere::resolve_atmosphere_for_density_altitude(da_m, Some(explicit_temp_c));
2566 assert_eq!(
2567 ballistics_density_altitude_temperature_c(da_m, explicit_temp_c),
2568 explicit_temp_c
2569 );
2570 assert_eq!(
2571 ballistics_density_altitude_temperature_c(da_m, explicit_temp_c),
2572 expected.1
2573 );
2574 assert_eq!(
2575 ballistics_density_altitude_pressure_hpa(da_m, explicit_temp_c),
2576 expected.2
2577 );
2578 assert_eq!(
2579 ballistics_density_altitude_altitude_m(da_m, explicit_temp_c),
2580 expected.0
2581 );
2582 }
2583
2584 #[test]
2585 fn density_altitude_ffi_non_finite_input_returns_nan() {
2586 assert!(
2587 ballistics_density_altitude_temperature_c(f64::INFINITY, FFI_NO_EXPLICIT_TEMPERATURE)
2588 .is_nan()
2589 );
2590 assert!(
2591 ballistics_density_altitude_pressure_hpa(f64::NAN, FFI_NO_EXPLICIT_TEMPERATURE).is_nan()
2592 );
2593 assert!(
2594 ballistics_density_altitude_altitude_m(f64::NEG_INFINITY, FFI_NO_EXPLICIT_TEMPERATURE)
2595 .is_nan()
2596 );
2597 }
2598
2599 #[test]
2604 fn ffi_trajectory_uses_the_density_altitude_derived_station_values() {
2605 let inputs = valid_trajectory_inputs();
2606 let da_m = 3000.0 * 0.3048; let altitude_m =
2608 ballistics_density_altitude_altitude_m(da_m, FFI_NO_EXPLICIT_TEMPERATURE);
2609 let temperature_c =
2610 ballistics_density_altitude_temperature_c(da_m, FFI_NO_EXPLICIT_TEMPERATURE);
2611 let pressure_hpa =
2612 ballistics_density_altitude_pressure_hpa(da_m, FFI_NO_EXPLICIT_TEMPERATURE);
2613
2614 let atmo_da = FFIAtmosphericConditions {
2615 temperature: temperature_c,
2616 pressure: pressure_hpa,
2617 humidity: 50.0,
2618 altitude: altitude_m,
2619 };
2620 let atmo_sea_level = FFIAtmosphericConditions {
2621 temperature: 15.0,
2622 pressure: 1013.25,
2623 humidity: 50.0,
2624 altitude: 0.0,
2625 };
2626
2627 unsafe {
2628 let a = ballistics_calculate_trajectory(
2629 &inputs,
2630 std::ptr::null(),
2631 &atmo_da,
2632 400.0,
2633 1.0,
2634 );
2635 let b = ballistics_calculate_trajectory(
2636 &inputs,
2637 std::ptr::null(),
2638 &atmo_sea_level,
2639 400.0,
2640 1.0,
2641 );
2642 assert!(!a.is_null() && !b.is_null());
2643 let drop_a = std::slice::from_raw_parts((*a).points, (*a).point_count as usize)
2644 .last()
2645 .unwrap()
2646 .position_y;
2647 let drop_b = std::slice::from_raw_parts((*b).points, (*b).point_count as usize)
2648 .last()
2649 .unwrap()
2650 .position_y;
2651 assert!(
2652 (drop_a - drop_b).abs() > 1e-6,
2653 "density-altitude-derived vs sea-level atmosphere must produce materially \
2654 different trajectories: {drop_a} vs {drop_b}"
2655 );
2656 ballistics_free_trajectory_result(a);
2657 ballistics_free_trajectory_result(b);
2658 }
2659 }
2660}