@@ -44,6 +44,7 @@ use log::{debug, info, warn};
4444use std:: fmt:: { Debug , Display } ;
4545use std:: io:: { self , Read , Write } ;
4646use std:: path:: Path ;
47+ use std:: time:: { Duration as StdDuration , Instant } ;
4748
4849use anyhow:: { Result , bail} ;
4950use chrono:: { DateTime , Duration , Utc } ;
@@ -60,9 +61,11 @@ use crate::messages::{Event, EventStream, GnssFaultModel, GnssScheduler};
6061
6162use crate :: { IMUData , StrapdownState , forward} ;
6263use health:: HealthMonitor ;
64+ use execution:: ExecutionMonitor ;
6365
6466// Re-export HealthLimits for easier access in tests and external users
6567pub use health:: HealthLimits ;
68+ pub use execution:: { ExecutionLimits , ExecutionMonitor } ;
6669
6770pub const DEFAULT_PROCESS_NOISE : [ f64 ; 15 ] = [
6871 // Default process noise if not provided
@@ -83,6 +86,10 @@ pub const DEFAULT_PROCESS_NOISE: [f64; 15] = [
8386 1e-8 , // gyro bias z noise
8487] ;
8588
89+ pub const DEFAULT_MAX_WALL_CLOCK_RATIO : f64 = 0.25 ;
90+ pub const DEFAULT_MAX_WALL_CLOCK_S : f64 = 1200.0 ;
91+ pub const DEFAULT_MAX_NO_PROGRESS_S : f64 = 600.0 ;
92+
8693fn de_f64_nan < ' de , D > ( deserializer : D ) -> Result < f64 , D :: Error >
8794where
8895 D : Deserializer < ' de > ,
@@ -1903,19 +1910,31 @@ pub fn dead_reckoning(records: &[TestDataRecord]) -> Vec<NavigationResult> {
19031910/// * `filter` - Mutable reference to a type implementing NavigationFilter
19041911/// * `stream` - Event stream containing IMU and measurement events
19051912/// * `health_limits` - Optional health limits for monitoring
1913+ /// * `execution_limits` - Optional wall-clock and no-progress limits
19061914///
19071915/// # Returns
19081916/// * `Vec<NavigationResult>` - A vector of navigation results
19091917pub fn run_closed_loop < F : NavigationFilter > (
19101918 filter : & mut F ,
19111919 stream : EventStream ,
19121920 health_limits : Option < HealthLimits > ,
1921+ execution_limits : Option < ExecutionLimits > ,
19131922) -> anyhow:: Result < Vec < NavigationResult > > {
19141923 let start_time = stream. start_time ;
19151924 let mut results: Vec < NavigationResult > = Vec :: with_capacity ( stream. events . len ( ) ) ;
19161925 let total = stream. events . len ( ) ;
19171926 let mut last_ts: Option < DateTime < Utc > > = None ;
19181927 let mut monitor = HealthMonitor :: new ( health_limits. unwrap_or_default ( ) ) ;
1928+ let sim_duration_s = stream
1929+ . events
1930+ . last ( )
1931+ . map ( |event| match event {
1932+ Event :: Imu { elapsed_s, .. } => * elapsed_s,
1933+ Event :: Measurement { elapsed_s, .. } => * elapsed_s,
1934+ } )
1935+ . unwrap_or ( 0.0 ) ;
1936+ let mut execution_monitor =
1937+ execution_limits. map ( |limits| ExecutionMonitor :: new ( limits, sim_duration_s) ) ;
19191938
19201939 info ! (
19211940 "Starting closed-loop navigation filter with {} events" ,
@@ -2005,6 +2024,10 @@ pub fn run_closed_loop<F: NavigationFilter>(
20052024 debug ! ( "Filter state at {}: {:?}" , ts, mean) ;
20062025 results. push ( NavigationResult :: from ( ( & ts, & mean, & cov) ) ) ;
20072026 }
2027+
2028+ if let Some ( ref mut monitor) = execution_monitor {
2029+ monitor. check ( "closed-loop" ) ?;
2030+ }
20082031 }
20092032 debug ! ( "Closed-loop simulation complete" ) ;
20102033 Ok ( results)
@@ -2525,6 +2548,125 @@ pub fn print_sim_status<F: NavigationFilter>(filter: &F) {
25252548 ) ;
25262549}
25272550
2551+ pub mod execution {
2552+ use super :: * ;
2553+
2554+ #[ derive( Clone , Debug , Serialize , Deserialize ) ]
2555+ pub struct ExecutionLimits {
2556+ /// Max wall-clock time as a ratio of simulated duration (<= 0 disables).
2557+ #[ serde( default = "default_max_wall_clock_ratio" ) ]
2558+ pub max_wall_clock_ratio : f64 ,
2559+ /// Max wall-clock time per trajectory in seconds (<= 0 disables).
2560+ #[ serde( default = "default_max_wall_clock_s" ) ]
2561+ pub max_wall_clock_s : f64 ,
2562+ /// Max wall-clock time without progress in seconds (<= 0 disables).
2563+ #[ serde( default = "default_max_no_progress_s" ) ]
2564+ pub max_no_progress_s : f64 ,
2565+ }
2566+
2567+ fn default_max_wall_clock_ratio ( ) -> f64 {
2568+ DEFAULT_MAX_WALL_CLOCK_RATIO
2569+ }
2570+
2571+ fn default_max_wall_clock_s ( ) -> f64 {
2572+ DEFAULT_MAX_WALL_CLOCK_S
2573+ }
2574+
2575+ fn default_max_no_progress_s ( ) -> f64 {
2576+ DEFAULT_MAX_NO_PROGRESS_S
2577+ }
2578+
2579+ impl Default for ExecutionLimits {
2580+ fn default ( ) -> Self {
2581+ Self {
2582+ max_wall_clock_ratio : default_max_wall_clock_ratio ( ) ,
2583+ max_wall_clock_s : default_max_wall_clock_s ( ) ,
2584+ max_no_progress_s : default_max_no_progress_s ( ) ,
2585+ }
2586+ }
2587+ }
2588+
2589+ #[ derive( Clone , Debug ) ]
2590+ pub struct ExecutionMonitor {
2591+ start_time : Instant ,
2592+ last_progress : Instant ,
2593+ max_wall_clock : Option < StdDuration > ,
2594+ max_no_progress : Option < StdDuration > ,
2595+ }
2596+
2597+ impl ExecutionMonitor {
2598+ pub fn new ( limits : ExecutionLimits , sim_duration_s : f64 ) -> Self {
2599+ let max_wall_clock = compute_max_wall_clock (
2600+ sim_duration_s,
2601+ limits. max_wall_clock_ratio ,
2602+ limits. max_wall_clock_s ,
2603+ ) ;
2604+ let max_no_progress = if limits. max_no_progress_s > 0.0 {
2605+ Some ( StdDuration :: from_secs_f64 ( limits. max_no_progress_s ) )
2606+ } else {
2607+ None
2608+ } ;
2609+ let now = Instant :: now ( ) ;
2610+
2611+ Self {
2612+ start_time : now,
2613+ last_progress : now,
2614+ max_wall_clock,
2615+ max_no_progress,
2616+ }
2617+ }
2618+
2619+ pub fn check ( & mut self , context : & str ) -> Result < ( ) > {
2620+ let now = Instant :: now ( ) ;
2621+ if let Some ( max_wall_clock) = self . max_wall_clock {
2622+ if now. duration_since ( self . start_time ) > max_wall_clock {
2623+ bail ! (
2624+ "Execution timeout ({context}): exceeded wall-clock limit of {:.2} s" ,
2625+ max_wall_clock. as_secs_f64( )
2626+ ) ;
2627+ }
2628+ }
2629+ if let Some ( max_no_progress) = self . max_no_progress {
2630+ let since_progress = now. duration_since ( self . last_progress ) ;
2631+ if since_progress > max_no_progress {
2632+ bail ! (
2633+ "Execution timeout ({context}): no progress for {:.2} s (limit {:.2} s)" ,
2634+ since_progress. as_secs_f64( ) ,
2635+ max_no_progress. as_secs_f64( )
2636+ ) ;
2637+ }
2638+ }
2639+ self . last_progress = now;
2640+ Ok ( ( ) )
2641+ }
2642+ }
2643+
2644+ fn compute_max_wall_clock (
2645+ sim_duration_s : f64 ,
2646+ max_ratio : f64 ,
2647+ max_wall_clock_s : f64 ,
2648+ ) -> Option < StdDuration > {
2649+ let mut max_s = None ;
2650+ if sim_duration_s > 0.0 && max_ratio > 0.0 {
2651+ max_s = Some ( sim_duration_s * max_ratio) ;
2652+ }
2653+ if max_wall_clock_s > 0.0 {
2654+ max_s = Some ( match max_s {
2655+ Some ( current) => current. min ( max_wall_clock_s) ,
2656+ None => max_wall_clock_s,
2657+ } ) ;
2658+ }
2659+
2660+ max_s. and_then ( |s| {
2661+ if s > 0.0 {
2662+ Some ( StdDuration :: from_secs_f64 ( s) )
2663+ } else {
2664+ None
2665+ }
2666+ } )
2667+ }
2668+ }
2669+
25282670pub mod health {
25292671 use super :: * ;
25302672
@@ -3004,6 +3146,9 @@ pub struct SimulationConfig {
30043146 /// Generate performance plot comparing navigation output to GPS measurements
30053147 #[ serde( default ) ]
30063148 pub generate_plot : bool ,
3149+ /// Execution time limits (wall-clock and no-progress)
3150+ #[ serde( default ) ]
3151+ pub execution_limits : ExecutionLimits ,
30073152 /// Logging configuration
30083153 #[ serde( default ) ]
30093154 pub logging : LoggingConfig ,
@@ -3034,6 +3179,7 @@ impl Default for SimulationConfig {
30343179 seed : default_seed ( ) ,
30353180 parallel : false ,
30363181 generate_plot : false ,
3182+ execution_limits : ExecutionLimits :: default ( ) ,
30373183 logging : LoggingConfig :: default ( ) ,
30383184 closed_loop : Some ( ClosedLoopConfig :: default ( ) ) ,
30393185 particle_filter : None ,
@@ -3704,7 +3850,7 @@ mod tests {
37043850 events : vec ! [ event] ,
37053851 } ;
37063852
3707- let res = run_closed_loop ( & mut ukf, stream, None ) ;
3853+ let res = run_closed_loop ( & mut ukf, stream, None , None ) ;
37083854 assert ! ( !res. unwrap( ) . is_empty( ) ) ;
37093855 }
37103856 #[ test]
@@ -4084,6 +4230,20 @@ mod tests {
40844230 assert ! ( limits. cov_diag_max > 0.0 ) ;
40854231 }
40864232
4233+ #[ test]
4234+ fn test_execution_monitor_no_progress_timeout ( ) {
4235+ let limits = ExecutionLimits {
4236+ max_wall_clock_ratio : 0.0 ,
4237+ max_wall_clock_s : 0.0 ,
4238+ max_no_progress_s : 0.01 ,
4239+ } ;
4240+ let mut monitor = ExecutionMonitor :: new ( limits, 1.0 ) ;
4241+
4242+ std:: thread:: sleep ( std:: time:: Duration :: from_millis ( 20 ) ) ;
4243+ let result = monitor. check ( "test" ) ;
4244+ assert ! ( result. is_err( ) ) ;
4245+ }
4246+
40874247 #[ test]
40884248 fn test_health_monitor_check_valid_state ( ) {
40894249 let limits = HealthLimits :: default ( ) ;
@@ -4574,7 +4734,7 @@ mod tests {
45744734 } ;
45754735
45764736 let health_limits = HealthLimits :: default ( ) ;
4577- let result = run_closed_loop ( & mut ukf, stream, Some ( health_limits) ) ;
4737+ let result = run_closed_loop ( & mut ukf, stream, Some ( health_limits) , None ) ;
45784738 assert ! ( result. is_ok( ) ) ;
45794739 }
45804740
0 commit comments