Found while fixing #105, which covers the same defect at the two exits outside the solve. This is the half that is inside the objective function, so it moves the argmax rather than just the reported state — hence a separate issue.
The gap
set_leaf_states_rates_from_psi_stem has three branches. The transpiring one derives the operating point's leaf temperature and re-derives the Farquhar block at it:
Tleaf_ = use_energy_balance_ ? leaf_temp_from_E(transpiration_) : leaf_temp_;
if (use_energy_balance_) {
update_temperature_dependent_params(Tleaf_);
}
The two zero-transpiration branches — psi_upstream >= psi_stem, and the inner assim_max_ < 0 — write Tleaf_ and stop:
ci_ = gamma_*umol_per_mol_to_Pa_;
transpiration_ = 0;
stom_cond_CO2_ = 0;
Tleaf_ = use_energy_balance_ ? leaf_temp_from_E(0.0) : leaf_temp_;
So ci_ is read off gamma_, and assim_colimited_ = assim_colimited(ci_) at the bottom of the function is evaluated, at whatever temperature the block currently holds.
Why this is sharper than #105, not milder
At the exits #105 fixed, the block held the Tair baseline — wrong, but at least a fixed wrong temperature. Here it holds whatever the previous candidate left, because the transpiring branch mutates the block per candidate and by design (update_temperature_dependent_params bypasses the cache). So these branches read neither Tair nor their own temperature: they read the last psi the optimiser happened to probe.
That makes the objective history-dependent — the value at a given psi depends on the order candidates were visited. Hazard 3 is about the argmax varying smoothly with inputs, and an objective whose value depends on evaluation order is a mechanism for exactly the kind of non-smoothness that hazard describes.
Why it was not folded into #105
profit_psi_stem_TF calls this function, so this is inside the function the collar solve maximises. Changing it moves the argmax on the PM path, where #105 only moved the reported state at exits the optimiser never reaches. Different blast radius, different evidence needed.
What is needed
- Whether
psi_upstream >= psi_stem is reachable during a collar solve at all, or only at bracket endpoints and through the standalone optimise_psi_stem_* entry points. The inner assim_max_ < 0 looks unreachable from find_root_collar_psi — prepare_collar_solve returns early on that condition before the optimiser runs — but it is reachable from the single-layer optimisers.
- The measured effect on the argmax and on
profit over the PM-path grid, since operating_points.tsv runs gate-off and cannot see it.
- ⚠️ It adds transcendentals to a branch of the hot path (~10³ candidates per solve), so hazard 5's interleaved A/B applies. The branch is presumably cold, but that should be measured rather than assumed.
Found while fixing #105, which covers the same defect at the two exits outside the solve. This is the half that is inside the objective function, so it moves the argmax rather than just the reported state — hence a separate issue.
The gap
set_leaf_states_rates_from_psi_stemhas three branches. The transpiring one derives the operating point's leaf temperature and re-derives the Farquhar block at it:The two zero-transpiration branches —
psi_upstream >= psi_stem, and the innerassim_max_ < 0— writeTleaf_and stop:So
ci_is read offgamma_, andassim_colimited_ = assim_colimited(ci_)at the bottom of the function is evaluated, at whatever temperature the block currently holds.Why this is sharper than #105, not milder
At the exits #105 fixed, the block held the Tair baseline — wrong, but at least a fixed wrong temperature. Here it holds whatever the previous candidate left, because the transpiring branch mutates the block per candidate and by design (
update_temperature_dependent_paramsbypasses the cache). So these branches read neither Tair nor their own temperature: they read the last psi the optimiser happened to probe.That makes the objective history-dependent — the value at a given psi depends on the order candidates were visited. Hazard 3 is about the argmax varying smoothly with inputs, and an objective whose value depends on evaluation order is a mechanism for exactly the kind of non-smoothness that hazard describes.
Why it was not folded into #105
profit_psi_stem_TFcalls this function, so this is inside the function the collar solve maximises. Changing it moves the argmax on the PM path, where #105 only moved the reported state at exits the optimiser never reaches. Different blast radius, different evidence needed.What is needed
psi_upstream >= psi_stemis reachable during a collar solve at all, or only at bracket endpoints and through the standaloneoptimise_psi_stem_*entry points. The innerassim_max_ < 0looks unreachable fromfind_root_collar_psi—prepare_collar_solvereturns early on that condition before the optimiser runs — but it is reachable from the single-layer optimisers.profitover the PM-path grid, sinceoperating_points.tsvruns gate-off and cannot see it.