Skip to main content

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::io::Write;
14use std::path::Path;
15
16use dimacs::DimacsProof;
17use drcp_format::Deduction;
18use drcp_format::Inference;
19use drcp_format::writer::ProofWriter;
20pub(crate) use finalizer::*;
21pub use inference_code::*;
22use proof_atomics::ProofAtomics;
23use pumpkin_checking::InvalidDeduction;
24use pumpkin_checking::SupportingInference;
25use pumpkin_checking::verify_deduction;
26
27#[cfg(doc)]
28use crate::Solver;
29use crate::containers::HashMap;
30use crate::containers::KeyGenerator;
31use crate::engine::Assignments;
32use crate::engine::variable_names::VariableNames;
33use crate::predicates::Predicate;
34use crate::variables::Literal;
35
36/// A proof log which logs the proof steps necessary to prove unsatisfiability or optimality. We
37/// allow the following types of proofs:
38/// - A CP proof log - This can be created using [`ProofLog::cp`].
39/// - A DIMACS proof log - This can be created using [`ProofLog::dimacs`].
40///
41/// When a proof log should not be generated, use the implementation of [`Default`].
42#[derive(Debug, Default)]
43pub struct ProofLog {
44    internal_proof: Option<ProofImpl>,
45    pub(crate) supporting_inferences: Vec<SupportingInference<Predicate>>,
46}
47
48impl ProofLog {
49    /// Create a CP proof logger.
50    pub fn cp(file_path: &Path, log_hints: bool) -> std::io::Result<ProofLog> {
51        let file = File::create(file_path)?;
52
53        let sink = if file_path.extension().is_some_and(|ext| ext == "gz") {
54            Sink::GzippedFile(flate2::write::GzEncoder::new(
55                file,
56                flate2::Compression::fast(),
57            ))
58        } else {
59            Sink::File(file)
60        };
61
62        let writer = ProofWriter::new(sink);
63
64        Ok(ProofLog {
65            internal_proof: Some(ProofImpl::CpProof {
66                writer,
67                propagation_order_hint: if log_hints { Some(vec![]) } else { None },
68                logged_domain_inferences: HashMap::default(),
69                proof_atomics: ProofAtomics::default(),
70            }),
71            supporting_inferences: vec![],
72        })
73    }
74
75    /// Create a dimacs proof logger.
76    pub fn dimacs(file_path: &Path) -> std::io::Result<ProofLog> {
77        let file = File::create(file_path)?;
78        Ok(ProofLog {
79            internal_proof: Some(ProofImpl::DimacsProof(DimacsProof::new(file))),
80            supporting_inferences: vec![],
81        })
82    }
83
84    /// Log an inference to the proof.
85    pub(crate) fn log_inference(
86        &mut self,
87        constraint_tags: &mut KeyGenerator<ConstraintTag>,
88        inference_code: InferenceCode,
89        premises: impl IntoIterator<Item = Predicate> + Clone,
90        propagated: Option<Predicate>,
91        variable_names: &VariableNames,
92        assignments: &Assignments,
93    ) -> std::io::Result<ConstraintTag> {
94        let inference_tag = constraint_tags.next_key();
95
96        if cfg!(feature = "check-deductions") {
97            self.supporting_inferences.push(SupportingInference {
98                premises: premises.clone().into_iter().collect(),
99                consequent: propagated,
100            });
101        }
102
103        let Some(ProofImpl::CpProof {
104            writer,
105            propagation_order_hint: Some(propagation_sequence),
106            proof_atomics,
107            ..
108        }) = self.internal_proof.as_mut()
109        else {
110            return Ok(inference_tag);
111        };
112
113        let inference = Inference {
114            constraint_id: inference_tag.into(),
115            premises: premises
116                .into_iter()
117                .filter(|&predicate| !is_likely_a_constant(predicate, variable_names, assignments))
118                .map(|premise| proof_atomics.map_predicate_to_proof_atomic(premise, variable_names))
119                .collect(),
120            consequent: propagated
121                .filter(|&predicate| !is_likely_a_constant(predicate, variable_names, assignments))
122                .map(|predicate| {
123                    proof_atomics.map_predicate_to_proof_atomic(predicate, variable_names)
124                }),
125            generated_by: Some(inference_code.tag().into()),
126            label: Some(inference_code.label()),
127        };
128
129        writer.log_inference(inference)?;
130
131        propagation_sequence.push(Some(inference_tag));
132
133        Ok(inference_tag)
134    }
135
136    /// Log an inference that claims the given predicate is part of the initial domain.
137    pub(crate) fn log_domain_inference(
138        &mut self,
139        predicate: Predicate,
140        variable_names: &VariableNames,
141        constraint_tags: &mut KeyGenerator<ConstraintTag>,
142        assignments: &Assignments,
143    ) -> std::io::Result<Option<ConstraintTag>> {
144        if cfg!(feature = "check-deductions") {
145            self.supporting_inferences.push(SupportingInference {
146                premises: vec![],
147                consequent: Some(predicate),
148            });
149        }
150
151        if is_likely_a_constant(predicate, variable_names, assignments) {
152            // The predicate is over a constant variable. We assume we do not want to
153            // log these if they have no name.
154
155            return Ok(None);
156        }
157
158        let inference_tag = constraint_tags.next_key();
159
160        let Some(ProofImpl::CpProof {
161            writer,
162            propagation_order_hint: Some(propagation_sequence),
163            logged_domain_inferences,
164            proof_atomics,
165            ..
166        }) = self.internal_proof.as_mut()
167        else {
168            return Ok(Some(inference_tag));
169        };
170
171        if let Some(hint_idx) = logged_domain_inferences.get(&predicate).copied() {
172            let tag = propagation_sequence[hint_idx]
173                .take()
174                .expect("the logged_domain_inferences always points to some index");
175            propagation_sequence.push(Some(tag));
176
177            let _ = logged_domain_inferences.insert(predicate, propagation_sequence.len() - 1);
178
179            return Ok(Some(tag));
180        }
181
182        let inference = Inference {
183            constraint_id: inference_tag.into(),
184            premises: vec![],
185            consequent: Some(
186                proof_atomics.map_predicate_to_proof_atomic(predicate, variable_names),
187            ),
188            generated_by: None,
189            label: Some("initial_domain"),
190        };
191
192        writer.log_inference(inference)?;
193
194        propagation_sequence.push(Some(inference_tag));
195
196        let _ = logged_domain_inferences.insert(predicate, propagation_sequence.len() - 1);
197
198        Ok(Some(inference_tag))
199    }
200
201    /// Log a deduction (learned nogood) to the proof.
202    ///
203    /// The inferences and marked propagations are assumed to be recorded in reverse-application
204    /// order.
205    pub(crate) fn log_deduction(
206        &mut self,
207        premises: impl IntoIterator<Item = Predicate> + Clone,
208        variable_names: &VariableNames,
209        constraint_tags: &mut KeyGenerator<ConstraintTag>,
210        assignments: &Assignments,
211    ) -> std::io::Result<ConstraintTag> {
212        let constraint_tag = constraint_tags.next_key();
213
214        if cfg!(feature = "check-deductions") {
215            self.verify_deduction_at_runtime(premises.clone());
216        }
217
218        match &mut self.internal_proof {
219            Some(ProofImpl::CpProof {
220                writer,
221                propagation_order_hint,
222                proof_atomics,
223                logged_domain_inferences,
224                ..
225            }) => {
226                // Reset the logged domain inferences.
227                logged_domain_inferences.clear();
228
229                let deduction = Deduction {
230                    constraint_id: constraint_tag.into(),
231                    premises: premises
232                        .into_iter()
233                        .filter(|&predicate| {
234                            !is_likely_a_constant(predicate, variable_names, assignments)
235                        })
236                        .map(|premise| {
237                            proof_atomics.map_predicate_to_proof_atomic(premise, variable_names)
238                        })
239                        .collect(),
240                    sequence: propagation_order_hint
241                        .as_ref()
242                        .iter()
243                        .flat_map(|vec| vec.iter().rev().copied())
244                        .flatten()
245                        .map(|tag| tag.into())
246                        .collect(),
247                };
248
249                writer.log_deduction(deduction)?;
250
251                // Clear the hints for the next nogood.
252                if let Some(hints) = propagation_order_hint.as_mut() {
253                    hints.clear();
254                }
255
256                Ok(constraint_tag)
257            }
258
259            Some(ProofImpl::DimacsProof(writer)) => {
260                let clause = premises.into_iter().map(|predicate| !predicate);
261                writer.learned_clause(clause, variable_names)?;
262                Ok(constraint_tag)
263            }
264
265            None => Ok(constraint_tag),
266        }
267    }
268
269    pub(crate) fn unsat(self, variable_names: &VariableNames) -> std::io::Result<()> {
270        match self.internal_proof {
271            Some(ProofImpl::CpProof { mut writer, .. }) => {
272                writer.log_conclusion::<&str>(drcp_format::Conclusion::Unsat)
273            }
274            Some(ProofImpl::DimacsProof(mut writer)) => writer
275                .learned_clause(std::iter::empty(), variable_names)
276                .map(|_| ()),
277            None => Ok(()),
278        }
279    }
280
281    pub(crate) fn optimal(
282        self,
283        objective_bound: Predicate,
284        variable_names: &VariableNames,
285    ) -> std::io::Result<()> {
286        match self.internal_proof {
287            Some(ProofImpl::CpProof {
288                mut writer,
289                mut proof_atomics,
290                ..
291            }) => {
292                let atomic =
293                    proof_atomics.map_predicate_to_proof_atomic(objective_bound, variable_names);
294
295                writer.log_conclusion::<&str>(drcp_format::Conclusion::DualBound(atomic))
296            }
297
298            Some(ProofImpl::DimacsProof(_)) => {
299                panic!("Cannot conclude optimality in DIMACS proof")
300            }
301
302            None => Ok(()),
303        }
304    }
305
306    pub fn is_logging_inferences(&self) -> bool {
307        matches!(
308            self.internal_proof,
309            Some(ProofImpl::CpProof {
310                propagation_order_hint: Some(_),
311                ..
312            })
313        ) || cfg!(feature = "check-deductions")
314    }
315
316    pub(crate) fn reify_predicate(&mut self, literal: Literal, predicate: Predicate) {
317        let Some(ProofImpl::CpProof {
318            ref mut proof_atomics,
319            ..
320        }) = self.internal_proof
321        else {
322            return;
323        };
324
325        proof_atomics.reify_predicate(literal, predicate);
326    }
327
328    pub(crate) fn is_logging_proof(&self) -> bool {
329        self.internal_proof.is_some()
330    }
331
332    fn verify_deduction_at_runtime(
333        &mut self,
334        premises: impl IntoIterator<Item = Predicate> + Clone,
335    ) {
336        match verify_deduction(
337            premises.clone(),
338            self.supporting_inferences.iter().cloned().rev(),
339        ) {
340            Ok(_) => {
341                self.supporting_inferences.clear();
342            }
343            Err(InvalidDeduction(ignored_inferences)) => {
344                eprintln!("Supporting inferences:");
345                for inference in self.supporting_inferences.iter() {
346                    eprintln!("{:?} -> {:?}", inference.premises, inference.consequent);
347                }
348
349                if !ignored_inferences.is_empty() {
350                    eprintln!("Ignored inferences:");
351                    for ignored_inference in ignored_inferences {
352                        eprintln!(
353                            "{:?} -> {:?}",
354                            ignored_inference.inference.premises,
355                            ignored_inference.inference.consequent
356                        );
357                    }
358                }
359
360                panic!(
361                    "Failed to verify deduction: {:?} -> false",
362                    itertools::join(premises, " & ")
363                );
364            }
365        }
366    }
367}
368
369/// Returns `true` if the given predicate is likely a constant from the model that was unnamed.
370fn is_likely_a_constant(
371    predicate: Predicate,
372    variable_names: &VariableNames,
373    assignments: &Assignments,
374) -> bool {
375    let domain = predicate.get_domain();
376
377    let is_fixed =
378        assignments.get_initial_lower_bound(domain) == assignments.get_initial_upper_bound(domain);
379
380    let is_unnamed = variable_names.get_int_name(domain).is_none();
381
382    is_fixed || is_unnamed
383}
384
385/// A wrapper around either a file or a gzipped file.
386///
387/// Whether or not we will gzip on the fly is a runtime decision, and this wrapper is the [`Write`]
388/// implementation that [`ProofWriter`] will write to.
389#[derive(Debug)]
390enum Sink {
391    File(File),
392    GzippedFile(flate2::write::GzEncoder<File>),
393}
394
395impl Write for Sink {
396    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
397        match self {
398            Sink::File(file) => file.write(buf),
399            Sink::GzippedFile(gz_encoder) => gz_encoder.write(buf),
400        }
401    }
402
403    fn flush(&mut self) -> std::io::Result<()> {
404        match self {
405            Sink::File(file) => file.flush(),
406            Sink::GzippedFile(gz_encoder) => gz_encoder.flush(),
407        }
408    }
409}
410
411#[derive(Debug)]
412#[allow(
413    clippy::large_enum_variant,
414    reason = "there will only ever be one per solver"
415)]
416#[allow(
417    variant_size_differences,
418    reason = "there will only ever be one per solver"
419)]
420enum ProofImpl {
421    CpProof {
422        writer: ProofWriter<Sink, i32>,
423        // If propagation hints are enabled, this is a buffer used to record propagations in the
424        // order they can be applied to derive the next nogood.
425        //
426        // Every element is optional, because when we log a domain inference multiple
427        // times, we have to move the corresponding constraint tag to the end of the hint.
428        // We do this by replacing the existing value with `None` and appending `Some` at
429        // the end.
430        propagation_order_hint: Option<Vec<Option<ConstraintTag>>>,
431        proof_atomics: ProofAtomics,
432        /// The domain inferences that are logged for the next deduction. For each
433        /// inference we keep the index in the propagation order hint.
434        logged_domain_inferences: HashMap<Predicate, usize>,
435    },
436    DimacsProof(DimacsProof<File>),
437}