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