mod params;
mod predict;
pub use params::{
Admit, FillEdgeCtx, GrowParams, GrowResult, LabelledNeighbour, SquareAttachPolicy,
};
pub(crate) use predict::{collect_labelled_neighbours, is_extrapolating, predict_from_neighbours};
use kiddo::{KdTree, SquaredEuclidean};
use nalgebra::Point2;
use std::collections::{HashMap, HashSet, VecDeque};
pub(crate) fn enqueue_cardinal_neighbours(
pos: (i32, i32),
labelled: &HashMap<(i32, i32), usize>,
boundary: &mut VecDeque<(i32, i32)>,
seen: &mut HashSet<(i32, i32)>,
) {
for (di, dj) in [(1, 0), (-1, 0), (0, 1), (0, -1)] {
let neigh = (pos.0 + di, pos.1 + dj);
if !labelled.contains_key(&neigh) && seen.insert(neigh) {
boundary.push_back(neigh);
}
}
}
pub(crate) fn collect_candidates<V: SquareAttachPolicy>(
tree: &KdTree<f32, 2>,
slot_to_corner: &[usize],
prediction: Point2<f32>,
search_r: f32,
policy: &V,
required_label: Option<u8>,
by_corner: &HashMap<usize, (i32, i32)>,
) -> Vec<(usize, f32)> {
let r2 = search_r * search_r;
let mut out: Vec<(usize, f32)> = Vec::new();
for nn in tree
.within_unsorted::<SquaredEuclidean>(&[prediction.x, prediction.y], r2)
.into_iter()
{
let idx = slot_to_corner[nn.item as usize];
if by_corner.contains_key(&idx) {
continue;
}
if let Some(req) = required_label {
let Some(got) = policy.label_of(idx) else {
continue;
};
if got != req {
continue;
}
}
let d = nn.distance.sqrt();
out.push((idx, d));
}
out.sort_by(|a, b| a.1.total_cmp(&b.1).then_with(|| a.0.cmp(&b.0)));
out
}
pub(crate) enum CandidateChoice {
None,
Ambiguous,
Unique(usize),
}
pub(crate) fn choose_unambiguous<V: SquareAttachPolicy>(
candidates: &[(usize, f32)],
ambiguity_factor: f32,
prediction: Point2<f32>,
positions: &[Point2<f32>],
policy: &V,
at: (i32, i32),
neighbours: &[LabelledNeighbour],
) -> CandidateChoice {
if candidates.is_empty() {
return CandidateChoice::None;
}
if candidates.len() >= 2 {
let (_, d0) = candidates[0];
let (_, d1) = candidates[1];
if d0 <= f32::EPSILON {
return CandidateChoice::Ambiguous;
}
if d1 / d0 < ambiguity_factor {
return CandidateChoice::Ambiguous;
}
}
for &(idx, _dist) in candidates {
let pos = positions[idx];
let _ = pos; match policy.accept_candidate(idx, at, prediction, neighbours) {
Admit::Accept => return CandidateChoice::Unique(idx),
Admit::Reject => continue,
}
}
CandidateChoice::None
}
pub(crate) fn any_cardinal_edge_ok<V: SquareAttachPolicy>(
c_idx: usize,
pos: (i32, i32),
labelled: &HashMap<(i32, i32), usize>,
policy: &V,
) -> bool {
let mut found_any = false;
for (di, dj) in [(1, 0), (-1, 0), (0, 1), (0, -1)] {
let neigh = (pos.0 + di, pos.1 + dj);
if let Some(&n_idx) = labelled.get(&neigh) {
found_any = true;
if policy.edge_ok(c_idx, n_idx, pos, neigh) {
return true;
}
}
}
!found_any
}
#[cfg(test)]
mod tests {
use super::*;
use nalgebra::Vector2;
#[test]
fn predict_weights_diagonal_less_than_cardinal() {
let s = 10.0_f32;
let u = Vector2::new(1.0, 0.0);
let v = Vector2::new(0.0, 1.0);
let target = (5, 5);
let cardinal = LabelledNeighbour {
idx: 0,
at: (5, 4),
position: Point2::new(50.0, 40.0),
};
let diagonal = LabelledNeighbour {
idx: 1,
at: (3, 3),
position: Point2::new(30.0, 34.0),
};
let positions = vec![cardinal.position, diagonal.position];
let mut labelled = HashMap::new();
labelled.insert(cardinal.at, 0usize);
labelled.insert(diagonal.at, 1usize);
let pred = predict_from_neighbours(
target,
&[cardinal, diagonal],
u,
v,
s,
&labelled,
&positions,
);
let expected_y = (50.0 + 0.125 * 54.0) / 1.125;
assert!(
(pred.x - 50.0).abs() < 1e-4,
"predicted x {} should equal 50",
pred.x
);
assert!(
(pred.y - expected_y).abs() < 1e-4,
"predicted y {} should equal {} (1/d² weighted)",
pred.y,
expected_y
);
let equal_weight_y = (50.0 + 54.0) * 0.5;
assert!(
(pred.y - 50.0) < (equal_weight_y - 50.0),
"weighted bias {} should be smaller than equal-weight bias {}",
pred.y - 50.0,
equal_weight_y - 50.0,
);
}
#[test]
fn predict_with_only_cardinal_recovers_exact_offset() {
let s = 12.0_f32;
let u = Vector2::new(1.0, 0.0);
let v = Vector2::new(0.0, 1.0);
let target = (2, 2);
let neighbour = LabelledNeighbour {
idx: 0,
at: (1, 2),
position: Point2::new(s, 2.0 * s),
};
let positions = vec![neighbour.position];
let mut labelled = HashMap::new();
labelled.insert(neighbour.at, 0usize);
let pred = predict_from_neighbours(target, &[neighbour], u, v, s, &labelled, &positions);
assert!((pred.x - 2.0 * s).abs() < 1e-4);
assert!((pred.y - 2.0 * s).abs() < 1e-4);
}
#[test]
fn predict_uses_local_step_when_neighbour_has_own_neighbours() {
let u = Vector2::new(1.0, 0.0);
let v = Vector2::new(0.0, 1.0);
let global_cell_size = 50.0_f32;
let neighbour = LabelledNeighbour {
idx: 0,
at: (3, 0),
position: Point2::new(300.0, 0.0),
};
let mut positions = vec![neighbour.position];
let mut labelled: HashMap<(i32, i32), usize> = HashMap::new();
labelled.insert((3, 0), 0);
positions.push(Point2::new(310.0, 0.0));
labelled.insert((4, 0), 1);
positions.push(Point2::new(320.0, 0.0));
labelled.insert((5, 0), 2);
let pred = predict_from_neighbours(
(2, 0),
&[neighbour],
u,
v,
global_cell_size,
&labelled,
&positions,
);
assert!(
(pred.x - 290.0).abs() < 1e-3,
"expected adaptive prediction at x=290, got {}",
pred.x
);
assert!((pred.y - 0.0).abs() < 1e-3);
}
#[test]
fn predict_falls_back_to_global_when_no_local_steps() {
let u = Vector2::new(1.0, 0.0);
let v = Vector2::new(0.0, 1.0);
let s = 25.0_f32;
let neighbour = LabelledNeighbour {
idx: 0,
at: (4, 4),
position: Point2::new(100.0, 100.0),
};
let positions = vec![neighbour.position];
let mut labelled: HashMap<(i32, i32), usize> = HashMap::new();
labelled.insert((4, 4), 0);
let pred = predict_from_neighbours((5, 4), &[neighbour], u, v, s, &labelled, &positions);
assert!((pred.x - (100.0 + s)).abs() < 1e-3);
assert!((pred.y - 100.0).abs() < 1e-3);
}
}