Skip to content

[TF24] Prototype of multirate stepper (DO NOT MERGE) - #569

Draft
aornugent wants to merge 12 commits into
traitecoevo:developfrom
aornugent:claude/tf24-multi-rate-stepper-n5audm
Draft

[TF24] Prototype of multirate stepper (DO NOT MERGE)#569
aornugent wants to merge 12 commits into
traitecoevo:developfrom
aornugent:claude/tf24-multi-rate-stepper-n5audm

Conversation

@aornugent

@aornugent aornugent commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Hi!

I have been tinkering with different solver configurations for TF24 including multirate integration (MRI) and implicit/explicit (IMEX) split operators.

This prototype has a bunch of baggage (e.g. cohort collocation, ruled out) and is only really a quick spike to see if there were any quick wins. I don't think it constitutes a production quality design, but I'm sharing it in case there are parts you'd like to cherry pick.

Maybe addresses some of: #566

This PR is paired with traitecoevo/odelia#42

claude added 12 commits July 18, 2026 00:36
Wire odelia's method="mri" multirate stepper through the SCM's resident
Solver, so the TF24 soil column (the stiff fast block) is sub-cycled while the
cohorts (the slow block) advance on a fixed macro grid.

The Patch is already laid out [cohorts | environment], which is exactly MRI's
[slow | fast] layout, so it becomes a multirate System with additive hooks and
no wrapper:
  - slow_size / fast_size / coupling_size (0: the fast block reads its slow
    context from the light field frozen per leg, not a linear aggregate),
  - freeze_slow(x): set the cohorts and rebuild the light field once per leg,
  - fast_rates / slow_rates: the soil and cohort tendencies.
patch.h includes odelia/mri.hpp so every Solver<Patch> TU has MriStep::step
defined, not just declared.

Control gains an ode_method string ("" == "rkck"), mapped to the odelia
Method enum where the SCM builds its Solver. Every other integration path is
untouched: an SCM run with ode_method "" or "rkck" is bit-identical to
before (verified: offspring and net_reproduction_ratios identical). On a real
TF24 run method="mri" at a daily macro step is stable and matches the adaptive
RKCK offspring to ~0.5%.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvyqPT8pVL7bCZvWqmhZHM
… item 4)

The multirate fast sub-cycle needs the per-layer root uptake at each micro-step.
That uptake is a density-weighted integral of per-cohort consumption over the
size distribution, so with the canopy frozen for a leg it can be quadratured at
m << N cohorts instead of re-solving every one.

- Individual::consumption_given_height(h, env): the factored coupling seam --
  per-cohort per-layer uptake at an arbitrary height under the frozen-light,
  soil-theta environment. Mirrors growth_rate_given_height.
- Species::set_collocation_nodes(m) / add_collocated_consumption(env, out):
  capture m log-spaced heights spanning the current distribution and their
  density*trapezium weights, then re-evaluate only those m cohorts under the
  current soil state. The same integral consumption_rate() computes, sampled at
  m points (O(m^-2)).
- control (0 = exact full-N): switchable, consulted only on
  the method="mri" path, so every other run is bit-identical.
- Patch::freeze_slow caches the nodes per leg; Patch::fast_rates uses the m-node
  quadrature instead of a full-N compute_rates.

No odelia change and no new class: coupling_size=0 + freeze_slow keep the
aggregate inside fast_rates, so the engine stays model-agnostic. Compiles clean;
collocated-vs-full-N accuracy + timing being measured next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvyqPT8pVL7bCZvWqmhZHM
The first cut placed collocation nodes at synthetic log-spaced heights and
interpolated the size distribution onto them. Interpolating the irregular
evolved distribution was the dominant error (soil ~8% at m=20), and log-density
interpolation produced NaN weights where a cohort's density had vanished
(log_density -> -Inf), crashing aged stands.

Subsample m of the N nodes evenly by index and quadrature over their actual
heights and densities -- the same trapezium consumption_rate uses, on a coarser
node set. No interpolation, no NaN: soil error falls to ~2.5% at m=20 and ~0.5%
at m=40 on a real N=84 stand, converging ~O(m^-2); m >= N reduces to exact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvyqPT8pVL7bCZvWqmhZHM
Measured on real cohorts: the even-index subsample + trapezium meets sub-1%
only for young/uniform distributions by m≈20; on mature skewed stands it needs
m close to N (life=8, N=91: m=20 -> 38% soil error, m=80 -> 0.8%). Correct the
control-field comment accordingly. Smarter, importance-weighted node placement
(plant#53 §6/§4.4) is the open item to make collocation meet gate 4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvyqPT8pVL7bCZvWqmhZHM
Global counter incremented per Patch::fast_rates call (one O(M) coupling eval,
the fast-block cost driver), with mri_fast_rate_calls_get/reset exported to R.
Measures fast-evals per macro step: current no-split MRI does ~58/macro (~9.7
micro-steps/macro) on a life=3 TF24 run -- the headroom the exact-flow split
(Lever 1) targets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvyqPT8pVL7bCZvWqmhZHM
…ver 1)

Bring TF24_Environment::analytic_partial_flow / residual_rhs / infiltration_rate
/ drainage_touchdown_time (+ R wrappers, + test-tf24-soil-split.R) onto this
branch. Reviewed for conformance: idiomatic (uses soil_parameter_value/dz),
infiltration_rate is a single-source refactor of the compute_rates inline code,
and the split reproduces compute_rates (test passes here). This is the exact,
knob-free fast-block flow map the multirate split inner (Lever 1) needs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvyqPT8pVL7bCZvWqmhZHM
…ontrol

Patch opts into odelia's split inner when control$mri_use_split is set and the
environment provides the R1 hooks (env_has_split trait; FF16 still compiles and
is never split). New hooks: mri_split() (the runtime switch), analytic_flow
(exact drainage recession on the soil block), residual_rhs (the gentle remainder
+ uptake; aux flux diagnostics left at zero rate during split legs). compute_rates
refactored into compute_species_rates + assemble_resource_depletion, reused by a
shared fast_block_uptake() helper (adaptive and split paths); production values
unchanged (test-patch / test-environment-TF24 pass, control default off).

E-A (daily macro grid, life=3): split is correct (offspring rel 4e-4, soil 8e-5
vs adaptive) but NOT faster -- 103 vs 58 fast-evals/macro, ~0.6x. Both ~10
micro-steps/macro: at the daily grid the sub-cycle is accuracy-driven, not
drainage-stiffness-driven, so exact flow doesn't cut the step count and ROS
Jacobian overhead dominates. Lever 1's win regime is coarse H (measuring next).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvyqPT8pVL7bCZvWqmhZHM
patch_rhs_calls counts Patch::compute_rates (one full coupled RHS eval), the
cost/step proxy shared by rkck and rodas, with get/reset exported to R. Used to
test whether the REAL coupled patch (not the soil-only/surrogate #43 analysis)
is accuracy- or stability-limited: does RODAS clear the RK45 tight-tolerance
wall and take fewer steps?

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvyqPT8pVL7bCZvWqmhZHM
… diagnostic)

Borrowed from the review branch (commit 8399794), reviewed for conformance:
set_shutdown_state left soil_consumption_/E_up_ stale, so a reused leaf fed a
phantom, soil-independent uptake into the balance -- draining soil when nothing
transpired and zeroing the reverse-mode uptake gradient across drought. Zero
them at shutdown so uptake vanishes smoothly at the wilting point. Idiomatic,
disjoint from the MRI work; test-tf24-shutdown.R passes. Also adds a
patch_rhs_calls diagnostic counter (full-RHS evals) used for the stiff-regime
method comparison.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvyqPT8pVL7bCZvWqmhZHM
Borrowed from the review branch (commit 4e96b4b), reconciled with R1: the soil
ODE state is now zeta = ln(theta - theta_res); theta recovered wherever the
physics needs it; R interface (set/get_soil_water_state) stays in theta; env
set_ode_state/ode_state/ode_rates pass raw zeta so the MRI partition is
transparent. Purpose: verify whether R-D changes the MRI drought accuracy
(predicted neutral, per the branch's rd_rodas_bench + the errors being
cohort-layer not soil-layer). NOTE: R-D's updated tests (test-patch,
test-environment-TF24, test-tf24-shutdown raw-state assertions, test-tf24-logchart)
are not yet ported -- keep only if the verification warrants.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvyqPT8pVL7bCZvWqmhZHM
…patch)

Maps control$ode_method='imex' to the odelia IMEX stepper. No Patch hook
changes: the MRI fast/slow partition already places the soil+aux block as the
contiguous tail, which is exactly the implicit block the block-FD Jacobian
needs. Bit-identical when off. Real-patch accuracy/timing verification in
progress.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvyqPT8pVL7bCZvWqmhZHM
…periment]"

This reverts commit fc0dd2b.

R-D (the zeta = ln(theta - theta_res) soil log-depletion chart) is confirmed
dead: the accuracy wall is in the cohort layer, not the soil layer, so a
soil-chart reshape is provably neutral (see the event-aware recharacterization
and BASELINE). Removing it returns tf24_environment.h to the plain theta state,
dropping named machinery that no longer earns its place. rkck production is
unaffected (soil state representation is internal; R interface stayed in theta).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvyqPT8pVL7bCZvWqmhZHM
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.

2 participants