scuttle_core/
prepro.rs

1//! # Instance Processing Happening _Before_ It's Being Passed To The Actual Solver
2
3use std::{
4    ffi::OsString,
5    fmt, fs, io,
6    path::{Path, PathBuf},
7};
8
9use rustsat::{
10    encodings::{cert::CollectClauses as CollectCertClauses, pb, CollectClauses},
11    instances::{fio, ManageVars, MultiOptInstance, Objective as RsObjective, ReindexVars},
12    types::{constraints::PbConstraint, Clause, Lit, RsHashMap},
13};
14
15use crate::types::{Instance, Objective, Parsed, Reindexer, VarManager};
16
17#[derive(Copy, Clone, PartialEq, Eq)]
18#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
19pub enum FileFormat {
20    /// Infer the file format from the file extension. `.mcnf`, `.bicnf`,
21    /// `.cnf`, `.wcnf` or `.dimacs` are all interpreted as DIMACS files and
22    /// `.opb` as an OPB file. All file extensions can also be prepended with
23    /// `.bz2` or `.gz` if compression is used.
24    Infer,
25    /// A DIMACS MCNF file
26    Dimacs,
27    /// A multi-objective OPB file
28    Opb,
29}
30
31impl fmt::Display for FileFormat {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        match self {
34            FileFormat::Infer => write!(f, "infer"),
35            FileFormat::Dimacs => write!(f, "dimacs"),
36            FileFormat::Opb => write!(f, "opb"),
37        }
38    }
39}
40
41macro_rules! is_one_of {
42    ($a:expr, $($b:expr),*) => {
43        $( $a == $b || )* false
44    }
45}
46
47#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
48pub enum Error {
49    #[error("Cannot infer file format from extension {0:?}")]
50    UnknownFileExtension(OsString),
51    #[error("To infer the file format, the file needs to have a file extension")]
52    NoFileExtension,
53}
54
55pub fn parse<P: AsRef<Path>>(
56    inst_path: P,
57    file_format: FileFormat,
58    opb_opts: fio::opb::Options,
59) -> anyhow::Result<Parsed> {
60    let inst_path = inst_path.as_ref();
61    match file_format {
62        FileFormat::Infer => {
63            if let Some(ext) = inst_path.extension() {
64                let path_without_compr = inst_path.with_extension("");
65                let ext = if is_one_of!(ext, "gz", "bz2", "xz") {
66                    // Strip compression extension
67                    match path_without_compr.extension() {
68                        Some(ext) => ext,
69                        None => anyhow::bail!(Error::NoFileExtension),
70                    }
71                } else {
72                    ext
73                };
74                if is_one_of!(ext, "mcnf", "bicnf", "wcnf", "cnf", "dimacs") {
75                    clausal(inst_path)
76                } else if is_one_of!(ext, "opb", "mopb", "pbmo") {
77                    pseudo_boolean(inst_path, opb_opts)
78                } else {
79                    anyhow::bail!(Error::UnknownFileExtension(OsString::from(ext)))
80                }
81            } else {
82                anyhow::bail!(Error::NoFileExtension)
83            }
84        }
85        FileFormat::Dimacs => clausal(inst_path),
86        FileFormat::Opb => pseudo_boolean(inst_path, opb_opts),
87    }
88}
89
90/// Processes a clausal input file, and optionally dumps an OPB file of the constraints for VeriPB
91/// to use as input
92fn clausal<P: AsRef<Path>>(input_path: P) -> anyhow::Result<Parsed> {
93    let (mut constr, objs) =
94        MultiOptInstance::<VarManager>::from_dimacs_path(input_path)?.decompose();
95    constr.var_manager_mut().mark_max_orig_var();
96    debug_assert_eq!(
97        constr.n_pbs(),
98        0,
99        "parsing should not convert constraint types yet"
100    );
101    debug_assert_eq!(
102        constr.n_cards(),
103        0,
104        "parsing should not convert constraint types yet"
105    );
106    let (constraints, vm) = constr.into_pbs();
107    Ok(Parsed {
108        constraints,
109        objs,
110        vm,
111    })
112}
113
114/// Processes a PB input file, and optionally dumps an OPB file where the objectives have been
115/// stripped for VeriPB to use as input
116fn pseudo_boolean<P: AsRef<Path>>(
117    input_path: P,
118    opb_opts: fio::opb::Options,
119) -> anyhow::Result<Parsed> {
120    let (mut constr, objs) =
121        MultiOptInstance::<VarManager>::from_opb_path(input_path, opb_opts)?.decompose();
122    constr.var_manager_mut().mark_max_orig_var();
123    debug_assert_eq!(
124        constr.n_clauses(),
125        0,
126        "parsing should not convert constraint types yet"
127    );
128    debug_assert_eq!(
129        constr.n_cards(),
130        0,
131        "parsing should not convert constraint types yet"
132    );
133    let (constraints, vm) = constr.into_pbs();
134    Ok(Parsed {
135        constraints,
136        objs,
137        vm,
138    })
139}
140
141#[cfg(feature = "maxpre")]
142fn constraints_to_clausal(
143    constraints: Vec<PbConstraint>,
144    vm: &mut VarManager,
145) -> Result<rustsat::instances::Cnf, rustsat::OutOfMemory> {
146    use rustsat::{encodings::pb::default_encode_pb_constraint, instances::Cnf};
147
148    let mut cnf = Cnf::new();
149    for constr in constraints {
150        default_encode_pb_constraint(constr, &mut cnf, vm)?;
151    }
152    Ok(cnf)
153}
154
155#[cfg(feature = "maxpre")]
156pub fn max_pre(
157    parsed: Parsed,
158    techniques: &str,
159    reindexing: bool,
160) -> Result<(maxpre::MaxPre, Instance), rustsat::OutOfMemory> {
161    use maxpre::PreproClauses;
162    let Parsed {
163        constraints,
164        objs,
165        mut vm,
166        ..
167    } = parsed;
168    let cnf = constraints_to_clausal(constraints, &mut vm)?;
169    let mut prepro = maxpre::MaxPre::new(
170        cnf,
171        objs.into_iter().map(|o| o.into_soft_cls()).collect(),
172        !reindexing,
173    );
174    prepro.preprocess(techniques, 0, 1e9);
175    let (cnf, objs) = prepro.prepro_instance();
176    let objs: Vec<_> = objs
177        .into_iter()
178        .enumerate()
179        .map(|(idx, (softs, offset))| {
180            Objective::new(
181                softs.into_iter().map(|(cl, w)| {
182                    debug_assert_eq!(cl.len(), 1);
183                    (!cl[0], w)
184                }),
185                offset,
186                idx,
187            )
188        })
189        .collect();
190    let max_var = cnf.iter().fold(rustsat::types::Var::new(0), |max, cl| {
191        cl.iter().fold(max, |max, l| std::cmp::max(max, l.var()))
192    });
193    let vm = VarManager::new(max_var, max_var);
194    Ok((
195        prepro,
196        Instance {
197            clauses: cnf.into_iter().map(|cl| (cl, None)).collect(),
198            objs,
199            vm,
200        },
201    ))
202}
203
204struct Collector(Vec<(Clause, Option<pigeons::AbsConstraintId>)>);
205
206impl CollectClauses for Collector {
207    fn n_clauses(&self) -> usize {
208        self.0.len()
209    }
210
211    fn extend_clauses<T>(&mut self, cl_iter: T) -> Result<(), rustsat::OutOfMemory>
212    where
213        T: IntoIterator<Item = Clause>,
214    {
215        self.0.extend(cl_iter.into_iter().map(|cl| (cl, None)));
216        Ok(())
217    }
218}
219
220impl CollectCertClauses for Collector {
221    fn extend_cert_clauses<T>(&mut self, cl_iter: T) -> Result<(), rustsat::OutOfMemory>
222    where
223        T: IntoIterator<Item = (Clause, pigeons::AbsConstraintId)>,
224    {
225        self.0
226            .extend(cl_iter.into_iter().map(|(cl, id)| (cl, Some(id))));
227        Ok(())
228    }
229}
230
231pub fn to_clausal(
232    parsed: Parsed,
233    proof_paths: &Option<(PathBuf, Option<PathBuf>)>,
234) -> anyhow::Result<(Option<pigeons::Proof<io::BufWriter<fs::File>>>, Instance)> {
235    let Parsed {
236        mut constraints,
237        objs,
238        mut vm,
239        ..
240    } = parsed;
241    let mut blits = RsHashMap::default();
242    // NOTE: soft clause to objective conversion is not certified
243    let objs: Vec<_> = objs
244        .into_iter()
245        .enumerate()
246        .map(|(idx, o)| process_objective(o, idx, &mut constraints, &mut blits, &mut vm))
247        .collect();
248    let mut proof = None;
249    // (certified) PB -> CNF conversion
250    let mut collector = Collector(vec![]);
251    if let Some((proof_path, veripb_input_path)) = proof_paths {
252        let n_constraints = constraints.iter().fold(0, |s, c| {
253            if matches!(c, PbConstraint::Eq(_)) {
254                s + 2
255            } else {
256                s + 1
257            }
258        });
259
260        if let Some(veripb_input_path) = veripb_input_path {
261            // dump constraints into OPB file for VeriPB to read
262            let mut writer = io::BufWriter::new(fs::File::create(veripb_input_path)?);
263            let iter = constraints
264                .iter()
265                .map(|c| fio::opb::FileLine::<Option<_>>::Pb(c.clone()));
266            fio::opb::write_opb_lines(&mut writer, iter, fio::opb::Options::default())?;
267        }
268
269        let mut the_proof = crate::algs::proofs::init_proof(
270            io::BufWriter::new(fs::File::create(proof_path)?),
271            n_constraints,
272            &objs,
273        )?;
274
275        let mut id = pigeons::AbsConstraintId::new(1);
276        for constr in constraints {
277            let eq = matches!(constr, PbConstraint::Eq(_));
278            pb::cert::default_encode_pb_constraint(
279                (constr, id),
280                &mut collector,
281                &mut vm,
282                &mut the_proof,
283            )?;
284            id += 1 + usize::from(eq);
285        }
286        #[cfg(feature = "verbose-proofs")]
287        the_proof.comment(&"end OPB translation")?;
288        proof = Some(the_proof);
289    } else {
290        for constr in constraints {
291            pb::default_encode_pb_constraint(constr, &mut collector, &mut vm)?;
292        }
293    }
294    vm.mark_max_enc_var();
295    Ok((
296        proof,
297        Instance {
298            clauses: collector.0,
299            objs,
300            vm,
301        },
302    ))
303}
304
305fn process_objective<VM: ManageVars>(
306    obj: RsObjective,
307    idx: usize,
308    constrs: &mut Vec<PbConstraint>,
309    blits: &mut RsHashMap<Clause, Lit>,
310    vm: &mut VM,
311) -> Objective {
312    let (soft_cls, offset) = obj.into_soft_cls();
313    Objective::new(
314        soft_cls.into_iter().map(|(mut cl, w)| {
315            debug_assert!(cl.len() > 0);
316            if cl.len() == 1 {
317                return (!cl[0], w);
318            }
319            if let Some(&blit) = blits.get(&cl) {
320                return (blit, w);
321            }
322            let blit = vm.new_var().pos_lit();
323            // Save blit in case same soft clause reappears
324            // TODO: find way to not have to clone the clause here
325            blits.insert(cl.clone(), blit);
326            // Relax clause and add it to the CNF
327            cl.add(blit);
328            constrs.push(cl.into());
329            (blit, w)
330        }),
331        offset,
332        idx,
333    )
334}
335
336pub fn reindexing(inst: Instance) -> (Reindexer, Instance) {
337    let Instance {
338        mut clauses,
339        mut objs,
340        vm,
341        ..
342    } = inst;
343    let mut reindexer = Reindexer::new(vm.max_orig_var());
344    for obj in &mut objs {
345        let new_obj = Objective::new(
346            obj.iter().map(|(l, w)| (reindexer.reindex_lit(l), w)),
347            obj.offset(),
348            obj.idx(),
349        );
350        *obj = new_obj;
351    }
352    for (cl, _) in &mut clauses {
353        for l in cl {
354            *l = reindexer.reindex_lit(*l);
355        }
356    }
357    let max_var = reindexer.max_var().unwrap();
358    let vm = VarManager::new(max_var, max_var);
359    (reindexer, Instance { clauses, objs, vm })
360}