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.

model.py 7.7 KiB

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