From 61b48463b26fddc402d48ecc1f9474e8b09cf808 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20S=C3=A1nchez-Gallego?= Date: Sun, 24 Mar 2024 13:42:45 -0700 Subject: [PATCH] Add ephemeris and tools to create schedules --- poetry.lock | 62 ++++++++- pyproject.toml | 5 +- src/lvmapi/app.py | 3 +- src/lvmapi/data/ephemeris.parquet | Bin 0 -> 9095 bytes src/lvmapi/routers/ephemeris.py | 45 ++++++ src/lvmapi/tools/schedule.py | 170 +++++++++++++++++++++++ typings/astropy/__init__.pyi | 4 + typings/astropy/coordinates/__init__.pyi | 4 + typings/astropy/io/__init__.pyi | 4 + typings/astropy/io/fits/__init__.pyi | 4 + typings/astropy/time/__init__.pyi | 4 + typings/astropy/wcs/__init__.pyi | 4 + 12 files changed, 306 insertions(+), 3 deletions(-) create mode 100644 src/lvmapi/data/ephemeris.parquet create mode 100644 src/lvmapi/routers/ephemeris.py create mode 100644 src/lvmapi/tools/schedule.py create mode 100644 typings/astropy/__init__.pyi create mode 100644 typings/astropy/coordinates/__init__.pyi create mode 100644 typings/astropy/io/__init__.pyi create mode 100644 typings/astropy/io/fits/__init__.pyi create mode 100644 typings/astropy/time/__init__.pyi create mode 100644 typings/astropy/wcs/__init__.pyi diff --git a/poetry.lock b/poetry.lock index 1feb308..5782530 100644 --- a/poetry.lock +++ b/poetry.lock @@ -200,6 +200,28 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphin test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (>=0.23)"] +[[package]] +name = "astroplan" +version = "0.9.1" +description = "Observation planning package for astronomers" +optional = false +python-versions = ">=3.7" +files = [ + {file = "astroplan-0.9.1.tar.gz", hash = "sha256:d98c5ea58f6131de391aa66c78e0b4d77649359b29dbc8fdee9385e0408c2f4b"}, +] + +[package.dependencies] +astropy = ">=4" +numpy = ">=1.17" +pytz = "*" +six = "*" + +[package.extras] +all = ["astroquery", "matplotlib (>=1.4)"] +docs = ["astroquery", "matplotlib (>=1.4)", "sphinx-astropy[confv2]", "sphinx-rtd-theme"] +plotting = ["astroquery", "matplotlib (>=1.4)"] +test = ["pytest-astropy", "pytest-mpl"] + [[package]] name = "astropy" version = "6.0.0" @@ -1411,6 +1433,44 @@ tomli = ">=1.2.2" [package.extras] poetry-plugin = ["poetry (>=1.0,<2.0)"] +[[package]] +name = "polars" +version = "0.20.16" +description = "Blazingly fast DataFrame library" +optional = false +python-versions = ">=3.8" +files = [ + {file = "polars-0.20.16-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:7530148621b541221b8a36cfad27cc576d77c0739e82d8383ee3a699935a5f63"}, + {file = "polars-0.20.16-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:92241d9aeb29de7c503d3b4f9191181f0831dd546ee6770e5c6fae3cead98edb"}, + {file = "polars-0.20.16-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b48593c44a20f09f1eb1449ea43e2e5a4adf3c82faf1fba797fb7364cfa2ca4"}, + {file = "polars-0.20.16-cp38-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:0c504cfdc88f3e4262d5ecf637c10ae154565b94250ee4263a9f5f346397cf98"}, + {file = "polars-0.20.16-cp38-abi3-win_amd64.whl", hash = "sha256:a1fa24ea374ff05766e43fe2f079d62edd682ade19e3e2d68526e9c7a3e23ddf"}, + {file = "polars-0.20.16.tar.gz", hash = "sha256:7a9ebb85bfc9dd964490612b6fee2afbde91eee6bfaa590b731c7868d225210b"}, +] + +[package.extras] +adbc = ["adbc-driver-manager", "adbc-driver-sqlite"] +all = ["polars[adbc,cloudpickle,connectorx,deltalake,fastexcel,fsspec,gevent,numpy,pandas,plot,pyarrow,pydantic,pyiceberg,sqlalchemy,timezone,xlsx2csv,xlsxwriter]"] +cloudpickle = ["cloudpickle"] +connectorx = ["connectorx (>=0.3.2)"] +deltalake = ["deltalake (>=0.14.0)"] +fastexcel = ["fastexcel (>=0.9)"] +fsspec = ["fsspec"] +gevent = ["gevent"] +matplotlib = ["matplotlib"] +numpy = ["numpy (>=1.16.0)"] +openpyxl = ["openpyxl (>=3.0.0)"] +pandas = ["pandas", "pyarrow (>=7.0.0)"] +plot = ["hvplot (>=0.9.1)"] +pyarrow = ["pyarrow (>=7.0.0)"] +pydantic = ["pydantic"] +pyiceberg = ["pyiceberg (>=0.5.0)"] +pyxlsb = ["pyxlsb (>=1.0)"] +sqlalchemy = ["pandas", "sqlalchemy"] +timezone = ["backports-zoneinfo", "tzdata"] +xlsx2csv = ["xlsx2csv (>=0.8.0)"] +xlsxwriter = ["xlsxwriter"] + [[package]] name = "prompt-toolkit" version = "3.0.43" @@ -2578,4 +2638,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.11,<3.13" -content-hash = "38907c3de62e1b1b17c94eccd49debc6e46cc7a824771ef37907d3c0e1f2e74b" +content-hash = "ae674ec6157f724e15e64df9adb7146be648f39fac166ca65dd21d03032894a0" diff --git a/pyproject.toml b/pyproject.toml index 35e1b05..7a89d24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ classifiers = [ packages = [ { include = "lvmapi", from = "src" } ] -include = [] +include = ["src/lvmapi/data/*"] [tool.poetry.dependencies] python = "^3.11,<3.13" @@ -39,6 +39,9 @@ slack-sdk = "^3.23.0" python-jose = {extras = ["cryptography"], version = "^3.3.0"} passlib = {extras = ["bcrypt"], version = "^1.7.4"} python-multipart = "^0.0.6" +astropy = "^6.0.0" +astroplan = "^0.9.1" +polars = "^0.20.16" [tool.poetry.group.dev.dependencies] ipython = ">=8.0.0" diff --git a/src/lvmapi/app.py b/src/lvmapi/app.py index aa7ebe6..4e31593 100644 --- a/src/lvmapi/app.py +++ b/src/lvmapi/app.py @@ -11,7 +11,7 @@ from fastapi import FastAPI from lvmapi import auth -from lvmapi.routers import slack, spectrographs, telescopes +from lvmapi.routers import ephemeris, slack, spectrographs, telescopes app = FastAPI() @@ -19,6 +19,7 @@ app.include_router(telescopes.router) app.include_router(spectrographs.router) app.include_router(slack.router) +app.include_router(ephemeris.router) @app.get("/") diff --git a/src/lvmapi/data/ephemeris.parquet b/src/lvmapi/data/ephemeris.parquet new file mode 100644 index 0000000000000000000000000000000000000000..a0de32d3b3f4b18e8974691a11c7a8f6541f059b GIT binary patch literal 9095 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 NNsz)|#)N;t`G1y0FH!&i literal 0 HcmV?d00001 diff --git a/src/lvmapi/routers/ephemeris.py b/src/lvmapi/routers/ephemeris.py new file mode 100644 index 0000000..a89a2b6 --- /dev/null +++ b/src/lvmapi/routers/ephemeris.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# @Author: José Sánchez-Gallego (gallegoj@uw.edu) +# @Date: 2024-03-24 +# @Filename: ephemeris.py +# @License: BSD 3-clause (http://www.opensource.org/licenses/BSD-3-Clause) + +from __future__ import annotations + +from fastapi import APIRouter +from pydantic import BaseModel + +from lvmapi.tools.schedule import get_ephemeris_summary + + +class EphemerisSummaryOut(BaseModel): + """Summary of the ephemeris.""" + + SJD: int + date: str + sunset: float + twilight_end: float + twilight_start: float + sunrise: float + is_night: bool + is_twilight: bool + time_to_sunset: float + time_to_sunrise: float + from_file: bool + + +router = APIRouter(prefix="/ephemeris", tags=["ephemeris"]) + + +@router.get("/") +@router.get( + "/summary", + description="Summary of the ephemeris", + response_model=EphemerisSummaryOut, +) +async def get_summary(): + """Returns a summary of the ephemeris.""" + + return get_ephemeris_summary() diff --git a/src/lvmapi/tools/schedule.py b/src/lvmapi/tools/schedule.py new file mode 100644 index 0000000..2396756 --- /dev/null +++ b/src/lvmapi/tools/schedule.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# @Author: José Sánchez-Gallego (gallegoj@uw.edu) +# @Date: 2024-03-24 +# @Filename: schedule.py +# @License: BSD 3-clause (http://www.opensource.org/licenses/BSD-3-Clause) + +from __future__ import annotations + +import pathlib + +import astroplan +import numpy +import polars +from astropy import units as uu +from astropy.time import Time + +from sdsstools import get_sjd + + +EPHEMERIS_FILE = pathlib.Path(__file__).parent / "../data/ephemeris.parquet" + + +def sjd_ephemeris(sjd: int, twilight_horizon: float = -18) -> polars.DataFrame: + """Returns the ephemeris for a given SJD.""" + + observer = astroplan.Observer.at_site("Las Campanas Observatory") + observer.pressure = 0.75 * uu.bar + + # Calculate Elevation of True Horizon. Astroplan does not provide this directly. + # See https://github.com/astropy/astroplan/issues/242 + h_observer = observer.elevation + R_earth = 6378100.0 * uu.m + dd = numpy.sqrt(h_observer * (2 * R_earth + h_observer)) + phi = (numpy.arccos((dd / R_earth).value) * uu.radian).to(uu.deg) + hzel = phi - 90 * uu.deg + + # Calculate time at ~15UT, which corresponds to about noon at LCO, so always + # before the beginning of the night. + time = Time(sjd - 0.35, format="mjd") + + sunset = observer.sun_set_time( + time, + which="next", + horizon=hzel - 0.25 * uu.deg, # Half the apparent size of the Sun. + ) + sunset_twilight = observer.sun_set_time( + time, + which="next", + horizon=twilight_horizon * uu.deg, + ) + + sunrise = observer.sun_rise_time( + time, + which="next", + horizon=hzel - 0.25 * uu.deg, + ) + sunrise_twilight = observer.sun_rise_time( + time, + which="next", + horizon=twilight_horizon * uu.deg, + ) + + df = polars.DataFrame( + [ + ( + sjd, + time.isot.split("T")[0], + sunset.jd, + sunset_twilight.jd, + sunrise_twilight.jd, + sunrise.jd, + ) + ], + schema={ + "SJD": polars.Int32, + "date": polars.String, + "sunset": polars.Float64, + "twilight_end": polars.Float64, + "twilight_start": polars.Float64, + "sunrise": polars.Float64, + }, + ) + + return df + + +def create_schedule( + end_sjd: int, + start_sjd: int | None = None, + twilight_horizon: float = -18, +) -> polars.DataFrame: + """Creates a schedule for the given time range. + + Parameters + ---------- + end_sjd + The final SJD of the schedule. + start_sjd + Optionally, the initial SJD. If not provided, the current time will be used. + + Returns + ------- + schedule + The schedule as a Polars dataframe. + + """ + + start_sjd = start_sjd or get_sjd("LCO") + + ephemeris: list[polars.DataFrame] = [] + for sjd in range(start_sjd, end_sjd + 1): + sjd_eph = sjd_ephemeris(sjd, twilight_horizon=twilight_horizon) + ephemeris.append(sjd_eph) + + return polars.concat(ephemeris) + + +def get_ephemeris_summary(sjd: int | None = None) -> dict: + """Returns a summary of the ephemeris for a given SJD.""" + + sjd = sjd or get_sjd("LCO") + + from_file = True + eph = polars.read_parquet(EPHEMERIS_FILE) + + data = eph.filter(polars.col("SJD") == sjd) + if len(data) == 0: + data = sjd_ephemeris(sjd) + from_file = False + + is_night = ( + Time(data["sunset"][0], format="jd") + < Time.now() + < Time(data["sunrise"][0], format="jd") + ) + + sunset = Time(data["sunset"][0], format="jd") + sunrise = Time(data["sunrise"][0], format="jd") + twilight_end = Time(data["twilight_end"][0], format="jd") + twilight_start = Time(data["twilight_start"][0], format="jd") + + time_to_sunset = (sunset - Time.now()).to(uu.h).value + if time_to_sunset < 0: + time_to_sunset = numpy.nan + + time_to_sunrise = (sunrise - Time.now()).to(uu.h).value + if time_to_sunrise < 0: + time_to_sunrise = numpy.nan + + is_twilight = ( + Time(data["twilight_end"][0], format="jd") + < Time.now() + < Time(data["twilight_start"][0], format="jd") + ) + + return { + "SJD": sjd, + "date": data["date"][0], + "sunset": sunset.jd, + "twilight_end": twilight_end.jd, + "twilight_start": twilight_start.jd, + "sunrise": sunrise.jd, + "is_night": is_night, + "is_twilight": is_twilight, + "time_to_sunset": round(time_to_sunset, 3), + "time_to_sunrise": round(time_to_sunrise, 3), + "from_file": from_file, + } diff --git a/typings/astropy/__init__.pyi b/typings/astropy/__init__.pyi new file mode 100644 index 0000000..c3ba156 --- /dev/null +++ b/typings/astropy/__init__.pyi @@ -0,0 +1,4 @@ +from typing import Any + + +def __getattr__(name: str) -> Any: ... diff --git a/typings/astropy/coordinates/__init__.pyi b/typings/astropy/coordinates/__init__.pyi new file mode 100644 index 0000000..c3ba156 --- /dev/null +++ b/typings/astropy/coordinates/__init__.pyi @@ -0,0 +1,4 @@ +from typing import Any + + +def __getattr__(name: str) -> Any: ... diff --git a/typings/astropy/io/__init__.pyi b/typings/astropy/io/__init__.pyi new file mode 100644 index 0000000..c3ba156 --- /dev/null +++ b/typings/astropy/io/__init__.pyi @@ -0,0 +1,4 @@ +from typing import Any + + +def __getattr__(name: str) -> Any: ... diff --git a/typings/astropy/io/fits/__init__.pyi b/typings/astropy/io/fits/__init__.pyi new file mode 100644 index 0000000..c3ba156 --- /dev/null +++ b/typings/astropy/io/fits/__init__.pyi @@ -0,0 +1,4 @@ +from typing import Any + + +def __getattr__(name: str) -> Any: ... diff --git a/typings/astropy/time/__init__.pyi b/typings/astropy/time/__init__.pyi new file mode 100644 index 0000000..c3ba156 --- /dev/null +++ b/typings/astropy/time/__init__.pyi @@ -0,0 +1,4 @@ +from typing import Any + + +def __getattr__(name: str) -> Any: ... diff --git a/typings/astropy/wcs/__init__.pyi b/typings/astropy/wcs/__init__.pyi new file mode 100644 index 0000000..c3ba156 --- /dev/null +++ b/typings/astropy/wcs/__init__.pyi @@ -0,0 +1,4 @@ +from typing import Any + + +def __getattr__(name: str) -> Any: ...