1.3.64
 
Loading...
Searching...
No Matches
CarbohydrateModel.cpp
Go to the documentation of this file.
1
16#include "PlantArchitecture.h"
17#include "../include/PlantArchitecture.h"
18
19using namespace helios;
20
21
22float Phytomer::calculatePhytomerConstructionCosts() const {
23 float leaf_carbon_percentage = plantarchitecture_ptr->plant_instances.at(this->plantID).carb_parameters.leaf_carbon_percentage;
24 float SLA = plantarchitecture_ptr->plant_instances.at(this->plantID).carb_parameters.SLA;
25
26 float leaf_construction_cost_base = leaf_carbon_percentage * 0.1 / (C_molecular_wt * SLA); // mol C/m^2
27
28 float phytomer_carbon_cost = 0.f; // mol C
29
30 // leaves (cost per area basis)
31 float leaf_area = 0;
32 uint p = 0;
33 for (const auto &petiole: leaf_objIDs) {
34 for (uint leaf_objID: petiole) {
35 if (context_ptr->doesObjectExist(leaf_objID)) {
36 float obj_area = context_ptr->getObjectArea(leaf_objID);
37 float scale_factor = current_leaf_scale_factor.at(p);
38 float scaled_area = obj_area / powi(scale_factor, 2);
39 leaf_area += scaled_area;
40 }
41 }
42 p++;
43 }
44 phytomer_carbon_cost += leaf_construction_cost_base * leaf_area;
45
46 return phytomer_carbon_cost;
47}
48
49float Phytomer::calculateFlowerConstructionCosts(const FloralBud &fbud) const {
50 float flower_carbon_cost = 0.f; // mol C
51
52 for (uint flower_objID: fbud.inflorescence_objIDs) {
53 flower_carbon_cost += plantarchitecture_ptr->plant_instances.at(this->plantID).carb_parameters.total_flower_cost;
54 }
55
56 return flower_carbon_cost;
57}
58
59void PlantArchitecture::initializeCarbohydratePool(float carbohydrate_concentration_molC_m3) const {
60 for (auto &plant: plant_instances) {
61 auto shoot_tree = &plant.second.shoot_tree;
62
63 for (auto &shoot: *shoot_tree) {
64 // calculate shoot volume
65 float shoot_volume = shoot->calculateShootInternodeVolume();
66 // set carbon pool
67 shoot->carbohydrate_pool_molC = shoot_volume * carbohydrate_concentration_molC_m3;
68 context_ptr->setObjectData(shoot->internode_tube_objID, "carbohydrate_concentration", carbohydrate_concentration_molC_m3);
69 context_ptr->setObjectData(shoot->internode_tube_objID, "carbohydrate_pool_molC", shoot->carbohydrate_pool_molC);
70 }
71 }
72}
73
74void PlantArchitecture::initializePlantCarbohydratePool(uint plantID, float carbohydrate_concentration_molC_m3) {
75
76 // Make sure that the plant exists in the context
77 if (plant_instances.find(plantID) == plant_instances.end()) {
78 helios_runtime_error("ERROR (PlantArchitecture::initializePlantCarbohydratePool): Plant with ID of " + std::to_string(plantID) + " does not exist.");
79 } else if (carbohydrate_concentration_molC_m3 < 0.f) {
80 helios_runtime_error("ERROR (PlantArchitecture::initializePlantCarbohydratePool): Carbohydrate concentration must be greater than or equal to zero.");
81 }
82
83 // loop over all shoots
84 for (auto &shoot: plant_instances.at(plantID).shoot_tree) {
85 initializeShootCarbohydratePool(plantID, shoot->ID, carbohydrate_concentration_molC_m3);
86 }
87}
88
89void PlantArchitecture::initializeShootCarbohydratePool(uint plantID, uint shootID, float carbohydrate_concentration_molC_m3) {
90 if (plant_instances.find(plantID) == plant_instances.end()) {
91 helios_runtime_error("ERROR (PlantArchitecture::initializeShootCarbohydratePool): Plant with ID of " + std::to_string(plantID) + " does not exist.");
92 } else if (shootID >= plant_instances.at(plantID).shoot_tree.size()) {
93 helios_runtime_error("ERROR (PlantArchitecture::initializeShootCarbohydratePool): Shoot with ID of " + std::to_string(shootID) + " does not exist.");
94 } else if (carbohydrate_concentration_molC_m3 < 0.f) {
95 helios_runtime_error("ERROR (PlantArchitecture::initializeShootCarbohydratePool): Carbohydrate concentration must be greater than or equal to zero.");
96 }
97
98 // calculate shoot volume
99 float shoot_volume = plant_instances.at(plantID).shoot_tree.at(shootID)->calculateShootInternodeVolume();
100
101 // set carbon pool
102 plant_instances.at(plantID).shoot_tree.at(shootID)->carbohydrate_pool_molC = shoot_volume * carbohydrate_concentration_molC_m3;
103}
104
106 for (const auto &[plantID, plant_instance]: plant_instances) {
107 auto shoot_tree = &plant_instance.shoot_tree;
108
109 for (auto &shoot: *shoot_tree) {
110 // Set cumulative photosynthesis of dormant shoots equal to zero.
111 if (shoot->isdormant) {
112 for (const auto &phytomer: shoot->phytomers) {
113 for (const auto &leaf_objID: flatten(phytomer->leaf_objIDs)) {
114 for (uint UUID: context_ptr->getObjectPrimitiveUUIDs(leaf_objID)) {
115 context_ptr->setPrimitiveData(UUID, "cumulative_net_photosynthesis", 0);
116 }
117 }
118 }
119 } else {
120 for (const auto &phytomer: shoot->phytomers) {
121 for (auto &leaf_objID: flatten(phytomer->leaf_objIDs)) {
122 for (uint UUID: context_ptr->getObjectPrimitiveUUIDs(leaf_objID)) {
123 float lUUID_area = context_ptr->getPrimitiveArea(UUID);
124 float current_net_photo = 0;
125
126 float leaf_A = 0.f;
127 if (context_ptr->doesPrimitiveDataExist(UUID, "net_photosynthesis")) {
128 context_ptr->getPrimitiveData(UUID, "net_photosynthesis", leaf_A);
129 }
130
131 float new_hourly_photo = leaf_A * lUUID_area * 2 * 3600.f * 1e-6f;
132 ; //net photosynthesis (mol C hr-1) from umol CO2 m-2 sec-1
133
134 if (context_ptr->doesPrimitiveDataExist(UUID, "cumulative_net_photosynthesis")) {
135 context_ptr->getPrimitiveData(UUID, "cumulative_net_photosynthesis", current_net_photo);
136 }
137 current_net_photo += new_hourly_photo;
138 context_ptr->setPrimitiveData(UUID, "cumulative_net_photosynthesis", current_net_photo);
139 }
140 }
141 }
142 }
143 }
144 }
145}
146
147void PlantArchitecture::accumulateShootPhotosynthesis() const {
148 uint A_prim_data_missing = 0;
149
150 for (auto &[plantID, plant_instance]: plant_instances) {
151
152 const auto shoot_tree = &plant_instance.shoot_tree;
153
154 for (const auto &shoot: *shoot_tree) {
155 uint shootID = shoot->ID;
156 float net_photosynthesis = 0;
157 float shoot_volume = plant_instances.at(plantID).shoot_tree.at(shoot->ID)->calculateShootInternodeVolume();
158
159 if (shoot->isdormant) {
160 for (const auto &phytomer: shoot->phytomers) {
161 for (const auto &leaf_objID: flatten(phytomer->leaf_objIDs)) {
162 for (uint UUID: context_ptr->getObjectPrimitiveUUIDs(leaf_objID)) {
163 context_ptr->setPrimitiveData(UUID, "cumulative_net_photosynthesis", 0.f);
164 }
165 }
166 }
167 } else {
168 for (const auto &phytomer: shoot->phytomers) {
169 for (const auto &leaf_objID: flatten(phytomer->leaf_objIDs)) {
170 for (uint UUID: context_ptr->getObjectPrimitiveUUIDs(leaf_objID)) {
171 if (context_ptr->doesPrimitiveDataExist(UUID, "cumulative_net_photosynthesis")) {
172 float A;
173 context_ptr->getPrimitiveData(UUID, "cumulative_net_photosynthesis", A);
174 net_photosynthesis += A;
175 context_ptr->setPrimitiveData(UUID, "cumulative_net_photosynthesis", 0.f);
176 } else {
177 A_prim_data_missing++;
178 context_ptr->setPrimitiveData(UUID, "cumulative_net_photosynthesis", 0.f);
179 }
180 }
181 }
182 }
183 }
184 if (net_photosynthesis >= 0.f) {
185 shoot->carbohydrate_pool_molC += net_photosynthesis;
186 }
187 context_ptr->setObjectData(shoot->internode_tube_objID, "carbohydrate_concentration", shoot->carbohydrate_pool_molC / shoot_volume);
188 context_ptr->setObjectData(shoot->internode_tube_objID, "carbohydrate_pool_molC", shoot->carbohydrate_pool_molC);
189 context_ptr->setObjectData(shoot->internode_tube_objID, "daily_net_photosynthesis", net_photosynthesis);
190 }
191 }
192
193 if (A_prim_data_missing > 0) {
194 std::cerr << "WARNING (PlantArchitecture::accumulateShootPhotosynthesis): " << A_prim_data_missing << " leaf primitives were missing net_photosynthesis primitive data. Did you run the photosynthesis model?" << std::endl;
195 }
196}
197
198void PlantArchitecture::subtractShootMaintenanceCarbon(float dt) const {
199 for (const auto &[plantID, plant_instance]: plant_instances) {
200 auto shoot_tree = &plant_instance.shoot_tree;
201
202 const CarbohydrateParameters &carbohydrate_params = plant_instances.at(plantID).carb_parameters;
203
204 float rho_cw = carbohydrate_params.stem_density * carbohydrate_params.stem_structural_carbon_percentage / C_molecular_wt; // Density of carbon in almond wood (mol C m^-3)
205
206 for (auto &shoot: *shoot_tree) {
207 if (context_ptr->doesObjectExist(shoot->internode_tube_objID)) {
208 if (shoot->isdormant && shoot->old_shoot_volume >= 0.f) {
209 shoot->carbohydrate_pool_molC -= shoot->old_shoot_volume * rho_cw * carbohydrate_params.stem_maintenance_respiration_rate * carbohydrate_params.dormant_respiration_fraction * dt; // remove shoot maintenance respiration (0.5 correction for living wood fraction, 0.2 correction for
210 shoot->carbohydrate_pool_molC -= shoot->old_shoot_volume * rho_cw * carbohydrate_params.root_maintenance_respiration_rate / carbohydrate_params.shoot_root_ratio * carbohydrate_params.dormant_respiration_fraction * dt; // remove root maintenance respiration portion
211 float net_respiration = (shoot->old_shoot_volume * rho_cw * carbohydrate_params.stem_maintenance_respiration_rate * carbohydrate_params.dormant_respiration_fraction * dt) + (shoot->old_shoot_volume * rho_cw * carbohydrate_params.root_maintenance_respiration_rate / carbohydrate_params.shoot_root_ratio * carbohydrate_params.dormant_respiration_fraction * dt);
212 context_ptr->setObjectData(shoot->internode_tube_objID, "daily_respiration", net_respiration);
213 } else if (shoot->old_shoot_volume >= 0.f) {
214 shoot->carbohydrate_pool_molC -= shoot->old_shoot_volume * rho_cw * carbohydrate_params.stem_maintenance_respiration_rate * dt; // remove shoot maintenance respiration
215 shoot->carbohydrate_pool_molC -= shoot->old_shoot_volume * rho_cw * carbohydrate_params.root_maintenance_respiration_rate / carbohydrate_params.shoot_root_ratio * dt; // remove root maintenance respiration portion
216 float net_respiration = (shoot->old_shoot_volume * rho_cw * carbohydrate_params.stem_maintenance_respiration_rate * dt) + (shoot->old_shoot_volume * rho_cw * carbohydrate_params.root_maintenance_respiration_rate / carbohydrate_params.shoot_root_ratio * dt);
217 context_ptr->setObjectData(shoot->internode_tube_objID, "daily_respiration", net_respiration);
218 }
219 }
220 }
221 }
222}
223
224void PlantArchitecture::subtractShootGrowthCarbon() {
225 for (auto &[plantID, plant_instance]: plant_instances) {
226 const auto shoot_tree = &plant_instance.shoot_tree;
227
228 const CarbohydrateParameters &carbohydrate_params = plant_instances.at(plantID).carb_parameters;
229
230 float rho_cw = carbohydrate_params.stem_density * carbohydrate_params.stem_structural_carbon_percentage / C_molecular_wt; // Mature density of carbon in almond wood (mol C m^-3)
231
232 for (const auto &shoot: *shoot_tree) {
233 float shoot_volume = plant_instances.at(plantID).shoot_tree.at(shoot->ID)->calculateShootInternodeVolume();
234 uint parentID = shoot->parent_shoot_ID;
235 float shoot_growth;
236
237 float phytomer_shoot_volume = 0;
238
239 for (int p = 0; p < shoot->phytomers.size(); p++) {
240 float phytomer_volume = plant_instances.at(plantID).shoot_tree.at(shoot->ID)->phytomers.at(p)->calculatePhytomerVolume(p);
241 float phytomer_age = plant_instances.at(plantID).shoot_tree.at(shoot->ID)->phytomers.at(p)->age;
242 float density_dynamic = phytomer_age / carbohydrate_params.maturity_age;
243 // Clamp dynamic carbon density between minimum value and density at full maturity
244 float rho_cw_dynamic = rho_cw * std::clamp(density_dynamic, carbohydrate_params.initial_density_ratio, 1.f); // Carbon density of the stem for the given phytomer (mol C / m^3 wood)
245 float phytomer_growth_carbon_demand = 0.f;
246 if (plant_instances.at(plantID).shoot_tree.at(shoot->ID)->old_shoot_volume > 0.f) {
247 phytomer_growth_carbon_demand = rho_cw_dynamic * (phytomer_volume - plant_instances.at(plantID).shoot_tree.at(shoot->ID)->phytomers.at(p)->old_phytomer_volume); // Structural carbon - mol C / m^3 wood
248 shoot->carbohydrate_pool_molC -= phytomer_growth_carbon_demand ; // Subtract construction carbon from the shoot's carbon pool
249 shoot->carbohydrate_pool_molC -= phytomer_growth_carbon_demand / carbohydrate_params.shoot_root_ratio; // Subtract construction carbon for the roots from the carbon pool
250 shoot_growth += (phytomer_growth_carbon_demand ) + (phytomer_growth_carbon_demand / carbohydrate_params.shoot_root_ratio);
251 }
252 phytomer_shoot_volume += phytomer_volume;
253
254 plant_instances.at(plantID).shoot_tree.at(shoot->ID)->phytomers.at(p)->old_phytomer_volume = phytomer_volume; // Update the old volume of the phytomer
255 }
256
257 // Update shoot's carbohydrate_concentration value (mol C / m^-3)
258 if (context_ptr->doesObjectExist(shoot->internode_tube_objID)) {
259 context_ptr->setObjectData(shoot->internode_tube_objID, "carbohydrate_concentration", shoot->carbohydrate_pool_molC / shoot_volume);
260 context_ptr->setObjectData(shoot->internode_tube_objID, "carbohydrate_pool_molC", shoot->carbohydrate_pool_molC);
261 context_ptr->setObjectData(shoot->internode_tube_objID, "daily_growth", shoot_growth);
262 }
263 }
264 }
265}
266
267
268float Phytomer::calculateFruitConstructionCosts(const FloralBud &fbud) const {
269 const CarbohydrateParameters &carbohydrate_params = plantarchitecture_ptr->plant_instances.at(this->plantID).carb_parameters;
270
271 float fruit_construction_cost_base = carbohydrate_params.fruit_density * carbohydrate_params.fruit_carbon_percentage / C_molecular_wt; // mol C/m^3
272
273 float fruit_carbon_cost = 0.f; // mol C
274
275 // fruit (cost per fruit basis)
276 for (uint fruit_objID: fbud.inflorescence_objIDs) {
277 float mature_volume = context_ptr->getPolymeshObjectVolume(fruit_objID) / fbud.current_fruit_scale_factor; // mature fruit volume
278 fruit_carbon_cost += fruit_construction_cost_base * (mature_volume) * (fbud.current_fruit_scale_factor - fbud.previous_fruit_scale_factor);
279 }
280
281 return fruit_carbon_cost;
282}
283
284void PlantArchitecture::checkCarbonPool_abortOrgans(float dt) {
285 for (auto &[plantID, plant_instance]: plant_instances) {
286 const auto shoot_tree = &plant_instance.shoot_tree;
287 const CarbohydrateParameters &carbohydrate_params = plant_instances.at(plantID).carb_parameters;
288
289 float fruit_construction_cost_base = carbohydrate_params.fruit_density * carbohydrate_params.fruit_carbon_percentage / C_molecular_wt; // mol C/m^3
290
291 for (const auto &shoot: *shoot_tree) {
292 uint shootID = shoot->ID;
293
294 float shoot_volume = plant_instances.at(plantID).shoot_tree.at(shootID)->calculateShootInternodeVolume();
295 // Establish a working carbon pool to see if you could stay above the carbon threshold by aborting fruits.
296 float working_carb_pool = shoot->carbohydrate_pool_molC;
297 if (dt > carbohydrate_params.bud_death_threshold_days) {
298 shoot->days_with_negative_carbon_balance += dt / 2; // Make sure you have at least two timesteps before starting to abort buds
299 } else {
300 shoot->days_with_negative_carbon_balance += dt;
301 }
302
303 // No need to mess with anything if you already have a carbon surplus
304 if (shoot->carbohydrate_pool_molC > carbohydrate_params.carbohydrate_abortion_threshold * carbohydrate_params.stem_density * shoot_volume / C_molecular_wt) {
305 shoot->days_with_negative_carbon_balance = 0;
306 goto shoot_balanced;
307 }
308 // Prevent any shoots from reaching negative carbon values: instant death
309 if (shoot->carbohydrate_pool_molC <= 0.f) {
310 pruneBranch(plantID, shootID, 0);
311 goto shoot_balanced;
312 }
313 if (shoot->sumShootLeafArea() == 0.f && shoot->sumChildVolume() == 0.f && shoot->isdormant == false) {
314 pruneBranch(plantID, shootID, 0);
315 goto shoot_balanced;
316 }
317 // Prune branches that can't stay above a sustainable threshold
318 if (shoot->carbohydrate_pool_molC < carbohydrate_params.carbohydrate_pruning_threshold * carbohydrate_params.stem_density * shoot_volume / C_molecular_wt &&
319 shoot->days_with_negative_carbon_balance > carbohydrate_params.branch_death_threshold_days) {
320 pruneBranch(plantID, shootID, 0);
321 goto shoot_balanced;
322 }
323 // Keep track of how many days the shoot has been below the threshold
324 if (shoot->days_with_negative_carbon_balance <= carbohydrate_params.bud_death_threshold_days) {
325 continue;
326 }
327
328 // Loop over fruiting buds and abort them one at a time until you would be able to stay above the threshold
329 const auto phytomers = &shoot->phytomers;
330
331 bool living_buds = true;
332 while (living_buds) {
333 living_buds = false;
334
335 for (const auto &phytomer: *phytomers) {
336 bool next_phytomer = false;
337
338 for (auto &petiole: phytomer->floral_buds) {
339 if (next_phytomer) {
340 break;
341 }
342
343 bool next_petiole = false;
344 for (auto &fbud: petiole) {
345 if (next_petiole) {
346 break;
347 }
348 if (fbud.state == BUD_DORMANT || fbud.state == BUD_DEAD) {
349 continue;
350 }
351
352 for (uint fruit_objID: fbud.inflorescence_objIDs) {
353 float mature_volume = context_ptr->getPolymeshObjectVolume(fruit_objID) / fbud.current_fruit_scale_factor; // mature fruit volume
354 working_carb_pool += fruit_construction_cost_base * (mature_volume) * (fbud.current_fruit_scale_factor - fbud.previous_fruit_scale_factor);
355 }
356 phytomer->setFloralBudState(BUD_DEAD, fbud);
357 // Kill a floral bud to eliminate it as a future sink
358
359 if (working_carb_pool > carbohydrate_params.carbohydrate_abortion_threshold * carbohydrate_params.stem_density * shoot_volume / C_molecular_wt) {
360 goto shoot_balanced;
361 } // If the amount of carbon you've eliminated by aborting flower buds would have given you a positive carbon balance, move on to the next shoot
362
363 living_buds = true;
364 // There was at least one living bud, so stay in the loop until there aren't any more
365 next_petiole = true;
366 // As soon as you've eliminated one bud from a given petiole, move to the next one
367 next_phytomer = true;
368 // As soon as you've eliminated one bud from a given phytomer, move to the next one
369 }
370 }
371 }
372 }
373 }
374
375 shoot_balanced:; // empty statement after the label to avoid a compiler warning
376 }
377}
378
379
380void PlantArchitecture::checkCarbonPool_adjustPhyllochron(float dt) {
381 for (auto &[plantID, plant_instance]: plant_instances) {
382 const auto shoot_tree = &plant_instance.shoot_tree;
383 const CarbohydrateParameters &carbohydrate_params = plant_instances.at(plantID).carb_parameters;
384
385 for (const auto &shoot: *shoot_tree) {
386 uint shootID = shoot->ID;
387
388 float shoot_volume = plant_instances.at(plantID).shoot_tree.at(shootID)->calculateShootInternodeVolume();
389
390 float phyllochron_carbon_ratio = shoot->carbohydrate_pool_molC / (carbohydrate_params.carbohydrate_phyllochron_threshold * carbohydrate_params.stem_density * shoot_volume / C_molecular_wt);
391
392 shoot->phyllochron_instantaneous = std::fmax(shoot->shoot_parameters.phyllochron_min.val(), shoot->phyllochron_min / (phyllochron_carbon_ratio * dt));
393 }
394 }
395}
396
397void PlantArchitecture::checkCarbonPool_transferCarbon(float dt) {
398 for (auto &[plantID, plant_instance]: plant_instances) {
399 const auto shoot_tree = &plant_instance.shoot_tree;
400 const auto shoot_tree_ptr = &plant_instances.at(plantID).shoot_tree;
401 const CarbohydrateParameters &carbohydrate_params = plant_instances.at(plantID).carb_parameters;
402
403 // Transfer carbon from parent shoots to distal child shoots (gradient-driven flux)
404 for (const auto &shoot_inner: *shoot_tree) {
405 if (!shoot_inner) {
406 continue; // Skip null shoots
407 }
408 uint shootID_inner = shoot_inner->ID;
409 float shoot_volume_inner = plant_instances.at(plantID).shoot_tree.at(shootID_inner)->calculateShootInternodeVolume();
410 float shoot_carb_pool_molC_inner = shoot_inner->carbohydrate_pool_molC;
411 float shoot_carb_conc_inner = shoot_carb_pool_molC_inner / shoot_volume_inner;
412 if (shoot_inner->carbohydrate_pool_molC > carbohydrate_params.carbohydrate_transfer_threshold_up * shoot_volume_inner * carbohydrate_params.stem_density / C_molecular_wt) {
413 float totalChildVolume = shoot_inner->sumChildVolume(0);
414 if (totalChildVolume <= 0.0f) {
415 continue;
416 }
417 // Determine carbon pool (mol C) available for transfer from parent shoot.
418 float available_fraction_of_carb =
419 (shoot_inner->carbohydrate_pool_molC - carbohydrate_params.carbohydrate_transfer_threshold_up * shoot_volume_inner * carbohydrate_params.stem_density / C_molecular_wt) / shoot_inner->carbohydrate_pool_molC;
420
421 for (int p = 0; p < shoot_inner->phytomers.size(); p++) {
422 // call recursively for child shoots
423 if (shoot_inner->childIDs.find(p) != shoot_inner->childIDs.end()) {
424 for (int child_shoot_ID: shoot_inner->childIDs.at(p)) {
425 float child_volume = plant_instances.at(plantID).shoot_tree.at(child_shoot_ID)->sumChildVolume(0) + plant_instances.at(plantID).shoot_tree.at(child_shoot_ID)->calculateShootInternodeVolume();
426 float child_ratio = child_volume / totalChildVolume;
427
428 float child_shoot_volume = plant_instances.at(plantID).shoot_tree.at(child_shoot_ID)->calculateShootInternodeVolume();
429
430 if (child_shoot_volume > 0.f) {
431 float child_shoot_carb_pool_molC = shoot_tree_ptr->at(child_shoot_ID)->carbohydrate_pool_molC;
432 float child_shoot_carb_conc = child_shoot_carb_pool_molC / child_shoot_volume;
433 // Only tranfer carbon if the parent shoot has greater carbon concentration than the child shoot.
434 if (shoot_carb_conc_inner > child_shoot_carb_conc) {
435 float transfer_volume = shoot_volume_inner;
436 if (child_shoot_volume < shoot_volume_inner) {
437 transfer_volume = child_shoot_volume;
438 }
439 float delta_C = shoot_carb_conc_inner - child_shoot_carb_conc;
440 float transfer_mol_C_demand = delta_C * child_ratio * transfer_volume * carbohydrate_params.carbon_conductance_up * dt;
441
442 float transfer_mol_C = std::clamp(transfer_mol_C_demand, 0.f, shoot_inner->carbohydrate_pool_molC * available_fraction_of_carb * child_ratio);
443
444 // Mass-balance transfer of carbon between shoots
445 plant_instances.at(plantID).shoot_tree.at(child_shoot_ID)->carbohydrate_pool_molC += transfer_mol_C;
446 shoot_inner->carbohydrate_pool_molC -= transfer_mol_C;
447 }
448 }
449 }
450 }
451 }
452 }
453 }
454
455 // Transfer carbon from distal child shoots to parent shoots (gradient-driven flux)
456 for (const auto &shoot: *shoot_tree) {
457 if (!shoot) {
458 continue; // Skip null shoots
459 }
460 uint shootID = shoot->ID;
461 uint parentID = shoot->parent_shoot_ID;
462
463 float shoot_volume = plant_instances.at(plantID).shoot_tree.at(shootID)->calculateShootInternodeVolume();
464
465 if (shoot_volume <= 0.0f) {
466 continue;
467 }
468
469 float shoot_carb_pool_molC = shoot->carbohydrate_pool_molC;
470 float shoot_carb_conc = shoot_carb_pool_molC / shoot_volume;
471
472 // Only transfer carbon if child shoot has greater carbon concentration than parent
473 if (shoot_carb_pool_molC > carbohydrate_params.carbohydrate_transfer_threshold_down * shoot_volume * carbohydrate_params.stem_density / C_molecular_wt) {
474 float available_fraction_of_carb = (shoot->carbohydrate_pool_molC - carbohydrate_params.carbohydrate_transfer_threshold_down * shoot_volume * carbohydrate_params.stem_density / C_molecular_wt) / shoot->carbohydrate_pool_molC;
475 if (parentID < 10000000) {
476 float parent_shoot_volume = plant_instances.at(plantID).shoot_tree.at(parentID)->calculateShootInternodeVolume();
477
478 float parent_shoot_carb_pool_molC = plant_instances.at(plantID).shoot_tree.at(parentID)->carbohydrate_pool_molC;
479 float parent_shoot_carb_conc = parent_shoot_carb_pool_molC / parent_shoot_volume;
480
481 float delta_C = shoot_carb_conc - parent_shoot_carb_conc;
482 float transfer_mol_C_demand = delta_C * shoot_volume * carbohydrate_params.carbon_conductance_down * dt;
483 // Ensure only available carbon is transferred
484 float transfer_mol_C = std::clamp(transfer_mol_C_demand, 0.f, shoot->carbohydrate_pool_molC * available_fraction_of_carb);
485 // Mass balance transfer of C (mol) from child to parent shoot
486 plant_instances.at(plantID).shoot_tree.at(parentID)->carbohydrate_pool_molC += transfer_mol_C;
487 shoot->carbohydrate_pool_molC -= transfer_mol_C;
488 }
489 }
490 }
491 }
492}
493
494
495void PlantArchitecture::incrementPhytomerInternodeGirth_carb(uint plantID, uint shootID, uint node_number, float dt, bool update_context_geometry) {
496 // Slow Radial growth of shoots if their carbon concentration falls below a sustainable threshold
497 const CarbohydrateParameters &carbohydrate_params = plant_instances.at(plantID).carb_parameters;
498
499 if (plant_instances.find(plantID) == plant_instances.end()) {
500 helios_runtime_error("ERROR (PlantArchitecture::incrementPhytomerInternodeGirth): Plant with ID of " + std::to_string(plantID) + " does not exist.");
501 }
502
503 auto shoot = plant_instances.at(plantID).shoot_tree.at(shootID);
504
505 if (shootID >= plant_instances.at(plantID).shoot_tree.size()) {
506 helios_runtime_error("ERROR (PlantArchitecture::incrementPhytomerInternodeGirth): Shoot with ID of " + std::to_string(shootID) + " does not exist.");
507 } else if (node_number >= shoot->current_node_number) {
508 helios_runtime_error("ERROR (PlantArchitecture::incrementPhytomerInternodeGirth): Cannot scale internode " + std::to_string(node_number) + " because there are only " + std::to_string(shoot->current_node_number) + " nodes in this shoot.");
509 }
510
511 auto phytomer = shoot->phytomers.at(node_number);
512
513 // float leaf_area = phytomer->calculateDownstreamLeafArea();
514 float leaf_area = phytomer->downstream_leaf_area;
515 if (context_ptr->doesObjectExist(shoot->internode_tube_objID)) {
516 context_ptr->setObjectData(shoot->internode_tube_objID, "leaf_area", leaf_area);
517 }
518
519 float phytomer_age = phytomer->age;
520 float girth_area_factor = shoot->shoot_parameters.girth_area_factor.val();
521 if (phytomer_age > 365) {
522 girth_area_factor = shoot->shoot_parameters.girth_area_factor.val() * 365 / phytomer_age;
523 }
524
525
526 float internode_area = girth_area_factor * leaf_area * 1e-4;
527
528 float phytomer_radius = sqrtf(internode_area / PI_F);
529
530 float rho_cw = carbohydrate_params.stem_density * carbohydrate_params.stem_structural_carbon_percentage / C_molecular_wt; // Density of carbon in almond wood (mol C m^-3)
531 float max_shoot_volume = internode_area * shoot->calculateShootLength();
532 float current_shoot_volume = shoot->calculateShootInternodeVolume();
533 float max_carbon_demand = (max_shoot_volume - current_shoot_volume) * rho_cw; //(mol C)
534
535 float threshold_carbon_pool = carbohydrate_params.carbohydrate_growth_threshold * current_shoot_volume * carbohydrate_params.stem_density / C_molecular_wt; //(mol C)
536
537 auto &segment = shoot->shoot_internode_radii.at(node_number);
538 for (float &radius: segment) {
539 if (phytomer_radius > radius) { // radius should only increase
540 float carbon_availability_ratio = std::clamp((shoot->carbohydrate_pool_molC - threshold_carbon_pool) / max_carbon_demand, .05f, 1.f);
541 radius = radius + carbon_availability_ratio * 0.5 * (phytomer_radius - radius);
542 }
543
544
545 if (update_context_geometry && context_ptr->doesObjectExist(shoot->internode_tube_objID)) {
546 context_ptr->setTubeRadii(shoot->internode_tube_objID, flatten(shoot->shoot_internode_radii));
547 }
548 }
549}
550
551
552bool Shoot::sampleVegetativeBudBreak_carb(uint node_index) const {
553 const CarbohydrateParameters &carbohydrate_params = plantarchitecture_ptr->plant_instances.at(plantID).carb_parameters;
554 float shoot_volume = calculateShootInternodeVolume();
555
556 if (node_index >= phytomers.size()) {
557 helios_runtime_error("ERROR (PlantArchitecture::sampleVegetativeBudBreak): Invalid node index. Node index must be less than the number of phytomers on the shoot.");
558 }
559
560 float probability_min = plantarchitecture_ptr->plant_instances.at(this->plantID).shoot_types_snapshot.at(this->shoot_type_label).vegetative_bud_break_probability_min.val();
561 float probability_max = 1.f;
562 float probability_decay = plantarchitecture_ptr->plant_instances.at(this->plantID).shoot_types_snapshot.at(this->shoot_type_label).vegetative_bud_break_probability_decay_rate.val();
563
564 if (carbohydrate_pool_molC < carbohydrate_params.carbohydrate_vegetative_break_threshold * shoot_volume * carbohydrate_params.stem_density / C_molecular_wt) {
565 probability_max = carbohydrate_pool_molC / (carbohydrate_params.carbohydrate_vegetative_break_threshold * shoot_volume * carbohydrate_params.stem_density / C_molecular_wt);
566 }
567
568 float bud_break_probability;
569 if (!shoot_parameters.growth_requires_dormancy && probability_decay < 0.f) {
570 bud_break_probability = probability_min;
571 } else if (probability_decay > 0.f) { // probability maximum at apex
572 bud_break_probability = std::fmax(probability_min, probability_max - probability_decay * float(this->current_node_number - node_index - 1));
573 } else if (probability_decay < 0.f) { // probability maximum at base
574 bud_break_probability = std::fmax(probability_min, probability_max - fabs(probability_decay) * float(node_index));
575 } else {
576 if (probability_decay == 0.f) {
577 bud_break_probability = probability_min;
578 } else {
579 bud_break_probability = probability_max;
580 }
581 }
582
583 bool bud_break = true;
584 if (context_ptr->randu() > bud_break_probability) {
585 bud_break = false;
586 }
587
588 return bud_break;
589}