Skip to main content

converge_optimization/gate/
budgets.rs

1//! Resource budgets for solver execution
2
3use serde::{Deserialize, Serialize};
4use std::time::Duration;
5
6/// Resource budgets for solver execution
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct SolveBudgets {
9    /// Maximum wall-clock time for solving (in seconds)
10    #[serde(with = "duration_serde")]
11    pub time_limit: Duration,
12    /// Maximum solver iterations
13    pub iteration_limit: usize,
14    /// Maximum candidate solutions to evaluate
15    pub candidate_cap: usize,
16    /// Maximum memory usage (bytes, 0 = unlimited)
17    pub memory_limit_bytes: usize,
18}
19
20impl Default for SolveBudgets {
21    fn default() -> Self {
22        Self {
23            time_limit: Duration::from_secs(30),
24            iteration_limit: 100_000,
25            candidate_cap: 1_000,
26            memory_limit_bytes: 0,
27        }
28    }
29}
30
31impl SolveBudgets {
32    /// Create budgets with time limit only
33    pub fn with_time_limit(seconds: u64) -> Self {
34        Self {
35            time_limit: Duration::from_secs(seconds),
36            ..Default::default()
37        }
38    }
39
40    /// Create strict budgets for testing
41    pub fn strict(time_seconds: u64, iterations: usize, candidates: usize) -> Self {
42        Self {
43            time_limit: Duration::from_secs(time_seconds),
44            iteration_limit: iterations,
45            candidate_cap: candidates,
46            memory_limit_bytes: 0,
47        }
48    }
49
50    /// Validate budgets are reasonable
51    pub fn validate(&self) -> crate::Result<()> {
52        if self.time_limit.is_zero() {
53            return Err(crate::Error::invalid_input("time_limit must be positive"));
54        }
55        if self.iteration_limit == 0 {
56            return Err(crate::Error::invalid_input(
57                "iteration_limit must be positive",
58            ));
59        }
60        if self.candidate_cap == 0 {
61            return Err(crate::Error::invalid_input(
62                "candidate_cap must be positive",
63            ));
64        }
65        Ok(())
66    }
67
68    /// Convert to SolverParams for existing solvers
69    pub fn to_solver_params(&self, seed: u64) -> crate::SolverParams {
70        crate::SolverParams {
71            time_limit_seconds: self.time_limit.as_secs_f64(),
72            iteration_limit: self.iteration_limit,
73            num_threads: 0,
74            random_seed: seed,
75            verbosity: 0,
76        }
77    }
78
79    /// Check if time budget allows more work
80    pub fn has_time_remaining(&self, elapsed: Duration) -> bool {
81        elapsed < self.time_limit
82    }
83}
84
85/// Serde support for Duration as seconds
86mod duration_serde {
87    use serde::{Deserialize, Deserializer, Serialize, Serializer};
88    use std::time::Duration;
89
90    pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
91    where
92        S: Serializer,
93    {
94        duration.as_secs_f64().serialize(serializer)
95    }
96
97    pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
98    where
99        D: Deserializer<'de>,
100    {
101        let secs = f64::deserialize(deserializer)?;
102        Ok(Duration::from_secs_f64(secs))
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_default_budgets() {
112        let budgets = SolveBudgets::default();
113        assert_eq!(budgets.time_limit, Duration::from_secs(30));
114        assert_eq!(budgets.iteration_limit, 100_000);
115        assert_eq!(budgets.candidate_cap, 1_000);
116    }
117
118    #[test]
119    fn test_validate_budgets() {
120        let budgets = SolveBudgets::default();
121        assert!(budgets.validate().is_ok());
122
123        let invalid = SolveBudgets {
124            time_limit: Duration::ZERO,
125            ..Default::default()
126        };
127        assert!(invalid.validate().is_err());
128    }
129
130    #[test]
131    fn test_to_solver_params() {
132        let budgets = SolveBudgets::with_time_limit(60);
133        let params = budgets.to_solver_params(42);
134        assert_eq!(params.time_limit_seconds, 60.0);
135        assert_eq!(params.random_seed, 42);
136    }
137
138    #[test]
139    fn test_serde_roundtrip() {
140        let budgets = SolveBudgets::with_time_limit(45);
141        let json = serde_json::to_string(&budgets).unwrap();
142        let restored: SolveBudgets = serde_json::from_str(&json).unwrap();
143        assert_eq!(restored.time_limit, budgets.time_limit);
144    }
145}