1.3.82
 
Loading...
Searching...
No Matches
PhotosynthesisModel.cpp
Go to the documentation of this file.
1
16#include "PhotosynthesisModel.h"
17#include <complex>
18
19using namespace std;
20using namespace helios;
21
22namespace {
23 // Material data labels for Empirical model
24 constexpr const char *LABEL_EMP_Tref = "photo_emp_Tref";
25 constexpr const char *LABEL_EMP_Ci_ref = "photo_emp_Ci_ref";
26 constexpr const char *LABEL_EMP_Asat = "photo_emp_Asat";
27 constexpr const char *LABEL_EMP_theta = "photo_emp_theta";
28 constexpr const char *LABEL_EMP_Tmin = "photo_emp_Tmin";
29 constexpr const char *LABEL_EMP_Topt = "photo_emp_Topt";
30 constexpr const char *LABEL_EMP_q = "photo_emp_q";
31 constexpr const char *LABEL_EMP_R = "photo_emp_R";
32 constexpr const char *LABEL_EMP_ER = "photo_emp_ER";
33 constexpr const char *LABEL_EMP_kC = "photo_emp_kC";
34
35 // Material data labels for Farquhar model
36 constexpr const char *LABEL_FQ_Vcmax = "photo_fq_Vcmax";
37 constexpr const char *LABEL_FQ_Jmax = "photo_fq_Jmax";
38 constexpr const char *LABEL_FQ_Rd = "photo_fq_Rd";
39 constexpr const char *LABEL_FQ_alpha = "photo_fq_alpha";
40 constexpr const char *LABEL_FQ_O = "photo_fq_O";
41 constexpr const char *LABEL_FQ_TPU_flag = "photo_fq_TPU_flag";
42 constexpr const char *LABEL_FQ_TPU = "photo_fq_TPU";
43 constexpr const char *LABEL_FQ_TPU_dHa = "photo_fq_TPU_dHa";
44 constexpr const char *LABEL_FQ_TPU_dHd = "photo_fq_TPU_dHd";
45 constexpr const char *LABEL_FQ_TPU_Topt = "photo_fq_TPU_Topt";
46 constexpr const char *LABEL_FQ_theta = "photo_fq_theta";
47 constexpr const char *LABEL_FQ_theta_dHa = "photo_fq_theta_dHa";
48 constexpr const char *LABEL_FQ_theta_dHd = "photo_fq_theta_dHd";
49 constexpr const char *LABEL_FQ_theta_Topt = "photo_fq_theta_Topt";
50 constexpr const char *LABEL_FQ_c_Rd = "photo_fq_c_Rd";
51 constexpr const char *LABEL_FQ_c_Vcmax = "photo_fq_c_Vcmax";
52 constexpr const char *LABEL_FQ_c_Jmax = "photo_fq_c_Jmax";
53 constexpr const char *LABEL_FQ_c_Gamma = "photo_fq_c_Gamma";
54 constexpr const char *LABEL_FQ_c_Kc = "photo_fq_c_Kc";
55 constexpr const char *LABEL_FQ_c_Ko = "photo_fq_c_Ko";
56 constexpr const char *LABEL_FQ_dH_Rd = "photo_fq_dH_Rd";
57 constexpr const char *LABEL_FQ_dH_Vcmax = "photo_fq_dH_Vcmax";
58 constexpr const char *LABEL_FQ_dH_Jmax = "photo_fq_dH_Jmax";
59 constexpr const char *LABEL_FQ_dH_Gamma = "photo_fq_dH_Gamma";
60 constexpr const char *LABEL_FQ_dH_Kc = "photo_fq_dH_Kc";
61 constexpr const char *LABEL_FQ_dH_Ko = "photo_fq_dH_Ko";
62 constexpr const char *LABEL_FQ_gm = "photo_fq_gm";
63 constexpr const char *LABEL_FQ_gm_dHa = "photo_fq_gm_dHa";
64 constexpr const char *LABEL_FQ_gm_dHd = "photo_fq_gm_dHd";
65 constexpr const char *LABEL_FQ_gm_Topt = "photo_fq_gm_Topt";
66
67 // Material data labels for von Caemmerer (2021) C4 model. Each temperature-responsive parameter
68 // serializes its full PhotosyntheticTemperatureResponseParameters quartet (value_at_25C, dHa, dHd, Topt).
69 constexpr const char *LABEL_C4_Vpmax = "photo_c4_Vpmax";
70 constexpr const char *LABEL_C4_Vpmax_dHa = "photo_c4_Vpmax_dHa";
71 constexpr const char *LABEL_C4_Vpmax_dHd = "photo_c4_Vpmax_dHd";
72 constexpr const char *LABEL_C4_Vpmax_Topt = "photo_c4_Vpmax_Topt";
73 constexpr const char *LABEL_C4_Vcmax = "photo_c4_Vcmax";
74 constexpr const char *LABEL_C4_Vcmax_dHa = "photo_c4_Vcmax_dHa";
75 constexpr const char *LABEL_C4_Vcmax_dHd = "photo_c4_Vcmax_dHd";
76 constexpr const char *LABEL_C4_Vcmax_Topt = "photo_c4_Vcmax_Topt";
77 constexpr const char *LABEL_C4_Jmax = "photo_c4_Jmax";
78 constexpr const char *LABEL_C4_Jmax_dHa = "photo_c4_Jmax_dHa";
79 constexpr const char *LABEL_C4_Jmax_dHd = "photo_c4_Jmax_dHd";
80 constexpr const char *LABEL_C4_Jmax_Topt = "photo_c4_Jmax_Topt";
81 constexpr const char *LABEL_C4_Rd = "photo_c4_Rd";
82 constexpr const char *LABEL_C4_Rd_dHa = "photo_c4_Rd_dHa";
83 constexpr const char *LABEL_C4_Rd_dHd = "photo_c4_Rd_dHd";
84 constexpr const char *LABEL_C4_Rd_Topt = "photo_c4_Rd_Topt";
85 constexpr const char *LABEL_C4_gm = "photo_c4_gm";
86 constexpr const char *LABEL_C4_gm_dHa = "photo_c4_gm_dHa";
87 constexpr const char *LABEL_C4_gm_dHd = "photo_c4_gm_dHd";
88 constexpr const char *LABEL_C4_gm_Topt = "photo_c4_gm_Topt";
89 // Kinetic constants
90 constexpr const char *LABEL_C4_Kc_25 = "photo_c4_Kc_25";
91 constexpr const char *LABEL_C4_Ko_25 = "photo_c4_Ko_25";
92 constexpr const char *LABEL_C4_Kp_25 = "photo_c4_Kp_25";
93 constexpr const char *LABEL_C4_gamma_star_25 = "photo_c4_gamma_star_25";
94 constexpr const char *LABEL_C4_Om_25 = "photo_c4_Om_25";
95 constexpr const char *LABEL_C4_dH_Kc = "photo_c4_dH_Kc";
96 constexpr const char *LABEL_C4_dH_Ko = "photo_c4_dH_Ko";
97 constexpr const char *LABEL_C4_dH_Kp = "photo_c4_dH_Kp";
98 constexpr const char *LABEL_C4_dH_gamma_star = "photo_c4_dH_gamma_star";
99 constexpr const char *LABEL_C4_dH_Om = "photo_c4_dH_Om";
100 // Scalar parameters
101 constexpr const char *LABEL_C4_alpha = "photo_c4_alpha";
102 constexpr const char *LABEL_C4_x = "photo_c4_x";
103 constexpr const char *LABEL_C4_Vpr = "photo_c4_Vpr";
104 constexpr const char *LABEL_C4_Rm_frac = "photo_c4_Rm_frac";
105 constexpr const char *LABEL_C4_fcyc = "photo_c4_fcyc";
106 constexpr const char *LABEL_C4_gbs = "photo_c4_gbs";
107 constexpr const char *LABEL_C4_ao = "photo_c4_ao";
108 constexpr const char *LABEL_C4_absorptance = "photo_c4_absorptance";
109 constexpr const char *LABEL_C4_f_spectral = "photo_c4_f_spectral";
110 constexpr const char *LABEL_C4_theta_etr = "photo_c4_theta_etr";
111 constexpr const char *LABEL_C4_h_protons = "photo_c4_h_protons";
112 constexpr const char *LABEL_C4_H_J = "photo_c4_H_J";
113 constexpr const char *LABEL_C4_H_Jcyc = "photo_c4_H_Jcyc";
114} // namespace
115
117 context = a_context;
118
119 // default values set here
120 model = "farquhar";
121
122 i_PAR_default = 0;
123 TL_default = 300;
124 CO2_default = 390;
125 gM_default = 0.25;
126 gH_default = 1;
127}
128
129
131 model = "empirical";
132}
133
135 model = "farquhar";
136}
137
139 model = "c4";
140}
141
142
144 empiricalmodelcoeffs = modelcoefficients;
145 empiricalmodel_coefficients.clear();
146 model = "empirical";
147}
148
149void PhotosynthesisModel::setModelCoefficients(const EmpiricalModelCoefficients &modelcoefficients, const std::vector<uint> &UUIDs) {
150 for (uint UUID: UUIDs) {
151 empiricalmodel_coefficients[UUID] = modelcoefficients;
152 }
153 model = "empirical";
154}
155
157 farquharmodelcoeffs = modelcoefficients;
158 farquharmodel_coefficients.clear();
159 model = "farquhar";
160}
161
162void PhotosynthesisModel::setModelCoefficients(const FarquharModelCoefficients &modelcoefficients, const std::vector<uint> &UUIDs) {
163 for (uint UUID: UUIDs) {
164 farquharmodel_coefficients[UUID] = modelcoefficients;
165 }
166 model = "farquhar";
167}
168
169void PhotosynthesisModel::setModelCoefficients(const std::vector<FarquharModelCoefficients> &modelcoefficients, const std::vector<uint> &UUIDs) {
170 if (modelcoefficients.size() != UUIDs.size()) {
171 std::cerr << "WARNING (PhotosynthesisModel::setModelCoefficients): number of model coefficients (" + std::to_string(modelcoefficients.size()) + ") does not match number of UUIDs (" + std::to_string(UUIDs.size()) + ")" << std::endl;
172 return;
173 }
174 for (uint i = 0; i < UUIDs.size(); i++) {
175 farquharmodel_coefficients[UUIDs.at(i)] = modelcoefficients.at(i);
176 }
177 model = "farquhar";
178}
179
181 c4modelcoeffs = modelcoefficients;
182 c4model_coefficients.clear();
183 model = "c4";
184}
185
186void PhotosynthesisModel::setModelCoefficients(const C4ModelCoefficients &modelcoefficients, const std::vector<uint> &UUIDs) {
187 for (uint UUID: UUIDs) {
188 c4model_coefficients[UUID] = modelcoefficients;
189 }
190 model = "c4";
191}
192
194 return getCoefficientsForPrimitive_C4(UUID);
195}
196
197void PhotosynthesisModel::setModelCoefficients(const std::string &material_label, const C4ModelCoefficients &coeffs) {
198 // Serialize the four PhotosyntheticTemperatureResponseParameters quartets
199 auto write_resp = [&](const char *base, const char *dHa, const char *dHd, const char *Topt, const PhotosyntheticTemperatureResponseParameters &r) {
200 context->setMaterialData(material_label, base, r.value_at_25C);
201 context->setMaterialData(material_label, dHa, r.dHa);
202 context->setMaterialData(material_label, dHd, r.dHd);
203 context->setMaterialData(material_label, Topt, r.Topt);
204 };
205 write_resp(LABEL_C4_Vpmax, LABEL_C4_Vpmax_dHa, LABEL_C4_Vpmax_dHd, LABEL_C4_Vpmax_Topt, coeffs.getVpmaxTempResponse());
206 write_resp(LABEL_C4_Vcmax, LABEL_C4_Vcmax_dHa, LABEL_C4_Vcmax_dHd, LABEL_C4_Vcmax_Topt, coeffs.getVcmaxTempResponse());
207 write_resp(LABEL_C4_Jmax, LABEL_C4_Jmax_dHa, LABEL_C4_Jmax_dHd, LABEL_C4_Jmax_Topt, coeffs.getJmaxTempResponse());
208 write_resp(LABEL_C4_Rd, LABEL_C4_Rd_dHa, LABEL_C4_Rd_dHd, LABEL_C4_Rd_Topt, coeffs.getRdTempResponse());
209 write_resp(LABEL_C4_gm, LABEL_C4_gm_dHa, LABEL_C4_gm_dHd, LABEL_C4_gm_Topt, coeffs.getMesophyllConductance_gmTempResponse());
210
211 context->setMaterialData(material_label, LABEL_C4_Kc_25, coeffs.Kc_25);
212 context->setMaterialData(material_label, LABEL_C4_Ko_25, coeffs.Ko_25);
213 context->setMaterialData(material_label, LABEL_C4_Kp_25, coeffs.Kp_25);
214 context->setMaterialData(material_label, LABEL_C4_gamma_star_25, coeffs.gamma_star_25);
215 context->setMaterialData(material_label, LABEL_C4_Om_25, coeffs.Om_25);
216 context->setMaterialData(material_label, LABEL_C4_dH_Kc, coeffs.dH_Kc);
217 context->setMaterialData(material_label, LABEL_C4_dH_Ko, coeffs.dH_Ko);
218 context->setMaterialData(material_label, LABEL_C4_dH_Kp, coeffs.dH_Kp);
219 context->setMaterialData(material_label, LABEL_C4_dH_gamma_star, coeffs.dH_gamma_star);
220 context->setMaterialData(material_label, LABEL_C4_dH_Om, coeffs.dH_Om);
221
222 context->setMaterialData(material_label, LABEL_C4_alpha, coeffs.alpha_psII_fraction);
223 context->setMaterialData(material_label, LABEL_C4_x, coeffs.x_etr_partition);
224 context->setMaterialData(material_label, LABEL_C4_Vpr, coeffs.Vpr);
225 context->setMaterialData(material_label, LABEL_C4_Rm_frac, coeffs.Rm_frac);
226 context->setMaterialData(material_label, LABEL_C4_fcyc, coeffs.fcyc);
227 context->setMaterialData(material_label, LABEL_C4_gbs, coeffs.gbs);
228 context->setMaterialData(material_label, LABEL_C4_ao, coeffs.ao);
229 context->setMaterialData(material_label, LABEL_C4_absorptance, coeffs.absorptance);
230 context->setMaterialData(material_label, LABEL_C4_f_spectral, coeffs.f_spectral);
231 context->setMaterialData(material_label, LABEL_C4_theta_etr, coeffs.theta_etr);
232 context->setMaterialData(material_label, LABEL_C4_h_protons, coeffs.h_protons);
233 context->setMaterialData(material_label, LABEL_C4_H_J, coeffs.H_J);
234 context->setMaterialData(material_label, LABEL_C4_H_Jcyc, coeffs.H_Jcyc);
235
236 // Invalidate the cache for this material so the next run() picks up the new values.
237 uint matID = context->getMaterialIDFromLabel(material_label);
238 material_coefficient_cache_c4.erase(matID);
239
240 model = "c4";
241}
242
243C4ModelCoefficients PhotosynthesisModel::getCoefficientsForPrimitive_C4(uint UUID) const {
244 // Resolution order: material data → cached material → per-UUID map → global default.
245 uint materialID = context->getPrimitiveMaterialID(UUID);
246
247 if (material_coefficient_cache_c4.find(materialID) != material_coefficient_cache_c4.end()) {
248 return material_coefficient_cache_c4.at(materialID);
249 }
250
251 C4ModelCoefficients coeffs;
252 bool found_in_material = false;
253 try {
254 const Material &mat = context->getMaterial(materialID);
255 // Use Vpmax presence as the sentinel — if it's set, assume the full C4 quartet is set.
256 if (mat.doesMaterialDataExist(LABEL_C4_Vpmax)) {
257 // Helper to deserialize a temperature-response quartet via the appropriate setter.
258 auto load_resp = [&](const char *base, const char *dHa_lbl, const char *dHd_lbl, const char *Topt_lbl, auto setter_constant, auto setter_arrhenius, auto setter_peaked) {
259 float v25 = 0.f, dHa = 0.f, dHd = 0.f, Topt = 0.f;
260 mat.getMaterialData(base, v25);
261 mat.getMaterialData(dHa_lbl, dHa);
262 mat.getMaterialData(dHd_lbl, dHd);
263 mat.getMaterialData(Topt_lbl, Topt);
264 if (Topt < 1000.f) {
265 setter_peaked(v25, dHa, Topt - 273.15f, dHd);
266 } else if (dHa != 0.f) {
267 setter_arrhenius(v25, dHa);
268 } else {
269 setter_constant(v25);
270 }
271 };
272
273 load_resp(LABEL_C4_Vpmax, LABEL_C4_Vpmax_dHa, LABEL_C4_Vpmax_dHd, LABEL_C4_Vpmax_Topt, [&](float v) { coeffs.setVpmax(v); }, [&](float v, float a) { coeffs.setVpmax(v, a); }, [&](float v, float a, float t, float d) { coeffs.setVpmax(v, a, t, d); });
274 load_resp(LABEL_C4_Vcmax, LABEL_C4_Vcmax_dHa, LABEL_C4_Vcmax_dHd, LABEL_C4_Vcmax_Topt, [&](float v) { coeffs.setVcmax(v); }, [&](float v, float a) { coeffs.setVcmax(v, a); }, [&](float v, float a, float t, float d) { coeffs.setVcmax(v, a, t, d); });
275 load_resp(LABEL_C4_Jmax, LABEL_C4_Jmax_dHa, LABEL_C4_Jmax_dHd, LABEL_C4_Jmax_Topt, [&](float v) { coeffs.setJmax(v); }, [&](float v, float a) { coeffs.setJmax(v, a); }, [&](float v, float a, float t, float d) { coeffs.setJmax(v, a, t, d); });
276 load_resp(LABEL_C4_Rd, LABEL_C4_Rd_dHa, LABEL_C4_Rd_dHd, LABEL_C4_Rd_Topt, [&](float v) { coeffs.setRd(v); }, [&](float v, float a) { coeffs.setRd(v, a); }, [&](float v, float a, float t, float d) { coeffs.setRd(v, a, t, d); });
277 load_resp(LABEL_C4_gm, LABEL_C4_gm_dHa, LABEL_C4_gm_dHd, LABEL_C4_gm_Topt, [&](float v) { coeffs.setMesophyllConductance_gm(v); }, [&](float v, float a) { coeffs.setMesophyllConductance_gm(v, a); }, [&](float v, float a, float t, float d) { coeffs.setMesophyllConductance_gm(v, a, t, d); });
278
279 // Kinetic constants — public fields, written directly. Use defaults if a label is missing
280 // (older serializations may pre-date a field; missing labels keep struct defaults).
281 if (mat.doesMaterialDataExist(LABEL_C4_Kc_25)) mat.getMaterialData(LABEL_C4_Kc_25, coeffs.Kc_25);
282 if (mat.doesMaterialDataExist(LABEL_C4_Ko_25)) mat.getMaterialData(LABEL_C4_Ko_25, coeffs.Ko_25);
283 if (mat.doesMaterialDataExist(LABEL_C4_Kp_25)) mat.getMaterialData(LABEL_C4_Kp_25, coeffs.Kp_25);
284 if (mat.doesMaterialDataExist(LABEL_C4_gamma_star_25)) mat.getMaterialData(LABEL_C4_gamma_star_25, coeffs.gamma_star_25);
285 if (mat.doesMaterialDataExist(LABEL_C4_Om_25)) mat.getMaterialData(LABEL_C4_Om_25, coeffs.Om_25);
286 if (mat.doesMaterialDataExist(LABEL_C4_dH_Kc)) mat.getMaterialData(LABEL_C4_dH_Kc, coeffs.dH_Kc);
287 if (mat.doesMaterialDataExist(LABEL_C4_dH_Ko)) mat.getMaterialData(LABEL_C4_dH_Ko, coeffs.dH_Ko);
288 if (mat.doesMaterialDataExist(LABEL_C4_dH_Kp)) mat.getMaterialData(LABEL_C4_dH_Kp, coeffs.dH_Kp);
289 if (mat.doesMaterialDataExist(LABEL_C4_dH_gamma_star)) mat.getMaterialData(LABEL_C4_dH_gamma_star, coeffs.dH_gamma_star);
290 if (mat.doesMaterialDataExist(LABEL_C4_dH_Om)) mat.getMaterialData(LABEL_C4_dH_Om, coeffs.dH_Om);
291
292 if (mat.doesMaterialDataExist(LABEL_C4_alpha)) mat.getMaterialData(LABEL_C4_alpha, coeffs.alpha_psII_fraction);
293 if (mat.doesMaterialDataExist(LABEL_C4_x)) mat.getMaterialData(LABEL_C4_x, coeffs.x_etr_partition);
294 if (mat.doesMaterialDataExist(LABEL_C4_Vpr)) mat.getMaterialData(LABEL_C4_Vpr, coeffs.Vpr);
295 if (mat.doesMaterialDataExist(LABEL_C4_Rm_frac)) mat.getMaterialData(LABEL_C4_Rm_frac, coeffs.Rm_frac);
296 if (mat.doesMaterialDataExist(LABEL_C4_fcyc)) mat.getMaterialData(LABEL_C4_fcyc, coeffs.fcyc);
297 if (mat.doesMaterialDataExist(LABEL_C4_gbs)) mat.getMaterialData(LABEL_C4_gbs, coeffs.gbs);
298 if (mat.doesMaterialDataExist(LABEL_C4_ao)) mat.getMaterialData(LABEL_C4_ao, coeffs.ao);
299 if (mat.doesMaterialDataExist(LABEL_C4_absorptance)) mat.getMaterialData(LABEL_C4_absorptance, coeffs.absorptance);
300 if (mat.doesMaterialDataExist(LABEL_C4_f_spectral)) mat.getMaterialData(LABEL_C4_f_spectral, coeffs.f_spectral);
301 if (mat.doesMaterialDataExist(LABEL_C4_theta_etr)) mat.getMaterialData(LABEL_C4_theta_etr, coeffs.theta_etr);
302 if (mat.doesMaterialDataExist(LABEL_C4_h_protons)) mat.getMaterialData(LABEL_C4_h_protons, coeffs.h_protons);
303 if (mat.doesMaterialDataExist(LABEL_C4_H_J)) mat.getMaterialData(LABEL_C4_H_J, coeffs.H_J);
304 if (mat.doesMaterialDataExist(LABEL_C4_H_Jcyc)) mat.getMaterialData(LABEL_C4_H_Jcyc, coeffs.H_Jcyc);
305
306 material_coefficient_cache_c4[materialID] = coeffs;
307 found_in_material = true;
308 }
309 } catch (const std::exception &) {
310 // No assigned material — fall through to UUID map / default.
311 }
312
313 if (found_in_material) {
314 return coeffs;
315 }
316 if (c4model_coefficients.find(UUID) != c4model_coefficients.end()) {
317 return c4model_coefficients.at(UUID);
318 }
319 return c4modelcoeffs;
320}
321
325 farquharmodelcoeffs = fmc;
326 farquharmodel_coefficients.clear();
327 model = "farquhar";
328}
329
330void PhotosynthesisModel::setFarquharCoefficientsFromLibrary(const std::string &species, const std::vector<uint> &UUIDs) {
333 for (uint UUID: UUIDs) {
334 farquharmodel_coefficients[UUID] = fmc;
335 }
336 model = "farquhar";
337}
338
339// Material-based coefficient setters
340
341void PhotosynthesisModel::setModelCoefficients(const std::string &material_label, const EmpiricalModelCoefficients &coeffs) {
342 context->setMaterialData(material_label, LABEL_EMP_Tref, coeffs.Tref);
343 context->setMaterialData(material_label, LABEL_EMP_Ci_ref, coeffs.Ci_ref);
344 context->setMaterialData(material_label, LABEL_EMP_Asat, coeffs.Asat);
345 context->setMaterialData(material_label, LABEL_EMP_theta, coeffs.theta);
346 context->setMaterialData(material_label, LABEL_EMP_Tmin, coeffs.Tmin);
347 context->setMaterialData(material_label, LABEL_EMP_Topt, coeffs.Topt);
348 context->setMaterialData(material_label, LABEL_EMP_q, coeffs.q);
349 context->setMaterialData(material_label, LABEL_EMP_R, coeffs.R);
350 context->setMaterialData(material_label, LABEL_EMP_ER, coeffs.ER);
351 context->setMaterialData(material_label, LABEL_EMP_kC, coeffs.kC);
352
353 // Clear cache for this material
354 uint matID = context->getMaterialIDFromLabel(material_label);
355 material_coefficient_cache_empirical.erase(matID);
356
357 model = "empirical";
358}
359
360void PhotosynthesisModel::setModelCoefficients(const std::string &material_label, const FarquharModelCoefficients &coeffs) {
361 // Get temperature response parameters
362 PhotosyntheticTemperatureResponseParameters vcmax_resp = const_cast<FarquharModelCoefficients &>(coeffs).getVcmaxTempResponse();
363 PhotosyntheticTemperatureResponseParameters jmax_resp = const_cast<FarquharModelCoefficients &>(coeffs).getJmaxTempResponse();
364 PhotosyntheticTemperatureResponseParameters rd_resp = const_cast<FarquharModelCoefficients &>(coeffs).getRdTempResponse();
365 PhotosyntheticTemperatureResponseParameters alpha_resp = const_cast<FarquharModelCoefficients &>(coeffs).getQuantumEfficiencyTempResponse();
366
367 // Serialize temperature response parameters for Vcmax
368 context->setMaterialData(material_label, LABEL_FQ_Vcmax, vcmax_resp.value_at_25C);
369 context->setMaterialData(material_label, "photo_fq_Vcmax_dHa", vcmax_resp.dHa);
370 context->setMaterialData(material_label, "photo_fq_Vcmax_dHd", vcmax_resp.dHd);
371 context->setMaterialData(material_label, "photo_fq_Vcmax_Topt", vcmax_resp.Topt);
372
373 // Serialize temperature response parameters for Jmax
374 context->setMaterialData(material_label, LABEL_FQ_Jmax, jmax_resp.value_at_25C);
375 context->setMaterialData(material_label, "photo_fq_Jmax_dHa", jmax_resp.dHa);
376 context->setMaterialData(material_label, "photo_fq_Jmax_dHd", jmax_resp.dHd);
377 context->setMaterialData(material_label, "photo_fq_Jmax_Topt", jmax_resp.Topt);
378
379 // Serialize temperature response parameters for Rd
380 context->setMaterialData(material_label, LABEL_FQ_Rd, rd_resp.value_at_25C);
381 context->setMaterialData(material_label, "photo_fq_Rd_dHa", rd_resp.dHa);
382 context->setMaterialData(material_label, "photo_fq_Rd_dHd", rd_resp.dHd);
383 context->setMaterialData(material_label, "photo_fq_Rd_Topt", rd_resp.Topt);
384
385 // Serialize temperature response parameters for alpha
386 context->setMaterialData(material_label, LABEL_FQ_alpha, alpha_resp.value_at_25C);
387 context->setMaterialData(material_label, "photo_fq_alpha_dHa", alpha_resp.dHa);
388
389 // Serialize temperature response parameters for TPU. Without these, a material with
390 // TPU_flag = 1 would deserialize with TPU silently reverted to the struct default.
391 PhotosyntheticTemperatureResponseParameters tpu_resp = const_cast<FarquharModelCoefficients &>(coeffs).getTPUTempResponse();
392 context->setMaterialData(material_label, LABEL_FQ_TPU, tpu_resp.value_at_25C);
393 context->setMaterialData(material_label, LABEL_FQ_TPU_dHa, tpu_resp.dHa);
394 context->setMaterialData(material_label, LABEL_FQ_TPU_dHd, tpu_resp.dHd);
395 context->setMaterialData(material_label, LABEL_FQ_TPU_Topt, tpu_resp.Topt);
396
397 // Serialize the light-response curvature theta. Without this, a non-rectangular
398 // hyperbola light response would silently deserialize as a rectangular one.
399 PhotosyntheticTemperatureResponseParameters theta_resp = const_cast<FarquharModelCoefficients &>(coeffs).getLightResponseCurvatureTempResponse();
400 context->setMaterialData(material_label, LABEL_FQ_theta, theta_resp.value_at_25C);
401 context->setMaterialData(material_label, LABEL_FQ_theta_dHa, theta_resp.dHa);
402 context->setMaterialData(material_label, LABEL_FQ_theta_dHd, theta_resp.dHd);
403 context->setMaterialData(material_label, LABEL_FQ_theta_Topt, theta_resp.Topt);
404
405 // Serialize mesophyll conductance gm (default value_at_25C is +infinity, meaning Cc ≡ Ci).
406 PhotosyntheticTemperatureResponseParameters gm_resp = coeffs.getMesophyllConductance_gmTempResponse();
407 context->setMaterialData(material_label, LABEL_FQ_gm, gm_resp.value_at_25C);
408 context->setMaterialData(material_label, LABEL_FQ_gm_dHa, gm_resp.dHa);
409 context->setMaterialData(material_label, LABEL_FQ_gm_dHd, gm_resp.dHd);
410 context->setMaterialData(material_label, LABEL_FQ_gm_Topt, gm_resp.Topt);
411
412 // Serialize other parameters
413 context->setMaterialData(material_label, LABEL_FQ_O, coeffs.O);
414 context->setMaterialData(material_label, LABEL_FQ_TPU_flag, coeffs.TPU_flag);
415
416 // Clear cache for this material
417 uint matID = context->getMaterialIDFromLabel(material_label);
418 material_coefficient_cache_farquhar.erase(matID);
419
420 model = "farquhar";
421}
422
423void PhotosynthesisModel::setFarquharCoefficientsFromLibrary(const std::string &species, const std::string &material_label) {
425 setModelCoefficients(material_label, coeffs);
426}
427
429 std::string s = std::move(species);
431 bool defaultSpecies = false;
432 if (s == "Almond" || s == "almond") {
433 fmc.setVcmax(72.6, 27.3, 315.3 - 273.15, 478.4);
434 fmc.setJmax(144.2, 64.1, 314.9 - 273.15, 508.4);
435 fmc.setTPU(6.4, 37.1, 311.3 - 273.15, 477.9);
436 fmc.setRd(0.2, 46.39f);
438 } else if (s == "Apple" || s == "apple") {
439 fmc.setVcmax(101.08, 65.33f);
440 fmc.setJmax(167.03, 47.62);
441 fmc.setRd(3.00, 46.39f);
443 } else if (s == "Cherry" || s == "cherry") {
444 fmc.setVcmax(75.65, 65.33f);
445 fmc.setJmax(129.06, 48.49);
446 fmc.setRd(2.12, 46.39f);
448 } else if (s == "Prune" || s == "prune") {
449 fmc.setVcmax(75.88, 65.33f);
450 fmc.setJmax(129.41, 48.58);
451 fmc.setRd(1.65, 46.39f);
453 } else if (s == "Pear" || s == "pear") {
454 fmc.setVcmax(107.69, 65.33f);
455 fmc.setJmax(176.71, 46.04);
456 fmc.setRd(1.510, 46.39f);
458 } else if (s == "PistachioFemale" || s == "pistachiofemale" || s == "pistachio_female" || s == "Pistachio_Female" || s == "Pistachio_female" || s == "pistachio" || s == "Pistachio") {
459 // Generic "pistachio" resolves to the female cultivar.
460 fmc.setVcmax(101.8, 56.5, 316.6 - 273.15, 483.1);
461 fmc.setJmax(223.0, 27.7, 314.6 - 273.15, 458.5);
462 fmc.setTPU(9.8, 39.9, 315.4 - 273.15, 494.3);
463 fmc.setRd(1.5, 46.39f);
466 } else if (s == "PistachioMale" || s == "pistachiomale" || s == "pistachio_male" || s == "Pistachio_Male" || s == "Pistachio_male") {
467 fmc.setVcmax(154.17, 65.33f);
468 fmc.setJmax(243.20, 50.89);
469 fmc.setRd(2.050, 46.39f);
471 } else if (s == "Walnut" || s == "walnut") {
472 fmc.setVcmax(81.6, 85.3, 316.5 - 273.15, 500.6);
473 fmc.setJmax(201.9, 41.4, 308.6 - 273.15, 308.2);
474 fmc.setTPU(10.2, 21.9, 310.4 - 273.15, 434.9);
475 fmc.setRd(0.9, 46.39f);
477 } else if (s == "Grape" || s == "grape") {
478 // cv. Cabernet Sauvignon
479 fmc.setVcmax(74.5, 76.1, 318.8 - 273.15, 499.8);
480 fmc.setJmax(180.2, 23.0, 313.8 - 273.15, 502.3);
481 fmc.setTPU(7.7, 24.0, 314.6 - 273.15, 496.4);
482 fmc.setRd(1.3, 46.39f);
485 } else if (s == "Elderberry" || s == "elderberry" || s == "blue_elderberry") {
486 fmc.setVcmax(37.7, 66.0, 319.4 - 273.15, 496.0);
487 fmc.setJmax(149.7, 24.5, 314.8 - 273.15, 492.9);
488 fmc.setTPU(7.3, 33.6, 314.5 - 273.15, 497.5);
489 fmc.setRd(1.3, 46.39f);
492 } else if (s == "Toyon" || s == "toyon") {
493 fmc.setVcmax(52.8, 42.1, 315.1 - 273.15, 483.0);
494 fmc.setJmax(142.4, 9.0, 313.0 - 273.15, 486.2);
495 fmc.setTPU(6.6, 14.0, 314.8 - 273.15, 493.8);
496 fmc.setRd(0.8, 46.39f);
499 } else if (s == "Big_Leaf_Maple" || s == "big_leaf_maple" || s == "Maple" || s == "maple") {
500 fmc.setVcmax(96.4, 48.9, 307.1 - 273.15, 505.0);
501 fmc.setJmax(168.0, 8.5, 304.7 - 273.15, 476.7);
502 fmc.setTPU(2.7, 32.1, 308.3 - 273.15, 471.6);
503 fmc.setRd(0.1, 46.39f);
505 } else if (s == "Western_Redbud" || s == "western_redbud" || s == "Redbud" || s == "redbud") {
506 fmc.setVcmax(68.5, 66.6, 315.1 - 273.15, 496.0);
507 fmc.setJmax(132.4, 41.2, 313.1 - 273.15, 474.0);
508 fmc.setTPU(6.6, 34.3, 312.8 - 273.15, 463.2);
509 fmc.setRd(0.8, 46.39f);
512 } else if (s == "Baylaurel" || s == "baylaurel" || s == "Bay_Laurel" || s == "bay_laurel" || s == "bay" || s == "Bay") {
513 fmc.setVcmax(97.5, 49.1, 308.6 - 273.15, 505.8);
514 fmc.setJmax(193.0, 34.0, 308.5 - 273.15, 456.7);
515 fmc.setTPU(3.3, 0.1, 309.4 - 273.15, 477.5);
516 fmc.setRd(0.1, 46.39f);
518 } else if (s == "Olive" || s == "olive") {
519 fmc.setVcmax(75.9, 55.4, 315.2 - 273.15, 497.0);
520 fmc.setJmax(170.4, 32.2, 312.5 - 273.15, 493.4);
521 fmc.setTPU(8.3, 37.2, 311.7 - 273.15, 498.9);
522 fmc.setRd(1.9, 46.39f);
524 } else if (s == "EasternRedbudSunlit" || s == "easternredbudsunlit" || s == "easternredbud_sunlit" || s == "EasternRedbud_Sunlit" || s == "EasternRedbud_sunlit" || s == "sunlitEasternRedbud" || s == "SunlitEasternRedbud" ||
525 s == "eastern_redbud_sunlit" || s == "Eastern_Redbud_Sunlit") {
526 fmc.setVcmax(104.35, 54.9927, 313.8828 - 273.15, 365.1581);
527 fmc.setJmax(211.3090, 46.5415, 310.9710 - 273.15, 200.3197);
528 fmc.setTPU(9.9965, 48.4469, 310.3164 - 273.15, 167.9181);
529 fmc.setRd(1.4136, 46.39f);
530 fmc.setQuantumEfficiency_alpha(0.4151);
531 } else if (s == "EasternRedbudShaded" || s == "easternredbudshaded" || s == "easternredbud_shaded" || s == "EasternRedbud_Shaded" || s == "EasternRedbud_shaded" || s == "shadedEasternRedbud" || s == "ShadedEasternRedbud" ||
532 s == "eastern_redbud_shaded" || s == "Eastern_Redbud_Shaded") {
533 fmc.setVcmax(84.7, 65.33f);
534 fmc.setJmax(190.53, 0.249);
535 fmc.setTPU(9.53, 0.0747);
536 fmc.setRd(1.13, 46.39f);
538 } else {
539 defaultSpecies = true;
540 if (message_flag) {
541 std::cerr << "WARNING (PhotosynthesisModel::getModelCoefficients): unknown species " << s << ". Setting default (Almond)." << std::endl;
542 }
543 }
544 if (!defaultSpecies) {
545 if (message_flag) {
546 std::cerr << "Setting Photosynthesis Model Coefficients to " << s << std::endl;
547 }
548 }
549 return fmc;
550}
551
552// ---------------------------------------------------------------------------
553// von Caemmerer (2021) C4 model — species parameter library.
554//
555// Each entry is internally complete (temperature-responsive rates, kinetic
556// constants, and scalar structural parameters) so that results are not
557// distorted by silently mixing an entry's headline rate constants with a
558// different entry's fixed assumptions. The C3 library above falls back to the
559// default species with a warning when the key is unknown; the C4 library is
560// new and follows the fail-fast philosophy instead — unknown keys raise
561// helios_runtime_error with the list of supported entries.
562// ---------------------------------------------------------------------------
563
566 setModelCoefficients(coeffs);
567}
568
569void PhotosynthesisModel::setC4CoefficientsFromLibrary(const std::string &species, const std::vector<uint> &UUIDs) {
571 setModelCoefficients(coeffs, UUIDs);
572}
573
574void PhotosynthesisModel::setC4CoefficientsFromLibrary(const std::string &species, const std::string &material_label) {
576 setModelCoefficients(material_label, coeffs);
577}
578
580 // Case-insensitive exact-key match.
581 std::string s = species;
582 std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return std::tolower(c); });
583
585
586 if (s == "setariaviridis_vc2021" || s == "setariaviridis" || s == "setaria_viridis_vc2021") {
587 // Setaria viridis, NADP-ME. von Caemmerer (2021) JXB 72:6003 Table 1.
588 // Kinetics: Boyd et al. (2015) Plant Physiol 169:1850 (Vpmax, Vcmax, Rd, Kc, Ko, Kp, γ*).
589 // gm: Ubierna et al. (2017) New Phytol 214:66. Jmax T-response: peaked-Arrhenius refit of the
590 // paper's Gaussian (June et al. 2004) — value at 25 °C matches Gaussian exactly.
591 c4.setVpmax(200.f, 50.1f);
592 c4.setVcmax(40.f, 78.0f);
593 c4.setJmax(247.69f, 77.9f, 43.f, 260.f);
594 c4.setRd(0.4f, 66.4f); // spec: Rd = 0.01 · Vcmax
595 c4.setMesophyllConductance_gm(1.0f, 49.8f);
596 // Kinetic + scalar fields keep struct-constructor defaults (Kc=1210, Ko=292000, Kp=82,
597 // γ*=3.8168e-4 with dH=+31.1, Om=210000, dH_Kc=64.2, dH_Ko=10.5, dH_Kp=38.3, alpha_psII=0,
598 // x=0.4, Vpr=80, Rm_frac=0.5, fcyc=0.45, H_J=3, H_Jcyc=3.4, gbs=0.003, ao=0.047,
599 // absorptance=0.85, f_spectral=0.15).
600 // NOTE: dH_gamma_star=+31.1 deliberately differs from the vC2021 Setaria spreadsheet value
601 // of -31.1 — see block comment on dH_gamma_star in PhotosynthesisModel.h. The spreadsheet
602 // sign is an error (copies Boyd 2015 S_c/o activation energy without flipping for γ*=0.5/S_c/o).
603 // Do NOT "fix" this to match the spreadsheet.
604 } else if (s == "genericc4_vc2000" || s == "genericc4" || s == "generic_c4" || s == "generic_vc2000") {
605 // Generic NADP-ME fallback. von Caemmerer (2000) Biochemical Models of Leaf Photosynthesis
606 // (CSIRO) as encoded by the plantecophys R package (Duursma 2015) AciC4 defaults.
607 // Q10 → Arrhenius conversion: Ea ≈ R · T_ref² · ln(Q10) / 10 with T_ref = 298.15 K.
608 c4.setVcmax(60.f, 61.6f); // Q10 = 2.3 → Ea = 8.314 · 298.15² · ln(2.3) / 10 ≈ 61.6 kJ/mol
609 c4.setVpmax(120.f, 61.6f); // Q10 = 2.3 → Ea ≈ 61.6 kJ/mol
610 c4.setJmax(400.f, 61.6f); // Q10 = 2.3 → Ea ≈ 61.6 kJ/mol
611 c4.setRd(1.0f, 51.2f); // Q10 = 2.0 → Ea = 8.314 · 298.15² · ln(2.0) / 10 ≈ 51.2 kJ/mol
612 c4.setMesophyllConductance_gm(1.0e4f); // no T-response; vC2000 has no gm term (effectively infinite)
613 c4.Kp_25 = 80.f; // vC2000 value (vs. Setaria 82); Kc/Ko/γ* left at Setaria defaults
614 c4.fcyc = 0.f; // vC2000 has no cyclic term
615 } else if (s == "maize_massad2007" || s == "maize" || s == "zea_mays_massad2007") {
616 // Zea mays cv. Chambord. Temperature responses: Massad, Tuzet, Bethenod (2007) Plant Cell
617 // Environ 30:1191 Fig. 6 (peaked Arrhenius: Ea, Hd, ΔS). 25 °C values: plantecophys AciC4
618 // defaults (consistent with Massad's "conform to literature" statement since the paper
619 // reports no single tabular point estimate).
620 //
621 // Massad (2007) Fig. 6 coefficients (for independent verification):
622 // Vcmax: Ea = 67.294 kJ/mol, Hd = 144.568 kJ/mol, ΔS = 472 J/mol/K
623 // Vpmax: Ea = 70.373 kJ/mol, Hd = 117.910 kJ/mol, ΔS = 376 J/mol/K
624 // Jmax: Ea = 77.900 kJ/mol, Hd = 191.929 kJ/mol, ΔS = 627 J/mol/K
625 //
626 // Convert (Ea, Hd, ΔS) → peaked-Arrhenius Topt via
627 // Topt [K] = Hd / (ΔS − R · ln(Ea / (Hd − Ea))) with R = 8.314 J/mol/K
628 // yielding Topt(Vcmax) ≈ 305.5 K (32.3 °C), Topt(Vpmax) ≈ 316.3 K (43.1 °C),
629 // Topt(Jmax) ≈ 304.6 K (31.5 °C). The Helios peaked-Arrhenius setter takes
630 // (value_at_25, dHa, Topt_°C, dHd).
631 c4.setVcmax(60.f, 67.3f, 32.3f, 144.6f);
632 c4.setVpmax(120.f, 70.4f, 43.1f, 117.9f);
633 c4.setJmax(400.f, 77.9f, 31.5f, 191.9f);
634 c4.setRd(0.6f, 66.4f); // 0.01·Vcmax vC2000 convention; Massad's own fits used Rd=0 — 0.6 is the library's usable default, not a Massad measurement
635 c4.setMesophyllConductance_gm(1.0e4f); // Massad assumed infinite gm
636 // Massad fit against Bernacchi (2001) C3-derived Kc/Ko — these are ~2× smaller than the
637 // Boyd 2015 C4 values used for Setaria. Keeping Massad's fixed assumptions intact is
638 // required to avoid biasing the Vcmax / Vpmax estimates.
639 c4.Kc_25 = 650.f;
640 c4.Ko_25 = 450000.f;
641 c4.dH_Kc = 79.43f;
642 c4.dH_Ko = 36.38f;
643 // Kp: Massad assumed Q10=2.1. Per spec, replace with Boyd (2015) Arrhenius for internal
644 // consistency (25 °C value essentially unchanged at 80 μbar).
645 c4.Kp_25 = 80.f;
646 c4.dH_Kp = 38.3f;
647 c4.fcyc = 0.f; // implicit in Massad's vC&F (1999) parent framework
648 } else {
649 helios_runtime_error("ERROR (PhotosynthesisModel::getC4CoefficientsFromLibrary): unknown C4 species '" + species +
650 "'. Available keys (case-insensitive): SetariaViridis_vC2021, GenericC4_vC2000, Maize_Massad2007.");
651 }
652
653 if (message_flag) {
654 std::cerr << "Setting C4 Photosynthesis Model Coefficients to " << species << std::endl;
655 }
656 return c4;
657}
658
659// Cached coefficient retrieval helpers
660
661EmpiricalModelCoefficients PhotosynthesisModel::getCoefficientsForPrimitive_Empirical(uint UUID) const {
662 uint materialID = context->getPrimitiveMaterialID(UUID);
663
664 // Check cache first
665 if (material_coefficient_cache_empirical.find(materialID) != material_coefficient_cache_empirical.end()) {
666 return material_coefficient_cache_empirical.at(materialID);
667 }
668
669 // Try to load from material data
671 bool found = true;
672
673 try {
674 const Material &mat = context->getMaterial(materialID);
675 if (mat.doesMaterialDataExist(LABEL_EMP_Tref) && mat.doesMaterialDataExist(LABEL_EMP_Ci_ref) && mat.doesMaterialDataExist(LABEL_EMP_Asat) && mat.doesMaterialDataExist(LABEL_EMP_theta) && mat.doesMaterialDataExist(LABEL_EMP_Tmin) &&
676 mat.doesMaterialDataExist(LABEL_EMP_Topt) && mat.doesMaterialDataExist(LABEL_EMP_q) && mat.doesMaterialDataExist(LABEL_EMP_R) && mat.doesMaterialDataExist(LABEL_EMP_ER) && mat.doesMaterialDataExist(LABEL_EMP_kC)) {
677
678 mat.getMaterialData(LABEL_EMP_Tref, coeffs.Tref);
679 mat.getMaterialData(LABEL_EMP_Ci_ref, coeffs.Ci_ref);
680 mat.getMaterialData(LABEL_EMP_Asat, coeffs.Asat);
681 mat.getMaterialData(LABEL_EMP_theta, coeffs.theta);
682 mat.getMaterialData(LABEL_EMP_Tmin, coeffs.Tmin);
683 mat.getMaterialData(LABEL_EMP_Topt, coeffs.Topt);
684 mat.getMaterialData(LABEL_EMP_q, coeffs.q);
685 mat.getMaterialData(LABEL_EMP_R, coeffs.R);
686 mat.getMaterialData(LABEL_EMP_ER, coeffs.ER);
687 mat.getMaterialData(LABEL_EMP_kC, coeffs.kC);
688
689 material_coefficient_cache_empirical[materialID] = coeffs;
690 return coeffs;
691 } else {
692 found = false;
693 }
694 } catch (const std::exception &) {
695 found = false;
696 }
697
698 // Fallback to legacy UUID map
699 if (!found && empiricalmodel_coefficients.find(UUID) != empiricalmodel_coefficients.end()) {
700 return empiricalmodel_coefficients.at(UUID);
701 }
702
703 // Fallback to global default
704 return empiricalmodelcoeffs;
705}
706
707FarquharModelCoefficients PhotosynthesisModel::getCoefficientsForPrimitive_Farquhar(uint UUID) const {
708 uint materialID = context->getPrimitiveMaterialID(UUID);
709
710 // Check cache first
711 if (material_coefficient_cache_farquhar.find(materialID) != material_coefficient_cache_farquhar.end()) {
712 return material_coefficient_cache_farquhar.at(materialID);
713 }
714
715 // Try to load from material data
717 bool found = true;
718
719 try {
720 const Material &mat = context->getMaterial(materialID);
721 if (mat.doesMaterialDataExist(LABEL_FQ_Vcmax) && mat.doesMaterialDataExist(LABEL_FQ_Jmax) && mat.doesMaterialDataExist(LABEL_FQ_Rd) && mat.doesMaterialDataExist(LABEL_FQ_alpha)) {
722
723 // Reconstruct temperature response parameters for Vcmax
724 float vcmax_25C, vcmax_dHa, vcmax_dHd, vcmax_Topt;
725 mat.getMaterialData(LABEL_FQ_Vcmax, vcmax_25C);
726 mat.getMaterialData("photo_fq_Vcmax_dHa", vcmax_dHa);
727 mat.getMaterialData("photo_fq_Vcmax_dHd", vcmax_dHd);
728 mat.getMaterialData("photo_fq_Vcmax_Topt", vcmax_Topt);
729
730 // Reconstruct for Jmax
731 float jmax_25C, jmax_dHa, jmax_dHd, jmax_Topt;
732 mat.getMaterialData(LABEL_FQ_Jmax, jmax_25C);
733 mat.getMaterialData("photo_fq_Jmax_dHa", jmax_dHa);
734 mat.getMaterialData("photo_fq_Jmax_dHd", jmax_dHd);
735 mat.getMaterialData("photo_fq_Jmax_Topt", jmax_Topt);
736
737 // Reconstruct for Rd
738 float rd_25C, rd_dHa, rd_dHd, rd_Topt;
739 mat.getMaterialData(LABEL_FQ_Rd, rd_25C);
740 mat.getMaterialData("photo_fq_Rd_dHa", rd_dHa);
741 mat.getMaterialData("photo_fq_Rd_dHd", rd_dHd);
742 mat.getMaterialData("photo_fq_Rd_Topt", rd_Topt);
743
744 // Reconstruct for alpha
745 float alpha_25C, alpha_dHa;
746 mat.getMaterialData(LABEL_FQ_alpha, alpha_25C);
747 mat.getMaterialData("photo_fq_alpha_dHa", alpha_dHa);
748
749 // Use setter methods to properly populate the coefficient struct
750 if (vcmax_Topt < 1000.f) { // Has optimum
751 coeffs.setVcmax(vcmax_25C, vcmax_dHa, vcmax_Topt - 273.15f, vcmax_dHd);
752 } else if (vcmax_dHa > 0.f) { // Monotonic increase
753 coeffs.setVcmax(vcmax_25C, vcmax_dHa);
754 } else { // Constant
755 coeffs.setVcmax(vcmax_25C);
756 }
757
758 if (jmax_Topt < 1000.f) {
759 coeffs.setJmax(jmax_25C, jmax_dHa, jmax_Topt - 273.15f, jmax_dHd);
760 } else if (jmax_dHa > 0.f) {
761 coeffs.setJmax(jmax_25C, jmax_dHa);
762 } else {
763 coeffs.setJmax(jmax_25C);
764 }
765
766 if (rd_Topt < 1000.f) {
767 coeffs.setRd(rd_25C, rd_dHa, rd_Topt - 273.15f, rd_dHd);
768 } else if (rd_dHa > 0.f) {
769 coeffs.setRd(rd_25C, rd_dHa);
770 } else {
771 coeffs.setRd(rd_25C);
772 }
773
774 if (alpha_dHa > 0.f) {
775 coeffs.setQuantumEfficiency_alpha(alpha_25C, alpha_dHa);
776 } else {
777 coeffs.setQuantumEfficiency_alpha(alpha_25C);
778 }
779
780 // Reconstruct gm temperature response if present. Missing labels leave the
781 // default (value_at_25C = +infinity, i.e., Cc ≡ Ci — legacy behavior).
782 if (mat.doesMaterialDataExist(LABEL_FQ_gm)) {
783 float gm_25C, gm_dHa = 0.f, gm_dHd = 0.f, gm_Topt = 10000.f;
784 mat.getMaterialData(LABEL_FQ_gm, gm_25C);
785 if (mat.doesMaterialDataExist(LABEL_FQ_gm_dHa)) {
786 mat.getMaterialData(LABEL_FQ_gm_dHa, gm_dHa);
787 }
788 if (mat.doesMaterialDataExist(LABEL_FQ_gm_dHd)) {
789 mat.getMaterialData(LABEL_FQ_gm_dHd, gm_dHd);
790 }
791 if (mat.doesMaterialDataExist(LABEL_FQ_gm_Topt)) {
792 mat.getMaterialData(LABEL_FQ_gm_Topt, gm_Topt);
793 }
794 if (gm_Topt < 1000.f) {
795 coeffs.setMesophyllConductance_gm(gm_25C, gm_dHa, gm_Topt - 273.15f, gm_dHd);
796 } else if (gm_dHa > 0.f) {
797 coeffs.setMesophyllConductance_gm(gm_25C, gm_dHa);
798 } else {
799 coeffs.setMesophyllConductance_gm(gm_25C);
800 }
801 }
802
803 // Reconstruct the TPU temperature response if present. Missing labels leave the
804 // struct default (older serializations pre-date these fields).
805 if (mat.doesMaterialDataExist(LABEL_FQ_TPU)) {
806 float tpu_25C, tpu_dHa = 0.f, tpu_dHd = 0.f, tpu_Topt = 10000.f;
807 mat.getMaterialData(LABEL_FQ_TPU, tpu_25C);
808 if (mat.doesMaterialDataExist(LABEL_FQ_TPU_dHa)) {
809 mat.getMaterialData(LABEL_FQ_TPU_dHa, tpu_dHa);
810 }
811 if (mat.doesMaterialDataExist(LABEL_FQ_TPU_dHd)) {
812 mat.getMaterialData(LABEL_FQ_TPU_dHd, tpu_dHd);
813 }
814 if (mat.doesMaterialDataExist(LABEL_FQ_TPU_Topt)) {
815 mat.getMaterialData(LABEL_FQ_TPU_Topt, tpu_Topt);
816 }
817 if (tpu_Topt < 1000.f) {
818 coeffs.setTPU(tpu_25C, tpu_dHa, tpu_Topt - 273.15f, tpu_dHd);
819 } else if (tpu_dHa > 0.f) {
820 coeffs.setTPU(tpu_25C, tpu_dHa);
821 } else {
822 coeffs.setTPU(tpu_25C);
823 }
824 }
825
826 // Reconstruct the light-response curvature theta if present.
827 if (mat.doesMaterialDataExist(LABEL_FQ_theta)) {
828 float theta_25C, theta_dHa = 0.f, theta_dHd = 0.f, theta_Topt = 10000.f;
829 mat.getMaterialData(LABEL_FQ_theta, theta_25C);
830 if (mat.doesMaterialDataExist(LABEL_FQ_theta_dHa)) {
831 mat.getMaterialData(LABEL_FQ_theta_dHa, theta_dHa);
832 }
833 if (mat.doesMaterialDataExist(LABEL_FQ_theta_dHd)) {
834 mat.getMaterialData(LABEL_FQ_theta_dHd, theta_dHd);
835 }
836 if (mat.doesMaterialDataExist(LABEL_FQ_theta_Topt)) {
837 mat.getMaterialData(LABEL_FQ_theta_Topt, theta_Topt);
838 }
839 if (theta_Topt < 1000.f) {
840 coeffs.setLightResponseCurvature_theta(theta_25C, theta_dHa, theta_Topt - 273.15f, theta_dHd);
841 } else if (theta_dHa > 0.f) {
842 coeffs.setLightResponseCurvature_theta(theta_25C, theta_dHa);
843 } else {
844 coeffs.setLightResponseCurvature_theta(theta_25C);
845 }
846 }
847
848 // Set O and TPU_flag. TPU_flag must be restored AFTER the setTPU() calls above,
849 // since setTPU() unconditionally enables the flag as a side effect.
850 mat.getMaterialData(LABEL_FQ_O, coeffs.O);
851 mat.getMaterialData(LABEL_FQ_TPU_flag, coeffs.TPU_flag);
852
853 material_coefficient_cache_farquhar[materialID] = coeffs;
854 return coeffs;
855 } else {
856 found = false;
857 }
858 } catch (const std::exception &) {
859 found = false;
860 }
861
862 // Fallback to legacy UUID map
863 if (!found && farquharmodel_coefficients.find(UUID) != farquharmodel_coefficients.end()) {
864 return farquharmodel_coefficients.at(UUID);
865 }
866
867 // Fallback to global default
868 return farquharmodelcoeffs;
869}
870
871void PhotosynthesisModel::setCm(float Cm, const std::vector<uint> &UUIDs) {
872 if (Cm < 0.f) {
873 helios_runtime_error("ERROR (PhotosynthesisModel::setCm): Cm must be non-negative. Received Cm = " + std::to_string(Cm) + " ubar.");
874 }
875 if (!std::isfinite(Cm)) {
876 helios_runtime_error("ERROR (PhotosynthesisModel::setCm): Cm must be a finite value.");
877 }
878 for (uint UUID: UUIDs) {
879 if (!context->doesPrimitiveExist(UUID)) {
880 helios_runtime_error("ERROR (PhotosynthesisModel::setCm): Primitive with UUID " + std::to_string(UUID) + " does not exist.");
881 }
882 manual_Cm[UUID] = Cm;
883 }
884}
885
886void PhotosynthesisModel::setCi(float Ci, const std::vector<uint> &UUIDs) {
887 // Fail-fast validation
888 if (Ci < 0.f) {
889 helios_runtime_error("ERROR (PhotosynthesisModel::setCi): Ci must be non-negative. Received Ci = " + std::to_string(Ci) + " umol/mol.");
890 }
891
892 if (!std::isfinite(Ci)) {
893 helios_runtime_error("ERROR (PhotosynthesisModel::setCi): Ci must be a finite value.");
894 }
895
896 // Reasonable range warning (typical C3 range: 50-800 umol/mol)
897 if (Ci > 2000.f && message_flag) {
898 std::cout << "WARNING (PhotosynthesisModel::setCi): Ci = " << Ci << " umol/mol is unusually high. Verify this is intentional." << std::endl;
899 }
900
901 // Set manual Ci for all specified UUIDs
902 for (uint UUID: UUIDs) {
903 if (!context->doesPrimitiveExist(UUID)) {
904 helios_runtime_error("ERROR (PhotosynthesisModel::setCi): Primitive with UUID " + std::to_string(UUID) + " does not exist.");
905 }
906 manual_Ci[UUID] = Ci;
907 }
908}
909
911 run(context->getAllUUIDs());
912}
913
914void PhotosynthesisModel::run(const std::vector<uint> &lUUIDs) {
915
916 WarningAggregator warnings;
917 warnings.setEnabled(message_flag); // Respect existing message flag
918
919 for (uint UUID: lUUIDs) {
920
921 float i_PAR;
922 if (context->doesPrimitiveDataExist(UUID, "radiation_flux_PAR") && context->getPrimitiveDataType("radiation_flux_PAR") == HELIOS_TYPE_FLOAT) {
923 context->getPrimitiveData(UUID, "radiation_flux_PAR", i_PAR);
924 i_PAR = i_PAR * 4.57f; // umol/m^2-s (ref https://www.controlledenvironments.org/wp-content/uploads/sites/6/2017/06/Ch01.pdf)
925 if (i_PAR < 0) {
926 i_PAR = 0;
927 warnings.addWarning("negative_par_clipped", "PAR flux value provided was negative. Clipping to zero.");
928 }
929 } else {
930 i_PAR = i_PAR_default;
931 }
932
933 float TL;
934 if (context->doesPrimitiveDataExist(UUID, "temperature") && context->getPrimitiveDataType("temperature") == HELIOS_TYPE_FLOAT) {
935 context->getPrimitiveData(UUID, "temperature", TL);
936 if (TL < 200) {
937 warnings.addWarning("low_temperature", "Primitive temperature value was very low (" + std::to_string(TL) + "K). Using default. Are you using absolute temperature units?");
938 TL = TL_default;
939 }
940 } else {
941 TL = TL_default;
942 }
943
944 float CO2;
945 if (context->doesPrimitiveDataExist(UUID, "air_CO2") && context->getPrimitiveDataType("air_CO2") == HELIOS_TYPE_FLOAT) {
946 context->getPrimitiveData(UUID, "air_CO2", CO2);
947 if (CO2 < 0) {
948 CO2 = 0;
949 warnings.addWarning("negative_co2_clipped", "CO2 concentration value provided was negative. Clipping to zero.");
950 }
951 } else {
952 CO2 = CO2_default;
953 }
954
955 float gM;
956 if (context->doesPrimitiveDataExist(UUID, "moisture_conductance") && context->getPrimitiveDataType("moisture_conductance") == HELIOS_TYPE_FLOAT) {
957 context->getPrimitiveData(UUID, "moisture_conductance", gM);
958 if (gM < 0) {
959 gM = 0;
960 warnings.addWarning("negative_moisture_conductance_clipped", "Moisture conductance value provided was negative. Clipping to zero.");
961 }
962 } else {
963 gM = gM_default;
964 }
965
966 // Number of sides - check material first, then primitive data
967 uint twosided_flag = context->getPrimitiveTwosidedFlag(UUID, 1);
968 uint Nsides = (twosided_flag == 0) ? 1 : 2;
969
970 float stomatal_sidedness = 0.f; // default all stomata on one side (hypostomatous)
971 if (Nsides == 2 && context->doesPrimitiveDataExist(UUID, "stomatal_sidedness") && context->getPrimitiveDataType("stomatal_sidedness") == HELIOS_TYPE_FLOAT) {
972 context->getPrimitiveData(UUID, "stomatal_sidedness", stomatal_sidedness);
973 }
974
975 float gH;
976 if (context->doesPrimitiveDataExist(UUID, "boundarylayer_conductance") && context->getPrimitiveDataType("boundarylayer_conductance") == HELIOS_TYPE_FLOAT) {
977 context->getPrimitiveData(UUID, "boundarylayer_conductance", gH);
978 } else if (context->doesPrimitiveDataExist(UUID, "boundarylayer_conductance_out") && context->getPrimitiveDataType("boundarylayer_conductance_out") == HELIOS_TYPE_FLOAT) {
979 context->getPrimitiveData(UUID, "boundarylayer_conductance_out", gH);
980 } else {
981 gH = gH_default;
982 }
983 if (gH < 0) {
984 gH = 0;
985 warnings.addWarning("negative_boundarylayer_conductance_clipped", "Boundary-layer conductance value provided was negative. Clipping to zero.");
986 }
987
988 // combine stomatal (gM) and boundary-layer (gH) conductances
989 if (gH == 0 && gM == 0) { // if somehow both go to zero, can get NaN
990 gM = 0;
991 } else {
992 gM = 1.08f * gH * gM * (stomatal_sidedness / (1.08f * gH + gM * stomatal_sidedness) + (1.f - stomatal_sidedness) / (1.08f * gH + gM * (1.f - stomatal_sidedness)));
993 }
994
995 float A, Ci, Gamma, J_over_Jmax = 0.f;
996 int limitation_state, TPU_flag = 0;
997 float Cm_c4 = 0.f, Vp_c4 = 0.f; // C4-specific outputs (unused for other models)
998
999 if (model == "farquhar") { // Farquhar-von Caemmerer-Berry Model
1000
1001 // Check for manual Ci override first
1002 if (manual_Ci.find(UUID) != manual_Ci.end()) {
1003 // Use manual Ci, bypassing iterative calculation
1004 Ci = manual_Ci.at(UUID);
1005
1006 FarquharModelCoefficients coeffs = getCoefficientsForPrimitive_Farquhar(UUID);
1007
1008 // Set up variables vector for evaluateCi_Farquhar
1009 std::vector<float> variables{CO2, i_PAR, TL, gM, 0.f, 0.f, 0.f, float(TPU_flag), 0.f};
1010
1011 // Call evaluateCi_Farquhar directly to compute A for the manual Ci
1012 // This bypasses the fzero iteration in evaluateFarquharModel
1013 evaluateCi_Farquhar(Ci, variables, &coeffs); // Return value (residual) is unused
1014
1015 // Extract results from variables vector
1016 A = variables[4];
1017 limitation_state = (int) variables[5];
1018 Gamma = variables[6];
1019 J_over_Jmax = variables[8];
1020
1021 // Store in previous_Ci for consistency
1022 previous_Ci[UUID] = Ci;
1023
1024 } else {
1025 // Normal iterative calculation
1026
1027 // Initialize Ci with previous timestep value for temporal continuity, or reasonable default
1028 if (previous_Ci.find(UUID) != previous_Ci.end()) {
1029 Ci = previous_Ci.at(UUID); // Use previous timestep's Ci
1030 } else {
1031 Ci = CO2 * 0.7f; // Default initial guess (typical Ci/Ca ratio)
1032 }
1033
1034 FarquharModelCoefficients coeffs = getCoefficientsForPrimitive_Farquhar(UUID);
1035 A = evaluateFarquharModel(coeffs, i_PAR, TL, CO2, gM, Ci, Gamma, limitation_state, TPU_flag, J_over_Jmax, warnings);
1036
1037 // Store computed Ci for next timestep (temporal continuity)
1038 previous_Ci[UUID] = Ci;
1039 }
1040
1041 } else if (model == "c4") { // von Caemmerer (2021) C4 Model
1042
1043 C4ModelCoefficients c4coeffs = getCoefficientsForPrimitive_C4(UUID);
1044
1045 if (manual_Cm.find(UUID) != manual_Cm.end()) {
1046 // Manual Cm path — skip both stomatal iteration and the Cm = Ci - A/gm fixed point.
1047 // A is computed directly at the supplied Cm; Ci is back-computed from Cm + A/gm.
1048 const float Cm_in = manual_Cm.at(UUID);
1049 std::vector<float> variables{CO2, i_PAR, TL, gM, 0.f, 0.f, 0.f, 0.f};
1050 evaluateCm_C4(Cm_in, variables, c4coeffs);
1051 A = variables[4];
1052 limitation_state = static_cast<int>(variables[5]);
1053 Cm_c4 = variables[6];
1054 Vp_c4 = variables[7];
1055 const float gm_eval = respondToTemperature(&c4coeffs.gmTempResponse, TL);
1056 Ci = (std::isfinite(gm_eval) && gm_eval > 0.f) ? (Cm_in + A / gm_eval) : Cm_in;
1057 previous_Ci[UUID] = Ci;
1058 } else if (manual_Ci.find(UUID) != manual_Ci.end()) {
1059 // Manual Ci path — bypass stomatal iteration, evaluate A via Cm = Ci - A/gm fixed point
1060 Ci = manual_Ci.at(UUID);
1061 std::vector<float> variables{CO2, i_PAR, TL, gM, 0.f, 0.f, 0.f, 0.f};
1062 evaluateCi_C4(Ci, variables, &c4coeffs);
1063 A = variables[4];
1064 limitation_state = static_cast<int>(variables[5]);
1065 Cm_c4 = variables[6];
1066 Vp_c4 = variables[7];
1067 previous_Ci[UUID] = Ci;
1068 } else {
1069 if (previous_Ci.find(UUID) != previous_Ci.end()) {
1070 Ci = previous_Ci.at(UUID);
1071 } else {
1072 Ci = CO2 * 0.4f; // C4 typically has lower Ci/Ca (~0.3-0.4) than C3
1073 }
1074 Cm_c4 = Ci;
1075 A = evaluateC4Model(c4coeffs, i_PAR, TL, CO2, gM, Ci, Cm_c4, Vp_c4, limitation_state, warnings);
1076 previous_Ci[UUID] = Ci;
1077 }
1078 Gamma = 0.f; // not defined the same way for C4 — leave zero
1079
1080 } else { // Empirical Model
1081
1082 EmpiricalModelCoefficients coeffs = getCoefficientsForPrimitive_Empirical(UUID);
1083 A = evaluateEmpiricalModel(coeffs, i_PAR, TL, CO2, gM);
1084 }
1085
1086 if (A == 0) {
1087 warnings.addWarning("convergence_failure", "Solution did not converge for primitive " + std::to_string(UUID) + ".");
1088 }
1089
1090 context->setPrimitiveData(UUID, "net_photosynthesis", A);
1091
1092 for (const auto &data: output_prim_data) {
1093 if (data == "Ci" && (model == "farquhar" || model == "c4")) {
1094 context->setPrimitiveData(UUID, "Ci", Ci);
1095 } else if (data == "limitation_state" && (model == "farquhar" || model == "c4")) {
1096 context->setPrimitiveData(UUID, "limitation_state", limitation_state);
1097 } else if (data == "Gamma_CO2" && model == "farquhar") {
1098 context->setPrimitiveData(UUID, "Gamma_CO2", Gamma);
1099 } else if (data == "electron_transport_ratio" && model == "farquhar") {
1100 context->setPrimitiveData(UUID, "electron_transport_ratio", J_over_Jmax);
1101 } else if (data == "Cm" && model == "c4") {
1102 context->setPrimitiveData(UUID, "Cm", Cm_c4);
1103 } else if (data == "Vp" && model == "c4") {
1104 context->setPrimitiveData(UUID, "Vp", Vp_c4);
1105 }
1106 }
1107 }
1108
1109 warnings.report(std::cerr);
1110}
1111
1112namespace {
1114
1119 float evaluateEmpiricalTemperatureResponse(const EmpiricalModelCoefficients &params, float TL) {
1120
1121 const float Tmin = params.Tmin;
1122 const float Topt = params.Topt;
1123 const float Tref = params.Tref;
1124 const float q = params.q;
1125
1126 // The coefficients are validated before the temperature-dependent early return below. These are
1127 // properties of the coefficient set rather than of the current leaf temperature, so checking them
1128 // afterwards would silently skip the check on every timestep where the leaf sits at or below Tmin
1129 // and report a well-formed A = -Rd instead.
1130 const float denom_ref = (1.f + q) * Topt - Tmin - q * Tref;
1131 if (Tref <= Tmin || std::fabs(denom_ref) < 1.0e-6f) {
1132 helios_runtime_error("ERROR (PhotosynthesisModel): Invalid empirical model temperature response coefficients. Requires Tref > Tmin and (1+q)*Topt - Tmin - q*Tref != 0. Received Tmin = " + std::to_string(Tmin) +
1133 " K, Topt = " + std::to_string(Topt) + " K, Tref = " + std::to_string(Tref) + " K, q = " + std::to_string(q) + ".");
1134 }
1135
1136 // Below the minimum temperature no assimilation occurs.
1137 if (TL <= Tmin) {
1138 return 0.f;
1139 }
1140
1141 const float numer = (1.f + q) * Topt - Tmin - q * TL;
1142 if (numer <= 0.f) {
1143 return 0.f; // beyond the upper temperature limit
1144 }
1145
1146 const float f_T = std::pow((TL - Tmin) / (Tref - Tmin), q) * numer / denom_ref;
1147
1148 return std::max(f_T, 0.f);
1149 }
1150} // namespace
1151
1152float PhotosynthesisModel::evaluateCi_Empirical(const EmpiricalModelCoefficients &params, float Ci, float CO2, float fL, float fT, float Rd, float gM) const {
1153
1154
1155 //--- CO2 Response Function --- //
1156
1157 float fC = params.kC * Ci / params.Ci_ref;
1158
1159
1160 //--- Assimilation Rate --- //
1161
1162 float A = params.Asat * fL * fT * fC - Rd;
1163
1164 //--- Calculate error and update --- //
1165
1166 float resid = 0.75f * gM * (CO2 - Ci) - A;
1167
1168
1169 return resid;
1170}
1171
1172float PhotosynthesisModel::evaluateEmpiricalModel(const EmpiricalModelCoefficients &params, float i_PAR, float TL, float CO2, float gM) {
1173
1174 // initial guess for intercellular CO2
1175 float Ci = CO2;
1176
1177 //--- Light Response Function --- //
1178
1179 float fL = i_PAR / (params.theta + i_PAR);
1180
1181 assert(fL >= 0 && fL <= 1);
1182
1183 //--- Temperature Response Function --- //
1184
1185 float fT = evaluateEmpiricalTemperatureResponse(params, TL);
1186
1187 //--- Respiration Rate --- //
1188
1189 float Rd = params.R * sqrt(TL - 273.f) * exp(-params.ER / TL);
1190
1191 float Ci_old = Ci;
1192 float Ci_old_old = 0.95f * Ci;
1193
1194 float resid_old = evaluateCi_Empirical(params, Ci_old, CO2, fL, fT, Rd, gM);
1195 float resid_old_old = evaluateCi_Empirical(params, Ci_old_old, CO2, fL, fT, Rd, gM);
1196
1197 float err = 10000, err_max = 0.01;
1198 int iter = 0, max_iter = 100;
1199 float resid;
1200 while (err > err_max && iter < max_iter) {
1201
1202 if (resid_old == resid_old_old) { // this condition will cause NaN
1203 break;
1204 }
1205
1206 Ci = fabs((Ci_old_old * resid_old - Ci_old * resid_old_old) / (resid_old - resid_old_old));
1207
1208 resid = evaluateCi_Empirical(params, Ci, CO2, fL, fT, Rd, gM);
1209
1210 resid_old_old = resid_old;
1211 resid_old = resid;
1212
1213 err = fabs(resid);
1214
1215 Ci_old_old = Ci_old;
1216 Ci_old = Ci;
1217
1218 iter++;
1219 }
1220
1221 float A;
1222 if (err > err_max) {
1223 A = 0;
1224 } else {
1225 float fC = params.kC * Ci / params.Ci_ref;
1226 A = params.Asat * fL * fT * fC - Rd;
1227 }
1228
1229 return A;
1230}
1231
1232float PhotosynthesisModel::evaluateCi_Farquhar(float Ci, std::vector<float> &variables, const void *parameters) {
1233
1234 const FarquharModelCoefficients modelcoeffs = *reinterpret_cast<const FarquharModelCoefficients *>(parameters);
1235
1236 float Vcmax, Jmax, TPU, Rd, alpha, theta;
1237
1238 float CO2 = variables[0];
1239 float Q = variables[1];
1240 float TL = variables[2];
1241 float gM = variables[3];
1242 int TPUflag = modelcoeffs.TPU_flag;
1243
1244 float R = 0.0083144598; // molar gas constant (kJ/K/mol)
1245 float O = 213.5; // ambient oxygen concentration (mmol/mol)
1246 float c_Gamma = 19.02;
1247 float dH_Gamma = 37.83;
1248 float c_Kc = 38.05;
1249 float dH_Kc = 79.43;
1250 float c_Ko = 20.30;
1251 float dH_Ko = 36.38;
1252
1253 float invDiffRT = (1.f / 298.15f - 1.f / TL) / R;
1254
1255 if (modelcoeffs.Vcmax > 0) {
1256 Vcmax = modelcoeffs.Vcmax * expf(modelcoeffs.dH_Vcmax * (invDiffRT));
1257 } else {
1258 Vcmax = respondToTemperature(&modelcoeffs.VcmaxTempResponse, TL);
1259 }
1260
1261 if (modelcoeffs.Jmax > 0) {
1262 Jmax = modelcoeffs.Jmax * expf(modelcoeffs.dH_Jmax * (invDiffRT));
1263 } else {
1264 Jmax = respondToTemperature(&modelcoeffs.JmaxTempResponse, TL);
1265 }
1266
1267 if (modelcoeffs.Rd > 0) {
1268 Rd = modelcoeffs.Rd * expf(modelcoeffs.dH_Rd * (invDiffRT));
1269 } else {
1270 Rd = respondToTemperature(&modelcoeffs.RdTempResponse, TL);
1271 }
1272
1273 if (modelcoeffs.alpha > 0) {
1274 alpha = modelcoeffs.alpha;
1275 } else {
1276 alpha = respondToTemperature(&modelcoeffs.alphaTempResponse, TL);
1277 }
1278
1279
1280 TPU = respondToTemperature(&modelcoeffs.TPUTempResponse, TL);
1281 theta = respondToTemperature(&modelcoeffs.thetaTempResponse, TL);
1282
1283 // Mesophyll conductance gm (mol CO2 / m^2 / s / bar). If gm is infinite/huge,
1284 // Cc ≡ Ci and we take the original legacy path below. Otherwise we solve a
1285 // quadratic in net A that couples Cc = Ci − A/gm (Ethier & Livingston 2004 /
1286 // Sharkey et al. 2007).
1287 const float gm = respondToTemperature(&modelcoeffs.gmTempResponse, TL);
1288 const bool gm_infinite = (!std::isfinite(gm) || gm > 1.0e6f);
1289
1290
1291 float Gamma_star = exp(c_Gamma - dH_Gamma / (R * TL));
1292 float Kc = exp(c_Kc - dH_Kc / (R * TL));
1293 float Ko = exp(c_Ko - dH_Ko / (R * TL));
1294 float Kco = Kc * (1.f + O / Ko);
1295
1296 // thetaJ^2 + -(alphaQ+Jmax)J + alphaQJmax = 0
1297 double a = std::max(theta, 0.0001f);
1298 double ia = 1.000f / a;
1299 double b = -(alpha * Q + Jmax);
1300 double c = alpha * Q * Jmax;
1301 double J = (-b - sqrt(pow(b, 2.000f) - 4.f * a * c)) * 0.5f * ia;
1302 // J = Jmax * alpha * Q / (alpha * Q + Jmax);
1303
1304 // Store J/Jmax ratio for fluorescence calculations
1305 float J_over_Jmax = (Jmax > 0.f) ? static_cast<float>(J / Jmax) : 0.f;
1306 variables[8] = J_over_Jmax;
1307
1308 float A;
1309 float limitation_state;
1310
1311 if (gm_infinite) {
1312 // Legacy path: Cc ≡ Ci (no mesophyll limitation).
1313 float Wc = Vcmax * Ci / (Ci + Kco);
1314 float Wj = J * Ci / (4.f * Ci + 8.f * Gamma_star);
1315
1316 float smooth_factor = 0.99f;
1317 float s = helios::clamp(0.5f + 0.5f * (Wc - Wj) / smooth_factor, 0.0f, 1.0f);
1318 float smooth_min = Wc * (1.f - s) + Wj * s - smooth_factor * s * (1.f - s);
1319
1320 A = (1.f - Gamma_star / Ci) * smooth_min - Rd;
1321
1322 // TPU comparison at assimilation level: A_TPU = 3*TPU - Rd (singularity-free)
1323 // The Ci-dependent terms in Wp = 3*TPU*Ci/(Ci - Gamma*) cancel with (1 - Gamma*/Ci),
1324 // so TPU-limited assimilation is simply a constant independent of Ci.
1325 if (TPUflag == 1) {
1326 float A_p = 3.f * TPU - Rd;
1327 smooth_factor = 0.99f;
1328 s = helios::clamp(0.5f + 0.5f * (A - A_p) / smooth_factor, 0.0f, 1.0f);
1329 A = A * (1.f - s) + A_p * s - smooth_factor * s * (1.f - s);
1330 }
1331
1332 if (Wj > Wc) { // Rubisco limited
1333 limitation_state = 0;
1334 } else { // Electron transport limited
1335 limitation_state = 1;
1336 }
1337 } else {
1338 // Finite-gm path. Substituting Cc = Ci − A/gm into the standard FvCB
1339 // expressions A = Vcmax·(Cc−Γ*)/(Cc+Kco) − Rd (Rubisco-limited) and
1340 // A = (J/4)·(Cc−Γ*)/(Cc+2Γ*) − Rd (electron-transport-limited) yields two
1341 // quadratics in net A of the form (1/gm)·A² + b·A + c = 0, with the
1342 // physical root being the smaller root (stable as gm → ∞).
1343 auto solve_quadratic_min_root = [](float aq, float bq, float cq) -> float {
1344 if (aq <= 0.f) {
1345 return -cq / bq;
1346 }
1347 float disc = bq * bq - 4.f * aq * cq;
1348 if (disc < 0.f) {
1349 disc = 0.f;
1350 }
1351 float sqrtD = std::sqrt(disc);
1352 float q = -0.5f * (bq - sqrtD); // bq < 0 in physical regime → safely large magnitude
1353 if (q == 0.f) {
1354 return (-bq + sqrtD) * 0.5f / aq;
1355 }
1356 return cq / q;
1357 };
1358
1359 const float inv_gm = 1.f / gm;
1360 // Rubisco-limited: A²/gm − A·[(Ci+Kco) + (Vcmax−Rd)/gm] + [Vcmax·(Ci−Γ*) − Rd·(Ci+Kco)] = 0
1361 const float a_c = inv_gm;
1362 const float b_c = -((Ci + Kco) + (Vcmax - Rd) * inv_gm);
1363 const float c_c = Vcmax * (Ci - Gamma_star) - Rd * (Ci + Kco);
1364 const float A_c_net = solve_quadratic_min_root(a_c, b_c, c_c);
1365
1366 // Electron-transport-limited: substitute Vcmax → J/4 and Kco → 2·Γ*
1367 const float J4 = 0.25f * static_cast<float>(J);
1368 const float a_j = inv_gm;
1369 const float b_j = -((Ci + 2.f * Gamma_star) + (J4 - Rd) * inv_gm);
1370 const float c_j = J4 * (Ci - Gamma_star) - Rd * (Ci + 2.f * Gamma_star);
1371 const float A_j_net = solve_quadratic_min_root(a_j, b_j, c_j);
1372
1373 // Smooth-min on net A — smaller A wins. s → 0 when A_c_net is smaller (Rubisco
1374 // limited); s → 1 when A_j_net is smaller (electron-transport limited).
1375 float smooth_factor = 0.99f;
1376 float s = helios::clamp(0.5f + 0.5f * (A_c_net - A_j_net) / smooth_factor, 0.0f, 1.0f);
1377 A = A_c_net * (1.f - s) + A_j_net * s - smooth_factor * s * (1.f - s);
1378
1379 // TPU limitation: A_p = 3·TPU − Rd is independent of Cc, so it enters the
1380 // net-A smooth-min unchanged.
1381 if (TPUflag == 1) {
1382 float A_p = 3.f * TPU - Rd;
1383 smooth_factor = 0.99f;
1384 s = helios::clamp(0.5f + 0.5f * (A - A_p) / smooth_factor, 0.0f, 1.0f);
1385 A = A * (1.f - s) + A_p * s - smooth_factor * s * (1.f - s);
1386 }
1387
1388 // Smaller A wins in the finite-gm branch (inequality flips vs. Wj > Wc).
1389 if (A_c_net < A_j_net) { // Rubisco limited
1390 limitation_state = 0;
1391 } else { // Electron transport limited
1392 limitation_state = 1;
1393 }
1394 }
1395
1396 //--- Calculate error and update --- //
1397
1398 float resid = 0.75f * gM * (CO2 - Ci) - A;
1399
1400 variables[4] = A;
1401 variables[5] = limitation_state;
1402
1403 float Gamma = (Gamma_star + Kco * Rd / Vcmax) / (1.f - Rd / Vcmax); // Equation 39 of Farquhar et al. (1980)
1404 variables[6] = Gamma;
1405
1406 return resid;
1407}
1408
1409
1410namespace {
1415 void computeC4RatesFromCm(float Cm, const C4ModelCoefficients &p, float Vpmax, float Vcmax, float Jmax, float Rd, float Kc, float Ko, float Kp, float gamma_star, float Om, float I_incident, float &Ac_out, float &Aj_out, float &Vp_out) {
1416
1417 const float Rm = p.Rm_frac * Rd;
1418
1419 // PEP carboxylation: V_p = min(C_m · V_pmax / (C_m + K_p), V_pr) Eq. 19
1420 const float Vp_MM = Cm * Vpmax / (Cm + Kp);
1421 const float Vp = std::min(Vp_MM, p.Vpr);
1422 Vp_out = Vp;
1423
1424 // Whole-chain electron transport rate J from non-rectangular hyperbola (Eq. 34)
1425 const float rho = (1.f - p.fcyc) / (2.f - p.fcyc);
1426 const float I2 = I_incident * p.absorptance * rho * (1.f - p.f_spectral);
1427 const float theta = std::max(p.theta_etr, 1.0e-4f);
1428 const float sum_IJ = I2 + Jmax;
1429 const float radicand_J = std::max(sum_IJ * sum_IJ - 4.f * theta * I2 * Jmax, 0.f);
1430 const float J = (sum_IJ - std::sqrt(radicand_J)) / (2.f * theta);
1431
1432 // ATP / linear-electron-transport ratio (vC2021 Eq. 31 generalized by Woodford et al. 2025):
1433 // z = (H_J · (1 − f_cyc) + H_Jcyc · f_cyc) / (h · (1 − f_cyc))
1434 // vC2021's original form z = (3 − f_cyc) / (h · (1 − f_cyc)) is the H_J = 3, H_Jcyc = 2 special
1435 // case. Woodford's H_Jcyc = 3.4 (NDH-dominated cyclic flow) with f_cyc = 0.45 gives z = 1.45.
1436 const float denom_z = std::max(p.h_protons * (1.f - p.fcyc), 1.0e-6f);
1437 const float z = (p.H_J * (1.f - p.fcyc) + p.H_Jcyc * p.fcyc) / denom_z;
1438
1439 const float alpha = p.alpha_psII_fraction;
1440 const float ao = std::max(p.ao, 1.0e-6f);
1441 const float gbs = p.gbs;
1442
1443 // --- Enzyme-limited rate A_c (quadratic in A) ---
1444 const float term_PEP = Vp - Rm + gbs * Cm; // (V_p − R_m + g_bs·C_m)
1445 const float term_Rub = Vcmax - Rd; // (V_cmax − R_d)
1446 const float Kco = 1.f + Om / Ko;
1447 const float a_c = 1.f - alpha * Kc / (ao * Ko);
1448 const float b_c = -(term_PEP + term_Rub + gbs * Kc * Kco + (alpha / ao) * (gamma_star * Vcmax + Rd * Kc / Ko));
1449 const float c_c = term_Rub * term_PEP - gbs * (Vcmax * gamma_star * Om + Rd * Kc * Kco);
1450 const float disc_c = std::max(b_c * b_c - 4.f * a_c * c_c, 0.f);
1451 // Smaller root — the physically meaningful solution (the larger root is unphysically high)
1452 Ac_out = (-b_c - std::sqrt(disc_c)) / (2.f * a_c);
1453
1454 // --- Electron-transport-limited rate A_j (quadratic in A) ---
1455 const float Jm = p.x_etr_partition * J; // x · J
1456 const float Jb = (1.f - p.x_etr_partition) * J; // (1 − x) · J
1457 const float term_mesJ = Jm * z * 0.5f - Rm + gbs * Cm; // mesophyll ATP-driven rate (Eq. 36-ish)
1458 const float term_bunJ = Jb * z / 3.f - Rd; // bundle-sheath Rubisco-ATP rate
1459 const float a_j = 1.f - 7.f * gamma_star * alpha / (3.f * ao);
1460 const float b_j = -(term_mesJ + term_bunJ + gbs * 7.f * gamma_star * Om / 3.f + (alpha * gamma_star / ao) * (Jb * z / 3.f + 7.f * Rd / 3.f));
1461 const float c_j = term_mesJ * term_bunJ - gbs * gamma_star * Om * (Jb * z / 3.f + 7.f * Rd / 3.f);
1462 const float disc_j = std::max(b_j * b_j - 4.f * a_j * c_j, 0.f);
1463 Aj_out = (-b_j - std::sqrt(disc_j)) / (2.f * a_j);
1464 }
1465} // namespace
1466
1467
1468float PhotosynthesisModel::evaluateCi_C4(float Ci, std::vector<float> &variables, const void *parameters) {
1469
1470 const C4ModelCoefficients &p = *reinterpret_cast<const C4ModelCoefficients *>(parameters);
1471
1472 const float CO2 = variables[0];
1473 const float I_incident = variables[1];
1474 const float TL = variables[2];
1475 const float gM = variables[3];
1476
1477 const float R = 0.0083144598f; // kJ/K/mol
1478 const float invDiffRT = (1.f / 298.15f - 1.f / TL) / R;
1479
1480 // Temperature-responsive parameters (peaked Arrhenius framework)
1481 const float Vpmax = respondToTemperature(&p.VpmaxTempResponse, TL);
1482 const float Vcmax = respondToTemperature(&p.VcmaxTempResponse, TL);
1483 const float Jmax = respondToTemperature(&p.JmaxTempResponse, TL);
1484 const float Rd = respondToTemperature(&p.RdTempResponse, TL);
1485 const float gm = respondToTemperature(&p.gmTempResponse, TL);
1486
1487 // Rubisco + PEPC kinetic constants (simple Arrhenius, same treatment as C3 Kc/Ko)
1488 const float Kc = p.Kc_25 * std::exp(p.dH_Kc * invDiffRT);
1489 const float Ko = p.Ko_25 * std::exp(p.dH_Ko * invDiffRT);
1490 const float Kp = p.Kp_25 * std::exp(p.dH_Kp * invDiffRT);
1491 const float gamma_star = p.gamma_star_25 * std::exp(p.dH_gamma_star * invDiffRT);
1492 const float Om = p.Om_25 * std::exp(p.dH_Om * invDiffRT);
1493
1494 // Solve C_m = C_i − A/g_m by damped fixed-point iteration. For typical parameters this
1495 // converges in 5-15 iterations; the damping factor prevents oscillation when A is large.
1496 float Cm = std::max(Ci, 0.1f); // initial guess
1497 if (!std::isfinite(gm) || gm <= 0.f) {
1498 // Should not happen with default gmTempResponse, but guard against user misuse
1499 helios::helios_runtime_error("ERROR (PhotosynthesisModel::evaluateCi_C4): Mesophyll conductance g_m must be positive and finite (got " + std::to_string(gm) + "). Check C4ModelCoefficients::setMesophyllConductance_gm().");
1500 }
1501
1502 float Ac = 0.f, Aj = 0.f, Vp = 0.f, A = 0.f;
1503 for (int iter = 0; iter < 50; ++iter) {
1504 computeC4RatesFromCm(Cm, p, Vpmax, Vcmax, Jmax, Rd, Kc, Ko, Kp, gamma_star, Om, I_incident, Ac, Aj, Vp);
1505 A = std::min(Ac, Aj);
1506 const float Cm_target = Ci - A / gm;
1507 if (std::fabs(Cm_target - Cm) < 1.0e-5f) {
1508 Cm = Cm_target;
1509 break;
1510 }
1511 Cm = 0.5f * Cm + 0.5f * Cm_target; // damped update
1512 }
1513 // Final re-evaluation at the converged C_m
1514 computeC4RatesFromCm(Cm, p, Vpmax, Vcmax, Jmax, Rd, Kc, Ko, Kp, gamma_star, Om, I_incident, Ac, Aj, Vp);
1515 A = std::min(Ac, Aj);
1516
1517 // Limitation state: 1 = Rubisco/PEPC enzyme-limited, 2 = electron transport-limited
1518 const int limitation = (Ac < Aj) ? 1 : 2;
1519
1520 variables[4] = A;
1521 variables[5] = static_cast<float>(limitation);
1522 variables[6] = Cm;
1523 variables[7] = Vp;
1524
1525 // Outer residual: stomatal-conductance balance (used by fzero when Ci is not manually overridden)
1526 return 0.75f * gM * (CO2 - Ci) - A;
1527}
1528
1529
1530float PhotosynthesisModel::evaluateCm_C4(float Cm, std::vector<float> &variables, const C4ModelCoefficients &params) {
1531
1532 const float I_incident = variables[1];
1533 const float TL = variables[2];
1534
1535 const float R = 0.0083144598f;
1536 const float invDiffRT = (1.f / 298.15f - 1.f / TL) / R;
1537
1538 const float Vpmax = respondToTemperature(&params.VpmaxTempResponse, TL);
1539 const float Vcmax = respondToTemperature(&params.VcmaxTempResponse, TL);
1540 const float Jmax = respondToTemperature(&params.JmaxTempResponse, TL);
1541 const float Rd = respondToTemperature(&params.RdTempResponse, TL);
1542 const float Kc = params.Kc_25 * std::exp(params.dH_Kc * invDiffRT);
1543 const float Ko = params.Ko_25 * std::exp(params.dH_Ko * invDiffRT);
1544 const float Kp = params.Kp_25 * std::exp(params.dH_Kp * invDiffRT);
1545 const float gamma_star = params.gamma_star_25 * std::exp(params.dH_gamma_star * invDiffRT);
1546 const float Om = params.Om_25 * std::exp(params.dH_Om * invDiffRT);
1547
1548 float Ac = 0.f, Aj = 0.f, Vp = 0.f;
1549 computeC4RatesFromCm(Cm, params, Vpmax, Vcmax, Jmax, Rd, Kc, Ko, Kp, gamma_star, Om, I_incident, Ac, Aj, Vp);
1550 const float A = std::min(Ac, Aj);
1551 const int limitation = (Ac < Aj) ? 1 : 2;
1552
1553 variables[4] = A;
1554 variables[5] = static_cast<float>(limitation);
1555 variables[6] = Cm;
1556 variables[7] = Vp;
1557 return A;
1558}
1559
1560
1561float PhotosynthesisModel::evaluateC4Model(const C4ModelCoefficients &params, float i_PAR, float TL, float CO2, float gM, float &Ci, float &Cm, float &Vp, int &limitation_state, helios::WarningAggregator &warnings) {
1562
1563 float A = 0.f;
1564 // variables layout: [CO2, I_abs, TL, gM, A_out, limitation_out, Cm_out, Vp_out]
1565 std::vector<float> variables{CO2, i_PAR, TL, gM, A, static_cast<float>(limitation_state), Cm, Vp};
1566
1567 std::vector<float> initial_guesses;
1568 if (Ci > 0 && std::isfinite(Ci)) {
1569 initial_guesses.push_back(Ci);
1570 }
1571 initial_guesses.insert(initial_guesses.end(), {CO2 * 0.6f, CO2 * 0.3f, 100.f, 50.f, CO2 * 0.9f});
1572
1573 bool overall_converged = false;
1574 for (float guess: initial_guesses) {
1575 std::vector<float> vars_attempt = variables;
1576 bool attempt_converged = false;
1577 float Ci_attempt = fzero(evaluateCi_C4, vars_attempt, &params, guess, attempt_converged, 0.001f, 200);
1578 if (attempt_converged && Ci_attempt > 0 && std::isfinite(Ci_attempt)) {
1579 Ci = Ci_attempt;
1580 variables = vars_attempt;
1581 overall_converged = true;
1582 break;
1583 }
1584 }
1585
1586 if (!overall_converged) {
1587 warnings.addWarning("photosynthesis_c4_ci_convergence_failure", "C4 photosynthesis model failed to converge for Ci after trying multiple initial guesses.");
1588 bool final_converged = false;
1589 Ci = fzero(evaluateCi_C4, variables, &params, 100.f, final_converged, 0.01f, 500);
1590 }
1591
1592 A = variables[4];
1593 limitation_state = static_cast<int>(variables[5]);
1594 Cm = variables[6];
1595 Vp = variables[7];
1596
1597 return A;
1598}
1599
1600
1601float PhotosynthesisModel::respondToTemperature(const PhotosyntheticTemperatureResponseParameters *params, float T_in_Kelvin) {
1602 float T = T_in_Kelvin;
1603 float R = 0.0083144598f;
1604 float v25 = params->value_at_25C;
1605 float dHa = params->dHa;
1606 float dHd = params->dHd;
1607 float Topt = params->Topt;
1608 if (dHa == 0) {
1609 return v25;
1610 } else {
1611 float logterm = logf(dHd / dHa - 1.f);
1612 float t1 = 1.f + expf(dHd / R * (1.f / Topt - 1.f / 298.15f) - logterm);
1613 float t2 = 1.f + expf(dHd / R * (1.f / Topt - 1.f / T) - logterm);
1614 return v25 * exp(dHa / R * (1.f / 298.15f - 1.f / T)) * t1 / t2;
1615 }
1616}
1617
1618
1619float PhotosynthesisModel::evaluateFarquharModel(const FarquharModelCoefficients &params, float i_PAR, float TL, float CO2, float gM, float &Ci, float &Gamma, int &limitation_state, int &TPU_flag, float &J_over_Jmax,
1620 helios::WarningAggregator &warnings) {
1621
1622 float A = 0;
1623 std::vector<float> variables{CO2, i_PAR, TL, gM, A, float(limitation_state), Gamma, float(TPU_flag), 0.f};
1624
1625 // Temporal continuity approach: use previous Ci as initial guess, with fallbacks
1626 // This leverages the fact that Ci changes slowly between timesteps
1627 std::vector<float> initial_guesses;
1628
1629 // First try previous Ci if available (temporal continuity)
1630 if (Ci > 0 && std::isfinite(Ci)) {
1631 initial_guesses.push_back(Ci);
1632 }
1633
1634 // Add fallback guesses based on research (physiologically reasonable values)
1635 initial_guesses.insert(initial_guesses.end(), {CO2 * 0.7f, 100.0f, CO2 * 0.3f, CO2 * 0.9f, 50.0f});
1636
1637 bool overall_converged = false;
1638 for (float guess: initial_guesses) {
1639 // Reset variables for each attempt
1640 std::vector<float> vars_attempt = variables;
1641 bool attempt_converged = false;
1642 float Ci_attempt = fzero(evaluateCi_Farquhar, vars_attempt, &params, guess, attempt_converged, 0.001f, 200);
1643
1644 // Check if solution converged and is physiologically reasonable
1645 if (attempt_converged && Ci_attempt > 0 && std::isfinite(Ci_attempt)) {
1646 Ci = Ci_attempt;
1647 variables = vars_attempt;
1648 overall_converged = true;
1649 break;
1650 }
1651 }
1652
1653 // If all initial guesses failed, add warning to aggregator
1654 if (!overall_converged) {
1655 warnings.addWarning("photosynthesis_ci_convergence_failure", "Photosynthesis model failed to converge for Ci calculation after trying multiple initial guesses.");
1656 // Use best available estimate from last attempt
1657 bool final_converged = false;
1658 Ci = fzero(evaluateCi_Farquhar, variables, &params, 100.0f, final_converged, 0.01f, 500);
1659 }
1660
1661 A = variables[4];
1662 limitation_state = (int) variables[5];
1663 Gamma = variables[6];
1664 J_over_Jmax = variables[8];
1665
1666 return A;
1667}
1668
1670 return getCoefficientsForPrimitive_Empirical(UUID);
1671}
1672
1674 return getCoefficientsForPrimitive_Farquhar(UUID);
1675}
1676
1678 message_flag = false;
1679}
1680
1682 message_flag = true;
1683}
1684
1686
1687 if (strcmp(label, "Ci") == 0 || strcmp(label, "limitation_state") == 0 || strcmp(label, "Gamma_CO2") == 0 || strcmp(label, "electron_transport_ratio") == 0 || strcmp(label, "Cm") == 0 || strcmp(label, "Vp") == 0) {
1688 output_prim_data.emplace_back(label);
1689 } else {
1690 if (message_flag) {
1691 static bool unknown_output_warning_shown = false;
1692 if (!unknown_output_warning_shown) {
1693 std::cerr << "WARNING (PhotosynthesisModel::optionalOutputPrimitiveData): unknown output primitive data " << label << std::endl;
1694 unknown_output_warning_shown = true;
1695 }
1696 }
1697 }
1698}
1699
1703
1704void PhotosynthesisModel::printDefaultValueReport(const std::vector<uint> &UUIDs) const {
1705
1706 size_t assumed_default_i = 0;
1707 size_t assumed_default_TL = 0;
1708 size_t assumed_default_CO2 = 0;
1709 size_t assumed_default_gM = 0;
1710 size_t assumed_default_gH = 0;
1711
1712 size_t Nprimitives = UUIDs.size();
1713
1714 for (uint UUID: UUIDs) {
1715
1716 if (!context->doesPrimitiveDataExist(UUID, "radiation_flux_PAR") || context->getPrimitiveDataType("radiation_flux_PAR") != HELIOS_TYPE_FLOAT) {
1717 assumed_default_i++;
1718 }
1719
1720 // surface temperature (K)
1721 if (!context->doesPrimitiveDataExist(UUID, "temperature") || context->getPrimitiveDataType("temperature") != HELIOS_TYPE_FLOAT) {
1722 assumed_default_TL++;
1723 }
1724
1725 // boundary-layer conductance to heat
1726 if ((!context->doesPrimitiveDataExist(UUID, "boundarylayer_conductance") || context->getPrimitiveDataType("boundarylayer_conductance") != HELIOS_TYPE_FLOAT) &&
1727 (!context->doesPrimitiveDataExist(UUID, "boundarylayer_conductance_out") || context->getPrimitiveDataType("boundarylayer_conductance_out") != HELIOS_TYPE_FLOAT)) {
1728 assumed_default_gH++;
1729 }
1730
1731 // stomatal conductance
1732 if (!context->doesPrimitiveDataExist(UUID, "moisture_conductance") || context->getPrimitiveDataType("moisture_conductance") != HELIOS_TYPE_FLOAT) {
1733 assumed_default_gM++;
1734 }
1735
1736 // ambient air CO2
1737 if (!context->doesPrimitiveDataExist(UUID, "air_CO2") || context->getPrimitiveDataType("air_CO2") != HELIOS_TYPE_FLOAT) {
1738 assumed_default_CO2++;
1739 }
1740 }
1741
1742 std::cout << "--- Photosynthesis Model Default Value Report ---" << std::endl;
1743
1744 std::cout << "PAR flux: " << assumed_default_i << " of " << Nprimitives << " used default value of " << i_PAR_default
1745 << " because "
1746 "radiation_flux_PAR"
1747 " primitive data did not exist"
1748 << std::endl;
1749 std::cout << "surface temperature: " << assumed_default_TL << " of " << Nprimitives << " used default value of " << TL_default
1750 << " because "
1751 "temperature"
1752 " primitive data did not exist"
1753 << std::endl;
1754 std::cout << "boundary-layer conductance: " << assumed_default_gH << " of " << Nprimitives << " used default value of " << gH_default
1755 << " because "
1756 "boundarylayer_conductance"
1757 " primitive data did not exist"
1758 << std::endl;
1759 std::cout << "moisture conductance: " << assumed_default_gM << " of " << Nprimitives << " used default value of " << gM_default
1760 << " because "
1761 "moisture_conductance"
1762 " primitive data did not exist"
1763 << std::endl;
1764 std::cout << "air CO2: " << assumed_default_CO2 << " of " << Nprimitives << " used default value of " << CO2_default
1765 << " because "
1766 "air_CO2"
1767 " primitive data did not exist"
1768 << std::endl;
1769
1770 std::cout << "--------------------------------------------------" << std::endl;
1771}