use crate::cnf::{CnfFormula, Literal, Reduced, ShowSet, Space, Weights};
use crate::diagnostics::diag;
use crate::error::VitriError;
use std::time::{Duration, Instant};
use super::arjun::{ArjunEffort, ArjunOptions, ArjunProjResult, ArjunResult};
use super::fork_budget::{ForkOutcome, run_forked_with_deadline};
mod budget_class;
mod shim;
use budget_class::keep_after_deadline;
pub(in crate::preprocess) use budget_class::keep_overrun_enabled;
use shim::{ArjunLib, validate_shim_env};
pub(super) const LITE_BACKBONE_MAX_CONFL_DEFAULT: i64 = -1;
pub(super) const ORACLE_FULL_WORSTCASE_MS: u128 = 30_000;
pub(super) const ORACLE_MULT_MIN: f64 = 0.05;
pub(super) fn oracle_mult_for_budget(remaining_ms: u128) -> f64 {
let raw = remaining_ms as f64 / ORACLE_FULL_WORSTCASE_MS as f64;
raw.clamp(ORACLE_MULT_MIN, 1.0)
}
pub(super) const PROJECTED_ORACLE_MAX_VARS_DEFAULT: u32 = 100_000;
pub(super) const ORACLE_MAX_VARS_FORM: &str =
"a variable count, above which the reduce skips Arjun's oracle";
pub(super) fn projected_oracle_max_vars(var: &'static str) -> Result<u32, VitriError> {
crate::env::parse(var, PROJECTED_ORACLE_MAX_VARS_DEFAULT, ORACLE_MAX_VARS_FORM)
}
const ARJUN_EFFORT_FORMS: &str = "`full` (default) or `lite`";
pub(super) fn parse_arjun_effort(val: Option<&str>) -> Result<ArjunEffort, VitriError> {
crate::env::from_forms(
"VITRI_ARJUN_EFFORT",
val,
ArjunEffort::Full,
&[("full", ArjunEffort::Full), ("lite", ArjunEffort::Lite)],
ARJUN_EFFORT_FORMS,
)
}
pub(crate) fn resolve_arjun_effort() -> Result<ArjunEffort, VitriError> {
let raw = crate::env::env_raw("VITRI_ARJUN_EFFORT", ARJUN_EFFORT_FORMS)?;
parse_arjun_effort(raw.as_deref())
}
pub(super) enum Spent {
Unmeasured,
Elapsed(Duration),
ElapsedOfBudget(Duration, Duration),
}
pub(super) fn giveup_line(label: &str, why: std::fmt::Arguments<'_>, spent: Spent) -> String {
match spent {
Spent::Unmeasured => format!("[{label}] give-up: {why}"),
Spent::Elapsed(elapsed) => format!(
"[{label}] give-up: {why} after {:.1}s",
elapsed.as_secs_f64()
),
Spent::ElapsedOfBudget(elapsed, budget) => format!(
"[{label}] give-up: {why} after {:.1}s (budget {:.1}s)",
elapsed.as_secs_f64(),
budget.as_secs_f64()
),
}
}
fn giveup(label: &str, why: std::fmt::Arguments<'_>, spent: Spent) {
diag!("{}", giveup_line(label, why, spent));
}
fn multiplier_or_giveup(a: &ArjunLib) -> Option<String> {
match a.cur_multiplier_decimal() {
Ok(decimal) => Some(decimal),
Err(e) => {
giveup("arjun-anytime", format_args!("{e}"), Spent::Unmeasured);
None
}
}
}
fn lit_weight_or_giveup(a: &ArjunLib, lit: i32) -> Option<num_rational::BigRational> {
let decimal = match a.lit_weight_decimal(lit) {
Ok(decimal) => decimal,
Err(e) => {
giveup("arjun-anytime", format_args!("{e}"), Spent::Unmeasured);
return None;
}
};
match crate::cnf::parse_weight(decimal.trim()) {
Ok(w) => Some(w),
Err(e) => {
giveup(
"arjun-anytime",
format_args!("literal {lit} weight {decimal:?} does not parse: {e}"),
Spent::Unmeasured,
);
None
}
}
}
pub(super) fn multiplier_decimal_to_exp(decimal: &str) -> Option<u32> {
use num_bigint::BigUint;
use num_traits::{One, Zero};
let n: BigUint = decimal.trim().parse().ok()?;
if n.is_zero() {
return None;
}
let tz = n.trailing_zeros()?;
if (BigUint::one() << tz) == n {
u32::try_from(tz).ok()
} else {
None
}
}
pub(crate) fn export_learned_clauses_enabled() -> Result<bool, VitriError> {
crate::env::env_flag("VITRI_ARJUN_EXPORT_LEARNED_CLAUSES")
}
fn finish_forked<T>(
label: &str,
outcome: ForkOutcome<Option<T>>,
started: Instant,
deadline: Instant,
) -> Option<T> {
match outcome {
ForkOutcome::Completed(r) => r,
ForkOutcome::Killed { .. } => {
giveup(
label,
format_args!("hard-killed at deadline"),
Spent::ElapsedOfBudget(
started.elapsed(),
deadline.saturating_duration_since(started),
),
);
None
}
ForkOutcome::Failed(why) => {
giveup(
label,
format_args!("forked arjun failed ({why})"),
Spent::Elapsed(started.elapsed()),
);
None
}
}
}
const ORACLE_MIN_RUNWAY_MS: u128 = 6000;
#[derive(Clone, Copy)]
enum ShimField {
Integer,
Rational,
}
enum Sampling<'a, S: Space> {
AllVarsListed,
AllVarsCleaned,
Projection(&'a ShowSet<S>),
}
impl<S: Space> Sampling<'_, S> {
fn all_indep(&self) -> bool {
!matches!(self, Sampling::Projection(_))
}
fn apply(&self, a: &mut ArjunLib, num_vars: u32) {
match self {
Sampling::AllVarsListed => {
let all: Vec<u32> = (0..num_vars).collect();
a.set_sampl(&all);
}
Sampling::AllVarsCleaned => a.clean_sampl(),
Sampling::Projection(show) => a.set_sampl(show.as_zero_based()),
}
}
}
#[derive(Clone, Copy)]
enum Oracle {
Off,
Gated {
max_vars: u32,
scale_mult: bool,
},
}
#[derive(Clone, Copy)]
enum PastDeadline {
Keep,
Classify {
keep_overrun: bool,
},
}
struct StageSpec<'a, S: Space> {
label: &'static str,
report_giveups: bool,
field: ShimField,
seed: u32,
sampling: Sampling<'a, S>,
weights: &'a [(i32, num_rational::BigRational)],
backbone_max_confl: Option<i64>,
oracle: Oracle,
no_sbva: bool,
no_bve: bool,
deadline: Instant,
past_deadline: PastDeadline,
}
impl<S: Space> StageSpec<'_, S> {
fn giveup(&self, started: Instant, why: &str) {
if self.report_giveups {
giveup(
self.label,
format_args!("{why}"),
Spent::Elapsed(started.elapsed()),
);
}
}
fn note_heavy_stage_failed(&self) {
if self.report_giveups {
diag!(
"[{}] heavy stage failed; keeping the stage-1 reduction",
self.label
);
}
}
fn giveup_vs_budget(&self, started: Instant, why: &str) {
if self.report_giveups {
giveup(
self.label,
format_args!("{why}"),
Spent::ElapsedOfBudget(
started.elapsed(),
self.deadline.saturating_duration_since(started),
),
);
}
}
}
struct StagedArjun<T> {
shim: ArjunLib,
started: Instant,
harvest: T,
}
fn run_stages<T, S: Space>(
formula: &CnfFormula,
spec: &StageSpec<'_, S>,
after_minimize: impl FnOnce(&ArjunLib) -> T,
) -> Option<StagedArjun<T>> {
if matches!(&spec.sampling, Sampling::Projection(show) if show.is_empty()) {
return None;
}
let started = Instant::now();
let shim = match spec.field {
ShimField::Integer => ArjunLib::new(spec.seed),
ShimField::Rational => ArjunLib::new_weighted(spec.seed),
};
let mut a = match shim {
Some(a) => a,
None => {
spec.giveup(started, "shim ctor failed (null)");
return None;
}
};
a.set_deadline(spec.deadline);
if let Some(max_confl) = spec.backbone_max_confl {
a.set_backbone_max_confl(max_confl);
}
a.new_vars(formula.num_vars);
let mut scratch: Vec<i32> = Vec::new();
for cl in &formula.clauses {
scratch.clear();
for l in &cl.literals {
scratch.push(l.to_dimacs());
}
a.add_clause_dimacs(&scratch);
}
if !spec.weights.is_empty() {
let projection = match &spec.sampling {
Sampling::Projection(show) => Some(*show),
Sampling::AllVarsListed | Sampling::AllVarsCleaned => None,
};
for (lit, w) in spec.weights {
let lit = Literal::from(*lit);
if projection.is_some_and(|show| !show.contains(lit.var)) {
continue;
}
if let Err(e) = a.set_lit_weight(lit, &format!("{}/{}", w.numer(), w.denom())) {
spec.giveup(started, &format!("{e}"));
return None;
}
}
}
spec.sampling.apply(&mut a, formula.num_vars);
let all_indep = spec.sampling.all_indep();
if crate::budget::remaining(spec.deadline).is_zero() {
spec.giveup_vs_budget(started, "deadline passed before stage-1");
return None;
}
if !a.stage_minimize_indep(all_indep) {
spec.giveup(started, "stage-1 minimize failed");
return None;
}
let harvest = after_minimize(&a);
let left = crate::budget::remaining(spec.deadline);
if !left.is_zero() {
let remaining_ms = left.as_millis();
let oracle = match spec.oracle {
Oracle::Off => false,
Oracle::Gated {
max_vars,
scale_mult,
} => {
let on = formula.num_vars <= max_vars && remaining_ms >= ORACLE_MIN_RUNWAY_MS;
if on && scale_mult {
a.set_oracle_mult(oracle_mult_for_budget(remaining_ms));
}
on
}
};
if !a.stage_simplify(all_indep, oracle, spec.no_sbva, spec.no_bve) {
spec.note_heavy_stage_failed();
}
}
if let PastDeadline::Classify { keep_overrun } = spec.past_deadline
&& !keep_after_deadline(
spec.label,
Instant::now(),
started,
spec.deadline,
a.deadline_armed(),
keep_overrun,
)
{
return None;
}
Some(StagedArjun {
shim: a,
started,
harvest,
})
}
pub(super) fn reduce_anytime(
formula: &CnfFormula,
deadline: Instant,
arjun: ArjunOptions,
force_no_sbva: bool,
) -> Result<Option<ArjunResult>, VitriError> {
validate_shim_env()?;
if arjun.keep_overrun {
return Ok(reduce_anytime_inner(
formula,
deadline,
arjun,
force_no_sbva,
));
}
let started = Instant::now();
let outcome = run_forked_with_deadline(deadline, || {
reduce_anytime_inner(formula, deadline, arjun, force_no_sbva)
});
Ok(finish_forked("arjun-anytime", outcome, started, deadline))
}
pub(super) fn reduce_anytime_inner(
formula: &CnfFormula,
deadline: Instant,
arjun: ArjunOptions,
no_sbva_call: bool,
) -> Option<ArjunResult> {
let (oracle, no_sbva, no_bve, backbone_max_confl) = match arjun.effort {
ArjunEffort::Full => (
Oracle::Gated {
max_vars: arjun.oracle_max_vars.plain.unwrap_or(u32::MAX),
scale_mult: true,
},
no_sbva_call,
false,
None,
),
ArjunEffort::Lite => (
Oracle::Off,
true,
true,
Some(LITE_BACKBONE_MAX_CONFL_DEFAULT),
),
};
let spec = StageSpec::<Reduced> {
label: "arjun-anytime",
report_giveups: true,
field: ShimField::Integer,
seed: arjun.seed,
sampling: Sampling::AllVarsListed,
weights: &[],
backbone_max_confl,
oracle,
no_sbva,
no_bve,
deadline,
past_deadline: PastDeadline::Classify {
keep_overrun: arjun.keep_overrun,
},
};
let StagedArjun {
shim: a,
started,
harvest: (backbone, equiv),
} = run_stages(formula, &spec, |a| (a.backbone(), a.eq_lits()))?;
let multiplier_exp = match multiplier_decimal_to_exp(&multiplier_or_giveup(&a)?) {
Some(e) => e,
None => {
giveup(
"arjun-anytime",
format_args!("multiplier not a power of two"),
Spent::Elapsed(started.elapsed()),
);
return None;
}
};
let full_formula = a.cur_formula();
let independent_support = ShowSet::from_zero_based(a.cur_sampl());
let learnt_clauses: Vec<Vec<i32>> = if arjun.export_learned_clauses {
let nv = full_formula.num_vars;
a.red_clauses()
.into_iter()
.filter(|cl| {
!cl.is_empty() && cl.iter().all(|&l| l.unsigned_abs().saturating_sub(1) < nv)
})
.collect()
} else {
Vec::new()
};
let input_to_reduced_lit = a.orig_to_new_lits(formula.num_vars);
Some(ArjunResult {
formula: full_formula,
multiplier_exp,
backbone,
equiv,
learnt_clauses,
independent_support,
input_to_reduced_lit,
})
}
pub(super) fn reduce_anytime_projected<S: Space>(
formula: &CnfFormula,
show: &ShowSet<S>,
deadline: Instant,
arjun: ArjunOptions,
force_no_sbva: bool,
) -> Result<Option<ArjunProjResult>, VitriError> {
validate_shim_env()?;
Ok(reduce_anytime_projected_inner(
formula,
show,
deadline,
arjun,
force_no_sbva,
))
}
fn reduce_anytime_projected_inner<S: Space>(
formula: &CnfFormula,
show: &ShowSet<S>,
deadline: Instant,
arjun: ArjunOptions,
no_sbva_call: bool,
) -> Option<ArjunProjResult> {
let spec = StageSpec {
label: "arjun-anytime-pmc",
report_giveups: false,
field: ShimField::Integer,
seed: arjun.seed,
sampling: Sampling::Projection(show),
weights: &[],
backbone_max_confl: None,
oracle: Oracle::Gated {
max_vars: arjun.oracle_max_vars.projected.unwrap_or(u32::MAX),
scale_mult: true,
},
no_sbva: no_sbva_call,
no_bve: false,
deadline,
past_deadline: PastDeadline::Keep,
};
let StagedArjun { shim: a, .. } = run_stages(formula, &spec, |_| ())?;
let multiplier_exp = multiplier_decimal_to_exp(&multiplier_or_giveup(&a)?)?;
let reduced = a.cur_formula();
let reduced_show = ShowSet::from_zero_based(a.cur_sampl());
let input_to_reduced_lit = a.orig_to_new_lits(formula.num_vars);
Some(ArjunProjResult {
formula: reduced,
show: reduced_show,
multiplier_exp,
input_to_reduced_lit,
})
}
pub(super) fn reduce_anytime_weighted(
formula: &CnfFormula,
weights: &[(i32, num_rational::BigRational)],
deadline: Instant,
arjun: ArjunOptions,
no_sbva: bool,
) -> Result<Option<super::arjun::ArjunWeightedResult>, VitriError> {
validate_shim_env()?;
let started = Instant::now();
let outcome = run_forked_with_deadline(deadline, || {
reduce_anytime_weighted_inner(formula, weights, deadline, arjun, no_sbva)
});
Ok(finish_forked(
"arjun-anytime-wmc",
outcome,
started,
deadline,
))
}
fn reduce_anytime_weighted_inner(
formula: &CnfFormula,
weights: &[(i32, num_rational::BigRational)],
deadline: Instant,
arjun: ArjunOptions,
no_sbva: bool,
) -> Option<super::arjun::ArjunWeightedResult> {
use num_rational::BigRational;
let spec = StageSpec::<Reduced> {
label: "arjun-anytime-wmc",
report_giveups: false,
field: ShimField::Rational,
seed: arjun.seed,
sampling: Sampling::AllVarsCleaned,
weights,
backbone_max_confl: None,
oracle: Oracle::Gated {
max_vars: arjun.oracle_max_vars.plain.unwrap_or(u32::MAX),
scale_mult: false,
},
no_sbva,
no_bve: false,
deadline,
past_deadline: PastDeadline::Classify {
keep_overrun: false,
},
};
let StagedArjun { shim: a, .. } = run_stages(formula, &spec, |_| ())?;
let multiplier: BigRational =
crate::cnf::parse_weight(multiplier_or_giveup(&a)?.trim()).ok()?;
let full_formula = a.cur_formula();
let reduced_weights =
Weights::try_from_dimacs_lits(full_formula.num_vars, |l| lit_weight_or_giveup(&a, l))?;
let input_to_reduced_lit = a.orig_to_new_lits(formula.num_vars);
Some(super::arjun::ArjunWeightedResult {
formula: full_formula,
weights: reduced_weights,
multiplier,
input_to_reduced_lit,
})
}
pub(super) fn reduce_anytime_weighted_projected<S: Space>(
formula: &CnfFormula,
show: &ShowSet<S>,
weights: &[(i32, num_rational::BigRational)],
deadline: Instant,
arjun: ArjunOptions,
force_no_sbva: bool,
) -> Result<Option<super::arjun::ArjunWeightedProjResult>, VitriError> {
validate_shim_env()?;
Ok(reduce_anytime_weighted_projected_inner(
formula,
show,
weights,
deadline,
arjun,
force_no_sbva,
))
}
fn reduce_anytime_weighted_projected_inner<S: Space>(
formula: &CnfFormula,
show: &ShowSet<S>,
weights: &[(i32, num_rational::BigRational)],
deadline: Instant,
arjun: ArjunOptions,
no_sbva_call: bool,
) -> Option<super::arjun::ArjunWeightedProjResult> {
use num_rational::BigRational;
let spec = StageSpec {
label: "arjun-anytime-pwmc",
report_giveups: false,
field: ShimField::Rational,
seed: arjun.seed,
sampling: Sampling::Projection(show),
weights,
backbone_max_confl: None,
oracle: Oracle::Gated {
max_vars: arjun.oracle_max_vars.weighted_projected.unwrap_or(u32::MAX),
scale_mult: true,
},
no_sbva: no_sbva_call,
no_bve: false,
deadline,
past_deadline: PastDeadline::Keep,
};
let StagedArjun { shim: a, .. } = run_stages(formula, &spec, |_| ())?;
let multiplier: BigRational =
crate::cnf::parse_weight(multiplier_or_giveup(&a)?.trim()).ok()?;
let reduced = a.cur_formula();
let mut reduced_show = ShowSet::<Reduced>::from_zero_based(a.cur_sampl());
let reduced_weights =
Weights::try_from_dimacs_lits(reduced.num_vars, |l| lit_weight_or_giveup(&a, l))?;
for var in reduced_weights.weighted_vars() {
reduced_show.insert(var);
}
let input_to_reduced_lit = a.orig_to_new_lits(formula.num_vars);
Some(super::arjun::ArjunWeightedProjResult {
formula: reduced,
show: reduced_show,
weights: reduced_weights,
multiplier,
input_to_reduced_lit,
})
}
#[cfg(test)]
mod tests;