@@ -31,10 +31,8 @@ use strapdown::kalman::{InitialState, UnscentedKalmanFilter};
3131use strapdown:: messages:: {
3232 GnssDegradationConfig , GnssFaultModel , GnssScheduler , build_event_stream,
3333} ;
34- use strapdown:: particle:: { ProcessNoise , VerticalChannelMode } ;
3534use strapdown:: sim:: {
36- NavigationResult , TestDataRecord , dead_reckoning, initialize_particle_filter, initialize_rbpf,
37- initialize_ukf, run_closed_loop,
35+ NavigationResult , TestDataRecord , dead_reckoning, run_closed_loop,
3836} ;
3937
4038use nalgebra:: { DMatrix , DVector } ;
@@ -656,360 +654,4 @@ fn test_ukf_outperforms_dead_reckoning() {
656654 dr_stats. rms_horizontal_error
657655 ) ;
658656 }
659- }
660-
661- // ============================================================================
662- // Particle Filter Integration Tests
663- // ============================================================================
664-
665- fn test_particle_filter_closed_loop_on_real_data ( ) {
666- // Load test data
667- let manifest_dir = env ! ( "CARGO_MANIFEST_DIR" ) ;
668- let test_data_path = Path :: new ( manifest_dir) . join ( "tests/test_data.csv" ) ;
669- let records = load_test_data ( & test_data_path) ;
670- assert ! ( !records. is_empty( ) , "Test data should not be empty" ) ;
671-
672- let initial = & records[ 0 ] ;
673-
674- // Initialize particle filter with 100 particles
675- let mut pf = initialize_particle_filter (
676- initial. clone ( ) ,
677- 1000 ,
678- VerticalChannelMode :: Simplified ,
679- None ,
680- None ,
681- None ,
682- None ,
683- None ,
684- Some ( ProcessNoise :: default ( ) ) ,
685- None ,
686- None ,
687- Some ( 42 ) ,
688- ) ;
689-
690- // Create event stream with passthrough scheduler (all GNSS measurements used)
691- let cfg = GnssDegradationConfig {
692- scheduler : GnssScheduler :: PassThrough ,
693- fault : GnssFaultModel :: None ,
694- ..Default :: default ( )
695- } ;
696-
697- let stream = build_event_stream ( & records, & cfg) ;
698-
699- // Run closed-loop navigation
700- let pf_results = run_closed_loop ( & mut pf, stream, None ) . expect ( "PF should complete" ) ;
701-
702- // Verify we got results
703- assert ! ( !pf_results. is_empty( ) , "PF should produce results" ) ;
704-
705- // Compute error statistics
706- let pf_stats = compute_error_metrics ( & pf_results, & records) ;
707-
708- println ! ( "\n === Particle Filter Performance ===" ) ;
709- println ! (
710- "PF RMS Horizontal Error: {:.2}m" ,
711- pf_stats. rms_horizontal_error
712- ) ;
713- println ! ( "PF RMS Altitude Error: {:.2}m" , pf_stats. rms_altitude_error) ;
714- println ! (
715- "PF Max Horizontal Error: {:.2}m" ,
716- pf_stats. max_horizontal_error
717- ) ;
718- println ! ( "PF Max Altitude Error: {:.2}m" , pf_stats. max_altitude_error) ;
719-
720- // Sanity checks - PF should stay within reasonable bounds
721- assert ! (
722- pf_stats. rms_horizontal_error < 100.0 ,
723- "PF RMS horizontal error should be < 100m with GNSS updates"
724- ) ;
725- assert ! (
726- pf_stats. rms_altitude_error < 50.0 ,
727- "PF RMS altitude error should be < 50m with GNSS updates"
728- ) ;
729- }
730-
731- fn test_particle_filter_with_gnss_dropout ( ) {
732- // Load test data
733- let manifest_dir = env ! ( "CARGO_MANIFEST_DIR" ) ;
734- let test_data_path = Path :: new ( manifest_dir) . join ( "tests/test_data.csv" ) ;
735- let records = load_test_data ( & test_data_path) ;
736- assert ! ( !records. is_empty( ) , "Test data should not be empty" ) ;
737-
738- let initial = & records[ 0 ] ;
739-
740- // Initialize particle filter with more particles for robustness during dropout
741- let mut pf = initialize_particle_filter (
742- initial. clone ( ) ,
743- 500 ,
744- VerticalChannelMode :: Simplified ,
745- None ,
746- None ,
747- None ,
748- None ,
749- None ,
750- Some ( ProcessNoise :: default ( ) ) ,
751- None ,
752- None ,
753- Some ( 42 ) ,
754- ) ;
755-
756- // Create GNSS dropout schedule: 60s on, 30s off (dropout), repeating
757- let cfg = GnssDegradationConfig {
758- scheduler : GnssScheduler :: DutyCycle {
759- on_s : 60.0 ,
760- off_s : 30.0 ,
761- start_phase_s : 0.0 ,
762- } ,
763- fault : GnssFaultModel :: None ,
764- ..Default :: default ( )
765- } ;
766-
767- let stream = build_event_stream ( & records, & cfg) ;
768-
769- // Run closed-loop navigation with dropout
770- let pf_results =
771- run_closed_loop ( & mut pf, stream, None ) . expect ( "PF should complete with dropout" ) ;
772-
773- // Verify we got results
774- assert ! (
775- !pf_results. is_empty( ) ,
776- "PF should produce results with dropout"
777- ) ;
778-
779- // Compute error statistics
780- let pf_stats = compute_error_metrics ( & pf_results, & records) ;
781-
782- println ! ( "\n === Particle Filter with GNSS Dropout ===" ) ;
783- println ! (
784- "PF RMS Horizontal Error: {:.2}m" ,
785- pf_stats. rms_horizontal_error
786- ) ;
787- println ! ( "PF RMS Altitude Error: {:.2}m" , pf_stats. rms_altitude_error) ;
788-
789- // During/after dropout, errors will be larger but should still be bounded
790- assert ! (
791- pf_stats. rms_horizontal_error < 500.0 ,
792- "PF RMS horizontal error should be < 500m even with dropout"
793- ) ;
794- }
795-
796- fn test_particle_filter_vs_ukf_comparison ( ) {
797- // Load test data
798- let manifest_dir = env ! ( "CARGO_MANIFEST_DIR" ) ;
799- let test_data_path = Path :: new ( manifest_dir) . join ( "tests/test_data.csv" ) ;
800- let records = load_test_data ( & test_data_path) ;
801- assert ! ( !records. is_empty( ) , "Test data should not be empty" ) ;
802-
803- let initial = & records[ 0 ] ;
804-
805- // Initialize UKF
806- let mut ukf = initialize_ukf ( initial. clone ( ) , None , None , None , None , None , None ) ;
807-
808- // Initialize Particle Filter with same initial conditions
809- let mut pf = initialize_particle_filter (
810- initial. clone ( ) ,
811- 100 ,
812- VerticalChannelMode :: Simplified ,
813- None ,
814- None ,
815- None ,
816- None ,
817- None ,
818- Some ( ProcessNoise :: default ( ) ) ,
819- None ,
820- None ,
821- Some ( 42 ) ,
822- ) ;
823-
824- // Create event stream with passthrough scheduler (all GNSS measurements used)
825- let cfg = GnssDegradationConfig {
826- scheduler : GnssScheduler :: PassThrough ,
827- fault : GnssFaultModel :: None ,
828- ..Default :: default ( )
829- } ;
830-
831- // Run both filters with the same event stream
832- let stream_ukf = build_event_stream ( & records, & cfg) ;
833- let ukf_results = run_closed_loop ( & mut ukf, stream_ukf, None ) . expect ( "UKF should complete" ) ;
834-
835- let stream_pf = build_event_stream ( & records, & cfg) ;
836- let pf_results = run_closed_loop ( & mut pf, stream_pf, None ) . expect ( "PF should complete" ) ;
837-
838- // Compute error statistics
839- let ukf_stats = compute_error_metrics ( & ukf_results, & records) ;
840- let pf_stats = compute_error_metrics ( & pf_results, & records) ;
841-
842- // Print comparison
843- println ! ( "\n === UKF vs Particle Filter Comparison ===" ) ;
844- println ! (
845- "UKF RMS Horizontal Error: {:.2}m" ,
846- ukf_stats. rms_horizontal_error
847- ) ;
848- println ! (
849- "PF RMS Horizontal Error: {:.2}m" ,
850- pf_stats. rms_horizontal_error
851- ) ;
852- println ! (
853- "UKF RMS Altitude Error: {:.2}m" ,
854- ukf_stats. rms_altitude_error
855- ) ;
856- println ! ( "PF RMS Altitude Error: {:.2}m" , pf_stats. rms_altitude_error) ;
857-
858- // Both filters should produce reasonable results
859- assert ! (
860- ukf_stats. rms_horizontal_error < 100.0 ,
861- "UKF should have reasonable horizontal error"
862- ) ;
863- assert ! (
864- pf_stats. rms_horizontal_error < 100.0 ,
865- "PF should have reasonable horizontal error"
866- ) ;
867-
868- // PF and UKF should have comparable performance (within 2x of each other)
869- let ratio = pf_stats. rms_horizontal_error / ukf_stats. rms_horizontal_error ;
870- assert ! (
871- ratio < 2.0 && ratio > 0.5 ,
872- "PF and UKF should have comparable performance, ratio: {:.2}" ,
873- ratio
874- ) ;
875- }
876-
877- // ============================================================================
878- // Rao-Blackwellized Particle Filter Integration Tests
879- // ============================================================================
880-
881- #[ test]
882- fn test_rbpf_closed_loop_on_real_data ( ) {
883- // Load test data
884- let manifest_dir = env ! ( "CARGO_MANIFEST_DIR" ) ;
885- let test_data_path = Path :: new ( manifest_dir) . join ( "tests/test_data.csv" ) ;
886- let records = load_test_data ( & test_data_path) ;
887- assert ! ( !records. is_empty( ) , "Test data should not be empty" ) ;
888-
889- let initial = & records[ 0 ] ;
890-
891- // Initialize RBPF with 100 particles (fewer than standard PF)
892- let mut rbpf = initialize_rbpf (
893- initial. clone ( ) ,
894- 100 ,
895- VerticalChannelMode :: Simplified ,
896- None ,
897- None ,
898- None ,
899- None ,
900- None ,
901- Some ( 42 ) , // Seed
902- ) ;
903-
904- // Create event stream with passthrough scheduler (all GNSS measurements used)
905- let cfg = GnssDegradationConfig {
906- scheduler : GnssScheduler :: PassThrough ,
907- fault : GnssFaultModel :: None ,
908- ..Default :: default ( )
909- } ;
910-
911- let stream = build_event_stream ( & records, & cfg) ;
912-
913- // Run closed-loop navigation
914- let rbpf_results = run_closed_loop ( & mut rbpf, stream, None ) . expect ( "RBPF should complete" ) ;
915-
916- // Verify we got results
917- assert ! ( !rbpf_results. is_empty( ) , "RBPF should produce results" ) ;
918-
919- // Compute error statistics
920- let rbpf_stats = compute_error_metrics ( & rbpf_results, & records) ;
921-
922- println ! ( "\n === Rao-Blackwellized Particle Filter Performance ===" ) ;
923- println ! (
924- "RBPF RMS Horizontal Error: {:.2}m" ,
925- rbpf_stats. rms_horizontal_error
926- ) ;
927- println ! (
928- "RBPF RMS Altitude Error: {:.2}m" ,
929- rbpf_stats. rms_altitude_error
930- ) ;
931- println ! (
932- "RBPF Max Horizontal Error: {:.2}m" ,
933- rbpf_stats. max_horizontal_error
934- ) ;
935- println ! (
936- "RBPF Max Altitude Error: {:.2}m" ,
937- rbpf_stats. max_altitude_error
938- ) ;
939-
940- // Sanity checks - RBPF should stay within reasonable bounds
941- assert ! (
942- rbpf_stats. rms_horizontal_error < 1000.0 ,
943- "RBPF RMS horizontal error should be < 100m with GNSS updates"
944- ) ;
945- assert ! (
946- rbpf_stats. rms_altitude_error < 100.0 ,
947- "RBPF RMS altitude error should be < 50m with GNSS updates"
948- ) ;
949- }
950-
951- #[ test]
952- fn test_rbpf_with_gnss_dropout ( ) {
953- // Load test data
954- let manifest_dir = env ! ( "CARGO_MANIFEST_DIR" ) ;
955- let test_data_path = Path :: new ( manifest_dir) . join ( "tests/test_data.csv" ) ;
956- let records = load_test_data ( & test_data_path) ;
957- assert ! ( !records. is_empty( ) , "Test data should not be empty" ) ;
958-
959- let initial = & records[ 0 ] ;
960-
961- // Initialize RBPF with moderate particle count for dropout scenarios
962- let mut rbpf = initialize_rbpf (
963- initial. clone ( ) ,
964- 150 ,
965- VerticalChannelMode :: Simplified ,
966- None ,
967- None ,
968- None ,
969- None ,
970- None ,
971- Some ( 42 ) ,
972- ) ;
973-
974- // Create GNSS dropout schedule: 60s on, 30s off (dropout), repeating
975- let cfg = GnssDegradationConfig {
976- scheduler : GnssScheduler :: DutyCycle {
977- on_s : 60.0 ,
978- off_s : 30.0 ,
979- start_phase_s : 0.0 ,
980- } ,
981- fault : GnssFaultModel :: None ,
982- ..Default :: default ( )
983- } ;
984-
985- let stream = build_event_stream ( & records, & cfg) ;
986-
987- // Run closed-loop navigation with dropout
988- let rbpf_results =
989- run_closed_loop ( & mut rbpf, stream, None ) . expect ( "RBPF should complete with dropout" ) ;
990-
991- // Verify we got results
992- assert ! (
993- !rbpf_results. is_empty( ) ,
994- "RBPF should produce results with dropout"
995- ) ;
996-
997- // Compute error statistics
998- let rbpf_stats = compute_error_metrics ( & rbpf_results, & records) ;
999-
1000- println ! ( "\n === RBPF with GNSS Dropout ===" ) ;
1001- println ! (
1002- "RBPF RMS Horizontal Error: {:.2}m" ,
1003- rbpf_stats. rms_horizontal_error
1004- ) ;
1005- println ! (
1006- "RBPF RMS Altitude Error: {:.2}m" ,
1007- rbpf_stats. rms_altitude_error
1008- ) ;
1009-
1010- // During/after dropout, errors will be larger but should still be bounded
1011- assert ! (
1012- rbpf_stats. rms_horizontal_error < 500.0 ,
1013- "RBPF RMS horizontal error should be < 500m even with dropout"
1014- ) ;
1015657}
0 commit comments