Repository navigation
feat(leakance): temperature-annealed gate replaces the continuous factor - #49
Merged
Merged
Conversation
zeta multiplies leakance_factor by K_D, so only their product reaches the physics: an exact scaling degeneracy. Measured over 346,321 CONUS reaches on a trained arm, rho(leakance_factor, K_D) = +0.9986 and all four learned outputs of that head hold 97 percent of their variance in one direction. Physically a reach either sits above a losing aquifer or it does not, so the factor should select where leakance acts rather than scale how much. A gate and a conductance are not degenerate with each other. The gate is a sharpening sigmoid on the head's normalized output, g = sigmoid(logit(clamp(u, eps, 1-eps)) / tau), annealed by an epoch-keyed schedule that mirrors the learning rate. tau = 1 is special-cased to return u untouched, so every config without params.leakance_gate is bit-exact unchanged. Differentiable at every positive tau, so autograd gives the exact gradient and no straight-through estimator is introduced. Applied to the head output before setup_inputs, so no routing operator or backward changes. Replicated across all three hand-written head readers rather than unified, per the WET preference, with the eval reader and dump_parameters using the FINAL temperature so the scored model is the one training ended on. A previous bug shipped because an eval reader silently dropped a learned head output. Gates: lib 377 passed; leakance_gate 11; gamma_eval_parity 4; sp8_gradcheck 20; leakance_gradcheck 16; leakance_off_parity 3; zeta_accum 11; leakance_reference_match 5; ddr_sandbox_match 1. compare_ddr_sandbox ABSOLUTE MATCH at 1.525879e-4 m3/s, unchanged. verify_doc_paths 0 unresolved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0143bx7gVz8ug2oc6DTy5jtz
taddyb
force-pushed
the
leakance-gate
branch
from
September 17, 2026 02:26
f1482e9 to
289163b
Compare
104 numbered equations over 13 sections, each derived from the code rather than from prior docs, with symbol tables giving units and whether each quantity is learned per reach, set by config, read from data, or derived. Covers the head and its normalization, the new gate, the geometry chain with a catalogue of every clamp and what it does to the gradient, stage-dependent roughness, the Muskingum coefficients, the sparse adjoint, the leakance flux with all six partials, the gradient-path labels, all five losses, and the diagnostics. It found ten code-versus-documentation discrepancies. The load-bearing one: the leakance term computes its width as (p*d)^q while the routing geometry computes the same reach's width as p*d^q, so the area multiplying K_D is smaller by the depth-independent factor p^(q-1) = 0.3445 at the prescribed p = 21, q = 0.65, and a learned K_D absorbs it. The flux and skill results are unaffected because a constant rescaling is absorbable, but K_D is not directly comparable to a measured streambed leakance without dividing it out. Inherited from DDR, so the port is faithful; leakance_reference_match pins that. validate_params.py now divides the factor out and records the caveat. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0143bx7gVz8ug2oc6DTy5jtz
…gradient-sink check The equations reference flags that nothing in the repo measures how often the geometry clamps saturate, which matters because a saturated clamp zeroes the gradient to the parameter behind it, so training cannot identify that parameter at that reach. Measured on the 2026-09-16T04-44-15Z arm over 64,892 eval reaches: the depth floor takes 2.1 percent, and the side-slope bounds, the bottom-width floor and both roughness range bounds take none. The side-slope clamp cannot fire in this configuration: with q fixed at 0.65 its lower bound needs a depth of 1,751 m and its upper bound a depth of 3.2 mm, which the 10 mm depth floor already forbids. So clamps are not the reason the learned outputs collapse onto one direction. That is consistent with the head-topology screen, which found capacity is not the constraint either, leaving the gradient and the objective. Figures are a lower bound: computed from each reach's mean eval depth, so a clamp that fires only at low flow is invisible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0143bx7gVz8ug2oc6DTy5jtz
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
zeta = leakance_factor · area_z · K_D · (depth − d_gw)multipliesleakance_factorandK_D, so only their product reaches the physics: an exact scaling degeneracy. Measured over 346,321 CONUS reaches on a trained arm,rho(leakance_factor, K_D) = +0.9986, and the four learned outputs of that head hold 97 % of their variance in one direction. Physically a reach either sits above a losing aquifer or it does not, soleakance_factorshould be a GATE selecting where leakance acts, not a continuous multiplier. A gate and a conductance are not degenerate with each other. On the same arm the model had repurposedd_gwas the switch (learned to +0.43 m against a median channel depth of 0.26 m so the losing-only clamp zeroed most reaches); with a real gated_gwcan go back to being a head difference.This does not re-open the leakance verdict (
research-status.md); it removes one degeneracy so a future arm can be judged on the gate rather than on the product.What
A temperature-annealed sharpening sigmoid on the head's normalized
leakance_factoroutputu ∈ (0, 1):src/training/gate.rs::leakance_gate— the transform.tau == 1.0returnsuuntouched (bit-exact identity, not round-tripped throughln/exp). Generic over the backend, so training (Autodiff<I>), eval, probe and dump all call the same function. Exact autograd gradient throughclamp → log → div → sigmoid; no straight-through estimator.params.leakance_gate: { temperature: {epoch: tau, ...} }(src/config.rs::LeakanceGate), absent by default. Epoch-keyed schedule resolved exactly likeexperiment.learning_rate(LeakanceGate::resolve: largest key<= epoch, first value before it).validate_leakance_gaterejects the block withoutuse_leakance: true, an empty schedule, and any temperature that is not positive and finite.forwardtakes the epoch's temperature as a newgate_tau: Option<f32>argument threaded from the driver (asserted to beSomeexactly when the block is configured);forward_eval_coreandprobe_forwardapply the FINAL scheduled temperature; bothdump_parameterscopies dump the gated factor at the final temperature. The run log states the temperature in force (epoch E lr=… leakance_gate_tau=…in training;leakance gate: final temperature tau=…from eval, probe and dump).src/routing/,TimestepLeakanceOp, and every backward operator are untouched.Epsilon
GATE_EPS = 1e-6.1 − 1e-6sits ~17 ulps below1.0_f32(ulp there is 2^-24 ≈ 6e-8), so1 − uat the clamp keeps ~3 % relative accuracy;1e-7would be only 2 ulps away with ~20 % error in1 − u.logit(1e-6) ≈ −13.8, so the clamp only alters values the head has already saturated past|pre-activation| > 13.8. Without the clamp a head sigmoid that has saturated to exactly 0.0 or 1.0 in f32 giveslogit = ±infand a NaN gradient. Everything stays f32.Tests
tests/leakance_gate.rs(new, 11 tests):tau = 1bit-exact identity on the bare function and end to end throughforward,forward_eval,probe_forwardagainst the same config with the block absent; a sharp final temperature demonstrably changes all three readers and they still agree; gradient vs central finite differences at tau ∈ {1, 0.5, 0.2, 0.05, 0.01} (f64 formula at the repo tolerance, plus the f32 implementation at its ulp noise floor); monotone inu,0.5a fixed point at every temperature,tau → 0limits; tau down tof32::MIN_POSITIVEwithuexactly 0 and 1 gives finite values and finite gradients;forwardrejects a mismatchedgate_tau.tests/gamma_eval_parity.rs: two new rows (leakance + gate,everything + gate) at final tau 0.05 across all three readers.src/config.rsunit tests: block rejected withoutuse_leakance, empty/non-positive temperatures rejected, schedule resolves at epoch boundaries andfinal_temperature.ddrs-dev/references/config.md§Leakance gate and validator rows;testing.mdgate list.Gates (Tier C;
src/routing/untouched)Note: the commit message lists the test counts of
leakance_gateandgamma_eval_parityswapped (it is 11 and 4, respectively).🤖 Generated with Claude Code
https://claude.ai/code/session_0143bx7gVz8ug2oc6DTy5jtz