1.3.82
 
Loading...
Searching...
No Matches
LeafOptics.cpp
Go to the documentation of this file.
1
16#include "LeafOptics.h"
17using namespace std;
18using namespace helios;
19
21
22 context = a_context; // just copying the pointer to the context
23
24 // Load leaf refraction index and specific absorption coefficients (400,401,...2500 nm)
25 context->loadXML("plugins/leafoptics/spectral_data/prospect_spectral_library.xml", true);
26
27 // Load leaf refraction index - refractiveindex (n)
28 std::vector<helios::vec2> data;
29 if (!context->doesGlobalDataExist("refraction_index")) {
30 helios_runtime_error("Refraction index data was not loaded properly from the prospect_spectral_library.xml file.");
31 }
32 context->getGlobalData("refraction_index", data);
33 if (data.size() != nw) {
34 helios_runtime_error("Size of refraction index data loaded from the prospect_spectral_library.xml file was not correct.");
35 }
36 refractiveindex.resize(nw);
37 wave_length.resize(nw);
38 for (int i = 0; i < nw; i++) {
39 refractiveindex.at(i) = data.at(i).y;
40 wave_length.at(i) = data.at(i).x;
41 }
42 // Load specific absorption coefficient (per elementary layer depth) for total chlorophyll - absorption_chlorophyll (cm^2/micro_g)
43 if (!context->doesGlobalDataExist("absorption_chlorophyll")) {
44 helios_runtime_error("Chlorophyll absorption spectral data was not loaded properly from the prospect_spectral_library.xml file.");
45 }
46 context->getGlobalData("absorption_chlorophyll", data);
47 if (data.size() != nw) {
48 helios_runtime_error("Size of chlorophyll absorption spectral data loaded from the prospect_spectral_library.xml file was not correct.");
49 }
50 absorption_chlorophyll.resize(nw);
51 for (int i = 0; i < nw; i++) {
52 absorption_chlorophyll.at(i) = data.at(i).y;
53 }
54
55 // Load specific absorption coefficient for total carotenoids - absorption_carotenoid (cm^2/micro_g)
56 if (!context->doesGlobalDataExist("absorption_carotenoid")) {
57 helios_runtime_error("Carotenoid absorption spectral data was not loaded properly from the prospect_spectral_library.xml file.");
58 }
59 context->getGlobalData("absorption_carotenoid", data);
60 if (data.size() != nw) {
61 helios_runtime_error("Size of carotenoid absorption spectral data loaded from the prospect_spectral_library.xml file was not correct.");
62 }
63 absorption_carotenoid.resize(nw);
64 for (int i = 0; i < nw; i++) {
65 absorption_carotenoid.at(i) = data.at(i).y;
66 }
67
68 // Load specific absorption coefficient for anthocyanins - sac_an (cm^2/micro_g)
69 if (!context->doesGlobalDataExist("absorption_anthocyanin")) {
70 helios_runtime_error("Anothocyanin absorption spectral data was not loaded properly from the prospect_spectral_library.xml file.");
71 }
72 context->getGlobalData("absorption_anthocyanin", data);
73 if (data.size() != nw) {
74 helios_runtime_error("Size of anthocyanin absorption spectral data loaded from the prospect_spectral_library.xml file was not correct.");
75 }
76 absorption_anthocyanin.resize(nw);
77 for (int i = 0; i < nw; i++) {
78 absorption_anthocyanin.at(i) = data.at(i).y;
79 }
80
81 // Load specific absorption coefficient for specific brown pigments (phenols during leaf death) - absorption_brown (arbitrary unit)
82 if (!context->doesGlobalDataExist("absorption_brown")) {
83 helios_runtime_error("Brown pigment absorption spectral data was not loaded properly from the prospect_spectral_library.xml file.");
84 }
85 context->getGlobalData("absorption_brown", data);
86 if (data.size() != nw) {
87 helios_runtime_error("Size of brown pigment absorption spectral data loaded from the prospect_spectral_library.xml file was not correct.");
88 }
89 absorption_brown.resize(nw);
90 for (int i = 0; i < nw; i++) {
91 absorption_brown.at(i) = data.at(i).y;
92 }
93
94 // Load specific absorption coefficient for mass of water per leaf area (EWT)- absorption_water (1/cm or cm^2/g)
95 if (!context->doesGlobalDataExist("absorption_water")) {
96 helios_runtime_error("Water absorption spectral data was not loaded properly from the prospect_spectral_library.xml file.");
97 }
98 context->getGlobalData("absorption_water", data);
99 if (data.size() != nw) {
100 helios_runtime_error("Size of water absorption spectral data loaded from the prospect_spectral_library.xml file was not correct.");
101 }
102 absorption_water.resize(nw);
103 for (int i = 0; i < nw; i++) {
104 absorption_water.at(i) = data.at(i).y;
105 }
106
107 // Load specific absorption coefficient for dry mass per leaf area (LMA)- absorption_drymass (cm^2/g)
108 if (!context->doesGlobalDataExist("absorption_drymass")) {
109 helios_runtime_error("Dry mass absorption spectral data was not loaded properly from the prospect_spectral_library.xml file.");
110 }
111 context->getGlobalData("absorption_drymass", data);
112 if (data.size() != nw) {
113 helios_runtime_error("Size of dry mass absorption spectral data loaded from the prospect_spectral_library.xml file was not correct.");
114 }
115 absorption_drymass.resize(nw);
116 for (int i = 0; i < nw; i++) {
117 absorption_drymass.at(i) = data.at(i).y;
118 }
119
120 // Load specific absorption coefficient for proteins- absorption_proteins (cm2.g-1)
121 if (!context->doesGlobalDataExist("absorption_proteins")) {
122 helios_runtime_error("Protein absorption spectral data was not loaded properly from the prospect_spectral_library.xml file.");
123 }
124 context->getGlobalData("absorption_proteins", data);
125 if (data.size() != nw) {
126 helios_runtime_error("Size of protein absorption spectral data loaded from the prospect_spectral_library.xml file was not correct.");
127 }
128 absorption_protein.resize(nw);
129 for (int i = 0; i < nw; i++) {
130 absorption_protein.at(i) = data.at(i).y;
131 }
132
133 // Load specific absorption coefficient for carbon based constituents- absorption_carbonconstituents (cm^2/g)
134 if (!context->doesGlobalDataExist("absorption_carbonconstituents")) {
135 helios_runtime_error("Carbon constituent absorption spectral data was not loaded properly from the prospect_spectral_library.xml file.");
136 }
137 context->getGlobalData("absorption_carbonconstituents", data);
138 if (data.size() != nw) {
139 helios_runtime_error("Size of carbon constituent absorption spectral data loaded from the prospect_spectral_library.xml file was not correct.");
140 }
141 absorption_carbonconstituents.resize(nw);
142 for (int i = 0; i < nw; i++) {
143 absorption_carbonconstituents.at(i) = data.at(i).y;
144 }
145
146 LeafOptics::surface(40.0, R_spec_normal);
147 // get surface (i.e. fresnel) reflectances for usage within Prospect function, relation from Stern
148 // 0..40° degrees range from the vertical = normal incidence on an average rough leaf
149
150 LeafOptics::surface(90.0, R_spec_diffuse);
151 // 0..90° degrees range from the vertical = diffuse incidence on a perfectly smooth leaf
152
153 // Initialize the species library with PROSPECT-D parameters from LOPEX93 dataset
154 initializeSpeciesLibrary();
155}
156
157void LeafOptics::initializeSpeciesLibrary() {
158 // PROSPECT-D parameters fitted to LOPEX93 spectral library samples
159 // All species use PROSPECT-D mode (drymass > 0, protein = 0, carbonconstituents = 0)
160 // Fitted using fit_prospect_visrobust.py with --no-calib flag
161
163
164 // Default species (original Helios default values)
165 props.numberlayers = 1.5f;
166 props.chlorophyllcontent = 30.0f;
167 props.carotenoidcontent = 7.0f;
168 props.anthocyancontent = 1.0f;
169 props.brownpigments = 0.0f;
170 props.watermass = 0.015f;
171 props.drymass = 0.09f;
172 props.protein = 0.0f;
173 props.carbonconstituents = 0.0f;
174 species_library["default"] = props;
175
176 // Garden lettuce (Lactuca sativa L.) - LOPEX93 sample 0021
177 // RMSE: 0.008355
178 props.numberlayers = 2.00517f;
179 props.chlorophyllcontent = 30.2697f;
180 props.carotenoidcontent = 6.9869f;
181 props.anthocyancontent = 1.35975f;
182 props.brownpigments = 0.107067f;
183 props.watermass = 0.0281985f;
184 props.drymass = 0.0052668f;
185 props.protein = 0.0f;
186 props.carbonconstituents = 0.0f;
187 species_library["garden_lettuce"] = props;
188
189 // Alfalfa (Medicago sativa L.) - LOPEX93 sample 0036
190 // RMSE: 0.007743
191 props.numberlayers = 2.00758f;
192 props.chlorophyllcontent = 43.6375f;
193 props.carotenoidcontent = 10.3145f;
194 props.anthocyancontent = 1.33894f;
195 props.brownpigments = 0.0f;
196 props.watermass = 0.0189936f;
197 props.drymass = 0.00473702f;
198 props.protein = 0.0f;
199 props.carbonconstituents = 0.0f;
200 species_library["alfalfa"] = props;
201
202 // Corn (Zea mays L.) - LOPEX93 sample 0041
203 // RMSE: 0.015966
204 props.numberlayers = 1.59203f;
205 props.chlorophyllcontent = 22.8664f;
206 props.carotenoidcontent = 3.9745f;
207 props.anthocyancontent = 0.0f;
208 props.brownpigments = 0.72677f;
209 props.watermass = 0.0149645f;
210 props.drymass = 0.00441283f;
211 props.protein = 0.0f;
212 props.carbonconstituents = 0.0f;
213 species_library["corn"] = props;
214
215 // Sunflower (Helianthus annuus L.) - LOPEX93 sample 0081
216 // RMSE: 0.007353
217 props.numberlayers = 1.76358f;
218 props.chlorophyllcontent = 54.0514f;
219 props.carotenoidcontent = 12.9027f;
220 props.anthocyancontent = 1.75194f;
221 props.brownpigments = 0.0112026f;
222 props.watermass = 0.0185557f;
223 props.drymass = 0.00644855f;
224 props.protein = 0.0f;
225 props.carbonconstituents = 0.0f;
226 species_library["sunflower"] = props;
227
228 // English walnut (Juglans regia L.) - LOPEX93 sample 0091
229 // RMSE: 0.007828
230 props.numberlayers = 1.56274f;
231 props.chlorophyllcontent = 55.9211f;
232 props.carotenoidcontent = 12.4596f;
233 props.anthocyancontent = 1.73981f;
234 props.brownpigments = 0.0f;
235 props.watermass = 0.0127743f;
236 props.drymass = 0.00583351f;
237 props.protein = 0.0f;
238 props.carbonconstituents = 0.0f;
239 species_library["english_walnut"] = props;
240
241 // Rice (Oryza sativa L.) - LOPEX93 sample 0106
242 // RMSE: 0.004041
243 props.numberlayers = 1.67081f;
244 props.chlorophyllcontent = 37.233f;
245 props.carotenoidcontent = 9.98756f;
246 props.anthocyancontent = 0.0f;
247 props.brownpigments = 0.0275106f;
248 props.watermass = 0.0100962f;
249 props.drymass = 0.00484587f;
250 props.protein = 0.0f;
251 props.carbonconstituents = 0.0f;
252 species_library["rice"] = props;
253
254 // Soybean (Glycine max L.) - LOPEX93 sample 0116
255 // RMSE: 0.005875
256 props.numberlayers = 1.5375f;
257 props.chlorophyllcontent = 46.4121f;
258 props.carotenoidcontent = 12.1394f;
259 props.anthocyancontent = 0.648353f;
260 props.brownpigments = 0.0f;
261 props.watermass = 0.0101049f;
262 props.drymass = 0.00292814f;
263 props.protein = 0.0f;
264 props.carbonconstituents = 0.0f;
265 species_library["soybean"] = props;
266
267 // Wine grape (Vitis vinifera L.) - LOPEX93 sample 0276
268 // RMSE: 0.005585
269 props.numberlayers = 1.42673f;
270 props.chlorophyllcontent = 50.918f;
271 props.carotenoidcontent = 12.5466f;
272 props.anthocyancontent = 1.43905f;
273 props.brownpigments = 0.0798702f;
274 props.watermass = 0.010922f;
275 props.drymass = 0.00599315f;
276 props.protein = 0.0f;
277 props.carbonconstituents = 0.0f;
278 species_library["wine_grape"] = props;
279
280 // Tomato (Lycopersicum esculentum) - LOPEX93 sample 0316
281 // RMSE: 0.005524
282 props.numberlayers = 1.40304f;
283 props.chlorophyllcontent = 48.3467f;
284 props.carotenoidcontent = 11.604f;
285 props.anthocyancontent = 1.45113f;
286 props.brownpigments = 0.0f;
287 props.watermass = 0.0155627f;
288 props.drymass = 0.00261571f;
289 props.protein = 0.0f;
290 props.carbonconstituents = 0.0f;
291 species_library["tomato"] = props;
292
293 // Common bean (Phaseolus vulgaris L.) - GEMINI field experiments, day 35
294 // RMSE: 0.009479
295 props.numberlayers = 1.44041f;
296 props.chlorophyllcontent = 42.3619f;
297 props.carotenoidcontent = 15.6263f;
298 props.anthocyancontent = 0.844536f;
299 props.brownpigments = 0.0f;
300 props.watermass = 0.0150048f;
301 props.drymass = 0.00196285f;
302 props.protein = 0.0f;
303 props.carbonconstituents = 0.0f;
304 species_library["common_bean"] = props;
305
306 // Cowpea (Vigna unguiculata L.) - GEMINI field experiments, day 48
307 // RMSE: 0.010680
308 props.numberlayers = 1.22669f;
309 props.chlorophyllcontent = 61.5204f;
310 props.carotenoidcontent = 25.6171f;
311 props.anthocyancontent = 2.51899f;
312 props.brownpigments = 0.0f;
313 props.watermass = 0.0221158f;
314 props.drymass = 0.00108066f;
315 props.protein = 0.0f;
316 props.carbonconstituents = 0.0f;
317 species_library["cowpea"] = props;
318}
319
320void LeafOptics::run(const std::vector<uint> &UUIDs, const LeafOpticsProperties &leafproperties, const std::string &label) {
321 std::vector<vec2> reflectivities_fit;
322 std::vector<vec2> transmissivities_fit;
323 getLeafSpectra(leafproperties, reflectivities_fit, transmissivities_fit);
324
325 std::string leaf_reflectivity_label = "leaf_reflectivity_" + label;
326 std::string leaf_transmissivity_label = "leaf_transmissivity_" + label;
327 context->setGlobalData(leaf_reflectivity_label.c_str(), reflectivities_fit);
328 context->setGlobalData(leaf_transmissivity_label.c_str(), transmissivities_fit);
329
330 context->setPrimitiveData(UUIDs, "reflectivity_spectrum", leaf_reflectivity_label);
331 context->setPrimitiveData(UUIDs, "transmissivity_spectrum", leaf_transmissivity_label);
332 setProperties(UUIDs, leafproperties);
333
334 // Write Fluspect-B biochemistry parameters to global data under the same label
335 // so the radiation plugin's SIF pipeline can look them up per primitive via the
336 // "fluspect_spectrum" primitive-data key below. Field order is fixed by convention
337 // and documented in plugins/radiation/include/FluspectB.h. This write is harmless
338 // when SIF is not in use — it just sits as unused global data.
339 //
340 // Field order: Cab, Cca, Cw, Cdm, Cs, Cant, Cp, Cbc, N, V2Z, fqe (11 fields).
341 std::string fluspect_biochem_label = "fluspect_biochem_" + label;
342 std::vector<float> fluspect_biochem = {
343 leafproperties.chlorophyllcontent, // Cab
344 leafproperties.carotenoidcontent, // Cca
345 leafproperties.watermass, // Cw
346 leafproperties.drymass, // Cdm
347 leafproperties.brownpigments, // Cs
348 leafproperties.anthocyancontent, // Cant
349 leafproperties.protein, // Cp
350 leafproperties.carbonconstituents, // Cbc
351 leafproperties.numberlayers, // N
352 leafproperties.V2Z,
353 leafproperties.fqe,
354 };
355 context->setGlobalData(fluspect_biochem_label.c_str(), fluspect_biochem);
356 context->setPrimitiveData(UUIDs, "fluspect_spectrum", fluspect_biochem_label);
357
358 // Store parameters in map for later retrieval
359 spectrum_parameters_map[label] = leafproperties;
360}
361
362void LeafOptics::run(const LeafOpticsProperties &leafproperties, const std::string &label) {
363 std::vector<vec2> reflectivities_fit;
364 std::vector<vec2> transmissivities_fit;
365 getLeafSpectra(leafproperties, reflectivities_fit, transmissivities_fit);
366
367 std::string leaf_reflectivity_label = "leaf_reflectivity_" + label;
368 std::string leaf_transmissivity_label = "leaf_transmissivity_" + label;
369 context->setGlobalData(leaf_reflectivity_label.c_str(), reflectivities_fit);
370 context->setGlobalData(leaf_transmissivity_label.c_str(), transmissivities_fit);
371
372 // Fluspect-B biochemistry parameters, written to global data under a parallel label
373 // so the radiation plugin's SIF pipeline can look them up. See the UUID-specific
374 // overload of run() for the field-order convention.
375 std::string fluspect_biochem_label = "fluspect_biochem_" + label;
376 std::vector<float> fluspect_biochem = {
377 leafproperties.chlorophyllcontent, leafproperties.carotenoidcontent, leafproperties.watermass,
378 leafproperties.drymass, leafproperties.brownpigments, leafproperties.anthocyancontent,
379 leafproperties.protein, leafproperties.carbonconstituents, leafproperties.numberlayers,
380 leafproperties.V2Z, leafproperties.fqe,
381 };
382 context->setGlobalData(fluspect_biochem_label.c_str(), fluspect_biochem);
383
384 // Store parameters in map for later retrieval
385 spectrum_parameters_map[label] = leafproperties;
386}
387
388
389void LeafOptics::PROSPECT(float numberlayers, float Chlorophyllcontent, float carotenoidcontent, float anthocyancontent, float brownpigments, float watermass, float drymass, float protein, float carbonconstituents,
390 std::vector<float> &reflectivities_fit, std::vector<float> &transmissivities_fit)
391// Implementation of Prospect-PRO, port of public available matlab code
392{
393 double k;
394 double tau, ralf, r12, talf, t12, t21, r21, denom, Ta, Ra, t, r;
395
396 // PROSPECT() is a spectrum-computing entry point in its own right, so it must apply the
397 // same input validation as getLeafSpectra(). Validating through a LeafOpticsProperties
398 // struct keeps the rules in one place rather than duplicating them here.
399 LeafOpticsProperties validation_properties;
400 validation_properties.numberlayers = numberlayers;
401 validation_properties.chlorophyllcontent = Chlorophyllcontent;
402 validation_properties.carotenoidcontent = carotenoidcontent;
403 validation_properties.anthocyancontent = anthocyancontent;
404 validation_properties.brownpigments = brownpigments;
405 validation_properties.watermass = watermass;
406 validation_properties.drymass = drymass;
407 validation_properties.protein = protein;
408 validation_properties.carbonconstituents = carbonconstituents;
409 validateProperties(validation_properties);
410
411 // These are output arguments - overwrite rather than append so that repeated
412 // calls with the same vectors do not accumulate results.
413 reflectivities_fit.clear();
414 transmissivities_fit.clear();
415 reflectivities_fit.reserve(nw);
416 transmissivities_fit.reserve(nw);
417
418 // Loop over wavelength, might be a way to be vectorized at least partly
419 for (int i = 0; i < LeafOptics::nw; i++) {
420 // k: the mean absorption coefficient of each elementary layer.
421 k = (Chlorophyllcontent * absorption_chlorophyll.at(i) + carotenoidcontent * absorption_carotenoid.at(i) + anthocyancontent * absorption_anthocyanin.at(i) + brownpigments * absorption_brown.at(i) + watermass * absorption_water.at(i) +
422 drymass * absorption_drymass.at(i) + protein * absorption_protein.at(i) + carbonconstituents * absorption_carbonconstituents.at(i)) /
423 numberlayers;
424
425 // diffuse transmittance through elementary layer, this integral needs more effort in C++
426 tau = transmittance(k);
427 // surface reflectance at radiated leaf side for near normal incident beam radiation,
428 ralf = R_spec_normal.at(i); // calculate 1-tav in surface function
429 // surface reflectance at radiated leaf side for diffuse radiation
430 r12 = R_spec_diffuse.at(i);
431
432
433 talf = 1 - ralf; // tav90
434 t12 = 1 - r12; // tav
435
436 // transmittance and reflectance for leaf internal diffuse light
437 t21 = t12 / (refractiveindex.at(i) * refractiveindex.at(i)); // tav/n^2
438 r21 = 1 - t21;
439
440 // top or incident surface side
441
442 denom = 1 - r21 * r21 * tau * tau; // (euqation1 in RPOSPECT)
443 Ta = talf * tau * t21 / denom; // transmittance of top surface (euqation2 in RPOSPECT) taua
444 Ra = ralf + r21 * tau * Ta; // reflectance of top surface (euqation1 in RPOSPECT) rhoa
445
446 // bottom surface side
447
448 t = t12 * tau * t21 / denom; // (page 78 paragraph 1 in RPOSPECT) tau90
449 r = r12 + r21 * tau * t; // (page 78 paragraph 1 in RPOSPECT) rho90
450
451 // reflectance and transmittance of numberlayers layers, Stokes' solution
452 double D, rq, tq, a, b, bNm1, bN2, a2, Rsub, Tsub;
453 D = sqrt((1. + r + t) * (1. + r - t) * (1. - r + t) * (1. - r - t));
454 rq = r * r;
455 tq = t * t;
456 a = (1. + rq - tq + D) / (2 * r);
457 b = (1. - rq + tq + D) / (2 * t);
458 bNm1 = std::pow(b, (numberlayers - 1));
459 bN2 = bNm1 * bNm1;
460 a2 = a * a;
461 denom = a2 * bN2 - 1.;
462 Rsub = a * (bN2 - 1.) / denom;
463 Tsub = bNm1 * (a2 - 1.) / denom;
464
465 // Case of zero absorption
466 // The boundary r+t == 1 must be included: there D collapses to zero and the
467 // analytic Stokes solution above is degenerate.
468
469 if ((r + t) >= 1.0) {
470 Tsub = t / (t + (1. - t) * (numberlayers - 1));
471 Rsub = 1 - Tsub;
472 }
473
474 // Reflectance and transmittance of the leaf: combine top layer with next numberlayers-1 layers
475
476 denom = 1 - Rsub * r;
477 transmissivities_fit.push_back(Ta * Tsub / denom); //(euqation8 in RPOSPEC
478 reflectivities_fit.push_back(Ra + Ta * Rsub * t / denom); //(euqation7 in RPOSPECT)
479 }
480}
481
482void LeafOptics::validateProperties(const LeafOpticsProperties &leafproperties) {
483
484 // The structure parameter N counts the elementary layers making up the leaf.
485 // Values below 1 are unphysical: they make the Stokes exponent (N-1) negative,
486 // which inverts the layer stack, and N == 0 divides by zero when computing the
487 // mean absorption coefficient of each layer.
488 if (leafproperties.numberlayers < 1.f) {
490 "ERROR (LeafOptics): the leaf structure parameter 'numberlayers' (N) was " + std::to_string(leafproperties.numberlayers) +
491 ", but PROSPECT requires numberlayers >= 1 because it counts the number of elementary layers in the leaf. Set numberlayers to 1 for a single compact layer, or a larger value (typically 1.0-3.0) for a more scattering leaf.");
492 }
493
494 // All constituent contents are masses (or concentrations) per unit leaf area and
495 // cannot be negative. A negative value would contribute negative absorption.
496 auto check_non_negative = [](float value, const char *name, const char *units) {
497 if (value < 0.f) {
498 helios_runtime_error("ERROR (LeafOptics): the leaf constituent '" + std::string(name) + "' was " + std::to_string(value) + " " + units + ", but constituent contents cannot be negative. Set it to zero to disable this constituent.");
499 }
500 };
501
502 check_non_negative(leafproperties.chlorophyllcontent, "chlorophyllcontent", "ug/cm^2");
503 check_non_negative(leafproperties.carotenoidcontent, "carotenoidcontent", "ug/cm^2");
504 check_non_negative(leafproperties.anthocyancontent, "anthocyancontent", "ug/cm^2");
505 check_non_negative(leafproperties.brownpigments, "brownpigments", "(unitless)");
506 check_non_negative(leafproperties.watermass, "watermass", "g/cm^2");
507 check_non_negative(leafproperties.drymass, "drymass", "g/cm^2");
508 check_non_negative(leafproperties.protein, "protein", "g/cm^2");
509 check_non_negative(leafproperties.carbonconstituents, "carbonconstituents", "g/cm^2");
510}
511
512void LeafOptics::getLeafSpectra(const LeafOpticsProperties &leafproperties, std::vector<helios::vec2> &reflectivities_fit, std::vector<helios::vec2> &transmissivities_fit) {
513
514 std::vector<float> reflectivities_fit_y, transmissivities_fit_y;
515
516 validateProperties(leafproperties);
517
518 // These are output arguments - overwrite rather than append so that repeated
519 // calls with the same vectors do not accumulate results.
520 reflectivities_fit.clear();
521 transmissivities_fit.clear();
522 reflectivities_fit.reserve(nw);
523 transmissivities_fit.reserve(nw);
524
525 float numberlayers = leafproperties.numberlayers;
526 float chlorophyllcontent = leafproperties.chlorophyllcontent;
527 float carotenoidcontent = leafproperties.carotenoidcontent;
528 float anthocyancontent = leafproperties.anthocyancontent;
529 float brownpigments = leafproperties.brownpigments;
530 float watermass = leafproperties.watermass;
531 float drymass = leafproperties.drymass;
532 float protein = leafproperties.protein;
533 float carbonconstituents = leafproperties.carbonconstituents;
534
535 if (protein == 0 && carbonconstituents == 0) {
536 if (drymass == 0 && message_flag) {
537 std::cerr << "Warning: No leaf mass given" << std::endl;
538 }
539 } else {
540 drymass = 0;
541 }
542 PROSPECT(numberlayers, chlorophyllcontent, carotenoidcontent, anthocyancontent, brownpigments, watermass, drymass, protein, carbonconstituents, reflectivities_fit_y, transmissivities_fit_y);
543
544 // Convert float to vec2
545 for (int iwave = 0; iwave < nw; iwave++) {
546 reflectivities_fit.push_back(make_vec2(iwave + 400, reflectivities_fit_y.at(iwave)));
547 transmissivities_fit.push_back(make_vec2(iwave + 400, transmissivities_fit_y.at(iwave)));
548 }
549}
550
551void LeafOptics::surface(float degree, std::vector<float> &reflectivities)
554{
555
556 double rad2degree = 57.2958;
557 // tav is the transmissivity of a dielectric plane surface, averaged over all directions of incidence and over all polarizations.
558 double n2, np, nm, a, k, sinvalue, b1, b2, b, b3, a3, ts, tp1, tp2, tp3, tp4, tp5, tp, tav;
559 for (int i = 0; i < LeafOptics::nw; i++) {
560 double n = refractiveindex.at(i); // refractive index
561 n2 = n * n;
562 np = n2 + 1;
563 nm = n2 - 1;
564 a = (n + 1) * (n + 1) / 2;
565 k = -(n2 - 1) * (n2 - 1) / 4;
566 sinvalue = sin(degree / rad2degree);
567 if (degree < 90.0) {
568 b1 = sqrt((sinvalue * sinvalue - np / 2) * (sinvalue * sinvalue - np / 2) + k);
569 } else {
570 b1 = 0.0;
571 }
572 b2 = sinvalue * sinvalue - np / 2;
573 b = b1 - b2;
574 b3 = b * b * b;
575 a3 = a * a * a;
576 ts = (k * k / (6 * b3) + k / b - b / 2) - (k * k / (6 * a3) + k / a - a / 2);
577 tp1 = -2 * n2 * (b - a) / (np * np);
578 tp2 = -2 * n2 * np * log(b / a) / (nm * nm);
579 tp3 = n2 * (1 / b - 1 / a) / 2;
580 tp4 = 16 * n2 * n2 * (n2 * n2 + 1) * log((2 * np * b - nm * nm) / (2 * np * a - nm * nm)) / (np * np * np * nm * nm);
581 tp5 = 16 * n2 * n2 * n2 * (1 / (2 * np * b - nm * nm) - 1 / (2 * np * a - nm * nm)) / (np * np * np);
582 tp = tp1 + tp2 + tp3 + tp4 + tp5;
583 tav = (ts + tp) / (2 * sinvalue * sinvalue);
584 reflectivities.push_back(1.0 - tav);
585 }
586 return;
587}
588
589float LeafOptics::transmittance(double k) {
593 double xx, yy;
594 float tau;
600 if (k <= 0.0)
601 return 1.0;
602
603 if (k < 4.0) {
604 xx = 0.5 * k - 1.0;
605 yy = (((((((((((((((-3.60311230482612224e-13L * xx + 3.46348526554087424e-12L) * xx - 2.99627399604128973e-11L) * xx + 2.57747807106988589e-10L) * xx - 2.09330568435488303e-9L) * xx + 1.59501329936987818e-8L) * xx - 1.13717900285428895e-7L) *
606 xx +
607 7.55292885309152956e-7L) *
608 xx -
609 4.64980751480619431e-6L) *
610 xx +
611 2.63830365675408129e-5L) *
612 xx -
613 1.37089870978830576e-4L) *
614 xx +
615 6.47686503728103400e-4L) *
616 xx -
617 2.76060141343627983e-3L) *
618 xx +
619 1.05306034687449505e-2L) *
620 xx -
621 3.57191348753631956e-2L) *
622 xx +
623 1.07774527938978692e-1L) *
624 xx -
625 2.96997075145080963e-1L;
626 yy = (yy * xx + 8.64664716763387311e-1L) * xx + 7.42047691268006429e-1L;
627 yy = yy - log(k);
628 tau = (1.0 - k) * exp(-k) + k * k * yy;
629 return tau;
630 }
631 if (k < 85.0) {
632 xx = 14.5 / (k + 3.25) - 1.0;
633 yy = (((((((((((((((-1.62806570868460749e-12L * xx - 8.95400579318284288e-13L) * xx - 4.08352702838151578e-12L) * xx - 1.45132988248537498e-11L) * xx - 8.35086918940757852e-11L) * xx - 2.13638678953766289e-10L) * xx -
634 1.10302431467069770e-9L) *
635 xx -
636 3.67128915633455484e-9L) *
637 xx -
638 1.66980544304104726e-8L) *
639 xx -
640 6.11774386401295125e-8L) *
641 xx -
642 2.70306163610271497e-7L) *
643 xx -
644 1.05565006992891261e-6L) *
645 xx -
646 4.72090467203711484e-6L) *
647 xx -
648 1.95076375089955937e-5L) *
649 xx -
650 9.16450482931221453e-5L) *
651 xx -
652 4.05892130452128677e-4L) *
653 xx -
654 2.14213055000334718e-3L;
655 yy = ((yy * xx - 1.06374875116569657e-2L) * xx - 8.50699154984571871e-2L) * xx + 9.23755307807784058e-1L;
656 yy = exp(-k) * yy / k;
657 tau = (1.0 - k) * exp(-k) + k * k * yy;
658 return tau;
659 }
662 return 0.0;
663}
664
665void LeafOptics::setProperties(const std::vector<uint> &UUIDs, const LeafOpticsProperties &leafproperties) {
666 for (const auto &data: output_prim_data) {
667 if (data == "chlorophyll") {
668 context->setPrimitiveData(UUIDs, "chlorophyll", leafproperties.chlorophyllcontent);
669 } else if (data == "carotenoid") {
670 context->setPrimitiveData(UUIDs, "carotenoid", leafproperties.carotenoidcontent);
671 } else if (data == "anthocyanin") {
672 context->setPrimitiveData(UUIDs, "anthocyanin", leafproperties.anthocyancontent);
673 } else if (data == "brown" && leafproperties.brownpigments > 0.0) {
674 context->setPrimitiveData(UUIDs, "brown", leafproperties.brownpigments);
675 } else if (data == "water") {
676 context->setPrimitiveData(UUIDs, "water", leafproperties.watermass);
677 } else if (data == "drymass" && leafproperties.drymass > 0.0) {
678 context->setPrimitiveData(UUIDs, "drymass", leafproperties.drymass);
679 } else if (data == "protein" && leafproperties.drymass == 0.0) {
680 context->setPrimitiveData(UUIDs, "protein", leafproperties.protein);
681 } else if (data == "cellulose" && leafproperties.drymass == 0.0) {
682 context->setPrimitiveData(UUIDs, "cellulose", leafproperties.carbonconstituents);
683 }
684 }
685}
686
687void LeafOptics::getPropertiesFromSpectrum(const std::vector<uint> &UUIDs) {
688 const std::string prefix = "leaf_reflectivity_";
689
690 for (uint UUID: UUIDs) {
691 // Check if primitive has reflectivity_spectrum data
692 if (!context->doesPrimitiveDataExist(UUID, "reflectivity_spectrum")) {
693 continue; // Skip silently if no spectrum data
694 }
695
696 // Get the spectrum label
697 std::string spectrum_label;
698 context->getPrimitiveData(UUID, "reflectivity_spectrum", spectrum_label);
699
700 // Check if this is a LeafOptics-generated spectrum (starts with prefix)
701 if (spectrum_label.find(prefix) != 0) {
702 continue; // Not a LeafOptics spectrum, skip silently
703 }
704
705 // Extract the user label by removing the prefix
706 std::string user_label = spectrum_label.substr(prefix.length());
707
708 // Check if we have parameters stored for this label
709 auto it = spectrum_parameters_map.find(user_label);
710 if (it == spectrum_parameters_map.end()) {
711 continue; // No parameters found, skip silently
712 }
713
714 // Retrieve the stored parameters
715 const LeafOpticsProperties &props = it->second;
716
717 // Assign primitive data using the same logic as setProperties()
718 for (const auto &data: output_prim_data) {
719 if (data == "chlorophyll") {
720 context->setPrimitiveData(UUID, "chlorophyll", props.chlorophyllcontent);
721 } else if (data == "carotenoid") {
722 context->setPrimitiveData(UUID, "carotenoid", props.carotenoidcontent);
723 } else if (data == "anthocyanin") {
724 context->setPrimitiveData(UUID, "anthocyanin", props.anthocyancontent);
725 } else if (data == "brown" && props.brownpigments > 0.0) {
726 context->setPrimitiveData(UUID, "brown", props.brownpigments);
727 } else if (data == "water") {
728 context->setPrimitiveData(UUID, "water", props.watermass);
729 } else if (data == "drymass" && props.drymass > 0.0) {
730 context->setPrimitiveData(UUID, "drymass", props.drymass);
731 } else if (data == "protein" && props.drymass == 0.0) {
732 context->setPrimitiveData(UUID, "protein", props.protein);
733 } else if (data == "cellulose" && props.drymass == 0.0) {
734 context->setPrimitiveData(UUID, "cellulose", props.carbonconstituents);
735 }
736 }
737 }
738}
739
741 getPropertiesFromSpectrum(std::vector<uint>{UUID});
742}
743
744void LeafOptics::getPropertiesFromLibrary(const std::string &species, LeafOpticsProperties &leafproperties) {
745 // Convert species name to lowercase for case-insensitive lookup
746 std::string species_lower = species;
747 std::transform(species_lower.begin(), species_lower.end(), species_lower.begin(), ::tolower);
748
749 // Look up species in library
750 auto it = species_library.find(species_lower);
751 if (it != species_library.end()) {
752 // Species found in library
753 leafproperties = it->second;
754 if (message_flag) {
755 std::cout << "Setting Leaf Optics Properties to species: " << species << std::endl;
756 }
757 } else {
758 // Species not found - use default and issue warning
759 if (message_flag) {
760 std::cerr << "WARNING (LeafOptics): unknown species \"" << species << "\". Using default properties." << std::endl;
761 }
762 leafproperties = species_library["default"];
763 }
764}
765
767 message_flag = false;
768}
769
771 message_flag = true;
772}
773
775 if (strcmp(label, "chlorophyll") == 0 || strcmp(label, "carotenoid") == 0 || strcmp(label, "anthocyanin") == 0 || strcmp(label, "brown") == 0 || strcmp(label, "water") == 0 || strcmp(label, "drymass") == 0 || strcmp(label, "protein") == 0 ||
776 strcmp(label, "cellulose") == 0) {
777 output_prim_data.emplace_back(label);
778 } else {
779 if (message_flag) {
780 std::cout << "WARNING (LeafOptics::optionalOutputPrimitiveData): unknown output primitive data " << label << std::endl;
781 }
782 }
783}
784
785// === Nitrogen mode helper methods ===
786
787LeafOpticsProperties LeafOptics::computePropertiesFromNitrogen(float N_area_gN_m2, const LeafOpticsProperties_Nauto &params) {
789
790 // Convert nitrogen concentration to chlorophyll content
791 // N (g/m2) -> N (ug/cm2): multiply by 100
792 // Then apply photosynthetic fraction and empirical coefficient
793 float N_area_ug_cm2 = N_area_gN_m2 * 100.0f;
794 props.chlorophyllcontent = N_area_ug_cm2 * params.f_photosynthetic * params.N_to_Cab_coefficient;
795
796 // Clamp chlorophyll to physically reasonable range [5, 80] ug/cm2
797 props.chlorophyllcontent = std::max(5.0f, std::min(80.0f, props.chlorophyllcontent));
798
799 // Calculate carotenoids from chlorophyll ratio
800 props.carotenoidcontent = props.chlorophyllcontent * params.Car_to_Cab_ratio;
801
802 // Copy fixed parameters from Nauto struct
803 props.numberlayers = params.numberlayers;
804 props.anthocyancontent = params.anthocyancontent;
805 props.brownpigments = params.brownpigments;
806 props.watermass = params.watermass;
807 props.drymass = params.drymass;
808 props.protein = params.protein;
809 props.carbonconstituents = params.carbonconstituents;
810 props.V2Z = params.V2Z;
811 props.fqe = params.fqe;
812
813 return props;
814}
815
816std::map<uint, std::vector<uint>> LeafOptics::groupPrimitivesByObject(const std::vector<uint> &UUIDs) {
817 std::map<uint, std::vector<uint>> object_groups;
818
820 warnings.setEnabled(message_flag);
821
822 for (uint UUID: UUIDs) {
823 if (!context->doesPrimitiveExist(UUID)) {
824 warnings.addWarning("primitive_does_not_exist", "Primitive UUID " + std::to_string(UUID) + " does not exist, skipping.");
825 continue;
826 }
827
828 uint objID = context->getPrimitiveParentObjectID(UUID);
829
830 // Object ID 0 means no parent object
831 if (objID == 0) {
832 warnings.addWarning("primitive_no_parent_object", "Primitive UUID " + std::to_string(UUID) + " has no parent object, skipping.");
833 continue;
834 }
835
836 object_groups[objID].push_back(UUID);
837 }
838
839 warnings.report(std::cerr);
840
841 return object_groups;
842}
843
844void LeafOptics::createAdaptiveBins(const std::vector<float> &nitrogen_values) {
845 nitrogen_bins.clear();
846
847 if (nitrogen_values.empty()) {
848 return;
849 }
850
851 // Sort nitrogen values for quantile calculation
852 std::vector<float> sorted_N = nitrogen_values;
853 std::sort(sorted_N.begin(), sorted_N.end());
854
855 uint count = sorted_N.size();
856 uint target_bins = nitrogen_params.num_bins;
857
858 // Adjust number of bins if fewer unique values than requested bins
859 if (count < target_bins) {
860 target_bins = count;
861 }
862
863 // Create quantile-based bin centers
864 std::vector<float> bin_centers;
865 for (uint i = 0; i < target_bins; i++) {
866 // Calculate index for quantile center
867 uint idx = (i * count / target_bins) + (count / (2 * target_bins));
868 if (idx >= count) {
869 idx = count - 1;
870 }
871 float center = sorted_N[idx];
872
873 // Only add if not a duplicate (within tolerance)
874 bool is_duplicate = false;
875 for (float existing: bin_centers) {
876 if (std::abs(center - existing) < 0.001f) {
877 is_duplicate = true;
878 break;
879 }
880 }
881 if (!is_duplicate) {
882 bin_centers.push_back(center);
883 }
884 }
885
886 // Generate PROSPECT spectrum for each bin
887 for (uint i = 0; i < bin_centers.size(); i++) {
888 SpectrumBin bin;
889 bin.N_center = bin_centers[i];
890 bin.spectrum_label = "Nauto_" + std::to_string(i);
891
892 // Compute PROSPECT properties and generate spectrum
893 LeafOpticsProperties props = computePropertiesFromNitrogen(bin.N_center, nitrogen_params);
894 run(props, bin.spectrum_label);
895
896 nitrogen_bins.push_back(bin);
897 }
898
899 if (message_flag) {
900 std::cout << "LeafOptics: Created " << nitrogen_bins.size() << " nitrogen-based spectrum bins." << std::endl;
901 }
902}
903
904uint LeafOptics::findNearestBin(float N_value) {
905 if (nitrogen_bins.empty()) {
906 helios_runtime_error("ERROR (LeafOptics::findNearestBin): No nitrogen bins have been created.");
907 }
908
909 uint best_bin = 0;
910 float best_distance = std::abs(N_value - nitrogen_bins[0].N_center);
911
912 for (uint i = 1; i < nitrogen_bins.size(); i++) {
913 float distance = std::abs(N_value - nitrogen_bins[i].N_center);
914 if (distance < best_distance) {
915 best_distance = distance;
916 best_bin = i;
917 }
918 }
919
920 return best_bin;
921}
922
923bool LeafOptics::shouldReassign(float current_N, uint current_bin) {
924 if (current_bin >= nitrogen_bins.size()) {
925 return false;
926 }
927
928 float bin_center = nitrogen_bins[current_bin].N_center;
929 float absolute_change = std::abs(current_N - bin_center);
930
931 // Check absolute threshold first
932 if (absolute_change < nitrogen_params.min_reassignment_change) {
933 return false;
934 }
935
936 // Check relative threshold
937 float relative_change = (bin_center > 0.0f) ? (absolute_change / bin_center) : 0.0f;
938 return relative_change > nitrogen_params.reassignment_threshold;
939}
940
941bool LeafOptics::isSignificantImprovement(float current_N, uint old_bin, uint new_bin) {
942 if (old_bin >= nitrogen_bins.size() || new_bin >= nitrogen_bins.size()) {
943 return false;
944 }
945
946 float old_distance = std::abs(current_N - nitrogen_bins[old_bin].N_center);
947 float new_distance = std::abs(current_N - nitrogen_bins[new_bin].N_center);
948
949 // Require improvement of at least half the minimum change threshold (hysteresis)
950 return (old_distance - new_distance) > nitrogen_params.min_reassignment_change * 0.5f;
951}
952
953void LeafOptics::assignSpectrumToPrimitives(const std::vector<uint> &UUIDs, uint bin_index) {
954 if (bin_index >= nitrogen_bins.size()) {
955 helios_runtime_error("ERROR (LeafOptics::assignSpectrumToPrimitives): Invalid bin index " + std::to_string(bin_index));
956 }
957
958 const std::string &label = nitrogen_bins[bin_index].spectrum_label;
959 std::string refl_label = "leaf_reflectivity_" + label;
960 std::string trans_label = "leaf_transmissivity_" + label;
961
962 context->setPrimitiveData(UUIDs, "reflectivity_spectrum", refl_label);
963 context->setPrimitiveData(UUIDs, "transmissivity_spectrum", trans_label);
964
965 // Stamp the Fluspect-B biochemistry label for this bin so the radiation plugin's
966 // SIF pipeline can find it. The bin's global data is written in ensureNitrogenBinsInitialized
967 // alongside the spectrum; here we just point the primitive at the right label.
968 std::string fluspect_label = "fluspect_biochem_" + label;
969 context->setPrimitiveData(UUIDs, "fluspect_spectrum", fluspect_label);
970
971 // Write optional primitive data if any are enabled
972 if (!output_prim_data.empty()) {
973 // Compute properties from the bin's nitrogen center value
974 LeafOpticsProperties props = computePropertiesFromNitrogen(nitrogen_bins[bin_index].N_center, nitrogen_params);
975
976 for (const auto &data: output_prim_data) {
977 if (data == "chlorophyll") {
978 context->setPrimitiveData(UUIDs, "chlorophyll", props.chlorophyllcontent);
979 } else if (data == "carotenoid") {
980 context->setPrimitiveData(UUIDs, "carotenoid", props.carotenoidcontent);
981 } else if (data == "anthocyanin") {
982 context->setPrimitiveData(UUIDs, "anthocyanin", props.anthocyancontent);
983 } else if (data == "brown" && props.brownpigments > 0.0) {
984 context->setPrimitiveData(UUIDs, "brown", props.brownpigments);
985 } else if (data == "water") {
986 context->setPrimitiveData(UUIDs, "water", props.watermass);
987 } else if (data == "drymass" && props.drymass > 0.0) {
988 context->setPrimitiveData(UUIDs, "drymass", props.drymass);
989 } else if (data == "protein" && props.drymass == 0.0) {
990 context->setPrimitiveData(UUIDs, "protein", props.protein);
991 } else if (data == "cellulose" && props.drymass == 0.0) {
992 context->setPrimitiveData(UUIDs, "cellulose", props.carbonconstituents);
993 }
994 }
995 }
996}
997
998void LeafOptics::run(const std::vector<uint> &UUIDs, const LeafOpticsProperties_Nauto &params) {
999 if (UUIDs.empty()) {
1000 if (message_flag) {
1001 std::cout << "LeafOptics: Empty UUID list provided to nitrogen mode, nothing to do." << std::endl;
1002 }
1003 return;
1004 }
1005
1006 // Group primitives by parent object
1007 std::map<uint, std::vector<uint>> object_groups = groupPrimitivesByObject(UUIDs);
1008
1009 if (object_groups.empty()) {
1010 if (message_flag) {
1011 std::cerr << "WARNING (LeafOptics::run): No valid objects found for nitrogen-based leaf optics." << std::endl;
1012 }
1013 return;
1014 }
1015
1016 // Collect nitrogen values from all objects
1017 std::vector<float> nitrogen_values;
1018 std::map<uint, float> object_nitrogen;
1019
1020 for (const auto &pair: object_groups) {
1021 uint objID = pair.first;
1022
1023 // Check if nitrogen data exists on this object
1024 if (!context->doesObjectDataExist(objID, "leaf_nitrogen_gN_m2")) {
1025 helios_runtime_error("ERROR (LeafOptics::run): Object " + std::to_string(objID) + " does not have 'leaf_nitrogen_gN_m2' data. Enable PlantArchitecture nitrogen model first.");
1026 }
1027
1028 float N_area;
1029 context->getObjectData(objID, "leaf_nitrogen_gN_m2", N_area);
1030 nitrogen_values.push_back(N_area);
1031 object_nitrogen[objID] = N_area;
1032 }
1033
1034 if (!nitrogen_mode_active) {
1035 // First call: Initialize nitrogen mode
1036 nitrogen_params = params;
1037
1038 // Create adaptive bins based on nitrogen distribution
1039 createAdaptiveBins(nitrogen_values);
1040
1041 // Assign each object to nearest bin
1042 for (const auto &pair: object_groups) {
1043 uint objID = pair.first;
1044 const std::vector<uint> &obj_UUIDs = pair.second;
1045 float N_area = object_nitrogen[objID];
1046
1047 uint best_bin = findNearestBin(N_area);
1048 assignSpectrumToPrimitives(obj_UUIDs, best_bin);
1049
1050 // Track assignment
1051 ObjectAssignment assignment;
1052 assignment.bin_index = best_bin;
1053 assignment.N_at_assignment = N_area;
1054 assignment.primitive_UUIDs = obj_UUIDs;
1055 object_assignments[objID] = assignment;
1056
1057 // Track primitive to object mapping
1058 for (uint UUID: obj_UUIDs) {
1059 primitive_to_object[UUID] = objID;
1060 }
1061 }
1062
1063 nitrogen_mode_active = true;
1064
1065 if (message_flag) {
1066 std::cout << "LeafOptics: Nitrogen mode initialized with " << object_assignments.size() << " leaf objects assigned to " << nitrogen_bins.size() << " spectrum bins." << std::endl;
1067 }
1068
1069 } else {
1070 // Subsequent call: Update assignments
1071
1072 // Build set of current object IDs
1073 std::set<uint> current_objects;
1074 for (const auto &pair: object_groups) {
1075 current_objects.insert(pair.first);
1076 }
1077
1078 // Build set of previously tracked objects
1079 std::set<uint> tracked_objects;
1080 for (const auto &pair: object_assignments) {
1081 tracked_objects.insert(pair.first);
1082 }
1083
1084 // Find removed objects (tracked but not in current)
1085 std::vector<uint> removed_objects;
1086 for (uint objID: tracked_objects) {
1087 if (current_objects.find(objID) == current_objects.end()) {
1088 removed_objects.push_back(objID);
1089 }
1090 }
1091
1092 // Remove tracking for removed objects
1093 for (uint objID: removed_objects) {
1094 const ObjectAssignment &assignment = object_assignments[objID];
1095 for (uint UUID: assignment.primitive_UUIDs) {
1096 primitive_to_object.erase(UUID);
1097 }
1098 object_assignments.erase(objID);
1099 }
1100
1101 // Process current objects
1102 for (const auto &pair: object_groups) {
1103 uint objID = pair.first;
1104 const std::vector<uint> &obj_UUIDs = pair.second;
1105 float N_area = object_nitrogen[objID];
1106
1107 if (tracked_objects.find(objID) == tracked_objects.end()) {
1108 // New object: assign to nearest existing bin
1109 uint best_bin = findNearestBin(N_area);
1110 assignSpectrumToPrimitives(obj_UUIDs, best_bin);
1111
1112 ObjectAssignment assignment;
1113 assignment.bin_index = best_bin;
1114 assignment.N_at_assignment = N_area;
1115 assignment.primitive_UUIDs = obj_UUIDs;
1116 object_assignments[objID] = assignment;
1117
1118 for (uint UUID: obj_UUIDs) {
1119 primitive_to_object[UUID] = objID;
1120 }
1121
1122 } else {
1123 // Existing object: check for reassignment
1124 ObjectAssignment &assignment = object_assignments[objID];
1125
1126 // Detect new primitives not in the previous assignment
1127 std::set<uint> prev_UUIDs(assignment.primitive_UUIDs.begin(), assignment.primitive_UUIDs.end());
1128 std::vector<uint> new_UUIDs;
1129 for (uint UUID: obj_UUIDs) {
1130 if (prev_UUIDs.find(UUID) == prev_UUIDs.end()) {
1131 new_UUIDs.push_back(UUID);
1132 }
1133 }
1134
1135 // Update primitive list (in case it changed)
1136 assignment.primitive_UUIDs = obj_UUIDs;
1137 for (uint UUID: obj_UUIDs) {
1138 primitive_to_object[UUID] = objID;
1139 }
1140
1141 // Always assign spectra to new primitives using the current bin
1142 if (!new_UUIDs.empty()) {
1143 uint best_bin = findNearestBin(N_area);
1144 assignSpectrumToPrimitives(new_UUIDs, best_bin);
1145 assignment.bin_index = best_bin;
1146 assignment.N_at_assignment = N_area;
1147 } else if (shouldReassign(N_area, assignment.bin_index)) {
1148 uint best_bin = findNearestBin(N_area);
1149
1150 if (best_bin != assignment.bin_index && isSignificantImprovement(N_area, assignment.bin_index, best_bin)) {
1151 assignSpectrumToPrimitives(obj_UUIDs, best_bin);
1152 assignment.bin_index = best_bin;
1153 assignment.N_at_assignment = N_area;
1154 }
1155 }
1156 }
1157 }
1158 }
1159}
1160
1162 if (!nitrogen_mode_active) {
1163 helios_runtime_error("ERROR (LeafOptics::updateNitrogenBasedSpectra): Nitrogen mode is not active. "
1164 "Call run() with LeafOpticsProperties_Nauto first to initialize nitrogen mode.");
1165 }
1166
1168 warnings.setEnabled(message_flag);
1169
1170 for (auto &pair: object_assignments) {
1171 uint objID = pair.first;
1172 ObjectAssignment &assignment = pair.second;
1173
1174 // Read current nitrogen value
1175 if (!context->doesObjectDataExist(objID, "leaf_nitrogen_gN_m2")) {
1176 warnings.addWarning("missing_nitrogen_data", "Object " + std::to_string(objID) + " no longer has 'leaf_nitrogen_gN_m2' data, skipping.");
1177 continue;
1178 }
1179
1180 float current_N;
1181 context->getObjectData(objID, "leaf_nitrogen_gN_m2", current_N);
1182
1183 // Check for reassignment
1184 if (shouldReassign(current_N, assignment.bin_index)) {
1185 uint best_bin = findNearestBin(current_N);
1186
1187 if (best_bin != assignment.bin_index && isSignificantImprovement(current_N, assignment.bin_index, best_bin)) {
1188 assignSpectrumToPrimitives(assignment.primitive_UUIDs, best_bin);
1189 assignment.bin_index = best_bin;
1190 assignment.N_at_assignment = current_N;
1191 }
1192 }
1193 }
1194
1195 warnings.report(std::cerr);
1196}