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