| //! Incremental solving. |
| |
| use partial_ref::{partial, PartialRef}; |
| |
| use varisat_formula::Lit; |
| use varisat_internal_proof::{clause_hash, lit_hash, ClauseHash, ProofStep}; |
| |
| use crate::{ |
| context::{parts::*, Context}, |
| proof, |
| prop::{enqueue_assignment, full_restart, Reason}, |
| state::SatState, |
| variables, |
| }; |
| |
| /// Incremental solving. |
| #[derive(Default)] |
| pub struct Assumptions { |
| assumptions: Vec<Lit>, |
| failed_core: Vec<Lit>, |
| user_failed_core: Vec<Lit>, |
| assumption_levels: usize, |
| failed_propagation_hashes: Vec<ClauseHash>, |
| } |
| |
| impl Assumptions { |
| /// Current number of decision levels used for assumptions. |
| pub fn assumption_levels(&self) -> usize { |
| self.assumption_levels |
| } |
| |
| /// Resets assumption_levels to zero on a full restart. |
| pub fn full_restart(&mut self) { |
| self.assumption_levels = 0; |
| } |
| |
| /// Subset of assumptions that made the formula unsatisfiable. |
| pub fn failed_core(&self) -> &[Lit] { |
| &self.failed_core |
| } |
| |
| /// Subset of assumptions that made the formula unsatisfiable. |
| pub fn user_failed_core(&self) -> &[Lit] { |
| &self.user_failed_core |
| } |
| |
| /// Current assumptions. |
| pub fn assumptions(&self) -> &[Lit] { |
| &self.assumptions |
| } |
| } |
| |
| /// Return type of [`enqueue_assumption`]. |
| pub enum EnqueueAssumption { |
| Done, |
| Enqueued, |
| Conflict, |
| } |
| |
| /// Change the currently active assumptions. |
| /// |
| /// The input uses user variable names. |
| pub fn set_assumptions<'a>( |
| mut ctx: partial!( |
| Context<'a>, |
| mut AnalyzeConflictP, |
| mut AssignmentP, |
| mut AssumptionsP, |
| mut BinaryClausesP, |
| mut ImplGraphP, |
| mut ProofP<'a>, |
| mut SolverStateP, |
| mut TmpFlagsP, |
| mut TrailP, |
| mut VariablesP, |
| mut VsidsP, |
| mut WatchlistsP, |
| ), |
| user_assumptions: &[Lit], |
| ) { |
| full_restart(ctx.borrow()); |
| |
| let state = ctx.part_mut(SolverStateP); |
| |
| state.sat_state = match state.sat_state { |
| SatState::Unsat => SatState::Unsat, |
| SatState::Sat | SatState::UnsatUnderAssumptions | SatState::Unknown => SatState::Unknown, |
| }; |
| |
| let (assumptions, mut ctx_2) = ctx.split_part_mut(AssumptionsP); |
| |
| for lit in assumptions.assumptions.iter() { |
| ctx_2 |
| .part_mut(VariablesP) |
| .var_data_solver_mut(lit.var()) |
| .assumed = false; |
| } |
| |
| variables::solver_from_user_lits( |
| ctx_2.borrow(), |
| &mut assumptions.assumptions, |
| user_assumptions, |
| true, |
| ); |
| |
| for lit in assumptions.assumptions.iter() { |
| ctx_2 |
| .part_mut(VariablesP) |
| .var_data_solver_mut(lit.var()) |
| .assumed = true; |
| } |
| |
| proof::add_step( |
| ctx_2.borrow(), |
| true, |
| &ProofStep::Assumptions { |
| assumptions: &assumptions.assumptions, |
| }, |
| ); |
| } |
| |
| /// Enqueue another assumption if possible. |
| /// |
| /// Returns whether an assumption was enqueued, whether no assumptions are left or whether the |
| /// assumptions result in a conflict. |
| pub fn enqueue_assumption<'a>( |
| mut ctx: partial!( |
| Context<'a>, |
| mut AssignmentP, |
| mut AssumptionsP, |
| mut ImplGraphP, |
| mut ProofP<'a>, |
| mut SolverStateP, |
| mut TmpFlagsP, |
| mut TrailP, |
| ClauseAllocP, |
| VariablesP, |
| ), |
| ) -> EnqueueAssumption { |
| while let Some(&assumption) = ctx |
| .part(AssumptionsP) |
| .assumptions |
| .get(ctx.part(TrailP).current_level()) |
| { |
| match ctx.part(AssignmentP).lit_value(assumption) { |
| Some(false) => { |
| analyze_assumption_conflict(ctx.borrow(), assumption); |
| return EnqueueAssumption::Conflict; |
| } |
| Some(true) => { |
| // The next assumption is already implied by other assumptions so we can remove it. |
| let level = ctx.part(TrailP).current_level(); |
| let assumptions = ctx.part_mut(AssumptionsP); |
| assumptions.assumptions.swap_remove(level); |
| } |
| None => { |
| ctx.part_mut(TrailP).new_decision_level(); |
| enqueue_assignment(ctx.borrow(), assumption, Reason::Unit); |
| let (assumptions, ctx) = ctx.split_part_mut(AssumptionsP); |
| assumptions.assumption_levels = ctx.part(TrailP).current_level(); |
| return EnqueueAssumption::Enqueued; |
| } |
| } |
| } |
| EnqueueAssumption::Done |
| } |
| |
| /// Analyze a conflicting set of assumptions. |
| /// |
| /// Compute a set of incompatible assumptions given an assumption that is incompatible with the |
| /// assumptions enqueued so far. |
| fn analyze_assumption_conflict<'a>( |
| mut ctx: partial!( |
| Context<'a>, |
| mut AssumptionsP, |
| mut ProofP<'a>, |
| mut SolverStateP, |
| mut TmpFlagsP, |
| ClauseAllocP, |
| ImplGraphP, |
| TrailP, |
| VariablesP, |
| ), |
| assumption: Lit, |
| ) { |
| let (assumptions, mut ctx) = ctx.split_part_mut(AssumptionsP); |
| let (tmp, mut ctx) = ctx.split_part_mut(TmpFlagsP); |
| let (trail, mut ctx) = ctx.split_part(TrailP); |
| let (impl_graph, mut ctx) = ctx.split_part(ImplGraphP); |
| |
| let flags = &mut tmp.flags; |
| |
| assumptions.failed_core.clear(); |
| assumptions.failed_core.push(assumption); |
| |
| assumptions.failed_propagation_hashes.clear(); |
| |
| flags[assumption.index()] = true; |
| let mut flag_count = 1; |
| |
| for &lit in trail.trail().iter().rev() { |
| if flags[lit.index()] { |
| flags[lit.index()] = false; |
| flag_count -= 1; |
| |
| match impl_graph.reason(lit.var()) { |
| Reason::Unit => { |
| if impl_graph.level(lit.var()) > 0 { |
| assumptions.failed_core.push(lit); |
| } |
| } |
| reason => { |
| let (ctx_lits, ctx) = ctx.split_borrow(); |
| let reason_lits = reason.lits(&ctx_lits); |
| |
| if ctx.part(ProofP).clause_hashes_required() { |
| let hash = clause_hash(reason_lits) ^ lit_hash(lit); |
| assumptions.failed_propagation_hashes.push(hash); |
| } |
| |
| for &reason_lit in reason_lits { |
| if !flags[reason_lit.index()] { |
| flags[reason_lit.index()] = true; |
| flag_count += 1; |
| } |
| } |
| } |
| } |
| |
| if flag_count == 0 { |
| break; |
| } |
| } |
| } |
| |
| assumptions.failed_propagation_hashes.reverse(); |
| |
| assumptions.user_failed_core.clear(); |
| assumptions |
| .user_failed_core |
| .extend(assumptions.failed_core.iter().map(|solver_lit| { |
| solver_lit |
| .map_var(|solver_var| ctx.part(VariablesP).existing_user_from_solver(solver_var)) |
| })); |
| |
| proof::add_step( |
| ctx.borrow(), |
| true, |
| &ProofStep::FailedAssumptions { |
| failed_core: &assumptions.failed_core, |
| propagation_hashes: &assumptions.failed_propagation_hashes, |
| }, |
| ); |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| use proptest::{bool, prelude::*}; |
| |
| use partial_ref::IntoPartialRefMut; |
| |
| use varisat_formula::{test::conditional_pigeon_hole, ExtendFormula, Var}; |
| |
| use crate::{cdcl::conflict_step, load::load_clause, solver::Solver, state::SatState}; |
| |
| proptest! { |
| #[test] |
| fn pigeon_hole_unsat_assumption_core_internal( |
| (enable_row, columns, formula) in conditional_pigeon_hole(1..5usize, 1..5usize), |
| chain in bool::ANY, |
| ) { |
| let mut ctx = Context::default(); |
| let mut ctx = ctx.into_partial_ref_mut(); |
| |
| for clause in formula.iter() { |
| load_clause(ctx.borrow(), clause); |
| } |
| |
| if chain { |
| for (&a, &b) in enable_row.iter().zip(enable_row.iter().skip(1)) { |
| load_clause(ctx.borrow(), &[!a, b]); |
| } |
| } |
| |
| while ctx.part(SolverStateP).sat_state == SatState::Unknown { |
| conflict_step(ctx.borrow()); |
| } |
| |
| prop_assert_eq!(ctx.part(SolverStateP).sat_state, SatState::Sat); |
| |
| set_assumptions(ctx.borrow(), &enable_row); |
| |
| prop_assert_eq!(ctx.part(SolverStateP).sat_state, SatState::Unknown); |
| |
| while ctx.part(SolverStateP).sat_state == SatState::Unknown { |
| conflict_step(ctx.borrow()); |
| } |
| |
| prop_assert_eq!(ctx.part(SolverStateP).sat_state, SatState::UnsatUnderAssumptions); |
| |
| let mut candidates = ctx.part(AssumptionsP).user_failed_core().to_owned(); |
| let mut core: Vec<Lit> = vec![]; |
| |
| loop { |
| set_assumptions(ctx.borrow(), &candidates[0..candidates.len() - 1]); |
| |
| while ctx.part(SolverStateP).sat_state == SatState::Unknown { |
| conflict_step(ctx.borrow()); |
| } |
| |
| match ctx.part(SolverStateP).sat_state { |
| SatState::Unknown => unreachable!(), |
| SatState::Unsat => break, |
| SatState::Sat => { |
| let skipped = *candidates.last().unwrap(); |
| core.push(skipped); |
| load_clause(ctx.borrow(), &[skipped]); |
| }, |
| SatState::UnsatUnderAssumptions => { |
| candidates = ctx.part(AssumptionsP).user_failed_core().to_owned(); |
| } |
| } |
| } |
| if chain { |
| prop_assert_eq!(core.len(), 1); |
| } else { |
| prop_assert_eq!(core.len(), columns + 1); |
| } |
| } |
| |
| #[test] |
| fn pigeon_hole_unsat_assumption_core_solver( |
| (enable_row, columns, formula) in conditional_pigeon_hole(1..5usize, 1..5usize), |
| ) { |
| let mut solver = Solver::new(); |
| solver.add_formula(&formula); |
| |
| prop_assert_eq!(solver.solve().ok(), Some(true)); |
| |
| let mut assumptions = enable_row; |
| |
| assumptions.push(Lit::positive(Var::from_index(formula.var_count() + 10))); |
| |
| solver.assume(&assumptions); |
| |
| prop_assert_eq!(solver.solve().ok(), Some(false)); |
| |
| |
| let mut candidates = solver.failed_core().unwrap().to_owned(); |
| let mut core: Vec<Lit> = vec![]; |
| |
| while !candidates.is_empty() { |
| |
| solver.assume(&candidates[0..candidates.len() - 1]); |
| |
| match solver.solve() { |
| Err(_) => unreachable!(), |
| Ok(true) => { |
| let skipped = *candidates.last().unwrap(); |
| core.push(skipped); |
| |
| solver.add_clause(&[skipped]); |
| solver.hide_var(skipped.var()); |
| }, |
| Ok(false) => { |
| candidates = solver.failed_core().unwrap().to_owned(); |
| } |
| } |
| } |
| |
| prop_assert_eq!(core.len(), columns + 1); |
| } |
| } |
| } |