1.3.64
 
Loading...
Searching...
No Matches
SolarPosition.cpp
Go to the documentation of this file.
1
16#include "SolarPosition.h"
18
19using namespace std;
20using namespace helios;
21
23 context = context_ptr;
24 UTC = context->getLocation().UTC_offset;
25 latitude = context->getLocation().latitude_deg;
26 longitude = context->getLocation().longitude_deg;
27}
28
29
30SolarPosition::SolarPosition(float UTC_hrs, float latitude_deg, float longitude_deg, helios::Context *context_ptr) {
31 context = context_ptr;
32 UTC = UTC_hrs;
33 latitude = latitude_deg;
34 longitude = longitude_deg;
35
36 if (latitude_deg < -90 || latitude_deg > 90) {
37 std::cerr << "WARNING (SolarPosition): Latitude must be between -90 and +90 deg (a latitude of " << latitude << " was given). Default latitude is being used." << std::endl;
38 latitude = 38.55;
39 } else {
40 latitude = latitude_deg;
41 }
42
43 if (longitude < -180 || longitude > 180) {
44 std::cerr << "WARNING (SolarPosition): Longitude must be between -180 and +180 deg (a longitude of " << longitude << " was given). Default longitude is being used." << std::endl;
45 longitude = 121.76;
46 } else {
47 longitude = longitude_deg;
48 }
49}
50
51SphericalCoord SolarPosition::calculateSunDirection(const helios::Time &time, const helios::Date &date) const {
52
53 int solstice_day, LSTM;
54 float Gamma, delta, time_dec, B, EoT, TC, LST, h, theta, phi, rad;
55
56 rad = M_PI / 180.f;
57
58 solstice_day = 81;
59
60 // day angle (Iqbal Eq. 1.1.2)
61 Gamma = 2.f * M_PI * (float(date.JulianDay() - 1)) / 365.f;
62
63 // solar declination angle (Iqbal Eq. 1.3.1 after Spencer)
64 delta = 0.006918f - 0.399912f * cos(Gamma) + 0.070257f * sin(Gamma) - 0.006758f * cos(2.f * Gamma) + 0.000907f * sin(2.f * Gamma) - 0.002697f * cos(3.f * Gamma) + 0.00148f * sin(3.f * Gamma);
65
66 // equation of time (Iqbal Eq. 1.4.1 after Spencer)
67 EoT = 229.18f * (0.000075f + 0.001868f * cos(Gamma) - 0.032077f * sin(Gamma) - 0.014615f * cos(2.f * Gamma) - 0.04089f * sin(2.f * Gamma));
68
69 time_dec = time.hour + time.minute / 60.f; //(hours)
70
71 LSTM = 15.f * UTC; // degrees
72
73 TC = 4.f * (LSTM - longitude) + EoT; // minutes
74 LST = time_dec + TC / 60.f; // hours
75
76 h = (LST - 12.f) * 15.f * rad; // hour angle (rad)
77
78 // solar zenith angle
79 theta = asin_safe(sin(latitude * rad) * sin(delta) + cos(latitude * rad) * cos(delta) * cos(h)); //(rad)
80
81 assert(theta > -0.5f * M_PI && theta < 0.5f * M_PI);
82
83 // solar elevation angle
84 phi = acos_safe((sin(delta) - sin(theta) * sin(latitude * rad)) / (cos(theta) * cos(latitude * rad)));
85
86 if (LST > 12.f) {
87 phi = 2.f * M_PI - phi;
88 }
89
90 assert(phi > 0 && phi < 2.f * M_PI);
91
92 return make_SphericalCoord(theta, phi);
93}
94
96
97 Time result = make_Time(0, 0); // default/fallback value
98 bool found = false;
99
100 for (uint h = 1; h <= 23 && !found; h++) {
101 for (uint m = 1; m <= 59 && !found; m++) {
102 SphericalCoord sun_dir = calculateSunDirection(make_Time(h, m, 0), context->getDate());
103 if (sun_dir.elevation > 0) {
104 result = make_Time(h, m);
105 found = true;
106 }
107 }
108 }
109
110 return result;
111}
112
114
115 Time result = make_Time(0, 0); // default/fallback value
116 bool found = false;
117
118 for (int h = 23; h >= 1 && !found; h--) {
119 for (int m = 59; m >= 1 && !found; m--) {
120 SphericalCoord sun_dir = calculateSunDirection(make_Time(h, m, 0), context->getDate());
121 if (sun_dir.elevation > 0) {
122 result = make_Time(h, m);
123 found = true;
124 }
125 }
126 }
127
128 return result;
129}
130
132 float elevation;
133 if (issolarpositionoverridden) {
134 elevation = sun_direction.elevation;
135 } else {
136 elevation = calculateSunDirection(context->getTime(), context->getDate()).elevation;
137 }
138 return elevation;
139}
140
142 float zenith;
143 if (issolarpositionoverridden) {
144 zenith = sun_direction.zenith;
145 } else {
146 zenith = calculateSunDirection(context->getTime(), context->getDate()).zenith;
147 }
148 return zenith;
149}
150
152 float azimuth;
153 if (issolarpositionoverridden) {
154 azimuth = sun_direction.azimuth;
155 } else {
156 azimuth = calculateSunDirection(context->getTime(), context->getDate()).azimuth;
157 }
158 return azimuth;
159}
160
162 SphericalCoord sundirection;
163 if (issolarpositionoverridden) {
164 sundirection = sun_direction;
165 } else {
166 sundirection = calculateSunDirection(context->getTime(), context->getDate());
167 }
168 return sphere2cart(sundirection);
169}
170
172 SphericalCoord sundirection;
173 if (issolarpositionoverridden) {
174 sundirection = sun_direction;
175 } else {
176 sundirection = calculateSunDirection(context->getTime(), context->getDate());
177 }
178 return sundirection;
179}
180
182 issolarpositionoverridden = true;
183 sun_direction = sundirection;
184}
185
186float SolarPosition::getSolarFlux(float pressure_Pa, float temperature_K, float humidity_rel, float turbidity) const {
187 // Deprecated method - kept for backward compatibility
188 float Eb_PAR, Eb_NIR, fdiff;
189 GueymardSolarModel(pressure_Pa, temperature_K, humidity_rel, turbidity, Eb_PAR, Eb_NIR, fdiff);
190 float Eb = Eb_PAR + Eb_NIR;
191 if (!cloudcalibrationlabel.empty()) {
192 applyCloudCalibration(Eb, fdiff);
193 }
194 return Eb;
195}
196
197float SolarPosition::getSolarFluxPAR(float pressure_Pa, float temperature_K, float humidity_rel, float turbidity) const {
198 // Deprecated method - kept for backward compatibility
199 float Eb_PAR, Eb_NIR, fdiff;
200 GueymardSolarModel(pressure_Pa, temperature_K, humidity_rel, turbidity, Eb_PAR, Eb_NIR, fdiff);
201 if (!cloudcalibrationlabel.empty()) {
202 applyCloudCalibration(Eb_PAR, fdiff);
203 }
204 return Eb_PAR;
205}
206
207float SolarPosition::getSolarFluxNIR(float pressure_Pa, float temperature_K, float humidity_rel, float turbidity) const {
208 // Deprecated method - kept for backward compatibility
209 float Eb_PAR, Eb_NIR, fdiff;
210 GueymardSolarModel(pressure_Pa, temperature_K, humidity_rel, turbidity, Eb_PAR, Eb_NIR, fdiff);
211 if (!cloudcalibrationlabel.empty()) {
212 applyCloudCalibration(Eb_NIR, fdiff);
213 }
214 return Eb_NIR;
215}
216
217float SolarPosition::getDiffuseFraction(float pressure_Pa, float temperature_K, float humidity_rel, float turbidity) const {
218 // Deprecated method - kept for backward compatibility
219 float Eb_PAR, Eb_NIR, fdiff;
220 GueymardSolarModel(pressure_Pa, temperature_K, humidity_rel, turbidity, Eb_PAR, Eb_NIR, fdiff);
221 if (!cloudcalibrationlabel.empty()) {
222 applyCloudCalibration(Eb_PAR, fdiff);
223 }
224 return fdiff;
225}
226
227void SolarPosition::GueymardSolarModel(float pressure, float temperature, float humidity, float turbidity, float &Eb_PAR, float &Eb_NIR, float &fdiff) const {
228
229 float beta = turbidity;
230
231 uint DOY = context->getJulianDate();
232
233 float theta = getSunZenith();
234
235 if (theta <= 0.f || theta > 0.5 * M_PI) {
236 Eb_PAR = 0.f;
237 Eb_NIR = 0.f;
238 fdiff = 1.f;
239 return;
240 }
241
242 float m = pow(cos(theta) + 0.15 * pow(93.885 - theta * 180 / M_PI, -1.25), -1);
243
244 float E0_PAR = 635.4;
245 float E0_NIR = 709.7;
246
247 vec2 alpha(1.3, 1.3);
248
249 //---- Rayleigh ----//
250 // NOTE: Rayleigh scattering dominates the atmospheric attenuation, and thus variations in the model predictions are almost entirely due to pressure (and theta)
251 float mR = 1.f / (cos(theta) + 0.48353 * pow(theta * 180 / M_PI, 0.095846) / pow(96.741 - theta * 180 / M_PI, 1.754));
252
253 float mR_p = mR * pressure / 101325;
254
255 float TR_PAR = (1.f + 1.8169 * mR_p - 0.033454 * mR_p * mR_p) / (1.f + 2.063 * mR_p + 0.31978 * mR_p * mR_p);
256
257 float TR_NIR = (1.f - 0.010394 * mR_p) / (1.f - 0.00011042 * mR_p * mR_p);
258
259 //---- Uniform gasses ----//
260 mR = 1.f / (cos(theta) + 0.48353 * pow(theta * 180 / M_PI, 0.095846) / pow(96.741 - theta * 180 / M_PI, 1.754));
261
262 mR_p = mR * pressure / 101325;
263
264 float Tg_PAR = (1.f + 0.95885 * mR_p + 0.012871 * mR_p * mR_p) / (1.f + 0.96321 * mR_p + 0.015455 * mR_p * mR_p);
265
266 float Tg_NIR = (1.f + 0.27284 * mR_p - 0.00063699 * mR_p * mR_p) / (1.f + 0.30306 * mR_p);
267
268 float BR_PAR = 0.5f * (0.89013 - 0.0049558 * mR + 0.000045721 * mR * mR);
269
270 float BR_NIR = 0.5;
271
272 float Ba = 1.f - exp(-0.6931 - 1.8326 * cos(theta));
273
274 //---- Ozone -----//
275 float uo = (235 + (150 + 40 * sin(0.9856 * (DOY - 30) * M_PI / 180.f) + 20 * sin(3 * (longitude * M_PI / 180.f + 20))) * pow(sin(1.28 * latitude * M_PI / 180.f), 2)) * 0.001f; // O3 atm-cm
276 // NOTE: uo model from van Heuklon (1979)
277 float mo = m;
278
279 float f1 = uo * (10.979 - 8.5421 * uo) / (1.f + 2.0115 * uo + 40.189 * uo * uo);
280 float f2 = uo * (-0.027589 - 0.005138 * uo) / (1.f - 2.4857 * uo + 13.942 * uo * uo);
281 float f3 = uo * (10.995 - 5.5001 * uo) / (1.f + 1.678 * uo + 42.406 * uo * uo);
282 float To_PAR = (1.f + f1 * mo + f2 * mo * mo) / (1.f + f3 * mo);
283
284 float To_NIR = 1.f;
285
286 //---- Nitrogen ---- //
287 float un = 0.0002; // N atm-cm
288 float mw = 1.f / (cos(theta) + 1.065 * pow(theta * 180 / M_PI, 1.6132) / pow(111.55 - theta * 180 / M_PI, 3.2629));
289
290 float g1 = (0.17499 + 41.654 * un - 2146.4 * un * un) / (1 + 22295 * un * un);
291 float g2 = un * (-1.2134 + 59.324 * un) / (1.f + 8847.8 * un * un);
292 float g3 = (0.17499 + 61.658 * un + 9196.4 * un * un) / (1.f + 74109 * un * un);
293 float Tn_PAR = fmin(1.f, (1.f + g1 * mw + g2 * mw * mw) / (1.f + g3 * mw));
294 float Tn_PAR_p = fmin(1.f, (1.f + g1 * 1.66 + g2 * 1.66 * 1.66) / (1.f + g3 * 1.66));
295
296 float Tn_NIR = 1.f;
297 float Tn_NIR_p = 1.f;
298
299 //---- Water -----//
300 float gamma = log(humidity) + 17.67 * (temperature - 273) / (243 + 25);
301 float tdp = 243 * gamma / (17.67 - gamma) * 9 / 5 + 32; // dewpoint temperature in Fahrenheit
302 float w = exp((0.1133 - log(4.0 + 1)) + 0.0393 * tdp); // cm of precipitable water
303 // NOTE: precipitable water model from Viswanadham (1981), Eq. 5
304 mw = 1.f / (cos(theta) + 1.1212 * pow(theta * 180 / M_PI, 0.6379) / pow(93.781 - theta * 180 / M_PI, 1.9203));
305
306 float h1 = w * (0.065445 + 0.00029901 * w) / (1.f + 1.2728 * w);
307 float h2 = w * (0.065687 + 0.0013218 * w) / (1.f + 1.2008 * w);
308 float Tw_PAR = (1.f + h1 * mw) / (1.f + h2 * mw);
309 float Tw_PAR_p = (1.f + h1 * 1.66) / (1.f + h2 * 1.66);
310
311 float c1 = w * (19.566 - 1.6506 * w + 1.0672 * w * w) / (1.f + 5.4248 * w + 1.6005 * w * w);
312 float c2 = w * (0.50158 - 0.14732 * w + 0.047584 * w * w) / (1.f + 1.1811 * w + 1.0699 * w * w);
313 float c3 = w * (21.286 - 0.39232 * w + 1.2692 * w * w) / (1.f + 4.8318 * w + 1.412 * w * w);
314 float c4 = w * (0.70992 - 0.23155 * w + 0.096541 * w * w) / (1.f + 0.44907 * w + 0.75425 * w * w);
315 float Tw_NIR = (1.f + c1 * mw + c2 * mw * mw) / (1.f + c3 * mw + c4 * mw * mw);
316 float Tw_NIR_p = (1.f + c1 * 1.66 + c2 * 1.66 * 1.66) / (1.f + c3 * 1.66 + c4 * 1.66 * 1.66);
317
318 //---- Aerosol ----//
319 float ma = 1.f / (cos(theta) + 0.16851 * pow(theta * 180 / M_PI, 0.18198) / pow(95.318 - theta * 180 / M_PI, 1.9542));
320 float ua = log(1.f + ma * beta);
321
322 float d0 = 0.57664 - 0.024743 * alpha.x;
323 float d1 = (0.093942 - 0.2269 * alpha.x + 0.12848 * alpha.x * alpha.x) / (1.f + 0.6418 * alpha.x);
324 float d2 = (-0.093819 + 0.36668 * alpha.x - 0.12775 * alpha.x * alpha.x) / (1.f - 0.11651 * alpha.x);
325 float d3 = alpha.x * (0.15232 - 0.08721 * alpha.x + 0.012664 * alpha.x * alpha.x) / (1.f - 0.90454 * alpha.x + 0.26167 * alpha.x * alpha.x);
326 float lambdae_PAR = (d0 + d1 * ua + d2 * ua * ua) / (1.f + d3 * ua * ua);
327 float Ta_PAR = exp(-ma * beta * pow(lambdae_PAR, -alpha.x));
328
329 float e0 = (1.183 - 0.022989 * alpha.y + 0.020829 * alpha.y * alpha.y) / (1.f + 0.11133 * alpha.y);
330 float e1 = (-0.50003 - 0.18329 * alpha.y + 0.23835 * alpha.y * alpha.y) / (1.f + 1.6756 * alpha.y);
331 float e2 = (-0.50001 + 1.1414 * alpha.y + 0.0083589 * alpha.y * alpha.y) / (1.f + 11.168 * alpha.y);
332 float e3 = (-0.70003 - 0.73587 * alpha.y + 0.51509 * alpha.y * alpha.y) / (1.f + 4.7665 * alpha.y);
333 float lambdae_NIR = (e0 + e1 * ua + e2 * ua * ua) / (1.f + e3 * ua);
334 float Ta_NIR = exp(-ma * beta * pow(lambdae_NIR, -alpha.y));
335
336 float omega_PAR = 1.0;
337 float omega_NIR = 1.0;
338
339 float Tas_PAR = exp(-ma * omega_PAR * beta * pow(lambdae_PAR, -alpha.x));
340
341 float Tas_NIR = exp(-ma * omega_NIR * beta * pow(lambdae_NIR, -alpha.y));
342
343 // direct irradiation
344 Eb_PAR = TR_PAR * Tg_PAR * To_PAR * Tn_PAR * Tw_PAR * Ta_PAR * E0_PAR;
345 Eb_NIR = TR_NIR * Tg_NIR * To_NIR * Tn_NIR * Tw_NIR * Ta_NIR * E0_NIR;
346 float Eb = Eb_PAR + Eb_NIR;
347
348 // diffuse irradiation
349 float Edp_PAR = To_PAR * Tg_PAR * Tn_PAR_p * Tw_PAR_p * (BR_PAR * (1.f - TR_PAR) * pow(Ta_PAR, 0.25) + Ba * TR_PAR * (1.f - pow(Tas_PAR, 0.25))) * E0_PAR;
350 float Edp_NIR = To_NIR * Tg_NIR * Tn_NIR_p * Tw_NIR_p * (BR_NIR * (1.f - TR_NIR) * pow(Ta_NIR, 0.25) + Ba * TR_NIR * (1.f - pow(Tas_NIR, 0.25))) * E0_NIR;
351 float Edp = Edp_PAR + Edp_NIR;
352
353 // diffuse fraction
354 fdiff = Edp / (Eb + Edp);
355
356 assert(fdiff >= 0.f && fdiff <= 1.f);
357}
358
359float SolarPosition::getAmbientLongwaveFlux(float temperature_K, float humidity_rel) const {
360 // Deprecated method - kept for backward compatibility
361 // Model from Prata (1996) Q. J. R. Meteorol. Soc.
362 float e0 = 611.f * exp(17.502f * (temperature_K - 273.f) / ((temperature_K - 273.f) + 240.9f)) * humidity_rel; // Pascals
363 float K = 0.465f; // cm-K/Pa
364 float xi = e0 / temperature_K * K;
365 float eps = 1.f - (1.f + xi) * exp(-sqrt(1.2f + 3.f * xi));
366
367 return eps * 5.67e-8 * pow(temperature_K, 4);
368}
369
370float SolarPosition::turbidityResidualFunction(float turbidity, std::vector<float> &parameters, const void *a_solarpositionmodel) {
371
372 auto *solarpositionmodel = reinterpret_cast<const SolarPosition *>(a_solarpositionmodel);
373
374 float pressure = parameters.at(0);
375 float temperature = parameters.at(1);
376 float humidity = parameters.at(2);
377 float flux_target = parameters.at(3);
378
379 // Clamp turbidity to minimum positive value (optimization can try negative values)
380 float turbidity_clamped = std::max(1e-5f, turbidity);
381
382 // Use new API: set atmospheric conditions, then get flux
383 // Note: const_cast is needed here because this is an optimization callback that needs to modify internal state
384 auto *mutable_model = const_cast<SolarPosition *>(solarpositionmodel);
385 mutable_model->setAtmosphericConditions(pressure, temperature, humidity, turbidity_clamped);
386 float flux_model = mutable_model->getSolarFlux() * cosf(mutable_model->getSunZenith());
387 return flux_model - flux_target;
388}
389
390float SolarPosition::calibrateTurbidityFromTimeseries(const std::string &timeseries_shortwave_flux_label_Wm2) const {
391
392 if (!context->doesTimeseriesVariableExist(timeseries_shortwave_flux_label_Wm2.c_str())) {
393 helios_runtime_error("ERROR (SolarPosition::calibrateTurbidityFromTimeseries): Timeseries variable " + timeseries_shortwave_flux_label_Wm2 + " does not exist.");
394 }
395
396 uint length = context->getTimeseriesLength(timeseries_shortwave_flux_label_Wm2.c_str());
397
398 float min_flux = 1e6;
399 float max_flux = 0;
400 int max_flux_index = 0;
401 for (int t = 0; t < length; t++) {
402 float flux = context->queryTimeseriesData(timeseries_shortwave_flux_label_Wm2.c_str(), t);
403 if (flux < min_flux) {
404 min_flux = flux;
405 }
406 if (flux > max_flux) {
407 max_flux = flux;
408 max_flux_index = t;
409 }
410 }
411
412 if (max_flux < 750 || max_flux > 1200) {
413 helios_runtime_error("ERROR (SolarPosition::calibrateTurbidityFromTimeseries): The maximum flux for the timeseries data is not within the expected range. Either it is not solar flux data, or there are no clear sky days in the dataset");
414 } else if (min_flux < 0) {
415 helios_runtime_error("ERROR (SolarPosition::calibrateTurbidityFromTimeseries): The minimum flux for the timeseries data is negative. Solar fluxes cannot be negative.");
416 }
417
418 std::vector<float> parameters{101325, 300, 0.5, max_flux};
419
420 SolarPosition solarposition_copy(UTC, latitude, longitude, context);
421 Date date_max = context->queryTimeseriesDate(timeseries_shortwave_flux_label_Wm2.c_str(), max_flux_index);
422 Time time_max = context->queryTimeseriesTime(timeseries_shortwave_flux_label_Wm2.c_str(), max_flux_index);
423
424 solarposition_copy.setSunDirection(solarposition_copy.calculateSunDirection(time_max, date_max));
425
427 float turbidity = fzero(turbidityResidualFunction, parameters, &solarposition_copy, 0.01, 0.0001f, 100, &warnings);
428 warnings.report(std::cerr);
429
430 return std::max(1e-4F, turbidity);
431}
432
433void SolarPosition::enableCloudCalibration(const std::string &timeseries_shortwave_flux_label_Wm2) {
434
435 if (!context->doesTimeseriesVariableExist(timeseries_shortwave_flux_label_Wm2.c_str())) {
436 helios_runtime_error("ERROR (SolarPosition::enableCloudCalibration): Timeseries variable " + timeseries_shortwave_flux_label_Wm2 + " does not exist.");
437 }
438
439 cloudcalibrationlabel = timeseries_shortwave_flux_label_Wm2;
440}
441
443 cloudcalibrationlabel = "";
444}
445
446void SolarPosition::setAtmosphericConditions(float pressure_Pa, float temperature_K, float humidity_rel, float turbidity) {
447 // Validate input parameters
448 if (pressure_Pa <= 0.f) {
449 helios_runtime_error("ERROR (SolarPosition::setAtmosphericConditions): Atmospheric pressure must be positive. Got " + std::to_string(pressure_Pa) + " Pa.");
450 }
451 if (temperature_K <= 0.f) {
452 helios_runtime_error("ERROR (SolarPosition::setAtmosphericConditions): Temperature must be positive Kelvin. Got " + std::to_string(temperature_K) + " K.");
453 }
454 if (humidity_rel < 0.f || humidity_rel > 1.f) {
455 helios_runtime_error("ERROR (SolarPosition::setAtmosphericConditions): Relative humidity must be between 0 and 1. Got " + std::to_string(humidity_rel) + ".");
456 }
457 if (turbidity < 0.f) {
458 helios_runtime_error("ERROR (SolarPosition::setAtmosphericConditions): Turbidity must be non-negative. Got " + std::to_string(turbidity) + ".");
459 }
460
461 // Set global data in the Context
462 context->setGlobalData("atmosphere_pressure_Pa", pressure_Pa);
463 context->setGlobalData("atmosphere_temperature_K", temperature_K);
464 context->setGlobalData("atmosphere_humidity_rel", humidity_rel);
465 context->setGlobalData("atmosphere_turbidity", turbidity);
466}
467
468void SolarPosition::getAtmosphericConditions(float &pressure_Pa, float &temperature_K, float &humidity_rel, float &turbidity) const {
469 // Default values
470 const float default_pressure = 101325.f; // 1 atm in Pa
471 const float default_temperature = 300.f; // 27°C in K
472 const float default_humidity = 0.5f; // 50%
473 const float default_turbidity = 0.02f; // clear sky
474
475 static bool warning_issued = false;
476
477 // Check if global data exists and retrieve it, otherwise use defaults
478 bool all_exist =
479 context->doesGlobalDataExist("atmosphere_pressure_Pa") && context->doesGlobalDataExist("atmosphere_temperature_K") && context->doesGlobalDataExist("atmosphere_humidity_rel") && context->doesGlobalDataExist("atmosphere_turbidity");
480
481 if (all_exist) {
482 context->getGlobalData("atmosphere_pressure_Pa", pressure_Pa);
483 context->getGlobalData("atmosphere_temperature_K", temperature_K);
484 context->getGlobalData("atmosphere_humidity_rel", humidity_rel);
485 context->getGlobalData("atmosphere_turbidity", turbidity);
486 } else {
487 if (!warning_issued) {
488 std::cerr << "WARNING (SolarPosition::getAtmosphericConditions): Atmospheric conditions have not been set via setAtmosphericConditions(). Using default values: pressure=" << default_pressure << " Pa, temperature=" << default_temperature
489 << " K, humidity=" << default_humidity << ", turbidity=" << default_turbidity << std::endl;
490 warning_issued = true;
491 }
492 pressure_Pa = default_pressure;
493 temperature_K = default_temperature;
494 humidity_rel = default_humidity;
495 turbidity = default_turbidity;
496 }
497}
498
500 float pressure, temperature, humidity, turbidity;
501 getAtmosphericConditions(pressure, temperature, humidity, turbidity);
502
503 float Eb_PAR, Eb_NIR, fdiff;
504 GueymardSolarModel(pressure, temperature, humidity, turbidity, Eb_PAR, Eb_NIR, fdiff);
505 float Eb = Eb_PAR + Eb_NIR;
506 if (!cloudcalibrationlabel.empty()) {
507 applyCloudCalibration(Eb, fdiff);
508 }
509 return Eb;
510}
511
513 float pressure, temperature, humidity, turbidity;
514 getAtmosphericConditions(pressure, temperature, humidity, turbidity);
515
516 float Eb_PAR, Eb_NIR, fdiff;
517 GueymardSolarModel(pressure, temperature, humidity, turbidity, Eb_PAR, Eb_NIR, fdiff);
518 if (!cloudcalibrationlabel.empty()) {
519 applyCloudCalibration(Eb_PAR, fdiff);
520 }
521 return Eb_PAR;
522}
523
525 float pressure, temperature, humidity, turbidity;
526 getAtmosphericConditions(pressure, temperature, humidity, turbidity);
527
528 float Eb_PAR, Eb_NIR, fdiff;
529 GueymardSolarModel(pressure, temperature, humidity, turbidity, Eb_PAR, Eb_NIR, fdiff);
530 if (!cloudcalibrationlabel.empty()) {
531 applyCloudCalibration(Eb_NIR, fdiff);
532 }
533 return Eb_NIR;
534}
535
537 float pressure, temperature, humidity, turbidity;
538 getAtmosphericConditions(pressure, temperature, humidity, turbidity);
539
540 float Eb_PAR, Eb_NIR, fdiff;
541 GueymardSolarModel(pressure, temperature, humidity, turbidity, Eb_PAR, Eb_NIR, fdiff);
542 if (!cloudcalibrationlabel.empty()) {
543 applyCloudCalibration(Eb_PAR, fdiff);
544 }
545 return fdiff;
546}
547
549 float pressure, temperature, humidity, turbidity;
550 getAtmosphericConditions(pressure, temperature, humidity, turbidity);
551
552 // Model from Prata (1996) Q. J. R. Meteorol. Soc.
553 float e0 = 611.f * exp(17.502f * (temperature - 273.f) / ((temperature - 273.f) + 240.9f)) * humidity; // Pascals
554 float K = 0.465f; // cm-K/Pa
555 float xi = e0 / temperature * K;
556 float eps = 1.f - (1.f + xi) * exp(-sqrt(1.2f + 3.f * xi));
557
558 return eps * 5.67e-8 * pow(temperature, 4);
559}
560
561void SolarPosition::applyCloudCalibration(float &R_calc_Wm2, float &fdiff_calc) const {
562
563 assert(context->doesTimeseriesVariableExist(cloudcalibrationlabel.c_str()));
564
565 float R_meas = context->queryTimeseriesData(cloudcalibrationlabel.c_str());
566 float R_calc_horiz = R_calc_Wm2 * cosf(getSunZenith());
567
568 float fdiff = fmin(fmax(0, 1.f - (R_meas - R_calc_horiz) / (R_calc_horiz)), 1);
569 float R = R_calc_Wm2 * R_meas / R_calc_horiz;
570
571 if (fdiff > 0.001 && R_calc_horiz > 1.f) {
572 R_calc_Wm2 = R;
573 fdiff_calc = fdiff;
574 }
575}
576
577// ===== SSolar-GOA Spectral Solar Radiation Model =====
578
579void SolarPosition::SpectralData::loadFromDirectory(const std::string &data_path) {
580 // Load wehrli.dat (TOA spectrum)
581 std::string wehrli_file = data_path + "/wehrli.dat";
582 std::ifstream wehrli_stream(wehrli_file);
583 if (!wehrli_stream.is_open()) {
584 helios_runtime_error("ERROR (SolarPosition::SpectralData::loadFromDirectory): Could not open file " + wehrli_file);
585 }
586
587 wavelengths_nm.clear();
588 toa_irradiance.clear();
589
590 std::string line;
591 while (std::getline(wehrli_stream, line)) {
592 // Skip comments and empty lines
593 if (line.empty() || line[0] == '#') {
594 continue;
595 }
596
597 std::istringstream iss(line);
598 float wavelength, irradiance;
599 if (iss >> wavelength >> irradiance) {
600 wavelengths_nm.push_back(wavelength);
601 toa_irradiance.push_back(irradiance);
602 }
603 }
604 wehrli_stream.close();
605
606 if (wavelengths_nm.empty()) {
607 helios_runtime_error("ERROR (SolarPosition::SpectralData::loadFromDirectory): No data loaded from " + wehrli_file);
608 }
609
610 // Load abscoef.dat (absorption coefficients)
611 std::string abscoef_file = data_path + "/abscoef.dat";
612 std::ifstream abscoef_stream(abscoef_file);
613 if (!abscoef_stream.is_open()) {
614 helios_runtime_error("ERROR (SolarPosition::SpectralData::loadFromDirectory): Could not open file " + abscoef_file);
615 }
616
617 h2o_coef.clear();
618 h2o_exp.clear();
619 o3_xsec.clear();
620 o2_coef.clear();
621
622 while (std::getline(abscoef_stream, line)) {
623 // Skip comments and empty lines
624 if (line.empty() || line[0] == '#') {
625 continue;
626 }
627
628 std::istringstream iss(line);
629 float wavelength, h2o_c, h2o_e, o3_x, o2_c;
630 float no2_x, co2_c; // We don't use these, but need to read them
631 if (iss >> wavelength >> h2o_c >> h2o_e >> o3_x >> o2_c >> no2_x >> co2_c) {
632 h2o_coef.push_back(h2o_c);
633 h2o_exp.push_back(h2o_e);
634 o3_xsec.push_back(o3_x);
635 o2_coef.push_back(o2_c);
636 }
637 }
638 abscoef_stream.close();
639
640 if (h2o_coef.empty()) {
641 helios_runtime_error("ERROR (SolarPosition::SpectralData::loadFromDirectory): No data loaded from " + abscoef_file);
642 }
643
644 // Validate that wavelength arrays match
645 if (wavelengths_nm.size() != h2o_coef.size()) {
646 helios_runtime_error("ERROR (SolarPosition::SpectralData::loadFromDirectory): Wavelength arrays from wehrli.dat and abscoef.dat do not match in size");
647 }
648
649 // Validate wavelength range (should be 300-2600 nm)
650 if (wavelengths_nm.front() < 299.5f || wavelengths_nm.front() > 300.5f) {
651 helios_runtime_error("ERROR (SolarPosition::SpectralData::loadFromDirectory): Expected wavelength range to start near 300 nm, got " + std::to_string(wavelengths_nm.front()));
652 }
653 if (wavelengths_nm.back() < 2599.5f || wavelengths_nm.back() > 2600.5f) {
654 helios_runtime_error("ERROR (SolarPosition::SpectralData::loadFromDirectory): Expected wavelength range to end near 2600 nm, got " + std::to_string(wavelengths_nm.back()));
655 }
656}
657
658float SolarPosition::SpectralData::interpolate(const std::vector<float> &x, const std::vector<float> &y, float x_val) {
659 if (x.empty() || y.empty() || x.size() != y.size()) {
660 helios_runtime_error("ERROR (SolarPosition::SpectralData::interpolate): Invalid input vectors");
661 }
662
663 // If x_val is outside the range, return boundary values
664 if (x_val <= x.front()) {
665 return y.front();
666 }
667 if (x_val >= x.back()) {
668 return y.back();
669 }
670
671 // Find the two points for interpolation using binary search
672 auto it = std::lower_bound(x.begin(), x.end(), x_val);
673 if (it == x.begin()) {
674 return y.front();
675 }
676 if (it == x.end()) {
677 return y.back();
678 }
679
680 size_t idx1 = std::distance(x.begin(), it) - 1;
681 size_t idx2 = idx1 + 1;
682
683 // Linear interpolation
684 float x1 = x[idx1];
685 float x2 = x[idx2];
686 float y1 = y[idx1];
687 float y2 = y[idx2];
688
689 return y1 + (y2 - y1) * (x_val - x1) / (x2 - x1);
690}
691
692float SolarPosition::calculateGeometricFactor(int julian_day) const {
693 // Fourier series coefficients for Earth-Sun distance correction
694 // Based on Duffie and Beckman (2013), Solar Engineering of Thermal Processes
695 const float c[] = {1.00011f, 0.03422f, 0.00128f, 0.000719f, 0.000077f};
696
697 // Day angle in radians
698 float day_angle = (julian_day - 1) * 2.0f * M_PI / 365.0f;
699 float day_angle_2 = day_angle * 2.0f;
700
701 // Calculate geometric factor (inverse square of Earth-Sun distance ratio)
702 float geo_factor = c[0] + c[1] * cosf(day_angle) + c[2] * sinf(day_angle) + c[3] * cosf(day_angle_2) + c[4] * sinf(day_angle_2);
703
704 return geo_factor;
705}
706
707void SolarPosition::calculateRayleighTransmittance(const std::vector<float> &wavelengths_um, float mu0, float pressure_ratio, std::vector<float> &tdir, std::vector<float> &tglb, std::vector<float> &tdif, std::vector<float> &atm_albedo) const {
708 // Bates (1984) formula for Rayleigh optical depth with Sobolev approximation
709 const float c[] = {117.2594f, -1.3215f, 0.000320f, -0.000076f};
710
711 // Resize output vectors
712 size_t n = wavelengths_um.size();
713 tdir.resize(n);
714 tglb.resize(n);
715 tdif.resize(n);
716 atm_albedo.resize(n);
717
718 for (size_t i = 0; i < n; ++i) {
719 float w = wavelengths_um[i];
720 float w2 = w * w;
721 float w4 = w2 * w2;
722
723 // Rayleigh optical depth (Bates formula)
724 float tau = pressure_ratio / (c[0] * w4 + c[1] * w2 + c[2] + c[3] / w2);
725
726 // Direct beam transmittance (Beer-Lambert law)
727 tdir[i] = expf(-tau / mu0);
728
729 // Global transmittance (Sobolev's two-stream approximation)
730 tglb[i] = ((2.0f / 3.0f + mu0) + (2.0f / 3.0f - mu0) * tdir[i]) / (4.0f / 3.0f + tau);
731
732 // Diffuse transmittance
733 tdif[i] = tglb[i] - tdir[i];
734
735 // Atmospheric albedo (spherical albedo for Rayleigh scattering)
736 atm_albedo[i] = tau * (1.0f - expf(-2.0f * tau)) / (2.0f + tau);
737 }
738}
739
740void SolarPosition::calculateAerosolTransmittance(const std::vector<float> &wavelengths_um, float mu0, float alpha, float beta, float w0, float g, std::vector<float> &tdir, std::vector<float> &tglb, std::vector<float> &tdif,
741 std::vector<float> &atm_albedo) const {
742 // Ångström law for aerosol optical depth with Ambartsumian solution for transmittance
743
744 // Resize output vectors
745 size_t n = wavelengths_um.size();
746 tdir.resize(n);
747 tglb.resize(n);
748 tdif.resize(n);
749 atm_albedo.resize(n);
750
751 // Ambartsumian's parameter
752 float K = sqrtf((1.0f - w0) * (1.0f - w0 * g));
753 float r0 = (K - 1.0f + w0) / (K + 1.0f - w0);
754
755 for (size_t i = 0; i < n; ++i) {
756 float w = wavelengths_um[i];
757
758 // Ångström formula for aerosol optical depth
759 float tau = beta / powf(w, alpha);
760
761 // Direct beam transmittance
762 tdir[i] = expf(-tau / mu0);
763
764 // Ambartsumian's solution for global transmittance
765 float tdir_k = powf(tdir[i], K);
766 tglb[i] = (1.0f - r0 * r0) * tdir_k / (1.0f - r0 * r0 * tdir_k * tdir_k);
767
768 // Diffuse transmittance
769 tdif[i] = tglb[i] - tdir[i];
770
771 // Atmospheric albedo for aerosols
772 float g_factor = (1.0f - g) * w0;
773 atm_albedo[i] = g_factor * tau / (2.0f + g_factor * tau) * (1.0f + expf(-g_factor * tau));
774 }
775}
776
777void SolarPosition::calculateMixtureTransmittance(const std::vector<float> &wavelengths_um, float mu0, float pressure_Pa, float turbidity_beta, float angstrom_alpha, float aerosol_ssa, float aerosol_g, bool coupling, std::vector<float> &tglb,
778 std::vector<float> &tdir, std::vector<float> &tdif, std::vector<float> &atm_albedo) const {
779 // Combined Rayleigh-aerosol transmittance with coupling (Cachorro et al. 2022, Eq. 20)
780
781 float pressure_ratio = pressure_Pa / 101325.0f;
782
783 // Calculate separate Rayleigh and aerosol components
784 std::vector<float> ray_tdir, ray_tglb, ray_tdif, ray_albedo;
785 std::vector<float> aer_tdir, aer_tglb, aer_tdif, aer_albedo;
786
787 calculateRayleighTransmittance(wavelengths_um, mu0, pressure_ratio, ray_tdir, ray_tglb, ray_tdif, ray_albedo);
788 calculateAerosolTransmittance(wavelengths_um, mu0, angstrom_alpha, turbidity_beta, aerosol_ssa, aerosol_g, aer_tdir, aer_tglb, aer_tdif, aer_albedo);
789
790 // Resize output vectors
791 size_t n = wavelengths_um.size();
792 tglb.resize(n);
793 tdir.resize(n);
794 tdif.resize(n);
795 atm_albedo.resize(n);
796
797 // Combine transmittances
798 for (size_t i = 0; i < n; ++i) {
799 if (coupling) {
800 // With Rayleigh-aerosol coupling (Cachorro et al. 2022, Eq. 20)
801 float beta_w = turbidity_beta / powf(wavelengths_um[i], angstrom_alpha);
802 float tau_w = beta_w / mu0;
803 float coup_term = tau_w * (1.0f - aer_tglb[i]);
804
805 tglb[i] = ray_tglb[i] * aer_tglb[i] + coup_term;
806 tdir[i] = ray_tdir[i] * aer_tdir[i];
807 } else {
808 // Without coupling (simple multiplication)
809 tglb[i] = ray_tglb[i] * aer_tglb[i];
810 tdir[i] = ray_tdir[i] * aer_tdir[i];
811 }
812
813 tdif[i] = tglb[i] - tdir[i];
814
815 // Combined atmospheric albedo (weighted average)
816 atm_albedo[i] = ray_albedo[i] + aer_albedo[i];
817 }
818}
819
820void SolarPosition::calculateWaterVaporTransmittance(const SpectralData &data, float mu0, float water_vapor_cm, std::vector<float> &transmittance) const {
821 // Water vapor absorption using empirical coefficients (Gueymard 1994)
822
823 size_t n = data.wavelengths_nm.size();
824 transmittance.resize(n);
825
826 for (size_t i = 0; i < n; ++i) {
827 float h2o_coef = data.h2o_coef[i];
828 float h2o_exp = data.h2o_exp[i];
829
830 if (h2o_exp < 1e-6f) {
831 // No absorption in this band
832 transmittance[i] = 1.0f;
833 } else {
834 // Power law absorption model
835 float optical_depth = powf(h2o_coef * water_vapor_cm / mu0, h2o_exp);
836 transmittance[i] = expf(-optical_depth);
837 }
838 }
839}
840
841void SolarPosition::calculateOzoneTransmittance(const SpectralData &data, float mu0, float ozone_DU, std::vector<float> &transmittance) const {
842 // Ozone absorption using measured cross sections
843 const float LOSCHMIDT = 2.687e19f; // molecules/cm³ at STP
844
845 size_t n = data.wavelengths_nm.size();
846 transmittance.resize(n);
847
848 for (size_t i = 0; i < n; ++i) {
849 // Convert cross section to absorption coefficient
850 float o3_coef = LOSCHMIDT * data.o3_xsec[i]; // cm⁻¹
851
852 // Convert ozone column from Dobson Units to cm
853 float o3_path_cm = ozone_DU * 1e-3f;
854
855 // Calculate optical depth and transmittance
856 float optical_depth = o3_coef * o3_path_cm / mu0;
857 transmittance[i] = expf(-optical_depth);
858 }
859}
860
861void SolarPosition::calculateOxygenTransmittance(const SpectralData &data, float mu0, std::vector<float> &transmittance) const {
862 // Oxygen absorption (primarily A-band at 760 nm and continuum)
863 const float O2_PATH = 0.209f * 173200.0f; // atm-cm
864 const float O2_EXP = 0.5641f;
865
866 size_t n = data.wavelengths_nm.size();
867 transmittance.resize(n);
868
869 for (size_t i = 0; i < n; ++i) {
870 float o2_coef = data.o2_coef[i];
871
872 // Power law absorption model for O2
873 float optical_depth = powf(o2_coef * O2_PATH / mu0, O2_EXP);
874 transmittance[i] = expf(-optical_depth);
875 }
876}
877
878void SolarPosition::calculateSpectralIrradianceComponents(std::vector<helios::vec2> &global_spectrum, std::vector<helios::vec2> &direct_spectrum, std::vector<helios::vec2> &diffuse_spectrum, float resolution_nm) const {
879 // Validate resolution
880 if (resolution_nm < 1.0f) {
881 helios_runtime_error("ERROR (SolarPosition::calculateSpectralIrradianceComponents): resolution_nm must be >= 1 nm, got " + std::to_string(resolution_nm));
882 }
883 if (resolution_nm > 2300.0f) {
884 helios_runtime_error("ERROR (SolarPosition::calculateSpectralIrradianceComponents): resolution_nm must be <= 2300 nm, got " + std::to_string(resolution_nm));
885 }
886 // Get atmospheric conditions
887 float pressure, temperature, humidity, turbidity_beta;
888 getAtmosphericConditions(pressure, temperature, humidity, turbidity_beta);
889
890 // Derive additional parameters
891 uint DOY = context->getJulianDate();
892
893 // Water vapor from Viswanadham (1981)
894 float gamma = logf(humidity) + 17.67f * (temperature - 273.0f) / 268.0f;
895 float tdp = 243.0f * gamma / (17.67f - gamma) * 9.0f / 5.0f + 32.0f;
896 float water_vapor_cm = expf((0.1133f - logf(5.0f)) + 0.0393f * tdp);
897
898 // Ozone from van Heuklon (1979)
899 float uo_atm_cm = (235.0f + (150.0f + 40.0f * sinf(0.9856f * (DOY - 30.0f) * M_PI / 180.0f) + 20.0f * sinf(3.0f * (longitude * M_PI / 180.0f + 20.0f))) * powf(sinf(1.28f * latitude * M_PI / 180.0f), 2)) * 0.001f;
900 float ozone_DU = uo_atm_cm * 1000.0f;
901
902 // Fixed parameters
903 const float angstrom_alpha = 1.3f;
904 const float surface_albedo = 0.2f;
905 const float aerosol_ssa = 0.90f;
906 const float aerosol_g = 0.85f;
907
908 // Load spectral data (cached after first load)
909 static SpectralData spectral_data;
910 static bool data_loaded = false;
911 if (!data_loaded) {
912 spectral_data.loadFromDirectory("plugins/solarposition/ssolar_goa");
913 data_loaded = true;
914 }
915
916 // Calculate solar geometry
917 float sun_zenith = getSunZenith();
918 float mu0 = cosf(sun_zenith);
919 if (mu0 <= 0.0f) {
920 helios_runtime_error("ERROR (SolarPosition::calculateSpectralIrradiance): Cannot calculate spectral irradiance when sun is below horizon (zenith angle = " + std::to_string(sun_zenith * 180.0f / M_PI) + " degrees)");
921 }
922
923 int julian_day = context->getJulianDate();
924 float geo_factor = calculateGeometricFactor(julian_day);
925
926 // Apply geometric factor to TOA spectrum
927 std::vector<float> toa_irr(spectral_data.toa_irradiance.size());
928 for (size_t i = 0; i < toa_irr.size(); ++i) {
929 toa_irr[i] = spectral_data.toa_irradiance[i] * geo_factor;
930 }
931
932 // Convert wavelengths from nm to μm
933 std::vector<float> wavelengths_um(spectral_data.wavelengths_nm.size());
934 for (size_t i = 0; i < wavelengths_um.size(); ++i) {
935 wavelengths_um[i] = spectral_data.wavelengths_nm[i] * 0.001f;
936 }
937
938 // Calculate atmospheric scattering transmittances
939 std::vector<float> t_scat_glb, t_scat_dir, t_scat_dif, atm_alb;
940 calculateMixtureTransmittance(wavelengths_um, mu0, pressure, turbidity_beta, angstrom_alpha, aerosol_ssa, aerosol_g, true, t_scat_glb, t_scat_dir, t_scat_dif, atm_alb);
941
942 // Calculate gas absorption transmittances
943 std::vector<float> t_h2o, t_o3, t_o2;
944 calculateWaterVaporTransmittance(spectral_data, mu0, water_vapor_cm, t_h2o);
945 calculateOzoneTransmittance(spectral_data, mu0, ozone_DU, t_o3);
946 calculateOxygenTransmittance(spectral_data, mu0, t_o2);
947
948 // Combine gas transmittances
949 std::vector<float> t_gas(spectral_data.wavelengths_nm.size());
950 for (size_t i = 0; i < t_gas.size(); ++i) {
951 t_gas[i] = t_h2o[i] * t_o3[i] * t_o2[i];
952 }
953
954 // Calculate amplification factor
955 std::vector<float> amp_factor(spectral_data.wavelengths_nm.size());
956 for (size_t i = 0; i < amp_factor.size(); ++i) {
957 amp_factor[i] = 1.0f / (1.0f - surface_albedo * atm_alb[i]);
958 }
959
960 // Calculate spectral irradiances
961 global_spectrum.clear();
962 direct_spectrum.clear();
963 diffuse_spectrum.clear();
964 global_spectrum.reserve(spectral_data.wavelengths_nm.size());
965 direct_spectrum.reserve(spectral_data.wavelengths_nm.size());
966 diffuse_spectrum.reserve(spectral_data.wavelengths_nm.size());
967
968 for (size_t i = 0; i < spectral_data.wavelengths_nm.size(); ++i) {
969 float wavelength_nm = spectral_data.wavelengths_nm[i];
970
971 float direct_irr = toa_irr[i] * t_scat_dir[i] * t_gas[i];
972 float global_irr = toa_irr[i] * mu0 * t_scat_glb[i] * t_gas[i] * amp_factor[i];
973 float diffuse_irr = global_irr - direct_irr * mu0;
974
975 global_spectrum.push_back(helios::make_vec2(wavelength_nm, global_irr));
976 direct_spectrum.push_back(helios::make_vec2(wavelength_nm, direct_irr));
977 diffuse_spectrum.push_back(helios::make_vec2(wavelength_nm, diffuse_irr));
978 }
979
980 // Downsample if requested resolution is coarser than 1 nm
981 if (resolution_nm > 1.0f + 1e-5f) {
982 std::vector<helios::vec2> global_downsampled, direct_downsampled, diffuse_downsampled;
983
984 for (float wl = 300.0f; wl <= 2600.0f; wl += resolution_nm) {
985 // Find closest wavelength in original spectrum
986 float min_dist = std::numeric_limits<float>::max();
987 size_t closest_idx = 0;
988 for (size_t i = 0; i < global_spectrum.size(); ++i) {
989 float dist = std::fabs(global_spectrum[i].x - wl);
990 if (dist < min_dist) {
991 min_dist = dist;
992 closest_idx = i;
993 }
994 }
995
996 global_downsampled.push_back(global_spectrum[closest_idx]);
997 direct_downsampled.push_back(direct_spectrum[closest_idx]);
998 diffuse_downsampled.push_back(diffuse_spectrum[closest_idx]);
999 }
1000
1001 global_spectrum = global_downsampled;
1002 direct_spectrum = direct_downsampled;
1003 diffuse_spectrum = diffuse_downsampled;
1004 }
1005}
1006
1007void SolarPosition::calculateDirectSolarSpectrum(const std::string &label, float resolution_nm) {
1008 std::vector<helios::vec2> global_spectrum, direct_spectrum, diffuse_spectrum;
1009 calculateSpectralIrradianceComponents(global_spectrum, direct_spectrum, diffuse_spectrum, resolution_nm);
1010 context->setGlobalData(label.c_str(), direct_spectrum);
1011}
1012
1013void SolarPosition::calculateDiffuseSolarSpectrum(const std::string &label, float resolution_nm) {
1014 std::vector<helios::vec2> global_spectrum, direct_spectrum, diffuse_spectrum;
1015 calculateSpectralIrradianceComponents(global_spectrum, direct_spectrum, diffuse_spectrum, resolution_nm);
1016 context->setGlobalData(label.c_str(), diffuse_spectrum);
1017}
1018
1019void SolarPosition::calculateGlobalSolarSpectrum(const std::string &label, float resolution_nm) {
1020 std::vector<helios::vec2> global_spectrum, direct_spectrum, diffuse_spectrum;
1021 calculateSpectralIrradianceComponents(global_spectrum, direct_spectrum, diffuse_spectrum, resolution_nm);
1022 context->setGlobalData(label.c_str(), global_spectrum);
1023}
1024
1025// ===== Prague Sky Model Implementation =====
1026
1028 if (prague_enabled) {
1029 std::cerr << "WARNING (SolarPosition::enablePragueSkyModel): Prague model already enabled." << std::endl;
1030 return;
1031 }
1032
1033 prague_model = std::make_unique<helios::PragueSkyModelInterface>();
1034
1035 // Always use the reduced dataset
1036 std::string data_path = "plugins/solarposition/lib/prague_sky_model/PragueSkyModelReduced.dat";
1037
1038 try {
1039 prague_model->initialize(data_path);
1040 prague_enabled = true;
1041 } catch (const std::exception &e) {
1042 helios_runtime_error("ERROR (SolarPosition::enablePragueSkyModel): Failed to initialize Prague Sky Model: " + std::string(e.what()));
1043 }
1044}
1045
1047 return prague_enabled;
1048}
1049
1050void SolarPosition::updatePragueSkyModel(float ground_albedo) {
1051 if (!prague_enabled) {
1052 helios_runtime_error("ERROR (SolarPosition::updatePragueSkyModel): Prague model not enabled. Call enablePragueSkyModel() first.");
1053 }
1054
1055 // Read turbidity from Context atmospheric conditions
1056 float turbidity = 0.02f; // Default: clear sky
1057 if (context->doesGlobalDataExist("atmosphere_turbidity")) {
1058 context->getGlobalData("atmosphere_turbidity", turbidity);
1059 }
1060
1062 float visibility_km = helios::PragueSkyModelInterface::turbidityToVisibility(turbidity);
1063
1064 // Mark data as invalid during computation
1065 context->setGlobalData("prague_sky_valid", 0);
1066
1067 // Wavelength grid: 360-1480 nm at 5nm spacing
1068 const float lambda_min = 360.0f;
1069 const float lambda_max = 1480.0f;
1070 const float lambda_step = 5.0f;
1071 const int n_wavelengths = 225;
1072 const int params_per_wavelength = 6;
1073
1074 // Pre-compute sampling directions ONCE (5-10% speedup)
1075 helios::vec3 zenith_dir(0.0f, 0.0f, 1.0f);
1076
1077 helios::vec3 sun_horizontal = normalize(make_vec3(sun_dir.x, sun_dir.y, 0.0f));
1078 helios::vec3 horizon_dir;
1079 if (sun_horizontal.magnitude() > 0.01f) {
1080 horizon_dir = normalize(make_vec3(-sun_horizontal.y, sun_horizontal.x, 0.0f));
1081 } else {
1082 horizon_dir = make_vec3(1.0f, 0.0f, 0.0f);
1083 }
1084
1085 helios::vec3 near_sun_dir = rotateDirectionTowardZenith(sun_dir, 5.0f * float(M_PI) / 180.0f);
1086 helios::vec3 mid_sun_dir = rotateDirectionTowardZenith(sun_dir, 15.0f * float(M_PI) / 180.0f);
1087 helios::vec3 away_dir = getDirectionAwayFromSun(sun_dir, 45.0f);
1088
1089 // Allocate output: [λ, L_zen, circ_str, circ_width, horiz_bright, norm] × 225
1090 std::vector<float> spectral_params(n_wavelengths * params_per_wavelength);
1091
1092// CRITICAL: OpenMP parallelization over wavelengths
1093// Expected speedup: 6-8× on 8-core CPU
1094#pragma omp parallel for schedule(dynamic, 8)
1095 for (int i = 0; i < n_wavelengths; ++i) {
1096 float wavelength = lambda_min + i * lambda_step;
1097
1098 float L_zenith, circ_str, circ_width, horiz_bright, normalization;
1099 fitAngularParametersAtWavelength(wavelength, visibility_km, ground_albedo, sun_dir, L_zenith, circ_str, circ_width, horiz_bright, normalization);
1100
1101 int base_idx = i * params_per_wavelength;
1102 spectral_params[base_idx + 0] = wavelength;
1103 spectral_params[base_idx + 1] = L_zenith;
1104 spectral_params[base_idx + 2] = circ_str;
1105 spectral_params[base_idx + 3] = circ_width;
1106 spectral_params[base_idx + 4] = horiz_bright;
1107 spectral_params[base_idx + 5] = normalization;
1108 }
1109
1110 // Store in Context
1111 context->setGlobalData("prague_sky_spectral_params", spectral_params);
1112 context->setGlobalData("prague_sky_sun_direction", sun_dir);
1113 context->setGlobalData("prague_sky_visibility_km", visibility_km);
1114 context->setGlobalData("prague_sky_ground_albedo", ground_albedo);
1115 context->setGlobalData("prague_sky_valid", 1);
1116
1117 // Update cache for lazy evaluation
1118 cached_sun_direction = sun_dir;
1119 cached_turbidity = turbidity;
1120 cached_albedo = ground_albedo;
1121}
1122
1123bool SolarPosition::pragueSkyModelNeedsUpdate(float ground_albedo, float sun_tolerance, float turbidity_tolerance, float albedo_tolerance) const {
1124 if (!prague_enabled) {
1125 return false;
1126 }
1127
1128 // Check if this is the first update
1129 if (cached_turbidity < 0.0f) {
1130 return true;
1131 }
1132
1133 // Read current turbidity from Context
1134 float turbidity = 0.02f; // Default: clear sky
1135 if (context->doesGlobalDataExist("atmosphere_turbidity")) {
1136 context->getGlobalData("atmosphere_turbidity", turbidity);
1137 }
1138
1139 // Check sun direction change
1141 float sun_change = (sun_dir - cached_sun_direction).magnitude();
1142 if (sun_change > sun_tolerance) {
1143 return true;
1144 }
1145
1146 // Check turbidity change (relative)
1147 float turb_change = std::abs(turbidity - cached_turbidity) / std::max(cached_turbidity, 0.01f);
1148 if (turb_change > turbidity_tolerance) {
1149 return true;
1150 }
1151
1152 // Check albedo change (absolute)
1153 if (std::abs(ground_albedo - cached_albedo) > albedo_tolerance) {
1154 return true;
1155 }
1156
1157 return false;
1158}
1159
1160void SolarPosition::fitAngularParametersAtWavelength(float wavelength, float visibility_km, float albedo, const helios::vec3 &sun_dir, float &L_zenith, float &circ_str, float &circ_width, float &horiz_bright, float &normalization) {
1161
1162 // Recompute directions (needed inside OpenMP parallel region)
1163 helios::vec3 zenith_dir(0.0f, 0.0f, 1.0f);
1164
1165 helios::vec3 sun_horizontal = normalize(make_vec3(sun_dir.x, sun_dir.y, 0.0f));
1166 helios::vec3 horizon_dir;
1167 if (sun_horizontal.magnitude() > 0.01f) {
1168 horizon_dir = normalize(make_vec3(-sun_horizontal.y, sun_horizontal.x, 0.0f));
1169 } else {
1170 horizon_dir = make_vec3(1.0f, 0.0f, 0.0f);
1171 }
1172
1173 helios::vec3 near_sun_dir = rotateDirectionTowardZenith(sun_dir, 5.0f * float(M_PI) / 180.0f);
1174 helios::vec3 mid_sun_dir = rotateDirectionTowardZenith(sun_dir, 15.0f * float(M_PI) / 180.0f);
1175
1176 // Sample Prague model at 5 key directions
1177 L_zenith = prague_model->getSkyRadiance(zenith_dir, sun_dir, wavelength, visibility_km, albedo);
1178
1179 float L_horizon = prague_model->getSkyRadiance(horizon_dir, sun_dir, wavelength, visibility_km, albedo);
1180
1181 float L_near_sun = prague_model->getSkyRadiance(near_sun_dir, sun_dir, wavelength, visibility_km, albedo);
1182
1183 float L_mid_sun = prague_model->getSkyRadiance(mid_sun_dir, sun_dir, wavelength, visibility_km, albedo);
1184
1185 // Fit horizon brightness
1186 horiz_bright = std::max(1.0f, L_horizon / std::max(L_zenith, 1e-10f));
1187
1188 // Fit circumsolar parameters using two sample points
1189 float cos_theta_near = std::max(0.0f, near_sun_dir.z);
1190 float cos_theta_mid = std::max(0.0f, mid_sun_dir.z);
1191 float h1 = 1.0f + (horiz_bright - 1.0f) * (1.0f - cos_theta_near);
1192 float h2 = 1.0f + (horiz_bright - 1.0f) * (1.0f - cos_theta_mid);
1193
1194 circ_width = fitCircumsolarWidth(L_near_sun, L_mid_sun, h1, h2, 5.0f, 15.0f);
1195 circ_width = std::clamp(circ_width, 5.0f, 60.0f);
1196
1197 float exp5 = std::exp(-5.0f / circ_width);
1198 float exp15 = std::exp(-15.0f / circ_width);
1199
1200 float ratio_near = L_near_sun / std::max(L_zenith * h1, 1e-10f);
1201 float ratio_mid = L_mid_sun / std::max(L_zenith * h2, 1e-10f);
1202
1203 float A_from_near = (ratio_near - 1.0f) / std::max(exp5, 1e-10f);
1204 float A_from_mid = (ratio_mid - 1.0f) / std::max(exp15, 1e-10f);
1205 circ_str = std::max(0.0f, 0.5f * (A_from_near + A_from_mid));
1206 circ_str = std::min(circ_str, 20.0f);
1207
1208 // Compute normalization
1209 normalization = computeAngularNormalization(circ_str, circ_width, horiz_bright);
1210}
1211
1212float SolarPosition::fitCircumsolarWidth(float L1, float L2, float h1, float h2, float gamma1, float gamma2) const {
1213 float best_B = 15.0f; // Default
1214 float best_error = 1e10f;
1215
1216 float target_ratio = (L1 * h2) / std::max(L2 * h1, 1e-10f);
1217
1218 // Grid search over plausible width range
1219 for (float B = 5.0f; B <= 50.0f; B += 1.0f) {
1220 float e1 = std::exp(-gamma1 / B);
1221 float e2 = std::exp(-gamma2 / B);
1222
1223 // For each B, find A that minimizes error
1224 float denom = e1 - target_ratio * e2;
1225 if (std::abs(denom) < 1e-10f)
1226 continue;
1227
1228 float A = (target_ratio - 1.0f) / denom;
1229 if (A < 0)
1230 continue; // Invalid solution
1231
1232 float predicted_ratio = (1.0f + A * e1) / (1.0f + A * e2);
1233 float error = std::abs(predicted_ratio - target_ratio);
1234
1235 if (error < best_error) {
1236 best_error = error;
1237 best_B = B;
1238 }
1239 }
1240
1241 return best_B;
1242}
1243
1244float SolarPosition::computeAngularNormalization(float circ_str, float circ_width, float horiz_bright) const {
1245 // Numerical integration of angular pattern over hemisphere
1246 const int N = 50;
1247 float integral = 0.0f;
1248
1249 for (int j = 0; j < N; ++j) {
1250 for (int i = 0; i < N; ++i) {
1251 float theta = 0.5f * float(M_PI) * (i + 0.5f) / N; // 0 to π/2
1252 float phi = 2.0f * float(M_PI) * (j + 0.5f) / N; // 0 to 2π
1253
1254 helios::vec3 dir = sphere2cart(make_SphericalCoord(0.5f * float(M_PI) - theta, phi));
1255
1256 // Compute angular pattern (without L_zenith, which factors out)
1257 float cos_theta = std::max(0.0f, dir.z);
1258 float horizon_term = 1.0f + (horiz_bright - 1.0f) * (1.0f - cos_theta);
1259
1260 // For normalization, average circumsolar contribution
1261 float circ_term = 1.0f; // Approximation: circumsolar averages out over azimuth
1262
1263 float pattern = circ_term * horizon_term;
1264
1265 // Solid angle element: sin(θ) dθ dφ
1266 integral += pattern * std::cos(theta) * std::sin(theta) * (float(M_PI) / (2.0f * N)) * (2.0f * float(M_PI) / N);
1267 }
1268 }
1269
1270 return 1.0f / std::max(integral, 1e-10f);
1271}
1272
1273helios::vec3 SolarPosition::rotateDirectionTowardZenith(const helios::vec3 &dir, float angle_rad) const {
1274 helios::vec3 zenith(0, 0, 1);
1275 helios::vec3 axis = cross(dir, zenith);
1276
1277 if (axis.magnitude() < 0.01f) {
1278 // dir is nearly parallel to zenith, use arbitrary perpendicular
1279 axis = make_vec3(1, 0, 0);
1280 }
1281
1282 axis = normalize(axis);
1283
1284 // Rodrigues rotation formula
1285 float c = std::cos(angle_rad);
1286 float s = std::sin(angle_rad);
1287
1288 helios::vec3 rotated = dir * c + cross(axis, dir) * s + axis * (axis.x * dir.x + axis.y * dir.y + axis.z * dir.z) * (1 - c);
1289 return normalize(rotated);
1290}
1291
1292helios::vec3 SolarPosition::getDirectionAwayFromSun(const helios::vec3 &sun_dir, float zenith_angle_deg) const {
1293 float theta = zenith_angle_deg * float(M_PI) / 180.0f;
1294
1295 // Find azimuth opposite to sun
1296 float sun_azimuth = std::atan2(sun_dir.y, sun_dir.x);
1297 float opposite_azimuth = sun_azimuth + float(M_PI);
1298
1299 return make_vec3(std::sin(theta) * std::cos(opposite_azimuth), std::sin(theta) * std::sin(opposite_azimuth), std::cos(theta));
1300}