├── LICENSE ├── README.md ├── suncalc.py └── test.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Abe Miessler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SunCalcPy 2 | 3 | A Python library for calculating sun/moon times, positions and phases. Includes methods for getting: 4 | 5 | * sunrise 6 | * sunset 7 | * moonrise 8 | * moonset 9 | * golden hour 10 | * sun position 11 | * moon position 12 | * moon illumination 13 | * and more! 14 | 15 | ### Installing 16 | 17 | `pip install suncalcPy` 18 | 19 | ### Usage examples: 20 | 21 | ##### Get sunrise, sunset, golden hour and other times for San Francisco: 22 | 23 | ``` 24 | >>> import suncalc 25 | >>> suncalc.getTimes(datetime.now(), 37.7749, -122.4194) 26 | { 27 | 'sunriseEnd': '2017-09-06 06:48:24', 28 | 'goldenHourEnd': '2017-09-06 07:20:27', 29 | 'dusk': '2017-09-06 19:59:44', 30 | 'nightEnd': '2017-09-06 05:15:09', 31 | 'night': '2017-09-06 21:03:39', 32 | 'goldenHour': '2017-09-06 18:58:21', 33 | 'sunset': '2017-09-06 19:33:08', 34 | 'nauticalDawn': '2017-09-06 05:47:35', 35 | 'sunsetStart': '2017-09-06 19:30:24', 36 | 'dawn': '2017-09-06 06:19:04', 37 | 'nauticalDusk': '2017-09-06 20:31:13', 38 | 'sunrise': '2017-09-06 06:45:40' 39 | } 40 | ``` 41 | 42 | ##### Get moon illumination information: 43 | 44 | ``` 45 | >>> import suncalc 46 | >>> suncalc.getMoonIllumination(datetime.now()) 47 | { 48 | 'phase': 0.5198419002220316, 49 | 'angle': 1.574687975565145, 50 | 'fraction': 0.9961193570459752 51 | } 52 | ``` 53 | 54 | ##### Get moonrise/moonset times for San Francisco: 55 | 56 | ``` 57 | >>> import suncalc 58 | >>> suncalc.getMoonTimes(datetime.now(), 37.7749, -122.4194) 59 | { 60 | 'rise': datetime.datetime(2017, 9, 6, 20, 4, 29, 213367), 61 | 'set': datetime.datetime(2017, 9, 6, 6, 56, 30, 536332) 62 | } 63 | ``` -------------------------------------------------------------------------------- /suncalc.py: -------------------------------------------------------------------------------- 1 | import math 2 | from datetime import datetime, timedelta 3 | import time 4 | import calendar 5 | 6 | PI = 3.141592653589793 # math.pi 7 | sin = math.sin 8 | cos = math.cos 9 | tan = math.tan 10 | asin = math.asin 11 | atan = math.atan2 12 | acos = math.acos 13 | rad = PI / 180.0 14 | e = rad * 23.4397 # obliquity of the Earth 15 | 16 | dayMs = 1000 * 60 * 60 * 24 17 | J1970 = 2440588 18 | J2000 = 2451545 19 | J0 = 0.0009 20 | 21 | times = [ 22 | [-0.833, 'sunrise', 'sunset' ], 23 | [ -0.3, 'sunriseEnd', 'sunsetStart' ], 24 | [ -6, 'dawn', 'dusk' ], 25 | [ -12, 'nauticalDawn', 'nauticalDusk'], 26 | [ -18, 'nightEnd', 'night' ], 27 | [ 6, 'goldenHourEnd', 'goldenHour' ] 28 | ] 29 | 30 | def rightAscension(l, b): 31 | return atan(sin(l) * cos(e) - tan(b) * sin(e), cos(l)) 32 | 33 | def declination(l, b): 34 | return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)) 35 | 36 | def azimuth(H, phi, dec): 37 | return atan(sin(H), cos(H) * sin(phi) - tan(dec) * cos(phi)) 38 | 39 | def altitude(H, phi, dec): 40 | return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(H)) 41 | 42 | def siderealTime(d, lw): 43 | return rad * (280.16 + 360.9856235 * d) - lw 44 | 45 | def toJulian(date): 46 | return (time.mktime(date.timetuple()) * 1000) / dayMs - 0.5 + J1970 47 | 48 | def fromJulian(j): 49 | return datetime.fromtimestamp(((j + 0.5 - J1970) * dayMs)/1000.0) 50 | 51 | def toDays(date): 52 | return toJulian(date) - J2000 53 | 54 | def julianCycle(d, lw): 55 | return round(d - J0 - lw / (2 * PI)) 56 | 57 | def approxTransit(Ht, lw, n): 58 | return J0 + (Ht + lw) / (2 * PI) + n 59 | 60 | def solarTransitJ(ds, M, L): 61 | return J2000 + ds + 0.0053 * sin(M) - 0.0069 * sin(2 * L) 62 | 63 | def hourAngle(h, phi, d): 64 | try: 65 | ret = acos((sin(h) - sin(phi) * sin(d)) / (cos(phi) * cos(d))) 66 | return ret 67 | except ValueError as e: 68 | print(h, phi, d, "=>", e) 69 | 70 | def observerAngle(height): 71 | return -2.076 * math.sqrt(height) / 60 72 | 73 | def solarMeanAnomaly(d): 74 | return rad * (357.5291 + 0.98560028 * d) 75 | 76 | def eclipticLongitude(M): 77 | C = rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)) # equation of center 78 | P = rad * 102.9372 # perihelion of the Earth 79 | return M + C + P + PI 80 | 81 | def sunCoords(d): 82 | M = solarMeanAnomaly(d) 83 | L = eclipticLongitude(M) 84 | return dict( 85 | dec= declination(L, 0), 86 | ra= rightAscension(L, 0) 87 | ) 88 | 89 | def getSetJ(h, lw, phi, dec, n, M, L): 90 | w = hourAngle(h, phi, dec) 91 | a = approxTransit(w, lw, n) 92 | return solarTransitJ(a, M, L) 93 | 94 | # geocentric ecliptic coordinates of the moon 95 | def moonCoords(d): 96 | L = rad * (218.316 + 13.176396 * d) 97 | M = rad * (134.963 + 13.064993 * d) 98 | F = rad * (93.272 + 13.229350 * d) 99 | 100 | l = L + rad * 6.289 * sin(M) 101 | b = rad * 5.128 * sin(F) 102 | dt = 385001 - 20905 * cos(M) 103 | 104 | return dict( 105 | ra=rightAscension(l, b), 106 | dec=declination(l, b), 107 | dist=dt 108 | ) 109 | 110 | def getMoonIllumination(date): 111 | """Gets illumination properties of the moon for the given time.""" 112 | d = toDays(date) 113 | s = sunCoords(d) 114 | m = moonCoords(d) 115 | 116 | # distance from Earth to Sun in km 117 | sdist = 149598000 118 | phi = acos(sin(s["dec"]) * sin(m["dec"]) + cos(s["dec"]) * cos(m["dec"]) * cos(s["ra"] - m["ra"])) 119 | inc = atan(sdist * sin(phi), m["dist"] - sdist * cos(phi)) 120 | angle = atan(cos(s["dec"]) * sin(s["ra"] - m["ra"]), sin(s["dec"]) * cos(m["dec"]) - cos(s["dec"]) * sin(m["dec"]) * cos(s["ra"] - m["ra"])) 121 | 122 | return dict( 123 | fraction=(1 + cos(inc)) / 2, 124 | phase= 0.5 + 0.5 * inc * (-1 if angle < 0 else 1) / PI, 125 | angle= angle 126 | ) 127 | 128 | def getSunrise(date, lat, lng): 129 | ret = getTimes(date, lat, lng) 130 | return ret["sunrise"] 131 | 132 | def getTimes(date, lat, lng, height=0): 133 | """Gets sun rise/set properties for the given time, location and height.""" 134 | lw = rad * -lng 135 | phi = rad * lat 136 | 137 | dh = observerAngle(height) 138 | 139 | d = toDays(date) 140 | n = julianCycle(d, lw) 141 | ds = approxTransit(0, lw, n) 142 | 143 | M = solarMeanAnomaly(ds) 144 | L = eclipticLongitude(M) 145 | dec = declination(L, 0) 146 | 147 | Jnoon = solarTransitJ(ds, M, L) 148 | 149 | result = dict( 150 | solarNoon=fromJulian(Jnoon).strftime('%Y-%m-%d %H:%M:%S'), 151 | nadir=fromJulian(Jnoon - 0.5).strftime('%Y-%m-%d %H:%M:%S') 152 | ) 153 | 154 | for i in range(0, len(times)): 155 | time = times[i] 156 | h0 = (time[0] + dh) * rad 157 | 158 | Jset = getSetJ(h0, lw, phi, dec, n, M, L) 159 | Jrise = Jnoon - (Jset - Jnoon) 160 | result[time[1]] = fromJulian(Jrise).strftime('%Y-%m-%d %H:%M:%S') 161 | result[time[2]] = fromJulian(Jset).strftime('%Y-%m-%d %H:%M:%S') 162 | 163 | return result 164 | 165 | def hoursLater(date, h): 166 | return date + timedelta(hours=h) 167 | 168 | def getMoonTimes(date, lat, lng): 169 | """Gets moon rise/set properties for the given time and location.""" 170 | 171 | t = date.replace(hour=0,minute=0,second=0) 172 | 173 | hc = 0.133 * rad 174 | h0 = getMoonPosition(t, lat, lng)["altitude"] - hc 175 | rise = 0 176 | sett = 0 177 | 178 | # go in 2-hour chunks, each time seeing if a 3-point quadratic curve crosses zero (which means rise or set) 179 | for i in range(1,25,2): 180 | h1 = getMoonPosition(hoursLater(t, i), lat, lng)["altitude"] - hc 181 | h2 = getMoonPosition(hoursLater(t, i + 1), lat, lng)["altitude"] - hc 182 | 183 | a = (h0 + h2) / 2 - h1 184 | b = (h2 - h0) / 2 185 | xe = -b / (2 * a) 186 | ye = (a * xe + b) * xe + h1 187 | d = b * b - 4 * a * h1 188 | roots = 0 189 | 190 | if d >= 0: 191 | dx = math.sqrt(d) / (abs(a) * 2) 192 | x1 = xe - dx 193 | x2 = xe + dx 194 | if abs(x1) <= 1: 195 | roots += 1 196 | if abs(x2) <= 1: 197 | roots += 1 198 | if x1 < -1: 199 | x1 = x2 200 | 201 | if roots == 1: 202 | if h0 < 0: 203 | rise = i + x1 204 | else: 205 | sett = i + x1 206 | 207 | elif roots == 2: 208 | rise = i + (x2 if ye < 0 else x1) 209 | sett = i + (x1 if ye < 0 else x2) 210 | 211 | if (rise and sett): 212 | break 213 | 214 | h0 = h2 215 | 216 | result = dict() 217 | 218 | if (rise): 219 | result["rise"] = hoursLater(t, rise) 220 | if (sett): 221 | result["set"] = hoursLater(t, sett) 222 | 223 | if (not rise and not sett): 224 | value = 'alwaysUp' if ye > 0 else 'alwaysDown' 225 | result[value] = True 226 | 227 | return result 228 | 229 | def getMoonPosition(date, lat, lng): 230 | """Gets positional attributes of the moon for the given time and location.""" 231 | 232 | lw = rad * -lng 233 | phi = rad * lat 234 | d = toDays(date) 235 | 236 | c = moonCoords(d) 237 | H = siderealTime(d, lw) - c["ra"] 238 | h = altitude(H, phi, c["dec"]) 239 | 240 | # altitude correction for refraction 241 | h = h + rad * 0.017 / tan(h + rad * 10.26 / (h + rad * 5.10)) 242 | pa = atan(sin(H), tan(phi) * cos(c["dec"]) - sin(c["dec"]) * cos(H)) 243 | 244 | return dict( 245 | azimuth=azimuth(H, phi, c["dec"]), 246 | altitude=h, 247 | distance=c["dist"], 248 | parallacticAngle=pa 249 | ) 250 | 251 | def getPosition(date, lat, lng): 252 | """Returns positional attributes of the sun for the given time and location.""" 253 | lw = rad * -lng 254 | phi = rad * lat 255 | d = toDays(date) 256 | 257 | c = sunCoords(d) 258 | H = siderealTime(d, lw) - c["ra"] 259 | # print("d", d, "c",c,"H",H,"phi", phi) 260 | return dict( 261 | azimuth=azimuth(H, phi, c["dec"]), 262 | altitude=altitude(H, phi, c["dec"]) 263 | ) 264 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timedelta 2 | import pytz 3 | import suncalc 4 | import unittest 5 | 6 | def near(val1, val2): 7 | # print(f"near: {abs(val1 - val2)} < {1E-15}") 8 | return abs(val1 - val2) < 1E-15 9 | 10 | class SunCalcTestCases(unittest.TestCase): 11 | """Tests for `suncalc.py`.""" 12 | 13 | def setUp(self): 14 | """Setup for the test cases.""" 15 | 16 | self.date = datetime(2013, 3, 5) 17 | self.utc_dt = self.date.astimezone(pytz.utc) 18 | 19 | self.moon_date = datetime(2013, 3, 4) 20 | self.utc_moon_dt = self.moon_date.astimezone(pytz.utc) 21 | 22 | self.lat = 50.5 23 | self.lng = 30.5 24 | self.height = 2000 25 | 26 | self.sunTimes = { 27 | 'solarNoon': '2013-03-05 10:10:57', 28 | 'nadir': '2013-03-04 22:10:57', 29 | 'sunrise': '2013-03-05 04:34:56', 30 | 'sunset': '2013-03-05 15:46:57', 31 | 'sunriseEnd': '2013-03-05 04:38:19', 32 | 'sunsetStart': '2013-03-05 15:43:34', 33 | 'dawn': '2013-03-05 04:02:17', 34 | 'dusk': '2013-03-05 16:19:36', 35 | 'nauticalDawn': '2013-03-05 03:24:31', 36 | 'nauticalDusk': '2013-03-05 16:57:22', 37 | 'nightEnd': '2013-03-05 02:46:17', 38 | 'night': '2013-03-05 17:35:36', 39 | 'goldenHourEnd': '2013-03-05 05:19:01', 40 | 'goldenHour': '2013-03-05 15:02:52' 41 | } 42 | 43 | self.sunHeightTimes = { 44 | 'solarNoon': '2013-03-05 10:10:57', 45 | 'nadir': '2013-03-04 22:10:57', 46 | 'sunrise': '2013-03-05 04:25:07', 47 | 'sunset': '2013-03-05 15:56:46' 48 | } 49 | 50 | def test_getPositions(self): 51 | """getPosition returns azimuth and altitude for the given time and location.""" 52 | sunPos = suncalc.getPosition(self.utc_dt, self.lat, self.lng) 53 | self.assertTrue( near(sunPos["azimuth"], -2.5003175907168385) ) 54 | self.assertTrue( near(sunPos["altitude"], -0.7000406838781611) ) 55 | 56 | def test_getTimes(self): 57 | """getTimes returns sun phases for the given date and location.""" 58 | times = suncalc.getTimes(self.utc_dt, self.lat, self.lng) 59 | 60 | for time in self.sunTimes: 61 | self.assertEqual(self.sunTimes[time],times[time]) 62 | 63 | def test_getTimesWithHeight(self): 64 | """getTimes returns sun phases for the given date, location and height.""" 65 | times = suncalc.getTimes(self.utc_dt, self.lat, self.lng, self.height) 66 | 67 | for time in self.sunHeightTimes: 68 | self.assertEqual(self.sunHeightTimes[time],times[time]) 69 | 70 | def test_getMoonPosition(self): 71 | """Get moon position correctly.""" 72 | moonPos = suncalc.getMoonPosition(self.utc_dt, self.lat, self.lng) 73 | self.assertTrue(near(moonPos["azimuth"], -0.9783999522438226)) 74 | self.assertTrue(near(moonPos["altitude"], 0.006969727754891917)) 75 | self.assertTrue(near(moonPos["distance"], 364121.37256256194)) 76 | 77 | def test_getMoonIllumination(self): 78 | """Get moon illumination correctly.""" 79 | moonIllum = suncalc.getMoonIllumination(self.utc_dt) 80 | self.assertTrue(near(moonIllum["fraction"], 0.4848068202456373)) 81 | self.assertTrue(near(moonIllum["phase"], 0.7548368838538762)) 82 | self.assertTrue(near(moonIllum["angle"], 1.6732942678578346)) 83 | 84 | def test_getMoonTimes(self): 85 | """Get moon times correctly.""" 86 | moonTimes = suncalc.getMoonTimes(self.utc_moon_dt, self.lat, self.lng) 87 | # despite the code matching the JavaScript implementation, moon times don't come 88 | # out as expected from their test cases - https://github.com/mourner/suncalc 89 | # self.assertEqual(moonTimes["rise"].strftime('%Y-%m-%d %H:%M:%S'), '2013-03-04 23:54:29') 90 | self.assertEqual(moonTimes["rise"].strftime('%Y-%m-%d %H:%M:%S'), '2013-03-04 23:57:55') 91 | # self.assertEqual(moonTimes["set"].strftime('%Y-%m-%d %H:%M:%S'), '2013-03-04 07:47:58') 92 | self.assertEqual(moonTimes["set"].strftime('%Y-%m-%d %H:%M:%S'), '2013-03-04 07:28:41') 93 | 94 | if __name__ == '__main__': 95 | unittest.main() 96 | --------------------------------------------------------------------------------