Per user feedback "no phase naming, give proper naming to files and functions": renames src/trainer/phase_a.rs -> perception.rs, src/data/phase_a_loader.rs -> data/loader.rs, and the corresponding types (PhaseATrainer -> PerceptionTrainer, PhaseALoader -> MultiHorizonLoader, PhaseAConfig -> MultiHorizonLoaderConfig, PhaseASequence -> LabeledSequence). Test files renamed in lock-step. Adds PerceptionTrainer.step() — full end-to-end forward + heads backward + cfc_step_backward (K=1 truncated BPTT) + 5 AdamW param groups (W_in, W_rec, b, heads_w, heads_b). reset_hidden_state() zeros h_old between independent samples. KNOWN ISSUE — synthetic-overfit smoke (tests/perception_overfit.rs) does NOT yet show loss shrinkage on the 200-step budget: initial_avg=0.6914, final_avg=0.6955 (random-baseline ln(2)=0.693) The kernels are individually correct (heads_bwd + cfc_bwd finite-diff at 5% rel, AdamW invariant ‖θ‖ 40->1 in 200 steps). The end-to-end chain doesn't converge — most likely due to half the CfC cells having near-1 decay from log-uniform tau init on a zeroed hidden state, so only the fast cells carry signal. Fix candidates for next session: - tau init narrower / per-task tuned for the smoke - longer step budget (1000+) with adjusted LR - validate end-to-end with explicit print of grad/probs across iters Task 13 is NOT complete (the gate criterion isn't met). Subsequent work continues from here. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
67 lines
2.2 KiB
Rust
67 lines
2.2 KiB
Rust
//! Phase A loader smoke tests.
|
|
//!
|
|
//! Real-data path is `--ignored`; runs only with FOXHUNT_TEST_DATA set.
|
|
|
|
use ml_alpha::data::loader::{MultiHorizonLoaderConfig, MultiHorizonLoader};
|
|
use std::path::PathBuf;
|
|
|
|
fn cfg_from_env() -> Option<MultiHorizonLoaderConfig> {
|
|
let root = std::env::var("FOXHUNT_TEST_DATA").ok()?;
|
|
let mbp10 = PathBuf::from(format!("{root}/mbp10"));
|
|
let predec = PathBuf::from(format!("{root}/predecoded"));
|
|
if !mbp10.exists() {
|
|
return None;
|
|
}
|
|
Some(MultiHorizonLoaderConfig {
|
|
mbp10_root: mbp10,
|
|
predecoded_dir: predec,
|
|
seq_len: 64,
|
|
horizons: [30, 100, 300, 1000, 6000],
|
|
n_max_sequences: 100,
|
|
seed: 0xA1A2_A3A4,
|
|
})
|
|
}
|
|
|
|
#[test]
|
|
#[ignore]
|
|
fn loader_yields_seq_with_valid_labels() {
|
|
let Some(cfg) = cfg_from_env() else {
|
|
eprintln!("skipping: FOXHUNT_TEST_DATA not set or mbp10 dir missing");
|
|
return;
|
|
};
|
|
let mut loader = MultiHorizonLoader::new(&cfg).expect("loader init");
|
|
assert!(loader.n_files() > 0, "no DBN files found in mbp10 root");
|
|
|
|
let mut count = 0;
|
|
while let Some(seq) = loader.next_sequence().expect("next") {
|
|
assert_eq!(seq.snapshots.len(), cfg.seq_len);
|
|
for h in 0..5 {
|
|
assert_eq!(seq.labels[h].len(), cfg.seq_len);
|
|
for &v in &seq.labels[h] {
|
|
assert!(v.is_nan() || v == 0.0 || v == 1.0, "label not binary: {v}");
|
|
}
|
|
}
|
|
let any_valid = seq.labels.iter().any(|l| l.iter().any(|v| !v.is_nan()));
|
|
assert!(any_valid, "expected at least one non-NaN label in a sequence");
|
|
count += 1;
|
|
if count >= 5 {
|
|
break;
|
|
}
|
|
}
|
|
assert!(count >= 5, "expected at least 5 sequences, got {count}");
|
|
}
|
|
|
|
#[test]
|
|
fn loader_errors_on_missing_root() {
|
|
let cfg = MultiHorizonLoaderConfig {
|
|
mbp10_root: PathBuf::from("/tmp/does_not_exist_foxhunt_test"),
|
|
predecoded_dir: PathBuf::from("/tmp/does_not_exist_foxhunt_test"),
|
|
seq_len: 64,
|
|
horizons: [30, 100, 300, 1000, 6000],
|
|
n_max_sequences: 10,
|
|
seed: 0,
|
|
};
|
|
let res = MultiHorizonLoader::new(&cfg);
|
|
assert!(res.is_err(), "expected error for missing mbp10 root");
|
|
}
|