solverforge_core/solver/
termination.rs

1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
4#[serde(rename_all = "camelCase")]
5pub struct TerminationConfig {
6    #[serde(skip_serializing_if = "Option::is_none")]
7    pub spent_limit: Option<String>,
8    #[serde(skip_serializing_if = "Option::is_none")]
9    pub unimproved_spent_limit: Option<String>,
10    #[serde(skip_serializing_if = "Option::is_none")]
11    pub unimproved_step_count: Option<u64>,
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub best_score_limit: Option<String>,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub best_score_feasible: Option<bool>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub step_count_limit: Option<u64>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub move_count_limit: Option<u64>,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub score_calculation_count_limit: Option<u64>,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub diminished_returns: Option<DiminishedReturnsConfig>,
24}
25
26impl TerminationConfig {
27    pub fn new() -> Self {
28        Self::default()
29    }
30
31    pub fn with_spent_limit(mut self, limit: impl Into<String>) -> Self {
32        self.spent_limit = Some(limit.into());
33        self
34    }
35
36    pub fn with_unimproved_spent_limit(mut self, limit: impl Into<String>) -> Self {
37        self.unimproved_spent_limit = Some(limit.into());
38        self
39    }
40
41    pub fn with_unimproved_step_count(mut self, count: u64) -> Self {
42        self.unimproved_step_count = Some(count);
43        self
44    }
45
46    pub fn with_best_score_limit(mut self, limit: impl Into<String>) -> Self {
47        self.best_score_limit = Some(limit.into());
48        self
49    }
50
51    pub fn with_best_score_feasible(mut self, feasible: bool) -> Self {
52        self.best_score_feasible = Some(feasible);
53        self
54    }
55
56    pub fn with_step_count_limit(mut self, count: u64) -> Self {
57        self.step_count_limit = Some(count);
58        self
59    }
60
61    pub fn with_move_count_limit(mut self, count: u64) -> Self {
62        self.move_count_limit = Some(count);
63        self
64    }
65
66    pub fn with_score_calculation_count_limit(mut self, count: u64) -> Self {
67        self.score_calculation_count_limit = Some(count);
68        self
69    }
70
71    pub fn with_diminished_returns(mut self, config: DiminishedReturnsConfig) -> Self {
72        self.diminished_returns = Some(config);
73        self
74    }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
78#[serde(rename_all = "camelCase")]
79pub struct DiminishedReturnsConfig {
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub minimum_improvement_ratio: Option<String>,
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub slow_improvement_limit: Option<String>,
84    #[serde(skip_serializing_if = "Option::is_none")]
85    pub slow_improvement_spent_limit: Option<String>,
86}
87
88impl DiminishedReturnsConfig {
89    pub fn new() -> Self {
90        Self::default()
91    }
92
93    pub fn with_minimum_improvement_ratio(mut self, ratio: impl Into<String>) -> Self {
94        self.minimum_improvement_ratio = Some(ratio.into());
95        self
96    }
97
98    pub fn with_slow_improvement_limit(mut self, limit: impl Into<String>) -> Self {
99        self.slow_improvement_limit = Some(limit.into());
100        self
101    }
102
103    pub fn with_slow_improvement_spent_limit(mut self, limit: impl Into<String>) -> Self {
104        self.slow_improvement_spent_limit = Some(limit.into());
105        self
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn test_termination_config_new() {
115        let config = TerminationConfig::new();
116        assert!(config.spent_limit.is_none());
117        assert!(config.unimproved_spent_limit.is_none());
118    }
119
120    #[test]
121    fn test_termination_config_spent_limit() {
122        let config = TerminationConfig::new().with_spent_limit("PT5M");
123        assert_eq!(config.spent_limit, Some("PT5M".to_string()));
124    }
125
126    #[test]
127    fn test_termination_config_unimproved_spent_limit() {
128        let config = TerminationConfig::new().with_unimproved_spent_limit("PT30S");
129        assert_eq!(config.unimproved_spent_limit, Some("PT30S".to_string()));
130    }
131
132    #[test]
133    fn test_termination_config_unimproved_step_count() {
134        let config = TerminationConfig::new().with_unimproved_step_count(100);
135        assert_eq!(config.unimproved_step_count, Some(100));
136    }
137
138    #[test]
139    fn test_termination_config_best_score_limit() {
140        let config = TerminationConfig::new().with_best_score_limit("0hard/-100soft");
141        assert_eq!(config.best_score_limit, Some("0hard/-100soft".to_string()));
142    }
143
144    #[test]
145    fn test_termination_config_best_score_feasible() {
146        let config = TerminationConfig::new().with_best_score_feasible(true);
147        assert_eq!(config.best_score_feasible, Some(true));
148    }
149
150    #[test]
151    fn test_termination_config_step_count_limit() {
152        let config = TerminationConfig::new().with_step_count_limit(1000);
153        assert_eq!(config.step_count_limit, Some(1000));
154    }
155
156    #[test]
157    fn test_termination_config_move_count_limit() {
158        let config = TerminationConfig::new().with_move_count_limit(10000);
159        assert_eq!(config.move_count_limit, Some(10000));
160    }
161
162    #[test]
163    fn test_termination_config_score_calculation_count_limit() {
164        let config = TerminationConfig::new().with_score_calculation_count_limit(1000000);
165        assert_eq!(config.score_calculation_count_limit, Some(1000000));
166    }
167
168    #[test]
169    fn test_termination_config_chained() {
170        let config = TerminationConfig::new()
171            .with_spent_limit("PT10M")
172            .with_unimproved_spent_limit("PT1M")
173            .with_best_score_feasible(true);
174
175        assert_eq!(config.spent_limit, Some("PT10M".to_string()));
176        assert_eq!(config.unimproved_spent_limit, Some("PT1M".to_string()));
177        assert_eq!(config.best_score_feasible, Some(true));
178    }
179
180    #[test]
181    fn test_diminished_returns_config_new() {
182        let config = DiminishedReturnsConfig::new();
183        assert!(config.minimum_improvement_ratio.is_none());
184    }
185
186    #[test]
187    fn test_diminished_returns_config_with_ratio() {
188        let config = DiminishedReturnsConfig::new().with_minimum_improvement_ratio("0.001");
189        assert_eq!(config.minimum_improvement_ratio, Some("0.001".to_string()));
190    }
191
192    #[test]
193    fn test_termination_config_with_diminished_returns() {
194        let dr = DiminishedReturnsConfig::new().with_minimum_improvement_ratio("0.01");
195        let config = TerminationConfig::new().with_diminished_returns(dr);
196        assert!(config.diminished_returns.is_some());
197    }
198
199    #[test]
200    fn test_termination_config_json_serialization() {
201        let config = TerminationConfig::new()
202            .with_spent_limit("PT5M")
203            .with_best_score_feasible(true);
204
205        let json = serde_json::to_string(&config).unwrap();
206        assert!(json.contains("\"spentLimit\":\"PT5M\""));
207        assert!(json.contains("\"bestScoreFeasible\":true"));
208
209        let parsed: TerminationConfig = serde_json::from_str(&json).unwrap();
210        assert_eq!(parsed, config);
211    }
212
213    #[test]
214    fn test_termination_config_json_omits_none() {
215        let config = TerminationConfig::new().with_spent_limit("PT1H");
216        let json = serde_json::to_string(&config).unwrap();
217        assert!(!json.contains("unimprovedSpentLimit"));
218        assert!(!json.contains("bestScoreLimit"));
219    }
220
221    #[test]
222    fn test_diminished_returns_json_serialization() {
223        let config = DiminishedReturnsConfig::new()
224            .with_minimum_improvement_ratio("0.001")
225            .with_slow_improvement_limit("PT30S");
226
227        let json = serde_json::to_string(&config).unwrap();
228        assert!(json.contains("\"minimumImprovementRatio\":\"0.001\""));
229        assert!(json.contains("\"slowImprovementLimit\":\"PT30S\""));
230
231        let parsed: DiminishedReturnsConfig = serde_json::from_str(&json).unwrap();
232        assert_eq!(parsed, config);
233    }
234
235    #[test]
236    fn test_termination_config_clone() {
237        let config = TerminationConfig::new().with_spent_limit("PT5M");
238        let cloned = config.clone();
239        assert_eq!(config, cloned);
240    }
241
242    #[test]
243    fn test_termination_config_debug() {
244        let config = TerminationConfig::new().with_spent_limit("PT5M");
245        let debug = format!("{:?}", config);
246        assert!(debug.contains("TerminationConfig"));
247        assert!(debug.contains("PT5M"));
248    }
249}