fix(loader): anchor sampler enforces lookback lower bound

Multi-resolution sequence builder reads from
[anchor + total - lookback, anchor + total), so anchor must be in
[max(0, lookback - total), snapshots.len() - (total + max_horizon)).
The previous gen_range(0..max_anchor) underflowed for default
3-scale (lookback=1510, total=32) at small anchors. Compute
min_anchor = lookback.saturating_sub(total) and sample
gen_range(min_anchor..max_anchor) with the ensure! upgraded to
check max_anchor > min_anchor.

Caught by implementer self-review of SDD-MR Task 3.
This commit is contained in:
jgrusewski
2026-05-22 20:22:16 +02:00
parent 3b1ed72eaa
commit dcbc51f432

View File

@@ -471,14 +471,24 @@ impl MultiHorizonLoader {
let n_files = self.files_loaded.len();
let file_idx = self.rng.gen_range(0..n_files);
let lf = &self.files_loaded[file_idx];
let needed = self.cfg.multi_resolution.total_positions() + max_horizon;
// Multi-resolution anchor sampling has TWO bounds:
// * Upper: anchor + total + max_horizon < snapshots.len()
// (room for the forward-label window after the input edge)
// * Lower: anchor + total >= lookback
// (room to look back across coarsest aggregation scale)
// For the default 3-scale config (total=32, lookback=1510) the lower
// bound is binding: anchor must be ≥ lookback - total = 1478.
let total = self.cfg.multi_resolution.total_positions();
let lookback = self.cfg.multi_resolution.required_lookback_ticks();
let min_anchor = lookback.saturating_sub(total);
let max_anchor = lf.snapshots.len().saturating_sub(total + max_horizon);
anyhow::ensure!(
lf.snapshots.len() > needed,
"file too short for total_positions={} max_horizon={}: {} <= {}",
self.cfg.multi_resolution.total_positions(), max_horizon, lf.snapshots.len(), needed
max_anchor > min_anchor,
"file too short for multi-resolution(total={total}, lookback={lookback}) max_horizon={max_horizon}: \
snapshots.len()={} cannot span [{min_anchor}, {max_anchor})",
lf.snapshots.len()
);
let max_anchor = lf.snapshots.len() - needed;
let anchor: usize = self.rng.gen_range(0..max_anchor);
let anchor: usize = self.rng.gen_range(min_anchor..max_anchor);
let mut labels: [Vec<f32>; N_HORIZONS] = Default::default();
let mut outcome_prof_long: [Vec<f32>; N_HORIZONS] = Default::default();