[TF24 hydraulics] TF24f: don't ask for an acclimation gradient in hydraulic shutdown (#576) - #582
Open
dfalster wants to merge 1 commit into
Open
[TF24 hydraulics] TF24f: don't ask for an acclimation gradient in hydraulic shutdown (#576)#582dfalster wants to merge 1 commit into
dfalster wants to merge 1 commit into
Conversation
TF24f died with "Extrapolation disabled and evaluation point outside of interpolated domain" in a narrow dry rainfall window (0.04-0.05 m/yr at theta = 0.005, five layers, 10 yr) while TF24 ran. The issue guessed a too-narrow spline domain in transpiration_to_psi_stem. That is where it surfaces, but both halves of the guess were wrong: it fails at the LOWER end of psi_from_transpiration, not the upper end of the vulnerability curve, so widening the domain would not have helped -- the domain starts at psi = 0 and the evaluation point is negative. The cause is an asymmetry between TF24f's two gradient methods, not a numerical limit. The finite-difference branch checks prepare_collar_solve()'s return value and takes a zero gradient when the operating point was *forced* by feasibility handling; the AD branch (the default) called evaluate_root_collar_psi(), which hides that return value, then asked for a gradient regardless. In shutdown, root_collar_psi_ is set to -root_psi_crit, where soil uptake is negative, so the stem potential that would carry it is wetter than saturation and the transport inverse genuinely has no solution. Per plant-meta's model-robustness rule this splits across both halves of the table: hydraulic shutdown is attainable, so represent it (gradient zero, no interval to move within -- the call #570 already made inside dprofit); a psi_stem wetter than saturation is not attainable, so the spline was right to refuse. Hence neither clamped nor widened -- clamping would have returned a plausible psi_stem for an impossible state. The AD branch now mirrors the FD branch, which also drops a redundant second prepare_collar_solve per step. Behaviour changes only in states that previously threw, so the TF24f scientific version does not move. Also the diagnostic the issue asked for, which is what turned this from a four-site bisect into one read: * the throwing transport lookups name the spline, the point, how far outside it fell, the domain, and the calling function; * failures inside the collar solve carry the bracket they were working in and which feasibility branch prepare_collar_solve exited through; * the guard keeps odelia's exact comparison (u < lo || u > hi) rather than negating an in-range test, so a non-finite point still falls through to the spline as before. That is load-bearing: profit_psi_stem_TF(NA, .) is documented to return NA, and the value is caught downstream by the isfinite(profit_) guards, which report far more context. Verified: the reproducer runs at rainfall 0.03/0.04/0.05/0.06, and a 72-run sweep over rainfall x theta0 x layers x lifetime for both models is clean (72/72). Two assertions in test-leaf.r that pinned odelia's old bare message now assert the named diagnosis instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Fixes #576.
What it turned out to be
Not a too-narrow spline domain. The issue's guess (
Leaf::transpiration_to_psi_stem) is where it surfaces, but both halves of the reasoning were wrong:psi_from_transpiration, not the upper end of the vulnerability curve — the domain starts at ψ = 0 and the evaluation point is negative, so widening would not have helped;The cause is an asymmetry between TF24f's two gradient methods. The FD branch checks
prepare_collar_solve()'s return value and takes a zero gradient when the operating point was forced by feasibility handling. The AD branch — the default — calledevaluate_root_collar_psi(), which hides that return value, then asked for a gradient regardless:In shutdown,
set_shutdown_state(-root_psi_crit)makesusedcome back asroot_psi_crit, where soil uptake is negative (three of four layers at the residual floor, ψ ≈ −1000 MPa, so water flows out of the roots). The ψ_stem that would carry a negative flux from a collar at 5.87 MPa is wetter than saturation. No such ψ_stem exists.That also explains the knife-edge: the window is where the soil is dry enough to trip the
E_column(-psi_crit) < 0shutdown test but not the earlier-wettest >= psi_critone. Rainfall 0 misses it because the whole profile is at the floor and the first test fires.Which side of the model-robustness table
Both halves, in different places — per
model-robustness.md, which names this issue:dprofit_droot_collar_psi.Changes
TF24f_Strategy::solve_leaf's AD branch now mirrors the FD branch. Incidentally drops a redundant secondprepare_collar_solveper step (the old form re-derived the soil-side caches for the restore call).prepare_collar_solveexited through.Deliberately not changed: the guard keeps odelia's exact comparison (
u < lo || u > hi) rather than negating an in-range test, so a non-finite point still falls through to the spline. That is load-bearing —profit_psi_stem_TF(NA, .)is documented intest-leaf.rto returnNA, and the value is caught downstream by theisfinite(profit_)guards, which report far more context than one lookup could. Negating the test reads like a tightening and is really an unrelated behaviour change.Verification
test-strategy-tf24f.R(57 expectations, including the reference comparison and the [AutoDiff] [acclimation] Smooth/analytic profit gradient for TF24f leaf-state tracking #527 AD-gradient tracking test) confirms healthy runs are unchanged.test-tf24-arid-corner.R: the dry window completes; both gradient methods survive shutdown (the asymmetry is what broke, so that is the property worth pinning); the diagnostic names spline/point/domain.test-leaf.rthat pinned odelia's old bare message now assert the named diagnosis.Behaviour changes only in states that previously threw, so the TF24f scientific version does not move.
Not fixed here
prepare_collar_solve'sbound_b = std::max(-root_crit, -root_psi_crit)is dead code — the comment says the bracket is "clamped toroot_psi_crit" but-root_critis a positive magnitude while-root_psi_critis negative, so themaxalways returns-root_crit. Reachable and measured; filed separately since it changes results for runs that currently complete.🤖 Generated with Claude Code