You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

227 lines
8.8 KiB

  1. #!/usr/bin/env python3
  2. # Kosmorro - Compute The Next Ephemeris
  3. # Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr>
  4. #
  5. # This program is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU Affero General Public License as
  7. # published by the Free Software Foundation, either version 3 of the
  8. # License, or (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU Affero General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU Affero General Public License
  16. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. from multiprocessing import Pool as ThreadPool
  18. from skyfield.api import Loader, Topos
  19. from skyfield import almanac
  20. class Ephemeris:
  21. MONTH = ['JAN', 'FEB', 'MAR', 'APR', 'MAY', 'JUN', 'JUL', 'AUG', 'SEP', 'OCT', 'NOV', 'DEC']
  22. PLANETS = ['MERCURY', 'VENUS', 'MARS', 'JUPITER BARYCENTER', 'SATURN BARYCENTER', 'URANUS BARYCENTER',
  23. 'NEPTUNE BARYCENTER', 'PLUTO BARYCENTER']
  24. position = None
  25. latitude = None
  26. longitude = None
  27. timescale = None
  28. planets = None
  29. def __init__(self, position):
  30. load = Loader('./cache')
  31. self.timescale = load.timescale()
  32. self.planets = load('de421.bsp')
  33. self.latitude = position['lat']
  34. self.longitude = position['lon']
  35. self.position = Topos(latitude_degrees=position['lat'], longitude_degrees=position['lon'])
  36. def get_sun(self, start_time, end_time) -> dict:
  37. times, is_risen = almanac.find_discrete(start_time,
  38. end_time,
  39. almanac.sunrise_sunset(self.planets, self.position))
  40. sunrise = times[0] if is_risen[0] else times[1]
  41. sunset = times[1] if not is_risen[1] else times[0]
  42. return {'rise': sunrise, 'set': sunset}
  43. def get_moon(self, year, month, day) -> dict:
  44. time1 = self.timescale.utc(year, month, day - 10)
  45. time2 = self.timescale.utc(year, month, day)
  46. _, moon_phase = almanac.find_discrete(time1, time2, almanac.moon_phases(self.planets))
  47. return {'phase': moon_phase[-1]}
  48. def get_planets(self, year: int, month: int, day: int) -> dict:
  49. args = []
  50. for planet_name in self.PLANETS:
  51. args.append({'planet': planet_name,
  52. 'observer': {'latitude': self.latitude, 'longitude': self.longitude},
  53. 'year': year, 'month': month, 'day': day})
  54. with ThreadPool() as pool:
  55. planets = pool.map(Ephemeris.get_planet, args)
  56. obj = {}
  57. for planet in planets:
  58. obj[planet['name'].split(' ')[0]] = {'rise': planet['rise'], 'maximum': planet['maximum'],
  59. 'set': planet['set']}
  60. return obj
  61. @staticmethod
  62. def get_planet(p_obj) -> dict:
  63. load = Loader('./cache')
  64. planets = load('de421.bsp')
  65. timescale = load.timescale()
  66. position = Topos(latitude_degrees=p_obj['observer']['latitude'],
  67. longitude_degrees=p_obj['observer']['longitude'])
  68. observer = planets['earth'] + position
  69. planet = planets[p_obj['planet']]
  70. rise_time = maximum_time = set_time = None
  71. is_risen = None
  72. is_elevating = None
  73. last_is_elevating = None
  74. last_position = None
  75. for hours in range(0, 24):
  76. time = timescale.utc(p_obj['year'], p_obj['month'], p_obj['day'], hours)
  77. position = observer.at(time).observe(planet).apparent().altaz()[0].degrees
  78. if is_risen is None:
  79. is_risen = position > 0
  80. if last_position is not None:
  81. is_elevating = last_position < position
  82. if rise_time is None and not is_risen and is_elevating and position > 0:
  83. # Planet has risen in the last hour, let's look for a more precise time!
  84. for minutes in range(0, 60):
  85. time = timescale.utc(p_obj['year'], p_obj['month'], p_obj['day'], hours - 1, minutes)
  86. position = observer.at(time).observe(planet).apparent().altaz()[0].degrees
  87. if position > 0:
  88. # Planet has just risen!
  89. rise_time = time
  90. is_risen = True
  91. break
  92. if set_time is None and is_risen and not is_elevating and position < 0:
  93. # Planet has set in the last hour, let's look for a more precise time!
  94. for minutes in range(0, 60):
  95. time = timescale.utc(p_obj['year'], p_obj['month'], p_obj['day'], hours - 1, minutes)
  96. position = observer.at(time).observe(planet).apparent().altaz()[0].degrees
  97. if position < 0:
  98. # Planet has just set!
  99. set_time = time
  100. is_risen = False
  101. break
  102. if not is_elevating and last_is_elevating:
  103. # Planet has reached its azimuth in the last hour, let's look for a more precise time!
  104. for minutes in range(0, 60):
  105. time = timescale.utc(p_obj['year'], p_obj['month'], p_obj['day'], hours - 1, minutes)
  106. position = observer.at(time).observe(planet).apparent().altaz()[0].degrees
  107. maximum_time = time
  108. if last_position > position:
  109. # Planet has reached its azimuth!
  110. is_elevating = False
  111. break
  112. last_position = position
  113. last_position = position
  114. last_is_elevating = is_elevating
  115. if rise_time is not None and set_time is not None and maximum_time is not None:
  116. return {
  117. 'name': p_obj['planet'],
  118. 'rise': rise_time,
  119. 'maximum': maximum_time,
  120. 'set': set_time
  121. }
  122. return {
  123. 'name': p_obj['planet'],
  124. 'rise': rise_time if rise_time is not None else None,
  125. 'maximum': maximum_time if maximum_time is not None else None,
  126. 'set': set_time if set_time is not None else None
  127. }
  128. def compute_ephemerides_for_day(self, year: int, month: int, day: int) -> dict:
  129. ephemeris = {}
  130. time1 = self.timescale.utc(year, month, day, 2)
  131. time2 = self.timescale.utc(year, month, day + 1, 2)
  132. # Compute sunrise and sunset
  133. ephemeris['sun'] = self.get_sun(time1, time2)
  134. ephemeris['moon'] = self.get_moon(year, month, day)
  135. ephemeris['planets'] = self.get_planets(year, month, day)
  136. return ephemeris
  137. def compute_ephemerides_for_month(self, year: int, month: int) -> list:
  138. if month == 2:
  139. is_leap_year = (year % 4 == 0 and year % 100 > 0) or (year % 400 == 0)
  140. max_day = 29 if is_leap_year else 28
  141. elif month < 8:
  142. max_day = 30 if month % 2 == 0 else 31
  143. else:
  144. max_day = 31 if month % 2 == 0 else 30
  145. ephemerides = []
  146. for day in range(1, max_day + 1):
  147. ephemerides.append(self.compute_ephemerides_for_day(year, month, day))
  148. return ephemerides
  149. def compute_ephemerides_for_year(self, year: int) -> dict:
  150. ephemerides = {}
  151. for month in range(0, 12):
  152. ephemerides[self.MONTH[month]] = self.compute_ephemerides_for_month(year, month + 1)
  153. ephemerides['seasons'] = self.get_seasons(year)
  154. return ephemerides
  155. def get_seasons(self, year: int) -> dict:
  156. start_time = self.timescale.utc(year, 1, 1)
  157. end_time = self.timescale.utc(year, 12, 31)
  158. times, almanac_seasons = almanac.find_discrete(start_time, end_time, almanac.seasons(self.planets))
  159. seasons = {}
  160. for time, almanac_season in zip(times, almanac_seasons):
  161. if almanac_season == 0:
  162. season = 'MARCH'
  163. elif almanac_season == 1:
  164. season = 'JUNE'
  165. elif almanac_season == 2:
  166. season = 'SEPTEMBER'
  167. elif almanac_season == 3:
  168. season = 'DECEMBER'
  169. else:
  170. raise AssertionError
  171. seasons[season] = time.utc_iso()
  172. return seasons
  173. def compute_ephemeris(self, year: int, month: int, day: int):
  174. if day is not None:
  175. return self.compute_ephemerides_for_day(year, month, day)
  176. if month is not None:
  177. return self.compute_ephemerides_for_month(year, month)
  178. return self.compute_ephemerides_for_year(year)