Leaf-to-air VPD, and Sperry's ProfitMax - #93
Conversation
Three changes, all on the energy-balance and single-layer paths; the golden file is bit-identical and the R gradient golden is bit-exact. 1. Fick's law now divides by the LEAF-to-air deficit, not the air's (PLAN 13.1, #7). Off the energy balance the leaf is at air temperature and `vpd_leaf_ == atm_vpd_` exactly, so nothing on the prescribed-temperature path moves. On it, the deficit is 1.5x the air's at Tair 25 and 3-4x by Tair 45, so gs and A were overstated by that factor. The collar first-order condition gains the matching `dgc_dT` term -- and the placeholder algebra that stood there while `dgc_dT` was zero did NOT generalise, so it is re-derived rather than sign-flipped. 2. `optimise_psi_stem_ProfitMax()` runs Sperry (2017) with both terms normalised, computing |A|max over the supply stream itself. It reports the lambda that makes `optimise_psi_stem_Sperry` find the same point, so the equivalence is checkable. `profitmax_curve()` returns the whole cost/gain/profit curve in one crossing. 3. An optional instantaneous thermal cost (Sicangco et al. 2026), default off: TC sigmoid in leaf temperature, Jmax scaled by (1-TC). NOT plant's ATLS ratchet, which has memory. Plus three bugs on the single-layer optimisers: a NaN `lambda_` was searched silently, and `opt_root_psi_`/`E_up_`/`soil_consumption_` survived from an earlier collar solve. The decoupling acceptance test now asserts on transpiration rather than conductance, because the deficit correction removes the conductance signature at those drivers. Measured in the test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`set_leaf_states_rates_from_psi_stem` zeroed transpiration and conductance wherever `assim_max_ < 0`. Transpiration on that path is the hydraulic supply at the candidate potential and does not depend on photosynthesis, so the branch made two leaves at the SAME operating point disagree about whether water was moving purely because one had respiration switched on. The shut-down STATE it reached for is unchanged: where assim_max_ < 0 the ci root-find has no root in [gamma*, ca] and takes its compensation-point fallback, giving ci = gamma* and net A = -R_d, which is what the branch set by hand. That fallback did not exist when the branch was written. Unreachable from the collar solve -- prepare_collar_solve has its own `assim_max_ < 0` exit and returns before any candidate is evaluated -- so plant is unaffected. The golden grid's minimum assim_max_ is 3.71, so the file is bit-identical and says nothing about this; test_transpiration_survives_negative_assim is the test that can. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Regenerating man/ also picks up a stale gradient_par_names.Rd, which said fifteen names where its own roxygen source has said sixteen since R_d_25 was added. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
brent_fmin is a local optimiser that steps in from the bounds, so it could return neither an endpoint nor the global maximum of a multi-modal objective -- and this objective is both, in the regime the model is interesting in. Measured at Tair 50 C with the thermal cost on: the profit is highest at full closure (-1.5314 at psi_soil) with a local maximum near 1.9, and the solver returned 1.643. It reported an open stoma where the objective says shut. The scan prepare_profitmax() already runs now stores A and Tleaf per point, so the objective is rebuilt on it for no extra model evaluations. An endpoint argmax is returned as the endpoint. optimise_psi_stem_TF and _Sperry share the hazard and are documented rather than changed: neither has a scan to reuse. The collar solve is unaffected -- it handles a pinned optimum explicitly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
What moved, and what did notThe golden file is bit-identical across all five commits, and that is a
C++ suite 486 → 534 checks, 0 failures. The baseline was built from a
The four fixes on the single-layer optimisers
Two things a reviewer should push back on if they disagreeThe
Where this came from
|
Doxygen reads a `|` in a paragraph as a table delimiter, and `\verbatim` is not allowed inside a table cell: it drops the OPEN and then reports `unexpected command endverbatim` at a line that is neither -- here, 80 lines further on, inside unrelated code. `|` in prose is common in these headers and was harmless until a comment that had one also gained an indented block, which the filter turns into a verbatim. Found by comparing every verbatim-carrying comment block on this branch against every one on master: a pipe was the only structural feature present in mine and absent from all of theirs.⚠️ Doxygen 1.17 renders the same input in silence; only the 1.9 that CI installs objects, so a local `doxygen` run is not evidence either way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Doxygen 1.9, which is what CI installs, drops the second `\verbatim` open in a `///` block and reports `unexpected command endverbatim` at a filtered-stream line 120 lines away in unrelated code. 1.17 renders it in silence, so the docs job was the only oracle. The ProfitMax banner had two displays; they are now one. docs.yml asserts the shape and names the block, so the next one fails legibly. The `|`-escaping added earlier is reverted: it was a wrong hypothesis, and master's prose is full of unescaped pipes.
The docs job, and the two explanations that were wrongThe failure was
So Doxygen 1.9 handles the first Two things I asserted before measuring, both wrong, both now removed from the tree:
The fix is the banner carrying one display instead of two.
|
One conflict: the generated hash line in `R/RcppR6.R`, where neither side's value describes the merged bindings. Resolved by regenerating rather than picking a side -- RcppR6::RcppR6(), compileAttributes(), roxygenise() -- which gives a9a3c8f and leaves every other generated line byte-identical to the auto-merge. C++ 534 checks / 0 failures, 576 golden points bit-identical. R suite 0 failures, `gradient_golden.tsv` worst relative difference 0. Both `docs.yml` checks pass locally, including the verbatim-block shape one against master's new `gradient.hpp` comments. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The branch changes results on the energy-balance path -- gs and A were overstated by the leaf-to-air deficit ratio, 3.2-4.0x over Tair 25-45 -- and had no NEWS entry, where #89 and #90 both do. Six sections: the VPD fix and its floor, ProfitMax and the unitless members it writes, the four single-layer optimiser fixes, the dgc_dT re-derivation, and the one-verbatim-run-per-block finding. Every number is from the PR comments, checked against the code rather than copied. No behaviour change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The section was written unwrapped, one line per paragraph, beside a file wrapped at ~80: longest prose line 666 characters against 85 in the neighbouring section. Wrapped to 81. Content is unchanged. The heading's issue link becomes a bare `(#7)`, matching the file's other headings, and the full link moves into the prose beside PLAN 13.1 -- a word-level diff of the whole file shows those two moves and nothing else. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…angco
Four conflicts, all in leaf_model.hpp, all where this branch and master changed the
same expression for different reasons:
* three conductance conversions -- this branch replaced atm_vpd_ with vpd_leaf_,
master made H2O_CO2_stom_diff_ratio a settable member. Both changes kept.
* dprofit's early-return dE_dpsi -- this branch rewrote the derivation to state it
directly, because the coefficient master's version cancels is no longer a single
number once the deficit moves with Tleaf. This branch's version kept; it does not
reference the ratio.
R/RcppR6.R was regenerated rather than hand-resolved, and man/ + NAMESPACE were
stale after the merge and regenerated too.
Verified: 566 C++ checks / 0 failures, golden bit-identical, gradient golden
unchanged, fingerprint unchanged at 3e36d9b94fd0, 1137 R tests / 0 failures,
R CMD check --as-cran Status: OK, ctest 2/2, bench targets build, and the CI
consumer program built against an installed prefix exits 0.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Merged master (#99, #100, #101, #102, #103). The four conflicts, all in
|
Two conflicts, both in the leaf's per-candidate state update. #106 wrote `Tleaf_` on all three branches of set_leaf_states_rates_from_psi_stem; this branch had already deleted one of them, the `assim_max_ < 0` exit, so the note now describes two. And #106 stores `Tleaf_` before the temperature update, which this branch had recomputing `leaf_temp_from_E` for its own `set_leaf_vpd` call: both now read the stored value, so the temperature the Farquhar parameters were derived at, the one the deficit was derived at, and the one reported to the caller cannot be three different numbers. `R/RcppR6.R` conflicted only on its generated hash; regenerated rather than merged. Golden files bit-identical: 576 operating points and 544 primitives, no cell moved. 605 checks, 0 failures.
itowers1
left a comment
There was a problem hiding this comment.
Looks good to me, though there is a lot of new content, so still having to get used to it. Question about optimise_psi_stem_ProfitMax vs optimise_psi_stem_sperry. For conceptual simplicity woud like to streamline number of optimisers.
| // since maximise_profit_over_collar handles a pinned optimum explicitly. | ||
|
|
||
| // need docs on Golden Section Search. | ||
| inline void Leaf::optimise_psi_stem_Sperry() { |
There was a problem hiding this comment.
Still need optimise_psi_stem_Sperry, if we also now have optimise_psi_stem_ProfitMax?
Merging master (#104, #106, #107, #108) in — what the conflicts were, and the blast radiusBlast radius: none. Two conflicts, both in
One thing found and deliberately not fixed hereThe remaining shut-down branch ( |
#93 added `vpd_leaf_`, derived from a leaf temperature and read by `g1_eff()`, and seated it at its own zero-transpiration branch -- but not at the two shut-down exits outside the solve, which is where this PR already had to fix the same class of disagreement. So a shut-down leaf reported a 39.3 C temperature beside the deficit belonging to 30 C, 2.44x too small. A no-op off the energy-balance path, where `set_leaf_vpd` returns `atm_vpd_` exactly. NEWS carries all four fields now.
Four sites described the model as it was before two merged fixes. The one that mattered is user-facing: fitting.Rmd told readers that changing a trait and re-driving takes a temperature-cache hit and silently reports the first vcmax it ever saw. #55 put every input of the temperature block in the key, so it does not. The advice to use set_traits() stands on the splines, the solved operating point and the #25 checks instead. Comments and prose only; both golden files bit-identical. The closed_form/#93 divergence is recorded, not repaired -- see NEWS.
Fick's law divided by the AIR deficit however hot the leaf got (#7,
PLAN 13.1). Off the energy balance the leaf is at air temperature and
vpd_leaf_ == atm_vpd_exactly, so the prescribed-temperature path andthe golden file are untouched. On it the deficit is 3-4x the air's over
Tair 25-45, so gs and A were overstated by that factor.
Also adds
optimise_psi_stem_ProfitMax()(Sperry 2017, both termsnormalised, reporting the lambda that makes the existing entry point
agree), an optional instantaneous thermal cost default off, and four
fixes on the single-layer optimisers. Blast radius in the first comment.