1use std::path::Path;
6use std::time::Duration;
7
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11#[derive(Debug, Error)]
13pub enum ConfigError {
14 #[error("IO error: {0}")]
15 Io(#[from] std::io::Error),
16
17 #[error("TOML parse error: {0}")]
18 Toml(#[from] toml::de::Error),
19
20 #[error("YAML parse error: {0}")]
21 Yaml(#[from] serde_yaml::Error),
22
23 #[error("Invalid configuration: {0}")]
24 Invalid(String),
25}
26
27#[derive(Debug, Clone, Default, Deserialize, Serialize)]
29#[serde(rename_all = "snake_case")]
30pub struct SolverConfig {
31 #[serde(default)]
33 pub environment_mode: EnvironmentMode,
34
35 #[serde(default)]
37 pub random_seed: Option<u64>,
38
39 #[serde(default)]
41 pub move_thread_count: MoveThreadCount,
42
43 #[serde(default)]
45 pub termination: Option<TerminationConfig>,
46
47 #[serde(default)]
49 pub score_director: Option<ScoreDirectorConfig>,
50
51 #[serde(default)]
53 pub phases: Vec<PhaseConfig>,
54}
55
56impl SolverConfig {
57 pub fn new() -> Self {
59 Self::default()
60 }
61
62 pub fn from_toml_file(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
64 let contents = std::fs::read_to_string(path)?;
65 Self::from_toml_str(&contents)
66 }
67
68 pub fn from_toml_str(s: &str) -> Result<Self, ConfigError> {
70 Ok(toml::from_str(s)?)
71 }
72
73 pub fn from_yaml_file(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
75 let contents = std::fs::read_to_string(path)?;
76 Self::from_yaml_str(&contents)
77 }
78
79 pub fn from_yaml_str(s: &str) -> Result<Self, ConfigError> {
81 Ok(serde_yaml::from_str(s)?)
82 }
83
84 pub fn with_termination_seconds(mut self, seconds: u64) -> Self {
86 self.termination = Some(TerminationConfig {
87 seconds_spent_limit: Some(seconds),
88 ..self.termination.unwrap_or_default()
89 });
90 self
91 }
92
93 pub fn with_random_seed(mut self, seed: u64) -> Self {
95 self.random_seed = Some(seed);
96 self
97 }
98
99 pub fn with_phase(mut self, phase: PhaseConfig) -> Self {
101 self.phases.push(phase);
102 self
103 }
104}
105
106#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
108#[serde(rename_all = "snake_case")]
109pub enum EnvironmentMode {
110 #[default]
112 NonReproducible,
113
114 Reproducible,
116
117 FastAssert,
119
120 FullAssert,
122}
123
124#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
126#[serde(rename_all = "snake_case")]
127pub enum MoveThreadCount {
128 #[default]
130 Auto,
131
132 None,
134
135 Count(usize),
137}
138
139#[derive(Debug, Clone, Default, Deserialize, Serialize)]
141#[serde(rename_all = "snake_case")]
142pub struct TerminationConfig {
143 pub seconds_spent_limit: Option<u64>,
145
146 pub minutes_spent_limit: Option<u64>,
148
149 pub best_score_limit: Option<String>,
151
152 pub step_count_limit: Option<usize>,
154
155 pub unimproved_step_count_limit: Option<usize>,
157
158 pub unimproved_seconds_spent_limit: Option<u64>,
160}
161
162impl TerminationConfig {
163 pub fn time_limit(&self) -> Option<Duration> {
165 let seconds =
166 self.seconds_spent_limit.unwrap_or(0) + self.minutes_spent_limit.unwrap_or(0) * 60;
167 if seconds > 0 {
168 Some(Duration::from_secs(seconds))
169 } else {
170 None
171 }
172 }
173}
174
175#[derive(Debug, Clone, Default, Deserialize, Serialize)]
177#[serde(rename_all = "snake_case")]
178pub struct ScoreDirectorConfig {
179 pub constraint_provider: Option<String>,
181
182 #[serde(default)]
184 pub constraint_match_enabled: bool,
185}
186
187#[derive(Debug, Clone, Deserialize, Serialize)]
189#[serde(tag = "type", rename_all = "snake_case")]
190pub enum PhaseConfig {
191 ConstructionHeuristic(ConstructionHeuristicConfig),
193
194 LocalSearch(LocalSearchConfig),
196
197 ExhaustiveSearch(ExhaustiveSearchConfig),
199
200 PartitionedSearch(PartitionedSearchConfig),
202
203 Custom(CustomPhaseConfig),
205}
206
207#[derive(Debug, Clone, Default, Deserialize, Serialize)]
209#[serde(rename_all = "snake_case")]
210pub struct ConstructionHeuristicConfig {
211 #[serde(default)]
213 pub construction_heuristic_type: ConstructionHeuristicType,
214
215 pub termination: Option<TerminationConfig>,
217}
218
219#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
221#[serde(rename_all = "snake_case")]
222pub enum ConstructionHeuristicType {
223 #[default]
225 FirstFit,
226
227 FirstFitDecreasing,
229
230 WeakestFit,
232
233 WeakestFitDecreasing,
235
236 StrongestFit,
238
239 StrongestFitDecreasing,
241
242 CheapestInsertion,
244
245 AllocateEntityFromQueue,
247
248 AllocateToValueFromQueue,
250}
251
252#[derive(Debug, Clone, Default, Deserialize, Serialize)]
254#[serde(rename_all = "snake_case")]
255pub struct LocalSearchConfig {
256 pub acceptor: Option<AcceptorConfig>,
258
259 pub forager: Option<ForagerConfig>,
261
262 pub move_selector: Option<MoveSelectorConfig>,
264
265 pub termination: Option<TerminationConfig>,
267}
268
269#[derive(Debug, Clone, Deserialize, Serialize)]
271#[serde(tag = "type", rename_all = "snake_case")]
272pub enum AcceptorConfig {
273 HillClimbing,
275
276 TabuSearch(TabuSearchConfig),
278
279 SimulatedAnnealing(SimulatedAnnealingConfig),
281
282 LateAcceptance(LateAcceptanceConfig),
284
285 GreatDeluge(GreatDelugeConfig),
287}
288
289#[derive(Debug, Clone, Default, Deserialize, Serialize)]
291#[serde(rename_all = "snake_case")]
292pub struct TabuSearchConfig {
293 pub entity_tabu_size: Option<usize>,
295
296 pub value_tabu_size: Option<usize>,
298
299 pub move_tabu_size: Option<usize>,
301
302 pub undo_move_tabu_size: Option<usize>,
304}
305
306#[derive(Debug, Clone, Default, Deserialize, Serialize)]
308#[serde(rename_all = "snake_case")]
309pub struct SimulatedAnnealingConfig {
310 pub starting_temperature: Option<String>,
312}
313
314#[derive(Debug, Clone, Default, Deserialize, Serialize)]
316#[serde(rename_all = "snake_case")]
317pub struct LateAcceptanceConfig {
318 pub late_acceptance_size: Option<usize>,
320}
321
322#[derive(Debug, Clone, Default, Deserialize, Serialize)]
324#[serde(rename_all = "snake_case")]
325pub struct GreatDelugeConfig {
326 pub water_level_increase_ratio: Option<f64>,
328}
329
330#[derive(Debug, Clone, Default, Deserialize, Serialize)]
332#[serde(rename_all = "snake_case")]
333pub struct ForagerConfig {
334 pub accepted_count_limit: Option<usize>,
336
337 pub pick_early_type: Option<PickEarlyType>,
339}
340
341#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
343#[serde(rename_all = "snake_case")]
344pub enum PickEarlyType {
345 #[default]
347 Never,
348
349 FirstBestScoreImproving,
351
352 FirstLastStepScoreImproving,
354}
355
356#[derive(Debug, Clone, Deserialize, Serialize)]
358#[serde(tag = "type", rename_all = "snake_case")]
359pub enum MoveSelectorConfig {
360 ChangeMoveSelector(ChangeMoveConfig),
362
363 SwapMoveSelector(SwapMoveConfig),
365
366 UnionMoveSelector(UnionMoveSelectorConfig),
368
369 CartesianProductMoveSelector(CartesianProductConfig),
371}
372
373#[derive(Debug, Clone, Default, Deserialize, Serialize)]
375#[serde(rename_all = "snake_case")]
376pub struct ChangeMoveConfig {
377 pub entity_class: Option<String>,
379}
380
381#[derive(Debug, Clone, Default, Deserialize, Serialize)]
383#[serde(rename_all = "snake_case")]
384pub struct SwapMoveConfig {
385 pub entity_class: Option<String>,
387}
388
389#[derive(Debug, Clone, Default, Deserialize, Serialize)]
391#[serde(rename_all = "snake_case")]
392pub struct UnionMoveSelectorConfig {
393 pub selectors: Vec<MoveSelectorConfig>,
395}
396
397#[derive(Debug, Clone, Default, Deserialize, Serialize)]
399#[serde(rename_all = "snake_case")]
400pub struct CartesianProductConfig {
401 pub selectors: Vec<MoveSelectorConfig>,
403}
404
405#[derive(Debug, Clone, Default, Deserialize, Serialize)]
407#[serde(rename_all = "snake_case")]
408pub struct ExhaustiveSearchConfig {
409 #[serde(default)]
411 pub exhaustive_search_type: ExhaustiveSearchType,
412
413 pub termination: Option<TerminationConfig>,
415}
416
417#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
419#[serde(rename_all = "snake_case")]
420pub enum ExhaustiveSearchType {
421 #[default]
423 BranchAndBound,
424
425 BruteForce,
427}
428
429#[derive(Debug, Clone, Default, Deserialize, Serialize)]
431#[serde(rename_all = "snake_case")]
432pub struct PartitionedSearchConfig {
433 pub partition_count: Option<usize>,
435
436 pub termination: Option<TerminationConfig>,
438}
439
440#[derive(Debug, Clone, Default, Deserialize, Serialize)]
442#[serde(rename_all = "snake_case")]
443pub struct CustomPhaseConfig {
444 pub custom_phase_class: Option<String>,
446}
447
448#[derive(Debug, Clone, Default)]
450pub struct SolverConfigOverride {
451 pub termination: Option<TerminationConfig>,
453}
454
455impl SolverConfigOverride {
456 pub fn with_termination(termination: TerminationConfig) -> Self {
458 SolverConfigOverride {
459 termination: Some(termination),
460 }
461 }
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 #[test]
469 fn test_toml_parsing() {
470 let toml = r#"
471 environment_mode = "reproducible"
472 random_seed = 42
473
474 [termination]
475 seconds_spent_limit = 30
476
477 [[phases]]
478 type = "construction_heuristic"
479 construction_heuristic_type = "first_fit_decreasing"
480
481 [[phases]]
482 type = "local_search"
483 [phases.acceptor]
484 type = "late_acceptance"
485 late_acceptance_size = 400
486 "#;
487
488 let config = SolverConfig::from_toml_str(toml).unwrap();
489 assert_eq!(config.environment_mode, EnvironmentMode::Reproducible);
490 assert_eq!(config.random_seed, Some(42));
491 assert_eq!(config.termination.unwrap().seconds_spent_limit, Some(30));
492 assert_eq!(config.phases.len(), 2);
493 }
494
495 #[test]
496 fn test_yaml_parsing() {
497 let yaml = r#"
498 environment_mode: reproducible
499 random_seed: 42
500 termination:
501 seconds_spent_limit: 30
502 phases:
503 - type: construction_heuristic
504 construction_heuristic_type: first_fit_decreasing
505 - type: local_search
506 acceptor:
507 type: late_acceptance
508 late_acceptance_size: 400
509 "#;
510
511 let config = SolverConfig::from_yaml_str(yaml).unwrap();
512 assert_eq!(config.environment_mode, EnvironmentMode::Reproducible);
513 assert_eq!(config.random_seed, Some(42));
514 }
515
516 #[test]
517 fn test_builder() {
518 let config = SolverConfig::new()
519 .with_random_seed(123)
520 .with_termination_seconds(60)
521 .with_phase(PhaseConfig::ConstructionHeuristic(
522 ConstructionHeuristicConfig::default(),
523 ))
524 .with_phase(PhaseConfig::LocalSearch(LocalSearchConfig::default()));
525
526 assert_eq!(config.random_seed, Some(123));
527 assert_eq!(config.phases.len(), 2);
528 }
529}