Parcourir la source

Merge pull request #27 from Deuchnord/enhance-moon-phase

Compute more accurate Moon phase
tags/v0.3.0
Jérôme Deuchnord il y a 4 ans
committed by GitHub
Parent
révision
d7901bce7a
Aucune clé connue n'a été trouvée dans la base pour cette signature ID de la clé GPG: 4AEE18F83AFDEB23
6 fichiers modifiés avec 179 ajouts et 18 suppressions
  1. +44
    -1
      kosmorrolib/core.py
  2. +29
    -4
      kosmorrolib/data.py
  3. +11
    -3
      kosmorrolib/dumper.py
  4. +23
    -7
      kosmorrolib/ephemerides.py
  5. +8
    -3
      test/dumper.py
  6. +64
    -0
      test/ephemerides.py

+ 44
- 1
kosmorrolib/core.py Voir le fichier

@@ -18,9 +18,13 @@


from shutil import rmtree from shutil import rmtree
from pathlib import Path from pathlib import Path
from typing import Union

from skyfield.api import Loader from skyfield.api import Loader
from skyfield.timelib import Time
from skyfield.nutationlib import iau2000b


from .data import Star, Planet, Satellite
from .data import Star, Planet, Satellite, MOON_PHASES, MoonPhase


CACHE_FOLDER = str(Path.home()) + '/.kosmorro-cache' CACHE_FOLDER = str(Path.home()) + '/.kosmorro-cache'


@@ -50,5 +54,44 @@ def get_skf_objects():
return get_loader()('de421.bsp') return get_loader()('de421.bsp')




def get_iau2000b(time: Time):
return iau2000b(time.tt)


def clear_cache(): def clear_cache():
rmtree(CACHE_FOLDER) rmtree(CACHE_FOLDER)


def skyfield_to_moon_phase(times: [Time], vals: [int], now: Time) -> Union[MoonPhase, None]:
tomorrow = get_timescale().utc(now.utc_datetime().year, now.utc_datetime().month, now.utc_datetime().day + 1)

phases = list(MOON_PHASES.keys())
current_phase = None
current_phase_time = None
next_phase_time = None
i = 0

if len(times) == 0:
return None

for i, time in enumerate(times):
if now.utc_iso() <= time.utc_iso():
if vals[i] in [0, 2, 4, 6]:
if time.utc_datetime() < tomorrow.utc_datetime():
current_phase_time = time
current_phase = phases[vals[i]]
else:
i -= 1
current_phase_time = None
current_phase = phases[vals[i]]
else:
current_phase = phases[vals[i]]

break

for j in range(i + 1, len(times)):
if vals[j] in [0, 2, 4, 6]:
next_phase_time = times[j]
break

return MoonPhase(current_phase, current_phase_time, next_phase_time)

+ 29
- 4
kosmorrolib/data.py Voir le fichier

@@ -24,15 +24,40 @@ from skyfield.timelib import Time


MOON_PHASES = { MOON_PHASES = {
'NEW_MOON': 'New Moon', 'NEW_MOON': 'New Moon',
'WAXING_CRESCENT': 'Waxing crescent',
'FIRST_QUARTER': 'First Quarter', 'FIRST_QUARTER': 'First Quarter',
'WAXING_GIBBOUS': 'Waxing gibbous',
'FULL_MOON': 'Full Moon', 'FULL_MOON': 'Full Moon',
'LAST_QUARTER': 'Last Quarter'
'WANING_GIBBOUS': 'Waning gibbous',
'LAST_QUARTER': 'Last Quarter',
'WANING_CRESCENT': 'Waning crescent'
} }




def skyfield_to_moon_phase(val: int) -> str:
phases = list(MOON_PHASES.keys())
return phases[val]
class MoonPhase:
def __init__(self, identifier: str, time: Union[Time, None], next_phase_date: Union[Time, None]):
if identifier not in MOON_PHASES.keys():
raise ValueError('identifier parameter must be one of %s (got %s)' % (', '.join(MOON_PHASES.keys()),
identifier))

self.identifier = identifier
self.time = time
self.next_phase_date = next_phase_date

def get_phase(self):
return MOON_PHASES[self.identifier]

def get_next_phase(self):
if self.identifier == 'NEW_MOON':
next_identifier = 'FIRST_QUARTER'
elif self.identifier == 'FIRST_QUARTER':
next_identifier = 'FULL_MOON'
elif self.identifier == 'FULL_MOON':
next_identifier = 'LAST_QUARTER'
else:
next_identifier = 'NEW_MOON'

return MOON_PHASES[next_identifier]




class Position: class Position:


+ 11
- 3
kosmorrolib/dumper.py Voir le fichier

@@ -22,7 +22,7 @@ import json
from tabulate import tabulate from tabulate import tabulate
from skyfield.timelib import Time from skyfield.timelib import Time
from numpy import int64 from numpy import int64
from .data import Object, AsterEphemerides, MOON_PHASES
from .data import Object, AsterEphemerides, MoonPhase




class Dumper(ABC): class Dumper(ABC):
@@ -55,6 +55,11 @@ class JsonDumper(Dumper):
return obj return obj
if isinstance(obj, AsterEphemerides): if isinstance(obj, AsterEphemerides):
return obj.__dict__ return obj.__dict__
if isinstance(obj, MoonPhase):
moon_phase = obj.__dict__
moon_phase['phase'] = moon_phase.pop('identifier')
moon_phase['date'] = moon_phase.pop('time')
return moon_phase


raise TypeError('Object of type "%s" could not be integrated in the JSON' % str(type(obj))) raise TypeError('Object of type "%s" could not be integrated in the JSON' % str(type(obj)))


@@ -94,5 +99,8 @@ class TextDumper(Dumper):
stralign='center', colalign=('left',)) stralign='center', colalign=('left',))


@staticmethod @staticmethod
def get_moon(moon_phase: str) -> str:
return 'Moon phase: %s' % MOON_PHASES[moon_phase]
def get_moon(moon_phase: MoonPhase) -> str:
return 'Moon phase: %s\n' \
'%s on %s' % (moon_phase.get_phase(),
moon_phase.get_next_phase(),
moon_phase.next_phase_date.utc_strftime('%a %b %-d, %Y %H:%M'))

+ 23
- 7
kosmorrolib/ephemerides.py Voir le fichier

@@ -17,11 +17,13 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.


import datetime import datetime

from skyfield import almanac from skyfield import almanac
from skyfield.timelib import Time from skyfield.timelib import Time
from skyfield.constants import tau


from .data import Object, Position, AsterEphemerides, skyfield_to_moon_phase
from .core import get_skf_objects, get_timescale, ASTERS, MONTHS
from .data import Object, Position, AsterEphemerides, MoonPhase
from .core import get_skf_objects, get_timescale, get_iau2000b, ASTERS, MONTHS, skyfield_to_moon_phase


RISEN_ANGLE = -0.8333 RISEN_ANGLE = -0.8333


@@ -42,13 +44,27 @@ class EphemeridesComputer:
return {'rise': sunrise, 'set': sunset} return {'rise': sunrise, 'set': sunset}


@staticmethod @staticmethod
def get_moon(year, month, day) -> str:
def get_moon_phase(year, month, day) -> MoonPhase:
earth = get_skf_objects()['earth']
moon = get_skf_objects()['moon']
sun = get_skf_objects()['sun']

def moon_phase_at(time: Time):
time._nutation_angles = get_iau2000b(time)
current_earth = earth.at(time)
_, mlon, _ = current_earth.observe(moon).apparent().ecliptic_latlon('date')
_, slon, _ = current_earth.observe(sun).apparent().ecliptic_latlon('date')
return (((mlon.radians - slon.radians) // (tau / 8)) % 8).astype(int)

moon_phase_at.rough_period = 7.0 # one lunar phase per week

today = get_timescale().utc(year, month, day)
time1 = get_timescale().utc(year, month, day - 10) time1 = get_timescale().utc(year, month, day - 10)
time2 = get_timescale().utc(year, month, day)
time2 = get_timescale().utc(year, month, day + 10)


_, moon_phase = almanac.find_discrete(time1, time2, almanac.moon_phases(get_skf_objects()))
times, phase = almanac.find_discrete(time1, time2, moon_phase_at)


return skyfield_to_moon_phase(moon_phase[-1])
return skyfield_to_moon_phase(times, phase, today)


@staticmethod @staticmethod
def get_asters_ephemerides_for_aster(aster, date: datetime.date, position: Position) -> Object: def get_asters_ephemerides_for_aster(aster, date: datetime.date, position: Position) -> Object:
@@ -89,7 +105,7 @@ class EphemeridesComputer:
return (year % 4 == 0 and year % 100 > 0) or (year % 400 == 0) return (year % 4 == 0 and year % 100 > 0) or (year % 400 == 0)


def compute_ephemerides_for_day(self, year: int, month: int, day: int) -> dict: def compute_ephemerides_for_day(self, year: int, month: int, day: int) -> dict:
return {'moon_phase': self.get_moon(year, month, day),
return {'moon_phase': self.get_moon_phase(year, month, day),
'details': [self.get_asters_ephemerides_for_aster(aster, datetime.date(year, month, day), self.position) 'details': [self.get_asters_ephemerides_for_aster(aster, datetime.date(year, month, day), self.position)
for aster in ASTERS]} for aster in ASTERS]}




+ 8
- 3
test/dumper.py Voir le fichier

@@ -1,13 +1,18 @@
import unittest import unittest
from kosmorrolib.data import AsterEphemerides, Planet
from kosmorrolib.data import AsterEphemerides, Planet, MoonPhase
from kosmorrolib.dumper import JsonDumper from kosmorrolib.dumper import JsonDumper
from kosmorrolib.core import get_timescale




class DumperTestCase(unittest.TestCase): class DumperTestCase(unittest.TestCase):
def test_json_dumper_returns_correct_json(self): def test_json_dumper_returns_correct_json(self):
data = self._get_data() data = self._get_data()
self.assertEqual('{\n' self.assertEqual('{\n'
' "moon_phase": "FULL_MOON",\n'
' "moon_phase": {\n'
' "next_phase_date": "2019-11-20T00:00:00Z",\n'
' "phase": "FULL_MOON",\n'
' "date": "2019-11-11T00:00:00Z"\n'
' },\n'
' "details": [\n' ' "details": [\n'
' {\n' ' {\n'
' "name": "Mars",\n' ' "name": "Mars",\n'
@@ -23,7 +28,7 @@ class DumperTestCase(unittest.TestCase):
@staticmethod @staticmethod
def _get_data(): def _get_data():
return { return {
'moon_phase': 'FULL_MOON',
'moon_phase': MoonPhase('FULL_MOON', get_timescale().utc(2019, 11, 11), get_timescale().utc(2019, 11, 20)),
'details': [Planet('Mars', 'MARS', AsterEphemerides(None, None, None))] 'details': [Planet('Mars', 'MARS', AsterEphemerides(None, None, None))]
} }




+ 64
- 0
test/ephemerides.py Voir le fichier

@@ -17,6 +17,70 @@ class EphemeridesComputerTestCase(unittest.TestCase):
self.assertEqual('2019-11-18T11:45:02Z', star.ephemerides.culmination_time.utc_iso()) self.assertEqual('2019-11-18T11:45:02Z', star.ephemerides.culmination_time.utc_iso())
self.assertEqual('2019-11-18T17:48:39Z', star.ephemerides.set_time.utc_iso()) self.assertEqual('2019-11-18T17:48:39Z', star.ephemerides.set_time.utc_iso())


###################################################################################################################
### MOON PHASE TESTS ###
###################################################################################################################

def test_moon_phase_new_moon(self):
phase = EphemeridesComputer.get_moon_phase(2019, 11, 25)
self.assertEqual('WANING_CRESCENT', phase.identifier)
self.assertIsNone(phase.time)
self.assertRegexpMatches(phase.next_phase_date.utc_iso(), '^2019-11-26T')

phase = EphemeridesComputer.get_moon_phase(2019, 11, 26)
self.assertEqual('NEW_MOON', phase.identifier)
self.assertRegexpMatches(phase.next_phase_date.utc_iso(), '^2019-12-04T')

phase = EphemeridesComputer.get_moon_phase(2019, 11, 27)
self.assertEqual('WAXING_CRESCENT', phase.identifier)
self.assertIsNone(phase.time)
self.assertRegexpMatches(phase.next_phase_date.utc_iso(), '^2019-12-04T')

def test_moon_phase_first_crescent(self):
phase = EphemeridesComputer.get_moon_phase(2019, 11, 3)
self.assertEqual('WAXING_CRESCENT', phase.identifier)
self.assertIsNone(phase.time)
self.assertRegexpMatches(phase.next_phase_date.utc_iso(), '^2019-11-04T')

phase = EphemeridesComputer.get_moon_phase(2019, 11, 4)
self.assertEqual('FIRST_QUARTER', phase.identifier)
self.assertRegexpMatches(phase.next_phase_date.utc_iso(), '^2019-11-12T')

phase = EphemeridesComputer.get_moon_phase(2019, 11, 5)
self.assertEqual('WAXING_GIBBOUS', phase.identifier)
self.assertIsNone(phase.time)
self.assertRegexpMatches(phase.next_phase_date.utc_iso(), '^2019-11-12T')

def test_moon_phase_full_moon(self):
phase = EphemeridesComputer.get_moon_phase(2019, 11, 11)
self.assertEqual('WAXING_GIBBOUS', phase.identifier)
self.assertIsNone(phase.time)
self.assertRegexpMatches(phase.next_phase_date.utc_iso(), '^2019-11-12T')

phase = EphemeridesComputer.get_moon_phase(2019, 11, 12)
self.assertEqual('FULL_MOON', phase.identifier)
self.assertRegexpMatches(phase.next_phase_date.utc_iso(), '^2019-11-19T')

phase = EphemeridesComputer.get_moon_phase(2019, 11, 13)
self.assertEqual('WANING_GIBBOUS', phase.identifier)
self.assertIsNone(phase.time)
self.assertRegexpMatches(phase.next_phase_date.utc_iso(), '^2019-11-19T')

def test_moon_phase_last_quarter(self):
phase = EphemeridesComputer.get_moon_phase(2019, 11, 18)
self.assertEqual('WANING_GIBBOUS', phase.identifier)
self.assertIsNone(phase.time)
self.assertRegexpMatches(phase.next_phase_date.utc_iso(), '^2019-11-19T')

phase = EphemeridesComputer.get_moon_phase(2019, 11, 19)
self.assertEqual('LAST_QUARTER', phase.identifier)
self.assertRegexpMatches(phase.next_phase_date.utc_iso(), '^2019-11-26T')

phase = EphemeridesComputer.get_moon_phase(2019, 11, 20)
self.assertEqual('WANING_CRESCENT', phase.identifier)
self.assertIsNone(phase.time)
self.assertRegexpMatches(phase.next_phase_date.utc_iso(), '^2019-11-26T')



if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()

Chargement…
Annuler
Enregistrer