Differentiate profit, the output plant bills - #89
Conversation
What changed
The envelope is applied as one assignment at the site that knows dY_dpsi <- (hi - lo) / (2 * h_psi)
if (identical(status, "interior")) dY_dpsi[["profit"]] <- 0
The measurement, which is the reason for the assignmentOver the golden grid's 136 interior rows (192 points scanned: 136 interior, 24
The dropped term is noise, not an The worst row (8.0e-05) sits in the band this repo calls a real difference rather Conditional on
|
aornugent
left a comment
There was a problem hiding this comment.
The profit column is the right thing to add and the envelope argument is the right
way to get it — carbon reaching plant's mass budget is leaf.profit_, so a gradient
set that stops at assim_colimited_ reaches no demographic model. The measurement
justifying the zeroing (eleven orders between the exact and differenced instruments
at the median) is the kind of evidence this decision needed.
Requesting changes on five points. The first two are rebase blockers rather than
design objections.
1. The golden file is stale, and it is not a text conflict
26574f4 (#86, R_d rises with temperature) landed after this branched and moved every
recorded cell. interior-1layer / vcmax_25 / A:
master 0x1.56ff4d6eecf96p-6
this PR 0x1.19f24c79f383p-6
So "the four existing columns are bit-identical" holds against the branch point and
not against master. Rebasing means regenerating tests/testthat/gradient_golden.tsv
in full via tools/gradient_golden.R — and per .claude/CLAUDE.md that has to happen
on macOS/arm64, so it cannot be done as part of a conflict resolution on any other
box. Worth doing before the rest of the review, since the regenerated numbers are what
the profit column's own values will be measured against.
2. helper-golden.R reopens a question master has already closed, at a number master's own CI says is too tight
Master added gradient_golden_tolerance() <- 5.0e-3 and a tolerance = parameter on
expect_golden(). This PR adds golden_tolerance(field, kind) returning 1e-3 for the
gradient file. The two are the same fix by different routes, but the numbers disagree
and master's is backed by the tighter measurement: it is not that the gradients are
argmax-evaluated, it is that the tolerance is set by the smallest differentiated
parameter's relative step. R_d_25 is 1.44 where vcmax_25 is 96, and master
records dA/dR_d_25 disagreeing by 1.3e-03 on Linux CI. A rebased version of this
PR carrying kind = "gradient" → 1e-3 would fail on that column.
Suggest dropping the kind argument entirely and using master's
tolerance = gradient_golden_tolerance() at the call site. It is also the more general
shape — per-call-site tolerance rather than a two-valued enum that has to grow a member
per file.
3. Two hand-maintained copies of the output names
.gradient_output_names in R/gradient.R and output_names() in gradient.hpp are
now both literals, each carrying a
is not. That comment is describing a hazard that does not have to exist: par_names
has the same problem and solves it by having test-gradient-batch.R read the C++ list
back and compare, and the output list can go one better by having R read it rather than
restate it — export a gradient_output_names() alongside the existing
gradient_par_names() and make .gradient_output_names a first-call cache over it,
like .gradient_outputs_idx() already is. Then adding a sixth output is one edit
instead of two that can disagree.
4. direct + rounded(0 * dpsi_dtheta) rather than assigning the column
In gradient_ift, profit's answer is reached by zeroing dY_dpsi[out_profit] and
letting the generic line multiply it by dpsi_dtheta. collar in the same loop is
handled the other way — assigned explicitly, with a comment saying that beats relying
on a difference of two identical numbers. The same argument applies here and buys more:
0 * d_psi is NaN for any non-finite d_psi, and #88 makes d_psi caller-supplied
(and .gradient_dpsi_dtheta checks is.numeric && !anyNA, not is.finite), so a
caller passing Inf gets ±Inf in four columns and NaN in profit alone. Setting
out[k * n_outputs + out_profit] = direct[out_profit] is immune and says the thing the
comment above it is already saying.
5. The shut-down rows record a number nothing asserts
shutdown-1layer / stem_b records dprofit/dstem_b ≈ 2.13 where A, gc, psi_stem
and collar are all exactly zero, at a row whose own status is no-gradient. As a
drift detector that is fine; as a baseline it pins a large value with nothing saying
what it ought to be, in the one regime where profit_ comes from a branch that sets it
without the other outputs (hazard 8). Worth an explicit assertion of the shut-down
profit column against its closed form, rather than only the recorded hex.
Minor
The envelope paragraph and its three-row table now appear in NEWS.md, the roxygen
block, the generated .Rd, README.md, two vignettes, and inline in both R/gradient.R
and gradient.hpp — seven coordinated edits if the decision ever changes. CLAUDE.md
makes this argument about commit bodies ("that is the habit to drop"); it applies to
source comments too. The inline C++ and R comments could be the decision plus a pointer
to ?leaf_gradient without losing anything.
The four differentiated outputs are what a gas-exchange calibration observes. plant bills carbon from `leaf.profit_` and water from `soil_consumption_`, so the two sets were disjoint and no trait gradient here reached a demographic model. Adds `profit` as a fifth column. It is the one output the envelope theorem reaches: at an interior optimum `dprofit/dpsi = 0`, so its gradient is the direct partial alone. The dropped term is noise, not truncation — a central difference of a flat maximum — and keeping it moves the answer by up to 8.0e-05. The four existing columns are bit-identical. Closes #87. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tolerance is keyed on a field NAME, and `profit` now names two quantities in different conditioning classes: the operating point's is the maximum itself and well conditioned at 1e-5, while the gradient's is a derivative of an argmax-evaluated quantity and belongs with its four siblings at 1e-3. Linux CI failed at 2.4e-04 while macOS passed bit-exactly. `expect_golden()` takes `kind`. And the envelope test asserted two magnitudes measured on macOS/arm64. The same point gives 6.6e-11 and 8.0e-10 on Linux, not 2.4e-15 and 2.1e-04 — a different noise floor, not a different answer. Those two are gated on the platform predicate the golden files already use; the correctness check holds everywhere. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Rebased onto master. `gradient_golden.tsv` regenerated in full on macOS/arm64; the four pre-existing columns are bit-identical to MASTER, not merely to the branch point, and the envelope measurement was re-taken after #84 and #86. - Dropped `golden_tolerance(kind =)` for master's `gradient_golden_tolerance()`, which is the same fix with the better-reasoned number: it is the smallest differentiated parameter's step that sets it, not the argmax amplification. - `gradient_output_names()` is exported and R reads it, so the output list has one definition rather than two that can disagree. - profit's column is ASSIGNED from the direct term rather than reached by multiplying a zeroed dY/dpsi -- the treatment `collar` already gets, and immune to a non-finite dpsi/dtheta where `0 * x` would be NaN in one column alone. - The shut-down profit column is asserted against its closed form: `A = -R_d` gives `dprofit/dR_d_25 = -1`, and the pinned collar gives `dcollar/dpsi_crit = 1`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
0ea6759 to
e1d8249
Compare
|
All five addressed, pushed as 1. Stale golden file — done, and the check is now against masterRebased onto So the four pre-existing columns are bit-identical to master, not to the
2.
|
The test comment described zeroing dY_dpsi["profit"], which the review replaced with assigning the column; the assignment is now what it says. The recorded-gradient tolerance's headroom is 2.1x, not the ~4x claimed: `profit` is smaller in magnitude than `A` at the same absolute floor, so Linux CI's whole-file worst goes 1.30e-03 -> 2.34e-03 against 5e-03. The same figure in CLAUDE.md is updated. `dY_dpsi[profit] = resid` is marked unexercised: its only reader is a forced ift at a pinned point, which throws at all 42 pinned rows. Comments only. C++ 486 checks / 0 failures, 576 golden points bit-identical; R suite 0 failures, golden worst difference 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Thanks, requested changes have been implemented
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 four differentiated outputs are what a gas-exchange calibration observes.
plant bills carbon from
leaf.profit_and water fromsoil_consumption_, sothe two sets were disjoint and no trait gradient here reached a demographic
model. Adds
profitas a fifth column.It is the one output the envelope theorem reaches: at an interior optimum
dprofit/dpsi = 0, so its gradient is the direct partial alone. The droppedterm is noise, not truncation — a central difference of a flat maximum — and
keeping it moves the answer by up to 8.0e-05.
The four existing columns are bit-identical. Closes #87.