|
| 1 | +"""Simple functions to calculate solar position and day-night terminator.""" |
| 2 | +# pylint: disable=invalid-name |
| 3 | +from __future__ import division |
| 4 | + |
| 5 | +import numpy as np |
| 6 | + |
| 7 | + |
| 8 | +def JulianDayFromDate(date, calendar="standard"): |
| 9 | + """Return Julian day from a :class:`datetime.datetime` object. |
| 10 | +
|
| 11 | + Algorithm: |
| 12 | +
|
| 13 | + Meeus, Jean (1998) Astronomical Algorithms (2nd Edition). |
| 14 | + Willmann-Bell, Virginia. p. 63 |
| 15 | +
|
| 16 | + Paramaters |
| 17 | + --------- |
| 18 | +
|
| 19 | + date : datetime.datetime |
| 20 | + a :class:`datetime.datetime` object |
| 21 | +
|
| 22 | + calendar : {'standard', 'gregorian', 'proleptic_gregorian', |
| 23 | + 'julian'}, optional |
| 24 | + if 'standard' or 'gregorian', Julian day follows the Julian |
| 25 | + calendar on and before 1582-10-05, and the Gregorian calendar |
| 26 | + after 1582-10-15; if 'proleptic_gregorian', Julian day follows |
| 27 | + the Gregorian calendar; if 'julian', Julian day follows the |
| 28 | + Julian calendar |
| 29 | +
|
| 30 | + Returns |
| 31 | + ------- |
| 32 | +
|
| 33 | + jd : float |
| 34 | + the Julian day, with resolution of 1 second |
| 35 | + """ |
| 36 | + |
| 37 | + # Based on `redate.py` by David Finlayson. |
| 38 | + year, month, day = date.year, date.month, date.day |
| 39 | + hour, minute, second = date.hour, date.minute, date.second |
| 40 | + |
| 41 | + # Convert time to fractions of a day. |
| 42 | + day = day + hour / 24.0 + minute / 1440.0 + second / 86400.0 |
| 43 | + |
| 44 | + # Start Meeus algorithm (variables are in his notation). |
| 45 | + if month < 3: |
| 46 | + month = month + 12 |
| 47 | + year = year - 1 |
| 48 | + A = int(year / 100) |
| 49 | + jd = ( |
| 50 | + int(365.25 * (year + 4716)) + |
| 51 | + int(30.6001 * (month + 1)) + |
| 52 | + day - 1524.5 |
| 53 | + ) |
| 54 | + |
| 55 | + # Optionally adjust `jd` for the switch from Julian to Gregorian calendar. |
| 56 | + # Here assumed to have occurred the day after 1582 October 4 |
| 57 | + if calendar in ["standard", "gregorian"]: |
| 58 | + if jd >= 2299170.5: |
| 59 | + # 1582 October 15 (Gregorian Calendar). |
| 60 | + B = 2 - A + int(A / 4) |
| 61 | + elif jd < 2299160.5: |
| 62 | + # 1582 October 5 (Julian Calendar). |
| 63 | + B = 0 |
| 64 | + else: |
| 65 | + raise ValueError("impossible date (falls in gap between end of " |
| 66 | + "Julian calendar and start of Gregorian calendar") |
| 67 | + elif calendar == "proleptic_gregorian": |
| 68 | + B = 2 - A + int(A / 4) |
| 69 | + elif calendar == "julian": |
| 70 | + B = 0 |
| 71 | + else: |
| 72 | + raise ValueError("unknown calendar '{0}' (must be one of 'standard', " |
| 73 | + "'gregorian'," |
| 74 | + " 'proleptic_gregorian' or 'julian'".format(calendar)) |
| 75 | + |
| 76 | + # Adjust for Julian calendar if necessary. |
| 77 | + jd = jd + B |
| 78 | + |
| 79 | + return jd |
| 80 | + |
| 81 | + |
| 82 | +def epem(date): |
| 83 | + """Return the Greenwich hour angle. |
| 84 | +
|
| 85 | + Parameters |
| 86 | + ---------- |
| 87 | +
|
| 88 | + date : datetime.datetime |
| 89 | + a :class:`datetime.datetime` object (assumed UTC) |
| 90 | +
|
| 91 | + Returns |
| 92 | + ------- |
| 93 | +
|
| 94 | + gha : float |
| 95 | + Greenwich hour angle, i.e. the angle between the Greenwich |
| 96 | + meridian and the meridian containing the subsolar point. |
| 97 | +
|
| 98 | + dec : float |
| 99 | + solar declination |
| 100 | + """ |
| 101 | + |
| 102 | + dg2rad = np.pi / 180. |
| 103 | + rad2dg = 1. / dg2rad |
| 104 | + |
| 105 | + # Compute Julian day from UTC `datetime.datetime` object (note that |
| 106 | + # `datetime.datetime` objects use proleptic Gregorian calendar). |
| 107 | + jday = JulianDayFromDate(date, calendar="proleptic_gregorian") |
| 108 | + jd = np.floor(jday) # Truncate to integer. |
| 109 | + |
| 110 | + # Compute UTC hour. |
| 111 | + ut = date.hour + date.minute / 60. + date.second / 3600. |
| 112 | + |
| 113 | + # Calculate number of centuries from J2000. |
| 114 | + t = (jd + (ut / 24.) - 2451545.0) / 36525. |
| 115 | + |
| 116 | + # Mean longitude corrected for aberration. |
| 117 | + l = (280.460 + 36000.770 * t) % 360 # noqa: E741 |
| 118 | + |
| 119 | + # Mean anomaly. |
| 120 | + g = 357.528 + 35999.050 * t |
| 121 | + |
| 122 | + # Ecliptic longitude. |
| 123 | + lm = l + 1.915 * np.sin(g * dg2rad) + 0.020 * np.sin(2 * g * dg2rad) |
| 124 | + |
| 125 | + # Obliquity of the ecliptic. |
| 126 | + ep = 23.4393 - 0.01300 * t |
| 127 | + |
| 128 | + # Equation of time. |
| 129 | + eqtime = ( |
| 130 | + -1.915 * np.sin(g * dg2rad) |
| 131 | + - 0.020 * np.sin(2 * g * dg2rad) |
| 132 | + + 2.466 * np.sin(2 * lm * dg2rad) |
| 133 | + - 0.053 * np.sin(4 * lm * dg2rad) |
| 134 | + ) |
| 135 | + |
| 136 | + # Greenwich hour angle. |
| 137 | + gha = 15 * ut - 180 + eqtime |
| 138 | + |
| 139 | + # Declination of Sun. |
| 140 | + dec = np.arcsin(np.sin(ep * dg2rad) * np.sin(lm * dg2rad)) * rad2dg |
| 141 | + |
| 142 | + return gha, dec |
| 143 | + |
| 144 | + |
| 145 | +def daynight_terminator(date, delta, lonmin, lonmax): |
| 146 | + """Return the day-night terminator. |
| 147 | +
|
| 148 | + Parameters |
| 149 | + ---------- |
| 150 | +
|
| 151 | + date : datetime.datetime |
| 152 | + a :class:`datetime.datetime` object (assumed UTC) |
| 153 | +
|
| 154 | + delta : float |
| 155 | + input longitude grid step in degrees used to compute the |
| 156 | + day-night terminator |
| 157 | +
|
| 158 | + lonmin : float |
| 159 | + minimum input longitude in degrees |
| 160 | +
|
| 161 | + lonmax : float |
| 162 | + maximum input longitude in degrees |
| 163 | +
|
| 164 | + Returns |
| 165 | + ------- |
| 166 | +
|
| 167 | + lons : array-like |
| 168 | + array of longitudes of the day-night terminator |
| 169 | +
|
| 170 | + lats : array-like |
| 171 | + array of latitudes of the day-night terminator |
| 172 | +
|
| 173 | + tau : float |
| 174 | + Greenwich hour angle for the input datetime |
| 175 | +
|
| 176 | + dec : float |
| 177 | + solar declination for the input datetime |
| 178 | + """ |
| 179 | + |
| 180 | + dg2rad = np.pi / 180.0 |
| 181 | + lons = np.arange( |
| 182 | + lonmin, |
| 183 | + lonmax + 0.5 * delta, |
| 184 | + delta, |
| 185 | + dtype=np.float32 |
| 186 | + ) |
| 187 | + |
| 188 | + # Compute Greenwich hour angle and solar declination. |
| 189 | + tau, dec = epem(date) |
| 190 | + |
| 191 | + # Compute day-night terminator from Greenwich hour angle and declination. |
| 192 | + longitude = lons + tau |
| 193 | + lats = ( |
| 194 | + np.arctan(-np.cos(longitude * dg2rad) / np.tan(dec * dg2rad)) / dg2rad |
| 195 | + ) |
| 196 | + |
| 197 | + return lons, lats, tau, dec |
| 198 | + |
| 199 | + |
| 200 | +def daynight_grid(date, delta, lonmin, lonmax): |
| 201 | + """Return day-night mask array. |
| 202 | +
|
| 203 | + Parameters |
| 204 | + ---------- |
| 205 | +
|
| 206 | + date : datetime.datetime |
| 207 | + a :class:`datetime.datetime` object (assumed UTC) |
| 208 | +
|
| 209 | + delta : float |
| 210 | + input longitude grid step in degrees used to compute the |
| 211 | + day-night terminator |
| 212 | +
|
| 213 | + lonmin : float |
| 214 | + minimum input longitude in degrees |
| 215 | +
|
| 216 | + lonmax : float |
| 217 | + maximum input longitude in degrees |
| 218 | +
|
| 219 | + Returns |
| 220 | + ------- |
| 221 | +
|
| 222 | + lons2 : array-like |
| 223 | + meshgrid of longitudes of the day-night mask array |
| 224 | +
|
| 225 | + lats2 : array-like |
| 226 | + meshgrid of latitudes of the day-night mask array |
| 227 | +
|
| 228 | + daynight : ~numpy.ma.MaskedArray |
| 229 | + day-night mask array (masked for day, 1 for night) |
| 230 | + """ |
| 231 | + |
| 232 | + lons, lats, _, dec = daynight_terminator(date, delta, lonmin, lonmax) |
| 233 | + |
| 234 | + # Create meshgrid of longitudes and latitudes. |
| 235 | + lats2 = np.arange(-90, 90 + 0.5 * delta, delta, dtype=np.float32) |
| 236 | + lons2, lats2 = np.meshgrid(lons, lats2) |
| 237 | + |
| 238 | + # Create day-night grid (0 for day, 1 for night). |
| 239 | + nlats, nlons = len(lats2), len(lons) |
| 240 | + lats = lats[np.newaxis, :] * np.ones((nlats, nlons), dtype=np.float32) |
| 241 | + daynight = np.ones(lons2.shape, np.int8) |
| 242 | + if dec > 0: # NH summer |
| 243 | + daynight = np.where(lats2 > lats, 0, daynight) |
| 244 | + else: # NH winter |
| 245 | + daynight = np.where(lats2 < lats, 0, daynight) |
| 246 | + |
| 247 | + # Create day-night masked array (with day areas masked). |
| 248 | + daynight_mask = 1 - daynight |
| 249 | + daynight = np.ma.array(daynight, mask=daynight_mask) |
| 250 | + |
| 251 | + return lons2, lats2, daynight |
0 commit comments