1.3.64
 
Loading...
Searching...
No Matches
NitrogenModel.cpp
Go to the documentation of this file.
1
16#include "PlantArchitecture.h"
17
18using namespace helios;
19
20// ==================== Enable/Disable Methods ==================== //
21
23 nitrogen_model_enabled = true;
24}
25
27 nitrogen_model_enabled = false;
28}
29
31 return nitrogen_model_enabled;
32}
33
34// ==================== Parameter Setters ==================== //
35
37 if (plant_instances.find(plantID) == plant_instances.end()) {
38 helios_runtime_error("ERROR (PlantArchitecture::setPlantNitrogenParameters): Plant ID " + std::to_string(plantID) + " does not exist.");
39 }
40 plant_instances.at(plantID).nitrogen_parameters = params;
41}
42
43void PlantArchitecture::setPlantNitrogenParameters(const std::vector<uint> &plantIDs, const NitrogenParameters &params) {
44 for (uint plantID: plantIDs) {
45 setPlantNitrogenParameters(plantID, params);
46 }
47}
48
49// ==================== Initialization Methods ==================== //
50
51void PlantArchitecture::initializeNitrogenPools(float initial_leaf_N_area) {
52 for (auto &[plantID, plant_instance]: plant_instances) {
53 initializePlantNitrogenPools(plantID, initial_leaf_N_area);
54 }
55}
56
57void PlantArchitecture::initializePlantNitrogenPools(uint plantID, float initial_leaf_N_area) {
58 // Validate inputs
59 if (plant_instances.find(plantID) == plant_instances.end()) {
60 helios_runtime_error("ERROR (PlantArchitecture::initializePlantNitrogenPools): Plant with ID of " + std::to_string(plantID) + " does not exist.");
61 }
62 if (initial_leaf_N_area < 0) {
63 helios_runtime_error("ERROR (PlantArchitecture::initializePlantNitrogenPools): Initial leaf N content per area must be >= 0.");
64 }
65
66 PlantInstance &plant = plant_instances.at(plantID);
67
68 // Initialize plant-level pools to zero
69 plant.root_nitrogen_pool_gN = 0;
71 plant.cumulative_N_uptake_gN = 0;
72
73 // Initialize per-leaf nitrogen content (area basis) based on current leaf areas
74 for (auto &shoot: plant.shoot_tree) {
75 // Clear existing leaf N pools for this shoot
76 shoot->leaf_nitrogen_gN_m2.clear();
77
78 for (auto &phytomer: shoot->phytomers) {
79 // Iterate through all leaves in this phytomer (2D vector structure)
80 for (uint petiole_idx = 0; petiole_idx < phytomer->leaf_objIDs.size(); petiole_idx++) {
81 for (uint leaf_idx = 0; leaf_idx < phytomer->leaf_objIDs.at(petiole_idx).size(); leaf_idx++) {
82 uint leaf_objID = phytomer->leaf_objIDs.at(petiole_idx).at(leaf_idx);
83
84 if (!context_ptr->doesObjectExist(leaf_objID)) {
85 continue;
86 }
87
88 // Get current leaf area
89 float leaf_area = context_ptr->getObjectArea(leaf_objID);
90
91 // Initialize leaf N content per area (g N/m²)
92 shoot->leaf_nitrogen_gN_m2[leaf_objID] = initial_leaf_N_area;
93
94 // Track total N added (for cumulative uptake)
95 float total_N_added = initial_leaf_N_area * leaf_area;
96 plant.cumulative_N_uptake_gN += total_N_added;
97 }
98 }
99 }
100 }
101}
102
103// ==================== Nitrogen Application ==================== //
104
105void PlantArchitecture::addPlantNitrogen(uint plantID, float amount_gN) {
106 if (plant_instances.find(plantID) == plant_instances.end()) {
107 helios_runtime_error("ERROR (PlantArchitecture::addPlantNitrogen): Plant ID " + std::to_string(plantID) + " does not exist.");
108 }
109 if (amount_gN < 0) {
110 helios_runtime_error("ERROR (PlantArchitecture::addPlantNitrogen): Nitrogen amount must be >= 0.");
111 }
112
113 PlantInstance &plant = plant_instances.at(plantID);
114 const NitrogenParameters &N_params = plant.nitrogen_parameters;
115
116 // Split between root and available pools
117 float root_N = amount_gN * N_params.root_allocation_fraction;
118 float available_N = amount_gN * (1.0f - N_params.root_allocation_fraction);
119
120 plant.root_nitrogen_pool_gN += root_N;
121 plant.available_nitrogen_pool_gN += available_N;
122 plant.cumulative_N_uptake_gN += amount_gN;
123}
124
125void PlantArchitecture::addPlantNitrogen(const std::vector<uint> &plantIDs, float amount_gN) {
126 for (uint plantID: plantIDs) {
127 addPlantNitrogen(plantID, amount_gN);
128 }
129}
130
131// ==================== Leaf Nitrogen Accumulation (Rate-Limited, Area Basis) ==================== //
132
133void PlantArchitecture::accumulateLeafNitrogen(float dt) {
134 for (auto &[plantID, plant]: plant_instances) {
135 const NitrogenParameters &N_params = plant.nitrogen_parameters;
136 bool has_available_nitrogen = (plant.available_nitrogen_pool_gN > 0);
137
138 // Iterate through all shoots and their leaves
139 for (auto &shoot: plant.shoot_tree) {
140 for (auto &phytomer: shoot->phytomers) {
141 // Iterate through all leaves in this phytomer (2D vector structure)
142 for (uint petiole_idx = 0; petiole_idx < phytomer->leaf_objIDs.size(); petiole_idx++) {
143 for (uint leaf_idx = 0; leaf_idx < phytomer->leaf_objIDs.at(petiole_idx).size(); leaf_idx++) {
144 uint leaf_objID = phytomer->leaf_objIDs.at(petiole_idx).at(leaf_idx);
145
146 if (!context_ptr->doesObjectExist(leaf_objID)) {
147 continue;
148 }
149
150 // Get current leaf N content per area (g N/m²)
151 // Initialize in map first so all leaves get tracked (even when no N available)
152 float current_N_area = 0;
153 if (shoot->leaf_nitrogen_gN_m2.count(leaf_objID)) {
154 current_N_area = shoot->leaf_nitrogen_gN_m2.at(leaf_objID);
155 } else {
156 // Initialize if not present
157 shoot->leaf_nitrogen_gN_m2[leaf_objID] = 0;
158 }
159
160 // Skip accumulation if no nitrogen available
161 if (!has_available_nitrogen) {
162 continue;
163 }
164
165 // Get current leaf area
166 float leaf_area = context_ptr->getObjectArea(leaf_objID);
167 if (leaf_area <= 0) {
168 continue; // Skip nitrogen accumulation for leaves with no area (prevents division by zero)
169 }
170
171 // Calculate N content per area demand (g N/m²)
172 float N_area_demand = std::max(0.0f, N_params.target_leaf_N_area - current_N_area);
173 if (N_area_demand <= 0) {
174 continue; // Leaf is at target
175 }
176
177 // Rate limiting: max N per m² per day
178 float max_N_area_this_step = N_params.max_N_accumulation_rate * dt;
179
180 // Calculate total N demand for this leaf (g N)
181 float total_N_demand = std::min(N_area_demand, max_N_area_this_step) * leaf_area;
182
183 // Also limited by available N pool
184 float N_to_add = std::min(total_N_demand, plant.available_nitrogen_pool_gN);
185
186 // Update leaf N content per area (g N/m²)
187 float N_area_to_add = N_to_add / leaf_area;
188 shoot->leaf_nitrogen_gN_m2[leaf_objID] += N_area_to_add;
189
190 // Update available pool
191 plant.available_nitrogen_pool_gN -= N_to_add;
192
193 if (plant.available_nitrogen_pool_gN <= 0) {
194 has_available_nitrogen = false; // Continue iterating to track all leaves, but skip accumulation
195 }
196 }
197 }
198 }
199 }
200 }
201}
202
203// ==================== Nitrogen Remobilization (Area Basis) ==================== //
204
205void PlantArchitecture::remobilizeNitrogen(float dt) {
206 for (auto &[plantID, plant]: plant_instances) {
207 const NitrogenParameters &N_params = plant.nitrogen_parameters;
208
209 // Calculate current N stress to determine remobilization intensity
210 float total_N_area = 0; // Sum of N_area across all leaves
211 float total_area = 0; // Total leaf area
212 int num_leaves = 0;
213
214 for (auto &shoot: plant.shoot_tree) {
215 for (auto &[leaf_objID, leaf_N_area]: shoot->leaf_nitrogen_gN_m2) {
216 if (!context_ptr->doesObjectExist(leaf_objID)) {
217 continue;
218 }
219 float leaf_area = context_ptr->getObjectArea(leaf_objID);
220 total_N_area += leaf_N_area * leaf_area; // g N
221 total_area += leaf_area; // m²
222 num_leaves++;
223 }
224 }
225
226 if (num_leaves == 0 || total_area == 0) {
227 continue; // No leaves
228 }
229
230 float avg_N_area = total_N_area / total_area; // Average g N/m²
231 float N_stress_factor = std::min(1.0f, avg_N_area / N_params.target_leaf_N_area);
232
233 // Classify leaves by age
234 std::vector<std::pair<uint, uint>> source_leaves; // (shoot_idx, leaf_objID)
235 std::vector<std::pair<uint, uint>> sink_leaves; // (shoot_idx, leaf_objID)
236 std::map<uint, float> source_N_available; // leaf_objID → available N (g N)
237 std::map<uint, float> sink_N_demand; // leaf_objID → N demand (g N)
238 float total_source_N_available = 0;
239 float total_sink_demand = 0;
240
241 uint shoot_idx = 0;
242 for (auto &shoot: plant.shoot_tree) {
243 for (auto &phytomer: shoot->phytomers) {
244 // Get leaf lifespan from plant instance
245 float leaf_lifespan = plant.max_leaf_lifespan;
246 if (leaf_lifespan <= 0) {
247 leaf_lifespan = 1e6; // Evergreen
248 }
249
250 float leaf_age = phytomer->age;
251 float age_fraction = leaf_age / leaf_lifespan;
252
253 // Iterate through all leaves in this phytomer
254 for (uint petiole_idx = 0; petiole_idx < phytomer->leaf_objIDs.size(); petiole_idx++) {
255 for (uint leaf_idx = 0; leaf_idx < phytomer->leaf_objIDs.at(petiole_idx).size(); leaf_idx++) {
256 uint leaf_objID = phytomer->leaf_objIDs.at(petiole_idx).at(leaf_idx);
257
258 if (!context_ptr->doesObjectExist(leaf_objID)) {
259 continue;
260 }
261
262 if (!shoot->leaf_nitrogen_gN_m2.count(leaf_objID)) {
263 continue;
264 }
265
266 float leaf_area = context_ptr->getObjectArea(leaf_objID);
267 float current_N_area = shoot->leaf_nitrogen_gN_m2.at(leaf_objID); // g N/m²
268
269 // Classify by age
270 if (age_fraction >= N_params.remobilization_age_threshold) {
271 // OLD LEAF: Source for remobilization (>70% of lifespan)
272
273 // Calculate remobilizable N per area (g N/m²)
274 float remobilizable_N_area = std::max(0.0f, (current_N_area - N_params.minimum_leaf_N_area) * N_params.leaf_remobilization_efficiency);
275
276 // Accelerate under N stress (up to 1.3x faster)
277 if (N_stress_factor < 1.0f) {
278 remobilizable_N_area *= (1.0f + 0.3f * (1.0f - N_stress_factor));
279 }
280
281 // Convert to total N available (g N)
282 float remobilizable_N_total = remobilizable_N_area * leaf_area;
283
284 if (remobilizable_N_total > 0) {
285 source_leaves.push_back({shoot_idx, leaf_objID});
286 source_N_available[leaf_objID] = remobilizable_N_total;
287 total_source_N_available += remobilizable_N_total;
288 }
289
290 } else if (age_fraction < 0.5f) {
291 // YOUNG LEAF: Sink for remobilized N (<50% of lifespan)
292
293 // Calculate N demand per area (g N/m²)
294 float N_area_demand = std::max(0.0f, N_params.target_leaf_N_area - current_N_area);
295
296 // Convert to total N demand (g N)
297 float N_total_demand = N_area_demand * leaf_area;
298
299 if (N_total_demand > 0) {
300 sink_leaves.push_back({shoot_idx, leaf_objID});
301 sink_N_demand[leaf_objID] = N_total_demand;
302 total_sink_demand += N_total_demand;
303 }
304 }
305 }
306 }
307 }
308 shoot_idx++;
309 }
310
311 // Remobilize N from old to young leaves
312 if (total_source_N_available > 0 && total_sink_demand > 0) {
313 float N_to_remobilize = std::min(total_source_N_available, total_sink_demand);
314
315 // Remove from source leaves proportionally
316 for (auto &[shoot_idx, source_objID]: source_leaves) {
317 float leaf_contribution = source_N_available[source_objID] / total_source_N_available;
318 float N_removed_total = N_to_remobilize * leaf_contribution; // g N
319
320 // Convert to per-area and update
321 float source_leaf_area = context_ptr->getObjectArea(source_objID);
322 if (source_leaf_area <= 0) {
323 continue; // Skip leaves with no area (prevents division by zero)
324 }
325 float N_removed_area = N_removed_total / source_leaf_area; // g N/m²
326 plant.shoot_tree.at(shoot_idx)->leaf_nitrogen_gN_m2[source_objID] -= N_removed_area;
327 }
328
329 // Add to sink leaves proportionally
330 for (auto &[shoot_idx, sink_objID]: sink_leaves) {
331 float leaf_demand_fraction = sink_N_demand[sink_objID] / total_sink_demand;
332 float N_added_total = N_to_remobilize * leaf_demand_fraction; // g N
333
334 // Convert to per-area and update
335 float sink_leaf_area = context_ptr->getObjectArea(sink_objID);
336 if (sink_leaf_area <= 0) {
337 continue; // Skip leaves with no area (prevents division by zero)
338 }
339 float N_added_area = N_added_total / sink_leaf_area; // g N/m²
340 plant.shoot_tree.at(shoot_idx)->leaf_nitrogen_gN_m2[sink_objID] += N_added_area;
341 }
342 }
343 }
344}
345
346// ==================== Nitrogen Stress Factor Calculation (Area Basis) ==================== //
347
348void PlantArchitecture::updateNitrogenStressFactor() {
349 for (auto &[plantID, plant]: plant_instances) {
350 const NitrogenParameters &N_params = plant.nitrogen_parameters;
351
352 // Calculate area-weighted average leaf N content per area across all leaves
353 float total_N = 0; // Total nitrogen (g N)
354 float total_area = 0; // Total leaf area (m²)
355 int num_leaves = 0;
356
357 for (auto &shoot: plant.shoot_tree) {
358 for (auto &phytomer: shoot->phytomers) {
359 for (uint petiole_idx = 0; petiole_idx < phytomer->leaf_objIDs.size(); petiole_idx++) {
360 for (uint leaf_idx = 0; leaf_idx < phytomer->leaf_objIDs.at(petiole_idx).size(); leaf_idx++) {
361 uint leaf_objID = phytomer->leaf_objIDs.at(petiole_idx).at(leaf_idx);
362
363 if (!context_ptr->doesObjectExist(leaf_objID)) {
364 continue;
365 }
366
367 if (!shoot->leaf_nitrogen_gN_m2.count(leaf_objID)) {
368 continue;
369 }
370
371 float leaf_N_area = shoot->leaf_nitrogen_gN_m2.at(leaf_objID); // g N/m²
372 float leaf_area = context_ptr->getObjectArea(leaf_objID); // m²
373
374 // Write leaf nitrogen per area to object data for visualization
375 context_ptr->setObjectData(leaf_objID, "leaf_nitrogen_gN_m2", leaf_N_area);
376
377 total_N += leaf_N_area * leaf_area; // g N
378 total_area += leaf_area; // m²
379 num_leaves++;
380 }
381 }
382 }
383 }
384
385 if (num_leaves == 0 || total_area == 0) {
386 continue; // No leaves to evaluate
387 }
388
389 // Calculate average leaf N content per area (g N/m²)
390 float avg_leaf_N_area = total_N / total_area;
391
392 // Calculate stress factor: simple ratio
393 float stress_factor = std::min(1.0f, avg_leaf_N_area / N_params.target_leaf_N_area);
394
395 // Clamp to [0, 1]
396 stress_factor = std::clamp(stress_factor, 0.0f, 1.0f);
397
398 // Write SINGLE output to plant object data
399 std::vector<uint> plant_objIDs = getAllPlantObjectIDs(plantID);
400 if (!plant_objIDs.empty()) {
401 context_ptr->setObjectData(plant_objIDs, "nitrogen_stress_factor", stress_factor);
402 }
403 }
404}
405
406// ==================== Fruit Nitrogen Removal (Area Basis) ==================== //
407
408void PlantArchitecture::removeFruitNitrogen() {
409 for (auto &[plantID, plant]: plant_instances) {
410 const NitrogenParameters &N_params = plant.nitrogen_parameters;
411
412 // Iterate through shoots to find fruit
413 for (auto &shoot: plant.shoot_tree) {
414 for (auto &phytomer: shoot->phytomers) {
415 // Check each floral bud
416 for (auto &petiole: phytomer->floral_buds) {
417 for (auto &fbud: petiole) {
418 if (fbud.state != BUD_FRUITING) {
419 continue; // Not a fruit
420 }
421
422 // Check if fruit grew this timestep
423 float scale_increment = fbud.current_fruit_scale_factor - fbud.previous_fruit_scale_factor;
424 if (scale_increment <= 0) {
425 continue; // No growth
426 }
427
428 // Calculate fruit area increment
429 float fruit_area_increment = 0;
430 for (uint fruit_objID: fbud.inflorescence_objIDs) {
431 if (!context_ptr->doesObjectExist(fruit_objID)) {
432 continue;
433 }
434
435 // Get current fruit area
436 float current_fruit_area = context_ptr->getObjectArea(fruit_objID);
437
438 // Calculate mature (unscaled) fruit area
439 float mature_fruit_area = current_fruit_area / (fbud.current_fruit_scale_factor * fbud.current_fruit_scale_factor);
440
441 // Calculate area increment (mature area × scale change × 2 for surface scaling)
442 float area_increment = mature_fruit_area * (fbud.current_fruit_scale_factor * fbud.current_fruit_scale_factor - fbud.previous_fruit_scale_factor * fbud.previous_fruit_scale_factor);
443
444 fruit_area_increment += area_increment;
445 }
446
447 if (fruit_area_increment > 0) {
448 // Calculate N demand for fruit growth (g N/m² × m² = g N)
449 float fruit_N_demand = fruit_area_increment * N_params.fruit_N_area;
450
451 // Deduct from available pool (supply-limited)
452 float N_removed = std::min(fruit_N_demand, plant.available_nitrogen_pool_gN);
453 plant.available_nitrogen_pool_gN -= N_removed;
454
455 // Safety clamp
456 if (plant.available_nitrogen_pool_gN < 0) {
457 plant.available_nitrogen_pool_gN = 0;
458 }
459 }
460 }
461 }
462 }
463 }
464 }
465}