| @@ -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 | |||||