use std::path::{Path, PathBuf};
use super::{
CANDIDATES_DIR, COMPONENTS_DIR, COMPONENTS_FORMAT_TAG, COMPONENTS_JSON_NAME, CandidateEntry,
ComponentEntry, ComponentPaths, ComponentWriteOptions, ComponentsManifest, SelectionEntry,
TreeDecompositionSummary,
};
use crate::bundle::{
DotFor, REDUCED_CNF_NAME, VTREE_NAME, ensure_dir, to_json_pretty, write_file, write_vtree_files,
};
use crate::candidates::CandidateSet;
use crate::cnf::{CnfFormula, DimacsHeader, Reduced, ShowSet, write_dimacs};
use crate::component::{LocalView, VtreeBuild, local_view};
use crate::error::VitriError;
use crate::vtree::VarId;
pub fn write_components(
dir: &Path,
reduced: &CnfFormula,
build: &VtreeBuild,
show_reduced: Option<&ShowSet<Reduced>>,
options: ComponentWriteOptions,
) -> Result<(ComponentsManifest, ComponentPaths), VitriError> {
let rank_metric = build
.candidate_sets
.iter()
.find(|b| !b.is_empty())
.map(|b| b.metric);
let mut cand_files: Vec<PathBuf> = Vec::new();
let reduced_mask = show_reduced.map(|s| s.mask(reduced.num_vars));
let Some(comps) = build.components.as_deref() else {
let cnf = REDUCED_CNF_NAME.to_string();
let vtree_file = VTREE_NAME.to_string();
let set = build.candidate_sets.first();
let dot = DotFor::when(options.dot, reduced, reduced_mask.as_ref());
let local_to_reduced: Vec<VarId> = (0..reduced.num_vars).map(VarId).collect();
let entry = ComponentEntry {
local_to_reduced_dimacs: local_to_reduced_dimacs(&local_to_reduced),
show_vars_local_dimacs: reduced_mask.as_ref().map(|m| m.restrict(&local_to_reduced)),
selection: selection_entry(build.selections.first()),
vtree_candidates: match set {
Some(b) => write_candidate_set(dir, 0, b, &vtree_file, &mut cand_files, dot)?,
None => Vec::new(),
},
cnf,
vtree: vtree_file,
};
let manifest = ComponentsManifest {
format: COMPONENTS_FORMAT_TAG.to_string(),
free_vars_reduced_dimacs: Vec::new(),
candidate_rank_metric: rank_metric,
components: vec![entry],
};
let path = write_manifest(dir, &manifest)?;
return Ok((
manifest,
ComponentPaths {
manifest: path,
files: Vec::new(),
candidates: cand_files,
},
));
};
let comp_dir = dir.join(COMPONENTS_DIR);
ensure_dir(&comp_dir)?;
let mut claimed = vec![false; reduced.num_vars as usize];
let mut entries = Vec::with_capacity(comps.len());
let mut files = Vec::with_capacity(comps.len() * 3);
for (index, cv) in comps.iter().enumerate() {
let LocalView {
formula: sub,
show: show_local,
local_to_outer: local_to_reduced,
} = local_view(reduced, &cv.clause_indices, reduced_mask.as_ref());
if cv.vtree.num_leaves() != sub.num_vars {
return Err(VitriError::mismatch(format!(
"component {index} vtree has {} leaves but its CNF has {} variables; \
the build does not belong to this formula",
cv.vtree.num_leaves(),
sub.num_vars,
)));
}
for v in &local_to_reduced {
claimed[v.idx()] = true;
}
let show_mask = show_local.as_ref().map(|s| s.mask(sub.num_vars));
let dot = DotFor::when(options.dot, &sub, show_mask.as_ref());
let stem = format!("comp{index:03}");
let cnf_rel = format!("{COMPONENTS_DIR}/{stem}.cnf");
let vtree_rel = format!("{COMPONENTS_DIR}/{stem}.vtree");
let cnf_path = dir.join(&cnf_rel);
write_dimacs(
&sub,
&DimacsHeader {
show: show_local.as_ref(),
..Default::default()
},
&cnf_path,
)?;
let (vtree_path, dot_path) = write_vtree_files(dir.join(&vtree_rel), &cv.vtree, dot)?;
files.extend([cnf_path, vtree_path]);
files.extend(dot_path);
let vtree_candidates = match build.candidate_sets.get(index) {
Some(b) => write_candidate_set(dir, index, b, &vtree_rel, &mut cand_files, dot)?,
None => Vec::new(),
};
entries.push(ComponentEntry {
local_to_reduced_dimacs: local_to_reduced_dimacs(&local_to_reduced),
show_vars_local_dimacs: show_local,
selection: selection_entry(build.selections.get(index)),
vtree_candidates,
cnf: cnf_rel,
vtree: vtree_rel,
});
}
let manifest = ComponentsManifest {
format: COMPONENTS_FORMAT_TAG.to_string(),
free_vars_reduced_dimacs: (0..reduced.num_vars)
.filter(|&v| !claimed[v as usize])
.map(|v| VarId(v).to_dimacs() as u32)
.collect(),
candidate_rank_metric: rank_metric,
components: entries,
};
let manifest_path = write_manifest(dir, &manifest)?;
Ok((
manifest,
ComponentPaths {
manifest: manifest_path,
files,
candidates: cand_files,
},
))
}
fn local_to_reduced_dimacs(local_to_reduced: &[VarId]) -> Vec<u32> {
local_to_reduced
.iter()
.map(|v| v.to_dimacs() as u32)
.collect()
}
fn selection_entry(record: Option<&crate::spec::SelectionRecord>) -> Option<SelectionEntry> {
let record = record?;
Some(SelectionEntry {
winning_spec: record.winning_spec.clone()?,
tree_decomposition: record.td_meta.as_deref().map(|m| TreeDecompositionSummary {
num_bags: m.num_bags(),
treewidth: m.treewidth(),
}),
})
}
fn write_candidate_set(
dir: &Path,
index: usize,
set: &CandidateSet,
selected_vtree: &str,
files: &mut Vec<PathBuf>,
dot: Option<DotFor<'_>>,
) -> Result<Vec<CandidateEntry>, VitriError> {
if set.is_empty() {
return Ok(Vec::new());
}
let mut out = Vec::with_capacity(set.candidates.len());
let mut made_dir = false;
for (rank, cand) in set.candidates.iter().enumerate() {
let vtree_rel = if rank == 0 {
selected_vtree.to_string()
} else {
if !made_dir {
ensure_dir(&dir.join(CANDIDATES_DIR))?;
made_dir = true;
}
let stem = format!("comp{index:03}.rank{rank:02}");
let vtree_rel = format!("{CANDIDATES_DIR}/{stem}.vtree");
let (vtree_path, dot_path) = write_vtree_files(dir.join(&vtree_rel), &cand.vtree, dot)?;
files.push(vtree_path);
files.extend(dot_path);
vtree_rel
};
debug_assert!(
(rank == 0) == cand.selected,
"the first entry of an emitted candidate set must be the selected vtree, and only it",
);
out.push(CandidateEntry {
built_by: cand.built_by.clone(),
vtree: vtree_rel,
scores: cand.scores,
});
}
Ok(out)
}
fn write_manifest(dir: &Path, manifest: &ComponentsManifest) -> Result<PathBuf, VitriError> {
ensure_dir(dir)?;
let path = dir.join(COMPONENTS_JSON_NAME);
write_file(&path, to_json_pretty(manifest))?;
Ok(path)
}