@@ -7,56 +7,6 @@ on: | |||||
branches: [main, features] | branches: [main, features] | ||||
jobs: | jobs: | ||||
legacy-tests: | |||||
runs-on: ${{ matrix.os }} | |||||
strategy: | |||||
fail-fast: false | |||||
matrix: | |||||
os: | |||||
- ubuntu-18.04 | |||||
- ubuntu-20.04 | |||||
- macos-10.15 | |||||
- macos-11.0 | |||||
- windows-2019 | |||||
python_version: | |||||
- '3.7' | |||||
- '3.8' | |||||
- '3.9' | |||||
name: Legacy tests (Python ${{ matrix.python_version }} on ${{ matrix.os }}) | |||||
steps: | |||||
- uses: actions/checkout@v1 | |||||
- name: Set up Python | |||||
uses: actions/setup-python@v1 | |||||
with: | |||||
python-version: ${{ matrix.python_version }} | |||||
- name: Prepare environment (non-Windows systems) | |||||
if: ${{ matrix.os != 'windows-2019' }} | |||||
run: | | |||||
pip install --upgrade pip pipenv | |||||
- name: Prepare environment (Windows) | |||||
if: ${{ matrix.os == 'windows-2019' }} | |||||
run: | | |||||
python -mpip install --upgrade pip pipenv | |||||
- name: Install dependencies (all systems) | |||||
run: | | |||||
pipenv lock --pre | |||||
pipenv sync --dev | |||||
pipenv install --dev | |||||
- name: Install dependencies (Windows) | |||||
if: ${{ matrix.os == 'windows-2019' }} | |||||
run: | | |||||
pipenv lock --dev -r > requirements.txt | |||||
python -mpip install -r requirements.txt | |||||
- name: Run legacy tests | |||||
run: | | |||||
make legacy-tests | |||||
doc-tests: | doc-tests: | ||||
runs-on: ${{ matrix.os }} | runs-on: ${{ matrix.os }} | ||||
strategy: | strategy: | ||||
@@ -1,21 +1,18 @@ | |||||
black: | black: | ||||
pipenv run black kosmorrolib tests setup.py | |||||
pipenv run black kosmorrolib setup.py | |||||
.PHONY: tests | .PHONY: tests | ||||
tests: legacy-tests doctests | |||||
tests: doctests | |||||
coverage-doctests: | coverage-doctests: | ||||
pipenv run python3 -m coverage run tests.py | pipenv run python3 -m coverage run tests.py | ||||
coverage-report: | |||||
pipenv run python3 -m coverage report | |||||
doctests: | doctests: | ||||
pipenv run python3 tests.py | pipenv run python3 tests.py | ||||
legacy-tests: | |||||
unset KOSMORRO_LATITUDE; \ | |||||
unset KOSMORRO_LONGITUDE; \ | |||||
unset KOSMORRO_TIMEZONE; \ | |||||
pipenv run python3 -m unittest tests | |||||
.PHONY: build | .PHONY: build | ||||
build: | build: | ||||
python3 setup.py sdist bdist_wheel | python3 setup.py sdist bdist_wheel | ||||
@@ -19,11 +19,21 @@ | |||||
from datetime import datetime, timezone, timedelta | from datetime import datetime, timezone, timedelta | ||||
def translate_to_timezone(date: datetime, to_tz: int, from_tz: int = None): | |||||
if from_tz is not None: | |||||
source_tz = timezone(timedelta(hours=from_tz)) | |||||
else: | |||||
source_tz = timezone.utc | |||||
def translate_to_timezone(date: datetime, to_tz: int): | |||||
"""Convert a datetime from a timezone to another. | |||||
>>> translate_to_timezone(datetime(2021, 6, 9, 5, 0, 0, tzinfo=timezone.utc), 2) | |||||
datetime.datetime(2021, 6, 9, 7, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))) | |||||
>>> translate_to_timezone(datetime(2021, 6, 9, 5, 0, 0, tzinfo=timezone(timedelta(hours=1))), 2) | |||||
datetime.datetime(2021, 6, 9, 6, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))) | |||||
If the datetime has no timezone information, then it is interpreted as UTC: | |||||
>>> translate_to_timezone(datetime(2021, 6, 9, 5, 0, 0), 2) | |||||
datetime.datetime(2021, 6, 9, 7, 0, tzinfo=datetime.timezone(datetime.timedelta(seconds=7200))) | |||||
""" | |||||
source_tz = date.tzinfo if date.tzinfo is not None else timezone.utc | |||||
return date.replace(tzinfo=source_tz).astimezone( | return date.replace(tzinfo=source_tz).astimezone( | ||||
tz=timezone(timedelta(hours=to_tz)) | tz=timezone(timedelta(hours=to_tz)) | ||||
@@ -22,14 +22,14 @@ from enum import Enum | |||||
class MoonPhaseType(Enum): | class MoonPhaseType(Enum): | ||||
"""An enumeration of moon phases.""" | """An enumeration of moon phases.""" | ||||
NEW_MOON = 1 | |||||
WAXING_CRESCENT = 2 | |||||
FIRST_QUARTER = 3 | |||||
WAXING_GIBBOUS = 4 | |||||
FULL_MOON = 5 | |||||
WANING_GIBBOUS = 6 | |||||
LAST_QUARTER = 7 | |||||
WANING_CRESCENT = 8 | |||||
NEW_MOON = 0 | |||||
WAXING_CRESCENT = 1 | |||||
FIRST_QUARTER = 2 | |||||
WAXING_GIBBOUS = 3 | |||||
FULL_MOON = 4 | |||||
WANING_GIBBOUS = 5 | |||||
LAST_QUARTER = 6 | |||||
WANING_CRESCENT = 7 | |||||
class SeasonType(Enum): | class SeasonType(Enum): | ||||
@@ -16,7 +16,7 @@ | |||||
# You should have received a copy of the GNU Affero General Public License | # You should have received a copy of the GNU Affero General Public License | ||||
# 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 | |||||
from datetime import date, datetime, timedelta | |||||
from typing import Union | from typing import Union | ||||
from skyfield.searchlib import find_discrete, find_maxima | from skyfield.searchlib import find_discrete, find_maxima | ||||
@@ -40,60 +40,63 @@ def _get_skyfield_to_moon_phase( | |||||
now.utc_datetime().year, now.utc_datetime().month, now.utc_datetime().day + 1 | now.utc_datetime().year, now.utc_datetime().month, now.utc_datetime().day + 1 | ||||
) | ) | ||||
phases = list(MoonPhaseType) | |||||
current_phase = None | |||||
current_phase_time = None | |||||
next_phase_time = None | next_phase_time = None | ||||
i = 0 | i = 0 | ||||
if len(times) == 0: | |||||
return None | |||||
# Find the current moon phase: | |||||
for i, time in enumerate(times): | 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]] | |||||
if now.utc_datetime() <= time.utc_datetime(): | |||||
if time.utc_datetime() >= tomorrow.utc_datetime(): | |||||
i -= 1 | |||||
break | break | ||||
current_phase = MoonPhaseType(vals[i]) | |||||
if current_phase in [ | |||||
MoonPhaseType.NEW_MOON, | |||||
MoonPhaseType.FIRST_QUARTER, | |||||
MoonPhaseType.FULL_MOON, | |||||
MoonPhaseType.LAST_QUARTER, | |||||
]: | |||||
current_phase_time = translate_to_timezone(times[i].utc_datetime(), timezone) | |||||
else: | |||||
current_phase_time = None | |||||
# Find the next moon phase | |||||
for j in range(i + 1, len(times)): | for j in range(i + 1, len(times)): | ||||
if vals[j] in [0, 2, 4, 6]: | if vals[j] in [0, 2, 4, 6]: | ||||
next_phase_time = times[j] | |||||
next_phase_time = translate_to_timezone(times[j].utc_datetime(), timezone) | |||||
break | break | ||||
return MoonPhase( | |||||
current_phase, | |||||
translate_to_timezone(current_phase_time.utc_datetime(), timezone) | |||||
if current_phase_time is not None | |||||
else None, | |||||
translate_to_timezone(next_phase_time.utc_datetime(), timezone) | |||||
if next_phase_time is not None | |||||
else None, | |||||
) | |||||
return MoonPhase(current_phase, current_phase_time, next_phase_time) | |||||
def get_moon_phase( | |||||
for_date: datetime.date = datetime.date.today(), timezone: int = 0 | |||||
) -> MoonPhase: | |||||
def get_moon_phase(for_date: date = date.today(), timezone: int = 0) -> MoonPhase: | |||||
"""Calculate and return the moon phase for the given date, adjusted to the given timezone if any. | """Calculate and return the moon phase for the given date, adjusted to the given timezone if any. | ||||
Get the moon phase for the 27 March, 2021: | Get the moon phase for the 27 March, 2021: | ||||
>>> get_moon_phase(datetime.date.fromisoformat("2021-03-27")) | |||||
>>> get_moon_phase(date(2021, 3, 27)) | |||||
<MoonPhase phase_type=MoonPhaseType.WAXING_GIBBOUS time=None next_phase_date=2021-03-28 18:48:10.902298+00:00> | <MoonPhase phase_type=MoonPhaseType.WAXING_GIBBOUS time=None next_phase_date=2021-03-28 18:48:10.902298+00:00> | ||||
When the moon phase is a new moon, a first quarter, a full moon or a last quarter, you get the exact time | |||||
of its happening too: | |||||
>>> get_moon_phase(datetime(2021, 3, 28)) | |||||
<MoonPhase phase_type=MoonPhaseType.FULL_MOON time=2021-03-28 18:48:10.902298+00:00 next_phase_date=2021-04-04 10:02:27.393689+00:00> | |||||
Get the moon phase for the 27 March, 2021, in the UTC+2 timezone: | Get the moon phase for the 27 March, 2021, in the UTC+2 timezone: | ||||
>>> get_moon_phase(datetime.date.fromisoformat("2021-03-27"), timezone=2) | |||||
>>> get_moon_phase(date(2021, 3, 27), timezone=2) | |||||
<MoonPhase phase_type=MoonPhaseType.WAXING_GIBBOUS time=None next_phase_date=2021-03-28 20:48:10.902298+02:00> | <MoonPhase phase_type=MoonPhaseType.WAXING_GIBBOUS time=None next_phase_date=2021-03-28 20:48:10.902298+02:00> | ||||
Note that the moon phase can only be computed for a date range. | |||||
Asking for the moon phase with an out of range date will result in an exception: | |||||
>>> get_moon_phase(date(1000, 1, 1)) | |||||
Traceback (most recent call last): | |||||
... | |||||
kosmorrolib.exceptions.OutOfRangeDateError: The date must be between 1899-08-09 and 2053-09-26 | |||||
""" | """ | ||||
earth = get_skf_objects()["earth"] | earth = get_skf_objects()["earth"] | ||||
moon = get_skf_objects()["moon"] | moon = get_skf_objects()["moon"] | ||||
@@ -109,56 +112,69 @@ def get_moon_phase( | |||||
moon_phase_at.rough_period = 7.0 # one lunar phase per week | moon_phase_at.rough_period = 7.0 # one lunar phase per week | ||||
today = get_timescale().utc(for_date.year, for_date.month, for_date.day) | today = get_timescale().utc(for_date.year, for_date.month, for_date.day) | ||||
time1 = get_timescale().utc(for_date.year, for_date.month, for_date.day - 10) | |||||
time2 = get_timescale().utc(for_date.year, for_date.month, for_date.day + 10) | |||||
start_time = get_timescale().utc(for_date.year, for_date.month, for_date.day - 10) | |||||
end_time = get_timescale().utc(for_date.year, for_date.month, for_date.day + 10) | |||||
try: | try: | ||||
times, phase = find_discrete(time1, time2, moon_phase_at) | |||||
times, phases = find_discrete(start_time, end_time, moon_phase_at) | |||||
return _get_skyfield_to_moon_phase(times, phases, today, timezone) | |||||
except EphemerisRangeError as error: | except EphemerisRangeError as error: | ||||
start = translate_to_timezone(error.start_time.utc_datetime(), timezone) | start = translate_to_timezone(error.start_time.utc_datetime(), timezone) | ||||
end = translate_to_timezone(error.end_time.utc_datetime(), timezone) | end = translate_to_timezone(error.end_time.utc_datetime(), timezone) | ||||
start = datetime.date(start.year, start.month, start.day) + datetime.timedelta( | |||||
days=12 | |||||
) | |||||
end = datetime.date(end.year, end.month, end.day) - datetime.timedelta(days=12) | |||||
start = date(start.year, start.month, start.day) + timedelta(days=12) | |||||
end = date(end.year, end.month, end.day) - timedelta(days=12) | |||||
raise OutOfRangeDateError(start, end) from error | raise OutOfRangeDateError(start, end) from error | ||||
return _get_skyfield_to_moon_phase(times, phase, today, timezone) | |||||
def get_ephemerides( | def get_ephemerides( | ||||
position: Position, date: datetime.date = datetime.date.today(), timezone: int = 0 | |||||
position: Position, for_date: date = date.today(), timezone: int = 0 | |||||
) -> [AsterEphemerides]: | ) -> [AsterEphemerides]: | ||||
"""Compute and return the ephemerides for the given position and date, adjusted to the given timezone if any. | """Compute and return the ephemerides for the given position and date, adjusted to the given timezone if any. | ||||
Compute the ephemerides for June 9th, 2021: | Compute the ephemerides for June 9th, 2021: | ||||
>>> pos = Position(50.5824, 3.0624) | >>> pos = Position(50.5824, 3.0624) | ||||
>>> get_ephemerides(pos, datetime.date(2021, 6, 9)) | |||||
>>> get_ephemerides(pos, date(2021, 6, 9)) | |||||
[<AsterEphemerides rise_time=2021-06-09 03:36:00 culmination_time=2021-06-09 11:47:00 set_time=2021-06-09 19:58:00 aster=<Object type=STAR name=SUN />>, <AsterEphemerides rise_time=2021-06-09 02:59:00 culmination_time=2021-06-09 11:02:00 set_time=2021-06-09 19:16:00 aster=<Object type=SATELLITE name=MOON />>, <AsterEphemerides rise_time=2021-06-09 04:06:00 culmination_time=2021-06-09 11:58:00 set_time=2021-06-09 19:49:00 aster=<Object type=PLANET name=MERCURY />>, <AsterEphemerides rise_time=2021-06-09 04:52:00 culmination_time=2021-06-09 13:13:00 set_time=2021-06-09 21:34:00 aster=<Object type=PLANET name=VENUS />>, <AsterEphemerides rise_time=2021-06-09 06:38:00 culmination_time=2021-06-09 14:40:00 set_time=2021-06-09 22:41:00 aster=<Object type=PLANET name=MARS />>, <AsterEphemerides rise_time=2021-06-09 23:43:00 culmination_time=2021-06-09 04:54:00 set_time=2021-06-09 10:01:00 aster=<Object type=PLANET name=JUPITER />>, <AsterEphemerides rise_time=2021-06-09 23:02:00 culmination_time=2021-06-09 03:41:00 set_time=2021-06-09 08:16:00 aster=<Object type=PLANET name=SATURN />>, <AsterEphemerides rise_time=2021-06-09 01:56:00 culmination_time=2021-06-09 09:18:00 set_time=2021-06-09 16:40:00 aster=<Object type=PLANET name=URANUS />>, <AsterEphemerides rise_time=2021-06-09 00:27:00 culmination_time=2021-06-09 06:13:00 set_time=2021-06-09 11:59:00 aster=<Object type=PLANET name=NEPTUNE />>, <AsterEphemerides rise_time=2021-06-09 22:22:00 culmination_time=2021-06-09 02:32:00 set_time=2021-06-09 06:38:00 aster=<Object type=PLANET name=PLUTO />>] | [<AsterEphemerides rise_time=2021-06-09 03:36:00 culmination_time=2021-06-09 11:47:00 set_time=2021-06-09 19:58:00 aster=<Object type=STAR name=SUN />>, <AsterEphemerides rise_time=2021-06-09 02:59:00 culmination_time=2021-06-09 11:02:00 set_time=2021-06-09 19:16:00 aster=<Object type=SATELLITE name=MOON />>, <AsterEphemerides rise_time=2021-06-09 04:06:00 culmination_time=2021-06-09 11:58:00 set_time=2021-06-09 19:49:00 aster=<Object type=PLANET name=MERCURY />>, <AsterEphemerides rise_time=2021-06-09 04:52:00 culmination_time=2021-06-09 13:13:00 set_time=2021-06-09 21:34:00 aster=<Object type=PLANET name=VENUS />>, <AsterEphemerides rise_time=2021-06-09 06:38:00 culmination_time=2021-06-09 14:40:00 set_time=2021-06-09 22:41:00 aster=<Object type=PLANET name=MARS />>, <AsterEphemerides rise_time=2021-06-09 23:43:00 culmination_time=2021-06-09 04:54:00 set_time=2021-06-09 10:01:00 aster=<Object type=PLANET name=JUPITER />>, <AsterEphemerides rise_time=2021-06-09 23:02:00 culmination_time=2021-06-09 03:41:00 set_time=2021-06-09 08:16:00 aster=<Object type=PLANET name=SATURN />>, <AsterEphemerides rise_time=2021-06-09 01:56:00 culmination_time=2021-06-09 09:18:00 set_time=2021-06-09 16:40:00 aster=<Object type=PLANET name=URANUS />>, <AsterEphemerides rise_time=2021-06-09 00:27:00 culmination_time=2021-06-09 06:13:00 set_time=2021-06-09 11:59:00 aster=<Object type=PLANET name=NEPTUNE />>, <AsterEphemerides rise_time=2021-06-09 22:22:00 culmination_time=2021-06-09 02:32:00 set_time=2021-06-09 06:38:00 aster=<Object type=PLANET name=PLUTO />>] | ||||
Compute the ephemerides for June 9th, 2021: | Compute the ephemerides for June 9th, 2021: | ||||
>>> get_ephemerides(pos, datetime.date(2021, 6, 9), timezone=2) | |||||
>>> get_ephemerides(pos, date(2021, 6, 9), timezone=2) | |||||
[<AsterEphemerides rise_time=2021-06-09 05:36:00 culmination_time=2021-06-09 13:47:00 set_time=2021-06-09 21:58:00 aster=<Object type=STAR name=SUN />>, <AsterEphemerides rise_time=2021-06-09 04:59:00 culmination_time=2021-06-09 13:02:00 set_time=2021-06-09 21:16:00 aster=<Object type=SATELLITE name=MOON />>, <AsterEphemerides rise_time=2021-06-09 06:06:00 culmination_time=2021-06-09 13:58:00 set_time=2021-06-09 21:49:00 aster=<Object type=PLANET name=MERCURY />>, <AsterEphemerides rise_time=2021-06-09 06:52:00 culmination_time=2021-06-09 15:13:00 set_time=2021-06-09 23:34:00 aster=<Object type=PLANET name=VENUS />>, <AsterEphemerides rise_time=2021-06-09 08:38:00 culmination_time=2021-06-09 16:40:00 set_time=2021-06-09 00:44:00 aster=<Object type=PLANET name=MARS />>, <AsterEphemerides rise_time=2021-06-09 01:47:00 culmination_time=2021-06-09 06:54:00 set_time=2021-06-09 12:01:00 aster=<Object type=PLANET name=JUPITER />>, <AsterEphemerides rise_time=2021-06-09 01:06:00 culmination_time=2021-06-09 05:41:00 set_time=2021-06-09 10:16:00 aster=<Object type=PLANET name=SATURN />>, <AsterEphemerides rise_time=2021-06-09 03:56:00 culmination_time=2021-06-09 11:18:00 set_time=2021-06-09 18:40:00 aster=<Object type=PLANET name=URANUS />>, <AsterEphemerides rise_time=2021-06-09 02:27:00 culmination_time=2021-06-09 08:13:00 set_time=2021-06-09 13:59:00 aster=<Object type=PLANET name=NEPTUNE />>, <AsterEphemerides rise_time=2021-06-09 00:26:00 culmination_time=2021-06-09 04:32:00 set_time=2021-06-09 08:38:00 aster=<Object type=PLANET name=PLUTO />>] | [<AsterEphemerides rise_time=2021-06-09 05:36:00 culmination_time=2021-06-09 13:47:00 set_time=2021-06-09 21:58:00 aster=<Object type=STAR name=SUN />>, <AsterEphemerides rise_time=2021-06-09 04:59:00 culmination_time=2021-06-09 13:02:00 set_time=2021-06-09 21:16:00 aster=<Object type=SATELLITE name=MOON />>, <AsterEphemerides rise_time=2021-06-09 06:06:00 culmination_time=2021-06-09 13:58:00 set_time=2021-06-09 21:49:00 aster=<Object type=PLANET name=MERCURY />>, <AsterEphemerides rise_time=2021-06-09 06:52:00 culmination_time=2021-06-09 15:13:00 set_time=2021-06-09 23:34:00 aster=<Object type=PLANET name=VENUS />>, <AsterEphemerides rise_time=2021-06-09 08:38:00 culmination_time=2021-06-09 16:40:00 set_time=2021-06-09 00:44:00 aster=<Object type=PLANET name=MARS />>, <AsterEphemerides rise_time=2021-06-09 01:47:00 culmination_time=2021-06-09 06:54:00 set_time=2021-06-09 12:01:00 aster=<Object type=PLANET name=JUPITER />>, <AsterEphemerides rise_time=2021-06-09 01:06:00 culmination_time=2021-06-09 05:41:00 set_time=2021-06-09 10:16:00 aster=<Object type=PLANET name=SATURN />>, <AsterEphemerides rise_time=2021-06-09 03:56:00 culmination_time=2021-06-09 11:18:00 set_time=2021-06-09 18:40:00 aster=<Object type=PLANET name=URANUS />>, <AsterEphemerides rise_time=2021-06-09 02:27:00 culmination_time=2021-06-09 08:13:00 set_time=2021-06-09 13:59:00 aster=<Object type=PLANET name=NEPTUNE />>, <AsterEphemerides rise_time=2021-06-09 00:26:00 culmination_time=2021-06-09 04:32:00 set_time=2021-06-09 08:38:00 aster=<Object type=PLANET name=PLUTO />>] | ||||
Objects may not rise or set on the given date (i.e. they rise the previous day or set the next day). | |||||
When this happens, you will get `None` values on the rise or set time. | |||||
Note that this is timezone-dependent: | |||||
>>> get_ephemerides(Position(50.5876, 3.0624), date(2021, 9, 14), timezone=2)[1] | |||||
<AsterEphemerides rise_time=2021-09-14 16:46:00 culmination_time=2021-09-14 20:29:00 set_time=None aster=<Object type=SATELLITE name=MOON />> | |||||
If an objet does not rise nor set due to your latitude, then both rise and set will be `None`: | If an objet does not rise nor set due to your latitude, then both rise and set will be `None`: | ||||
>>> north_pole = Position(70, 20) | >>> north_pole = Position(70, 20) | ||||
>>> south_pole = Position(-70, 20) | >>> south_pole = Position(-70, 20) | ||||
>>> get_ephemerides(north_pole, datetime.date(2021, 6, 20)) | |||||
>>> get_ephemerides(north_pole, date(2021, 6, 20)) | |||||
[<AsterEphemerides rise_time=None culmination_time=2021-06-20 10:42:00 set_time=None aster=<Object type=STAR name=SUN />>, <AsterEphemerides rise_time=2021-06-20 14:30:00 culmination_time=2021-06-20 18:44:00 set_time=2021-06-20 22:53:00 aster=<Object type=SATELLITE name=MOON />>, <AsterEphemerides rise_time=2021-06-20 22:56:00 culmination_time=2021-06-20 09:47:00 set_time=2021-06-20 20:34:00 aster=<Object type=PLANET name=MERCURY />>, <AsterEphemerides rise_time=None culmination_time=2021-06-20 12:20:00 set_time=None aster=<Object type=PLANET name=VENUS />>, <AsterEphemerides rise_time=None culmination_time=2021-06-20 13:17:00 set_time=None aster=<Object type=PLANET name=MARS />>, <AsterEphemerides rise_time=2021-06-20 23:06:00 culmination_time=2021-06-20 03:04:00 set_time=2021-06-20 06:58:00 aster=<Object type=PLANET name=JUPITER />>, <AsterEphemerides rise_time=2021-06-20 23:28:00 culmination_time=2021-06-20 01:48:00 set_time=2021-06-20 04:05:00 aster=<Object type=PLANET name=SATURN />>, <AsterEphemerides rise_time=2021-06-20 21:53:00 culmination_time=2021-06-20 07:29:00 set_time=2021-06-20 17:02:00 aster=<Object type=PLANET name=URANUS />>, <AsterEphemerides rise_time=2021-06-20 22:51:00 culmination_time=2021-06-20 04:22:00 set_time=2021-06-20 09:50:00 aster=<Object type=PLANET name=NEPTUNE />>, <AsterEphemerides rise_time=None culmination_time=2021-06-20 00:40:00 set_time=None aster=<Object type=PLANET name=PLUTO />>] | [<AsterEphemerides rise_time=None culmination_time=2021-06-20 10:42:00 set_time=None aster=<Object type=STAR name=SUN />>, <AsterEphemerides rise_time=2021-06-20 14:30:00 culmination_time=2021-06-20 18:44:00 set_time=2021-06-20 22:53:00 aster=<Object type=SATELLITE name=MOON />>, <AsterEphemerides rise_time=2021-06-20 22:56:00 culmination_time=2021-06-20 09:47:00 set_time=2021-06-20 20:34:00 aster=<Object type=PLANET name=MERCURY />>, <AsterEphemerides rise_time=None culmination_time=2021-06-20 12:20:00 set_time=None aster=<Object type=PLANET name=VENUS />>, <AsterEphemerides rise_time=None culmination_time=2021-06-20 13:17:00 set_time=None aster=<Object type=PLANET name=MARS />>, <AsterEphemerides rise_time=2021-06-20 23:06:00 culmination_time=2021-06-20 03:04:00 set_time=2021-06-20 06:58:00 aster=<Object type=PLANET name=JUPITER />>, <AsterEphemerides rise_time=2021-06-20 23:28:00 culmination_time=2021-06-20 01:48:00 set_time=2021-06-20 04:05:00 aster=<Object type=PLANET name=SATURN />>, <AsterEphemerides rise_time=2021-06-20 21:53:00 culmination_time=2021-06-20 07:29:00 set_time=2021-06-20 17:02:00 aster=<Object type=PLANET name=URANUS />>, <AsterEphemerides rise_time=2021-06-20 22:51:00 culmination_time=2021-06-20 04:22:00 set_time=2021-06-20 09:50:00 aster=<Object type=PLANET name=NEPTUNE />>, <AsterEphemerides rise_time=None culmination_time=2021-06-20 00:40:00 set_time=None aster=<Object type=PLANET name=PLUTO />>] | ||||
>>> get_ephemerides(north_pole, datetime.date(2021, 12, 21)) | |||||
>>> get_ephemerides(north_pole, date(2021, 12, 21)) | |||||
[<AsterEphemerides rise_time=None culmination_time=2021-12-21 10:38:00 set_time=None aster=<Object type=STAR name=SUN />>, <AsterEphemerides rise_time=None culmination_time=2021-12-21 00:04:00 set_time=None aster=<Object type=SATELLITE name=MOON />>, <AsterEphemerides rise_time=None culmination_time=2021-12-21 11:33:00 set_time=None aster=<Object type=PLANET name=MERCURY />>, <AsterEphemerides rise_time=2021-12-21 11:58:00 culmination_time=2021-12-21 12:33:00 set_time=2021-12-21 13:08:00 aster=<Object type=PLANET name=VENUS />>, <AsterEphemerides rise_time=None culmination_time=2021-12-21 08:54:00 set_time=None aster=<Object type=PLANET name=MARS />>, <AsterEphemerides rise_time=2021-12-21 11:07:00 culmination_time=2021-12-21 14:43:00 set_time=2021-12-21 18:19:00 aster=<Object type=PLANET name=JUPITER />>, <AsterEphemerides rise_time=2021-12-21 11:32:00 culmination_time=2021-12-21 13:33:00 set_time=2021-12-21 15:33:00 aster=<Object type=PLANET name=SATURN />>, <AsterEphemerides rise_time=2021-12-21 09:54:00 culmination_time=2021-12-21 19:13:00 set_time=2021-12-21 04:37:00 aster=<Object type=PLANET name=URANUS />>, <AsterEphemerides rise_time=2021-12-21 10:49:00 culmination_time=2021-12-21 16:05:00 set_time=2021-12-21 21:21:00 aster=<Object type=PLANET name=NEPTUNE />>, <AsterEphemerides rise_time=None culmination_time=2021-12-21 12:31:00 set_time=None aster=<Object type=PLANET name=PLUTO />>] | [<AsterEphemerides rise_time=None culmination_time=2021-12-21 10:38:00 set_time=None aster=<Object type=STAR name=SUN />>, <AsterEphemerides rise_time=None culmination_time=2021-12-21 00:04:00 set_time=None aster=<Object type=SATELLITE name=MOON />>, <AsterEphemerides rise_time=None culmination_time=2021-12-21 11:33:00 set_time=None aster=<Object type=PLANET name=MERCURY />>, <AsterEphemerides rise_time=2021-12-21 11:58:00 culmination_time=2021-12-21 12:33:00 set_time=2021-12-21 13:08:00 aster=<Object type=PLANET name=VENUS />>, <AsterEphemerides rise_time=None culmination_time=2021-12-21 08:54:00 set_time=None aster=<Object type=PLANET name=MARS />>, <AsterEphemerides rise_time=2021-12-21 11:07:00 culmination_time=2021-12-21 14:43:00 set_time=2021-12-21 18:19:00 aster=<Object type=PLANET name=JUPITER />>, <AsterEphemerides rise_time=2021-12-21 11:32:00 culmination_time=2021-12-21 13:33:00 set_time=2021-12-21 15:33:00 aster=<Object type=PLANET name=SATURN />>, <AsterEphemerides rise_time=2021-12-21 09:54:00 culmination_time=2021-12-21 19:13:00 set_time=2021-12-21 04:37:00 aster=<Object type=PLANET name=URANUS />>, <AsterEphemerides rise_time=2021-12-21 10:49:00 culmination_time=2021-12-21 16:05:00 set_time=2021-12-21 21:21:00 aster=<Object type=PLANET name=NEPTUNE />>, <AsterEphemerides rise_time=None culmination_time=2021-12-21 12:31:00 set_time=None aster=<Object type=PLANET name=PLUTO />>] | ||||
>>> get_ephemerides(south_pole, datetime.date(2021, 6, 20)) | |||||
>>> get_ephemerides(south_pole, date(2021, 6, 20)) | |||||
[<AsterEphemerides rise_time=None culmination_time=2021-06-20 10:42:00 set_time=None aster=<Object type=STAR name=SUN />>, <AsterEphemerides rise_time=2021-06-20 11:10:00 culmination_time=2021-06-20 19:06:00 set_time=2021-06-20 01:20:00 aster=<Object type=SATELLITE name=MOON />>, <AsterEphemerides rise_time=2021-06-20 07:47:00 culmination_time=2021-06-20 09:47:00 set_time=2021-06-20 11:48:00 aster=<Object type=PLANET name=MERCURY />>, <AsterEphemerides rise_time=None culmination_time=2021-06-20 12:20:00 set_time=None aster=<Object type=PLANET name=VENUS />>, <AsterEphemerides rise_time=2021-06-20 12:14:00 culmination_time=2021-06-20 13:17:00 set_time=2021-06-20 14:21:00 aster=<Object type=PLANET name=MARS />>, <AsterEphemerides rise_time=2021-06-20 18:32:00 culmination_time=2021-06-20 03:04:00 set_time=2021-06-20 11:32:00 aster=<Object type=PLANET name=JUPITER />>, <AsterEphemerides rise_time=2021-06-20 15:20:00 culmination_time=2021-06-20 01:48:00 set_time=2021-06-20 12:12:00 aster=<Object type=PLANET name=SATURN />>, <AsterEphemerides rise_time=2021-06-20 04:32:00 culmination_time=2021-06-20 07:29:00 set_time=2021-06-20 10:26:00 aster=<Object type=PLANET name=URANUS />>, <AsterEphemerides rise_time=2021-06-20 21:28:00 culmination_time=2021-06-20 04:22:00 set_time=2021-06-20 11:13:00 aster=<Object type=PLANET name=NEPTUNE />>, <AsterEphemerides rise_time=None culmination_time=2021-06-20 00:40:00 set_time=None aster=<Object type=PLANET name=PLUTO />>] | [<AsterEphemerides rise_time=None culmination_time=2021-06-20 10:42:00 set_time=None aster=<Object type=STAR name=SUN />>, <AsterEphemerides rise_time=2021-06-20 11:10:00 culmination_time=2021-06-20 19:06:00 set_time=2021-06-20 01:20:00 aster=<Object type=SATELLITE name=MOON />>, <AsterEphemerides rise_time=2021-06-20 07:47:00 culmination_time=2021-06-20 09:47:00 set_time=2021-06-20 11:48:00 aster=<Object type=PLANET name=MERCURY />>, <AsterEphemerides rise_time=None culmination_time=2021-06-20 12:20:00 set_time=None aster=<Object type=PLANET name=VENUS />>, <AsterEphemerides rise_time=2021-06-20 12:14:00 culmination_time=2021-06-20 13:17:00 set_time=2021-06-20 14:21:00 aster=<Object type=PLANET name=MARS />>, <AsterEphemerides rise_time=2021-06-20 18:32:00 culmination_time=2021-06-20 03:04:00 set_time=2021-06-20 11:32:00 aster=<Object type=PLANET name=JUPITER />>, <AsterEphemerides rise_time=2021-06-20 15:20:00 culmination_time=2021-06-20 01:48:00 set_time=2021-06-20 12:12:00 aster=<Object type=PLANET name=SATURN />>, <AsterEphemerides rise_time=2021-06-20 04:32:00 culmination_time=2021-06-20 07:29:00 set_time=2021-06-20 10:26:00 aster=<Object type=PLANET name=URANUS />>, <AsterEphemerides rise_time=2021-06-20 21:28:00 culmination_time=2021-06-20 04:22:00 set_time=2021-06-20 11:13:00 aster=<Object type=PLANET name=NEPTUNE />>, <AsterEphemerides rise_time=None culmination_time=2021-06-20 00:40:00 set_time=None aster=<Object type=PLANET name=PLUTO />>] | ||||
>>> get_ephemerides(south_pole, datetime.date(2021, 12, 22)) | |||||
>>> get_ephemerides(south_pole, date(2021, 12, 22)) | |||||
[<AsterEphemerides rise_time=None culmination_time=2021-12-22 10:39:00 set_time=None aster=<Object type=STAR name=SUN />>, <AsterEphemerides rise_time=None culmination_time=2021-12-22 01:01:00 set_time=None aster=<Object type=SATELLITE name=MOON />>, <AsterEphemerides rise_time=None culmination_time=2021-12-22 11:35:00 set_time=None aster=<Object type=PLANET name=MERCURY />>, <AsterEphemerides rise_time=None culmination_time=2021-12-22 12:27:00 set_time=None aster=<Object type=PLANET name=VENUS />>, <AsterEphemerides rise_time=None culmination_time=2021-12-22 08:53:00 set_time=None aster=<Object type=PLANET name=MARS />>, <AsterEphemerides rise_time=2021-12-22 05:52:00 culmination_time=2021-12-22 14:40:00 set_time=2021-12-22 23:26:00 aster=<Object type=PLANET name=JUPITER />>, <AsterEphemerides rise_time=2021-12-22 02:41:00 culmination_time=2021-12-22 13:29:00 set_time=2021-12-22 00:21:00 aster=<Object type=PLANET name=SATURN />>, <AsterEphemerides rise_time=2021-12-22 16:01:00 culmination_time=2021-12-22 19:09:00 set_time=2021-12-22 22:17:00 aster=<Object type=PLANET name=URANUS />>, <AsterEphemerides rise_time=2021-12-22 08:59:00 culmination_time=2021-12-22 16:01:00 set_time=2021-12-22 23:04:00 aster=<Object type=PLANET name=NEPTUNE />>, <AsterEphemerides rise_time=None culmination_time=2021-12-22 12:27:00 set_time=None aster=<Object type=PLANET name=PLUTO />>] | [<AsterEphemerides rise_time=None culmination_time=2021-12-22 10:39:00 set_time=None aster=<Object type=STAR name=SUN />>, <AsterEphemerides rise_time=None culmination_time=2021-12-22 01:01:00 set_time=None aster=<Object type=SATELLITE name=MOON />>, <AsterEphemerides rise_time=None culmination_time=2021-12-22 11:35:00 set_time=None aster=<Object type=PLANET name=MERCURY />>, <AsterEphemerides rise_time=None culmination_time=2021-12-22 12:27:00 set_time=None aster=<Object type=PLANET name=VENUS />>, <AsterEphemerides rise_time=None culmination_time=2021-12-22 08:53:00 set_time=None aster=<Object type=PLANET name=MARS />>, <AsterEphemerides rise_time=2021-12-22 05:52:00 culmination_time=2021-12-22 14:40:00 set_time=2021-12-22 23:26:00 aster=<Object type=PLANET name=JUPITER />>, <AsterEphemerides rise_time=2021-12-22 02:41:00 culmination_time=2021-12-22 13:29:00 set_time=2021-12-22 00:21:00 aster=<Object type=PLANET name=SATURN />>, <AsterEphemerides rise_time=2021-12-22 16:01:00 culmination_time=2021-12-22 19:09:00 set_time=2021-12-22 22:17:00 aster=<Object type=PLANET name=URANUS />>, <AsterEphemerides rise_time=2021-12-22 08:59:00 culmination_time=2021-12-22 16:01:00 set_time=2021-12-22 23:04:00 aster=<Object type=PLANET name=NEPTUNE />>, <AsterEphemerides rise_time=None culmination_time=2021-12-22 12:27:00 set_time=None aster=<Object type=PLANET name=PLUTO />>] | ||||
Note that the ephemerides can only be computed for a date range. | |||||
Asking for the ephemerides with an out of range date will result in an exception: | |||||
>>> get_ephemerides(pos, date(1000, 1, 1)) | |||||
Traceback (most recent call last): | |||||
... | |||||
kosmorrolib.exceptions.OutOfRangeDateError: The date must be between 1899-07-29 and 2053-10-07 | |||||
""" | """ | ||||
ephemerides = [] | ephemerides = [] | ||||
@@ -167,7 +183,7 @@ def get_ephemerides( | |||||
return ( | return ( | ||||
position.get_planet_topos() | position.get_planet_topos() | ||||
.at(time) | .at(time) | ||||
.observe(for_aster.get_skyfield_object()) | |||||
.observe(for_aster.skyfield_object) | |||||
.apparent() | .apparent() | ||||
.altaz()[0] | .altaz()[0] | ||||
.degrees | .degrees | ||||
@@ -183,26 +199,28 @@ def get_ephemerides( | |||||
fun.rough_period = 0.5 | fun.rough_period = 0.5 | ||||
return fun | return fun | ||||
start_time = get_timescale().utc(date.year, date.month, date.day, -timezone) | |||||
start_time = get_timescale().utc( | |||||
for_date.year, for_date.month, for_date.day, -timezone | |||||
) | |||||
end_time = get_timescale().utc( | end_time = get_timescale().utc( | ||||
date.year, date.month, date.day, 23 - timezone, 59, 59 | |||||
for_date.year, for_date.month, for_date.day, 23 - timezone, 59, 59 | |||||
) | ) | ||||
try: | try: | ||||
for aster in ASTERS: | for aster in ASTERS: | ||||
rise_times, arr = find_discrete(start_time, end_time, is_risen(aster)) | rise_times, arr = find_discrete(start_time, end_time, is_risen(aster)) | ||||
try: | |||||
culmination_time, _ = find_maxima( | |||||
start_time, | |||||
end_time, | |||||
f=get_angle(aster), | |||||
epsilon=1.0 / 3600 / 24, | |||||
num=12, | |||||
) | |||||
culmination_time = ( | |||||
culmination_time[0] if len(culmination_time) > 0 else None | |||||
) | |||||
except ValueError: | |||||
culmination_time, _ = find_maxima( | |||||
start_time, | |||||
end_time, | |||||
f=get_angle(aster), | |||||
epsilon=1.0 / 3600 / 24, | |||||
num=12, | |||||
) | |||||
if len(culmination_time) == 1: | |||||
culmination_time = culmination_time[0] | |||||
else: | |||||
culmination_time = None | culmination_time = None | ||||
rise_time, set_time = None, None | rise_time, set_time = None, None | ||||
@@ -244,8 +262,8 @@ def get_ephemerides( | |||||
start = translate_to_timezone(error.start_time.utc_datetime(), timezone) | start = translate_to_timezone(error.start_time.utc_datetime(), timezone) | ||||
end = translate_to_timezone(error.end_time.utc_datetime(), timezone) | end = translate_to_timezone(error.end_time.utc_datetime(), timezone) | ||||
start = datetime.date(start.year, start.month, start.day + 1) | |||||
end = datetime.date(end.year, end.month, end.day - 1) | |||||
start = date(start.year, start.month, start.day + 1) | |||||
end = date(end.year, end.month, end.day - 1) | |||||
raise OutOfRangeDateError(start, end) from error | raise OutOfRangeDateError(start, end) from error | ||||
@@ -25,21 +25,32 @@ from skyfield.units import Angle | |||||
from skyfield import almanac, eclipselib | from skyfield import almanac, eclipselib | ||||
from numpy import pi | from numpy import pi | ||||
from kosmorrolib.model import Object, Event, Star, Planet, ASTERS, EARTH | |||||
from kosmorrolib.model import ( | |||||
Object, | |||||
Event, | |||||
Object, | |||||
Star, | |||||
Planet, | |||||
get_aster, | |||||
ASTERS, | |||||
EARTH, | |||||
) | |||||
from kosmorrolib.dateutil import translate_to_timezone | from kosmorrolib.dateutil import translate_to_timezone | ||||
from kosmorrolib.enum import EventType, ObjectIdentifier, SeasonType, LunarEclipseType | from kosmorrolib.enum import EventType, ObjectIdentifier, SeasonType, LunarEclipseType | ||||
from kosmorrolib.exceptions import OutOfRangeDateError | from kosmorrolib.exceptions import OutOfRangeDateError | ||||
from kosmorrolib.core import get_timescale, get_skf_objects, flatten_list | from kosmorrolib.core import get_timescale, get_skf_objects, flatten_list | ||||
def _search_conjunction(start_time: Time, end_time: Time, timezone: int) -> [Event]: | |||||
def _search_conjunctions_occultations( | |||||
start_time: Time, end_time: Time, timezone: int | |||||
) -> [Event]: | |||||
"""Function to search conjunction. | """Function to search conjunction. | ||||
**Warning:** this is an internal function, not intended for use by end-developers. | **Warning:** this is an internal function, not intended for use by end-developers. | ||||
Will return MOON and VENUS opposition on 2021-06-12: | Will return MOON and VENUS opposition on 2021-06-12: | ||||
>>> conjunction = _search_conjunction(get_timescale().utc(2021,6,12),get_timescale().utc(2021,6,13),0) | |||||
>>> conjunction = _search_conjunctions_occultations(get_timescale().utc(2021, 6, 12), get_timescale().utc(2021, 6, 13), 0) | |||||
>>> len(conjunction) | >>> len(conjunction) | ||||
1 | 1 | ||||
>>> conjunction[0].objects | >>> conjunction[0].objects | ||||
@@ -47,8 +58,13 @@ def _search_conjunction(start_time: Time, end_time: Time, timezone: int) -> [Eve | |||||
Will return nothing if no conjunction happens: | Will return nothing if no conjunction happens: | ||||
>>> _search_conjunction(get_timescale().utc(2021,6,17),get_timescale().utc(2021,6,18),0) | |||||
>>> _search_conjunctions_occultations(get_timescale().utc(2021, 6, 17),get_timescale().utc(2021, 6, 18), 0) | |||||
[] | [] | ||||
This function detects occultations too: | |||||
>>> _search_conjunctions_occultations(get_timescale().utc(2021, 4, 17),get_timescale().utc(2021, 4, 18), 0) | |||||
[<Event type=OCCULTATION objects=[<Object type=SATELLITE name=MOON />, <Object type=PLANET name=MARS />] start=2021-04-17 12:08:16.115650+00:00 end=None details=None />] | |||||
""" | """ | ||||
earth = get_skf_objects()["earth"] | earth = get_skf_objects()["earth"] | ||||
aster1 = None | aster1 = None | ||||
@@ -57,10 +73,10 @@ def _search_conjunction(start_time: Time, end_time: Time, timezone: int) -> [Eve | |||||
def is_in_conjunction(time: Time): | def is_in_conjunction(time: Time): | ||||
earth_pos = earth.at(time) | earth_pos = earth.at(time) | ||||
_, aster1_lon, _ = ( | _, aster1_lon, _ = ( | ||||
earth_pos.observe(aster1.get_skyfield_object()).apparent().ecliptic_latlon() | |||||
earth_pos.observe(aster1.skyfield_object).apparent().ecliptic_latlon() | |||||
) | ) | ||||
_, aster2_lon, _ = ( | _, aster2_lon, _ = ( | ||||
earth_pos.observe(aster2.get_skyfield_object()).apparent().ecliptic_latlon() | |||||
earth_pos.observe(aster2.skyfield_object).apparent().ecliptic_latlon() | |||||
) | ) | ||||
return ((aster1_lon.radians - aster2_lon.radians) / pi % 2.0).astype( | return ((aster1_lon.radians - aster2_lon.radians) / pi % 2.0).astype( | ||||
@@ -70,7 +86,7 @@ def _search_conjunction(start_time: Time, end_time: Time, timezone: int) -> [Eve | |||||
is_in_conjunction.rough_period = 60.0 | is_in_conjunction.rough_period = 60.0 | ||||
computed = [] | computed = [] | ||||
conjunctions = [] | |||||
events = [] | |||||
for aster1 in ASTERS: | for aster1 in ASTERS: | ||||
# Ignore the Sun | # Ignore the Sun | ||||
@@ -85,20 +101,21 @@ def _search_conjunction(start_time: Time, end_time: Time, timezone: int) -> [Eve | |||||
for i, time in enumerate(times): | for i, time in enumerate(times): | ||||
if is_conjs[i]: | if is_conjs[i]: | ||||
aster1_pos = (aster1.get_skyfield_object() - earth).at(time) | |||||
aster2_pos = (aster2.get_skyfield_object() - earth).at(time) | |||||
aster1_pos = (aster1.skyfield_object - earth).at(time) | |||||
aster2_pos = (aster2.skyfield_object - earth).at(time) | |||||
distance = aster1_pos.separation_from(aster2_pos).degrees | distance = aster1_pos.separation_from(aster2_pos).degrees | ||||
if distance - aster2.get_apparent_radius( | |||||
time, earth | |||||
) < aster1.get_apparent_radius(time, earth): | |||||
if ( | |||||
distance - aster2.get_apparent_radius(time).degrees | |||||
< aster1.get_apparent_radius(time).degrees | |||||
): | |||||
occulting_aster = ( | occulting_aster = ( | ||||
[aster1, aster2] | [aster1, aster2] | ||||
if aster1_pos.distance().km < aster2_pos.distance().km | if aster1_pos.distance().km < aster2_pos.distance().km | ||||
else [aster2, aster1] | else [aster2, aster1] | ||||
) | ) | ||||
conjunctions.append( | |||||
events.append( | |||||
Event( | Event( | ||||
EventType.OCCULTATION, | EventType.OCCULTATION, | ||||
occulting_aster, | occulting_aster, | ||||
@@ -106,7 +123,7 @@ def _search_conjunction(start_time: Time, end_time: Time, timezone: int) -> [Eve | |||||
) | ) | ||||
) | ) | ||||
else: | else: | ||||
conjunctions.append( | |||||
events.append( | |||||
Event( | Event( | ||||
EventType.CONJUNCTION, | EventType.CONJUNCTION, | ||||
[aster1, aster2], | [aster1, aster2], | ||||
@@ -116,7 +133,7 @@ def _search_conjunction(start_time: Time, end_time: Time, timezone: int) -> [Eve | |||||
computed.append(aster1) | computed.append(aster1) | ||||
return conjunctions | |||||
return events | |||||
def _search_oppositions(start_time: Time, end_time: Time, timezone: int) -> [Event]: | def _search_oppositions(start_time: Time, end_time: Time, timezone: int) -> [Event]: | ||||
@@ -151,7 +168,7 @@ def _search_oppositions(start_time: Time, end_time: Time, timezone: int) -> [Eve | |||||
sun_pos = earth_pos.observe( | sun_pos = earth_pos.observe( | ||||
sun | sun | ||||
).apparent() # Never do this without eyes protection! | ).apparent() # Never do this without eyes protection! | ||||
aster_pos = earth_pos.observe(get_skf_objects()[aster.skyfield_name]).apparent() | |||||
aster_pos = earth_pos.observe(aster.skyfield_object).apparent() | |||||
_, lon1, _ = sun_pos.ecliptic_latlon() | _, lon1, _ = sun_pos.ecliptic_latlon() | ||||
_, lon2, _ = aster_pos.ecliptic_latlon() | _, lon2, _ = aster_pos.ecliptic_latlon() | ||||
@@ -189,26 +206,39 @@ def _search_oppositions(start_time: Time, end_time: Time, timezone: int) -> [Eve | |||||
def _search_maximal_elongations( | def _search_maximal_elongations( | ||||
start_time: Time, end_time: Time, timezone: int | start_time: Time, end_time: Time, timezone: int | ||||
) -> [Event]: | ) -> [Event]: | ||||
"""Function to search oppositions. | |||||
**Warning:** this is an internal function, not intended for use by end-developers. | |||||
Will return Mercury maimum elogation for September 14, 2021: | |||||
>>> get_events(date(2021, 9, 14)) | |||||
[<Event type=MAXIMAL_ELONGATION objects=[<Object type=PLANET name=MERCURY />] start=2021-09-14 04:13:46.664879+00:00 end=None details={'deg': 26.8} />] | |||||
""" | |||||
earth = get_skf_objects()["earth"] | earth = get_skf_objects()["earth"] | ||||
sun = get_skf_objects()["sun"] | sun = get_skf_objects()["sun"] | ||||
aster = None | |||||
def get_elongation(time: Time): | |||||
sun_pos = (sun - earth).at(time) | |||||
aster_pos = (aster.get_skyfield_object() - earth).at(time) | |||||
separation = sun_pos.separation_from(aster_pos) | |||||
return separation.degrees | |||||
def get_elongation(planet: Object): | |||||
def f(time: Time): | |||||
sun_pos = (sun - earth).at(time) | |||||
aster_pos = (planet.skyfield_object - earth).at(time) | |||||
separation = sun_pos.separation_from(aster_pos) | |||||
return separation.degrees | |||||
get_elongation.rough_period = 1.0 | |||||
f.rough_period = 1.0 | |||||
events = [] | |||||
return f | |||||
for aster in ASTERS: | |||||
if aster.skyfield_name not in ["MERCURY", "VENUS"]: | |||||
continue | |||||
events = [] | |||||
for identifier in [ObjectIdentifier.MERCURY, ObjectIdentifier.VENUS]: | |||||
planet = get_aster(identifier) | |||||
times, elongations = find_maxima( | times, elongations = find_maxima( | ||||
start_time, end_time, f=get_elongation, epsilon=1.0 / 24 / 3600, num=12 | |||||
start_time, | |||||
end_time, | |||||
f=get_elongation(planet), | |||||
epsilon=1.0 / 24 / 3600, | |||||
num=12, | |||||
) | ) | ||||
for i, time in enumerate(times): | for i, time in enumerate(times): | ||||
@@ -216,7 +246,7 @@ def _search_maximal_elongations( | |||||
events.append( | events.append( | ||||
Event( | Event( | ||||
EventType.MAXIMAL_ELONGATION, | EventType.MAXIMAL_ELONGATION, | ||||
[aster], | |||||
[planet], | |||||
translate_to_timezone(time.utc_datetime(), timezone), | translate_to_timezone(time.utc_datetime(), timezone), | ||||
details={"deg": elongation}, | details={"deg": elongation}, | ||||
) | ) | ||||
@@ -227,8 +257,8 @@ def _search_maximal_elongations( | |||||
def _get_distance(to_aster: Object, from_aster: Object): | def _get_distance(to_aster: Object, from_aster: Object): | ||||
def get_distance(time: Time): | def get_distance(time: Time): | ||||
from_pos = from_aster.get_skyfield_object().at(time) | |||||
to_pos = from_pos.observe(to_aster.get_skyfield_object()) | |||||
from_pos = from_aster.skyfield_object.at(time) | |||||
to_pos = from_pos.observe(to_aster.skyfield_object) | |||||
return to_pos.distance().km | return to_pos.distance().km | ||||
@@ -370,25 +400,23 @@ def _search_lunar_eclipse(start_time: Time, end_time: Time, timezone: int) -> [E | |||||
>>> _search_lunar_eclipse(get_timescale().utc(2017, 2, 11), get_timescale().utc(2017, 2, 12), 0) | >>> _search_lunar_eclipse(get_timescale().utc(2017, 2, 11), get_timescale().utc(2017, 2, 12), 0) | ||||
[<Event type=LUNAR_ECLIPSE objects=[<Object type=SATELLITE name=MOON />] start=2017-02-10 22:02:59.016572+00:00 end=2017-02-11 03:25:07.627886+00:00 details={'type': <LunarEclipseType.PENUMBRAL: 0>, 'maximum': datetime.datetime(2017, 2, 11, 0, 43, 51, 793786, tzinfo=datetime.timezone.utc)} />] | [<Event type=LUNAR_ECLIPSE objects=[<Object type=SATELLITE name=MOON />] start=2017-02-10 22:02:59.016572+00:00 end=2017-02-11 03:25:07.627886+00:00 details={'type': <LunarEclipseType.PENUMBRAL: 0>, 'maximum': datetime.datetime(2017, 2, 11, 0, 43, 51, 793786, tzinfo=datetime.timezone.utc)} />] | ||||
""" | """ | ||||
moon = ASTERS[1] | |||||
moon = get_aster(ObjectIdentifier.MOON) | |||||
events = [] | events = [] | ||||
t, y, details = eclipselib.lunar_eclipses(start_time, end_time, get_skf_objects()) | t, y, details = eclipselib.lunar_eclipses(start_time, end_time, get_skf_objects()) | ||||
for ti, yi in zip(t, y): | for ti, yi in zip(t, y): | ||||
penumbra_radius = Angle(radians=details["penumbra_radius_radians"][0]) | penumbra_radius = Angle(radians=details["penumbra_radius_radians"][0]) | ||||
_, max_lon, _ = ( | _, max_lon, _ = ( | ||||
EARTH.get_skyfield_object() | |||||
.at(ti) | |||||
.observe(moon.get_skyfield_object()) | |||||
EARTH.skyfield_object.at(ti) | |||||
.observe(moon.skyfield_object) | |||||
.apparent() | .apparent() | ||||
.ecliptic_latlon() | .ecliptic_latlon() | ||||
) | ) | ||||
def is_in_penumbra(time: Time): | def is_in_penumbra(time: Time): | ||||
_, lon, _ = ( | _, lon, _ = ( | ||||
EARTH.get_skyfield_object() | |||||
.at(time) | |||||
.observe(moon.get_skyfield_object()) | |||||
EARTH.skyfield_object.at(time) | |||||
.observe(moon.skyfield_object) | |||||
.apparent() | .apparent() | ||||
.ecliptic_latlon() | .ecliptic_latlon() | ||||
) | ) | ||||
@@ -453,6 +481,14 @@ def get_events(for_date: date = date.today(), timezone: int = 0) -> [Event]: | |||||
>>> get_events(date(2021, 4, 20)) | >>> get_events(date(2021, 4, 20)) | ||||
[] | [] | ||||
Note that the events can only be found for a date range. | |||||
Asking for the events with an out of range date will result in an exception: | |||||
>>> get_events(date(1000, 1, 1)) | |||||
Traceback (most recent call last): | |||||
... | |||||
kosmorrolib.exceptions.OutOfRangeDateError: The date must be between 1899-07-28 and 2053-10-08 | |||||
:param for_date: the date for which the events must be calculated | :param for_date: the date for which the events must be calculated | ||||
:param timezone: the timezone to adapt the results to. If not given, defaults to 0. | :param timezone: the timezone to adapt the results to. If not given, defaults to 0. | ||||
:return: a list of events found for the given date. | :return: a list of events found for the given date. | ||||
@@ -470,7 +506,7 @@ def get_events(for_date: date = date.today(), timezone: int = 0) -> [Event]: | |||||
for fun in [ | for fun in [ | ||||
_search_oppositions, | _search_oppositions, | ||||
_search_conjunction, | |||||
_search_conjunctions_occultations, | |||||
_search_maximal_elongations, | _search_maximal_elongations, | ||||
_search_apogee(ASTERS[1]), | _search_apogee(ASTERS[1]), | ||||
_search_perigee(ASTERS[1]), | _search_perigee(ASTERS[1]), | ||||
@@ -19,24 +19,14 @@ | |||||
from datetime import date | from datetime import date | ||||
class UnavailableFeatureError(RuntimeError): | |||||
def __init__(self, msg: str): | |||||
super().__init__() | |||||
self.msg = msg | |||||
class OutOfRangeDateError(RuntimeError): | |||||
class OutOfRangeDateError(ValueError): | |||||
def __init__(self, min_date: date, max_date: date): | def __init__(self, min_date: date, max_date: date): | ||||
super().__init__() | |||||
super().__init__( | |||||
"The date must be between %s and %s" | |||||
% ( | |||||
min_date.strftime("%Y-%m-%d"), | |||||
max_date.strftime("%Y-%m-%d"), | |||||
) | |||||
) | |||||
self.min_date = min_date | self.min_date = min_date | ||||
self.max_date = max_date | self.max_date = max_date | ||||
self.msg = "The date must be between %s and %s" % ( | |||||
min_date.strftime("%Y-%m-%d"), | |||||
max_date.strftime("%Y-%m-%d"), | |||||
) | |||||
class CompileError(RuntimeError): | |||||
def __init__(self, msg): | |||||
super().__init__() | |||||
self.msg = msg |
@@ -18,14 +18,14 @@ | |||||
from abc import ABC, abstractmethod | from abc import ABC, abstractmethod | ||||
from typing import Union | from typing import Union | ||||
from datetime import datetime | |||||
from datetime import datetime, timezone | |||||
from numpy import pi, arcsin | |||||
import numpy | |||||
from skyfield.api import Topos, Time | |||||
from skyfield.api import Topos, Time, Angle | |||||
from skyfield.vectorlib import VectorSum as SkfPlanet | from skyfield.vectorlib import VectorSum as SkfPlanet | ||||
from .core import get_skf_objects | |||||
from .core import get_skf_objects, get_timescale | |||||
from .enum import MoonPhaseType, EventType, ObjectIdentifier, ObjectType | from .enum import MoonPhaseType, EventType, ObjectIdentifier, ObjectType | ||||
@@ -54,6 +54,48 @@ class MoonPhase(Serializable): | |||||
) | ) | ||||
def get_next_phase(self): | def get_next_phase(self): | ||||
"""Helper to get the Moon phase that follows the one described by the object. | |||||
If the current Moon phase is New Moon or Waxing crescent, the next one will be First Quarter: | |||||
>>> moon_phase = MoonPhase(MoonPhaseType.NEW_MOON) | |||||
>>> moon_phase.get_next_phase() | |||||
<MoonPhaseType.FIRST_QUARTER: 2> | |||||
>>> moon_phase = MoonPhase(MoonPhaseType.NEW_MOON) | |||||
>>> moon_phase.get_next_phase() | |||||
<MoonPhaseType.FIRST_QUARTER: 2> | |||||
If the current Moon phase is First Quarter or Waxing gibbous, the next one will be Full Moon: | |||||
>>> moon_phase = MoonPhase(MoonPhaseType.FIRST_QUARTER) | |||||
>>> moon_phase.get_next_phase() | |||||
<MoonPhaseType.FULL_MOON: 4> | |||||
>>> moon_phase = MoonPhase(MoonPhaseType.WAXING_GIBBOUS) | |||||
>>> moon_phase.get_next_phase() | |||||
<MoonPhaseType.FULL_MOON: 4> | |||||
If the current Moon phase is Full Moon or Waning gibbous, the next one will be Last Quarter: | |||||
>>> moon_phase = MoonPhase(MoonPhaseType.FULL_MOON) | |||||
>>> moon_phase.get_next_phase() | |||||
<MoonPhaseType.LAST_QUARTER: 6> | |||||
>>> moon_phase = MoonPhase(MoonPhaseType.WANING_GIBBOUS) | |||||
>>> moon_phase.get_next_phase() | |||||
<MoonPhaseType.LAST_QUARTER: 6> | |||||
If the current Moon phase is Last Quarter Moon or Waning crescent, the next one will be New Moon: | |||||
>>> moon_phase = MoonPhase(MoonPhaseType.LAST_QUARTER) | |||||
>>> moon_phase.get_next_phase() | |||||
<MoonPhaseType.NEW_MOON: 0> | |||||
>>> moon_phase = MoonPhase(MoonPhaseType.WANING_CRESCENT) | |||||
>>> moon_phase.get_next_phase() | |||||
<MoonPhaseType.NEW_MOON: 0> | |||||
""" | |||||
if self.phase_type in [MoonPhaseType.NEW_MOON, MoonPhaseType.WAXING_CRESCENT]: | if self.phase_type in [MoonPhaseType.NEW_MOON, MoonPhaseType.WAXING_CRESCENT]: | ||||
return MoonPhaseType.FIRST_QUARTER | return MoonPhaseType.FIRST_QUARTER | ||||
if self.phase_type in [ | if self.phase_type in [ | ||||
@@ -83,18 +125,20 @@ class Object(Serializable): | |||||
""" | """ | ||||
def __init__( | def __init__( | ||||
self, identifier: ObjectIdentifier, skyfield_name: str, radius: float = None | |||||
self, | |||||
identifier: ObjectIdentifier, | |||||
skyfield_object: SkfPlanet, | |||||
radius: float = None, | |||||
): | ): | ||||
""" | """ | ||||
Initialize an astronomical object | Initialize an astronomical object | ||||
:param ObjectIdentifier identifier: the official name of the object (may be internationalized) | :param ObjectIdentifier identifier: the official name of the object (may be internationalized) | ||||
:param str skyfield_name: the internal name of the object in Skyfield library | |||||
:param str skyfield_object: the object from Skyfield library | |||||
:param float radius: the radius (in km) of the object | :param float radius: the radius (in km) of the object | ||||
:param AsterEphemerides ephemerides: the ephemerides associated to the object | |||||
""" | """ | ||||
self.identifier = identifier | self.identifier = identifier | ||||
self.skyfield_name = skyfield_name | |||||
self.skyfield_object = skyfield_object | |||||
self.radius = radius | self.radius = radius | ||||
def __repr__(self): | def __repr__(self): | ||||
@@ -103,32 +147,41 @@ class Object(Serializable): | |||||
self.identifier.name, | self.identifier.name, | ||||
) | ) | ||||
def get_skyfield_object(self) -> SkfPlanet: | |||||
return get_skf_objects()[self.skyfield_name] | |||||
@abstractmethod | @abstractmethod | ||||
def get_type(self) -> ObjectType: | def get_type(self) -> ObjectType: | ||||
pass | pass | ||||
def get_apparent_radius(self, time: Time, from_place) -> float: | |||||
""" | |||||
Calculate the apparent radius, in degrees, of the object from the given place at a given time. | |||||
:param time: | |||||
:param from_place: | |||||
:return: | |||||
""" | |||||
if self.radius is None: | |||||
raise ValueError("Missing radius for %s" % self.identifier.name) | |||||
def get_apparent_radius(self, for_date: Union[Time, datetime]) -> Angle: | |||||
"""Calculate the apparent radius, in degrees, of the object from the given place at a given time. | |||||
return ( | |||||
360 | |||||
/ pi | |||||
* arcsin( | |||||
self.radius | |||||
/ from_place.at(time).observe(self.get_skyfield_object()).distance().km | |||||
) | |||||
**Warning:** this is an internal function, not intended for use by end-developers. | |||||
For an easier usage, this method accepts datetime and Skyfield's Time objects: | |||||
>>> sun = ASTERS[0] | |||||
>>> sun.get_apparent_radius(datetime(2021, 6, 9, tzinfo=timezone.utc)) | |||||
<Angle 00deg 31' 31.6"> | |||||
>>> sun.get_apparent_radius(get_timescale().utc(2021, 6, 9)) | |||||
<Angle 00deg 31' 31.6"> | |||||
Source of the algorithm: https://rhodesmill.org/skyfield/examples.html#what-is-the-angular-diameter-of-a-planet-given-its-radius | |||||
:param for_date: the date for which the apparent radius has to be returned | |||||
:return: an object representing a Skyfield angle | |||||
""" | |||||
if isinstance(for_date, datetime): | |||||
for_date = get_timescale().from_datetime(for_date) | |||||
ra, dec, distance = ( | |||||
EARTH.skyfield_object.at(for_date) | |||||
.observe(self.skyfield_object) | |||||
.apparent() | |||||
.radec() | |||||
) | ) | ||||
return Angle(radians=numpy.arcsin(self.radius / distance.km) * 2.0) | |||||
def serialize(self) -> dict: | def serialize(self) -> dict: | ||||
"""Serialize the given object | """Serialize the given object | ||||
@@ -243,22 +296,47 @@ class AsterEphemerides(Serializable): | |||||
} | } | ||||
EARTH = Planet(ObjectIdentifier.EARTH, "EARTH") | |||||
EARTH = Planet(ObjectIdentifier.EARTH, get_skf_objects()["EARTH"]) | |||||
ASTERS = [ | ASTERS = [ | ||||
Star(ObjectIdentifier.SUN, "SUN", radius=696342), | |||||
Satellite(ObjectIdentifier.MOON, "MOON", radius=1737.4), | |||||
Planet(ObjectIdentifier.MERCURY, "MERCURY", radius=2439.7), | |||||
Planet(ObjectIdentifier.VENUS, "VENUS", radius=6051.8), | |||||
Planet(ObjectIdentifier.MARS, "MARS", radius=3396.2), | |||||
Planet(ObjectIdentifier.JUPITER, "JUPITER BARYCENTER", radius=71492), | |||||
Planet(ObjectIdentifier.SATURN, "SATURN BARYCENTER", radius=60268), | |||||
Planet(ObjectIdentifier.URANUS, "URANUS BARYCENTER", radius=25559), | |||||
Planet(ObjectIdentifier.NEPTUNE, "NEPTUNE BARYCENTER", radius=24764), | |||||
Planet(ObjectIdentifier.PLUTO, "PLUTO BARYCENTER", radius=1185), | |||||
Star(ObjectIdentifier.SUN, get_skf_objects()["SUN"], radius=696342), | |||||
Satellite(ObjectIdentifier.MOON, get_skf_objects()["MOON"], radius=1737.4), | |||||
Planet(ObjectIdentifier.MERCURY, get_skf_objects()["MERCURY"], radius=2439.7), | |||||
Planet(ObjectIdentifier.VENUS, get_skf_objects()["VENUS"], radius=6051.8), | |||||
Planet(ObjectIdentifier.MARS, get_skf_objects()["MARS"], radius=3396.2), | |||||
Planet( | |||||
ObjectIdentifier.JUPITER, get_skf_objects()["JUPITER BARYCENTER"], radius=71492 | |||||
), | |||||
Planet( | |||||
ObjectIdentifier.SATURN, get_skf_objects()["SATURN BARYCENTER"], radius=60268 | |||||
), | |||||
Planet( | |||||
ObjectIdentifier.URANUS, get_skf_objects()["URANUS BARYCENTER"], radius=25559 | |||||
), | |||||
Planet( | |||||
ObjectIdentifier.NEPTUNE, get_skf_objects()["NEPTUNE BARYCENTER"], radius=24764 | |||||
), | |||||
Planet(ObjectIdentifier.PLUTO, get_skf_objects()["PLUTO BARYCENTER"], radius=1185), | |||||
] | ] | ||||
def get_aster(identifier: ObjectIdentifier) -> Object: | |||||
"""Return the aster with the given identifier | |||||
>>> get_aster(ObjectIdentifier.SATURN) | |||||
<Object type=PLANET name=SATURN /> | |||||
You can also use it to get the `EARTH` object, even though it has its own constant: | |||||
<Object type=PLANET name=EARTH /> | |||||
""" | |||||
if identifier == ObjectIdentifier.EARTH: | |||||
return EARTH | |||||
for aster in ASTERS: | |||||
if aster.identifier == identifier: | |||||
return aster | |||||
class Position: | class Position: | ||||
def __init__(self, latitude: float, longitude: float): | def __init__(self, latitude: float, longitude: float): | ||||
self.latitude = latitude | self.latitude = latitude | ||||
@@ -267,7 +345,7 @@ class Position: | |||||
def get_planet_topos(self) -> Topos: | def get_planet_topos(self) -> Topos: | ||||
if self._topos is None: | if self._topos is None: | ||||
self._topos = EARTH.get_skyfield_object() + Topos( | |||||
self._topos = EARTH.skyfield_object + Topos( | |||||
latitude_degrees=self.latitude, longitude_degrees=self.longitude | latitude_degrees=self.latitude, longitude_degrees=self.longitude | ||||
) | ) | ||||
@@ -1,21 +1,5 @@ | |||||
#!/usr/bin/env python3 | #!/usr/bin/env python3 | ||||
# Kosmorrolib - The Library To Compute Your Ephemerides | |||||
# Copyright (C) 2021 Jérôme Deuchnord <jerome@deuchnord.fr> | |||||
# | |||||
# This program is free software: you can redistribute it and/or modify | |||||
# it under the terms of the GNU Affero General Public License as | |||||
# published by the Free Software Foundation, either version 3 of the | |||||
# License, or (at your option) any later version. | |||||
# | |||||
# This program is distributed in the hope that it will be useful, | |||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||||
# GNU Affero General Public License for more details. | |||||
# | |||||
# You should have received a copy of the GNU Affero General Public License | |||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. | |||||
import doctest | import doctest | ||||
from kosmorrolib import * | from kosmorrolib import * | ||||
@@ -1,23 +0,0 @@ | |||||
#!/usr/bin/env python3 | |||||
# Kosmorrolib - The Library To Compute Your Ephemerides | |||||
# Copyright (C) 2021 Jérôme Deuchnord <jerome@deuchnord.fr> | |||||
# | |||||
# This program is free software: you can redistribute it and/or modify | |||||
# it under the terms of the GNU Affero General Public License as | |||||
# published by the Free Software Foundation, either version 3 of the | |||||
# License, or (at your option) any later version. | |||||
# | |||||
# This program is distributed in the hope that it will be useful, | |||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||||
# GNU Affero General Public License for more details. | |||||
# | |||||
# You should have received a copy of the GNU Affero General Public License | |||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. | |||||
from .core import * | |||||
from .data import * | |||||
from .ephemerides import * | |||||
from .testutils import * | |||||
from .dateutil import * |
@@ -1,33 +0,0 @@ | |||||
#!/usr/bin/env python3 | |||||
# Kosmorrolib - The Library To Compute Your Ephemerides | |||||
# Copyright (C) 2021 Jérôme Deuchnord <jerome@deuchnord.fr> | |||||
# | |||||
# This program is free software: you can redistribute it and/or modify | |||||
# it under the terms of the GNU Affero General Public License as | |||||
# published by the Free Software Foundation, either version 3 of the | |||||
# License, or (at your option) any later version. | |||||
# | |||||
# This program is distributed in the hope that it will be useful, | |||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||||
# GNU Affero General Public License for more details. | |||||
# | |||||
# You should have received a copy of the GNU Affero General Public License | |||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. | |||||
import unittest | |||||
import kosmorrolib.core as core | |||||
class CoreTestCase(unittest.TestCase): | |||||
def test_flatten_list(self): | |||||
self.assertEqual( | |||||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], | |||||
core.flatten_list([0, 1, 2, [3, 4, [5, 6], 7], 8, [9]]), | |||||
) | |||||
if __name__ == "__main__": | |||||
unittest.main() |
@@ -1,38 +0,0 @@ | |||||
#!/usr/bin/env python3 | |||||
# Kosmorrolib - The Library To Compute Your Ephemerides | |||||
# Copyright (C) 2021 Jérôme Deuchnord <jerome@deuchnord.fr> | |||||
# | |||||
# This program is free software: you can redistribute it and/or modify | |||||
# it under the terms of the GNU Affero General Public License as | |||||
# published by the Free Software Foundation, either version 3 of the | |||||
# License, or (at your option) any later version. | |||||
# | |||||
# This program is distributed in the hope that it will be useful, | |||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||||
# GNU Affero General Public License for more details. | |||||
# | |||||
# You should have received a copy of the GNU Affero General Public License | |||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. | |||||
import unittest | |||||
from kosmorrolib import model, core | |||||
from kosmorrolib.enum import ObjectIdentifier | |||||
class DataTestCase(unittest.TestCase): | |||||
def test_object_radius_must_be_set_to_get_apparent_radius(self): | |||||
o = model.Planet(ObjectIdentifier.SATURN, "SATURN") | |||||
with self.assertRaises(ValueError) as context: | |||||
o.get_apparent_radius( | |||||
core.get_timescale().now(), core.get_skf_objects()["earth"] | |||||
) | |||||
self.assertEqual(("Missing radius for SATURN",), context.exception.args) | |||||
if __name__ == "__main__": | |||||
unittest.main() |
@@ -1,46 +0,0 @@ | |||||
#!/usr/bin/env python3 | |||||
# Kosmorrolib - The Library To Compute Your Ephemerides | |||||
# Copyright (C) 2021 Jérôme Deuchnord <jerome@deuchnord.fr> | |||||
# | |||||
# This program is free software: you can redistribute it and/or modify | |||||
# it under the terms of the GNU Affero General Public License as | |||||
# published by the Free Software Foundation, either version 3 of the | |||||
# License, or (at your option) any later version. | |||||
# | |||||
# This program is distributed in the hope that it will be useful, | |||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||||
# GNU Affero General Public License for more details. | |||||
# | |||||
# You should have received a copy of the GNU Affero General Public License | |||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. | |||||
import unittest | |||||
from kosmorrolib import dateutil | |||||
from datetime import datetime | |||||
class DateUtilTestCase(unittest.TestCase): | |||||
def test_translate_to_timezone(self): | |||||
date = dateutil.translate_to_timezone(datetime(2020, 6, 9, 4), to_tz=-2) | |||||
self.assertEqual(2, date.hour) | |||||
date = dateutil.translate_to_timezone(datetime(2020, 6, 9, 0), to_tz=2) | |||||
self.assertEqual(2, date.hour) | |||||
date = dateutil.translate_to_timezone( | |||||
datetime(2020, 6, 9, 8), to_tz=2, from_tz=6 | |||||
) | |||||
self.assertEqual(4, date.hour) | |||||
date = dateutil.translate_to_timezone( | |||||
datetime(2020, 6, 9, 1), to_tz=0, from_tz=2 | |||||
) | |||||
self.assertEqual(8, date.day) | |||||
self.assertEqual(23, date.hour) | |||||
if __name__ == "__main__": | |||||
unittest.main() |
@@ -1,143 +0,0 @@ | |||||
#!/usr/bin/env python3 | |||||
# Kosmorrolib - The Library To Compute Your Ephemerides | |||||
# Copyright (C) 2021 Jérôme Deuchnord <jerome@deuchnord.fr> | |||||
# | |||||
# This program is free software: you can redistribute it and/or modify | |||||
# it under the terms of the GNU Affero General Public License as | |||||
# published by the Free Software Foundation, either version 3 of the | |||||
# License, or (at your option) any later version. | |||||
# | |||||
# This program is distributed in the hope that it will be useful, | |||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||||
# GNU Affero General Public License for more details. | |||||
# | |||||
# You should have received a copy of the GNU Affero General Public License | |||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. | |||||
import unittest | |||||
from kosmorrolib.enum import MoonPhaseType | |||||
from .testutils import expect_assertions | |||||
from kosmorrolib import ephemerides | |||||
from kosmorrolib.model import Position, MoonPhase | |||||
from datetime import date | |||||
from kosmorrolib.exceptions import OutOfRangeDateError | |||||
class EphemeridesTestCase(unittest.TestCase): | |||||
def test_get_ephemerides_for_aster_returns_correct_hours(self): | |||||
position = Position(0, 0) | |||||
eph = ephemerides.get_ephemerides(date=date(2019, 11, 18), position=position) | |||||
@expect_assertions(self.assertRegex, num=3) | |||||
def do_assertions(assert_regex): | |||||
for ephemeris in eph: | |||||
if ephemeris.object.skyfield_name == "SUN": | |||||
assert_regex(ephemeris.rise_time.isoformat(), "^2019-11-18T05:42:") | |||||
assert_regex( | |||||
ephemeris.culmination_time.isoformat(), "^2019-11-18T11:45:" | |||||
) | |||||
assert_regex(ephemeris.set_time.isoformat(), "^2019-11-18T17:49:") | |||||
break | |||||
do_assertions() | |||||
################################################################################################################### | |||||
### MOON PHASE TESTS ### | |||||
################################################################################################################### | |||||
def test_moon_phase_new_moon(self): | |||||
phase = ephemerides.get_moon_phase(date(2019, 11, 25)) | |||||
self.assertEqual(MoonPhaseType.WANING_CRESCENT, phase.phase_type) | |||||
self.assertIsNone(phase.time) | |||||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), "^2019-11-26T") | |||||
phase = ephemerides.get_moon_phase(date(2019, 11, 26)) | |||||
self.assertEqual(MoonPhaseType.NEW_MOON, phase.phase_type) | |||||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), "^2019-12-04T") | |||||
phase = ephemerides.get_moon_phase(date(2019, 11, 27)) | |||||
self.assertEqual(MoonPhaseType.WAXING_CRESCENT, phase.phase_type) | |||||
self.assertIsNone(phase.time) | |||||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), "^2019-12-04T") | |||||
def test_moon_phase_first_crescent(self): | |||||
phase = ephemerides.get_moon_phase(date(2019, 11, 3)) | |||||
self.assertEqual(MoonPhaseType.WAXING_CRESCENT, phase.phase_type) | |||||
self.assertIsNone(phase.time) | |||||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), "^2019-11-04T") | |||||
phase = ephemerides.get_moon_phase(date(2019, 11, 4)) | |||||
self.assertEqual(MoonPhaseType.FIRST_QUARTER, phase.phase_type) | |||||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), "^2019-11-12T") | |||||
phase = ephemerides.get_moon_phase(date(2019, 11, 5)) | |||||
self.assertEqual(MoonPhaseType.WAXING_GIBBOUS, phase.phase_type) | |||||
self.assertIsNone(phase.time) | |||||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), "^2019-11-12T") | |||||
def test_moon_phase_full_moon(self): | |||||
phase = ephemerides.get_moon_phase(date(2019, 11, 11)) | |||||
self.assertEqual(MoonPhaseType.WAXING_GIBBOUS, phase.phase_type) | |||||
self.assertIsNone(phase.time) | |||||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), "^2019-11-12T") | |||||
phase = ephemerides.get_moon_phase(date(2019, 11, 12)) | |||||
self.assertEqual(MoonPhaseType.FULL_MOON, phase.phase_type) | |||||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), "^2019-11-19T") | |||||
phase = ephemerides.get_moon_phase(date(2019, 11, 13)) | |||||
self.assertEqual(MoonPhaseType.WANING_GIBBOUS, phase.phase_type) | |||||
self.assertIsNone(phase.time) | |||||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), "^2019-11-19T") | |||||
def test_moon_phase_last_quarter(self): | |||||
phase = ephemerides.get_moon_phase(date(2019, 11, 18)) | |||||
self.assertEqual(MoonPhaseType.WANING_GIBBOUS, phase.phase_type) | |||||
self.assertIsNone(phase.time) | |||||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), "^2019-11-19T") | |||||
phase = ephemerides.get_moon_phase(date(2019, 11, 19)) | |||||
self.assertEqual(MoonPhaseType.LAST_QUARTER, phase.phase_type) | |||||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), "^2019-11-26T") | |||||
phase = ephemerides.get_moon_phase(date(2019, 11, 20)) | |||||
self.assertEqual(MoonPhaseType.WANING_CRESCENT, phase.phase_type) | |||||
self.assertIsNone(phase.time) | |||||
self.assertRegexpMatches(phase.next_phase_date.isoformat(), "^2019-11-26T") | |||||
def test_moon_phase_prediction(self): | |||||
phase = MoonPhase(MoonPhaseType.NEW_MOON, None, None) | |||||
self.assertEqual(MoonPhaseType.FIRST_QUARTER, phase.get_next_phase()) | |||||
phase = MoonPhase(MoonPhaseType.WAXING_CRESCENT, None, None) | |||||
self.assertEqual(MoonPhaseType.FIRST_QUARTER, phase.get_next_phase()) | |||||
phase = MoonPhase(MoonPhaseType.FIRST_QUARTER, None, None) | |||||
self.assertEqual(MoonPhaseType.FULL_MOON, phase.get_next_phase()) | |||||
phase = MoonPhase(MoonPhaseType.WAXING_GIBBOUS, None, None) | |||||
self.assertEqual(MoonPhaseType.FULL_MOON, phase.get_next_phase()) | |||||
phase = MoonPhase(MoonPhaseType.FULL_MOON, None, None) | |||||
self.assertEqual(MoonPhaseType.LAST_QUARTER, phase.get_next_phase()) | |||||
phase = MoonPhase(MoonPhaseType.WANING_GIBBOUS, None, None) | |||||
self.assertEqual(MoonPhaseType.LAST_QUARTER, phase.get_next_phase()) | |||||
phase = MoonPhase(MoonPhaseType.LAST_QUARTER, None, None) | |||||
self.assertEqual(MoonPhaseType.NEW_MOON, phase.get_next_phase()) | |||||
phase = MoonPhase(MoonPhaseType.WANING_CRESCENT, None, None) | |||||
self.assertEqual(MoonPhaseType.NEW_MOON, phase.get_next_phase()) | |||||
def test_get_ephemerides_raises_exception_on_out_of_date_range(self): | |||||
with self.assertRaises(OutOfRangeDateError): | |||||
ephemerides.get_ephemerides(Position(0, 0), date(1789, 5, 5)) | |||||
def test_get_moon_phase_raises_exception_on_out_of_date_range(self): | |||||
with self.assertRaises(OutOfRangeDateError): | |||||
ephemerides.get_moon_phase(date(1789, 5, 5)) | |||||
if __name__ == "__main__": | |||||
unittest.main() |
@@ -1,67 +0,0 @@ | |||||
#!/usr/bin/env python3 | |||||
# Kosmorrolib - The Library To Compute Your Ephemerides | |||||
# Copyright (C) 2021 Jérôme Deuchnord <jerome@deuchnord.fr> | |||||
# | |||||
# This program is free software: you can redistribute it and/or modify | |||||
# it under the terms of the GNU Affero General Public License as | |||||
# published by the Free Software Foundation, either version 3 of the | |||||
# License, or (at your option) any later version. | |||||
# | |||||
# This program is distributed in the hope that it will be useful, | |||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||||
# GNU Affero General Public License for more details. | |||||
# | |||||
# You should have received a copy of the GNU Affero General Public License | |||||
# along with this program. If not, see <https://www.gnu.org/licenses/>. | |||||
import functools | |||||
from unittest import mock | |||||
def expect_assertions(assert_fun, num=1): | |||||
"""Asserts that an assertion function is called as expected. | |||||
This is very useful when the assertions are in loops. | |||||
To use it, create a nested function in the the tests function. | |||||
The nested function will receive as parameter the mocked assertion function to use in place of the original one. | |||||
Finally, run the nested function. | |||||
Example of use: | |||||
>>> # the function we tests: | |||||
>>> def my_sum_function(n, m): | |||||
>>> # some code here | |||||
>>> pass | |||||
>>> # The unit tests: | |||||
>>> def test_sum(self): | |||||
>>> @expect_assertions(self.assertEqual, num=10): | |||||
>>> def make_assertions(assert_equal): | |||||
>>> for i in range (-5, 5): | |||||
>>> for j in range(-5, 5): | |||||
>>> assert_equal(i + j, my_sum_function(i, j) | |||||
>>> | |||||
>>> make_assertions() # You don't need to give any parameter, the decorator does it for you. | |||||
:param assert_fun: the assertion function to tests | |||||
:param num: the number of times the assertion function is expected to be called | |||||
""" | |||||
assert_fun_mock = mock.Mock(side_effect=assert_fun) | |||||
def fun_decorator(fun): | |||||
@functools.wraps(fun) | |||||
def sniff_function(): | |||||
fun(assert_fun_mock) | |||||
count = assert_fun_mock.call_count | |||||
if count != num: | |||||
raise AssertionError( | |||||
"Expected %d call(s) to function %s but called %d time(s)." | |||||
% (num, assert_fun.__name__, count) | |||||
) | |||||
return sniff_function | |||||
return fun_decorator |