Skip to main content

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 (basic variables).
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, basic variables).
316    CheapestInsertion,
317
318    /// Allocate entity from queue.
319    AllocateEntityFromQueue,
320
321    /// Allocate to value from queue.
322    AllocateToValueFromQueue,
323
324    /// List round-robin construction: distributes elements evenly across entities.
325    ListRoundRobin,
326
327    /// List cheapest insertion: inserts each element at the score-minimizing position.
328    ListCheapestInsertion,
329
330    /// List regret insertion: inserts elements in order of highest placement regret.
331    ListRegretInsertion,
332}
333
334/// Local search configuration.
335#[derive(Debug, Clone, Default, Deserialize, Serialize)]
336#[serde(rename_all = "snake_case")]
337pub struct LocalSearchConfig {
338    /// Acceptor configuration.
339    pub acceptor: Option<AcceptorConfig>,
340
341    /// Forager configuration.
342    pub forager: Option<ForagerConfig>,
343
344    /// Move selector configuration.
345    pub move_selector: Option<MoveSelectorConfig>,
346
347    /// Phase termination configuration.
348    pub termination: Option<TerminationConfig>,
349}
350
351/// Acceptor configuration.
352#[derive(Debug, Clone, Deserialize, Serialize)]
353#[serde(tag = "type", rename_all = "snake_case")]
354pub enum AcceptorConfig {
355    /// Hill climbing (only accept improving moves).
356    HillClimbing,
357
358    /// Tabu search acceptor.
359    TabuSearch(TabuSearchConfig),
360
361    /// Simulated annealing acceptor.
362    SimulatedAnnealing(SimulatedAnnealingConfig),
363
364    /// Late acceptance acceptor.
365    LateAcceptance(LateAcceptanceConfig),
366
367    /// Great deluge acceptor.
368    GreatDeluge(GreatDelugeConfig),
369}
370
371/// Tabu search configuration.
372#[derive(Debug, Clone, Default, Deserialize, Serialize)]
373#[serde(rename_all = "snake_case")]
374pub struct TabuSearchConfig {
375    /// Size of entity tabu list.
376    pub entity_tabu_size: Option<usize>,
377
378    /// Size of value tabu list.
379    pub value_tabu_size: Option<usize>,
380
381    /// Size of move tabu list.
382    pub move_tabu_size: Option<usize>,
383
384    /// Size of undo move tabu list.
385    pub undo_move_tabu_size: Option<usize>,
386}
387
388/// Simulated annealing configuration.
389#[derive(Debug, Clone, Default, Deserialize, Serialize)]
390#[serde(rename_all = "snake_case")]
391pub struct SimulatedAnnealingConfig {
392    /// Starting temperature.
393    pub starting_temperature: Option<String>,
394}
395
396/// Late acceptance configuration.
397#[derive(Debug, Clone, Default, Deserialize, Serialize)]
398#[serde(rename_all = "snake_case")]
399pub struct LateAcceptanceConfig {
400    /// Size of late acceptance list.
401    pub late_acceptance_size: Option<usize>,
402}
403
404/// Great deluge configuration.
405#[derive(Debug, Clone, Default, Deserialize, Serialize)]
406#[serde(rename_all = "snake_case")]
407pub struct GreatDelugeConfig {
408    /// Water level increase ratio.
409    pub water_level_increase_ratio: Option<f64>,
410}
411
412/// Forager configuration.
413#[derive(Debug, Clone, Default, Deserialize, Serialize)]
414#[serde(rename_all = "snake_case")]
415pub struct ForagerConfig {
416    /// Maximum number of accepted moves to consider.
417    pub accepted_count_limit: Option<usize>,
418
419    /// Whether to pick early if an improving move is found.
420    pub pick_early_type: Option<PickEarlyType>,
421}
422
423/// Pick early type.
424#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
425#[serde(rename_all = "snake_case")]
426pub enum PickEarlyType {
427    /// Never pick early.
428    #[default]
429    Never,
430
431    /// Pick first improving move.
432    FirstBestScoreImproving,
433
434    /// Pick first last step score improving move.
435    FirstLastStepScoreImproving,
436}
437
438/// Move selector configuration.
439#[derive(Debug, Clone, Deserialize, Serialize)]
440#[serde(tag = "type", rename_all = "snake_case")]
441pub enum MoveSelectorConfig {
442    /// Change move selector (basic variables).
443    ChangeMoveSelector(ChangeMoveConfig),
444
445    /// Swap move selector (basic variables).
446    SwapMoveSelector(SwapMoveConfig),
447
448    /// List change move selector — relocates single elements within/between routes.
449    ListChangeMoveSelector(ListChangeMoveConfig),
450
451    /// Nearby list change move selector — distance-pruned element relocation.
452    NearbyListChangeMoveSelector(NearbyListChangeMoveConfig),
453
454    /// List swap move selector — swaps single elements within/between routes.
455    ListSwapMoveSelector(ListSwapMoveConfig),
456
457    /// Nearby list swap move selector — distance-pruned element swap.
458    NearbyListSwapMoveSelector(NearbyListSwapMoveConfig),
459
460    /// Sublist change move selector (Or-opt) — relocates contiguous segments.
461    SubListChangeMoveSelector(SubListChangeMoveConfig),
462
463    /// Sublist swap move selector — swaps contiguous segments between routes.
464    SubListSwapMoveSelector(SubListSwapMoveConfig),
465
466    /// List reverse move selector (2-opt) — reverses segments within a route.
467    ListReverseMoveSelector(ListReverseMoveConfig),
468
469    /// K-opt move selector — generalised route reconnection.
470    KOptMoveSelector(KOptMoveSelectorConfig),
471
472    /// List ruin move selector (LNS) — removes elements for reinsertion.
473    ListRuinMoveSelector(ListRuinMoveSelectorConfig),
474
475    /// Union of multiple selectors.
476    UnionMoveSelector(UnionMoveSelectorConfig),
477
478    /// Cartesian product of selectors.
479    CartesianProductMoveSelector(CartesianProductConfig),
480}
481
482/// Configuration for `ListChangeMoveSelector`.
483#[derive(Debug, Clone, Default, Deserialize, Serialize)]
484#[serde(rename_all = "snake_case")]
485pub struct ListChangeMoveConfig {
486    /// Variable name filter. If None, applies to all list variables.
487    pub variable_name: Option<String>,
488}
489
490/// Configuration for `NearbyListChangeMoveSelector`.
491#[derive(Debug, Clone, Deserialize, Serialize)]
492#[serde(rename_all = "snake_case")]
493pub struct NearbyListChangeMoveConfig {
494    /// Maximum nearby destination positions to consider per source element.
495    pub max_nearby: usize,
496    /// Variable name filter. If None, applies to all list variables.
497    pub variable_name: Option<String>,
498}
499
500impl Default for NearbyListChangeMoveConfig {
501    fn default() -> Self {
502        Self {
503            max_nearby: 10,
504            variable_name: None,
505        }
506    }
507}
508
509/// Configuration for `ListSwapMoveSelector`.
510#[derive(Debug, Clone, Default, Deserialize, Serialize)]
511#[serde(rename_all = "snake_case")]
512pub struct ListSwapMoveConfig {
513    /// Variable name filter. If None, applies to all list variables.
514    pub variable_name: Option<String>,
515}
516
517/// Configuration for `NearbyListSwapMoveSelector`.
518#[derive(Debug, Clone, Deserialize, Serialize)]
519#[serde(rename_all = "snake_case")]
520pub struct NearbyListSwapMoveConfig {
521    /// Maximum nearby swap partners to consider per source element.
522    pub max_nearby: usize,
523    /// Variable name filter. If None, applies to all list variables.
524    pub variable_name: Option<String>,
525}
526
527impl Default for NearbyListSwapMoveConfig {
528    fn default() -> Self {
529        Self {
530            max_nearby: 10,
531            variable_name: None,
532        }
533    }
534}
535
536/// Configuration for `SubListChangeMoveSelector` (Or-opt).
537#[derive(Debug, Clone, Deserialize, Serialize)]
538#[serde(rename_all = "snake_case")]
539pub struct SubListChangeMoveConfig {
540    /// Minimum segment size (inclusive). Default: 1.
541    pub min_sublist_size: usize,
542    /// Maximum segment size (inclusive). Default: 3.
543    pub max_sublist_size: usize,
544    /// Variable name filter. If None, applies to all list variables.
545    pub variable_name: Option<String>,
546}
547
548impl Default for SubListChangeMoveConfig {
549    fn default() -> Self {
550        Self {
551            min_sublist_size: 1,
552            max_sublist_size: 3,
553            variable_name: None,
554        }
555    }
556}
557
558/// Configuration for `SubListSwapMoveSelector`.
559#[derive(Debug, Clone, Deserialize, Serialize)]
560#[serde(rename_all = "snake_case")]
561pub struct SubListSwapMoveConfig {
562    /// Minimum segment size (inclusive). Default: 1.
563    pub min_sublist_size: usize,
564    /// Maximum segment size (inclusive). Default: 3.
565    pub max_sublist_size: usize,
566    /// Variable name filter. If None, applies to all list variables.
567    pub variable_name: Option<String>,
568}
569
570impl Default for SubListSwapMoveConfig {
571    fn default() -> Self {
572        Self {
573            min_sublist_size: 1,
574            max_sublist_size: 3,
575            variable_name: None,
576        }
577    }
578}
579
580/// Configuration for `ListReverseMoveSelector` (2-opt).
581#[derive(Debug, Clone, Default, Deserialize, Serialize)]
582#[serde(rename_all = "snake_case")]
583pub struct ListReverseMoveConfig {
584    /// Variable name filter. If None, applies to all list variables.
585    pub variable_name: Option<String>,
586}
587
588/// Configuration for `KOptMoveSelector`.
589#[derive(Debug, Clone, Deserialize, Serialize)]
590#[serde(rename_all = "snake_case")]
591pub struct KOptMoveSelectorConfig {
592    /// K value (number of cuts). Default: 3.
593    pub k: usize,
594    /// Minimum segment length between cuts. Default: 1.
595    pub min_segment_len: usize,
596    /// Variable name filter. If None, applies to all list variables.
597    pub variable_name: Option<String>,
598}
599
600impl Default for KOptMoveSelectorConfig {
601    fn default() -> Self {
602        Self {
603            k: 3,
604            min_segment_len: 1,
605            variable_name: None,
606        }
607    }
608}
609
610/// Configuration for `ListRuinMoveSelector` (LNS).
611#[derive(Debug, Clone, Deserialize, Serialize)]
612#[serde(rename_all = "snake_case")]
613pub struct ListRuinMoveSelectorConfig {
614    /// Minimum number of elements to ruin per move. Default: 2.
615    pub min_ruin_count: usize,
616    /// Maximum number of elements to ruin per move. Default: 5.
617    pub max_ruin_count: usize,
618    /// Number of ruin moves to generate per step. Default: 10.
619    pub moves_per_step: Option<usize>,
620    /// Variable name filter. If None, applies to all list variables.
621    pub variable_name: Option<String>,
622}
623
624impl Default for ListRuinMoveSelectorConfig {
625    fn default() -> Self {
626        Self {
627            min_ruin_count: 2,
628            max_ruin_count: 5,
629            moves_per_step: None,
630            variable_name: None,
631        }
632    }
633}
634
635/// Change move configuration.
636#[derive(Debug, Clone, Default, Deserialize, Serialize)]
637#[serde(rename_all = "snake_case")]
638pub struct ChangeMoveConfig {
639    /// Entity class filter.
640    pub entity_class: Option<String>,
641}
642
643/// Swap move configuration.
644#[derive(Debug, Clone, Default, Deserialize, Serialize)]
645#[serde(rename_all = "snake_case")]
646pub struct SwapMoveConfig {
647    /// Entity class filter.
648    pub entity_class: Option<String>,
649}
650
651/// Union move selector configuration.
652#[derive(Debug, Clone, Default, Deserialize, Serialize)]
653#[serde(rename_all = "snake_case")]
654pub struct UnionMoveSelectorConfig {
655    /// Child selectors.
656    pub selectors: Vec<MoveSelectorConfig>,
657}
658
659/// Cartesian product move selector configuration.
660#[derive(Debug, Clone, Default, Deserialize, Serialize)]
661#[serde(rename_all = "snake_case")]
662pub struct CartesianProductConfig {
663    /// Child selectors.
664    pub selectors: Vec<MoveSelectorConfig>,
665}
666
667/// Exhaustive search configuration.
668#[derive(Debug, Clone, Default, Deserialize, Serialize)]
669#[serde(rename_all = "snake_case")]
670pub struct ExhaustiveSearchConfig {
671    /// Exhaustive search type.
672    #[serde(default)]
673    pub exhaustive_search_type: ExhaustiveSearchType,
674
675    /// Phase termination configuration.
676    pub termination: Option<TerminationConfig>,
677}
678
679/// Exhaustive search types.
680#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
681#[serde(rename_all = "snake_case")]
682pub enum ExhaustiveSearchType {
683    /// Branch and bound.
684    #[default]
685    BranchAndBound,
686
687    /// Brute force.
688    BruteForce,
689}
690
691/// Partitioned search configuration.
692#[derive(Debug, Clone, Default, Deserialize, Serialize)]
693#[serde(rename_all = "snake_case")]
694pub struct PartitionedSearchConfig {
695    /// Number of partitions.
696    pub partition_count: Option<usize>,
697
698    /// Phase termination configuration.
699    pub termination: Option<TerminationConfig>,
700}
701
702/// Custom phase configuration.
703#[derive(Debug, Clone, Default, Deserialize, Serialize)]
704#[serde(rename_all = "snake_case")]
705pub struct CustomPhaseConfig {
706    /// Custom phase class name.
707    pub custom_phase_class: Option<String>,
708}
709
710/// Runtime configuration overrides.
711#[derive(Debug, Clone, Default)]
712pub struct SolverConfigOverride {
713    /// Override termination configuration.
714    pub termination: Option<TerminationConfig>,
715}
716
717impl SolverConfigOverride {
718    /// Creates a new override with termination configuration.
719    pub fn with_termination(termination: TerminationConfig) -> Self {
720        SolverConfigOverride {
721            termination: Some(termination),
722        }
723    }
724}
725
726#[cfg(test)]
727mod tests;