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 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.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 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 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 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 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 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
367fn 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#[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 propagation_order_hint: Option<Vec<Option<ConstraintTag>>>,
429 proof_atomics: ProofAtomics,
430 logged_domain_inferences: HashMap<Predicate, usize>,
433 },
434 DimacsProof(DimacsProof<File>),
435}