1.3.49
 
Loading...
Searching...
No Matches
SolarPosition.cpp
Go to the documentation of this file.
1
16#include "SolarPosition.h"
17
18using namespace std;
19using namespace helios;
20
22 context = context_ptr;
23 UTC = context->getLocation().UTC_offset;
24 latitude = context->getLocation().latitude_deg;
25 longitude = context->getLocation().longitude_deg;
26}
27
28
29SolarPosition::SolarPosition(float UTC_hrs, float latitude_deg, float longitude_deg, helios::Context *context_ptr) {
30 context = context_ptr;
31 UTC = UTC_hrs;
32 latitude = latitude_deg;
33 longitude = longitude_deg;
34
35 if (latitude_deg < -90 || latitude_deg > 90) {
36 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;
37 latitude = 38.55;
38 } else {
39 latitude = latitude_deg;
40 }
41
42 if (longitude < -180 || longitude > 180) {
43 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;
44 longitude = 121.76;
45 } else {
46 longitude = longitude_deg;
47 }
48}
49
50SphericalCoord SolarPosition::calculateSunDirection(const helios::Time &time, const helios::Date &date) const {
51
52 int solstice_day, LSTM;
53 float Gamma, delta, time_dec, B, EoT, TC, LST, h, theta, phi, rad;
54
55 rad = M_PI / 180.f;
56
57 solstice_day = 81;
58
59 // day angle (Iqbal Eq. 1.1.2)
60 Gamma = 2.f * M_PI * (float(date.JulianDay() - 1)) / 365.f;
61
62 // solar declination angle (Iqbal Eq. 1.3.1 after Spencer)
63 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);
64
65 // equation of time (Iqbal Eq. 1.4.1 after Spencer)
66 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));
67
68 time_dec = time.hour + time.minute / 60.f; //(hours)
69
70 LSTM = 15.f * UTC; // degrees
71
72 TC = 4.f * (LSTM - longitude) + EoT; // minutes
73 LST = time_dec + TC / 60.f; // hours
74
75 h = (LST - 12.f) * 15.f * rad; // hour angle (rad)
76
77 // solar zenith angle
78 theta = asin_safe(sin(latitude * rad) * sin(delta) + cos(latitude * rad) * cos(delta) * cos(h)); //(rad)
79
80 assert(theta > -0.5f * M_PI && theta < 0.5f * M_PI);
81
82 // solar elevation angle
83 phi = acos_safe((sin(delta) - sin(theta) * sin(latitude * rad)) / (cos(theta) * cos(latitude * rad)));
84
85 if (LST > 12.f) {
86 phi = 2.f * M_PI - phi;
87 }
88
89 assert(phi > 0 && phi < 2.f * M_PI);
90
91 return make_SphericalCoord(theta, phi);
92}
93
95
96 Time result = make_Time(0, 0); // default/fallback value
97 bool found = false;
98
99 for (uint h = 1; h <= 23 && !found; h++) {
100 for (uint m = 1; m <= 59 && !found; m++) {
101 SphericalCoord sun_dir = calculateSunDirection(make_Time(h, m, 0), context->getDate());
102 if (sun_dir.elevation > 0) {
103 result = make_Time(h, m);
104 found = true;
105 }
106 }
107 }
108
109 return result;
110}
111
113
114 Time result = make_Time(0, 0); // default/fallback value
115 bool found = false;
116
117 for (int h = 23; h >= 1 && !found; h--) {
118 for (int m = 59; m >= 1 && !found; m--) {
119 SphericalCoord sun_dir = calculateSunDirection(make_Time(h, m, 0), context->getDate());
120 if (sun_dir.elevation > 0) {
121 result = make_Time(h, m);
122 found = true;
123 }
124 }
125 }
126
127 return result;
128}
129
131 float elevation;
132 if (issolarpositionoverridden) {
133 elevation = sun_direction.elevation;
134 } else {
135 elevation = calculateSunDirection(context->getTime(), context->getDate()).elevation;
136 }
137 return elevation;
138}
139
141 float zenith;
142 if (issolarpositionoverridden) {
143 zenith = sun_direction.zenith;
144 } else {
145 zenith = calculateSunDirection(context->getTime(), context->getDate()).zenith;
146 }
147 return zenith;
148}
149
151 float azimuth;
152 if (issolarpositionoverridden) {
153 azimuth = sun_direction.azimuth;
154 } else {
155 azimuth = calculateSunDirection(context->getTime(), context->getDate()).azimuth;
156 }
157 return azimuth;
158}
159
161 SphericalCoord sundirection;
162 if (issolarpositionoverridden) {
163 sundirection = sun_direction;
164 } else {
165 sundirection = calculateSunDirection(context->getTime(), context->getDate());
166 }
167 return sphere2cart(sundirection);
168}
169
171 SphericalCoord sundirection;
172 if (issolarpositionoverridden) {
173 sundirection = sun_direction;
174 } else {
175 sundirection = calculateSunDirection(context->getTime(), context->getDate());
176 }
177 return sundirection;
178}
179
181 issolarpositionoverridden = true;
182 sun_direction = sundirection;
183}
184
185float SolarPosition::getSolarFlux(float pressure_Pa, float temperature_K, float humidity_rel, float turbidity) const {
186 float Eb_PAR, Eb_NIR, fdiff;
187 GueymardSolarModel(pressure_Pa, temperature_K, humidity_rel, turbidity, Eb_PAR, Eb_NIR, fdiff);
188 float Eb = Eb_PAR + Eb_NIR;
189 if (!cloudcalibrationlabel.empty()) {
190 applyCloudCalibration(Eb, fdiff);
191 }
192 return Eb;
193}
194
195float SolarPosition::getSolarFluxPAR(float pressure_Pa, float temperature_K, float humidity_rel, float turbidity) const {
196 float Eb_PAR, Eb_NIR, fdiff;
197 GueymardSolarModel(pressure_Pa, temperature_K, humidity_rel, turbidity, Eb_PAR, Eb_NIR, fdiff);
198 if (!cloudcalibrationlabel.empty()) {
199 applyCloudCalibration(Eb_PAR, fdiff);
200 }
201 return Eb_PAR;
202}
203
204float SolarPosition::getSolarFluxNIR(float pressure_Pa, float temperature_K, float humidity_rel, float turbidity) const {
205 float Eb_PAR, Eb_NIR, fdiff;
206 GueymardSolarModel(pressure_Pa, temperature_K, humidity_rel, turbidity, Eb_PAR, Eb_NIR, fdiff);
207 if (!cloudcalibrationlabel.empty()) {
208 applyCloudCalibration(Eb_NIR, fdiff);
209 }
210 return Eb_NIR;
211}
212
213float SolarPosition::getDiffuseFraction(float pressure_Pa, float temperature_K, float humidity_rel, float turbidity) const {
214 float Eb_PAR, Eb_NIR, fdiff;
215 GueymardSolarModel(pressure_Pa, temperature_K, humidity_rel, turbidity, Eb_PAR, Eb_NIR, fdiff);
216 if (!cloudcalibrationlabel.empty()) {
217 applyCloudCalibration(Eb_PAR, fdiff);
218 }
219 return fdiff;
220}
221
222void SolarPosition::GueymardSolarModel(float pressure, float temperature, float humidity, float turbidity, float &Eb_PAR, float &Eb_NIR, float &fdiff) const {
223
224 float beta = turbidity;
225
226 uint DOY = context->getJulianDate();
227
228 float theta = getSunZenith();
229
230 if (theta <= 0.f || theta > 0.5 * M_PI) {
231 Eb_PAR = 0.f;
232 Eb_NIR = 0.f;
233 fdiff = 1.f;
234 return;
235 }
236
237 float m = pow(cos(theta) + 0.15 * pow(93.885 - theta * 180 / M_PI, -1.25), -1);
238
239 float E0_PAR = 635.4;
240 float E0_NIR = 709.7;
241
242 vec2 alpha(1.3, 1.3);
243
244 //---- Rayleigh ----//
245 // NOTE: Rayleigh scattering dominates the atmospheric attenuation, and thus variations in the model predictions are almost entirely due to pressure (and theta)
246 float mR = 1.f / (cos(theta) + 0.48353 * pow(theta * 180 / M_PI, 0.095846) / pow(96.741 - theta * 180 / M_PI, 1.754));
247
248 float mR_p = mR * pressure / 101325;
249
250 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);
251
252 float TR_NIR = (1.f - 0.010394 * mR_p) / (1.f - 0.00011042 * mR_p * mR_p);
253
254 //---- Uniform gasses ----//
255 mR = 1.f / (cos(theta) + 0.48353 * pow(theta * 180 / M_PI, 0.095846) / pow(96.741 - theta * 180 / M_PI, 1.754));
256
257 mR_p = mR * pressure / 101325;
258
259 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);
260
261 float Tg_NIR = (1.f + 0.27284 * mR_p - 0.00063699 * mR_p * mR_p) / (1.f + 0.30306 * mR_p);
262
263 float BR_PAR = 0.5f * (0.89013 - 0.0049558 * mR + 0.000045721 * mR * mR);
264
265 float BR_NIR = 0.5;
266
267 float Ba = 1.f - exp(-0.6931 - 1.8326 * cos(theta));
268
269 //---- Ozone -----//
270 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
271 // NOTE: uo model from van Heuklon (1979)
272 float mo = m;
273
274 float f1 = uo * (10.979 - 8.5421 * uo) / (1.f + 2.0115 * uo + 40.189 * uo * uo);
275 float f2 = uo * (-0.027589 - 0.005138 * uo) / (1.f - 2.4857 * uo + 13.942 * uo * uo);
276 float f3 = uo * (10.995 - 5.5001 * uo) / (1.f + 1.678 * uo + 42.406 * uo * uo);
277 float To_PAR = (1.f + f1 * mo + f2 * mo * mo) / (1.f + f3 * mo);
278
279 float To_NIR = 1.f;
280
281 //---- Nitrogen ---- //
282 float un = 0.0002; // N atm-cm
283 float mw = 1.f / (cos(theta) + 1.065 * pow(theta * 180 / M_PI, 1.6132) / pow(111.55 - theta * 180 / M_PI, 3.2629));
284
285 float g1 = (0.17499 + 41.654 * un - 2146.4 * un * un) / (1 + 22295 * un * un);
286 float g2 = un * (-1.2134 + 59.324 * un) / (1.f + 8847.8 * un * un);
287 float g3 = (0.17499 + 61.658 * un + 9196.4 * un * un) / (1.f + 74109 * un * un);
288 float Tn_PAR = fmin(1.f, (1.f + g1 * mw + g2 * mw * mw) / (1.f + g3 * mw));
289 float Tn_PAR_p = fmin(1.f, (1.f + g1 * 1.66 + g2 * 1.66 * 1.66) / (1.f + g3 * 1.66));
290
291 float Tn_NIR = 1.f;
292 float Tn_NIR_p = 1.f;
293
294 //---- Water -----//
295 float gamma = log(humidity) + 17.67 * (temperature - 273) / (243 + 25);
296 float tdp = 243 * gamma / (17.67 - gamma) * 9 / 5 + 32; // dewpoint temperature in Fahrenheit
297 float w = exp((0.1133 - log(4.0 + 1)) + 0.0393 * tdp); // cm of precipitable water
298 // NOTE: precipitable water model from Viswanadham (1981), Eq. 5
299 mw = 1.f / (cos(theta) + 1.1212 * pow(theta * 180 / M_PI, 0.6379) / pow(93.781 - theta * 180 / M_PI, 1.9203));
300
301 float h1 = w * (0.065445 + 0.00029901 * w) / (1.f + 1.2728 * w);
302 float h2 = w * (0.065687 + 0.0013218 * w) / (1.f + 1.2008 * w);
303 float Tw_PAR = (1.f + h1 * mw) / (1.f + h2 * mw);
304 float Tw_PAR_p = (1.f + h1 * 1.66) / (1.f + h2 * 1.66);
305
306 float c1 = w * (19.566 - 1.6506 * w + 1.0672 * w * w) / (1.f + 5.4248 * w + 1.6005 * w * w);
307 float c2 = w * (0.50158 - 0.14732 * w + 0.047584 * w * w) / (1.f + 1.1811 * w + 1.0699 * w * w);
308 float c3 = w * (21.286 - 0.39232 * w + 1.2692 * w * w) / (1.f + 4.8318 * w + 1.412 * w * w);
309 float c4 = w * (0.70992 - 0.23155 * w + 0.096541 * w * w) / (1.f + 0.44907 * w + 0.75425 * w * w);
310 float Tw_NIR = (1.f + c1 * mw + c2 * mw * mw) / (1.f + c3 * mw + c4 * mw * mw);
311 float Tw_NIR_p = (1.f + c1 * 1.66 + c2 * 1.66 * 1.66) / (1.f + c3 * 1.66 + c4 * 1.66 * 1.66);
312
313 //---- Aerosol ----//
314 float ma = 1.f / (cos(theta) + 0.16851 * pow(theta * 180 / M_PI, 0.18198) / pow(95.318 - theta * 180 / M_PI, 1.9542));
315 float ua = log(1.f + ma * beta);
316
317 float d0 = 0.57664 - 0.024743 * alpha.x;
318 float d1 = (0.093942 - 0.2269 * alpha.x + 0.12848 * alpha.x * alpha.x) / (1.f + 0.6418 * alpha.x);
319 float d2 = (-0.093819 + 0.36668 * alpha.x - 0.12775 * alpha.x * alpha.x) / (1.f - 0.11651 * alpha.x);
320 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);
321 float lambdae_PAR = (d0 + d1 * ua + d2 * ua * ua) / (1.f + d3 * ua * ua);
322 float Ta_PAR = exp(-ma * beta * pow(lambdae_PAR, -alpha.x));
323
324 float e0 = (1.183 - 0.022989 * alpha.y + 0.020829 * alpha.y * alpha.y) / (1.f + 0.11133 * alpha.y);
325 float e1 = (-0.50003 - 0.18329 * alpha.y + 0.23835 * alpha.y * alpha.y) / (1.f + 1.6756 * alpha.y);
326 float e2 = (-0.50001 + 1.1414 * alpha.y + 0.0083589 * alpha.y * alpha.y) / (1.f + 11.168 * alpha.y);
327 float e3 = (-0.70003 - 0.73587 * alpha.y + 0.51509 * alpha.y * alpha.y) / (1.f + 4.7665 * alpha.y);
328 float lambdae_NIR = (e0 + e1 * ua + e2 * ua * ua) / (1.f + e3 * ua);
329 float Ta_NIR = exp(-ma * beta * pow(lambdae_NIR, -alpha.y));
330
331 float omega_PAR = 1.0;
332 float omega_NIR = 1.0;
333
334 float Tas_PAR = exp(-ma * omega_PAR * beta * pow(lambdae_PAR, -alpha.x));
335
336 float Tas_NIR = exp(-ma * omega_NIR * beta * pow(lambdae_NIR, -alpha.y));
337
338 // direct irradiation
339 Eb_PAR = TR_PAR * Tg_PAR * To_PAR * Tn_PAR * Tw_PAR * Ta_PAR * E0_PAR;
340 Eb_NIR = TR_NIR * Tg_NIR * To_NIR * Tn_NIR * Tw_NIR * Ta_NIR * E0_NIR;
341 float Eb = Eb_PAR + Eb_NIR;
342
343 // diffuse irradiation
344 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;
345 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;
346 float Edp = Edp_PAR + Edp_NIR;
347
348 // diffuse fraction
349 fdiff = Edp / (Eb + Edp);
350
351 assert(fdiff >= 0.f && fdiff <= 1.f);
352}
353
354float SolarPosition::getAmbientLongwaveFlux(float temperature_K, float humidity_rel) const {
355
356 // Model from Prata (1996) Q. J. R. Meteorol. Soc.
357
358 float e0 = 611.f * exp(17.502f * (temperature_K - 273.f) / ((temperature_K - 273.f) + 240.9f)) * humidity_rel; // Pascals
359
360 float K = 0.465f; // cm-K/Pa
361
362 float xi = e0 / temperature_K * K;
363 float eps = 1.f - (1.f + xi) * exp(-sqrt(1.2f + 3.f * xi));
364
365 return eps * 5.67e-8 * pow(temperature_K, 4);
366}
367
368float SolarPosition::turbidityResidualFunction(float turbidity, std::vector<float> &parameters, const void *a_solarpositionmodel) {
369
370 auto *solarpositionmodel = reinterpret_cast<const SolarPosition *>(a_solarpositionmodel);
371
372 float pressure = parameters.at(0);
373 float temperature = parameters.at(1);
374 float humidity = parameters.at(2);
375 float flux_target = parameters.at(3);
376
377 float flux_model = solarpositionmodel->getSolarFlux(pressure, temperature, humidity, turbidity) * cosf(solarpositionmodel->getSunZenith());
378 return flux_model - flux_target;
379}
380
381float SolarPosition::calibrateTurbidityFromTimeseries(const std::string &timeseries_shortwave_flux_label_Wm2) const {
382
383 if (!context->doesTimeseriesVariableExist(timeseries_shortwave_flux_label_Wm2.c_str())) {
384 helios_runtime_error("ERROR (SolarPosition::calibrateTurbidityFromTimeseries): Timeseries variable " + timeseries_shortwave_flux_label_Wm2 + " does not exist.");
385 }
386
387 uint length = context->getTimeseriesLength(timeseries_shortwave_flux_label_Wm2.c_str());
388
389 float min_flux = 1e6;
390 float max_flux = 0;
391 int max_flux_index = 0;
392 for (int t = 0; t < length; t++) {
393 float flux = context->queryTimeseriesData(timeseries_shortwave_flux_label_Wm2.c_str(), t);
394 if (flux < min_flux) {
395 min_flux = flux;
396 }
397 if (flux > max_flux) {
398 max_flux = flux;
399 max_flux_index = t;
400 }
401 }
402
403 if (max_flux < 750 || max_flux > 1200) {
404 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");
405 } else if (min_flux < 0) {
406 helios_runtime_error("ERROR (SolarPosition::calibrateTurbidityFromTimeseries): The minimum flux for the timeseries data is negative. Solar fluxes cannot be negative.");
407 }
408
409 std::vector<float> parameters{101325, 300, 0.5, max_flux};
410
411 SolarPosition solarposition_copy(UTC, latitude, longitude, context);
412 Date date_max = context->queryTimeseriesDate(timeseries_shortwave_flux_label_Wm2.c_str(), max_flux_index);
413 Time time_max = context->queryTimeseriesTime(timeseries_shortwave_flux_label_Wm2.c_str(), max_flux_index);
414
415 solarposition_copy.setSunDirection(solarposition_copy.calculateSunDirection(time_max, date_max));
416
417 float turbidity = fzero(turbidityResidualFunction, parameters, &solarposition_copy, 0.01);
418
419 return std::max(1e-4F, turbidity);
420}
421
422void SolarPosition::enableCloudCalibration(const std::string &timeseries_shortwave_flux_label_Wm2) {
423
424 if (!context->doesTimeseriesVariableExist(timeseries_shortwave_flux_label_Wm2.c_str())) {
425 helios_runtime_error("ERROR (SolarPosition::enableCloudCalibration): Timeseries variable " + timeseries_shortwave_flux_label_Wm2 + " does not exist.");
426 }
427
428 cloudcalibrationlabel = timeseries_shortwave_flux_label_Wm2;
429}
430
432 cloudcalibrationlabel = "";
433}
434
435void SolarPosition::applyCloudCalibration(float &R_calc_Wm2, float &fdiff_calc) const {
436
437 assert(context->doesTimeseriesVariableExist(cloudcalibrationlabel.c_str()));
438
439 float R_meas = context->queryTimeseriesData(cloudcalibrationlabel.c_str());
440 float R_calc_horiz = R_calc_Wm2 * cosf(getSunZenith());
441
442 float fdiff = fmin(fmax(0, 1.f - (R_meas - R_calc_horiz) / (R_calc_horiz)), 1);
443 float R = R_calc_Wm2 * R_meas / R_calc_horiz;
444
445 if (fdiff > 0.001 && R_calc_horiz > 1.f) {
446 R_calc_Wm2 = R;
447 fdiff_calc = fdiff;
448 }
449}