Code Structure¶
This guide documents how state is advanced each timestep and the conventions that keep flux calculations pure and pool updates centralized.
Timestep Phases (in updateState()
)¶
1) Initialize fluxes
- Zero all fluxes.*
and fluxes.event*
.
2) Compute fluxes (pure calculations)
- calculateFluxes()
computes photosynthesis, respiration, water/snow, etc.
- processEvents()
converts scheduled/instant events to fluxes.event*
deltas (no pool mutation).
- soilDegradation()
and other biogeochemical modules compute additional flux rates only (no pool mutation).
- No function in this phase mutates envi.*
or trackers.*
.
3) Apply pool updates (single place)
- applyPoolUpdates()
is the only code that changes envi.*
.
- For each pool P: ΔP = (sum of rate fluxes to P) * climate.length + (sum of fluxes.event*
deltas to P).
- Apply bounds, conservation, and cross-pool constraints here.
4) Trackers and running means
- updateTrackers()
uses timestep-integrated values (rate * climate.length) plus event deltas.
5) Output
- outputState()
and any optional diagnostics/logging.
Pseudocode outline¶
- updateState():
- zeroFluxes()
- calculateFluxes() // pure rates
- processEvents() // sets fluxes.event* deltas only
- soilDegradation() // pure rates
- applyPoolUpdates() // the only place that mutates envi.*
- updateTrackers()
- outputState()
Mutability Rules (must-follow)¶
- Only
applyPoolUpdates()
may changeenvi.*
. - Flux calculators:
- May read
envi.*
,params.*
,ctx.*
,climate.*
. - May write
fluxes.*
(rates) andfluxes.event*
(event deltas). - Must not mutate
envi.*
,trackers.*
, or perform I/O as logic side-effects. - Events never change pools directly; they only add to
fluxes.event*
.
Units and Integration¶
- Rate fluxes in
fluxes.*
are per-day rates (pool units per day). - Event deltas in
fluxes.event*
are direct pool deltas (same units as pools), not rates. - Integration per pool each timestep:
- Δpool_from_rates = (sum of relevant
fluxes.*
) * climate.length - Δpool_from_events = (sum of relevant
fluxes.event*
) - pool += Δpool_from_rates + Δpool_from_events
Naming Conventions¶
- envi.* State variables (pools, water, snow, canopy, soil layers).
- fluxes.* Per-day flux rates computed in the flux phase.
- fluxes.event* Instantaneous/event deltas to be applied during pool update.
- trackers.* Integrated timestep values, cumulative sums, yearly aggregates.
- params.* Fixed run parameters (immutable during a run).
- ctx.* Feature flags / configuration switches.
- climate.* Forcing for the current timestep (e.g., length, met drivers).
- diag.* Optional transient diagnostics (no side effects on state).
Name fluxes by direction and target, e.g., fluxes.NPP
, fluxes.soilRespiration
, fluxes.leafLitterToSoil
, fluxes.eventHarvestC
. Prefer “to/from” clarity for transfers.
Pool Update Responsibilities¶
- Apply all additions/removals in a consistent order if constraints require it (e.g., water first if it bounds biochemical rates next step).
- Enforce invariants:
- No negative pools; clamp with tracked deficits and warnings if needed.
- Mass conservation across linked pools (e.g., C/N stoichiometry) with balanced cross-pool transfers.
- Centralize any event-specific application here (e.g., harvest removing biomass, adding residues).
Adding a New Flux or Event¶
- Rates: add a
fluxes.*
variable, compute it in a flux function, and integrate it inapplyPoolUpdates()
. - Events: add a
fluxes.event*
delta, accumulate inprocessEvents()
, apply it inapplyPoolUpdates()
. - Do not mutate
envi.*
in calculators or event processors.
Logging & Errors¶
- Use
logError()
andlogWarning()
(not printf) so tests can capture output. - Messages should include timestep context: year, day, event type (if relevant), and the offending value(s).
- Emit warnings on clamping, conservation corrections, or unexpected negative fluxes.# Code Structure
This guide documents how state is advanced each timestep and the conventions that keep flux calculations pure and pool updates centralized.
Timestep Phases (in updateState()
)¶
1) Initialize fluxes
- Zero all fluxes.*
and fluxes.event*
.
2) Compute fluxes (pure calculations)
- calculateFluxes()
computes photosynthesis, respiration, water/snow, etc.
- processEvents()
converts scheduled/instant events to fluxes.event*
deltas (no pool mutation).
- soilDegradation()
and other biogeochemical modules compute additional flux rates only (no pool mutation).
- No function in this phase mutates envi.*
or trackers.*
.
3) Apply pool updates (single place)
- applyPoolUpdates()
is the only code that changes envi.*
.
- For each pool P: ΔP = (sum of rate fluxes to P) * climate.length + (sum of fluxes.event*
deltas to P).
- Apply bounds, conservation, and cross-pool constraints here.
4) Trackers and running means
- updateTrackers()
uses timestep-integrated values (rate * climate.length) plus event deltas.
5) Output
- outputState()
and any optional diagnostics/logging.
Pseudocode outline¶
- updateState():
- zeroFluxes()
- calculateFluxes() // pure rates
- processEvents() // sets fluxes.event* deltas only
- soilDegradation() // pure rates
- applyPoolUpdates() // the only place that mutates envi.*
- updateTrackers()
- outputState()
Mutability Rules (must-follow)¶
- Only
applyPoolUpdates()
may changeenvi.*
. - Flux calculators:
- May read
envi.*
,params.*
,ctx.*
,climate.*
. - May write
fluxes.*
(rates) andfluxes.event*
(event deltas). - Must not mutate
envi.*
,trackers.*
, or perform I/O as logic side-effects. - Events never change pools directly; they only add to
fluxes.event*
.
Units and Integration¶
- Rate fluxes in
fluxes.*
are per-day rates (pool units per day). - Event deltas in
fluxes.event*
are direct pool deltas (same units as pools), not rates. - Integration per pool each timestep:
- Δpool_from_rates = (sum of relevant
fluxes.*
) * climate.length - Δpool_from_events = (sum of relevant
fluxes.event*
) - pool += Δpool_from_rates + Δpool_from_events
Naming Conventions¶
- envi.* State variables (pools, water, snow, canopy, soil layers).
- fluxes.* Per-day flux rates computed in the flux phase.
- fluxes.event* Instantaneous/event deltas to be applied during pool update.
- trackers.* Integrated timestep values, cumulative sums, yearly aggregates.
- params.* Fixed run parameters (immutable during a run).
- ctx.* Feature flags / configuration switches.
- climate.* Forcing for the current timestep (e.g., length, met drivers).
- diag.* Optional transient diagnostics (no side effects on state).
Name fluxes by direction and target, e.g., fluxes.NPP
, fluxes.soilRespiration
, fluxes.leafLitterToSoil
, fluxes.eventHarvestC
. Prefer “to/from” clarity for transfers.
Pool Update Responsibilities¶
- Apply all additions/removals in a consistent order if constraints require it (e.g., water first if it bounds biochemical rates next step).
- Enforce invariants:
- No negative pools; clamp with tracked deficits and warnings if needed.
- Mass conservation across linked pools (e.g., C/N stoichiometry) with balanced cross-pool transfers.
- Centralize any event-specific application here (e.g., harvest removing biomass, adding residues).
Adding a New Flux or Event¶
- Rates: add a
fluxes.*
variable, compute it in a flux function, and integrate it inapplyPoolUpdates()
. - Events: add a
fluxes.event*
delta, accumulate inprocessEvents()
, apply it inapplyPoolUpdates()
. - Do not mutate
envi.*
in calculators or event processors.
Logging & Errors¶
- Use
logError()
andlogWarning()
(not printf) so tests can capture output. - Messages should include timestep context: year, day, event type (if relevant), and the offending value(s).
- Emit warnings on clamping, conservation corrections, or unexpected negative fluxes.