1mod 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#[derive(Debug, Default)]
43pub struct ProofLog {
44 internal_proof: Option<ProofImpl>,
45 pub(crate) supporting_inferences: Vec<SupportingInference<Predicate>>,
46}
47
48impl ProofLog {
49 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 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 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 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 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 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 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 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
369fn 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#[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 propagation_order_hint: Option<Vec<Option<ConstraintTag>>>,
431 proof_atomics: ProofAtomics,
432 logged_domain_inferences: HashMap<Predicate, usize>,
435 },
436 DimacsProof(DimacsProof<File>),
437}