use amberfork_model::{Fork, Move, MoveKind};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ForkParams {
pub tau: f64,
pub resync_k: usize,
}
impl Default for ForkParams {
fn default() -> Self {
Self {
tau: 0.3,
resync_k: 2,
}
}
}
pub fn find_fork(alignment: &[Move], params: &ForkParams) -> Option<Fork> {
let in_sync = |m: &Move| m.kind == MoveKind::Sync && m.cost <= params.tau;
let mut idx = 0;
while idx < alignment.len() {
if in_sync(&alignment[idx]) {
idx += 1;
continue;
}
let block_start = idx;
while idx < alignment.len() && !in_sync(&alignment[idx]) {
idx += 1;
}
let resynced = alignment[idx..].iter().take_while(|m| in_sync(m)).count();
if resynced >= params.resync_k {
continue; }
let first = &alignment[block_start];
return Some(Fork {
index: block_start,
a_step: first.a_idx,
b_step: first.b_idx,
confidence: divergence_confidence(first, params.tau),
});
}
None
}
fn divergence_confidence(m: &Move, tau: f64) -> f64 {
let evidence = match m.kind {
MoveKind::Sync => m.cost,
MoveKind::Log | MoveKind::Model => m.confidence,
};
((evidence - tau) / (1.0 - tau).max(f64::EPSILON)).clamp(0.0, 1.0)
}
#[cfg(test)]
mod tests {
use super::*;
const OPEN: f64 = 0.6;
fn sync(i: usize, cost: f64) -> Move {
Move::sync(i, i, cost, 1.0 - cost)
}
fn log(i: usize, conf: f64) -> Move {
Move::log(i, OPEN, conf)
}
fn kinds_at(fork: &Fork, moves: &[Move]) -> MoveKind {
moves[fork.index].kind
}
#[test]
fn all_sync_alignment_converges() {
let moves = vec![sync(0, 0.0), sync(1, 0.1), sync(2, 0.2)];
assert_eq!(find_fork(&moves, &ForkParams::default()), None);
}
#[test]
fn empty_alignment_converges() {
assert_eq!(find_fork(&[], &ForkParams::default()), None);
}
#[test]
fn cost_exactly_tau_is_in_sync() {
let params = ForkParams::default();
let moves = vec![sync(0, params.tau)];
assert_eq!(find_fork(&moves, ¶ms), None);
}
#[test]
fn benign_blip_is_recovered_from() {
let moves = vec![sync(0, 0.0), log(1, 1.0), sync(1, 0.1), sync(2, 0.0)];
assert_eq!(find_fork(&moves, &ForkParams::default()), None);
}
#[test]
fn insufficient_resync_is_a_fork() {
let moves = vec![sync(0, 0.0), log(1, 1.0), sync(1, 0.1)];
let fork = find_fork(&moves, &ForkParams::default()).expect("fork");
assert_eq!(fork.index, 1);
assert_eq!(fork.b_step, Some(1));
assert_eq!(fork.a_step, None);
}
#[test]
fn costly_sync_is_divergence() {
let moves = vec![sync(0, 0.0), sync(1, 0.9), sync(2, 0.95)];
let fork = find_fork(&moves, &ForkParams::default()).expect("fork");
assert_eq!(fork.index, 1);
assert_eq!((fork.a_step, fork.b_step), (Some(1), Some(1)));
}
#[test]
fn divergence_running_to_the_end_is_a_fork() {
let moves = vec![sync(0, 0.0), sync(1, 0.0), log(2, 0.8), log(3, 0.9)];
let fork = find_fork(&moves, &ForkParams::default()).expect("fork");
assert_eq!(fork.index, 2);
}
#[test]
fn fork_is_first_unrecovered_block_not_first_blip() {
let moves = vec![
sync(0, 0.0),
log(1, 1.0), sync(1, 0.0), sync(2, 0.1), sync(3, 0.9), log(4, 1.0), ];
let fork = find_fork(&moves, &ForkParams::default()).expect("fork");
assert_eq!(fork.index, 4, "must skip the recovered blip");
assert!(kinds_at(&fork, &moves) == MoveKind::Sync);
}
#[test]
fn confidence_scales_with_evidence_over_tau() {
let params = ForkParams::default();
let marginal = find_fork(&[sync(0, 0.31)], ¶ms).expect("fork");
assert!(marginal.confidence < 0.05, "marginal call must read low");
let certain = find_fork(&[sync(0, 1.0)], ¶ms).expect("fork");
assert_eq!(certain.confidence, 1.0);
let gappy = find_fork(&[log(0, 0.65)], ¶ms).expect("fork");
let expected = (0.65 - params.tau) / (1.0 - params.tau);
assert!((gappy.confidence - expected).abs() < 1e-12);
let twinned = find_fork(&[log(0, 0.1)], ¶ms).expect("fork");
assert_eq!(twinned.confidence, 0.0);
}
}