A library that computes the ephemerides.
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.
 
 

275 lines
8.5 KiB

  1. #!/usr/bin/env python3
  2. # Kosmorrolib - The Library To Compute Your Ephemerides
  3. # Copyright (C) 2021 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 abc import ABC, abstractmethod
  18. from typing import Union
  19. from datetime import datetime
  20. from numpy import pi, arcsin
  21. from skyfield.api import Topos, Time
  22. from skyfield.vectorlib import VectorSum as SkfPlanet
  23. from .core import get_skf_objects
  24. from .enum import MoonPhaseType, EventType, ObjectIdentifier, ObjectType
  25. class Serializable(ABC):
  26. @abstractmethod
  27. def serialize(self) -> dict:
  28. pass
  29. class MoonPhase(Serializable):
  30. def __init__(
  31. self,
  32. phase_type: MoonPhaseType,
  33. time: datetime = None,
  34. next_phase_date: datetime = None,
  35. ):
  36. self.phase_type = phase_type
  37. self.time = time
  38. self.next_phase_date = next_phase_date
  39. def __repr__(self):
  40. return "<MoonPhase phase_type=%s time=%s next_phase_date=%s>" % (
  41. self.phase_type,
  42. self.time,
  43. self.next_phase_date,
  44. )
  45. def get_next_phase(self):
  46. if self.phase_type in [MoonPhaseType.NEW_MOON, MoonPhaseType.WAXING_CRESCENT]:
  47. return MoonPhaseType.FIRST_QUARTER
  48. if self.phase_type in [
  49. MoonPhaseType.FIRST_QUARTER,
  50. MoonPhaseType.WAXING_GIBBOUS,
  51. ]:
  52. return MoonPhaseType.FULL_MOON
  53. if self.phase_type in [MoonPhaseType.FULL_MOON, MoonPhaseType.WANING_GIBBOUS]:
  54. return MoonPhaseType.LAST_QUARTER
  55. return MoonPhaseType.NEW_MOON
  56. def serialize(self) -> dict:
  57. return {
  58. "phase": self.phase_type.name,
  59. "time": self.time.isoformat() if self.time is not None else None,
  60. "next": {
  61. "phase": self.get_next_phase().name,
  62. "time": self.next_phase_date.isoformat(),
  63. },
  64. }
  65. class Object(Serializable):
  66. """
  67. An astronomical object.
  68. """
  69. def __init__(
  70. self, identifier: ObjectIdentifier, skyfield_name: str, radius: float = None
  71. ):
  72. """
  73. Initialize an astronomical object
  74. :param ObjectIdentifier identifier: the official name of the object (may be internationalized)
  75. :param str skyfield_name: the internal name of the object in Skyfield library
  76. :param float radius: the radius (in km) of the object
  77. :param AsterEphemerides ephemerides: the ephemerides associated to the object
  78. """
  79. self.identifier = identifier
  80. self.skyfield_name = skyfield_name
  81. self.radius = radius
  82. def __repr__(self):
  83. return "<Object type=%s name=%s />" % (
  84. self.get_type().name,
  85. self.identifier.name,
  86. )
  87. def get_skyfield_object(self) -> SkfPlanet:
  88. return get_skf_objects()[self.skyfield_name]
  89. @abstractmethod
  90. def get_type(self) -> ObjectType:
  91. pass
  92. def get_apparent_radius(self, time: Time, from_place) -> float:
  93. """
  94. Calculate the apparent radius, in degrees, of the object from the given place at a given time.
  95. :param time:
  96. :param from_place:
  97. :return:
  98. """
  99. if self.radius is None:
  100. raise ValueError("Missing radius for %s" % self.identifier.name)
  101. return (
  102. 360
  103. / pi
  104. * arcsin(
  105. self.radius
  106. / from_place.at(time).observe(self.get_skyfield_object()).distance().km
  107. )
  108. )
  109. def serialize(self) -> dict:
  110. """Serialize the given object
  111. >>> planet = Planet(ObjectIdentifier.MARS, "MARS")
  112. >>> planet.serialize()
  113. {'identifier': 'MARS', 'type': 'PLANET', 'radius': None}
  114. """
  115. return {
  116. "identifier": self.identifier.name,
  117. "type": self.get_type().name,
  118. "radius": self.radius,
  119. }
  120. class Star(Object):
  121. def get_type(self) -> ObjectType:
  122. return ObjectType.STAR
  123. class Planet(Object):
  124. def get_type(self) -> ObjectType:
  125. return ObjectType.PLANET
  126. class DwarfPlanet(Planet):
  127. def get_type(self) -> ObjectType:
  128. return ObjectType.DWARF_PLANET
  129. class Satellite(Object):
  130. def get_type(self) -> ObjectType:
  131. return ObjectType.SATELLITE
  132. class Event(Serializable):
  133. def __init__(
  134. self,
  135. event_type: EventType,
  136. objects: [Object],
  137. start_time: datetime,
  138. end_time: Union[datetime, None] = None,
  139. details: {str: any} = None,
  140. ):
  141. self.event_type = event_type
  142. self.objects = objects
  143. self.start_time = start_time
  144. self.end_time = end_time
  145. self.details = details
  146. def __repr__(self):
  147. return "<Event type=%s objects=%s start=%s end=%s details=%s />" % (
  148. self.event_type.name,
  149. self.objects,
  150. self.start_time,
  151. self.end_time,
  152. self.details,
  153. )
  154. def get_description(self, show_details: bool = True) -> str:
  155. description = self.event_type.value % self._get_objects_name()
  156. if show_details and self.details is not None:
  157. description += " ({:s})".format(self.details)
  158. return description
  159. def _get_objects_name(self):
  160. if len(self.objects) == 1:
  161. return self.objects[0].name
  162. return tuple(object.name for object in self.objects)
  163. def serialize(self) -> dict:
  164. return {
  165. "objects": [object.serialize() for object in self.objects],
  166. "EventType": self.event_type.name,
  167. "starts_at": self.start_time.isoformat(),
  168. "ends_at": self.end_time.isoformat() if self.end_time is not None else None,
  169. "details": self.details,
  170. }
  171. class AsterEphemerides(Serializable):
  172. def __init__(
  173. self,
  174. rise_time: Union[datetime, None],
  175. culmination_time: Union[datetime, None],
  176. set_time: Union[datetime, None],
  177. aster: Object,
  178. ):
  179. self.rise_time = rise_time
  180. self.culmination_time = culmination_time
  181. self.set_time = set_time
  182. self.object = aster
  183. def __repr__(self):
  184. return (
  185. "<AsterEphemerides rise_time=%s culmination_time=%s set_time=%s aster=%s>"
  186. % (self.rise_time, self.culmination_time, self.set_time, self.object)
  187. )
  188. def serialize(self) -> dict:
  189. return {
  190. "object": self.object.serialize(),
  191. "rise_time": self.rise_time.isoformat()
  192. if self.rise_time is not None
  193. else None,
  194. "culmination_time": self.culmination_time.isoformat()
  195. if self.culmination_time is not None
  196. else None,
  197. "set_time": self.set_time.isoformat()
  198. if self.set_time is not None
  199. else None,
  200. }
  201. EARTH = Planet(ObjectIdentifier.EARTH, "EARTH")
  202. ASTERS = [
  203. Star(ObjectIdentifier.SUN, "SUN", radius=696342),
  204. Satellite(ObjectIdentifier.MOON, "MOON", radius=1737.4),
  205. Planet(ObjectIdentifier.MERCURY, "MERCURY", radius=2439.7),
  206. Planet(ObjectIdentifier.VENUS, "VENUS", radius=6051.8),
  207. Planet(ObjectIdentifier.MARS, "MARS", radius=3396.2),
  208. Planet(ObjectIdentifier.JUPITER, "JUPITER BARYCENTER", radius=71492),
  209. Planet(ObjectIdentifier.SATURN, "SATURN BARYCENTER", radius=60268),
  210. Planet(ObjectIdentifier.URANUS, "URANUS BARYCENTER", radius=25559),
  211. Planet(ObjectIdentifier.NEPTUNE, "NEPTUNE BARYCENTER", radius=24764),
  212. Planet(ObjectIdentifier.PLUTO, "PLUTO BARYCENTER", radius=1185),
  213. ]
  214. class Position:
  215. def __init__(self, latitude: float, longitude: float):
  216. self.latitude = latitude
  217. self.longitude = longitude
  218. self._topos = None
  219. def get_planet_topos(self) -> Topos:
  220. if self._topos is None:
  221. self._topos = EARTH.get_skyfield_object() + Topos(
  222. latitude_degrees=self.latitude, longitude_degrees=self.longitude
  223. )
  224. return self._topos