1use std::cell::RefCell;
4
5use xlog_core::{Result, XlogError};
6
7use crate::{Clause, Literal, Objective, SolveInstance};
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub enum SolverServiceStatus {
12 Sat,
14 Unsat,
16 Unknown,
18 Timeout,
20 Optimal(u64),
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum SolverServiceBudget {
27 NoSearch,
29 AssignmentLimit(u64),
31 Exhaustive,
33}
34
35#[derive(Debug, Clone, Default, PartialEq, Eq)]
37pub struct SolverServiceTrace {
38 pub learned_clause_transfers: usize,
40 pub cpu_assignment_enumerations: u64,
42 pub cpu_maxsat_enumerations: u64,
44}
45
46impl SolverServiceTrace {
47 pub fn require_production_metric_eligibility(&self) -> Result<()> {
49 Err(XlogError::UnsupportedEpistemicConstruct {
50 construct: "CPU semantic-oracle solver service".to_string(),
51 context: format!(
52 "SolverService is fixture-only and cannot satisfy production solver metrics; \
53 cpu_assignment_enumerations={} cpu_maxsat_enumerations={} \
54 learned_clause_transfers={}",
55 self.cpu_assignment_enumerations,
56 self.cpu_maxsat_enumerations,
57 self.learned_clause_transfers
58 ),
59 })
60 }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct SolverServiceResult {
66 pub status: SolverServiceStatus,
68 pub assignment: Option<Vec<bool>>,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub struct LearnedClauseTransfer {
75 pub clauses: usize,
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum SolverPortfolioStatus {
82 Deferred {
84 reason: &'static str,
86 },
87}
88
89pub struct SolverService {
91 instance: SolveInstance,
92 assumptions: Vec<Option<Literal>>,
93 learned_clauses: RefCell<Vec<ScopedLearnedClause>>,
94 trace: RefCell<SolverServiceTrace>,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
98struct ScopedLearnedClause {
99 clause: Clause,
100 assumption_scope: Vec<Literal>,
101}
102
103impl SolverService {
104 pub fn new(instance: SolveInstance) -> Self {
106 Self {
107 instance,
108 assumptions: Vec::new(),
109 learned_clauses: RefCell::new(Vec::new()),
110 trace: RefCell::new(SolverServiceTrace::default()),
111 }
112 }
113
114 pub fn assume(&mut self, literal: Literal) -> usize {
116 let token = self.assumptions.len();
117 self.assumptions.push(Some(literal));
118 token
119 }
120
121 pub fn retract_assumption(&mut self, token: usize) -> bool {
123 let Some(slot) = self.assumptions.get_mut(token) else {
124 return false;
125 };
126 let existed = slot.is_some();
127 *slot = None;
128 existed
129 }
130
131 pub fn solve(&self) -> SolverServiceResult {
133 self.solve_with_budget(SolverServiceBudget::Exhaustive)
134 }
135
136 pub fn solve_with_budget(&self, budget: SolverServiceBudget) -> SolverServiceResult {
138 match budget {
139 SolverServiceBudget::NoSearch => SolverServiceResult {
140 status: SolverServiceStatus::Unknown,
141 assignment: None,
142 },
143 SolverServiceBudget::AssignmentLimit(0) => SolverServiceResult {
144 status: SolverServiceStatus::Timeout,
145 assignment: None,
146 },
147 SolverServiceBudget::AssignmentLimit(limit) => self.solve_assignments(Some(limit)),
148 SolverServiceBudget::Exhaustive => self.solve_assignments(None),
149 }
150 }
151
152 pub fn transfer_learned_clauses_to(&self, target: &mut SolverService) -> LearnedClauseTransfer {
154 let learned = self.learned_clauses.borrow();
155 let count = learned.len();
156 target
157 .learned_clauses
158 .borrow_mut()
159 .extend(learned.iter().cloned());
160 target.trace.borrow_mut().learned_clause_transfers += count;
161 LearnedClauseTransfer { clauses: count }
162 }
163
164 pub fn trace(&self) -> SolverServiceTrace {
166 self.trace.borrow().clone()
167 }
168
169 pub fn gpu_portfolio_status(&self) -> SolverPortfolioStatus {
171 SolverPortfolioStatus::Deferred {
172 reason: "GPU portfolio solving is not implemented in the semantic-oracle facade and blocks solver-service integration closure",
173 }
174 }
175
176 fn solve_assignments(&self, limit: Option<u64>) -> SolverServiceResult {
177 let max = 1u64.checked_shl(self.instance.num_vars).unwrap_or(u64::MAX);
178 let search_max = limit.map_or(max, |limit| limit.min(max));
179 let hard_clauses = self.hard_clauses();
180
181 if self.instance.objective == Objective::MaxSat {
182 return self.solve_maxsat(
183 search_max,
184 limit.is_some() && search_max < max,
185 &hard_clauses,
186 );
187 }
188
189 for mask in 0..search_max {
190 let assignment = assignment_from_mask(self.instance.num_vars, mask);
191 if hard_clauses
192 .iter()
193 .all(|clause| clause.is_satisfied(&assignment))
194 {
195 self.record_cpu_assignment_enumerations(mask.saturating_add(1));
196 return SolverServiceResult {
197 status: SolverServiceStatus::Sat,
198 assignment: Some(assignment),
199 };
200 }
201 }
202 self.record_cpu_assignment_enumerations(search_max);
203
204 if limit.is_some() && search_max < max {
205 return SolverServiceResult {
206 status: SolverServiceStatus::Timeout,
207 assignment: None,
208 };
209 }
210
211 self.record_unsat_learning(&hard_clauses);
212 SolverServiceResult {
213 status: SolverServiceStatus::Unsat,
214 assignment: None,
215 }
216 }
217
218 fn solve_maxsat(
219 &self,
220 search_max: u64,
221 timed_out: bool,
222 hard_clauses: &[Clause],
223 ) -> SolverServiceResult {
224 let mut best_assignment = None;
225 let mut best_score = f64::NEG_INFINITY;
226 for mask in 0..search_max {
227 let assignment = assignment_from_mask(self.instance.num_vars, mask);
228 if !hard_clauses
229 .iter()
230 .all(|clause| clause.is_satisfied(&assignment))
231 {
232 continue;
233 }
234 let score = self.instance.weighted_satisfaction(&assignment);
235 if score > best_score {
236 best_score = score;
237 best_assignment = Some(assignment);
238 }
239 }
240 self.record_cpu_maxsat_enumerations(search_max);
241
242 if timed_out {
243 return SolverServiceResult {
244 status: SolverServiceStatus::Timeout,
245 assignment: None,
246 };
247 }
248
249 match best_assignment {
250 Some(assignment) => SolverServiceResult {
251 status: SolverServiceStatus::Optimal(best_score as u64),
252 assignment: Some(assignment),
253 },
254 None => SolverServiceResult {
255 status: SolverServiceStatus::Unsat,
256 assignment: None,
257 },
258 }
259 }
260
261 fn hard_clauses(&self) -> Vec<Clause> {
262 let mut clauses = if self.instance.objective == Objective::MaxSat {
263 Vec::new()
264 } else {
265 self.instance.clauses.clone()
266 };
267 let active_assumptions = self.active_assumptions();
268 clauses.extend(active_assumptions.iter().copied().map(Clause::unit));
269 clauses.extend(
270 self.learned_clauses
271 .borrow()
272 .iter()
273 .filter(|learned| {
274 learned
275 .assumption_scope
276 .iter()
277 .all(|literal| active_assumptions.contains(literal))
278 })
279 .map(|learned| learned.clause.clone()),
280 );
281 clauses
282 }
283
284 fn record_unsat_learning(&self, hard_clauses: &[Clause]) {
285 let mut learned = self.learned_clauses.borrow_mut();
286 if learned.is_empty() && !hard_clauses.is_empty() {
287 learned.push(ScopedLearnedClause {
288 clause: Clause::new(Vec::new()),
289 assumption_scope: self.active_assumptions(),
290 });
291 }
292 }
293
294 fn record_cpu_assignment_enumerations(&self, count: u64) {
295 let mut trace = self.trace.borrow_mut();
296 trace.cpu_assignment_enumerations = trace.cpu_assignment_enumerations.saturating_add(count);
297 }
298
299 fn record_cpu_maxsat_enumerations(&self, count: u64) {
300 let mut trace = self.trace.borrow_mut();
301 trace.cpu_maxsat_enumerations = trace.cpu_maxsat_enumerations.saturating_add(count);
302 }
303
304 fn active_assumptions(&self) -> Vec<Literal> {
305 self.assumptions.iter().flatten().copied().collect()
306 }
307}
308
309fn assignment_from_mask(num_vars: u32, mask: u64) -> Vec<bool> {
310 (0..num_vars)
311 .map(|var| (mask & (1u64 << var)) != 0)
312 .collect()
313}