use crate::{
scale::{HistogramEma, NUM_BINS},
shape::ShapeAnchor,
};
const STENCIL_OFFSETS: [i32; 19] = [
-400, -340, -305, -260, -200, -165, -140, -120, -105, -60, 0, 35, 60, 95, 140, 200, 260, 340, 400, ];
pub(crate) const N_ARMS: usize = STENCIL_OFFSETS.len();
#[inline(always)]
fn bin_value(ema: &HistogramEma, idx: i64) -> f64 {
if idx >= 0 && (idx as usize) < NUM_BINS {
ema[idx as usize]
} else {
0.0
}
}
fn arms_at(ema: &HistogramEma, center: i64) -> [f64; N_ARMS] {
STENCIL_OFFSETS.map(|offset| bin_value(ema, center + offset as i64))
}
pub(crate) fn normalized_arms_at(ema: &HistogramEma, center: i64) -> Option<[f64; N_ARMS]> {
let mut arms = arms_at(ema, center);
let sum: f64 = arms.iter().sum();
if sum <= 0.0 {
return None;
}
for arm in &mut arms {
*arm /= sum;
}
Some(arms)
}
pub(crate) fn find_best_bin(
ema: &HistogramEma,
prev_bin: f64,
search_below: usize,
search_above: usize,
shape: &ShapeAnchor,
) -> f64 {
let center = prev_bin.round() as usize;
let search_start = center.saturating_sub(search_below);
let search_end = (center + search_above + 1).min(NUM_BINS);
if search_start >= search_end {
return prev_bin;
}
let mut arm_peaks = [0.0f64; N_ARMS];
for (i, &offset) in STENCIL_OFFSETS.iter().enumerate() {
for bin in search_start..search_end {
arm_peaks[i] = arm_peaks[i].max(bin_value(ema, bin as i64 + offset as i64));
}
}
let score = |bin: usize| -> f64 {
let mut total = 0.0;
for (i, &offset) in STENCIL_OFFSETS.iter().enumerate() {
if arm_peaks[i] > 0.0 {
total += bin_value(ema, bin as i64 + offset as i64) / arm_peaks[i];
}
}
total += shape.score(ema, bin as i64);
total
};
let mut best_bin = search_start;
let mut best_score = score(search_start);
for bin in (search_start + 1)..search_end {
let candidate = score(bin);
if candidate > best_score {
best_score = candidate;
best_bin = bin;
}
}
let score_center = best_score;
let score_left = if best_bin > search_start {
score(best_bin - 1)
} else {
score_center
};
let score_right = if best_bin + 1 < search_end {
score(best_bin + 1)
} else {
score_center
};
let denom = score_left - 2.0 * score_center + score_right;
let sub_bin = if denom.abs() > 1e-10 {
(0.5 * (score_left - score_right) / denom).clamp(-0.5, 0.5)
} else {
0.0
};
best_bin as f64 + sub_bin
}