3#define DOCTEST_CONFIG_IMPLEMENT
5#include "doctest_utils.h"
11DOCTEST_TEST_CASE(
"EnergyBalanceModel Equilibrium Test") {
13 std::vector<uint> UUIDs;
18 context_test.
setPrimitiveData(UUIDs,
"radiation_flux_LW",
float(2.f * 5.67e-8 * pow(Tref, 4)));
22 energymodeltest.disableMessages();
23 energymodeltest.addRadiationBand(
"LW");
24 DOCTEST_CHECK_NOTHROW(energymodeltest.run());
26 for (
int p = 0; p < UUIDs.size(); p++) {
28 DOCTEST_CHECK_NOTHROW(context_test.
getPrimitiveData(UUIDs.at(p),
"temperature", T));
29 DOCTEST_CHECK(T == doctest::Approx(Tref).epsilon(err_tol));
33DOCTEST_TEST_CASE(
"EnergyBalanceModel Energy Budget Closure") {
35 std::vector<uint> UUIDs_2;
40 context_2.
setPrimitiveData(UUIDs_2,
"radiation_flux_LW",
float(2.f * 5.67e-8 * pow(T, 4)));
45 energymodel_2.disableMessages();
46 energymodel_2.addRadiationBand(
"LW");
47 energymodel_2.addRadiationBand(
"SW");
48 DOCTEST_CHECK_NOTHROW(energymodel_2.run());
50 for (
int p = 0; p < UUIDs_2.size(); p++) {
51 float sensible_flux, latent_flux,
R, temperature;
54 DOCTEST_CHECK_NOTHROW(context_2.
getPrimitiveData(UUIDs_2.at(p),
"sensible_flux", sensible_flux));
55 DOCTEST_CHECK_NOTHROW(context_2.
getPrimitiveData(UUIDs_2.at(p),
"latent_flux", latent_flux));
56 DOCTEST_CHECK_NOTHROW(context_2.
getPrimitiveData(UUIDs_2.at(p),
"radiation_flux_LW",
R));
58 DOCTEST_CHECK_NOTHROW(context_2.
getPrimitiveData(UUIDs_2.at(p),
"radiation_flux_SW",
R));
60 DOCTEST_CHECK_NOTHROW(context_2.
getPrimitiveData(UUIDs_2.at(p),
"temperature", temperature));
62 float Rout = 2.f * 5.67e-8 * pow(temperature, 4);
63 float resid = Rin - Rout - sensible_flux - latent_flux;
64 DOCTEST_CHECK(resid == doctest::Approx(0.0f).epsilon(err_tol));
68DOCTEST_TEST_CASE(
"EnergyBalanceModel Temperature Solution Check 1") {
73 context_3.
setPrimitiveData(UUID_3,
"radiation_flux_LW",
float(5.67e-8 * pow(T, 4)));
84 energymodel_3.disableMessages();
85 energymodel_3.addRadiationBand(
"LW");
86 energymodel_3.addRadiationBand(
"SW");
87 DOCTEST_CHECK_NOTHROW(energymodel_3.run());
89 float sensible_flux, latent_flux, temperature;
90 float sensible_flux_exact = 48.7017;
91 float latent_flux_exact = 21.6094;
92 float temperature_exact = 329.307;
94 DOCTEST_CHECK_NOTHROW(context_3.
getPrimitiveData(UUID_3,
"sensible_flux", sensible_flux));
95 DOCTEST_CHECK_NOTHROW(context_3.
getPrimitiveData(UUID_3,
"latent_flux", latent_flux));
96 DOCTEST_CHECK_NOTHROW(context_3.
getPrimitiveData(UUID_3,
"temperature", temperature));
98 DOCTEST_CHECK(sensible_flux == doctest::Approx(sensible_flux_exact).epsilon(err_tol));
99 DOCTEST_CHECK(latent_flux == doctest::Approx(latent_flux_exact).epsilon(err_tol));
100 DOCTEST_CHECK(temperature == doctest::Approx(temperature_exact).epsilon(err_tol));
103DOCTEST_TEST_CASE(
"EnergyBalanceModel Temperature Solution Check 2 - Object Length") {
108 context_3.
setPrimitiveData(UUID_3,
"radiation_flux_LW",
float(5.67e-8 * pow(T, 4)));
119 energymodel_3.disableMessages();
120 energymodel_3.addRadiationBand(
"LW");
121 energymodel_3.addRadiationBand(
"SW");
125 DOCTEST_CHECK_NOTHROW(energymodel_3.run());
127 float sensible_flux, latent_flux, temperature;
128 float sensible_flux_exact = 89.2024;
129 float latent_flux_exact = 20.0723;
130 float temperature_exact = 324.386;
132 DOCTEST_CHECK_NOTHROW(context_3.
getPrimitiveData(UUID_3,
"sensible_flux", sensible_flux));
133 DOCTEST_CHECK_NOTHROW(context_3.
getPrimitiveData(UUID_3,
"latent_flux", latent_flux));
134 DOCTEST_CHECK_NOTHROW(context_3.
getPrimitiveData(UUID_3,
"temperature", temperature));
136 DOCTEST_CHECK(sensible_flux == doctest::Approx(sensible_flux_exact).epsilon(err_tol));
137 DOCTEST_CHECK(latent_flux == doctest::Approx(latent_flux_exact).epsilon(err_tol));
138 DOCTEST_CHECK(temperature == doctest::Approx(temperature_exact).epsilon(err_tol));
141DOCTEST_TEST_CASE(
"EnergyBalanceModel Temperature Solution Check 3 - Manual Boundary Layer Conductance") {
146 context_3.
setPrimitiveData(UUID_3,
"radiation_flux_LW",
float(5.67e-8 * pow(T, 4)));
157 energymodel_3.disableMessages();
158 energymodel_3.addRadiationBand(
"LW");
159 energymodel_3.addRadiationBand(
"SW");
162 context_3.
setPrimitiveData(UUID_3,
"boundarylayer_conductance",
float(0.134f));
163 DOCTEST_CHECK_NOTHROW(energymodel_3.run());
165 float sensible_flux, latent_flux, temperature;
166 float sensible_flux_exact = 61.5411f;
167 float latent_flux_exact = 21.6718f;
168 float temperature_exact = 327.701f;
170 DOCTEST_CHECK_NOTHROW(context_3.
getPrimitiveData(UUID_3,
"sensible_flux", sensible_flux));
171 DOCTEST_CHECK_NOTHROW(context_3.
getPrimitiveData(UUID_3,
"latent_flux", latent_flux));
172 DOCTEST_CHECK_NOTHROW(context_3.
getPrimitiveData(UUID_3,
"temperature", temperature));
174 DOCTEST_CHECK(sensible_flux == doctest::Approx(sensible_flux_exact).epsilon(err_tol));
175 DOCTEST_CHECK(latent_flux == doctest::Approx(latent_flux_exact).epsilon(err_tol));
176 DOCTEST_CHECK(temperature == doctest::Approx(temperature_exact).epsilon(err_tol));
179DOCTEST_TEST_CASE(
"EnergyBalanceModel Optional Primitive Data Output Check") {
184 energymodel_4.disableMessages();
185 energymodel_4.addRadiationBand(
"LW");
186 DOCTEST_CHECK_NOTHROW(energymodel_4.optionalOutputPrimitiveData(
"boundarylayer_conductance_out"));
187 DOCTEST_CHECK_NOTHROW(energymodel_4.optionalOutputPrimitiveData(
"vapor_pressure_deficit"));
190 DOCTEST_CHECK_NOTHROW(energymodel_4.run());
196DOCTEST_TEST_CASE(
"EnergyBalanceModel Dynamic Model Check") {
198 float dt_5 = 1.f, T_5 = 3600, To_5 = 300.f, cp_5 = 2000;
199 float Rlow = 50.f, Rhigh = 500.f;
209 energybalance_5.disableMessages();
210 energybalance_5.addRadiationBand(
"SW");
211 DOCTEST_CHECK_NOTHROW(energybalance_5.optionalOutputPrimitiveData(
"boundarylayer_conductance_out"));
213 std::vector<float> temperature_dyn;
214 int N = round(T_5 / dt_5);
215 for (
int t = 0; t < N; t++) {
219 DOCTEST_CHECK_NOTHROW(energybalance_5.run(dt_5));
222 DOCTEST_CHECK_NOTHROW(context_5.
getPrimitiveData(UUID_5,
"temperature", temp));
223 temperature_dyn.push_back(temp);
227 DOCTEST_CHECK_NOTHROW(context_5.
getPrimitiveData(UUID_5,
"boundarylayer_conductance_out", gH_5));
228 float tau_5 = cp_5 / gH_5 / 29.25f;
231 DOCTEST_CHECK_NOTHROW(energybalance_5.run());
233 DOCTEST_CHECK_NOTHROW(context_5.
getPrimitiveData(UUID_5,
"temperature", Tlow));
236 DOCTEST_CHECK_NOTHROW(energybalance_5.run());
238 DOCTEST_CHECK_NOTHROW(context_5.
getPrimitiveData(UUID_5,
"temperature", Thigh));
241 for (
int t = round(0.5f * N); t < N; t++) {
242 float time = dt_5 * (t - round(0.5f * N));
243 float temperature_ref = Tlow + (Thigh - Tlow) * (1.f - exp(-time / tau_5));
244 err += pow(temperature_ref - temperature_dyn.at(t), 2);
247 err = sqrt(err /
float(N));
248 DOCTEST_CHECK(err == doctest::Approx(0.0f).epsilon(0.2f));
251DOCTEST_TEST_CASE(
"EnergyBalanceModel Enable/Disable Messages") {
255 DOCTEST_CHECK_NOTHROW(testModel.enableMessages());
256 DOCTEST_CHECK_NOTHROW(testModel.disableMessages());
259DOCTEST_TEST_CASE(
"EnergyBalanceModel Radiation Band Management") {
263 DOCTEST_CHECK_NOTHROW(testModel.addRadiationBand(
"LW"));
264 DOCTEST_CHECK_NOTHROW(testModel.addRadiationBand(
"LW"));
265 DOCTEST_CHECK_NOTHROW(testModel.addRadiationBand(
"SW"));
268DOCTEST_TEST_CASE(
"EnergyBalanceModel Optional Output Primitive Data") {
272 DOCTEST_CHECK_NOTHROW(testModel.optionalOutputPrimitiveData(
"boundarylayer_conductance_out"));
274 bool has_cerr_output;
277 testModel.optionalOutputPrimitiveData(
"invalid_label");
280 DOCTEST_CHECK(has_cerr_output);
283DOCTEST_TEST_CASE(
"EnergyBalanceModel Print Default Value Report") {
287 std::stringstream buffer;
288 std::streambuf *old = std::cout.rdbuf(buffer.rdbuf());
290 DOCTEST_CHECK_NOTHROW(testModel.printDefaultValueReport());
292 std::cout.rdbuf(old);
294 std::string output = buffer.str();
295 std::vector<std::string> required_keywords = {
"surface temperature",
"air pressure",
"air temperature",
"air humidity",
"boundary-layer conductance",
"moisture conductance",
"surface humidity",
"two-sided flag",
"evaporating faces"};
297 for (
const auto &keyword: required_keywords) {
298 DOCTEST_CHECK(output.find(keyword) != std::string::npos);
302DOCTEST_TEST_CASE(
"EnergyBalanceModel Print Default Value Report with UUIDs") {
305 std::vector<uint> testUUIDs;
308 std::stringstream buffer;
309 std::streambuf *old = std::cout.rdbuf(buffer.rdbuf());
311 DOCTEST_CHECK_NOTHROW(testModel.printDefaultValueReport(testUUIDs));
313 std::cout.rdbuf(old);
315 std::string output = buffer.str();
316 std::vector<std::string> required_keywords = {
"surface temperature",
"air pressure",
"air temperature",
"air humidity",
"boundary-layer conductance",
"moisture conductance",
"surface humidity",
"two-sided flag",
"evaporating faces"};
318 for (
const auto &keyword: required_keywords) {
319 DOCTEST_CHECK(output.find(keyword) != std::string::npos);
323DOCTEST_TEST_CASE(
"EnergyBalanceModel Additional Dynamic Model Check") {
325 float dt = 1.f, Tfinal = 3600, To = 300.f, cp = 2000;
326 float Rlow = 50.f, Rhigh = 500.f;
336 energybalance_dyn.disableMessages();
337 energybalance_dyn.addRadiationBand(
"SW");
339 std::vector<float> temperature_dyn;
340 int N = round(Tfinal / dt);
341 for (
int t = 0; t < N; t++) {
345 DOCTEST_CHECK_NOTHROW(energybalance_dyn.run(dt));
348 DOCTEST_CHECK_NOTHROW(context_dyn.
getPrimitiveData(UUID_dyn,
"temperature", temp));
349 temperature_dyn.push_back(temp);
352 DOCTEST_CHECK(!temperature_dyn.empty());
355#ifdef HELIOS_CUDA_AVAILABLE
356DOCTEST_TEST_CASE(
"EnergyBalanceModel GPU vs CPU Consistency") {
363 context_gpu_cpu.
setPrimitiveData(UUID_gpu_cpu,
"moisture_conductance", 0.1f);
366 model_gpu_cpu.disableMessages();
367 model_gpu_cpu.addRadiationBand(
"SW");
370 bool gpu_available = model_gpu_cpu.isGPUAccelerationEnabled();
371 if (!gpu_available) {
372 DOCTEST_WARN(
"GPU not available - skipping GPU vs CPU comparison test");
377 DOCTEST_CHECK_NOTHROW(model_gpu_cpu.run());
379 DOCTEST_CHECK_NOTHROW(context_gpu_cpu.
getPrimitiveData(UUID_gpu_cpu,
"temperature", T_gpu));
382 model_gpu_cpu.disableGPUAcceleration();
383 DOCTEST_CHECK(model_gpu_cpu.isGPUAccelerationEnabled() ==
false);
384 DOCTEST_CHECK_NOTHROW(model_gpu_cpu.run());
386 DOCTEST_CHECK_NOTHROW(context_gpu_cpu.
getPrimitiveData(UUID_gpu_cpu,
"temperature", T_cpu));
389 DOCTEST_CHECK(T_cpu == doctest::Approx(T_gpu).epsilon(1e-4));
393DOCTEST_TEST_CASE(
"EnergyBalanceModel - Default value warnings") {
395 DOCTEST_SUBCASE(
"Missing air_temperature triggers warning") {
407 model.addRadiationBand(
"LW");
411 DOCTEST_CHECK(output.find(
"missing_air_temperature") != std::string::npos);
412 DOCTEST_CHECK(output.find(
"WARNING:") != std::string::npos);
413 DOCTEST_CHECK(output.find(
"instance") != std::string::npos);
416 DOCTEST_SUBCASE(
"Missing wind_speed triggers warning") {
428 model.addRadiationBand(
"LW");
432 DOCTEST_CHECK(output.find(
"missing_wind_speed") != std::string::npos);
433 DOCTEST_CHECK(output.find(
"WARNING:") != std::string::npos);
436 DOCTEST_SUBCASE(
"Missing air_humidity triggers warning") {
448 model.addRadiationBand(
"LW");
452 DOCTEST_CHECK(output.find(
"missing_air_humidity") != std::string::npos);
453 DOCTEST_CHECK(output.find(
"WARNING:") != std::string::npos);
456 DOCTEST_SUBCASE(
"Missing surface temperature triggers warning") {
468 model.addRadiationBand(
"LW");
472 DOCTEST_CHECK(output.find(
"missing_surface_temperature") != std::string::npos);
473 DOCTEST_CHECK(output.find(
"WARNING:") != std::string::npos);
476 DOCTEST_SUBCASE(
"Missing emissivity triggers warning") {
488 model.addRadiationBand(
"LW");
492 DOCTEST_CHECK(output.find(
"missing_emissivity") != std::string::npos);
493 DOCTEST_CHECK(output.find(
"WARNING:") != std::string::npos);
496 DOCTEST_SUBCASE(
"Air temperature < 250K triggers warning") {
507 model.addRadiationBand(
"LW");
511 DOCTEST_CHECK(output.find(
"air_temperature_likely_celsius") != std::string::npos);
512 DOCTEST_CHECK(output.find(
"WARNING:") != std::string::npos);
515 DOCTEST_SUBCASE(
"Air humidity out of range triggers warning") {
527 model.addRadiationBand(
"LW");
531 DOCTEST_CHECK(output.find(
"air_humidity_out_of_range_high") != std::string::npos);
532 DOCTEST_CHECK(output.find(
"WARNING:") != std::string::npos);
535 DOCTEST_SUBCASE(
"Messages can be disabled") {
546 model.disableMessages();
547 model.addRadiationBand(
"LW");
552 DOCTEST_CHECK(output.find(
"WARNING:") == std::string::npos);
555 DOCTEST_SUBCASE(
"Multiple missing parameters produce aggregated warnings") {
557 std::vector<uint> UUIDs;
559 for (
int i = 0; i < 10; i++) {
561 UUIDs.push_back(UUID);
570 model.addRadiationBand(
"LW");
576 DOCTEST_CHECK(output.find(
"missing_air_temperature") != std::string::npos);
577 DOCTEST_CHECK(output.find(
"missing_wind_speed") != std::string::npos);
578 DOCTEST_CHECK(output.find(
"missing_air_humidity") != std::string::npos);
581 DOCTEST_CHECK(output.find(
"10 instances") != std::string::npos);
584 DOCTEST_CHECK(output.find(
"(showing first") == std::string::npos);
589 return helios::runDoctestWithValidation(argc, argv);