use std::time::{Duration, Instant};
use crate::cnf::{CnfFormula, ShowSet, Space, VarId};
use crate::error::VitriError;
use crate::sat::{Bounded, CaDiCal, Status, WallClockTerminator};
pub fn eliminate_hidden<S: Space>(
formula: &CnfFormula,
show: &ShowSet<S>,
) -> Result<CnfFormula, VitriError> {
if let Some(var) = show.iter_vars().find(|var| var.0 >= formula.num_vars) {
return Err(VitriError::input(format!(
"show variable {} exceeds formula variable count {}",
var.to_dimacs(),
formula.num_vars,
)));
}
Ok(crate::preprocess::bve_project::bve_project(
formula,
&show.mask(formula.num_vars),
))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct HiddenDefinabilityConfig {
pub max_conflicts_per_var: i32,
pub time_budget: Option<Duration>,
}
impl Default for HiddenDefinabilityConfig {
fn default() -> Self {
Self {
max_conflicts_per_var: 2_000,
time_budget: Some(Duration::from_secs(3)),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct HiddenDefinability {
pub defined: Vec<VarId>,
pub not_defined: Vec<VarId>,
pub unknown: Vec<VarId>,
pub wall: Duration,
}
pub fn classify_hidden_defined_by_show<S: Space>(
formula: &CnfFormula,
show: &ShowSet<S>,
hidden: impl IntoIterator<Item = VarId>,
config: HiddenDefinabilityConfig,
) -> Result<HiddenDefinability, VitriError> {
if config.max_conflicts_per_var <= 0 {
return Err(VitriError::config(format!(
"hidden-definability max_conflicts_per_var={} must be positive",
config.max_conflicts_per_var,
)));
}
if config.time_budget == Some(Duration::ZERO) {
return Err(VitriError::config(
"hidden-definability time_budget is armed with zero duration".to_owned(),
));
}
if let Some(var) = show.iter_vars().find(|var| var.0 >= formula.num_vars) {
return Err(VitriError::input(format!(
"show variable {} exceeds formula variable count {}",
var.to_dimacs(),
formula.num_vars,
)));
}
let mut seen = vec![false; formula.num_vars as usize];
let hidden: Vec<VarId> = hidden
.into_iter()
.filter(|var| {
if var.0 >= formula.num_vars {
return true;
}
let first = !seen[var.idx()];
seen[var.idx()] = true;
first
})
.collect();
if let Some(var) = hidden.iter().find(|var| var.0 >= formula.num_vars) {
return Err(VitriError::input(format!(
"hidden variable {} exceeds formula variable count {}",
var.to_dimacs(),
formula.num_vars,
)));
}
if let Some(var) = hidden.iter().find(|&&var| show.contains(var)) {
return Err(VitriError::input(format!(
"variable {} is both shown and requested as hidden",
var.to_dimacs(),
)));
}
let start = Instant::now();
if hidden.is_empty() || formula.clauses.is_empty() {
return Ok(HiddenDefinability {
not_defined: hidden,
wall: start.elapsed(),
..HiddenDefinability::default()
});
}
if formula.clauses.iter().any(|clause| clause.is_empty()) {
return Ok(HiddenDefinability {
defined: hidden,
wall: start.elapsed(),
..HiddenDefinability::default()
});
}
let mut appears = vec![false; formula.num_vars as usize];
let mut incidence = vec![0u32; formula.num_vars as usize];
for clause in &formula.clauses {
for literal in &clause.literals {
appears[literal.var.idx()] = true;
incidence[literal.var.idx()] = incidence[literal.var.idx()].saturating_add(1);
}
}
let candidates: Vec<u32> = (0..formula.num_vars)
.filter(|&var| appears[var as usize])
.collect();
let appearing_show: Vec<u32> = show
.iter_vars()
.filter(|var| appears[var.idx()])
.map(|var| var.0)
.collect();
let mut ordered: Vec<u32> = hidden
.iter()
.filter(|var| appears[var.idx()])
.map(|var| var.0)
.collect();
ordered.sort_by_key(|&var| std::cmp::Reverse((incidence[var as usize], var)));
let absent: Vec<VarId> = hidden
.iter()
.copied()
.filter(|var| !appears[var.idx()])
.collect();
let mut result = HiddenDefinability::default();
if budget_spent(start, config.time_budget) {
result.unknown.extend(absent);
result.unknown.extend(ordered.into_iter().map(VarId));
result.wall = start.elapsed();
return Ok(result);
}
let Some(mut dual) = crate::preprocess::build_dual_cnf_with_indicators(
&formula.clauses,
formula.num_vars as usize,
&candidates,
) else {
result.unknown.extend(absent);
result.unknown.extend(ordered.into_iter().map(VarId));
result.wall = start.elapsed();
return Ok(result);
};
if budget_spent(start, config.time_budget) {
result.unknown.extend(absent);
result.unknown.extend(ordered.into_iter().map(VarId));
result.wall = start.elapsed();
return Ok(result);
}
if !absent.is_empty() {
match solve_with_limits(&mut dual.solver, start, config) {
Status::Unsatisfiable => {
result.defined = hidden;
result.wall = start.elapsed();
return Ok(result);
}
Status::Satisfiable => result.not_defined.extend(absent),
Status::Unknown => result.unknown.extend(absent),
}
}
if budget_spent(start, config.time_budget) {
result.unknown.extend(ordered.into_iter().map(VarId));
result.wall = start.elapsed();
return Ok(result);
}
for (at, &var) in ordered.iter().enumerate() {
if budget_spent(start, config.time_budget) {
result
.unknown
.extend(ordered[at..].iter().copied().map(VarId));
break;
}
dual.solver.assume(dual.layout.original_dimacs(var));
let candidate = candidates
.binary_search(&var)
.expect("an appearing hidden variable is a dual-CNF candidate");
dual.solver.assume(-dual.layout.primed_dimacs(candidate));
for &shown in &appearing_show {
let candidate = candidates
.binary_search(&shown)
.expect("an appearing shown variable is a dual-CNF candidate");
dual.solver.assume(dual.layout.indicator_dimacs(candidate));
}
let status = solve_with_limits(&mut dual.solver, start, config);
match status {
Status::Unsatisfiable => result.defined.push(VarId(var)),
Status::Satisfiable => result.not_defined.push(VarId(var)),
Status::Unknown => result.unknown.push(VarId(var)),
}
}
result.wall = start.elapsed();
Ok(result)
}
fn budget_spent(start: Instant, budget: Option<Duration>) -> bool {
budget.is_some_and(|budget| start.elapsed() >= budget)
}
fn solve_with_limits(
solver: &mut CaDiCal,
start: Instant,
config: HiddenDefinabilityConfig,
) -> Status {
if budget_spent(start, config.time_budget) {
return Status::Unknown;
}
solver.limit(c"conflicts", config.max_conflicts_per_var);
match config.time_budget {
Some(budget) => {
let remaining = budget.saturating_sub(start.elapsed());
Bounded::new(solver, WallClockTerminator::new(remaining)).solve()
}
None => solver.solve(),
}
}