7#define DOCTEST_CONFIG_IMPLEMENT
9#include "doctest_utils.h"
12using namespace helios;
16DOCTEST_TEST_CASE(
"PhotosynthesisModel Farquhar Model Type Setting") {
21 DOCTEST_CHECK_NOTHROW(photomodel.setModelType_Farquhar());
24 DOCTEST_CHECK(coeffs.Vcmax == -1.0f);
25 DOCTEST_CHECK(coeffs.Jmax == -1.0f);
28DOCTEST_TEST_CASE(
"PhotosynthesisModel Light Response Curve - Farquhar Model") {
33 float Qin[9] = {0, 50, 100, 200, 400, 800, 1200, 1500, 2000};
34 std::vector<float> AQ_expected{-2.37932, 8.29752, 12.5566, 16.2075, 16.7448, 16.7448, 16.7448, 16.7448, 16.7448};
40 fcoeffs.
setRd(2.12f, 46.39f);
43 DOCTEST_CHECK_NOTHROW(photomodel.setModelCoefficients(fcoeffs));
45 for (
int i = 0; i < 9; i++) {
46 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"radiation_flux_PAR", Qin[i]));
47 DOCTEST_CHECK_NOTHROW(photomodel.run());
50 DOCTEST_CHECK_NOTHROW(context_test.
getPrimitiveData(UUID,
"net_photosynthesis", A));
51 DOCTEST_CHECK(A == doctest::Approx(AQ_expected[i]).epsilon(err_tol));
55DOCTEST_TEST_CASE(
"PhotosynthesisModel CO2 Response Curve - Farquhar Model") {
60 float CO2[9] = {100, 200, 300, 400, 500, 600, 700, 800, 1000};
61 std::vector<float> ACi_expected{1.72714, 7.32672, 12.4749, 17.199, 21.5281, 25.4923, 28.4271, 29.656, 31.3813};
66 fcoeffs.
setRd(2.12f, 46.39f);
69 DOCTEST_CHECK_NOTHROW(photomodel.setModelCoefficients(fcoeffs));
70 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"radiation_flux_PAR", 2000.0f));
72 for (
int i = 0; i < 9; i++) {
73 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"air_CO2", CO2[i]));
74 DOCTEST_CHECK_NOTHROW(photomodel.run());
77 DOCTEST_CHECK_NOTHROW(context_test.
getPrimitiveData(UUID,
"net_photosynthesis", A));
78 DOCTEST_CHECK(A == doctest::Approx(ACi_expected[i]).epsilon(err_tol));
82DOCTEST_TEST_CASE(
"PhotosynthesisModel Temperature Response Curve - Farquhar Model") {
87 float TL[7] = {270, 280, 290, 300, 310, 320, 330};
88 std::vector<float> AT_expected{3.87863, 8.74928, 14.4075, 17.199, 16.0928, 11.6431, 4.00821};
93 fcoeffs.
setRd(2.12f, 46.39f);
96 DOCTEST_CHECK_NOTHROW(photomodel.setModelCoefficients(fcoeffs));
97 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"radiation_flux_PAR", 2000.0f));
98 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"air_CO2", 400.0f));
100 for (
int i = 0; i < 7; i++) {
101 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"temperature", TL[i]));
102 DOCTEST_CHECK_NOTHROW(photomodel.run());
105 DOCTEST_CHECK_NOTHROW(context_test.
getPrimitiveData(UUID,
"net_photosynthesis", A));
106 DOCTEST_CHECK(A == doctest::Approx(AT_expected[i]).epsilon(err_tol));
110DOCTEST_TEST_CASE(
"PhotosynthesisModel Farquhar gm setter overloads") {
114 DOCTEST_CHECK(std::isinf(fq.getMesophyllConductance_gmTempResponse().value_at_25C));
116 SUBCASE(
"Constant gm") {
118 DOCTEST_CHECK(fq.getMesophyllConductance_gmTempResponse().value_at_25C == doctest::Approx(0.5f));
119 DOCTEST_CHECK(fq.getMesophyllConductance_gmTempResponse().dHa == doctest::Approx(0.f));
121 SUBCASE(
"Arrhenius gm") {
123 DOCTEST_CHECK(fq.getMesophyllConductance_gmTempResponse().value_at_25C == doctest::Approx(0.8f));
124 DOCTEST_CHECK(fq.getMesophyllConductance_gmTempResponse().dHa == doctest::Approx(50.f));
126 SUBCASE(
"Peaked Arrhenius gm") {
128 DOCTEST_CHECK(fq.getMesophyllConductance_gmTempResponse().value_at_25C == doctest::Approx(0.4f));
129 DOCTEST_CHECK(fq.getMesophyllConductance_gmTempResponse().Topt == doctest::Approx(273.15f + 35.f));
131 SUBCASE(
"Peaked Arrhenius gm with explicit dHd") {
133 DOCTEST_CHECK(fq.getMesophyllConductance_gmTempResponse().dHd == doctest::Approx(420.f));
137DOCTEST_TEST_CASE(
"PhotosynthesisModel Farquhar default gm = infinity preserves legacy A") {
142 const float strict_tol = 1e-5f;
144 auto make_coeffs = []() {
147 fcoeffs.
setJmax(150.f, 43.54f);
148 fcoeffs.
setRd(2.12f, 46.39f);
153 SUBCASE(
"Light response") {
157 float Qin[9] = {0, 50, 100, 200, 400, 800, 1200, 1500, 2000};
158 std::vector<float> AQ_expected{-2.37932f, 8.29752f, 12.5566f, 16.2075f, 16.7448f, 16.7448f, 16.7448f, 16.7448f, 16.7448f};
160 photomodel.setModelCoefficients(fcoeffs);
161 for (
int i = 0; i < 9; i++) {
166 DOCTEST_CHECK(A == doctest::Approx(AQ_expected[i]).epsilon(strict_tol));
169 SUBCASE(
"CO2 response") {
173 float CO2[9] = {100, 200, 300, 400, 500, 600, 700, 800, 1000};
174 std::vector<float> ACi_expected{1.72714f, 7.32672f, 12.4749f, 17.199f, 21.5281f, 25.4923f, 28.4271f, 29.656f, 31.3813f};
176 photomodel.setModelCoefficients(fcoeffs);
178 for (
int i = 0; i < 9; i++) {
183 DOCTEST_CHECK(A == doctest::Approx(ACi_expected[i]).epsilon(strict_tol));
186 SUBCASE(
"Temperature response") {
190 float TL[7] = {270, 280, 290, 300, 310, 320, 330};
191 std::vector<float> AT_expected{3.87863f, 8.74928f, 14.4075f, 17.199f, 16.0928f, 11.6431f, 4.00821f};
193 photomodel.setModelCoefficients(fcoeffs);
196 for (
int i = 0; i < 7; i++) {
201 DOCTEST_CHECK(A == doctest::Approx(AT_expected[i]).epsilon(strict_tol));
206DOCTEST_TEST_CASE(
"PhotosynthesisModel Farquhar very large gm reproduces legacy") {
217 legacy_coeffs.
setVcmax(80.f, 65.33f);
218 legacy_coeffs.
setJmax(150.f, 43.54f);
219 legacy_coeffs.
setRd(2.0f, 46.39f);
225 photo_legacy.setModelCoefficients(legacy_coeffs);
226 photo_gm.setModelCoefficients(gm_coeffs);
229 float Ci_sweep[5] = {200.f, 400.f, 600.f, 800.f, 1000.f};
230 for (
float CO2: Ci_sweep) {
238 DOCTEST_CHECK(A_gm == doctest::Approx(A_legacy).epsilon(1e-3));
242DOCTEST_TEST_CASE(
"PhotosynthesisModel Farquhar gm finite regression") {
252 fcoeffs.
setJmax(150.f, 43.54f);
253 fcoeffs.
setRd(2.f, 46.39f);
257 photomodel.setModelCoefficients(fcoeffs);
260 float CO2_sweep[6] = {100.f, 200.f, 400.f, 600.f, 800.f, 1000.f};
264 float A_expected[6] = {1.43302f, 5.90503f, 14.192f, 21.6148f, 27.8598f, 30.2768f};
265 for (
int i = 0; i < 6; ++i) {
270 DOCTEST_CHECK(A == doctest::Approx(A_expected[i]).epsilon(1e-3));
274DOCTEST_TEST_CASE(
"PhotosynthesisModel Empirical Model Coefficients") {
280 emp_coeffs.Asat = 20.0f;
282 DOCTEST_CHECK_NOTHROW(photomodel.setModelCoefficients(emp_coeffs));
285 DOCTEST_CHECK(retrieved_coeffs.Asat == doctest::Approx(20.0f).epsilon(err_tol));
288DOCTEST_TEST_CASE(
"PhotosynthesisModel Empirical Model Coefficients with UUIDs") {
294 emp_coeffs.Asat = 20.0f;
295 std::vector<uint> UUIDs = {UUID};
297 DOCTEST_CHECK_NOTHROW(photomodel.setModelCoefficients(emp_coeffs, UUIDs));
300 DOCTEST_CHECK(retrieved_coeffs.Asat == doctest::Approx(20.0f).epsilon(err_tol));
303DOCTEST_TEST_CASE(
"PhotosynthesisModel Farquhar Model Coefficients with UUIDs") {
309 farq_coeffs.Vcmax = 90.0f;
310 std::vector<uint> UUIDs = {UUID};
312 DOCTEST_CHECK_NOTHROW(photomodel.setModelCoefficients(farq_coeffs, UUIDs));
315 DOCTEST_CHECK(retrieved_coeffs.Vcmax == doctest::Approx(90.0f).epsilon(err_tol));
318DOCTEST_TEST_CASE(
"PhotosynthesisModel Invalid Input Handling") {
325 DOCTEST_CHECK_NOTHROW(photomodel.setModelCoefficients(fcoeffs));
328 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"radiation_flux_PAR", -50.0f));
329 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"temperature", 150.0f));
330 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"air_CO2", -10.0f));
333 photomodel.disableMessages();
334 DOCTEST_CHECK_NOTHROW(photomodel.run());
335 photomodel.enableMessages();
338 DOCTEST_CHECK_NOTHROW(context_test.
getPrimitiveData(UUID,
"net_photosynthesis", A_invalid));
341 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"radiation_flux_PAR", 0.0f));
342 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"temperature", 300.0f));
343 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"air_CO2", 390.0f));
345 DOCTEST_CHECK_NOTHROW(photomodel.run());
348 DOCTEST_CHECK_NOTHROW(context_test.
getPrimitiveData(UUID,
"net_photosynthesis", A_expected));
350 DOCTEST_CHECK(A_invalid == doctest::Approx(A_expected).epsilon(err_tol));
353DOCTEST_TEST_CASE(
"PhotosynthesisModel Optional Output Primitive Data") {
358 DOCTEST_CHECK_NOTHROW(photomodel.optionalOutputPrimitiveData(
"Ci"));
359 DOCTEST_CHECK_NOTHROW(photomodel.optionalOutputPrimitiveData(
"limitation_state"));
360 DOCTEST_CHECK_NOTHROW(photomodel.optionalOutputPrimitiveData(
"Gamma_CO2"));
361 DOCTEST_CHECK_NOTHROW(photomodel.optionalOutputPrimitiveData(
"electron_transport_ratio"));
363 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"radiation_flux_PAR", 400.0f));
364 DOCTEST_CHECK_NOTHROW(photomodel.run());
366 float Ci, Gamma, J_over_Jmax;
367 int limitation_state;
369 DOCTEST_CHECK_NOTHROW(context_test.
getPrimitiveData(UUID,
"limitation_state", limitation_state));
370 DOCTEST_CHECK_NOTHROW(context_test.
getPrimitiveData(UUID,
"Gamma_CO2", Gamma));
371 DOCTEST_CHECK_NOTHROW(context_test.
getPrimitiveData(UUID,
"electron_transport_ratio", J_over_Jmax));
373 DOCTEST_CHECK(Ci != 0.0f);
374 DOCTEST_CHECK(Gamma != 0.0f);
375 DOCTEST_CHECK(J_over_Jmax >= 0.0f);
376 DOCTEST_CHECK(J_over_Jmax <= 1.0f);
379DOCTEST_TEST_CASE(
"PhotosynthesisModel Print Default Value Report") {
384 photomodel.disableMessages();
388 DOCTEST_CHECK_NOTHROW(photomodel.printDefaultValueReport());
392 DOCTEST_CHECK_MESSAGE(report_output.find(
"Photosynthesis Model Default Value Report") != std::string::npos,
"Default value report should be generated");
395 photomodel.enableMessages();
398DOCTEST_TEST_CASE(
"PhotosynthesisModel Empirical Model Type Setting") {
403 DOCTEST_CHECK_NOTHROW(photomodel.setModelType_Empirical());
407 DOCTEST_CHECK(coeffs.Asat > 0);
410DOCTEST_TEST_CASE(
"PhotosynthesisModel Vector Coefficients with Size Mismatch") {
416 std::vector<FarquharModelCoefficients> coeffs_vector(2);
417 std::vector<uint> UUIDs = {UUID1};
423 DOCTEST_CHECK_NOTHROW(photomodel.setModelCoefficients(coeffs_vector, UUIDs));
427 DOCTEST_CHECK_MESSAGE(captured_warnings.find(
"number of model coefficients (2) does not match number of UUIDs (1)") != std::string::npos,
"Size mismatch should produce warning message");
430DOCTEST_TEST_CASE(
"PhotosynthesisModel Vector Coefficients Matching Size") {
436 std::vector<FarquharModelCoefficients> coeffs_vector(2);
437 coeffs_vector[0].setVcmax(80.0f);
438 coeffs_vector[1].setVcmax(90.0f);
439 std::vector<uint> UUIDs = {UUID1, UUID2};
441 DOCTEST_CHECK_NOTHROW(photomodel.setModelCoefficients(coeffs_vector, UUIDs));
445 DOCTEST_CHECK(retrieved1.getVcmaxTempResponse().value_at_25C == doctest::Approx(80.0f).epsilon(err_tol));
446 DOCTEST_CHECK(retrieved2.getVcmaxTempResponse().value_at_25C == doctest::Approx(90.0f).epsilon(err_tol));
449DOCTEST_TEST_CASE(
"PhotosynthesisModel Library Species") {
455 photomodel.disableMessages();
458 DOCTEST_CHECK_NOTHROW(photomodel.getFarquharCoefficientsFromLibrary(
"Almond"));
461 DOCTEST_CHECK_NOTHROW(photomodel.setFarquharCoefficientsFromLibrary(
"Almond"));
464 std::vector<uint> UUIDs = {UUID};
465 DOCTEST_CHECK_NOTHROW(photomodel.setFarquharCoefficientsFromLibrary(
"Apple", UUIDs));
468 photomodel.enableMessages();
472 DOCTEST_CHECK(almond_coeffs.getVcmaxTempResponse().value_at_25C == doctest::Approx(101.08f).epsilon(err_tol));
475DOCTEST_TEST_CASE(
"PhotosynthesisModel Message Control") {
480 DOCTEST_CHECK_NOTHROW(photomodel.disableMessages());
481 DOCTEST_CHECK_NOTHROW(photomodel.enableMessages());
484DOCTEST_TEST_CASE(
"PhotosynthesisModel Default Value Reports") {
491 photomodel.disableMessages();
495 DOCTEST_CHECK_NOTHROW(photomodel.printDefaultValueReport());
497 DOCTEST_CHECK_MESSAGE(report_all.find(
"Photosynthesis Model Default Value Report") != std::string::npos,
"Default value report should be generated for all primitives");
501 std::vector<uint> UUIDs = {UUID1, UUID2};
502 DOCTEST_CHECK_NOTHROW(photomodel.printDefaultValueReport(UUIDs));
504 DOCTEST_CHECK_MESSAGE(report_subset.find(
"Photosynthesis Model Default Value Report") != std::string::npos,
"Default value report should be generated for specific UUIDs");
507 photomodel.enableMessages();
510DOCTEST_TEST_CASE(
"PhotosynthesisModel Run with Specific UUIDs") {
521 photomodel.disableMessages();
524 std::vector<uint> UUIDs = {UUID1};
525 DOCTEST_CHECK_NOTHROW(photomodel.run(UUIDs));
528 DOCTEST_CHECK_NOTHROW(context_test.
getPrimitiveData(UUID1,
"net_photosynthesis", A1));
531 DOCTEST_CHECK_NOTHROW(photomodel.run());
532 DOCTEST_CHECK_NOTHROW(context_test.
getPrimitiveData(UUID2,
"net_photosynthesis", A2));
535 photomodel.enableMessages();
537 DOCTEST_CHECK(A1 != 0.0f);
538 DOCTEST_CHECK(A2 != 0.0f);
541DOCTEST_TEST_CASE(
"PhotosyntheticTemperatureResponseParameters Constructors") {
544 DOCTEST_CHECK(params_default.value_at_25C == doctest::Approx(100.0f).epsilon(err_tol));
545 DOCTEST_CHECK(params_default.dHa == doctest::Approx(60.0f).epsilon(err_tol));
546 DOCTEST_CHECK(params_default.dHd == doctest::Approx(600.0f).epsilon(err_tol));
547 DOCTEST_CHECK(params_default.Topt == doctest::Approx(10000.0f).epsilon(err_tol));
551 DOCTEST_CHECK(params_single.value_at_25C == doctest::Approx(75.0f).epsilon(err_tol));
552 DOCTEST_CHECK(params_single.dHa == doctest::Approx(0.0f).epsilon(err_tol));
556 DOCTEST_CHECK(params_negative.value_at_25C == doctest::Approx(80.0f).epsilon(err_tol));
557 DOCTEST_CHECK(params_negative.dHa == doctest::Approx(-10.0f).epsilon(err_tol));
558 DOCTEST_CHECK(params_negative.dHd == doctest::Approx(600.0f).epsilon(err_tol));
562 DOCTEST_CHECK(params_three.value_at_25C == doctest::Approx(90.0f).epsilon(err_tol));
563 DOCTEST_CHECK(params_three.dHa == doctest::Approx(50.0f).epsilon(err_tol));
564 DOCTEST_CHECK(params_three.dHd == doctest::Approx(500.0f).epsilon(err_tol));
565 DOCTEST_CHECK(params_three.Topt == doctest::Approx(273.15f + 35.0f).epsilon(err_tol));
569 DOCTEST_CHECK(params_three_neg.value_at_25C == doctest::Approx(85.0f).epsilon(err_tol));
570 DOCTEST_CHECK(params_three_neg.dHa == doctest::Approx(-15.0f).epsilon(err_tol));
571 DOCTEST_CHECK(params_three_neg.dHd == doctest::Approx(600.0f).epsilon(err_tol));
572 DOCTEST_CHECK(params_three_neg.Topt == doctest::Approx(273.15f + 32.0f).epsilon(err_tol));
576 DOCTEST_CHECK(params_full.value_at_25C == doctest::Approx(95.0f).epsilon(err_tol));
577 DOCTEST_CHECK(params_full.dHa == doctest::Approx(45.0f).epsilon(err_tol));
578 DOCTEST_CHECK(params_full.dHd == doctest::Approx(400.0f).epsilon(err_tol));
579 DOCTEST_CHECK(params_full.Topt == doctest::Approx(273.15f + 30.0f).epsilon(err_tol));
582DOCTEST_TEST_CASE(
"FarquharModelCoefficients Temperature Response Methods") {
592 coeffs.
setJmax(150.0f, 43.5f, 35.0f);
594 DOCTEST_CHECK(jmax_params.value_at_25C == doctest::Approx(150.0f).epsilon(err_tol));
595 DOCTEST_CHECK(jmax_params.dHa == doctest::Approx(43.5f).epsilon(err_tol));
598 coeffs.
setJmax(160.0f, 45.0f, 40.0f, 500.0f);
599 jmax_params = coeffs.getJmaxTempResponse();
600 DOCTEST_CHECK(jmax_params.dHd == doctest::Approx(500.0f).epsilon(err_tol));
603 coeffs.
setVcmax(100.0f, 65.0f, 32.0f);
605 DOCTEST_CHECK(vcmax_params.value_at_25C == doctest::Approx(100.0f).epsilon(err_tol));
608 coeffs.
setVcmax(110.0f, 70.0f, 38.0f, 600.0f);
609 vcmax_params = coeffs.getVcmaxTempResponse();
610 DOCTEST_CHECK(vcmax_params.dHd == doctest::Approx(600.0f).epsilon(err_tol));
615 DOCTEST_CHECK(tpu_params.value_at_25C == doctest::Approx(8.0f).epsilon(err_tol));
616 DOCTEST_CHECK(coeffs.TPU_flag == 1);
618 coeffs.
setTPU(9.0f, 25.0f);
619 tpu_params = coeffs.getTPUTempResponse();
620 DOCTEST_CHECK(tpu_params.dHa == doctest::Approx(25.0f).epsilon(err_tol));
622 coeffs.
setTPU(10.0f, 30.0f, 33.0f);
623 tpu_params = coeffs.getTPUTempResponse();
624 DOCTEST_CHECK(tpu_params.Topt == doctest::Approx(273.15f + 33.0f).epsilon(err_tol));
626 coeffs.
setTPU(11.0f, 35.0f, 36.0f, 450.0f);
627 tpu_params = coeffs.getTPUTempResponse();
628 DOCTEST_CHECK(tpu_params.dHd == doctest::Approx(450.0f).epsilon(err_tol));
631 coeffs.
setRd(2.5f, 46.0f, 34.0f);
633 DOCTEST_CHECK(rd_params.value_at_25C == doctest::Approx(2.5f).epsilon(err_tol));
635 coeffs.
setRd(2.8f, 48.0f, 37.0f, 480.0f);
636 rd_params = coeffs.getRdTempResponse();
637 DOCTEST_CHECK(rd_params.dHd == doctest::Approx(480.0f).epsilon(err_tol));
642 DOCTEST_CHECK(alpha_params.dHa == doctest::Approx(20.0f).epsilon(err_tol));
645 alpha_params = coeffs.getQuantumEfficiencyTempResponse();
646 DOCTEST_CHECK(alpha_params.Topt == doctest::Approx(273.15f + 30.0f).epsilon(err_tol));
649 alpha_params = coeffs.getQuantumEfficiencyTempResponse();
650 DOCTEST_CHECK(alpha_params.dHd == doctest::Approx(350.0f).epsilon(err_tol));
655 DOCTEST_CHECK(theta_params.dHa == doctest::Approx(15.0f).epsilon(err_tol));
658 theta_params = coeffs.getLightResponseCurvatureTempResponse();
659 DOCTEST_CHECK(theta_params.Topt == doctest::Approx(273.15f + 35.0f).epsilon(err_tol));
662 theta_params = coeffs.getLightResponseCurvatureTempResponse();
663 DOCTEST_CHECK(theta_params.dHd == doctest::Approx(400.0f).epsilon(err_tol));
666DOCTEST_TEST_CASE(
"PhotosynthesisModel Edge Cases and Error Conditions") {
672 photomodel.disableMessages();
675 DOCTEST_CHECK_NOTHROW(photomodel.optionalOutputPrimitiveData(
"unknown_primitive"));
678 DOCTEST_CHECK_NOTHROW(photomodel.run());
682 DOCTEST_CHECK(emp_default.Asat == doctest::Approx(18.18f).epsilon(err_tol));
686 DOCTEST_CHECK(farq_default.Vcmax == doctest::Approx(-1.0f).epsilon(err_tol));
690 photomodel.enableMessages();
700 DOCTEST_CHECK_NOTHROW(photomodel.run());
704 DOCTEST_CHECK_MESSAGE(captured_warnings.find(
"Photosynthesis model failed to converge") != std::string::npos,
"Extreme conditions should produce convergence warnings");
708 DOCTEST_CHECK_NOTHROW(context_test.
getPrimitiveData(UUID,
"net_photosynthesis", A));
709 DOCTEST_CHECK(!std::isnan(A));
712DOCTEST_TEST_CASE(
"FarquharModelCoefficients TPU Testing") {
718 DOCTEST_CHECK(coeffs.TPU_flag == 0);
722 DOCTEST_CHECK(coeffs.TPU_flag == 1);
724 coeffs.
setTPU(6.0f, 20.0f);
725 DOCTEST_CHECK(coeffs.TPU_flag == 1);
727 coeffs.
setTPU(7.0f, 25.0f, 30.0f);
728 DOCTEST_CHECK(coeffs.TPU_flag == 1);
730 coeffs.
setTPU(8.0f, 30.0f, 35.0f, 400.0f);
731 DOCTEST_CHECK(coeffs.TPU_flag == 1);
734DOCTEST_TEST_CASE(
"Farquhar TPU Singularity Fix") {
742 photomodel.disableMessages();
746 DOCTEST_CHECK(coeffs.TPU_flag == 1);
750 std::vector<float> Ci_values = {40.0f, 50.0f, 55.0f, 56.0f, 57.0f, 58.0f, 60.0f, 80.0f, 150.0f, 250.0f};
751 std::vector<float> A_values;
753 for (
float Ci_val : Ci_values) {
756 photomodel.setModelCoefficients(coeffs, std::vector<uint>{UUID});
763 photomodel.setCi(Ci_val, std::vector<uint>{UUID});
765 photomodel.run(std::vector<uint>{UUID});
769 A_values.push_back(A);
774 for (
size_t i = 0; i < A_values.size(); i++) {
775 DOCTEST_CHECK_MESSAGE(std::isfinite(A_values[i]),
776 "Non-finite A at Ci=" << Ci_values[i] <<
": A=" << A_values[i]);
777 if (Ci_values[i] < 55.0f) {
778 DOCTEST_CHECK_MESSAGE(A_values[i] < 0.0f,
779 "A should be negative below compensation point: Ci=" << Ci_values[i]
780 <<
" A=" << A_values[i]);
785 for (
size_t i = 1; i < A_values.size(); i++) {
786 DOCTEST_CHECK_MESSAGE(A_values[i] >= A_values[i-1] - 0.5f,
787 "Non-monotonic A: A[Ci=" << Ci_values[i] <<
"]=" << A_values[i]
788 <<
" < A[Ci=" << Ci_values[i-1] <<
"]=" << A_values[i-1]);
792DOCTEST_TEST_CASE(
"PhotosynthesisModel Complex Farquhar Testing") {
801 coeffs.
setRd(2.0f, 46.0f);
805 photomodel.setModelCoefficients(coeffs);
813 photomodel.optionalOutputPrimitiveData(
"Ci");
814 photomodel.optionalOutputPrimitiveData(
"limitation_state");
815 photomodel.optionalOutputPrimitiveData(
"Gamma_CO2");
817 DOCTEST_CHECK_NOTHROW(photomodel.run());
821 int limitation_state;
822 DOCTEST_CHECK_NOTHROW(context_test.
getPrimitiveData(UUID,
"net_photosynthesis", A));
824 DOCTEST_CHECK_NOTHROW(context_test.
getPrimitiveData(UUID,
"Gamma_CO2", Gamma));
825 DOCTEST_CHECK_NOTHROW(context_test.
getPrimitiveData(UUID,
"limitation_state", limitation_state));
828 DOCTEST_CHECK(A != 0.0f);
829 DOCTEST_CHECK(Ci > 0.0f);
830 DOCTEST_CHECK(Gamma > 0.0f);
831 DOCTEST_CHECK(limitation_state >= 0);
834DOCTEST_TEST_CASE(
"PhotosynthesisModel Empirical Model Edge Cases") {
841 empirical_coeffs.Asat = 25.0f;
842 empirical_coeffs.theta = 70.0f;
844 photomodel.setModelCoefficients(empirical_coeffs);
851 DOCTEST_CHECK_NOTHROW(photomodel.run());
854 DOCTEST_CHECK_NOTHROW(context_test.
getPrimitiveData(UUID,
"net_photosynthesis", A));
855 DOCTEST_CHECK(!std::isnan(A));
859 DOCTEST_CHECK_NOTHROW(photomodel.run());
861 DOCTEST_CHECK_NOTHROW(context_test.
getPrimitiveData(UUID,
"net_photosynthesis", A));
862 DOCTEST_CHECK(A < 0.0f);
865DOCTEST_TEST_CASE(
"PhotosynthesisModel Temperature Response Edge Cases") {
877 photomodel.setModelCoefficients(coeffs);
880 std::vector<float> temperatures = {280.0f, 300.0f, 320.0f};
881 std::vector<float> results;
886 for (
float temp: temperatures) {
888 DOCTEST_CHECK_NOTHROW(photomodel.run());
891 DOCTEST_CHECK_NOTHROW(context_test.
getPrimitiveData(UUID,
"net_photosynthesis", A));
892 results.push_back(A);
897 for (
float result: results) {
898 DOCTEST_CHECK(!std::isnan(result));
899 DOCTEST_CHECK(std::isfinite(result));
903DOCTEST_TEST_CASE(
"PhotosynthesisModel - Material-Based Coefficients") {
908 photomodel.disableMessages();
910 SUBCASE(
"Farquhar Model - Set and Retrieve Coefficients via Material") {
918 custom_coeffs.
setRd(2.0f);
920 custom_coeffs.O = 210.0f;
922 photomodel.setModelCoefficients(
"test_leaf", custom_coeffs);
937 float Vcmax, Jmax, Rd, alpha;
943 DOCTEST_CHECK(Vcmax == doctest::Approx(150.0f));
944 DOCTEST_CHECK(Jmax == doctest::Approx(200.0f));
945 DOCTEST_CHECK(Rd == doctest::Approx(2.0f));
946 DOCTEST_CHECK(alpha == doctest::Approx(0.4f));
949 SUBCASE(
"Multiple Primitives Share Material Coefficients") {
959 photomodel.setModelCoefficients(
"shared_leaf", coeffs);
962 std::vector<uint> primitives;
963 for (
int i = 0; i < 50; i++) {
965 primitives.push_back(p);
971 for (
uint p: primitives) {
980 SUBCASE(
"Library Integration with Materials") {
986 photomodel.setFarquharCoefficientsFromLibrary(
"Almond",
"almond_leaf");
987 photomodel.setFarquharCoefficientsFromLibrary(
"Grape",
"grape_leaf");
994 float almond_Vcmax, grape_Vcmax;
995 context.
getMaterialData(
"almond_leaf",
"photo_fq_Vcmax", almond_Vcmax);
997 DOCTEST_CHECK(almond_Vcmax != grape_Vcmax);
1000 SUBCASE(
"Empirical Model - Material-Based API") {
1009 DOCTEST_CHECK_NOTHROW(photomodel.setModelCoefficients(
"empirical_mat", emp));
1015 float Asat_retrieved;
1016 context.
getMaterialData(
"empirical_mat",
"photo_emp_Asat", Asat_retrieved);
1017 DOCTEST_CHECK(Asat_retrieved == doctest::Approx(25.0f));
1021DOCTEST_TEST_CASE(
"PhotosynthesisModel - Manual Ci Setting") {
1029 fcoeffs.
setJmax(150.f, 43.54f);
1030 fcoeffs.
setRd(2.12f, 46.39f);
1032 DOCTEST_CHECK_NOTHROW(photomodel.setModelCoefficients(fcoeffs));
1035 DOCTEST_CHECK_NOTHROW(photomodel.optionalOutputPrimitiveData(
"Ci"));
1038 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"radiation_flux_PAR", 800.0f));
1039 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"temperature", 298.15f));
1040 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"air_CO2", 400.0f));
1041 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"moisture_conductance", 0.3f));
1044 float manual_Ci_value = 280.0f;
1045 std::vector<uint> UUIDs = {UUID};
1046 DOCTEST_CHECK_NOTHROW(photomodel.setCi(manual_Ci_value, UUIDs));
1049 DOCTEST_CHECK_NOTHROW(photomodel.run());
1051 DOCTEST_CHECK_NOTHROW(context_test.
getPrimitiveData(UUID,
"Ci", Ci_retrieved));
1052 DOCTEST_CHECK(Ci_retrieved == doctest::Approx(manual_Ci_value).epsilon(err_tol));
1055DOCTEST_TEST_CASE(
"PhotosynthesisModel - Manual Ci Persistence") {
1062 fcoeffs.
setJmax(150.f, 43.54f);
1063 fcoeffs.
setRd(2.12f, 46.39f);
1065 DOCTEST_CHECK_NOTHROW(photomodel.setModelCoefficients(fcoeffs));
1066 DOCTEST_CHECK_NOTHROW(photomodel.optionalOutputPrimitiveData(
"Ci"));
1068 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"radiation_flux_PAR", 800.0f));
1069 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"temperature", 298.15f));
1070 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"air_CO2", 400.0f));
1071 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"moisture_conductance", 0.3f));
1074 float manual_Ci_value = 250.0f;
1075 std::vector<uint> UUIDs = {UUID};
1076 DOCTEST_CHECK_NOTHROW(photomodel.setCi(manual_Ci_value, UUIDs));
1079 DOCTEST_CHECK_NOTHROW(photomodel.run());
1083 DOCTEST_CHECK_NOTHROW(photomodel.run());
1087 DOCTEST_CHECK_NOTHROW(photomodel.run());
1092 DOCTEST_CHECK(Ci_run1 == doctest::Approx(manual_Ci_value).epsilon(err_tol));
1093 DOCTEST_CHECK(Ci_run2 == doctest::Approx(manual_Ci_value).epsilon(err_tol));
1094 DOCTEST_CHECK(Ci_run3 == doctest::Approx(manual_Ci_value).epsilon(err_tol));
1097DOCTEST_TEST_CASE(
"PhotosynthesisModel - Invalid Ci Error Handling") {
1102 std::vector<uint> UUIDs = {UUID};
1106 DOCTEST_CHECK_THROWS_AS(photomodel.setCi(-100.0f, UUIDs), std::runtime_error);
1109 DOCTEST_CHECK_THROWS_AS(photomodel.setCi(std::numeric_limits<float>::quiet_NaN(), UUIDs), std::runtime_error);
1112 DOCTEST_CHECK_THROWS_AS(photomodel.setCi(std::numeric_limits<float>::infinity(), UUIDs), std::runtime_error);
1115 std::vector<uint> invalid_UUIDs = {99999};
1116 DOCTEST_CHECK_THROWS_AS(photomodel.setCi(300.0f, invalid_UUIDs), std::runtime_error);
1119DOCTEST_TEST_CASE(
"PhotosynthesisModel - Mixed Manual and Automatic Ci") {
1127 fcoeffs.
setJmax(150.f, 43.54f);
1128 fcoeffs.
setRd(2.12f, 46.39f);
1130 DOCTEST_CHECK_NOTHROW(photomodel.setModelCoefficients(fcoeffs));
1131 DOCTEST_CHECK_NOTHROW(photomodel.optionalOutputPrimitiveData(
"Ci"));
1134 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID1,
"radiation_flux_PAR", 800.0f));
1135 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID2,
"radiation_flux_PAR", 800.0f));
1136 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID1,
"temperature", 298.15f));
1137 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID2,
"temperature", 298.15f));
1138 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID1,
"air_CO2", 400.0f));
1139 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID2,
"air_CO2", 400.0f));
1140 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID1,
"moisture_conductance", 0.3f));
1141 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID2,
"moisture_conductance", 0.3f));
1144 float manual_Ci_value = 250.0f;
1145 std::vector<uint> manual_UUIDs = {UUID1};
1146 DOCTEST_CHECK_NOTHROW(photomodel.setCi(manual_Ci_value, manual_UUIDs));
1149 DOCTEST_CHECK_NOTHROW(photomodel.run());
1154 DOCTEST_CHECK(Ci1 == doctest::Approx(manual_Ci_value).epsilon(err_tol));
1159 DOCTEST_CHECK(Ci2 != doctest::Approx(manual_Ci_value).epsilon(err_tol));
1160 DOCTEST_CHECK(Ci2 > 0.0f);
1161 DOCTEST_CHECK(Ci2 < 400.0f);
1164DOCTEST_TEST_CASE(
"PhotosynthesisModel - Topt Parameter Validation") {
1169 SUBCASE(
"Negative Topt should throw error") {
1171 DOCTEST_CHECK_THROWS_WITH_AS(coeffs.
setVcmax(100.0f, 60.0f, -10.0f),
1172 "ERROR (PhotosyntheticTemperatureResponseParameters): Optimal temperature cannot be negative. Received Topt = -10.000000 C. Please check that temperature is provided in units of Celsius, not Kelvin.",
1173 std::runtime_error);
1176 DOCTEST_CHECK_THROWS_WITH_AS(coeffs.
setJmax(200.0f, 50.0f, -5.0f),
1177 "ERROR (PhotosyntheticTemperatureResponseParameters): Optimal temperature cannot be negative. Received Topt = -5.000000 C. Please check that temperature is provided in units of Celsius, not Kelvin.",
1178 std::runtime_error);
1181 DOCTEST_CHECK_THROWS_WITH_AS(coeffs.
setRd(2.0f, 40.0f, -15.0f),
1182 "ERROR (PhotosyntheticTemperatureResponseParameters): Optimal temperature cannot be negative. Received Topt = -15.000000 C. Please check that temperature is provided in units of Celsius, not Kelvin.",
1183 std::runtime_error);
1186 DOCTEST_CHECK_THROWS_WITH_AS(coeffs.
setTPU(5.0f, 50.0f, -20.0f),
1187 "ERROR (PhotosyntheticTemperatureResponseParameters): Optimal temperature cannot be negative. Received Topt = -20.000000 C. Please check that temperature is provided in units of Celsius, not Kelvin.",
1188 std::runtime_error);
1192 "ERROR (PhotosyntheticTemperatureResponseParameters): Optimal temperature cannot be negative. Received Topt = -25.000000 C. Please check that temperature is provided in units of Celsius, not Kelvin.",
1193 std::runtime_error);
1197 "ERROR (PhotosyntheticTemperatureResponseParameters): Optimal temperature cannot be negative. Received Topt = -30.000000 C. Please check that temperature is provided in units of Celsius, not Kelvin.",
1198 std::runtime_error);
1201 SUBCASE(
"Topt > 100C should throw error (likely Kelvin instead of Celsius)") {
1203 DOCTEST_CHECK_THROWS_WITH_AS(
1204 coeffs.
setVcmax(100.0f, 60.0f, 310.0f),
1205 "ERROR (PhotosyntheticTemperatureResponseParameters): Optimal temperature cannot exceed 100 C. Received Topt = 310.000000 C. This value is biologically unrealistic and likely indicates temperature was provided in Kelvin instead of Celsius. Please convert to Celsius (subtract 273.15 from Kelvin value).",
1206 std::runtime_error);
1209 DOCTEST_CHECK_THROWS_WITH_AS(
1210 coeffs.
setJmax(200.0f, 50.0f, 305.0f),
1211 "ERROR (PhotosyntheticTemperatureResponseParameters): Optimal temperature cannot exceed 100 C. Received Topt = 305.000000 C. This value is biologically unrealistic and likely indicates temperature was provided in Kelvin instead of Celsius. Please convert to Celsius (subtract 273.15 from Kelvin value).",
1212 std::runtime_error);
1215 DOCTEST_CHECK_THROWS_WITH_AS(
1216 coeffs.
setRd(2.0f, 40.0f, 300.0f),
1217 "ERROR (PhotosyntheticTemperatureResponseParameters): Optimal temperature cannot exceed 100 C. Received Topt = 300.000000 C. This value is biologically unrealistic and likely indicates temperature was provided in Kelvin instead of Celsius. Please convert to Celsius (subtract 273.15 from Kelvin value).",
1218 std::runtime_error);
1221 DOCTEST_CHECK_THROWS_WITH_AS(
1222 coeffs.
setTPU(5.0f, 50.0f, 315.0f),
1223 "ERROR (PhotosyntheticTemperatureResponseParameters): Optimal temperature cannot exceed 100 C. Received Topt = 315.000000 C. This value is biologically unrealistic and likely indicates temperature was provided in Kelvin instead of Celsius. Please convert to Celsius (subtract 273.15 from Kelvin value).",
1224 std::runtime_error);
1227 DOCTEST_CHECK_THROWS_WITH_AS(
1229 "ERROR (PhotosyntheticTemperatureResponseParameters): Optimal temperature cannot exceed 100 C. Received Topt = 320.000000 C. This value is biologically unrealistic and likely indicates temperature was provided in Kelvin instead of Celsius. Please convert to Celsius (subtract 273.15 from Kelvin value).",
1230 std::runtime_error);
1233 DOCTEST_CHECK_THROWS_WITH_AS(
1235 "ERROR (PhotosyntheticTemperatureResponseParameters): Optimal temperature cannot exceed 100 C. Received Topt = 325.000000 C. This value is biologically unrealistic and likely indicates temperature was provided in Kelvin instead of Celsius. Please convert to Celsius (subtract 273.15 from Kelvin value).",
1236 std::runtime_error);
1239 SUBCASE(
"Valid Topt values should work correctly") {
1241 DOCTEST_CHECK_NOTHROW(coeffs.
setVcmax(100.0f, 60.0f, 35.0f));
1242 DOCTEST_CHECK_NOTHROW(coeffs.
setJmax(200.0f, 50.0f, 40.0f));
1243 DOCTEST_CHECK_NOTHROW(coeffs.
setRd(2.0f, 40.0f, 30.0f));
1244 DOCTEST_CHECK_NOTHROW(coeffs.
setTPU(5.0f, 50.0f, 33.0f));
1250 DOCTEST_CHECK(vcmax_params.Topt == doctest::Approx(273.15f + 35.0f).epsilon(err_tol));
1253 DOCTEST_CHECK(jmax_params.Topt == doctest::Approx(273.15f + 40.0f).epsilon(err_tol));
1256 DOCTEST_CHECK(rd_params.Topt == doctest::Approx(273.15f + 30.0f).epsilon(err_tol));
1259 DOCTEST_CHECK(tpu_params.Topt == doctest::Approx(273.15f + 33.0f).epsilon(err_tol));
1262 DOCTEST_CHECK(alpha_params.Topt == doctest::Approx(273.15f + 25.0f).epsilon(err_tol));
1265 DOCTEST_CHECK(theta_params.Topt == doctest::Approx(273.15f + 28.0f).epsilon(err_tol));
1268 SUBCASE(
"Edge cases: 0C and 100C should be valid") {
1270 DOCTEST_CHECK_NOTHROW(coeffs.
setVcmax(100.0f, 60.0f, 0.0f));
1271 DOCTEST_CHECK_NOTHROW(coeffs.
setJmax(200.0f, 50.0f, 100.0f));
1274 DOCTEST_CHECK(vcmax_params.Topt == doctest::Approx(273.15f).epsilon(err_tol));
1277 DOCTEST_CHECK(jmax_params.Topt == doctest::Approx(373.15f).epsilon(err_tol));
1285DOCTEST_TEST_CASE(
"PhotosynthesisModel C4 Model Type Setting and Defaults") {
1289 photomodel.disableMessages();
1291 DOCTEST_CHECK_NOTHROW(photomodel.setModelType_C4());
1296 DOCTEST_CHECK(c4.
Kc_25 == doctest::Approx(1210.f).epsilon(err_tol));
1297 DOCTEST_CHECK(c4.
Ko_25 == doctest::Approx(292000.f).epsilon(err_tol));
1298 DOCTEST_CHECK(c4.
Kp_25 == doctest::Approx(82.f).epsilon(err_tol));
1299 DOCTEST_CHECK(c4.
gamma_star_25 == doctest::Approx(3.81679e-4f).epsilon(err_tol));
1300 DOCTEST_CHECK(c4.
Om_25 == doctest::Approx(210000.f).epsilon(err_tol));
1302 DOCTEST_CHECK(c4.
x_etr_partition == doctest::Approx(0.4f).epsilon(err_tol));
1303 DOCTEST_CHECK(c4.
Vpr == doctest::Approx(80.f).epsilon(err_tol));
1304 DOCTEST_CHECK(c4.
Rm_frac == doctest::Approx(0.5f).epsilon(err_tol));
1305 DOCTEST_CHECK(c4.
fcyc == doctest::Approx(0.45f).epsilon(err_tol));
1306 DOCTEST_CHECK(c4.
H_J == doctest::Approx(3.f).epsilon(err_tol));
1307 DOCTEST_CHECK(c4.
H_Jcyc == doctest::Approx(3.4f).epsilon(err_tol));
1308 DOCTEST_CHECK(c4.
gbs == doctest::Approx(0.003f).epsilon(err_tol));
1309 DOCTEST_CHECK(c4.
ao == doctest::Approx(0.047f).epsilon(err_tol));
1312 DOCTEST_CHECK(c4.getVpmaxTempResponse().value_at_25C == doctest::Approx(200.f).epsilon(err_tol));
1313 DOCTEST_CHECK(c4.getVcmaxTempResponse().value_at_25C == doctest::Approx(40.f).epsilon(err_tol));
1314 DOCTEST_CHECK(c4.getJmaxTempResponse().value_at_25C == doctest::Approx(247.69f).epsilon(err_tol));
1315 DOCTEST_CHECK(c4.getRdTempResponse().value_at_25C == doctest::Approx(1.f).epsilon(err_tol));
1316 DOCTEST_CHECK(c4.getMesophyllConductance_gmTempResponse().value_at_25C == doctest::Approx(1.f).epsilon(err_tol));
1319DOCTEST_TEST_CASE(
"PhotosynthesisModel C4 Setter Overloads - Temperature Response") {
1322 SUBCASE(
"Constant Vpmax") {
1324 DOCTEST_CHECK(c4.getVpmaxTempResponse().value_at_25C == doctest::Approx(150.f));
1325 DOCTEST_CHECK(c4.getVpmaxTempResponse().dHa == doctest::Approx(0.f));
1327 SUBCASE(
"Arrhenius Vcmax") {
1329 DOCTEST_CHECK(c4.getVcmaxTempResponse().value_at_25C == doctest::Approx(35.f));
1330 DOCTEST_CHECK(c4.getVcmaxTempResponse().dHa == doctest::Approx(70.f));
1332 SUBCASE(
"Peaked Arrhenius Jmax") {
1333 c4.
setJmax(220.f, 45.f, 40.f);
1334 DOCTEST_CHECK(c4.getJmaxTempResponse().value_at_25C == doctest::Approx(220.f));
1335 DOCTEST_CHECK(c4.getJmaxTempResponse().Topt == doctest::Approx(273.15f + 40.f));
1337 SUBCASE(
"Peaked Arrhenius Rd with explicit dHd") {
1338 c4.
setRd(1.2f, 60.f, 42.f, 400.f);
1339 DOCTEST_CHECK(c4.getRdTempResponse().dHd == doctest::Approx(400.f));
1341 SUBCASE(
"gm setters") {
1343 DOCTEST_CHECK(c4.getMesophyllConductance_gmTempResponse().value_at_25C == doctest::Approx(0.5f));
1345 DOCTEST_CHECK(c4.getMesophyllConductance_gmTempResponse().dHa == doctest::Approx(50.f));
1349DOCTEST_TEST_CASE(
"PhotosynthesisModel C4 - von Caemmerer 2021 spreadsheet verification at T=25C") {
1364 photomodel.disableMessages();
1373 c4.
setJmax(247.6891682624015f);
1378 photomodel.setModelCoefficients(c4);
1380 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"temperature", 298.15f));
1381 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"radiation_flux_PAR", 2000.f / 4.57f));
1382 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"air_CO2", 400.f));
1384 struct C4TestPoint {
1389 const std::vector<C4TestPoint> points = {
1399 {100.f, 34.862438f},
1400 {150.f, 34.928111f},
1401 {250.f, 35.056593f},
1402 {500.f, 35.361352f},
1403 {780.f, 35.675665f},
1406 for (
const auto &pt: points) {
1407 photomodel.setCm(pt.Cm, {UUID});
1408 DOCTEST_CHECK_NOTHROW(photomodel.run());
1413 DOCTEST_CHECK_MESSAGE(A == doctest::Approx(pt.A_expected).epsilon(0.005),
1414 "Cm=" << pt.Cm <<
" expected A=" << pt.A_expected <<
" got " << A);
1418DOCTEST_TEST_CASE(
"PhotosynthesisModel C4 - Woodford 2025 default A vs Cm regression at T=25C") {
1432 photomodel.disableMessages();
1435 c4.
setJmax(247.6891682624015f);
1439 photomodel.setModelCoefficients(c4);
1441 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"temperature", 298.15f));
1442 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"radiation_flux_PAR", 2000.f / 4.57f));
1443 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"air_CO2", 400.f));
1445 struct C4TestPoint {
1449 const std::vector<C4TestPoint> points = {
1469 for (
const auto &pt: points) {
1470 photomodel.setCm(pt.Cm, {UUID});
1471 DOCTEST_CHECK_NOTHROW(photomodel.run());
1474 DOCTEST_CHECK_MESSAGE(A == doctest::Approx(pt.A_expected).epsilon(0.005),
1475 "Cm=" << pt.Cm <<
" expected A=" << pt.A_expected <<
" got " << A);
1479DOCTEST_TEST_CASE(
"PhotosynthesisModel C4 - setCm round-trip: Ci = Cm + A/gm") {
1484 photomodel.disableMessages();
1485 photomodel.setModelType_C4();
1486 photomodel.optionalOutputPrimitiveData(
"Ci");
1487 photomodel.optionalOutputPrimitiveData(
"Cm");
1494 const float gm_25 = defaults.getMesophyllConductance_gmTempResponse().value_at_25C;
1496 for (
float Cm_in: {20.f, 60.f, 120.f}) {
1497 photomodel.setCm(Cm_in, {UUID});
1498 DOCTEST_CHECK_NOTHROW(photomodel.run());
1499 float A, Ci_out, Cm_out;
1503 DOCTEST_CHECK(Cm_out == doctest::Approx(Cm_in).epsilon(1e-5));
1504 DOCTEST_CHECK(Ci_out == doctest::Approx(Cm_in + A / gm_25).epsilon(1e-3));
1508DOCTEST_TEST_CASE(
"PhotosynthesisModel C4 - Temperature response wires through to A") {
1516 photomodel.disableMessages();
1517 photomodel.setModelType_C4();
1521 photomodel.setCm(80.f, {UUID});
1525 DOCTEST_CHECK_NOTHROW(photomodel.run());
1531 DOCTEST_CHECK_NOTHROW(photomodel.run());
1537 DOCTEST_CHECK_MESSAGE(A_35 > A_25 * 1.1f,
1538 "T-response not wired through: A(35°C)=" << A_35 <<
" not meaningfully > A(25°C)=" << A_25);
1542 DOCTEST_CHECK(A_25 == doctest::Approx(34.012f).epsilon(0.005));
1543 DOCTEST_CHECK(A_35 == doctest::Approx(60.687f).epsilon(0.005));
1546DOCTEST_TEST_CASE(
"PhotosynthesisModel C4 - Vpr cap activates at high Cm") {
1552 photomodel.disableMessages();
1553 photomodel.setModelType_C4();
1554 photomodel.optionalOutputPrimitiveData(
"Vp");
1559 photomodel.setCi(600.f, {UUID});
1561 DOCTEST_CHECK_NOTHROW(photomodel.run());
1565 DOCTEST_CHECK(Vp_out <= defaults.
Vpr + 1e-4f);
1566 DOCTEST_CHECK(Vp_out == doctest::Approx(defaults.
Vpr).epsilon(1e-3));
1569DOCTEST_TEST_CASE(
"PhotosynthesisModel C4 - Patched Woodford spreadsheet anchors") {
1591 auto gaussian_Jmax = [](
float T_C) {
1592 const float Topt = 43.f;
1593 const float Omega = 26.f;
1594 const float Jmax_at_Topt = 400.f;
1595 const float dT = (T_C - Topt) / Omega;
1596 return Jmax_at_Topt * std::exp(-dT * dT);
1599 const std::filesystem::path csv_path =
helios::resolveFilePath(
"plugins/photosynthesis/tests/C4_test_anchors.csv");
1600 std::ifstream csv(csv_path);
1601 DOCTEST_REQUIRE_MESSAGE(csv.good(),
"Failed to open " << csv_path.string());
1606 photomodel.disableMessages();
1607 DOCTEST_CHECK_NOTHROW(photomodel.setModelType_C4());
1609 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"radiation_flux_PAR", 2000.f / 4.57f));
1610 DOCTEST_CHECK_NOTHROW(context_test.
setPrimitiveData(UUID,
"air_CO2", 400.f));
1613 std::getline(csv, line);
1614 DOCTEST_REQUIRE(line.substr(0, 8) ==
"scenario");
1617 while (std::getline(csv, line)) {
1618 if (line.empty())
continue;
1619 std::stringstream ss(line);
1620 std::string scenario, T_str, Cm_str, Ac_str, Aj_str, Amin_str;
1621 std::getline(ss, scenario,
',');
1622 std::getline(ss, T_str,
',');
1623 std::getline(ss, Cm_str,
',');
1624 std::getline(ss, Ac_str,
',');
1625 std::getline(ss, Aj_str,
',');
1626 std::getline(ss, Amin_str,
',');
1627 const float T_C = std::stof(T_str);
1628 const float Cm = std::stof(Cm_str);
1629 const float Amin_expected = std::stof(Amin_str);
1633 c4.
setJmax(gaussian_Jmax(T_C));
1634 photomodel.setModelCoefficients(c4);
1636 photomodel.setCm(Cm, {UUID});
1638 DOCTEST_CHECK_NOTHROW(photomodel.run());
1641 DOCTEST_CHECK_MESSAGE(A == doctest::Approx(Amin_expected).epsilon(0.005),
1642 "T=" << T_C <<
"°C Cm=" << Cm
1643 <<
" expected A=" << Amin_expected <<
" got A=" << A);
1646 DOCTEST_CHECK_MESSAGE(n_points >= 60,
"Expected ≥60 anchor rows in CSV; got " << n_points);
1649DOCTEST_TEST_CASE(
"PhotosynthesisModel C4 - Material-data caching round-trip") {
1656 photomodel.disableMessages();
1662 custom.
Vpr = 1.0e4f;
1666 const std::string mat =
"test_c4_material";
1669 photomodel.setModelCoefficients(mat, custom);
1674 photomodel.setCm(50.f, {UUID});
1675 DOCTEST_CHECK_NOTHROW(photomodel.run());
1683 pm2.disableMessages();
1684 pm2.setModelCoefficients(custom, {UUID2});
1688 pm2.setCm(50.f, {UUID2});
1689 DOCTEST_CHECK_NOTHROW(pm2.run());
1694 DOCTEST_CHECK(A_material == doctest::Approx(A_uuid).epsilon(1e-4));
1698 DOCTEST_CHECK(retrieved.getVcmaxTempResponse().value_at_25C == doctest::Approx(60.f));
1699 DOCTEST_CHECK(retrieved.getVcmaxTempResponse().dHa == doctest::Approx(70.f));
1700 DOCTEST_CHECK(retrieved.getJmaxTempResponse().value_at_25C == doctest::Approx(300.f));
1701 DOCTEST_CHECK(retrieved.
Vpr == doctest::Approx(1.0e4f));
1705DOCTEST_TEST_CASE(
"PhotosynthesisModel C4 - Coefficient assignment per-UUID") {
1710 photomodel.disableMessages();
1715 photomodel.setModelCoefficients(c4_hi, {UUID1});
1720 photomodel.setModelCoefficients(c4_lo, {UUID2});
1722 DOCTEST_CHECK(photomodel.getC4ModelCoefficients(UUID1).getVcmaxTempResponse().value_at_25C == doctest::Approx(80.f));
1723 DOCTEST_CHECK(photomodel.getC4ModelCoefficients(UUID2).getVcmaxTempResponse().value_at_25C == doctest::Approx(20.f));
1729 context_test.
setPrimitiveData(UUID1,
"radiation_flux_PAR", 2000.f / 4.57f);
1730 context_test.
setPrimitiveData(UUID2,
"radiation_flux_PAR", 2000.f / 4.57f);
1733 photomodel.setCm(50.f, {UUID1, UUID2});
1734 DOCTEST_CHECK_NOTHROW(photomodel.run());
1739 DOCTEST_CHECK(A1 > A2 + 1.f);
1746DOCTEST_TEST_CASE(
"PhotosynthesisModel C4 Library - Parameter switching") {
1752 photomodel.disableMessages();
1754 SUBCASE(
"SetariaViridis_vC2021") {
1755 DOCTEST_CHECK_NOTHROW(photomodel.setC4CoefficientsFromLibrary(
"SetariaViridis_vC2021"));
1757 DOCTEST_CHECK(c.getVcmaxTempResponse().value_at_25C == doctest::Approx(40.f).epsilon(err_tol));
1758 DOCTEST_CHECK(c.getVcmaxTempResponse().dHa == doctest::Approx(78.f).epsilon(err_tol));
1759 DOCTEST_CHECK(c.getVpmaxTempResponse().value_at_25C == doctest::Approx(200.f).epsilon(err_tol));
1760 DOCTEST_CHECK(c.getVpmaxTempResponse().dHa == doctest::Approx(50.1f).epsilon(err_tol));
1761 DOCTEST_CHECK(c.getRdTempResponse().value_at_25C == doctest::Approx(0.4f).epsilon(err_tol));
1762 DOCTEST_CHECK(c.getMesophyllConductance_gmTempResponse().value_at_25C == doctest::Approx(1.0f).epsilon(err_tol));
1763 DOCTEST_CHECK(c.
Kc_25 == doctest::Approx(1210.f).epsilon(err_tol));
1764 DOCTEST_CHECK(c.
Kp_25 == doctest::Approx(82.f).epsilon(err_tol));
1765 DOCTEST_CHECK(c.
dH_Kc == doctest::Approx(64.2f).epsilon(err_tol));
1766 DOCTEST_CHECK(c.
fcyc == doctest::Approx(0.45f).epsilon(err_tol));
1767 DOCTEST_CHECK(c.
H_Jcyc == doctest::Approx(3.4f).epsilon(err_tol));
1770 SUBCASE(
"GenericC4_vC2000") {
1771 DOCTEST_CHECK_NOTHROW(photomodel.setC4CoefficientsFromLibrary(
"GenericC4_vC2000"));
1773 DOCTEST_CHECK(c.getVcmaxTempResponse().value_at_25C == doctest::Approx(60.f).epsilon(err_tol));
1774 DOCTEST_CHECK(c.getVpmaxTempResponse().value_at_25C == doctest::Approx(120.f).epsilon(err_tol));
1775 DOCTEST_CHECK(c.getJmaxTempResponse().value_at_25C == doctest::Approx(400.f).epsilon(err_tol));
1776 DOCTEST_CHECK(c.getRdTempResponse().value_at_25C == doctest::Approx(1.0f).epsilon(err_tol));
1778 DOCTEST_CHECK(c.getVcmaxTempResponse().dHa == doctest::Approx(61.6f).epsilon(err_tol));
1779 DOCTEST_CHECK(c.getRdTempResponse().dHa == doctest::Approx(51.2f).epsilon(err_tol));
1780 DOCTEST_CHECK(c.getMesophyllConductance_gmTempResponse().value_at_25C == doctest::Approx(1.0e4f).epsilon(err_tol));
1781 DOCTEST_CHECK(c.
Kp_25 == doctest::Approx(80.f).epsilon(err_tol));
1782 DOCTEST_CHECK(c.
fcyc == doctest::Approx(0.f).epsilon(err_tol));
1785 SUBCASE(
"Maize_Massad2007") {
1786 DOCTEST_CHECK_NOTHROW(photomodel.setC4CoefficientsFromLibrary(
"Maize_Massad2007"));
1788 DOCTEST_CHECK(c.getVcmaxTempResponse().value_at_25C == doctest::Approx(60.f).epsilon(err_tol));
1789 DOCTEST_CHECK(c.getVcmaxTempResponse().dHa == doctest::Approx(67.3f).epsilon(err_tol));
1791 DOCTEST_CHECK(c.getVcmaxTempResponse().Topt == doctest::Approx(273.15f + 32.3f).epsilon(err_tol));
1792 DOCTEST_CHECK(c.getVpmaxTempResponse().Topt == doctest::Approx(273.15f + 43.1f).epsilon(err_tol));
1793 DOCTEST_CHECK(c.getJmaxTempResponse().Topt == doctest::Approx(273.15f + 31.5f).epsilon(err_tol));
1795 DOCTEST_CHECK(c.
Kc_25 == doctest::Approx(650.f).epsilon(err_tol));
1796 DOCTEST_CHECK(c.
Ko_25 == doctest::Approx(450000.f).epsilon(err_tol));
1797 DOCTEST_CHECK(c.
dH_Kc == doctest::Approx(79.43f).epsilon(err_tol));
1798 DOCTEST_CHECK(c.
Kp_25 == doctest::Approx(80.f).epsilon(err_tol));
1799 DOCTEST_CHECK(c.getRdTempResponse().value_at_25C == doctest::Approx(0.6f).epsilon(err_tol));
1800 DOCTEST_CHECK(c.getMesophyllConductance_gmTempResponse().value_at_25C == doctest::Approx(1.0e4f).epsilon(err_tol));
1801 DOCTEST_CHECK(c.
fcyc == doctest::Approx(0.f).epsilon(err_tol));
1804 SUBCASE(
"Case-insensitive key matching") {
1805 DOCTEST_CHECK_NOTHROW(photomodel.getC4CoefficientsFromLibrary(
"setariaviridis_vc2021"));
1806 DOCTEST_CHECK_NOTHROW(photomodel.getC4CoefficientsFromLibrary(
"MAIZE_MASSAD2007"));
1807 DOCTEST_CHECK_NOTHROW(photomodel.getC4CoefficientsFromLibrary(
"GenericC4"));
1810 SUBCASE(
"Entries are distinguishable") {
1813 C4ModelCoefficients setaria = photomodel.getC4CoefficientsFromLibrary(
"SetariaViridis_vC2021");
1814 C4ModelCoefficients generic = photomodel.getC4CoefficientsFromLibrary(
"GenericC4_vC2000");
1817 DOCTEST_CHECK(setaria.getVcmaxTempResponse().value_at_25C != generic.getVcmaxTempResponse().value_at_25C);
1819 DOCTEST_CHECK(setaria.
fcyc != generic.fcyc);
1822 DOCTEST_CHECK(generic.getVpmaxTempResponse().Topt > 9000.f);
1823 DOCTEST_CHECK(maize.getVpmaxTempResponse().Topt < 400.f);
1827DOCTEST_TEST_CASE(
"PhotosynthesisModel C4 Library - Reasonable photosynthesis values") {
1831 const std::vector<std::string> species = {
"SetariaViridis_vC2021",
"GenericC4_vC2000",
"Maize_Massad2007"};
1833 for (
const auto &sp: species) {
1837 photomodel.disableMessages();
1838 photomodel.setC4CoefficientsFromLibrary(sp);
1839 photomodel.optionalOutputPrimitiveData(
"limitation_state");
1846 DOCTEST_CHECK_NOTHROW(photomodel.run());
1848 int limitation_state = 0;
1854 bool A_high_in_range = (A_high >= 10.f) && (A_high <= 60.f);
1855 bool lim_valid = (limitation_state == 1) || (limitation_state == 2);
1856 DOCTEST_CHECK_MESSAGE(A_high_in_range, sp <<
": A at high PAR out of reasonable C4 range (" << A_high <<
")");
1857 DOCTEST_CHECK_MESSAGE(lim_valid, sp <<
": unexpected limitation_state " << limitation_state);
1861 DOCTEST_CHECK_NOTHROW(photomodel.run());
1864 bool A_low_valid = (A_low > 0.f) && (A_low < A_high);
1865 DOCTEST_CHECK_MESSAGE(A_low_valid, sp <<
": low-light A=" << A_low <<
" (should be 0<A<" << A_high <<
")");
1869DOCTEST_TEST_CASE(
"PhotosynthesisModel C4 Library - Temperature discrimination") {
1873 auto run_once = [](
const std::string &sp,
float T_K) {
1877 pm.disableMessages();
1878 pm.setC4CoefficientsFromLibrary(sp);
1888 float setaria_25 = run_once(
"SetariaViridis_vC2021", 298.15f);
1889 float setaria_35 = run_once(
"SetariaViridis_vC2021", 308.15f);
1890 float maize_25 = run_once(
"Maize_Massad2007", 298.15f);
1891 float maize_35 = run_once(
"Maize_Massad2007", 308.15f);
1894 DOCTEST_CHECK(setaria_25 > 0.f);
1895 DOCTEST_CHECK(setaria_35 > 0.f);
1896 DOCTEST_CHECK(maize_25 > 0.f);
1897 DOCTEST_CHECK(maize_35 > 0.f);
1900 DOCTEST_CHECK(std::abs(setaria_25 - maize_25) > 0.5f);
1901 DOCTEST_CHECK(std::abs(setaria_35 - maize_35) > 0.5f);
1904 DOCTEST_CHECK(setaria_35 > setaria_25);
1907DOCTEST_TEST_CASE(
"PhotosynthesisModel C4 Library - Unknown species raises error") {
1911 photomodel.disableMessages();
1913 DOCTEST_CHECK_THROWS_AS(photomodel.getC4CoefficientsFromLibrary(
"NotARealSpecies"), std::runtime_error);
1914 DOCTEST_CHECK_THROWS_AS(photomodel.setC4CoefficientsFromLibrary(
""), std::runtime_error);
1916 DOCTEST_CHECK_THROWS_AS(photomodel.getC4CoefficientsFromLibrary(
"Maiz"), std::runtime_error);
1925DOCTEST_TEST_CASE(
"PhotosynthesisModel Empirical temperature response is applied") {
1933 photomodel.disableMessages();
1936 photomodel.setModelCoefficients(emp);
1950 DOCTEST_CHECK(A_at_opt > A_hot);
1955 float A_default_topt;
1959 emp_shifted.Topt = 280.f;
1960 photomodel.setModelCoefficients(emp_shifted);
1962 float A_shifted_topt;
1965 DOCTEST_CHECK(std::abs(A_default_topt - A_shifted_topt) > 1e-4f);
1969 photomodel.setModelCoefficients(emp_cold);
1974 DOCTEST_CHECK(A_cold < 0.f);
1977DOCTEST_TEST_CASE(
"PhotosynthesisModel Empirical invalid temperature coefficients throw below Tmin") {
1987 photomodel.disableMessages();
1988 photomodel.setModelType_Empirical();
1992 bad_coeffs.Tmin = 290.f;
1993 bad_coeffs.Tref = 280.f;
1994 photomodel.setModelCoefficients(bad_coeffs);
1998 DOCTEST_CHECK_THROWS(photomodel.run());
2002 DOCTEST_CHECK_THROWS(photomodel.run());
2005DOCTEST_TEST_CASE(
"PhotosynthesisModel Farquhar material round-trip preserves TPU and theta") {
2015 photomodel.disableMessages();
2020 fq.
setRd(1.3f, 46.39f);
2025 photomodel.setModelCoefficients(
"fq_roundtrip_mat", fq);
2028 DOCTEST_CHECK(rt.TPU_flag == 1);
2029 DOCTEST_CHECK(rt.getTPUTempResponse().value_at_25C == doctest::Approx(7.7f).epsilon(1e-4));
2030 DOCTEST_CHECK(rt.getTPUTempResponse().dHa == doctest::Approx(24.0f).epsilon(1e-4));
2031 DOCTEST_CHECK(rt.getLightResponseCurvatureTempResponse().value_at_25C == doctest::Approx(0.601f).epsilon(1e-4));
2032 DOCTEST_CHECK(rt.getQuantumEfficiencyTempResponse().value_at_25C == doctest::Approx(0.304f).epsilon(1e-4));
2033 DOCTEST_CHECK(rt.getVcmaxTempResponse().value_at_25C == doctest::Approx(74.5f).epsilon(1e-4));
2042 fq_notpu.
setJmax(200.f, 46.36f);
2043 fq_notpu.
setRd(1.5f, 46.39f);
2045 photomodel.setModelCoefficients(
"fq_notpu_mat", fq_notpu);
2048 DOCTEST_CHECK(rt2.TPU_flag == 0);
2049 DOCTEST_CHECK(rt2.getLightResponseCurvatureTempResponse().value_at_25C == doctest::Approx(0.f).epsilon(1e-6));
2052DOCTEST_TEST_CASE(
"PhotosynthesisModel peaked temperature response rejects dHd <= dHa") {
2060 DOCTEST_CHECK_THROWS_AS(fq.
setVcmax(100.f, 65.f, 30.f, 50.f), std::runtime_error);
2061 DOCTEST_CHECK_THROWS_AS(fq.
setJmax(200.f, 46.f, 30.f, 40.f), std::runtime_error);
2062 DOCTEST_CHECK_THROWS_AS(fq.
setRd(1.5f, 46.f, 30.f, 46.f), std::runtime_error);
2063 DOCTEST_CHECK_THROWS_AS(fq.
setTPU(7.7f, 24.f, 30.f, 20.f), std::runtime_error);
2066 DOCTEST_CHECK_THROWS_AS(c4.
setVpmax(200.f, 50.f, 30.f, 40.f), std::runtime_error);
2067 DOCTEST_CHECK_THROWS_AS(c4.
setJmax(247.f, 77.9f, 43.f, 70.f), std::runtime_error);
2070 DOCTEST_CHECK_NOTHROW(fq.
setVcmax(100.f, 65.f, 30.f, 200.f));
2078 photomodel.disableMessages();
2080 good.
setVcmax(100.f, 65.f, 30.f, 650.f);
2082 good.
setRd(1.5f, 46.39f);
2084 photomodel.setModelCoefficients(good);
2088 DOCTEST_CHECK(std::isfinite(A));
2091DOCTEST_TEST_CASE(
"PhotosynthesisModel Farquhar setters take precedence over legacy fields") {
2101 photomodel.disableMessages();
2102 photomodel.setCi(250.f, {UUID});
2107 baseline.
setRd(1.5f);
2109 photomodel.setModelCoefficients(baseline);
2116 overridden.Vcmax = 50.f;
2119 overridden.
setRd(1.5f);
2121 photomodel.setModelCoefficients(overridden);
2126 DOCTEST_CHECK(A_overridden == doctest::Approx(A_setter).epsilon(1e-4));
2129 DOCTEST_CHECK(overridden.Vcmax < 0.f);
2132DOCTEST_TEST_CASE(
"PhotosynthesisModel Farquhar library values match documentation") {
2136 photomodel.disableMessages();
2139 const char *species;
2147 const std::vector<LibExpect> expected{
2148 {
"Almond", 72.6f, 144.2f, 0.2f, 0.094f},
2149 {
"Walnut", 81.6f, 201.9f, 0.9f, 0.362f},
2150 {
"Pistachio", 101.8f, 223.f, 1.5f, 0.216f},
2151 {
"Apple", 101.08f, 167.03f, 3.00f, 0.432f},
2152 {
"Cherry", 75.65f, 129.06f, 2.12f, 0.404f},
2153 {
"Pear", 107.69f, 176.71f, 1.51f, 0.274f},
2154 {
"Prune", 75.88f, 129.41f, 1.65f, 0.402f},
2155 {
"Grape", 74.5f, 180.2f, 1.3f, 0.304f},
2156 {
"Olive", 75.9f, 170.4f, 1.9f, 0.398f},
2157 {
"Toyon", 52.8f, 142.4f, 0.8f, 0.29f},
2158 {
"Elderberry", 37.7f, 149.7f, 1.3f, 0.202f},
2159 {
"Maple", 96.4f, 168.f, 0.1f, 0.077f},
2160 {
"Redbud", 68.5f, 132.4f, 0.8f, 0.41f},
2163 for (
const auto &e: expected) {
2165 DOCTEST_INFO(
"species = " << e.species);
2166 DOCTEST_CHECK(c.getVcmaxTempResponse().value_at_25C == doctest::Approx(e.Vcmax25).epsilon(1e-3));
2167 DOCTEST_CHECK(c.getJmaxTempResponse().value_at_25C == doctest::Approx(e.Jmax25).epsilon(1e-3));
2168 DOCTEST_CHECK(c.getRdTempResponse().value_at_25C == doctest::Approx(e.Rd25).epsilon(1e-3));
2169 DOCTEST_CHECK(c.getQuantumEfficiencyTempResponse().value_at_25C == doctest::Approx(e.alpha).epsilon(1e-3));
2173 for (
const char *sp: {
"Almond",
"Walnut",
"Pistachio"}) {
2175 DOCTEST_INFO(
"species = " << sp);
2176 DOCTEST_CHECK(c.TPU_flag == 1);
2180 for (
const char *sp: {
"Apple",
"Cherry",
"Pear",
"Prune"}) {
2182 DOCTEST_INFO(
"species = " << sp);
2183 DOCTEST_CHECK(c.TPU_flag == 0);
2191 DOCTEST_CHECK(pist_default.getVcmaxTempResponse().value_at_25C == doctest::Approx(pist_female.getVcmaxTempResponse().value_at_25C).epsilon(1e-4));
2192 DOCTEST_CHECK(pist_default.getJmaxTempResponse().value_at_25C == doctest::Approx(pist_female.getJmaxTempResponse().value_at_25C).epsilon(1e-4));
2193 DOCTEST_CHECK(pist_default.TPU_flag == pist_female.TPU_flag);
2196 DOCTEST_CHECK(pist_male.getVcmaxTempResponse().value_at_25C == doctest::Approx(154.17f).epsilon(1e-3));
2197 DOCTEST_CHECK(pist_male.getJmaxTempResponse().value_at_25C == doctest::Approx(243.20f).epsilon(1e-3));
2198 DOCTEST_CHECK(pist_male.getRdTempResponse().value_at_25C == doctest::Approx(2.05f).epsilon(1e-3));
2199 DOCTEST_CHECK(pist_male.getQuantumEfficiencyTempResponse().value_at_25C == doctest::Approx(0.335f).epsilon(1e-3));
2200 DOCTEST_CHECK(pist_male.TPU_flag == 0);
2201 DOCTEST_CHECK(pist_male.getVcmaxTempResponse().value_at_25C != doctest::Approx(pist_female.getVcmaxTempResponse().value_at_25C).epsilon(1e-4));
2204int PhotosynthesisModel::selfTest(
int argc,
char **argv) {
2205 return helios::runDoctestWithValidation(argc, argv);