Skip to content

Zero-E branch derives its own temperature block - #117

Merged
dfalster merged 1 commit into
masterfrom
fix/shutdown-branch-temperature-block
Aug 20, 2026
Merged

Zero-E branch derives its own temperature block#117
dfalster merged 1 commit into
masterfrom
fix/shutdown-branch-temperature-block

Conversation

@dfalster

@dfalster dfalster commented Aug 20, 2026

Copy link
Copy Markdown
Member

set_leaf_states_rates_from_psi_stem's zero-transpiration branch wrote
Tleaf_ but re-derived neither the Farquhar block nor the leaf-to-air
deficit at it -- while ci_ is read off gamma_ and assim_colimited_ off
the whole block. The transpiring branch re-derives per candidate by
design, so this one inherited the last psi the optimiser probed, making
the objective evaluation-order dependent. Both are derived now, before
ci_ is read.

Moves results on the PM path, the ProfitMax argmax among them (0.5.0 ->
0.5.1, NEWS has the table). Closes #110, #112.

@dfalster

Copy link
Copy Markdown
Member Author

What was wrong

set_leaf_states_rates_from_psi_stem's psi_upstream >= psi_stem branch is the no-flow / reversed-gradient case. It wrote ci_, transpiration_, stom_cond_CO2_ and (since #106) Tleaf_ — but re-derived neither the Farquhar block nor the leaf-to-air deficit at that Tleaf_. Three things then read a temperature the branch had not seated:

  • ci_ = gamma_ * umol_per_mol_to_Pa_ — the compensation point is a member of the temperature block;
  • assim_colimited_ = assim_colimited(ci_) at the bottom of the function — the whole block;
  • vpd_leaf_, which is a reported field and is what g1_eff() divides by (The shut-down branch leaves vpd_leaf_ stale #110).

The temperature it read was not the air baseline. That is the part that made #112 separate from #105 rather than a repeat of it: the transpiring branch calls update_temperature_dependent_params per candidate by design (it bypasses the photo_temp cache — Tleaf varies per operating point), so the zero-E branch inherited whatever ψ the optimiser last probed. The objective's value at a fixed ψ therefore depended on the order candidates were visited.

Reachability, since the issues asked for it first

Four routes, established by reading the callers and confirmed in the probe:

  1. prepare_profitmax's scan, at i == 0, on every run. The scan starts at psi_soil and calls set_leaf_states_rates_from_psi_stem(psi_soil, psi_soil) — equal potentials, so this branch. Its A is recorded and is a legitimate profit candidate (that is hazard 11 / Leaf-to-air VPD, and Sperry's ProfitMax #93's grid argmax), so this reaches an argmax, not just a report.
  2. profit_at_collar_psi, which clamps a caller's target into [bound_a, bound_b] where bound_a is root_zero_Epsi_stem == bound_a exactly there. So the branch is the final reported state on the prescribed-collar path (TF24f's acclimation route).
  3. maximise_profit_over_collar's two off-normal exits — the golden-section fallback and the two-pin SolverRefused case — both evaluate profit_psi_stem_TF at lo/bound_a.
  4. The standalone optimise_psi_stem_* optimisers at their wet endpoint.

find_root_collar_psi's normal path does not reach it: dprofit_at_collar_psi has its own psi >= psi_stem exit, and the collar it returns is stepped inside the wet bound. The assim_max_ < 0 branch #112 originally paired with this one is gone — #93 deleted it, for a better reason.

The measurement

Probe built against master and against this branch from separate trees (a git worktree --detach, not a stash — git stash cannot revert a commit, and a half-reverted build reads as a real finding).

The state the branch reports. Tair 30 °C, PPFD 900, ψ_soil 1 MPa, D_air 1.5 kPa. The branch's own leaf temperature is 39.325 °C against Tair 30, so there is a large gap to get wrong:

reported at the zero-E point cold object (Tair baseline) after one transpiring candidate this branch
assim_colimited_ −1.9888 −2.5355 −3.0978
R_d_ 1.9888 2.5355 3.0978
vpd_leaf_ (kPa) 1.5000 2.7368 4.3714
ci_ (Pa) 5.5700 6.9492 8.7173

Two columns, two different histories, same call. Respiration was 36% low and the deficit 66% low at the worse of them, both in the direction that makes a shut leaf look cheaper than it is — the same direction #105 found, for the same physical reason (no latent cooling, so this is the hottest the leaf gets).

The ProfitMax argmax moves, in both directions. Thermal cost on, PPFD 900, D_air 1.5:

Tair ψ_soil argmax before argmax after profit before profit after
40 0.5 0.5000 (shut) 1.6052 −1.2825 −1.2309
40 1.0 1.0000 (shut) 1.7595 −1.3032 −1.2983
40 2.0 2.0000 (shut) 2.0000 (shut) −1.3351 −1.3657
50 0.5 0.5004 (open) 0.5000 (shut) −1.8720 −1.8720
55 0.5 0.5004 (open) 0.5000 (shut) −1.7755 −1.7754

At Tair 40 the closure candidate's carbon was too generous, so closure won when it should not have; correcting it hands the interior point the argmax and a higher profit. At Tair 50–55 it goes the other way — the objective is nearly flat there and the answer becomes exactly shut. Neither direction is a rounding difference.

The prescribed-collar path, clamped onto the wet bound after a real solve had warmed the block:

Tair profit before profit after vpd_leaf_ before after
25 −2.4329 −2.5415 3.4861 3.7486
40 −3.84281 −3.84282 6.0536 6.0538
50 (unchanged: takes the shade-death exit, already fixed by #111)

find_root_collar_psi is unmoved at every PM driver tried — the checksum over four soils at Tair 40 is bit-identical across the two arms. Stated as what it is: a statement about the drivers tried, not a proof, since routes 3 and 4 above exist.

Cost (hazard 5)

This adds two transcendental blocks to a branch of a function the collar solve runs ~10³ times per solve, so it was measured rather than assumed cold. Both binaries built from their own trees, interleaved ×4 at load ~5.4, min-of-400:

arm before after
zero-E branch + one transpiring candidate, PM on (µs/pair) 4.972 / 4.990 / 5.031 / 5.178 5.018 / 5.012 / 5.205 / 5.218 +0.4% … +3.5%, median +0.9%
find_root_collar_psi, PM on (µs) 249.2 / 251.3 / 258.0 / 255.5 247.3 / 243.2 / 256.8 / 258.3 no signal
optimise_psi_stem_ProfitMax, PM on (µs) 2647 / 2645 / 2680 / 2710 2620 / 2627 / 2707 / 2717 no signal
find_root_collar_psi, PM off (µs) 84.66 / 84.72 / 88.14 / 88.02 84.65 / 88.10 / 88.10 / 88.81 no signal

The round-to-round scatter on the last three arms is wider than any difference between the arms, and the per-pair figure is dominated by the transpiring candidate that shares each iteration. Gate-off is unchanged by construction as well as by measurement: the recompute is inside if (use_energy_balance_), and set_leaf_vpd returns atm_vpd_ exactly there.

Evidence and its limits

  • Both golden files bit-identical, and that is not evidence here — both grids run gate-off, where there is no second temperature to disagree with. Same for leaf_behaviour_fingerprint(), which is golden-derived. This change signals only through NEWS and the version bump (0.5.0 → 0.6.0).
  • 670 C++ checks, 0 failures; make bench builds; the R testthat suite is green including gradient_golden.tsv bit-exact.
  • The new test's sharp assertion is order-independence rather than a value: the same (psi_stem, psi_upstream) on a cold object and after a transpiring candidate must agree bit-for-bit in Tleaf_, assim_colimited_, R_d_, vpd_leaf_ and ci_. A value assertion would not have caught the defect this change is about, since the value was plausible at either history.
  • One assertion had to be weakened and it is worth recording why. assim_colimited_ == -R_d_ is exact at the PM shut-down reports Tleaf but R_d at air temperature #105 exits, which assign it; here it comes through assim_colimited(gamma*), where gross assimilation is analytically zero but the co-limiting expression rounds — measured 4.44e-16, one ULP. The bit-exact statement that survives is against the model's own function at the model's own ci_, which is what pins the temperature.

What this does not do

optimise_psi_stem_TF and optimise_psi_stem_Sperry still have hazard 11 (#94), and the surface question #114 raises is untouched.

@dfalster
dfalster force-pushed the fix/shutdown-branch-temperature-block branch from 2d4e324 to 0925dad Compare August 20, 2026 04:00
Comment thread inst/include/phylloptim/leaf_model.hpp Outdated
set_leaf_states_rates_from_psi_stem's zero-transpiration branch wrote
Tleaf_ but re-derived neither the Farquhar block nor the leaf-to-air
deficit at it -- while ci_ is read off gamma_ and assim_colimited_ off
the whole block. The transpiring branch re-derives per candidate by
design, so this one inherited the last psi the optimiser probed, making
the objective evaluation-order dependent. Both are derived now, before
ci_ is read.

Moves results on the PM path, the ProfitMax argmax among them (0.5.0 ->
0.5.1, NEWS has the table). Closes #110, #112.
@dfalster
dfalster force-pushed the fix/shutdown-branch-temperature-block branch from 0925dad to 027fe08 Compare August 20, 2026 04:03
@dfalster
dfalster merged commit 0cbbaf5 into master Aug 20, 2026
6 checks passed
@dfalster
dfalster deleted the fix/shutdown-branch-temperature-block branch August 20, 2026 04:04
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.

The shut-down branch leaves vpd_leaf_ stale

1 participant