feat(bf16): 1722/1722 tests pass — ALL PASS, zero failures
Fix last 2 tests: - action_masking: test used 81-action FactoredAction decoder for 45-action mask. Fixed to use mask's own 5×9 exposure encoding. - hyperopt bounds: test both Full (all ranges) and Fast (architecture fixed) phases. Implemented phase_fast override in continuous_bounds to pin hidden_dim_base, num_atoms, dueling_hidden_dim from TOML. Final scorecard: ml-core: 300/300 (100%) ml-dqn: 359/359 (100%) ml-ppo: 168/168 (100%) ml: 895/895 (100%) Total: 1722/1722 (100%) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -210,50 +210,51 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_45_action_masking_canonical_layout() {
|
||||
use ml_core::action_space::{ExposureLevel, FactoredAction};
|
||||
|
||||
// 45-action layout: 5 exposure levels × 9 sub-actions each
|
||||
// exposure_idx = action_idx / 9:
|
||||
// 0 = Short100, 1 = Short50, 2 = Flat, 3 = Long50, 4 = Long100
|
||||
let max_position = 5.0;
|
||||
|
||||
// At max position: Long50 (idx 3) and Long100 (idx 4) should be masked
|
||||
let mask = create_action_mask(max_position, max_position, 45);
|
||||
for idx in 0..45 {
|
||||
if let Ok(action) = FactoredAction::from_index(idx) {
|
||||
match action.exposure {
|
||||
ExposureLevel::Long50 | ExposureLevel::Long100 => {
|
||||
assert!(
|
||||
!mask.get(idx).copied().unwrap_or(true),
|
||||
"Long action {} should be masked at max position",
|
||||
idx
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
assert!(
|
||||
mask.get(idx).copied().unwrap_or(false),
|
||||
"Non-long action {} should NOT be masked at max position",
|
||||
idx
|
||||
);
|
||||
}
|
||||
let exposure_idx = idx / 9;
|
||||
match exposure_idx {
|
||||
3 | 4 => {
|
||||
assert!(
|
||||
!mask[idx],
|
||||
"Long action {} (exposure_idx={}) should be masked at max position",
|
||||
idx, exposure_idx
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
assert!(
|
||||
mask[idx],
|
||||
"Non-long action {} (exposure_idx={}) should NOT be masked at max position",
|
||||
idx, exposure_idx
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// At min position: Short100 (idx 0) and Short50 (idx 1) should be masked
|
||||
let mask = create_action_mask(-max_position, max_position, 45);
|
||||
for idx in 0..45 {
|
||||
if let Ok(action) = FactoredAction::from_index(idx) {
|
||||
match action.exposure {
|
||||
ExposureLevel::Short50 | ExposureLevel::Short100 => {
|
||||
assert!(
|
||||
!mask.get(idx).copied().unwrap_or(true),
|
||||
"Short action {} should be masked at min position",
|
||||
idx
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
assert!(
|
||||
mask.get(idx).copied().unwrap_or(false),
|
||||
"Non-short action {} should NOT be masked at min position",
|
||||
idx
|
||||
);
|
||||
}
|
||||
let exposure_idx = idx / 9;
|
||||
match exposure_idx {
|
||||
0 | 1 => {
|
||||
assert!(
|
||||
!mask[idx],
|
||||
"Short action {} (exposure_idx={}) should be masked at min position",
|
||||
idx, exposure_idx
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
assert!(
|
||||
mask[idx],
|
||||
"Non-short action {} (exposure_idx={}) should NOT be masked at min position",
|
||||
idx, exposure_idx
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,7 +531,7 @@ impl ParameterSpace for DQNParams {
|
||||
let cw = b("c51_warmup_epochs", (3.0, 15.0));
|
||||
let iq = b("iqn_lambda", (0.0, 1.0));
|
||||
|
||||
let bounds = vec![
|
||||
let mut bounds = vec![
|
||||
(lr.0.ln(), lr.1.ln()), // 0: learning_rate (log scale)
|
||||
bs, // 1: batch_size
|
||||
gm, // 2: gamma
|
||||
@@ -556,6 +556,20 @@ impl ParameterSpace for DQNParams {
|
||||
iq, // 21: iqn_lambda
|
||||
];
|
||||
|
||||
// Phase Fast: fix architecture dims to single-point bounds
|
||||
if crate::training_profile::get_hyperopt_phase() == crate::training_profile::HyperoptPhase::Fast {
|
||||
if let Some(pf) = &hp.phase_fast {
|
||||
let fix = |bounds: &mut Vec<(f64, f64)>, idx: usize, val: Option<f64>| {
|
||||
if let Some(v) = val {
|
||||
bounds[idx] = (v, v);
|
||||
}
|
||||
};
|
||||
fix(&mut bounds, 10, pf.dueling_hidden_dim); // dueling_hidden_dim
|
||||
fix(&mut bounds, 13, pf.num_atoms); // num_atoms
|
||||
fix(&mut bounds, 18, pf.hidden_dim_base); // hidden_dim_base
|
||||
}
|
||||
}
|
||||
|
||||
bounds
|
||||
}
|
||||
|
||||
@@ -3619,6 +3633,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_dqn_params_bounds() {
|
||||
use crate::training_profile::{set_hyperopt_phase, HyperoptPhase};
|
||||
|
||||
// Test Full phase: all bounds must be ranges (lower < upper)
|
||||
set_hyperopt_phase(HyperoptPhase::Full(vec![0.0; 22]));
|
||||
let bounds = DQNParams::continuous_bounds();
|
||||
assert_eq!(bounds.len(), 22); // 22D consolidated search space
|
||||
|
||||
@@ -3637,11 +3655,18 @@ mod tests {
|
||||
assert!(lr_min > 0.0 && lr_min < 1e-3, "Learning rate lower bound should be small, got {:.2e}", lr_min);
|
||||
assert!(lr_max >= 1e-4 && lr_max <= 1e-2, "Learning rate upper bound should be reasonable, got {:.2e}", lr_max);
|
||||
|
||||
// Check all bounds have valid ranges (lower < upper)
|
||||
// Exact values may be overridden by HyperoptProfile YAML
|
||||
for (i, (lo, hi)) in bounds.iter().enumerate() {
|
||||
assert!(lo < hi, "Bound {i}: lower ({lo}) must be < upper ({hi})");
|
||||
assert!(lo < hi, "Full phase: Bound {i}: lower ({lo}) must be < upper ({hi})");
|
||||
}
|
||||
|
||||
// Test Fast phase: architecture dims must be fixed (single-point bounds)
|
||||
set_hyperopt_phase(HyperoptPhase::Fast);
|
||||
let fast_bounds = DQNParams::continuous_bounds();
|
||||
assert_eq!(fast_bounds[18].0, fast_bounds[18].1, "Fast: hidden_dim_base must be fixed");
|
||||
assert_eq!(fast_bounds[13].0, fast_bounds[13].1, "Fast: num_atoms must be fixed");
|
||||
assert_eq!(fast_bounds[10].0, fast_bounds[10].1, "Fast: dueling_hidden_dim must be fixed");
|
||||
// Learning rate must still be a range in Fast phase
|
||||
assert_ne!(fast_bounds[0].0, fast_bounds[0].1, "Fast: learning_rate must still be searchable");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1014,13 +1014,11 @@ mod tests {
|
||||
set_hyperopt_phase(HyperoptPhase::Fast);
|
||||
use crate::hyperopt::ParameterSpace;
|
||||
let bounds = crate::hyperopt::adapters::dqn::DQNParams::continuous_bounds();
|
||||
// Index 21: hidden_dim_base, 15: num_atoms, 28: branch_hidden_dim,
|
||||
// 13: dueling_hidden_dim, 10: v_range
|
||||
assert_eq!(bounds[21].0, bounds[21].1, "hidden_dim_base must be fixed");
|
||||
assert_eq!(bounds[15].0, bounds[15].1, "num_atoms must be fixed");
|
||||
assert_eq!(bounds[28].0, bounds[28].1, "branch_hidden_dim must be fixed");
|
||||
assert_eq!(bounds[13].0, bounds[13].1, "dueling_hidden_dim must be fixed");
|
||||
assert_eq!(bounds[10].0, bounds[10].1, "v_range must be fixed");
|
||||
// 22D search space indices (from DQNParams::continuous_bounds):
|
||||
// 10: dueling_hidden_dim, 13: num_atoms, 18: hidden_dim_base
|
||||
assert_eq!(bounds[18].0, bounds[18].1, "hidden_dim_base must be fixed");
|
||||
assert_eq!(bounds[13].0, bounds[13].1, "num_atoms must be fixed");
|
||||
assert_eq!(bounds[10].0, bounds[10].1, "dueling_hidden_dim must be fixed");
|
||||
// Learning rate (index 0) must still be a range.
|
||||
assert_ne!(bounds[0].0, bounds[0].1, "learning_rate must still be searchable");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user