scuttle_core/algs/
proofs.rs

1//! # Proof Writing Functionality
2
3use core::fmt;
4use std::{
5    io,
6    marker::PhantomData,
7    sync::{atomic::AtomicBool, Arc},
8};
9
10#[cfg(feature = "interrupt-oracle")]
11use std::sync::Mutex;
12
13use cadical_veripb_tracer::{CadicalCertCollector, CadicalTracer};
14use pigeons::{
15    AbsConstraintId, Axiom, ConstraintId, ConstraintLike, Derivation, OperationLike,
16    OperationSequence, Order, OrderVar, Proof, ProofGoal, ProofGoalId, ProofOnlyVar, Substitution,
17    VarLike,
18};
19use rustsat::{
20    encodings::{atomics, card::Totalizer, cert::CollectClauses, pb::GeneralizedTotalizer},
21    instances::ManageVars,
22    solvers::Initialize,
23    types::{Assignment, Clause, Lit, RsHashMap, TernaryVal, Var},
24};
25use rustsat_cadical::CaDiCaL;
26
27use crate::{
28    types::{Instance, ObjEncoding, Objective, VarManager},
29    KernelOptions, Limits, Stats,
30};
31
32use super::default_blocking_clause;
33
34/// Trait for initializing algorithms
35pub trait InitCert: super::Init {
36    type ProofWriter: io::Write;
37
38    /// Initialization of the algorithm providing all optional input
39    fn new_cert<Cls>(
40        clauses: Cls,
41        objs: Vec<Objective>,
42        var_manager: VarManager,
43        opts: KernelOptions,
44        proof: Proof<Self::ProofWriter>,
45        block_clause_gen: <Self as super::Init>::BlockClauseGen,
46    ) -> anyhow::Result<Self>
47    where
48        Cls: IntoIterator<Item = (Clause, pigeons::AbsConstraintId)>;
49
50    /// Initialization of the algorithm using an [`Instance`] rather than iterators
51    fn from_instance_cert(
52        inst: Instance,
53        opts: KernelOptions,
54        proof: Proof<Self::ProofWriter>,
55        block_clause_gen: <Self as super::Init>::BlockClauseGen,
56    ) -> anyhow::Result<Self> {
57        Self::new_cert(
58            inst.clauses.into_iter().map(|(cl, id)| (cl, id.unwrap())),
59            inst.objs,
60            inst.vm,
61            opts,
62            proof,
63            block_clause_gen,
64        )
65    }
66}
67
68pub trait InitCertDefaultBlock: InitCert<BlockClauseGen = fn(Assignment) -> Clause> {
69    /// Initializes the algorithm with the default blocking clause generator
70    fn new_default_blocking_cert<Cls>(
71        clauses: Cls,
72        objs: Vec<Objective>,
73        var_manager: VarManager,
74        opts: KernelOptions,
75        proof: Proof<Self::ProofWriter>,
76    ) -> anyhow::Result<Self>
77    where
78        Cls: IntoIterator<Item = (Clause, pigeons::AbsConstraintId)>,
79    {
80        Self::new_cert(
81            clauses,
82            objs,
83            var_manager,
84            opts,
85            proof,
86            default_blocking_clause,
87        )
88    }
89
90    /// Initializes the algorithm using an [`Instance`] rather than iterators with the default
91    /// blocking clause generator
92    fn from_instance_default_blocking_cert(
93        inst: Instance,
94        opts: KernelOptions,
95        proof: Proof<Self::ProofWriter>,
96    ) -> anyhow::Result<Self> {
97        Self::new_cert(
98            inst.clauses.into_iter().map(|(cl, id)| (cl, id.unwrap())),
99            inst.objs,
100            inst.vm,
101            opts,
102            proof,
103            default_blocking_clause,
104        )
105    }
106}
107
108impl<Alg> InitCertDefaultBlock for Alg where Alg: InitCert<BlockClauseGen = fn(Assignment) -> Clause>
109{}
110
111/// Stuff to keep in the solver for proof logging
112pub struct ProofStuff<ProofW: io::Write> {
113    /// The handle of the proof tracer
114    pub pt_handle: rustsat_cadical::ProofTracerHandle<CadicalTracer<ProofW>>,
115    /// Mapping literal values to other literal values or other expressions
116    pub value_map: Vec<(Axiom<AnyVar>, Value)>,
117    /// Reified objective constraints
118    obj_bound_constrs: RsHashMap<(usize, usize), (Axiom<AnyVar>, AbsConstraintId, AbsConstraintId)>,
119}
120
121#[derive(Debug, Clone, Copy)]
122pub enum Value {
123    Identical(Lit),
124    ObjAtLeast(usize, usize),
125}
126
127pub fn init_proof<W>(
128    writer: W,
129    n_constraints: usize,
130    objs: &[Objective],
131) -> io::Result<pigeons::Proof<W>>
132where
133    W: io::Write,
134{
135    let mut proof = pigeons::Proof::new_with_conclusion(
136        writer,
137        n_constraints,
138        false,
139        pigeons::OutputGuarantee::None,
140        &pigeons::Conclusion::<&str>::Unsat(Some(pigeons::ConstraintId::last(1))),
141    )?;
142    let order = crate::algs::proofs::objectives_as_order(objs);
143    proof.define_order(&order)?;
144    proof.load_order(order.name(), order.used_vars())?;
145    Ok(proof)
146}
147
148fn objectives_as_order(objs: &[Objective]) -> Order<Var, LbConstraint<OrderVar<Var>>> {
149    let mut order = Order::<Var, LbConstraint<_>>::new(String::from("pareto"));
150    for (idx, obj) in objs.iter().enumerate() {
151        let mult = isize::try_from(obj.unit_weight())
152            .expect("can only handle unit weight up to `isize::MAX`");
153        let constr = LbConstraint {
154            lits: obj
155                .iter()
156                .flat_map(|(l, w)| {
157                    let w = isize::try_from(w).expect("can only handle weights up to `isize::MAX`");
158                    let (u, v) = order.use_var(l.var());
159                    [
160                        (-w * mult, u.axiom(l.is_neg())),
161                        (w * mult, v.axiom(l.is_neg())),
162                    ]
163                })
164                .collect(),
165            bound: 0,
166        };
167        // For O_idx, this proof sums up the following constraints
168        // - O_idx(u) <= O_idx(v)
169        // - O_idx(v) <= O_idx(w)
170        // - O_idx(u) > O_idx(w)
171        // This will always lead to a contradiction, proving transitivity
172        let trans_proof = vec![Derivation::from(
173            OperationSequence::<OrderVar<Var>>::from(ConstraintId::abs(idx + 1))
174                + ConstraintId::abs(objs.len() + idx + 1)
175                + ConstraintId::last(1),
176        )];
177        order.add_definition_constraint(constr, trans_proof, None)
178    }
179    order
180}
181
182pub fn certify_pmin_cut<ProofW>(
183    obj_encs: &[ObjEncoding<GeneralizedTotalizer, Totalizer>],
184    objs: &[Objective],
185    costs: &[usize],
186    witness: &Assignment,
187    max_enc_var: Var,
188    proof_stuff: &mut ProofStuff<ProofW>,
189    oracle: &mut rustsat_cadical::CaDiCaL<'_, '_>,
190) -> io::Result<AbsConstraintId>
191where
192    ProofW: io::Write + 'static,
193{
194    #[cfg(feature = "verbose-proofs")]
195    {
196        use itertools::Itertools;
197        oracle
198            .proof_tracer_mut(&proof_stuff.pt_handle)
199            .proof_mut()
200            .comment(&format_args!(
201                "Introducing P-minimal cut for costs [{}] based on the following witness:",
202                costs.iter().format(", "),
203            ))?;
204        oracle
205            .proof_tracer_mut(&proof_stuff.pt_handle)
206            .proof_mut()
207            .comment(&format_args!("{witness}"))?;
208    }
209
210    // buid the "ideal" cut. this is only valid with the strict proof semantics
211    let cut_data: Vec<_> = obj_encs
212        .iter()
213        .zip(objs)
214        .zip(costs)
215        .map(|((enc, obj), cst)| {
216            if *cst <= obj.lower_bound() {
217                debug_assert!(*cst == 0 || obj.reform_id().is_some());
218                return (None, obj.reform_id());
219            }
220            if obj.n_lits() == 1 {
221                let lit = !obj.iter().next().unwrap().0;
222                return (Some(AnyVar::Solver(lit.var()).axiom(lit.is_neg())), None);
223            }
224            // weird edge case with a single oll totalizer output as the objective encoding
225            if enc.n_output_lits() == 1 {
226                let lit = enc.enforce_ub(enc.offset()).unwrap()[0];
227                return (Some(AnyVar::Solver(lit.var()).axiom(lit.is_neg())), None);
228            }
229            let (lit, def) = if enc.is_buffer_empty() {
230                // totalizer output semantics are identical with the required semantics, can
231                // therefore reuse totalizer output
232                let (olit, defs) = enc.output_proof_details(*cst);
233                (
234                    AnyVar::Solver(olit.var()).axiom(olit.is_neg()),
235                    defs.only_if_def,
236                )
237            } else {
238                // totalizer output semantics do _not_ include the entire objective and can
239                // therefore not be used
240                let (lit, _, def) = get_obj_bound_constraint(*cst, obj, proof_stuff, oracle)
241                    .expect("failed to write proof");
242                (lit, Some(def))
243            };
244            debug_assert!(def.is_some());
245            (Some(!lit), def)
246        })
247        .collect();
248    let cut = LbConstraint::clause(cut_data.iter().filter_map(|&(l, _)| l));
249
250    let ProofStuff {
251        pt_handle,
252        value_map,
253        ..
254    } = proof_stuff;
255    let proof = oracle.proof_tracer_mut(pt_handle).proof_mut();
256
257    // Extend witness to encoding variables under strict semantics
258    // TODO: avoid clone
259    let fixed_witness: Vec<Axiom<AnyVar>> = {
260        // NOTE: assignments from `extend_assignment` have precendence, as they weill overwrite
261        // assignments coming from the witness
262        let mut fixed_witness: Assignment = witness
263            .iter()
264            .chain(
265                obj_encs
266                    .iter()
267                    .flat_map(|enc| enc.extend_assignment(witness)),
268            )
269            .collect();
270        // NOTE: Need to do this in two steps since the identities depend on the encoding
271        // assignments
272        let mut solver_vars = Vec::new();
273        // TODO: clean this up a bit
274        for &(this, that) in value_map.iter() {
275            match that {
276                Value::Identical(that) => match fixed_witness.lit_value(that) {
277                    TernaryVal::True => match this.var() {
278                        AnyVar::Proof(_) => solver_vars.push(this),
279                        AnyVar::Solver(var) => fixed_witness.assign_lit(var.lit(this.is_neg())),
280                    },
281                    TernaryVal::False => match this.var() {
282                        AnyVar::Proof(_) => solver_vars.push(!this),
283                        AnyVar::Solver(var) => fixed_witness.assign_lit(var.lit(!this.is_neg())),
284                    },
285                    TernaryVal::DontCare => {
286                        panic!("need assignment for left of identity ({this:?}, {that})")
287                    }
288                },
289                Value::ObjAtLeast(obj_idx, value) => {
290                    if costs[obj_idx] >= value {
291                        match this.var() {
292                            AnyVar::Proof(_) => solver_vars.push(this),
293                            AnyVar::Solver(var) => fixed_witness.assign_lit(var.lit(this.is_neg())),
294                        }
295                    } else {
296                        match this.var() {
297                            AnyVar::Proof(_) => solver_vars.push(!this),
298                            AnyVar::Solver(var) => {
299                                fixed_witness.assign_lit(var.lit(!this.is_neg()))
300                            }
301                        }
302                    }
303                }
304            }
305        }
306        fixed_witness
307            .into_iter()
308            .map(axiom)
309            .chain(solver_vars)
310            .collect()
311    };
312
313    let negation_id = ConstraintId::from(proof.next_id());
314
315    // Map all weakly dominated solutions to the witness itself
316    let map_dom = proof.redundant(
317        &LbConstraint {
318            lits: fixed_witness
319                .iter()
320                .map(|l| (1, *l))
321                .chain(
322                    cut.lits
323                        .iter()
324                        .map(|(_, l)| (isize::try_from(fixed_witness.len() + 1).unwrap(), *l)),
325                )
326                .collect(),
327            bound: isize::try_from(fixed_witness.len())
328                .expect("can only handle bounds up to `usize::MAX`"),
329        },
330        fixed_witness.iter().map(|l| Substitution::from(*l)),
331        cut_data
332            .into_iter()
333            .zip(objs)
334            .zip(costs)
335            .enumerate()
336            .filter_map(|(idx, ((dat, obj), cst))| {
337                match dat {
338                    (None, Some(reform_id)) => {
339                        // Cost is lower bound derived in core boosting
340                        debug_assert!(*cst <= obj.lower_bound());
341                        Some(
342                            ProofGoal::new(
343                                ProofGoalId::specific(idx + 2),
344                                [Derivation::from(
345                                    OperationSequence::from(ConstraintId::from(reform_id))
346                                        + ConstraintId::last(1),
347                                )],
348                            )
349                            .into(),
350                        )
351                    }
352                    (Some(lit), Some(def)) => {
353                        // Prove that the witness dominates
354                        let mut conf_deriv = (OperationSequence::from(ConstraintId::last(1))
355                            * (*cst - obj.lower_bound()))
356                            + def;
357                        if let Some(reform_id) = obj.reform_id() {
358                            conf_deriv += reform_id;
359                        }
360                        conf_deriv *= obj.unit_weight();
361                        conf_deriv += ConstraintId::last(2);
362                        Some(
363                            ProofGoal::new(
364                                ProofGoalId::specific(idx + 2),
365                                [
366                                    Derivation::Rup(
367                                        LbConstraint::clause([!lit]),
368                                        vec![negation_id],
369                                    ),
370                                    Derivation::from(conf_deriv),
371                                ],
372                            )
373                            .into(),
374                        )
375                    }
376                    (_, None) => {
377                        // Cost is trivial lower bound of objective or objective is trivial
378                        // NOTE: third case is that the encoding has only one output
379                        // debug_assert!(obj.n_lits() == 1 || *cst == 0);
380                        None
381                    }
382                }
383            }),
384    )?;
385
386    // Exclude witness
387    let exclude = proof.exclude_solution(
388        witness
389            .clone()
390            .truncate(max_enc_var)
391            .iter()
392            .map(Axiom::from),
393    )?;
394    #[cfg(feature = "verbose-proofs")]
395    proof.equals(
396        &LbConstraint::clause(fixed_witness.iter().map(|l| !*l)),
397        Some(exclude.into()),
398    )?;
399
400    // Add the actual P-minimal cut as a clause
401    let cut_id =
402        proof.reverse_unit_prop(&cut, [map_dom, exclude].into_iter().map(ConstraintId::from))?;
403    // Need to move to core to be able to delete the derivation constraints
404    proof.move_ids_to_core([ConstraintId::from(cut_id)])?;
405
406    // Remove auxiliary constraints
407    //proof.delete_ids::<AnyVar, LbConstraint<AnyVar>, _, _>(
408    proof.delete_ids(
409        [exclude, map_dom].into_iter().map(ConstraintId::from),
410        [ProofGoal::new(
411            ProofGoalId::specific(1),
412            [Derivation::Rup(
413                Clause::default(),
414                vec![ConstraintId::last(1), cut_id.into()],
415            )],
416        )
417        .into()],
418    )?;
419
420    Ok(cut_id)
421}
422
423/// Certifies a reification of a cube of assumptions stemming from an encoding
424///
425/// The certification will make the first assumption equal to the reification literal, while all
426/// others are implied by the reification literal
427///
428/// Returns the ID stating that the first assumption implies the reification literal
429pub fn certify_assump_reification<ProofW>(
430    oracle: &mut CaDiCaL<'_, '_>,
431    proof_stuff: &mut ProofStuff<ProofW>,
432    obj: &Objective,
433    enc: &ObjEncoding<GeneralizedTotalizer, Totalizer>,
434    value: usize,
435    reif_lit: Lit,
436    assumps: &[Lit],
437) -> anyhow::Result<AbsConstraintId>
438where
439    ProofW: io::Write + 'static,
440{
441    #[cfg(feature = "verbose-proofs")]
442    oracle
443        .proof_tracer_mut(&proof_stuff.pt_handle)
444        .proof_mut()
445        .comment(&"reification of multiple assumptions for one objective encoding")?;
446    Ok(if enc.is_buffer_empty() {
447        let ProofStuff {
448            pt_handle,
449            value_map,
450            ..
451        } = proof_stuff;
452        let proof = oracle.proof_tracer_mut(pt_handle).proof_mut();
453
454        // NOTE: this assumes that the assumptions are outputs in increasing order
455        let mut assumps = assumps.iter();
456        let a = *assumps.next().unwrap();
457        // in the proof, the reification literal is going to be _equal_ to the lowest
458        // output
459        let clause = atomics::lit_impl_lit(reif_lit, a);
460        let if_def = proof.redundant(&clause, [reif_lit.var().substitute_fixed(false)], None)?;
461        let only_if_def = proof.redundant(
462            &atomics::lit_impl_lit(a, reif_lit),
463            [reif_lit.var().substitute_fixed(true)],
464            None,
465        )?;
466        value_map.push((axiom(reif_lit), Value::Identical(a)));
467        CadicalCertCollector::new(oracle, pt_handle).add_cert_clause(clause, if_def)?;
468        let (first_olit, first_sems) = enc.output_proof_details(value);
469        debug_assert_eq!(!first_olit, a);
470        // all remaining assumptions are implied by the reification literal
471        let mut val = value;
472        for &a in assumps {
473            let proof = oracle.proof_tracer_mut(pt_handle).proof_mut();
474            val = enc.next_higher(val);
475            // first convince veripb that `olit -> first_olit`
476            let (olit, sems) = enc.output_proof_details(val);
477            debug_assert_eq!(!olit, a);
478            let implication = proof.operations::<Var>(
479                &((OperationSequence::from(first_sems.if_def.unwrap())
480                    + sems.only_if_def.unwrap())
481                    / (val - value + 1))
482                    .saturate(),
483            )?;
484            #[cfg(feature = "verbose-proofs")]
485            proof.equals(
486                &rustsat::clause![!olit, first_olit],
487                Some(ConstraintId::last(1)),
488            )?;
489            let clause = atomics::lit_impl_lit(reif_lit, a);
490            let id = proof.reverse_unit_prop(&clause, [implication.into(), if_def.into()])?;
491            CadicalCertCollector::new(oracle, pt_handle).add_cert_clause(clause, id)?;
492            // delete implication
493            oracle
494                .proof_tracer_mut(pt_handle)
495                .proof_mut()
496                .delete_ids::<AnyVar, LbConstraint<_>, _, _>(
497                    [ConstraintId::from(implication)],
498                    None,
499                )?;
500        }
501        only_if_def
502    } else {
503        // cannot reuse first totalizer output as semantics of the reification literal, need to
504        // introduce new proof variable for needed semantics
505        let (ideal_lit, def_1, _) = get_obj_bound_constraint(value, obj, proof_stuff, oracle)?;
506
507        let ProofStuff {
508            pt_handle,
509            value_map,
510            ..
511        } = proof_stuff;
512        let proof = oracle.proof_tracer_mut(pt_handle).proof_mut();
513
514        // the reification variable is implied by the objective bound
515        let if_def = proof.redundant(
516            &LbConstraint::clause([axiom(!reif_lit), !ideal_lit]),
517            [AnyVar::Solver(reif_lit.var()).substitute_fixed(false)],
518            None,
519        )?;
520        let only_if_def = proof.redundant(
521            &LbConstraint::clause([axiom(reif_lit), (ideal_lit)]),
522            [AnyVar::Solver(reif_lit.var()).substitute_fixed(true)],
523            None,
524        )?;
525        value_map.push((axiom(!reif_lit), Value::ObjAtLeast(obj.idx(), value)));
526        let mut val = value;
527        for &a in assumps {
528            let proof = oracle.proof_tracer_mut(pt_handle).proof_mut();
529            let clause = atomics::lit_impl_lit(reif_lit, a);
530            let (olit, sems) = enc.output_proof_details(val);
531            // NOTE: this assumes that objective encoding variables are higher than GTE variables
532            // and that the buffered input variables are first in the assumptions
533            let id = if a.var() < olit.var() {
534                // this is an input literal with weight higher than the bound
535                proof.reverse_unit_prop(
536                    &clause,
537                    [if_def, def_1].into_iter().map(ConstraintId::from),
538                )?
539            } else {
540                // this is an output literal of a subtree
541                debug_assert_eq!(!olit, a);
542                let tmp = proof.operations::<AnyVar>(
543                    &(OperationSequence::from(def_1) + sems.only_if_def.unwrap()),
544                )?;
545                let id = proof.reverse_unit_prop(
546                    &clause,
547                    [if_def, tmp].into_iter().map(ConstraintId::from),
548                )?;
549                proof.delete_ids::<AnyVar, LbConstraint<AnyVar>, _, _>(
550                    [ConstraintId::from(tmp)],
551                    None,
552                )?;
553                val = enc.next_higher(val);
554                id
555            };
556            CadicalCertCollector::new(oracle, pt_handle).add_cert_clause(clause, id)?;
557        }
558        only_if_def
559    })
560}
561
562pub fn linsu_certify_lower_bound<ProofW>(
563    base_assumps: &[Lit],
564    cost: usize,
565    core: &[Lit],
566    obj: &Objective,
567    encoding: &ObjEncoding<GeneralizedTotalizer, Totalizer>,
568    proof_stuff: &mut ProofStuff<ProofW>,
569    oracle: &mut rustsat_cadical::CaDiCaL<'_, '_>,
570) -> io::Result<AbsConstraintId>
571where
572    ProofW: io::Write + 'static,
573{
574    // derive lower bound on objective in proof
575    let core_id = oracle
576        .proof_tracer_mut(&proof_stuff.pt_handle)
577        .core_id()
578        .expect("expected core id in proof");
579    #[cfg(feature = "verbose-proofs")]
580    {
581        use itertools::Itertools;
582        oracle
583            .proof_tracer_mut(&proof_stuff.pt_handle)
584            .proof_mut()
585            .comment(&format_args!(
586                "certifying linsu lower bound for bound {cost} from core [{}]",
587                core.iter().format(", ")
588            ))?;
589    }
590    let mut start = 0;
591    for &ba in base_assumps {
592        if core[start] == !ba {
593            start += 1;
594        }
595    }
596    let core_id = if encoding.is_buffer_empty() {
597        // encoding has empty buffer, output semantics can therefore be reused for objective bound
598        // semantics
599        let (first_olit, first_sems) = encoding.output_proof_details(cost);
600        if core.len() == 1 {
601            // unit core explicitly implies bound
602            debug_assert_eq!(core[0], first_olit);
603            core_id
604        } else {
605            let proof = oracle.proof_tracer_mut(&proof_stuff.pt_handle).proof_mut();
606
607            // convince veripb that `core_lit -> first_olit` and therefore
608            // rewrite core as `first_olit` unit
609            // NOTE: this assumes that
610            // - the base assumptions are in the core first
611            // - the outputs are in the core in order of increasing value
612            let start = if core[start] == first_olit {
613                start + 1
614            } else {
615                start
616            };
617            let mut implications = Vec::with_capacity(core.len());
618            let mut val = cost;
619            for &clit in &core[start..] {
620                let (mut olit, mut sems) = encoding.output_proof_details(val);
621                while clit != olit {
622                    val = encoding.next_higher(val);
623                    (olit, sems) = encoding.output_proof_details(val);
624                }
625                let implication = proof.operations::<Var>(
626                    &((OperationSequence::from(first_sems.if_def.unwrap())
627                        + sems.only_if_def.unwrap())
628                        / (val - cost + 1))
629                        .saturate(),
630                )?;
631                #[cfg(feature = "verbose-proofs")]
632                proof.equals(
633                    &rustsat::clause![!olit, first_olit],
634                    Some(ConstraintId::from(implication)),
635                )?;
636                implications.push(implication);
637                val = encoding.next_higher(val);
638            }
639            if !implications.is_empty() || !base_assumps.is_empty() {
640                // NOTE: we always run this case with base assumptions to ensure that all base
641                // assumptions are present in the returned ID
642
643                // rewrite the core
644                let core_id = proof.reverse_unit_prop(
645                    &atomics::cube_impl_lit(base_assumps, first_olit),
646                    implications
647                        .iter()
648                        .copied()
649                        .chain([core_id])
650                        .map(ConstraintId::from),
651                )?;
652                if !implications.is_empty() {
653                    // delete implications
654                    proof.delete_ids::<AnyVar, LbConstraint<_>, _, _>(
655                        implications.into_iter().map(ConstraintId::from),
656                        None,
657                    )?;
658                }
659                core_id
660            } else {
661                core_id
662            }
663        }
664    } else {
665        // cannot reuse first totalizer output as semantics of the reification literal, need to
666        // introduce new proof variable for needed semantics
667        let (ideal_lit, def_1, _) = get_obj_bound_constraint(cost, obj, proof_stuff, oracle)?;
668
669        let proof = oracle.proof_tracer_mut(&proof_stuff.pt_handle).proof_mut();
670
671        let mut implications = Vec::with_capacity(core.len());
672        let mut val = cost;
673        for &clit in &core[start..] {
674            let clause = LbConstraint::clause([axiom(!clit), ideal_lit]);
675            let (mut olit, mut sems) = encoding.output_proof_details(val);
676            let implication = if clit.var() < olit.var() {
677                proof.reverse_unit_prop(&clause, [ConstraintId::from(def_1)])?
678            } else {
679                while clit.var() != olit.var() {
680                    val = encoding.next_higher(val);
681                    (olit, sems) = encoding.output_proof_details(val);
682                }
683                let tmp = proof.operations::<AnyVar>(
684                    &(OperationSequence::from(def_1) + sems.only_if_def.unwrap()),
685                )?;
686                let implication = proof.reverse_unit_prop(&clause, [ConstraintId::from(tmp)])?;
687                proof.delete_ids::<AnyVar, LbConstraint<AnyVar>, _, _>(
688                    [ConstraintId::from(tmp)],
689                    None,
690                )?;
691                val = encoding.next_higher(val);
692                implication
693            };
694            implications.push(implication);
695        }
696        // rewrite the core
697        // we do this always to ensure that all base assumptions and the proof-only var are there
698        #[cfg(feature = "verbose-proofs")]
699        proof.comment(&"the next constraint is a linsu bound")?;
700        let core_id = proof.reverse_unit_prop(
701            &LbConstraint::clause(base_assumps.iter().map(|l| axiom(!*l)).chain([ideal_lit])),
702            implications
703                .iter()
704                .copied()
705                .chain([core_id])
706                .map(ConstraintId::from),
707        )?;
708        // delete implications
709        proof.delete_ids::<AnyVar, LbConstraint<_>, _, _>(
710            implications.into_iter().map(ConstraintId::from),
711            None,
712        )?;
713        core_id
714    };
715    Ok(core_id)
716}
717
718pub fn get_obj_bound_constraint<ProofW>(
719    value: usize,
720    obj: &Objective,
721    proof_stuff: &mut ProofStuff<ProofW>,
722    oracle: &mut rustsat_cadical::CaDiCaL<'_, '_>,
723) -> io::Result<(Axiom<AnyVar>, AbsConstraintId, AbsConstraintId)>
724where
725    ProofW: io::Write + 'static,
726{
727    let ProofStuff {
728        pt_handle,
729        value_map,
730        obj_bound_constrs,
731    } = proof_stuff;
732    let proof = oracle.proof_tracer_mut(pt_handle).proof_mut();
733
734    if let Some((lit, def_1, def_2)) = obj_bound_constrs.get(&(obj.idx(), value)) {
735        return Ok((*lit, *def_1, *def_2));
736    }
737    // introduce a new objective bound reification
738    let obj_reif_var = AnyVar::Proof(proof.new_proof_var());
739    value_map.push((
740        obj_reif_var.pos_axiom(),
741        Value::ObjAtLeast(obj.idx(), value),
742    ));
743    #[cfg(feature = "verbose-proofs")]
744    proof.comment(&format_args!(
745        "{obj_reif_var} = objective {} is >= {value}",
746        obj.idx()
747    ))?;
748    let bound = isize::try_from(obj.iter().fold(0, |sum, (_, w)| sum + w)).unwrap() + 1
749        - isize::try_from(value).unwrap();
750    let def_1 = proof.redundant(
751        &LbConstraint {
752            lits: obj
753                .iter()
754                .map(|(l, w)| {
755                    let w = isize::try_from(w).expect("can only handle weights up to `isize::MAX`");
756                    (w, AnyVar::Solver(l.var()).axiom(l.is_pos()))
757                })
758                .chain([(bound, obj_reif_var.pos_axiom())])
759                .collect(),
760            bound,
761        },
762        [obj_reif_var.substitute_fixed(true)],
763        [],
764    )?;
765    let bound = isize::try_from(value).unwrap();
766    let def_2 = proof.redundant(
767        &LbConstraint {
768            lits: obj
769                .iter()
770                .map(|(l, w)| {
771                    let w = isize::try_from(w).expect("can only handle weights up to `isize::MAX`");
772                    (w, AnyVar::Solver(l.var()).axiom(l.is_neg()))
773                })
774                .chain([(bound, obj_reif_var.neg_axiom())])
775                .collect(),
776            bound,
777        },
778        [obj_reif_var.substitute_fixed(false)],
779        [],
780    )?;
781    obj_bound_constrs.insert((obj.idx(), value), (obj_reif_var.pos_axiom(), def_1, def_2));
782    Ok((obj_reif_var.pos_axiom(), def_1, def_2))
783}
784
785pub struct LbConstraint<V: VarLike> {
786    lits: Vec<(isize, Axiom<V>)>,
787    bound: isize,
788}
789
790impl<V: VarLike> LbConstraint<V> {
791    pub fn clause<I: IntoIterator<Item = Axiom<V>>>(iter: I) -> Self {
792        LbConstraint {
793            lits: iter.into_iter().map(|a| (1, a)).collect(),
794            bound: 1,
795        }
796    }
797}
798
799impl<V: VarLike> ConstraintLike<V> for LbConstraint<V> {
800    fn rhs(&self) -> isize {
801        self.bound
802    }
803
804    fn sum_iter(&self) -> impl Iterator<Item = (isize, Axiom<V>)> {
805        self.lits.iter().copied()
806    }
807}
808
809#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
810pub enum AnyVar {
811    Proof(ProofOnlyVar),
812    Solver(Var),
813}
814
815impl From<ProofOnlyVar> for AnyVar {
816    fn from(value: ProofOnlyVar) -> Self {
817        AnyVar::Proof(value)
818    }
819}
820
821impl From<Var> for AnyVar {
822    fn from(value: Var) -> Self {
823        AnyVar::Solver(value)
824    }
825}
826
827impl VarLike for AnyVar {
828    type Formatter = Self;
829}
830
831pub fn axiom(lit: Lit) -> Axiom<AnyVar> {
832    AnyVar::Solver(lit.var()).axiom(lit.is_neg())
833}
834
835impl fmt::Display for AnyVar {
836    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
837        match self {
838            AnyVar::Proof(v) => write!(f, "{}", <ProofOnlyVar as VarLike>::Formatter::from(*v)),
839            AnyVar::Solver(v) => write!(f, "{}", <Var as VarLike>::Formatter::from(*v)),
840        }
841    }
842}
843
844impl<'term, 'learn, ProofW, OInit, BCG>
845    super::Kernel<rustsat_cadical::CaDiCaL<'term, 'learn>, ProofW, OInit, BCG>
846where
847    ProofW: io::Write,
848    OInit: Initialize<rustsat_cadical::CaDiCaL<'term, 'learn>>,
849    BCG: Fn(Assignment) -> Clause,
850{
851    /// **NOTE**: the order must be already loaded in the proof, e.g., through calling
852    /// [`init_proof`]
853    pub fn new_cert<Cls>(
854        clauses: Cls,
855        objs: Vec<Objective>,
856        var_manager: VarManager,
857        bcg: BCG,
858        proof: Proof<ProofW>,
859        opts: KernelOptions,
860    ) -> anyhow::Result<Self>
861    where
862        ProofW: io::Write + 'static,
863        Cls: IntoIterator<Item = (Clause, pigeons::AbsConstraintId)>,
864    {
865        use rustsat::{encodings::cert::CollectClauses, solvers::Solve};
866
867        let mut stats = Stats {
868            n_objs: 0,
869            n_real_objs: 0,
870            n_orig_clauses: 0,
871            ..Default::default()
872        };
873        let mut oracle = OInit::init();
874        let pt_handle = oracle.connect_proof_tracer(CadicalTracer::new(proof), true);
875        oracle.reserve(var_manager.max_var().unwrap())?;
876        let clauses: Vec<_> = clauses.into_iter().collect();
877        let orig_cnf = if opts.store_cnf {
878            Some(clauses.iter().map(|(cl, _)| cl.clone()).collect())
879        } else {
880            None
881        };
882        stats.n_orig_clauses = clauses.len();
883
884        // Add clauses to solver
885        let mut collector = CadicalCertCollector::new(&mut oracle, &pt_handle);
886        collector.extend_cert_clauses(clauses)?;
887
888        stats.n_objs = objs.len();
889        stats.n_real_objs = objs.iter().fold(0, |cnt, o| {
890            if matches!(o, Objective::Constant { .. }) {
891                cnt
892            } else {
893                cnt + 1
894            }
895        });
896
897        // Record objective literal occurrences
898        #[cfg(feature = "sol-tightening")]
899        let obj_lit_data = {
900            use rustsat::types::RsHashMap;
901
902            use crate::types::ObjLitData;
903
904            let mut obj_lit_data: RsHashMap<_, ObjLitData> = RsHashMap::default();
905            for (idx, obj) in objs.iter().enumerate() {
906                match obj {
907                    Objective::Weighted { lits, .. } => {
908                        for &olit in lits.keys() {
909                            match obj_lit_data.get_mut(&olit) {
910                                Some(data) => data.objs.push(idx),
911                                None => {
912                                    obj_lit_data.insert(olit, ObjLitData { objs: vec![idx] });
913                                }
914                            }
915                        }
916                    }
917                    Objective::Unweighted { lits, .. } => {
918                        for &olit in lits {
919                            match obj_lit_data.get_mut(&olit) {
920                                Some(data) => data.objs.push(idx),
921                                None => {
922                                    obj_lit_data.insert(olit, ObjLitData { objs: vec![idx] });
923                                }
924                            }
925                        }
926                    }
927                    Objective::Constant { .. } => (),
928                }
929            }
930            obj_lit_data
931        };
932        #[cfg(feature = "sol-tightening")]
933        {
934            use rustsat::solvers::FreezeVar;
935            // Freeze objective variables so that they are not removed
936            for o in &objs {
937                for (l, _) in o.iter() {
938                    oracle.freeze_var(l.var())?;
939                }
940            }
941        }
942        #[cfg(feature = "interrupt-oracle")]
943        let interrupter = {
944            use rustsat::solvers::Interrupt;
945            oracle.interrupter()
946        };
947
948        Ok(Self {
949            oracle,
950            var_manager,
951            #[cfg(feature = "sol-tightening")]
952            obj_lit_data,
953            objs,
954            orig_cnf,
955            block_clause_gen: bcg,
956            opts,
957            stats,
958            lims: Limits::none(),
959            #[cfg(feature = "maxpre")]
960            inpro: None,
961            logger: None,
962            term_flag: Arc::new(AtomicBool::new(false)),
963            #[cfg(feature = "interrupt-oracle")]
964            oracle_interrupter: Arc::new(Mutex::new(Box::new(interrupter))),
965            proof_stuff: Some(ProofStuff {
966                pt_handle,
967                value_map: Vec::default(),
968                obj_bound_constrs: RsHashMap::default(),
969            }),
970            _factory: PhantomData,
971        })
972    }
973}
974
975#[cfg(test)]
976mod tests {
977    use std::{
978        fs::File,
979        io::{BufRead, BufReader},
980        path::Path,
981        process::Command,
982    };
983
984    use pigeons::{Conclusion, ConstraintId, OutputGuarantee};
985    use rustsat::lit;
986
987    use crate::types::Objective;
988
989    fn print_file<P: AsRef<Path>>(path: P) {
990        println!();
991        for line in BufReader::new(File::open(path).expect("could not open file")).lines() {
992            println!("{}", line.unwrap());
993        }
994        println!();
995    }
996
997    fn verify_proof<P1: AsRef<Path>, P2: AsRef<Path>>(instance: P1, proof: P2) {
998        println!("start checking proof");
999        let out = Command::new("veripb")
1000            .arg(instance.as_ref())
1001            .arg(proof.as_ref())
1002            .output()
1003            .expect("failed to run veripb");
1004        print_file(proof);
1005        if out.status.success() {
1006            return;
1007        }
1008        panic!("verification failed: {out:?}")
1009    }
1010
1011    fn new_proof(
1012        num_constraints: usize,
1013        optimization: bool,
1014    ) -> pigeons::Proof<tempfile::NamedTempFile> {
1015        let file = tempfile::NamedTempFile::new().expect("failed to create temporary proof file");
1016        pigeons::Proof::new_with_conclusion(
1017            file,
1018            num_constraints,
1019            optimization,
1020            OutputGuarantee::None,
1021            &Conclusion::<&str>::Unsat(Some(ConstraintId::last(1))),
1022        )
1023        .expect("failed to start proof")
1024    }
1025
1026    #[test]
1027    fn order_format() {
1028        let objectives = [
1029            Objective::Unweighted {
1030                offset: 3,
1031                unit_weight: 2,
1032                lits: vec![lit![0], !lit![1], lit![2], lit![3]],
1033                idx: 0,
1034                lower_bound: 0,
1035                reform_id: None,
1036            },
1037            Objective::Weighted {
1038                offset: 42,
1039                lits: [(lit![4], 4), (lit![2], 2), (lit![42], 42)]
1040                    .into_iter()
1041                    .collect(),
1042                idx: 1,
1043                lower_bound: 0,
1044                reform_id: None,
1045            },
1046            Objective::Constant {
1047                offset: 11,
1048                idx: 2,
1049                lower_bound: 0,
1050                reform_id: None,
1051            },
1052        ];
1053        let order = super::objectives_as_order(&objectives);
1054        let formatted = format!("{order}");
1055        let expected = r#"def_order pareto
1056  vars
1057    left u_x1 u_x43 u_x4 u_x2 u_x5 u_x3
1058    right v_x1 v_x43 v_x4 v_x2 v_x5 v_x3
1059    aux
1060  end
1061  def
1062    -2 u_x1 2 v_x1 -2 ~u_x2 2 ~v_x2 -2 u_x3 2 v_x3 -2 u_x4 2 v_x4 >= 0 ;
1063    -4 u_x5 4 v_x5 -2 u_x3 2 v_x3 -42 u_x43 42 v_x43 >= 0 ;
1064     >= 0 ;
1065  end
1066  transitivity
1067    vars
1068      fresh_right w_x1 w_x43 w_x4 w_x2 w_x5 w_x3
1069    end
1070    proof
1071      proofgoal #1
1072        pol 1 4 + -1 +
1073      qed -1
1074      proofgoal #2
1075        pol 2 5 + -1 +
1076      qed -1
1077      proofgoal #3
1078        pol 3 6 + -1 +
1079      qed -1
1080    qed
1081  end
1082end"#;
1083        debug_assert_eq!(&formatted, expected);
1084    }
1085
1086    #[test]
1087    fn order_check() {
1088        let objectives = [
1089            Objective::Unweighted {
1090                offset: 3,
1091                unit_weight: 2,
1092                lits: vec![lit![0], !lit![1], lit![2], lit![3]],
1093                idx: 0,
1094                lower_bound: 0,
1095                reform_id: None,
1096            },
1097            Objective::Weighted {
1098                offset: 42,
1099                lits: [(lit![4], 4), (lit![2], 2), (lit![42], 42)]
1100                    .into_iter()
1101                    .collect(),
1102                idx: 1,
1103                lower_bound: 0,
1104                reform_id: None,
1105            },
1106            Objective::Constant {
1107                offset: 11,
1108                idx: 2,
1109                lower_bound: 0,
1110                reform_id: None,
1111            },
1112        ];
1113        let order = super::objectives_as_order(&objectives);
1114
1115        let mut proof = new_proof(0, false);
1116        proof.define_order(&order).unwrap();
1117        proof.load_order(order.name(), order.used_vars()).unwrap();
1118
1119        let proof_file = proof
1120            .conclude(
1121                pigeons::OutputGuarantee::None,
1122                &pigeons::Conclusion::<&str>::None,
1123            )
1124            .unwrap();
1125        let manifest = std::env::var("CARGO_MANIFEST_DIR").unwrap();
1126        verify_proof(
1127            format!("{manifest}/../rustsat/data/empty.opb"),
1128            proof_file.path(),
1129        );
1130    }
1131}