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