Add an optional coarse-root pool to FF16 and TF24 - #644
Conversation
Both models carried one root pool, fine roots proportional to leaf area, so below-ground structure did not scale with tree size. Coarse roots are now the below-ground continuation of the sapwood cylinder, m_cr = a_cr1 * m_s, which makes the structural root:shoot ratio size-invariant and keeps the growth allocation closed-form. a_cr1 defaults to 0, so no model's output and no scientific version moves. expand_state() gains a mass_coarse_root column. Closes #349 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Why this shapeThree functional forms were on the table for
Why
|
| FF16 | TF24 | TF24f | |
|---|---|---|---|
a_cr1 |
0 | 0 | 0 |
k_cr |
0.2 (= k_s) |
0.2 | 0.2 |
r_cr |
6.5987 (= r_s) |
6.5987 | 6.5987 |
— with no existing default changed and no version string moved. r_cr/k_cr default to the sapwood rates because coarse roots are woody; as with r_b, that is a default and not a link, so setting r_s afterwards does not move them.
Coarse roots are kept out of TF24's water uptake
TF24 distributes fine-root mass over soil depth to drive uptake and reads a_r1 directly to do it. Absorbing surface is a fine-root property, so coarse roots are excluded by construction, and test-coarse-roots.R pins it: at fixed size, root_mass, transpiration, E_up_, opt_psi_stem, opt_root_psi and assimilation are all bit-identical with the pool on.
Testing
New tests/testthat/test-coarse-roots.R, 22 assertions, split between "it really is inert at the defaults" and "it is correct when switched on" — a default-off feature only ever run at its default is not tested at all.
The load-bearing one reconstructs dH/dt outright from the published chain rather than checking a ratio, because a ratio survives an error common to both denominators:
dH/dt = dH/dA_l * P * (1 - r(H)) / (dM_live/dA_l)
with r_cr = k_cr = 0 so net production is identical on both sides and the whole change is allocation. Agreement to 1e-12 against R formulas written from inst/docs/FF16/FF16-eqns.csv, not read back from the strategy.
Also extended: test-expand-state.R re-runs its oracle comparison with a_cr1 = 0.25 (at the default every coarse-root term is zero, so the existing checks would pass even if C++ ignored a_cr1 entirely), and both AD kernel tests now carry a non-zero a_cr1 so the new terms are actually differentiated.
Full suite: 3221 pass, 0 fail, plus the scenario gateway run separately with PLANT_RUN_SCENARIOS=1 — green, so TF24's end-to-end numbers have not moved.
Note for whoever builds this
develop pins odelia (== 0.3.1) but 0.4.0 is what's installed locally, so compile_dll() refuses before it compiles anything (the #643 blocker). I built against a git archive of the v0.3.1 tag into a scratch library rather than touching the pin.
Not done here
The vertical distribution of root mass over depth — @aornugent's comment on #349, mapping the Rhizomorph cases with an eta-analogue over (depth_max, 0] — is a separate piece of work. This adds the pool; it does not place it in the soil.
Both models carried one root pool, fine roots proportional to leaf
area, so below-ground structure did not scale with tree size. Coarse
roots are now the below-ground continuation of the sapwood cylinder,
m_cr = a_cr1 * m_s, which makes the structural root:shoot ratio
size-invariant and keeps the growth allocation closed-form.
a_cr1 defaults to 0, so no model's output and no scientific version
moves. expand_state() gains a mass_coarse_root column.
Closes #349