|
| 1 | +from pvlib.tools import sind, cosd, asind |
| 2 | +import numpy as np |
| 3 | +import pandas as pd |
| 4 | +from solposx import refraction |
| 5 | +from solposx.tools import _pandas_to_utc, _fractional_hour |
| 6 | + |
| 7 | + |
| 8 | +def michalsky(times, latitude, longitude, spencer_correction=True, |
| 9 | + julian_date='original'): |
| 10 | + """ |
| 11 | + Calculate solar position using Michalsky's algorithm. |
| 12 | +
|
| 13 | + Michalsky's algorithm [1]_ has a stated accuracy of 0.01 degrees |
| 14 | + from 1950 to 2050. |
| 15 | +
|
| 16 | + Parameters |
| 17 | + ---------- |
| 18 | + times : pandas.DatetimeIndex |
| 19 | + Must be localized or UTC will be assumed. |
| 20 | + latitude : float |
| 21 | + Latitude in decimal degrees. Positive north of equator, negative |
| 22 | + to south. [degrees] |
| 23 | + longitude : float |
| 24 | + Longitude in decimal degrees. Positive east of prime meridian, |
| 25 | + negative to west. [degrees] |
| 26 | + spencer_correction : bool, default True |
| 27 | + Applies the correction suggested by Spencer [2]_ so the algorithm |
| 28 | + works for all latitudes. |
| 29 | + julian_date : string, default 'original' |
| 30 | + Julian date calculation. Can be one of the following: |
| 31 | + * ``'original'``: calculation based on Michalsky's paper [1]_. |
| 32 | + * ``'pandas'``: calculation using a pandas build-in function |
| 33 | +
|
| 34 | + Returns |
| 35 | + ------- |
| 36 | + DataFrame with the following columns (all values in degrees): |
| 37 | +
|
| 38 | + * elevation : actual sun elevation (not accounting for refraction). |
| 39 | + * apparent_elevation : sun elevation, accounting for |
| 40 | + atmospheric refraction. |
| 41 | + * zenith : actual sun zenith (not accounting for refraction). |
| 42 | + * apparent_zenith : sun zenith, accounting for atmospheric |
| 43 | + refraction. |
| 44 | + * azimuth : sun azimuth, east of north. |
| 45 | +
|
| 46 | + Raises |
| 47 | + ------ |
| 48 | + ValueError |
| 49 | + An error is raised if the julian_date calculation is not `original` |
| 50 | + or `pandas`. |
| 51 | +
|
| 52 | + Notes |
| 53 | + ----- |
| 54 | + The Michalsky algorithm is based equations in the Astronomical Almanac. |
| 55 | +
|
| 56 | + As pointed out by Spencer (1989) [2]_, the original Michalsky |
| 57 | + algorithm [1]_ did not work for the southern hemisphere. This |
| 58 | + implementation includes by default the correction provided by Spencer such |
| 59 | + that it works for all latitudes. |
| 60 | +
|
| 61 | + Minor clarifications were made to the original paper have been published |
| 62 | + as errata in [3]_ and [4]_. |
| 63 | +
|
| 64 | + The Julian date calculation in the original Michalsky paper [1]_ ensures |
| 65 | + the stated accuracy (0.01 degrees) only for the period 1950 - 2050. Outside |
| 66 | + this period, the Julian date does not handle leap years correctly. Thus, |
| 67 | + there is the option to use the |
| 68 | + :py:func:`pandas.DatetimeIndex.to_julian_date` method. |
| 69 | +
|
| 70 | + References |
| 71 | + ---------- |
| 72 | + .. [1] J. J. Michalsky, "The Astronomical Almanac’s algorithm for |
| 73 | + approximate solar position (1950–2050)," Solar Energy, vol. 40, no. 3. |
| 74 | + Elsevier BV, pp. 227–235, 1988. :doi:`10.1016/0038-092x(88)90045-x`. |
| 75 | + .. [2] J. W. Spencer, “Comments on The Astronomical Almanac’s Algorithm for |
| 76 | + Approximate Solar Position (1950–2050),” Solar Energy, vol. 42, no. 4. |
| 77 | + Elsevier BV, p. 353, 1989. :doi:`10.1016/0038-092x(89)90039-x`. |
| 78 | + .. [3] J. J. Michalsky, “Errata,” Solar Energy, vol. 41, no. 1. Elsevier |
| 79 | + BV, p. 113, 1988. :doi:`10.1016/0038-092x(88)90122-3`. |
| 80 | + .. [4] J. J. Michalsky, “Errata,” Solar Energy, vol. 43, no. 5. Elsevier |
| 81 | + BV, p. 323, 1989. :doi:`10.1016/0038-092x(89)90122-9`. |
| 82 | + """ |
| 83 | + times_utc = _pandas_to_utc(times) |
| 84 | + |
| 85 | + hour = _fractional_hour(times_utc) |
| 86 | + |
| 87 | + if julian_date == 'original': |
| 88 | + year = times_utc.year |
| 89 | + day = times_utc.dayofyear |
| 90 | + delta = year - 1949 |
| 91 | + leap = np.floor(delta / 4) |
| 92 | + jd = 2432916.5 + delta * 365 + leap + day + hour / 24 |
| 93 | + elif julian_date == 'pandas': |
| 94 | + jd = times_utc.to_julian_date() |
| 95 | + else: |
| 96 | + raise ValueError( |
| 97 | + "Either `original` or `pandas` has to be chosen for the Julian" |
| 98 | + " date calculation.") |
| 99 | + |
| 100 | + n = jd - 2451545.0 |
| 101 | + |
| 102 | + # L - mean longitude [degrees] |
| 103 | + L = 280.460 + 0.9856474 * n |
| 104 | + # L has to be between 0 and 360 deg |
| 105 | + L = L % 360 |
| 106 | + |
| 107 | + # g - mean anomaly [degrees] |
| 108 | + g = 357.528 + 0.9856003 * n |
| 109 | + # g has to be between 0 and 360 deg |
| 110 | + g = g % 360 |
| 111 | + |
| 112 | + # l - ecliptic longitude [degrees] |
| 113 | + l = L + 1.915 * sind(g) + 0.02 * sind(2*g) |
| 114 | + # l has to be between 0 and 360 deg |
| 115 | + l = l % 360 |
| 116 | + |
| 117 | + # ep - obliquity of the ecliptic [degrees] |
| 118 | + ep = 23.439 - 0.0000004 * n |
| 119 | + |
| 120 | + # ra - right ascension [degrees] |
| 121 | + ra = np.rad2deg(np.arctan2(cosd(ep) * sind(l), cosd(l))) |
| 122 | + # ra has to be between 0 and 360 deg |
| 123 | + ra = ra % 360 |
| 124 | + |
| 125 | + # dec - declination angle [degrees] |
| 126 | + dec = asind(sind(ep) * sind(l)) |
| 127 | + |
| 128 | + # gmst - Greenwich mean sidereal time [hr] |
| 129 | + gmst = 6.697375 + 0.0657098242 * n + hour |
| 130 | + # gmst has to be between 0 and 24 h |
| 131 | + gmst = gmst % 24 |
| 132 | + |
| 133 | + # lmst - local mean sideral time [hr] |
| 134 | + lmst = gmst + longitude / 15 # to convert deg to h, divide with 15 |
| 135 | + # lmst has to be between 0 and 24 h |
| 136 | + lmst = lmst % 24 |
| 137 | + |
| 138 | + # ha - hour angle [hr] |
| 139 | + ha = lmst - ra / 15 |
| 140 | + # ha has to be between -12 and 12 h |
| 141 | + ha = (ha + 12) % 24 - 12 |
| 142 | + |
| 143 | + # el - solar elevation angle [degrees] |
| 144 | + el = asind(sind(dec) * sind(latitude) + cosd(dec) * cosd(latitude) |
| 145 | + * cosd(15 * ha)) # to convert h to deg, multiply with 15 |
| 146 | + |
| 147 | + # az - azimuth [degrees] |
| 148 | + az = asind(-cosd(dec) * sind(15 * ha) / cosd(el)) |
| 149 | + |
| 150 | + if spencer_correction: |
| 151 | + # Spencer correction for the azimuth quadrant assignment |
| 152 | + cos_az = sind(dec) - sind(el) * sind(latitude) |
| 153 | + az = np.where((cos_az >= 0) & (sind(az) < 0), 360 + az, az) |
| 154 | + az = np.where(cos_az < 0, 180 - az, az) |
| 155 | + else: |
| 156 | + # Original Michalsky implementation does not work for all latitudes |
| 157 | + # calcualte critical elevation |
| 158 | + elc = asind(sind(dec) / sind(latitude)) |
| 159 | + # correct azimuth using critical elevation |
| 160 | + az = np.where(el >= elc, 180 - az, az) |
| 161 | + az = np.where((el <= elc) & (ha > 0), az + 360, az) |
| 162 | + az = az % 360 |
| 163 | + |
| 164 | + # refraction correction |
| 165 | + r = refraction.michalsky(el) |
| 166 | + |
| 167 | + result = pd.DataFrame({ |
| 168 | + 'elevation': el, |
| 169 | + 'apparent_elevation': el + r, |
| 170 | + 'zenith': 90 - el, |
| 171 | + 'apparent_zenith': 90 - (el + r), |
| 172 | + 'azimuth': az, |
| 173 | + }, index=times) |
| 174 | + return result |
0 commit comments