1.3.64
 
Loading...
Searching...
No Matches
selfTest.cpp
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("EnergyBalanceModel Equilibrium Test") {
12 Context context_test;
13 std::vector<uint> UUIDs;
14 UUIDs.push_back(context_test.addPatch(make_vec3(1, 2, 3), make_vec2(3, 2)));
15 UUIDs.push_back(context_test.addTriangle(make_vec3(4, 5, 6), make_vec3(5, 5, 6), make_vec3(5, 6, 6)));
16
17 float Tref = 350;
18 context_test.setPrimitiveData(UUIDs, "radiation_flux_LW", float(2.f * 5.67e-8 * pow(Tref, 4)));
19 context_test.setPrimitiveData(UUIDs, "air_temperature", Tref);
20
21 EnergyBalanceModel energymodeltest(&context_test);
22 energymodeltest.disableMessages();
23 energymodeltest.addRadiationBand("LW");
24 DOCTEST_CHECK_NOTHROW(energymodeltest.run());
25
26 for (int p = 0; p < UUIDs.size(); p++) {
27 float T;
28 DOCTEST_CHECK_NOTHROW(context_test.getPrimitiveData(UUIDs.at(p), "temperature", T));
29 DOCTEST_CHECK(T == doctest::Approx(Tref).epsilon(err_tol));
30 }
31}
32
33DOCTEST_TEST_CASE("EnergyBalanceModel Energy Budget Closure") {
34 Context context_2;
35 std::vector<uint> UUIDs_2;
36 UUIDs_2.push_back(context_2.addPatch(make_vec3(1, 2, 3), make_vec2(3, 2)));
37 UUIDs_2.push_back(context_2.addTriangle(make_vec3(4, 5, 6), make_vec3(5, 5, 6), make_vec3(5, 6, 6)));
38
39 float T = 300;
40 context_2.setPrimitiveData(UUIDs_2, "radiation_flux_LW", float(2.f * 5.67e-8 * pow(T, 4)));
41 context_2.setPrimitiveData(UUIDs_2, "radiation_flux_SW", float(300.f));
42 context_2.setPrimitiveData(UUIDs_2, "air_temperature", T);
43
44 EnergyBalanceModel energymodel_2(&context_2);
45 energymodel_2.disableMessages();
46 energymodel_2.addRadiationBand("LW");
47 energymodel_2.addRadiationBand("SW");
48 DOCTEST_CHECK_NOTHROW(energymodel_2.run());
49
50 for (int p = 0; p < UUIDs_2.size(); p++) {
51 float sensible_flux, latent_flux, R, temperature;
52 float Rin = 0;
53
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));
57 Rin += R;
58 DOCTEST_CHECK_NOTHROW(context_2.getPrimitiveData(UUIDs_2.at(p), "radiation_flux_SW", R));
59 Rin += R;
60 DOCTEST_CHECK_NOTHROW(context_2.getPrimitiveData(UUIDs_2.at(p), "temperature", temperature));
61
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));
65 }
66}
67
68DOCTEST_TEST_CASE("EnergyBalanceModel Temperature Solution Check 1") {
69 Context context_3;
70 uint UUID_3 = context_3.addPatch(make_vec3(1, 2, 3), make_vec2(3, 2));
71
72 float T = 312.f;
73 context_3.setPrimitiveData(UUID_3, "radiation_flux_LW", float(5.67e-8 * pow(T, 4)));
74 context_3.setPrimitiveData(UUID_3, "radiation_flux_SW", float(350.f));
75 context_3.setPrimitiveData(UUID_3, "wind_speed", float(1.244f));
76 context_3.setPrimitiveData(UUID_3, "moisture_conductance", float(0.05f));
77 context_3.setPrimitiveData(UUID_3, "air_humidity", float(0.4f));
78 context_3.setPrimitiveData(UUID_3, "air_pressure", float(956789));
79 context_3.setPrimitiveData(UUID_3, "other_surface_flux", float(150.f));
80 context_3.setPrimitiveData(UUID_3, "air_temperature", T);
81 context_3.setPrimitiveData(UUID_3, "twosided_flag", uint(0));
82
83 EnergyBalanceModel energymodel_3(&context_3);
84 energymodel_3.disableMessages();
85 energymodel_3.addRadiationBand("LW");
86 energymodel_3.addRadiationBand("SW");
87 DOCTEST_CHECK_NOTHROW(energymodel_3.run());
88
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;
93
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));
97
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));
101}
102
103DOCTEST_TEST_CASE("EnergyBalanceModel Temperature Solution Check 2 - Object Length") {
104 Context context_3;
105 uint UUID_3 = context_3.addPatch(make_vec3(1, 2, 3), make_vec2(3, 2));
106
107 float T = 312.f;
108 context_3.setPrimitiveData(UUID_3, "radiation_flux_LW", float(5.67e-8 * pow(T, 4)));
109 context_3.setPrimitiveData(UUID_3, "radiation_flux_SW", float(350.f));
110 context_3.setPrimitiveData(UUID_3, "wind_speed", float(1.244f));
111 context_3.setPrimitiveData(UUID_3, "moisture_conductance", float(0.05f));
112 context_3.setPrimitiveData(UUID_3, "air_humidity", float(0.4f));
113 context_3.setPrimitiveData(UUID_3, "air_pressure", float(956789));
114 context_3.setPrimitiveData(UUID_3, "other_surface_flux", float(150.f));
115 context_3.setPrimitiveData(UUID_3, "air_temperature", T);
116 context_3.setPrimitiveData(UUID_3, "twosided_flag", uint(0));
117
118 EnergyBalanceModel energymodel_3(&context_3);
119 energymodel_3.disableMessages();
120 energymodel_3.addRadiationBand("LW");
121 energymodel_3.addRadiationBand("SW");
122
123 // Use object length instead of sqrt(area)
124 context_3.setPrimitiveData(UUID_3, "object_length", float(0.374f));
125 DOCTEST_CHECK_NOTHROW(energymodel_3.run());
126
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;
131
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));
135
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));
139}
140
141DOCTEST_TEST_CASE("EnergyBalanceModel Temperature Solution Check 3 - Manual Boundary Layer Conductance") {
142 Context context_3;
143 uint UUID_3 = context_3.addPatch(make_vec3(1, 2, 3), make_vec2(3, 2));
144
145 float T = 312.f;
146 context_3.setPrimitiveData(UUID_3, "radiation_flux_LW", float(5.67e-8 * pow(T, 4)));
147 context_3.setPrimitiveData(UUID_3, "radiation_flux_SW", float(350.f));
148 context_3.setPrimitiveData(UUID_3, "wind_speed", float(1.244f));
149 context_3.setPrimitiveData(UUID_3, "moisture_conductance", float(0.05f));
150 context_3.setPrimitiveData(UUID_3, "air_humidity", float(0.4f));
151 context_3.setPrimitiveData(UUID_3, "air_pressure", float(956789));
152 context_3.setPrimitiveData(UUID_3, "other_surface_flux", float(150.f));
153 context_3.setPrimitiveData(UUID_3, "air_temperature", T);
154 context_3.setPrimitiveData(UUID_3, "twosided_flag", uint(0));
155
156 EnergyBalanceModel energymodel_3(&context_3);
157 energymodel_3.disableMessages();
158 energymodel_3.addRadiationBand("LW");
159 energymodel_3.addRadiationBand("SW");
160
161 // Manually set boundary-layer conductance
162 context_3.setPrimitiveData(UUID_3, "boundarylayer_conductance", float(0.134f));
163 DOCTEST_CHECK_NOTHROW(energymodel_3.run());
164
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;
169
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));
173
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));
177}
178
179DOCTEST_TEST_CASE("EnergyBalanceModel Optional Primitive Data Output Check") {
180 Context context_4;
181 uint UUID_4 = context_4.addPatch(make_vec3(1, 2, 3), make_vec2(3, 2));
182
183 EnergyBalanceModel energymodel_4(&context_4);
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"));
188
189 context_4.setPrimitiveData(UUID_4, "radiation_flux_LW", 0.f);
190 DOCTEST_CHECK_NOTHROW(energymodel_4.run());
191
192 DOCTEST_CHECK(context_4.doesPrimitiveDataExist(UUID_4, "vapor_pressure_deficit"));
193 DOCTEST_CHECK(context_4.doesPrimitiveDataExist(UUID_4, "boundarylayer_conductance_out"));
194}
195
196DOCTEST_TEST_CASE("EnergyBalanceModel Dynamic Model Check") {
197 Context context_5;
198 float dt_5 = 1.f, T_5 = 3600, To_5 = 300.f, cp_5 = 2000;
199 float Rlow = 50.f, Rhigh = 500.f;
200
201 uint UUID_5 = context_5.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
202 context_5.setPrimitiveData(UUID_5, "radiation_flux_SW", Rlow);
203 context_5.setPrimitiveData(UUID_5, "temperature", To_5);
204 context_5.setPrimitiveData(UUID_5, "heat_capacity", cp_5);
205 context_5.setPrimitiveData(UUID_5, "twosided_flag", uint(0));
206 context_5.setPrimitiveData(UUID_5, "emissivity_SW", 0.f);
207
208 EnergyBalanceModel energybalance_5(&context_5);
209 energybalance_5.disableMessages();
210 energybalance_5.addRadiationBand("SW");
211 DOCTEST_CHECK_NOTHROW(energybalance_5.optionalOutputPrimitiveData("boundarylayer_conductance_out"));
212
213 std::vector<float> temperature_dyn;
214 int N = round(T_5 / dt_5);
215 for (int t = 0; t < N; t++) {
216 if (t > 0.5f * N) {
217 context_5.setPrimitiveData(UUID_5, "radiation_flux_SW", Rhigh);
218 }
219 DOCTEST_CHECK_NOTHROW(energybalance_5.run(dt_5));
220
221 float temp;
222 DOCTEST_CHECK_NOTHROW(context_5.getPrimitiveData(UUID_5, "temperature", temp));
223 temperature_dyn.push_back(temp);
224 }
225
226 float gH_5;
227 DOCTEST_CHECK_NOTHROW(context_5.getPrimitiveData(UUID_5, "boundarylayer_conductance_out", gH_5));
228 float tau_5 = cp_5 / gH_5 / 29.25f;
229
230 context_5.setPrimitiveData(UUID_5, "radiation_flux_SW", Rlow);
231 DOCTEST_CHECK_NOTHROW(energybalance_5.run());
232 float Tlow;
233 DOCTEST_CHECK_NOTHROW(context_5.getPrimitiveData(UUID_5, "temperature", Tlow));
234
235 context_5.setPrimitiveData(UUID_5, "radiation_flux_SW", Rhigh);
236 DOCTEST_CHECK_NOTHROW(energybalance_5.run());
237 float Thigh;
238 DOCTEST_CHECK_NOTHROW(context_5.getPrimitiveData(UUID_5, "temperature", Thigh));
239
240 float err = 0;
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);
245 }
246
247 err = sqrt(err / float(N));
248 DOCTEST_CHECK(err == doctest::Approx(0.0f).epsilon(0.2f));
249}
250
251DOCTEST_TEST_CASE("EnergyBalanceModel Enable/Disable Messages") {
252 Context context_enable;
253 EnergyBalanceModel testModel(&context_enable);
254
255 DOCTEST_CHECK_NOTHROW(testModel.enableMessages());
256 DOCTEST_CHECK_NOTHROW(testModel.disableMessages());
257}
258
259DOCTEST_TEST_CASE("EnergyBalanceModel Radiation Band Management") {
260 Context context_radiation;
261 EnergyBalanceModel testModel(&context_radiation);
262
263 DOCTEST_CHECK_NOTHROW(testModel.addRadiationBand("LW"));
264 DOCTEST_CHECK_NOTHROW(testModel.addRadiationBand("LW")); // Should not duplicate
265 DOCTEST_CHECK_NOTHROW(testModel.addRadiationBand("SW"));
266}
267
268DOCTEST_TEST_CASE("EnergyBalanceModel Optional Output Primitive Data") {
269 Context context_output;
270 EnergyBalanceModel testModel(&context_output);
271
272 DOCTEST_CHECK_NOTHROW(testModel.optionalOutputPrimitiveData("boundarylayer_conductance_out"));
273
274 bool has_cerr_output;
275 {
276 capture_cerr cerr_buffer;
277 testModel.optionalOutputPrimitiveData("invalid_label"); // Should print warning
278 has_cerr_output = cerr_buffer.has_output();
279 } // capture destroyed here before assertion
280 DOCTEST_CHECK(has_cerr_output);
281}
282
283DOCTEST_TEST_CASE("EnergyBalanceModel Print Default Value Report") {
284 Context context_print;
285 EnergyBalanceModel testModel(&context_print);
286
287 std::stringstream buffer;
288 std::streambuf *old = std::cout.rdbuf(buffer.rdbuf()); // Redirect std::cout
289
290 DOCTEST_CHECK_NOTHROW(testModel.printDefaultValueReport());
291
292 std::cout.rdbuf(old); // Restore std::cout
293
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"};
296
297 for (const auto &keyword: required_keywords) {
298 DOCTEST_CHECK(output.find(keyword) != std::string::npos);
299 }
300}
301
302DOCTEST_TEST_CASE("EnergyBalanceModel Print Default Value Report with UUIDs") {
303 Context context_print;
304 EnergyBalanceModel testModel(&context_print);
305 std::vector<uint> testUUIDs;
306 testUUIDs.push_back(context_print.addPatch(make_vec3(1, 2, 3), make_vec2(3, 2)));
307
308 std::stringstream buffer;
309 std::streambuf *old = std::cout.rdbuf(buffer.rdbuf()); // Redirect std::cout
310
311 DOCTEST_CHECK_NOTHROW(testModel.printDefaultValueReport(testUUIDs));
312
313 std::cout.rdbuf(old); // Restore std::cout
314
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"};
317
318 for (const auto &keyword: required_keywords) {
319 DOCTEST_CHECK(output.find(keyword) != std::string::npos);
320 }
321}
322
323DOCTEST_TEST_CASE("EnergyBalanceModel Additional Dynamic Model Check") {
324 Context context_dyn;
325 float dt = 1.f, Tfinal = 3600, To = 300.f, cp = 2000;
326 float Rlow = 50.f, Rhigh = 500.f;
327
328 uint UUID_dyn = context_dyn.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
329 context_dyn.setPrimitiveData(UUID_dyn, "radiation_flux_SW", Rlow);
330 context_dyn.setPrimitiveData(UUID_dyn, "temperature", To);
331 context_dyn.setPrimitiveData(UUID_dyn, "heat_capacity", cp);
332 context_dyn.setPrimitiveData(UUID_dyn, "twosided_flag", uint(0));
333 context_dyn.setPrimitiveData(UUID_dyn, "emissivity_SW", 0.f);
334
335 EnergyBalanceModel energybalance_dyn(&context_dyn);
336 energybalance_dyn.disableMessages();
337 energybalance_dyn.addRadiationBand("SW");
338
339 std::vector<float> temperature_dyn;
340 int N = round(Tfinal / dt);
341 for (int t = 0; t < N; t++) {
342 if (t > 0.5f * N) {
343 context_dyn.setPrimitiveData(UUID_dyn, "radiation_flux_SW", Rhigh);
344 }
345 DOCTEST_CHECK_NOTHROW(energybalance_dyn.run(dt));
346
347 float temp;
348 DOCTEST_CHECK_NOTHROW(context_dyn.getPrimitiveData(UUID_dyn, "temperature", temp));
349 temperature_dyn.push_back(temp);
350 }
351
352 DOCTEST_CHECK(!temperature_dyn.empty());
353}
354
355#ifdef HELIOS_CUDA_AVAILABLE
356DOCTEST_TEST_CASE("EnergyBalanceModel GPU vs CPU Consistency") {
357 Context context_gpu_cpu;
358 uint UUID_gpu_cpu = context_gpu_cpu.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
359
360 context_gpu_cpu.setPrimitiveData(UUID_gpu_cpu, "radiation_flux_SW", 400.f);
361 context_gpu_cpu.setPrimitiveData(UUID_gpu_cpu, "air_temperature", 300.f);
362 context_gpu_cpu.setPrimitiveData(UUID_gpu_cpu, "air_humidity", 0.5f);
363 context_gpu_cpu.setPrimitiveData(UUID_gpu_cpu, "moisture_conductance", 0.1f);
364
365 EnergyBalanceModel model_gpu_cpu(&context_gpu_cpu);
366 model_gpu_cpu.disableMessages();
367 model_gpu_cpu.addRadiationBand("SW");
368
369 // Check if GPU is available
370 bool gpu_available = model_gpu_cpu.isGPUAccelerationEnabled();
371 if (!gpu_available) {
372 DOCTEST_WARN("GPU not available - skipping GPU vs CPU comparison test");
373 return;
374 }
375
376 // Run with GPU
377 DOCTEST_CHECK_NOTHROW(model_gpu_cpu.run());
378 float T_gpu;
379 DOCTEST_CHECK_NOTHROW(context_gpu_cpu.getPrimitiveData(UUID_gpu_cpu, "temperature", T_gpu));
380
381 // Run with CPU
382 model_gpu_cpu.disableGPUAcceleration();
383 DOCTEST_CHECK(model_gpu_cpu.isGPUAccelerationEnabled() == false);
384 DOCTEST_CHECK_NOTHROW(model_gpu_cpu.run());
385 float T_cpu;
386 DOCTEST_CHECK_NOTHROW(context_gpu_cpu.getPrimitiveData(UUID_gpu_cpu, "temperature", T_cpu));
387
388 // Verify GPU and CPU give same result (within floating-point precision)
389 DOCTEST_CHECK(T_cpu == doctest::Approx(T_gpu).epsilon(1e-4));
390}
391#endif
392
393DOCTEST_TEST_CASE("EnergyBalanceModel - Default value warnings") {
394
395 DOCTEST_SUBCASE("Missing air_temperature triggers warning") {
396 Context context;
397 uint UUID = context.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
398
399 // Set only radiation, leave air_temperature unset
400 context.setPrimitiveData(UUID, "radiation_flux_LW", 400.f);
401
402 // Capture cout to check warnings - must go out of scope before assertions
403 std::string output;
404 {
405 capture_cout capture;
406 EnergyBalanceModel model(&context);
407 model.addRadiationBand("LW");
408 model.run();
409 output = capture.get_captured_output();
410 }
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); // singular for 1 primitive
414 }
415
416 DOCTEST_SUBCASE("Missing wind_speed triggers warning") {
417 Context context;
418 uint UUID = context.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
419
420 context.setPrimitiveData(UUID, "radiation_flux_LW", 400.f);
421 context.setPrimitiveData(UUID, "air_temperature", 300.f);
422 // Leave wind_speed and boundarylayer_conductance unset
423
424 std::string output;
425 {
426 capture_cout capture;
427 EnergyBalanceModel model(&context);
428 model.addRadiationBand("LW");
429 model.run();
430 output = capture.get_captured_output();
431 }
432 DOCTEST_CHECK(output.find("missing_wind_speed") != std::string::npos);
433 DOCTEST_CHECK(output.find("WARNING:") != std::string::npos);
434 }
435
436 DOCTEST_SUBCASE("Missing air_humidity triggers warning") {
437 Context context;
438 uint UUID = context.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
439
440 context.setPrimitiveData(UUID, "radiation_flux_LW", 400.f);
441 context.setPrimitiveData(UUID, "air_temperature", 300.f);
442 // Leave air_humidity unset
443
444 std::string output;
445 {
446 capture_cout capture;
447 EnergyBalanceModel model(&context);
448 model.addRadiationBand("LW");
449 model.run();
450 output = capture.get_captured_output();
451 }
452 DOCTEST_CHECK(output.find("missing_air_humidity") != std::string::npos);
453 DOCTEST_CHECK(output.find("WARNING:") != std::string::npos);
454 }
455
456 DOCTEST_SUBCASE("Missing surface temperature triggers warning") {
457 Context context;
458 uint UUID = context.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
459
460 context.setPrimitiveData(UUID, "radiation_flux_LW", 400.f);
461 context.setPrimitiveData(UUID, "air_temperature", 300.f);
462 // Leave temperature unset
463
464 std::string output;
465 {
466 capture_cout capture;
467 EnergyBalanceModel model(&context);
468 model.addRadiationBand("LW");
469 model.run();
470 output = capture.get_captured_output();
471 }
472 DOCTEST_CHECK(output.find("missing_surface_temperature") != std::string::npos);
473 DOCTEST_CHECK(output.find("WARNING:") != std::string::npos);
474 }
475
476 DOCTEST_SUBCASE("Missing emissivity triggers warning") {
477 Context context;
478 uint UUID = context.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
479
480 context.setPrimitiveData(UUID, "radiation_flux_LW", 400.f);
481 context.setPrimitiveData(UUID, "air_temperature", 300.f);
482 // Leave emissivity_LW unset (will default to 1.0)
483
484 std::string output;
485 {
486 capture_cout capture;
487 EnergyBalanceModel model(&context);
488 model.addRadiationBand("LW");
489 model.run();
490 output = capture.get_captured_output();
491 }
492 DOCTEST_CHECK(output.find("missing_emissivity") != std::string::npos);
493 DOCTEST_CHECK(output.find("WARNING:") != std::string::npos);
494 }
495
496 DOCTEST_SUBCASE("Air temperature < 250K triggers warning") {
497 Context context;
498 uint UUID = context.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
499
500 context.setPrimitiveData(UUID, "radiation_flux_LW", 400.f);
501 context.setPrimitiveData(UUID, "air_temperature", 25.f); // Likely Celsius
502
503 std::string output;
504 {
505 capture_cout capture;
506 EnergyBalanceModel model(&context);
507 model.addRadiationBand("LW");
508 model.run();
509 output = capture.get_captured_output();
510 }
511 DOCTEST_CHECK(output.find("air_temperature_likely_celsius") != std::string::npos);
512 DOCTEST_CHECK(output.find("WARNING:") != std::string::npos);
513 }
514
515 DOCTEST_SUBCASE("Air humidity out of range triggers warning") {
516 Context context;
517 uint UUID = context.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
518
519 context.setPrimitiveData(UUID, "radiation_flux_LW", 400.f);
520 context.setPrimitiveData(UUID, "air_temperature", 300.f);
521 context.setPrimitiveData(UUID, "air_humidity", 1.5f); // Out of range
522
523 std::string output;
524 {
525 capture_cout capture;
526 EnergyBalanceModel model(&context);
527 model.addRadiationBand("LW");
528 model.run();
529 output = capture.get_captured_output();
530 }
531 DOCTEST_CHECK(output.find("air_humidity_out_of_range_high") != std::string::npos);
532 DOCTEST_CHECK(output.find("WARNING:") != std::string::npos);
533 }
534
535 DOCTEST_SUBCASE("Messages can be disabled") {
536 Context context;
537 uint UUID = context.addPatch(make_vec3(0, 0, 0), make_vec2(1, 1));
538
539 context.setPrimitiveData(UUID, "radiation_flux_LW", 400.f);
540 // Leave air_temperature unset
541
542 std::string output;
543 {
544 capture_cout capture;
545 EnergyBalanceModel model(&context);
546 model.disableMessages();
547 model.addRadiationBand("LW");
548 model.run();
549 output = capture.get_captured_output();
550 }
551 // Should produce no warnings
552 DOCTEST_CHECK(output.find("WARNING:") == std::string::npos);
553 }
554
555 DOCTEST_SUBCASE("Multiple missing parameters produce aggregated warnings") {
556 Context context;
557 std::vector<uint> UUIDs;
558 // Create 10 primitives with missing parameters
559 for (int i = 0; i < 10; i++) {
560 uint UUID = context.addPatch(make_vec3(i, 0, 0), make_vec2(1, 1));
561 UUIDs.push_back(UUID);
562 context.setPrimitiveData(UUID, "radiation_flux_LW", 400.f);
563 // Leave air_temperature, wind_speed, air_humidity unset
564 }
565
566 std::string output;
567 {
568 capture_cout capture;
569 EnergyBalanceModel model(&context);
570 model.addRadiationBand("LW");
571 model.run();
572 output = capture.get_captured_output();
573 }
574
575 // Should have warnings for missing parameters
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);
579
580 // Should show counts (10 instances for each)
581 DOCTEST_CHECK(output.find("10 instances") != std::string::npos);
582
583 // Should NOT show individual example messages (compact mode)
584 DOCTEST_CHECK(output.find("(showing first") == std::string::npos);
585 }
586}
587
588int EnergyBalanceModel::selfTest(int argc, char **argv) {
589 return helios::runDoctestWithValidation(argc, argv);
590}