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        let writer = ProofWriter::new(file);
51
52        Ok(ProofLog {
53            internal_proof: Some(ProofImpl::CpProof {
54                writer,
55                propagation_order_hint: if log_hints { Some(vec![]) } else { None },
56                logged_domain_inferences: HashMap::default(),
57                inference_codes: KeyedVec::default(),
58                proof_atomics: ProofAtomics::default(),
59                constraint_tags: KeyGenerator::default(),
60            }),
61        })
62    }
63
64    /// Create a dimacs proof logger.
65    pub fn dimacs(file_path: &Path) -> std::io::Result<ProofLog> {
66        let file = File::create(file_path)?;
67        Ok(ProofLog {
68            internal_proof: Some(ProofImpl::DimacsProof(DimacsProof::new(file))),
69        })
70    }
71
72    /// Log an inference to the proof.
73    pub(crate) fn log_inference(
74        &mut self,
75        inference_code: InferenceCode,
76        premises: impl IntoIterator<Item = Predicate>,
77        propagated: Option<Predicate>,
78        variable_names: &VariableNames,
79    ) -> std::io::Result<ConstraintTag> {
80        let Some(ProofImpl::CpProof {
81            writer,
82            propagation_order_hint: Some(propagation_sequence),
83            inference_codes,
84            constraint_tags,
85            proof_atomics,
86            ..
87        }) = self.internal_proof.as_mut()
88        else {
89            return Ok(ConstraintTag::create_from_index(0));
90        };
91
92        let (tag, label) = inference_codes[inference_code].clone();
93
94        let inference_tag = constraint_tags.next_key();
95
96        let inference = Inference {
97            constraint_id: inference_tag.into(),
98            premises: premises
99                .into_iter()
100                .map(|premise| proof_atomics.map_predicate_to_proof_atomic(premise, variable_names))
101                .collect(),
102            consequent: propagated.map(|predicate| {
103                proof_atomics.map_predicate_to_proof_atomic(predicate, variable_names)
104            }),
105            generated_by: Some(tag.into()),
106            label: Some(label),
107        };
108
109        writer.log_inference(inference)?;
110
111        propagation_sequence.push(Some(inference_tag));
112
113        Ok(inference_tag)
114    }
115
116    /// Log an inference that claims the given predicate is part of the initial domain.
117    pub(crate) fn log_domain_inference(
118        &mut self,
119        predicate: Predicate,
120        variable_names: &VariableNames,
121    ) -> std::io::Result<ConstraintTag> {
122        let Some(ProofImpl::CpProof {
123            writer,
124            propagation_order_hint: Some(propagation_sequence),
125            logged_domain_inferences,
126            constraint_tags,
127            proof_atomics,
128            ..
129        }) = self.internal_proof.as_mut()
130        else {
131            return Ok(ConstraintTag::create_from_index(0));
132        };
133
134        if let Some(hint_idx) = logged_domain_inferences.get(&predicate).copied() {
135            let tag = propagation_sequence[hint_idx]
136                .take()
137                .expect("the logged_domain_inferences always points to some index");
138            propagation_sequence.push(Some(tag));
139
140            let _ = logged_domain_inferences.insert(predicate, propagation_sequence.len() - 1);
141
142            return Ok(tag);
143        }
144
145        let inference_tag = constraint_tags.next_key();
146
147        let inference = Inference {
148            constraint_id: inference_tag.into(),
149            premises: vec![],
150            consequent: Some(
151                proof_atomics.map_predicate_to_proof_atomic(predicate, variable_names),
152            ),
153            generated_by: None,
154            label: Some("initial_domain"),
155        };
156
157        writer.log_inference(inference)?;
158
159        propagation_sequence.push(Some(inference_tag));
160
161        let _ = logged_domain_inferences.insert(predicate, propagation_sequence.len() - 1);
162
163        Ok(inference_tag)
164    }
165
166    /// Log a deduction (learned nogood) to the proof.
167    ///
168    /// The inferences and marked propagations are assumed to be recorded in reverse-application
169    /// order.
170    pub(crate) fn log_deduction(
171        &mut self,
172        premises: impl IntoIterator<Item = Predicate>,
173        variable_names: &VariableNames,
174    ) -> std::io::Result<ConstraintTag> {
175        match &mut self.internal_proof {
176            Some(ProofImpl::CpProof {
177                writer,
178                propagation_order_hint,
179                constraint_tags,
180                proof_atomics,
181                logged_domain_inferences,
182                ..
183            }) => {
184                // Reset the logged domain inferences.
185                logged_domain_inferences.clear();
186
187                let constraint_tag = constraint_tags.next_key();
188
189                let deduction = Deduction {
190                    constraint_id: constraint_tag.into(),
191                    premises: premises
192                        .into_iter()
193                        .map(|premise| {
194                            proof_atomics.map_predicate_to_proof_atomic(premise, variable_names)
195                        })
196                        .collect(),
197                    sequence: propagation_order_hint
198                        .as_ref()
199                        .iter()
200                        .flat_map(|vec| vec.iter().rev().copied())
201                        .flatten()
202                        .map(|tag| tag.into())
203                        .collect(),
204                };
205
206                writer.log_deduction(deduction)?;
207
208                // Clear the hints for the next nogood.
209                if let Some(hints) = propagation_order_hint.as_mut() {
210                    hints.clear();
211                }
212
213                Ok(constraint_tag)
214            }
215
216            Some(ProofImpl::DimacsProof(writer)) => {
217                let clause = premises.into_iter().map(|predicate| !predicate);
218                writer.learned_clause(clause, variable_names)?;
219                Ok(ConstraintTag::create_from_index(0))
220            }
221
222            None => Ok(ConstraintTag::create_from_index(0)),
223        }
224    }
225
226    pub(crate) fn unsat(self, variable_names: &VariableNames) -> std::io::Result<()> {
227        match self.internal_proof {
228            Some(ProofImpl::CpProof { mut writer, .. }) => {
229                writer.log_conclusion::<&str>(drcp_format::Conclusion::Unsat)
230            }
231            Some(ProofImpl::DimacsProof(mut writer)) => writer
232                .learned_clause(std::iter::empty(), variable_names)
233                .map(|_| ()),
234            None => Ok(()),
235        }
236    }
237
238    pub(crate) fn optimal(
239        self,
240        objective_bound: Predicate,
241        variable_names: &VariableNames,
242    ) -> std::io::Result<()> {
243        match self.internal_proof {
244            Some(ProofImpl::CpProof {
245                mut writer,
246                mut proof_atomics,
247                ..
248            }) => {
249                let atomic =
250                    proof_atomics.map_predicate_to_proof_atomic(objective_bound, variable_names);
251
252                writer.log_conclusion::<&str>(drcp_format::Conclusion::DualBound(atomic))
253            }
254
255            Some(ProofImpl::DimacsProof(_)) => {
256                panic!("Cannot conclude optimality in DIMACS proof")
257            }
258
259            None => Ok(()),
260        }
261    }
262
263    pub(crate) fn is_logging_inferences(&self) -> bool {
264        matches!(
265            self.internal_proof,
266            Some(ProofImpl::CpProof {
267                propagation_order_hint: Some(_),
268                ..
269            })
270        )
271    }
272
273    pub(crate) fn reify_predicate(&mut self, literal: Literal, predicate: Predicate) {
274        let Some(ProofImpl::CpProof {
275            ref mut proof_atomics,
276            ..
277        }) = self.internal_proof
278        else {
279            return;
280        };
281
282        proof_atomics.reify_predicate(literal, predicate);
283    }
284
285    /// Create a new [`InferenceCode`] for a [`ConstraintTag`] and [`InferenceLabel`] combination.
286    /// The inference codes are required to log inferences with [`Self::log_inference`].
287    pub(crate) fn create_inference_code(
288        &mut self,
289        constraint_tag: ConstraintTag,
290        inference_label: impl InferenceLabel,
291    ) -> InferenceCode {
292        match &mut self.internal_proof {
293            Some(ProofImpl::CpProof {
294                inference_codes, ..
295            }) => inference_codes.push((constraint_tag, inference_label.to_str())),
296
297            // If we are not logging a CP proof, then we do not care about this value.
298            _ => InferenceCode::create_from_index(0),
299        }
300    }
301
302    /// Create a new constraint tag.
303    pub(crate) fn new_constraint_tag(&mut self) -> ConstraintTag {
304        match self.internal_proof {
305            Some(ProofImpl::CpProof {
306                ref mut constraint_tags,
307                ..
308            }) => constraint_tags.next_key(),
309            _ => ConstraintTag::create_from_index(0),
310        }
311    }
312}
313
314#[derive(Debug)]
315enum ProofImpl {
316    CpProof {
317        writer: ProofWriter<File, i32>,
318        inference_codes: KeyedVec<InferenceCode, (ConstraintTag, Arc<str>)>,
319        /// The [`ConstraintTag`]s generated for this proof.
320        constraint_tags: KeyGenerator<ConstraintTag>,
321        // If propagation hints are enabled, this is a buffer used to record propagations in the
322        // order they can be applied to derive the next nogood.
323        //
324        // Every element is optional, because when we log a domain inference multiple
325        // times, we have to move the corresponding constraint tag to the end of the hint.
326        // We do this by replacing the existing value with `None` and appending `Some` at
327        // the end.
328        propagation_order_hint: Option<Vec<Option<ConstraintTag>>>,
329        proof_atomics: ProofAtomics,
330        /// The domain inferences that are logged for the next deduction. For each
331        /// inference we keep the index in the propagation order hint.
332        logged_domain_inferences: HashMap<Predicate, usize>,
333    },
334    DimacsProof(DimacsProof<File>),
335}