Skip to main content

oxiz_proof/
explanation.rs

1//! Natural language proof explanations and analysis.
2//!
3//! This module provides utilities for generating human-readable explanations
4//! of proof steps, useful for debugging, education, and proof understanding.
5
6use crate::proof::{Proof, ProofNode, ProofNodeId, ProofStep};
7use std::fmt;
8
9/// Verbosity level for proof explanations.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Verbosity {
12    /// Minimal explanation (just rule names)
13    Minimal,
14    /// Concise explanation (rule + premise count)
15    Concise,
16    /// Detailed explanation (full premise list)
17    Detailed,
18    /// Verbose explanation (with conclusions)
19    Verbose,
20}
21
22/// A single explained proof step.
23#[derive(Debug, Clone)]
24pub struct ExplainedStep {
25    /// The node ID
26    pub node_id: ProofNodeId,
27    /// Natural language explanation
28    pub explanation: String,
29    /// Depth in the proof tree
30    pub depth: u32,
31}
32
33impl fmt::Display for ExplainedStep {
34    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35        write!(f, "{}: {}", self.node_id, self.explanation)
36    }
37}
38
39/// Proof explanation generator.
40pub struct ProofExplainer {
41    verbosity: Verbosity,
42}
43
44impl ProofExplainer {
45    /// Create a new proof explainer with default verbosity.
46    #[must_use]
47    pub fn new() -> Self {
48        Self {
49            verbosity: Verbosity::Concise,
50        }
51    }
52
53    /// Create a proof explainer with specific verbosity.
54    #[must_use]
55    pub fn with_verbosity(verbosity: Verbosity) -> Self {
56        Self { verbosity }
57    }
58
59    /// Explain a single proof node.
60    #[must_use]
61    pub fn explain_node(&self, proof: &Proof, node: &ProofNode) -> ExplainedStep {
62        let explanation = match &node.step {
63            ProofStep::Axiom { conclusion } => self.explain_axiom(conclusion),
64            ProofStep::Inference {
65                rule,
66                premises,
67                conclusion,
68                args,
69            } => self.explain_inference(proof, rule, premises, conclusion, args),
70        };
71
72        ExplainedStep {
73            node_id: node.id,
74            explanation,
75            depth: node.depth,
76        }
77    }
78
79    /// Explain an axiom step.
80    fn explain_axiom(&self, conclusion: &str) -> String {
81        match self.verbosity {
82            Verbosity::Minimal => "Axiom".to_string(),
83            Verbosity::Concise => "Assumed as axiom".to_string(),
84            Verbosity::Detailed | Verbosity::Verbose => {
85                format!("Assumed '{}' as an axiom", conclusion)
86            }
87        }
88    }
89
90    /// Explain an inference step.
91    fn explain_inference(
92        &self,
93        proof: &Proof,
94        rule: &str,
95        premises: &[ProofNodeId],
96        conclusion: &str,
97        args: &[String],
98    ) -> String {
99        let rule_desc = self.describe_rule(rule);
100
101        match self.verbosity {
102            Verbosity::Minimal => rule_desc.to_string(),
103            Verbosity::Concise => {
104                if premises.is_empty() {
105                    format!("Applied {}", rule_desc)
106                } else {
107                    format!("Applied {} using {} premise(s)", rule_desc, premises.len())
108                }
109            }
110            Verbosity::Detailed => {
111                if premises.is_empty() {
112                    format!("Applied {}", rule_desc)
113                } else {
114                    let premise_ids: Vec<String> =
115                        premises.iter().map(|id| id.to_string()).collect();
116                    format!(
117                        "Applied {} using premises [{}]",
118                        rule_desc,
119                        premise_ids.join(", ")
120                    )
121                }
122            }
123            Verbosity::Verbose => {
124                let mut parts = vec![format!("Applied {}", rule_desc)];
125
126                if !premises.is_empty() {
127                    let premise_descs: Vec<String> = premises
128                        .iter()
129                        .filter_map(|&id| proof.get_node(id).map(|n| (id, n)))
130                        .map(|(id, n)| match &n.step {
131                            ProofStep::Axiom { conclusion } => format!("{} ({})", id, conclusion),
132                            ProofStep::Inference { conclusion, .. } => {
133                                format!("{} ({})", id, conclusion)
134                            }
135                        })
136                        .collect();
137                    parts.push(format!("from [{}]", premise_descs.join(", ")));
138                }
139
140                if !args.is_empty() {
141                    parts.push(format!("with arguments [{}]", args.join(", ")));
142                }
143
144                parts.push(format!("to derive '{}'", conclusion));
145                parts.join(" ")
146            }
147        }
148    }
149
150    /// Describe a rule in natural language.
151    fn describe_rule<'a>(&self, rule: &'a str) -> &'a str {
152        match rule {
153            "and" => "conjunction introduction",
154            "or" => "disjunction introduction",
155            "not" => "negation introduction",
156            "implies" => "implication introduction",
157            "resolution" => "resolution",
158            "unit_propagation" => "unit propagation",
159            "modus_ponens" => "modus ponens",
160            "transitivity" => "transitivity",
161            "symmetry" => "symmetry",
162            "reflexivity" => "reflexivity",
163            "congruence" => "congruence",
164            "farkas" => "Farkas lemma",
165            "split" => "case split",
166            "instantiate" => "quantifier instantiation",
167            "skolemize" => "skolemization",
168            _ => rule,
169        }
170    }
171
172    /// Explain an entire proof.
173    #[must_use]
174    pub fn explain_proof(&self, proof: &Proof) -> Vec<ExplainedStep> {
175        proof
176            .nodes()
177            .iter()
178            .map(|node| self.explain_node(proof, node))
179            .collect()
180    }
181
182    /// Generate a step-by-step summary of the proof.
183    #[must_use]
184    pub fn summarize_proof(&self, proof: &Proof) -> String {
185        let explanations = self.explain_proof(proof);
186
187        if explanations.is_empty() {
188            return "Empty proof".to_string();
189        }
190
191        let mut summary = String::new();
192        summary.push_str(&format!("Proof Summary ({} steps):\n", explanations.len()));
193        summary.push_str("─".repeat(60).as_str());
194        summary.push('\n');
195
196        for (i, step) in explanations.iter().enumerate() {
197            let indent = "  ".repeat(step.depth as usize);
198            summary.push_str(&format!(
199                "{}{}. {} (depth {})\n",
200                indent,
201                i + 1,
202                step.explanation,
203                step.depth
204            ));
205        }
206
207        summary
208    }
209
210    /// Identify the critical path in a proof (longest path from axiom to conclusion).
211    #[must_use]
212    pub fn critical_path(&self, proof: &Proof) -> Vec<ProofNodeId> {
213        if proof.is_empty() {
214            return Vec::new();
215        }
216
217        // Find the node with maximum depth
218        let max_depth_node = proof
219            .nodes()
220            .iter()
221            .max_by_key(|n| n.depth)
222            .expect("proof has at least one node");
223
224        // Trace back to axioms
225        let mut path = vec![max_depth_node.id];
226        let mut current = max_depth_node;
227
228        while let ProofStep::Inference { premises, .. } = &current.step {
229            if premises.is_empty() {
230                break;
231            }
232
233            // Find the premise with maximum depth
234            let max_premise = premises
235                .iter()
236                .filter_map(|&id| proof.get_node(id))
237                .max_by_key(|n| n.depth);
238
239            if let Some(premise) = max_premise {
240                path.push(premise.id);
241                current = premise;
242            } else {
243                break;
244            }
245        }
246
247        path.reverse();
248        path
249    }
250
251    /// Analyze proof complexity.
252    #[must_use]
253    pub fn analyze_complexity(&self, proof: &Proof) -> ProofComplexity {
254        let total_steps = proof.len();
255        let max_depth = proof.depth();
256
257        let axiom_count = proof
258            .nodes()
259            .iter()
260            .filter(|n| matches!(n.step, ProofStep::Axiom { .. }))
261            .count();
262
263        let inference_count = total_steps - axiom_count;
264
265        let avg_premises = if inference_count > 0 {
266            proof
267                .nodes()
268                .iter()
269                .filter_map(|n| match &n.step {
270                    ProofStep::Inference { premises, .. } => Some(premises.len()),
271                    _ => None,
272                })
273                .sum::<usize>() as f64
274                / inference_count as f64
275        } else {
276            0.0
277        };
278
279        let branching_factor = if max_depth > 0 {
280            total_steps as f64 / max_depth as f64
281        } else {
282            0.0
283        };
284
285        ProofComplexity {
286            total_steps,
287            max_depth,
288            axiom_count,
289            inference_count,
290            avg_premises,
291            branching_factor,
292        }
293    }
294}
295
296impl Default for ProofExplainer {
297    fn default() -> Self {
298        Self::new()
299    }
300}
301
302/// Complexity analysis of a proof.
303#[derive(Debug, Clone)]
304pub struct ProofComplexity {
305    /// Total number of steps
306    pub total_steps: usize,
307    /// Maximum depth
308    pub max_depth: u32,
309    /// Number of axioms
310    pub axiom_count: usize,
311    /// Number of inferences
312    pub inference_count: usize,
313    /// Average number of premises per inference
314    pub avg_premises: f64,
315    /// Branching factor (steps per depth level)
316    pub branching_factor: f64,
317}
318
319impl fmt::Display for ProofComplexity {
320    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
321        writeln!(f, "Proof Complexity Analysis:")?;
322        writeln!(f, "  Total steps: {}", self.total_steps)?;
323        writeln!(f, "  Maximum depth: {}", self.max_depth)?;
324        writeln!(f, "  Axioms: {}", self.axiom_count)?;
325        writeln!(f, "  Inferences: {}", self.inference_count)?;
326        writeln!(f, "  Avg premises per inference: {:.2}", self.avg_premises)?;
327        writeln!(f, "  Branching factor: {:.2}", self.branching_factor)?;
328        Ok(())
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335
336    #[test]
337    fn test_explain_axiom() {
338        let explainer = ProofExplainer::new();
339        let mut proof = Proof::new();
340        let id = proof.add_axiom("p");
341        let node = proof.get_node(id).expect("test operation should succeed");
342
343        let explained = explainer.explain_node(&proof, node);
344        assert!(explained.explanation.contains("Assumed"));
345    }
346
347    #[test]
348    fn test_explain_inference() {
349        let explainer = ProofExplainer::new();
350        let mut proof = Proof::new();
351        let a1 = proof.add_axiom("p");
352        let a2 = proof.add_axiom("q");
353        let i1 = proof.add_inference("and", vec![a1, a2], "p /\\ q");
354        let node = proof.get_node(i1).expect("test operation should succeed");
355
356        let explained = explainer.explain_node(&proof, node);
357        assert!(explained.explanation.contains("conjunction"));
358    }
359
360    #[test]
361    fn test_verbosity_levels() {
362        let mut proof = Proof::new();
363        let a1 = proof.add_axiom("p");
364        let a2 = proof.add_axiom("q");
365        let i1 = proof.add_inference("and", vec![a1, a2], "p /\\ q");
366        let node = proof.get_node(i1).expect("test operation should succeed");
367
368        let minimal = ProofExplainer::with_verbosity(Verbosity::Minimal);
369        let concise = ProofExplainer::with_verbosity(Verbosity::Concise);
370        let detailed = ProofExplainer::with_verbosity(Verbosity::Detailed);
371        let verbose = ProofExplainer::with_verbosity(Verbosity::Verbose);
372
373        let exp_min = minimal.explain_node(&proof, node);
374        let exp_con = concise.explain_node(&proof, node);
375        let exp_det = detailed.explain_node(&proof, node);
376        let exp_ver = verbose.explain_node(&proof, node);
377
378        // Verbosity should increase explanation length
379        assert!(exp_min.explanation.len() < exp_con.explanation.len());
380        assert!(exp_det.explanation.len() > exp_con.explanation.len());
381        assert!(exp_ver.explanation.len() > exp_det.explanation.len());
382    }
383
384    #[test]
385    fn test_explain_proof() {
386        let explainer = ProofExplainer::new();
387        let mut proof = Proof::new();
388        proof.add_axiom("p");
389        proof.add_axiom("q");
390
391        let explanations = explainer.explain_proof(&proof);
392        assert_eq!(explanations.len(), 2);
393    }
394
395    #[test]
396    fn test_summarize_proof() {
397        let explainer = ProofExplainer::new();
398        let mut proof = Proof::new();
399        proof.add_axiom("p");
400        proof.add_axiom("q");
401
402        let summary = explainer.summarize_proof(&proof);
403        assert!(summary.contains("Proof Summary"));
404        assert!(summary.contains("2 steps"));
405    }
406
407    #[test]
408    fn test_critical_path() {
409        let explainer = ProofExplainer::new();
410        let mut proof = Proof::new();
411        let a1 = proof.add_axiom("p");
412        let a2 = proof.add_axiom("q");
413        let i1 = proof.add_inference("and", vec![a1, a2], "p /\\ q");
414        let i2 = proof.add_inference("not", vec![i1], "!(p /\\ q)");
415
416        let path = explainer.critical_path(&proof);
417        assert!(path.len() >= 2);
418        assert_eq!(*path.last().expect("collection should not be empty"), i2);
419    }
420
421    #[test]
422    fn test_analyze_complexity() {
423        let explainer = ProofExplainer::new();
424        let mut proof = Proof::new();
425        let a1 = proof.add_axiom("p");
426        let a2 = proof.add_axiom("q");
427        proof.add_inference("and", vec![a1, a2], "p /\\ q");
428
429        let complexity = explainer.analyze_complexity(&proof);
430        assert_eq!(complexity.total_steps, 3);
431        assert_eq!(complexity.axiom_count, 2);
432        assert_eq!(complexity.inference_count, 1);
433        assert_eq!(complexity.max_depth, 1);
434    }
435
436    #[test]
437    fn test_empty_proof_complexity() {
438        let explainer = ProofExplainer::new();
439        let proof = Proof::new();
440
441        let complexity = explainer.analyze_complexity(&proof);
442        assert_eq!(complexity.total_steps, 0);
443        assert_eq!(complexity.max_depth, 0);
444    }
445
446    #[test]
447    fn test_rule_descriptions() {
448        let explainer = ProofExplainer::new();
449
450        assert_eq!(explainer.describe_rule("and"), "conjunction introduction");
451        assert_eq!(explainer.describe_rule("resolution"), "resolution");
452        assert_eq!(explainer.describe_rule("farkas"), "Farkas lemma");
453        assert_eq!(explainer.describe_rule("unknown"), "unknown");
454    }
455
456    #[test]
457    fn test_complexity_display() {
458        let complexity = ProofComplexity {
459            total_steps: 10,
460            max_depth: 5,
461            axiom_count: 3,
462            inference_count: 7,
463            avg_premises: 2.5,
464            branching_factor: 2.0,
465        };
466
467        let display = complexity.to_string();
468        assert!(display.contains("Total steps: 10"));
469        assert!(display.contains("Maximum depth: 5"));
470    }
471}