solverforge_core/solver/
termination.rs1use 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}