1.3.64
 
Loading...
Searching...
No Matches
EnergyBalanceModel.cpp
Go to the documentation of this file.
1
16#include "EnergyBalanceModel.h"
17#include "../include/EnergyBalanceModel.h"
18
19#include "global.h"
20
21#ifdef HELIOS_CUDA_AVAILABLE
22#include <cuda_runtime.h>
23#endif
24
25#ifdef USE_OPENMP
26#include <omp.h>
27#endif
28
29using namespace helios;
30
32
33 // All default values set here
34
35 temperature_default = 300; // Kelvin
36
37 wind_speed_default = 1.f; // m/s
38
39 air_temperature_default = 300; // Kelvin
40
41 air_humidity_default = 0.5; // %
42
43 pressure_default = 101000; // Pa
44
45 gS_default = 0.; // mol/m^2-s
46
47 Qother_default = 0; // W/m^2
48
49 heatcapacity_default = 0; // J/m^2-oC
50
51 surface_humidity_default = 1; //(unitless)
52
53 air_energy_balance_enabled = false;
54
55 message_flag = true; // print messages to screen by default
56
57 // Copy pointer to the context
58 context = __context;
59
60#ifdef HELIOS_CUDA_AVAILABLE
61 // Initialize GPU acceleration flags
62 gpu_acceleration_enabled = true; // Try to use GPU by default
63
64 // Perform runtime GPU detection
65 initializeGPUAcceleration();
66#endif
67}
68
70 run(context->getAllUUIDs());
71}
72
74 run(context->getAllUUIDs(), dt);
75}
76
77void EnergyBalanceModel::run(const std::vector<uint> &UUIDs) {
78 run(UUIDs, 0.f);
79}
80
81
82void EnergyBalanceModel::run(const std::vector<uint> &UUIDs, float dt) {
83
84 if (message_flag) {
85 std::cout << "Running energy balance model..." << std::flush;
86 }
87
88 // Check that some primitives exist in the context
89 if (UUIDs.empty()) {
90 std::cerr << "WARNING (EnergyBalanceModel::run): No primitives have been added to the context. There is nothing to simulate. Exiting..." << std::endl;
91 return;
92 }
93
94 evaluateSurfaceEnergyBalance(UUIDs, dt);
95
96 if (message_flag) {
97 std::cout << "done." << std::endl;
98 }
99}
100
101#ifdef HELIOS_CUDA_AVAILABLE
102void EnergyBalanceModel::initializeGPUAcceleration() {
103 // Check if CUDA is actually available at runtime
104 int deviceCount = 0;
105 cudaError_t err = cudaGetDeviceCount(&deviceCount);
106
107 if (err != cudaSuccess || deviceCount == 0) {
108 // CUDA not available - disable GPU acceleration and fall back to CPU
109 if (message_flag) {
110 std::cout << "INFO (EnergyBalanceModel): CUDA runtime unavailable (" << cudaGetErrorString(err) << "). Using OpenMP CPU implementation." << std::endl;
111 }
112 gpu_acceleration_enabled = false;
113 return;
114 }
115
116 // GPU available
117 if (message_flag) {
118 std::cout << "INFO (EnergyBalanceModel): GPU acceleration enabled (" << deviceCount << " device(s) found)." << std::endl;
119 }
120 gpu_acceleration_enabled = true;
121}
122#endif
123
124void EnergyBalanceModel::evaluateSurfaceEnergyBalance(const std::vector<uint> &UUIDs, float dt) {
125#ifdef HELIOS_CUDA_AVAILABLE
126 if (gpu_acceleration_enabled) {
127 evaluateSurfaceEnergyBalance_GPU(UUIDs, dt);
128 } else {
129 evaluateSurfaceEnergyBalance_CPU(UUIDs, dt);
130 }
131#else
132 // CPU-only build: always use OpenMP implementation
133 evaluateSurfaceEnergyBalance_CPU(UUIDs, dt);
134#endif
135}
136
137#ifdef HELIOS_CUDA_AVAILABLE
138void EnergyBalanceModel::enableGPUAcceleration() {
139 // Try to enable GPU, but respect hardware availability
140 initializeGPUAcceleration();
141}
142
143void EnergyBalanceModel::disableGPUAcceleration() {
144 gpu_acceleration_enabled = false;
145}
146
147bool EnergyBalanceModel::isGPUAccelerationEnabled() const {
148 return gpu_acceleration_enabled;
149}
150#endif
151
152void EnergyBalanceModel::evaluateAirEnergyBalance(float dt_sec, float time_advance_sec) {
153 evaluateAirEnergyBalance(context->getAllUUIDs(), dt_sec, time_advance_sec);
154}
155
156void EnergyBalanceModel::evaluateAirEnergyBalance(const std::vector<uint> &UUIDs, float dt_sec, float time_advance_sec) {
157
158 if (dt_sec <= 0) {
159 helios_runtime_error("ERROR (EnergyBalanceModel::evaluateAirEnergyBalance): dt_sec must be greater than zero to run the air energy balance.");
160 }
161
162 // Create warning aggregator
164 warnings.setEnabled(message_flag);
165
166 float air_temperature_reference = air_temperature_default; // Default air temperature in Kelvin
167 if (context->doesGlobalDataExist("air_temperature_reference") && context->getGlobalDataType("air_temperature_reference") == helios::HELIOS_TYPE_FLOAT) {
168 context->getGlobalData("air_temperature_reference", air_temperature_reference);
169 }
170
171 float air_humidity_reference = air_humidity_default; // Default air relative humidity
172 if (context->doesGlobalDataExist("air_humidity_reference") && context->getGlobalDataType("air_humidity_reference") == helios::HELIOS_TYPE_FLOAT) {
173 context->getGlobalData("air_humidity_reference", air_humidity_reference);
174 }
175
176 float wind_speed_reference = wind_speed_default; // Default wind speed in m/s
177 if (context->doesGlobalDataExist("wind_speed_reference") && context->getGlobalDataType("wind_speed_reference") == helios::HELIOS_TYPE_FLOAT) {
178 context->getGlobalData("wind_speed_reference", wind_speed_reference);
179 }
180
181 // Variables to be set externally via global data
182
183 float Patm;
184 if (context->doesGlobalDataExist("air_pressure") && context->getGlobalDataType("air_pressure") == helios::HELIOS_TYPE_FLOAT) {
185 context->getGlobalData("air_pressure", Patm);
186 } else {
187 Patm = pressure_default;
188 }
189
190 float air_temperature_average;
191 if (context->doesGlobalDataExist("air_temperature_average") && context->getGlobalDataType("air_temperature_average") == helios::HELIOS_TYPE_FLOAT) {
192 context->getGlobalData("air_temperature_average", air_temperature_average);
193 } else {
194 air_temperature_average = air_temperature_reference;
195 }
196
197 float air_moisture_average;
198 if (context->doesGlobalDataExist("air_moisture_average") && context->getGlobalDataType("air_moisture_average") == helios::HELIOS_TYPE_FLOAT) {
199 context->getGlobalData("air_moisture_average", air_moisture_average);
200 } else {
201 air_moisture_average = air_humidity_reference * esat_Pa(air_temperature_reference) / Patm;
202 }
203
204 float air_temperature_ABL = 0; // initialize to 0 as a flag so that if it's not available from global data, we'll initialize it below
205 if (context->doesGlobalDataExist("air_temperature_ABL") && context->getGlobalDataType("air_temperature_ABL") == helios::HELIOS_TYPE_FLOAT) {
206 context->getGlobalData("air_temperature_ABL", air_temperature_ABL);
207 }
208
209 float air_moisture_ABL;
210 if (context->doesGlobalDataExist("air_moisture_ABL") && context->getGlobalDataType("air_moisture_ABL") == helios::HELIOS_TYPE_FLOAT) {
211 context->getGlobalData("air_moisture_ABL", air_moisture_ABL);
212 } else {
213 air_moisture_ABL = air_humidity_reference * esat_Pa(air_temperature_reference) / Patm;
214 }
215
216 // Get dimensions of canopy volume
217 if (canopy_dimensions == make_vec3(0, 0, 0)) {
218 vec2 xbounds, ybounds, zbounds;
219 context->getDomainBoundingBox(UUIDs, xbounds, ybounds, zbounds);
220 canopy_dimensions = make_vec3(xbounds.y - xbounds.x, ybounds.y - ybounds.x, zbounds.y - zbounds.x);
221 }
222 assert(canopy_dimensions.x > 0 && canopy_dimensions.y > 0 && canopy_dimensions.z > 0);
223 if (canopy_height_m == 0) {
224 canopy_height_m = canopy_dimensions.z; // Set the canopy height if not already set
225 }
226 if (reference_height_m == 0) {
227 reference_height_m = canopy_height_m; // Set the reference height if not already set
228 }
229
230 std::cout << "Read in initial values. Ta = " << air_temperature_average << "; e = " << air_moisture_average << "; " << air_moisture_average / esat_Pa(air_temperature_reference) * Patm << std::endl;
231
232 std::cout << "Canopy dimensions: " << canopy_dimensions.x << " x " << canopy_dimensions.y << " x " << canopy_height_m << std::endl;
233
234 float displacement_height = 0.67f * canopy_height_m;
235 float zo_m = 0.1f * canopy_height_m; // Roughness length for momentum transfer (m)
236 float zo_h = 0.01f * canopy_height_m; // Roughness length for heat transfer (m)
237
238 float abl_height_m = 1000.f;
239
240 float time = 0;
241 float air_temperature_old = 0;
242 float air_moisture_old = 0;
243 float air_temperature_ABL_old = 0;
244 float air_moisture_ABL_old = 0;
245 while (time < time_advance_sec) {
246 float dt_actual = dt_sec;
247 if (time + dt_actual > time_advance_sec) {
248 dt_actual = time_advance_sec - time; // Adjust the time step to not exceed the total time
249 }
250
251 std::cout << "Air moisture average: " << air_moisture_average << std::endl;
252
253 // Update the surface energy balance
254 this->run(UUIDs);
255
256 // Calculate temperature source term
257 float sensible_source_flux_W_m3;
258 context->calculatePrimitiveDataAreaWeightedSum(UUIDs, "sensible_flux", sensible_source_flux_W_m3); // units: Watts
259 sensible_source_flux_W_m3 /= canopy_dimensions.x * canopy_dimensions.y * canopy_height_m; // Convert to W/m^3
260
261 // Calculate moisture source term
262 float moisture_source_flux_W_m3;
263 context->calculatePrimitiveDataAreaWeightedSum(UUIDs, "latent_flux", moisture_source_flux_W_m3); // units: Watts
264 moisture_source_flux_W_m3 /= canopy_dimensions.x * canopy_dimensions.y * canopy_height_m; // Convert to W/m^3
265 float moisture_source_flux_mol_s_m3 = moisture_source_flux_W_m3 / lambda_mol; // Convert to mol/s/m^3
266
267 float rho_air_mol_m3 = Patm / (R * air_temperature_average);
268 ; // Molar density of air (mol/m^3).
269
270 // --- canopy / ABL interface & implicit‐Euler update ---
271
272 // 1) canopy‐scale source fluxes per unit ground area
273 float H_s = sensible_source_flux_W_m3 * canopy_height_m; // W m⁻²
274 float E_s = moisture_source_flux_mol_s_m3 * canopy_height_m; // mol m⁻² s⁻¹
275
276 // 2) neutral friction velocity for Obukhov length
277 float u_star_neutral = von_Karman_constant * wind_speed_reference / std::log((reference_height_m - displacement_height) / zo_m);
278
279 // 3) Obukhov length L [m]
280 float rho_air_kg_m3 = rho_air_mol_m3 * 0.02896f;
281 float cp_air_kg = cp_air_mol / 0.02896f;
282 float L = 1e6f;
283 if (std::fabs(H_s) > 1e-6f) {
284 L = -rho_air_kg_m3 * cp_air_kg * std::pow(u_star_neutral, 3) * air_temperature_reference / (von_Karman_constant * 9.81f * H_s);
285 }
286
287 // 4) stability functions (Businger–Dyer)
288 auto psi_unstable_u = [&](float zeta) {
289 float x = std::pow(1.f - 16.f * zeta, 0.25f);
290 return 2.f * std::log((1.f + x) / 2.f) + std::log((1.f + x * x) / 2.f) - 2.f * std::atan(x) + 0.5f * M_PI;
291 };
292 auto psi_unstable_h = [&](float zeta) { return 2.f * std::log((1.f + std::sqrt(1.f - 16.f * zeta)) / 2.f); };
293 auto psi_stable = [&](float zeta) { return -5.f * zeta; };
294
295 float zeta_r = (reference_height_m - displacement_height) / L;
296 float psi_m_r = (zeta_r < 0.f) ? psi_unstable_u(zeta_r) : psi_stable(zeta_r);
297 float psi_h_r = (zeta_r < 0.f) ? psi_unstable_h(zeta_r) : psi_stable(zeta_r);
298
299 // 5) stability‐corrected friction velocity
300 float u_star = von_Karman_constant * wind_speed_reference / (std::log((reference_height_m - displacement_height) / zo_m) - psi_m_r);
301
302 // 6) Monin–Obukhov scales θ_* and q_*
303 float theta_star = H_s / (rho_air_mol_m3 * cp_air_mol * u_star);
304 float q_star = E_s / (rho_air_mol_m3 * u_star);
305
306 // 7) corrected log‐law lengths
307 float ln_m_corr = std::log((reference_height_m - displacement_height) / zo_m) - psi_m_r;
308 float ln_h_corr = std::log((reference_height_m - displacement_height) / zo_h) - psi_h_r;
309
310 // 8) aerodynamic conductance ga [mol m⁻² s⁻¹]
311 float ga = std::pow(von_Karman_constant, 2) * wind_speed_reference / (ln_m_corr * ln_h_corr) * rho_air_mol_m3;
312
313 // 9) interface values at canopy top (Monin–Obukhov log-law)
314 float air_moisture_reference = esat_Pa(air_temperature_reference) / Patm * air_humidity_reference;
315 float T_int = air_temperature_reference + theta_star / von_Karman_constant * ln_h_corr;
316 float x_int = air_moisture_reference + q_star / von_Karman_constant * ln_h_corr;
317
318 // Cap relative humidity to 100%
319 float x_sat = esat_Pa(T_int) / Patm;
320 if (x_int > x_sat) {
321 // std::cerr << "WARNING (EnergyBalanceModel::evaluateAirEnergyBalance): Air moisture exceeds saturation. Capping to saturation value." << std::endl;
322 x_int = 0.99f * x_sat;
323 }
324
325 // 10) entrainment conductance g_e (mol m⁻² s⁻¹)
326 float H_canopy = sensible_source_flux_W_m3 * canopy_height_m;
327 float g_e;
328 if (air_temperature_ABL == 0.f) {
329 // first‐step equilibrium initialization
330 float w_star0 = H_canopy > 0.f ? std::pow(9.81f * H_canopy * canopy_height_m / (rho_air_mol_m3 * cp_air_mol * air_temperature_reference), 1.f / 3.f) : 0.f;
331 const float A_e = 0.02f;
332 float w_e0 = A_e * w_star0;
333 g_e = rho_air_mol_m3 * w_e0;
334 // initialize ABL state
335 air_temperature_ABL = (ga * air_temperature_average + g_e * air_temperature_reference) / (ga + g_e);
336 air_moisture_ABL = (ga * air_moisture_average + g_e * air_moisture_reference) / (ga + g_e);
337 } else {
338 // ongoing entrainment
339 float w_star = 0.f;
340 if (H_canopy > 0.f) {
341 w_star = std::pow(9.81f * H_canopy * canopy_height_m / (rho_air_mol_m3 * cp_air_mol * air_temperature_ABL), 1.f / 3.f);
342 }
343 float u_star_shear = von_Karman_constant * wind_speed_reference / std::log((reference_height_m - displacement_height) / zo_m);
344 float w_e = std::fmax(0.01f * w_star, 0.005f * u_star_shear);
345 g_e = rho_air_mol_m3 * w_e;
346 }
347
348 // if ( g_e>0.05 ) {
349 // std::cerr << "Capping g_e value of " << g_e << std::endl;
350 // g_e = 0.05f;
351 // }
352 g_e = 2.0;
353
354 // 11) canopy→ABL fluxes
355 float sensible_upper_flux_W_m2 = cp_air_mol * ga * (air_temperature_average - T_int);
356
357 // 12) update canopy‐air temperature (implicit Euler)
358 // numerator: T^n + (Δt / (ρ c_p h_c)) * [H_s + c_p g_a T_int]
359 // denominator: 1 + Δt·g_a/(ρ h_c)
360 float numerator_temp = air_temperature_average + dt_actual / (rho_air_mol_m3 * cp_air_mol * canopy_height_m) * (H_s + cp_air_mol * ga * T_int);
361 float denominator_temp = 1.f + dt_actual * ga / (rho_air_mol_m3 * canopy_height_m);
362 air_temperature_average = numerator_temp / denominator_temp;
363
364 // 13) update canopy‐air moisture (implicit Euler)
365
366 // --- canopy implicit‐Euler moisture update ---
367 // update: ρh (x_new - x_old)/dt = E_s - E_top
368 float E_top = ga * (air_moisture_average - x_int);
369 float f = std::pow(1.f - (1.f - 0.622f) * air_moisture_average, 2) / 0.622f;
370 float numer_can_x = air_moisture_average + dt_actual * f / (rho_air_mol_m3 * canopy_height_m) * (E_s + ga * x_int);
371 float denom_can_x = 1.f + dt_actual * f * ga / (rho_air_mol_m3 * canopy_height_m);
372 air_moisture_average = numer_can_x / denom_can_x;
373
374 // 14) update ABL‐air temperature (implicit Euler)
375 float denom_abl_T = 1.f + dt_actual * (ga + g_e) / (rho_air_mol_m3 * cp_air_mol * abl_height_m);
376 float num_abl_T = air_temperature_ABL + dt_actual / (rho_air_mol_m3 * cp_air_mol * abl_height_m) * (sensible_upper_flux_W_m2 + cp_air_mol * g_e * (air_temperature_reference - air_temperature_ABL));
377 air_temperature_ABL = num_abl_T / denom_abl_T;
378
379 // 15) update ABL‐air moisture (implicit Euler)
380 // source into ABL storage
381 // ρ_ab·h_ab · (x_ab_new - x_ab_old)/dt = E_top - E_e
382 float E_e = g_e * (air_moisture_ABL - air_moisture_reference);
383 float numer_abl_x = air_moisture_ABL + dt_actual / (rho_air_mol_m3 * abl_height_m) * (E_top - E_e);
384 float denom_abl_x = 1.f + dt_actual * (ga + g_e) / (rho_air_mol_m3 * abl_height_m);
385 air_moisture_ABL = numer_abl_x / denom_abl_x;
386
387 // Cap relative humidity to 100%
388 float esat = esat_Pa(air_temperature_average) / Patm;
389 if (air_moisture_average > esat) {
390 warnings.addWarning("air_moisture_exceeds_saturation", "Air moisture exceeds saturation. Capping to saturation value.");
391 air_moisture_average = 0.99f * esat;
392 }
393 esat = esat_Pa(air_temperature_ABL) / Patm;
394 if (air_moisture_ABL > esat) {
395 air_moisture_ABL = 0.99f * esat;
396 }
397
398 // Check that timestep is not too large based on aerodynamic resistance
399 if (dt_actual > 0.5f * canopy_height_m * rho_air_mol_m3 / ga) {
400 warnings.addWarning("timestep_too_large", "Time step is too large. The air energy balance may not converge properly.");
401 }
402
403 float heat_from_Htop = dt_actual / (rho_air_mol_m3 * cp_air_mol * abl_height_m) * sensible_upper_flux_W_m2;
404 float heat_from_entrainment = dt_actual * g_e * (air_temperature_reference - air_temperature_ABL) / (rho_air_mol_m3 * abl_height_m);
405
406 std::cerr << "ABL ENERGY UPDATE:\n"
407 << " T_ABL_n = " << air_temperature_ABL << "\n"
408 << " T_ref = " << air_temperature_reference << "\n"
409 << " g_e = " << g_e << " mol/m²/s\n"
410 << " Heat from H_top = " << heat_from_Htop << " K\n"
411 << " Heat from H_e = " << heat_from_entrainment << " K\n"
412 << " Numerator = " << num_abl_T << " K\n"
413 << " Denominator = " << denom_abl_T << "\n"
414 << " T_ABL_new = " << (num_abl_T / denom_abl_T) << "\n";
415
416
417#ifdef HELIOS_DEBUG
418
419 // -- check energy consistency -- //
420 H_s = sensible_source_flux_W_m3 * canopy_height_m; // W m⁻², canopy source
421 float H_top = cp_air_mol * ga * (air_temperature_average - T_int); // W m⁻², canopy→ABL
422 float H_e = cp_air_mol * g_e * (air_temperature_ABL - air_temperature_reference); // W m⁻², ABL→free atm
423
424 E_s = moisture_source_flux_mol_s_m3 * canopy_height_m; // mol m⁻² s⁻¹, canopy source
425 E_top = ga * (air_moisture_average - x_int); // mol m⁻² s⁻¹, canopy→ABL
426 E_e = g_e * (air_moisture_ABL - air_moisture_reference); // mol m⁻² s⁻¹, ABL→free atm
427
428 // Print a concise table of key values
429 std::cout << std::fixed << std::setprecision(5) << "step=" << time << " T_c=" << air_temperature_average << " T_int=" << T_int << " T_b=" << air_temperature_ABL << " H_s=" << H_s << " H_top=" << H_top << " H_e=" << H_e
430 << " E_s=" << E_s << " E_top=" << E_top << " E_e=" << E_e << " psi_m=" << psi_m_r << " psi_h=" << psi_h_r << " u*=" << u_star << " L=" << L << std::endl;
431
432 // temperature balance
433
434 const float tol = 1e-5f;
435 float S_can = rho_air_mol_m3 * cp_air_mol * canopy_height_m * (air_temperature_average - air_temperature_old) / dt_actual;
436 if (fabs(H_s - H_top - S_can) > tol)
437 std::cerr << "Canopy budget error: " << (H_s - H_top - S_can) << "\n";
438
439 float S_abl = rho_air_mol_m3 * cp_air_mol * abl_height_m * (air_temperature_ABL - air_temperature_ABL_old) / dt_actual;
440 if (fabs(H_top - H_e - S_abl) > tol)
441 std::cerr << "ABL budget error: " << (H_top - H_e - S_abl) << "\n";
442
443 // moisture balance per unit ground area
444
445 S_can = rho_air_mol_m3 * canopy_height_m * (air_moisture_average - air_moisture_old) / dt_actual;
446
447 const float tol_m = 1e-6f;
448 if (std::fabs(E_s - E_top - S_can) > tol_m) {
449 std::cerr << "CANOPY MOISTURE BUDGET ERROR: " << "E_s - E_up - S_can = " << E_s - E_top - S_can << " mol/m2/s\n";
450 }
451
452 S_abl = rho_air_mol_m3 * abl_height_m * (air_moisture_ABL - air_moisture_ABL_old) / dt_actual;
453
454 if (std::fabs(E_top - E_e - S_abl) > tol_m) {
455 std::cerr << "ABL MOISTURE BUDGET ERROR: " << "E_up - E_e - S_abl = " << E_top - E_e - S_abl << " mol/m2/s\n";
456 }
457
458
459#endif
460
461 // Set primitive data
462 context->setPrimitiveData(UUIDs, "air_temperature", air_temperature_average);
463 float air_humidity = air_moisture_average * Patm / esat_Pa(air_temperature_average);
464 context->setPrimitiveData(UUIDs, "air_humidity", air_humidity);
465
466 // Set global data
467 context->setGlobalData("air_temperature_average", air_temperature_average);
468 context->setGlobalData("air_humidity_average", air_humidity);
469 context->setGlobalData("air_moisture_average", air_moisture_average);
470
471 context->setGlobalData("air_temperature_ABL", air_temperature_ABL);
472 float air_humidity_ABL = air_moisture_ABL * Patm / esat_Pa(air_temperature_ABL);
473 context->setGlobalData("air_humidity_ABL", air_humidity_ABL);
474
475 context->setGlobalData("air_temperature_interface", T_int);
476 float air_humidity_interface = x_int * Patm / esat_Pa(T_int);
477 context->setGlobalData("air_humidity_interface", air_humidity_interface);
478
479 context->setGlobalData("aerodynamic_resistance", rho_air_mol_m3 / ga);
480
481 std::cout << "Computed air temperature: " << air_temperature_average << " " << air_temperature_old << std::endl;
482 std::cout << "Computed air humidity: " << air_humidity << std::endl;
483 std::cout << "Computed air moisture: " << air_moisture_average << " " << air_moisture_old << std::endl;
484
485 if (time > 0 && std::abs(air_temperature_average - air_temperature_old) / air_temperature_old < 0.003f && std::abs(air_moisture_average - air_moisture_old) / air_moisture_old < 0.003f) {
486 // If the air temperature and humidity have not changed significantly, we can stop iterating
487 std::cout << "Converged" << std::endl;
488 break;
489 }
490 air_temperature_old = air_temperature_average;
491 air_moisture_old = air_moisture_average;
492 air_temperature_ABL_old = air_temperature_ABL;
493 air_moisture_ABL_old = air_moisture_ABL;
494
495 time += dt_actual;
496 }
497
498 // Report aggregated warnings
499 warnings.report(std::cerr);
500}
501
503 message_flag = true;
504}
505
507 message_flag = false;
508}
509
511 if (std::find(radiation_bands.begin(), radiation_bands.end(), band) == radiation_bands.end()) { // only add band if it doesn't exist
512 radiation_bands.emplace_back(band);
513 }
514}
515
516void EnergyBalanceModel::addRadiationBand(const std::vector<std::string> &bands) {
517 for (auto &band: bands) {
518 addRadiationBand(band.c_str());
519 }
520}
521
525
526void EnergyBalanceModel::enableAirEnergyBalance(float canopy_height_m, float reference_height_m) {
527
528 if (canopy_height_m < 0) {
529 helios_runtime_error("ERROR (EnergyBalanceModel::enableAirEnergyBalance): Canopy height must be greater than or equal to zero.");
530 } else if (canopy_height_m != 0 && reference_height_m < canopy_height_m) {
531 helios_runtime_error("ERROR (EnergyBalanceModel::enableAirEnergyBalance): Reference height must be greater than or equal to canopy height.");
532 }
533
534 air_energy_balance_enabled = true;
535 this->canopy_height_m = canopy_height_m;
536 this->reference_height_m = reference_height_m;
537}
538
540
541 if (strcmp(label, "boundarylayer_conductance_out") == 0 || strcmp(label, "vapor_pressure_deficit") == 0 || strcmp(label, "storage_flux") == 0 || strcmp(label, "net_radiation_flux") == 0) {
542 output_prim_data.emplace_back(label);
543 } else {
544 std::cerr << "WARNING (EnergyBalanceModel::optionalOutputPrimitiveData): unknown output primitive data '" << label << "' will be ignored." << std::endl;
545 }
546}
547
551
552void EnergyBalanceModel::printDefaultValueReport(const std::vector<uint> &UUIDs) const {
553
554 size_t assumed_default_TL = 0;
555 size_t assumed_default_U = 0;
556 size_t assumed_default_L = 0;
557 size_t assumed_default_p = 0;
558 size_t assumed_default_Ta = 0;
559 size_t assumed_default_rh = 0;
560 size_t assumed_default_gH = 0;
561 size_t assumed_default_gs = 0;
562 size_t assumed_default_Qother = 0;
563 size_t assumed_default_heatcapacity = 0;
564 size_t assumed_default_fs = 0;
565 size_t twosided_0 = 0;
566 size_t twosided_1 = 0;
567 size_t Ne_1 = 0;
568 size_t Ne_2 = 0;
569
570 size_t Nprimitives = UUIDs.size();
571
572 for (uint UUID: UUIDs) {
573
574 // surface temperature (K)
575 if (!context->doesPrimitiveDataExist(UUID, "temperature") || context->getPrimitiveDataType("temperature") != HELIOS_TYPE_FLOAT) {
576 assumed_default_TL++;
577 }
578
579 // air pressure (Pa)
580 if (!context->doesPrimitiveDataExist(UUID, "air_pressure") || context->getPrimitiveDataType("air_pressure") != HELIOS_TYPE_FLOAT) {
581 assumed_default_p++;
582 }
583
584 // air temperature (K)
585 if (!context->doesPrimitiveDataExist(UUID, "air_temperature") || context->getPrimitiveDataType("air_temperature") != HELIOS_TYPE_FLOAT) {
586 assumed_default_Ta++;
587 }
588
589 // air humidity
590 if (!context->doesPrimitiveDataExist(UUID, "air_humidity") || context->getPrimitiveDataType("air_humidity") != HELIOS_TYPE_FLOAT) {
591 assumed_default_rh++;
592 }
593
594 // boundary-layer conductance to heat
595 if (!context->doesPrimitiveDataExist(UUID, "boundarylayer_conductance") || context->getPrimitiveDataType("boundarylayer_conductance") != HELIOS_TYPE_FLOAT) {
596 assumed_default_gH++;
597 }
598
599 // wind speed
600 if (!context->doesPrimitiveDataExist(UUID, "wind_speed") || context->getPrimitiveDataType("wind_speed") != HELIOS_TYPE_FLOAT) {
601 assumed_default_U++;
602 }
603
604 // object length
605 if (!context->doesPrimitiveDataExist(UUID, "object_length") || context->getPrimitiveDataType("object_length") != HELIOS_TYPE_FLOAT) {
606 assumed_default_L++;
607 }
608
609 // moisture conductance
610 if (!context->doesPrimitiveDataExist(UUID, "moisture_conductance") || context->getPrimitiveDataType("moisture_conductance") != HELIOS_TYPE_FLOAT) {
611 assumed_default_gs++;
612 }
613
614 // Heat capacity
615 if (!context->doesPrimitiveDataExist(UUID, "heat_capacity") || context->getPrimitiveDataType("heat_capacity") != HELIOS_TYPE_FLOAT) {
616 assumed_default_heatcapacity++;
617 }
618
619 //"Other" heat fluxes
620 if (!context->doesPrimitiveDataExist(UUID, "other_surface_flux") || context->getPrimitiveDataType("other_surface_flux") != HELIOS_TYPE_FLOAT) {
621 assumed_default_Qother++;
622 }
623
624 // two-sided flag - check material first, then primitive data
625 uint twosided = context->getPrimitiveTwosidedFlag(UUID, 1);
626 if (twosided == 0) {
627 twosided_0++;
628 } else {
629 twosided_1++;
630 }
631
632 // number of evaporating faces
633 if (context->doesPrimitiveDataExist(UUID, "evaporating_faces") && context->getPrimitiveDataType("evaporating_faces") == HELIOS_TYPE_UINT) {
634 uint Ne;
635 context->getPrimitiveData(UUID, "evaporating_faces", Ne);
636 if (Ne == 1) {
637 Ne_1++;
638 } else if (Ne == 2) {
639 Ne_2++;
640 }
641 } else {
642 Ne_1++;
643 }
644
645 // Surface humidity
646 if (!context->doesPrimitiveDataExist(UUID, "surface_humidity") || context->getPrimitiveDataType("surface_humidity") != HELIOS_TYPE_FLOAT) {
647 assumed_default_fs++;
648 }
649 }
650
651 std::cout << "--- Energy Balance Model Default Value Report ---" << std::endl;
652
653 std::cout << "surface temperature (initial guess): " << assumed_default_TL << " of " << Nprimitives << " used default value of " << temperature_default
654 << " because "
655 "temperature"
656 " primitive data did not exist"
657 << std::endl;
658 std::cout << "air pressure: " << assumed_default_p << " of " << Nprimitives << " used default value of " << pressure_default
659 << " because "
660 "air_pressure"
661 " primitive data did not exist"
662 << std::endl;
663 std::cout << "air temperature: " << assumed_default_Ta << " of " << Nprimitives << " used default value of " << air_temperature_default
664 << " because "
665 "air_temperature"
666 " primitive data did not exist"
667 << std::endl;
668 std::cout << "air humidity: " << assumed_default_rh << " of " << Nprimitives << " used default value of " << air_humidity_default
669 << " because "
670 "air_humidity"
671 " primitive data did not exist"
672 << std::endl;
673 std::cout << "boundary-layer conductance: " << assumed_default_gH << " of " << Nprimitives
674 << " calculated boundary-layer conductance from Polhausen equation because "
675 "boundarylayer_conductance"
676 " primitive data did not exist"
677 << std::endl;
678 if (assumed_default_gH > 0) {
679 std::cout << " - wind speed: " << assumed_default_U << " of " << assumed_default_gH << " using Polhausen equation used default value of " << wind_speed_default
680 << " because "
681 "wind_speed"
682 " primitive data did not exist"
683 << std::endl;
684 std::cout << " - object length: " << assumed_default_L << " of " << assumed_default_gH
685 << " using Polhausen equation used the primitive/object length/area to calculate object length because "
686 "object_length"
687 " primitive data did not exist"
688 << std::endl;
689 }
690 std::cout << "moisture conductance: " << assumed_default_gs << " of " << Nprimitives << " used default value of " << gS_default
691 << " because "
692 "moisture_conductance"
693 " primitive data did not exist"
694 << std::endl;
695 std::cout << "surface humidity: " << assumed_default_fs << " of " << Nprimitives << " used default value of " << surface_humidity_default
696 << " because "
697 "surface_humidity"
698 " primitive data did not exist"
699 << std::endl;
700 std::cout << "two-sided flag: " << twosided_0 << " of " << Nprimitives << " used two-sided flag=0; " << twosided_1 << " of " << Nprimitives << " used two-sided flag=1 (default)" << std::endl;
701 std::cout << "evaporating faces: " << Ne_1 << " of " << Nprimitives << " used Ne = 1 (default); " << Ne_2 << " of " << Nprimitives << " used Ne = 2" << std::endl;
702
703 std::cout << "------------------------------------------------------" << std::endl;
704}
705
706// Helper function: CPU version of energy balance evaluation
707static inline float evaluateEnergyBalance_CPU(float T, float R, float Qother, float eps, float Ta, float ea, float pressure, float gH, float gS, uint Nsides, float stomatal_sidedness, float heatcapacity, float surfacehumidity, float dt,
708 float Tprev) {
709
710 // Outgoing emission flux
711 float Rout = float(Nsides) * eps * 5.67e-8F * T * T * T * T;
712
713 // Sensible heat flux
714 float QH = cp_air_mol * gH * (T - Ta);
715
716 // Latent heat flux
717 float es = 611.0f * expf(17.502f * (T - 273.f) / (T - 273.f + 240.97f));
718 float gM = 1.08f * gH * gS * (stomatal_sidedness / (1.08f * gH + gS * stomatal_sidedness) + (1.f - stomatal_sidedness) / (1.08f * gH + gS * (1.f - stomatal_sidedness)));
719 if (gH == 0 && gS == 0) {
720 gM = 0;
721 }
722
723 float QL = gM * lambda_mol * (es - ea * surfacehumidity) / pressure;
724
725 // Storage heat flux
726 float storage = 0.f;
727 if (dt > 0) {
728 storage = heatcapacity * (T - Tprev) / dt;
729 }
730
731 // Residual
732 return R - Rout - QH - QL - Qother - storage;
733}
734void EnergyBalanceModel::evaluateSurfaceEnergyBalance_CPU(const std::vector<uint> &UUIDs, float dt) {
735
736 // Create warning aggregator for default value warnings
738 warnings.setEnabled(message_flag);
739
740 //---- Sum up to get total absorbed radiation across all bands ----//
741
742 if (radiation_bands.empty()) {
743 helios_runtime_error("ERROR (EnergyBalanceModel::run): No radiation bands were found.");
744 }
745
746 const uint Nprimitives = UUIDs.size();
747
748 std::vector<float> Rn(Nprimitives, 0);
749 std::vector<float> emissivity(Nprimitives);
750
751 for (size_t u = 0; u < Nprimitives; u++) {
752 emissivity.at(u) = 1.f;
753 }
754
755 // Accumulate radiation across bands
756 for (int b = 0; b < radiation_bands.size(); b++) {
757 for (size_t u = 0; u < Nprimitives; u++) {
758 size_t p = UUIDs.at(u);
759
760 char str[50];
761 snprintf(str, sizeof(str), "radiation_flux_%s", radiation_bands.at(b).c_str());
762 if (!context->doesPrimitiveDataExist(p, str)) {
763 helios_runtime_error("ERROR (EnergyBalanceModel::run): No radiation was found in the context for band " + std::string(radiation_bands.at(b)) + ". Did you run the radiation model for this band?");
764 } else if (context->getPrimitiveDataType(str) != helios::HELIOS_TYPE_FLOAT) {
765 helios_runtime_error("ERROR (EnergyBalanceModel::run): Radiation primitive data for band " + std::string(radiation_bands.at(b)) + " does not have the correct type of 'float'");
766 }
767 float R;
768 context->getPrimitiveData(p, str, R);
769 Rn.at(u) += R;
770
771 snprintf(str, sizeof(str), "emissivity_%s", radiation_bands.at(b).c_str());
772 if (context->doesPrimitiveDataExist(p, str) && context->getPrimitiveDataType(str) == helios::HELIOS_TYPE_FLOAT) {
773 context->getPrimitiveData(p, str, emissivity.at(u));
774 } else {
775 warnings.addWarning("missing_emissivity", "Primitive data 'emissivity_" + std::string(radiation_bands.at(b)) + "' not set, using default (1.0)");
776 }
777 }
778 }
779
780 //---- Set up temperature solution ----//
781
782 // Allocate arrays for inputs
783 std::vector<float> To(Nprimitives);
784 std::vector<float> R(Nprimitives);
785 std::vector<float> Qother(Nprimitives);
786 std::vector<float> eps(Nprimitives);
787 std::vector<float> Ta(Nprimitives);
788 std::vector<float> ea(Nprimitives);
789 std::vector<float> pressure(Nprimitives);
790 std::vector<float> gH(Nprimitives);
791 std::vector<float> gS(Nprimitives);
792 std::vector<uint> Nsides(Nprimitives);
793 std::vector<float> stomatal_sidedness(Nprimitives);
794 std::vector<float> heatcapacity(Nprimitives);
795 std::vector<float> surfacehumidity(Nprimitives);
796
797 bool calculated_blconductance_used = false;
798 bool primitive_length_used = false;
799
800 // Data preparation loop - same as CUDA version
801 for (uint u = 0; u < Nprimitives; u++) {
802 size_t p = UUIDs.at(u);
803
804 // Initial guess for surface temperature
805 if (context->doesPrimitiveDataExist(p, "temperature") && context->getPrimitiveDataType("temperature") == helios::HELIOS_TYPE_FLOAT) {
806 context->getPrimitiveData(p, "temperature", To[u]);
807 } else {
808 To[u] = temperature_default;
809 warnings.addWarning("missing_surface_temperature", "Primitive data 'temperature' not set, using default (" + std::to_string(temperature_default) + " K)");
810 }
811 if (To[u] == 0) {
812 To[u] = 300;
813 }
814
815 // Air temperature
816 if (context->doesPrimitiveDataExist(p, "air_temperature") && context->getPrimitiveDataType("air_temperature") == helios::HELIOS_TYPE_FLOAT) {
817 context->getPrimitiveData(p, "air_temperature", Ta[u]);
818 if (Ta[u] < 250.f) {
819 warnings.addWarning("air_temperature_likely_celsius", "Value of " + std::to_string(Ta[u]) + " in 'air_temperature' is very small - should be in Kelvin, using default (" + std::to_string(air_temperature_default) + " K)");
820 Ta[u] = air_temperature_default;
821 }
822 } else {
823 Ta[u] = air_temperature_default;
824 warnings.addWarning("missing_air_temperature", "Primitive data 'air_temperature' not set, using default (" + std::to_string(air_temperature_default) + " K)");
825 }
826
827 // Air relative humidity
828 float hr;
829 if (context->doesPrimitiveDataExist(p, "air_humidity") && context->getPrimitiveDataType("air_humidity") == helios::HELIOS_TYPE_FLOAT) {
830 context->getPrimitiveData(p, "air_humidity", hr);
831 if (hr > 1.f) {
832 warnings.addWarning("air_humidity_out_of_range_high", "Value of " + std::to_string(hr) + " in 'air_humidity' > 1.0 - should be fractional [0,1], using default (" + std::to_string(air_humidity_default) + ")");
833 hr = air_humidity_default;
834 } else if (hr < 0.f) {
835 warnings.addWarning("air_humidity_out_of_range_low", "Value of " + std::to_string(hr) + " in 'air_humidity' < 0.0 - should be fractional [0,1], using default (" + std::to_string(air_humidity_default) + ")");
836 hr = air_humidity_default;
837 }
838 } else {
839 hr = air_humidity_default;
840 warnings.addWarning("missing_air_humidity", "Primitive data 'air_humidity' not set, using default (" + std::to_string(air_humidity_default) + ")");
841 }
842
843 // Air vapor pressure
844 float esat = esat_Pa(Ta[u]);
845 ea[u] = hr * esat;
846
847 // Air pressure
848 if (context->doesPrimitiveDataExist(p, "air_pressure") && context->getPrimitiveDataType("air_pressure") == helios::HELIOS_TYPE_FLOAT) {
849 context->getPrimitiveData(p, "air_pressure", pressure[u]);
850 if (pressure[u] < 10000.f) {
851 if (message_flag) {
852 std::cout << "WARNING (EnergyBalanceModel::run): Value of " << pressure[u] << " given in 'air_pressure' primitive data is very small. "
853 << "Values should be given in units of Pascals. Assuming default value of " << pressure_default << std::endl;
854 }
855 pressure[u] = pressure_default;
856 }
857 } else {
858 pressure[u] = pressure_default;
859 }
860
861 // Number of sides emitting radiation
862 uint twosided_flag = context->getPrimitiveTwosidedFlag(p, 1);
863 Nsides[u] = (twosided_flag == 0) ? 1 : 2;
864
865 // Number of evaporating/transpiring faces
866 stomatal_sidedness[u] = 0.f;
867 if (Nsides[u] == 2 && context->doesPrimitiveDataExist(p, "stomatal_sidedness") && context->getPrimitiveDataType("stomatal_sidedness") == helios::HELIOS_TYPE_FLOAT) {
868 context->getPrimitiveData(p, "stomatal_sidedness", stomatal_sidedness[u]);
869 } else if (Nsides[u] == 2 && context->doesPrimitiveDataExist(p, "evaporating_faces") && context->getPrimitiveDataType("evaporating_faces") == helios::HELIOS_TYPE_UINT) {
870 uint flag;
871 context->getPrimitiveData(p, "evaporating_faces", flag);
872 if (flag == 1) {
873 stomatal_sidedness[u] = 0.f;
874 } else if (flag == 2) {
875 stomatal_sidedness[u] = 0.5f;
876 }
877 }
878
879 // Boundary-layer conductance to heat
880 if (context->doesPrimitiveDataExist(p, "boundarylayer_conductance") && context->getPrimitiveDataType("boundarylayer_conductance") == helios::HELIOS_TYPE_FLOAT) {
881 context->getPrimitiveData(p, "boundarylayer_conductance", gH[u]);
882 } else {
883 // Wind speed
884 float U;
885 if (context->doesPrimitiveDataExist(p, "wind_speed") && context->getPrimitiveDataType("wind_speed") == helios::HELIOS_TYPE_FLOAT) {
886 context->getPrimitiveData(p, "wind_speed", U);
887 } else {
888 U = wind_speed_default;
889 warnings.addWarning("missing_wind_speed", "Primitive data 'wind_speed' not set, using default (" + std::to_string(wind_speed_default) + " m/s)");
890 }
891
892 // Characteristic size of primitive
893 float L;
894 if (context->doesPrimitiveDataExist(p, "object_length") && context->getPrimitiveDataType("object_length") == helios::HELIOS_TYPE_FLOAT) {
895 context->getPrimitiveData(p, "object_length", L);
896 if (L == 0) {
897 L = sqrt(context->getPrimitiveArea(p));
898 primitive_length_used = true;
899 }
900 } else if (context->getPrimitiveParentObjectID(p) > 0) {
901 uint objID = context->getPrimitiveParentObjectID(p);
902 L = sqrt(context->getObjectArea(objID));
903 } else {
904 L = sqrt(context->getPrimitiveArea(p));
905 primitive_length_used = true;
906 }
907
908 gH[u] = 0.135f * sqrt(U / L) * float(Nsides[u]);
909 calculated_blconductance_used = true;
910 }
911
912 // Moisture conductance
913 if (context->doesPrimitiveDataExist(p, "moisture_conductance") && context->getPrimitiveDataType("moisture_conductance") == helios::HELIOS_TYPE_FLOAT) {
914 context->getPrimitiveData(p, "moisture_conductance", gS[u]);
915 } else {
916 gS[u] = gS_default;
917 }
918
919 // Other fluxes
920 if (context->doesPrimitiveDataExist(p, "other_surface_flux") && context->getPrimitiveDataType("other_surface_flux") == helios::HELIOS_TYPE_FLOAT) {
921 context->getPrimitiveData(p, "other_surface_flux", Qother[u]);
922 } else {
923 Qother[u] = Qother_default;
924 }
925
926 // Object heat capacity
927 if (context->doesPrimitiveDataExist(p, "heat_capacity") && context->getPrimitiveDataType("heat_capacity") == helios::HELIOS_TYPE_FLOAT) {
928 context->getPrimitiveData(p, "heat_capacity", heatcapacity[u]);
929 } else {
930 heatcapacity[u] = heatcapacity_default;
931 }
932
933 // Surface humidity
934 if (context->doesPrimitiveDataExist(p, "surface_humidity") && context->getPrimitiveDataType("surface_humidity") == helios::HELIOS_TYPE_FLOAT) {
935 context->getPrimitiveData(p, "surface_humidity", surfacehumidity[u]);
936 } else {
937 surfacehumidity[u] = surface_humidity_default;
938 }
939
940 // Emissivity
941 eps[u] = emissivity.at(u);
942
943 // Net absorbed radiation
944 R[u] = Rn.at(u);
945 }
946
947 // Report all accumulated warnings
948 warnings.report(std::cout, true); // true = compact mode
949
950 // Enable output for calculated boundary-layer conductance if used
951 if (calculated_blconductance_used) {
952 auto it = find(output_prim_data.begin(), output_prim_data.end(), "boundarylayer_conductance_out");
953 if (it == output_prim_data.end()) {
954 output_prim_data.emplace_back("boundarylayer_conductance_out");
955 }
956 }
957
958 // Warning about primitive length usage
959 if (message_flag && primitive_length_used) {
960 std::cout << "WARNING (EnergyBalanceModel::run): The length of a primitive that is not a member of a compound object "
961 << "was used to calculate the boundary-layer conductance. This often results in incorrect values because "
962 << "the length should be that of the object (e.g., leaf, stem) not the primitive. "
963 << "Make sure this is what you intended." << std::endl;
964 }
965
966//---- Solve energy balance using OpenMP ----//
967
968// Issue warning if OpenMP not available (use aggregator to avoid spam)
969#ifndef USE_OPENMP
970 static bool openmp_warning_issued = false;
971 if (message_flag && !openmp_warning_issued) {
972 std::cout << "WARNING (EnergyBalanceModel): OpenMP not available. Using serial CPU implementation. "
973 << "Performance will be significantly slower. Consider installing OpenMP for parallel execution." << std::endl;
974 openmp_warning_issued = true;
975 }
976#endif
977
978 // Allocate result array
979 std::vector<float> T(Nprimitives);
980
981// Thread-local storage for convergence warnings
982#ifdef USE_OPENMP
983 int num_threads = omp_get_max_threads();
984 std::vector<int> thread_convergence_failures(num_threads, 0);
985#else
986 int convergence_failures = 0;
987#endif
988
989// Parallel loop over primitives
990#ifdef USE_OPENMP
991#pragma omp parallel for schedule(dynamic, 64)
992#endif
993 for (int p = 0; p < (int) Nprimitives; p++) {
994
995 // Secant method parameters
996 const float err_max = 0.0001f;
997 const uint max_iter = 100;
998
999 // Initial guesses
1000 float T_old_old = To[p];
1001 float T_old = T_old_old;
1002 T_old_old = 400.f;
1003
1004 // Evaluate residual at initial guesses
1005 float resid_old = evaluateEnergyBalance_CPU(T_old, R[p], Qother[p], eps[p], Ta[p], ea[p], pressure[p], gH[p], gS[p], Nsides[p], stomatal_sidedness[p], heatcapacity[p], surfacehumidity[p], dt, To[p]);
1006 float resid_old_old = evaluateEnergyBalance_CPU(T_old_old, R[p], Qother[p], eps[p], Ta[p], ea[p], pressure[p], gH[p], gS[p], Nsides[p], stomatal_sidedness[p], heatcapacity[p], surfacehumidity[p], dt, To[p]);
1007
1008 // Secant method iteration
1009 float T_new;
1010 float resid = 100;
1011 float err = resid;
1012 uint iter = 0;
1013 while (err > err_max && iter < max_iter) {
1014
1015 if (resid_old == resid_old_old) {
1016 err = 0;
1017 break;
1018 }
1019
1020 T_new = fabs((T_old_old * resid_old - T_old * resid_old_old) / (resid_old - resid_old_old));
1021
1022 resid = evaluateEnergyBalance_CPU(T_new, R[p], Qother[p], eps[p], Ta[p], ea[p], pressure[p], gH[p], gS[p], Nsides[p], stomatal_sidedness[p], heatcapacity[p], surfacehumidity[p], dt, To[p]);
1023
1024 resid_old_old = resid_old;
1025 resid_old = resid;
1026
1027 err = fabs(T_old - T_old_old) / fabs(T_old_old);
1028
1029 T_old_old = T_old;
1030 T_old = T_new;
1031
1032 iter++;
1033 }
1034
1035 // Track convergence failures (thread-safe)
1036 if (err > err_max) {
1037#ifdef USE_OPENMP
1038 int thread_id = omp_get_thread_num();
1039 thread_convergence_failures[thread_id]++;
1040#else
1041 convergence_failures++;
1042#endif
1043 }
1044
1045 T[p] = T_new;
1046 }
1047
1048// Report convergence warnings
1049#ifdef USE_OPENMP
1050 int total_failures = 0;
1051 for (int i = 0; i < num_threads; i++) {
1052 total_failures += thread_convergence_failures[i];
1053 }
1054 if (total_failures > 0 && message_flag) {
1055 std::cout << "WARNING (EnergyBalanceModel::solveEnergyBalance): Energy balance did not converge for " << total_failures << " primitives." << std::endl;
1056 }
1057#else
1058 if (convergence_failures > 0 && message_flag) {
1059 std::cout << "WARNING (EnergyBalanceModel::solveEnergyBalance): Energy balance did not converge for " << convergence_failures << " primitives." << std::endl;
1060 }
1061#endif
1062
1063 //---- Write results back to Context ----//
1064
1065 for (uint u = 0; u < Nprimitives; u++) {
1066 size_t UUID = UUIDs.at(u);
1067
1068 if (T[u] != T[u]) { // NaN check
1069 T[u] = temperature_default;
1070 }
1071
1072 context->setPrimitiveData(UUID, "temperature", T[u]);
1073
1074 float QH = cp_air_mol * gH[u] * (T[u] - Ta[u]);
1075 context->setPrimitiveData(UUID, "sensible_flux", QH);
1076
1077 float es = esat_Pa(T[u]);
1078 float gM = 1.08f * gH[u] * gS[u] * (stomatal_sidedness[u] / (1.08f * gH[u] + gS[u] * stomatal_sidedness[u]) + (1.f - stomatal_sidedness[u]) / (1.08f * gH[u] + gS[u] * (1.f - stomatal_sidedness[u])));
1079 if (gH[u] == 0 && gS[u] == 0) {
1080 gM = 0;
1081 }
1082 float QL = lambda_mol * gM * (es - ea[u]) / pressure[u];
1083 context->setPrimitiveData(UUID, "latent_flux", QL);
1084
1085 for (const auto &data_label: output_prim_data) {
1086 if (data_label == "boundarylayer_conductance_out") {
1087 context->setPrimitiveData(UUID, "boundarylayer_conductance_out", gH[u]);
1088 } else if (data_label == "vapor_pressure_deficit") {
1089 float vpd = (es - ea[u]) / pressure[u];
1090 context->setPrimitiveData(UUID, "vapor_pressure_deficit", vpd);
1091 } else if (data_label == "net_radiation_flux") {
1092 float Rnet = R[u] - float(Nsides[u]) * eps[u] * 5.67e-8F * std::pow(T[u], 4);
1093 context->setPrimitiveData(UUID, "net_radiation_flux", Rnet);
1094 } else if (data_label == "storage_flux") {
1095 float storage = 0.f;
1096 if (dt > 0) {
1097 storage = heatcapacity[u] * (T[u] - To[u]) / dt;
1098 }
1099 context->setPrimitiveData(UUID, "storage_flux", storage);
1100 }
1101 }
1102 }
1103}