scuttle_core/algs/
lowerbounding.rs

1//! # Multi-Objective Lower-Bounding Search
2//!
3//! Algorithm proposed in \[1\].
4//!
5//! ## References
6//!
7//! - \[1\] Joao Cortes and Ines Lynce and Vasco M. Maquinho: _New Core-Guided
8//!     and Hitting Set Algorithms for Multi-Objective Combinatorial Optimization_,
9//!     TACAS 2023.
10
11use std::{fs, io};
12
13use cadical_veripb_tracer::CadicalCertCollector;
14use pigeons::ConstraintId;
15use rustsat::{
16    clause,
17    encodings::{
18        self,
19        card::{self, Totalizer},
20        pb::{self, GeneralizedTotalizer},
21        Monotone,
22    },
23    solvers::{
24        DefaultInitializer, Initialize, Solve, SolveIncremental, SolveStats, SolverResult,
25        SolverStats,
26    },
27    types::{Assignment, Clause, Lit, Var},
28};
29use scuttle_proc::KernelFunctions;
30
31use crate::{
32    options::{AfterCbOptions, CoreBoostingOptions},
33    termination::ensure,
34    types::{NonDomPoint, ParetoFront, VarManager},
35    EncodingStats, ExtendedSolveStats, KernelFunctions, KernelOptions, Limits,
36    MaybeTerminatedError::{self, Done},
37    Phase,
38};
39
40use super::{coreboosting::MergeOllRef, proofs, CoreBoost, Kernel, ObjEncoding, Objective};
41
42/// The lower-bounding algorithm type
43///
44/// # Generics
45///
46/// - `O`: the SAT solver oracle
47/// - `PBE`: pseudo-Boolean objective encoding
48/// - `CE`: cardinality objective encoding
49/// - `ProofW`: the proof writer
50/// - `OInit`: the oracle initializer
51/// - `BCG`: the blocking clause generator
52#[derive(KernelFunctions)]
53pub struct LowerBounding<
54    O,
55    PBE = GeneralizedTotalizer,
56    CE = Totalizer,
57    ProofW = io::BufWriter<fs::File>,
58    OInit = DefaultInitializer,
59    BCG = fn(Assignment) -> Clause,
60> where
61    ProofW: io::Write,
62{
63    /// The solver kernel
64    kernel: Kernel<O, ProofW, OInit, BCG>,
65    /// A cardinality or pseudo-boolean encoding for each objective
66    obj_encs: Vec<ObjEncoding<PBE, CE>>,
67    /// The current fence
68    fence: Fence,
69    /// The Pareto front discovered so far
70    pareto_front: ParetoFront,
71}
72
73impl<'learn, 'term, ProofW, OInit, BCG> super::Solve
74    for LowerBounding<
75        rustsat_cadical::CaDiCaL<'term, 'learn>,
76        GeneralizedTotalizer,
77        Totalizer,
78        ProofW,
79        OInit,
80        BCG,
81    >
82where
83    BCG: Fn(Assignment) -> Clause,
84    ProofW: io::Write + 'static,
85{
86    fn solve(&mut self, limits: Limits) -> MaybeTerminatedError {
87        self.kernel.start_solving(limits);
88        self.alg_main()
89    }
90
91    fn all_stats(
92        &self,
93    ) -> (
94        crate::Stats,
95        Option<SolverStats>,
96        Option<Vec<EncodingStats>>,
97    ) {
98        use crate::ExtendedSolveStats;
99        (
100            self.kernel.stats,
101            Some(self.oracle_stats()),
102            Some(self.encoding_stats()),
103        )
104    }
105}
106
107impl<'learn, 'term, ProofW, OInit, BCG> super::Init
108    for LowerBounding<
109        rustsat_cadical::CaDiCaL<'learn, 'term>,
110        GeneralizedTotalizer,
111        Totalizer,
112        ProofW,
113        OInit,
114        BCG,
115    >
116where
117    ProofW: io::Write + 'static,
118    OInit: Initialize<rustsat_cadical::CaDiCaL<'learn, 'term>>,
119    BCG: Fn(Assignment) -> Clause,
120{
121    type Oracle = rustsat_cadical::CaDiCaL<'learn, 'term>;
122    type BlockClauseGen = BCG;
123
124    /// Initializes a default solver with a configured oracle and options. The
125    /// oracle should _not_ have any clauses loaded yet.
126    fn new<Cls>(
127        clauses: Cls,
128        objs: Vec<Objective>,
129        var_manager: VarManager,
130        opts: KernelOptions,
131        block_clause_gen: BCG,
132    ) -> anyhow::Result<Self>
133    where
134        Cls: IntoIterator<Item = Clause>,
135    {
136        let kernel = Kernel::new(clauses, objs, var_manager, block_clause_gen, opts)?;
137        Self::init(kernel)
138    }
139}
140
141impl<'term, 'learn, ProofW, OInit, BCG> super::InitCert
142    for LowerBounding<
143        rustsat_cadical::CaDiCaL<'term, 'learn>,
144        GeneralizedTotalizer,
145        Totalizer,
146        ProofW,
147        OInit,
148        BCG,
149    >
150where
151    OInit: Initialize<rustsat_cadical::CaDiCaL<'term, 'learn>>,
152    ProofW: io::Write + 'static,
153    BCG: Fn(Assignment) -> Clause,
154{
155    type ProofWriter = ProofW;
156
157    /// Initializes a default solver with a configured oracle and options. The
158    /// oracle should _not_ have any clauses loaded yet.
159    fn new_cert<Cls>(
160        clauses: Cls,
161        objs: Vec<Objective>,
162        var_manager: VarManager,
163        opts: KernelOptions,
164        proof: pigeons::Proof<Self::ProofWriter>,
165        block_clause_gen: BCG,
166    ) -> anyhow::Result<Self>
167    where
168        Cls: IntoIterator<Item = (Clause, pigeons::AbsConstraintId)>,
169    {
170        let kernel = Kernel::new_cert(clauses, objs, var_manager, block_clause_gen, proof, opts)?;
171        Self::init(kernel)
172    }
173}
174
175impl<O, PBE, CE, ProofW, OInit, BCG> ExtendedSolveStats
176    for LowerBounding<O, PBE, CE, ProofW, OInit, BCG>
177where
178    O: SolveStats,
179    ProofW: io::Write,
180    PBE: encodings::EncodeStats,
181    CE: encodings::EncodeStats,
182{
183    fn oracle_stats(&self) -> SolverStats {
184        self.kernel.oracle.stats()
185    }
186
187    fn encoding_stats(&self) -> Vec<EncodingStats> {
188        self.kernel
189            .objs
190            .iter()
191            .zip(self.obj_encs.iter())
192            .map(|(obj, enc)| {
193                let mut s = EncodingStats {
194                    offset: obj.offset(),
195                    ..Default::default()
196                };
197                if let Objective::Unweighted { unit_weight, .. } = obj {
198                    s.unit_weight = Some(*unit_weight);
199                };
200                match enc {
201                    ObjEncoding::Weighted(enc, _) => {
202                        s.n_vars = enc.n_vars();
203                        s.n_clauses = enc.n_clauses()
204                    }
205                    ObjEncoding::Unweighted(enc, _) => {
206                        s.n_vars = enc.n_vars();
207                        s.n_clauses = enc.n_clauses()
208                    }
209                    ObjEncoding::Constant => (),
210                };
211                s
212            })
213            .collect()
214    }
215}
216
217impl<O, PBE, CE, ProofW, OInit, BCG> LowerBounding<O, PBE, CE, ProofW, OInit, BCG>
218where
219    ProofW: io::Write + 'static,
220    PBE: pb::BoundUpperIncremental + FromIterator<(Lit, usize)> + Monotone,
221    CE: card::BoundUpperIncremental + FromIterator<Lit> + Monotone,
222{
223    /// Initializes the solver
224    fn init(mut kernel: Kernel<O, ProofW, OInit, BCG>) -> anyhow::Result<Self> {
225        // Initialize objective encodings
226        let fence_data = Vec::with_capacity(kernel.objs.len());
227        let obj_encs = kernel
228            .objs
229            .iter()
230            .map(|obj| match obj {
231                Objective::Weighted { lits, .. } => ObjEncoding::<PBE, CE>::new_weighted(
232                    lits.iter().map(|(&l, &w)| (l, w)),
233                    kernel.opts.reserve_enc_vars,
234                    &mut kernel.var_manager,
235                ),
236                Objective::Unweighted { lits, .. } => ObjEncoding::<PBE, CE>::new_unweighted(
237                    lits.iter().copied(),
238                    kernel.opts.reserve_enc_vars,
239                    &mut kernel.var_manager,
240                ),
241                Objective::Constant { .. } => ObjEncoding::Constant,
242            })
243            .collect();
244        Ok(Self {
245            kernel,
246            obj_encs,
247            fence: Fence { data: fence_data },
248            pareto_front: Default::default(),
249        })
250    }
251}
252
253impl<'learn, 'term, ProofW, OInit, BCG>
254    LowerBounding<
255        rustsat_cadical::CaDiCaL<'learn, 'term>,
256        GeneralizedTotalizer,
257        Totalizer,
258        ProofW,
259        OInit,
260        BCG,
261    >
262where
263    ProofW: io::Write + 'static,
264    BCG: Fn(Assignment) -> Clause,
265{
266    /// The solving algorithm main routine.
267    fn alg_main(&mut self) -> MaybeTerminatedError {
268        debug_assert_eq!(self.obj_encs.len(), self.kernel.stats.n_objs);
269        self.kernel.log_routine_start("lower-bounding")?;
270        // Initialize fence here if not yet done
271        if self.fence.data.is_empty() {
272            for enc in self.obj_encs.iter_mut() {
273                if let Some(proofs::ProofStuff { pt_handle, .. }) = &self.kernel.proof_stuff {
274                    let proof: *mut _ = self.kernel.oracle.proof_tracer_mut(pt_handle).proof_mut();
275                    let mut collector =
276                        CadicalCertCollector::new(&mut self.kernel.oracle, pt_handle);
277                    enc.encode_ub_change_cert(
278                        enc.offset()..enc.offset() + 1,
279                        &mut collector,
280                        &mut self.kernel.var_manager,
281                        unsafe { &mut *proof },
282                    )?;
283                } else {
284                    enc.encode_ub_change(
285                        enc.offset()..enc.offset() + 1,
286                        &mut self.kernel.oracle,
287                        &mut self.kernel.var_manager,
288                    )?;
289                }
290                let assumps = enc.enforce_ub(enc.offset()).unwrap();
291                self.fence.data.push((enc.offset(), assumps));
292            }
293        }
294        loop {
295            let assumps: Vec<_> = self.fence.assumps().collect();
296            let res = self.kernel.solve_assumps(&assumps)?;
297            match res {
298                SolverResult::Sat => self.kernel.harvest(
299                    &self.fence,
300                    &mut self.obj_encs,
301                    &[],
302                    &mut self.pareto_front,
303                )?,
304                SolverResult::Unsat => {
305                    let core = self.kernel.oracle.core()?;
306                    if core.is_empty() {
307                        self.kernel.log_routine_end()?;
308                        return Done(());
309                    }
310                    #[cfg(debug_assertions)]
311                    let old_fence = self.fence.bounds();
312                    self.kernel
313                        .update_fence(&mut self.fence, core, &mut self.obj_encs)?;
314                    #[cfg(debug_assertions)]
315                    {
316                        let new_fence = self.fence.bounds();
317                        let mut increased = false;
318                        for idx in 0..old_fence.len() {
319                            debug_assert!(old_fence[idx] <= new_fence[idx]);
320                            if old_fence[idx] < new_fence[idx] {
321                                increased = true;
322                            }
323                        }
324                        if !increased {
325                            panic!("fence has not increased");
326                        }
327                    }
328                }
329                SolverResult::Interrupted => panic!("should have errored before"),
330            }
331        }
332    }
333}
334
335impl<'learn, 'term, PBE, CE, ProofW, OInit, BCG> CoreBoost
336    for LowerBounding<rustsat_cadical::CaDiCaL<'learn, 'term>, PBE, CE, ProofW, OInit, BCG>
337where
338    ProofW: io::Write + 'static,
339    (PBE, CE): MergeOllRef<PBE = PBE, CE = CE>,
340    OInit: Initialize<rustsat_cadical::CaDiCaL<'learn, 'term>>,
341{
342    fn core_boost(&mut self, opts: CoreBoostingOptions) -> MaybeTerminatedError<bool> {
343        ensure!(
344            self.kernel.stats.n_solve_calls == 0,
345            "cannot perform core boosting after solve has been called"
346        );
347        let Some(cb_res) = self.kernel.core_boost()? else {
348            return Done(false);
349        };
350        self.kernel.check_termination()?;
351        let reset_dbs = match &opts.after {
352            AfterCbOptions::Nothing => false,
353            AfterCbOptions::Reset => {
354                self.kernel.reset_oracle(true)?;
355                self.kernel.check_termination()?;
356                true
357            }
358            #[cfg(feature = "maxpre")]
359            AfterCbOptions::Inpro(techs) => {
360                self.obj_encs = self.kernel.inprocess(techs, cb_res)?;
361                self.kernel.check_termination()?;
362                return Done(true);
363            }
364        };
365        self.kernel.log_routine_start("merge encodings")?;
366        for (oidx, (reform, mut tot_db)) in cb_res.into_iter().enumerate() {
367            if reset_dbs {
368                debug_assert!(self.kernel.proof_stuff.is_none());
369                tot_db.reset_vars();
370            }
371            if !matches!(self.kernel.objs[oidx], Objective::Constant { .. }) {
372                if let Some(proofs::ProofStuff { pt_handle, .. }) = &self.kernel.proof_stuff {
373                    if !reform.reformulations.is_empty() {
374                        // delete remaining reformulation constraints from proof
375                        let proof = self.kernel.oracle.proof_tracer_mut(pt_handle).proof_mut();
376                        #[cfg(feature = "verbose-proofs")]
377                        proof.comment(&format_args!(
378                            "deleting remaining reformulation constraints from OLL of objective {oidx}"
379                        ))?;
380                        proof.delete_ids::<Var, Clause, _, _>(
381                            reform
382                                .reformulations
383                                .values()
384                                .map(|re| ConstraintId::from(re.proof_id.unwrap())),
385                            None,
386                        )?;
387                    }
388                }
389
390                self.obj_encs[oidx] = <(PBE, CE)>::merge(reform, tot_db, opts.rebase);
391            }
392            self.kernel.check_termination()?;
393        }
394        self.kernel.log_routine_end()?;
395        Done(true)
396    }
397}
398
399/// Data related to the current fence
400pub(crate) struct Fence {
401    /// The current bounds and enforcing literals
402    pub data: Vec<(usize, Vec<Lit>)>,
403}
404
405impl Fence {
406    pub fn assumps(&self) -> impl Iterator<Item = Lit> + '_ {
407        self.data
408            .iter()
409            .flat_map(|(_, assumps)| assumps.iter().copied())
410    }
411
412    pub fn bounds(&self) -> Vec<usize> {
413        self.data.iter().map(|&(b, _)| b).collect()
414    }
415}
416
417impl<'learn, 'term, ProofW, OInit, BCG>
418    Kernel<rustsat_cadical::CaDiCaL<'learn, 'term>, ProofW, OInit, BCG>
419where
420    ProofW: io::Write + 'static,
421{
422    pub fn update_fence(
423        &mut self,
424        fence: &mut Fence,
425        core: Vec<Lit>,
426        obj_encs: &mut [ObjEncoding<GeneralizedTotalizer, Totalizer>],
427    ) -> MaybeTerminatedError {
428        let mut found = vec![false; fence.data.len()];
429        'core: for clit in core {
430            for (obj_idx, (bound, assumps)) in fence.data.iter_mut().enumerate() {
431                if found[obj_idx] {
432                    // the bound for this objective has already been increased
433                    continue;
434                }
435                let mut matches = false;
436                for alit in assumps.iter() {
437                    if !*alit == clit {
438                        matches = true;
439                        break;
440                    }
441                }
442                if matches {
443                    found[obj_idx] = true;
444                    // update bound
445                    let enc = &mut obj_encs[obj_idx];
446                    *bound = enc.next_higher(*bound);
447                    self.extend_encoding(enc, *bound..*bound + 1)?;
448                    *assumps = enc.enforce_ub(*bound).unwrap();
449                    continue 'core;
450                }
451            }
452        }
453        if let Some(logger) = &mut self.logger {
454            logger.log_fence(&fence.bounds())?
455        }
456        Done(())
457    }
458}
459
460impl<'term, 'learn, ProofW, OInit, BCG>
461    Kernel<rustsat_cadical::CaDiCaL<'term, 'learn>, ProofW, OInit, BCG>
462where
463    ProofW: io::Write + 'static,
464    BCG: Fn(Assignment) -> Clause,
465{
466    /// Runs the P-Minimal algorithm within the fence to harvest solutions
467    pub fn harvest<Col>(
468        &mut self,
469        fence: &Fence,
470        obj_encs: &mut [ObjEncoding<GeneralizedTotalizer, Totalizer>],
471        base_assumps: &[Lit],
472        collector: &mut Col,
473    ) -> MaybeTerminatedError
474    where
475        Col: Extend<NonDomPoint>,
476    {
477        debug_assert_eq!(obj_encs.len(), self.stats.n_objs);
478        self.log_routine_start("harvest")?;
479        let mut assumps = Vec::from(base_assumps);
480        loop {
481            assumps.drain(base_assumps.len()..);
482            // Find minimization starting point
483            assumps.extend(fence.assumps());
484            let res = self.solve_assumps(&assumps)?;
485            if SolverResult::Unsat == res {
486                self.log_routine_end()?;
487                return Done(());
488            }
489            self.check_termination()?;
490
491            // Minimize solution
492            let (costs, solution) = self.get_solution_and_internal_costs(
493                self.opts
494                    .heuristic_improvements
495                    .solution_tightening
496                    .wanted(Phase::OuterLoop),
497            )?;
498            self.log_candidate(&costs, Phase::OuterLoop)?;
499            self.check_termination()?;
500            self.phase_solution(solution.clone())?;
501            let (costs, solution, block_switch) =
502                self.p_minimization(costs, solution, base_assumps, obj_encs)?;
503
504            let assumps: Vec<_> = self.enforce_dominating(&costs, obj_encs)?.collect();
505            self.yield_solutions(costs.clone(), &assumps, solution.clone(), collector)?;
506
507            // Block last Pareto point, if temporarily blocked
508            if let Some((block_lit, ids)) = block_switch {
509                if let Some(proof_stuff) = &mut self.proof_stuff {
510                    use pigeons::{ConstraintId, Derivation, ProofGoal, ProofGoalId};
511                    use rustsat::encodings::cert::CollectClauses;
512
513                    let (reified_cut, reified_assump_ids) = ids.unwrap();
514                    let id = proofs::certify_pmin_cut(
515                        obj_encs,
516                        &self.objs,
517                        &costs,
518                        &solution,
519                        self.var_manager.max_enc_var(),
520                        proof_stuff,
521                        &mut self.oracle,
522                    )?;
523                    let proof = self
524                        .oracle
525                        .proof_tracer_mut(&proof_stuff.pt_handle)
526                        .proof_mut();
527                    let hints = [ConstraintId::last(2), ConstraintId::last(1), id.into()]
528                        .into_iter()
529                        .chain(reified_assump_ids.iter().map(|id| ConstraintId::from(*id)));
530                    let unit = clause![block_lit];
531                    let unit_id = proof.redundant(
532                        &unit,
533                        [],
534                        [ProofGoal::new(
535                            ProofGoalId::from(ConstraintId::from(reified_cut)),
536                            [Derivation::Rup(clause![], hints.collect())],
537                        )
538                        .into()],
539                    )?;
540                    cadical_veripb_tracer::CadicalCertCollector::new(
541                        &mut self.oracle,
542                        &proof_stuff.pt_handle,
543                    )
544                    .add_cert_clause(unit, unit_id)?;
545                } else {
546                    self.oracle.add_unit(block_lit)?;
547                }
548            }
549        }
550    }
551}