diff --git a/crates/ml-alpha/tests/integrated_trainer_smoke.rs b/crates/ml-alpha/tests/integrated_trainer_smoke.rs index f792aa3cc..66f75a98c 100644 --- a/crates/ml-alpha/tests/integrated_trainer_smoke.rs +++ b/crates/ml-alpha/tests/integrated_trainer_smoke.rs @@ -105,16 +105,26 @@ fn integrated_trainer_step_with_lobsim_runs_without_panic() { assert!(stats.l_v.is_finite(), "l_v is not finite: {}", stats.l_v); assert!(stats.l_total.is_finite(), "l_total is not finite: {}", stats.l_total); - // λs sum to ~1.0 (loss-balance default before any controller fires). - let lambda_sum = stats.lambdas.bce - + stats.lambdas.q - + stats.lambdas.pi - + stats.lambdas.v - + stats.lambdas.aux; - assert!( - (lambda_sum - 1.0).abs() < 1e-4, - "loss-balance λs should sum to 1.0; got {lambda_sum}" - ); + // Each λ is finite + positive. The per-head divide-by-5 happens + // at the trainer's loss-combine site (see `loss_balance.rs::Default` + // doc), so each λ defaults to 1.0 (sum 5.0) on first observation. + // Asserting the SUM equals a specific value locks the test against + // any future controller-driven re-weighting (canonical + // `pearl_tests_must_prove_not_lock_observations`); the invariant + // we actually care about is "every head has a finite positive + // weight so the encoder gets a real-valued gradient signal." + for (name, lam) in [ + ("bce", stats.lambdas.bce), + ("q", stats.lambdas.q), + ("pi", stats.lambdas.pi), + ("v", stats.lambdas.v), + ("aux", stats.lambdas.aux), + ] { + assert!( + lam.is_finite() && lam > 0.0, + "lambda[{name}] must be finite + positive; got {lam}" + ); + } eprintln!( "R6 OK — step_with_lobsim end-to-end: l_bce={} l_q={} l_pi={} l_v={} l_aux={} l_total={}", diff --git a/crates/ml-alpha/tests/r5_controllers_and_soft_update.rs b/crates/ml-alpha/tests/r5_controllers_and_soft_update.rs index 5fd254639..ee21afe50 100644 --- a/crates/ml-alpha/tests/r5_controllers_and_soft_update.rs +++ b/crates/ml-alpha/tests/r5_controllers_and_soft_update.rs @@ -118,7 +118,13 @@ fn g3_per_step_controllers_move_isv_outputs_when_fed_real_emas() { (RL_KL_PI_EMA_INDEX, 0.1), // → rl_ppo_clip (RL_ENTROPY_OBSERVED_EMA_INDEX, 0.5), // → rl_entropy_coef (RL_ADVANTAGE_VAR_RATIO_EMA_INDEX, 5.0), // → rl_rollout_steps - (RL_TD_KURTOSIS_EMA_INDEX, 10.0), // → rl_per_alpha + // td_kurtosis = 20.0 chosen so the per_α controller's target + // (0.4 + 0.2 × (20 − 3) / 7 = 0.886) is distinct from the + // bootstrap 0.6 — a fixture value of 10.0 mapped exactly to + // target = 0.6, making the Wiener blend produce 0.6 regardless + // of α and rendering the "moved off bootstrap" assertion + // unfalsifiable (canonical `pearl_tests_must_prove_not_lock_observations`). + (RL_TD_KURTOSIS_EMA_INDEX, 20.0), // → rl_per_alpha (RL_MEAN_ABS_PNL_EMA_INDEX, 50.0), // → rl_reward_scale ]; for (slot, obs_val) in inputs {