1use std::path::Path;
44use std::time::Duration;
45
46use serde::{Deserialize, Serialize};
47use thiserror::Error;
48
49#[derive(Debug, Error)]
51pub enum ConfigError {
52 #[error("IO error: {0}")]
53 Io(#[from] std::io::Error),
54
55 #[error("TOML parse error: {0}")]
56 Toml(#[from] toml::de::Error),
57
58 #[error("YAML parse error: {0}")]
59 Yaml(#[from] serde_yaml::Error),
60
61 #[error("Invalid configuration: {0}")]
62 Invalid(String),
63}
64
65#[derive(Debug, Clone, Default, Deserialize, Serialize)]
67#[serde(rename_all = "snake_case")]
68pub struct SolverConfig {
69 #[serde(default)]
71 pub environment_mode: EnvironmentMode,
72
73 #[serde(default)]
75 pub random_seed: Option<u64>,
76
77 #[serde(default)]
79 pub move_thread_count: MoveThreadCount,
80
81 #[serde(default)]
83 pub termination: Option<TerminationConfig>,
84
85 #[serde(default)]
87 pub score_director: Option<ScoreDirectorConfig>,
88
89 #[serde(default)]
91 pub phases: Vec<PhaseConfig>,
92}
93
94impl SolverConfig {
95 pub fn new() -> Self {
97 Self::default()
98 }
99
100 pub fn load(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
106 Self::from_toml_file(path)
107 }
108
109 pub fn from_toml_file(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
111 let contents = std::fs::read_to_string(path)?;
112 Self::from_toml_str(&contents)
113 }
114
115 pub fn from_toml_str(s: &str) -> Result<Self, ConfigError> {
117 Ok(toml::from_str(s)?)
118 }
119
120 pub fn from_yaml_file(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
122 let contents = std::fs::read_to_string(path)?;
123 Self::from_yaml_str(&contents)
124 }
125
126 pub fn from_yaml_str(s: &str) -> Result<Self, ConfigError> {
128 Ok(serde_yaml::from_str(s)?)
129 }
130
131 pub fn with_termination_seconds(mut self, seconds: u64) -> Self {
133 self.termination = Some(TerminationConfig {
134 seconds_spent_limit: Some(seconds),
135 ..self.termination.unwrap_or_default()
136 });
137 self
138 }
139
140 pub fn with_random_seed(mut self, seed: u64) -> Self {
142 self.random_seed = Some(seed);
143 self
144 }
145
146 pub fn with_phase(mut self, phase: PhaseConfig) -> Self {
148 self.phases.push(phase);
149 self
150 }
151
152 pub fn time_limit(&self) -> Option<Duration> {
170 self.termination.as_ref().and_then(|t| t.time_limit())
171 }
172}
173
174#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
176#[serde(rename_all = "snake_case")]
177pub enum EnvironmentMode {
178 #[default]
180 NonReproducible,
181
182 Reproducible,
184
185 FastAssert,
187
188 FullAssert,
190}
191
192#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
194#[serde(rename_all = "snake_case")]
195pub enum MoveThreadCount {
196 #[default]
198 Auto,
199
200 None,
202
203 Count(usize),
205}
206
207#[derive(Debug, Clone, Default, Deserialize, Serialize)]
209#[serde(rename_all = "snake_case")]
210pub struct TerminationConfig {
211 pub seconds_spent_limit: Option<u64>,
213
214 pub minutes_spent_limit: Option<u64>,
216
217 pub best_score_limit: Option<String>,
219
220 pub step_count_limit: Option<u64>,
222
223 pub unimproved_step_count_limit: Option<u64>,
225
226 pub unimproved_seconds_spent_limit: Option<u64>,
228}
229
230impl TerminationConfig {
231 pub fn time_limit(&self) -> Option<Duration> {
233 let seconds =
234 self.seconds_spent_limit.unwrap_or(0) + self.minutes_spent_limit.unwrap_or(0) * 60;
235 if seconds > 0 {
236 Some(Duration::from_secs(seconds))
237 } else {
238 None
239 }
240 }
241
242 pub fn unimproved_time_limit(&self) -> Option<Duration> {
244 self.unimproved_seconds_spent_limit.map(Duration::from_secs)
245 }
246}
247
248#[derive(Debug, Clone, Default, Deserialize, Serialize)]
250#[serde(rename_all = "snake_case")]
251pub struct ScoreDirectorConfig {
252 pub constraint_provider: Option<String>,
254
255 #[serde(default)]
257 pub constraint_match_enabled: bool,
258}
259
260#[derive(Debug, Clone, Deserialize, Serialize)]
262#[serde(tag = "type", rename_all = "snake_case")]
263pub enum PhaseConfig {
264 ConstructionHeuristic(ConstructionHeuristicConfig),
266
267 LocalSearch(LocalSearchConfig),
269
270 ExhaustiveSearch(ExhaustiveSearchConfig),
272
273 PartitionedSearch(PartitionedSearchConfig),
275
276 Custom(CustomPhaseConfig),
278}
279
280#[derive(Debug, Clone, Default, Deserialize, Serialize)]
282#[serde(rename_all = "snake_case")]
283pub struct ConstructionHeuristicConfig {
284 #[serde(default)]
286 pub construction_heuristic_type: ConstructionHeuristicType,
287
288 pub termination: Option<TerminationConfig>,
290}
291
292#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
294#[serde(rename_all = "snake_case")]
295pub enum ConstructionHeuristicType {
296 #[default]
298 FirstFit,
299
300 FirstFitDecreasing,
302
303 WeakestFit,
305
306 WeakestFitDecreasing,
308
309 StrongestFit,
311
312 StrongestFitDecreasing,
314
315 CheapestInsertion,
317
318 AllocateEntityFromQueue,
320
321 AllocateToValueFromQueue,
323}
324
325#[derive(Debug, Clone, Default, Deserialize, Serialize)]
327#[serde(rename_all = "snake_case")]
328pub struct LocalSearchConfig {
329 pub acceptor: Option<AcceptorConfig>,
331
332 pub forager: Option<ForagerConfig>,
334
335 pub move_selector: Option<MoveSelectorConfig>,
337
338 pub termination: Option<TerminationConfig>,
340}
341
342#[derive(Debug, Clone, Deserialize, Serialize)]
344#[serde(tag = "type", rename_all = "snake_case")]
345pub enum AcceptorConfig {
346 HillClimbing,
348
349 TabuSearch(TabuSearchConfig),
351
352 SimulatedAnnealing(SimulatedAnnealingConfig),
354
355 LateAcceptance(LateAcceptanceConfig),
357
358 GreatDeluge(GreatDelugeConfig),
360}
361
362#[derive(Debug, Clone, Default, Deserialize, Serialize)]
364#[serde(rename_all = "snake_case")]
365pub struct TabuSearchConfig {
366 pub entity_tabu_size: Option<usize>,
368
369 pub value_tabu_size: Option<usize>,
371
372 pub move_tabu_size: Option<usize>,
374
375 pub undo_move_tabu_size: Option<usize>,
377}
378
379#[derive(Debug, Clone, Default, Deserialize, Serialize)]
381#[serde(rename_all = "snake_case")]
382pub struct SimulatedAnnealingConfig {
383 pub starting_temperature: Option<String>,
385}
386
387#[derive(Debug, Clone, Default, Deserialize, Serialize)]
389#[serde(rename_all = "snake_case")]
390pub struct LateAcceptanceConfig {
391 pub late_acceptance_size: Option<usize>,
393}
394
395#[derive(Debug, Clone, Default, Deserialize, Serialize)]
397#[serde(rename_all = "snake_case")]
398pub struct GreatDelugeConfig {
399 pub water_level_increase_ratio: Option<f64>,
401}
402
403#[derive(Debug, Clone, Default, Deserialize, Serialize)]
405#[serde(rename_all = "snake_case")]
406pub struct ForagerConfig {
407 pub accepted_count_limit: Option<usize>,
409
410 pub pick_early_type: Option<PickEarlyType>,
412}
413
414#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
416#[serde(rename_all = "snake_case")]
417pub enum PickEarlyType {
418 #[default]
420 Never,
421
422 FirstBestScoreImproving,
424
425 FirstLastStepScoreImproving,
427}
428
429#[derive(Debug, Clone, Deserialize, Serialize)]
431#[serde(tag = "type", rename_all = "snake_case")]
432pub enum MoveSelectorConfig {
433 ChangeMoveSelector(ChangeMoveConfig),
435
436 SwapMoveSelector(SwapMoveConfig),
438
439 UnionMoveSelector(UnionMoveSelectorConfig),
441
442 CartesianProductMoveSelector(CartesianProductConfig),
444}
445
446#[derive(Debug, Clone, Default, Deserialize, Serialize)]
448#[serde(rename_all = "snake_case")]
449pub struct ChangeMoveConfig {
450 pub entity_class: Option<String>,
452}
453
454#[derive(Debug, Clone, Default, Deserialize, Serialize)]
456#[serde(rename_all = "snake_case")]
457pub struct SwapMoveConfig {
458 pub entity_class: Option<String>,
460}
461
462#[derive(Debug, Clone, Default, Deserialize, Serialize)]
464#[serde(rename_all = "snake_case")]
465pub struct UnionMoveSelectorConfig {
466 pub selectors: Vec<MoveSelectorConfig>,
468}
469
470#[derive(Debug, Clone, Default, Deserialize, Serialize)]
472#[serde(rename_all = "snake_case")]
473pub struct CartesianProductConfig {
474 pub selectors: Vec<MoveSelectorConfig>,
476}
477
478#[derive(Debug, Clone, Default, Deserialize, Serialize)]
480#[serde(rename_all = "snake_case")]
481pub struct ExhaustiveSearchConfig {
482 #[serde(default)]
484 pub exhaustive_search_type: ExhaustiveSearchType,
485
486 pub termination: Option<TerminationConfig>,
488}
489
490#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
492#[serde(rename_all = "snake_case")]
493pub enum ExhaustiveSearchType {
494 #[default]
496 BranchAndBound,
497
498 BruteForce,
500}
501
502#[derive(Debug, Clone, Default, Deserialize, Serialize)]
504#[serde(rename_all = "snake_case")]
505pub struct PartitionedSearchConfig {
506 pub partition_count: Option<usize>,
508
509 pub termination: Option<TerminationConfig>,
511}
512
513#[derive(Debug, Clone, Default, Deserialize, Serialize)]
515#[serde(rename_all = "snake_case")]
516pub struct CustomPhaseConfig {
517 pub custom_phase_class: Option<String>,
519}
520
521#[derive(Debug, Clone, Default)]
523pub struct SolverConfigOverride {
524 pub termination: Option<TerminationConfig>,
526}
527
528impl SolverConfigOverride {
529 pub fn with_termination(termination: TerminationConfig) -> Self {
531 SolverConfigOverride {
532 termination: Some(termination),
533 }
534 }
535}
536
537#[cfg(test)]
538mod tests {
539 use super::*;
540
541 #[test]
542 fn test_toml_parsing() {
543 let toml = r#"
544 environment_mode = "reproducible"
545 random_seed = 42
546
547 [termination]
548 seconds_spent_limit = 30
549
550 [[phases]]
551 type = "construction_heuristic"
552 construction_heuristic_type = "first_fit_decreasing"
553
554 [[phases]]
555 type = "local_search"
556 [phases.acceptor]
557 type = "late_acceptance"
558 late_acceptance_size = 400
559 "#;
560
561 let config = SolverConfig::from_toml_str(toml).unwrap();
562 assert_eq!(config.environment_mode, EnvironmentMode::Reproducible);
563 assert_eq!(config.random_seed, Some(42));
564 assert_eq!(config.termination.unwrap().seconds_spent_limit, Some(30));
565 assert_eq!(config.phases.len(), 2);
566 }
567
568 #[test]
569 fn test_yaml_parsing() {
570 let yaml = r#"
571 environment_mode: reproducible
572 random_seed: 42
573 termination:
574 seconds_spent_limit: 30
575 phases:
576 - type: construction_heuristic
577 construction_heuristic_type: first_fit_decreasing
578 - type: local_search
579 acceptor:
580 type: late_acceptance
581 late_acceptance_size: 400
582 "#;
583
584 let config = SolverConfig::from_yaml_str(yaml).unwrap();
585 assert_eq!(config.environment_mode, EnvironmentMode::Reproducible);
586 assert_eq!(config.random_seed, Some(42));
587 }
588
589 #[test]
590 fn test_builder() {
591 let config = SolverConfig::new()
592 .with_random_seed(123)
593 .with_termination_seconds(60)
594 .with_phase(PhaseConfig::ConstructionHeuristic(
595 ConstructionHeuristicConfig::default(),
596 ))
597 .with_phase(PhaseConfig::LocalSearch(LocalSearchConfig::default()));
598
599 assert_eq!(config.random_seed, Some(123));
600 assert_eq!(config.phases.len(), 2);
601 }
602}