pumpkin_core/proof/
mod.rs

1//! Pumpkin supports proof logging for SAT and CP problems. During search, the solver produces a
2//! [`ProofLog`], which is a list of deductions made by the solver.
3//!
4//! Proof logging for CP is supported in the DRCP format. This format explicitly supports usage
5//! where the solver logs a proof scaffold which later processed into a full proof after search
6//! has completed.
7mod dimacs;
8mod finalizer;
9mod inference_code;
10mod proof_atomics;
11
12use std::fs::File;
13use std::path::Path;
14use std::sync::Arc;
15
16use dimacs::DimacsProof;
17use drcp_format::writer::ProofWriter;
18use drcp_format::Deduction;
19use drcp_format::Inference;
20pub(crate) use finalizer::*;
21pub use inference_code::*;
22use proof_atomics::ProofAtomics;
23
24use crate::containers::HashMap;
25use crate::containers::KeyGenerator;
26use crate::containers::KeyedVec;
27use crate::containers::StorageKey;
28use crate::engine::variable_names::VariableNames;
29use crate::predicates::Predicate;
30use crate::variables::Literal;
31#[cfg(doc)]
32use crate::Solver;
33
34/// A proof log which logs the proof steps necessary to prove unsatisfiability or optimality. We
35/// allow the following types of proofs:
36/// - A CP proof log - This can be created using [`ProofLog::cp`].
37/// - A DIMACS proof log - This can be created using [`ProofLog::dimacs`].
38///
39/// When a proof log should not be generated, use the implementation of [`Default`].
40#[derive(Debug, Default)]
41pub struct ProofLog {
42    internal_proof: Option<ProofImpl>,
43}
44
45impl ProofLog {
46    /// Create a CP proof logger.
47    pub fn cp(file_path: &Path, log_hints: bool) -> std::io::Result<ProofLog> {
48        let file = File::create(file_path)?;
49
50        #[cfg(feature = "gzipped-proofs")]
51        let writer = {
52            let encoder = flate2::write::GzEncoder::new(file, flate2::Compression::fast());
53            ProofWriter::new(encoder)
54        };
55
56        #[cfg(not(feature = "gzipped-proofs"))]
57        let writer = ProofWriter::new(file);
58
59        Ok(ProofLog {
60            internal_proof: Some(ProofImpl::CpProof {
61                writer,
62                propagation_order_hint: if log_hints { Some(vec![]) } else { None },
63                logged_domain_inferences: HashMap::default(),
64                inference_codes: KeyedVec::default(),
65                proof_atomics: ProofAtomics::default(),
66                constraint_tags: KeyGenerator::default(),
67            }),
68        })
69    }
70
71    /// Create a dimacs proof logger.
72    pub fn dimacs(file_path: &Path) -> std::io::Result<ProofLog> {
73        let file = File::create(file_path)?;
74        Ok(ProofLog {
75            internal_proof: Some(ProofImpl::DimacsProof(DimacsProof::new(file))),
76        })
77    }
78
79    /// Log an inference to the proof.
80    pub(crate) fn log_inference(
81        &mut self,
82        inference_code: InferenceCode,
83        premises: impl IntoIterator<Item = Predicate>,
84        propagated: Option<Predicate>,
85        variable_names: &VariableNames,
86    ) -> std::io::Result<ConstraintTag> {
87        let Some(ProofImpl::CpProof {
88            writer,
89            propagation_order_hint: Some(propagation_sequence),
90            inference_codes,
91            constraint_tags,
92            proof_atomics,
93            ..
94        }) = self.internal_proof.as_mut()
95        else {
96            return Ok(ConstraintTag::create_from_index(0));
97        };
98
99        let (tag, label) = inference_codes[inference_code].clone();
100
101        let inference_tag = constraint_tags.next_key();
102
103        let inference = Inference {
104            constraint_id: inference_tag.into(),
105            premises: premises
106                .into_iter()
107                .map(|premise| proof_atomics.map_predicate_to_proof_atomic(premise, variable_names))
108                .collect(),
109            consequent: propagated.map(|predicate| {
110                proof_atomics.map_predicate_to_proof_atomic(predicate, variable_names)
111            }),
112            generated_by: Some(tag.into()),
113            label: Some(label),
114        };
115
116        writer.log_inference(inference)?;
117
118        propagation_sequence.push(Some(inference_tag));
119
120        Ok(inference_tag)
121    }
122
123    /// Log an inference that claims the given predicate is part of the initial domain.
124    pub(crate) fn log_domain_inference(
125        &mut self,
126        predicate: Predicate,
127        variable_names: &VariableNames,
128    ) -> std::io::Result<ConstraintTag> {
129        let Some(ProofImpl::CpProof {
130            writer,
131            propagation_order_hint: Some(propagation_sequence),
132            logged_domain_inferences,
133            constraint_tags,
134            proof_atomics,
135            ..
136        }) = self.internal_proof.as_mut()
137        else {
138            return Ok(ConstraintTag::create_from_index(0));
139        };
140
141        if let Some(hint_idx) = logged_domain_inferences.get(&predicate).copied() {
142            let tag = propagation_sequence[hint_idx]
143                .take()
144                .expect("the logged_domain_inferences always points to some index");
145            propagation_sequence.push(Some(tag));
146
147            let _ = logged_domain_inferences.insert(predicate, propagation_sequence.len() - 1);
148
149            return Ok(tag);
150        }
151
152        let inference_tag = constraint_tags.next_key();
153
154        let inference = Inference {
155            constraint_id: inference_tag.into(),
156            premises: vec![],
157            consequent: Some(
158                proof_atomics.map_predicate_to_proof_atomic(predicate, variable_names),
159            ),
160            generated_by: None,
161            label: Some("initial_domain"),
162        };
163
164        writer.log_inference(inference)?;
165
166        propagation_sequence.push(Some(inference_tag));
167
168        let _ = logged_domain_inferences.insert(predicate, propagation_sequence.len() - 1);
169
170        Ok(inference_tag)
171    }
172
173    /// Log a deduction (learned nogood) to the proof.
174    ///
175    /// The inferences and marked propagations are assumed to be recorded in reverse-application
176    /// order.
177    pub(crate) fn log_deduction(
178        &mut self,
179        premises: impl IntoIterator<Item = Predicate>,
180        variable_names: &VariableNames,
181    ) -> std::io::Result<ConstraintTag> {
182        match &mut self.internal_proof {
183            Some(ProofImpl::CpProof {
184                writer,
185                propagation_order_hint,
186                constraint_tags,
187                proof_atomics,
188                logged_domain_inferences,
189                ..
190            }) => {
191                // Reset the logged domain inferences.
192                logged_domain_inferences.clear();
193
194                let constraint_tag = constraint_tags.next_key();
195
196                let deduction = Deduction {
197                    constraint_id: constraint_tag.into(),
198                    premises: premises
199                        .into_iter()
200                        .map(|premise| {
201                            proof_atomics.map_predicate_to_proof_atomic(premise, variable_names)
202                        })
203                        .collect(),
204                    sequence: propagation_order_hint
205                        .as_ref()
206                        .iter()
207                        .flat_map(|vec| vec.iter().rev().copied())
208                        .flatten()
209                        .map(|tag| tag.into())
210                        .collect(),
211                };
212
213                writer.log_deduction(deduction)?;
214
215                // Clear the hints for the next nogood.
216                if let Some(hints) = propagation_order_hint.as_mut() {
217                    hints.clear();
218                }
219
220                Ok(constraint_tag)
221            }
222
223            Some(ProofImpl::DimacsProof(writer)) => {
224                let clause = premises.into_iter().map(|predicate| !predicate);
225                writer.learned_clause(clause, variable_names)?;
226                Ok(ConstraintTag::create_from_index(0))
227            }
228
229            None => Ok(ConstraintTag::create_from_index(0)),
230        }
231    }
232
233    pub(crate) fn unsat(self, variable_names: &VariableNames) -> std::io::Result<()> {
234        match self.internal_proof {
235            Some(ProofImpl::CpProof { mut writer, .. }) => {
236                writer.log_conclusion::<&str>(drcp_format::Conclusion::Unsat)
237            }
238            Some(ProofImpl::DimacsProof(mut writer)) => writer
239                .learned_clause(std::iter::empty(), variable_names)
240                .map(|_| ()),
241            None => Ok(()),
242        }
243    }
244
245    pub(crate) fn optimal(
246        self,
247        objective_bound: Predicate,
248        variable_names: &VariableNames,
249    ) -> std::io::Result<()> {
250        match self.internal_proof {
251            Some(ProofImpl::CpProof {
252                mut writer,
253                mut proof_atomics,
254                ..
255            }) => {
256                let atomic =
257                    proof_atomics.map_predicate_to_proof_atomic(objective_bound, variable_names);
258
259                writer.log_conclusion::<&str>(drcp_format::Conclusion::DualBound(atomic))
260            }
261
262            Some(ProofImpl::DimacsProof(_)) => {
263                panic!("Cannot conclude optimality in DIMACS proof")
264            }
265
266            None => Ok(()),
267        }
268    }
269
270    pub(crate) fn is_logging_inferences(&self) -> bool {
271        matches!(
272            self.internal_proof,
273            Some(ProofImpl::CpProof {
274                propagation_order_hint: Some(_),
275                ..
276            })
277        )
278    }
279
280    pub(crate) fn reify_predicate(&mut self, literal: Literal, predicate: Predicate) {
281        let Some(ProofImpl::CpProof {
282            ref mut proof_atomics,
283            ..
284        }) = self.internal_proof
285        else {
286            return;
287        };
288
289        proof_atomics.reify_predicate(literal, predicate);
290    }
291
292    /// Create a new [`InferenceCode`] for a [`ConstraintTag`] and [`InferenceLabel`] combination.
293    /// The inference codes are required to log inferences with [`Self::log_inference`].
294    pub(crate) fn create_inference_code(
295        &mut self,
296        constraint_tag: ConstraintTag,
297        inference_label: impl InferenceLabel,
298    ) -> InferenceCode {
299        match &mut self.internal_proof {
300            Some(ProofImpl::CpProof {
301                inference_codes, ..
302            }) => inference_codes.push((constraint_tag, inference_label.to_str())),
303
304            // If we are not logging a CP proof, then we do not care about this value.
305            _ => InferenceCode::create_from_index(0),
306        }
307    }
308
309    /// Create a new constraint tag.
310    pub(crate) fn new_constraint_tag(&mut self) -> ConstraintTag {
311        match self.internal_proof {
312            Some(ProofImpl::CpProof {
313                ref mut constraint_tags,
314                ..
315            }) => constraint_tags.next_key(),
316            _ => ConstraintTag::create_from_index(0),
317        }
318    }
319}
320
321#[derive(Debug)]
322#[allow(
323    clippy::large_enum_variant,
324    reason = "there will only every be one per solver"
325)]
326enum ProofImpl {
327    CpProof {
328        #[cfg(feature = "gzipped-proofs")]
329        writer: ProofWriter<flate2::write::GzEncoder<File>, i32>,
330        #[cfg(not(feature = "gzipped-proofs"))]
331        writer: ProofWriter<File, i32>,
332        inference_codes: KeyedVec<InferenceCode, (ConstraintTag, Arc<str>)>,
333        /// The [`ConstraintTag`]s generated for this proof.
334        constraint_tags: KeyGenerator<ConstraintTag>,
335        // If propagation hints are enabled, this is a buffer used to record propagations in the
336        // order they can be applied to derive the next nogood.
337        //
338        // Every element is optional, because when we log a domain inference multiple
339        // times, we have to move the corresponding constraint tag to the end of the hint.
340        // We do this by replacing the existing value with `None` and appending `Some` at
341        // the end.
342        propagation_order_hint: Option<Vec<Option<ConstraintTag>>>,
343        proof_atomics: ProofAtomics,
344        /// The domain inferences that are logged for the next deduction. For each
345        /// inference we keep the index in the propagation order hint.
346        logged_domain_inferences: HashMap<Predicate, usize>,
347    },
348    DimacsProof(DimacsProof<File>),
349}