1use 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
34pub trait InitCert: super::Init {
36 type ProofWriter: io::Write;
37
38 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 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 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 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
111pub struct ProofStuff<ProofW: io::Write> {
113 pub pt_handle: rustsat_cadical::ProofTracerHandle<CadicalTracer<ProofW>>,
115 pub value_map: Vec<(Axiom<AnyVar>, Value)>,
117 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 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 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 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 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 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 let fixed_witness: Vec<Axiom<AnyVar>> = {
260 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 let mut solver_vars = Vec::new();
273 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 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 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 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 None
381 }
382 }
383 }),
384 )?;
385
386 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 let cut_id =
402 proof.reverse_unit_prop(&cut, [map_dom, exclude].into_iter().map(ConstraintId::from))?;
403 proof.move_ids_to_core([ConstraintId::from(cut_id)])?;
405
406 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
423pub 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 let mut assumps = assumps.iter();
456 let a = *assumps.next().unwrap();
457 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 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 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 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 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 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 let id = if a.var() < olit.var() {
534 proof.reverse_unit_prop(
536 &clause,
537 [if_def, def_1].into_iter().map(ConstraintId::from),
538 )?
539 } else {
540 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 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 let (first_olit, first_sems) = encoding.output_proof_details(cost);
600 if core.len() == 1 {
601 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 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 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 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 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 #[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 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 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 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 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 #[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 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}