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    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.map(|predicate| {
121                proof_atomics.map_predicate_to_proof_atomic(predicate, variable_names)
122            }),
123            generated_by: Some(inference_code.tag().into()),
124            label: Some(inference_code.label()),
125        };
126
127        writer.log_inference(inference)?;
128
129        propagation_sequence.push(Some(inference_tag));
130
131        Ok(inference_tag)
132    }
133
134    /// Log an inference that claims the given predicate is part of the initial domain.
135    pub(crate) fn log_domain_inference(
136        &mut self,
137        predicate: Predicate,
138        variable_names: &VariableNames,
139        constraint_tags: &mut KeyGenerator<ConstraintTag>,
140        assignments: &Assignments,
141    ) -> std::io::Result<Option<ConstraintTag>> {
142        if cfg!(feature = "check-deductions") {
143            self.supporting_inferences.push(SupportingInference {
144                premises: vec![],
145                consequent: Some(predicate),
146            });
147        }
148
149        if is_likely_a_constant(predicate, variable_names, assignments) {
150            // The predicate is over a constant variable. We assume we do not want to
151            // log these if they have no name.
152
153            return Ok(None);
154        }
155
156        let inference_tag = constraint_tags.next_key();
157
158        let Some(ProofImpl::CpProof {
159            writer,
160            propagation_order_hint: Some(propagation_sequence),
161            logged_domain_inferences,
162            proof_atomics,
163            ..
164        }) = self.internal_proof.as_mut()
165        else {
166            return Ok(Some(inference_tag));
167        };
168
169        if let Some(hint_idx) = logged_domain_inferences.get(&predicate).copied() {
170            let tag = propagation_sequence[hint_idx]
171                .take()
172                .expect("the logged_domain_inferences always points to some index");
173            propagation_sequence.push(Some(tag));
174
175            let _ = logged_domain_inferences.insert(predicate, propagation_sequence.len() - 1);
176
177            return Ok(Some(tag));
178        }
179
180        let inference = Inference {
181            constraint_id: inference_tag.into(),
182            premises: vec![],
183            consequent: Some(
184                proof_atomics.map_predicate_to_proof_atomic(predicate, variable_names),
185            ),
186            generated_by: None,
187            label: Some("initial_domain"),
188        };
189
190        writer.log_inference(inference)?;
191
192        propagation_sequence.push(Some(inference_tag));
193
194        let _ = logged_domain_inferences.insert(predicate, propagation_sequence.len() - 1);
195
196        Ok(Some(inference_tag))
197    }
198
199    /// Log a deduction (learned nogood) to the proof.
200    ///
201    /// The inferences and marked propagations are assumed to be recorded in reverse-application
202    /// order.
203    pub(crate) fn log_deduction(
204        &mut self,
205        premises: impl IntoIterator<Item = Predicate> + Clone,
206        variable_names: &VariableNames,
207        constraint_tags: &mut KeyGenerator<ConstraintTag>,
208        assignments: &Assignments,
209    ) -> std::io::Result<ConstraintTag> {
210        let constraint_tag = constraint_tags.next_key();
211
212        if cfg!(feature = "check-deductions") {
213            self.verify_deduction_at_runtime(premises.clone());
214        }
215
216        match &mut self.internal_proof {
217            Some(ProofImpl::CpProof {
218                writer,
219                propagation_order_hint,
220                proof_atomics,
221                logged_domain_inferences,
222                ..
223            }) => {
224                // Reset the logged domain inferences.
225                logged_domain_inferences.clear();
226
227                let deduction = Deduction {
228                    constraint_id: constraint_tag.into(),
229                    premises: premises
230                        .into_iter()
231                        .filter(|&predicate| {
232                            !is_likely_a_constant(predicate, variable_names, assignments)
233                        })
234                        .map(|premise| {
235                            proof_atomics.map_predicate_to_proof_atomic(premise, variable_names)
236                        })
237                        .collect(),
238                    sequence: propagation_order_hint
239                        .as_ref()
240                        .iter()
241                        .flat_map(|vec| vec.iter().rev().copied())
242                        .flatten()
243                        .map(|tag| tag.into())
244                        .collect(),
245                };
246
247                writer.log_deduction(deduction)?;
248
249                // Clear the hints for the next nogood.
250                if let Some(hints) = propagation_order_hint.as_mut() {
251                    hints.clear();
252                }
253
254                Ok(constraint_tag)
255            }
256
257            Some(ProofImpl::DimacsProof(writer)) => {
258                let clause = premises.into_iter().map(|predicate| !predicate);
259                writer.learned_clause(clause, variable_names)?;
260                Ok(constraint_tag)
261            }
262
263            None => Ok(constraint_tag),
264        }
265    }
266
267    pub(crate) fn unsat(self, variable_names: &VariableNames) -> std::io::Result<()> {
268        match self.internal_proof {
269            Some(ProofImpl::CpProof { mut writer, .. }) => {
270                writer.log_conclusion::<&str>(drcp_format::Conclusion::Unsat)
271            }
272            Some(ProofImpl::DimacsProof(mut writer)) => writer
273                .learned_clause(std::iter::empty(), variable_names)
274                .map(|_| ()),
275            None => Ok(()),
276        }
277    }
278
279    pub(crate) fn optimal(
280        self,
281        objective_bound: Predicate,
282        variable_names: &VariableNames,
283    ) -> std::io::Result<()> {
284        match self.internal_proof {
285            Some(ProofImpl::CpProof {
286                mut writer,
287                mut proof_atomics,
288                ..
289            }) => {
290                let atomic =
291                    proof_atomics.map_predicate_to_proof_atomic(objective_bound, variable_names);
292
293                writer.log_conclusion::<&str>(drcp_format::Conclusion::DualBound(atomic))
294            }
295
296            Some(ProofImpl::DimacsProof(_)) => {
297                panic!("Cannot conclude optimality in DIMACS proof")
298            }
299
300            None => Ok(()),
301        }
302    }
303
304    pub fn is_logging_inferences(&self) -> bool {
305        matches!(
306            self.internal_proof,
307            Some(ProofImpl::CpProof {
308                propagation_order_hint: Some(_),
309                ..
310            })
311        ) || cfg!(feature = "check-deductions")
312    }
313
314    pub(crate) fn reify_predicate(&mut self, literal: Literal, predicate: Predicate) {
315        let Some(ProofImpl::CpProof {
316            ref mut proof_atomics,
317            ..
318        }) = self.internal_proof
319        else {
320            return;
321        };
322
323        proof_atomics.reify_predicate(literal, predicate);
324    }
325
326    pub(crate) fn is_logging_proof(&self) -> bool {
327        self.internal_proof.is_some()
328    }
329
330    fn verify_deduction_at_runtime(
331        &mut self,
332        premises: impl IntoIterator<Item = Predicate> + Clone,
333    ) {
334        match verify_deduction(
335            premises.clone(),
336            self.supporting_inferences.iter().cloned().rev(),
337        ) {
338            Ok(_) => {
339                self.supporting_inferences.clear();
340            }
341            Err(InvalidDeduction(ignored_inferences)) => {
342                eprintln!("Supporting inferences:");
343                for inference in self.supporting_inferences.iter() {
344                    eprintln!("{:?} -> {:?}", inference.premises, inference.consequent);
345                }
346
347                if !ignored_inferences.is_empty() {
348                    eprintln!("Ignored inferences:");
349                    for ignored_inference in ignored_inferences {
350                        eprintln!(
351                            "{:?} -> {:?}",
352                            ignored_inference.inference.premises,
353                            ignored_inference.inference.consequent
354                        );
355                    }
356                }
357
358                panic!(
359                    "Failed to verify deduction: {:?} -> false",
360                    itertools::join(premises, " & ")
361                );
362            }
363        }
364    }
365}
366
367/// Returns `true` if the given predicate is likely a constant from the model that was unnamed.
368fn is_likely_a_constant(
369    predicate: Predicate,
370    variable_names: &VariableNames,
371    assignments: &Assignments,
372) -> bool {
373    let domain = predicate.get_domain();
374
375    let is_fixed =
376        assignments.get_initial_lower_bound(domain) == assignments.get_initial_upper_bound(domain);
377
378    let is_unnamed = variable_names.get_int_name(domain).is_none();
379
380    is_fixed && is_unnamed
381}
382
383/// A wrapper around either a file or a gzipped file.
384///
385/// Whether or not we will gzip on the fly is a runtime decision, and this wrapper is the [`Write`]
386/// implementation that [`ProofWriter`] will write to.
387#[derive(Debug)]
388enum Sink {
389    File(File),
390    GzippedFile(flate2::write::GzEncoder<File>),
391}
392
393impl Write for Sink {
394    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
395        match self {
396            Sink::File(file) => file.write(buf),
397            Sink::GzippedFile(gz_encoder) => gz_encoder.write(buf),
398        }
399    }
400
401    fn flush(&mut self) -> std::io::Result<()> {
402        match self {
403            Sink::File(file) => file.flush(),
404            Sink::GzippedFile(gz_encoder) => gz_encoder.flush(),
405        }
406    }
407}
408
409#[derive(Debug)]
410#[allow(
411    clippy::large_enum_variant,
412    reason = "there will only ever be one per solver"
413)]
414#[allow(
415    variant_size_differences,
416    reason = "there will only ever be one per solver"
417)]
418enum ProofImpl {
419    CpProof {
420        writer: ProofWriter<Sink, i32>,
421        // If propagation hints are enabled, this is a buffer used to record propagations in the
422        // order they can be applied to derive the next nogood.
423        //
424        // Every element is optional, because when we log a domain inference multiple
425        // times, we have to move the corresponding constraint tag to the end of the hint.
426        // We do this by replacing the existing value with `None` and appending `Some` at
427        // the end.
428        propagation_order_hint: Option<Vec<Option<ConstraintTag>>>,
429        proof_atomics: ProofAtomics,
430        /// The domain inferences that are logged for the next deduction. For each
431        /// inference we keep the index in the propagation order hint.
432        logged_domain_inferences: HashMap<Predicate, usize>,
433    },
434    DimacsProof(DimacsProof<File>),
435}