Skip to main content

oxiz_proof/
stats.rs

1//! Advanced proof statistics and analysis.
2//!
3//! This module provides detailed statistics and metrics for analyzing
4//! proof quality, complexity, and structure.
5
6use crate::proof::{Proof, ProofStep};
7use crate::theory::{TheoryProof, TheoryRule};
8use std::collections::HashMap;
9
10/// Detailed proof statistics.
11#[derive(Debug, Clone)]
12pub struct DetailedProofStats {
13    /// Total number of steps.
14    pub total_steps: usize,
15    /// Number of axiom/assumption steps.
16    pub axioms: usize,
17    /// Number of inference steps.
18    pub inferences: usize,
19    /// Maximum proof depth.
20    pub max_depth: usize,
21    /// Average depth of leaves.
22    pub avg_leaf_depth: f64,
23    /// Total number of premises used.
24    pub total_premises: usize,
25    /// Average premises per inference.
26    pub avg_premises: f64,
27    /// Number of unique conclusions.
28    pub unique_conclusions: usize,
29    /// Rule usage counts.
30    pub rule_usage: HashMap<String, usize>,
31    /// Proof complexity score (higher = more complex).
32    pub complexity_score: f64,
33}
34
35impl DetailedProofStats {
36    /// Compute detailed statistics for a proof.
37    #[must_use]
38    pub fn compute(proof: &Proof) -> Self {
39        let total_steps = proof.node_count();
40        let max_depth = proof.depth();
41        let leaf_nodes = proof.leaf_nodes();
42        let axioms = leaf_nodes.len();
43        let inferences = total_steps - axioms;
44
45        let mut total_premises = 0;
46        let mut unique_conclusions = std::collections::HashSet::new();
47        let mut rule_usage: HashMap<String, usize> = HashMap::new();
48
49        for node in proof.nodes() {
50            match &node.step {
51                ProofStep::Axiom { conclusion } => {
52                    unique_conclusions.insert(conclusion.clone());
53                }
54                ProofStep::Inference {
55                    rule,
56                    premises,
57                    conclusion,
58                    ..
59                } => {
60                    total_premises += premises.len();
61                    unique_conclusions.insert(conclusion.clone());
62                    *rule_usage.entry(rule.clone()).or_insert(0) += 1;
63                }
64            }
65        }
66
67        let avg_premises = if inferences > 0 {
68            total_premises as f64 / inferences as f64
69        } else {
70            0.0
71        };
72
73        // Compute average leaf depth
74        let mut total_leaf_depth = 0;
75        if let Some(root) = proof.root() {
76            for &leaf in &leaf_nodes {
77                total_leaf_depth += compute_depth_to_node(proof, root, leaf);
78            }
79        }
80        let avg_leaf_depth = if !leaf_nodes.is_empty() {
81            total_leaf_depth as f64 / leaf_nodes.len() as f64
82        } else {
83            0.0
84        };
85
86        // Compute complexity score
87        // Factors: depth, branching, unique rules, premise count
88        let depth_factor = max_depth as f64;
89        let branching_factor = avg_premises;
90        let rule_diversity = rule_usage.len() as f64;
91        let complexity_score = (depth_factor * branching_factor * rule_diversity).sqrt();
92
93        Self {
94            total_steps,
95            axioms,
96            inferences,
97            max_depth: max_depth as usize,
98            avg_leaf_depth,
99            total_premises,
100            avg_premises,
101            unique_conclusions: unique_conclusions.len(),
102            rule_usage,
103            complexity_score,
104        }
105    }
106
107    /// Compute detailed statistics for a theory proof.
108    #[must_use]
109    pub fn compute_theory(proof: &TheoryProof) -> TheoryProofStats {
110        let total_steps = proof.len();
111        let mut axioms = 0;
112        let mut inferences = 0;
113        let mut total_premises = 0;
114        let mut rule_usage: HashMap<String, usize> = HashMap::new();
115        let mut theory_usage: HashMap<String, usize> = HashMap::new();
116
117        for step in proof.steps() {
118            if step.premises.is_empty() {
119                axioms += 1;
120            } else {
121                inferences += 1;
122                total_premises += step.premises.len();
123            }
124
125            let rule_name = format!("{}", step.rule);
126            *rule_usage.entry(rule_name).or_insert(0) += 1;
127
128            // Categorize by theory
129            let theory = categorize_theory_rule(&step.rule);
130            *theory_usage.entry(theory).or_insert(0) += 1;
131        }
132
133        let avg_premises = if inferences > 0 {
134            total_premises as f64 / inferences as f64
135        } else {
136            0.0
137        };
138
139        TheoryProofStats {
140            total_steps,
141            axioms,
142            inferences,
143            total_premises,
144            avg_premises,
145            rule_usage,
146            theory_usage,
147        }
148    }
149}
150
151/// Statistics specific to theory proofs.
152#[derive(Debug, Clone)]
153pub struct TheoryProofStats {
154    /// Total number of steps.
155    pub total_steps: usize,
156    /// Number of axiom steps.
157    pub axioms: usize,
158    /// Number of inference steps.
159    pub inferences: usize,
160    /// Total premises used.
161    pub total_premises: usize,
162    /// Average premises per inference.
163    pub avg_premises: f64,
164    /// Rule usage counts.
165    pub rule_usage: HashMap<String, usize>,
166    /// Theory usage counts (EUF, Arithmetic, Arrays, etc.).
167    pub theory_usage: HashMap<String, usize>,
168}
169
170/// Compute depth from root to a specific node.
171fn compute_depth_to_node(
172    proof: &Proof,
173    current: crate::proof::ProofNodeId,
174    target: crate::proof::ProofNodeId,
175) -> usize {
176    if current == target {
177        return 0;
178    }
179
180    if let Some(premises) = proof.premises(current) {
181        for &premise in premises {
182            let depth = compute_depth_to_node(proof, premise, target);
183            if depth > 0 || premise == target {
184                return depth + 1;
185            }
186        }
187    }
188
189    0
190}
191
192/// Categorize a theory rule by its theory.
193fn categorize_theory_rule(rule: &TheoryRule) -> String {
194    match rule {
195        TheoryRule::Refl
196        | TheoryRule::Symm
197        | TheoryRule::Trans
198        | TheoryRule::Cong
199        | TheoryRule::FuncEq
200        | TheoryRule::Distinct => "EUF".to_string(),
201
202        TheoryRule::LaGeneric
203        | TheoryRule::LaMult
204        | TheoryRule::LaAdd
205        | TheoryRule::LaTighten
206        | TheoryRule::LaDiv
207        | TheoryRule::LaTotality
208        | TheoryRule::LaDiseq => "Arithmetic".to_string(),
209
210        TheoryRule::BvBlastEq
211        | TheoryRule::BvExtract
212        | TheoryRule::BvConcat
213        | TheoryRule::BvZeroExtend
214        | TheoryRule::BvSignExtend
215        | TheoryRule::BvBitwise
216        | TheoryRule::BvArith
217        | TheoryRule::BvCompare
218        | TheoryRule::BvOverflow => "BitVector".to_string(),
219
220        TheoryRule::ArrReadWrite1
221        | TheoryRule::ArrReadWrite2
222        | TheoryRule::ArrExt
223        | TheoryRule::ArrConst => "Array".to_string(),
224
225        TheoryRule::ForallElim
226        | TheoryRule::ExistsIntro
227        | TheoryRule::Skolemize
228        | TheoryRule::QuantInst
229        | TheoryRule::AlphaEquiv => "Quantifier".to_string(),
230
231        TheoryRule::TheoryConflict | TheoryRule::TheoryProp => "Theory".to_string(),
232
233        TheoryRule::IteElim | TheoryRule::BoolSimp => "Boolean".to_string(),
234
235        TheoryRule::Custom(name) => format!("Custom({})", name),
236    }
237}
238
239/// Proof quality metrics.
240#[derive(Debug, Clone)]
241pub struct ProofQuality {
242    /// Redundancy ratio (0.0 = no redundancy, 1.0 = highly redundant).
243    pub redundancy: f64,
244    /// Efficiency score (higher = more efficient).
245    pub efficiency: f64,
246    /// Compactness score (higher = more compact).
247    pub compactness: f64,
248    /// Overall quality score (0-100).
249    pub overall_score: f64,
250}
251
252impl ProofQuality {
253    /// Compute quality metrics for a proof.
254    #[must_use]
255    pub fn compute(stats: &DetailedProofStats) -> Self {
256        // Redundancy: ratio of duplicate conclusions
257        let redundancy = if stats.total_steps > 0 {
258            1.0 - (stats.unique_conclusions as f64 / stats.total_steps as f64)
259        } else {
260            0.0
261        };
262
263        // Efficiency: inversely proportional to average premises
264        let efficiency = if stats.avg_premises > 0.0 {
265            1.0 / (1.0 + stats.avg_premises)
266        } else {
267            1.0
268        };
269
270        // Compactness: inversely proportional to proof size relative to depth
271        let compactness = if stats.total_steps > 0 && stats.max_depth > 0 {
272            stats.max_depth as f64 / stats.total_steps as f64
273        } else {
274            1.0
275        };
276
277        // Overall score (0-100)
278        let overall_score =
279            ((1.0 - redundancy) * 0.3 + efficiency * 0.4 + compactness * 0.3) * 100.0;
280
281        Self {
282            redundancy,
283            efficiency,
284            compactness,
285            overall_score,
286        }
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn test_detailed_proof_stats() {
296        let mut proof = Proof::new();
297        let p1 = proof.add_axiom("p");
298        let p2 = proof.add_axiom("q");
299        let _p3 = proof.add_inference("and", vec![p1, p2], "(and p q)");
300
301        let stats = DetailedProofStats::compute(&proof);
302
303        assert_eq!(stats.total_steps, 3);
304        assert_eq!(stats.axioms, 2);
305        assert_eq!(stats.inferences, 1);
306        assert_eq!(stats.unique_conclusions, 3);
307        assert_eq!(stats.rule_usage.get("and"), Some(&1));
308    }
309
310    #[test]
311    fn test_theory_proof_stats() {
312        let mut proof = TheoryProof::new();
313
314        let s1 = proof.add_axiom(TheoryRule::Custom("assert".into()), "(= a b)");
315        let s2 = proof.add_axiom(TheoryRule::Custom("assert".into()), "(= b c)");
316        proof.trans(s1, s2, "a", "c");
317
318        let stats = DetailedProofStats::compute_theory(&proof);
319
320        assert_eq!(stats.total_steps, 3);
321        assert_eq!(stats.axioms, 2);
322        assert_eq!(stats.inferences, 1);
323        assert_eq!(stats.theory_usage.get("EUF"), Some(&1));
324        assert_eq!(stats.theory_usage.get("Custom(assert)"), Some(&2));
325    }
326
327    #[test]
328    fn test_categorize_theory_rule() {
329        assert_eq!(categorize_theory_rule(&TheoryRule::Refl), "EUF");
330        assert_eq!(categorize_theory_rule(&TheoryRule::LaGeneric), "Arithmetic");
331        assert_eq!(categorize_theory_rule(&TheoryRule::BvBlastEq), "BitVector");
332        assert_eq!(categorize_theory_rule(&TheoryRule::ArrReadWrite1), "Array");
333    }
334
335    #[test]
336    fn test_proof_quality() {
337        let mut proof = Proof::new();
338        let p1 = proof.add_axiom("p");
339        let p2 = proof.add_axiom("q");
340        let _p3 = proof.add_inference("and", vec![p1, p2], "(and p q)");
341
342        let stats = DetailedProofStats::compute(&proof);
343        let quality = ProofQuality::compute(&stats);
344
345        assert!(quality.redundancy >= 0.0 && quality.redundancy <= 1.0);
346        assert!(quality.efficiency > 0.0);
347        assert!(quality.compactness > 0.0);
348        assert!(quality.overall_score >= 0.0 && quality.overall_score <= 100.0);
349    }
350
351    #[test]
352    fn test_complexity_score() {
353        let mut proof = Proof::new();
354        let p1 = proof.add_axiom("p");
355        let p2 = proof.add_axiom("q");
356        let _p3 = proof.add_inference("and", vec![p1, p2], "(and p q)");
357
358        let stats = DetailedProofStats::compute(&proof);
359
360        assert!(stats.complexity_score > 0.0);
361    }
362
363    #[test]
364    fn test_avg_leaf_depth() {
365        let mut proof = Proof::new();
366        let p1 = proof.add_axiom("p");
367        let p2 = proof.add_axiom("q");
368        let p3 = proof.add_inference("and", vec![p1, p2], "(and p q)");
369        let _p4 = proof.add_inference("or", vec![p3], "(or (and p q))");
370
371        let stats = DetailedProofStats::compute(&proof);
372
373        assert!(stats.avg_leaf_depth > 0.0);
374    }
375}