Skip to main content

problemreductions/solvers/
resolver.rs

1//! Shared deterministic solver dispatch.
2
3use super::registry::CompiledIlpPipeline;
4use super::registry::{solver_capability_registry, CustomizedSolverRegistration, ExactProblemKey};
5use crate::registry::LoadedDynProblem;
6use serde::Serialize;
7
8/// Public solver override. Omission is represented by [`SolverRequest::Default`].
9#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
10pub enum SolverRequest {
11    #[default]
12    Default,
13    Customized,
14    Ilp,
15    BruteForce,
16}
17
18/// Information about the backend execution that produced a solve result.
19#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
20#[serde(tag = "kind", rename_all = "kebab-case")]
21pub enum SolverExecution {
22    Customized { implementation: &'static str },
23    Ilp { reduction_path: Vec<String> },
24    BruteForce,
25}
26
27/// Type-erased result returned by deterministic solver dispatch.
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub struct SolveResult {
30    pub solver: SolverExecution,
31    pub outcome: SolveOutcome,
32}
33
34/// Semantic result of a completed solve under the selected backend's numerical contract.
35#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
36#[serde(tag = "status", rename_all = "snake_case")]
37pub enum SolveOutcome {
38    /// The selected backend established optimality and returned a solution.
39    /// ILP optimality is subject to backend numerical tolerances.
40    Optimal {
41        solution: serde_json::Value,
42        evaluation: String,
43    },
44    /// The selected backend established infeasibility under its numerical contract.
45    Infeasible,
46}
47
48fn problem_key(problem: &LoadedDynProblem) -> ExactProblemKey {
49    ExactProblemKey::new(problem.problem_name(), problem.variant_map())
50}
51
52fn solve_customized(
53    problem: &LoadedDynProblem,
54    registration: &'static CustomizedSolverRegistration,
55) -> Result<SolveResult, super::SolveError> {
56    let outcome = match (registration.solve_fn)(problem.as_any())? {
57        Some(solution) => SolveOutcome::Optimal {
58            evaluation: problem.evaluate_dyn(&solution)?,
59            solution,
60        },
61        None => SolveOutcome::Infeasible,
62    };
63    Ok(SolveResult {
64        solver: SolverExecution::Customized {
65            implementation: registration.implementation,
66        },
67        outcome,
68    })
69}
70
71fn solve_ilp(
72    problem: &LoadedDynProblem,
73    pipeline: &CompiledIlpPipeline,
74) -> Result<SolveResult, super::SolveError> {
75    let outcome = match pipeline.solve(problem.as_any(), &super::ILPSolver::new()) {
76        Ok(solution) => SolveOutcome::Optimal {
77            evaluation: problem.evaluate_dyn(&solution)?,
78            solution,
79        },
80        Err(super::ILPSolveError::Infeasible) => SolveOutcome::Infeasible,
81        Err(source) => {
82            return Err(super::SolveError::IlpSolve {
83                problem: problem_key(problem).label(),
84                source,
85            });
86        }
87    };
88    Ok(SolveResult {
89        solver: SolverExecution::Ilp {
90            reduction_path: pipeline.path_labels(),
91        },
92        outcome,
93    })
94}
95
96fn solve_brute_force(
97    problem: &LoadedDynProblem,
98    registration: &'static super::BruteForceRegistration,
99) -> Result<SolveResult, super::SolveError> {
100    let outcome = match (registration.solve_fn)(problem.as_any())? {
101        Some((solution, evaluation)) => SolveOutcome::Optimal {
102            solution,
103            evaluation,
104        },
105        None => SolveOutcome::Infeasible,
106    };
107    Ok(SolveResult {
108        solver: SolverExecution::BruteForce,
109        outcome,
110    })
111}
112
113/// Solve a loaded problem using deterministic exact-variant dispatch.
114///
115/// Default dispatch is customized, then the registered fixed ILP pipeline, then
116/// brute force. Once selected, backend failure is returned without fallback.
117pub fn solve(
118    problem: &LoadedDynProblem,
119    request: SolverRequest,
120) -> Result<SolveResult, super::SolveError> {
121    let registry = solver_capability_registry().map_err(super::SolveError::InvalidRegistry)?;
122    let key = problem_key(problem);
123    let capabilities = registry.lookup(&key);
124
125    match request {
126        SolverRequest::BruteForce => solve_brute_force(
127            problem,
128            capabilities
129                .brute_force
130                .ok_or_else(|| super::SolveError::MissingRegistration(key.label()))?,
131        ),
132        SolverRequest::Customized => {
133            let registration = capabilities
134                .customized
135                .ok_or_else(|| super::SolveError::MissingCustomizedCapability(key.label()))?;
136            solve_customized(problem, registration)
137        }
138        SolverRequest::Ilp => {
139            let pipeline = capabilities
140                .ilp
141                .ok_or_else(|| super::SolveError::MissingIlpCapability(key.label()))?;
142            solve_ilp(problem, pipeline)
143        }
144        SolverRequest::Default => {
145            if let Some(customized) = capabilities.customized {
146                return solve_customized(problem, customized);
147            }
148            if let Some(pipeline) = capabilities.ilp {
149                return solve_ilp(problem, pipeline);
150            }
151            solve_brute_force(
152                problem,
153                capabilities
154                    .brute_force
155                    .ok_or_else(|| super::SolveError::MissingRegistration(key.label()))?,
156            )
157        }
158    }
159}
160
161#[cfg(test)]
162#[path = "../unit_tests/solvers/resolver.rs"]
163mod tests;