Skip to main content

oxiz_proof/
diff.rs

1//! Proof diffing utilities for comparing two proofs.
2
3use crate::proof::{Proof, ProofNodeId, ProofStep};
4use std::fmt;
5
6/// Represents a difference between two proofs.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum ProofDiff {
9    /// Node exists only in the first proof
10    OnlyInFirst(ProofNodeId, String),
11    /// Node exists only in the second proof
12    OnlyInSecond(ProofNodeId, String),
13    /// Node exists in both but with different steps
14    Different {
15        id1: ProofNodeId,
16        id2: ProofNodeId,
17        conclusion1: String,
18        conclusion2: String,
19    },
20    /// Structural difference in proof shape
21    StructuralDifference(String),
22}
23
24impl fmt::Display for ProofDiff {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        match self {
27            ProofDiff::OnlyInFirst(id, conclusion) => {
28                write!(f, "- [{}] {}", id, conclusion)
29            }
30            ProofDiff::OnlyInSecond(id, conclusion) => {
31                write!(f, "+ [{}] {}", id, conclusion)
32            }
33            ProofDiff::Different {
34                id1,
35                id2,
36                conclusion1,
37                conclusion2,
38            } => {
39                write!(f, "~ [{}] {} ≠ [{}] {}", id1, conclusion1, id2, conclusion2)
40            }
41            ProofDiff::StructuralDifference(msg) => {
42                write!(f, "! {}", msg)
43            }
44        }
45    }
46}
47
48/// Compare two proofs and return their differences.
49///
50/// This function compares two proofs node-by-node and identifies:
51/// - Nodes that exist only in the first proof
52/// - Nodes that exist only in the second proof
53/// - Nodes with matching IDs but different content
54/// - Structural differences
55///
56/// # Arguments
57///
58/// * `proof1` - The first proof
59/// * `proof2` - The second proof
60///
61/// # Returns
62///
63/// A vector of differences found between the proofs
64///
65/// # Example
66///
67/// ```
68/// use oxiz_proof::proof::Proof;
69/// use oxiz_proof::diff::diff_proofs;
70///
71/// let mut proof1 = Proof::new();
72/// proof1.add_axiom("p");
73/// proof1.add_axiom("q");
74///
75/// let mut proof2 = Proof::new();
76/// proof2.add_axiom("p");
77/// proof2.add_axiom("r");
78///
79/// let diffs = diff_proofs(&proof1, &proof2);
80/// assert!(!diffs.is_empty());
81/// ```
82pub fn diff_proofs(proof1: &Proof, proof2: &Proof) -> Vec<ProofDiff> {
83    let mut diffs = Vec::new();
84
85    // Check for size differences
86    if proof1.len() != proof2.len() {
87        diffs.push(ProofDiff::StructuralDifference(format!(
88            "Size mismatch: {} vs {} nodes",
89            proof1.len(),
90            proof2.len()
91        )));
92    }
93
94    // Check for root count differences
95    if proof1.roots().len() != proof2.roots().len() {
96        diffs.push(ProofDiff::StructuralDifference(format!(
97            "Root count mismatch: {} vs {}",
98            proof1.roots().len(),
99            proof2.roots().len()
100        )));
101    }
102
103    // Build conclusion maps for both proofs
104    let mut conclusions1: std::collections::HashMap<String, ProofNodeId> =
105        std::collections::HashMap::new();
106    let mut conclusions2: std::collections::HashMap<String, ProofNodeId> =
107        std::collections::HashMap::new();
108
109    for node in proof1.nodes() {
110        let conclusion = match &node.step {
111            ProofStep::Axiom { conclusion } => conclusion.clone(),
112            ProofStep::Inference { conclusion, .. } => conclusion.clone(),
113        };
114        conclusions1.insert(conclusion, node.id);
115    }
116
117    for node in proof2.nodes() {
118        let conclusion = match &node.step {
119            ProofStep::Axiom { conclusion } => conclusion.clone(),
120            ProofStep::Inference { conclusion, .. } => conclusion.clone(),
121        };
122        conclusions2.insert(conclusion, node.id);
123    }
124
125    // Find conclusions only in proof1
126    for (conclusion, id) in &conclusions1 {
127        if !conclusions2.contains_key(conclusion) {
128            diffs.push(ProofDiff::OnlyInFirst(*id, conclusion.clone()));
129        }
130    }
131
132    // Find conclusions only in proof2
133    for (conclusion, id) in &conclusions2 {
134        if !conclusions1.contains_key(conclusion) {
135            diffs.push(ProofDiff::OnlyInSecond(*id, conclusion.clone()));
136        }
137    }
138
139    diffs
140}
141
142/// Compute similarity metrics between two proofs.
143#[derive(Debug, Clone, Copy)]
144pub struct ProofSimilarity {
145    /// Jaccard similarity based on conclusions (0.0 to 1.0)
146    pub jaccard_similarity: f64,
147    /// Ratio of common nodes to total nodes (0.0 to 1.0)
148    pub node_overlap: f64,
149    /// Structural similarity based on depth and shape (0.0 to 1.0)
150    pub structural_similarity: f64,
151}
152
153impl fmt::Display for ProofSimilarity {
154    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155        writeln!(f, "Proof Similarity Metrics:")?;
156        writeln!(
157            f,
158            "  Jaccard similarity: {:.2}%",
159            self.jaccard_similarity * 100.0
160        )?;
161        writeln!(f, "  Node overlap: {:.2}%", self.node_overlap * 100.0)?;
162        writeln!(
163            f,
164            "  Structural similarity: {:.2}%",
165            self.structural_similarity * 100.0
166        )
167    }
168}
169
170/// Compute similarity metrics between two proofs.
171///
172/// # Arguments
173///
174/// * `proof1` - The first proof
175/// * `proof2` - The second proof
176///
177/// # Returns
178///
179/// Similarity metrics comparing the two proofs
180pub fn compute_similarity(proof1: &Proof, proof2: &Proof) -> ProofSimilarity {
181    // Collect conclusions from both proofs
182    let mut conclusions1 = std::collections::HashSet::new();
183    let mut conclusions2 = std::collections::HashSet::new();
184
185    for node in proof1.nodes() {
186        let conclusion = match &node.step {
187            ProofStep::Axiom { conclusion } => conclusion.clone(),
188            ProofStep::Inference { conclusion, .. } => conclusion.clone(),
189        };
190        conclusions1.insert(conclusion);
191    }
192
193    for node in proof2.nodes() {
194        let conclusion = match &node.step {
195            ProofStep::Axiom { conclusion } => conclusion.clone(),
196            ProofStep::Inference { conclusion, .. } => conclusion.clone(),
197        };
198        conclusions2.insert(conclusion);
199    }
200
201    // Compute Jaccard similarity
202    let intersection = conclusions1.intersection(&conclusions2).count();
203    let union = conclusions1.union(&conclusions2).count();
204    let jaccard_similarity = if union == 0 {
205        1.0
206    } else {
207        intersection as f64 / union as f64
208    };
209
210    // Compute node overlap
211    let total_nodes = proof1.len() + proof2.len();
212    let node_overlap = if total_nodes == 0 {
213        1.0
214    } else {
215        (2 * intersection) as f64 / total_nodes as f64
216    };
217
218    // Compute structural similarity based on depth
219    let stats1 = proof1.stats();
220    let stats2 = proof2.stats();
221
222    let depth_diff = (stats1.max_depth as f64 - stats2.max_depth as f64).abs();
223    let max_depth = stats1.max_depth.max(stats2.max_depth) as f64;
224    let depth_similarity = if max_depth == 0.0 {
225        1.0
226    } else {
227        1.0 - (depth_diff / max_depth)
228    };
229
230    let root_diff = (stats1.root_count as f64 - stats2.root_count as f64).abs();
231    let max_roots = stats1.root_count.max(stats2.root_count) as f64;
232    let root_similarity = if max_roots == 0.0 {
233        1.0
234    } else {
235        1.0 - (root_diff / max_roots)
236    };
237
238    let structural_similarity = (depth_similarity + root_similarity) / 2.0;
239
240    ProofSimilarity {
241        jaccard_similarity,
242        node_overlap,
243        structural_similarity,
244    }
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250
251    #[test]
252    fn test_diff_identical_proofs() {
253        let mut proof1 = Proof::new();
254        proof1.add_axiom("p");
255        proof1.add_axiom("q");
256
257        let mut proof2 = Proof::new();
258        proof2.add_axiom("p");
259        proof2.add_axiom("q");
260
261        let diffs = diff_proofs(&proof1, &proof2);
262        // Should have no content differences, only possibly ID differences
263        assert!(
264            diffs
265                .iter()
266                .all(|d| matches!(d, ProofDiff::StructuralDifference(_)))
267                || diffs.is_empty()
268        );
269    }
270
271    #[test]
272    fn test_diff_different_proofs() {
273        let mut proof1 = Proof::new();
274        proof1.add_axiom("p");
275        proof1.add_axiom("q");
276
277        let mut proof2 = Proof::new();
278        proof2.add_axiom("p");
279        proof2.add_axiom("r");
280
281        let diffs = diff_proofs(&proof1, &proof2);
282        assert!(!diffs.is_empty());
283    }
284
285    #[test]
286    fn test_similarity_identical() {
287        let mut proof1 = Proof::new();
288        proof1.add_axiom("p");
289        proof1.add_axiom("q");
290
291        let mut proof2 = Proof::new();
292        proof2.add_axiom("p");
293        proof2.add_axiom("q");
294
295        let sim = compute_similarity(&proof1, &proof2);
296        assert_eq!(sim.jaccard_similarity, 1.0);
297        assert_eq!(sim.node_overlap, 1.0);
298    }
299
300    #[test]
301    fn test_similarity_disjoint() {
302        let mut proof1 = Proof::new();
303        proof1.add_axiom("p");
304        proof1.add_axiom("q");
305
306        let mut proof2 = Proof::new();
307        proof2.add_axiom("r");
308        proof2.add_axiom("s");
309
310        let sim = compute_similarity(&proof1, &proof2);
311        assert_eq!(sim.jaccard_similarity, 0.0);
312    }
313
314    #[test]
315    fn test_similarity_partial_overlap() {
316        let mut proof1 = Proof::new();
317        proof1.add_axiom("p");
318        proof1.add_axiom("q");
319
320        let mut proof2 = Proof::new();
321        proof2.add_axiom("p");
322        proof2.add_axiom("r");
323
324        let sim = compute_similarity(&proof1, &proof2);
325        assert!(sim.jaccard_similarity > 0.0 && sim.jaccard_similarity < 1.0);
326    }
327
328    #[test]
329    fn test_diff_display() {
330        let diff = ProofDiff::OnlyInFirst(ProofNodeId(0), "p".to_string());
331        let display = format!("{}", diff);
332        assert!(display.contains("p"));
333    }
334
335    #[test]
336    fn test_similarity_display() {
337        let sim = ProofSimilarity {
338            jaccard_similarity: 0.75,
339            node_overlap: 0.8,
340            structural_similarity: 0.9,
341        };
342        let display = format!("{}", sim);
343        assert!(display.contains("75"));
344    }
345
346    mod proptests {
347        use super::*;
348        use proptest::prelude::*;
349
350        proptest! {
351            #[test]
352            fn prop_similarity_bounds(size in 1usize..20) {
353                let mut proof1 = Proof::new();
354                let mut proof2 = Proof::new();
355
356                for i in 0..size {
357                    proof1.add_axiom(format!("p{}", i));
358                    proof2.add_axiom(format!("q{}", i));
359                }
360
361                let sim = compute_similarity(&proof1, &proof2);
362
363                // All similarity metrics should be between 0 and 1
364                prop_assert!(sim.jaccard_similarity >= 0.0 && sim.jaccard_similarity <= 1.0);
365                prop_assert!(sim.node_overlap >= 0.0 && sim.node_overlap <= 1.0);
366                prop_assert!(sim.structural_similarity >= 0.0 && sim.structural_similarity <= 1.0);
367            }
368
369            #[test]
370            fn prop_identical_proofs_similarity(size in 1usize..20) {
371                let mut proof1 = Proof::new();
372                for i in 0..size {
373                    proof1.add_axiom(format!("p{}", i));
374                }
375
376                let mut proof2 = Proof::new();
377                for i in 0..size {
378                    proof2.add_axiom(format!("p{}", i));
379                }
380
381                let sim = compute_similarity(&proof1, &proof2);
382
383                // Identical proofs should have perfect similarity
384                prop_assert_eq!(sim.jaccard_similarity, 1.0);
385                prop_assert_eq!(sim.node_overlap, 1.0);
386            }
387
388            #[test]
389            fn prop_disjoint_proofs_zero_jaccard(size1 in 1usize..10, size2 in 1usize..10) {
390                let mut proof1 = Proof::new();
391                for i in 0..size1 {
392                    proof1.add_axiom(format!("p{}", i));
393                }
394
395                let mut proof2 = Proof::new();
396                for i in 0..size2 {
397                    proof2.add_axiom(format!("q{}", i));
398                }
399
400                let sim = compute_similarity(&proof1, &proof2);
401
402                // Disjoint proofs should have zero Jaccard similarity
403                prop_assert_eq!(sim.jaccard_similarity, 0.0);
404            }
405
406            #[test]
407            fn prop_diff_empty_proofs(_n in 0usize..1) {
408                let proof1 = Proof::new();
409                let proof2 = Proof::new();
410
411                let diffs = diff_proofs(&proof1, &proof2);
412
413                // Empty proofs should have no differences
414                prop_assert!(diffs.is_empty());
415            }
416
417            #[test]
418            fn prop_diff_self_is_empty(size in 1usize..20) {
419                let mut proof = Proof::new();
420                for i in 0..size {
421                    proof.add_axiom(format!("p{}", i));
422                }
423
424                let diffs = diff_proofs(&proof, &proof);
425
426                // A proof compared with itself should only have structural diffs (if any)
427                prop_assert!(diffs.iter().all(|d| matches!(d, ProofDiff::StructuralDifference(_))) || diffs.is_empty());
428            }
429
430            #[test]
431            fn prop_similarity_symmetric(size in 1usize..15) {
432                let mut proof1 = Proof::new();
433                let mut proof2 = Proof::new();
434
435                for i in 0..size {
436                    proof1.add_axiom(format!("p{}", i));
437                    if i % 2 == 0 {
438                        proof2.add_axiom(format!("p{}", i));
439                    } else {
440                        proof2.add_axiom(format!("q{}", i));
441                    }
442                }
443
444                let sim1 = compute_similarity(&proof1, &proof2);
445                let sim2 = compute_similarity(&proof2, &proof1);
446
447                // Similarity should be symmetric
448                prop_assert_eq!(sim1.jaccard_similarity, sim2.jaccard_similarity);
449                prop_assert_eq!(sim1.node_overlap, sim2.node_overlap);
450            }
451        }
452    }
453}