use std::cell::RefCell;
use xlog_core::{Result, XlogError};
use crate::{Clause, Literal, Objective, SolveInstance};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SolverServiceStatus {
Sat,
Unsat,
Unknown,
Timeout,
Optimal(u64),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SolverServiceBudget {
NoSearch,
AssignmentLimit(u64),
Exhaustive,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SolverServiceTrace {
pub learned_clause_transfers: usize,
pub cpu_assignment_enumerations: u64,
pub cpu_maxsat_enumerations: u64,
}
impl SolverServiceTrace {
pub fn require_production_metric_eligibility(&self) -> Result<()> {
Err(XlogError::UnsupportedEpistemicConstruct {
construct: "CPU semantic-oracle solver service".to_string(),
context: format!(
"SolverService is fixture-only and cannot satisfy production solver metrics; \
cpu_assignment_enumerations={} cpu_maxsat_enumerations={} \
learned_clause_transfers={}",
self.cpu_assignment_enumerations,
self.cpu_maxsat_enumerations,
self.learned_clause_transfers
),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SolverServiceResult {
pub status: SolverServiceStatus,
pub assignment: Option<Vec<bool>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LearnedClauseTransfer {
pub clauses: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SolverPortfolioStatus {
Deferred {
reason: &'static str,
},
}
pub struct SolverService {
instance: SolveInstance,
assumptions: Vec<Option<Literal>>,
learned_clauses: RefCell<Vec<ScopedLearnedClause>>,
trace: RefCell<SolverServiceTrace>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ScopedLearnedClause {
clause: Clause,
assumption_scope: Vec<Literal>,
}
impl SolverService {
pub fn new(instance: SolveInstance) -> Self {
Self {
instance,
assumptions: Vec::new(),
learned_clauses: RefCell::new(Vec::new()),
trace: RefCell::new(SolverServiceTrace::default()),
}
}
pub fn assume(&mut self, literal: Literal) -> usize {
let token = self.assumptions.len();
self.assumptions.push(Some(literal));
token
}
pub fn retract_assumption(&mut self, token: usize) -> bool {
let Some(slot) = self.assumptions.get_mut(token) else {
return false;
};
let existed = slot.is_some();
*slot = None;
existed
}
pub fn solve(&self) -> SolverServiceResult {
self.solve_with_budget(SolverServiceBudget::Exhaustive)
}
pub fn solve_with_budget(&self, budget: SolverServiceBudget) -> SolverServiceResult {
match budget {
SolverServiceBudget::NoSearch => SolverServiceResult {
status: SolverServiceStatus::Unknown,
assignment: None,
},
SolverServiceBudget::AssignmentLimit(0) => SolverServiceResult {
status: SolverServiceStatus::Timeout,
assignment: None,
},
SolverServiceBudget::AssignmentLimit(limit) => self.solve_assignments(Some(limit)),
SolverServiceBudget::Exhaustive => self.solve_assignments(None),
}
}
pub fn transfer_learned_clauses_to(&self, target: &mut SolverService) -> LearnedClauseTransfer {
let learned = self.learned_clauses.borrow();
let count = learned.len();
target
.learned_clauses
.borrow_mut()
.extend(learned.iter().cloned());
target.trace.borrow_mut().learned_clause_transfers += count;
LearnedClauseTransfer { clauses: count }
}
pub fn trace(&self) -> SolverServiceTrace {
self.trace.borrow().clone()
}
pub fn gpu_portfolio_status(&self) -> SolverPortfolioStatus {
SolverPortfolioStatus::Deferred {
reason: "GPU portfolio solving is not implemented in the semantic-oracle facade and blocks G090_SOLVER closure",
}
}
fn solve_assignments(&self, limit: Option<u64>) -> SolverServiceResult {
let max = 1u64.checked_shl(self.instance.num_vars).unwrap_or(u64::MAX);
let search_max = limit.map_or(max, |limit| limit.min(max));
let hard_clauses = self.hard_clauses();
if self.instance.objective == Objective::MaxSat {
return self.solve_maxsat(
search_max,
limit.is_some() && search_max < max,
&hard_clauses,
);
}
for mask in 0..search_max {
let assignment = assignment_from_mask(self.instance.num_vars, mask);
if hard_clauses
.iter()
.all(|clause| clause.is_satisfied(&assignment))
{
self.record_cpu_assignment_enumerations(mask.saturating_add(1));
return SolverServiceResult {
status: SolverServiceStatus::Sat,
assignment: Some(assignment),
};
}
}
self.record_cpu_assignment_enumerations(search_max);
if limit.is_some() && search_max < max {
return SolverServiceResult {
status: SolverServiceStatus::Timeout,
assignment: None,
};
}
self.record_unsat_learning(&hard_clauses);
SolverServiceResult {
status: SolverServiceStatus::Unsat,
assignment: None,
}
}
fn solve_maxsat(
&self,
search_max: u64,
timed_out: bool,
hard_clauses: &[Clause],
) -> SolverServiceResult {
let mut best_assignment = None;
let mut best_score = f64::NEG_INFINITY;
for mask in 0..search_max {
let assignment = assignment_from_mask(self.instance.num_vars, mask);
if !hard_clauses
.iter()
.all(|clause| clause.is_satisfied(&assignment))
{
continue;
}
let score = self.instance.weighted_satisfaction(&assignment);
if score > best_score {
best_score = score;
best_assignment = Some(assignment);
}
}
self.record_cpu_maxsat_enumerations(search_max);
if timed_out {
return SolverServiceResult {
status: SolverServiceStatus::Timeout,
assignment: None,
};
}
match best_assignment {
Some(assignment) => SolverServiceResult {
status: SolverServiceStatus::Optimal(best_score as u64),
assignment: Some(assignment),
},
None => SolverServiceResult {
status: SolverServiceStatus::Unsat,
assignment: None,
},
}
}
fn hard_clauses(&self) -> Vec<Clause> {
let mut clauses = if self.instance.objective == Objective::MaxSat {
Vec::new()
} else {
self.instance.clauses.clone()
};
let active_assumptions = self.active_assumptions();
clauses.extend(active_assumptions.iter().copied().map(Clause::unit));
clauses.extend(
self.learned_clauses
.borrow()
.iter()
.filter(|learned| {
learned
.assumption_scope
.iter()
.all(|literal| active_assumptions.contains(literal))
})
.map(|learned| learned.clause.clone()),
);
clauses
}
fn record_unsat_learning(&self, hard_clauses: &[Clause]) {
let mut learned = self.learned_clauses.borrow_mut();
if learned.is_empty() && !hard_clauses.is_empty() {
learned.push(ScopedLearnedClause {
clause: Clause::new(Vec::new()),
assumption_scope: self.active_assumptions(),
});
}
}
fn record_cpu_assignment_enumerations(&self, count: u64) {
let mut trace = self.trace.borrow_mut();
trace.cpu_assignment_enumerations = trace.cpu_assignment_enumerations.saturating_add(count);
}
fn record_cpu_maxsat_enumerations(&self, count: u64) {
let mut trace = self.trace.borrow_mut();
trace.cpu_maxsat_enumerations = trace.cpu_maxsat_enumerations.saturating_add(count);
}
fn active_assumptions(&self) -> Vec<Literal> {
self.assumptions.iter().flatten().copied().collect()
}
}
fn assignment_from_mask(num_vars: u32, mask: u64) -> Vec<bool> {
(0..num_vars)
.map(|var| (mask & (1u64 << var)) != 0)
.collect()
}