solverforge_config/solver_config.rs
1use std::num::NonZeroUsize;
2use std::path::Path;
3use std::time::Duration;
4
5use serde::{Deserialize, Serialize};
6
7use crate::director::DirectorConfig;
8use crate::error::ConfigError;
9use crate::phase::PhaseConfig;
10use crate::termination::TerminationConfig;
11
12// Main solver configuration.
13#[derive(Debug, Clone, Default, Deserialize, Serialize)]
14#[serde(rename_all = "snake_case")]
15pub struct SolverConfig {
16 // Environment mode affecting reproducibility and assertions.
17 #[serde(default)]
18 pub environment_mode: EnvironmentMode,
19
20 // Random seed for reproducible results.
21 #[serde(default)]
22 pub random_seed: Option<u64>,
23
24 // Number of threads for parallel move evaluation.
25 #[serde(default)]
26 pub move_thread_count: MoveThreadCount,
27
28 // Termination configuration.
29 #[serde(default)]
30 pub termination: Option<TerminationConfig>,
31
32 // Score director configuration.
33 #[serde(default)]
34 pub score_director: Option<DirectorConfig>,
35
36 // Phase configurations.
37 #[serde(default)]
38 pub phases: Vec<PhaseConfig>,
39
40 /// Optional bounded candidate-pull trace for cross-runtime diagnostics.
41 ///
42 /// This is deliberately opt-in: recording a trace retains one owned
43 /// identity per engine-consumed candidate. A non-zero cap is required so
44 /// diagnostic runs cannot accidentally retain an unbounded neighborhood.
45 #[serde(default)]
46 pub candidate_trace: Option<CandidateTraceConfig>,
47}
48
49impl SolverConfig {
50 pub fn new() -> Self {
51 Self::default()
52 }
53
54 /// Loads configuration from a TOML file.
55 ///
56 /// # Errors
57 ///
58 /// Returns error if file doesn't exist or contains invalid TOML.
59 pub fn load(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
60 Self::from_toml_file(path)
61 }
62
63 /// Loads configuration from a TOML file.
64 pub fn from_toml_file(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
65 let contents = std::fs::read_to_string(path)?;
66 Self::from_toml_str(&contents)
67 }
68
69 /// Parses configuration from a TOML string.
70 pub fn from_toml_str(s: &str) -> Result<Self, ConfigError> {
71 Ok(toml::from_str(s)?)
72 }
73
74 /// Loads configuration from a YAML file.
75 pub fn from_yaml_file(path: impl AsRef<Path>) -> Result<Self, ConfigError> {
76 let contents = std::fs::read_to_string(path)?;
77 Self::from_yaml_str(&contents)
78 }
79
80 /// Parses configuration from a YAML string.
81 pub fn from_yaml_str(s: &str) -> Result<Self, ConfigError> {
82 Ok(serde_yaml::from_str(s)?)
83 }
84
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 pub fn with_random_seed(mut self, seed: u64) -> Self {
94 self.random_seed = Some(seed);
95 self
96 }
97
98 pub fn with_phase(mut self, phase: PhaseConfig) -> Self {
99 self.phases.push(phase);
100 self
101 }
102
103 /// Returns a deterministic, complete representation of this effective
104 /// configuration for diagnostic provenance.
105 ///
106 /// The representation is intentionally produced by the same serde model
107 /// used to load solver configuration. Consumers compare this exact value
108 /// together with the resolved phase plan in the candidate trace header.
109 pub fn canonical_toml(&self) -> String {
110 toml::to_string(self)
111 .expect("SolverConfig serialization must succeed for a serde-derived configuration")
112 }
113
114 /// Returns the canonical solver-document representation of one phase.
115 ///
116 /// Runtime provenance uses this rather than debug formatting so every
117 /// configured selector, acceptor, forager, target, and termination field
118 /// participates in the candidate-trace plan digest.
119 pub fn canonical_phase_toml(phase: &PhaseConfig) -> String {
120 Self {
121 phases: vec![phase.clone()],
122 ..Self::default()
123 }
124 .canonical_toml()
125 }
126
127 /// Returns the termination time limit, if configured.
128 ///
129 /// Convenience method that delegates to `termination.time_limit()`.
130 ///
131 /// # Examples
132 ///
133 /// ```
134 /// use solverforge_config::SolverConfig;
135 /// use std::time::Duration;
136 ///
137 /// let config = SolverConfig::from_toml_str(r#"
138 /// [termination]
139 /// seconds_spent_limit = 30
140 /// "#).unwrap();
141 ///
142 /// assert_eq!(config.time_limit(), Some(Duration::from_secs(30)));
143 /// ```
144 pub fn time_limit(&self) -> Option<Duration> {
145 self.termination.as_ref().and_then(|t| t.time_limit())
146 }
147}
148
149/// Opt-in bounded candidate-pull trace configuration.
150///
151/// Enable from TOML with, for example:
152///
153/// ```toml
154/// [candidate_trace]
155/// max_entries = 100_000
156/// ```
157///
158/// A zero cap is rejected during deserialization. The trace records its
159/// total pull count even after the cap, marks itself truncated, and never
160/// retains identities beyond this limit.
161#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
162#[serde(rename_all = "snake_case")]
163pub struct CandidateTraceConfig {
164 pub max_entries: NonZeroUsize,
165}
166
167impl CandidateTraceConfig {
168 pub const fn new(max_entries: NonZeroUsize) -> Self {
169 Self { max_entries }
170 }
171}
172
173// Environment mode affecting solver behavior.
174#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
175#[serde(rename_all = "snake_case")]
176pub enum EnvironmentMode {
177 // Non-reproducible mode with minimal overhead.
178 #[default]
179 NonReproducible,
180
181 // Reproducible mode with deterministic behavior.
182 Reproducible,
183
184 // Fast assert mode with basic assertions.
185 FastAssert,
186
187 // Full assert mode with comprehensive assertions.
188 FullAssert,
189}
190
191// Move thread count configuration.
192#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize, Serialize)]
193#[serde(rename_all = "snake_case")]
194pub enum MoveThreadCount {
195 // Sequential until benchmarks set an automatic policy.
196 #[default]
197 Auto,
198
199 // No parallel move evaluation.
200 None,
201
202 // Evaluator count, main director included; bounded by the solve's
203 // Rayon threads and by one 32-candidate evaluation window.
204 Count(usize),
205}
206
207// Runtime configuration overrides.
208#[derive(Debug, Clone, Default)]
209pub struct SolverConfigOverride {
210 // Override termination configuration.
211 pub termination: Option<TerminationConfig>,
212}
213
214impl SolverConfigOverride {
215 pub fn with_termination(termination: TerminationConfig) -> Self {
216 SolverConfigOverride {
217 termination: Some(termination),
218 }
219 }
220}