• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

NREL / SolTrace / 19380488106

14 Nov 2025 11:30PM UTC coverage: 88.752% (-1.3%) from 90.052%
19380488106

push

github

web-flow
Sun position calcs (#83)

* adding solar position calculators

* adding initial sun position tests

* Remove unused solar calculator utility modules

Deleted lib_pv_incidence_modifier, lib_util, and lib_weatherfile modules from solar_position_calculators, and updated CMakeLists and includes accordingly. These files are no longer required for current solar position calculations (SPA).

* Add SolarPositionCalculator class and tests

Introduces SolarPositionCalculator with multiple calculation methods (LEGACY, DUFFIE, SOLPOS, SPA_ORIGINAL, SPA), input validation, and output retrieval. Updates CMakeLists and headers for integration, refactors sun position logic, and adds comprehensive unit tests for validation and cross-method consistency.

* fixing some compiler errors and warnings

* Guard M_PI definition with preprocessor check

Added a preprocessor guard to define M_PI only if it is not already defined, preventing potential redefinition errors. DTOR and RTOD constants are now always defined without constexpr.

* Update google-tests/unit-tests/simulation_data/sun_position_test.cpp

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* addressing PR comments

* Update sun_position_test.cpp

updating test value

* Update expected sun vector values in test

Adjusted the expected_sun_x value and relaxed the precision for sun_x comparison in the CrossValidationTest to reflect updated calculation results.

* minor changes

* Initialize elevation in SunPositionCalculator

* Update solar_position_calculator.cpp

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Taylor Brown <60201147+taylorbrown75@users.noreply.github.com>

996 of 1195 new or added lines in 4 files covered. (83.35%)

5492 of 6188 relevant lines covered (88.75%)

8572678.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.76
/coretrace/simulation_data/solar_position_calculators/lib_irradproc.cpp
1
/*
2
BSD 3-Clause License
3

4
Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NREL/ssc/blob/develop/LICENSE
5
All rights reserved.
6

7
Redistribution and use in source and binary forms, with or without
8
modification, are permitted provided that the following conditions are met:
9

10
1. Redistributions of source code must retain the above copyright notice, this
11
   list of conditions and the following disclaimer.
12

13
2. Redistributions in binary form must reproduce the above copyright notice,
14
   this list of conditions and the following disclaimer in the documentation
15
   and/or other materials provided with the distribution.
16

17
3. Neither the name of the copyright holder nor the names of its
18
   contributors may be used to endorse or promote products derived from
19
   this software without specific prior written permission.
20

21
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
24
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
25
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
27
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
29
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
*/
32

33

34
#include <iomanip>
35
#include <iostream>
36
#include <limits>
37
#include <math.h>
38
#include <stdio.h>
39
#include <stdlib.h>
40
#include <string.h>
41
#include <vector>
42
#include <numeric>
43
#include <assert.h>
44

45
#include "constants.hpp"
46

47
//#include "lib_util.h"
48
#include "lib_irradproc.h"
49
//#include "lib_pv_incidence_modifier.h"
50
//#include "lib_weatherfile.h"
51

52
#ifndef M_PI
53
    const double M_PI = SolTrace::Data::PI;
54
#endif // !M_PI
55

56
const double DTOR = SolTrace::Data::D2R;
57
const double RTOD = SolTrace::Data::R2D;
58

59

60
static const int __nday[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
61
/// Compute the Julian day of year
62
static int julian(int yr, int month, int day) {
554✔
63
    int i = 1, jday = 0, k;
554✔
64

65
    if (yr % 4 == 0)                      /* For leap years */
554✔
66
        k = 1;
540✔
67
    else
68
        k = 0;
14✔
69

70
    while (i < month) {
3,870✔
71
        jday = jday + __nday[i - 1];
3,316✔
72
        i++;
3,316✔
73
    }
74
    if (month > 2)
554✔
75
        jday = jday + k + day;
554✔
76
    else
NEW
77
        jday = jday + day;
×
78
    return (jday);
554✔
79
}
80

81
/// Compute the day of year
82
int day_of_year(int month, int day_of_month) /* returns 1-365 */
1,647✔
83
{
84
    int i = 1, iday = 0;
1,647✔
85

86
    while (i < month)
11,510✔
87
        iday += __nday[i++ - 1];
9,863✔
88

89
    return iday + day_of_month;
1,647✔
90
}
91

92
void
93
solarpos(int year, int month, int day, int hour, double minute, double lat, double lng, double tz, double sunn[9]) {
554✔
94
    /*
95
        Revised 5/15/98. Replaced algorithm for solar azimuth with one by Iqbal
96
        so latitudes below the equator are correctly handled. Also put in checks
97
        to allow an elevation of 90 degrees without crashing the program and prevented
98
        elevation from exceeding 90 degrees after refraction correction.
99
     */
100

101
    int jday, delta, leap;                           /* Local variables */
102
    double zulu, jd, time, mnlong, mnanom,
103
            eclong, oblqec, num, den, ra, dec, gmst, lmst, ha, elv, azm, refrac,
104
            E, ws, sunrise, sunset, Eo, tst;
105
    double arg, hextra, Gon, zen;
106

107
    jday = julian(year, month, day);    /* Get julian day of year */
554✔
108
    zulu = hour + minute / 60.0 - tz;      /* Convert local time to zulu time */
554✔
109
    if (zulu < 0.0)                     /* Force time between 0-24 hrs */
554✔
110
    {                                 /* Adjust julian day if needed */
111
        zulu = zulu + 24.0;
38✔
112
        jday = jday - 1;
38✔
113
    }
114
    else if (zulu > 24.0) {
516✔
NEW
115
        zulu = zulu - 24.0;
×
NEW
116
        jday = jday + 1;
×
117
    }
118
    delta = year - 1949;
554✔
119
    leap = delta / 4;
554✔
120
    jd = 32916.5 + delta * 365 + leap + jday + zulu / 24.0;
554✔
121
    time = jd - 51545.0;     /* Time in days referenced from noon 1 Jan 2000 */
554✔
122

123
    mnlong = 280.46 + 0.9856474 * time;
554✔
124
    mnlong = fmod((double) mnlong, 360.0);         /* Finds doubleing point remainder */
554✔
125
    if (mnlong < 0.0)
554✔
NEW
126
        mnlong = mnlong + 360.0;          /* Mean longitude between 0-360 deg */
×
127

128
    mnanom = 357.528 + 0.9856003 * time;
554✔
129
    mnanom = fmod((double) mnanom, 360.0);
554✔
130
    if (mnanom < 0.0)
554✔
NEW
131
        mnanom = mnanom + 360.0;
×
132
    mnanom = mnanom * DTOR;             /* Mean anomaly between 0-2pi radians */
554✔
133

134
    eclong = mnlong + 1.915 * sin(mnanom) + 0.020 * sin(2.0 * mnanom);
554✔
135
    eclong = fmod((double) eclong, 360.0);
554✔
136
    if (eclong < 0.0)
554✔
NEW
137
        eclong = eclong + 360.0;
×
138
    eclong = eclong * DTOR;       /* Ecliptic longitude between 0-2pi radians */
554✔
139

140
    oblqec = (23.439 - 0.0000004 * time) * DTOR;   /* Obliquity of ecliptic in radians */
554✔
141
    num = cos(oblqec) * sin(eclong);
554✔
142
    den = cos(eclong);
554✔
143
    ra = atan(num / den);                         /* Right ascension in radians */
554✔
144
    if (den < 0.0)
554✔
145
        ra = ra + M_PI;
326✔
146
    else if (num < 0.0)
228✔
147
        ra = ra + 2.0 * M_PI;
90✔
148

149
    dec = asin(sin(oblqec) * sin(eclong));       /* Declination in radians */
554✔
150

151
    gmst = 6.697375 + 0.0657098242 * time + zulu;
554✔
152
    gmst = fmod((double) gmst, 24.0);
554✔
153
    if (gmst < 0.0)
554✔
NEW
154
        gmst = gmst + 24.0;         /* Greenwich mean sidereal time in hours */
×
155

156
    lmst = gmst + lng / 15.0;
554✔
157
    lmst = fmod((double) lmst, 24.0);
554✔
158
    if (lmst < 0.0)
554✔
159
        lmst = lmst + 24.0;
37✔
160
    lmst = lmst * 15.0 * DTOR;         /* Local mean sidereal time in radians */
554✔
161

162
    ha = lmst - ra;
554✔
163
    if (ha < -M_PI)
554✔
164
        ha = ha + 2 * M_PI;
33✔
165
    else if (ha > M_PI)
521✔
166
        ha = ha - 2 * M_PI;             /* Hour angle in radians between -pi and pi */
18✔
167

168
    lat = lat * DTOR;                /* Change latitude to radians */
554✔
169

170
    arg = sin(dec) * sin(lat) + cos(dec) * cos(lat) * cos(ha);  /* For elevation in radians */
554✔
171
    if (arg > 1.0)
554✔
NEW
172
        elv = M_PI / 2.0;
×
173
    else if (arg < -1.0)
554✔
NEW
174
        elv = -M_PI / 2.0;
×
175
    else
176
        elv = asin(arg);
554✔
177

178
    if (cos(elv) == 0.0) {
554✔
NEW
179
        azm = M_PI;         /* Assign azimuth = 180 deg if elv = 90 or -90 */
×
180
    }
181
    else {                 /* For solar azimuth in radians per Iqbal */
182
        arg = ((sin(elv) * sin(lat) - sin(dec)) / (cos(elv) * cos(lat))); /* for azimuth */
554✔
183
        if (arg > 1.0)
554✔
NEW
184
            azm = 0.0;              /* Azimuth(radians)*/
×
185
        else if (arg < -1.0)
554✔
NEW
186
            azm = M_PI;
×
187
        else
188
            azm = acos(arg);
554✔
189

190
        if ((ha <= 0.0 && ha >= -M_PI) || ha >= M_PI)
554✔
191
            azm = M_PI - azm;
242✔
192
        else
193
            azm = M_PI + azm;
312✔
194
    }
195

196
    elv = elv / DTOR;          /* Change to degrees for atmospheric correction */
554✔
197
    if (elv > -0.56)
554✔
198
        refrac = 3.51561 * (0.1594 + 0.0196 * elv + 0.00002 * elv * elv) / (1.0 + 0.505 * elv + 0.0845 * elv * elv);
549✔
199
    else
200
        refrac = 0.56;
5✔
201
    if (elv + refrac > 90.0)
554✔
NEW
202
        elv = 90.0 * DTOR;
×
203
    else
204
        elv = (elv + refrac) * DTOR; /* Atmospheric corrected elevation(radians) */
554✔
205

206
    E = (mnlong - ra / DTOR) / 15.0;       /* Equation of time in hours */
554✔
207
    if (E < -0.33)   /* Adjust for error occuring if mnlong and ra are in quadrants I and IV */
554✔
NEW
208
        E = E + 24.0;
×
209
    else if (E > 0.33)
554✔
NEW
210
        E = E - 24.0;
×
211

212
    arg = -tan(lat) * tan(dec);
554✔
213
    if (arg >= 1.0)  // No sunrise, continuous nights
554✔
214
    {
NEW
215
        ws = 0.0;
×
NEW
216
        sunrise = 100.0; //make sunrise and sunset sufficiently large that even if they get rolled by 24 hours, they're still out of the bounds 0-24
×
NEW
217
        sunset = -100.0;
×
218
    }
219
    else if (arg <= -1.0) // No sunset, continuous days
554✔
220
    {
221
        ws = M_PI;
2✔
222
        sunrise = -100.0; //make sunrise and sunset sufficiently large that even if they get rolled by 24 hours, they're still out of the bounds 0-24
2✔
223
        sunset = 100.0;
2✔
224
    }
225
    else {
226
        ws = acos(arg); // Sunrise hour angle in radians
552✔
227
        // Sunrise and sunset in local standard time
228
        sunrise = 12.0 - (ws / DTOR) / 15.0 - (lng / 15.0 - tz) - E; //sunrise in units of hours (e.g. 5.25 = 5:15 am)
552✔
229
        sunset = 12.0 + (ws / DTOR) / 15.0 - (lng / 15.0 - tz) - E; //sunset in units of hours (e.g. 18.75 = 6:45 pm)
552✔
230
        //now a bunch of error checks to try to correctly catch weird behavior
231
        //both sunrise and sunset may be shifted by 24 hours (example: Fiji- positive tz negative lng), if so, roll them both back
232
        if (sunrise > 24.0 && sunset > 24.0) {
552✔
233
            sunrise -= 24.0;
1✔
234
            sunset -= 24.0;
1✔
235
        }
236
        //no examples of the opposing case, but let's catch it anyways, just in case
237
        if (sunrise < 0.0 && sunset < 0.0) {
552✔
NEW
238
            sunrise += 24.0;
×
NEW
239
            sunset += 24.0;
×
240
        }
241
    }
242

243
    Eo = 1.00014 - 0.01671 * cos(mnanom) - 0.00014 * cos(2.0 * mnanom);  /* Earth-sun distance (AU) */
554✔
244
    Eo = 1.0 / (Eo * Eo);                    /* Eccentricity correction factor */
554✔
245

246
    tst = hour + minute / 60.0 + (lng / 15.0 - tz) + E;  /* True solar time (hr) */
554✔
247

248
    /* 25aug2011 apd: addition of calculation of horizontal extraterrestrial irradiance */
249
    zen = 0.5 * M_PI - elv;
554✔
250
    Gon = 1367 * (1 + 0.033 * cos(360.0 / 365.0 * day_of_year(month, day) * M_PI /
554✔
251
                                  180)); /* D&B eq 1.4.1a, using solar constant=1367 W/m2 */
252
    if (zen > 0 && zen < M_PI / 2) /* if sun is up */
554✔
253
        hextra = Gon * cos(zen); /* elevation is incidence angle (zen=90-elv) with horizontal */
549✔
254
    else if (zen == 0)
5✔
NEW
255
        hextra = Gon;
×
256
    else
257
        hextra = 0.0;
5✔
258

259
    sunn[0] = azm;                        /* Variables returned in array sunn[] */
554✔
260
    sunn[1] = zen;               /*  Zenith */
554✔
261
    sunn[2] = elv;
554✔
262
    sunn[3] = dec;
554✔
263
    sunn[4] = sunrise;
554✔
264
    sunn[5] = sunset;
554✔
265
    sunn[6] = Eo;
554✔
266
    sunn[7] = tst;
554✔
267
    sunn[8] = hextra;
554✔
268
}
554✔
269

270
const double B_TERMS[2][5][3] = //Terms used for the calculation of the Earth heliocentric latitude (B)
271
        {
272
                {
273
                        {280.0, 3.199, 84334.662},
274
                        {102.0, 5.422, 5507.553},
275
                        {80, 3.88, 5223.69},
276
                        {44, 3.7, 2352.87},
277
                        {32, 4, 1577.34}
278
                },
279
                {
280
                        {9,     3.9,   5507.55},
281
                        {6,     1.73,  5223.69}
282
                }
283
        };
284

285
double limit_degrees(double degrees) //Limit angle values to degrees within 0-360°
19,829✔
286
{
287
    double limited;
288

289
    degrees /= 360.0;
19,829✔
290
    limited = 360.0 * (degrees - floor(degrees));
19,829✔
291
    if (limited < 0) limited += 360.0;
19,829✔
292

293
    return limited;
19,829✔
294
}
295

296
double limit_minutes(double minutes) //Limit the time value to a value within 0-20 minutes
551✔
297
{
298
    double limited = minutes;
551✔
299

300
    if (limited < -20.0) limited += 1440.0;
551✔
301
    else if (limited > 20.0) limited -= 1440.0;
551✔
302

303
    return limited;
551✔
304
}
305

306
double limit_degrees180(double degrees) //Limit angles to values between 0-180°
549✔
307
{
308
    double limited;
309

310
    degrees /= 180.0;
549✔
311
    limited = 180.0 * (degrees - floor(degrees));
549✔
312
    if (limited < 0) limited += 180.0;
549✔
313

314
    return limited;
549✔
315
}
316

317
double limit_zero2one(double value) //Limit parameter to a value between 0-1
3,306✔
318
{
319
    double limited;
320

321
    limited = value - floor(value);
3,306✔
322
    if (limited < 0) limited += 1.0;
3,306✔
323

324
    return limited;
3,306✔
325
}
326

327
double limit_degrees180pm(
1,653✔
328
        double degrees) //Limit degrees to -180-180° (positive westward from the meridian, negative eastward from the meridian)
329
{
330
    double limited;
331

332
    degrees /= 360.0;
1,653✔
333
    limited = 360.0 * (degrees - floor(degrees));
1,653✔
334
    if (limited < -180.0) limited += 360.0;
1,653✔
335
    else if (limited > 180.0) limited -= 360.0;
1,653✔
336

337
    return limited;
1,653✔
338
}
339

340
double third_order_polynomial(double a, double b, double c, double d, double x) //Calculate third order polynomial
13,770✔
341
{
342
    double result = ((a * x + b) * x + c) * x + d;
13,770✔
343
    return result;
13,770✔
344
}
345

346
double dayfrac_to_local_hr(double dayfrac, double timezone) //Convert day fraction to local hour
1,653✔
347
{
348
    return 24.0 * limit_zero2one(dayfrac + timezone / 24.0);
1,653✔
349
}
350

351
double
352
julian_day(int year, int month, int day, int hour, int minute, double second, double dut1, double tz) //Julian day
1,101✔
353
{
354
    double day_decimal, jd;
355

356
    day_decimal = day + (hour - tz + (minute + (second + dut1) / 60.0) / 60.0) / 24.0;
1,101✔
357

358
    if (month < 3) {
1,101✔
NEW
359
        month += 12;
×
NEW
360
        year--;
×
361
    }
362
    int julian_day_1st_term = 365.25 * (year + 4716.0);
1,101✔
363
    int julian_day_2nd_term = 30.6001 * (month + 1);
1,101✔
364
    jd = julian_day_1st_term + julian_day_2nd_term + day_decimal - 1524.5;
1,101✔
365

366
    if (jd > 2299160.0) {
1,101✔
367
        int a = year / 100;
1,101✔
368
        int a_over_4 = a / 4;
1,101✔
369
        jd += (2 - a + a_over_4);
1,101✔
370
    }
371

372
    return jd;
1,101✔
373
}
374

375
double julian_century(double jd) // Julian century
2,754✔
376
{
377
    double jc = (jd - 2451545.0) / 36525.0;
2,754✔
378
    return jc;
2,754✔
379
}
380

381
double julian_ephemeris_day(double jd, double delta_t) //Julian ephemeris day
2,754✔
382
{
383
    double jde = jd + delta_t / 86400.0;
2,754✔
384
    return jde;
2,754✔
385
}
386

387
double julian_ephemeris_century(double jde) //Julian ephemeris century
2,754✔
388
{
389
    double jce = (jde - 2451545.0) / 36525.0;
2,754✔
390
    return jce;
2,754✔
391
}
392

393
double julian_ephemeris_millennium(double jce) //Julian ephemeris millennium
2,754✔
394
{
395
    double jme = jce / 10.0;
2,754✔
396
    return jme;
2,754✔
397
}
398

399
double earth_periodic_term_summation(const double terms[][3], int count,
5,508✔
400
                                     double jme) //Summation of the terms used to calculate Earth heliocentric values
401
{
402
    int i;
403
    double sum = 0;
5,508✔
404

405
    for (i = 0; i < count; i++)
24,786✔
406
        sum += terms[i][0] * cos(terms[i][1] + terms[i][2] * jme);
19,278✔
407

408
    return sum;
5,508✔
409
}
410

411
double earth_values(double term_sum[], int count,
2,754✔
412
                    double jme)  //Used to calculate Earth heliocentric longitude, latitude, radius vector
413
{
414
    int i;
415
    double sum = 0;
2,754✔
416

417
    for (i = 0; i < count; i++)
8,262✔
418
        sum += term_sum[i] * pow(jme, i);
5,508✔
419

420
    sum /= 1.0e8;
2,754✔
421

422
    return sum;
2,754✔
423
}
424

425
double earth_heliocentric_longitude(double j_star_tt, double jme) //Earth heliocentric longitude (degrees)
2,754✔
426
{
427
    const double LGS_terms[10][3] = { {1.0 / 365.261278, 3.401508e-2 , 1.600780},
2,754✔
428
        {1.0 / 182.632412, 3.486440e-4, 1.662976},
429
        {1.0 / 29.530634, 3.136227e-5, -1.195905},
430
        {1.0 / 399.529850, 3.578979e-5, -1.042052},
431
        {1.0 / 291.956812, 2.676185e-5, 2.012613 },
432
        {1.0 / 583.598201, 2.333925e-5, -2.867714},
433
        {1.0 / 4652.629372, 1.221214e-5, 1.225038},
434
        {1.0 / 1450.236684, 1.217941e-5, -0.828601},
435
        {1.0 / 199.459709, 1.343914e-5, -3.108253},
436
        {1.0 / 365.355291, 8.499475e-4, -2.353709}
437
    };
438

439
    
440
    double rho_L_k = 0;
2,754✔
441
    double f_L_k = 0;
2,754✔
442
    double phi_L_k = 0;
2,754✔
443
    double a_L = 1.0 / 58.130101;
2,754✔
444
    double b_L = 1.742145;
2,754✔
445
    double LG2 = a_L * j_star_tt + b_L;
2,754✔
446
    for (int i = 0; i < 10; i++) {
30,294✔
447
        rho_L_k = LGS_terms[i][1];
27,540✔
448
        f_L_k = LGS_terms[i][0];
27,540✔
449
        phi_L_k = LGS_terms[i][2];
27,540✔
450
        LG2 += rho_L_k * cos(2.0 * M_PI * f_L_k * j_star_tt - phi_L_k);
27,540✔
451
    }
452
    double LG2_final = limit_degrees(RTOD * LG2);
2,754✔
453
    return LG2_final;
2,754✔
454

455
}
456

457
double earth_heliocentric_latitude(double jme) //Earth heliocentric latitude (degrees)
2,754✔
458
{
459
    const int B_COUNT = 2;
2,754✔
460
    int b_subcount[2] = {5, 2};
2,754✔
461
    double sum[B_COUNT];
462
    int i;
463

464
    for (i = 0; i < B_COUNT; i++)
8,262✔
465
        sum[i] = earth_periodic_term_summation(B_TERMS[i], b_subcount[i], jme);
5,508✔
466

467
    double earth_helio_latitude = RTOD * (earth_values(sum, B_COUNT, jme));
2,754✔
468
    return earth_helio_latitude;
2,754✔
469

470
}
471

472
double earth_radius_vector(double j_star_tt, double jme) //Earth radius vector (Astronomical Units (AU))
2,754✔
473
{
474
    double a_R = 0;
2,754✔
475
    double b_R = 1.000140;
2,754✔
476
    double f_R = 1.0 / 365.254902;
2,754✔
477
    double rho_R = 0.016704;
2,754✔
478
    double phi_R = -3.091159;
2,754✔
479

480
    double R_SG2 = rho_R * cos(2.0 * M_PI * f_R * j_star_tt - phi_R) + a_R * j_star_tt + b_R;
2,754✔
481
    return R_SG2;
2,754✔
482
}
483

484
double geocentric_longitude(double l) //geocentric longitude (degrees)
2,754✔
485
{
486
    double theta = l + 180.0;
2,754✔
487

488
    if (theta >= 360.0) theta -= 360.0;
2,754✔
489

490
    return theta;
2,754✔
491
}
492

493
double geocentric_latitude(double b) //geocentric latitude (degrees)
2,754✔
494
{
495
    return -b;
2,754✔
496
}
497

498
double mean_elongation_moon_sun(double jce) //mean elongation of the moon from the sun (X0) (degrees)
2,754✔
499
{
500
    double mean_elong_moon_sun = third_order_polynomial(1.0 / 189474.0, -0.0019142, 445267.11148, 297.85036, jce);
2,754✔
501
    return mean_elong_moon_sun;
2,754✔
502
}
503

504
double mean_anomaly_sun(double jce) //mean anomaly of the sun (X1) (degrees)
2,754✔
505
{
506
    double mean_anom_sun = third_order_polynomial(-1.0 / 300000.0, -0.0001603, 35999.05034, 357.52772, jce);
2,754✔
507
    return mean_anom_sun;
2,754✔
508
}
509

510

511
double mean_anomaly_moon(double jce) //mean anomaly of the moon, X2 (degrees)
2,754✔
512
{
513
    double mean_anom_moon = third_order_polynomial(1.0 / 56250.0, 0.0086972, 477198.867398, 134.96298, jce);
2,754✔
514
    return mean_anom_moon;
2,754✔
515
}
516

517
double argument_latitude_moon(double jce) //moon's argument of latitude (X3) (degrees)
2,754✔
518
{
519
    double argument_lat_moon = third_order_polynomial(1.0 / 327270.0, -0.0036825, 483202.017538, 93.27191, jce);
2,754✔
520
    return argument_lat_moon;
2,754✔
521
}
522

523
double ascending_longitude_moon(
2,754✔
524
        double jce) //longitude fo teh ascending node of the moon's mean orbit on the ecliptic, measured from the mean equinox of the date (X4) (degrees)
525
{
526
    double ascending_long_moon = third_order_polynomial(1.0 / 450000.0, 0.0020708, -1934.136261, 125.04452, jce);
2,754✔
527
    return ascending_long_moon;
2,754✔
528
}
529

530
void nutation_longitude_and_obliquity(double j_star_tt, double jce, double x[5],
2,754✔
531
                                      double delta_values[2]) //nutation in longitude and obliquity (both degrees)
532
{
533
    double f_psi = 1.0 / 6791.164405;
2,754✔
534
    double rho_psi = 8.329092e-5;
2,754✔
535
    double phi_psi = -2.052757;
2,754✔
536
    double del_psi = RTOD * rho_psi * cos(2.0 * M_PI * f_psi * j_star_tt - phi_psi);
2,754✔
537
    delta_values[0] = del_psi;
2,754✔
538
    //delta_values[1] = sum_epsilon / 36000000.0; //del_epsilon
539
}
2,754✔
540

541
double ecliptic_mean_obliquity(double jme) //mean obliquity of the ecliptic (arc seconds)
2,754✔
542
{
543
    double u = jme / 10.0;
2,754✔
544
    double eclip_mean_obliquity = 84381.448 + u * (-4680.93 + u * (-1.55 + u * (1999.25 + u * (-51.38 + u * (-249.67 +
2,754✔
545
                                                                                                             u *
2,754✔
546
                                                                                                             (-39.05 +
2,754✔
547
                                                                                                              u *
2,754✔
548
                                                                                                              (7.12 +
2,754✔
549
                                                                                                               u *
2,754✔
550
                                                                                                               (27.87 +
2,754✔
551
                                                                                                                u *
2,754✔
552
                                                                                                                (5.79 +
2,754✔
553
                                                                                                                 u *
2,754✔
554
                                                                                                                 2.45)))))))));
555
    return eclip_mean_obliquity;
2,754✔
556
}
557

558
double ecliptic_true_obliquity(double j_star_tt) //true obliquity of the ecliptic (degrees)
2,754✔
559
{
560
    double a_eps = -6.216374e-9;
2,754✔
561
    double b_eps = 4.091383e-1;
2,754✔
562
    double f_eps = 1.0 / 6791.164405;
2,754✔
563
    double rho_eps = 4.456183e-5;
2,754✔
564
    double phi_eps = 2.660352;
2,754✔
565
    double eclip_true_obliquity = rho_eps * cos(2.0 * M_PI * f_eps * j_star_tt - phi_eps) + a_eps * j_star_tt + b_eps;
2,754✔
566
    return RTOD * eclip_true_obliquity;
2,754✔
567
}
568

569
double aberration_correction(double r) //aberration correction (degrees)
2,754✔
570
{
571
    double delta_tau = -9.933735e-5; //rad
2,754✔
572
    return delta_tau;
2,754✔
573
}
574

575
double apparent_sun_longitude(double theta, double delta_psi, double delta_tau) //apparent sun longitude (degrees)
2,754✔
576
{
577
    double lambda = theta + delta_psi + delta_tau;
2,754✔
578
    return lambda;
2,754✔
579
}
580

581
double greenwich_mean_sidereal_time(double j_star_ut, double jd, double jc) //greenwich mean sidereal time (degrees)
2,754✔
582
{
583
    double nu0_old = limit_degrees(280.46061837 + 360.98564736629 * (jd - 2451545.0) +
5,508✔
584
                               jc * jc * (0.000387933 - jc / 38710000.0));
2,754✔
585
                               
586
    //double nu0 = limit_degrees(RTOD * (6.3000388 * j_star_ut + 1.742079));
587
    double nu0 = limit_degrees(RTOD * (6.3000388 * (j_star_ut-2) + 1.742079));
2,754✔
588
    return nu0_old;
2,754✔
589
}
590

591
double greenwich_sidereal_time(double nu0, double delta_psi, double epsilon) //greenwich apparent sidereal time (degrees)
2,754✔
592
{
593
    return nu0 + delta_psi * cos(DTOR * (epsilon));
2,754✔
594
}
595

596
double
597
geocentric_right_ascension(double lamda, double epsilon, double beta) //geocentric sun right ascension angle (degrees)
2,754✔
598
{
599
    double lamda_rad = DTOR * (lamda);
2,754✔
600
    double epsilon_rad = DTOR * (epsilon);
2,754✔
601
    double alpha = limit_degrees(RTOD * (atan2(sin(lamda_rad) * cos(epsilon_rad) -
5,508✔
602
                                               tan(DTOR * (beta)) * sin(epsilon_rad), cos(lamda_rad))));
2,754✔
603
    return alpha;
2,754✔
604
}
605

606
double geocentric_declination(double beta, double epsilon, double lamda) //geocentric sun declination angle (degrees)
2,754✔
607
{
608
    double beta_rad = DTOR * (beta);
2,754✔
609
    double epsilon_rad = DTOR * (epsilon);
2,754✔
610
    double delta = RTOD * (asin(sin(beta_rad) * cos(epsilon_rad) +
2,754✔
611
                                cos(beta_rad) * sin(epsilon_rad) * sin(DTOR * (lamda))));
2,754✔
612
    return delta;
2,754✔
613
}
614

615
double observer_hour_angle(double nu, double longitude, double alpha_deg) //observer local hour angle (degrees)
2,754✔
616
{
617
    double H = limit_degrees(nu + longitude - alpha_deg);
2,754✔
618
    return H;
2,754✔
619
}
620

621
double sun_equatorial_horizontal_parallax(double r) //sun equatorial horizontal parallax of the sun (degrees)
2,754✔
622
{
623
    double xi = 4.263521e-5;
2,754✔
624
    return xi;
2,754✔
625
}
626

627
void right_ascension_parallax_and_topocentric_dec(double latitude, double elevation,
2,754✔
628
                                                  double xi, double h, double delta,
629
                                                  double delta_alpha_prime[2]) //right sun ascension parallax and topocentric declination angle (both degrees)
630
{
631
    double delta_alpha_rad;
632
    double lat_rad = DTOR * (latitude);
2,754✔
633
    double xi_rad = DTOR * (xi);
2,754✔
634
    double h_rad = DTOR * (h);
2,754✔
635
    double delta_rad = DTOR * (delta);
2,754✔
636
    double u = atan(0.99664719 * tan(lat_rad));
2,754✔
637
    double y = 0.99664719 * sin(u) + elevation * sin(lat_rad) / 6378140.0;
2,754✔
638
    double x = cos(u) + elevation * cos(lat_rad) / 6378140.0;
2,754✔
639
    delta_alpha_prime[1] = RTOD * -x * (sin(h_rad) / cos(delta_rad)) * xi_rad;
2,754✔
640
    delta_alpha_prime[0] = RTOD * (delta_rad + (x * cos(h_rad) * sin(delta_rad) - y * cos(delta_rad)) * xi_rad);
2,754✔
641
}
2,754✔
642

643
double topocentric_right_ascension(double alpha_deg, double delta_alpha) //topocentric sun right ascension (degrees)
2,754✔
644
{
645
    double alpha_prime = alpha_deg + delta_alpha;
2,754✔
646
    return alpha_prime;
2,754✔
647
}
648

649
double topocentric_local_hour_angle(double h, double delta_alpha) //topocentric local hour angle (degrees)
2,754✔
650
{
651
    double h_prime = h - delta_alpha;
2,754✔
652
    return h_prime;
2,754✔
653
}
654

655
double topocentric_elevation_angle(double latitude, double delta_prime,
2,754✔
656
                                   double h_prime) //topocentric elevation angle not corrected for atmospheric refraction (degrees)
657
{
658
    double lat_rad = DTOR * (latitude);
2,754✔
659
    double delta_prime_rad = DTOR * (delta_prime);
2,754✔
660
    double e0 = RTOD * (asin(sin(lat_rad) * sin(delta_prime_rad) +
2,754✔
661
                             cos(lat_rad) * cos(delta_prime_rad) * cos(DTOR * (h_prime))));
2,754✔
662
    return e0;
2,754✔
663
}
664

665
double atmospheric_refraction_correction(double pressure, double temperature,
2,754✔
666
                                         double atmos_refract, double e0) //atmospheric refraction correction (degrees)
667
{
668
    double del_e = 0;
2,754✔
669
    double SUN_RADIUS = 0.26667;
2,754✔
670
    if (e0 >= -1 * (SUN_RADIUS + atmos_refract)) {
2,754✔
671
        del_e = (pressure / 1010.0) * (283.0 / (273.0 + temperature)) *
1,334✔
672
            1.02 / (60.0 * tan(DTOR * (e0 + 10.3 / (e0 + 5.11))));
1,334✔
673
    }
674

675
    return del_e;
2,754✔
676
}
677

678
double topocentric_elevation_angle_corrected(double e0,
2,754✔
679
                                             double delta_e) //topocentric elevation angle corrected for atmospheric refraction (degrees)
680
{
681
    double e = e0 + delta_e;
2,754✔
682
    if (e > 90.0) {
2,754✔
NEW
683
        e = 90.0;
×
684
    }
685
    else if (e < -90.0) {
2,754✔
NEW
686
        e = -90.0;
×
687
    }
688
    return e;
2,754✔
689
}
690

691
double topocentric_zenith_angle(double e) //topocentric zenith angle (degrees)
2,754✔
692
{
693

694
    double theta = 90.0 - e;
2,754✔
695
    return theta;
2,754✔
696
}
697

698
double topocentric_azimuth_angle_astro(double h_prime, double latitude,
2,754✔
699
                                       double delta_prime) //topocentric azimuth angle from astronomers point of view (measured west from south)
700
{
701
    double h_prime_rad = DTOR * (h_prime);
2,754✔
702
    double lat_rad = DTOR * (latitude);
2,754✔
703
    double azimuth_astro = limit_degrees(RTOD * (atan2(sin(h_prime_rad),
5,508✔
704
                                                       cos(h_prime_rad) * sin(lat_rad) -
2,754✔
705
                                                       tan(DTOR * (delta_prime)) * cos(lat_rad))));
2,754✔
706
    return azimuth_astro;
2,754✔
707
}
708

709
double topocentric_azimuth_angle(
2,754✔
710
        double azimuth_astro) //topocentric azimuth angle for solar radiation purposes (measured east from north) (degrees)
711
{
712
    double azimuth = limit_degrees(azimuth_astro + 180.0);
2,754✔
713
    return azimuth;
2,754✔
714
}
715

NEW
716
double surface_incidence_angle(double zenith, double azimuth_astro, double azm_rotation,
×
717
                               double slope) //surface incidence angle for surface oriented in any direction
718
{
NEW
719
    double zenith_rad = DTOR * (zenith);
×
NEW
720
    double slope_rad = DTOR * (slope);
×
NEW
721
    double aoi = RTOD * (acos(cos(zenith_rad) * cos(slope_rad) +
×
NEW
722
                              sin(slope_rad) * sin(zenith_rad) * cos(DTOR * (azimuth_astro - azm_rotation))));
×
NEW
723
    return aoi;
×
724
}
725

726
double sun_mean_longitude(double jme) // sun mean longitude (degrees)
551✔
727
{
728
    return limit_degrees(280.4664567 + jme * (360007.6982779 + jme * (0.03032028 +
1,102✔
729
                                                                      jme * (1 / 49931.0 + jme * (-1 / 15300.0 + jme *
551✔
730
                                                                                                                 (-1 /
731
                                                                                                                  2000000.0))))));
551✔
732
}
733

734
double eot(double m, double alpha, double del_psi, double epsilon) //Equation of Time (minutes)
551✔
735
{
736
    double E = limit_minutes(4.0 * (m - 0.0057183 - alpha + del_psi * cos(DTOR * (epsilon))));
551✔
737
    return E;
551✔
738
}
739

740
double approx_sun_transit_time(double alpha_zero, double longitude, double nu) {
551✔
741
    double m0 = (alpha_zero - longitude - nu) / 360.0;
551✔
742
    return m0;
551✔
743
}
744

745
double sun_hour_angle_at_rise_set(double latitude, double delta_zero, double h0_prime) {
551✔
746
    double h0 = -99999;
551✔
747
    double latitude_rad = DTOR * (latitude);
551✔
748
    double delta_zero_rad = DTOR * (delta_zero);
551✔
749
    double argument = (sin(DTOR * (h0_prime)) - sin(latitude_rad) * sin(delta_zero_rad)) /
551✔
750
                      (cos(latitude_rad) * cos(delta_zero_rad));
551✔
751

752
    if (std::abs(argument) <= 1) {
551✔
753
        h0 = limit_degrees180(RTOD * (acos(argument)));
549✔
754
    }
755
    else if (argument < -1) {
2✔
756
        h0 = RTOD * M_PI;
2✔
757
    }
NEW
758
    else if (argument > 1) {
×
NEW
759
        h0 = 0;
×
760
    }
761

762

763
    //h0 = limit_degrees180(RTOD * (acos(argument)));
764

765
    return h0;
551✔
766
}
767

768
void approx_sun_rise_and_set(double h0,
551✔
769
                             double m_rts[3]) //approximate sunrise and sunset times based on local hour angle (approximate sun transit times)
770
{
771
    double h0_dfrac = h0 / 360.0;
551✔
772
    double m0_calc = m_rts[0];
551✔
773
    m_rts[0] = limit_zero2one(m0_calc); //m0 sun transit time (fraction of day)
551✔
774
    m_rts[1] = limit_zero2one(m0_calc - h0_dfrac); //approximate sunrise time in fraction of day
551✔
775
    m_rts[2] = limit_zero2one(m0_calc + h0_dfrac); // approximate sunset time in fraction of day
551✔
776
    /*if (m_rts[2] < m_rts[1])
777
    {
778
        m_rts[2] = 1+ m0_calc + h0_dfrac;
779
    }*/
780
}
551✔
781

782
double rts_alpha_delta_prime(double n,
3,306✔
783
                             double ad[3]) //alpha and delta prime variables based on day prior, day of, day following right ascension and declination parameters
784
{
785
    double a = ad[1] - ad[0];
3,306✔
786
    double b = ad[2] - ad[1];
3,306✔
787

788
    if (std::abs(a) >= 2.0) a = limit_zero2one(a);
3,306✔
789
    if (std::abs(b) >= 2.0) b = limit_zero2one(b);
3,306✔
790

791
    return ad[1] + n * (a + b + (b - a) * n) / 2.0;
3,306✔
792
}
793

794
double
795
rts_sun_altitude(double latitude, double delta_prime, double h_prime) //sun altitude for sun transit, sunrise, sunset
1,653✔
796
{
797
    double latitude_rad = DTOR * (latitude);
1,653✔
798
    double delta_prime_rad = DTOR * (delta_prime);
1,653✔
799

800
    return RTOD * (asin(sin(latitude_rad) * sin(delta_prime_rad) +
1,653✔
801
                        cos(latitude_rad) * cos(delta_prime_rad) * cos(DTOR * (h_prime))));
1,653✔
802
}
803

804
double sun_rise_and_set(double *m_rts, double *h_rts, double *delta_prime, double latitude,
1,102✔
805
                        double *h_prime, double h0_prime, int sun) //sunrise and sunset calculations (fraction of day)
806
{
807
    return m_rts[sun] + (h_rts[sun] - h0_prime) /
1,102✔
808
                        (360.0 * cos(DTOR * (delta_prime[sun])) * cos(DTOR * (latitude)) * sin(DTOR * (h_prime[sun])));
1,102✔
809
}
810

NEW
811
bool spa_table_key::operator==(const spa_table_key &other) const {
×
NEW
812
    return (jd == other.jd
×
NEW
813
                && delta_t == other.delta_t
×
NEW
814
                && pressure == other.pressure
×
NEW
815
                && temp == other.temp
×
NEW
816
                && ascension == other.ascension
×
NEW
817
                && declination == other.declination
×
NEW
818
                );
×
819
}
820

821
spa_table_key::spa_table_key(double j, double dt, double p, double t, double a, double d):
2,754✔
822
    jd(j), delta_t(dt), ascension(a), declination(d) {
2,754✔
823
        int pressure_bucket = 10;
2,754✔
824
        pressure = ((int)(p + pressure_bucket/2) / pressure_bucket) * pressure_bucket;
2,754✔
825
        pressure = (int)pressure;
2,754✔
826
        int temp_bucket = 5;
2,754✔
827
        temp = ((int)(t + temp_bucket / 2) / temp_bucket) * temp_bucket;
2,754✔
828
        temp = (int)temp;
2,754✔
829
    }
2,754✔
830

NEW
831
std::size_t std::hash<spa_table_key>::operator()(const spa_table_key& k) const
×
832
{
833
using std::hash;
834
// Compute individual hash values for first, second, etc and combine them using XOR and bit shifting:
835
return 
836
((((
NEW
837
        ((((hash<double>()(k.jd)
×
NEW
838
            ^ (hash<double>()(k.delta_t) << 1)) >> 1)
×
NEW
839
            ^ (hash<int>()(k.pressure) << 1)) >> 1)
×
NEW
840
            ^ (hash<int>()(k.temp) << 1)) >> 1)
×
NEW
841
            ^ (hash<double>()(k.ascension) << 1)) >> 1)
×
NEW
842
            ^ (hash<double>()(k.declination) << 1)
×
843
            ;
844
}
845

NEW
846
solarpos_lookup::solarpos_lookup(){
×
NEW
847
    clear_spa_table();
×
NEW
848
}
×
849

NEW
850
void solarpos_lookup::clear_spa_table() {
×
NEW
851
    spa_table.clear();
×
NEW
852
    spa_table_day = 0;
×
NEW
853
};
×
854

855
// The algorithm reuses the outputs from the last 3 days or so, so the hash table is emptied every 3 days to reduce size
NEW
856
void solarpos_lookup::roll_spa_table_forward(int day) {
×
NEW
857
    if (std::abs(spa_table_day - day) > 3){
×
NEW
858
        spa_table_day = day;
×
NEW
859
        spa_table.clear();
×
860
    }
NEW
861
};
×
862

NEW
863
std::vector<double>* solarpos_lookup::find(spa_table_key spa_key_inputs) {
×
NEW
864
    auto spa_pos = spa_table.end();
×
NEW
865
    spa_pos = spa_table.find(spa_key_inputs);
×
NEW
866
    if (spa_pos != spa_table.end())
×
NEW
867
        return &spa_pos->second;
×
NEW
868
    return nullptr;
×
869
}
870

NEW
871
void solarpos_lookup::insert(spa_table_key spa_key_inputs, std::vector<double> spa_outputs) {
×
NEW
872
    spa_table[spa_key_inputs] = spa_outputs;
×
NEW
873
}
×
874

875
void
876
calculate_spa(double jd, double lat, double lng, double alt, double pressure, double temp, double delta_t, double tilt,
2,754✔
877
              double azm_rotation, double ascension_and_declination[2], double needed_values[9], std::shared_ptr<solarpos_lookup> spa_table) {
878
    //Calculate the Julian and Julian Ephemeris, Day, Century, and Millennium (3.1)
879
    //double jd = julian_day(year, month, day, hour, minute, second, delta_t, tz);
880
    double jc = julian_century(jd); // for 2000 standard epoch
2,754✔
881
    double jde = julian_ephemeris_day(jd,
2,754✔
882
                                      delta_t); //Adjusted for difference between Earth rotation time and the Terrestrial Time (TT) (derived from observation, reported yearly in Astronomical Almanac)
883

884
    spa_table_key spa_key_inputs = {jd, delta_t, pressure, temp, ascension_and_declination[0], ascension_and_declination[1]};
2,754✔
885
    if (spa_table) {
2,754✔
NEW
886
        std::vector<double>* results = spa_table->find(spa_key_inputs);
×
NEW
887
        if (results){
×
NEW
888
            needed_values[0] = (*results)[0];
×
NEW
889
            needed_values[1] = (*results)[1];
×
NEW
890
            needed_values[2] = (*results)[2];
×
NEW
891
            needed_values[3] = (*results)[3];
×
NEW
892
            needed_values[4] = (*results)[4];
×
NEW
893
            needed_values[5] = (*results)[5];
×
NEW
894
            needed_values[6] = (*results)[6];
×
NEW
895
            needed_values[7] = (*results)[7];
×
NEW
896
            needed_values[8] = (*results)[8];
×
NEW
897
            ascension_and_declination[0] = (*results)[9];
×
NEW
898
            ascension_and_declination[1] = (*results)[10];
×
NEW
899
            return;
×
900
        }
901
    }
902
    
903
    double jce = julian_ephemeris_century(jde); //for 2000 standard epoch
2,754✔
904
    double jme = julian_ephemeris_millennium(jce); // jce/10 (for 2000 standard epoch)
2,754✔
905
    needed_values[0] = jme;
2,754✔
906

907
    //Calculate the Earth heliocentric longitude latitude, and radius vector (L, B, and R) (3.2)
908
    // Heliocentric - Earth position is calculated with respect to the center of the sun
909
    double j_star_tt = jde - 2444239.5;
2,754✔
910
    double j_star_ut = jd - 2444239.5;
2,754✔
911
    double l = earth_heliocentric_longitude(j_star_tt, jme); //L0-L5 values listed beginning line ?, L limited to 0-360°
2,754✔
912
    double b = earth_heliocentric_latitude(jme); // B0-B1 values listed beginning line ?, B limited to 0-360°
2,754✔
913
    double r = earth_radius_vector(j_star_tt, jme); // R0-R4 values listed beginning line ?,  R in Astronomical Units (AU)
2,754✔
914
    needed_values[1] = 1 / (r * r); //
2,754✔
915

916
    //Calculate the geocentric longitude and latitude (theta and beta) (3.3)
917
    // Geocentric - sun position calculated with respect to the Earth center
918
    double theta = geocentric_longitude(l); // Limited to 0-360°
2,754✔
919
    double beta = geocentric_latitude(b); // Limited to 0-360°
2,754✔
920

921
    //Calculate the nutation in longitude and obliquity (del_psi and del_eps) (3.4)
922
    double x[5];
923
    x[0] = mean_elongation_moon_sun(jce); // degrees
2,754✔
924
    x[1] = mean_anomaly_sun(jce); // degrees
2,754✔
925
    x[2] = mean_anomaly_moon(jce); // degrees
2,754✔
926
    x[3] = argument_latitude_moon(jce); // degrees
2,754✔
927
    x[4] = ascending_longitude_moon(jce); // degrees
2,754✔
928

929
    double delta_values[2]; // allocate storage for nutation in longitude (del_psi) and nutation in obliquity (del_epsilon)
930
    nutation_longitude_and_obliquity(j_star_tt, jce, x, delta_values);
2,754✔
931
    double del_psi = delta_values[0]; //store value for use in further spa calculations
2,754✔
932
    needed_values[2] = del_psi; //pass del_psi value to sunrise sunset calculations
2,754✔
933
    double del_epsilon = delta_values[1]; //store value for use in further spa calculations
2,754✔
934

935
    //Calculate the true obliquity of the ecliptic, epsilon (3.5)
936
    double epsilon0 = ecliptic_mean_obliquity(jme); //mean obliquity of the ecliptic (arc seconds)
2,754✔
937
    double epsilon = ecliptic_true_obliquity(j_star_tt); //true obliquity of the ecliptic (degrees)
2,754✔
938
    needed_values[3] = epsilon;
2,754✔
939

940
    //Calculate the aberration correction (3.6)
941
    double del_tau = aberration_correction(r); // degrees
2,754✔
942

943
    //Calculate the apparent sun longitude (3.7)
944
    double lamda = apparent_sun_longitude(theta, del_psi, del_tau); // degrees
2,754✔
945

946
    //Calculate the apparent sidereal time at Greenwich at any given time (3.8)
947
    double nu0 = greenwich_mean_sidereal_time(j_star_ut, jd, jc); //degrees
2,754✔
948
    double nu = greenwich_sidereal_time(nu0, del_psi, epsilon); // degrees
2,754✔
949
    needed_values[4] = nu;
2,754✔
950

951
    //Calculate the geocentric sun right ascension (degrees) (3.9)
952
    double alpha = geocentric_right_ascension(lamda, epsilon, beta); // degrees, limited to 0-360°
2,754✔
953
    ascension_and_declination[0] = alpha;
2,754✔
954

955
    //Calculate the geocentric sun declination (degrees) (3.10)
956
    double delta = geocentric_declination(beta, epsilon, lamda); // degrees
2,754✔
957
    ascension_and_declination[1] = delta;
2,754✔
958

959
    //Calculate local hour angle (3.11)
960
    double H = observer_hour_angle(nu, lng, alpha); // positive for east of Greenwich, limited to 0-360°
2,754✔
961

962
    //Calculate the topocentric sun right ascension (3.12)
963
    // Topocentric - sun position is calculated with respect to the observer local position at the Earth surface
964
    double xi = sun_equatorial_horizontal_parallax(r); // degrees
2,754✔
965
    double delta_alpha_prime[2]; //storage for parallax in the sun right ascension, topocentric sun declination
966
    right_ascension_parallax_and_topocentric_dec(lat, alt, xi, H, delta,
2,754✔
967
                                                 delta_alpha_prime); //outputs parallax in the sun right ascension (delta_alpha) and sun right ascension (delta_prime)
968
    double delta_alpha = delta_alpha_prime[1]; // topocentric sun parallax in the sun right ascension (degrees)
2,754✔
969
    double delta_prime = delta_alpha_prime[0]; // topocentric declination (degrees)
2,754✔
970
    needed_values[5] = delta_prime; // pass declination as output of calculate_spa
2,754✔
971
    double alpha_prime = topocentric_right_ascension(alpha, delta_alpha); // topocentric sun right ascenion (degrees)
2,754✔
972

973
    //Calculate topocentric local hour angle (3.13)
974
    double H_prime = topocentric_local_hour_angle(H, delta_alpha); // degrees
2,754✔
975

976
    //Calculate the topocentric zenith angle (3.14)
977
    double e0 = topocentric_elevation_angle(lat, delta_prime, H_prime); // without atmospheric refraction (degrees)
2,754✔
978
    double atmos_refract = 0.5667; // atmospheric refraction for check if sun is below horizon
2,754✔
979
    double del_e = atmospheric_refraction_correction(pressure, temp, atmos_refract,
2,754✔
980
                                                     e0); // atmospheric refraction correction in degrees, returns 0 if sun is below horizon
981
    if (std::isnan(del_e)) del_e = 0;
2,754✔
982

983
    double e = topocentric_elevation_angle_corrected(e0,
2,754✔
984
                                                     del_e); // Topocentric elevation angle corrected for refraction (degrees)
985
    needed_values[6] = e; // Pass topocentric elevation angle as an output of solarpos_spa (degrees)
2,754✔
986
    double zenith = topocentric_zenith_angle(e); // Topocentric zenith angle (degrees) (90 - e)
2,754✔
987
    needed_values[7] = zenith; // Pass topocentric zenith angle as an output of solarpos_spa (degrees)
2,754✔
988

989
    //Calculate the topocentric azimuth angle (3.15)
990
    double azimuth_astro = topocentric_azimuth_angle_astro(H_prime, lat,
2,754✔
991
                                                           delta_prime); //topocentric astronomers azimuth angle (degrees) (measured westward from south)
992
    double azimuth = topocentric_azimuth_angle(
2,754✔
993
            azimuth_astro); // topocentric azimuth angle (degrees) (measured eastward from north)
994
    needed_values[8] = azimuth;
2,754✔
995
    if (cos(DTOR * e) == 0.0) {
2,754✔
NEW
996
        azimuth = M_PI;
×
997
    }
998

999
    if (spa_table) {
2,754✔
NEW
1000
        std::vector<double> spa_outputs = {needed_values[0], needed_values[1], needed_values[2], needed_values[3], needed_values[4], needed_values[5], needed_values[6], needed_values[7], needed_values[8],
×
NEW
1001
            ascension_and_declination[0], ascension_and_declination[1]};
×
NEW
1002
        spa_table->insert(spa_key_inputs, spa_outputs);
×
NEW
1003
    }
×
1004

1005
    //Calculate the incidence angle for a selected surface (3.16)
1006
    //double aoi = surface_incidence_angle(zenith, azimuth_astro, azm_rotation, tilt); //incidence angle for a surface oriented in any direction (degrees)
1007
}
1008

1009

1010
void
1011
calculate_eot_and_sun_rise_transit_set(double jme, double tz, double alpha, double del_psi, double epsilon, double jd,
551✔
1012
                                       int year, int month, int day, double lat, double lng, double alt,
1013
                                       double pressure, double temp, double tilt, double delta_t, double azm_rotation,
1014
                                       double needed_values[4], std::shared_ptr<solarpos_lookup> spa_table) {
1015
    // Equation of Time (A.1)
1016
    double M = sun_mean_longitude(jme); // degrees
551✔
1017
    double E = eot(M, alpha, del_psi, epsilon); //Equation of Time (degrees)
551✔
1018
    needed_values[0] = E; // Pass Equation of Time to solarpos_spa level
551✔
1019
    double needed_values_nu[9]; //create storage for iterating calculate_spa section
1020

1021
    // Sunrise, Sunset, and Sun Transit (A.2)
1022
    int i; //initialize iterator
1023
    double sun_declination_and_ascension[2] {0, 0}; // storage for iterating sun declination and ascension results
551✔
1024
    double needed_values2[13]; //where is this used?
1025
    double alpha_array[3]; //storage for alphas in iteration
1026
    double delta_array[3]; //storage for deltas in iteration
1027

1028
    //double nu = needed_values[3];
1029
    double jd_array[3];
1030
    jd_array[1] = julian_day(year, month, day, 0, 0, 0, 0,
551✔
1031
                             0); //initialize Julian array with day in question (0 UT (0h0mm0s0tz)
1032
    //run calculate_spa to obtain apparent sidereal time at Greenwich at 0 UT (v, degrees)
1033
    calculate_spa(jd_array[1], lat, lng, alt, pressure, temp, 67, tilt, azm_rotation, sun_declination_and_ascension,
551✔
1034
                  needed_values_nu, spa_table);
1035
    double delta_test = sun_declination_and_ascension[1];
551✔
1036
    double nu = needed_values_nu[4]; //store apparent sidereal time at Greenwich
551✔
1037
    jd_array[0] = jd_array[1] - 1; //Julian day for the day prior to the day in question
551✔
1038
    jd_array[2] = jd_array[1] + 1; //Julian day for the day following the day in question
551✔
1039
    for (i = 0; i < 3; i++) { // iterate through day behind (0), day in question (1), and day following (2)
2,204✔
1040
        //Calculate the spa for each day at 0 TT by setting the delta_T difference between UT and TT to 0
1041
        calculate_spa(jd_array[i], lat, lng, alt, pressure, temp, 0, tilt, azm_rotation, sun_declination_and_ascension,
1,653✔
1042
                      needed_values2, spa_table);
1043
        alpha_array[i] = sun_declination_and_ascension[0]; //store geocentric right ascension for each iteration (degrees)
1,653✔
1044
        delta_array[i] = sun_declination_and_ascension[1]; //store geocentric declination for each iteration (degrees)
1,653✔
1045
    }
1046

1047
    double m0 = approx_sun_transit_time(alpha_array[1], lng, nu); //approximate sun transit time (fraction of day)
551✔
1048
    //double atmos_refract = 0.5667; //fillin
1049
    double h0_prime = -0.8333; //sun elevation for sunrise and sunset
551✔
1050
    //double h0_prime = 0;
1051
    //double h0_prime = -1 * (0.26667 + atmos_refract);
1052
    //double h0 = sun_hour_angle_at_rise_set(lat, delta_array[1], h0_prime); //sun hour angle correpsonding to sun elevation at sunrise/sunset (degrees) limited to 0-180°
1053
    double h0 = sun_hour_angle_at_rise_set(lat, delta_test,
551✔
1054
                                           h0_prime); //sun hour angle correpsonding to sun elevation at sunrise/sunset (degrees) limited to 0-180°
1055
    needed_values[1] = h0;
551✔
1056

1057
    double approx_times_array[3]; //store approximate sun transit (solar noon) (m0), sunrise (m1), and sunset (m2) times
1058
    approx_times_array[0] = m0; //approximate sun transit time
551✔
1059
    double nu_i_array[3]; //storage array for sidreal times at Greenwich for time i (0-transit, 1-sunrise, 2-sunset)
1060
    double n; //term n shifting time by delta_T
1061
    double alpha_prime_array[3]; //storage array for alpha__prime terms
1062
    double delta_prime_array[3]; //storage array for delta_prime terms
1063
    double h_prime_array[3]; //local hour angle storage for
1064
    double h_rts_array[3]; //sun altitude storage array for time i (0-transit, 1-sunrise, 2-sunset)
1065
    if (h0 > 0) { //check for valid values of local hour angle h0
551✔
1066
        approx_sun_rise_and_set(h0,
551✔
1067
                                approx_times_array); //calculate the approximate sun transit, sunrise, sunset (each limited between 0-1)
1068
        if (approx_times_array[2] + tz / 24 < 0 && approx_times_array[2] + 1 + tz / 24 < 1) {
551✔
1069
            approx_times_array[2] += 1;
83✔
1070
        }
1071
        //needed_values[1] = dayfrac_to_local_hr(approx_times_array[1],tz); //pass approximate sunrise time to the solarpos_spa outputs
1072
        //needed_values[2] = dayfrac_to_local_hr(approx_times_array[2],tz); //pass approximate sunset time to the solarpos_spa outputs
1073

1074
        for (i = 0; i < 3; i++) {
2,204✔
1075

1076
            nu_i_array[i] = nu + 360.985647 *
1,653✔
1077
                                 approx_times_array[i]; //sidereal time at Greenwich for time i (0-transit,1-sunrise,2-sunset)
1,653✔
1078

1079
            n = approx_times_array[i] + delta_t / 86400; //n_i term at given time (degrees)
1,653✔
1080

1081
            alpha_prime_array[i] = rts_alpha_delta_prime(n, alpha_array); // alpha_prime_i term at time i (degrees)
1,653✔
1082
            delta_prime_array[i] = rts_alpha_delta_prime(n, delta_array); // delta_prime_i term at time i (degrees)
1,653✔
1083

1084
            h_prime_array[i] = limit_degrees180pm(nu_i_array[i] + lng -
3,306✔
1085
                                                  alpha_prime_array[i]); // local hour angle at time i (0-transit,1-sunrise,2-sunset) (degrees)
1,653✔
1086

1087
            h_rts_array[i] = rts_sun_altitude(lat, delta_prime_array[i],
1,653✔
1088
                                              h_prime_array[i]); //sun altitude at time i (0-transit,1-sunrise,2-sunset) (degrees)
1089
        }
1090

1091
        double srha = h_prime_array[1]; //sunrise hour angle (degrees)
551✔
1092
        double ssha = h_prime_array[2]; //sunset hour angle (degrees)
551✔
1093
        double h_rts = h_rts_array[0]; //sun altitude for sun transit (degrees)
551✔
1094

1095
        double suntransit = dayfrac_to_local_hr(m0 - h_prime_array[0] / 360, tz); //sun transit (fraction of day)
551✔
1096
        double sunrise = dayfrac_to_local_hr(
551✔
1097
                sun_rise_and_set(approx_times_array, h_rts_array, delta_prime_array, lat, h_prime_array, h0_prime, 1),
1098
                tz); //sunrise (fraction of day)
1099
        //double sunrise = 12.0 - (h0 / DTOR) / 15.0 - (lng / 15.0 - tz) - E;
1100
        //needed_values[1] = sunrise - (lng/ 15.0) - E/60; //sunrise in local standard time
1101
        needed_values[2] = sunrise;
551✔
1102

1103
        double sunset = dayfrac_to_local_hr(
551✔
1104
                sun_rise_and_set(approx_times_array, h_rts_array, delta_prime_array, lat, h_prime_array, h0_prime, 2),
1105
                tz); //sunset (fraction of day)
1106
        /*if (sunset < sunrise)
1107
        {
1108
            //double sunset_unadjusted = sun_rise_and_set(approx_times_array, h_rts_array, delta_prime_array, lat, h_prime_array, h0_prime, 2);
1109
            //sunset = 24 * (sunset_unadjusted + tz / 24);
1110
            sunset += 24;
1111
        }
1112
        //double sunset = dayfrac_to_local_hr(sun_rise_and_set(approx_times_array, h_rts_array, delta_prime_array, lat, h_prime_array, h0_prime, 2), tz); //sunset (fraction of day)
1113
        //double sunset = 12.0 + (h0 / DTOR) / 15.0 - (lng / 15.0 - tz) - E;*/
1114
        needed_values[3] = sunset;//sunrise in local standard time*/
551✔
1115

1116

1117
    }
1118
    else {
NEW
1119
        double srha = -99999; // if h0 is outside of expected values, sunrise calculations do not occur
×
NEW
1120
        double ssha = -99999;
×
NEW
1121
        double sta = -99999;
×
NEW
1122
        double suntransit = -99999;
×
NEW
1123
        double sunrise = -99999;
×
NEW
1124
        needed_values[1] = sunrise;
×
NEW
1125
        double sunset = -99999;
×
NEW
1126
        needed_values[2] = sunset;
×
1127
    }
1128
}
551✔
1129

1130
void
1131
solarpos_spa(int year, int month, int day, int hour, double minute, double second, double lat, double lng, double tz,
550✔
1132
             double dut1, double alt, double pressure, double temp, double tilt, double azm_rotation, double sunn[9], 
1133
             std::shared_ptr<solarpos_lookup> spa_table) {
1134

1135
    int t;
1136
    double delta_t;
1137
    if (year >= 1961 && year <= 1986) {
550✔
NEW
1138
        t = year - 1975;
×
NEW
1139
        delta_t = 45.45 + 1.067 * t - pow(t, 2) / 260 - pow(t, 3) / 718;
×
1140
    }
1141
    else if (year > 1986 && year <= 2005) {
550✔
NEW
1142
        t = year - 2000;
×
NEW
1143
        delta_t = 63.86 + 0.3345 * t - .060374 * pow(t, 2) + .0017275 * pow(t, 3) + 0.000651814 * pow(t, 4);
×
1144
    }
1145
    else if (year > 2005 && year <= 2050) {
550✔
1146
        t = year - 2000;
550✔
1147
        delta_t = 62.92 + 0.32217 * t + 0.005589 * pow(t, 2);
550✔
1148
    }
1149
    else {
NEW
1150
        t = 0;
×
NEW
1151
        delta_t = 66.7;
×
1152
    }
1153
    double jd = julian_day(year, month, day, hour, minute, second, dut1, tz); //julian day
550✔
1154
    double ascension_and_declination[2] {0, 0}; //preallocate storage for sun right ascension and declination (both degrees)
550✔
1155
    double needed_values_spa[9];
1156
    double needed_values_eot[4]; //preallocate storage for output from calculate_spa
1157
    double needed_values_eot_check[4];
1158

1159
    if (spa_table){
550✔
NEW
1160
        spa_table->roll_spa_table_forward(day);
×
1161
    }
1162
    calculate_spa(jd, lat, lng, alt, pressure, temp, delta_t, tilt, azm_rotation, ascension_and_declination,
550✔
1163
                  needed_values_spa, spa_table); //calculate solar position algorithm values
1164
    calculate_eot_and_sun_rise_transit_set(needed_values_spa[0], tz, ascension_and_declination[0], needed_values_spa[2],
550✔
1165
                                           needed_values_spa[3], jd, year, month, day, lat, lng, alt, pressure, temp,
1166
                                           tilt, delta_t, azm_rotation, needed_values_eot, spa_table); //calculate Equation of Time and sunrise/sunset values
1167

1168
    double __n_days[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
550✔
1169

1170
    if (needed_values_eot[3] <
550✔
1171
        needed_values_eot[2]) //sunset is legitimately the next day but we're not in endless days, so recalculate sunset from the previous day
550✔
1172
    {
1173
        double sunanglestemp[9];
1174
        if (day < __n_days[month - 1]) //simply decrement day during month
1✔
1175
            calculate_eot_and_sun_rise_transit_set(needed_values_spa[0], tz, ascension_and_declination[0],
1✔
1176
                                                   needed_values_spa[2], needed_values_spa[3], jd, year, month, day + 1,
1177
                                                   lat, lng, alt, pressure, temp, tilt, delta_t, azm_rotation,
1178
                                                   needed_values_eot_check, spa_table); //calculate Equation of Time and sunrise/sunset values
NEW
1179
        else if (month < 12) //on the 1st of the month, need to switch to the last day of previous month
×
NEW
1180
            calculate_eot_and_sun_rise_transit_set(needed_values_spa[0], tz, ascension_and_declination[0],
×
1181
                                                   needed_values_spa[2], needed_values_spa[3], jd, year, month + 1, 1,
1182
                                                   lat, lng, alt, pressure, temp, tilt, delta_t, azm_rotation,
1183
                                                   needed_values_eot_check, spa_table);
1184
        else //on the first day of the year, need to switch to Dec 31 of last year
NEW
1185
            calculate_eot_and_sun_rise_transit_set(needed_values_spa[0], tz, ascension_and_declination[0],
×
1186
                                                   needed_values_spa[2], needed_values_spa[3], jd, year + 1, 1, 1, lat,
1187
                                                   lng, alt, pressure, temp, tilt, delta_t, azm_rotation,
1188
                                                   needed_values_eot_check, spa_table);
1189

1190
        //on the last day of endless days, sunset is returned as 100 (hour angle too large for calculation), so use today's sunset time as a proxy
1191
        needed_values_eot[3] = needed_values_eot_check[3] + 24;
1✔
1192
    }
1193

1194
    double tst =
550✔
1195
            hour + minute / 60.0 + (lng / 15.0 - tz) + needed_values_eot[0] / 60; //true solar time (output of function)
550✔
1196

1197
    double zen = DTOR * needed_values_spa[7]; //zenith angle in radians
550✔
1198
    if (zen > M_PI) //check for rounding error going from degrees to radians
550✔
1199
    {
NEW
1200
        zen = M_PI;
×
1201
    }
1202
    else if (zen < 0) {
550✔
NEW
1203
        zen = 0;
×
1204
    }
1205

1206
    double Gon = 1367 * (1 + 0.033 * cos(360.0 / 365.0 * day_of_year(month, day) * M_PI /
550✔
1207
                                         180)); /* D&B eq 1.4.1a, using solar constant=1367 W/m2 */
550✔
1208
    double hextra;
1209

1210
    if (zen > 0 && zen < M_PI / 2) /* if sun is up */
550✔
1211
        hextra = Gon * cos(zen); /* elevation is incidence angle (zen=90-elv) with horizontal */
549✔
1212
    else if (zen == 0)
1✔
NEW
1213
        hextra = Gon;
×
1214
    else
1215
        hextra = 0.0;
1✔
1216

1217
    double sunrise, sunset;
1218

1219
    double h0_test = needed_values_eot[1];
550✔
1220
    if (h0_test == (180.0)) {
550✔
1221
        sunrise = -100.0;
2✔
1222
        sunset = 100.0;
2✔
1223
    }
1224
    else if (h0_test == 0) {
548✔
NEW
1225
        sunrise = 100.0;
×
NEW
1226
        sunset = -100.0;
×
1227
    }
1228
    else {
1229
        sunrise = needed_values_eot[2];
548✔
1230
        sunset = needed_values_eot[3];
548✔
1231
    }
1232

1233
    sunn[0] = DTOR *
550✔
1234
              needed_values_spa[8]; //sun azimuth in radians, measured east from north, 0 to 2*pi                  /* Variables returned in array sunn[] */
550✔
1235
    sunn[1] = zen; //sun zenith in radians           /*  Zenith */
550✔
1236
    sunn[2] = DTOR * needed_values_spa[6]; // sun elevation in radians
550✔
1237
    sunn[3] = DTOR * needed_values_spa[5]; // sun declination in radians
550✔
1238
    sunn[4] = sunrise; //sunrise in local standard time (hrs), not corrected for refraction
550✔
1239
    sunn[5] = sunset; //sunset in local standard time (hrs), not corrected for refraction
550✔
1240
    sunn[6] = needed_values_spa[01]; //eccentricity correction factor
550✔
1241
    sunn[7] = tst; //true solar time (hrs)
550✔
1242
    sunn[8] = hextra; //extraterrestrial solar irradaince on horizontal at particular time (W/m2)
550✔
1243
}
550✔
1244

1245
// Removing unused methods this way to keep file consistent with SAM's original source for easier future updates
1246
#if 0
1247

1248
void incidence(int mode, double tilt, double sazm, double rlim, double zen,
1249
               double azm, bool en_backtrack, double gcr, double slope_tilt, double slope_azm,
1250
               bool force_to_stow, double stow_angle_deg, bool useCustomAngle, double customAngle, double angle[5]) {
1251
    /*
1252
    Calculate panel orientation, angle of incidence with beam radiation, and
1253
    tracker rotation angles (where applicable).
1254
    */
1255

1256
    // Azimuth angles are for N=0 or 2pi, E=pi/2, S=pi, and W=3pi/2.  8/13/98
1257

1258
    /* Local variables: rot is the angle that the collector is rotated about the
1259
    axis when viewed from the raised end of the 1-axis tracker. If rotated
1260
    counter clockwise the angle is negative. Range is -180 to +180 degrees.
1261
    When xsazm = azm : rot = 0, tilt = xtilt, and sazm = xsazm = azm  */
1262

1263
    double arg, inc = 0, xsazm, xtilt, rot = 0, btdiff = 0, truetracking_rotation = 0;
1264

1265
    if (mode == 4)
1266
        mode = 0; //treat timeseries tilt as fixed tilt for each timestep
1267

1268
    switch (mode) {
1269
        case 0:              /* Fixed-Tilt, */
1270
        case 3:              /* or Azimuth Axis*/
1271
            tilt = tilt * DTOR;    /* Change tilt and surface azimuth to radians */
1272
            sazm = (mode == 0) ? sazm * DTOR : azm; /* either fixed surface azimuth or solar azimuth */
1273
            arg = sin(zen) * cos(azm - sazm) * sin(tilt) + cos(zen) * cos(tilt);
1274
            rot = 0;
1275
            if (arg < -1.0)
1276
                inc = M_PI;
1277
            else if (arg > 1.0)
1278
                inc = 0.0;
1279
            else
1280
                inc = acos(arg);
1281
            break;
1282
        case 1:                 /* One-Axis Tracking */
1283
            xtilt = tilt * DTOR;   /* Change axis tilt, surface azimuth, and rotation limit to radians */
1284
            xsazm = sazm * DTOR;
1285
            rlim = rlim * DTOR;
1286
            /* Find rotation angle of axis for peak tracking */
1287
            if (std::abs(cos(xtilt)) < 0.001745)    /* 89.9 to 90.1 degrees */
1288
            {          /* For vertical axis only */
1289
                if (xsazm <= M_PI) {
1290
                    if (azm <= xsazm + M_PI)
1291
                        rot = azm - xsazm;
1292
                    else
1293
                        rot = azm - xsazm - 2.0 * M_PI;
1294
                }
1295
                else        /* For xsazm > pi */
1296
                {
1297
                    if (azm >= xsazm - M_PI)
1298
                        rot = azm - xsazm;
1299
                    else
1300
                        rot = azm - xsazm + 2.0 * M_PI;
1301
                }
1302
            }
1303
            else          /* For other than vertical axis */
1304
            {
1305
                rot = truetrack(azm * 180 / M_PI, zen * 180 / M_PI, tilt, sazm);
1306
                rot *= M_PI / 180;
1307
            }
1308

1309
            truetracking_rotation = rot;
1310

1311
            if (rot < -rlim) /* Do not let rotation exceed physical constraints */
1312
                rot = -rlim;
1313
            else if (rot > rlim)
1314
                rot = rlim;
1315

1316
            //optionally force the tracker to a "stow" angle if specified
1317
            if (force_to_stow) {
1318
                rot = stow_angle_deg * DTOR;
1319
                //do not report angle difference for backtracking if forced to stow, since this isn't backtracking
1320
            }
1321
            else if (en_backtrack) {
1322
                double cross_axis_slope = calc_cross_axis_slope(slope_tilt, azm, slope_azm);
1323
                double backtracking_rotation = backtrack(truetracking_rotation * 180 / M_PI, gcr, cross_axis_slope);
1324
                backtracking_rotation *= M_PI / 180;
1325
                backtracking_rotation = backtracking_rotation > rlim ? rlim : backtracking_rotation;
1326
                backtracking_rotation = backtracking_rotation < -rlim ? -rlim : backtracking_rotation;
1327

1328
                btdiff = (backtracking_rotation - rot); // log the difference (radians)
1329
                rot = backtracking_rotation;
1330
            }
1331

1332
            /*Check if custom tracker rotation angles enabled, apply timeseries value*/
1333
            if (useCustomAngle) {
1334
                rot = customAngle * DTOR; //overwrite rotation angle with input from array
1335
            }
1336

1337
            /* Find tilt angle for the tracking surface */
1338
            arg = cos(xtilt) * cos(rot);
1339
            if (arg < -1.0)
1340
                tilt = M_PI;
1341
            else if (arg > 1.0)
1342
                tilt = 0.0;
1343
            else
1344
                tilt = acos(arg);
1345
            /* Find surface azimuth for the tracking surface */
1346
            if (tilt == 0.0)
1347
                sazm = M_PI;     /* Assign any value if tilt is zero */
1348
            else {
1349
                arg = sin(rot) / sin(tilt);
1350
                if (arg < -1.0)
1351
                    sazm = 1.5 * M_PI + xsazm;
1352
                else if (arg > 1.0)
1353
                    sazm = 0.5 * M_PI + xsazm;
1354
                else if (rot < -0.5 * M_PI)
1355
                    sazm = xsazm - M_PI - asin(arg);
1356
                else if (rot > 0.5 * M_PI)
1357
                    sazm = xsazm + M_PI - asin(arg);
1358
                else
1359
                    sazm = asin(arg) + xsazm;
1360
                if (sazm > 2.0 * M_PI)       /* Keep between 0 and 2pi */
1361
                    sazm = sazm - 2.0 * M_PI;
1362
                else if (sazm < 0.0)
1363
                    sazm = sazm + 2.0 * M_PI;
1364
            }
1365
            /* printf("zen=%6.1f azm-sazm=%6.1f tilt=%6.1f arg=%7.4f\n",zen/DTOR,(azm-sazm)/DTOR,tilt/DTOR,arg); */
1366
            /* Find incident angle */
1367
            arg = sin(zen) * cos(azm - sazm) * sin(tilt) + cos(zen) * cos(tilt);
1368
            if (arg < -1.0)
1369
                inc = M_PI;
1370
            else if (arg > 1.0)
1371
                inc = 0.0;
1372
            else
1373
                inc = acos(arg);
1374
            break;
1375
        case 2:                 /* Two-Axis Tracking */
1376
            tilt = zen;
1377
            sazm = azm;
1378
            inc = 0.0;
1379
            rot = 0.0;
1380
            break;
1381
    }
1382
    angle[0] = inc;           /* Variables returned in array angle[] */
1383
    angle[1] = tilt;
1384
    angle[2] = sazm;
1385
    angle[3] = rot;
1386
    angle[4] = btdiff;
1387
}
1388

1389
#define SMALL 1e-6
1390

1391
void hdkr(double hextra, double dn, double df, double alb, double inc, double tilt, double zen, double poa[3],
1392
          double diffc[3] /* can be null */) {
1393
    /* added aug2011 by aron dobos. Defines Hay, Davies, Klutcher, Reindl model for diffuse irradiance on a tilted surface
1394

1395
        List of Parameters Passed to Function:
1396
        hextra = extraterrestrial irradiance on horizontal surface (W/m2)
1397
        dn     = direct normal radiation (W/m2)
1398
        df     = diffuse horizontal radiation (W/m2)
1399
        alb    = surface albedo (decimal fraction)
1400
        inc    = incident angle of direct beam radiation to surface in radians
1401
        tilt   = surface tilt angle from horizontal in radians
1402
        zen    = sun zenith angle in radians
1403

1404
        Variable Returned
1405
        poa    = plane-of-array irradiances (W/m2)
1406
                    poa[0]: incident beam
1407
                    poa[1]: incident sky diffuse
1408
                    poa[2]: incident ground diffuse
1409

1410
        diffc   = diffuse components, if an array is provided
1411
                    diffc[0] = isotropic
1412
                    diffc[1] = circumsolar
1413
                    diffc[2] = horizon brightening*/
1414

1415
    double hb = dn * cos(zen); /* beam irradiance on horizontal */
1416
    double ht = hb + df; /* total irradiance on horizontal */
1417
    if (ht < SMALL) ht = SMALL;
1418
    if (hextra < SMALL) hextra = SMALL;
1419

1420
    double Rb = cos(inc) / cos(zen); /* ratio of beam on surface to beam on horizontal (D&B eq 1.8.1) */
1421
    double Ai = hb / hextra; /* anisotropy index, term for forward scattering circumsolar diffuse (D&B eq 2.16.3) */
1422
    double f = sqrt(hb / ht); /* modulating factor for horizontal brightening correction */
1423
    double s3 = pow(sin(tilt * 0.5), 3); /* horizontal brightening correction */
1424

1425
    /* see ESTIMATING DIFFUSE RADIATION ON HORIZONTAL SURFACES AND TOTAL RADIATION ON TILTED SURFACES
1426
         Master's Thesis, Douglas T Reindl, 1988, U.Wisc-Madison, Solar Energy Laboratory, http://sel.me.wisc.edu/publications/theses/reindl88.zip */
1427

1428
    double cir = df * Ai * Rb;
1429
    double iso = df * (1 - Ai) * 0.5 * (1 + cos(tilt));
1430
    double isohor = df * (1.0 - Ai) * 0.5 * (1.0 + cos(tilt)) * (1.0 + f * s3);
1431

1432
    poa[0] = dn * cos(inc);
1433
    poa[1] = isohor + cir;
1434
    poa[2] = (hb + df) * alb * (1.0 - cos(tilt)) / 2.0;
1435

1436
    //prevent from returning negative poa values, added by jmf 7/28/14
1437
    if (poa[0] < 0) poa[0] = 0;
1438
    if (poa[1] < 0) poa[1] = 0;
1439
    if (poa[2] < 0) poa[2] = 0;
1440

1441
    if (diffc != nullptr) {
1442
        diffc[0] = iso;
1443
        diffc[1] = cir;
1444
        diffc[2] = isohor - iso;
1445
    }
1446
}
1447

1448
double Min(double v1, double v2) {
1449

1450
    // Check if both are NAN
1451

1452
    if (v1 != v1 && v2 != v2) return NAN;
1453

1454
    if (v1 <= v2) return v1;
1455
    else return v2;
1456
}
1457

1458
double Max(double v1, double v2) {
1459

1460
    // Check if both are NAN
1461

1462
    if (v1 != v1 && v2 != v2) return NAN;
1463

1464
    if (v1 >= v2) return v1;
1465
    else return v2;
1466
}
1467

1468
double GTI_DIRINT(const double poa[3], const double inc[3], double zen, double tilt, double ext, double alb, int doy,
1469
                  double tDew, double elev, double &dnOut, double &dfOut, double &ghOut, double poaCompOut[3]) {
1470

1471
    double diff = 1E6;
1472
    double bestDiff = 1E6;
1473
    double Ktp = 0;
1474
    double GTI[] = {poa[0], poa[1], poa[2]};
1475

1476
    double Ci[30] = {1., 1., 1., 0.5, 0.5,
1477
                     0.5, 0.5, 0.5, 0.5, 0.5,
1478
                     0.25, 0.25, 0.25, 0.25, 0.25,
1479
                     0.25, 0.25, 0.25, 0.25, 0.25,
1480
                     0.125, 0.125, 0.125, 0.125, 0.125,
1481
                     0.125, 0.125, 0.125, 0.125, 0.125};
1482

1483
    double poa_tmp[3], diffc_tmp[3], poaBest[3] = {0, 0, 0};
1484

1485
    // Begin iterative solution for Kt
1486
//        double Io = 1367.0 * (1.0 + 0.033 * cos(0.0172142 * doy));    // Extraterrestrial dn (Taken from DIRINT Model)
1487
    double cz = cos(zen);
1488
    int i = 0;
1489

1490
    while (std::abs(diff) > 1.0 && i++ < 30) {
1491

1492
        // Calculate Kt using GTI and Eq. 2
1493
//                double Kt_inc = GTI[1] / (Io * Max(0.065, cos(inc[1])));
1494

1495
        //Calculate DNI using Kt and DIRINT Eq.s
1496
        double dn_tmp;
1497
        double Ktp_tmp = ModifiedDISC(GTI, inc, tDew, elev, doy, dn_tmp);
1498

1499
        //Calculate DHI using Eq. 3
1500
        double df_tmp = GTI[1] * Max(0.065, cz) / Max(0.065, cos(inc[1])) - dn_tmp * cz;
1501
        // if( df_tmp < 0 ) df_tmp = 0; //jmf removed 11/30/18 to allow error to be reported by poaDecomp if calculated dn is negative
1502

1503
        //Model POA using Perez model and find diff from GTI (Model - GTI)
1504
        perez(ext, dn_tmp, df_tmp, alb, inc[1], tilt, zen, poa_tmp, diffc_tmp);
1505

1506
        //Compare modeled POA to measured POA
1507
        diff = (poa_tmp[0] + poa_tmp[1] + poa_tmp[2]) - poa[1];
1508

1509
        //Check for best Difference. If found, save results
1510
        if (std::abs(diff) < std::abs(bestDiff)) {
1511
            bestDiff = diff;
1512
            Ktp = Ktp_tmp;
1513
            dnOut = dn_tmp;
1514
            dfOut = df_tmp;
1515
            poaBest[0] = poa_tmp[0];
1516
            poaBest[1] = poa_tmp[1];
1517
            poaBest[2] = poa_tmp[2];
1518
        }
1519

1520
        // Adjust GTI using Eq. 4
1521
        // Apply the same change to previous/ subsequent GTI's as well (based on Bill's email)
1522
        GTI[0] = Max(1.0, GTI[0] - Ci[i] * diff);
1523
        GTI[1] = Max(1.0, GTI[1] - Ci[i] * diff);
1524
        GTI[2] = Max(1.0, GTI[2] - Ci[i] * diff);
1525

1526
    }
1527

1528
    poaCompOut[0] = poaBest[0];
1529
    poaCompOut[1] = poaBest[1];
1530
    poaCompOut[2] = poaBest[2];
1531

1532
    ghOut = dnOut * cos(inc[1]) + dfOut;
1533

1534
    return Ktp;
1535
}
1536

1537
int poaDecomp(double, double angle[], double sun[], double alb, poaDecompReq *pA, double &dn, double &df, double &gh,
1538
              double poa[3], double diffc[3]) {
1539

1540
    int errorcode = 0; //code to return whether the decomposition method succeeded or failed
1541

1542
    /* Decomposes POA into direct normal and diffuse irradiances */
1543

1544
    double r90(M_PI / 2), r80(80.0 / 180 * M_PI), r65(65.0 / 180 * M_PI);
1545

1546
    if (angle[0] < r90) {  // Check if incident angle if greater than 90 degrees
1547

1548
        double gti[] = {pA->POA[pA->i - 1], pA->POA[pA->i], pA->POA[pA->i + 1]};
1549
        double inc[] = {pA->inc[pA->i - 1], pA->inc[pA->i], pA->inc[pA->i + 1]};
1550

1551
        GTI_DIRINT(gti, inc, sun[1], angle[1], sun[8], alb, pA->doy, pA->tDew, pA->elev, dn, df, gh, poa);
1552

1553
    }
1554
    else {
1555

1556
        size_t stepsInDay = 24;
1557
        if (pA->stepScale == 'm') {
1558
            stepsInDay *= 60 / (unsigned int) pA->stepSize;
1559
        }
1560

1561
        size_t noon = pA->dayStart + stepsInDay / 2;
1562
        size_t start, stop;
1563
        // Check for a morning value or evening, set looping bounds accordingly
1564
        if (pA->i < noon) { // Calculate morning value
1565
            start = pA->dayStart;
1566
            stop = noon;
1567
        }
1568
        else {
1569
            start = noon;
1570
            stop = pA->dayStart + stepsInDay;
1571
        }
1572

1573

1574
        // Determine an average Kt prime value
1575
        int count = 0;
1576
        double avgKtp = 0;
1577

1578
        for (size_t j = start; j < stop; j++) {
1579

1580

1581
            if ((pA->inc[j] < r80) && (pA->inc[j] > r65)) {
1582
                count++;
1583
                double gti[] = {pA->POA[j - 1], pA->POA[j], pA->POA[j + 1]};
1584
                double inc[] = {pA->inc[j - 1], pA->inc[j], pA->inc[j + 1]};
1585

1586
                double dnTmp, dfTmp, ghTmp, poaTmp[3];
1587
                avgKtp += GTI_DIRINT(gti, inc, pA->zen[j], pA->tilt[j], pA->exTer[j], alb, pA->doy, pA->tDew, pA->elev,
1588
                                     dnTmp, dfTmp, ghTmp, poaTmp);
1589
            }
1590
        }
1591
        // fails when count = 0;
1592
        //avgKtp /= count;
1593
        if (count > 0)
1594
            avgKtp /= count;
1595

1596
        //Calculate Kt
1597
        double am = Min(15.25, 1.0 / (cos(sun[1]) + 0.15 * (pow(93.9 - sun[1] * 180 / M_PI, -1.253)))); // air mass
1598
        double ktpam = am * exp(-0.0001184 * pA->elev);
1599
        double Kt = avgKtp * (1.031 * exp(-1.4 / (0.9 + 9.4 / ktpam)) + 0.1);
1600

1601
        //Calculate DNI using DIRINT
1602
        double Kt_[3] = {-999, Kt, -999};
1603
        double Ktp_[3] = {-999, avgKtp, -999};
1604
        double gti[3] = {-999, pA->POA[pA->i], -999};
1605
        double zen[3] = {-999, sun[1], -999}; // Might need to be Zenith angle instead of inciden
1606

1607
        ModifiedDISC(Kt_, Ktp_, gti, zen, pA->tDew, pA->elev, pA->doy, dn);
1608

1609
        // Calculate DHI and GHI
1610
        double ct = cos(angle[1]);
1611
        df = (2 * pA->POA[pA->i] - dn * cos(sun[1]) * alb * (1 - ct)) / (1 + ct + alb * (1 - ct));
1612
        gh = dn * cos(angle[0]) + df;
1613

1614
        // Get component poa from Perez
1615
        perez(sun[8], dn, df, alb, angle[0], angle[1], sun[1], poa, diffc);
1616

1617
    }
1618

1619
    //Check for bad values and return an error code as applicable
1620
    if (gh < 0) {
1621
        gh = 0;
1622
        errorcode = 42;
1623
    }
1624
    if (df < 0) //check for df before gh because gh is only calculated using dn and df, so is least likely to be the actual culprit
1625
    {
1626
        df = 0;
1627
        errorcode = 41;
1628
    }
1629
    if (dn < 0) //check for dn last because it is the most likely culprit of the problem
1630
    {
1631
        dn = 0;
1632
        errorcode = 40;
1633
    }
1634

1635
    return errorcode;
1636
}
1637

1638
void isotropic(double, double dn, double df, double alb, double inc, double tilt, double zen, double poa[3],
1639
               double diffc[3]) {
1640
    /* added aug2011 by aron dobos. Defines isotropic sky model for diffuse irradiance on a tilted surface
1641

1642
        List of Parameters Passed to Function:
1643
        hextra = extraterrestrial irradiance on horizontal surface (W/m2) (unused for isotropic sky)
1644
        dn     = direct normal radiation (W/m2)
1645
        df     = diffuse horizontal radiation (W/m2)
1646
        alb    = surface albedo (decimal fraction)
1647
        inc    = incident angle of direct beam radiation to surface in radians
1648
        tilt   = surface tilt angle from horizontal in radians
1649
        zen    = sun zenith angle in radians
1650

1651
        Variable Returned
1652
        poa    = plane-of-array irradiances (W/m2)
1653
                    poa[0]: incident beam
1654
                    poa[1]: incident sky diffuse
1655
                    poa[2]: incident ground diffuse
1656

1657
        diffc   = diffuse components, if an array is provided
1658
                    diffc[0] = isotro        angle[]  = array of elements to return angles to calling function
1659
        angle[0] = inc  = incident angle in radians
1660
        angle[1] = tilt = tilt angle of surface from horizontal in radians
1661
        angle[2] = sazm = surface azimuth in radians, measured east from north
1662
        angle[3] = rot = tracking axis rotation angle in radians, measured from surface normal of unrotating axis (only for 1 axis trackers)
1663
        angle[4] = btdiff = (rot - ideal_rot) will be zero except in case of backtracking for 1 axis trackingpic
1664
                    diffc[1] = circumsolar
1665
                    diffc[2] = horizon brightening
1666
                    */
1667

1668
    poa[0] = dn * cos(inc);
1669
    poa[1] = df * (1.0 + cos(tilt)) / 2.0;
1670
    poa[2] = (dn * cos(zen) + df) * alb * (1.0 - cos(tilt)) / 2.0;
1671

1672
    //prevent from returning negative poa values, added by jmf 7/28/14
1673
    if (poa[0] < 0) poa[0] = 0;
1674
    if (poa[1] < 0) poa[1] = 0;
1675
    if (poa[2] < 0) poa[2] = 0;
1676

1677
    if (diffc != 0) {
1678
        diffc[0] = poa[1];
1679
        diffc[1] = 0; // no circumsolar
1680
        diffc[2] = 0; // no horizon brightening
1681
    }
1682
}
1683

1684
void
1685
perez(double, double dn, double df, double alb, double inc, double tilt, double zen, double poa[3], double diffc[3]) {
1686
    /*
1687
        Based on original FORTRAN program by Howard Bisner.
1688
        Total POA is poa[0]+poa[1]+poa[2]
1689
        Modified aug2011 by aron dobos to split out beam, diffuse, ground for output.
1690
        Modified 6/10/98 so that for zenith angles between 87.5 and 90.0 degrees,
1691
        the diffuse radiation is treated as isotropic instead of 0.0.
1692

1693
    */
1694

1695
    /* Local variables */
1696
    double F11R[8] = {-0.0083117, 0.1299457, 0.3296958, 0.5682053,
1697
                      0.8730280, 1.1326077, 1.0601591, 0.6777470};
1698
    double F12R[8] = {0.5877285, 0.6825954, 0.4868735, 0.1874525,
1699
                      -0.3920403, -1.2367284, -1.5999137, -0.3272588};
1700
    double F13R[8] = {-0.0620636, -0.1513752, -0.2210958, -0.2951290,
1701
                      -0.3616149, -0.4118494, -0.3589221, -0.2504286};
1702
    double F21R[8] = {-0.0596012, -0.0189325, 0.0554140, 0.1088631,
1703
                      0.2255647, 0.2877813, 0.2642124, 0.1561313};
1704
    double F22R[8] = {0.0721249, 0.0659650, -0.0639588, -0.1519229,
1705
                      -0.4620442, -0.8230357, -1.1272340, -1.3765031};
1706
    double F23R[8] = {-0.0220216, -0.0288748, -0.0260542, -0.0139754,
1707
                      0.0012448, 0.0558651, 0.1310694, 0.2506212};
1708
    double EPSBINS[7] = {1.065, 1.23, 1.5, 1.95, 2.8, 4.5, 6.2};
1709
    double B2 = 0.000005534,
1710
            EPS, T, D, DELTA, A, B, C, ZH, F1, F2, COSINC, x;
1711
    double CZ, ZC, ZENITH, AIRMASS;
1712

1713
    int i;
1714

1715
    if (diffc != 0)
1716
        diffc[0] = diffc[1] = diffc[2] = 0.0;
1717

1718
    if (dn < 0.0)           /* Negative values may be measured if cloudy */
1719
        dn = 0.0;
1720

1721
    if (zen < 0.0 || zen > 1.5271631) /* Zen not between 0 and 87.5 deg */
1722
    {
1723
        if (df < 0.0)
1724
            df = 0.0;
1725
        if (cos(inc) > 0.0 && zen < 1.5707963)  /* Zen between 87.5 and 90 */
1726
        {                                      /* and incident < 90 deg   */
1727
            poa[0] = dn * cos(inc);
1728
            poa[1] = df * (1.0 + cos(tilt)) / 2.0;
1729
            poa[2] = 0.0;
1730

1731
            if (diffc != 0) diffc[0] = poa[1]; /* isotropic only */
1732
            return;
1733
        }
1734
        else {
1735
            poa[0] = 0;
1736
            poa[1] = df * (1.0 + cos(tilt)) / 2.0;   /* Isotropic diffuse only */
1737
            poa[2] = 0.0;
1738

1739
            if (diffc != 0) diffc[0] = poa[1]; /* isotropic only */
1740
            return;
1741
        }
1742
    }
1743
    else                      /* Zen between 0 and 87.5 deg */
1744
    {
1745
        CZ = cos(zen);
1746
        ZH = (CZ > 0.0871557) ? CZ : 0.0871557;    /* Maximum of 85 deg */
1747
        D = df;                /* Horizontal diffuse radiation */
1748
        if (D <= 0.0)        /* Diffuse is zero or less      */
1749
        {
1750
            if (cos(inc) > 0.0)    /* Incident < 90 deg */
1751
            {
1752
                poa[0] = dn * cos(inc);
1753
                poa[1] = 0.0;
1754
                poa[2] = 0.0;
1755
                return;
1756
            }
1757
            else {
1758
                poa[0] = 0;
1759
                poa[1] = 0;
1760
                poa[2] = 0;
1761
                return;
1762
            }
1763
        }
1764
        else                   /* Diffuse is greater than zero */
1765
        {
1766
            ZENITH = zen / DTOR;
1767
            AIRMASS = 1.0 / (CZ + 0.15 * pow(93.9 - ZENITH, -1.253));
1768
            DELTA = D * AIRMASS / 1367.0;
1769
            T = pow(ZENITH, 3.0);
1770
            EPS = (dn + D) / D;
1771
            EPS = (EPS + T * B2) / (1.0 + T * B2);
1772
            i = 0;
1773
            while (i < 7 && EPS > EPSBINS[i])
1774
                i++;
1775
            x = F11R[i] + F12R[i] * DELTA + F13R[i] * zen;
1776
            F1 = (0.0 > x) ? 0.0 : x;
1777
            F2 = F21R[i] + F22R[i] * DELTA + F23R[i] * zen;
1778
            COSINC = cos(inc);
1779
            if (COSINC < 0.0)
1780
                ZC = 0.0;
1781
            else
1782
                ZC = COSINC;
1783

1784
            // apd 7oct2011: reorganized from original pvwatts code
1785
            // see duffie&beckman 2006, eqn 2.16.14
1786
            A = D * (1 - F1) * (1.0 + cos(tilt)) / 2.0; // isotropic diffuse
1787
            B = D * F1 * ZC / ZH; // circumsolar diffuse
1788
            C = D * F2 * sin(tilt); // horizon brightness term
1789

1790
            if (diffc != 0) {
1791
                diffc[0] = A;
1792
                diffc[1] = B;
1793
                diffc[2] = C;
1794
            }
1795

1796
            // original PVWatts: poa = A + F1*B + F2*C + alb*(dn*CZ+D)*(1.0 - cos(tilt) )/2.0 + dn*ZC;
1797
            poa[0] = dn * ZC; // beam
1798
            poa[1] = A + B + C; // total sky diffuse
1799
            poa[2] = alb * (dn * CZ + D) * (1.0 - cos(tilt)) / 2.0; // ground diffuse
1800
            return;
1801
        }
1802
    }
1803
}
1804

1805
void ineichen(double clearsky_results[3], double apparent_zenith, int month, int day, double pressure = 101325.0, double linke_turbidity = 1.0, double altitude = 0.0, double dni_extra = 1364.0, bool perez_enhancement = false) {
1806
    double cos_zenith = Max(cosd(apparent_zenith), 0);
1807
    double tl = linke_turbidity;
1808

1809
    double fh1 = exp(-altitude / 8000.0);
1810
    double fh2 = exp(-altitude / 1250.0);
1811
    double cg1 = 5.09e-5 * altitude + 0.868;
1812
    double cg2 = 3.92e-5 * altitude + 0.0387;
1813

1814
    //double am = Min(15.25, 1.0 / (cosd(apparent_zenith) + 0.15 * (pow(93.9 - apparent_zenith, -1.253)))); // air mass
1815
    double am = Min(15.25, 1.0 / (cosd(apparent_zenith) + 0.50572 * (pow(6.07995 + (90 - apparent_zenith), -1.6364)))); // air mass kastenyoung1989 pvlib
1816
    //am = 1.5;
1817
    double abs_am = am * pressure / 101325.0;
1818

1819
    double ghi = exp(-cg2 * abs_am * (fh1 + fh2 * (tl - 1)));
1820
    if (perez_enhancement) ghi *= exp(0.01 * pow(abs_am, 1.8));
1821

1822
    double Gon = 1367 * (1 + 0.033 * cos(360.0 / 365.0 * day_of_year(month, day) * M_PI /
1823
        180));
1824

1825
    if (dni_extra != 0) Gon = dni_extra;
1826

1827
    ghi = cg1 * Gon * cos_zenith * tl / tl * Max(ghi, 0);
1828

1829
    double b = 0.664 + 0.163 / fh1;
1830

1831
    double bnci = b * exp(-0.09 * abs_am * (tl - 1));
1832
    bnci = Gon * Max(bnci, 0);
1833

1834
    double bnci_2 = ((1 - (0.1 - 0.2 * exp(-tl)) / (0.1 + 0.882 / fh1)) / cos_zenith);
1835
    bnci_2 = ghi * Min(Max(bnci_2, 0), 1e20);
1836

1837
    double dni = Min(bnci, bnci_2);
1838

1839
    double dhi = ghi - dni * cos_zenith;
1840
    clearsky_results[0] = ghi;
1841
    clearsky_results[1] = dni;
1842
    clearsky_results[2] = dhi;
1843
    return;
1844
}
1845

1846

1847
void irrad::setup() {
1848
    year = month = day = hour = -999;
1849
    minute = delt = latitudeDegrees = longitudeDegrees = timezone = -999;
1850

1851
    // defaults for optional inputs
1852
    elevation = 0;
1853
    pressure = 1013.25;
1854
    tamb = 15;
1855

1856
    globalHorizontal = directNormal = diffuseHorizontal = -999;
1857

1858
    for (int i = 0; i < 9; i++) {
1859
        sunAnglesRadians[i] = std::numeric_limits<double>::quiet_NaN();
1860

1861
    }
1862
    surfaceAnglesRadians[0] = surfaceAnglesRadians[1] = surfaceAnglesRadians[2] = surfaceAnglesRadians[3] = surfaceAnglesRadians[4] = std::numeric_limits<double>::quiet_NaN();
1863
    planeOfArrayIrradianceFront[0] = planeOfArrayIrradianceFront[1] = planeOfArrayIrradianceFront[2] = diffuseIrradianceFront[0] = diffuseIrradianceFront[1] = diffuseIrradianceFront[2] = std::numeric_limits<double>::quiet_NaN();
1864
    planeOfArrayIrradianceRear[0] = planeOfArrayIrradianceRear[1] = planeOfArrayIrradianceRear[2] = diffuseIrradianceRear[0] = diffuseIrradianceRear[1] = diffuseIrradianceRear[2] = std::numeric_limits<double>::quiet_NaN();
1865
    timeStepSunPosition[0] = timeStepSunPosition[1] = timeStepSunPosition[2] = -999;
1866
    planeOfArrayIrradianceRearAverage = 0;
1867

1868
    calculatedDirectNormal = directNormal;
1869
    calculatedDiffuseHorizontal = 0.0;
1870
    poaRearGroundReflected = 0.;
1871
    poaRearDirectDiffuse = 0.;
1872
    poaRearRowReflections = 0.;
1873
    poaRearSelfShaded = 0.;
1874
    useCustomRotAngles = 0.;
1875
}
1876

1877
irrad::irrad() {
1878
    setup();
1879
    spa_table = std::make_shared<solarpos_lookup>(solarpos_lookup());
1880
}
1881

1882
irrad::irrad(weather_header hdr,
1883
             int skyModelIn, int radiationModeIn, int trackModeIn,
1884
             bool instantaneousWeather, bool backtrackingEnabled, bool forceToStowIn,
1885
             double dtHour, double tiltDegreesIn, double azimuthDegreesIn, double trackerRotationLimitDegreesIn, double stowAngleDegreesIn,
1886
             double groundCoverageRatioIn, double slopeTiltIn, double slopeAzmIn, poaDecompReq *poaAllIn, bool enableSubhourlyClipping) :
1887
        skyModel(skyModelIn), radiationMode(radiationModeIn), trackingMode(trackModeIn),
1888
        enableBacktrack(backtrackingEnabled), forceToStow(forceToStowIn),
1889
        delt(dtHour), tiltDegrees(tiltDegreesIn), surfaceAzimuthDegrees(azimuthDegreesIn),
1890
        rotationLimitDegrees(trackerRotationLimitDegreesIn),
1891
        stowAngleDegrees(stowAngleDegreesIn), groundCoverageRatio(groundCoverageRatioIn), slopeTilt(slopeTiltIn), slopeAzm(slopeAzmIn), poaAll(poaAllIn) {
1892
    setup();
1893

1894
    delt = instantaneousWeather ? IRRADPROC_NO_INTERPOLATE_SUNRISE_SUNSET : dtHour;
1895

1896
    set_sky_model(skyModel, albedo, albedoSpatial);
1897

1898
    set_subhourly_clipping(enableSubhourlyClipping);
1899

1900
    set_location(hdr.lat, hdr.lon, hdr.tz);
1901
}
1902

1903
int irrad::check() {
1904
    if (year < 0 || month < 0 || day < 0 || hour < 0 || minute < 0 || delt > 1 ) {
1905
        errorMessage = util::format("Invalid year (%d), month (%d), hour (%d), minute (%d) data, or invalid time step (%lg) hours", year, month, day, hour, minute, delt);
1906
        return -1;
1907
    }
1908
    if (latitudeDegrees < -90 || latitudeDegrees > 90 || longitudeDegrees < -180 || longitudeDegrees > 180 ||
1909
        timezone < -15 || timezone > 15) {
1910
        errorMessage = util::format("Invalid latitude (%lg), longitude (%lg), or time zone (%lg), latitude must be between -90 and 90 degrees, longitude must be between -180 and 180 degrees, time zone must be between -15 and 15", latitudeDegrees, longitudeDegrees, timezone);
1911
        return -2;
1912
    }
1913
    if (radiationMode < irrad::DN_DF || radiationMode > irrad::POA_P || skyModel < 0 || skyModel > 2) {
1914
        errorMessage = util::format("Invalid radiation mode (%d) or sky model (%d)", radiationMode, skyModel);
1915
        return -3;
1916
    }
1917
    if (trackingMode < 0 || trackingMode > 4) {
1918
        errorMessage = util::format("Invalid tracking mode (%d)", trackingMode);
1919
        return -4;
1920
    }
1921
    if (radiationMode == irrad::DN_DF &&
1922
        (directNormal < 0 || directNormal > irrad::irradiationMax || diffuseHorizontal < 0 || diffuseHorizontal > 1500)) {
1923
        errorMessage = util::format("Invalid DNI (%lg) or DHI (%lg), DNI must be between 0 and %lg W/m2, DHI must be between 0 and 1500 W/m2", directNormal, diffuseHorizontal, irrad::irradiationMax);
1924
        return -5;
1925
    }
1926
    if (radiationMode == irrad::DN_GH &&
1927
        (globalHorizontal < 0 || globalHorizontal > 1500 || directNormal < 0 || directNormal > 1500)) {
1928
        errorMessage = util::format("Invalid DNI (%lg) or GHI (%lg), DNI must be between 0 and %lg W/m2, GHI must be between 0 and 1500 W/m2", directNormal, diffuseHorizontal, irrad::irradiationMax);
1929
        return -6;
1930
    }
1931
    if (albedo < 0 || albedo > 1) {
1932
        errorMessage = util::format("Invalid albedo (%lg), must be between 0 and 1", albedo);
1933
        return -7;
1934
    }
1935
    if (tiltDegrees < 0 || tiltDegrees > 90) {
1936
        errorMessage = util::format("Invalid tilt angle (%lg), must be between 0 and 90 degrees", tiltDegrees);
1937
        return -8;
1938
    }
1939
    if (surfaceAzimuthDegrees < 0 || surfaceAzimuthDegrees >= 360) {
1940
        errorMessage = util::format("Invalid azimuth (%lg), must be between 0 and 360 degrees", surfaceAzimuthDegrees);
1941
        return -9;
1942
    }
1943
    if (rotationLimitDegrees < -90 || rotationLimitDegrees > 90) {
1944
        errorMessage = util::format("Invalid tracker rotation limit (%lg), must be between -90 and 90 degrees", rotationLimitDegrees);
1945
        return -10;
1946
    }
1947
    if (stowAngleDegrees < -90 || stowAngleDegrees > 90) {
1948
        errorMessage = util::format("Invalid stow angle (%lg), must be between -90 and 90 degrees", stowAngleDegrees);
1949
        return -12;
1950
    }
1951
    if (radiationMode == irrad::GH_DF &&
1952
        (globalHorizontal < 0 || globalHorizontal > 1500 || diffuseHorizontal < 0 || diffuseHorizontal > 1500)) {
1953
        errorMessage = util::format("Invalid GHI (%lg) or DHI (%lg), must be between 0 and 1500 W/m2", globalHorizontal, diffuseHorizontal);
1954
        return -11;
1955
    }
1956

1957
    return 0;
1958
}
1959

1960
std::string irrad::getErrorMessage() {
1961
    return errorMessage;
1962
}
1963

1964
void irrad::setup_solarpos_outputs_for_lifetime(size_t ts_per_year) {
1965
    solarpos_outputs_for_lifetime.resize(ts_per_year);
1966
}
1967

1968
bool irrad::getStoredSolarposOutputs() {
1969
    if (solarpos_outputs_for_lifetime.size() == 0) return false;
1970

1971
    size_t timeIndex = util::yearIndex(0, this->month, this->day, this->hour, this->minute, solarpos_outputs_for_lifetime.size() / 8760);
1972
    
1973
    auto& outputs = solarpos_outputs_for_lifetime[timeIndex];
1974
    if (outputs.empty()) return false;
1975

1976
    timeStepSunPosition[0] = (int)outputs[0];
1977
    timeStepSunPosition[1] = (int)outputs[1];
1978
    timeStepSunPosition[2] = (int)outputs[2];
1979
    sunAnglesRadians[0] = outputs[3];
1980
    sunAnglesRadians[1] = outputs[4];
1981
    sunAnglesRadians[2] = outputs[5];
1982
    sunAnglesRadians[3] = outputs[6];
1983
    sunAnglesRadians[4] = outputs[7];
1984
    sunAnglesRadians[5] = outputs[8];
1985
    sunAnglesRadians[6] = outputs[9];
1986
    sunAnglesRadians[7] = outputs[10];
1987
    sunAnglesRadians[8] = outputs[11];
1988
    return true;
1989
}
1990

1991
void irrad::storeSolarposOutputs() {
1992
    if (!solarpos_outputs_for_lifetime.size()) return;
1993

1994
    size_t timeIndex = util::yearIndex(0, this->month, this->day, this->hour, this->minute, solarpos_outputs_for_lifetime.size() / 8760);
1995
    auto& outputs = solarpos_outputs_for_lifetime[timeIndex];
1996
    if (!outputs.empty()) return;
1997

1998
    outputs = {
1999
        (double)timeStepSunPosition[0], (double)timeStepSunPosition[1], (double)timeStepSunPosition[2],
2000
        sunAnglesRadians[0], sunAnglesRadians[1], sunAnglesRadians[2],
2001
        sunAnglesRadians[3], sunAnglesRadians[4], sunAnglesRadians[5],
2002
        sunAnglesRadians[6], sunAnglesRadians[7], sunAnglesRadians[8]
2003
    };
2004
    // solarpos_outputs_for_lifetime[timeIndex] = outputs;
2005
}
2006

2007
double irrad::getAlbedo() {
2008
    return albedo;
2009
}
2010

2011
std::vector<double> irrad::getAlbedoSpatial() {
2012
    return albedoSpatial;
2013
}
2014

2015
double irrad::get_sunpos_calc_hour() {
2016
    return ((double) timeStepSunPosition[0]) + ((double) timeStepSunPosition[1]) / 60.0;
2017
}
2018

2019
void irrad::get_sun(double *solazi,
2020
                    double *solzen,
2021
                    double *solelv,
2022
                    double *soldec,
2023
                    double *sunrise,
2024
                    double *sunset,
2025
                    int *sunup,
2026
                    double *eccfac,
2027
                    double *tst,
2028
                    double *hextra) {
2029
    if (solazi != 0) *solazi = sunAnglesRadians[0] * (180 / M_PI);
2030
    if (solzen != 0) *solzen = sunAnglesRadians[1] * (180 / M_PI);
2031
    if (solelv != 0) *solelv = sunAnglesRadians[2] * (180 / M_PI);
2032
    if (soldec != 0) *soldec = sunAnglesRadians[3] * (180 / M_PI);
2033
    if (sunrise != 0) *sunrise = sunAnglesRadians[4];
2034
    if (sunset != 0) *sunset = sunAnglesRadians[5];
2035
    if (sunup != 0) *sunup = timeStepSunPosition[2];
2036
    if (eccfac != 0) *eccfac = sunAnglesRadians[6];
2037
    if (tst != 0) *tst = sunAnglesRadians[7];
2038
    if (hextra != 0) *hextra = sunAnglesRadians[8];
2039
}
2040

2041
void irrad::get_angles(double *aoi,
2042
                       double *surftilt,
2043
                       double *surfazi,
2044
                       double *axisrot,
2045
                       double *btdiff) {
2046
    if (aoi != 0) *aoi = surfaceAnglesRadians[0] * (180 / M_PI);
2047
    if (surftilt != 0) *surftilt = surfaceAnglesRadians[1] * (180 / M_PI);
2048
    if (surfazi != 0) *surfazi = surfaceAnglesRadians[2] * (180 / M_PI);
2049
    if (axisrot != 0) *axisrot = surfaceAnglesRadians[3] * (180 / M_PI);
2050
    if (btdiff != 0) *btdiff = surfaceAnglesRadians[4] * (180 / M_PI);
2051
}
2052

2053
void irrad::get_poa(double *beam, double *skydiff, double *gnddiff,
2054
                    double *isotrop, double *circum, double *horizon) {
2055
    if (beam != 0) *beam = planeOfArrayIrradianceFront[0];
2056
    if (skydiff != 0) *skydiff = planeOfArrayIrradianceFront[1];
2057
    if (gnddiff != 0) *gnddiff = planeOfArrayIrradianceFront[2];
2058
    if (isotrop != 0) *isotrop = diffuseIrradianceFront[0];
2059
    if (circum != 0) *circum = diffuseIrradianceFront[1];
2060
    if (horizon != 0) *horizon = diffuseIrradianceFront[2];
2061
}
2062

2063
void irrad::get_poa_clearsky(double* beam, double* skydiff, double* gnddiff,
2064
    double* isotrop, double* circum, double* horizon) {
2065
    if (beam != 0) *beam = planeOfArrayIrradianceFrontCS[0];
2066
    if (skydiff != 0) *skydiff = planeOfArrayIrradianceFrontCS[1];
2067
    if (gnddiff != 0) *gnddiff = planeOfArrayIrradianceFrontCS[2];
2068
    if (isotrop != 0) *isotrop = diffuseIrradianceFrontCS[0];
2069
    if (circum != 0) *circum = diffuseIrradianceFrontCS[1];
2070
    if (horizon != 0) *horizon = diffuseIrradianceFrontCS[2];
2071
}
2072

2073

2074
double irrad::get_poa_rear() {
2075
    return planeOfArrayIrradianceRearAverage;
2076
}
2077

2078
double irrad::get_poa_rear_clearsky() {
2079
    return planeOfArrayIrradianceRearAverageCS;
2080
}
2081

2082
std::vector<double> irrad::get_poa_rear_spatial() {
2083
    return planeOfArrayIrradianceRearSpatial;
2084
}
2085

2086
double irrad::get_ground_incident() {
2087
    // Returns average irradiance incident on the ground
2088
    double ground_incident = 0.;
2089
    for (size_t i = 0; i < groundIrradianceSpatial.size(); i++) {
2090
        ground_incident += groundIrradianceSpatial.at(i) / groundIrradianceSpatial.size();
2091
    }
2092
    return ground_incident;
2093
}
2094

2095
std::vector<double> irrad::get_ground_spatial() {
2096
    return groundIrradianceSpatial;
2097
}
2098

2099
double irrad::get_ground_absorbed() {
2100
    // Returns average irradiance absorbed by the ground
2101
    if (albedoSpatial.size() <= 1) {
2102
        albedoSpatial.assign(groundIrradianceSpatial.size(), albedo);
2103
    }
2104

2105
    double ground_absorbed = 0.;
2106
    for (size_t i = 0; i < groundIrradianceSpatial.size(); i++) {
2107
        ground_absorbed += groundIrradianceSpatial.at(i) * (1. - albedoSpatial.at(i)) / groundIrradianceSpatial.size();
2108
    }
2109
    return ground_absorbed;
2110
}
2111

2112
double irrad::get_ground_reflected() {
2113
    // Returns average ground reflected irradiance onto the rear, considering view factor
2114
    return poaRearGroundReflected;
2115
}
2116

2117
double irrad::get_rear_direct_diffuse() {
2118
    // Returns average direct and diffuse irradiance onto the rear
2119
    return poaRearDirectDiffuse;
2120
}
2121

2122
double irrad::get_rear_row_reflections() {
2123
    // Returns the average reflected irradiance from the rear row onto the rear
2124
    return poaRearRowReflections;
2125
}
2126

2127
double irrad::get_rear_self_shaded() {
2128
    // Returns the average direct and circumsolar shaded from being incident on the rear
2129
    return poaRearSelfShaded;
2130
}
2131

2132
void irrad::get_irrad(double *ghi, double *dni, double *dhi) {
2133
    *ghi = globalHorizontal;
2134
    *dni = directNormal;
2135
    *dhi = diffuseHorizontal;
2136
}
2137

2138
void irrad::get_clearsky_irrad(double* ghi_cs, double* dni_cs, double* dhi_cs) {
2139
    *ghi_cs = clearskyIrradiance[0];
2140
    *dni_cs = clearskyIrradiance[1];
2141
    *dhi_cs = clearskyIrradiance[2];
2142
}
2143

2144
void irrad::set_time(int y, int m, int d, int h, double min, double delt_hr) {
2145
    this->year = y;
2146
    this->month = m;
2147
    this->day = d;
2148
    this->hour = h;
2149
    this->minute = min;
2150
    this->delt = delt_hr;
2151
}
2152

2153
void irrad::set_location(double latDegrees, double longDegrees, double tz) {
2154
    this->latitudeDegrees = latDegrees;
2155
    this->longitudeDegrees = longDegrees;
2156
    this->timezone = tz;
2157
}
2158

2159
void irrad::set_optional(double elev, double pres, double t_amb) //defaults of 0 meters elevation, atmospheric pressure, 15°C average annual temperature
2160
{
2161
    if (!std::isnan(elev) && elev >= 0)
2162
        this->elevation = elev;
2163
    if (!std::isnan(pres) && pres > 800)
2164
        this->pressure = pres;
2165
    if (!std::isnan(t_amb))
2166
        this->tamb = t_amb;
2167
}
2168

2169
void irrad::get_optional(double *elev, double *pres, double *t_amb) //defaults of 0 meters elevation, atmospheric pressure, 15°C average annual temperature
2170
{
2171
    *elev = this->elevation;
2172
    *pres = this->pressure;
2173
    *t_amb = this->tamb;
2174
}
2175

2176
void irrad::set_subhourly_clipping(bool enable)
2177
{
2178
    if (enable) this->enableSubhourlyClipping = true;
2179
}
2180

2181
void irrad::set_custom_rot_angles(bool enable, double angle)
2182
{
2183
    this->useCustomRotAngles = enable;
2184
    this->customRotAngle = angle;
2185
}
2186

2187
void irrad::set_sky_model(int sm, double alb, const std::vector<double> &albSpatial) {
2188
    this->skyModel = sm;
2189
    this->albedo = alb;
2190
    if (albSpatial.size() > 0) {
2191
        this->albedoSpatial = albSpatial;
2192
    }
2193
    else {
2194
        this->albedoSpatial.assign(10, alb);
2195
    }
2196
}
2197

2198
void
2199
irrad::set_surface(int tracking, double tilt_deg, double azimuth_deg, double rotlim_deg, bool enBacktrack, double gcr, double slope_tilt, double slope_azm,
2200
                   bool forceToStowFlag, double stowAngle) {
2201
    this->trackingMode = tracking;
2202
    if (tracking == 4)
2203
        this->trackingMode = 0; //treat timeseries tilt as fixed tilt
2204
    this->tiltDegrees = tilt_deg;
2205
    this->surfaceAzimuthDegrees = azimuth_deg;
2206
    this->rotationLimitDegrees = rotlim_deg;
2207
    this->forceToStow = forceToStowFlag;
2208
    this->stowAngleDegrees = stowAngle;
2209
    this->enableBacktrack = enBacktrack;
2210
    this->groundCoverageRatio = gcr;
2211
    this->slopeTilt = slope_tilt;
2212
    this->slopeAzm = slope_azm;
2213
}
2214

2215
void irrad::set_beam_diffuse(double beam, double diffuse) {
2216
    this->directNormal = beam;
2217
    this->diffuseHorizontal = diffuse;
2218
    this->radiationMode = irrad::DN_DF;
2219
}
2220

2221
void irrad::set_global_beam(double global, double beam) {
2222
    this->globalHorizontal = global;
2223
    this->directNormal = beam;
2224
    this->radiationMode = irrad::DN_GH;
2225
}
2226

2227
void irrad::set_global_diffuse(double global, double diffuse) {
2228
    this->globalHorizontal = global;
2229
    this->diffuseHorizontal = diffuse;
2230
    this->radiationMode = irrad::GH_DF;
2231
}
2232

2233
void irrad::set_poa_reference(double poaIrradianceFront, poaDecompReq *pA) {
2234
    this->weatherFilePOA = poaIrradianceFront;
2235
    this->radiationMode = irrad::POA_R;
2236
    this->poaAll = pA;
2237
}
2238

2239
void irrad::set_poa_pyranometer(double poaIrradianceFront, poaDecompReq *pA) {
2240
    this->weatherFilePOA = poaIrradianceFront;
2241
    this->radiationMode = irrad::POA_P;
2242
    this->poaAll = pA;
2243
}
2244

2245
void irrad::set_sun_component(size_t index, double value) {
2246
    if (index < sizeof(sunAnglesRadians) / sizeof(sunAnglesRadians[0])) {
2247
        sunAnglesRadians[index] = value;
2248
    }
2249
}
2250

2251
void irrad::set_from_weather_record(weather_record wf, weather_header hdr, int trackModeIn, std::vector<double>& monthlyTiltDegrees, 
2252
        bool useWeatherFileAlbedo, std::vector<double>& userSpecifiedAlbedo, poaDecompReq *poaAllIn, bool useSpatialAlbedos, const util::matrix_t<double>* userSpecifiedSpatialAlbedos, 
2253
        bool useCustomRotAngles, double customRotAngle) {
2254
    set_time(wf.year, wf.month, wf.day, wf.hour, wf.minute, delt);
2255
    set_optional(hdr.elev, wf.pres, wf.tdry);
2256
    if (radiationMode == irrad::DN_DF) set_beam_diffuse(wf.dn, wf.df);
2257
    else if (radiationMode == irrad::DN_GH) set_global_beam(wf.gh, wf.dn);
2258
    else if (radiationMode == irrad::GH_DF) set_global_diffuse(wf.gh, wf.df);
2259
    else if (radiationMode == irrad::POA_R) set_poa_reference(wf.poa, poaAllIn);
2260
    else if (radiationMode == irrad::POA_P) set_poa_pyranometer(wf.poa, poaAllIn);
2261

2262
    int month_idx = wf.month - 1;
2263
    if (useWeatherFileAlbedo && std::isfinite(wf.alb) && wf.alb > 0 && wf.alb < 1) {
2264
        albedo = wf.alb;
2265
        albedoSpatial.assign(userSpecifiedSpatialAlbedos->ncols(), albedo);
2266
    }
2267
    else if (useSpatialAlbedos) {
2268
        albedoSpatial = userSpecifiedSpatialAlbedos->row(month_idx).to_vector();
2269
        albedo = std::accumulate(albedoSpatial.begin(), albedoSpatial.end(), 0.) / albedoSpatial.size();
2270
    }
2271
    else {
2272
        albedo = userSpecifiedAlbedo[month_idx];
2273
        albedoSpatial.assign(userSpecifiedSpatialAlbedos->ncols(), albedo);
2274
    }
2275
    if (trackModeIn == TRACKING::SEASONAL_TILT) {
2276
        tiltDegrees = monthlyTiltDegrees[month_idx];
2277
        trackingMode = TRACKING::FIXED_TILT;
2278
    }
2279
    set_custom_rot_angles(useCustomRotAngles, customRotAngle);
2280
}
2281

2282
int irrad::calc() {
2283
    int code = check();
2284
    if (code < 0)
2285
        return -100 + code;
2286
    /*
2287
        calculates effective sun position at current timestep, with delt specified in hours
2288

2289
        sunAnglesRadians: results from solarpos
2290
        timeStepSunPosition: [0]  effective hour of day used for sun position
2291
                [1]  effective minute of hour used for sun position
2292
                [2]  is sun up?  (0=no, 1=midday, 2=sunup, 3=sundown)
2293
        surfaceAnglesRadians: result from incidence
2294
        planeOfArrayIrradianceFront: result from sky model
2295
        diff: broken out diffuse components from sky model
2296
    */
2297
    double t_cur = hour + minute / 60.0;
2298

2299
    if (!getStoredSolarposOutputs()) {
2300

2301
        // calculate sunrise and sunset hours in local standard time for the current day
2302
        solarpos_spa(year, month, day, 12, 0.0, 0.0, latitudeDegrees, longitudeDegrees, timezone, dut1, elevation, pressure, tamb, tiltDegrees, surfaceAzimuthDegrees, sunAnglesRadians, spa_table);
2303

2304
        double t_sunrise = sunAnglesRadians[4];
2305
        double t_sunset = sunAnglesRadians[5];
2306

2307
        if (t_sunset > 24.0 && t_sunset !=
2308
                            100.0) //sunset is legitimately the next day but we're not in endless days, so recalculate sunset from the previous day
2309
        {
2310
            double sunanglestemp[9];
2311
            if (day > 1) //simply decrement day during month
2312
                solarpos_spa(year, month, day - 1, 12, 0.0, 0.0, latitudeDegrees, longitudeDegrees, timezone, dut1, elevation, pressure, tamb, tiltDegrees, surfaceAzimuthDegrees, sunanglestemp, spa_table);
2313
            else if (month > 1) //on the 1st of the month, need to switch to the last day of previous month
2314
                solarpos_spa(year, month - 1, __nday[month - 2], 12, 0.0, 0.0, latitudeDegrees, longitudeDegrees, timezone, dut1, elevation, pressure, tamb, tiltDegrees, surfaceAzimuthDegrees, sunanglestemp, spa_table);
2315
            else //on the first day of the year, need to switch to Dec 31 of last year
2316
                solarpos_spa(year - 1, 12, 31, 12, 0.0, 0.0, latitudeDegrees, longitudeDegrees, timezone, dut1, elevation, pressure, tamb, tiltDegrees, surfaceAzimuthDegrees, sunanglestemp, spa_table);
2317
            //on the last day of endless days, sunset is returned as 100 (hour angle too large for calculation), so use today's sunset time as a proxy
2318
            if (sunanglestemp[5] == 100.0)
2319
                t_sunset -= 24.0;
2320
                //if sunset from yesterday WASN'T today, then it's ok to leave sunset > 24, which will cause the sun to rise today and not set today
2321
            else if (sunanglestemp[5] >= 24.0)
2322
                t_sunset = sunanglestemp[5] - 24.0;
2323
        }
2324

2325
        if (t_sunrise < 0.0 && t_sunrise !=
2326
                            -100.0) //sunrise is legitimately the previous day but we're not in endless days, so recalculate for next day
2327
        {
2328
            double sunanglestemp[9];
2329
            if (day < __nday[month - 1]) //simply increment the day during the month, month is 1-indexed and __nday is 0-indexed
2330
                solarpos_spa(year, month, day + 1, 12, 0.0, 0.0, latitudeDegrees, longitudeDegrees, timezone, dut1, elevation, pressure, tamb, tiltDegrees, surfaceAzimuthDegrees, sunanglestemp, spa_table);
2331
            else if (month < 12) //on the last day of the month, need to switch to the first day of the next month
2332
                solarpos_spa(year, month + 1, 1, 12, 0.0, 0.0, latitudeDegrees, longitudeDegrees, timezone, dut1, elevation, pressure, tamb, tiltDegrees, surfaceAzimuthDegrees, sunanglestemp, spa_table);
2333
            else //on the last day of the year, need to switch to Jan 1 of the next year
2334
                solarpos_spa(year + 1, 1, 1, 12, 0.0, 0.0, latitudeDegrees, longitudeDegrees, timezone, dut1, elevation, pressure, tamb, tiltDegrees, surfaceAzimuthDegrees, sunanglestemp, spa_table);
2335
            //on the last day of endless days, sunrise would be returned as -100 (hour angle too large for calculations), so use today's sunrise time as a proxy
2336
            if (sunanglestemp[4] == -100.0)
2337
                t_sunrise += 24.0;
2338
                //if sunrise from tomorrow isn't today, then it's ok to leave sunrise < 0, which will cause the sun to set at the right time and not rise until tomorrow
2339
            else if (sunanglestemp[4] < 0.0)
2340
                t_sunrise = sunanglestemp[4] + 24.0;
2341
        }
2342

2343
        // recall: if delt <= 0.0, do not interpolate sunrise and sunset hours, just use specified time stamp
2344
        // time step encompasses the sunrise
2345
        if (delt > 0 && t_cur >= t_sunrise - delt / 2.0 && t_cur < t_sunrise + delt / 2.0) {
2346
            double t_calc = (t_sunrise + (t_cur + delt / 2.0)) / 2.0; // midpoint of sunrise and end of timestep
2347
            int hr_calc = (int) t_calc;
2348
            double min_calc = (t_calc - hr_calc) * 60.0;
2349

2350
            timeStepSunPosition[0] = hr_calc;
2351
            timeStepSunPosition[1] = (int) min_calc;
2352

2353
            solarpos_spa(year, month, day, hr_calc, min_calc, 0.0, latitudeDegrees, longitudeDegrees, timezone, dut1, elevation, pressure, tamb, tiltDegrees, surfaceAzimuthDegrees, sunAnglesRadians, spa_table);
2354

2355
            timeStepSunPosition[2] = 2;
2356
        }
2357
            // timestep encompasses the sunset
2358
        else if (delt > 0 && t_cur > t_sunset - delt / 2.0 && t_cur <= t_sunset + delt / 2.0) {
2359
            double t_calc = ((t_cur - delt / 2.0) + t_sunset) / 2.0; // midpoint of beginning of timestep and sunset
2360
            int hr_calc = (int) t_calc;
2361
            double min_calc = (t_calc - hr_calc) * 60.0;
2362

2363
            timeStepSunPosition[0] = hr_calc;
2364
            timeStepSunPosition[1] = (int) min_calc;
2365

2366
            solarpos_spa(year, month, day, hr_calc, min_calc, 0.0, latitudeDegrees, longitudeDegrees, timezone, dut1, elevation, pressure, tamb, tiltDegrees, surfaceAzimuthDegrees, sunAnglesRadians, spa_table);
2367

2368
            timeStepSunPosition[2] = 3;
2369
        }
2370
            // timestep is not sunrise nor sunset, but sun is up  (calculate position at provided t_cur)
2371
        else if ((t_sunrise < t_sunset && t_cur >= t_sunrise && t_cur <= t_sunset) || //this captures normal daylight cases
2372
                (t_sunrise > t_sunset && (t_cur <= t_sunset || t_cur >=
2373
                                                                t_sunrise))) //this captures cases where sunset (from previous day) is 1:30AM, sunrise 2:30AM, in arctic circle
2374
        {
2375
            timeStepSunPosition[0] = hour;
2376
            timeStepSunPosition[1] = (int)minute;
2377
            solarpos_spa(year, month, day, hour, minute, 0.0, latitudeDegrees, longitudeDegrees, timezone, dut1, elevation, pressure, tamb, tiltDegrees, surfaceAzimuthDegrees, sunAnglesRadians, spa_table);
2378
            timeStepSunPosition[2] = 1;
2379
        }
2380
        else {
2381
            // sun is down, assign sundown values
2382
            solarpos_spa(year, month, day, hour, minute, 0.0, latitudeDegrees, longitudeDegrees, timezone, dut1, elevation, pressure, tamb, tiltDegrees, surfaceAzimuthDegrees, sunAnglesRadians, spa_table);
2383
            timeStepSunPosition[0] = hour;
2384
            timeStepSunPosition[1] = (int) minute;
2385
            timeStepSunPosition[2] = 0;
2386
        }
2387
        storeSolarposOutputs();
2388
    }
2389
    //clearsky
2390
    ineichen(clearskyIrradiance, RTOD * sunAnglesRadians[1], month, day, pressure * 100.0, 1.0, elevation, 0, true);
2391

2392

2393
    planeOfArrayIrradianceFront[0] = planeOfArrayIrradianceFront[1] = planeOfArrayIrradianceFront[2] = 0;
2394
    planeOfArrayIrradianceFrontCS[0] = planeOfArrayIrradianceFrontCS[1] = planeOfArrayIrradianceFrontCS[2] = 0;
2395
    diffuseIrradianceFront[0] = diffuseIrradianceFront[1] = diffuseIrradianceFront[2] = 0;
2396
    diffuseIrradianceFrontCS[0] = diffuseIrradianceFrontCS[1] = diffuseIrradianceFrontCS[2] = 0;
2397

2398
    surfaceAnglesRadians[0] = surfaceAnglesRadians[1] = surfaceAnglesRadians[2] = surfaceAnglesRadians[3] = surfaceAnglesRadians[4] = 0;
2399

2400
    // do irradiance calculations if sun is up
2401
    if (timeStepSunPosition[2] > 0) {
2402
        // compute incidence angles onto fixed or tracking surface
2403
        incidence(trackingMode, tiltDegrees, surfaceAzimuthDegrees, rotationLimitDegrees, sunAnglesRadians[1],
2404
                  sunAnglesRadians[0],
2405
                  enableBacktrack, groundCoverageRatio, slopeTilt, slopeAzm, forceToStow, stowAngleDegrees, useCustomRotAngles, customRotAngle, surfaceAnglesRadians);
2406
        if (radiationMode < irrad::POA_R) {
2407
            double hextra = sunAnglesRadians[8];
2408
            double hbeam = directNormal *
2409
                           cos(sunAnglesRadians[1]); // calculated beam on horizontal surface: sunAnglesRadians[1]=zenith
2410
            if (directNormal < 0) {
2411
                hbeam = 0;
2412
            }
2413
            // check beam irradiance against extraterrestrial irradiance
2414
            if (hbeam > hextra)// && radiationMode != GH_DF)
2415
            {
2416
                //beam irradiance on horizontal W/m2 exceeded calculated extraterrestrial irradiance
2417
                return -1;
2418
            }
2419

2420
            // compute beam and diffuse inputs on horizontal based on irradiance inputs mode
2421
            if (radiationMode == irrad::DN_DF)  // Beam+Diffuse
2422
            {
2423
                calculatedDiffuseHorizontal = diffuseHorizontal;
2424
                calculatedDirectNormal = directNormal;
2425
            }
2426
            else if (radiationMode == irrad::DN_GH) // Total+Beam
2427
            {
2428
                calculatedDiffuseHorizontal = globalHorizontal - hbeam;
2429
                if (calculatedDiffuseHorizontal < 0) calculatedDiffuseHorizontal = 0;
2430
                calculatedDirectNormal = directNormal;
2431
            }
2432
            else if (radiationMode == irrad::GH_DF) //Total+Diffuse
2433
            {
2434
                calculatedDiffuseHorizontal = diffuseHorizontal;
2435
                calculatedDirectNormal = (globalHorizontal - diffuseHorizontal) /
2436
                                         cos(sunAnglesRadians[1]); //compute beam from total, diffuse, and zenith angle
2437
                if (calculatedDirectNormal > irrad::irradiationMax) calculatedDirectNormal = irrad::irradiationMax;
2438
                if (calculatedDirectNormal < 0) calculatedDirectNormal = 0;
2439
            }
2440
            else
2441
                return -2; // just in case of a weird error
2442

2443

2444
            // compute incident irradiance on tilted surface
2445
            switch (skyModel) {
2446
                case 0:
2447
                    isotropic(hextra, calculatedDirectNormal, calculatedDiffuseHorizontal, albedo,
2448
                              surfaceAnglesRadians[0], surfaceAnglesRadians[1], sunAnglesRadians[1],
2449
                              planeOfArrayIrradianceFront, diffuseIrradianceFront);
2450
                    break;
2451
                case 1:
2452
                    hdkr(hextra, calculatedDirectNormal, calculatedDiffuseHorizontal, albedo, surfaceAnglesRadians[0],
2453
                         surfaceAnglesRadians[1], sunAnglesRadians[1], planeOfArrayIrradianceFront,
2454
                         diffuseIrradianceFront);
2455
                    break;
2456
                default:
2457
                    perez(hextra, calculatedDirectNormal, calculatedDiffuseHorizontal, albedo, surfaceAnglesRadians[0],
2458
                          surfaceAnglesRadians[1], sunAnglesRadians[1], planeOfArrayIrradianceFront,
2459
                          diffuseIrradianceFront);
2460
                    break;
2461
            }
2462

2463
            if (enableSubhourlyClipping) {
2464
                switch (skyModel) {
2465
                    case 0:
2466
                        isotropic(hextra, clearskyIrradiance[1], clearskyIrradiance[2], albedo,
2467
                            surfaceAnglesRadians[0], surfaceAnglesRadians[1], sunAnglesRadians[1],
2468
                            planeOfArrayIrradianceFrontCS, diffuseIrradianceFrontCS);
2469
                        break;
2470
                    case 1:
2471
                        hdkr(hextra, clearskyIrradiance[1], clearskyIrradiance[2], albedo, surfaceAnglesRadians[0],
2472
                            surfaceAnglesRadians[1], sunAnglesRadians[1], planeOfArrayIrradianceFrontCS,
2473
                            diffuseIrradianceFrontCS);
2474
                        break;
2475
                    default:
2476
                        perez(hextra, clearskyIrradiance[1], clearskyIrradiance[2], albedo, surfaceAnglesRadians[0],
2477
                            surfaceAnglesRadians[1], sunAnglesRadians[1], planeOfArrayIrradianceFrontCS,
2478
                            diffuseIrradianceFrontCS);
2479
                        break;
2480
                }
2481
            }
2482
        }
2483
        else { // Sev 2015/09/11 - perform a POA decomp.
2484
            int errorcode = poaDecomp(weatherFilePOA, surfaceAnglesRadians, sunAnglesRadians, albedo, poaAll,
2485
                                      directNormal, diffuseHorizontal, globalHorizontal, planeOfArrayIrradianceFront,
2486
                                      diffuseIrradianceFront);
2487
            if (enableSubhourlyClipping) {
2488
                double hextra = sunAnglesRadians[8];
2489
                switch (skyModel) {
2490
                case 0:
2491
                    isotropic(hextra, clearskyIrradiance[1], clearskyIrradiance[2], albedo,
2492
                        surfaceAnglesRadians[0], surfaceAnglesRadians[1], sunAnglesRadians[1],
2493
                        planeOfArrayIrradianceFrontCS, diffuseIrradianceFrontCS);
2494
                    break;
2495
                case 1:
2496
                    hdkr(hextra, clearskyIrradiance[1], clearskyIrradiance[2], albedo, surfaceAnglesRadians[0],
2497
                        surfaceAnglesRadians[1], sunAnglesRadians[1], planeOfArrayIrradianceFrontCS,
2498
                        diffuseIrradianceFrontCS);
2499
                    break;
2500
                default:
2501
                    perez(hextra, clearskyIrradiance[1], clearskyIrradiance[2], albedo, surfaceAnglesRadians[0],
2502
                        surfaceAnglesRadians[1], sunAnglesRadians[1], planeOfArrayIrradianceFrontCS,
2503
                        diffuseIrradianceFrontCS);
2504
                    break;
2505
                }
2506
            }
2507
            calculatedDirectNormal = directNormal;
2508
            calculatedDiffuseHorizontal = diffuseHorizontal;
2509
            
2510
            return errorcode; //this will return 0 if successful, otherwise 40, 41, or 42 if calculated decomposed dni, dhi, or ghi are negative
2511
        }
2512
    }
2513
    else {
2514
        globalHorizontal = 0;
2515
        directNormal = 0;
2516
        diffuseHorizontal = 0;
2517
    } //sun is below horizon
2518

2519
    return 0;
2520

2521
}
2522

2523
int irrad::calc_rear_side(double transmissionFactor, double groundClearanceHeight, double slopeLength) {
2524
    // do irradiance calculations if sun is up
2525
    if (timeStepSunPosition[2] > 0) {
2526

2527
        double tiltRadian = surfaceAnglesRadians[1];
2528

2529
        double clearanceGround = std::numeric_limits<double>::quiet_NaN();          // distance between bottom edge of module to ground
2530
        if (this->trackingMode == 1) {                                              // if horizontal single-axis tracking
2531
            clearanceGround = groundClearanceHeight - (0.5 * slopeLength) * sin(tiltRadian);
2532
        }
2533
        else {
2534
            clearanceGround = groundClearanceHeight;
2535
        }
2536

2537
        // System geometry
2538
        double verticalHeight = slopeLength * sin(tiltRadian);
2539
        double horizontalLength = slopeLength * cos(tiltRadian);
2540
        double rowToRow = slopeLength / this->groundCoverageRatio;                  // distance between front of row to front of next row
2541
        double distanceBetweenRows = rowToRow - horizontalLength;                   // distance between back of row to front of next row
2542

2543
        // Determine the view factors for points on the ground to the sky in the rowToRow interval
2544
        std::vector<double> rearSkyConfigFactors, frontSkyConfigFactors;
2545
        this->getSkyConfigurationFactors(rowToRow, verticalHeight, clearanceGround, distanceBetweenRows,
2546
                                         horizontalLength, rearSkyConfigFactors, frontSkyConfigFactors);
2547

2548
        // Determine whether points on the ground in the rowToRow interval are shaded or not from DNI (also shaded fraction of PV back and front)
2549
        double pvBackShadeFraction, pvFrontShadeFraction, maxShadow;
2550
        pvBackShadeFraction = pvFrontShadeFraction = maxShadow = 0;
2551
        std::vector<int> rearGroundShade, frontGroundShade;
2552
        this->getGroundShadeFactors(rowToRow, verticalHeight, clearanceGround, distanceBetweenRows, horizontalLength,
2553
                                    sunAnglesRadians[0], sunAnglesRadians[2], rearGroundShade, frontGroundShade,
2554
                                    maxShadow, pvBackShadeFraction, pvFrontShadeFraction);
2555

2556
        // Calculate GHI for the points on the ground in the rowToRow interval, considering shading and view factors to the sky
2557
        std::vector<double> rearGroundGHI, frontGroundGHI;
2558
        this->getGroundGHI(transmissionFactor, rearSkyConfigFactors, frontSkyConfigFactors, rearGroundShade,
2559
                           frontGroundShade, rearGroundGHI, frontGroundGHI);
2560
        groundIrradianceSpatial = condenseAndAlignGroundIrrad(rearGroundGHI, groundIrradOutputRes, trackingMode == 1, horizontalLength, rowToRow, surfaceAnglesRadians[3]);
2561

2562
        // Calculate the irradiance on the front of the PV module (to get front reflected)
2563
        std::vector<double> frontIrradiancePerCellrow, frontReflected;
2564
        double frontAverageIrradiance = 0;
2565
        getFrontSurfaceIrradiances(pvFrontShadeFraction, rowToRow, verticalHeight, clearanceGround, distanceBetweenRows,
2566
                                   horizontalLength, frontGroundGHI, frontIrradiancePerCellrow, frontAverageIrradiance,
2567
                                   frontReflected);
2568

2569
        // Calculate the irradiance on the back of the PV module
2570
        std::vector<double> rearIrradiancePerCellrow;
2571
        double rearAverageIrradiance = 0;
2572
        std::vector<double> rearIrradiancePerCellrowCS;
2573
        double rearAverageIrradianceCS = 0;
2574
        getBackSurfaceIrradiances(pvBackShadeFraction, rowToRow, verticalHeight, clearanceGround, distanceBetweenRows,
2575
                                  horizontalLength, rearGroundGHI, frontGroundGHI, frontReflected,
2576
                                  rearIrradiancePerCellrow, rearAverageIrradiance);
2577

2578
        getBackSurfaceIrradiancesCS(pvBackShadeFraction, rowToRow, verticalHeight, clearanceGround, distanceBetweenRows,
2579
            horizontalLength, rearGroundGHI, frontGroundGHI, frontReflected,
2580
            rearIrradiancePerCellrowCS, rearAverageIrradianceCS);
2581
        planeOfArrayIrradianceRearAverage = rearAverageIrradiance;
2582
        planeOfArrayIrradianceRearSpatial = rearIrradiancePerCellrow;
2583

2584
        planeOfArrayIrradianceRearAverageCS = rearAverageIrradianceCS;
2585
        planeOfArrayIrradianceRearSpatialCS = rearIrradiancePerCellrowCS;
2586
    }
2587
    else {
2588
        groundIrradianceSpatial.assign(groundIrradOutputRes, 0.);
2589
        planeOfArrayIrradianceRearAverage = 0.;
2590
        planeOfArrayIrradianceRearSpatial.assign(poaRearIrradRes, 0.);
2591
    }
2592
    return true;
2593
}
2594

2595
void irrad::getSkyConfigurationFactors(double rowToRow, double verticalHeight, double clearanceGround,
2596
                                       double distanceBetweenRows, double horizontalLength,
2597
                                       std::vector<double> &rearSkyConfigFactors,
2598
                                       std::vector<double> &frontSkyConfigFactors) {
2599
    // Calculate sky configuration factors using 100 intervals
2600
    size_t intervals = 100;
2601
    double deltaInterval = static_cast<double>(rowToRow / intervals);
2602
    double x = -deltaInterval / 2.0;
2603

2604
    for (size_t i = 0; i != intervals; i++) {
2605
        x += deltaInterval;
2606
        double angleA = atan((verticalHeight + clearanceGround) / (2.0 * rowToRow + horizontalLength - x));
2607
        if (angleA < 0.0) {
2608
            angleA += M_PI;
2609
        }
2610
        double angleB = atan(clearanceGround / (2.0 * rowToRow - x));
2611
        if (angleB < 0.0) {
2612
            angleB += M_PI;
2613
        }
2614
        double beta1 = fmax(angleA, angleB);
2615

2616
        double angleC = atan((verticalHeight + clearanceGround) / (rowToRow + horizontalLength - x));
2617
        if (angleC < 0.0) {
2618
            angleC += M_PI;
2619
        }
2620
        double angleD = atan(clearanceGround / (rowToRow - x));
2621
        if (angleD < 0.0) {
2622
            angleD += M_PI;
2623
        }
2624
        double beta2 = fmin(angleC, angleD);
2625
        double beta3 = fmax(angleC, angleD);
2626

2627
        double beta4 = atan((verticalHeight + clearanceGround) / (horizontalLength - x));
2628
        if (beta4 < 0.0) {
2629
            beta4 += M_PI;
2630
        }
2631

2632
        double beta5 = atan(clearanceGround / (-x));
2633
        if (beta5 < 0.0) {
2634
            beta5 += M_PI;
2635
        }
2636

2637
        double beta6 = atan((verticalHeight + clearanceGround) / (-distanceBetweenRows - x));
2638
        if (beta6 < 0.0) {
2639
            beta6 += M_PI;
2640
        }
2641
        double sky1, sky2, sky3, skyAll;
2642
        sky1 = sky2 = sky3 = skyAll = 0;
2643
        if (beta2 > beta1) {
2644
            sky1 = 0.5 * (cos(beta1) - cos(beta2));
2645
        }
2646
        if (beta4 > beta3) {
2647
            sky2 = 0.5 * (cos(beta3) - cos(beta4));
2648
        }
2649
        if (beta6 > beta5) {
2650
            sky3 = 0.5 * (cos(beta5) - cos(beta6));
2651
        }
2652
        skyAll = sky1 + sky2 + sky3;
2653

2654
        rearSkyConfigFactors.push_back(skyAll);
2655
        frontSkyConfigFactors.push_back(skyAll);
2656
    }
2657
}
2658

2659
void
2660
irrad::getGroundShadeFactors(double rowToRow, double verticalHeight, double clearanceGround, double distanceBetweenRows,
2661
                             double horizontalLength, double solarAzimuthRadians, double solarElevationRadians,
2662
                             std::vector<int> &rearGroundShade, std::vector<int> &frontGroundShade, double &maxShadow,
2663
                             double &pvBackSurfaceShadeFraction, double &pvFrontSurfaceShadeFraction) {
2664
    // calculate ground shade factors using 100 intervals
2665
    size_t intervals = 100;
2666
    double deltaInterval = static_cast<double>(rowToRow / intervals);
2667
    double surfaceAzimuthAngleRadians = surfaceAnglesRadians[2];
2668
    double shadingStart1, shadingStart2, shadingEnd1, shadingEnd2;
2669
    shadingStart1 = shadingStart2 = shadingEnd1 = shadingEnd2 = pvBackSurfaceShadeFraction = 0;
2670

2671
    // Horizontal length (from back of row) of shadow perpendicular to row if no ground clearance (Px in Fig. 6 of Appelbaum, 1979)
2672
    double Lh = (verticalHeight / tan(solarElevationRadians)) * cos(surfaceAzimuthAngleRadians - solarAzimuthRadians);
2673

2674
    // Horizontal length from back of row to end of shadow, perpendicular to row
2675
    double Lhc = ((clearanceGround + verticalHeight) / tan(solarElevationRadians)) * cos(surfaceAzimuthAngleRadians - solarAzimuthRadians);
2676

2677
    // Horizontal length from back of row to start of shadow, perpendicular to row
2678
    double Lc = (clearanceGround / tan(solarElevationRadians)) * cos(surfaceAzimuthAngleRadians - solarAzimuthRadians);
2679

2680
    // Front partially shaded, back completely shaded, ground completely shaded
2681
    if (Lh > distanceBetweenRows) {
2682
        pvFrontSurfaceShadeFraction = (Lh - distanceBetweenRows) / (Lh + horizontalLength);
2683
        pvBackSurfaceShadeFraction = 1.0;
2684
        shadingStart1 = 0.0;
2685
        shadingEnd1 = rowToRow;
2686
    }
2687
    // Front completely shaded, back partially shaded, ground completely shaded
2688
    else if (Lh < -(rowToRow + horizontalLength)) {
2689
        pvFrontSurfaceShadeFraction = 1.0;
2690
        pvBackSurfaceShadeFraction = (Lh + rowToRow + horizontalLength) / (Lh + horizontalLength);
2691
        shadingStart1 = 0.0;
2692
        shadingEnd1 = rowToRow;
2693
    }
2694
    // Assume ground is partially shaded
2695
    else {
2696
        if (Lhc >= 0) {
2697
            pvFrontSurfaceShadeFraction = 0.0;
2698
            pvBackSurfaceShadeFraction = 1.0;
2699
            double shadowStart = Lc;
2700
            double shadowEnd = Lhc + horizontalLength;
2701
            while (shadowStart > rowToRow) {
2702
                shadowStart -= rowToRow;
2703
                shadowEnd -= rowToRow;
2704
            }
2705
            shadingStart1 = shadowStart;
2706
            shadingEnd1 = shadowEnd;
2707
            if (shadingEnd1 > rowToRow) {
2708
                shadingEnd1 = rowToRow;
2709
                shadingStart2 = 0.0;
2710
                shadingEnd2 = shadowEnd - rowToRow;
2711
                // ground completely shaded
2712
                if (shadingEnd2 > shadingStart1) {
2713
                    shadingStart1 = 0.0;
2714
                    shadingEnd1 = rowToRow;
2715
                }
2716
            }
2717
        }
2718
        // Shadow to front of row, either front or back might be shaded, depending on tilt and other factors
2719
        else {
2720
            double shadowStart = 0.0;
2721
            double shadowEnd = 0.0;
2722
            if (Lc < Lhc + horizontalLength) {
2723
                pvFrontSurfaceShadeFraction = 0.0;
2724
                pvBackSurfaceShadeFraction = 1.0;
2725
                shadowStart = Lc;
2726
                shadowEnd = Lhc + horizontalLength;
2727
            }
2728
            else {
2729
                pvFrontSurfaceShadeFraction = 1.0;
2730
                pvBackSurfaceShadeFraction = 0.0;
2731
                shadowStart = Lhc + horizontalLength;
2732
                shadowEnd = Lc;
2733

2734
            }
2735
            while (shadowStart < 0.0) {
2736
                shadowStart += rowToRow;
2737
                shadowEnd += rowToRow;
2738
            }
2739

2740
            shadingStart1 = shadowStart;
2741
            shadingEnd1 = shadowEnd;
2742

2743
            if (shadingEnd1 > rowToRow) {
2744
                shadingEnd1 = rowToRow;
2745
                shadingStart2 = 0.0;
2746
                shadingEnd2 = shadowEnd - rowToRow;
2747
                if (shadingEnd2 > shadingStart1) {
2748
                    shadingStart1 = 0.0;
2749
                    shadingEnd1 = rowToRow;
2750
                }
2751
            }
2752
        }
2753

2754
    }
2755
    double x = -deltaInterval / 2.0;
2756
    for (size_t i = 0; i != intervals; i++) {
2757
        x += deltaInterval;
2758
        if ((x >= shadingStart1 && x < shadingEnd1) || (x >= shadingStart2 && x < shadingEnd2)) {
2759
            rearGroundShade.push_back(1);
2760
            frontGroundShade.push_back(1);
2761
        }
2762
        else {
2763
            rearGroundShade.push_back(0);
2764
            frontGroundShade.push_back(0);
2765
        }
2766
    }
2767
    maxShadow = fmax(shadingStart1, shadingEnd1);
2768
}
2769

2770
void irrad::getGroundGHI(double transmissionFactor, std::vector<double> rearSkyConfigFactors,
2771
                         std::vector<double> frontSkyConfigFactors, std::vector<int> rearGroundShade,
2772
                         std::vector<int> frontGroundShade, std::vector<double> &rearGroundGHI,
2773
                         std::vector<double> &frontGroundGHI) {
2774
    // Calculate the irradiance components on horizontal unobstructed ground
2775
    perez(0, calculatedDirectNormal, calculatedDiffuseHorizontal, albedo, sunAnglesRadians[1], 0.0, sunAnglesRadians[1],
2776
          planeOfArrayIrradianceRear, diffuseIrradianceRear);
2777
    double incidentBeam = planeOfArrayIrradianceRear[0];
2778
    double isotropicDiffuse = diffuseIrradianceRear[0];
2779
    double circumsolarDiffuse = diffuseIrradianceRear[1];
2780

2781
    // Sum the irradiance components for each of the ground segments to the front and rear of the front of the PV row
2782
    for (size_t i = 0; i != 100; i++) {
2783
        // Isotropic diffuse irradiance incident on ground
2784
        rearGroundGHI.push_back(rearSkyConfigFactors[i] * isotropicDiffuse);
2785
        frontGroundGHI.push_back(frontSkyConfigFactors[i] * isotropicDiffuse);
2786

2787
        if (rearGroundShade[i] == 0) {
2788
            // Add beam and circumsolar component if not shaded
2789
            rearGroundGHI[i] += incidentBeam + circumsolarDiffuse;
2790
        }
2791
        else {
2792
            // Add beam and circumsolar component transmitted thru module if shaded
2793
            rearGroundGHI[i] += (incidentBeam + circumsolarDiffuse) * transmissionFactor;
2794
        }
2795

2796
        // Repeat for ground in front of row
2797
        if (frontGroundShade[i] == 0) {
2798
            frontGroundGHI[i] += incidentBeam + circumsolarDiffuse;
2799
        }
2800
        else {
2801
            frontGroundGHI[i] += (incidentBeam + circumsolarDiffuse) * transmissionFactor;
2802
        }
2803
    }
2804
}
2805

2806
void irrad::getFrontSurfaceIrradiances(double pvFrontShadeFraction, double rowToRow, double verticalHeight,
2807
                                       double clearanceGround, double distanceBetweenRows, double horizontalLength,
2808
                                       std::vector<double> frontGroundGHI, std::vector<double> &frontIrradiance,
2809
                                       double &frontAverageIrradiance, std::vector<double> &frontReflected) {
2810
    // front surface assumed to be glass
2811
    double n2 = 1.526;
2812

2813
    size_t intervals = 100;
2814
    double solarAzimuthRadians = sunAnglesRadians[0];
2815
    double solarZenithRadians = sunAnglesRadians[1];
2816
    double tiltRadians = surfaceAnglesRadians[1];
2817
    double surfaceAzimuthRadians = surfaceAnglesRadians[2];
2818

2819
    // Calculate diffuse isotropic irradiance for a horizontal surface
2820
    double *poa = planeOfArrayIrradianceRear;
2821
    double *diffc = diffuseIrradianceRear;
2822
    perez(0, calculatedDirectNormal, calculatedDiffuseHorizontal, albedo, solarZenithRadians, 0, solarZenithRadians,
2823
          poa, diffc);
2824
    double isotropicSkyDiffuse = diffc[0];
2825

2826
    // Calculate irradiance components for a 90 degree tilt to get horizon brightening
2827
    double angleTmp[5] = {0, 0, 0, 0, 0};            // ([0] = incidence angle, [1] = tilt)
2828
    incidence(0, 90.0, 180.0, 45.0, solarZenithRadians, solarAzimuthRadians, this->enableBacktrack,
2829
              this->groundCoverageRatio, this->slopeTilt, this->slopeAzm, this->forceToStow, this->stowAngleDegrees, this->useCustomRotAngles, this->customRotAngle, angleTmp);
2830
    perez(0, calculatedDirectNormal, calculatedDiffuseHorizontal, albedo, angleTmp[0], angleTmp[1], solarZenithRadians,
2831
          poa, diffc);
2832
    double horizonDiffuse = diffc[2];
2833

2834
    // Calculate x,y coordinates of bottom and top edges of PV row in back of desired PV row so that portions of sky and ground viewed by the
2835
    // PV cell may be determined. Origin of x-y axis is the ground point below the lower front edge of the desired PV row. The row in back of
2836
    // the desired row is in the positive x direction.
2837
    double PbotX = -rowToRow;                        // x value for point on bottom edge of PV module/panel of row in front of (in PV panel slope lengths)
2838
    double PbotY = clearanceGround;                  // y value for point on bottom edge of PV module/panel of row in front of (in PV panel slope lengths)
2839
    double PtopX = -distanceBetweenRows;             // x value for point on top edge of PV module/panel of row in front of (in PV panel slope lengths)
2840
    double PtopY = verticalHeight +
2841
                   clearanceGround; // y value for point on top edge of PV module/panel of row in front of (in PV panel slope lengths)
2842

2843
    // Calculate diffuse and direct component irradiances for each cell row (assuming 6 rows)
2844
    size_t cellRows = poaFrontIrradRes;
2845
    for (size_t i = 0; i != cellRows; i++) {
2846
        // Calculate diffuse irradiances and reflected amounts for each cell row over its field of view of 180 degrees,
2847
        // beginning with the angle providing the upper most view of the sky (j=0)
2848
        double PcellX = horizontalLength * (i + 0.5) /
2849
                        ((double) cellRows);                   // x value for location of PV cell with OFFSET FOR SARA REFERENCE CELLS     4/26/2016
2850
        double PcellY = clearanceGround + verticalHeight * (i + 0.5) /
2851
                                          ((double) cellRows); // y value for location of PV cell with OFFSET FOR SARA REFERENCE CELLS     4/26/2016
2852
        double elevationAngleUp = atan((PtopY - PcellY) / (PcellX -
2853
                                                           PtopX));          // Elevation angle up from PV cell to top of PV module/panel, radians
2854
        double elevationAngleDown = atan((PcellY - PbotY) / (PcellX -
2855
                                                             PbotX));        // Elevation angle down from PV cell to bottom of PV module/panel, radians
2856
        size_t iStopIso = (size_t) round((M_PI - tiltRadians - elevationAngleUp) /
2857
                                         DTOR);                               // Last whole degree in arc range that sees sky, first is 0
2858
        size_t iHorBright = (size_t) round(fmax(0.0, 6.0 - elevationAngleUp /
2859
                                                           DTOR));                       // Number of whole degrees for which horizon brightening occurs
2860
        size_t iStartGrd = (size_t) round((M_PI - tiltRadians + elevationAngleDown) /
2861
                                          DTOR);                          // First whole degree in arc range that sees ground, last is 180
2862

2863
        frontIrradiance.push_back(0.);
2864
        frontReflected.push_back(0.);
2865
        double reflectanceNormalIncidence = pow((n2 - 1.0) / (n2 + 1.0), 2.0);
2866

2867
        // Add sky diffuse component and horizon brightening if present
2868
        for (size_t j = 0; j != iStopIso; j++) {
2869
            frontIrradiance[i] += 0.5 * (cos(j * DTOR) - cos((j + 1) * DTOR)) * MarionAOICorrectionFactorsGlass[j] *
2870
                                  isotropicSkyDiffuse;
2871
            frontReflected[i] += 0.5 * (cos(j * DTOR) - cos((j + 1) * DTOR)) * isotropicSkyDiffuse *
2872
                                 (1.0 - MarionAOICorrectionFactorsGlass[j] * (1.0 - reflectanceNormalIncidence));
2873

2874
            if ((iStopIso - j) <= iHorBright) {
2875
                frontIrradiance[i] += 0.5 * (cos(j * DTOR) - cos((j + 1) * DTOR)) * MarionAOICorrectionFactorsGlass[j] *
2876
                                      horizonDiffuse / 0.052246; // 0.052246 = 0.5 * [cos(84) - cos(90)]
2877
                frontReflected[i] += 0.5 * (cos(j * DTOR) - cos((j + 1) * DTOR)) * (horizonDiffuse / 0.052246) *
2878
                                     (1.0 - MarionAOICorrectionFactorsGlass[j] * (1.0 - reflectanceNormalIncidence));
2879
            }
2880
        }
2881

2882

2883
        // Add ground reflected component
2884
        std::vector<double> albedoAligned;
2885
        if (trackingMode == 0 || trackingMode == 1 || trackingMode == 4) {          // 0=fixed, 1=one-axis, 4=seasonal tilt
2886
            // subdivide spatial albedos to match ground GHI length and align reference point at front of row
2887
            albedoAligned = divideAndAlignAlbedos(albedoSpatial, intervals, trackingMode == 1, horizontalLength, rowToRow, surfaceAnglesRadians[3]);
2888
        }
2889
        else {
2890
            double average_albedo = std::accumulate(albedoSpatial.begin(), albedoSpatial.end(), 0.) / albedoSpatial.size();
2891
            albedoAligned.assign(intervals, average_albedo);
2892
        }
2893

2894
        for (size_t j = iStartGrd; j < 180; j++) {
2895
            double startElevationDown = (j - iStartGrd) * DTOR + elevationAngleDown;
2896
            double stopElevationDown = (j + 1 - iStartGrd) * DTOR + elevationAngleDown;
2897
            double projectedX1 = PcellX - PcellY / tan(startElevationDown);
2898
            double projectedX2 = PcellX - PcellY / tan(stopElevationDown);
2899
            double actualGroundGHI = 0.0;
2900
            double reflectedGroundGHI = 0.0;
2901

2902
            if (std::abs(projectedX1 - projectedX2) > 0.99 * rowToRow) {
2903
                // Use average value if projection approximates the rtr
2904
                actualGroundGHI = std::accumulate(frontGroundGHI.begin(), frontGroundGHI.end(), 0.) / frontGroundGHI.size();
2905
                reflectedGroundGHI = actualGroundGHI * std::accumulate(albedoAligned.begin(), albedoAligned.end(), 0.) / albedoAligned.size();
2906
            }
2907
            else {
2908
                projectedX1 = intervals * projectedX1 / rowToRow;
2909
                projectedX2 = intervals * projectedX2 / rowToRow;
2910

2911
                // offset so array indexes are positive
2912
                while (projectedX1 < 0.0 || projectedX2 < 0.0) {
2913
                    projectedX1 += intervals;
2914
                    projectedX2 += intervals;
2915
                }
2916

2917
                size_t index1 = static_cast<size_t>(projectedX1);
2918
                size_t index2 = static_cast<size_t>(projectedX2);
2919

2920
                if (index1 == index2) {
2921
                    actualGroundGHI = frontGroundGHI[index1];
2922
                    reflectedGroundGHI = frontGroundGHI[index1] * albedoAligned[index1];
2923
                }
2924
                else {
2925
                    // Sum irradiances on the ground if projects are in different groundGHI elements
2926
                    for (size_t k = index1; k <= index2; k++) {
2927
                        if (k == index1) {
2928
                            actualGroundGHI += frontGroundGHI[k] * (k + 1.0 - projectedX1);
2929
                            reflectedGroundGHI += frontGroundGHI[k] * (k + 1.0 - projectedX1) * albedoAligned[k];
2930
                        }
2931
                        else if (k == index2) {
2932
                            if (k < intervals) {
2933
                                actualGroundGHI += frontGroundGHI[k] * (projectedX2 - k);
2934
                                reflectedGroundGHI += frontGroundGHI[k] * (projectedX2 - k) * albedoAligned[k];
2935
                            }
2936
                            else {
2937
                                actualGroundGHI += frontGroundGHI[k - 100] * (projectedX2 - k);
2938
                                reflectedGroundGHI += frontGroundGHI[k - 100] * (projectedX2 - k) * albedoAligned[k - 100];
2939
                            }
2940
                        }
2941
                        else {
2942
                            if (k < intervals) {
2943
                                actualGroundGHI += frontGroundGHI[k];
2944
                                reflectedGroundGHI += frontGroundGHI[k] * albedoAligned[k];
2945
                            }
2946
                            else {
2947
                                actualGroundGHI += frontGroundGHI[k - 100];
2948
                                reflectedGroundGHI += frontGroundGHI[k - 100] * albedoAligned[k - 100];
2949
                            }
2950
                        }
2951
                    }
2952
                    // Irradiance on the ground in the 1-degree field of view
2953
                    actualGroundGHI /= projectedX2 - projectedX1;
2954
                    reflectedGroundGHI /= projectedX2 - projectedX1;
2955
                }
2956
            }
2957
            frontIrradiance[i] +=
2958
                    0.5 * (cos(j * DTOR) - cos((j + 1) * DTOR)) * MarionAOICorrectionFactorsGlass[j] * reflectedGroundGHI;
2959
            frontReflected[i] += 0.5 * (cos(j * DTOR) - cos((j + 1) * DTOR)) * reflectedGroundGHI *
2960
                                 (1.0 - MarionAOICorrectionFactorsGlass[j] * (1.0 - reflectanceNormalIncidence));
2961
        }
2962
        // Calculate and add direct and circumsolar irradiance components
2963
        incidence(0, tiltRadians * RTOD, surfaceAzimuthRadians * RTOD, 45.0, solarZenithRadians, solarAzimuthRadians,
2964
                  this->enableBacktrack, this->groundCoverageRatio, this->slopeTilt, this->slopeAzm, 
2965
                  this->forceToStow, this->stowAngleDegrees, this->useCustomRotAngles, this->customRotAngle, surfaceAnglesRadians);
2966
        perez(0, calculatedDirectNormal, calculatedDiffuseHorizontal, albedo, surfaceAnglesRadians[0],
2967
              surfaceAnglesRadians[1], solarZenithRadians, poa, diffc);
2968

2969
        double cellShade = pvFrontShadeFraction * cellRows - i;
2970

2971
        // Fully shaded if >1, no shade if < 0, otherwise fractionally shaded
2972
        if (cellShade > 1.0) {
2973
            cellShade = 1.0;
2974
        }
2975
        else if (cellShade < 0.0) {
2976
            cellShade = 0.0;
2977
        }
2978

2979
        // Cell not shaded entirely and incidence angle < 90 degrees
2980
        if (cellShade < 1.0 && surfaceAnglesRadians[0] < M_PI / 2.0) {
2981
            double cor = iamSjerpsKoomen(n2, surfaceAnglesRadians[0]);
2982
            frontIrradiance[i] += (1.0 - cellShade) * (poa[0] + diffc[1]) * cor;
2983
        }
2984
        frontAverageIrradiance += frontIrradiance[i] / cellRows;
2985
    }
2986
}
2987

2988
void irrad::getBackSurfaceIrradiances(double pvBackShadeFraction, double rowToRow, double verticalHeight,
2989
                                      double clearanceGround, double, double horizontalLength,
2990
                                      std::vector<double> rearGroundGHI, std::vector<double> frontGroundGHI,
2991
                                      std::vector<double> frontReflected, std::vector<double> &rearIrradiance,
2992
                                      double &rearAverageIrradiance) {
2993
    // front surface assumed to be glass
2994
    double n2 = 1.526;
2995

2996
    size_t intervals = 100;
2997
    double solarAzimuthRadians = sunAnglesRadians[0];
2998
    double solarZenithRadians = sunAnglesRadians[1];
2999
    double tiltRadians = surfaceAnglesRadians[1];
3000
    double surfaceAzimuthRadians = surfaceAnglesRadians[2];
3001

3002
    // Calculate diffuse isotropic irradiance for a horizontal surface
3003
    perez(0, calculatedDirectNormal, calculatedDiffuseHorizontal, albedo, solarZenithRadians, 0, solarZenithRadians,
3004
          planeOfArrayIrradianceRear, diffuseIrradianceRear);
3005
    double isotropicSkyDiffuse = diffuseIrradianceRear[0];
3006

3007
    // Calculate components for a 90 degree tilt to get horizon brightening
3008
    double surfaceAnglesRadians90[5] = {0, 0, 0, 0, 0};
3009
    incidence(0, 90.0, 180.0, 45.0, solarZenithRadians, solarAzimuthRadians, this->enableBacktrack,
3010
              this->groundCoverageRatio, this->slopeTilt, this->slopeAzm, this->forceToStow, this->stowAngleDegrees, this->useCustomRotAngles, this->customRotAngle, surfaceAnglesRadians90);
3011
    perez(0, calculatedDirectNormal, calculatedDiffuseHorizontal, albedo, surfaceAnglesRadians90[0],
3012
          surfaceAnglesRadians90[1], solarZenithRadians, planeOfArrayIrradianceRear, diffuseIrradianceRear);
3013
    double horizonDiffuse = diffuseIrradianceRear[2];
3014

3015
    // Calculate x,y coordinates of bottom and top edges of PV row in back of desired PV row so that portions of sky and ground viewed by the
3016
    // PV cell may be determined. Origin of x-y axis is the ground point below the lower front edge of the desired PV row. The row in back of
3017
    // the desired row is in the positive x direction.
3018
    double PbotX = rowToRow;                         // x value for point on bottom edge of PV module/panel of row in back of (in PV panel slope lengths)
3019
    double PbotY = clearanceGround;                  // y value for point on bottom edge of PV module/panel of row in back of (in PV panel slope lengths)
3020
    double PtopX = rowToRow +
3021
                   horizontalLength;      // x value for point on top edge of PV module/panel of row in back of (in PV panel slope lengths)
3022
    double PtopY = verticalHeight +
3023
                   clearanceGround; // y value for point on top edge of PV module/panel of row in back of (in PV panel slope lengths)
3024

3025
    // Calculate diffuse and direct component irradiances for each cell row (assuming 6 rows)
3026
    std::vector<double> rearDirectDiffuse;              // the direct and sky diffuse irradiance incident on the rear for each cell row, before losses (shading, soiling, etc.)
3027
    poaRearDirectDiffuse = 0.;                          // the average direct and sky diffuse irradiance incident on the rear, before losses (shading, soiling, etc.)
3028
    std::vector<double> rearRowReflections;             // the reflected irradiance from the rear row on the rear of each cell row
3029
    poaRearRowReflections = 0.;                         // the average reflected irradiance from the rear row on the rear
3030
    std::vector<double> rearGroundReflected;            // the ground reflected irradiance onto the rear of each cell row, considering view factor
3031
    poaRearGroundReflected = 0.;                        // the average ground reflected irradiance onto the rear, considering view factor
3032
    std::vector<double> rearSelfShaded;                 // the direct and circumsolar shaded from being incident on the rear, for each cell
3033
    poaRearSelfShaded = 0.;                             // the average direct and circumsolar shaded from being incident on the rear
3034
    size_t cellRows = poaRearIrradRes;
3035
    for (size_t i = 0; i != cellRows; i++) {
3036
        // Calculate diffuse irradiances and reflected amounts for each cell row over its field of view of 180 degrees,
3037
        // beginning with the angle providing the upper most view of the sky (j=0)
3038
        double PcellX = horizontalLength * (i + 0.5) /
3039
                        ((double) cellRows);                   // x value for location of PV cell with OFFSET FOR SARA REFERENCE CELLS     4/26/2016
3040
        double PcellY = clearanceGround + verticalHeight * (i + 0.5) /
3041
                                          ((double) cellRows); // y value for location of PV cell with OFFSET FOR SARA REFERENCE CELLS     4/26/2016
3042
        double elevationAngleUp = atan((PtopY - PcellY) / (PtopX -
3043
                                                           PcellX));          // Elevation angle up from PV cell to top of PV module/panel, radians
3044
        double elevationAngleDown = atan((PcellY - PbotY) / (PbotX -
3045
                                                             PcellX));        // Elevation angle down from PV cell to bottom of PV module/panel, radians
3046
        size_t iStopIso = (size_t) round((tiltRadians - elevationAngleUp) /
3047
                                         DTOR);                               // Last whole degree in arc range that sees sky, first is 0
3048
        size_t iHorBright = (size_t) round(fmax(0.0, 6.0 - elevationAngleUp /
3049
                                                           DTOR));                       // Number of whole degrees for which horizon brightening occurs
3050
        size_t iStartGrd = (size_t) round((tiltRadians + elevationAngleDown) /
3051
                                          DTOR);                          // First whole degree in arc range that sees ground, last is 180
3052

3053
        rearIrradiance.push_back(0);
3054
        rearDirectDiffuse.push_back(0);
3055
        for (size_t j = 0; j != iStopIso; j++) {
3056
            double rear_isotropic_horizon_diffuse = 0.5 * (cos(j * DTOR) - cos((j + 1) * DTOR)) * MarionAOICorrectionFactorsGlass[j] * isotropicSkyDiffuse;
3057
            if ((iStopIso - j) <= iHorBright) {
3058
                rear_isotropic_horizon_diffuse += 0.5 * (cos(j * DTOR) - cos((j + 1) * DTOR)) * MarionAOICorrectionFactorsGlass[j] *
3059
                    horizonDiffuse / (0.5 * (cos(84 * DTOR) - cos(90 * DTOR)));
3060
            }
3061
            rearIrradiance[i] += rear_isotropic_horizon_diffuse;
3062
            rearDirectDiffuse[i] += rear_isotropic_horizon_diffuse;
3063
        }
3064

3065
        // Add reflections from PV module front surfaces
3066
        rearRowReflections.push_back(0);
3067
        for (size_t j = iStopIso; j < iStartGrd; j++) {
3068
            double diagonalDistance = (PbotX - PcellX) / cos(elevationAngleDown);
3069
            double startAlpha = -(double) (j - iStopIso) * DTOR + elevationAngleUp + elevationAngleDown;
3070
            double stopAlpha = -(double) (j + 1 - iStopIso) * DTOR + elevationAngleUp + elevationAngleDown;
3071
            double m = diagonalDistance * sin(startAlpha);
3072
            double theta = M_PI - elevationAngleDown - (M_PI / 2.0 - startAlpha) - tiltRadians;
3073
            double projectedX2 = m / cos(theta);
3074

3075
            m = diagonalDistance * sin(stopAlpha);
3076
            theta = M_PI - elevationAngleDown - (M_PI / 2.0 - stopAlpha) - tiltRadians;
3077
            double projectedX1 = m / cos(theta);
3078
            projectedX1 = fmax(0.0, projectedX1);
3079

3080
            double PVreflectedIrradiance = 0.0;
3081
            double deltaCell = 1.0 / cellRows;
3082
            double tolerance = 0.0001;
3083
            for (size_t k = 0; k < cellRows; k++) {
3084
                double cellBottom = k * deltaCell;
3085
                double cellTop = (k + 1) * deltaCell;
3086
                double cellLengthSeen = 0.0;
3087

3088
                if (cellBottom >= projectedX1 - tolerance && cellTop <= projectedX2 + tolerance) {
3089
                    cellLengthSeen = cellTop - cellBottom;
3090
                }
3091
                else if (cellBottom <= projectedX1 + tolerance && cellTop >= projectedX2 - tolerance) {
3092
                    cellLengthSeen = projectedX2 - projectedX1;
3093
                }
3094
                else if (cellBottom >= projectedX1 - tolerance && projectedX2 > cellBottom - tolerance &&
3095
                         cellTop >= projectedX2 - tolerance) {
3096
                    cellLengthSeen = projectedX2 - cellBottom;
3097
                }
3098
                else if (cellBottom <= projectedX1 + tolerance && projectedX1 < cellTop + tolerance &&
3099
                         cellTop <= projectedX2 + tolerance) {
3100
                    cellLengthSeen = cellTop - projectedX1;
3101
                }
3102

3103
                PVreflectedIrradiance += cellLengthSeen * frontReflected[k];
3104
            }
3105
            PVreflectedIrradiance /= projectedX2 - projectedX1;
3106
            double rear_row_reflections = 0.5 * (cos(j * DTOR) - cos((j + 1) * DTOR)) * MarionAOICorrectionFactorsGlass[j] *
3107
                                 PVreflectedIrradiance;                                                                             // ** Rear row reflected, through glass
3108
            rearIrradiance[i] += rear_row_reflections;
3109
            rearRowReflections[i] += rear_row_reflections;
3110
        }
3111

3112

3113
        // Add ground reflected component
3114
        std::vector<double> albedoAligned;
3115
        if (trackingMode == 0 || trackingMode == 1 || trackingMode == 4) {          // 0=fixed, 1=one-axis, 4=seasonal tilt
3116
            // subdivide spatial albedos to match ground GHI length and align reference point at front of row
3117
            albedoAligned = divideAndAlignAlbedos(albedoSpatial, intervals, trackingMode == 1, horizontalLength, rowToRow, surfaceAnglesRadians[3]);
3118
        }
3119
        else {
3120
            double average_albedo = std::accumulate(albedoSpatial.begin(), albedoSpatial.end(), 0.) / albedoSpatial.size();
3121
            albedoAligned.assign(intervals, average_albedo);
3122
        }
3123

3124
        rearGroundReflected.push_back(0);
3125
        for (size_t j = iStartGrd; j < 180; j++) {
3126
            double startElevationDown = (double) (j - iStartGrd) * DTOR + elevationAngleDown;
3127
            double stopElevationDown = (double) (j + 1 - iStartGrd) * DTOR + elevationAngleDown;
3128
            double projectedX2 = PcellX + PcellY / tan(startElevationDown);
3129
            double projectedX1 = PcellX + PcellY / tan(stopElevationDown);
3130
            double actualGroundGHI = 0.0;
3131
            double reflectedGroundGHI = 0.0;
3132

3133
            if (std::abs(projectedX1 - projectedX2) > 0.99 * rowToRow) {
3134
                // Use average value if projection approximates the rtr
3135
                actualGroundGHI = std::accumulate(rearGroundGHI.begin(), rearGroundGHI.end(), 0.) / rearGroundGHI.size();
3136
                reflectedGroundGHI = actualGroundGHI * std::accumulate(albedoAligned.begin(), albedoAligned.end(), 0.) / albedoAligned.size();
3137
            }
3138
            else {
3139
                projectedX1 = intervals * projectedX1 / rowToRow;
3140
                projectedX2 = intervals * projectedX2 / rowToRow;
3141

3142
                // offset so array indexed are less than number of intervals
3143
                while (projectedX1 >= intervals || projectedX2 >= intervals) {
3144
                    projectedX1 -= intervals;
3145
                    projectedX2 -= intervals;
3146
                }
3147
                while (projectedX1 < -(int) intervals || projectedX2 < -(int) intervals) {
3148
                    projectedX1 += intervals;
3149
                    projectedX2 += intervals;
3150
                }
3151
                int index1 = std::min(static_cast<int>(intervals - 1), static_cast<int>(projectedX1 + intervals) - (int) intervals);
3152
                int index2 = std::min(static_cast<int>(intervals - 1), static_cast<int>(projectedX2 + intervals) - (int) intervals);
3153

3154
                if (index1 == index2) {
3155
                    if (index1 < 0) {
3156
                        actualGroundGHI = frontGroundGHI[index1 + 100];
3157
                        reflectedGroundGHI = frontGroundGHI[index1 + 100] * albedoAligned[index1 + 100];
3158
                    }
3159
                    else {
3160
                        actualGroundGHI = rearGroundGHI[index1];
3161
                        reflectedGroundGHI = rearGroundGHI[index1] * albedoAligned[index1];
3162
                    }
3163
                }
3164
                else {
3165
                    // Sum irradiances on the ground if projects are in different groundGHI elements
3166
                    for (int k = index1; k <= index2; k++) {
3167
                        if (k == index1) {
3168
                            if (k < 0) {
3169
                                actualGroundGHI += frontGroundGHI[k + intervals] * (k + 1.0 - projectedX1);
3170
                                reflectedGroundGHI += frontGroundGHI[k + intervals] * (k + 1.0 - projectedX1) * albedoAligned[k + intervals];
3171
                            }
3172
                            else {
3173
                                actualGroundGHI += rearGroundGHI[k] * (k + 1.0 - projectedX1);
3174
                                reflectedGroundGHI += rearGroundGHI[k] * (k + 1.0 - projectedX1) * albedoAligned[k];
3175
                            }
3176
                        }
3177
                        else if (k == index2) {
3178
                            if (k < 0) {
3179
                                actualGroundGHI += frontGroundGHI[k + intervals] * (projectedX2 - k);
3180
                                reflectedGroundGHI += frontGroundGHI[k + intervals] * (projectedX2 - k) * albedoAligned[k + intervals];
3181
                            }
3182
                            else {
3183
                                actualGroundGHI += rearGroundGHI[k] * (projectedX2 - k);
3184
                                reflectedGroundGHI += rearGroundGHI[k] * (projectedX2 - k) * albedoAligned[k];
3185
                            }
3186
                        }
3187
                        else {
3188
                            if (k < 0) {
3189
                                actualGroundGHI += frontGroundGHI[k + 100];
3190
                                reflectedGroundGHI += frontGroundGHI[k + 100] * albedoAligned[k + 100];
3191
                            }
3192
                            else {
3193
                                actualGroundGHI += rearGroundGHI[k];
3194
                                reflectedGroundGHI += rearGroundGHI[k] * albedoAligned[k];
3195
                            }
3196
                        }
3197
                    }
3198
                    // Irradiance on the ground in the 1-degree field of view
3199
                    actualGroundGHI /= projectedX2 - projectedX1;
3200
                    reflectedGroundGHI /= projectedX2 - projectedX1;
3201
                }
3202
            }
3203
            double rear_ground_reflected = 0.5 * (cos(j * DTOR) - cos((j + 1) * DTOR)) * MarionAOICorrectionFactorsGlass[j] * reflectedGroundGHI;          // ** Ground reflected, through glass ("View factor to rear row")
3204
            rearIrradiance[i] += rear_ground_reflected;
3205
            rearGroundReflected[i] += rear_ground_reflected;                    
3206
        }
3207
        // Calculate and add direct and circumsolar irradiance components
3208
        incidence(0, 180.0 - tiltRadians * RTOD, (surfaceAzimuthRadians * RTOD - 180.0), 45.0, solarZenithRadians,
3209
                  solarAzimuthRadians, this->enableBacktrack,
3210
                  this->groundCoverageRatio, this->slopeTilt, this->slopeAzm, this->forceToStow, this->stowAngleDegrees, this->useCustomRotAngles, this->customRotAngle, surfaceAnglesRadians);
3211
        perez(0, calculatedDirectNormal, calculatedDiffuseHorizontal, albedo, surfaceAnglesRadians[0],
3212
              surfaceAnglesRadians[1], solarZenithRadians, planeOfArrayIrradianceRear, diffuseIrradianceRear);
3213

3214
        double rear_direct_circumsolar = planeOfArrayIrradianceRear[0] + diffuseIrradianceRear[1];
3215
        rearDirectDiffuse[i] += rear_direct_circumsolar;
3216

3217
        double cellShade = pvBackShadeFraction * cellRows - i;
3218

3219
        // Fully shaded if >1, no shade if < 0, otherwise fractionally shaded
3220
        if (cellShade > 1.0) {
3221
            cellShade = 1.0;
3222
        }
3223
        else if (cellShade < 0.0) {
3224
            cellShade = 0.0;
3225
        }
3226

3227
        // Cell not shaded entirely and incidence angle < 90 degrees
3228
        rearSelfShaded.push_back(0);
3229
        if (cellShade < 1.0 && surfaceAnglesRadians[0] < M_PI / 2.0) {
3230
            double iamMod = iamSjerpsKoomen(n2, surfaceAnglesRadians[0]);
3231
            rearIrradiance[i] += (1.0 - cellShade) * rear_direct_circumsolar * iamMod;                        // ** (1 - Rear self shading loss) * (Rear direct and diffuse (circumsolar only)), through glass loss
3232
            rearSelfShaded[i] = cellShade * rear_direct_circumsolar * iamMod;
3233
        }
3234

3235
        rearAverageIrradiance += rearIrradiance[i] / cellRows;
3236
        poaRearDirectDiffuse += rearDirectDiffuse[i] / cellRows;
3237
        poaRearRowReflections += rearRowReflections[i] / cellRows;
3238
        poaRearSelfShaded += rearSelfShaded[i] / cellRows;
3239
        poaRearGroundReflected += rearGroundReflected[i] / cellRows;
3240
        double xy = 1.;
3241
    }
3242

3243
    // Flip the row rear spatial irradiance if tracking after solar noon (because the tilt range = [0, 90] degrees, therefore the tilt convention flips at solar noon)
3244
    if (trackingMode == 1 && surfaceAnglesRadians[3] > 0.) {
3245
        std::reverse(rearIrradiance.begin(), rearIrradiance.end());
3246
    }
3247
}
3248

3249
void irrad::getBackSurfaceIrradiancesCS(double pvBackShadeFraction, double rowToRow, double verticalHeight,
3250
    double clearanceGround, double, double horizontalLength,
3251
    std::vector<double> rearGroundGHI, std::vector<double> frontGroundGHI,
3252
    std::vector<double> frontReflected, std::vector<double>& rearIrradiance,
3253
    double& rearAverageIrradiance) {
3254
    // front surface assumed to be glass
3255
    double n2 = 1.526;
3256

3257
    size_t intervals = 100;
3258
    double solarAzimuthRadians = sunAnglesRadians[0];
3259
    double solarZenithRadians = sunAnglesRadians[1];
3260
    double tiltRadians = surfaceAnglesRadians[1];
3261
    double surfaceAzimuthRadians = surfaceAnglesRadians[2];
3262

3263
    // Calculate diffuse isotropic irradiance for a horizontal surface
3264
    perez(0, clearskyIrradiance[1], clearskyIrradiance[2], albedo, solarZenithRadians, 0, solarZenithRadians,
3265
        planeOfArrayIrradianceRearCS, diffuseIrradianceRearCS);
3266
    double isotropicSkyDiffuse = diffuseIrradianceRearCS[0];
3267

3268
    // Calculate components for a 90 degree tilt to get horizon brightening
3269
    double surfaceAnglesRadians90[5] = { 0, 0, 0, 0, 0 };
3270
    incidence(0, 90.0, 180.0, 45.0, solarZenithRadians, solarAzimuthRadians, this->enableBacktrack,
3271
        this->groundCoverageRatio, this->slopeTilt, this->slopeAzm, this->forceToStow, this->stowAngleDegrees, this->useCustomRotAngles, this->customRotAngle, surfaceAnglesRadians90);
3272
    perez(0, clearskyIrradiance[1], clearskyIrradiance[2], albedo, surfaceAnglesRadians90[0],
3273
        surfaceAnglesRadians90[1], solarZenithRadians, planeOfArrayIrradianceRearCS, diffuseIrradianceRear);
3274
    double horizonDiffuse = diffuseIrradianceRearCS[2];
3275

3276
    // Calculate x,y coordinates of bottom and top edges of PV row in back of desired PV row so that portions of sky and ground viewed by the
3277
    // PV cell may be determined. Origin of x-y axis is the ground point below the lower front edge of the desired PV row. The row in back of
3278
    // the desired row is in the positive x direction.
3279
    double PbotX = rowToRow;                         // x value for point on bottom edge of PV module/panel of row in back of (in PV panel slope lengths)
3280
    double PbotY = clearanceGround;                  // y value for point on bottom edge of PV module/panel of row in back of (in PV panel slope lengths)
3281
    double PtopX = rowToRow +
3282
        horizontalLength;      // x value for point on top edge of PV module/panel of row in back of (in PV panel slope lengths)
3283
    double PtopY = verticalHeight +
3284
        clearanceGround; // y value for point on top edge of PV module/panel of row in back of (in PV panel slope lengths)
3285

3286
// Calculate diffuse and direct component irradiances for each cell row (assuming 6 rows)
3287
    std::vector<double> rearDirectDiffuse;              // the direct and sky diffuse irradiance incident on the rear for each cell row, before losses (shading, soiling, etc.)
3288
    poaRearDirectDiffuseCS = 0.;                          // the average direct and sky diffuse irradiance incident on the rear, before losses (shading, soiling, etc.)
3289
    std::vector<double> rearRowReflections;             // the reflected irradiance from the rear row on the rear of each cell row
3290
    poaRearRowReflectionsCS = 0.;                         // the average reflected irradiance from the rear row on the rear
3291
    std::vector<double> rearGroundReflected;            // the ground reflected irradiance onto the rear of each cell row, considering view factor
3292
    poaRearGroundReflectedCS = 0.;                        // the average ground reflected irradiance onto the rear, considering view factor
3293
    std::vector<double> rearSelfShaded;                 // the direct and circumsolar shaded from being incident on the rear, for each cell
3294
    poaRearSelfShadedCS = 0.;                             // the average direct and circumsolar shaded from being incident on the rear
3295
    size_t cellRows = poaRearIrradRes;
3296
    for (size_t i = 0; i != cellRows; i++) {
3297
        // Calculate diffuse irradiances and reflected amounts for each cell row over its field of view of 180 degrees,
3298
        // beginning with the angle providing the upper most view of the sky (j=0)
3299
        double PcellX = horizontalLength * (i + 0.5) /
3300
            ((double)cellRows);                   // x value for location of PV cell with OFFSET FOR SARA REFERENCE CELLS     4/26/2016
3301
        double PcellY = clearanceGround + verticalHeight * (i + 0.5) /
3302
            ((double)cellRows); // y value for location of PV cell with OFFSET FOR SARA REFERENCE CELLS     4/26/2016
3303
        double elevationAngleUp = atan((PtopY - PcellY) / (PtopX -
3304
            PcellX));          // Elevation angle up from PV cell to top of PV module/panel, radians
3305
        double elevationAngleDown = atan((PcellY - PbotY) / (PbotX -
3306
            PcellX));        // Elevation angle down from PV cell to bottom of PV module/panel, radians
3307
        size_t iStopIso = (size_t)round((tiltRadians - elevationAngleUp) /
3308
            DTOR);                               // Last whole degree in arc range that sees sky, first is 0
3309
        size_t iHorBright = (size_t)round(fmax(0.0, 6.0 - elevationAngleUp /
3310
            DTOR));                       // Number of whole degrees for which horizon brightening occurs
3311
        size_t iStartGrd = (size_t)round((tiltRadians + elevationAngleDown) /
3312
            DTOR);                          // First whole degree in arc range that sees ground, last is 180
3313

3314
        rearIrradiance.push_back(0);
3315
        rearDirectDiffuse.push_back(0);
3316
        for (size_t j = 0; j != iStopIso; j++) {
3317
            double rear_isotropic_horizon_diffuse = 0.5 * (cos(j * DTOR) - cos((j + 1) * DTOR)) * MarionAOICorrectionFactorsGlass[j] * isotropicSkyDiffuse;
3318
            if ((iStopIso - j) <= iHorBright) {
3319
                rear_isotropic_horizon_diffuse += 0.5 * (cos(j * DTOR) - cos((j + 1) * DTOR)) * MarionAOICorrectionFactorsGlass[j] *
3320
                    horizonDiffuse / (0.5 * (cos(84 * DTOR) - cos(90 * DTOR)));
3321
            }
3322
            rearIrradiance[i] += rear_isotropic_horizon_diffuse;
3323
            rearDirectDiffuse[i] += rear_isotropic_horizon_diffuse;
3324
        }
3325

3326
        // Add reflections from PV module front surfaces
3327
        rearRowReflections.push_back(0);
3328
        for (size_t j = iStopIso; j < iStartGrd; j++) {
3329
            double diagonalDistance = (PbotX - PcellX) / cos(elevationAngleDown);
3330
            double startAlpha = -(double)(j - iStopIso) * DTOR + elevationAngleUp + elevationAngleDown;
3331
            double stopAlpha = -(double)(j + 1 - iStopIso) * DTOR + elevationAngleUp + elevationAngleDown;
3332
            double m = diagonalDistance * sin(startAlpha);
3333
            double theta = M_PI - elevationAngleDown - (M_PI / 2.0 - startAlpha) - tiltRadians;
3334
            double projectedX2 = m / cos(theta);
3335

3336
            m = diagonalDistance * sin(stopAlpha);
3337
            theta = M_PI - elevationAngleDown - (M_PI / 2.0 - stopAlpha) - tiltRadians;
3338
            double projectedX1 = m / cos(theta);
3339
            projectedX1 = fmax(0.0, projectedX1);
3340

3341
            double PVreflectedIrradiance = 0.0;
3342
            double deltaCell = 1.0 / cellRows;
3343
            double tolerance = 0.0001;
3344
            for (size_t k = 0; k < cellRows; k++) {
3345
                double cellBottom = k * deltaCell;
3346
                double cellTop = (k + 1) * deltaCell;
3347
                double cellLengthSeen = 0.0;
3348

3349
                if (cellBottom >= projectedX1 - tolerance && cellTop <= projectedX2 + tolerance) {
3350
                    cellLengthSeen = cellTop - cellBottom;
3351
                }
3352
                else if (cellBottom <= projectedX1 + tolerance && cellTop >= projectedX2 - tolerance) {
3353
                    cellLengthSeen = projectedX2 - projectedX1;
3354
                }
3355
                else if (cellBottom >= projectedX1 - tolerance && projectedX2 > cellBottom - tolerance &&
3356
                    cellTop >= projectedX2 - tolerance) {
3357
                    cellLengthSeen = projectedX2 - cellBottom;
3358
                }
3359
                else if (cellBottom <= projectedX1 + tolerance && projectedX1 < cellTop + tolerance &&
3360
                    cellTop <= projectedX2 + tolerance) {
3361
                    cellLengthSeen = cellTop - projectedX1;
3362
                }
3363

3364
                PVreflectedIrradiance += cellLengthSeen * frontReflected[k];
3365
            }
3366
            PVreflectedIrradiance /= projectedX2 - projectedX1;
3367
            double rear_row_reflections = 0.5 * (cos(j * DTOR) - cos((j + 1) * DTOR)) * MarionAOICorrectionFactorsGlass[j] *
3368
                PVreflectedIrradiance;                                                                             // ** Rear row reflected, through glass
3369
            rearIrradiance[i] += rear_row_reflections;
3370
            rearRowReflections[i] += rear_row_reflections;
3371
        }
3372

3373

3374
        // Add ground reflected component
3375
        std::vector<double> albedoAligned;
3376
        if (trackingMode == 0 || trackingMode == 1 || trackingMode == 4) {          // 0=fixed, 1=one-axis, 4=seasonal tilt
3377
            // subdivide spatial albedos to match ground GHI length and align reference point at front of row
3378
            albedoAligned = divideAndAlignAlbedos(albedoSpatial, intervals, trackingMode == 1, horizontalLength, rowToRow, surfaceAnglesRadians[3]);
3379
        }
3380
        else {
3381
            double average_albedo = std::accumulate(albedoSpatial.begin(), albedoSpatial.end(), 0.) / albedoSpatial.size();
3382
            albedoAligned.assign(intervals, average_albedo);
3383
        }
3384

3385
        rearGroundReflected.push_back(0);
3386
        for (size_t j = iStartGrd; j < 180; j++) {
3387
            double startElevationDown = (double)(j - iStartGrd) * DTOR + elevationAngleDown;
3388
            double stopElevationDown = (double)(j + 1 - iStartGrd) * DTOR + elevationAngleDown;
3389
            double projectedX2 = PcellX + PcellY / tan(startElevationDown);
3390
            double projectedX1 = PcellX + PcellY / tan(stopElevationDown);
3391
            double actualGroundGHI = 0.0;
3392
            double reflectedGroundGHI = 0.0;
3393

3394
            if (std::abs(projectedX1 - projectedX2) > 0.99 * rowToRow) {
3395
                // Use average value if projection approximates the rtr
3396
                actualGroundGHI = std::accumulate(rearGroundGHI.begin(), rearGroundGHI.end(), 0.) / rearGroundGHI.size();
3397
                reflectedGroundGHI = actualGroundGHI * std::accumulate(albedoAligned.begin(), albedoAligned.end(), 0.) / albedoAligned.size();
3398
            }
3399
            else {
3400
                projectedX1 = intervals * projectedX1 / rowToRow;
3401
                projectedX2 = intervals * projectedX2 / rowToRow;
3402

3403
                // offset so array indexed are less than number of intervals
3404
                while (projectedX1 >= intervals || projectedX2 >= intervals) {
3405
                    projectedX1 -= intervals;
3406
                    projectedX2 -= intervals;
3407
                }
3408
                while (projectedX1 < -(int)intervals || projectedX2 < -(int)intervals) {
3409
                    projectedX1 += intervals;
3410
                    projectedX2 += intervals;
3411
                }
3412
                int index1 = std::min(static_cast<int>(intervals - 1), static_cast<int>(projectedX1 + intervals) - (int)intervals);
3413
                int index2 = std::min(static_cast<int>(intervals - 1), static_cast<int>(projectedX2 + intervals) - (int)intervals);
3414

3415
                if (index1 == index2) {
3416
                    if (index1 < 0) {
3417
                        actualGroundGHI = frontGroundGHI[index1 + 100];
3418
                        reflectedGroundGHI = frontGroundGHI[index1 + 100] * albedoAligned[index1 + 100];
3419
                    }
3420
                    else {
3421
                        actualGroundGHI = rearGroundGHI[index1];
3422
                        reflectedGroundGHI = rearGroundGHI[index1] * albedoAligned[index1];
3423
                    }
3424
                }
3425
                else {
3426
                    // Sum irradiances on the ground if projects are in different groundGHI elements
3427
                    for (int k = index1; k <= index2; k++) {
3428
                        if (k == index1) {
3429
                            if (k < 0) {
3430
                                actualGroundGHI += frontGroundGHI[k + intervals] * (k + 1.0 - projectedX1);
3431
                                reflectedGroundGHI += frontGroundGHI[k + intervals] * (k + 1.0 - projectedX1) * albedoAligned[k + intervals];
3432
                            }
3433
                            else {
3434
                                actualGroundGHI += rearGroundGHI[k] * (k + 1.0 - projectedX1);
3435
                                reflectedGroundGHI += rearGroundGHI[k] * (k + 1.0 - projectedX1) * albedoAligned[k];
3436
                            }
3437
                        }
3438
                        else if (k == index2) {
3439
                            if (k < 0) {
3440
                                actualGroundGHI += frontGroundGHI[k + intervals] * (projectedX2 - k);
3441
                                reflectedGroundGHI += frontGroundGHI[k + intervals] * (projectedX2 - k) * albedoAligned[k + intervals];
3442
                            }
3443
                            else {
3444
                                actualGroundGHI += rearGroundGHI[k] * (projectedX2 - k);
3445
                                reflectedGroundGHI += rearGroundGHI[k] * (projectedX2 - k) * albedoAligned[k];
3446
                            }
3447
                        }
3448
                        else {
3449
                            if (k < 0) {
3450
                                actualGroundGHI += frontGroundGHI[k + 100];
3451
                                reflectedGroundGHI += frontGroundGHI[k + 100] * albedoAligned[k + 100];
3452
                            }
3453
                            else {
3454
                                actualGroundGHI += rearGroundGHI[k];
3455
                                reflectedGroundGHI += rearGroundGHI[k] * albedoAligned[k];
3456
                            }
3457
                        }
3458
                    }
3459
                    // Irradiance on the ground in the 1-degree field of view
3460
                    actualGroundGHI /= projectedX2 - projectedX1;
3461
                    reflectedGroundGHI /= projectedX2 - projectedX1;
3462
                }
3463
            }
3464
            double rear_ground_reflected = 0.5 * (cos(j * DTOR) - cos((j + 1) * DTOR)) * MarionAOICorrectionFactorsGlass[j] * reflectedGroundGHI;          // ** Ground reflected, through glass ("View factor to rear row")
3465
            rearIrradiance[i] += rear_ground_reflected;
3466
            rearGroundReflected[i] += rear_ground_reflected;
3467
        }
3468
        // Calculate and add direct and circumsolar irradiance components
3469
        incidence(0, 180.0 - tiltRadians * RTOD, (surfaceAzimuthRadians * RTOD - 180.0), 45.0, solarZenithRadians,
3470
            solarAzimuthRadians, this->enableBacktrack,
3471
            this->groundCoverageRatio, this->slopeTilt, this->slopeAzm, this->forceToStow, this->stowAngleDegrees, this->useCustomRotAngles, this->customRotAngle, surfaceAnglesRadians);
3472
        perez(0, clearskyIrradiance[1], clearskyIrradiance[2], albedo, surfaceAnglesRadians[0],
3473
            surfaceAnglesRadians[1], solarZenithRadians, planeOfArrayIrradianceRearCS, diffuseIrradianceRearCS);
3474

3475
        double rear_direct_circumsolar = planeOfArrayIrradianceRearCS[0] + diffuseIrradianceRearCS[1];
3476
        rearDirectDiffuse[i] += rear_direct_circumsolar;
3477

3478
        double cellShade = pvBackShadeFraction * cellRows - i;
3479

3480
        // Fully shaded if >1, no shade if < 0, otherwise fractionally shaded
3481
        if (cellShade > 1.0) {
3482
            cellShade = 1.0;
3483
        }
3484
        else if (cellShade < 0.0) {
3485
            cellShade = 0.0;
3486
        }
3487

3488
        // Cell not shaded entirely and incidence angle < 90 degrees
3489
        rearSelfShaded.push_back(0);
3490
        if (cellShade < 1.0 && surfaceAnglesRadians[0] < M_PI / 2.0) {
3491
            double iamMod = iamSjerpsKoomen(n2, surfaceAnglesRadians[0]);
3492
            rearIrradiance[i] += (1.0 - cellShade) * rear_direct_circumsolar * iamMod;                        // ** (1 - Rear self shading loss) * (Rear direct and diffuse (circumsolar only)), through glass loss
3493
            rearSelfShaded[i] = cellShade * rear_direct_circumsolar * iamMod;
3494
        }
3495

3496
        rearAverageIrradiance += rearIrradiance[i] / cellRows;
3497
        poaRearDirectDiffuseCS += rearDirectDiffuse[i] / cellRows;
3498
        poaRearRowReflectionsCS += rearRowReflections[i] / cellRows;
3499
        poaRearSelfShadedCS += rearSelfShaded[i] / cellRows;
3500
        poaRearGroundReflectedCS += rearGroundReflected[i] / cellRows;
3501
        double xy = 1.;
3502
    }
3503

3504
    // Flip the row rear spatial irradiance if tracking after solar noon (because the tilt range = [0, 90] degrees, therefore the tilt convention flips at solar noon)
3505
    if (trackingMode == 1 && surfaceAnglesRadians[3] > 0.) {
3506
        std::reverse(rearIrradiance.begin(), rearIrradiance.end());
3507
    }
3508
}
3509

3510
double shadeFraction1x(double solar_azimuth, double solar_zenith,
3511
                       double axis_tilt, double axis_azimuth,
3512
                       double gcr, double rotation, double slope_tilt, double slope_azimuth) {
3513
    /*
3514
    Calculate the fraction of a row's width affected by row-to-row beam shading.
3515
    All input angles in degrees.
3516
    Changed 2020-10-15 from complex row-to-row 3D geometry to equivalent (?) simple equations
3517
    */
3518
    double cross_axis_slope = calc_cross_axis_slope(slope_tilt, axis_azimuth, slope_azimuth);
3519
    double truetracking_angle = truetrack(solar_azimuth, solar_zenith, axis_tilt, axis_azimuth);
3520
    double numerator =
3521
            gcr * cosd(rotation) + (gcr * sind(rotation) - tand(cross_axis_slope)) * tand(truetracking_angle) - 1;
3522
    double denominator = gcr * (sind(rotation) * tand(truetracking_angle) + cosd(rotation));
3523
    double fs = numerator / denominator;
3524
    fs = fs < 0 ? 0 : fs;
3525
    fs = fs > 1 ? 1 : fs;
3526
    return fs;
3527
}
3528

3529
std::vector<double> divideAndAlignAlbedos(const std::vector<double>& albedo /*-*/, size_t n_divisions /*-*/, bool isOneAxisTracking /*-*/,
3530
                                          double horizontalLength /*m*/, double rowToRow /*m*/, double surface_rotation /*rad*/) {
3531
    /*
3532
    Subdivide spatial albedos and if 1-axis tracking change reference from the row midline to the front
3533
    */
3534
    assert(n_divisions % albedo.size() == 0);                           // functionality only works for even divisions
3535

3536
    // Upsample vector to n_divisions
3537
    std::vector<double> albedo_aligned;
3538
    for (size_t i = 0; i < albedo.size(); i++) {
3539
        for (size_t j = 0; j < n_divisions / albedo.size(); j++) {
3540
            albedo_aligned.push_back(albedo.at(i));
3541
        }
3542
    }
3543

3544
    if (isOneAxisTracking) {
3545
        // Flip the albedo array if tracking after solar noon (because the tilt range = [0, 90] degrees, therefore the tilt convention flips at solar noon)
3546
        if (surface_rotation > 0.) {
3547
            std::reverse(albedo_aligned.begin(), albedo_aligned.end());
3548
        }
3549

3550
        // Rotate the albedo vector so the first index is at (or overlapping) the front of the row instead of at center
3551
        double L_division = rowToRow / n_divisions;                     // length of a single albedo division
3552
        double n = 0.5 * horizontalLength / L_division;                 // fractional number of albedo segments between front of row and center of row
3553
        size_t leading_segments = n_divisions - std::ceil(n);           // whole albedo segments between center of row and front of row behind
3554
        rotate(albedo_aligned.begin(), albedo_aligned.begin() + leading_segments, albedo_aligned.end());   // move the leading segments to the back of the vector
3555

3556
        // 'Shift' the actual ground albedo locations to front of row by weighting-averaging adjacent divisions
3557
        double frac_div_extending = std::ceil(n) - n;                   // fraction of now first albedo division extending beyond front of row
3558
        double albedo_front_orig = albedo_aligned.front();
3559
        for (size_t i = 0; i < n_divisions - 1; i++) {                  // do last segment separately
3560
            albedo_aligned.at(i) = albedo_aligned.at(i) * (1 - frac_div_extending) + albedo_aligned.at(i + 1) * frac_div_extending;
3561
        }
3562
        albedo_aligned.back() = albedo_aligned.back() * (1 - frac_div_extending) + albedo_front_orig * frac_div_extending;
3563
    }
3564

3565
    return albedo_aligned;
3566
}
3567

3568
std::vector<double> condenseAndAlignGroundIrrad(const std::vector<double>& ground_irr /*W/m2*/, size_t n_divisions /*-*/, bool isOneAxisTracking /*-*/,
3569
                                            double horizontalLength /*m*/, double rowToRow /*m*/, double surface_rotation /*rad*/) {
3570
    /*
3571
    Condense spatial ground irradiances and if 1-axis tracking change reference from the row front to the midline
3572
    */
3573
    assert(ground_irr.size() % n_divisions == 0);                           // functionality only works for even divisions
3574

3575
    std::vector<double> ground_aligned = ground_irr;
3576

3577
    if (isOneAxisTracking) {
3578
        // Rotate the ground irradiance vector so the first index is at (or overlapping) the center of the row instead of at the midline
3579
        double L_division = rowToRow / ground_aligned.size();           // length of a single ground division
3580
        double n = 0.5 * horizontalLength / L_division;                 // fractional number of ground segments between front of row and center of row
3581
        size_t leading_segments = std::floor(n);                        // whole ground segments between front of row and center of row
3582
        rotate(ground_aligned.begin(), ground_aligned.begin() + leading_segments, ground_aligned.end());   // move the leading segments to the back of the vector
3583

3584
        // 'Shift' the actual ground irradiance locations to center of row by weighting-averaging adjacent divisions
3585
        double frac_div_extending = n - std::floor(n);                  // fraction of now first ground division extending beyond center of row
3586
        double ground_front_orig = ground_aligned.front();
3587
        for (size_t i = 0; i < ground_aligned.size() - 1; i++) {        // do last segment separately
3588
            ground_aligned.at(i) = ground_aligned.at(i) * (1 - frac_div_extending) + ground_aligned.at(i + 1) * frac_div_extending;
3589
        }
3590
        ground_aligned.back() = ground_aligned.back() * (1 - frac_div_extending) + ground_front_orig * frac_div_extending;
3591

3592
        // Flip the ground irradiance if tracking after solar noon (because the tilt range = [0, 90] degrees, therefore the tilt convention flips at solar noon)
3593
        if (surface_rotation > 0.) {
3594
            std::reverse(ground_aligned.begin(), ground_aligned.end());
3595
        }
3596
    }
3597

3598
    // Downsample vector to n_divisions
3599
    std::vector<double> ground_condensed;
3600
    size_t num_to_avg = ground_aligned.size() / n_divisions;
3601
    size_t i = 0;
3602
    double sum = 0.;
3603
    while (i < ground_aligned.size()) {
3604
        sum += ground_aligned.at(i);
3605
        if ((i + 1) % num_to_avg == 0) {
3606
            ground_condensed.push_back(sum / num_to_avg);     // add average to output
3607
            sum = 0.;
3608
        }
3609
        i++;
3610
    }
3611

3612
    return ground_condensed;
3613
}
3614

3615
double truetrack(double solar_azimuth, double solar_zenith, double axis_tilt, double axis_azimuth) {
3616
    /*
3617
    Calculate the tracking rotation that minimizes the angle of incidence between
3618
    direct irradiance and the module front surface normal.
3619
    All input and output angles in degrees.
3620
    */
3621
    double solar_elevation = 90 - solar_zenith;
3622
    double sx = cosd(solar_elevation) * sind(solar_azimuth);
3623
    double sy = cosd(solar_elevation) * cosd(solar_azimuth);
3624
    double sz = sind(solar_elevation);
3625
    double sin_ya = sind(axis_azimuth);
3626
    double cos_ya = cosd(axis_azimuth);
3627
    double sin_ba = sind(axis_tilt);
3628
    double cos_ba = cosd(axis_tilt);
3629

3630
    double sxp = sx * cos_ya - sy * sin_ya;
3631
    double szp = sx * sin_ya * sin_ba + sy * sin_ba * cos_ya + sz * cos_ba;
3632
    double theta_t = atan2(sxp, szp) * 180 / M_PI;
3633
    return theta_t;
3634
}
3635

3636
double calc_cross_axis_slope(double slope_tilt, double axis_azimuth, double slope_azimuth) {
3637
    double delta_azimuth = axis_azimuth - slope_azimuth;
3638
    double beta_a = atan(tand(slope_tilt) * cosd(delta_azimuth)) * 180 / M_PI;
3639
    double v_x = sind(delta_azimuth) * cosd(beta_a) * cosd(slope_tilt);
3640
    double v_y = sind(beta_a) * sind(slope_tilt) + cosd(delta_azimuth) * cosd(beta_a) * cosd(slope_tilt);
3641
    double v_z = -sind(delta_azimuth) * sind(slope_tilt) * cosd(beta_a);
3642
    double abs_v = sqrt(pow(v_x, 2) + pow(v_y, 2) + pow(v_z, 2));
3643
    double beta_c = asin(((v_x * cosd(delta_azimuth) - v_y * sind(delta_azimuth)) * sind(beta_a) + v_z * cosd(beta_a)) / abs_v) * 180 / M_PI;
3644
    return beta_c;
3645
}
3646

3647
//Find optimum angle using backtracking.
3648
double backtrack(double truetracking_rotation, double gcr, double axis_slope) {
3649
    /*
3650
    Calculate the backtracking rotation that prevents row to row beam shading
3651
    in 1-axis trackers.
3652
    All input and output angles in degrees.
3653
    Changed 2020-10-15 from iterative self-shading avoidance to closed-form equations
3654
    */
3655
    double cross_axis_slope = axis_slope;
3656

3657
    // check backtracking criterion; if there is no self-shading to avoid, then
3658
    // return the true-tracking angle unmodified:
3659
    double correction_projection =
3660
        std::abs(cosd(truetracking_rotation - cross_axis_slope)) / (gcr * cosd(cross_axis_slope));
3661
    if (std::abs(correction_projection) >= 1) {
3662
        return truetracking_rotation;
3663
    }
3664
    int sign = truetracking_rotation > 0 ? 1 : -1;
3665
    double correction = -sign * acosd(correction_projection);
3666
    return truetracking_rotation + correction;
3667
}
3668

3669
// Begin modified DISC code
3670

3671
double cm[6][6][7][5] =
3672
        {{{{0.385230, 0.385230, 0.385230, 0.462880, 0.317440},
3673
                  {0.338390, 0.338390, 0.221270, 0.316730, 0.503650},
3674
                  {0.235680, 0.235680, 0.241280, 0.157830, 0.269440},
3675
                  {0.830130, 0.830130, 0.171970, 0.841070, 0.457370},
3676
                  {0.548010, 0.548010, 0.478000, 0.966880, 1.036370},
3677
                  {0.548010, 0.548010, 1.000000, 3.012370, 1.976540},
3678
                  {0.582690, 0.582690, 0.229720, 0.892710, 0.569950}},
3679

3680
                 {{0.131280, 0.131280, 0.385460, 0.511070, 0.127940},
3681
                         {0.223710, 0.223710, 0.193560, 0.304560, 0.193940},
3682
                         {0.229970, 0.229970, 0.275020, 0.312730, 0.244610},
3683
                         {0.090100, 0.184580, 0.260500, 0.687480, 0.579440},
3684
                         {0.131530, 0.131530, 0.370190, 1.380350, 1.052270},
3685
                         {1.116250, 1.116250, 0.928030, 3.525490, 2.316920},
3686
                         {0.090100, 0.237000, 0.300040, 0.812470, 0.664970}},
3687

3688
                 {{0.587510, 0.130000, 0.400000, 0.537210, 0.832490},
3689
                         {0.306210, 0.129830, 0.204460, 0.500000, 0.681640},
3690
                         {0.224020, 0.260620, 0.334080, 0.501040, 0.350470},
3691
                         {0.421540, 0.753970, 0.750660, 3.706840, 0.983790},
3692
                         {0.706680, 0.373530, 1.245670, 0.864860, 1.992630},
3693
                         {4.864400, 0.117390, 0.265180, 0.359180, 3.310820},
3694
                         {0.392080, 0.493290, 0.651560, 1.932780, 0.898730}},
3695

3696
                 {{0.126970, 0.126970, 0.126970, 0.126970, 0.126970},
3697
                         {0.810820, 0.810820, 0.810820, 0.810820, 0.810820},
3698
                         {3.241680, 2.500000, 2.291440, 2.291440, 2.291440},
3699
                         {4.000000, 3.000000, 2.000000, 0.975430, 1.965570},
3700
                         {12.494170, 12.494170, 8.000000, 5.083520, 8.792390},
3701
                         {21.744240, 21.744240, 21.744240, 21.744240, 21.744240},
3702
                         {3.241680, 12.494170, 1.620760, 1.375250, 2.331620}},
3703

3704
                 {{0.126970, 0.126970, 0.126970, 0.126970, 0.126970},
3705
                         {0.810820, 0.810820, 0.810820, 0.810820, 0.810820},
3706
                         {3.241680, 2.500000, 2.291440, 2.291440, 2.291440},
3707
                         {4.000000, 3.000000, 2.000000, 0.975430, 1.965570},
3708
                         {12.494170, 12.494170, 8.000000, 5.083520, 8.792390},
3709
                         {21.744240, 21.744240, 21.744240, 21.744240, 21.744240},
3710
                         {3.241680, 12.494170, 1.620760, 1.375250, 2.331620}},
3711

3712
                 {{0.126970, 0.126970, 0.126970, 0.126970, 0.126970},
3713
                         {0.810820, 0.810820, 0.810820, 0.810820, 0.810820},
3714
                         {3.241680, 2.500000, 2.291440, 2.291440, 2.291440},
3715
                         {4.000000, 3.000000, 2.000000, 0.975430, 1.965570},
3716
                         {12.494170, 12.494170, 8.000000, 5.083520, 8.792390},
3717
                         {21.744240, 21.744240, 21.744240, 21.744240, 21.744240},
3718
                         {3.241680, 12.494170, 1.620760, 1.375250, 2.331620}}},
3719

3720
         {{{0.337440, 0.337440, 0.969110, 1.097190, 1.116080},
3721
                  {0.337440, 0.337440, 0.969110, 1.116030, 0.623900},
3722
                  {0.337440, 0.337440, 1.530590, 1.024420, 0.908480},
3723
                  {0.584040, 0.584040, 0.847250, 0.914940, 1.289300},
3724
                  {0.337440, 0.337440, 0.310240, 1.435020, 1.852830},
3725
                  {0.337440, 0.337440, 1.015010, 1.097190, 2.117230},
3726
                  {0.337440, 0.337440, 0.969110, 1.145730, 1.476400}},
3727

3728
                 {{0.300000, 0.300000, 0.700000, 1.100000, 0.796940},
3729
                         {0.219870, 0.219870, 0.526530, 0.809610, 0.649300},
3730
                         {0.386650, 0.386650, 0.119320, 0.576120, 0.685460},
3731
                         {0.746730, 0.399830, 0.470970, 0.986530, 0.785370},
3732
                         {0.575420, 0.936700, 1.649200, 1.495840, 1.335590},
3733
                         {1.319670, 4.002570, 1.276390, 2.644550, 2.518670},
3734
                         {0.665190, 0.678910, 1.012360, 1.199940, 0.986580}},
3735

3736
                 {{0.378870, 0.974060, 0.500000, 0.491880, 0.665290},
3737
                         {0.105210, 0.263470, 0.407040, 0.553460, 0.582590},
3738
                         {0.312900, 0.345240, 1.144180, 0.854790, 0.612280},
3739
                         {0.119070, 0.365120, 0.560520, 0.793720, 0.802600},
3740
                         {0.781610, 0.837390, 1.270420, 1.537980, 1.292950},
3741
                         {1.152290, 1.152290, 1.492080, 1.245370, 2.177100},
3742
                         {0.424660, 0.529550, 0.966910, 1.033460, 0.958730}},
3743

3744
                 {{0.310590, 0.714410, 0.252450, 0.500000, 0.607600},
3745
                         {0.975190, 0.363420, 0.500000, 0.400000, 0.502800},
3746
                         {0.175580, 0.196250, 0.476360, 1.072470, 0.490510},
3747
                         {0.719280, 0.698620, 0.657770, 1.190840, 0.681110},
3748
                         {0.426240,  1.464840,  0.678550, 1.157730, 0.978430},
3749
                         {2.501120,  1.789130,  1.387090,  2.394180,  2.394180},
3750
                         {0.491640, 0.677610,  0.685610, 1.082400, 0.735410}},
3751

3752
                 {{0.597000, 0.500000, 0.300000, 0.310050, 0.413510},
3753
                         {0.314790, 0.336310, 0.400000, 0.400000, 0.442460},
3754
                         {0.166510, 0.460440, 0.552570, 1.000000, 0.461610},
3755
                         {0.401020, 0.559110, 0.403630, 1.016710, 0.671490},
3756
                         {0.400360,  0.750830,  0.842640, 1.802600, 1.023830},
3757
                         {3.315300,  1.510380,  2.443650,  1.638820,  2.133990},
3758
                         {0.530790, 0.745850,  0.693050, 1.458040, 0.804500}},
3759

3760
                 {{0.597000, 0.500000, 0.300000, 0.310050, 0.800920},
3761
                         {0.314790, 0.336310, 0.400000, 0.400000, 0.237040},
3762
                         {0.166510, 0.460440, 0.552570, 1.000000, 0.581990},
3763
                         {0.401020, 0.559110, 0.403630, 1.016710, 0.898570},
3764
                         {0.400360,  0.750830,  0.842640, 1.802600, 3.400390},
3765
                         {3.315300,  1.510380,  2.443650,  1.638820,  2.508780},
3766
                         {0.204340, 1.157740,  2.003080, 2.622080, 1.409380}}},
3767

3768
         {{{1.242210, 1.242210, 1.242210, 1.242210, 1.242210},
3769
                  {0.056980, 0.056980, 0.656990, 0.656990, 0.925160},
3770
                  {0.089090, 0.089090, 1.040430, 1.232480, 1.205300},
3771
                  {1.053850, 1.053850, 1.399690, 1.084640, 1.233340},
3772
                  {1.151540, 1.151540, 1.118290, 1.531640, 1.411840},
3773
                  {1.494980, 1.494980, 1.700000, 1.800810, 1.671600},
3774
                  {1.018450, 1.018450, 1.153600, 1.321890, 1.294670}},
3775

3776
                 {{0.700000, 0.700000, 1.023460, 0.700000, 0.945830},
3777
                         {0.886300, 0.886300, 1.333620, 0.800000, 1.066620},
3778
                         {0.902180, 0.902180, 0.954330, 1.126690, 1.097310},
3779
                         {1.095300, 1.075060, 1.176490, 1.139470, 1.096110},
3780
                         {1.201660, 1.201660, 1.438200, 1.256280, 1.198060},
3781
                         {1.525850, 1.525850, 1.869160, 1.985410, 1.911590},
3782
                         {1.288220, 1.082810, 1.286370, 1.166170, 1.119330}},
3783

3784
                 {{0.600000, 1.029910, 0.859890, 0.550000, 0.813600},
3785
                         {0.604450, 1.029910, 0.859890, 0.656700, 0.928840},
3786
                         {0.455850, 0.750580, 0.804930, 0.823000, 0.911000},
3787
                         {0.526580, 0.932310, 0.908620, 0.983520, 0.988090},
3788
                         {1.036110, 1.100690, 0.848380, 1.035270, 1.042380},
3789
                         {1.048440, 1.652720, 0.900000, 2.350410, 1.082950},
3790
                         {0.817410, 0.976160, 0.861300, 0.974780, 1.004580}},
3791

3792
                 {{0.782110, 0.564280, 0.600000, 0.600000, 0.665740},
3793
                         {0.894480, 0.680730, 0.541990, 0.800000, 0.669140},
3794
                         {0.487460, 0.818950, 0.841830, 0.872540, 0.709040},
3795
                         {0.709310, 0.872780, 0.908480, 0.953290, 0.844350},
3796
                         {0.863920,  0.947770,  0.876220, 1.078750, 0.936910},
3797
                         {1.280350,  0.866720,  0.769790,  1.078750,  0.975130},
3798
                         {0.725420, 0.869970,  0.868810, 0.951190, 0.829220}},
3799

3800
                 {{0.791750, 0.654040, 0.483170, 0.409000, 0.597180},
3801
                         {0.566140, 0.948990, 0.971820, 0.653570, 0.718550},
3802
                         {0.648710, 0.637730, 0.870510, 0.860600, 0.694300},
3803
                         {0.637630, 0.767610, 0.925670, 0.990310, 0.847670},
3804
                         {0.736380,  0.946060,  1.117590, 1.029340, 0.947020},
3805
                         {1.180970,  0.850000,  1.050000,  0.950000,  0.888580},
3806
                         {0.700560, 0.801440,  0.961970, 0.906140, 0.823880}},
3807

3808
                 {{0.500000, 0.500000, 0.586770, 0.470550, 0.629790},
3809
                         {0.500000, 0.500000, 1.056220, 1.260140, 0.658140},
3810
                         {0.500000, 0.500000, 0.631830, 0.842620, 0.582780},
3811
                         {0.554710, 0.734730, 0.985820, 0.915640, 0.898260},
3812
                         {0.712510,  1.205990,  0.909510, 1.078260, 0.885610},
3813
                         {1.899260,  1.559710,  1.000000,  1.150000,  1.120390},
3814
                         {0.653880, 0.793120,  0.903320, 0.944070, 0.796130}}},
3815

3816
         {{{1.000000, 1.000000, 1.050000, 1.170380, 1.178090},
3817
                  {0.960580, 0.960580, 1.059530, 1.179030, 1.131690},
3818
                  {0.871470, 0.871470, 0.995860, 1.141910, 1.114600},
3819
                  {1.201590, 1.201590, 0.993610, 1.109380, 1.126320},
3820
                  {1.065010, 1.065010, 0.828660, 0.939970, 1.017930},
3821
                  {1.065010, 1.065010, 0.623690, 1.119620, 1.132260},
3822
                  {1.071570, 1.071570, 0.958070, 1.114130, 1.127110}},
3823

3824
                 {{0.950000, 0.973390, 0.852520, 1.092200, 1.096590},
3825
                         {0.804120, 0.913870, 0.980990, 1.094580, 1.042420},
3826
                         {0.737540, 0.935970, 0.999940, 1.056490, 1.050060},
3827
                         {1.032980, 1.034540, 0.968460, 1.032080, 1.015780},
3828
                         {0.900000, 0.977210, 0.945960, 1.008840, 0.969960},
3829
                         {0.600000, 0.750000, 0.750000, 0.844710, 0.899100},
3830
                         {0.926800, 0.965030, 0.968520, 1.044910, 1.032310}},
3831

3832
                 {{0.850000, 1.029710, 0.961100, 1.055670, 1.009700},
3833
                         {0.818530, 0.960010, 0.996450, 1.081970, 1.036470},
3834
                         {0.765380, 0.953500, 0.948260, 1.052110, 1.000140},
3835
                         {0.775610, 0.909610, 0.927800, 0.987800, 0.952100},
3836
                         {1.000990, 0.881880, 0.875950, 0.949100, 0.893690},
3837
                         {0.902370, 0.875960, 0.807990, 0.942410, 0.917920},
3838
                         {0.856580, 0.928270, 0.946820, 1.032260, 0.972990}},
3839

3840
                 {{0.750000, 0.857930, 0.983800, 1.056540, 0.980240},
3841
                         {0.750000, 0.987010, 1.013730, 1.133780, 1.038250},
3842
                         {0.800000, 0.947380, 1.012380, 1.091270, 0.999840},
3843
                         {0.800000, 0.914550, 0.908570, 0.999190, 0.915230},
3844
                         {0.778540,  0.800590,  0.799070, 0.902180, 0.851560},
3845
                         {0.680190,  0.317410,  0.507680,  0.388910,  0.646710},
3846
                         {0.794920, 0.912780,  0.960830, 1.057110, 0.947950}},
3847

3848
                 {{0.750000, 0.833890, 0.867530, 1.059890, 0.932840},
3849
                         {0.979700, 0.971470, 0.995510, 1.068490, 1.030150},
3850
                         {0.858850, 0.987920, 1.043220, 1.108700, 1.044900},
3851
                         {0.802400, 0.955110, 0.911660, 1.045070, 0.944470},
3852
                         {0.884890,  0.766210,  0.885390, 0.859070, 0.818190},
3853
                         {0.615680,  0.700000,  0.850000,  0.624620,  0.669300},
3854
                         {0.835570, 0.946150,  0.977090, 1.049350, 0.979970}},
3855

3856
                 {{0.689220, 0.809600, 0.900000, 0.789500, 0.853990},
3857
                         {0.854660, 0.852840, 0.938200, 0.923110, 0.955010},
3858
                         {0.938600, 0.932980, 1.010390, 1.043950, 1.041640},
3859
                         {0.843620, 0.981300, 0.951590, 0.946100, 0.966330},
3860
                         {0.694740,  0.814690,  0.572650, 0.400000, 0.726830},
3861
                         {0.211370,  0.671780,  0.416340,  0.297290,  0.498050},
3862
                         {0.843540, 0.882330,  0.911760, 0.898420, 0.960210}}},
3863

3864
         {{{1.054880, 1.075210, 1.068460, 1.153370, 1.069220},
3865
                  {1.000000, 1.062220, 1.013470, 1.088170, 1.046200},
3866
                  {0.885090, 0.993530, 0.942590, 1.054990, 1.012740},
3867
                  {0.920000, 0.950000, 0.978720, 1.020280, 0.984440},
3868
                  {0.850000, 0.908500, 0.839940, 0.985570, 0.962180},
3869
                  {0.800000, 0.800000, 0.810080, 0.950000, 0.961550},
3870
                  {1.038590, 1.063200, 1.034440, 1.112780, 1.037800}},
3871

3872
                 {{1.017610, 1.028360, 1.058960, 1.133180, 1.045620},
3873
                         {0.920000, 0.998970, 1.033590, 1.089030, 1.022060},
3874
                         {0.912370, 0.949930, 0.979770, 1.020420, 0.981770},
3875
                         {0.847160, 0.935300, 0.930540, 0.955050, 0.946560},
3876
                         {0.880260, 0.867110, 0.874130, 0.972650, 0.883420},
3877
                         {0.627150, 0.627150, 0.700000, 0.774070, 0.845130},
3878
                         {0.973700, 1.006240, 1.026190, 1.071960, 1.017240}},
3879

3880
                 {{1.028710, 1.017570, 1.025900, 1.081790, 1.024240},
3881
                         {0.924980, 0.985500, 1.014100, 1.092210, 0.999610},
3882
                         {0.828570, 0.934920, 0.994950, 1.024590, 0.949710},
3883
                         {0.900810, 0.901330, 0.928830, 0.979570, 0.913100},
3884
                         {0.761030, 0.845150, 0.805360, 0.936790, 0.853460},
3885
                         {0.626400, 0.546750, 0.730500, 0.850000, 0.689050},
3886
                         {0.957630, 0.985480, 0.991790, 1.050220, 0.987900}},
3887

3888
                 {{0.992730, 0.993880, 1.017150, 1.059120, 1.017450},
3889
                         {0.975610, 0.987160, 1.026820, 1.075440, 1.007250},
3890
                         {0.871090, 0.933190, 0.974690, 0.979840, 0.952730},
3891
                         {0.828750, 0.868090, 0.834920, 0.905510, 0.871530},
3892
                         {0.781540,  0.782470,  0.767910, 0.764140, 0.795890},
3893
                         {0.743460,  0.693390,  0.514870,  0.630150,  0.715660},
3894
                         {0.934760, 0.957870,  0.959640, 0.972510, 0.981640}},
3895

3896
                 {{0.965840, 0.941240, 0.987100, 1.022540, 1.011160},
3897
                         {0.988630, 0.994770, 0.976590, 0.950000, 1.034840},
3898
                         {0.958200, 1.018080, 0.974480, 0.920000, 0.989870},
3899
                         {0.811720, 0.869090, 0.812020, 0.850000, 0.821050},
3900
                         {0.682030,  0.679480,  0.632450, 0.746580, 0.738550},
3901
                         {0.668290,  0.445860,  0.500000,  0.678920,  0.696510},
3902
                         {0.926940, 0.953350,  0.959050, 0.876210, 0.991490}},
3903

3904
                 {{0.948940, 0.997760, 0.850000, 0.826520, 0.998470},
3905
                         {1.017860, 0.970000, 0.850000, 0.700000, 0.988560},
3906
                         {1.000000, 0.950000, 0.850000, 0.606240, 0.947260},
3907
                         {1.000000, 0.746140, 0.751740, 0.598390, 0.725230},
3908
                         {0.922210,  0.500000,  0.376800, 0.517110, 0.548630},
3909
                         {0.500000,  0.450000,  0.429970,  0.404490,  0.539940},
3910
                         {0.960430, 0.881630,  0.775640, 0.596350, 0.937680}}},
3911

3912
         {{{1.030000, 1.040000, 1.000000, 1.000000, 1.049510},
3913
                  {1.050000, 0.990000, 0.990000, 0.950000, 0.996530},
3914
                  {1.050000, 0.990000, 0.990000, 0.820000, 0.971940},
3915
                  {1.050000, 0.790000, 0.880000, 0.820000, 0.951840},
3916
                  {1.000000, 0.530000, 0.440000, 0.710000, 0.928730},
3917
                  {0.540000, 0.470000, 0.500000, 0.550000, 0.773950},
3918
                  {1.038270, 0.920180, 0.910930, 0.821140, 1.034560}},
3919

3920
                 {{1.041020, 0.997520, 0.961600, 1.000000, 1.035780},
3921
                         {0.948030, 0.980000, 0.900000, 0.950360, 0.977460},
3922
                         {0.950000, 0.977250, 0.869270, 0.800000, 0.951680},
3923
                         {0.951870, 0.850000, 0.748770, 0.700000, 0.883850},
3924
                         {0.900000, 0.823190, 0.727450, 0.600000, 0.839870},
3925
                         {0.850000, 0.805020, 0.692310, 0.500000, 0.788410},
3926
                         {1.010090, 0.895270, 0.773030, 0.816280, 1.011680}},
3927

3928
                 {{1.022450, 1.004600, 0.983650, 1.000000, 1.032940},
3929
                         {0.943960, 0.999240, 0.983920, 0.905990, 0.978150},
3930
                         {0.936240, 0.946480, 0.850000, 0.850000, 0.930320},
3931
                         {0.816420, 0.885000, 0.644950, 0.817650, 0.865310},
3932
                         {0.742960, 0.765690, 0.561520, 0.700000, 0.827140},
3933
                         {0.643870, 0.596710, 0.474460, 0.600000, 0.651200},
3934
                         {0.971740, 0.940560, 0.714880, 0.864380, 1.001650}},
3935

3936
                 {{0.995260, 0.977010, 1.000000, 1.000000, 1.035250},
3937
                         {0.939810, 0.975250, 0.939980, 0.950000, 0.982550},
3938
                         {0.876870, 0.879440, 0.850000, 0.900000, 0.917810},
3939
                         {0.873480, 0.873450, 0.751470, 0.850000, 0.863040},
3940
                         {0.761470,  0.702360,  0.638770, 0.750000, 0.783120},
3941
                         {0.734080,  0.650000,  0.600000,  0.650000,  0.715660},
3942
                         {0.942160, 0.919100,  0.770340, 0.731170, 0.995180}},
3943

3944
                 {{0.952560, 0.916780, 0.920000, 0.900000, 1.005880},
3945
                         {0.928620, 0.994420, 0.900000, 0.900000, 0.983720},
3946
                         {0.913070, 0.850000, 0.850000, 0.800000, 0.924280},
3947
                         {0.868090, 0.807170, 0.823550, 0.600000, 0.844520},
3948
                         {0.769570,  0.719870,  0.650000, 0.550000, 0.733500},
3949
                         {0.580250,  0.650000,  0.600000,  0.500000,  0.628850},
3950
                         {0.904770, 0.852650,  0.708370, 0.493730, 0.949030}},
3951

3952
                 {{0.911970, 0.800000, 0.800000, 0.800000, 0.956320},
3953
                         {0.912620, 0.682610, 0.750000, 0.700000, 0.950110},
3954
                         {0.653450, 0.659330, 0.700000, 0.600000, 0.856110},
3955
                         {0.648440, 0.600000, 0.641120, 0.500000, 0.695780},
3956
                         {0.570000,  0.550000,  0.598800, 0.400000, 0.560150},
3957
                         {0.475230,  0.500000,  0.518640,  0.339970,  0.520230},
3958
                         {0.743440, 0.592190,  0.603060, 0.316930, 0.794390}}}};
3959

3960
double
3961
ModifiedDISC(const double g[3], const double z[3], double td, double alt, int doy, double &dn) // aka DIRINT model
3962
{
3963
    // Modification history:
3964
    // 25/10/2015 Converted to C++ for use in SAM by David Severin Ryberg
3965
    // 4/14/2015 Corrected error in incrementing i,j, and k array indices
3966
    // 7/16/13. Converted by Bill Marion to C#. The 7/5/91 version provided by Daryl that this
3967
    //  is based on was significantly different than the 6/26/91 I had got from Martin years ago in that
3968
    //  the section on bin interpolating for clear stable cases had been removed.
3969
    // 6/28/2013. Converted to C# from Howard Bisner FORTRAN77 code
3970
    // 5/24/91. Richard's code to do linear interpolation between highest kt' bins added by RS.
3971
    //  RS fixed some typos in Richard's untested code.
3972
    // 6/10/91. Corrected bin interpolation near label 141 [divide by 0.007 and zbin2 not zbin]
3973
    // 6/26/91: Richard perez: Modification of DKT1 calculation
3974
    //  to avoid very low sun distorsion caused by questionable
3975
    //  cosine response of pyranometers
3976
    // 7/5/91:  RS: lines extending beyond col 72 fixed.
3977
    //  Questionable use of x**-y changed to x**(-y)
3978
    //  Made reference to intrinsic dmax1 agree with type
3979

3980
    double cz[3], zenith[3], kt[3], am[3], ktpam[3], kt1[3];
3981

3982
    double ktbin[5] = {0.24, 0.4, 0.56, 0.7, 0.8};
3983
    double zbin[5] = {25.0, 40.0, 55.0, 70.0, 80.0};
3984
    double dktbin[5] = {0.015, 0.035, 0.07, 0.15, 0.3};
3985
    double wbin[3] = {1.0, 2.0, 3.0};
3986
    double rtod = 57.295779513082316;
3987
    double a, b, c, w, knc, bmax, dkt1, io;
3988

3989
    //double dn = 0.0;
3990
    if (g[1] >= 1.0 && cos(z[1]) > 0.0) {   // Model only if present global >= 1 and present zenith < 90 deg
3991
        io = 1367.0 * (1.0 + 0.033 * cos(0.0172142 * doy));    // Extraterrestrial dn
3992
        int j = 0, k = 2, i = 0, l = 0;
3993
        if (g[0] < -998.0 || z[0] < -998.0) {   // Prehour global and zenith were passed missing -999.0
3994
            j = 1;
3995
            kt1[0] = -999.0;
3996
        }
3997
        if (g[2] < -998.0 || z[2] < -998.0) {   // Posthour global and zenith were passed missing -999.0
3998
            k = 1;
3999
            kt1[2] = -999.0;
4000
        }
4001
        for (i = j; i <= k; i++) {   // For each of the 3 hours that have data, find kt prime
4002
            cz[i] = cos(z[i]); // Cosine of zenith angle
4003
            if (cz[i] < 0.0)
4004
                kt1[i] = -999.0;
4005
            else {
4006
                zenith[i] = z[i] * rtod;
4007
                kt[i] = g[i] / (io * Max(0.065, cz[i]));   // Kt
4008
                am[i] = Min(15.25, 1.0 / (cz[i] + 0.15 * (pow(93.9 - zenith[i], -1.253))));
4009
                ktpam[i] = am[i] * exp(-0.0001184 * alt);
4010
                kt1[i] = kt[i] / (1.031 * exp(-1.4 / (0.9 + 9.4 / ktpam[i])) + 0.1);   // Kt prime
4011
            }
4012
        }
4013
        if (kt[1] <= 0.6) {
4014
            a = 0.512 - 1.56 * kt[1] + 2.286 * pow(kt[1], 2.0) - 2.22 * pow(kt[1], 3.0);
4015
            b = 0.37 + 0.962 * kt[1];
4016
            c = -0.28 + 0.932 * kt[1] - 2.048 * pow(kt[1], 2.0);
4017
        }
4018
        else {
4019
            a = -5.743 + 21.77 * kt[1] - 27.49 * pow(kt[1], 2.0) + 11.56 * pow(kt[1], 3.0);
4020
            b = 41.40 - 118.5 * kt[1] + 66.05 * pow(kt[1], 2.0) + 31.9 * pow(kt[1], 3.0);
4021
            c = -47.01 + 184.2 * kt[1] - 222.0 * pow(kt[1], 2.0) + 73.81 * pow(kt[1], 3.0);
4022
        }
4023
        knc = 0.866 - 0.122 * am[1] + 0.0121 * pow(am[1], 2.0) - 0.000653 * pow(am[1], 3.0) +
4024
              0.000014 * pow(am[1], 4.0);
4025
        bmax = io * (knc - (a + b * exp(c * am[1])));
4026
        if (kt1[0] < -998.0 && kt1[2] < -998.0)
4027
            k = 6;
4028
        else {
4029
            if (kt1[0] < -998.0 || zenith[0] >= 85.0)
4030
                dkt1 = std::abs(kt1[2] - kt1[1]);
4031
            else if (kt1[2] < -998.0 || zenith[2] >= 85.0)
4032
                dkt1 = std::abs(kt1[1] - kt1[0]);
4033
            else
4034
                dkt1 = 0.5 * (std::abs(kt1[1] - kt1[0]) + std::abs(kt1[2] - kt1[1]));
4035

4036
            k = 0;
4037
            //while (k < 4 && dkt1 >= dktbin[k])
4038
            while (k < 5 && dkt1 >= dktbin[k])      // Error fix 4/14/2015
4039
                k++;
4040
        }
4041
        i = 0;
4042
        //while (i < 4 && kt1[1] >= ktbin[i])
4043
        while (i < 5 && kt1[1] >= ktbin[i])         // Error fix 4/14/2015
4044
            i++;
4045
        j = 0;
4046
        //while (j < 4 && zenith[1] >= zbin[j])
4047
        while (j < 5 && zenith[1] >= zbin[j])       // Error fix 4/14/2015
4048
            j++;
4049
        if (td < -998.0)
4050
            l = 4;  // l = letter "l'
4051
        else {
4052
            w = exp(-0.075 + 0.07 * td);
4053
            l = 0;
4054
            while (l < 3 && w >= wbin[l])
4055
                l++;
4056
        }
4057
        dn = bmax * cm[i][j][k][l];
4058
        // dn = Max(0.0, dn); //jmf removed 11/30/18 to allow error to be reported by poaDecomp if calculated dn is negative
4059
    }   // End of if present global >= 1
4060

4061
    return kt1[1];
4062
}   // End of ModifiedDISC
4063

4064

4065

4066
void
4067
ModifiedDISC(const double kt[3], const double kt1[3], const double g[3], const double z[3], double td, double /*alt*/,
4068
             int doy, double &dn) // aka DIRINT model
4069
{
4070
    // Calculates direct normal (beam) radiation from global horizontal radiation.
4071
    double cz[3], zenith[3], am[3];
4072
    double ktbin[5] = {0.24, 0.4, 0.56, 0.7, 0.8};
4073
    double zbin[5] = {25.0, 40.0, 55.0, 70.0, 80.0};
4074
    double dktbin[5] = {0.015, 0.035, 0.07, 0.15, 0.3};
4075
    double wbin[3] = {1.0, 2.0, 3.0};
4076
    double rtod = 57.295779513082316;
4077
    double a, b, c, w, knc, bmax, dkt1, io;
4078

4079
    //double dn = 0.0;
4080
    if (g[1] >= 1.0 && cos(z[1]) > 0.0) {   // Model only if present global >= 1 and present zenith < 90 deg
4081

4082
        //std::cout << "yes!\n";
4083

4084
        io = 1367.0 * (1.0 + 0.033 * cos(0.0172142 * doy));    // Extraterrestrial dn
4085
        int j = 0, k = 2, i = 0, l = 0;
4086

4087
        for (i = j; i <= k; i++) {   // For each of the 3 hours that have data, find kt prime
4088
            cz[i] = cos(z[i]); // Cosine of zenith angle
4089
            zenith[i] = z[i] * rtod;
4090
            am[i] = Min(15.25, 1.0 / (cz[i] + 0.15 * (pow(93.9 - zenith[i], -1.253))));
4091
        }
4092
        if (kt[1] <= 0.6) {
4093
            a = 0.512 - 1.56 * kt[1] + 2.286 * pow(kt[1], 2.0) - 2.22 * pow(kt[1], 3.0);
4094
            b = 0.37 + 0.962 * kt[1];
4095
            c = -0.28 + 0.932 * kt[1] - 2.048 * pow(kt[1], 2.0);
4096
        }
4097
        else {
4098
            a = -5.743 + 21.77 * kt[1] - 27.49 * pow(kt[1], 2.0) + 11.56 * pow(kt[1], 3.0);
4099
            b = 41.40 - 118.5 * kt[1] + 66.05 * pow(kt[1], 2.0) + 31.9 * pow(kt[1], 3.0);
4100
            c = -47.01 + 184.2 * kt[1] - 222.0 * pow(kt[1], 2.0) + 73.81 * pow(kt[1], 3.0);
4101
        }
4102
        knc = 0.866 - 0.122 * am[1] + 0.0121 * pow(am[1], 2.0) - 0.000653 * pow(am[1], 3.0) +
4103
              0.000014 * pow(am[1], 4.0);
4104
        bmax = io * (knc - (a + b * exp(c * am[1])));
4105
        //std::cout << "Kt: " << kt[1] << std::endl;
4106
        //std::cout << io << " " << knc << " " << a << " " << b << " " << c << " " << am[1] << std::endl;
4107

4108

4109
        if (kt1[0] < -998.0 && kt1[2] < -998.0)
4110
            k = 6;
4111
        else {
4112
            if (kt1[0] < -998.0 || zenith[0] >= 85.0)
4113
                dkt1 = std::abs(kt1[2] - kt1[1]);
4114
            else if (kt1[2] < -998.0 || zenith[2] >= 85.0)
4115
                dkt1 = std::abs(kt1[1] - kt1[0]);
4116
            else
4117
                dkt1 = 0.5 * (std::abs(kt1[1] - kt1[0]) + std::abs(kt1[2] - kt1[1]));
4118

4119
            k = 0;
4120
            //while (k < 4 && dkt1 >= dktbin[k])
4121
            while (k < 5 && dkt1 >= dktbin[k])      // Error fix 4/14/2015
4122
                k++;
4123
        }
4124
        i = 0;
4125
        //while (i < 4 && kt1[1] >= ktbin[i])
4126
        while (i < 5 && kt1[1] >= ktbin[i])         // Error fix 4/14/2015
4127
            i++;
4128
        j = 0;
4129
        //while (j < 4 && zenith[1] >= zbin[j])
4130
        while (j < 5 && zenith[1] >= zbin[j])       // Error fix 4/14/2015
4131
            j++;
4132
        if (td < -998.0)
4133
            l = 4;  // l = letter "l'
4134
        else {
4135
            w = exp(-0.075 + 0.07 * td);
4136
            l = 0;
4137
            while (l < 3 && w >= wbin[l])
4138
                l++;
4139
        }
4140

4141
        dn = bmax * cm[i][j][k][l];
4142
        // dn = Max(0.0, bmax * cm[i][j][k][l]); //jmf removed 11/30/18 to allow error to be reported by poaDecomp if calculated dn is negative
4143
        //std::cout << dn << " " << bmax << " " << cm[i][j][k][l] << std::endl;
4144
    }   // End of if present global >= 1
4145
    else
4146
        dn = 0;
4147
    return;
4148
}   // End of ModifiedDISC
4149

4150
#endif
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc