20using namespace helios;
23 context = context_ptr;
31 context = context_ptr;
33 latitude = latitude_deg;
34 longitude = longitude_deg;
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;
40 latitude = latitude_deg;
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;
47 longitude = longitude_deg;
53 int solstice_day, LSTM;
54 float Gamma, delta, time_dec, B, EoT, TC, LST, h, theta, phi, rad;
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);
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));
73 TC = 4.f * (LSTM - longitude) + EoT;
74 LST = time_dec + TC / 60.f;
76 h = (LST - 12.f) * 15.f * rad;
79 theta =
asin_safe(sin(latitude * rad) * sin(delta) + cos(latitude * rad) * cos(delta) * cos(h));
81 assert(theta > -0.5f *
M_PI && theta < 0.5f *
M_PI);
84 phi =
acos_safe((sin(delta) - sin(theta) * sin(latitude * rad)) / (cos(theta) * cos(latitude * rad)));
87 phi = 2.f *
M_PI - phi;
90 assert(phi > 0 && phi < 2.f *
M_PI);
100 for (
uint h = 1; h <= 23 && !found; h++) {
101 for (
uint m = 1; m <= 59 && !found; m++) {
118 for (
int h = 23; h >= 1 && !found; h--) {
119 for (
int m = 59; m >= 1 && !found; m--) {
133 if (issolarpositionoverridden) {
143 if (issolarpositionoverridden) {
144 zenith = sun_direction.
zenith;
153 if (issolarpositionoverridden) {
154 azimuth = sun_direction.
azimuth;
163 if (issolarpositionoverridden) {
164 sundirection = sun_direction;
166 sundirection = calculateSunDirection(context->
getTime(), context->
getDate());
173 if (issolarpositionoverridden) {
174 sundirection = sun_direction;
176 sundirection = calculateSunDirection(context->
getTime(), context->
getDate());
182 issolarpositionoverridden =
true;
183 sun_direction = sundirection;
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);
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);
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);
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);
227void SolarPosition::GueymardSolarModel(
float pressure,
float temperature,
float humidity,
float turbidity,
float &Eb_PAR,
float &Eb_NIR,
float &fdiff)
const {
229 float beta = turbidity;
235 if (theta <= 0.f || theta > 0.5 *
M_PI) {
242 float m = pow(cos(theta) + 0.15 * pow(93.885 - theta * 180 /
M_PI, -1.25), -1);
244 float E0_PAR = 635.4;
245 float E0_NIR = 709.7;
247 vec2 alpha(1.3, 1.3);
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));
253 float mR_p = mR * pressure / 101325;
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);
257 float TR_NIR = (1.f - 0.010394 * mR_p) / (1.f - 0.00011042 * mR_p * mR_p);
260 mR = 1.f / (cos(theta) + 0.48353 * pow(theta * 180 /
M_PI, 0.095846) / pow(96.741 - theta * 180 /
M_PI, 1.754));
262 mR_p = mR * pressure / 101325;
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);
266 float Tg_NIR = (1.f + 0.27284 * mR_p - 0.00063699 * mR_p * mR_p) / (1.f + 0.30306 * mR_p);
268 float BR_PAR = 0.5f * (0.89013 - 0.0049558 * mR + 0.000045721 * mR * mR);
272 float Ba = 1.f - exp(-0.6931 - 1.8326 * cos(theta));
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;
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);
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));
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));
297 float Tn_NIR_p = 1.f;
300 float gamma = log(humidity) + 17.67 * (temperature - 273) / (243 + 25);
301 float tdp = 243 * gamma / (17.67 - gamma) * 9 / 5 + 32;
302 float w = exp((0.1133 - log(4.0 + 1)) + 0.0393 * tdp);
304 mw = 1.f / (cos(theta) + 1.1212 * pow(theta * 180 /
M_PI, 0.6379) / pow(93.781 - theta * 180 /
M_PI, 1.9203));
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);
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);
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);
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));
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));
336 float omega_PAR = 1.0;
337 float omega_NIR = 1.0;
339 float Tas_PAR = exp(-ma * omega_PAR * beta * pow(lambdae_PAR, -alpha.x));
341 float Tas_NIR = exp(-ma * omega_NIR * beta * pow(lambdae_NIR, -alpha.y));
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;
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;
354 fdiff = Edp / (Eb + Edp);
356 assert(fdiff >= 0.f && fdiff <= 1.f);
362 float e0 = 611.f * exp(17.502f * (temperature_K - 273.f) / ((temperature_K - 273.f) + 240.9f)) * humidity_rel;
364 float xi = e0 / temperature_K * K;
365 float eps = 1.f - (1.f + xi) * exp(-sqrt(1.2f + 3.f * xi));
367 return eps * 5.67e-8 * pow(temperature_K, 4);
370float SolarPosition::turbidityResidualFunction(
float turbidity, std::vector<float> ¶meters,
const void *a_solarpositionmodel) {
372 auto *solarpositionmodel =
reinterpret_cast<const SolarPosition *
>(a_solarpositionmodel);
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);
380 float turbidity_clamped = std::max(1e-5f, turbidity);
384 auto *mutable_model =
const_cast<SolarPosition *
>(solarpositionmodel);
386 float flux_model = mutable_model->getSolarFlux() * cosf(mutable_model->getSunZenith());
387 return flux_model - flux_target;
393 helios_runtime_error(
"ERROR (SolarPosition::calibrateTurbidityFromTimeseries): Timeseries variable " + timeseries_shortwave_flux_label_Wm2 +
" does not exist.");
398 float min_flux = 1e6;
400 int max_flux_index = 0;
401 for (
int t = 0; t < length; t++) {
403 if (flux < min_flux) {
406 if (flux > max_flux) {
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.");
418 std::vector<float> parameters{101325, 300, 0.5, max_flux};
420 SolarPosition solarposition_copy(UTC, latitude, longitude, context);
424 solarposition_copy.
setSunDirection(solarposition_copy.calculateSunDirection(time_max, date_max));
427 float turbidity =
fzero(turbidityResidualFunction, parameters, &solarposition_copy, 0.01, 0.0001f, 100, &warnings);
428 warnings.
report(std::cerr);
430 return std::max(1e-4F, turbidity);
436 helios_runtime_error(
"ERROR (SolarPosition::enableCloudCalibration): Timeseries variable " + timeseries_shortwave_flux_label_Wm2 +
" does not exist.");
439 cloudcalibrationlabel = timeseries_shortwave_flux_label_Wm2;
443 cloudcalibrationlabel =
"";
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.");
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.");
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) +
".");
457 if (turbidity < 0.f) {
458 helios_runtime_error(
"ERROR (SolarPosition::setAtmosphericConditions): Turbidity must be non-negative. Got " + std::to_string(turbidity) +
".");
462 context->
setGlobalData(
"atmosphere_pressure_Pa", pressure_Pa);
463 context->
setGlobalData(
"atmosphere_temperature_K", temperature_K);
464 context->
setGlobalData(
"atmosphere_humidity_rel", humidity_rel);
470 const float default_pressure = 101325.f;
471 const float default_temperature = 300.f;
472 const float default_humidity = 0.5f;
473 const float default_turbidity = 0.02f;
475 static bool warning_issued =
false;
482 context->
getGlobalData(
"atmosphere_pressure_Pa", pressure_Pa);
483 context->
getGlobalData(
"atmosphere_temperature_K", temperature_K);
484 context->
getGlobalData(
"atmosphere_humidity_rel", humidity_rel);
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;
492 pressure_Pa = default_pressure;
493 temperature_K = default_temperature;
494 humidity_rel = default_humidity;
495 turbidity = default_turbidity;
500 float pressure, temperature, humidity, turbidity;
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);
513 float pressure, temperature, humidity, turbidity;
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);
525 float pressure, temperature, humidity, turbidity;
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);
537 float pressure, temperature, humidity, turbidity;
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);
549 float pressure, temperature, humidity, turbidity;
553 float e0 = 611.f * exp(17.502f * (temperature - 273.f) / ((temperature - 273.f) + 240.9f)) * humidity;
555 float xi = e0 / temperature * K;
556 float eps = 1.f - (1.f + xi) * exp(-sqrt(1.2f + 3.f * xi));
558 return eps * 5.67e-8 * pow(temperature, 4);
561void SolarPosition::applyCloudCalibration(
float &R_calc_Wm2,
float &fdiff_calc)
const {
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;
571 if (fdiff > 0.001 && R_calc_horiz > 1.f) {
579void SolarPosition::SpectralData::loadFromDirectory(
const std::string &data_path) {
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);
587 wavelengths_nm.clear();
588 toa_irradiance.clear();
591 while (std::getline(wehrli_stream, line)) {
593 if (line.empty() || line[0] ==
'#') {
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);
604 wehrli_stream.close();
606 if (wavelengths_nm.empty()) {
607 helios_runtime_error(
"ERROR (SolarPosition::SpectralData::loadFromDirectory): No data loaded from " + wehrli_file);
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);
622 while (std::getline(abscoef_stream, line)) {
624 if (line.empty() || line[0] ==
'#') {
628 std::istringstream iss(line);
629 float wavelength, h2o_c, h2o_e, o3_x, o2_c;
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);
638 abscoef_stream.close();
640 if (h2o_coef.empty()) {
641 helios_runtime_error(
"ERROR (SolarPosition::SpectralData::loadFromDirectory): No data loaded from " + abscoef_file);
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");
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()));
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()));
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");
664 if (x_val <= x.front()) {
667 if (x_val >= x.back()) {
672 auto it = std::lower_bound(x.begin(), x.end(), x_val);
673 if (it == x.begin()) {
680 size_t idx1 = std::distance(x.begin(), it) - 1;
681 size_t idx2 = idx1 + 1;
689 return y1 + (y2 - y1) * (x_val - x1) / (x2 - x1);
692float SolarPosition::calculateGeometricFactor(
int julian_day)
const {
695 const float c[] = {1.00011f, 0.03422f, 0.00128f, 0.000719f, 0.000077f};
698 float day_angle = (julian_day - 1) * 2.0f *
M_PI / 365.0f;
699 float day_angle_2 = day_angle * 2.0f;
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);
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 {
709 const float c[] = {117.2594f, -1.3215f, 0.000320f, -0.000076f};
712 size_t n = wavelengths_um.size();
716 atm_albedo.resize(n);
718 for (
size_t i = 0; i < n; ++i) {
719 float w = wavelengths_um[i];
724 float tau = pressure_ratio / (c[0] * w4 + c[1] * w2 + c[2] + c[3] / w2);
727 tdir[i] = expf(-tau / mu0);
730 tglb[i] = ((2.0f / 3.0f + mu0) + (2.0f / 3.0f - mu0) * tdir[i]) / (4.0f / 3.0f + tau);
733 tdif[i] = tglb[i] - tdir[i];
736 atm_albedo[i] = tau * (1.0f - expf(-2.0f * tau)) / (2.0f + tau);
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 {
745 size_t n = wavelengths_um.size();
749 atm_albedo.resize(n);
752 float K = sqrtf((1.0f - w0) * (1.0f - w0 * g));
753 float r0 = (K - 1.0f + w0) / (K + 1.0f - w0);
755 for (
size_t i = 0; i < n; ++i) {
756 float w = wavelengths_um[i];
759 float tau = beta / powf(w, alpha);
762 tdir[i] = expf(-tau / mu0);
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);
769 tdif[i] = tglb[i] - tdir[i];
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));
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 {
781 float pressure_ratio = pressure_Pa / 101325.0f;
784 std::vector<float> ray_tdir, ray_tglb, ray_tdif, ray_albedo;
785 std::vector<float> aer_tdir, aer_tglb, aer_tdif, aer_albedo;
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);
791 size_t n = wavelengths_um.size();
795 atm_albedo.resize(n);
798 for (
size_t i = 0; i < n; ++i) {
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]);
805 tglb[i] = ray_tglb[i] * aer_tglb[i] + coup_term;
806 tdir[i] = ray_tdir[i] * aer_tdir[i];
809 tglb[i] = ray_tglb[i] * aer_tglb[i];
810 tdir[i] = ray_tdir[i] * aer_tdir[i];
813 tdif[i] = tglb[i] - tdir[i];
816 atm_albedo[i] = ray_albedo[i] + aer_albedo[i];
820void SolarPosition::calculateWaterVaporTransmittance(
const SpectralData &data,
float mu0,
float water_vapor_cm, std::vector<float> &transmittance)
const {
823 size_t n = data.wavelengths_nm.size();
824 transmittance.resize(n);
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];
830 if (h2o_exp < 1e-6f) {
832 transmittance[i] = 1.0f;
835 float optical_depth = powf(h2o_coef * water_vapor_cm / mu0, h2o_exp);
836 transmittance[i] = expf(-optical_depth);
841void SolarPosition::calculateOzoneTransmittance(
const SpectralData &data,
float mu0,
float ozone_DU, std::vector<float> &transmittance)
const {
843 const float LOSCHMIDT = 2.687e19f;
845 size_t n = data.wavelengths_nm.size();
846 transmittance.resize(n);
848 for (
size_t i = 0; i < n; ++i) {
850 float o3_coef = LOSCHMIDT * data.o3_xsec[i];
853 float o3_path_cm = ozone_DU * 1e-3f;
856 float optical_depth = o3_coef * o3_path_cm / mu0;
857 transmittance[i] = expf(-optical_depth);
861void SolarPosition::calculateOxygenTransmittance(
const SpectralData &data,
float mu0, std::vector<float> &transmittance)
const {
863 const float O2_PATH = 0.209f * 173200.0f;
864 const float O2_EXP = 0.5641f;
866 size_t n = data.wavelengths_nm.size();
867 transmittance.resize(n);
869 for (
size_t i = 0; i < n; ++i) {
870 float o2_coef = data.o2_coef[i];
873 float optical_depth = powf(o2_coef * O2_PATH / mu0, O2_EXP);
874 transmittance[i] = expf(-optical_depth);
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 {
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));
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));
887 float pressure, temperature, humidity, turbidity_beta;
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);
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;
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;
909 static SpectralData spectral_data;
910 static bool data_loaded =
false;
912 spectral_data.loadFromDirectory(
"plugins/solarposition/ssolar_goa");
918 float mu0 = cosf(sun_zenith);
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)");
924 float geo_factor = calculateGeometricFactor(julian_day);
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;
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;
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);
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);
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];
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]);
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());
968 for (
size_t i = 0; i < spectral_data.wavelengths_nm.size(); ++i) {
969 float wavelength_nm = spectral_data.wavelengths_nm[i];
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;
981 if (resolution_nm > 1.0f + 1e-5f) {
982 std::vector<helios::vec2> global_downsampled, direct_downsampled, diffuse_downsampled;
984 for (
float wl = 300.0f; wl <= 2600.0f; wl += resolution_nm) {
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) {
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]);
1001 global_spectrum = global_downsampled;
1002 direct_spectrum = direct_downsampled;
1003 diffuse_spectrum = diffuse_downsampled;
1008 std::vector<helios::vec2> global_spectrum, direct_spectrum, diffuse_spectrum;
1009 calculateSpectralIrradianceComponents(global_spectrum, direct_spectrum, diffuse_spectrum, resolution_nm);
1014 std::vector<helios::vec2> global_spectrum, direct_spectrum, diffuse_spectrum;
1015 calculateSpectralIrradianceComponents(global_spectrum, direct_spectrum, diffuse_spectrum, resolution_nm);
1020 std::vector<helios::vec2> global_spectrum, direct_spectrum, diffuse_spectrum;
1021 calculateSpectralIrradianceComponents(global_spectrum, direct_spectrum, diffuse_spectrum, resolution_nm);
1028 if (prague_enabled) {
1029 std::cerr <<
"WARNING (SolarPosition::enablePragueSkyModel): Prague model already enabled." << std::endl;
1033 prague_model = std::make_unique<helios::PragueSkyModelInterface>();
1036 std::string data_path =
"plugins/solarposition/lib/prague_sky_model/PragueSkyModelReduced.dat";
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()));
1047 return prague_enabled;
1051 if (!prague_enabled) {
1052 helios_runtime_error(
"ERROR (SolarPosition::updatePragueSkyModel): Prague model not enabled. Call enablePragueSkyModel() first.");
1056 float turbidity = 0.02f;
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;
1079 if (sun_horizontal.
magnitude() > 0.01f) {
1080 horizon_dir = normalize(
make_vec3(-sun_horizontal.
y, sun_horizontal.
x, 0.0f));
1082 horizon_dir =
make_vec3(1.0f, 0.0f, 0.0f);
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);
1090 std::vector<float> spectral_params(n_wavelengths * params_per_wavelength);
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;
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);
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;
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);
1118 cached_sun_direction = sun_dir;
1119 cached_turbidity = turbidity;
1120 cached_albedo = ground_albedo;
1124 if (!prague_enabled) {
1129 if (cached_turbidity < 0.0f) {
1134 float turbidity = 0.02f;
1141 float sun_change = (sun_dir - cached_sun_direction).magnitude();
1142 if (sun_change > sun_tolerance) {
1147 float turb_change = std::abs(turbidity - cached_turbidity) / std::max(cached_turbidity, 0.01f);
1148 if (turb_change > turbidity_tolerance) {
1153 if (std::abs(ground_albedo - cached_albedo) > albedo_tolerance) {
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) {
1167 if (sun_horizontal.
magnitude() > 0.01f) {
1168 horizon_dir = normalize(
make_vec3(-sun_horizontal.
y, sun_horizontal.
x, 0.0f));
1170 horizon_dir =
make_vec3(1.0f, 0.0f, 0.0f);
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);
1177 L_zenith = prague_model->getSkyRadiance(zenith_dir, sun_dir, wavelength, visibility_km, albedo);
1179 float L_horizon = prague_model->getSkyRadiance(horizon_dir, sun_dir, wavelength, visibility_km, albedo);
1181 float L_near_sun = prague_model->getSkyRadiance(near_sun_dir, sun_dir, wavelength, visibility_km, albedo);
1183 float L_mid_sun = prague_model->getSkyRadiance(mid_sun_dir, sun_dir, wavelength, visibility_km, albedo);
1186 horiz_bright = std::max(1.0f, L_horizon / std::max(L_zenith, 1e-10f));
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);
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);
1197 float exp5 = std::exp(-5.0f / circ_width);
1198 float exp15 = std::exp(-15.0f / circ_width);
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);
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);
1209 normalization = computeAngularNormalization(circ_str, circ_width, horiz_bright);
1212float SolarPosition::fitCircumsolarWidth(
float L1,
float L2,
float h1,
float h2,
float gamma1,
float gamma2)
const {
1213 float best_B = 15.0f;
1214 float best_error = 1e10f;
1216 float target_ratio = (L1 * h2) / std::max(L2 * h1, 1e-10f);
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);
1224 float denom = e1 - target_ratio * e2;
1225 if (std::abs(denom) < 1e-10f)
1228 float A = (target_ratio - 1.0f) / denom;
1232 float predicted_ratio = (1.0f + A * e1) / (1.0f + A * e2);
1233 float error = std::abs(predicted_ratio - target_ratio);
1235 if (error < best_error) {
1244float SolarPosition::computeAngularNormalization(
float circ_str,
float circ_width,
float horiz_bright)
const {
1247 float integral = 0.0f;
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;
1252 float phi = 2.0f * float(
M_PI) * (j + 0.5f) / N;
1257 float cos_theta = std::max(0.0f, dir.
z);
1258 float horizon_term = 1.0f + (horiz_bright - 1.0f) * (1.0f - cos_theta);
1261 float circ_term = 1.0f;
1263 float pattern = circ_term * horizon_term;
1266 integral += pattern * std::cos(theta) * std::sin(theta) * (float(
M_PI) / (2.0f * N)) * (2.0f * float(
M_PI) / N);
1270 return 1.0f / std::max(integral, 1e-10f);
1282 axis = normalize(axis);
1285 float c = std::cos(angle_rad);
1286 float s = std::sin(angle_rad);
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);
1293 float theta = zenith_angle_deg * float(
M_PI) / 180.0f;
1296 float sun_azimuth = std::atan2(sun_dir.
y, sun_dir.
x);
1297 float opposite_azimuth = sun_azimuth + float(
M_PI);
1299 return make_vec3(std::sin(theta) * std::cos(opposite_azimuth), std::sin(theta) * std::sin(opposite_azimuth), std::cos(theta));