1.3.82
 
Loading...
Searching...
No Matches
selfTest.cpp
1#include "LeafOptics.h"
2
3#define DOCTEST_CONFIG_IMPLEMENT
4#include <doctest.h>
5#include "doctest_utils.h"
6
7using namespace helios;
8
9float err_tol = 1e-3;
10
11DOCTEST_TEST_CASE("LeafOpticsProperties Default Constructor") {
13
14 DOCTEST_CHECK(props.numberlayers == doctest::Approx(1.5f).epsilon(err_tol));
15 DOCTEST_CHECK(props.brownpigments == doctest::Approx(0.f).epsilon(err_tol));
16 DOCTEST_CHECK(props.chlorophyllcontent == doctest::Approx(30.f).epsilon(err_tol));
17 DOCTEST_CHECK(props.carotenoidcontent == doctest::Approx(7.f).epsilon(err_tol));
18 DOCTEST_CHECK(props.anthocyancontent == doctest::Approx(1.f).epsilon(err_tol));
19 DOCTEST_CHECK(props.watermass == doctest::Approx(0.015f).epsilon(err_tol));
20 DOCTEST_CHECK(props.drymass == doctest::Approx(0.09f).epsilon(err_tol));
21 DOCTEST_CHECK(props.protein == doctest::Approx(0.f).epsilon(err_tol));
22 DOCTEST_CHECK(props.carbonconstituents == doctest::Approx(0.f).epsilon(err_tol));
23}
24
25DOCTEST_TEST_CASE("LeafOpticsProperties Parameterized Constructor") {
26 float chl = 50.0f, car = 10.0f, ant = 2.0f, water = 0.020f, dry = 0.08f, prot = 0.001f, carb = 0.005f;
27 LeafOpticsProperties props(chl, car, ant, water, dry, prot, carb);
28
29 DOCTEST_CHECK(props.chlorophyllcontent == doctest::Approx(chl).epsilon(err_tol));
30 DOCTEST_CHECK(props.carotenoidcontent == doctest::Approx(car).epsilon(err_tol));
31 DOCTEST_CHECK(props.anthocyancontent == doctest::Approx(ant).epsilon(err_tol));
32 DOCTEST_CHECK(props.watermass == doctest::Approx(water).epsilon(err_tol));
33 DOCTEST_CHECK(props.drymass == doctest::Approx(dry).epsilon(err_tol));
34 DOCTEST_CHECK(props.protein == doctest::Approx(prot).epsilon(err_tol));
35 DOCTEST_CHECK(props.carbonconstituents == doctest::Approx(carb).epsilon(err_tol));
36
37 // Default values should remain unchanged
38 DOCTEST_CHECK(props.numberlayers == doctest::Approx(1.5f).epsilon(err_tol));
39 DOCTEST_CHECK(props.brownpigments == doctest::Approx(0.f).epsilon(err_tol));
40}
41
42DOCTEST_TEST_CASE("LeafOpticsProperties Edge Cases") {
43 // Test with zero values
44 LeafOpticsProperties zero_props(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f);
45 DOCTEST_CHECK(zero_props.chlorophyllcontent == doctest::Approx(0.0f).epsilon(err_tol));
46 DOCTEST_CHECK(zero_props.carotenoidcontent == doctest::Approx(0.0f).epsilon(err_tol));
47
48 // Test with extreme values
49 LeafOpticsProperties extreme_props(1000.0f, 500.0f, 100.0f, 1.0f, 2.0f, 0.5f, 1.5f);
50 DOCTEST_CHECK(extreme_props.chlorophyllcontent == doctest::Approx(1000.0f).epsilon(err_tol));
51 DOCTEST_CHECK(extreme_props.protein == doctest::Approx(0.5f).epsilon(err_tol));
52}
53
54DOCTEST_TEST_CASE("LeafOptics Constructor and Initialization") {
55 Context context_test;
56
57 DOCTEST_CHECK_NOTHROW(LeafOptics leafoptics(&context_test));
58
59 LeafOptics leafoptics(&context_test);
60 leafoptics.disableMessages();
61
62 // Test that spectral data was loaded correctly
63 DOCTEST_CHECK(context_test.doesGlobalDataExist("refraction_index"));
64 DOCTEST_CHECK(context_test.doesGlobalDataExist("absorption_chlorophyll"));
65 DOCTEST_CHECK(context_test.doesGlobalDataExist("absorption_carotenoid"));
66 DOCTEST_CHECK(context_test.doesGlobalDataExist("absorption_anthocyanin"));
67 DOCTEST_CHECK(context_test.doesGlobalDataExist("absorption_brown"));
68 DOCTEST_CHECK(context_test.doesGlobalDataExist("absorption_water"));
69 DOCTEST_CHECK(context_test.doesGlobalDataExist("absorption_drymass"));
70 DOCTEST_CHECK(context_test.doesGlobalDataExist("absorption_proteins"));
71 DOCTEST_CHECK(context_test.doesGlobalDataExist("absorption_carbonconstituents"));
72}
73
74DOCTEST_TEST_CASE("LeafOptics Enable/Disable Messages") {
75 Context context_test;
76 LeafOptics leafoptics(&context_test);
77
78 DOCTEST_CHECK_NOTHROW(leafoptics.enableMessages());
79 DOCTEST_CHECK_NOTHROW(leafoptics.disableMessages());
80}
81
82DOCTEST_TEST_CASE("LeafOptics Basic Run with Label Only") {
83 Context context_test;
84 LeafOptics leafoptics(&context_test);
85 leafoptics.disableMessages();
86
88 std::string label = "test_basic";
89
90 DOCTEST_CHECK_NOTHROW(leafoptics.run(props, label));
91
92 // Verify global data was created
93 std::string refl_label = "leaf_reflectivity_" + label;
94 std::string trans_label = "leaf_transmissivity_" + label;
95
96 DOCTEST_CHECK(context_test.doesGlobalDataExist(refl_label.c_str()));
97 DOCTEST_CHECK(context_test.doesGlobalDataExist(trans_label.c_str()));
98
99 // Verify data sizes
100 std::vector<vec2> refl_data, trans_data;
101 DOCTEST_CHECK_NOTHROW(context_test.getGlobalData(refl_label.c_str(), refl_data));
102 DOCTEST_CHECK_NOTHROW(context_test.getGlobalData(trans_label.c_str(), trans_data));
103
104 DOCTEST_CHECK(refl_data.size() == 2101); // nw wavelengths
105 DOCTEST_CHECK(trans_data.size() == 2101);
106}
107
108DOCTEST_TEST_CASE("LeafOptics Run with UUIDs") {
109 Context context_test;
110 LeafOptics leafoptics(&context_test);
111 leafoptics.disableMessages();
112
113 // Create test primitives
114 std::vector<uint> UUIDs;
115 UUIDs.push_back(context_test.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1)));
116 UUIDs.push_back(context_test.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0, 1, 0)));
117
119 std::string label = "test_uuids";
120
121 DOCTEST_CHECK_NOTHROW(leafoptics.run(UUIDs, props, label));
122
123 // Verify global data was created
124 std::string refl_label = "leaf_reflectivity_" + label;
125 std::string trans_label = "leaf_transmissivity_" + label;
126
127 DOCTEST_CHECK(context_test.doesGlobalDataExist(refl_label.c_str()));
128 DOCTEST_CHECK(context_test.doesGlobalDataExist(trans_label.c_str()));
129
130 // Verify primitive data was set
131 for (uint UUID: UUIDs) {
132 DOCTEST_CHECK(context_test.doesPrimitiveDataExist(UUID, "reflectivity_spectrum"));
133 DOCTEST_CHECK(context_test.doesPrimitiveDataExist(UUID, "transmissivity_spectrum"));
134
135 std::string refl_spectrum_label, trans_spectrum_label;
136 DOCTEST_CHECK_NOTHROW(context_test.getPrimitiveData(UUID, "reflectivity_spectrum", refl_spectrum_label));
137 DOCTEST_CHECK_NOTHROW(context_test.getPrimitiveData(UUID, "transmissivity_spectrum", trans_spectrum_label));
138
139 DOCTEST_CHECK(refl_spectrum_label == refl_label);
140 DOCTEST_CHECK(trans_spectrum_label == trans_label);
141 }
142}
143
144DOCTEST_TEST_CASE("LeafOptics Run with Empty UUID Vector") {
145 Context context_test;
146 LeafOptics leafoptics(&context_test);
147 leafoptics.disableMessages();
148
149 std::vector<uint> empty_UUIDs;
151 std::string label = "test_empty";
152
153 DOCTEST_CHECK_NOTHROW(leafoptics.run(empty_UUIDs, props, label));
154
155 // Global data should still be created
156 std::string refl_label = "leaf_reflectivity_" + label;
157 std::string trans_label = "leaf_transmissivity_" + label;
158
159 DOCTEST_CHECK(context_test.doesGlobalDataExist(refl_label.c_str()));
160 DOCTEST_CHECK(context_test.doesGlobalDataExist(trans_label.c_str()));
161}
162
163DOCTEST_TEST_CASE("LeafOptics GetLeafSpectra - Default Properties") {
164 Context context_test;
165 LeafOptics leafoptics(&context_test);
166 leafoptics.disableMessages();
167
169 std::vector<vec2> reflectivities, transmissivities;
170
171 DOCTEST_CHECK_NOTHROW(leafoptics.getLeafSpectra(props, reflectivities, transmissivities));
172
173 DOCTEST_CHECK(reflectivities.size() == 2101);
174 DOCTEST_CHECK(transmissivities.size() == 2101);
175
176 // Check wavelength range (400-2500 nm)
177 DOCTEST_CHECK(reflectivities[0].x == doctest::Approx(400.0f).epsilon(err_tol));
178 DOCTEST_CHECK(reflectivities.back().x == doctest::Approx(2500.0f).epsilon(err_tol));
179 DOCTEST_CHECK(transmissivities[0].x == doctest::Approx(400.0f).epsilon(err_tol));
180 DOCTEST_CHECK(transmissivities.back().x == doctest::Approx(2500.0f).epsilon(err_tol));
181
182 // Check that values are physically reasonable (0-1 range)
183 for (const auto &refl: reflectivities) {
184 DOCTEST_CHECK(refl.y >= 0.0f);
185 DOCTEST_CHECK(refl.y <= 1.0f);
186 }
187 for (const auto &trans: transmissivities) {
188 DOCTEST_CHECK(trans.y >= 0.0f);
189 DOCTEST_CHECK(trans.y <= 1.0f);
190 }
191
192 // Energy conservation: R + T <= 1 (allowing for absorption)
193 for (size_t i = 0; i < reflectivities.size(); ++i) {
194 DOCTEST_CHECK(reflectivities[i].y + transmissivities[i].y <= 1.01f); // Small tolerance for numerical precision
195 }
196}
197
198DOCTEST_TEST_CASE("LeafOptics GetLeafSpectra - High Chlorophyll") {
199 Context context_test;
200 LeafOptics leafoptics(&context_test);
201 leafoptics.disableMessages();
202
204 props.chlorophyllcontent = 80.0f; // High chlorophyll
205 props.carotenoidcontent = 15.0f;
206
207 std::vector<vec2> reflectivities, transmissivities;
208 DOCTEST_CHECK_NOTHROW(leafoptics.getLeafSpectra(props, reflectivities, transmissivities));
209
210 DOCTEST_CHECK(reflectivities.size() == 2101);
211 DOCTEST_CHECK(transmissivities.size() == 2101);
212
213 // High chlorophyll should result in strong absorption in blue/red regions
214 // and higher reflectance in green/NIR regions
215}
216
217DOCTEST_TEST_CASE("LeafOptics GetLeafSpectra - Autumn Leaves") {
218 Context context_test;
219 LeafOptics leafoptics(&context_test);
220 leafoptics.disableMessages();
221
223 props.chlorophyllcontent = 5.0f; // Low chlorophyll
224 props.carotenoidcontent = 20.0f; // High carotenoids
225 props.anthocyancontent = 15.0f; // High anthocyanins
226 props.brownpigments = 0.5f; // Some brown pigments
227
228 std::vector<vec2> reflectivities, transmissivities;
229 DOCTEST_CHECK_NOTHROW(leafoptics.getLeafSpectra(props, reflectivities, transmissivities));
230
231 DOCTEST_CHECK(reflectivities.size() == 2101);
232 DOCTEST_CHECK(transmissivities.size() == 2101);
233}
234
235DOCTEST_TEST_CASE("LeafOptics GetLeafSpectra - Prospect-PRO Mode") {
236 Context context_test;
237 LeafOptics leafoptics(&context_test);
238 leafoptics.disableMessages();
239
241 props.protein = 0.002f; // Enable PRO mode
242 props.carbonconstituents = 0.008f;
243 props.drymass = 0.0f; // Should use protein/carbon instead
244
245 std::vector<vec2> reflectivities, transmissivities;
246 DOCTEST_CHECK_NOTHROW(leafoptics.getLeafSpectra(props, reflectivities, transmissivities));
247
248 DOCTEST_CHECK(reflectivities.size() == 2101);
249 DOCTEST_CHECK(transmissivities.size() == 2101);
250}
251
252DOCTEST_TEST_CASE("LeafOptics GetLeafSpectra - Extreme Values") {
253 Context context_test;
254 LeafOptics leafoptics(&context_test);
255 leafoptics.disableMessages();
256
257 // Test with extreme values
258 LeafOpticsProperties extreme_props;
259 extreme_props.numberlayers = 5.0f; // Very thick leaf
260 extreme_props.chlorophyllcontent = 200.0f; // Very high
261 extreme_props.watermass = 0.1f; // Very high water content
262
263 std::vector<vec2> reflectivities, transmissivities;
264 DOCTEST_CHECK_NOTHROW(leafoptics.getLeafSpectra(extreme_props, reflectivities, transmissivities));
265
266 DOCTEST_CHECK(reflectivities.size() == 2101);
267 DOCTEST_CHECK(transmissivities.size() == 2101);
268
269 // Very thick leaf should have very low transmittance
270 float avg_transmittance = 0.0f;
271 for (const auto &trans: transmissivities) {
272 avg_transmittance += trans.y;
273 }
274 avg_transmittance /= transmissivities.size();
275 DOCTEST_CHECK(avg_transmittance < 0.1f); // Should be quite low
276}
277
278DOCTEST_TEST_CASE("LeafOptics GetLeafSpectra - Zero Values") {
279 Context context_test;
280 LeafOptics leafoptics(&context_test);
281 leafoptics.disableMessages();
282
283 // Test with zero pigment values
284 LeafOpticsProperties zero_props;
285 zero_props.chlorophyllcontent = 0.0f;
286 zero_props.carotenoidcontent = 0.0f;
287 zero_props.anthocyancontent = 0.0f;
288 zero_props.brownpigments = 0.0f;
289
290 std::vector<vec2> reflectivities, transmissivities;
291 DOCTEST_CHECK_NOTHROW(leafoptics.getLeafSpectra(zero_props, reflectivities, transmissivities));
292
293 DOCTEST_CHECK(reflectivities.size() == 2101);
294 DOCTEST_CHECK(transmissivities.size() == 2101);
295
296 // Without pigments, should have higher overall transmittance
297 float avg_transmittance = 0.0f;
298 for (const auto &trans: transmissivities) {
299 avg_transmittance += trans.y;
300 }
301 avg_transmittance /= transmissivities.size();
302 DOCTEST_CHECK(avg_transmittance > 0.1f); // Should be relatively high
303}
304
305DOCTEST_TEST_CASE("LeafOptics SetProperties") {
306 Context context_test;
307 LeafOptics leafoptics(&context_test);
308 leafoptics.disableMessages();
309
310 // Create test primitives
311 std::vector<uint> UUIDs;
312 UUIDs.push_back(context_test.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1)));
313 UUIDs.push_back(context_test.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0, 1, 0)));
314
316 props.chlorophyllcontent = 45.0f;
317 props.carotenoidcontent = 12.0f;
318 props.anthocyancontent = 3.0f;
319 props.watermass = 0.018f;
320 props.drymass = 0.085f;
321
322 // Enable optional outputs before calling setProperties
323 leafoptics.optionalOutputPrimitiveData("chlorophyll");
324 leafoptics.optionalOutputPrimitiveData("carotenoid");
325 leafoptics.optionalOutputPrimitiveData("anthocyanin");
326 leafoptics.optionalOutputPrimitiveData("water");
327 leafoptics.optionalOutputPrimitiveData("drymass");
328
329 DOCTEST_CHECK_NOTHROW(leafoptics.setProperties(UUIDs, props));
330
331 // Verify properties were set on primitives
332 for (uint UUID: UUIDs) {
333 float chl, car, ant, water, dry;
334
335 DOCTEST_CHECK_NOTHROW(context_test.getPrimitiveData(UUID, "chlorophyll", chl));
336 DOCTEST_CHECK_NOTHROW(context_test.getPrimitiveData(UUID, "carotenoid", car));
337 DOCTEST_CHECK_NOTHROW(context_test.getPrimitiveData(UUID, "anthocyanin", ant));
338 DOCTEST_CHECK_NOTHROW(context_test.getPrimitiveData(UUID, "water", water));
339 DOCTEST_CHECK_NOTHROW(context_test.getPrimitiveData(UUID, "drymass", dry));
340
341 DOCTEST_CHECK(chl == doctest::Approx(props.chlorophyllcontent).epsilon(err_tol));
342 DOCTEST_CHECK(car == doctest::Approx(props.carotenoidcontent).epsilon(err_tol));
343 DOCTEST_CHECK(ant == doctest::Approx(props.anthocyancontent).epsilon(err_tol));
344 DOCTEST_CHECK(water == doctest::Approx(props.watermass).epsilon(err_tol));
345 DOCTEST_CHECK(dry == doctest::Approx(props.drymass).epsilon(err_tol));
346 }
347}
348
349DOCTEST_TEST_CASE("LeafOptics SetProperties with Brown Pigments") {
350 Context context_test;
351 LeafOptics leafoptics(&context_test);
352 leafoptics.disableMessages();
353
354 std::vector<uint> UUIDs;
355 UUIDs.push_back(context_test.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1)));
356
358 props.brownpigments = 0.3f; // Non-zero brown pigments
359
360 // Enable optional output for brown pigments
361 leafoptics.optionalOutputPrimitiveData("brown");
362
363 DOCTEST_CHECK_NOTHROW(leafoptics.setProperties(UUIDs, props));
364
365 // Verify brown pigments were set
366 float brown;
367 DOCTEST_CHECK_NOTHROW(context_test.getPrimitiveData(UUIDs[0], "brown", brown));
368 DOCTEST_CHECK(brown == doctest::Approx(props.brownpigments).epsilon(err_tol));
369}
370
371DOCTEST_TEST_CASE("LeafOptics SetProperties with Protein Mode") {
372 Context context_test;
373 LeafOptics leafoptics(&context_test);
374 leafoptics.disableMessages();
375
376 std::vector<uint> UUIDs;
377 UUIDs.push_back(context_test.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1)));
378
380 props.drymass = 0.0f; // Zero dry mass
381 props.protein = 0.001f;
382 props.carbonconstituents = 0.005f;
383
384 // Enable optional outputs for protein mode
385 leafoptics.optionalOutputPrimitiveData("protein");
386 leafoptics.optionalOutputPrimitiveData("cellulose");
387
388 DOCTEST_CHECK_NOTHROW(leafoptics.setProperties(UUIDs, props));
389
390 // Should set protein and carbon instead of dry mass
391 float protein, carbon;
392 DOCTEST_CHECK_NOTHROW(context_test.getPrimitiveData(UUIDs[0], "protein", protein));
393 DOCTEST_CHECK_NOTHROW(context_test.getPrimitiveData(UUIDs[0], "cellulose", carbon));
394
395 DOCTEST_CHECK(protein == doctest::Approx(props.protein).epsilon(err_tol));
396 DOCTEST_CHECK(carbon == doctest::Approx(props.carbonconstituents).epsilon(err_tol));
397}
398
399DOCTEST_TEST_CASE("LeafOptics SetProperties with Empty UUID Vector") {
400 Context context_test;
401 LeafOptics leafoptics(&context_test);
402 leafoptics.disableMessages();
403
404 std::vector<uint> empty_UUIDs;
406
407 // Should not crash with empty vector
408 DOCTEST_CHECK_NOTHROW(leafoptics.setProperties(empty_UUIDs, props));
409}
410
411DOCTEST_TEST_CASE("LeafOptics Different Label Formats") {
412 Context context_test;
413 LeafOptics leafoptics(&context_test);
414 leafoptics.disableMessages();
415
417
418 // Test empty label
419 DOCTEST_CHECK_NOTHROW(leafoptics.run(props, ""));
420 DOCTEST_CHECK(context_test.doesGlobalDataExist("leaf_reflectivity_"));
421
422 // Test label with special characters
423 std::string special_label = "test_123-abc.xyz";
424 DOCTEST_CHECK_NOTHROW(leafoptics.run(props, special_label));
425 std::string expected_refl = "leaf_reflectivity_" + special_label;
426 DOCTEST_CHECK(context_test.doesGlobalDataExist(expected_refl.c_str()));
427
428 // Test very long label
429 std::string long_label(100, 'x');
430 DOCTEST_CHECK_NOTHROW(leafoptics.run(props, long_label));
431 std::string expected_long_refl = "leaf_reflectivity_" + long_label;
432 DOCTEST_CHECK(context_test.doesGlobalDataExist(expected_long_refl.c_str()));
433}
434
435DOCTEST_TEST_CASE("LeafOptics Wavelength Range and Data Consistency") {
436 Context context_test;
437 LeafOptics leafoptics(&context_test);
438 leafoptics.disableMessages();
439
441 std::vector<vec2> reflectivities, transmissivities;
442
443 leafoptics.getLeafSpectra(props, reflectivities, transmissivities);
444
445 // Verify wavelength consistency
446 for (size_t i = 0; i < reflectivities.size(); ++i) {
447 DOCTEST_CHECK(reflectivities[i].x == doctest::Approx(transmissivities[i].x).epsilon(err_tol));
448
449 // Check wavelength increment (should be 1 nm)
450 if (i > 0) {
451 float wavelength_diff = reflectivities[i].x - reflectivities[i - 1].x;
452 DOCTEST_CHECK(wavelength_diff == doctest::Approx(1.0f).epsilon(err_tol));
453 }
454 }
455}
456
457DOCTEST_TEST_CASE("LeafOptics Physical Realism Checks") {
458 Context context_test;
459 LeafOptics leafoptics(&context_test);
460 leafoptics.disableMessages();
461
463 props.numberlayers = 2.0f;
464 props.chlorophyllcontent = 60.0f;
465
466 std::vector<vec2> reflectivities, transmissivities;
467 leafoptics.getLeafSpectra(props, reflectivities, transmissivities);
468
469 // Check for physically realistic behavior
470 // 1. Red edge should show increased reflectance around 700-750 nm
471 bool found_red_edge = false;
472 for (size_t i = 1; i < reflectivities.size() - 1; ++i) {
473 float wavelength = reflectivities[i].x;
474 if (wavelength >= 700.0f && wavelength <= 750.0f) {
475 float slope = reflectivities[i + 1].y - reflectivities[i - 1].y;
476 if (slope > 0.001f) { // Positive slope indicating red edge
477 found_red_edge = true;
478 break;
479 }
480 }
481 }
482 DOCTEST_CHECK(found_red_edge);
483
484 // 2. NIR reflectance should generally be higher than visible
485 float visible_avg = 0.0f, nir_avg = 0.0f;
486 int visible_count = 0, nir_count = 0;
487
488 for (const auto &refl: reflectivities) {
489 if (refl.x >= 400.0f && refl.x <= 700.0f) {
490 visible_avg += refl.y;
491 visible_count++;
492 } else if (refl.x >= 800.0f && refl.x <= 1200.0f) {
493 nir_avg += refl.y;
494 nir_count++;
495 }
496 }
497
498 visible_avg /= visible_count;
499 nir_avg /= nir_count;
500
501 DOCTEST_CHECK(nir_avg > visible_avg); // NIR should be higher than visible
502}
503
504DOCTEST_TEST_CASE("LeafOptics Multiple Runs Data Consistency") {
505 Context context_test;
506 LeafOptics leafoptics(&context_test);
507 leafoptics.disableMessages();
508
510 props.chlorophyllcontent = 40.0f;
511
512 std::vector<vec2> refl1, trans1, refl2, trans2;
513
514 // Run twice with same parameters
515 leafoptics.getLeafSpectra(props, refl1, trans1);
516 leafoptics.getLeafSpectra(props, refl2, trans2);
517
518 // Results should be identical
519 DOCTEST_CHECK(refl1.size() == refl2.size());
520 DOCTEST_CHECK(trans1.size() == trans2.size());
521
522 for (size_t i = 0; i < refl1.size(); ++i) {
523 DOCTEST_CHECK(refl1[i].x == doctest::Approx(refl2[i].x).epsilon(err_tol));
524 DOCTEST_CHECK(refl1[i].y == doctest::Approx(refl2[i].y).epsilon(err_tol));
525 DOCTEST_CHECK(trans1[i].x == doctest::Approx(trans2[i].x).epsilon(err_tol));
526 DOCTEST_CHECK(trans1[i].y == doctest::Approx(trans2[i].y).epsilon(err_tol));
527 }
528}
529
530DOCTEST_TEST_CASE("LeafOptics GetPropertiesFromSpectrum - Basic Functionality") {
531 Context context_test;
532 LeafOptics leafoptics(&context_test);
533 leafoptics.disableMessages();
534
535 // Create test primitives
536 std::vector<uint> UUIDs;
537 UUIDs.push_back(context_test.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1)));
538 UUIDs.push_back(context_test.addTriangle(make_vec3(0, 0, 0), make_vec3(1, 0, 0), make_vec3(0, 1, 0)));
539
540 // Create custom properties
542 props.chlorophyllcontent = 45.0f;
543 props.carotenoidcontent = 12.0f;
544 props.anthocyancontent = 3.0f;
545 props.watermass = 0.018f;
546 props.drymass = 0.085f;
547
548 std::string label = "test_get_props";
549
550 // Enable optional outputs for the parameters we want to retrieve
551 leafoptics.optionalOutputPrimitiveData("chlorophyll");
552 leafoptics.optionalOutputPrimitiveData("carotenoid");
553 leafoptics.optionalOutputPrimitiveData("anthocyanin");
554 leafoptics.optionalOutputPrimitiveData("water");
555 leafoptics.optionalOutputPrimitiveData("drymass");
556
557 // Generate spectra and assign to primitives
558 leafoptics.run(UUIDs, props, label);
559
560 // Clear existing parameter data from primitives (run() calls setProperties())
561 for (uint UUID: UUIDs) {
562 context_test.clearPrimitiveData(UUID, "chlorophyll");
563 context_test.clearPrimitiveData(UUID, "carotenoid");
564 context_test.clearPrimitiveData(UUID, "anthocyanin");
565 context_test.clearPrimitiveData(UUID, "water");
566 context_test.clearPrimitiveData(UUID, "drymass");
567 }
568
569 // Verify parameters were cleared
570 DOCTEST_CHECK(!context_test.doesPrimitiveDataExist(UUIDs[0], "chlorophyll"));
571
572 // Now retrieve parameters from spectrum
573 DOCTEST_CHECK_NOTHROW(leafoptics.getPropertiesFromSpectrum(UUIDs));
574
575 // Verify all parameters were correctly assigned
576 for (uint UUID: UUIDs) {
577 float chl, car, ant, water, dry;
578
579 DOCTEST_CHECK_NOTHROW(context_test.getPrimitiveData(UUID, "chlorophyll", chl));
580 DOCTEST_CHECK_NOTHROW(context_test.getPrimitiveData(UUID, "carotenoid", car));
581 DOCTEST_CHECK_NOTHROW(context_test.getPrimitiveData(UUID, "anthocyanin", ant));
582 DOCTEST_CHECK_NOTHROW(context_test.getPrimitiveData(UUID, "water", water));
583 DOCTEST_CHECK_NOTHROW(context_test.getPrimitiveData(UUID, "drymass", dry));
584
585 DOCTEST_CHECK(chl == doctest::Approx(props.chlorophyllcontent).epsilon(err_tol));
586 DOCTEST_CHECK(car == doctest::Approx(props.carotenoidcontent).epsilon(err_tol));
587 DOCTEST_CHECK(ant == doctest::Approx(props.anthocyancontent).epsilon(err_tol));
588 DOCTEST_CHECK(water == doctest::Approx(props.watermass).epsilon(err_tol));
589 DOCTEST_CHECK(dry == doctest::Approx(props.drymass).epsilon(err_tol));
590 }
591}
592
593DOCTEST_TEST_CASE("LeafOptics GetPropertiesFromSpectrum - Single UUID Overload") {
594 Context context_test;
595 LeafOptics leafoptics(&context_test);
596 leafoptics.disableMessages();
597
598 uint UUID = context_test.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
599
601 props.chlorophyllcontent = 55.0f;
602 props.carotenoidcontent = 14.0f;
603
604 std::string label = "test_single_uuid";
605
606 // Enable optional outputs
607 leafoptics.optionalOutputPrimitiveData("chlorophyll");
608 leafoptics.optionalOutputPrimitiveData("carotenoid");
609
610 // Generate spectrum
611 leafoptics.run(std::vector<uint>{UUID}, props, label);
612
613 // Clear parameters
614 context_test.clearPrimitiveData(UUID, "chlorophyll");
615 context_test.clearPrimitiveData(UUID, "carotenoid");
616
617 // Retrieve using single UUID overload
618 DOCTEST_CHECK_NOTHROW(leafoptics.getPropertiesFromSpectrum(UUID));
619
620 // Verify parameters were assigned
621 float chl, car;
622 DOCTEST_CHECK_NOTHROW(context_test.getPrimitiveData(UUID, "chlorophyll", chl));
623 DOCTEST_CHECK_NOTHROW(context_test.getPrimitiveData(UUID, "carotenoid", car));
624
625 DOCTEST_CHECK(chl == doctest::Approx(props.chlorophyllcontent).epsilon(err_tol));
626 DOCTEST_CHECK(car == doctest::Approx(props.carotenoidcontent).epsilon(err_tol));
627}
628
629DOCTEST_TEST_CASE("LeafOptics GetPropertiesFromSpectrum - Non-LeafOptics Spectrum") {
630 Context context_test;
631 LeafOptics leafoptics(&context_test);
632 leafoptics.disableMessages();
633
634 uint UUID = context_test.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
635
636 // Assign a spectrum label that doesn't match LeafOptics pattern
637 context_test.setPrimitiveData(UUID, "reflectivity_spectrum", "custom_spectrum");
638
639 // Should not crash, just skip silently
640 DOCTEST_CHECK_NOTHROW(leafoptics.getPropertiesFromSpectrum(UUID));
641
642 // No parameters should be set
643 DOCTEST_CHECK(!context_test.doesPrimitiveDataExist(UUID, "chlorophyll"));
644}
645
646DOCTEST_TEST_CASE("LeafOptics GetPropertiesFromSpectrum - Missing Spectrum") {
647 Context context_test;
648 LeafOptics leafoptics(&context_test);
649 leafoptics.disableMessages();
650
651 uint UUID = context_test.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
652
653 // Primitive has no reflectivity_spectrum data
654 // Should not crash, just skip silently
655 DOCTEST_CHECK_NOTHROW(leafoptics.getPropertiesFromSpectrum(UUID));
656
657 // No parameters should be set
658 DOCTEST_CHECK(!context_test.doesPrimitiveDataExist(UUID, "chlorophyll"));
659}
660
661DOCTEST_TEST_CASE("LeafOptics GetPropertiesFromSpectrum - Unknown Label") {
662 Context context_test;
663 LeafOptics leafoptics(&context_test);
664 leafoptics.disableMessages();
665
666 uint UUID = context_test.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
667
668 // Assign a LeafOptics-style label that wasn't generated by this instance
669 context_test.setPrimitiveData(UUID, "reflectivity_spectrum", "leaf_reflectivity_unknown");
670
671 // Should not crash, just skip silently
672 DOCTEST_CHECK_NOTHROW(leafoptics.getPropertiesFromSpectrum(UUID));
673
674 // No parameters should be set (or they might remain from previous operations)
675}
676
677DOCTEST_TEST_CASE("LeafOptics GetPropertiesFromSpectrum - With Brown Pigments") {
678 Context context_test;
679 LeafOptics leafoptics(&context_test);
680 leafoptics.disableMessages();
681
682 uint UUID = context_test.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
683
685 props.chlorophyllcontent = 20.0f;
686 props.brownpigments = 0.4f; // Non-zero brown pigments
687
688 std::string label = "test_brown";
689
690 // Enable optional outputs
691 leafoptics.optionalOutputPrimitiveData("chlorophyll");
692 leafoptics.optionalOutputPrimitiveData("brown");
693
694 leafoptics.run(std::vector<uint>{UUID}, props, label);
695
696 // Clear parameters
697 context_test.clearPrimitiveData(UUID, "chlorophyll");
698 context_test.clearPrimitiveData(UUID, "brown");
699
700 // Retrieve parameters
701 leafoptics.getPropertiesFromSpectrum(UUID);
702
703 // Verify brown pigments were assigned
704 float brown;
705 DOCTEST_CHECK_NOTHROW(context_test.getPrimitiveData(UUID, "brown", brown));
706 DOCTEST_CHECK(brown == doctest::Approx(props.brownpigments).epsilon(err_tol));
707}
708
709DOCTEST_TEST_CASE("LeafOptics GetPropertiesFromSpectrum - PROSPECT-PRO Mode") {
710 Context context_test;
711 LeafOptics leafoptics(&context_test);
712 leafoptics.disableMessages();
713
714 uint UUID = context_test.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
715
717 props.chlorophyllcontent = 40.0f;
718 props.drymass = 0.0f; // Zero dry mass
719 props.protein = 0.002f;
720 props.carbonconstituents = 0.008f;
721
722 std::string label = "test_pro_mode";
723
724 // Enable optional outputs for PRO mode
725 leafoptics.optionalOutputPrimitiveData("chlorophyll");
726 leafoptics.optionalOutputPrimitiveData("protein");
727 leafoptics.optionalOutputPrimitiveData("cellulose");
728 leafoptics.optionalOutputPrimitiveData("drymass"); // Request drymass but it won't be written since drymass=0
729
730 leafoptics.run(std::vector<uint>{UUID}, props, label);
731
732 // Clear parameters
733 context_test.clearPrimitiveData(UUID, "chlorophyll");
734 context_test.clearPrimitiveData(UUID, "protein");
735 context_test.clearPrimitiveData(UUID, "cellulose");
736
737 // Retrieve parameters
738 leafoptics.getPropertiesFromSpectrum(UUID);
739
740 // Verify protein and carbon were assigned (not dry mass)
741 float protein, carbon;
742 DOCTEST_CHECK_NOTHROW(context_test.getPrimitiveData(UUID, "protein", protein));
743 DOCTEST_CHECK_NOTHROW(context_test.getPrimitiveData(UUID, "cellulose", carbon));
744
745 DOCTEST_CHECK(protein == doctest::Approx(props.protein).epsilon(err_tol));
746 DOCTEST_CHECK(carbon == doctest::Approx(props.carbonconstituents).epsilon(err_tol));
747
748 // Should not have dry mass data (drymass=0 so it's not written even if requested)
749 DOCTEST_CHECK(!context_test.doesPrimitiveDataExist(UUID, "drymass"));
750}
751
752DOCTEST_TEST_CASE("LeafOptics GetPropertiesFromSpectrum - Multiple Spectra") {
753 Context context_test;
754 LeafOptics leafoptics(&context_test);
755 leafoptics.disableMessages();
756
757 // Create two primitives with different spectra
758 uint UUID1 = context_test.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
759 uint UUID2 = context_test.addPatch(make_vec3(1, 0, 0), make_vec2(1, 1));
760
762 props1.chlorophyllcontent = 30.0f;
763 props1.carotenoidcontent = 8.0f;
764
766 props2.chlorophyllcontent = 60.0f;
767 props2.carotenoidcontent = 16.0f;
768
769 // Enable optional outputs
770 leafoptics.optionalOutputPrimitiveData("chlorophyll");
771 leafoptics.optionalOutputPrimitiveData("carotenoid");
772
773 // Generate different spectra
774 leafoptics.run(std::vector<uint>{UUID1}, props1, "spectrum1");
775 leafoptics.run(std::vector<uint>{UUID2}, props2, "spectrum2");
776
777 // Clear parameters
778 context_test.clearPrimitiveData(UUID1, "chlorophyll");
779 context_test.clearPrimitiveData(UUID2, "chlorophyll");
780
781 // Retrieve parameters for both
782 leafoptics.getPropertiesFromSpectrum(std::vector<uint>{UUID1, UUID2});
783
784 // Verify each primitive got its correct parameters
785 float chl1, chl2;
786 context_test.getPrimitiveData(UUID1, "chlorophyll", chl1);
787 context_test.getPrimitiveData(UUID2, "chlorophyll", chl2);
788
789 DOCTEST_CHECK(chl1 == doctest::Approx(props1.chlorophyllcontent).epsilon(err_tol));
790 DOCTEST_CHECK(chl2 == doctest::Approx(props2.chlorophyllcontent).epsilon(err_tol));
791}
792
793DOCTEST_TEST_CASE("LeafOptics GetPropertiesFromSpectrum - Empty UUID Vector") {
794 Context context_test;
795 LeafOptics leafoptics(&context_test);
796 leafoptics.disableMessages();
797
798 std::vector<uint> empty_UUIDs;
799
800 // Should not crash with empty vector
801 DOCTEST_CHECK_NOTHROW(leafoptics.getPropertiesFromSpectrum(empty_UUIDs));
802}
803
804DOCTEST_TEST_CASE("LeafOptics GetPropertiesFromLibrary - Default Species") {
805 Context context_test;
806 LeafOptics leafoptics(&context_test);
807 leafoptics.disableMessages();
808
810
811 // Test with "default" species
812 DOCTEST_CHECK_NOTHROW(leafoptics.getPropertiesFromLibrary("default", props));
813
814 // Verify all parameters match expected default values
815 DOCTEST_CHECK(props.numberlayers == doctest::Approx(1.5f).epsilon(err_tol));
816 DOCTEST_CHECK(props.chlorophyllcontent == doctest::Approx(30.0f).epsilon(err_tol));
817 DOCTEST_CHECK(props.carotenoidcontent == doctest::Approx(7.0f).epsilon(err_tol));
818 DOCTEST_CHECK(props.anthocyancontent == doctest::Approx(1.0f).epsilon(err_tol));
819 DOCTEST_CHECK(props.brownpigments == doctest::Approx(0.0f).epsilon(err_tol));
820 DOCTEST_CHECK(props.watermass == doctest::Approx(0.015f).epsilon(err_tol));
821 DOCTEST_CHECK(props.drymass == doctest::Approx(0.09f).epsilon(err_tol));
822 DOCTEST_CHECK(props.protein == doctest::Approx(0.0f).epsilon(err_tol));
823 DOCTEST_CHECK(props.carbonconstituents == doctest::Approx(0.0f).epsilon(err_tol));
824}
825
826DOCTEST_TEST_CASE("LeafOptics GetPropertiesFromLibrary - Case Insensitivity") {
827 Context context_test;
828 LeafOptics leafoptics(&context_test);
829 leafoptics.disableMessages();
830
831 LeafOpticsProperties props_lower, props_upper;
832
833 // Test case insensitivity
834 DOCTEST_CHECK_NOTHROW(leafoptics.getPropertiesFromLibrary("default", props_lower));
835 DOCTEST_CHECK_NOTHROW(leafoptics.getPropertiesFromLibrary("Default", props_upper));
836
837 // Both should have identical values
838 DOCTEST_CHECK(props_lower.chlorophyllcontent == doctest::Approx(props_upper.chlorophyllcontent).epsilon(err_tol));
839 DOCTEST_CHECK(props_lower.watermass == doctest::Approx(props_upper.watermass).epsilon(err_tol));
840 DOCTEST_CHECK(props_lower.drymass == doctest::Approx(props_upper.drymass).epsilon(err_tol));
841}
842
843DOCTEST_TEST_CASE("LeafOptics GetPropertiesFromLibrary - Unknown Species") {
844 Context context_test;
845 LeafOptics leafoptics(&context_test);
846 leafoptics.disableMessages();
847
849
850 // Test with unknown species - should use default without crashing
851 DOCTEST_CHECK_NOTHROW(leafoptics.getPropertiesFromLibrary("UnknownSpecies", props));
852
853 // Should have default values
854 DOCTEST_CHECK(props.chlorophyllcontent == doctest::Approx(30.0f).epsilon(err_tol));
855 DOCTEST_CHECK(props.watermass == doctest::Approx(0.015f).epsilon(err_tol));
856 DOCTEST_CHECK(props.drymass == doctest::Approx(0.09f).epsilon(err_tol));
857}
858
859DOCTEST_TEST_CASE("LeafOptics GetPropertiesFromLibrary - Integration with Run") {
860 Context context_test;
861 LeafOptics leafoptics(&context_test);
862 leafoptics.disableMessages();
863
864 // Create test primitives
865 std::vector<uint> UUIDs;
866 UUIDs.push_back(context_test.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1)));
867
868 // Get properties from library
870 leafoptics.getPropertiesFromLibrary("default", props);
871
872 // Enable optional outputs
873 leafoptics.optionalOutputPrimitiveData("chlorophyll");
874 leafoptics.optionalOutputPrimitiveData("water");
875 leafoptics.optionalOutputPrimitiveData("drymass");
876
877 // Use properties to run the model
878 std::string label = "test_library_integration";
879 DOCTEST_CHECK_NOTHROW(leafoptics.run(UUIDs, props, label));
880
881 // Verify spectra were created
882 std::string refl_label = "leaf_reflectivity_" + label;
883 std::string trans_label = "leaf_transmissivity_" + label;
884
885 DOCTEST_CHECK(context_test.doesGlobalDataExist(refl_label.c_str()));
886 DOCTEST_CHECK(context_test.doesGlobalDataExist(trans_label.c_str()));
887
888 // Verify primitive data was set with correct values
889 float chl, water, dry;
890 DOCTEST_CHECK_NOTHROW(context_test.getPrimitiveData(UUIDs[0], "chlorophyll", chl));
891 DOCTEST_CHECK_NOTHROW(context_test.getPrimitiveData(UUIDs[0], "water", water));
892 DOCTEST_CHECK_NOTHROW(context_test.getPrimitiveData(UUIDs[0], "drymass", dry));
893
894 DOCTEST_CHECK(chl == doctest::Approx(30.0f).epsilon(err_tol));
895 DOCTEST_CHECK(water == doctest::Approx(0.015f).epsilon(err_tol));
896 DOCTEST_CHECK(dry == doctest::Approx(0.09f).epsilon(err_tol));
897}
898
899DOCTEST_TEST_CASE("LeafOptics GetPropertiesFromLibrary - LOPEX93 Species Library") {
900 Context context_test;
901 LeafOptics leafoptics(&context_test);
902 leafoptics.disableMessages();
903
904 // Test all 9 LOPEX93 species in the library
905 struct SpeciesTestData {
906 std::string name;
907 float expected_N;
908 float expected_Cab;
909 float expected_drymass;
910 };
911
912 std::vector<SpeciesTestData> species_data = {{"garden_lettuce", 2.00517f, 30.2697f, 0.0052668f}, {"alfalfa", 2.00758f, 43.6375f, 0.00473702f}, {"corn", 1.59203f, 22.8664f, 0.00441283f},
913 {"sunflower", 1.76358f, 54.0514f, 0.00644855f}, {"english_walnut", 1.56274f, 55.9211f, 0.00583351f}, {"rice", 1.67081f, 37.233f, 0.00484587f},
914 {"soybean", 1.5375f, 46.4121f, 0.00292814f}, {"wine_grape", 1.42673f, 50.918f, 0.00599315f}, {"tomato", 1.40304f, 48.3467f, 0.00261571f}};
915
916 for (const auto &species: species_data) {
918 DOCTEST_CHECK_NOTHROW(leafoptics.getPropertiesFromLibrary(species.name, props));
919
920 // Check key parameters match fitted values
921 DOCTEST_CHECK(props.numberlayers == doctest::Approx(species.expected_N).epsilon(err_tol));
922 DOCTEST_CHECK(props.chlorophyllcontent == doctest::Approx(species.expected_Cab).epsilon(err_tol));
923 DOCTEST_CHECK(props.drymass == doctest::Approx(species.expected_drymass).epsilon(err_tol));
924
925 // All LOPEX93 species should use PROSPECT-D mode
926 DOCTEST_CHECK(props.drymass > 0.0f);
927 DOCTEST_CHECK(props.protein == doctest::Approx(0.0f).epsilon(err_tol));
928 DOCTEST_CHECK(props.carbonconstituents == doctest::Approx(0.0f).epsilon(err_tol));
929 }
930}
931
932DOCTEST_TEST_CASE("LeafOptics GetPropertiesFromLibrary - LOPEX93 Species Case Insensitivity") {
933 Context context_test;
934 LeafOptics leafoptics(&context_test);
935 leafoptics.disableMessages();
936
937 LeafOpticsProperties props_lower, props_upper, props_mixed;
938
939 // Test case insensitivity for specific species
940 DOCTEST_CHECK_NOTHROW(leafoptics.getPropertiesFromLibrary("corn", props_lower));
941 DOCTEST_CHECK_NOTHROW(leafoptics.getPropertiesFromLibrary("CORN", props_upper));
942 DOCTEST_CHECK_NOTHROW(leafoptics.getPropertiesFromLibrary("Corn", props_mixed));
943
944 // All three should have identical values
945 DOCTEST_CHECK(props_lower.chlorophyllcontent == doctest::Approx(props_upper.chlorophyllcontent).epsilon(err_tol));
946 DOCTEST_CHECK(props_lower.chlorophyllcontent == doctest::Approx(props_mixed.chlorophyllcontent).epsilon(err_tol));
947 DOCTEST_CHECK(props_lower.drymass == doctest::Approx(props_upper.drymass).epsilon(err_tol));
948}
949
950DOCTEST_TEST_CASE("LeafOptics GetPropertiesFromLibrary - LOPEX93 Complete Parameter Check") {
951 Context context_test;
952 LeafOptics leafoptics(&context_test);
953 leafoptics.disableMessages();
954
955 // Test one species with all parameters explicitly checked (sunflower)
957 leafoptics.getPropertiesFromLibrary("sunflower", props);
958
959 DOCTEST_CHECK(props.numberlayers == doctest::Approx(1.76358f).epsilon(err_tol));
960 DOCTEST_CHECK(props.chlorophyllcontent == doctest::Approx(54.0514f).epsilon(err_tol));
961 DOCTEST_CHECK(props.carotenoidcontent == doctest::Approx(12.9027f).epsilon(err_tol));
962 DOCTEST_CHECK(props.anthocyancontent == doctest::Approx(1.75194f).epsilon(err_tol));
963 DOCTEST_CHECK(props.brownpigments == doctest::Approx(0.0112026f).epsilon(err_tol));
964 DOCTEST_CHECK(props.watermass == doctest::Approx(0.0185557f).epsilon(err_tol));
965 DOCTEST_CHECK(props.drymass == doctest::Approx(0.00644855f).epsilon(err_tol));
966 DOCTEST_CHECK(props.protein == doctest::Approx(0.0f).epsilon(err_tol));
967 DOCTEST_CHECK(props.carbonconstituents == doctest::Approx(0.0f).epsilon(err_tol));
968}
969
970DOCTEST_TEST_CASE("LeafOptics GetPropertiesFromLibrary - LOPEX93 Integration Test") {
971 Context context_test;
972 LeafOptics leafoptics(&context_test);
973 leafoptics.disableMessages();
974
975 // Create test primitives
976 std::vector<uint> UUIDs;
977 UUIDs.push_back(context_test.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1)));
978 UUIDs.push_back(context_test.addPatch(make_vec3(1, 0, 0), make_vec2(1, 1)));
979
980 // Get properties for soybean from library
981 LeafOpticsProperties soybean_props;
982 leafoptics.getPropertiesFromLibrary("soybean", soybean_props);
983
984 // Enable optional outputs
985 leafoptics.optionalOutputPrimitiveData("chlorophyll");
986
987 // Run model with library properties
988 std::string label = "test_soybean";
989 DOCTEST_CHECK_NOTHROW(leafoptics.run(UUIDs, soybean_props, label));
990
991 // Verify spectra were created
992 DOCTEST_CHECK(context_test.doesGlobalDataExist("leaf_reflectivity_test_soybean"));
993 DOCTEST_CHECK(context_test.doesGlobalDataExist("leaf_transmissivity_test_soybean"));
994
995 // Verify primitive data matches library values
996 for (uint UUID: UUIDs) {
997 float chl;
998 context_test.getPrimitiveData(UUID, "chlorophyll", chl);
999 DOCTEST_CHECK(chl == doctest::Approx(46.4121f).epsilon(err_tol)); // Soybean chlorophyll
1000 }
1001}
1002
1003DOCTEST_TEST_CASE("LeafOptics GetPropertiesFromLibrary - LOPEX93 Species Comparison") {
1004 Context context_test;
1005 LeafOptics leafoptics(&context_test);
1006 leafoptics.disableMessages();
1007
1008 LeafOpticsProperties lettuce_props, walnut_props;
1009 leafoptics.getPropertiesFromLibrary("garden_lettuce", lettuce_props);
1010 leafoptics.getPropertiesFromLibrary("english_walnut", walnut_props);
1011
1012 // Verify species have different properties
1013 DOCTEST_CHECK(lettuce_props.chlorophyllcontent != doctest::Approx(walnut_props.chlorophyllcontent).epsilon(err_tol));
1014 DOCTEST_CHECK(lettuce_props.numberlayers != doctest::Approx(walnut_props.numberlayers).epsilon(err_tol));
1015
1016 // English walnut should have higher chlorophyll than lettuce
1017 DOCTEST_CHECK(walnut_props.chlorophyllcontent > lettuce_props.chlorophyllcontent);
1018}
1019
1020DOCTEST_TEST_CASE("LeafOptics optionalOutputPrimitiveData - No Output By Default") {
1021 Context context_test;
1022 LeafOptics leafoptics(&context_test);
1023 leafoptics.disableMessages();
1024
1025 // Create test primitives
1026 std::vector<uint> UUIDs;
1027 UUIDs.push_back(context_test.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1)));
1028
1030 props.chlorophyllcontent = 45.0f;
1031 props.carotenoidcontent = 12.0f;
1032 props.watermass = 0.018f;
1033 props.drymass = 0.085f;
1034
1035 // Run without enabling any optional outputs
1036 leafoptics.run(UUIDs, props, "test_no_output");
1037
1038 // Verify no primitive data was written (except for spectrum labels)
1039 DOCTEST_CHECK(!context_test.doesPrimitiveDataExist(UUIDs[0], "chlorophyll"));
1040 DOCTEST_CHECK(!context_test.doesPrimitiveDataExist(UUIDs[0], "carotenoid"));
1041 DOCTEST_CHECK(!context_test.doesPrimitiveDataExist(UUIDs[0], "anthocyanin"));
1042 DOCTEST_CHECK(!context_test.doesPrimitiveDataExist(UUIDs[0], "water"));
1043 DOCTEST_CHECK(!context_test.doesPrimitiveDataExist(UUIDs[0], "drymass"));
1044
1045 // Spectrum labels should still be written
1046 DOCTEST_CHECK(context_test.doesPrimitiveDataExist(UUIDs[0], "reflectivity_spectrum"));
1047 DOCTEST_CHECK(context_test.doesPrimitiveDataExist(UUIDs[0], "transmissivity_spectrum"));
1048}
1049
1050DOCTEST_TEST_CASE("LeafOptics optionalOutputPrimitiveData - Selective Output") {
1051 Context context_test;
1052 LeafOptics leafoptics(&context_test);
1053 leafoptics.disableMessages();
1054
1055 std::vector<uint> UUIDs;
1056 UUIDs.push_back(context_test.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1)));
1057
1059 props.chlorophyllcontent = 45.0f;
1060 props.carotenoidcontent = 12.0f;
1061 props.anthocyancontent = 3.0f;
1062 props.watermass = 0.018f;
1063 props.drymass = 0.085f;
1064
1065 // Enable only chlorophyll and water
1066 leafoptics.optionalOutputPrimitiveData("chlorophyll");
1067 leafoptics.optionalOutputPrimitiveData("water");
1068
1069 leafoptics.run(UUIDs, props, "test_selective");
1070
1071 // Only chlorophyll and water should be written
1072 DOCTEST_CHECK(context_test.doesPrimitiveDataExist(UUIDs[0], "chlorophyll"));
1073 DOCTEST_CHECK(context_test.doesPrimitiveDataExist(UUIDs[0], "water"));
1074
1075 // Other properties should not be written
1076 DOCTEST_CHECK(!context_test.doesPrimitiveDataExist(UUIDs[0], "carotenoid"));
1077 DOCTEST_CHECK(!context_test.doesPrimitiveDataExist(UUIDs[0], "anthocyanin"));
1078 DOCTEST_CHECK(!context_test.doesPrimitiveDataExist(UUIDs[0], "drymass"));
1079
1080 // Verify correct values
1081 float chl, water;
1082 context_test.getPrimitiveData(UUIDs[0], "chlorophyll", chl);
1083 context_test.getPrimitiveData(UUIDs[0], "water", water);
1084 DOCTEST_CHECK(chl == doctest::Approx(props.chlorophyllcontent).epsilon(err_tol));
1085 DOCTEST_CHECK(water == doctest::Approx(props.watermass).epsilon(err_tol));
1086}
1087
1088DOCTEST_TEST_CASE("LeafOptics optionalOutputPrimitiveData - Invalid Label Warning") {
1089 Context context_test;
1090 LeafOptics leafoptics(&context_test);
1091
1092 // Capture stdout for warning message
1093 helios::capture_cout capture;
1094
1095 // Try to add invalid label (messages enabled)
1096 leafoptics.optionalOutputPrimitiveData("invalid_label");
1097
1098 std::string output = capture.get_captured_output();
1099 DOCTEST_CHECK(output.find("WARNING") != std::string::npos);
1100 DOCTEST_CHECK(output.find("invalid_label") != std::string::npos);
1101}
1102
1103DOCTEST_TEST_CASE("LeafOptics optionalOutputPrimitiveData - All Valid Labels") {
1104 Context context_test;
1105 LeafOptics leafoptics(&context_test);
1106 leafoptics.disableMessages();
1107
1108 // All these should be valid labels (no warnings)
1109 DOCTEST_CHECK_NOTHROW(leafoptics.optionalOutputPrimitiveData("chlorophyll"));
1110 DOCTEST_CHECK_NOTHROW(leafoptics.optionalOutputPrimitiveData("carotenoid"));
1111 DOCTEST_CHECK_NOTHROW(leafoptics.optionalOutputPrimitiveData("anthocyanin"));
1112 DOCTEST_CHECK_NOTHROW(leafoptics.optionalOutputPrimitiveData("brown"));
1113 DOCTEST_CHECK_NOTHROW(leafoptics.optionalOutputPrimitiveData("water"));
1114 DOCTEST_CHECK_NOTHROW(leafoptics.optionalOutputPrimitiveData("drymass"));
1115 DOCTEST_CHECK_NOTHROW(leafoptics.optionalOutputPrimitiveData("protein"));
1116 DOCTEST_CHECK_NOTHROW(leafoptics.optionalOutputPrimitiveData("cellulose"));
1117}
1118
1119// ============== Nitrogen Mode Tests ==============
1120
1121DOCTEST_TEST_CASE("LeafOpticsProperties_Nauto Default Constructor") {
1123
1124 // Nitrogen-to-pigment conversion coefficients
1125 DOCTEST_CHECK(params.f_photosynthetic == doctest::Approx(0.50f).epsilon(err_tol));
1126 DOCTEST_CHECK(params.N_to_Cab_coefficient == doctest::Approx(0.40f).epsilon(err_tol));
1127 DOCTEST_CHECK(params.Car_to_Cab_ratio == doctest::Approx(0.25f).epsilon(err_tol));
1128
1129 // Fixed PROSPECT parameters
1130 DOCTEST_CHECK(params.numberlayers == doctest::Approx(1.5f).epsilon(err_tol));
1131 DOCTEST_CHECK(params.anthocyancontent == doctest::Approx(1.0f).epsilon(err_tol));
1132 DOCTEST_CHECK(params.brownpigments == doctest::Approx(0.0f).epsilon(err_tol));
1133 DOCTEST_CHECK(params.watermass == doctest::Approx(0.015f).epsilon(err_tol));
1134 DOCTEST_CHECK(params.drymass == doctest::Approx(0.006f).epsilon(err_tol));
1135 DOCTEST_CHECK(params.protein == doctest::Approx(0.0f).epsilon(err_tol));
1136 DOCTEST_CHECK(params.carbonconstituents == doctest::Approx(0.0f).epsilon(err_tol));
1137
1138 // Binning parameters
1139 DOCTEST_CHECK(params.num_bins == 20);
1140 DOCTEST_CHECK(params.reassignment_threshold == doctest::Approx(0.30f).epsilon(err_tol));
1141 DOCTEST_CHECK(params.min_reassignment_change == doctest::Approx(0.3f).epsilon(err_tol));
1142}
1143
1144DOCTEST_TEST_CASE("Nitrogen Mode - First Call Creates Bins") {
1145 Context context_test;
1146 LeafOptics leafoptics(&context_test);
1147 leafoptics.disableMessages();
1148
1149 // Create some compound objects with leaf nitrogen data
1150 std::vector<uint> all_UUIDs;
1151 std::vector<uint> objIDs;
1152
1153 for (int i = 0; i < 5; i++) {
1154 uint objID = context_test.addTileObject(make_vec3(float(i), 0, 0), make_vec2(0.1f, 0.1f), make_SphericalCoord(0, 0), make_int2(2, 2));
1155 objIDs.push_back(objID);
1156
1157 // Set nitrogen data on each object (varying nitrogen levels)
1158 float N_area = 1.0f + float(i) * 0.5f; // 1.0, 1.5, 2.0, 2.5, 3.0
1159 context_test.setObjectData(objID, "leaf_nitrogen_gN_m2", N_area);
1160
1161 // Get primitives for this object
1162 std::vector<uint> obj_UUIDs = context_test.getObjectPrimitiveUUIDs(objID);
1163 all_UUIDs.insert(all_UUIDs.end(), obj_UUIDs.begin(), obj_UUIDs.end());
1164 }
1165
1167 params.num_bins = 3; // Request 3 bins
1168
1169 // Run nitrogen mode
1170 DOCTEST_CHECK_NOTHROW(leafoptics.run(all_UUIDs, params));
1171
1172 // Verify spectra were created
1173 DOCTEST_CHECK(context_test.doesGlobalDataExist("leaf_reflectivity_Nauto_0"));
1174 DOCTEST_CHECK(context_test.doesGlobalDataExist("leaf_transmissivity_Nauto_0"));
1175
1176 // Verify primitives were assigned spectra
1177 std::string refl_label;
1178 context_test.getPrimitiveData(all_UUIDs[0], "reflectivity_spectrum", refl_label);
1179 DOCTEST_CHECK(refl_label.find("leaf_reflectivity_Nauto_") == 0);
1180}
1181
1182DOCTEST_TEST_CASE("Nitrogen Mode - Subsequent Call Reuses Bins") {
1183 Context context_test;
1184 LeafOptics leafoptics(&context_test);
1185 leafoptics.disableMessages();
1186
1187 // Create objects with nitrogen data
1188 std::vector<uint> all_UUIDs;
1189 std::vector<uint> objIDs;
1190
1191 for (int i = 0; i < 3; i++) {
1192 uint objID = context_test.addTileObject(make_vec3(float(i), 0, 0), make_vec2(0.1f, 0.1f), make_SphericalCoord(0, 0), make_int2(2, 2));
1193 objIDs.push_back(objID);
1194 float N_area = 1.5f + float(i) * 0.3f;
1195 context_test.setObjectData(objID, "leaf_nitrogen_gN_m2", N_area);
1196 std::vector<uint> obj_UUIDs = context_test.getObjectPrimitiveUUIDs(objID);
1197 all_UUIDs.insert(all_UUIDs.end(), obj_UUIDs.begin(), obj_UUIDs.end());
1198 }
1199
1201 params.num_bins = 2;
1202
1203 // First call
1204 leafoptics.run(all_UUIDs, params);
1205
1206 // Record initial spectrum assignments
1207 std::string initial_label;
1208 context_test.getPrimitiveData(all_UUIDs[0], "reflectivity_spectrum", initial_label);
1209
1210 // Subsequent call with same primitives (no nitrogen change)
1211 DOCTEST_CHECK_NOTHROW(leafoptics.run(all_UUIDs, params));
1212
1213 // Spectrum assignment should remain the same
1214 std::string after_label;
1215 context_test.getPrimitiveData(all_UUIDs[0], "reflectivity_spectrum", after_label);
1216 DOCTEST_CHECK(initial_label == after_label);
1217}
1218
1219DOCTEST_TEST_CASE("Nitrogen Mode - New Objects Assigned to Existing Bins") {
1220 Context context_test;
1221 LeafOptics leafoptics(&context_test);
1222 leafoptics.disableMessages();
1223
1224 // Create initial objects
1225 std::vector<uint> all_UUIDs;
1226 for (int i = 0; i < 3; i++) {
1227 uint objID = context_test.addTileObject(make_vec3(float(i), 0, 0), make_vec2(0.1f, 0.1f), make_SphericalCoord(0, 0), make_int2(2, 2));
1228 float N_area = 1.5f + float(i) * 0.5f;
1229 context_test.setObjectData(objID, "leaf_nitrogen_gN_m2", N_area);
1230 std::vector<uint> obj_UUIDs = context_test.getObjectPrimitiveUUIDs(objID);
1231 all_UUIDs.insert(all_UUIDs.end(), obj_UUIDs.begin(), obj_UUIDs.end());
1232 }
1233
1235 params.num_bins = 2;
1236
1237 // First call initializes bins
1238 leafoptics.run(all_UUIDs, params);
1239
1240 // Add a new object
1241 uint new_objID = context_test.addTileObject(make_vec3(5, 0, 0), make_vec2(0.1f, 0.1f), make_SphericalCoord(0, 0), make_int2(2, 2));
1242 context_test.setObjectData(new_objID, "leaf_nitrogen_gN_m2", 2.0f);
1243 std::vector<uint> new_UUIDs = context_test.getObjectPrimitiveUUIDs(new_objID);
1244 all_UUIDs.insert(all_UUIDs.end(), new_UUIDs.begin(), new_UUIDs.end());
1245
1246 // Second call should assign new object to existing bin
1247 DOCTEST_CHECK_NOTHROW(leafoptics.run(all_UUIDs, params));
1248
1249 // New object should have a spectrum assigned
1250 std::string refl_label;
1251 context_test.getPrimitiveData(new_UUIDs[0], "reflectivity_spectrum", refl_label);
1252 DOCTEST_CHECK(refl_label.find("leaf_reflectivity_Nauto_") == 0);
1253}
1254
1255DOCTEST_TEST_CASE("Nitrogen Mode - Removed Objects Untracked") {
1256 Context context_test;
1257 LeafOptics leafoptics(&context_test);
1258 leafoptics.disableMessages();
1259
1260 // Create objects
1261 std::vector<uint> all_UUIDs;
1262 std::vector<uint> objIDs;
1263 for (int i = 0; i < 3; i++) {
1264 uint objID = context_test.addTileObject(make_vec3(float(i), 0, 0), make_vec2(0.1f, 0.1f), make_SphericalCoord(0, 0), make_int2(2, 2));
1265 objIDs.push_back(objID);
1266 context_test.setObjectData(objID, "leaf_nitrogen_gN_m2", 1.5f + float(i) * 0.3f);
1267 std::vector<uint> obj_UUIDs = context_test.getObjectPrimitiveUUIDs(objID);
1268 all_UUIDs.insert(all_UUIDs.end(), obj_UUIDs.begin(), obj_UUIDs.end());
1269 }
1270
1272 params.num_bins = 2;
1273
1274 // First call
1275 leafoptics.run(all_UUIDs, params);
1276
1277 // Remove last object from the list
1278 std::vector<uint> reduced_UUIDs;
1279 for (int i = 0; i < 2; i++) {
1280 std::vector<uint> obj_UUIDs = context_test.getObjectPrimitiveUUIDs(objIDs[i]);
1281 reduced_UUIDs.insert(reduced_UUIDs.end(), obj_UUIDs.begin(), obj_UUIDs.end());
1282 }
1283
1284 // Second call with reduced list (should not throw)
1285 DOCTEST_CHECK_NOTHROW(leafoptics.run(reduced_UUIDs, params));
1286}
1287
1288DOCTEST_TEST_CASE("Nitrogen Mode - Reassignment When N Changes Significantly") {
1289 Context context_test;
1290 LeafOptics leafoptics(&context_test);
1291 leafoptics.disableMessages();
1292
1293 // Create objects with distinct nitrogen levels
1294 std::vector<uint> all_UUIDs;
1295 std::vector<uint> objIDs;
1296 for (int i = 0; i < 4; i++) {
1297 uint objID = context_test.addTileObject(make_vec3(float(i), 0, 0), make_vec2(0.1f, 0.1f), make_SphericalCoord(0, 0), make_int2(2, 2));
1298 objIDs.push_back(objID);
1299 float N_area = 1.0f + float(i) * 1.0f; // 1.0, 2.0, 3.0, 4.0
1300 context_test.setObjectData(objID, "leaf_nitrogen_gN_m2", N_area);
1301 std::vector<uint> obj_UUIDs = context_test.getObjectPrimitiveUUIDs(objID);
1302 all_UUIDs.insert(all_UUIDs.end(), obj_UUIDs.begin(), obj_UUIDs.end());
1303 }
1304
1306 params.num_bins = 2;
1307 params.reassignment_threshold = 0.3f;
1308 params.min_reassignment_change = 0.3f;
1309
1310 // First call
1311 leafoptics.run(all_UUIDs, params);
1312
1313 // Record initial assignment for first object
1314 std::vector<uint> first_obj_UUIDs = context_test.getObjectPrimitiveUUIDs(objIDs[0]);
1315 std::string initial_label;
1316 context_test.getPrimitiveData(first_obj_UUIDs[0], "reflectivity_spectrum", initial_label);
1317
1318 // Change nitrogen significantly for first object (from 1.0 to 3.5)
1319 context_test.setObjectData(objIDs[0], "leaf_nitrogen_gN_m2", 3.5f);
1320
1321 // Second call should reassign
1322 leafoptics.run(all_UUIDs, params);
1323
1324 std::string after_label;
1325 context_test.getPrimitiveData(first_obj_UUIDs[0], "reflectivity_spectrum", after_label);
1326
1327 // Label may or may not change depending on bin structure, but it should not throw
1328 DOCTEST_CHECK(!after_label.empty());
1329}
1330
1331DOCTEST_TEST_CASE("Nitrogen Mode - Missing Nitrogen Data Error") {
1332 Context context_test;
1333 LeafOptics leafoptics(&context_test);
1334 leafoptics.disableMessages();
1335
1336 // Create object WITHOUT nitrogen data
1337 uint objID = context_test.addTileObject(make_vec3(0, 0, 0), make_vec2(0.1f, 0.1f), make_SphericalCoord(0, 0), make_int2(2, 2));
1338 std::vector<uint> UUIDs = context_test.getObjectPrimitiveUUIDs(objID);
1339
1341
1342 // Should throw error because nitrogen data is missing
1343 DOCTEST_CHECK_THROWS(leafoptics.run(UUIDs, params));
1344}
1345
1346DOCTEST_TEST_CASE("Nitrogen Mode - Primitives Without Parent Skipped") {
1347 Context context_test;
1348 LeafOptics leafoptics(&context_test);
1349 leafoptics.disableMessages();
1350
1351 // Create a standalone primitive (no parent object)
1352 uint standalone_UUID = context_test.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
1353
1354 // Create an object with nitrogen data
1355 uint objID = context_test.addTileObject(make_vec3(1, 0, 0), make_vec2(0.1f, 0.1f), make_SphericalCoord(0, 0), make_int2(2, 2));
1356 context_test.setObjectData(objID, "leaf_nitrogen_gN_m2", 2.0f);
1357 std::vector<uint> obj_UUIDs = context_test.getObjectPrimitiveUUIDs(objID);
1358
1359 // Mix standalone and object primitives
1360 std::vector<uint> mixed_UUIDs = {standalone_UUID};
1361 mixed_UUIDs.insert(mixed_UUIDs.end(), obj_UUIDs.begin(), obj_UUIDs.end());
1362
1364 params.num_bins = 1;
1365
1366 // Should handle gracefully (warning but no throw)
1367 DOCTEST_CHECK_NOTHROW(leafoptics.run(mixed_UUIDs, params));
1368
1369 // Object primitives should still be assigned
1370 std::string refl_label;
1371 context_test.getPrimitiveData(obj_UUIDs[0], "reflectivity_spectrum", refl_label);
1372 DOCTEST_CHECK(!refl_label.empty());
1373}
1374
1375DOCTEST_TEST_CASE("Nitrogen Mode - updateNitrogenBasedSpectra Without Init Error") {
1376 Context context_test;
1377 LeafOptics leafoptics(&context_test);
1378 leafoptics.disableMessages();
1379
1380 // Should throw because nitrogen mode was never initialized
1381 DOCTEST_CHECK_THROWS(leafoptics.updateNitrogenBasedSpectra());
1382}
1383
1384DOCTEST_TEST_CASE("Nitrogen Mode - updateNitrogenBasedSpectra After Init") {
1385 Context context_test;
1386 LeafOptics leafoptics(&context_test);
1387 leafoptics.disableMessages();
1388
1389 // Create objects with nitrogen data
1390 std::vector<uint> all_UUIDs;
1391 std::vector<uint> objIDs;
1392 for (int i = 0; i < 3; i++) {
1393 uint objID = context_test.addTileObject(make_vec3(float(i), 0, 0), make_vec2(0.1f, 0.1f), make_SphericalCoord(0, 0), make_int2(2, 2));
1394 objIDs.push_back(objID);
1395 context_test.setObjectData(objID, "leaf_nitrogen_gN_m2", 1.5f + float(i) * 0.5f);
1396 std::vector<uint> obj_UUIDs = context_test.getObjectPrimitiveUUIDs(objID);
1397 all_UUIDs.insert(all_UUIDs.end(), obj_UUIDs.begin(), obj_UUIDs.end());
1398 }
1399
1401 params.num_bins = 2;
1402
1403 // Initialize nitrogen mode
1404 leafoptics.run(all_UUIDs, params);
1405
1406 // updateNitrogenBasedSpectra should work now
1407 DOCTEST_CHECK_NOTHROW(leafoptics.updateNitrogenBasedSpectra());
1408}
1409
1410DOCTEST_TEST_CASE("Nitrogen Mode - Spectrum Count Bounded by num_bins") {
1411 Context context_test;
1412 LeafOptics leafoptics(&context_test);
1413 leafoptics.disableMessages();
1414
1415 // Create many objects with varying nitrogen
1416 std::vector<uint> all_UUIDs;
1417 for (int i = 0; i < 50; i++) {
1418 uint objID = context_test.addTileObject(make_vec3(float(i) * 0.2f, 0, 0), make_vec2(0.05f, 0.05f), make_SphericalCoord(0, 0), make_int2(1, 1));
1419 float N_area = 0.5f + float(i) * 0.1f; // Wide range of nitrogen values
1420 context_test.setObjectData(objID, "leaf_nitrogen_gN_m2", N_area);
1421 std::vector<uint> obj_UUIDs = context_test.getObjectPrimitiveUUIDs(objID);
1422 all_UUIDs.insert(all_UUIDs.end(), obj_UUIDs.begin(), obj_UUIDs.end());
1423 }
1424
1426 params.num_bins = 5; // Limit to 5 bins
1427
1428 leafoptics.run(all_UUIDs, params);
1429
1430 // Count unique spectra created (should be <= num_bins)
1431 uint spectrum_count = 0;
1432 for (uint i = 0; i < 100; i++) {
1433 std::string label = "leaf_reflectivity_Nauto_" + std::to_string(i);
1434 if (context_test.doesGlobalDataExist(label.c_str())) {
1435 spectrum_count++;
1436 }
1437 }
1438
1439 DOCTEST_CHECK(spectrum_count <= params.num_bins);
1440 DOCTEST_CHECK(spectrum_count > 0);
1441}
1442
1443DOCTEST_TEST_CASE("Nitrogen Mode - Empty UUID List") {
1444 Context context_test;
1445 LeafOptics leafoptics(&context_test);
1446 leafoptics.disableMessages();
1447
1448 std::vector<uint> empty_UUIDs;
1450
1451 // Should handle gracefully (no throw, no crash)
1452 DOCTEST_CHECK_NOTHROW(leafoptics.run(empty_UUIDs, params));
1453}
1454
1455DOCTEST_TEST_CASE("Nitrogen Mode - Single Object") {
1456 Context context_test;
1457 LeafOptics leafoptics(&context_test);
1458 leafoptics.disableMessages();
1459
1460 uint objID = context_test.addTileObject(make_vec3(0, 0, 0), make_vec2(0.1f, 0.1f), make_SphericalCoord(0, 0), make_int2(2, 2));
1461 context_test.setObjectData(objID, "leaf_nitrogen_gN_m2", 2.0f);
1462 std::vector<uint> UUIDs = context_test.getObjectPrimitiveUUIDs(objID);
1463
1465 params.num_bins = 5;
1466
1467 DOCTEST_CHECK_NOTHROW(leafoptics.run(UUIDs, params));
1468
1469 // Should create exactly 1 bin (since only 1 object)
1470 DOCTEST_CHECK(context_test.doesGlobalDataExist("leaf_reflectivity_Nauto_0"));
1471
1472 // Primitives should be assigned
1473 std::string refl_label;
1474 context_test.getPrimitiveData(UUIDs[0], "reflectivity_spectrum", refl_label);
1475 DOCTEST_CHECK(refl_label == "leaf_reflectivity_Nauto_0");
1476}
1477
1478DOCTEST_TEST_CASE("Nitrogen Mode - Chlorophyll Clamping Low") {
1479 Context context_test;
1480 LeafOptics leafoptics(&context_test);
1481 leafoptics.disableMessages();
1482
1483 // Create object with very low nitrogen (should clamp Cab to 5)
1484 uint objID = context_test.addTileObject(make_vec3(0, 0, 0), make_vec2(0.1f, 0.1f), make_SphericalCoord(0, 0), make_int2(2, 2));
1485 context_test.setObjectData(objID, "leaf_nitrogen_gN_m2", 0.1f); // Very low N
1486 std::vector<uint> UUIDs = context_test.getObjectPrimitiveUUIDs(objID);
1487
1489 params.num_bins = 1;
1490
1491 // Should not throw
1492 DOCTEST_CHECK_NOTHROW(leafoptics.run(UUIDs, params));
1493
1494 // Verify spectrum was created
1495 DOCTEST_CHECK(context_test.doesGlobalDataExist("leaf_reflectivity_Nauto_0"));
1496}
1497
1498DOCTEST_TEST_CASE("Nitrogen Mode - Chlorophyll Clamping High") {
1499 Context context_test;
1500 LeafOptics leafoptics(&context_test);
1501 leafoptics.disableMessages();
1502
1503 // Create object with very high nitrogen (should clamp Cab to 80)
1504 uint objID = context_test.addTileObject(make_vec3(0, 0, 0), make_vec2(0.1f, 0.1f), make_SphericalCoord(0, 0), make_int2(2, 2));
1505 context_test.setObjectData(objID, "leaf_nitrogen_gN_m2", 10.0f); // Very high N
1506 std::vector<uint> UUIDs = context_test.getObjectPrimitiveUUIDs(objID);
1507
1509 params.num_bins = 1;
1510
1511 // Should not throw
1512 DOCTEST_CHECK_NOTHROW(leafoptics.run(UUIDs, params));
1513
1514 // Verify spectrum was created
1515 DOCTEST_CHECK(context_test.doesGlobalDataExist("leaf_reflectivity_Nauto_0"));
1516}
1517
1518DOCTEST_TEST_CASE("Nitrogen Mode - Chlorophyll Calculation Verification") {
1519 // Verify the nitrogen-to-chlorophyll conversion formula:
1520 // Cab (ug/cm2) = N_area (g/m2) * 100 * f_photosynthetic * N_to_Cab_coefficient
1521 // With defaults: f_photosynthetic=0.5, N_to_Cab_coefficient=0.4
1522 // N=2.0 g/m2 -> Cab = 2.0 * 100 * 0.5 * 0.4 = 40 ug/cm2
1523
1524 Context context_test;
1525 LeafOptics leafoptics(&context_test);
1526 leafoptics.disableMessages();
1527
1528 // Enable chlorophyll output so we can verify
1529 leafoptics.optionalOutputPrimitiveData("chlorophyll");
1530 leafoptics.optionalOutputPrimitiveData("carotenoid");
1531
1532 uint objID = context_test.addTileObject(make_vec3(0, 0, 0), make_vec2(0.1f, 0.1f), make_SphericalCoord(0, 0), make_int2(2, 2));
1533 context_test.setObjectData(objID, "leaf_nitrogen_gN_m2", 2.0f);
1534 std::vector<uint> UUIDs = context_test.getObjectPrimitiveUUIDs(objID);
1535
1537 params.f_photosynthetic = 0.5f;
1538 params.N_to_Cab_coefficient = 0.4f;
1539 params.Car_to_Cab_ratio = 0.25f;
1540 params.num_bins = 1;
1541
1542 leafoptics.run(UUIDs, params);
1543
1544 // Use getPropertiesFromSpectrum to retrieve the PROSPECT parameters
1545 leafoptics.getPropertiesFromSpectrum(UUIDs);
1546
1547 // Check chlorophyll: 2.0 * 100 * 0.5 * 0.4 = 40.0 ug/cm2
1548 float chl;
1549 context_test.getPrimitiveData(UUIDs[0], "chlorophyll", chl);
1550 DOCTEST_CHECK(chl == doctest::Approx(40.0f).epsilon(0.01f));
1551
1552 // Check carotenoid: 40.0 * 0.25 = 10.0 ug/cm2
1553 float car;
1554 context_test.getPrimitiveData(UUIDs[0], "carotenoid", car);
1555 DOCTEST_CHECK(car == doctest::Approx(10.0f).epsilon(0.01f));
1556}
1557
1558DOCTEST_TEST_CASE("Nitrogen Mode - Hysteresis Prevents Oscillation") {
1559 // Test that a leaf at a bin boundary doesn't flip-flop between bins
1560 // due to the hysteresis mechanism
1561
1562 Context context_test;
1563 LeafOptics leafoptics(&context_test);
1564 leafoptics.disableMessages();
1565
1566 // Create two objects with distinctly different nitrogen to create 2 bins
1567 std::vector<uint> all_UUIDs;
1568 std::vector<uint> objIDs;
1569
1570 // Object 0: low nitrogen
1571 uint obj0 = context_test.addTileObject(make_vec3(0, 0, 0), make_vec2(0.1f, 0.1f), make_SphericalCoord(0, 0), make_int2(2, 2));
1572 context_test.setObjectData(obj0, "leaf_nitrogen_gN_m2", 1.0f);
1573 objIDs.push_back(obj0);
1574 std::vector<uint> UUIDs0 = context_test.getObjectPrimitiveUUIDs(obj0);
1575 all_UUIDs.insert(all_UUIDs.end(), UUIDs0.begin(), UUIDs0.end());
1576
1577 // Object 1: high nitrogen
1578 uint obj1 = context_test.addTileObject(make_vec3(1, 0, 0), make_vec2(0.1f, 0.1f), make_SphericalCoord(0, 0), make_int2(2, 2));
1579 context_test.setObjectData(obj1, "leaf_nitrogen_gN_m2", 3.0f);
1580 objIDs.push_back(obj1);
1581 std::vector<uint> UUIDs1 = context_test.getObjectPrimitiveUUIDs(obj1);
1582 all_UUIDs.insert(all_UUIDs.end(), UUIDs1.begin(), UUIDs1.end());
1583
1585 params.num_bins = 2;
1586 params.reassignment_threshold = 0.3f;
1587 params.min_reassignment_change = 0.3f;
1588
1589 // First call creates bins at ~1.0 and ~3.0
1590 leafoptics.run(all_UUIDs, params);
1591
1592 // Record initial assignment for object 0
1593 std::string initial_label;
1594 context_test.getPrimitiveData(UUIDs0[0], "reflectivity_spectrum", initial_label);
1595
1596 // Move object 0 to boundary region (2.0 - midpoint between bins)
1597 // This is within reassignment threshold of bin center 1.0
1598 // but hysteresis should prevent oscillation
1599 context_test.setObjectData(obj0, "leaf_nitrogen_gN_m2", 1.5f);
1600 leafoptics.run(all_UUIDs, params);
1601
1602 std::string after_small_change;
1603 context_test.getPrimitiveData(UUIDs0[0], "reflectivity_spectrum", after_small_change);
1604
1605 // Small change should NOT trigger reassignment due to hysteresis
1606 // (relative change is 50% but improvement may not be significant)
1607 // The key is that repeated calls with same value don't oscillate
1608
1609 // Call again with same nitrogen - should be stable
1610 leafoptics.run(all_UUIDs, params);
1611 std::string after_repeat;
1612 context_test.getPrimitiveData(UUIDs0[0], "reflectivity_spectrum", after_repeat);
1613
1614 // Assignment should be stable (no oscillation)
1615 DOCTEST_CHECK(after_small_change == after_repeat);
1616}
1617
1618DOCTEST_TEST_CASE("Nitrogen Mode - All Same Nitrogen Values") {
1619 // When all objects have identical nitrogen, only 1 bin should be created
1620 // regardless of num_bins setting
1621
1622 Context context_test;
1623 LeafOptics leafoptics(&context_test);
1624 leafoptics.disableMessages();
1625
1626 std::vector<uint> all_UUIDs;
1627 const float same_nitrogen = 2.0f;
1628
1629 // Create 10 objects all with identical nitrogen
1630 for (int i = 0; i < 10; i++) {
1631 uint objID = context_test.addTileObject(make_vec3(float(i) * 0.2f, 0, 0), make_vec2(0.05f, 0.05f), make_SphericalCoord(0, 0), make_int2(1, 1));
1632 context_test.setObjectData(objID, "leaf_nitrogen_gN_m2", same_nitrogen);
1633 std::vector<uint> obj_UUIDs = context_test.getObjectPrimitiveUUIDs(objID);
1634 all_UUIDs.insert(all_UUIDs.end(), obj_UUIDs.begin(), obj_UUIDs.end());
1635 }
1636
1638 params.num_bins = 5; // Request 5 bins, but should only create 1
1639
1640 leafoptics.run(all_UUIDs, params);
1641
1642 // Count how many bins were actually created
1643 uint spectrum_count = 0;
1644 for (uint i = 0; i < 10; i++) {
1645 std::string label = "leaf_reflectivity_Nauto_" + std::to_string(i);
1646 if (context_test.doesGlobalDataExist(label.c_str())) {
1647 spectrum_count++;
1648 }
1649 }
1650
1651 // Should only have 1 bin since all nitrogen values are identical
1652 DOCTEST_CHECK(spectrum_count == 1);
1653
1654 // All primitives should be assigned to the same spectrum
1655 std::string first_label;
1656 context_test.getPrimitiveData(all_UUIDs[0], "reflectivity_spectrum", first_label);
1657
1658 for (uint UUID: all_UUIDs) {
1659 std::string label;
1660 context_test.getPrimitiveData(UUID, "reflectivity_spectrum", label);
1661 DOCTEST_CHECK(label == first_label);
1662 }
1663}
1664
1665DOCTEST_TEST_CASE("Nitrogen Mode - Verify Spectrum Physical Properties") {
1666 // Verify that nitrogen-derived spectra have physically reasonable properties
1667
1668 Context context_test;
1669 LeafOptics leafoptics(&context_test);
1670 leafoptics.disableMessages();
1671
1672 uint objID = context_test.addTileObject(make_vec3(0, 0, 0), make_vec2(0.1f, 0.1f), make_SphericalCoord(0, 0), make_int2(2, 2));
1673 context_test.setObjectData(objID, "leaf_nitrogen_gN_m2", 2.0f);
1674 std::vector<uint> UUIDs = context_test.getObjectPrimitiveUUIDs(objID);
1675
1677 params.num_bins = 1;
1678
1679 leafoptics.run(UUIDs, params);
1680
1681 // Retrieve the generated spectrum
1682 std::vector<vec2> refl_data, trans_data;
1683 context_test.getGlobalData("leaf_reflectivity_Nauto_0", refl_data);
1684 context_test.getGlobalData("leaf_transmissivity_Nauto_0", trans_data);
1685
1686 // Check spectrum size (400-2500nm at 1nm resolution = 2101 points)
1687 DOCTEST_CHECK(refl_data.size() == 2101);
1688 DOCTEST_CHECK(trans_data.size() == 2101);
1689
1690 // Check physical bounds: 0 <= R, T <= 1
1691 for (size_t i = 0; i < refl_data.size(); i++) {
1692 DOCTEST_CHECK(refl_data[i].y >= 0.0f);
1693 DOCTEST_CHECK(refl_data[i].y <= 1.0f);
1694 DOCTEST_CHECK(trans_data[i].y >= 0.0f);
1695 DOCTEST_CHECK(trans_data[i].y <= 1.0f);
1696
1697 // Energy conservation: R + T <= 1 (with small tolerance)
1698 DOCTEST_CHECK(refl_data[i].y + trans_data[i].y <= 1.01f);
1699 }
1700
1701 // Check wavelength range
1702 DOCTEST_CHECK(refl_data.front().x == doctest::Approx(400.0f).epsilon(0.01f));
1703 DOCTEST_CHECK(refl_data.back().x == doctest::Approx(2500.0f).epsilon(0.01f));
1704}
1705
1706DOCTEST_TEST_CASE("LeafOptics Zero Absorption - PROSPECT-PRO with no water") {
1707 // Regression test: when every absorption term vanishes, the mean absorption
1708 // coefficient k is exactly zero. The reference PROSPECT implementation sets
1709 // tau = 1 in that case (zero absorption => perfect transmission through the
1710 // elementary layer). Returning tau = 0 instead drives the internal
1711 // transmissivity t to zero, which divides by zero in the Stokes solution and
1712 // produces NaN reflectance/transmittance.
1713 //
1714 // absorption_proteins is identically zero over 400-1432 nm, and PROSPECT-PRO
1715 // mode forces drymass to zero, so this is reachable with legal inputs.
1716
1717 Context context_test;
1718 LeafOptics leafoptics(&context_test);
1719 leafoptics.disableMessages();
1720
1722 props.chlorophyllcontent = 0.0f;
1723 props.carotenoidcontent = 0.0f;
1724 props.anthocyancontent = 0.0f;
1725 props.brownpigments = 0.0f;
1726 props.watermass = 0.0f; // no water absorption
1727 props.drymass = 0.0f;
1728 props.protein = 0.001f; // enables PROSPECT-PRO mode
1729 props.carbonconstituents = 0.0f;
1730
1731 std::vector<vec2> reflectivities, transmissivities;
1732 leafoptics.getLeafSpectra(props, reflectivities, transmissivities);
1733
1734 DOCTEST_CHECK(reflectivities.size() == 2101);
1735 DOCTEST_CHECK(transmissivities.size() == 2101);
1736
1737 // Every point must be finite and physically bounded.
1738 for (size_t i = 0; i < reflectivities.size(); i++) {
1739 DOCTEST_CHECK(std::isfinite(reflectivities.at(i).y));
1740 DOCTEST_CHECK(std::isfinite(transmissivities.at(i).y));
1741 DOCTEST_CHECK(reflectivities.at(i).y >= 0.0f);
1742 DOCTEST_CHECK(reflectivities.at(i).y <= 1.0f);
1743 DOCTEST_CHECK(transmissivities.at(i).y >= 0.0f);
1744 DOCTEST_CHECK(transmissivities.at(i).y <= 1.0f);
1745 DOCTEST_CHECK(reflectivities.at(i).y + transmissivities.at(i).y <= 1.01f);
1746 }
1747
1748 // In the non-absorbing limit the leaf must transmit nearly all incident
1749 // radiation. Sample 1400 nm (index 1000), inside the protein-only zero-k window.
1750 DOCTEST_CHECK(transmissivities.at(1000).y > 0.4f);
1751 DOCTEST_CHECK(reflectivities.at(1000).y + transmissivities.at(1000).y > 0.99f);
1752}
1753
1754DOCTEST_TEST_CASE("LeafOptics Spectra Are Finite For All Library Species") {
1755 // Broad guard: no species in the built-in library may produce non-finite or
1756 // out-of-range spectra.
1757
1758 Context context_test;
1759 LeafOptics leafoptics(&context_test);
1760 leafoptics.disableMessages();
1761
1762 std::vector<std::string> species = {"default", "garden_lettuce", "alfalfa", "corn", "sunflower", "english_walnut", "rice", "soybean", "wine_grape", "tomato", "common_bean", "cowpea"};
1763
1764 for (const auto &name: species) {
1766 leafoptics.getPropertiesFromLibrary(name, props);
1767
1768 std::vector<vec2> reflectivities, transmissivities;
1769 leafoptics.getLeafSpectra(props, reflectivities, transmissivities);
1770
1771 DOCTEST_CHECK(reflectivities.size() == 2101);
1772 DOCTEST_CHECK(transmissivities.size() == 2101);
1773
1774 for (size_t i = 0; i < reflectivities.size(); i++) {
1775 DOCTEST_CHECK(std::isfinite(reflectivities.at(i).y));
1776 DOCTEST_CHECK(std::isfinite(transmissivities.at(i).y));
1777 DOCTEST_CHECK(reflectivities.at(i).y >= 0.0f);
1778 DOCTEST_CHECK(reflectivities.at(i).y <= 1.0f);
1779 DOCTEST_CHECK(transmissivities.at(i).y >= 0.0f);
1780 DOCTEST_CHECK(transmissivities.at(i).y <= 1.0f);
1781 }
1782 }
1783}
1784
1785DOCTEST_TEST_CASE("LeafOptics Transmittance Continuity Across Branch Boundaries") {
1786 // Regression test: the exponential-integral approximation in transmittance()
1787 // is evaluated over separate k intervals. Boundary values (k == 4, k == 85)
1788 // must not fall through to a degenerate return. The absorption coefficient k
1789 // is swept by scaling chlorophyll content, and the resulting spectra must vary
1790 // smoothly - a dropped boundary shows up as a discontinuous jump.
1791
1792 Context context_test;
1793 LeafOptics leafoptics(&context_test);
1794 leafoptics.disableMessages();
1795
1796 // Sweep chlorophyll finely so that k crosses 4.0 at many wavelengths.
1797 float previous_mean = -1.0f;
1798 for (int step = 0; step <= 40; step++) {
1800 props.chlorophyllcontent = 5.0f + static_cast<float>(step) * 2.5f;
1801
1802 std::vector<vec2> reflectivities, transmissivities;
1803 leafoptics.getLeafSpectra(props, reflectivities, transmissivities);
1804
1805 float mean_transmittance = 0.0f;
1806 for (const auto &value: transmissivities) {
1807 DOCTEST_CHECK(std::isfinite(value.y));
1808 mean_transmittance += value.y;
1809 }
1810 mean_transmittance /= static_cast<float>(transmissivities.size());
1811
1812 // Increasing chlorophyll must monotonically decrease mean transmittance,
1813 // with no abrupt jumps from a dropped branch boundary.
1814 if (previous_mean >= 0.0f) {
1815 DOCTEST_CHECK(mean_transmittance <= previous_mean + 1e-4f);
1816 DOCTEST_CHECK(std::abs(mean_transmittance - previous_mean) < 0.05f);
1817 }
1818 previous_mean = mean_transmittance;
1819 }
1820}
1821
1822DOCTEST_TEST_CASE("LeafOptics Output Vectors Are Overwritten Not Appended") {
1823 // Regression test: getLeafSpectra() takes its spectra as [out] parameters, so
1824 // reusing the same vectors across calls must overwrite rather than append.
1825
1826 Context context_test;
1827 LeafOptics leafoptics(&context_test);
1828 leafoptics.disableMessages();
1829
1831 props.chlorophyllcontent = 40.0f;
1832
1833 std::vector<vec2> reflectivities, transmissivities;
1834 leafoptics.getLeafSpectra(props, reflectivities, transmissivities);
1835
1836 DOCTEST_CHECK(reflectivities.size() == 2101);
1837 DOCTEST_CHECK(transmissivities.size() == 2101);
1838
1839 // Second call with the same vectors must not grow them.
1840 leafoptics.getLeafSpectra(props, reflectivities, transmissivities);
1841
1842 DOCTEST_CHECK(reflectivities.size() == 2101);
1843 DOCTEST_CHECK(transmissivities.size() == 2101);
1844
1845 // Wavelength axis must still be correct (not restarted mid-vector).
1846 DOCTEST_CHECK(reflectivities.front().x == doctest::Approx(400.0f).epsilon(0.01f));
1847 DOCTEST_CHECK(reflectivities.back().x == doctest::Approx(2500.0f).epsilon(0.01f));
1848 DOCTEST_CHECK(transmissivities.front().x == doctest::Approx(400.0f).epsilon(0.01f));
1849 DOCTEST_CHECK(transmissivities.back().x == doctest::Approx(2500.0f).epsilon(0.01f));
1850}
1851
1852DOCTEST_TEST_CASE("LeafOptics Invalid Input Parameters Throw") {
1853 // PROSPECT requires the structure parameter N >= 1 (it counts elementary
1854 // layers). N < 1 makes the Stokes exponent negative and N == 0 divides by
1855 // zero when computing k. Negative constituent contents are equally unphysical.
1856
1857 Context context_test;
1858 LeafOptics leafoptics(&context_test);
1859 leafoptics.disableMessages();
1860
1861 std::vector<vec2> reflectivities, transmissivities;
1862
1863 {
1864 capture_cerr capture;
1865
1866 LeafOpticsProperties zero_layers;
1867 zero_layers.numberlayers = 0.0f;
1868 DOCTEST_CHECK_THROWS(leafoptics.getLeafSpectra(zero_layers, reflectivities, transmissivities));
1869
1870 LeafOpticsProperties sub_unit_layers;
1871 sub_unit_layers.numberlayers = 0.5f;
1872 DOCTEST_CHECK_THROWS(leafoptics.getLeafSpectra(sub_unit_layers, reflectivities, transmissivities));
1873
1874 LeafOpticsProperties negative_chlorophyll;
1875 negative_chlorophyll.chlorophyllcontent = -10.0f;
1876 DOCTEST_CHECK_THROWS(leafoptics.getLeafSpectra(negative_chlorophyll, reflectivities, transmissivities));
1877
1878 LeafOpticsProperties negative_water;
1879 negative_water.watermass = -0.01f;
1880 DOCTEST_CHECK_THROWS(leafoptics.getLeafSpectra(negative_water, reflectivities, transmissivities));
1881 }
1882
1883 // N exactly 1 is the valid lower bound (a single compact layer).
1884 LeafOpticsProperties single_layer;
1885 single_layer.numberlayers = 1.0f;
1886 DOCTEST_CHECK_NOTHROW(leafoptics.getLeafSpectra(single_layer, reflectivities, transmissivities));
1887 for (size_t i = 0; i < reflectivities.size(); i++) {
1888 DOCTEST_CHECK(std::isfinite(reflectivities.at(i).y));
1889 DOCTEST_CHECK(std::isfinite(transmissivities.at(i).y));
1890 }
1891}
1892
1893DOCTEST_TEST_CASE("LeafOptics PROSPECT Direct Entry Point Validates Inputs") {
1894 // PROSPECT() is a spectrum-computing entry point in its own right, so it must reject
1895 // the same unphysical inputs getLeafSpectra() rejects. numberlayers divides the mean
1896 // absorption coefficient k, so N == 0 divides by zero and produces non-finite spectra
1897 // rather than an error.
1898
1899 Context context_test;
1900 LeafOptics leafoptics(&context_test);
1901 leafoptics.disableMessages();
1902
1903 std::vector<float> reflectivities, transmissivities;
1904
1905 {
1906 capture_cerr capture;
1907
1908 // N == 0 divides by zero when computing k.
1909 DOCTEST_CHECK_THROWS(leafoptics.PROSPECT(0.0f, 30.f, 10.f, 0.f, 0.f, 0.01f, 0.005f, 0.f, 0.f, reflectivities, transmissivities));
1910
1911 // N < 1 inverts the layer stack via the negative Stokes exponent.
1912 DOCTEST_CHECK_THROWS(leafoptics.PROSPECT(0.5f, 30.f, 10.f, 0.f, 0.f, 0.01f, 0.005f, 0.f, 0.f, reflectivities, transmissivities));
1913
1914 // Negative constituent contents would contribute negative absorption.
1915 DOCTEST_CHECK_THROWS(leafoptics.PROSPECT(1.5f, -10.f, 10.f, 0.f, 0.f, 0.01f, 0.005f, 0.f, 0.f, reflectivities, transmissivities));
1916
1917 DOCTEST_CHECK_THROWS(leafoptics.PROSPECT(1.5f, 30.f, 10.f, 0.f, 0.f, -0.01f, 0.005f, 0.f, 0.f, reflectivities, transmissivities));
1918 }
1919
1920 // Valid inputs must still compute a finite spectrum.
1921 DOCTEST_CHECK_NOTHROW(leafoptics.PROSPECT(1.5f, 30.f, 10.f, 0.f, 0.f, 0.01f, 0.005f, 0.f, 0.f, reflectivities, transmissivities));
1922 DOCTEST_CHECK(!reflectivities.empty());
1923 for (size_t i = 0; i < reflectivities.size(); i++) {
1924 DOCTEST_CHECK(std::isfinite(reflectivities.at(i)));
1925 DOCTEST_CHECK(std::isfinite(transmissivities.at(i)));
1926 }
1927}
1928
1929int LeafOptics::selfTest(int argc, char **argv) {
1930 return helios::runDoctestWithValidation(argc, argv);
1931}