Skip to content

[TF24 hydraulics] TF24f: don't ask for an acclimation gradient in hydraulic shutdown (#576) - #582

Open
dfalster wants to merge 1 commit into
developfrom
fix/tf24f-shutdown-gradient-576
Open

[TF24 hydraulics] TF24f: don't ask for an acclimation gradient in hydraulic shutdown (#576)#582
dfalster wants to merge 1 commit into
developfrom
fix/tf24f-shutdown-gradient-576

Conversation

@dfalster

Copy link
Copy Markdown
Member

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:

  • it fails at the lower end of 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 lookup is fine. It is being asked a question with no answer, from a state the feasibility analysis had already rejected.

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 — called evaluate_root_collar_psi(), which hides that return value, then asked for a gradient regardless:

leaf.evaluate_root_collar_psi(tracked_root_psi_);
const double used = -leaf.root_collar_psi_;
dprofit_dpsi_ = leaf.dprofit_droot_collar_psi(used);   // at a rejected collar potential

In shutdown, set_shutdown_state(-root_psi_crit) makes used come back as root_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) < 0 shutdown test but not the earlier -wettest >= psi_crit one. 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:

  • Hydraulic shutdown is attainable → represent it. Gradient zero; there is no interval to move within. Same call [TF24 hydraulics] [Env drivers] Unblock site-level prediction: six TF24 fixes #570 already made inside dprofit_droot_collar_psi.
  • A ψ_stem wetter than saturation is not attainable → fail. So the spline error was correct and stays — neither clamped nor widened. Clamping would have returned a plausible ψ_stem for an impossible state, which is the failure mode the doc says costs the most.

Changes

  • TF24f_Strategy::solve_leaf's AD branch now mirrors the FD branch. Incidentally drops a redundant second prepare_collar_solve per step (the old form re-derived the soil-side caches for the restore call).
  • The four 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.

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 in test-leaf.r to return NA, and the value is caught downstream by the isfinite(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

  • Reproducer runs. Rainfall 0.03 / 0.04 / 0.05 / 0.06 all complete (0.04 and 0.05 were the failures).
  • Re-ran this issue's sweep shape — rainfall × θ₀ × layers × lifetime, both models, 72 configurations: 72/72 run, no failures.
  • Full suite green. 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.
  • Three new tests in 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.
  • Two assertions in test-leaf.r that 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's bound_b = std::max(-root_crit, -root_psi_crit) is dead code — the comment says the bracket is "clamped to root_psi_crit" but -root_crit is a positive magnitude while -root_psi_crit is negative, so the max always returns -root_crit. Reachable and measured; filed separately since it changes results for runs that currently complete.

🤖 Generated with Claude Code

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[TF24 hydraulics] TF24f: hydraulic spline evaluated outside its domain in a narrow dry rainfall window

1 participant