Skip to main content

solverforge_solver/
solver.rs

1// Solver implementation.
2
3use std::fmt::Debug;
4use std::marker::PhantomData;
5use std::sync::atomic::AtomicBool;
6use std::time::Duration;
7
8use solverforge_config::SolverConfig;
9use solverforge_core::domain::PlanningSolution;
10use solverforge_scoring::Director;
11
12use crate::manager::{SolverRuntime, SolverTerminalReason};
13use crate::phase::Phase;
14use crate::scope::ProgressCallback;
15use crate::scope::SolverScope;
16use crate::stats::{
17    CandidateTraceExecutionPolicy, CandidateTraceHeader, CandidateTracePhasePlan,
18    QualifiedCandidateTraceRunProvenance, SolverStats,
19};
20use crate::termination::Termination;
21
22/* Result of a solve operation containing solution and telemetry.
23
24This is the canonical return type for `Solver::solve()`. It provides
25both the optimized solution and comprehensive statistics about the
26solving process.
27*/
28#[derive(Debug)]
29pub struct SolveResult<S: PlanningSolution> {
30    // The best solution found during solving.
31    pub solution: S,
32    // The final working score when solving stopped.
33    pub current_score: Option<S::Score>,
34    // The canonical best score for the solve.
35    pub best_score: S::Score,
36    // Why solving stopped.
37    pub terminal_reason: SolverTerminalReason,
38    // Solver statistics including steps, moves evaluated, and acceptance rates.
39    pub stats: SolverStats,
40}
41
42impl<S: PlanningSolution> SolveResult<S> {
43    pub fn solution(&self) -> &S {
44        &self.solution
45    }
46
47    pub fn into_solution(self) -> S {
48        self.solution
49    }
50
51    pub fn current_score(&self) -> Option<&S::Score> {
52        self.current_score.as_ref()
53    }
54
55    pub fn best_score(&self) -> &S::Score {
56        &self.best_score
57    }
58
59    pub fn terminal_reason(&self) -> SolverTerminalReason {
60        self.terminal_reason
61    }
62
63    pub fn stats(&self) -> &SolverStats {
64        &self.stats
65    }
66
67    pub fn step_count(&self) -> u64 {
68        self.stats.step_count
69    }
70
71    pub fn moves_evaluated(&self) -> u64 {
72        self.stats.moves_evaluated
73    }
74
75    pub fn moves_accepted(&self) -> u64 {
76        self.stats.moves_accepted
77    }
78}
79
80/// The main solver that optimizes planning solutions.
81///
82/// Uses macro-generated tuple implementations for phases, preserving
83/// concrete types through the entire pipeline (zero-erasure architecture).
84///
85/// # Type Parameters
86/// * `'t` - Lifetime of the termination flag reference
87/// * `P` - Tuple of phases to execute
88/// * `T` - Termination condition (use `Option<ConcreteTermination>`)
89/// * `S` - Solution type
90/// * `D` - Score director type
91/// * `ProgressCb` - Progress callback type (default `()`)
92pub struct Solver<'t, P, T, S: PlanningSolution, D, ProgressCb = ()> {
93    phases: P,
94    termination: T,
95    terminate: Option<&'t AtomicBool>,
96    runtime: Option<SolverRuntime<S>>,
97    config: Option<SolverConfig>,
98    /// Canonical resolved policy supplied by the configured runtime entrypoint.
99    /// Direct generic `Solver` construction leaves this absent on purpose: an
100    /// arbitrary Rust termination cannot be reconstructed honestly from the
101    /// input config alone.
102    candidate_trace_execution_policy: Option<CandidateTraceExecutionPolicy>,
103    /// Explicit fail-closed benchmark attestation for qualified comparisons.
104    candidate_trace_qualified_run_provenance: Option<QualifiedCandidateTraceRunProvenance>,
105    time_limit: Option<Duration>,
106    // Callback invoked when the solver should publish progress.
107    progress_callback: ProgressCb,
108    _phantom: PhantomData<fn(S, D)>,
109}
110
111impl<P: Debug, T: Debug, S: PlanningSolution, D, ProgressCb> Debug
112    for Solver<'_, P, T, S, D, ProgressCb>
113{
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        f.debug_struct("Solver")
116            .field("phases", &self.phases)
117            .field("termination", &self.termination)
118            .finish()
119    }
120}
121
122impl<P, S, D> Solver<'static, P, NoTermination, S, D, ()>
123where
124    S: PlanningSolution,
125{
126    pub fn new(phases: P) -> Self {
127        Solver {
128            phases,
129            termination: NoTermination,
130            terminate: None,
131            runtime: None,
132            config: None,
133            candidate_trace_execution_policy: None,
134            candidate_trace_qualified_run_provenance: None,
135            time_limit: None,
136            progress_callback: (),
137            _phantom: PhantomData,
138        }
139    }
140
141    pub fn with_termination<T>(self, termination: T) -> Solver<'static, P, Option<T>, S, D, ()> {
142        Solver {
143            phases: self.phases,
144            termination: Some(termination),
145            terminate: self.terminate,
146            runtime: self.runtime,
147            config: self.config,
148            candidate_trace_execution_policy: self.candidate_trace_execution_policy,
149            candidate_trace_qualified_run_provenance: self.candidate_trace_qualified_run_provenance,
150            time_limit: self.time_limit,
151            progress_callback: self.progress_callback,
152            _phantom: PhantomData,
153        }
154    }
155}
156
157impl<'t, P, T, S, D, ProgressCb> Solver<'t, P, T, S, D, ProgressCb>
158where
159    S: PlanningSolution,
160{
161    /// Sets the external termination flag.
162    ///
163    /// The solver will check this flag periodically and terminate early if set.
164    pub fn with_terminate(self, terminate: &'t AtomicBool) -> Solver<'t, P, T, S, D, ProgressCb> {
165        Solver {
166            phases: self.phases,
167            termination: self.termination,
168            terminate: Some(terminate),
169            runtime: self.runtime,
170            config: self.config,
171            candidate_trace_execution_policy: self.candidate_trace_execution_policy,
172            candidate_trace_qualified_run_provenance: self.candidate_trace_qualified_run_provenance,
173            time_limit: self.time_limit,
174            progress_callback: self.progress_callback,
175            _phantom: PhantomData,
176        }
177    }
178
179    pub fn with_time_limit(mut self, limit: Duration) -> Self {
180        self.time_limit = Some(limit);
181        self
182    }
183
184    pub fn with_config(mut self, config: SolverConfig) -> Self {
185        self.config = Some(config);
186        self
187    }
188
189    /// Supplies the canonical policy resolved by a first-party entrypoint.
190    ///
191    /// This remains crate-private so external users cannot accidentally claim
192    /// an arbitrary custom termination is equivalent to a built-in policy.
193    pub(crate) fn with_candidate_trace_execution_policy(
194        mut self,
195        execution_policy: CandidateTraceExecutionPolicy,
196    ) -> Self {
197        self.candidate_trace_execution_policy = Some(execution_policy);
198        self
199    }
200
201    /// Requests a qualified candidate trace with a complete, immutable
202    /// model/instance/state/core-tree/build attestation. Construction of the
203    /// value validates the attestation before execution; it is never inferred
204    /// from environment, callbacks, or working state.
205    pub fn with_qualified_candidate_trace_run_provenance(
206        mut self,
207        provenance: QualifiedCandidateTraceRunProvenance,
208    ) -> Self {
209        self.candidate_trace_qualified_run_provenance = Some(provenance);
210        self
211    }
212
213    /// Sets a callback to be invoked for exact solver progress and best-solution updates.
214    ///
215    /// Transitions the callback type parameter to the concrete closure type.
216    pub fn with_progress_callback<F>(self, callback: F) -> Solver<'t, P, T, S, D, F> {
217        Solver {
218            phases: self.phases,
219            termination: self.termination,
220            terminate: self.terminate,
221            runtime: self.runtime,
222            config: self.config,
223            candidate_trace_execution_policy: self.candidate_trace_execution_policy,
224            candidate_trace_qualified_run_provenance: self.candidate_trace_qualified_run_provenance,
225            time_limit: self.time_limit,
226            progress_callback: callback,
227            _phantom: PhantomData,
228        }
229    }
230
231    pub fn config(&self) -> Option<&SolverConfig> {
232        self.config.as_ref()
233    }
234
235    pub(crate) fn with_runtime(mut self, runtime: SolverRuntime<S>) -> Self {
236        self.runtime = Some(runtime);
237        self
238    }
239}
240
241// Marker type indicating no termination.
242#[derive(Debug, Clone, Copy, Default)]
243pub struct NoTermination;
244
245/// Marker trait for termination types that can be used in Solver.
246pub trait MaybeTermination<
247    S: PlanningSolution,
248    D: Director<S>,
249    ProgressCb: ProgressCallback<S> = (),
250>: Send
251{
252    // Checks if the solver should terminate.
253    fn should_terminate(&self, solver_scope: &SolverScope<'_, S, D, ProgressCb>) -> bool;
254
255    /* Installs in-phase termination limits on the solver scope.
256
257    This allows `Termination` conditions (step count, move count, etc.) to fire
258    inside the phase step loop, not only between phases (T1 fix).
259
260    The default implementation is a no-op. Override for terminations that
261    express a concrete limit via a scope field.
262    */
263    fn install_inphase_limits(&self, _solver_scope: &mut SolverScope<'_, S, D, ProgressCb>) {}
264}
265
266impl<S, D, ProgressCb, T> MaybeTermination<S, D, ProgressCb> for Option<T>
267where
268    S: PlanningSolution,
269    D: Director<S>,
270    ProgressCb: ProgressCallback<S>,
271    T: Termination<S, D, ProgressCb>,
272{
273    fn should_terminate(&self, solver_scope: &SolverScope<'_, S, D, ProgressCb>) -> bool {
274        match self {
275            Some(t) => t.is_terminated(solver_scope),
276            None => false,
277        }
278    }
279
280    fn install_inphase_limits(&self, solver_scope: &mut SolverScope<'_, S, D, ProgressCb>) {
281        if let Some(t) = self {
282            t.install_inphase_limits(solver_scope);
283        }
284    }
285}
286
287impl<S, D, ProgressCb> MaybeTermination<S, D, ProgressCb> for NoTermination
288where
289    S: PlanningSolution,
290    D: Director<S>,
291    ProgressCb: ProgressCallback<S>,
292{
293    fn should_terminate(&self, _solver_scope: &SolverScope<'_, S, D, ProgressCb>) -> bool {
294        false
295    }
296
297    // install_inphase_limits: no-op (default)
298}
299
300impl<S, D, ProgressCb> Termination<S, D, ProgressCb> for NoTermination
301where
302    S: PlanningSolution,
303    D: Director<S>,
304    ProgressCb: ProgressCallback<S>,
305{
306    fn is_terminated(&self, _solver_scope: &SolverScope<'_, S, D, ProgressCb>) -> bool {
307        false
308    }
309}
310
311macro_rules! impl_solver {
312    ($($idx:tt: $P:ident),+) => {
313        impl<'t, S, D, T, ProgressCb, $($P),+> Solver<'t, ($($P,)+), T, S, D, ProgressCb>
314        where
315            S: PlanningSolution,
316            D: Director<S>,
317            T: MaybeTermination<S, D, ProgressCb>,
318            ProgressCb: ProgressCallback<S>,
319            $($P: Phase<S, D, ProgressCb>,)+
320        {
321            /// Solves using the provided score director.
322            ///
323            /// Returns a `SolveResult` containing the best solution found
324            /// and comprehensive solver statistics.
325            pub fn solve(self, score_director: D) -> SolveResult<S> {
326                let Solver {
327                    mut phases,
328                    termination,
329                    terminate,
330                    runtime,
331                    config,
332                    candidate_trace_execution_policy,
333                    candidate_trace_qualified_run_provenance,
334                    time_limit,
335                    progress_callback,
336                    ..
337                } = self;
338
339                let mut solver_scope = SolverScope::new_with_callback(
340                    score_director,
341                    progress_callback,
342                    terminate,
343                    runtime,
344                );
345                if let Some(trace_config) = config.as_ref().and_then(|config| config.candidate_trace) {
346                    let phase_children = vec![$(phases.$idx.candidate_trace_plan(),)+];
347                    let resolved_phase_plan = CandidateTracePhasePlan::known(
348                        "solverforge.solver",
349                        [("top_level_phase_count", phase_children.len().to_string())],
350                        phase_children,
351                    );
352                    let configured_input = config
353                        .as_ref()
354                        .expect("candidate trace configuration requires SolverConfig")
355                        .canonical_toml();
356                    let execution_policy = candidate_trace_execution_policy.unwrap_or_else(|| {
357                        CandidateTraceExecutionPolicy::opaque_with_attributes(
358                            "solverforge.execution_policy.direct_generic_termination",
359                            [(
360                                "solver_with_time_limit_ns",
361                                time_limit.map_or_else(
362                                    || "none".to_string(),
363                                    |limit| limit.as_nanos().to_string(),
364                                ),
365                            )],
366                        )
367                    });
368                    let header = match candidate_trace_qualified_run_provenance {
369                        Some(provenance) => CandidateTraceHeader::new_qualified(
370                            configured_input,
371                            execution_policy,
372                            resolved_phase_plan,
373                            provenance,
374                        ),
375                        None => CandidateTraceHeader::new(
376                            configured_input,
377                            execution_policy,
378                            resolved_phase_plan,
379                            None,
380                        ),
381                    };
382                    solver_scope.enable_candidate_trace(header, trace_config.max_entries.get());
383                }
384                if let Some(environment_mode) = config.as_ref().map(|cfg| cfg.environment_mode) {
385                    solver_scope = solver_scope.with_environment_mode(environment_mode);
386                }
387                if let Some(seed) = config.as_ref().and_then(|cfg| cfg.random_seed) {
388                    solver_scope = solver_scope.with_seed(seed);
389                }
390                if let Some(limit) = time_limit {
391                    solver_scope.set_time_limit(limit);
392                }
393                solver_scope.initialize_working_solution_as_best();
394                solver_scope.report_best_solution();
395                solver_scope.pause_if_requested();
396
397                // Install in-phase termination limits so phases can check them
398                // inside their step loops (T1: StepCountTermination, MoveCountTermination, etc.)
399                termination.install_inphase_limits(&mut solver_scope);
400
401                // Execute phases with termination checking
402                $(
403                    solver_scope.pause_if_requested();
404                    if !check_termination(&termination, &mut solver_scope) {
405                        tracing::debug!(
406                            "Starting phase {} ({})",
407                            $idx,
408                            phases.$idx.phase_type_name()
409                        );
410                        phases.$idx.solve(&mut solver_scope);
411                        solver_scope.pause_if_requested();
412                        tracing::debug!(
413                            "Finished phase {} ({}) with score {:?}",
414                            $idx,
415                            phases.$idx.phase_type_name(),
416                            solver_scope.best_score()
417                        );
418                    }
419                )+
420
421                // Every configured top-level phase receives one terminal
422                // lifecycle notification, even if cancellation or configured
423                // termination skipped its solve method. This is deliberately
424                // after the full phase loop so hooks cannot observe or create
425                // an intermediate completion boundary.
426                $(
427                    phases.$idx.on_solver_terminal(&mut solver_scope);
428                    solver_scope.pause_if_requested();
429                    let _ = check_termination(&termination, &mut solver_scope);
430                )+
431
432                // Extract solution and stats before consuming scope
433                let (solution, current_score, best_score, stats, terminal_reason) =
434                    solver_scope.take_solution_and_stats();
435                SolveResult {
436                    solution,
437                    current_score,
438                    best_score,
439                    terminal_reason,
440                    stats,
441                }
442            }
443        }
444    };
445}
446
447fn check_termination<S, D, ProgressCb, T>(
448    termination: &T,
449    solver_scope: &mut SolverScope<'_, S, D, ProgressCb>,
450) -> bool
451where
452    S: PlanningSolution,
453    D: Director<S>,
454    ProgressCb: ProgressCallback<S>,
455    T: MaybeTermination<S, D, ProgressCb>,
456{
457    if solver_scope.is_terminate_early() {
458        solver_scope.mark_cancelled();
459        return true;
460    }
461    if termination.should_terminate(solver_scope) {
462        solver_scope.mark_terminated_by_config();
463        true
464    } else {
465        false
466    }
467}
468
469macro_rules! impl_solver_with_director {
470    ($($idx:tt: $P:ident),+) => {
471        impl<'t, S, T, ProgressCb, $($P),+> Solver<'t, ($($P,)+), T, S, (), ProgressCb>
472        where
473            S: PlanningSolution,
474            T: Send,
475            ProgressCb: Send + Sync,
476        {
477            /// Solves using a provided score director.
478            pub fn solve_with_director<D>(self, director: D) -> SolveResult<S>
479            where
480                D: Director<S>,
481                ProgressCb: ProgressCallback<S>,
482                T: MaybeTermination<S, D, ProgressCb>,
483                $($P: Phase<S, D, ProgressCb>,)+
484            {
485                let solver: Solver<'t, ($($P,)+), T, S, D, ProgressCb> = Solver {
486                    phases: self.phases,
487                    termination: self.termination,
488                    terminate: self.terminate,
489                    runtime: self.runtime,
490                    config: self.config,
491                    candidate_trace_execution_policy: self.candidate_trace_execution_policy,
492                    candidate_trace_qualified_run_provenance: self.candidate_trace_qualified_run_provenance,
493                    time_limit: self.time_limit,
494                    progress_callback: self.progress_callback,
495                    _phantom: PhantomData,
496                };
497                solver.solve(director)
498            }
499        }
500    };
501}
502
503impl_solver_with_director!(0: P0);
504impl_solver_with_director!(0: P0, 1: P1);
505impl_solver_with_director!(0: P0, 1: P1, 2: P2);
506impl_solver_with_director!(0: P0, 1: P1, 2: P2, 3: P3);
507impl_solver_with_director!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4);
508impl_solver_with_director!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4, 5: P5);
509impl_solver_with_director!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4, 5: P5, 6: P6);
510impl_solver_with_director!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4, 5: P5, 6: P6, 7: P7);
511
512impl_solver!(0: P0);
513impl_solver!(0: P0, 1: P1);
514impl_solver!(0: P0, 1: P1, 2: P2);
515impl_solver!(0: P0, 1: P1, 2: P2, 3: P3);
516impl_solver!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4);
517impl_solver!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4, 5: P5);
518impl_solver!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4, 5: P5, 6: P6);
519impl_solver!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4, 5: P5, 6: P6, 7: P7);
520
521#[cfg(test)]
522#[path = "solver_tests.rs"]
523mod tests;