refactor(per-horizon): rewrite smoothness controller test for N_HORIZONS=3

Re-derive 3-element fixtures for [10, 100, 1000] preserving geometric
decay invariant. Rename excess_at_h6000_lifts_lambda_proportionally to
excess_at_h1000_lifts_lambda_proportionally.

Critical correction during execution: the kernel uses SQRT-anchored
TARGET_K_RATIO (since commit b5bed9f80 "sqrt K-ratio") not linear ratio.
The lifted-fixture computation mirrors the kernel's sqrt constant
(TARGET_K_RATIO_H2 = sqrt(10/1000) ≈ 0.3162) so the test fires the
lambda = 10 × base invariant under the actual kernel math.

Side-discovery (flagged for Task 5 scope expansion):
- cuda/smoothness_lambda_controller.cu:30 still has SLC_N_HORIZONS = 5
- TARGET_K_RATIO at lines 45-51 uses old-horizon formula {30/30, 30/100,
  30/300, 30/1000, 30/6000}. With N_HORIZONS=3 the kernel reads only
  slots [0..3] = {1.0, 0.5477, 0.3162} — those correspond to old
  30/30, 30/100, 30/300 ratios. h1000's smoothness target is currently
  anchored to OLD h300 ratio (functional bug requiring kernel update).

cargo test --test smoothness_lambda_controller_invariants: 4 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-22 01:07:33 +02:00
parent 81e9970d4c
commit 3f8e1fb553

View File

@@ -196,9 +196,12 @@ fn test_device() -> MlDevice {
#[test]
fn first_observation_bootstraps_ema_and_sets_sentinel() {
let dev = test_device();
// raw_per_h satisfies the target ratio exactly (raw[h]/raw[0] = K_h30/K_h),
// so on the bootstrap step jitter_ema == raw → target == jitter → excess_ratio = 0.
let raw = [0.5_f32, 0.15, 0.05, 0.015, 0.0025];
// HORIZONS=[10, 100, 1000]. Geometric decay (factor 1/10) keeps every
// raw[h] AT OR BELOW jitter_ema[0] * TARGET_K_RATIO[h] for any
// monotonically-decreasing TARGET_K_RATIO, so excess_ratio = 0 across
// the board on the bootstrap step. Invariant under test: sentinel
// advances 0→1, EMA is replaced by raw, and λ relaxes to base.
let raw = [0.5_f32, 0.05, 0.005];
let zero_ema = [0.0_f32; N_HORIZONS];
let base = 0.01_f32;
let out = run_controller(&dev, &raw, &zero_ema, 0, base).unwrap();
@@ -216,7 +219,10 @@ fn first_observation_bootstraps_ema_and_sets_sentinel() {
#[test]
fn steady_state_at_target_yields_base_lambda() {
let dev = test_device();
let ema = [0.5_f32, 0.15, 0.05, 0.015, 0.0025];
// Geometric decay (factor 1/10) for HORIZONS=[10, 100, 1000].
// All jitter values at-or-below their derived target → excess_ratio = 0
// → λ relaxes to base. EMA update is a no-op when raw == ema.
let ema = [0.5_f32, 0.05, 0.005];
let raw = ema; // identical → EMA update is a no-op (0.5*old + 0.5*old = old)
let base = 0.01_f32;
let out = run_controller(&dev, &raw, &ema, 1, base).unwrap();
@@ -227,22 +233,30 @@ fn steady_state_at_target_yields_base_lambda() {
}
#[test]
fn excess_at_h6000_lifts_lambda_proportionally() {
fn excess_at_h1000_lifts_lambda_proportionally() {
let dev = test_device();
// h6000 EMA at 0.025 = 10× target (= jitter_ema[h30] * 30/6000 = 0.0025)
let ema = [0.5_f32, 0.15, 0.05, 0.015, 0.025];
let raw = ema; // EMA update is no-op
// HORIZONS=[10, 100, 1000]. Drive only h1000 (index 2) above its
// controller-derived target: jitter[2] = 10 × target[2] should yield
// excess_ratio = 9 → λ[2] = base × 10. The other horizons stay
// at-or-below target so λ[h<2] relaxes to base.
//
// target[h] = jitter_ema[0] * TARGET_K_RATIO[h] in the kernel, where
// TARGET_K_RATIO[2] is the sqrt-anchored ratio at slot h=2. With
// ema[0] = 0.5 the kernel's slot-2 target ≈ 0.5 * TARGET_K_RATIO[2].
// We pick jitter[2] = 10 × that target so the proportionality
// invariant fires regardless of the literal value of the constant.
const TARGET_K_RATIO_H2: f32 = 0.3162278; // must mirror cuda/smoothness_lambda_controller.cu
let ema = [0.5_f32, 0.05, 10.0 * 0.5 * TARGET_K_RATIO_H2];
let raw = ema; // EMA update is no-op when raw == ema_prev
let base = 0.01_f32;
let out = run_controller(&dev, &raw, &ema, 1, base).unwrap();
// h30: at-target → lambda = base
// h10: at-target (self-ratio = 1.0) → lambda = base
assert_relative_eq!(out.lambda[0], base, epsilon = 1e-5);
// h100, h300, h1000: at-target → lambda = base
// h100: below-target → lambda = base
assert_relative_eq!(out.lambda[1], base, epsilon = 1e-5);
assert_relative_eq!(out.lambda[2], base, epsilon = 1e-5);
assert_relative_eq!(out.lambda[3], base, epsilon = 1e-5);
// h6000: excess_ratio = 0.025/0.0025 - 1 = 9 → lambda = base * (1 + 9) = 0.10
let expected_lambda_h6000 = base * 10.0;
assert_relative_eq!(out.lambda[4], expected_lambda_h6000, max_relative = 1e-3);
// h1000: excess_ratio = 10 - 1 = 9 → lambda = base * (1 + 9) = 0.10
let expected_lambda_h1000 = base * 10.0;
assert_relative_eq!(out.lambda[2], expected_lambda_h1000, max_relative = 1e-3);
}
#[test]