solverforge_config/
lib.rs

1//! Configuration system for SolverForge
2//!
3//! Supports TOML and YAML configuration formats.
4
5use std::path::Path;
6use std::time::Duration;
7
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11/// Configuration error
12#[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/// Main solver configuration.
28#[derive(Debug, Clone, Default, Deserialize, Serialize)]
29#[serde(rename_all = "snake_case")]
30pub struct SolverConfig {
31    /// Environment mode affecting reproducibility and assertions.
32    #[serde(default)]
33    pub environment_mode: EnvironmentMode,
34
35    /// Random seed for reproducible results.
36    #[serde(default)]
37    pub random_seed: Option<u64>,
38
39    /// Number of threads for parallel move evaluation.
40    #[serde(default)]
41    pub move_thread_count: MoveThreadCount,
42
43    /// Termination configuration.
44    #[serde(default)]
45    pub termination: Option<TerminationConfig>,
46
47    /// Score director configuration.
48    #[serde(default)]
49    pub score_director: Option<ScoreDirectorConfig>,
50
51    /// Phase configurations.
52    #[serde(default)]
53    pub phases: Vec<PhaseConfig>,
54}
55
56impl SolverConfig {
57    /// Creates a new default configuration.
58    pub fn new() -> Self {
59        Self::default()
60    }
61
62    /// Loads configuration from a TOML file.
63    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    /// Parses configuration from a TOML string.
69    pub fn from_toml_str(s: &str) -> Result<Self, ConfigError> {
70        Ok(toml::from_str(s)?)
71    }
72
73    /// Loads configuration from a YAML file.
74    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    /// Parses configuration from a YAML string.
80    pub fn from_yaml_str(s: &str) -> Result<Self, ConfigError> {
81        Ok(serde_yaml::from_str(s)?)
82    }
83
84    /// Sets the termination time limit.
85    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    /// Sets the random seed.
94    pub fn with_random_seed(mut self, seed: u64) -> Self {
95        self.random_seed = Some(seed);
96        self
97    }
98
99    /// Adds a phase configuration.
100    pub fn with_phase(mut self, phase: PhaseConfig) -> Self {
101        self.phases.push(phase);
102        self
103    }
104}
105
106/// Environment mode affecting solver behavior.
107#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
108#[serde(rename_all = "snake_case")]
109pub enum EnvironmentMode {
110    /// Non-reproducible mode with minimal overhead.
111    #[default]
112    NonReproducible,
113
114    /// Reproducible mode with deterministic behavior.
115    Reproducible,
116
117    /// Fast assert mode with basic assertions.
118    FastAssert,
119
120    /// Full assert mode with comprehensive assertions.
121    FullAssert,
122}
123
124/// Move thread count configuration.
125#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
126#[serde(rename_all = "snake_case")]
127pub enum MoveThreadCount {
128    /// Automatically determine thread count.
129    #[default]
130    Auto,
131
132    /// No parallel move evaluation.
133    None,
134
135    /// Specific number of threads.
136    Count(usize),
137}
138
139/// Termination configuration.
140#[derive(Debug, Clone, Default, Deserialize, Serialize)]
141#[serde(rename_all = "snake_case")]
142pub struct TerminationConfig {
143    /// Maximum seconds to spend solving.
144    pub seconds_spent_limit: Option<u64>,
145
146    /// Maximum minutes to spend solving.
147    pub minutes_spent_limit: Option<u64>,
148
149    /// Target best score to achieve (as string, e.g., "0hard/0soft").
150    pub best_score_limit: Option<String>,
151
152    /// Maximum number of steps.
153    pub step_count_limit: Option<usize>,
154
155    /// Maximum unimproved steps before terminating.
156    pub unimproved_step_count_limit: Option<usize>,
157
158    /// Maximum seconds without improvement.
159    pub unimproved_seconds_spent_limit: Option<u64>,
160}
161
162impl TerminationConfig {
163    /// Returns the time limit as a Duration, if any.
164    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/// Score director configuration.
176#[derive(Debug, Clone, Default, Deserialize, Serialize)]
177#[serde(rename_all = "snake_case")]
178pub struct ScoreDirectorConfig {
179    /// Fully qualified name of the constraint provider type.
180    pub constraint_provider: Option<String>,
181
182    /// Whether to enable constraint matching assertions.
183    #[serde(default)]
184    pub constraint_match_enabled: bool,
185}
186
187/// Phase configuration.
188#[derive(Debug, Clone, Deserialize, Serialize)]
189#[serde(tag = "type", rename_all = "snake_case")]
190pub enum PhaseConfig {
191    /// Construction heuristic phase.
192    ConstructionHeuristic(ConstructionHeuristicConfig),
193
194    /// Local search phase.
195    LocalSearch(LocalSearchConfig),
196
197    /// Exhaustive search phase.
198    ExhaustiveSearch(ExhaustiveSearchConfig),
199
200    /// Partitioned search phase.
201    PartitionedSearch(PartitionedSearchConfig),
202
203    /// Custom phase.
204    Custom(CustomPhaseConfig),
205}
206
207/// Construction heuristic configuration.
208#[derive(Debug, Clone, Default, Deserialize, Serialize)]
209#[serde(rename_all = "snake_case")]
210pub struct ConstructionHeuristicConfig {
211    /// Type of construction heuristic.
212    #[serde(default)]
213    pub construction_heuristic_type: ConstructionHeuristicType,
214
215    /// Phase termination configuration.
216    pub termination: Option<TerminationConfig>,
217}
218
219/// Construction heuristic types.
220#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
221#[serde(rename_all = "snake_case")]
222pub enum ConstructionHeuristicType {
223    /// First fit heuristic.
224    #[default]
225    FirstFit,
226
227    /// First fit decreasing (by entity difficulty).
228    FirstFitDecreasing,
229
230    /// Weakest fit heuristic.
231    WeakestFit,
232
233    /// Weakest fit decreasing.
234    WeakestFitDecreasing,
235
236    /// Strongest fit heuristic.
237    StrongestFit,
238
239    /// Strongest fit decreasing.
240    StrongestFitDecreasing,
241
242    /// Cheapest insertion (greedy).
243    CheapestInsertion,
244
245    /// Allocate entity from queue.
246    AllocateEntityFromQueue,
247
248    /// Allocate to value from queue.
249    AllocateToValueFromQueue,
250}
251
252/// Local search configuration.
253#[derive(Debug, Clone, Default, Deserialize, Serialize)]
254#[serde(rename_all = "snake_case")]
255pub struct LocalSearchConfig {
256    /// Acceptor configuration.
257    pub acceptor: Option<AcceptorConfig>,
258
259    /// Forager configuration.
260    pub forager: Option<ForagerConfig>,
261
262    /// Move selector configuration.
263    pub move_selector: Option<MoveSelectorConfig>,
264
265    /// Phase termination configuration.
266    pub termination: Option<TerminationConfig>,
267}
268
269/// Acceptor configuration.
270#[derive(Debug, Clone, Deserialize, Serialize)]
271#[serde(tag = "type", rename_all = "snake_case")]
272pub enum AcceptorConfig {
273    /// Hill climbing (only accept improving moves).
274    HillClimbing,
275
276    /// Tabu search acceptor.
277    TabuSearch(TabuSearchConfig),
278
279    /// Simulated annealing acceptor.
280    SimulatedAnnealing(SimulatedAnnealingConfig),
281
282    /// Late acceptance acceptor.
283    LateAcceptance(LateAcceptanceConfig),
284
285    /// Great deluge acceptor.
286    GreatDeluge(GreatDelugeConfig),
287}
288
289/// Tabu search configuration.
290#[derive(Debug, Clone, Default, Deserialize, Serialize)]
291#[serde(rename_all = "snake_case")]
292pub struct TabuSearchConfig {
293    /// Size of entity tabu list.
294    pub entity_tabu_size: Option<usize>,
295
296    /// Size of value tabu list.
297    pub value_tabu_size: Option<usize>,
298
299    /// Size of move tabu list.
300    pub move_tabu_size: Option<usize>,
301
302    /// Size of undo move tabu list.
303    pub undo_move_tabu_size: Option<usize>,
304}
305
306/// Simulated annealing configuration.
307#[derive(Debug, Clone, Default, Deserialize, Serialize)]
308#[serde(rename_all = "snake_case")]
309pub struct SimulatedAnnealingConfig {
310    /// Starting temperature.
311    pub starting_temperature: Option<String>,
312}
313
314/// Late acceptance configuration.
315#[derive(Debug, Clone, Default, Deserialize, Serialize)]
316#[serde(rename_all = "snake_case")]
317pub struct LateAcceptanceConfig {
318    /// Size of late acceptance list.
319    pub late_acceptance_size: Option<usize>,
320}
321
322/// Great deluge configuration.
323#[derive(Debug, Clone, Default, Deserialize, Serialize)]
324#[serde(rename_all = "snake_case")]
325pub struct GreatDelugeConfig {
326    /// Water level increase ratio.
327    pub water_level_increase_ratio: Option<f64>,
328}
329
330/// Forager configuration.
331#[derive(Debug, Clone, Default, Deserialize, Serialize)]
332#[serde(rename_all = "snake_case")]
333pub struct ForagerConfig {
334    /// Maximum number of accepted moves to consider.
335    pub accepted_count_limit: Option<usize>,
336
337    /// Whether to pick early if an improving move is found.
338    pub pick_early_type: Option<PickEarlyType>,
339}
340
341/// Pick early type.
342#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
343#[serde(rename_all = "snake_case")]
344pub enum PickEarlyType {
345    /// Never pick early.
346    #[default]
347    Never,
348
349    /// Pick first improving move.
350    FirstBestScoreImproving,
351
352    /// Pick first last step score improving move.
353    FirstLastStepScoreImproving,
354}
355
356/// Move selector configuration.
357#[derive(Debug, Clone, Deserialize, Serialize)]
358#[serde(tag = "type", rename_all = "snake_case")]
359pub enum MoveSelectorConfig {
360    /// Change move selector.
361    ChangeMoveSelector(ChangeMoveConfig),
362
363    /// Swap move selector.
364    SwapMoveSelector(SwapMoveConfig),
365
366    /// Union of multiple selectors.
367    UnionMoveSelector(UnionMoveSelectorConfig),
368
369    /// Cartesian product of selectors.
370    CartesianProductMoveSelector(CartesianProductConfig),
371}
372
373/// Change move configuration.
374#[derive(Debug, Clone, Default, Deserialize, Serialize)]
375#[serde(rename_all = "snake_case")]
376pub struct ChangeMoveConfig {
377    /// Entity class filter.
378    pub entity_class: Option<String>,
379}
380
381/// Swap move configuration.
382#[derive(Debug, Clone, Default, Deserialize, Serialize)]
383#[serde(rename_all = "snake_case")]
384pub struct SwapMoveConfig {
385    /// Entity class filter.
386    pub entity_class: Option<String>,
387}
388
389/// Union move selector configuration.
390#[derive(Debug, Clone, Default, Deserialize, Serialize)]
391#[serde(rename_all = "snake_case")]
392pub struct UnionMoveSelectorConfig {
393    /// Child selectors.
394    pub selectors: Vec<MoveSelectorConfig>,
395}
396
397/// Cartesian product move selector configuration.
398#[derive(Debug, Clone, Default, Deserialize, Serialize)]
399#[serde(rename_all = "snake_case")]
400pub struct CartesianProductConfig {
401    /// Child selectors.
402    pub selectors: Vec<MoveSelectorConfig>,
403}
404
405/// Exhaustive search configuration.
406#[derive(Debug, Clone, Default, Deserialize, Serialize)]
407#[serde(rename_all = "snake_case")]
408pub struct ExhaustiveSearchConfig {
409    /// Exhaustive search type.
410    #[serde(default)]
411    pub exhaustive_search_type: ExhaustiveSearchType,
412
413    /// Phase termination configuration.
414    pub termination: Option<TerminationConfig>,
415}
416
417/// Exhaustive search types.
418#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
419#[serde(rename_all = "snake_case")]
420pub enum ExhaustiveSearchType {
421    /// Branch and bound.
422    #[default]
423    BranchAndBound,
424
425    /// Brute force.
426    BruteForce,
427}
428
429/// Partitioned search configuration.
430#[derive(Debug, Clone, Default, Deserialize, Serialize)]
431#[serde(rename_all = "snake_case")]
432pub struct PartitionedSearchConfig {
433    /// Number of partitions.
434    pub partition_count: Option<usize>,
435
436    /// Phase termination configuration.
437    pub termination: Option<TerminationConfig>,
438}
439
440/// Custom phase configuration.
441#[derive(Debug, Clone, Default, Deserialize, Serialize)]
442#[serde(rename_all = "snake_case")]
443pub struct CustomPhaseConfig {
444    /// Custom phase class name.
445    pub custom_phase_class: Option<String>,
446}
447
448/// Runtime configuration overrides.
449#[derive(Debug, Clone, Default)]
450pub struct SolverConfigOverride {
451    /// Override termination configuration.
452    pub termination: Option<TerminationConfig>,
453}
454
455impl SolverConfigOverride {
456    /// Creates a new override with termination configuration.
457    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}