solverforge_config/
lib.rs

1//! Configuration system for SolverForge.
2//!
3//! Load solver configuration from TOML files to control termination,
4//! phases, and acceptors without code changes.
5//!
6//! # Examples
7//!
8//! Load configuration from TOML string:
9//!
10//! ```
11//! use solverforge_config::SolverConfig;
12//! use std::time::Duration;
13//!
14//! let config = SolverConfig::from_toml_str(r#"
15//!     [termination]
16//!     seconds_spent_limit = 30
17//!     unimproved_seconds_spent_limit = 5
18//!
19//!     [[phases]]
20//!     type = "construction_heuristic"
21//!     construction_heuristic_type = "first_fit"
22//!
23//!     [[phases]]
24//!     type = "local_search"
25//!     [phases.acceptor]
26//!     type = "late_acceptance"
27//!     late_acceptance_size = 400
28//! "#).unwrap();
29//!
30//! assert_eq!(config.time_limit(), Some(Duration::from_secs(30)));
31//! assert_eq!(config.phases.len(), 2);
32//! ```
33//!
34//! Use default config when file is missing:
35//!
36//! ```
37//! use solverforge_config::SolverConfig;
38//!
39//! let config = SolverConfig::load("solver.toml").unwrap_or_default();
40//! // Proceeds with defaults if file doesn't exist
41//! ```
42
43use std::path::Path;
44use std::time::Duration;
45
46use serde::{Deserialize, Serialize};
47use thiserror::Error;
48
49/// Configuration error
50#[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/// Main solver configuration.
66#[derive(Debug, Clone, Default, Deserialize, Serialize)]
67#[serde(rename_all = "snake_case")]
68pub struct SolverConfig {
69    /// Environment mode affecting reproducibility and assertions.
70    #[serde(default)]
71    pub environment_mode: EnvironmentMode,
72
73    /// Random seed for reproducible results.
74    #[serde(default)]
75    pub random_seed: Option<u64>,
76
77    /// Number of threads for parallel move evaluation.
78    #[serde(default)]
79    pub move_thread_count: MoveThreadCount,
80
81    /// Termination configuration.
82    #[serde(default)]
83    pub termination: Option<TerminationConfig>,
84
85    /// Score director configuration.
86    #[serde(default)]
87    pub score_director: Option<ScoreDirectorConfig>,
88
89    /// Phase configurations.
90    #[serde(default)]
91    pub phases: Vec<PhaseConfig>,
92}
93
94impl SolverConfig {
95    /// Creates a new default configuration.
96    pub fn new() -> Self {
97        Self::default()
98    }
99
100    /// Loads configuration from a TOML file.
101    ///
102    /// # Errors
103    ///
104    /// Returns error if file doesn't exist or contains invalid TOML.
105    pub fn load(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
106        Self::from_toml_file(path)
107    }
108
109    /// Loads configuration from a TOML file.
110    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    /// Parses configuration from a TOML string.
116    pub fn from_toml_str(s: &str) -> Result<Self, ConfigError> {
117        Ok(toml::from_str(s)?)
118    }
119
120    /// Loads configuration from a YAML file.
121    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    /// Parses configuration from a YAML string.
127    pub fn from_yaml_str(s: &str) -> Result<Self, ConfigError> {
128        Ok(serde_yaml::from_str(s)?)
129    }
130
131    /// Sets the termination time limit.
132    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    /// Sets the random seed.
141    pub fn with_random_seed(mut self, seed: u64) -> Self {
142        self.random_seed = Some(seed);
143        self
144    }
145
146    /// Adds a phase configuration.
147    pub fn with_phase(mut self, phase: PhaseConfig) -> Self {
148        self.phases.push(phase);
149        self
150    }
151
152    /// Returns the termination time limit, if configured.
153    ///
154    /// Convenience method that delegates to `termination.time_limit()`.
155    ///
156    /// # Examples
157    ///
158    /// ```
159    /// use solverforge_config::SolverConfig;
160    /// use std::time::Duration;
161    ///
162    /// let config = SolverConfig::from_toml_str(r#"
163    ///     [termination]
164    ///     seconds_spent_limit = 30
165    /// "#).unwrap();
166    ///
167    /// assert_eq!(config.time_limit(), Some(Duration::from_secs(30)));
168    /// ```
169    pub fn time_limit(&self) -> Option<Duration> {
170        self.termination.as_ref().and_then(|t| t.time_limit())
171    }
172}
173
174/// Environment mode affecting solver behavior.
175#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
176#[serde(rename_all = "snake_case")]
177pub enum EnvironmentMode {
178    /// Non-reproducible mode with minimal overhead.
179    #[default]
180    NonReproducible,
181
182    /// Reproducible mode with deterministic behavior.
183    Reproducible,
184
185    /// Fast assert mode with basic assertions.
186    FastAssert,
187
188    /// Full assert mode with comprehensive assertions.
189    FullAssert,
190}
191
192/// Move thread count configuration.
193#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
194#[serde(rename_all = "snake_case")]
195pub enum MoveThreadCount {
196    /// Automatically determine thread count.
197    #[default]
198    Auto,
199
200    /// No parallel move evaluation.
201    None,
202
203    /// Specific number of threads.
204    Count(usize),
205}
206
207/// Termination configuration.
208#[derive(Debug, Clone, Default, Deserialize, Serialize)]
209#[serde(rename_all = "snake_case")]
210pub struct TerminationConfig {
211    /// Maximum seconds to spend solving.
212    pub seconds_spent_limit: Option<u64>,
213
214    /// Maximum minutes to spend solving.
215    pub minutes_spent_limit: Option<u64>,
216
217    /// Target best score to achieve (as string, e.g., "0hard/0soft").
218    pub best_score_limit: Option<String>,
219
220    /// Maximum number of steps.
221    pub step_count_limit: Option<u64>,
222
223    /// Maximum unimproved steps before terminating.
224    pub unimproved_step_count_limit: Option<u64>,
225
226    /// Maximum seconds without improvement.
227    pub unimproved_seconds_spent_limit: Option<u64>,
228}
229
230impl TerminationConfig {
231    /// Returns the time limit as a Duration, if any.
232    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    /// Returns the unimproved time limit as a Duration, if any.
243    pub fn unimproved_time_limit(&self) -> Option<Duration> {
244        self.unimproved_seconds_spent_limit.map(Duration::from_secs)
245    }
246}
247
248/// Score director configuration.
249#[derive(Debug, Clone, Default, Deserialize, Serialize)]
250#[serde(rename_all = "snake_case")]
251pub struct ScoreDirectorConfig {
252    /// Fully qualified name of the constraint provider type.
253    pub constraint_provider: Option<String>,
254
255    /// Whether to enable constraint matching assertions.
256    #[serde(default)]
257    pub constraint_match_enabled: bool,
258}
259
260/// Phase configuration.
261#[derive(Debug, Clone, Deserialize, Serialize)]
262#[serde(tag = "type", rename_all = "snake_case")]
263pub enum PhaseConfig {
264    /// Construction heuristic phase.
265    ConstructionHeuristic(ConstructionHeuristicConfig),
266
267    /// Local search phase.
268    LocalSearch(LocalSearchConfig),
269
270    /// Exhaustive search phase.
271    ExhaustiveSearch(ExhaustiveSearchConfig),
272
273    /// Partitioned search phase.
274    PartitionedSearch(PartitionedSearchConfig),
275
276    /// Custom phase.
277    Custom(CustomPhaseConfig),
278}
279
280/// Construction heuristic configuration.
281#[derive(Debug, Clone, Default, Deserialize, Serialize)]
282#[serde(rename_all = "snake_case")]
283pub struct ConstructionHeuristicConfig {
284    /// Type of construction heuristic.
285    #[serde(default)]
286    pub construction_heuristic_type: ConstructionHeuristicType,
287
288    /// Phase termination configuration.
289    pub termination: Option<TerminationConfig>,
290}
291
292/// Construction heuristic types.
293#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
294#[serde(rename_all = "snake_case")]
295pub enum ConstructionHeuristicType {
296    /// First fit heuristic.
297    #[default]
298    FirstFit,
299
300    /// First fit decreasing (by entity difficulty).
301    FirstFitDecreasing,
302
303    /// Weakest fit heuristic.
304    WeakestFit,
305
306    /// Weakest fit decreasing.
307    WeakestFitDecreasing,
308
309    /// Strongest fit heuristic.
310    StrongestFit,
311
312    /// Strongest fit decreasing.
313    StrongestFitDecreasing,
314
315    /// Cheapest insertion (greedy).
316    CheapestInsertion,
317
318    /// Allocate entity from queue.
319    AllocateEntityFromQueue,
320
321    /// Allocate to value from queue.
322    AllocateToValueFromQueue,
323}
324
325/// Local search configuration.
326#[derive(Debug, Clone, Default, Deserialize, Serialize)]
327#[serde(rename_all = "snake_case")]
328pub struct LocalSearchConfig {
329    /// Acceptor configuration.
330    pub acceptor: Option<AcceptorConfig>,
331
332    /// Forager configuration.
333    pub forager: Option<ForagerConfig>,
334
335    /// Move selector configuration.
336    pub move_selector: Option<MoveSelectorConfig>,
337
338    /// Phase termination configuration.
339    pub termination: Option<TerminationConfig>,
340}
341
342/// Acceptor configuration.
343#[derive(Debug, Clone, Deserialize, Serialize)]
344#[serde(tag = "type", rename_all = "snake_case")]
345pub enum AcceptorConfig {
346    /// Hill climbing (only accept improving moves).
347    HillClimbing,
348
349    /// Tabu search acceptor.
350    TabuSearch(TabuSearchConfig),
351
352    /// Simulated annealing acceptor.
353    SimulatedAnnealing(SimulatedAnnealingConfig),
354
355    /// Late acceptance acceptor.
356    LateAcceptance(LateAcceptanceConfig),
357
358    /// Great deluge acceptor.
359    GreatDeluge(GreatDelugeConfig),
360}
361
362/// Tabu search configuration.
363#[derive(Debug, Clone, Default, Deserialize, Serialize)]
364#[serde(rename_all = "snake_case")]
365pub struct TabuSearchConfig {
366    /// Size of entity tabu list.
367    pub entity_tabu_size: Option<usize>,
368
369    /// Size of value tabu list.
370    pub value_tabu_size: Option<usize>,
371
372    /// Size of move tabu list.
373    pub move_tabu_size: Option<usize>,
374
375    /// Size of undo move tabu list.
376    pub undo_move_tabu_size: Option<usize>,
377}
378
379/// Simulated annealing configuration.
380#[derive(Debug, Clone, Default, Deserialize, Serialize)]
381#[serde(rename_all = "snake_case")]
382pub struct SimulatedAnnealingConfig {
383    /// Starting temperature.
384    pub starting_temperature: Option<String>,
385}
386
387/// Late acceptance configuration.
388#[derive(Debug, Clone, Default, Deserialize, Serialize)]
389#[serde(rename_all = "snake_case")]
390pub struct LateAcceptanceConfig {
391    /// Size of late acceptance list.
392    pub late_acceptance_size: Option<usize>,
393}
394
395/// Great deluge configuration.
396#[derive(Debug, Clone, Default, Deserialize, Serialize)]
397#[serde(rename_all = "snake_case")]
398pub struct GreatDelugeConfig {
399    /// Water level increase ratio.
400    pub water_level_increase_ratio: Option<f64>,
401}
402
403/// Forager configuration.
404#[derive(Debug, Clone, Default, Deserialize, Serialize)]
405#[serde(rename_all = "snake_case")]
406pub struct ForagerConfig {
407    /// Maximum number of accepted moves to consider.
408    pub accepted_count_limit: Option<usize>,
409
410    /// Whether to pick early if an improving move is found.
411    pub pick_early_type: Option<PickEarlyType>,
412}
413
414/// Pick early type.
415#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
416#[serde(rename_all = "snake_case")]
417pub enum PickEarlyType {
418    /// Never pick early.
419    #[default]
420    Never,
421
422    /// Pick first improving move.
423    FirstBestScoreImproving,
424
425    /// Pick first last step score improving move.
426    FirstLastStepScoreImproving,
427}
428
429/// Move selector configuration.
430#[derive(Debug, Clone, Deserialize, Serialize)]
431#[serde(tag = "type", rename_all = "snake_case")]
432pub enum MoveSelectorConfig {
433    /// Change move selector.
434    ChangeMoveSelector(ChangeMoveConfig),
435
436    /// Swap move selector.
437    SwapMoveSelector(SwapMoveConfig),
438
439    /// Union of multiple selectors.
440    UnionMoveSelector(UnionMoveSelectorConfig),
441
442    /// Cartesian product of selectors.
443    CartesianProductMoveSelector(CartesianProductConfig),
444}
445
446/// Change move configuration.
447#[derive(Debug, Clone, Default, Deserialize, Serialize)]
448#[serde(rename_all = "snake_case")]
449pub struct ChangeMoveConfig {
450    /// Entity class filter.
451    pub entity_class: Option<String>,
452}
453
454/// Swap move configuration.
455#[derive(Debug, Clone, Default, Deserialize, Serialize)]
456#[serde(rename_all = "snake_case")]
457pub struct SwapMoveConfig {
458    /// Entity class filter.
459    pub entity_class: Option<String>,
460}
461
462/// Union move selector configuration.
463#[derive(Debug, Clone, Default, Deserialize, Serialize)]
464#[serde(rename_all = "snake_case")]
465pub struct UnionMoveSelectorConfig {
466    /// Child selectors.
467    pub selectors: Vec<MoveSelectorConfig>,
468}
469
470/// Cartesian product move selector configuration.
471#[derive(Debug, Clone, Default, Deserialize, Serialize)]
472#[serde(rename_all = "snake_case")]
473pub struct CartesianProductConfig {
474    /// Child selectors.
475    pub selectors: Vec<MoveSelectorConfig>,
476}
477
478/// Exhaustive search configuration.
479#[derive(Debug, Clone, Default, Deserialize, Serialize)]
480#[serde(rename_all = "snake_case")]
481pub struct ExhaustiveSearchConfig {
482    /// Exhaustive search type.
483    #[serde(default)]
484    pub exhaustive_search_type: ExhaustiveSearchType,
485
486    /// Phase termination configuration.
487    pub termination: Option<TerminationConfig>,
488}
489
490/// Exhaustive search types.
491#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
492#[serde(rename_all = "snake_case")]
493pub enum ExhaustiveSearchType {
494    /// Branch and bound.
495    #[default]
496    BranchAndBound,
497
498    /// Brute force.
499    BruteForce,
500}
501
502/// Partitioned search configuration.
503#[derive(Debug, Clone, Default, Deserialize, Serialize)]
504#[serde(rename_all = "snake_case")]
505pub struct PartitionedSearchConfig {
506    /// Number of partitions.
507    pub partition_count: Option<usize>,
508
509    /// Phase termination configuration.
510    pub termination: Option<TerminationConfig>,
511}
512
513/// Custom phase configuration.
514#[derive(Debug, Clone, Default, Deserialize, Serialize)]
515#[serde(rename_all = "snake_case")]
516pub struct CustomPhaseConfig {
517    /// Custom phase class name.
518    pub custom_phase_class: Option<String>,
519}
520
521/// Runtime configuration overrides.
522#[derive(Debug, Clone, Default)]
523pub struct SolverConfigOverride {
524    /// Override termination configuration.
525    pub termination: Option<TerminationConfig>,
526}
527
528impl SolverConfigOverride {
529    /// Creates a new override with termination configuration.
530    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}