1use crate::proof::{Proof, ProofNodeId, ProofStep};
4use std::fmt;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub enum ProofDiff {
9 OnlyInFirst(ProofNodeId, String),
11 OnlyInSecond(ProofNodeId, String),
13 Different {
15 id1: ProofNodeId,
16 id2: ProofNodeId,
17 conclusion1: String,
18 conclusion2: String,
19 },
20 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
48pub fn diff_proofs(proof1: &Proof, proof2: &Proof) -> Vec<ProofDiff> {
83 let mut diffs = Vec::new();
84
85 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 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 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 for (conclusion, id) in &conclusions1 {
127 if !conclusions2.contains_key(conclusion) {
128 diffs.push(ProofDiff::OnlyInFirst(*id, conclusion.clone()));
129 }
130 }
131
132 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#[derive(Debug, Clone, Copy)]
144pub struct ProofSimilarity {
145 pub jaccard_similarity: f64,
147 pub node_overlap: f64,
149 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
170pub fn compute_similarity(proof1: &Proof, proof2: &Proof) -> ProofSimilarity {
181 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 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 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 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 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 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 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 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 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 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 prop_assert_eq!(sim1.jaccard_similarity, sim2.jaccard_similarity);
449 prop_assert_eq!(sim1.node_overlap, sim2.node_overlap);
450 }
451 }
452 }
453}