use std::time::{Duration, Instant};
use crate::error::VitriError;
use crate::preprocess::ArjunOptions;
use crate::spec::DEFAULT_VTREE_SPEC;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComponentPolicy {
Split,
Whole,
}
impl ComponentPolicy {
const ALL: &'static [ComponentPolicy] = &[ComponentPolicy::Split, ComponentPolicy::Whole];
pub fn token(self) -> &'static str {
match self {
ComponentPolicy::Split => "split",
ComponentPolicy::Whole => "whole",
}
}
pub fn parse(token: &str) -> Option<Self> {
ComponentPolicy::ALL
.iter()
.copied()
.find(|p| p.token() == token)
}
pub fn names() -> impl Iterator<Item = &'static str> {
ComponentPolicy::ALL.iter().map(|p| p.token())
}
pub fn is_whole(self) -> bool {
matches!(self, ComponentPolicy::Whole)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PreprocessStages {
pub simplify: bool,
pub arjun: bool,
}
impl Default for PreprocessStages {
fn default() -> Self {
PreprocessStages {
simplify: true,
arjun: true,
}
}
}
impl PreprocessStages {
#[must_use]
pub fn read_under(mode: crate::cnf::Mode) -> Self {
Chain::for_mode(mode).stages_read()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DvePolicy {
pub rounds: usize,
pub budget_ms: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PreprocessClock {
#[default]
WallClock,
Deterministic {
configured_wall_ms: Option<u64>,
},
}
impl Default for DvePolicy {
fn default() -> Self {
DvePolicy {
rounds: 30,
budget_ms: 3_000,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SimplifyPolicy {
pub backbone_budget_ms: Option<u64>,
pub equivalence_budget_ms: Option<u64>,
pub detect_gates: bool,
pub dve: Option<DvePolicy>,
}
impl Default for SimplifyPolicy {
fn default() -> Self {
SimplifyPolicy {
backbone_budget_ms: Some(300_000),
equivalence_budget_ms: Some(300),
detect_gates: true,
dve: Some(DvePolicy::default()),
}
}
}
impl SimplifyPolicy {
fn customizes_count_only(self) -> bool {
let default = Self::default();
self.detect_gates != default.detect_gates || self.dve != default.dve
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ArjunBudget {
#[default]
Derived,
Exact(Duration),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum ArjunClauseGrowth {
#[default]
Reject,
KeepSound,
RejectAgainst(usize),
}
impl ArjunClauseGrowth {
fn requires_count_arjun(self) -> bool {
!matches!(self, Self::Reject)
}
pub(crate) fn clause_count_baseline(self, input_clauses: usize) -> usize {
match self {
Self::Reject | Self::KeepSound => input_clauses,
Self::RejectAgainst(baseline) => baseline,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ProjectionPolicy {
#[default]
Full,
ArjunOnly(ProjectionNoGain),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ProjectionNoGain {
#[default]
Reject,
KeepSound,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Chain {
Count,
Projection,
Compile,
}
impl Chain {
pub(crate) fn for_mode(mode: crate::cnf::Mode) -> Self {
use crate::cnf::Mode;
match mode {
Mode::Mc | Mode::Wmc => Chain::Count,
Mode::Pmc | Mode::Pwmc => Chain::Projection,
Mode::Compile => Chain::Compile,
}
}
pub(crate) fn stages_read(self) -> PreprocessStages {
match self {
Chain::Count => PreprocessStages {
simplify: true,
arjun: true,
},
Chain::Projection => PreprocessStages {
simplify: false,
arjun: true,
},
Chain::Compile => PreprocessStages {
simplify: true,
arjun: false,
},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum ConstructionBudget {
#[default]
Share,
WholeRemaining,
Until(Instant),
Deterministic {
units: u64,
},
}
impl ConstructionBudget {
pub const UNITS_PER_MS: u64 = crate::decompose::meter::UNITS_PER_MS;
pub fn units_for_wall_ms(ms: u64) -> u64 {
ms.saturating_mul(Self::UNITS_PER_MS)
}
pub fn for_wall_ms(ms: u64) -> Self {
ConstructionBudget::Deterministic {
units: Self::units_for_wall_ms(ms),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RunConfig {
pub budget_ms: Option<u64>,
pub deadline: Option<Instant>,
pub preprocess_clock: PreprocessClock,
pub arjun_budget: ArjunBudget,
pub arjun_clause_growth: ArjunClauseGrowth,
pub projection_policy: ProjectionPolicy,
pub construction_budget: ConstructionBudget,
pub vtree_spec: String,
pub reading: crate::decompose::Reading,
pub stages: PreprocessStages,
pub simplify: SimplifyPolicy,
pub components: ComponentPolicy,
pub candidates: usize,
pub mode: Option<crate::cnf::Mode>,
pub retain_arjun_input: bool,
pub arjun: ArjunOptions,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedMode {
pub mode: crate::cnf::Mode,
pub notices: Vec<String>,
}
impl Default for RunConfig {
fn default() -> Self {
RunConfig {
budget_ms: None,
deadline: None,
preprocess_clock: PreprocessClock::default(),
arjun_budget: ArjunBudget::default(),
arjun_clause_growth: ArjunClauseGrowth::default(),
projection_policy: ProjectionPolicy::default(),
construction_budget: ConstructionBudget::default(),
vtree_spec: DEFAULT_VTREE_SPEC.to_string(),
reading: crate::decompose::Reading::default(),
stages: PreprocessStages::default(),
simplify: SimplifyPolicy::default(),
components: ComponentPolicy::Split,
candidates: 1,
mode: None,
retain_arjun_input: false,
arjun: ArjunOptions::default(),
}
}
}
impl RunConfig {
pub fn from_env_defaults() -> Result<Self, VitriError> {
Ok(RunConfig {
budget_ms: budget_hint_ms(crate::env::env_opt("VITRI_BUDGET_MS").as_deref()),
arjun: crate::preprocess::env_defaults()?,
..Self::default()
})
}
pub fn validate(&self) -> Result<(), VitriError> {
crate::spec::validate_vtree_spec(&self.vtree_spec)?;
if let Some(dve) = self.simplify.dve
&& (dve.rounds == 0 || dve.budget_ms == 0)
{
return Err(VitriError::config(format!(
"simplify.dve is armed with rounds={} and budget_ms={}: both must be positive; \
use simplify.dve=None to disable DVE",
dve.rounds, dve.budget_ms,
)));
}
if self.simplify.backbone_budget_ms.is_none()
&& self.simplify.equivalence_budget_ms.is_some()
{
return Err(VitriError::config(
"simplify.equivalence_budget_ms is inert when \
simplify.backbone_budget_ms=None because SAT equivalence probing belongs to \
the backbone prefix: set simplify.equivalence_budget_ms=None or provide a \
backbone budget",
));
}
if !self.stages.simplify && self.simplify != SimplifyPolicy::default() {
return Err(VitriError::config(
"a non-default simplify policy is inert because the simplify stage is off: \
enable the stage, or use SimplifyPolicy::default()",
));
}
if self.arjun_clause_growth.requires_count_arjun() && !self.stages.arjun {
let mode = self
.mode
.map(|mode| format!(" under explicit mode {}", mode.token()))
.unwrap_or_default();
return Err(VitriError::config(format!(
"arjun_clause_growth {:?} is inert{mode} because the Arjun stage is off: \
no clause-growth decision can be made. Enable the Arjun stage in mc/wmc, \
or use ArjunClauseGrowth::Reject",
self.arjun_clause_growth,
)));
}
if let ProjectionPolicy::ArjunOnly(no_gain) = self.projection_policy
&& !self.stages.arjun
{
return Err(VitriError::config(format!(
"projection_policy ArjunOnly({no_gain:?}) is inert because the Arjun stage is \
off: an Arjun-only projection request has no stage to run. Let the Arjun \
stage run, or use ProjectionPolicy::Full",
)));
}
if let ArjunBudget::Exact(duration) = self.arjun_budget
&& !self.stages.arjun
{
return Err(VitriError::config(format!(
"arjun_budget Exact({duration:?}) is inert because the Arjun stage is off: \
an exact Arjun budget has no stage to spend it. Let the Arjun stage run, \
or use ArjunBudget::Derived",
)));
}
if let Some(mode) = self.mode {
self.refuse_inert(mode)?;
}
if self.construction_budget == (ConstructionBudget::Deterministic { units: 0 }) {
return Err(VitriError::config(
"a deterministic construction budget of 0 work units asks construction to do \
no work at all — pass the work a construction should be allowed to do, which \
ConstructionBudget::for_wall_ms converts from a wall in milliseconds",
));
}
if self.candidates == 0 {
return Err(VitriError::config(
"candidates must be at least 1 (the selected vtree is always kept)",
));
}
if self.candidates > crate::candidates::MAX_CANDIDATES {
return Err(VitriError::config(format!(
"candidates is {} but the ceiling is {} — every retained candidate holds a \
live vtree over the formula being built, so the retained set is a peak-memory \
decision and is refused rather than silently truncated",
self.candidates,
crate::candidates::MAX_CANDIDATES,
)));
}
if crate::candidates::retains_set(self.candidates)
&& !crate::spec::spec_has_candidates(&self.vtree_spec)
{
return Err(VitriError::config(format!(
"candidates is {} but vtree spec {:?} builds a single vtree — only the \
portfolio spec ({}) scores several candidates and therefore has a candidate set to \
retain",
self.candidates, self.vtree_spec, DEFAULT_VTREE_SPEC,
)));
}
Ok(())
}
pub(crate) fn refuse_inert(&self, mode: crate::cnf::Mode) -> Result<(), VitriError> {
let read = PreprocessStages::read_under(mode);
let chain = Chain::for_mode(mode);
if self.simplify != SimplifyPolicy::default() && !read.simplify {
let how = if self.mode.is_some() {
String::new()
} else {
" (detected from the instance's own headers — no --mode was given)".to_string()
};
return Err(VitriError::config(format!(
"a non-default simplify policy does nothing under mode {}{how}: that mode uses \
the projection-preserving chain, which has no simplify stage. Use \
SimplifyPolicy::default(), or run mc/wmc/compile",
mode.token(),
)));
}
if chain == Chain::Compile && self.simplify.customizes_count_only() {
return Err(VitriError::config(format!(
"simplify.detect_gates and simplify.dve are count-only; changing either does \
nothing under mode {} because compile caps both stages off. Leave both at \
SimplifyPolicy::default(), or run mc/wmc",
mode.token(),
)));
}
if let ProjectionPolicy::ArjunOnly(no_gain) = self.projection_policy
&& Chain::for_mode(mode) != Chain::Projection
{
let how = if self.mode.is_some() {
String::new()
} else {
" (detected from the instance's own headers — no --mode was given)".to_string()
};
return Err(VitriError::config(format!(
"projection_policy ArjunOnly({no_gain:?}) does nothing under mode {}{how}: \
the policy requires projected Arjun. Use ProjectionPolicy::Full, or run \
pmc/pwmc",
mode.token(),
)));
}
if self.arjun_clause_growth.requires_count_arjun() && !read.arjun {
let how = if self.mode.is_some() {
String::new()
} else {
" (detected from the instance's own headers — no --mode was given)".to_string()
};
return Err(VitriError::config(format!(
"arjun_clause_growth {:?} does nothing under mode {}{how}: that mode's \
preprocessing has no Arjun stage whose clause-growth decision it could change. \
Use ArjunClauseGrowth::Reject, or enable Arjun in mc/wmc",
self.arjun_clause_growth,
mode.token(),
)));
}
if self.arjun_clause_growth.requires_count_arjun() && Chain::for_mode(mode) != Chain::Count
{
let how = if self.mode.is_some() {
String::new()
} else {
" (detected from the instance's own headers — no --mode was given)".to_string()
};
return Err(VitriError::config(format!(
"arjun_clause_growth {:?} does nothing under mode {}{how}: that mode's \
preprocessing has no NotSmaller clause-growth gate. Use \
ArjunClauseGrowth::Reject, or run mc/wmc with Arjun enabled",
self.arjun_clause_growth,
mode.token(),
)));
}
if let ArjunBudget::Exact(duration) = self.arjun_budget
&& !read.arjun
{
let how = if self.mode.is_some() {
String::new()
} else {
" (detected from the instance's own headers — no --mode was given)".to_string()
};
return Err(VitriError::config(format!(
"arjun_budget Exact({duration:?}) does nothing under mode {}{how}: that \
mode's preprocessing has no Arjun stage to spend an exact Arjun budget. \
Use ArjunBudget::Derived, or run a mode whose preprocessing has an \
Arjun stage",
mode.token(),
)));
}
for (off, reads, flag, stage) in [
(
!self.stages.simplify,
read.simplify,
"--no-simplify",
"simplify",
),
(!self.stages.arjun, read.arjun, "--no-arjun", "Arjun"),
] {
if off && !reads {
let how = if self.mode.is_some() {
String::new()
} else {
" (detected from the instance's own headers — no --mode was given)".to_string()
};
return Err(VitriError::config(format!(
"{flag} does nothing under mode {}{how}: that mode's preprocessing has no \
{stage} stage to skip. Drop the flag, or run a mode whose preprocessing has one",
mode.token(),
)));
}
}
if self.arjun.export_learned_clauses {
if mode != crate::cnf::Mode::Mc {
return Err(VitriError::config(format!(
"arjun.export_learned_clauses (VITRI_ARJUN_EXPORT_LEARNED_CLAUSES) does nothing under \
mode {}: the clauses come from the Arjun stage of the count-preserving chain, \
which only mode {} runs. Drop the request, or preprocess under {}",
mode.token(),
crate::cnf::Mode::Mc.token(),
crate::cnf::Mode::Mc.token(),
)));
}
if !self.stages.arjun {
return Err(VitriError::config(
"arjun.export_learned_clauses (VITRI_ARJUN_EXPORT_LEARNED_CLAUSES) does nothing with the \
Arjun stage off (--no-arjun): Arjun's own solver is what derives the clauses, and \
no other stage does. Drop the request, or let the Arjun stage run",
));
}
}
Ok(())
}
pub fn resolve_mode(&self, meta: &crate::cnf::CnfMeta) -> Result<ResolvedMode, VitriError> {
use crate::cnf::Mode;
let declares_weights = meta.mode().is_weighted() || meta.declared_weights().is_some();
let declares_show = meta.mode().is_projected() || meta.declared_show_vars().is_some();
let detected = match (declares_show, declares_weights) {
(false, false) => Mode::Mc,
(false, true) => Mode::Wmc,
(true, false) => Mode::Pmc,
(true, true) => Mode::Pwmc,
};
let Some(asked) = self.mode else {
require_show_set(detected, meta)?;
return Ok(ResolvedMode {
mode: detected,
notices: Vec::new(),
});
};
require_show_set(asked, meta)?;
let mut notices = Vec::new();
if asked != Mode::Compile {
if declares_weights && !asked.is_weighted() {
notices.push(format!(
"c note: ignoring weight declarations (mode {})",
asked.token(),
));
}
if declares_show && !asked.is_projected() {
notices.push(format!(
"c note: ignoring the projection show set (mode {})",
asked.token(),
));
}
}
Ok(ResolvedMode {
mode: asked,
notices,
})
}
pub fn resolved_deadline(&self, now: Instant) -> Option<Instant> {
self.deadline
.or_else(|| self.budget_ms.map(|ms| now + Duration::from_millis(ms)))
}
pub fn effective_budget_ms(&self, now: Instant) -> Option<u64> {
self.budget_ms.or_else(|| {
self.deadline
.map(|d| d.saturating_duration_since(now).as_millis() as u64)
})
}
pub fn construction_deadline(&self, now: Instant) -> Option<Instant> {
match self.construction_budget {
ConstructionBudget::Deterministic { units } => {
crate::budget::deterministic_deadline(units, now)
}
ConstructionBudget::Share => Some(crate::budget::vtree_share_deadline(
self.resolved_deadline(now)?,
now,
)),
ConstructionBudget::WholeRemaining => self.resolved_deadline(now),
ConstructionBudget::Until(t) => Some(t.min(self.resolved_deadline(now)?)),
}
}
pub(crate) fn anchored(&self, now: Instant) -> RunConfig {
RunConfig {
deadline: self.resolved_deadline(now),
budget_ms: self.effective_budget_ms(now),
..self.clone()
}
}
}
fn budget_hint_ms(raw: Option<&str>) -> Option<u64> {
raw.and_then(|t| t.parse::<u64>().ok())
}
fn require_show_set(mode: crate::cnf::Mode, meta: &crate::cnf::CnfMeta) -> Result<(), VitriError> {
if mode.is_projected() && meta.declared_show_vars().is_none() {
return Err(VitriError::config(format!(
"mode {} is projected, but the instance carries no `c p show` line — there is no \
show set to preserve, so the mode is inert. Use {} for this file, or add \
the show set",
mode.token(),
if mode.is_weighted() { "wmc" } else { "mc" },
)));
}
Ok(())
}
#[cfg(test)]
mod tests;