1use std::{fs, io};
19
20use cadical_veripb_tracer::CadicalCertCollector;
21use pigeons::{AbsConstraintId, ConstraintId};
22use rustsat::{
23 clause,
24 encodings::{
25 self, atomics,
26 card::{self, Totalizer},
27 pb::{self, GeneralizedTotalizer},
28 Monotone,
29 },
30 instances::ManageVars,
31 solvers::{
32 DefaultInitializer, Initialize, Solve, SolveIncremental, SolveStats, SolverResult,
33 SolverStats,
34 },
35 types::{Assignment, Clause, Lit, Var},
36};
37use scuttle_proc::{oracle_bounds, KernelFunctions};
38
39use crate::{
40 options::{AfterCbOptions, CoreBoostingOptions, EnumOptions},
41 termination::ensure,
42 types::{ParetoFront, VarManager},
43 EncodingStats, ExtendedSolveStats, KernelFunctions, KernelOptions, Limits,
44 MaybeTerminatedError::{self, Done},
45 Phase,
46};
47
48use super::{coreboosting::MergeOllRef, proofs, CoreBoost, Kernel, ObjEncoding, Objective};
49
50#[derive(KernelFunctions)]
61pub struct PMinimal<
62 O,
63 PBE = GeneralizedTotalizer,
64 CE = Totalizer,
65 ProofW = io::BufWriter<fs::File>,
66 OInit = DefaultInitializer,
67 BCG = fn(Assignment) -> Clause,
68> where
69 ProofW: io::Write,
70{
71 kernel: Kernel<O, ProofW, OInit, BCG>,
73 obj_encs: Vec<ObjEncoding<PBE, CE>>,
75 pareto_front: ParetoFront,
77}
78
79impl<'learn, 'term, ProofW, OInit, BCG> super::Solve
80 for PMinimal<
81 rustsat_cadical::CaDiCaL<'term, 'learn>,
82 GeneralizedTotalizer,
83 Totalizer,
84 ProofW,
85 OInit,
86 BCG,
87 >
88where
89 BCG: Fn(Assignment) -> Clause,
90 ProofW: io::Write + 'static,
91{
92 fn solve(&mut self, limits: Limits) -> MaybeTerminatedError {
93 self.kernel.start_solving(limits);
94 self.alg_main()
95 }
96
97 fn all_stats(
98 &self,
99 ) -> (
100 crate::Stats,
101 Option<SolverStats>,
102 Option<Vec<EncodingStats>>,
103 ) {
104 use crate::ExtendedSolveStats;
105 (
106 self.kernel.stats,
107 Some(self.oracle_stats()),
108 Some(self.encoding_stats()),
109 )
110 }
111}
112
113#[oracle_bounds]
114impl<O, PBE, CE, ProofW, OInit, BCG> super::Init for PMinimal<O, PBE, CE, ProofW, OInit, BCG>
115where
116 O: SolveIncremental,
117 ProofW: io::Write,
118 PBE: pb::BoundUpperIncremental + FromIterator<(Lit, usize)> + Monotone,
119 CE: card::BoundUpperIncremental + FromIterator<Lit> + Monotone,
120 OInit: Initialize<O>,
121 BCG: Fn(Assignment) -> Clause,
122{
123 type Oracle = O;
124 type BlockClauseGen = BCG;
125
126 fn new<Cls>(
129 clauses: Cls,
130 objs: Vec<Objective>,
131 var_manager: VarManager,
132 opts: KernelOptions,
133 block_clause_gen: BCG,
134 ) -> anyhow::Result<Self>
135 where
136 Cls: IntoIterator<Item = Clause>,
137 {
138 let kernel = Kernel::new(clauses, objs, var_manager, block_clause_gen, opts)?;
139 Ok(Self::init(kernel))
140 }
141}
142
143impl<'term, 'learn, PBE, CE, ProofW, OInit, BCG> super::InitCert
144 for PMinimal<rustsat_cadical::CaDiCaL<'term, 'learn>, PBE, CE, ProofW, OInit, BCG>
145where
146 PBE: pb::BoundUpperIncremental + FromIterator<(Lit, usize)> + Monotone,
147 CE: card::BoundUpperIncremental + FromIterator<Lit> + Monotone,
148 OInit: Initialize<rustsat_cadical::CaDiCaL<'term, 'learn>>,
149 ProofW: io::Write + 'static,
150 BCG: Fn(Assignment) -> Clause,
151{
152 type ProofWriter = ProofW;
153
154 fn new_cert<Cls>(
157 clauses: Cls,
158 objs: Vec<Objective>,
159 var_manager: VarManager,
160 opts: KernelOptions,
161 proof: pigeons::Proof<Self::ProofWriter>,
162 block_clause_gen: BCG,
163 ) -> anyhow::Result<Self>
164 where
165 Cls: IntoIterator<Item = (Clause, pigeons::AbsConstraintId)>,
166 {
167 let kernel = Kernel::new_cert(clauses, objs, var_manager, block_clause_gen, proof, opts)?;
168 Ok(Self::init(kernel))
169 }
170}
171
172impl<O, PBE, CE, ProofW, OInit, BCG> ExtendedSolveStats for PMinimal<O, PBE, CE, ProofW, OInit, BCG>
173where
174 O: SolveStats,
175 ProofW: io::Write,
176 PBE: encodings::EncodeStats,
177 CE: encodings::EncodeStats,
178{
179 fn oracle_stats(&self) -> SolverStats {
180 self.kernel.oracle.stats()
181 }
182
183 fn encoding_stats(&self) -> Vec<EncodingStats> {
184 self.kernel
185 .objs
186 .iter()
187 .zip(self.obj_encs.iter())
188 .map(|(obj, enc)| {
189 let mut s = EncodingStats {
190 offset: obj.offset(),
191 ..Default::default()
192 };
193 if let Objective::Unweighted { unit_weight, .. } = obj {
194 s.unit_weight = Some(*unit_weight);
195 };
196 match enc {
197 ObjEncoding::Weighted(enc, _) => {
198 s.n_vars = enc.n_vars();
199 s.n_clauses = enc.n_clauses()
200 }
201 ObjEncoding::Unweighted(enc, _) => {
202 s.n_vars = enc.n_vars();
203 s.n_clauses = enc.n_clauses()
204 }
205 ObjEncoding::Constant => (),
206 };
207 s
208 })
209 .collect()
210 }
211}
212
213impl<O, PBE, CE, ProofW, OInit, BCG> PMinimal<O, PBE, CE, ProofW, OInit, BCG>
214where
215 ProofW: io::Write,
216 PBE: pb::BoundUpperIncremental + FromIterator<(Lit, usize)> + Monotone,
217 CE: card::BoundUpperIncremental + FromIterator<Lit> + Monotone,
218{
219 fn init(mut kernel: Kernel<O, ProofW, OInit, BCG>) -> Self {
221 let obj_encs = kernel
223 .objs
224 .iter()
225 .map(|obj| match obj {
226 Objective::Weighted { lits, .. } => ObjEncoding::new_weighted(
227 lits.iter().map(|(&l, &w)| (l, w)),
228 kernel.opts.reserve_enc_vars,
229 &mut kernel.var_manager,
230 ),
231 Objective::Unweighted { lits, .. } => ObjEncoding::new_unweighted(
232 lits.iter().copied(),
233 kernel.opts.reserve_enc_vars,
234 &mut kernel.var_manager,
235 ),
236 Objective::Constant { .. } => ObjEncoding::Constant,
237 })
238 .collect();
239 Self {
240 kernel,
241 obj_encs,
242 pareto_front: Default::default(),
243 }
244 }
245}
246
247impl<'learn, 'term, ProofW, OInit, BCG>
248 PMinimal<
249 rustsat_cadical::CaDiCaL<'learn, 'term>,
250 GeneralizedTotalizer,
251 Totalizer,
252 ProofW,
253 OInit,
254 BCG,
255 >
256where
257 BCG: Fn(Assignment) -> Clause,
258 ProofW: io::Write + 'static,
259{
260 fn alg_main(&mut self) -> MaybeTerminatedError {
262 debug_assert_eq!(self.obj_encs.len(), self.kernel.stats.n_objs);
263 self.kernel.log_routine_start("p-minimal")?;
264 loop {
265 let res = self.kernel.solve()?;
267 if SolverResult::Unsat == res {
268 self.kernel.log_routine_end()?;
269 return Done(());
270 }
271 self.kernel.check_termination()?;
272
273 let (costs, solution) = self.kernel.get_solution_and_internal_costs(
275 self.kernel
276 .opts
277 .heuristic_improvements
278 .solution_tightening
279 .wanted(Phase::OuterLoop),
280 )?;
281 self.kernel.log_candidate(&costs, Phase::OuterLoop)?;
282 self.kernel.check_termination()?;
283 self.kernel.phase_solution(solution.clone())?;
284 let (costs, solution, block_switch) =
285 self.kernel
286 .p_minimization(costs, solution, &[], &mut self.obj_encs)?;
287
288 let assumps: Vec<_> = self
289 .kernel
290 .enforce_dominating(&costs, &mut self.obj_encs)?
291 .collect();
292 self.kernel.yield_solutions(
293 costs.clone(),
294 &assumps,
295 solution.clone(),
296 &mut self.pareto_front,
297 )?;
298
299 if let Some((block_lit, ids)) = block_switch {
301 if let Some(proof_stuff) = &mut self.kernel.proof_stuff {
302 use pigeons::{ConstraintId, Derivation, ProofGoal, ProofGoalId};
303 use rustsat::encodings::cert::CollectClauses;
304
305 let (reified_cut, reified_assump_ids) = ids.unwrap();
306 let id = proofs::certify_pmin_cut(
307 &self.obj_encs,
308 &self.kernel.objs,
309 &costs,
310 &solution,
311 self.kernel.var_manager.max_enc_var(),
312 proof_stuff,
313 &mut self.kernel.oracle,
314 )?;
315 let proof = self
316 .kernel
317 .oracle
318 .proof_tracer_mut(&proof_stuff.pt_handle)
319 .proof_mut();
320 let hints = [ConstraintId::last(2), ConstraintId::last(1), id.into()]
321 .into_iter()
322 .chain(reified_assump_ids.iter().map(|id| ConstraintId::from(*id)));
323 let unit = clause![block_lit];
324 let unit_id = proof.redundant(
325 &unit,
326 [],
327 [ProofGoal::new(
328 ProofGoalId::from(ConstraintId::from(reified_cut)),
329 [Derivation::Rup(clause![], hints.collect())],
330 )
331 .into()],
332 )?;
333 cadical_veripb_tracer::CadicalCertCollector::new(
334 &mut self.kernel.oracle,
335 &proof_stuff.pt_handle,
336 )
337 .add_cert_clause(unit, unit_id)?;
338 } else {
339 self.kernel.oracle.add_unit(block_lit)?;
340 }
341 }
342 }
343 }
344}
345
346impl<'learn, 'term, PBE, CE, ProofW, OInit, BCG> CoreBoost
347 for PMinimal<rustsat_cadical::CaDiCaL<'learn, 'term>, PBE, CE, ProofW, OInit, BCG>
348where
349 ProofW: io::Write + 'static,
350 (PBE, CE): MergeOllRef<PBE = PBE, CE = CE>,
351 OInit: Initialize<rustsat_cadical::CaDiCaL<'learn, 'term>>,
352{
353 fn core_boost(&mut self, opts: CoreBoostingOptions) -> MaybeTerminatedError<bool> {
354 ensure!(
355 self.kernel.stats.n_solve_calls == 0,
356 "cannot perform core boosting after solve has been called"
357 );
358 let Some(cb_res) = self.kernel.core_boost()? else {
359 return Done(false);
360 };
361 self.kernel.check_termination()?;
362 let reset_dbs = match &opts.after {
363 AfterCbOptions::Nothing => false,
364 AfterCbOptions::Reset => {
365 self.kernel.reset_oracle(true)?;
366 self.kernel.check_termination()?;
367 true
368 }
369 #[cfg(feature = "maxpre")]
370 AfterCbOptions::Inpro(techs) => {
371 self.obj_encs = self.kernel.inprocess(techs, cb_res)?;
372 self.kernel.check_termination()?;
373 return Done(true);
374 }
375 };
376 self.kernel.log_routine_start("merge encodings")?;
377 for (oidx, (reform, mut tot_db)) in cb_res.into_iter().enumerate() {
378 if reset_dbs {
379 debug_assert!(self.kernel.proof_stuff.is_none());
380 tot_db.reset_vars();
381 }
382 if !matches!(self.kernel.objs[oidx], Objective::Constant { .. }) {
383 if let Some(proofs::ProofStuff { pt_handle, .. }) = &self.kernel.proof_stuff {
384 if !reform.reformulations.is_empty() {
385 let proof = self.kernel.oracle.proof_tracer_mut(pt_handle).proof_mut();
387 #[cfg(feature = "verbose-proofs")]
388 proof.comment(&format_args!(
389 "deleting remaining reformulation constraints from OLL of objective {oidx}"
390 ))?;
391 proof.delete_ids::<Var, Clause, _, _>(
392 reform
393 .reformulations
394 .values()
395 .map(|re| ConstraintId::from(re.proof_id.unwrap())),
396 None,
397 )?;
398 }
399 }
400
401 self.obj_encs[oidx] = <(PBE, CE)>::merge(reform, tot_db, opts.rebase);
402 }
403 self.kernel.check_termination()?;
404 }
405 self.kernel.log_routine_end()?;
406 Done(true)
407 }
408}
409
410impl<'term, 'learn, ProofW, OInit, BCG>
411 Kernel<rustsat_cadical::CaDiCaL<'term, 'learn>, ProofW, OInit, BCG>
412where
413 ProofW: io::Write + 'static,
414{
415 pub fn p_minimization(
417 &mut self,
418 mut costs: Vec<usize>,
419 mut solution: Assignment,
420 base_assumps: &[Lit],
421 obj_encs: &mut [ObjEncoding<GeneralizedTotalizer, Totalizer>],
422 ) -> MaybeTerminatedError<(
423 Vec<usize>,
424 Assignment,
425 Option<(Lit, Option<(AbsConstraintId, Vec<AbsConstraintId>)>)>,
426 )> {
427 debug_assert_eq!(costs.len(), self.stats.n_objs);
428 self.log_routine_start("p minimization")?;
429 let mut block_switch: Option<(Lit, Option<(AbsConstraintId, Vec<AbsConstraintId>)>)> = None;
430 let mut assumps = Vec::from(base_assumps);
431 #[cfg(feature = "coarse-convergence")]
432 let mut coarse = true;
433 loop {
434 #[cfg(feature = "coarse-convergence")]
435 let bound_costs: Vec<_> = costs
436 .iter()
437 .enumerate()
438 .map(|(_oidx, &c)| {
439 if coarse {
440 return obj_encs[_oidx].coarse_ub(c);
441 }
442 c
443 })
444 .collect();
445 assumps.drain(base_assumps.len()..);
446 if self.opts.enumeration == EnumOptions::NoEnum {
448 let (block_clause, reification_ids) =
450 self.dominated_block_clause(&costs, obj_encs)?;
451 if let Some(proof_stuff) = &mut self.proof_stuff {
452 use rustsat::encodings::cert::CollectClauses;
453
454 let cut_id = proofs::certify_pmin_cut(
456 obj_encs,
457 &self.objs,
458 &costs,
459 &solution,
460 self.var_manager.max_enc_var(),
461 proof_stuff,
462 &mut self.oracle,
463 )?;
464 let proof = self
465 .oracle
466 .proof_tracer_mut(&proof_stuff.pt_handle)
467 .proof_mut();
468 let clause_id = proof.reverse_unit_prop(
471 &block_clause,
472 reification_ids
473 .into_iter()
474 .chain([cut_id])
475 .map(ConstraintId::from),
476 )?;
477 let mut collector = cadical_veripb_tracer::CadicalCertCollector::new(
478 &mut self.oracle,
479 &proof_stuff.pt_handle,
480 );
481 collector.add_cert_clause(block_clause, clause_id)?;
482 } else {
483 self.oracle.add_clause(block_clause)?;
484 }
485 } else {
486 if let Some((block_lit, ids)) = block_switch {
488 if let Some(proof_stuff) = &mut self.proof_stuff {
489 use pigeons::{ConstraintId, Derivation, ProofGoal, ProofGoalId};
490 use rustsat::encodings::cert::CollectClauses;
491
492 let (reified_cut, reified_assump_ids) = ids.unwrap();
493 let id = proofs::certify_pmin_cut(
494 obj_encs,
495 &self.objs,
496 &costs,
497 &solution,
498 self.var_manager.max_enc_var(),
499 proof_stuff,
500 &mut self.oracle,
501 )?;
502 let proof = self
503 .oracle
504 .proof_tracer_mut(&proof_stuff.pt_handle)
505 .proof_mut();
506 let hints = [ConstraintId::last(2), ConstraintId::last(1), id.into()]
507 .into_iter()
508 .chain(reified_assump_ids.into_iter().map(ConstraintId::from));
509 let unit = clause![block_lit];
510 let unit_id = proof.redundant(
511 &unit,
512 [],
513 [ProofGoal::new(
514 ProofGoalId::from(ConstraintId::from(reified_cut)),
515 [Derivation::Rup(clause![], hints.collect())],
516 )
517 .into()],
518 )?;
519 cadical_veripb_tracer::CadicalCertCollector::new(
520 &mut self.oracle,
521 &proof_stuff.pt_handle,
522 )
523 .add_cert_clause(unit, unit_id)?;
524 } else {
525 self.oracle.add_unit(block_lit)?;
526 }
527 }
528 let block_info = self.tmp_block_dominated(&costs, obj_encs)?;
530 let blit = block_info.0;
531 block_switch = Some(block_info);
532 assumps.push(blit);
533 }
534 #[cfg(not(feature = "coarse-convergence"))]
536 assumps.extend(self.enforce_dominating(&costs, obj_encs)?);
537 #[cfg(feature = "coarse-convergence")]
538 assumps.extend(self.enforce_dominating_cert(&bound_costs, obj_encs)?);
539
540 let res = self.solve_assumps(&assumps)?;
542 if res == SolverResult::Unsat {
543 #[cfg(feature = "coarse-convergence")]
544 if bound_costs != costs {
545 coarse = false;
547 continue;
548 }
549 self.log_routine_end()?;
550 return Done((costs, solution, block_switch));
552 }
553 self.check_termination()?;
554
555 (costs, solution) = self.get_solution_and_internal_costs(
556 self.opts
557 .heuristic_improvements
558 .solution_tightening
559 .wanted(Phase::Minimization),
560 )?;
561 self.log_candidate(&costs, Phase::Minimization)?;
562 self.check_termination()?;
563 self.phase_solution(solution.clone())?;
564 }
565 }
566
567 pub fn enforce_dominating<'a>(
570 &'a mut self,
571 costs: &'a [usize],
572 obj_encs: &'a mut [ObjEncoding<GeneralizedTotalizer, Totalizer>],
573 ) -> anyhow::Result<impl Iterator<Item = Lit> + 'a> {
574 debug_assert_eq!(costs.len(), self.stats.n_objs);
575 if let Some(proofs::ProofStuff { pt_handle, .. }) = &self.proof_stuff {
576 let proof: *mut _ = self.oracle.proof_tracer_mut(pt_handle).proof_mut();
577 #[cfg(feature = "verbose-proofs")]
578 {
579 use itertools::Itertools;
580 let proof = unsafe { &mut *proof };
581 proof.comment(&format_args!(
582 "building assumptions to dominate [{}]",
583 costs.iter().format(", ")
584 ))?;
585 }
586 let mut collector = CadicalCertCollector::new(&mut self.oracle, pt_handle);
587 for (idx, &cst) in costs.iter().enumerate() {
588 let enc = &mut obj_encs[idx];
589 enc.encode_ub_change_cert(
590 cst..cst + 1,
591 &mut collector,
592 &mut self.var_manager,
593 unsafe { &mut *proof },
594 )?;
595 }
596 } else {
597 for (idx, &cst) in costs.iter().enumerate() {
598 let enc = &mut obj_encs[idx];
599 enc.encode_ub_change(cst..cst + 1, &mut self.oracle, &mut self.var_manager)?;
600 }
601 }
602 Ok(costs.iter().enumerate().flat_map(|(idx, &cst)| {
603 let enc = &mut obj_encs[idx];
604 enc.enforce_ub(cst).unwrap().into_iter()
605 }))
606 }
607
608 pub fn dominated_block_clause(
611 &mut self,
612 costs: &[usize],
613 obj_encs: &mut [ObjEncoding<GeneralizedTotalizer, Totalizer>],
614 ) -> anyhow::Result<(Clause, Vec<AbsConstraintId>)> {
615 debug_assert_eq!(costs.len(), obj_encs.len());
616 let mut reification_ids = Vec::new();
617 let mut clause = Clause::default();
618 for (idx, &cst) in costs.iter().enumerate() {
619 if cst <= obj_encs[idx].offset() {
621 continue;
622 }
623 let enc = &mut obj_encs[idx];
624 if matches!(enc, ObjEncoding::Constant) {
625 continue;
626 }
627 self.extend_encoding(enc, cst - 1..cst)?;
629 let assumps = enc.enforce_ub(cst - 1)?;
630 if assumps.len() == 1 {
631 clause.add(assumps[0]);
632 } else {
633 debug_assert!(!assumps.is_empty());
634 let and_lit = self.var_manager.new_var().pos_lit();
635 if let Some(proof_stuff) = &mut self.proof_stuff {
636 let only_if_def = proofs::certify_assump_reification(
637 &mut self.oracle,
638 proof_stuff,
639 &self.objs[idx],
640 enc,
641 cst,
642 and_lit,
643 &assumps,
644 )?;
645 reification_ids.push(only_if_def);
646 } else {
647 for cl in atomics::lit_impl_cube(and_lit, &assumps) {
648 self.oracle.add_clause(cl)?;
649 }
650 }
651 clause.add(and_lit)
652 }
653 }
654 Ok((clause, reification_ids))
655 }
656
657 pub fn tmp_block_dominated(
661 &mut self,
662 costs: &[usize],
663 obj_encs: &mut [ObjEncoding<GeneralizedTotalizer, Totalizer>],
664 ) -> anyhow::Result<(Lit, Option<(AbsConstraintId, Vec<AbsConstraintId>)>)> {
665 use pigeons::VarLike;
666
667 debug_assert_eq!(costs.len(), self.stats.n_objs);
668 let (mut clause, reification_ids) = self.dominated_block_clause(costs, obj_encs)?;
669 let block_lit = self.var_manager.new_var().pos_lit();
670 clause.add(block_lit);
671 self.oracle.add_clause_ref(&clause).unwrap();
672 if let Some(proofs::ProofStuff { pt_handle, .. }) = &self.proof_stuff {
673 let proof = self.oracle.proof_tracer_mut(pt_handle).proof_mut();
674 let id = proof.redundant(&clause, [block_lit.var().substitute_fixed(true)], None)?;
675 Ok((!block_lit, Some((id, reification_ids))))
676 } else {
677 Ok((!block_lit, None))
678 }
679 }
680}