1use amberfork_model::{Fork, Move, MoveKind};
11
12#[derive(Debug, Clone, Copy, PartialEq)]
16pub struct ForkParams {
17 pub tau: f64,
19 pub resync_k: usize,
21}
22
23impl Default for ForkParams {
24 fn default() -> Self {
25 Self {
26 tau: 0.3,
27 resync_k: 2,
28 }
29 }
30}
31
32pub fn find_fork(alignment: &[Move], params: &ForkParams) -> Option<Fork> {
40 let in_sync = |m: &Move| m.kind == MoveKind::Sync && m.cost <= params.tau;
41
42 let mut idx = 0;
43 while idx < alignment.len() {
44 if in_sync(&alignment[idx]) {
45 idx += 1;
46 continue;
47 }
48 let block_start = idx;
50 while idx < alignment.len() && !in_sync(&alignment[idx]) {
51 idx += 1;
52 }
53 let resynced = alignment[idx..].iter().take_while(|m| in_sync(m)).count();
55 if resynced >= params.resync_k {
56 continue; }
58 let first = &alignment[block_start];
59 return Some(Fork {
60 index: block_start,
61 a_step: first.a_idx,
62 b_step: first.b_idx,
63 confidence: divergence_confidence(first, params.tau),
64 });
65 }
66 None
67}
68
69fn divergence_confidence(m: &Move, tau: f64) -> f64 {
74 let evidence = match m.kind {
75 MoveKind::Sync => m.cost,
76 MoveKind::Log | MoveKind::Model => m.confidence,
77 };
78 ((evidence - tau) / (1.0 - tau).max(f64::EPSILON)).clamp(0.0, 1.0)
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 const OPEN: f64 = 0.6;
86
87 fn sync(i: usize, cost: f64) -> Move {
88 Move::sync(i, i, cost, 1.0 - cost)
89 }
90
91 fn log(i: usize, conf: f64) -> Move {
93 Move::log(i, OPEN, conf)
94 }
95
96 fn kinds_at(fork: &Fork, moves: &[Move]) -> MoveKind {
97 moves[fork.index].kind
98 }
99
100 #[test]
101 fn all_sync_alignment_converges() {
102 let moves = vec![sync(0, 0.0), sync(1, 0.1), sync(2, 0.2)];
103 assert_eq!(find_fork(&moves, &ForkParams::default()), None);
104 }
105
106 #[test]
107 fn empty_alignment_converges() {
108 assert_eq!(find_fork(&[], &ForkParams::default()), None);
109 }
110
111 #[test]
112 fn cost_exactly_tau_is_in_sync() {
113 let params = ForkParams::default();
114 let moves = vec![sync(0, params.tau)];
115 assert_eq!(find_fork(&moves, ¶ms), None);
116 }
117
118 #[test]
119 fn benign_blip_is_recovered_from() {
120 let moves = vec![sync(0, 0.0), log(1, 1.0), sync(1, 0.1), sync(2, 0.0)];
122 assert_eq!(find_fork(&moves, &ForkParams::default()), None);
123 }
124
125 #[test]
126 fn insufficient_resync_is_a_fork() {
127 let moves = vec![sync(0, 0.0), log(1, 1.0), sync(1, 0.1)];
129 let fork = find_fork(&moves, &ForkParams::default()).expect("fork");
130 assert_eq!(fork.index, 1);
131 assert_eq!(fork.b_step, Some(1));
132 assert_eq!(fork.a_step, None);
133 }
134
135 #[test]
136 fn costly_sync_is_divergence() {
137 let moves = vec![sync(0, 0.0), sync(1, 0.9), sync(2, 0.95)];
139 let fork = find_fork(&moves, &ForkParams::default()).expect("fork");
140 assert_eq!(fork.index, 1);
141 assert_eq!((fork.a_step, fork.b_step), (Some(1), Some(1)));
142 }
143
144 #[test]
145 fn divergence_running_to_the_end_is_a_fork() {
146 let moves = vec![sync(0, 0.0), sync(1, 0.0), log(2, 0.8), log(3, 0.9)];
147 let fork = find_fork(&moves, &ForkParams::default()).expect("fork");
148 assert_eq!(fork.index, 2);
149 }
150
151 #[test]
152 fn fork_is_first_unrecovered_block_not_first_blip() {
153 let moves = vec![
155 sync(0, 0.0),
156 log(1, 1.0), sync(1, 0.0), sync(2, 0.1), sync(3, 0.9), log(4, 1.0), ];
162 let fork = find_fork(&moves, &ForkParams::default()).expect("fork");
163 assert_eq!(fork.index, 4, "must skip the recovered blip");
164 assert!(kinds_at(&fork, &moves) == MoveKind::Sync);
165 }
166
167 #[test]
168 fn confidence_scales_with_evidence_over_tau() {
169 let params = ForkParams::default();
170 let marginal = find_fork(&[sync(0, 0.31)], ¶ms).expect("fork");
172 assert!(marginal.confidence < 0.05, "marginal call must read low");
173 let certain = find_fork(&[sync(0, 1.0)], ¶ms).expect("fork");
175 assert_eq!(certain.confidence, 1.0);
176 let gappy = find_fork(&[log(0, 0.65)], ¶ms).expect("fork");
178 let expected = (0.65 - params.tau) / (1.0 - params.tau);
179 assert!((gappy.confidence - expected).abs() < 1e-12);
180 let twinned = find_fork(&[log(0, 0.1)], ¶ms).expect("fork");
182 assert_eq!(twinned.confidence, 0.0);
183 }
184}