From ff958dc0090838c436893dc15ac1904998d90843 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 1 Jul 2026 19:38:42 +0000 Subject: [PATCH 1/3] docs: convert ANISE tutorials from ipynb to markdown Co-authored-by: ChristopherRabotin <4823784+ChristopherRabotin@users.noreply.github.com> --- .../Tutorial 01 - Querying SPK files.ipynb | 982 ---------- .../Tutorial 01 - Querying SPK files.md | 592 ++++++ ...SPICE and ANISE files (meta almanac).ipynb | 589 ------ ...te SPICE and ANISE files (meta almanac).md | 341 ++++ ...and working with the Orbit structure.ipynb | 495 ----- ...ng and working with the Orbit structure.md | 285 +++ ...ing Azimuth Elevation and Range data.ipynb | 1550 ---------------- ...puting Azimuth Elevation and Range data.md | 722 ++++++++ ...e kernels and text planetary kernels.ipynb | 341 ---- ...rame kernels and text planetary kernels.md | 201 ++ .../Tutorial 06 - Sun probe Earth angle.ipynb | 620 ------- .../Tutorial 06 - Sun probe Earth angle.md | 437 +++++ .../tutorials/Tutorial 07 - Analysis.ipynb | 1629 ----------------- .../anise/tutorials/Tutorial 07 - Analysis.md | 440 +++++ .../Tutorial 07 - Analysis_7_0.png | Bin 0 -> 67123 bytes .../Tutorial 08 - OEM and Covariance.ipynb | 372 ---- .../Tutorial 08 - OEM and Covariance.md | 242 +++ mkdocs.yml | 18 +- 18 files changed, 3269 insertions(+), 6587 deletions(-) delete mode 100644 docs/anise/tutorials/Tutorial 01 - Querying SPK files.ipynb create mode 100644 docs/anise/tutorials/Tutorial 01 - Querying SPK files.md delete mode 100644 docs/anise/tutorials/Tutorial 02 - Loading remote SPICE and ANISE files (meta almanac).ipynb create mode 100644 docs/anise/tutorials/Tutorial 02 - Loading remote SPICE and ANISE files (meta almanac).md delete mode 100644 docs/anise/tutorials/Tutorial 03 - Defining and working with the Orbit structure.ipynb create mode 100644 docs/anise/tutorials/Tutorial 03 - Defining and working with the Orbit structure.md delete mode 100644 docs/anise/tutorials/Tutorial 04 - Computing Azimuth Elevation and Range data.ipynb create mode 100644 docs/anise/tutorials/Tutorial 04 - Computing Azimuth Elevation and Range data.md delete mode 100644 docs/anise/tutorials/Tutorial 05 - Using frame kernels and text planetary kernels.ipynb create mode 100644 docs/anise/tutorials/Tutorial 05 - Using frame kernels and text planetary kernels.md delete mode 100644 docs/anise/tutorials/Tutorial 06 - Sun probe Earth angle.ipynb create mode 100644 docs/anise/tutorials/Tutorial 06 - Sun probe Earth angle.md delete mode 100644 docs/anise/tutorials/Tutorial 07 - Analysis.ipynb create mode 100644 docs/anise/tutorials/Tutorial 07 - Analysis.md create mode 100644 docs/anise/tutorials/Tutorial 07 - Analysis_files/Tutorial 07 - Analysis_7_0.png delete mode 100644 docs/anise/tutorials/Tutorial 08 - OEM and Covariance.ipynb create mode 100644 docs/anise/tutorials/Tutorial 08 - OEM and Covariance.md diff --git a/docs/anise/tutorials/Tutorial 01 - Querying SPK files.ipynb b/docs/anise/tutorials/Tutorial 01 - Querying SPK files.ipynb deleted file mode 100644 index 2bfdacc..0000000 --- a/docs/anise/tutorials/Tutorial 01 - Querying SPK files.ipynb +++ /dev/null @@ -1,982 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "c6f0bc55-c0be-46c6-bb54-32df56aeba1f", - "metadata": {}, - "source": [ - "# ANISE\n", - "\n", - "ANISE is a modern rewrite of NAIF SPICE, written in Rust and providing interfaces to other languages including Python.\n", - "\n", - "Evidently, this tutorial applies to the Python usage of ANISE.\n", - "\n", - "## Goal\n", - "By the end of this tutorial, you should be able to know how to load local files and query the Almanac for the position and velocity of different celestial objects at different times, expressed in different time systems.\n", - "\n", - "Let's start by installing ANISE: `pip install anise`" - ] - }, - { - "cell_type": "markdown", - "id": "01c54376-a0cb-47b1-bdcd-da9a85242602", - "metadata": {}, - "source": [ - "## Introduction" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "2ca6dd47-507d-4e9a-a767-e46d2cc5a836", - "metadata": {}, - "outputs": [], - "source": [ - "import anise" - ] - }, - { - "cell_type": "markdown", - "id": "dc4ff306-68be-407f-9571-0ce23db65e27", - "metadata": {}, - "source": [ - "ANISE allows one to query SPICE files, like those created by JPL, for position and rotation information. A contrario to SPICE, ANISE is fully thread safe. To support this, ANISE has an object called the `Almanac` which stores all of the loaded files, whereas SPICE would have a single thread-unsafe shared context.\n", - "\n", - "Let's import that class." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "1502349d-444c-4512-90ae-01bcae54a573", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\u001b[0;31mInit signature:\u001b[0m \u001b[0mAlmanac\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpath\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mDocstring:\u001b[0m \n", - "An Almanac contains all of the loaded SPICE and ANISE data.\n", - "\n", - "# Limitations\n", - "The stack space required depends on the maximum number of each type that can be loaded.\n", - "\u001b[0;31mFile:\u001b[0m ~/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages/anise/__init__.py\n", - "\u001b[0;31mType:\u001b[0m type\n", - "\u001b[0;31mSubclasses:\u001b[0m " - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from anise import Almanac\n", - "Almanac?" - ] - }, - { - "cell_type": "markdown", - "id": "f215f0f3-9bd7-4c45-b097-aefa55ff523a", - "metadata": {}, - "source": [ - "As much as possible, the functions and classes in ANISE are documented. In typical Python, you can do `help(Almanac)` and in Jupyter notebook, you can simply enter the name of what you're seeking help on followed by a question mark, and execute the cell (Ctrl+Enter).\n", - "\n", - "Let's load on the files provided in the ANISE repo. Note that ANISE supports automatically downloading remote files as well, but that is the topic of another tutorial." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "4c91ffd3-ebe3-4a52-8617-5a8b7293ecfe", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Almanac: #SPK = 1\t#BPC = 0\n" - ] - } - ], - "source": [ - "almanac = Almanac(\"../../data/de440s.bsp\")\n", - "print(almanac)" - ] - }, - { - "cell_type": "markdown", - "id": "3759ef17-cd5f-435e-869d-a68b21eedeee", - "metadata": {}, - "source": [ - "You may also use the `describe` function to see the details of what is loaded, optionally showing only some of the loaded data." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "67ee44ce-d5b6-4c86-ba49-7ea89254a9b3", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\u001b[0;31mSignature:\u001b[0m\n", - "\u001b[0malmanac\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdescribe\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mspk\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mbpc\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mplanetary\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mtime_scale\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mround_time\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mDocstring:\u001b[0m\n", - "Pretty prints the description of this Almanac, showing everything by default. Default time scale is TDB.\n", - "If any parameter is set to true, then nothing other than that will be printed.\n", - "\u001b[0;31mType:\u001b[0m builtin_function_or_method" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "almanac.describe?" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "d120774f-09a5-4e38-bc96-f632e6fd746b", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "=== SPK #0 ===\n", - "┌────────────────┬─────────────────────────────┬───────────────────────────────┬───────────────────────────────────┬───────────────────────────────────┬─────────────┬────────────────────┐\n", - "│ Name │ Target │ Center │ Start epoch │ End epoch │ Duration │ Interpolation kind │\n", - "├────────────────┼─────────────────────────────┼───────────────────────────────┼───────────────────────────────────┼───────────────────────────────────┼─────────────┼────────────────────┤\n", - "│ DE-0440LE-0440 │ Mercury J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Venus J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Earth-Moon Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Mars Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Jupiter Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Saturn Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Uranus Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Neptune Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Pluto Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Sun J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Moon J2000 │ Earth-Moon Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Earth J2000 │ Earth-Moon Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ body 199 J2000 │ Mercury J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ body 299 J2000 │ Venus J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "└────────────────┴─────────────────────────────┴───────────────────────────────┴───────────────────────────────────┴───────────────────────────────────┴─────────────┴────────────────────┘\n" - ] - } - ], - "source": [ - "almanac.describe(bpc=True) # This will print nothing because no BPC is loaded\n", - "almanac.describe(spk=True)" - ] - }, - { - "cell_type": "markdown", - "id": "256e5365-1f6d-4c98-b6cb-f63a8ad18c51", - "metadata": {}, - "source": [ - "As of version 0.2.2, in Python, one can only perform transformations, and simple translations, but not extract the rotation information like the DCM (it is only available in Rust for now). What this means in practice is that one can query for the state of the Moon in the Earth body fixed frame (IAU Earth for example), and ANISE will perform all of the required transformations, but you cannot only query for the rotation parameters.\n", - "\n", - "**To query something, we need several pieces of information:**\n", - "\n", - "1. The epoch (or \"datetime\") at which to compute the state;\n", - "2. The frames we want the observer and target to be in;\n", - "3. The aberration corrections desired.\n" - ] - }, - { - "cell_type": "markdown", - "id": "9f9ee8b4-1d39-4d5c-a713-d8dd6d7f2eb1", - "metadata": {}, - "source": [ - "## Time management\n", - "For time management, ANISE uses [Hifitime](https://nyxspace.com/hifitime/intro/), arguably the most precise time management library in the world. Let's create an epoch by importing the hifitime classes, which are re-exported from ANISE." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "c4c1d46e-4701-4ccb-9a27-0832265f973e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2010-12-21T03:04:05 UTC\n", - "2010-12-21T03:03:31 UTC\n", - "-34 s\n" - ] - } - ], - "source": [ - "from anise.time import Epoch\n", - "\n", - "epoch_utc = Epoch(\"2010-12-21T03:04:05\")\n", - "print(epoch_utc)\n", - "\n", - "epoch_tai = Epoch(\"2010-12-21T03:04:05 TAI\")\n", - "print(epoch_tai)\n", - "print(epoch_tai.timedelta(epoch_utc)) # This will print the number of leap seconds between UTC and TAI." - ] - }, - { - "cell_type": "markdown", - "id": "0a31ba37-1173-4e07-a5af-785799e1c08e", - "metadata": {}, - "source": [ - "We created an Epoch in the TAI and in the UTC time scales. UTC is a glitchy time scale, which is never used in astrodynamics or astronomy. If the notion of a time scale is new to you, that's fine, just please read the Hifitime link above which explains the concept of time scales and why they are so important.\n", - "\n", - "So if UTC is not used in astrodynamics, what is? Well that would be the GPST time scale for low Earth orbiters and the TDB time scale for most things out of LEO." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "5526b332-d48a-469c-a24d-eb59c85dbd90", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2010-12-21T03:04:05 UTC is 11307 days 3 h 4 min 20 s after the GPS reference time of 1980-01-06T00:00:00 UTC\n", - "2010-12-21T03:04:05 UTC is 4006 days 15 h 5 min 11 s 183 ms 602 μs 262 ns after the Dynamical Barycentric Time reference time of 2000-01-01T11:58:55.816072704 UTC\n" - ] - } - ], - "source": [ - "epoch = epoch_utc\n", - "\n", - "print(f\"{epoch} is {epoch.to_gpst_duration()} after the GPS reference time of {Epoch.init_from_gpst_nanoseconds(0)}\")\n", - "\n", - "print(f\"{epoch} is {epoch.to_tdb_duration()} after the Dynamical Barycentric Time reference time of {Epoch.init_from_tdb_seconds(0.0)}\")" - ] - }, - { - "cell_type": "markdown", - "id": "725f779a-9a42-4dcd-b65e-353d824bde87", - "metadata": {}, - "source": [ - "^^^ That is one of the most useful features of Hifitime: seamless conversion between time scales, all with nanosecond precision, and for ~65,000 centuries around 01 January 1900.\n", - "\n", - "At this point, we've got one epoch. More analysis requires us to check stuff at different times.\n", - "\n", - "To do so, Hifitime provides arithmetics on epochs. Let's try that out." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "2ea6b1b1-ce75-45c3-9324-133e05190762", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2010-12-22T16:40:33.800000020 UTC\n", - "2010-12-22T16:40:33.800000020 UTC\n" - ] - } - ], - "source": [ - "from anise.time import Duration, Unit\n", - "\n", - "print(epoch + Duration(\"1.567 days 20 ns\"))\n", - "print(epoch + Unit.Day*1.567 + Unit.Nanosecond*20)" - ] - }, - { - "cell_type": "markdown", - "id": "0cc14e3f-05bb-47b6-a565-661fbf86f5d3", - "metadata": {}, - "source": [ - "Durations can be initialized using either their string representation using standard unit abbreviations.\n", - "\n", - "If we know exactly that we want to sample a certain number of times between two epoch, we can either write a for loop and add a duration, or we can use the `TimeSeries` class of Hifitime." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "5e234050-3153-48b3-bae1-005c215df7e9", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2010-12-21T03:04:05 UTC\n", - "2010-12-21T06:46:05 UTC\n", - "2010-12-21T10:28:05 UTC\n", - "2010-12-21T14:10:05 UTC\n", - "2010-12-21T17:52:05 UTC\n", - "2010-12-21T21:34:05 UTC\n", - "2010-12-22T01:16:05 UTC\n", - "2010-12-22T04:58:05 UTC\n", - "2010-12-22T08:40:05 UTC\n", - "2010-12-22T12:22:05 UTC\n", - "2010-12-22T16:04:05 UTC\n", - "2010-12-22T19:46:05 UTC\n", - "2010-12-22T23:28:05 UTC\n", - "2010-12-23T03:10:05 UTC\n", - "2010-12-23T06:52:05 UTC\n", - "2010-12-23T10:34:05 UTC\n", - "2010-12-23T14:16:05 UTC\n", - "2010-12-23T17:58:05 UTC\n", - "2010-12-23T21:40:05 UTC\n", - "2010-12-24T01:22:05 UTC\n", - "2010-12-24T05:04:05 UTC\n", - "2010-12-24T08:46:05 UTC\n", - "2010-12-24T12:28:05 UTC\n", - "2010-12-24T16:10:05 UTC\n", - "2010-12-24T19:52:05 UTC\n", - "2010-12-24T23:34:05 UTC\n" - ] - } - ], - "source": [ - "from anise.time import TimeSeries\n", - "\n", - "start = Epoch(\"2010-12-21T03:04:05\")\n", - "\n", - "for epoch_from_series in TimeSeries(start=start, end=start + Unit.Day*4, step=Unit.Hour*3.7, inclusive=True):\n", - " print(epoch_from_series)" - ] - }, - { - "cell_type": "markdown", - "id": "dc3f816b-9393-4075-88b0-c1048e5cbb34", - "metadata": {}, - "source": [ - "Finally, as a last bit of this introduction on time, let's demonstrate how durations can be rounded in hifitime.\n", - "Most humans don't usually care about nanosecond precision, so we can round those away." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "bd49354c-f5cc-46b8-bb56-4ff485da6b35", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "13 days 5 h 36 min 23 ns\n", - "13 days\n", - "13 days 5 h 36 min\n", - "9131 days 6 h\n" - ] - } - ], - "source": [ - "orig = Duration(\"13 d 5.6 h 23 ns\")\n", - "print(orig)\n", - "\n", - "print(orig.round(Unit.Day*1)) # Rounded to the nearest day\n", - "\n", - "print(orig.floor(Unit.Second*1)) # Floored to the nearest second\n", - "\n", - "print(orig.ceil(Unit.Century*0.25)) # Ceiled to the nearest quarter of a century" - ] - }, - { - "cell_type": "markdown", - "id": "0150cf41-df82-43e9-afdb-7281c39ea1fa", - "metadata": {}, - "source": [ - "### Learning exercise\n", - "\n", - "Your exercise is simple: create two dates in different time systems, run a time series over them at your desired time step, and print the difference between the start date and \"current\" date of the iterator as the printing happens. Don't hesitate to look at the [Python user guide of Hifitime](https://nyxspace.com/hifitime/python/).\n" - ] - }, - { - "cell_type": "markdown", - "id": "68eb5caf-252c-4495-a314-31bd1e3e7795", - "metadata": {}, - "source": [ - "## Frame management\n", - "\n", - "Frames are absolutely crucial in astrodynamics. They defined the point of origin and the orientation of that frame for each state.\n", - "\n", - "In terms of orientation, one will typically use the J2000 orientation. This is an inertial frame (well, quasi-inertial) which is tilted by about 23 degrees compared to the Ecliptic frame. The Ecliptic frame can be thought of as the average plane in which all of the planets rotate around the Sun. As such, the J2000 frame can be thought of the frame where the Z axis points _on average_ in the direction of the axis of rotation of the Earth.\n", - "\n", - "If a state is communicated in the \"J2000 frame\" without any centeral object provided, it's safe to assume that's the \"EME2000\" frame, which is the Earth Mean Equator J2000 orientation frame.\n", - "\n", - "There are _lots_ of frames, since one can define any frame with any center and any orientation. ANISE tries to help out: you can create a frame simply specifying its central object (as it NAIF ID) and its orientation ID (also matches the NAIF IDs). ANISE also provides constants for most common frames." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "286bfaf7-8ffd-4f77-bc61-c41b03f52983", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['EARTH_ECLIPJ2000',\n", - " 'EARTH_ITRF93',\n", - " 'EARTH_J2000',\n", - " 'EARTH_MOON_BARYCENTER_J2000',\n", - " 'EME2000',\n", - " 'IAU_EARTH_FRAME',\n", - " 'IAU_JUPITER_FRAME',\n", - " 'IAU_MARS_FRAME',\n", - " 'IAU_MERCURY_FRAME',\n", - " 'IAU_MOON_FRAME',\n", - " 'IAU_NEPTUNE_FRAME',\n", - " 'IAU_SATURN_FRAME',\n", - " 'IAU_URANUS_FRAME',\n", - " 'IAU_VENUS_FRAME',\n", - " 'JUPITER_BARYCENTER_J2000',\n", - " 'MARS_BARYCENTER_J2000',\n", - " 'MERCURY_J2000',\n", - " 'MOON_J2000',\n", - " 'NEPTUNE_BARYCENTER_J2000',\n", - " 'PLUTO_BARYCENTER_J2000',\n", - " 'SATURN_BARYCENTER_J2000',\n", - " 'SSB_J2000',\n", - " 'SUN_J2000',\n", - " 'URANUS_BARYCENTER_J2000',\n", - " 'VENUS_J2000',\n", - " '__class__',\n", - " '__delattr__',\n", - " '__dir__',\n", - " '__doc__',\n", - " '__eq__',\n", - " '__format__',\n", - " '__ge__',\n", - " '__getattribute__',\n", - " '__getstate__',\n", - " '__gt__',\n", - " '__hash__',\n", - " '__init__',\n", - " '__init_subclass__',\n", - " '__le__',\n", - " '__lt__',\n", - " '__module__',\n", - " '__ne__',\n", - " '__new__',\n", - " '__reduce__',\n", - " '__reduce_ex__',\n", - " '__repr__',\n", - " '__setattr__',\n", - " '__sizeof__',\n", - " '__str__',\n", - " '__subclasshook__']" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from anise.astro.constants import Frames\n", - "dir(Frames) # List of built-in frames" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "e87a0926-8018-4ef8-a581-2d8274e19f9e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Earth-Moon Barycenter J2000\n", - "True\n" - ] - } - ], - "source": [ - "# One can build a frame from its ephemeris and orientation IDs, which are the same as the NAIF codes.\n", - "# For example, ephemeris center 3 is the Earth Moon Barycenter, and orientation 1 is J2000\n", - "from anise.astro import Frame\n", - "custom_frame = Frame(3, 1)\n", - "print(custom_frame)\n", - "print(custom_frame == Frames.EARTH_MOON_BARYCENTER_J2000)" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "f3e821b2-78f4-4acb-8592-10a741d39674", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['__class__',\n", - " '__delattr__',\n", - " '__dir__',\n", - " '__doc__',\n", - " '__eq__',\n", - " '__format__',\n", - " '__ge__',\n", - " '__getattribute__',\n", - " '__getnewargs__',\n", - " '__getstate__',\n", - " '__gt__',\n", - " '__hash__',\n", - " '__init__',\n", - " '__init_subclass__',\n", - " '__le__',\n", - " '__lt__',\n", - " '__module__',\n", - " '__ne__',\n", - " '__new__',\n", - " '__reduce__',\n", - " '__reduce_ex__',\n", - " '__repr__',\n", - " '__setattr__',\n", - " '__sizeof__',\n", - " '__str__',\n", - " '__subclasshook__',\n", - " 'ephem_origin_id_match',\n", - " 'ephem_origin_match',\n", - " 'ephemeris_id',\n", - " 'flattening',\n", - " 'is_celestial',\n", - " 'is_geodetic',\n", - " 'mean_equatorial_radius_km',\n", - " 'mu_km3_s2',\n", - " 'orient_origin_id_match',\n", - " 'orient_origin_match',\n", - " 'orientation_id',\n", - " 'polar_radius_km',\n", - " 'semi_major_radius_km',\n", - " 'shape',\n", - " 'with_ephem',\n", - " 'with_orient']" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "eme2k = Frames.EARTH_J2000\n", - "dir(eme2k)" - ] - }, - { - "cell_type": "markdown", - "id": "f460a3de-30ba-4935-9560-98fa1097de93", - "metadata": {}, - "source": [ - "Frames include _potentially_ a lot of information. This is because the frame data is used to compute orbital elements, which we'll cover later on.\n", - "\n", - "However, ANISE does not provide any defaults for this data. You must load a Planetary Constants ANISE (\"PCA\") file for ANISE to know that these exist. Otherwise, you'll get an exception:" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "c9a2eb6c-0783-49d8-ab5b-8cf914f5803e", - "metadata": {}, - "outputs": [ - { - "ename": "Exception", - "evalue": "retrieving gravitational parameter requires the frame Earth J2000 to have mu_km3_s2 defined", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mException\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[14], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43meme2k\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmu_km3_s2\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[0;31mException\u001b[0m: retrieving gravitational parameter requires the frame Earth J2000 to have mu_km3_s2 defined" - ] - } - ], - "source": [ - "eme2k.mu_km3_s2()" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "ad5af23d-3a04-4122-ac51-a818bf4451e4", - "metadata": {}, - "outputs": [], - "source": [ - "# Let's load the PCA\n", - "almanac = almanac.load(\"../../data/pck08.pca\")" - ] - }, - { - "cell_type": "markdown", - "id": "ca9cf9b1-bb8b-40b8-8364-de9fbf4f0aae", - "metadata": {}, - "source": [ - "**VERY IMPORTANT:** Note how we've _reassigned_ the `almanac` variable in the `load` call. This is _also_ part of the thread safety: the almanac structure is _read only_ and the reassignment is what allows us to grab a reference to the almanac that has the newly loaded file.\n", - "\n", - "(Note that this also means you can have _multiple_ Almanac objects and compare the data loaded into them, similar to a \"BOA\" for those familiar with JPL MONTE.)" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "f4374ba5-2c0c-4768-869c-cdb6edb7651b", - "metadata": {}, - "outputs": [ - { - "ename": "Exception", - "evalue": "retrieving gravitational parameter requires the frame Earth J2000 to have mu_km3_s2 defined", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mException\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[16], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[43meme2k\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mmu_km3_s2\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m)\n", - "\u001b[0;31mException\u001b[0m: retrieving gravitational parameter requires the frame Earth J2000 to have mu_km3_s2 defined" - ] - } - ], - "source": [ - "print(eme2k.mu_km3_s2())" - ] - }, - { - "cell_type": "markdown", - "id": "e550acba-dbe5-45cb-bb41-cd627b1d07b3", - "metadata": {}, - "source": [ - "Uh! One way for ANISE to be thread safe without mutexes is for the data to be stored solely in the almanac structure. So loading the data in the almanac is not sufficient: we need to retrieve it.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "43ac7a62-2c1e-4c34-9b62-4646079c11d0", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Earth J2000 (μ = 398600.435436096 km^3/s^2, eq. radius = 6378.14 km, polar radius = 6356.75 km, f = 0.0033536422844278)\n" - ] - } - ], - "source": [ - "loaded_eme2k = almanac.frame_info(eme2k)\n", - "print(loaded_eme2k)" - ] - }, - { - "cell_type": "markdown", - "id": "12a7a0d9-f4c4-460e-b059-999fe8dd9ab0", - "metadata": {}, - "source": [ - "All of this data was created from the KPL/TPC files that NAIF provides. Another tutorial will guide you through modifying these." - ] - }, - { - "cell_type": "markdown", - "id": "fc8ce818-393f-4781-8343-4f04818747e9", - "metadata": {}, - "source": [ - "### Exercise #1\n", - "\n", - "1. Investigate what are some of the available fields in this loaded EME2000 frame.\n", - "2. Load the Venus J2000 frame and repeat point 1." - ] - }, - { - "cell_type": "markdown", - "id": "d09ea2e7-b41c-4c5e-88fa-8a64c098fa26", - "metadata": {}, - "source": [ - "## Querying the almanac\n", - "\n", - "So we've already seen how to build an epoch, and how to find a frame. Let's now quickly look into aberration corrections. The best explanation you'll find is that from NAIF itself: ." - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "6ecc0641-ec60-4c62-98b0-efecfbe6ee2c", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\u001b[0;31mInit signature:\u001b[0m \u001b[0mAberration\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mname\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mDocstring:\u001b[0m \n", - "Represents the aberration correction options in ANISE.\n", - "\n", - "In space science and engineering, accurately pointing instruments (like optical cameras or radio antennas) at a target is crucial. This task is complicated by the finite speed of light, necessitating corrections for the apparent position of the target.\n", - "\n", - "This structure holds parameters for aberration corrections applied to a target's position or state vector. These corrections account for the difference between the target's geometric (true) position and its apparent position as observed.\n", - "\n", - "# Rule of tumb\n", - "In most Earth orbits, one does _not_ need to provide any aberration corrections. Light time to the target is less than one second (the Moon is about one second away).\n", - "In near Earth orbits, e.g. inner solar system, preliminary analysis can benefit from enabling unconverged light time correction. Stellar aberration is probably not required.\n", - "For deep space missions, preliminary analysis would likely require both light time correction and stellar aberration. Mission planning and operations will definitely need converged light-time calculations.\n", - "\n", - "For more details, .\n", - "\n", - "# SPICE Validation\n", - "\n", - "The validation test `validate_jplde_de440s_aberration_lt` checks 101,000 pairs of ephemeris computations and shows that the unconverged Light Time computation matches the SPICE computations almost all the time.\n", - "More specifically, the 99th percentile of error is less than 5 meters, the 75th percentile is less than one meter, and the median error is less than 2 millimeters.\n", - "\u001b[0;31mFile:\u001b[0m ~/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages/anise/__init__.py\n", - "\u001b[0;31mType:\u001b[0m type\n", - "\u001b[0;31mSubclasses:\u001b[0m " - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from anise import Aberration\n", - "Aberration?" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "4410e354-d9dc-42a8-94b9-09cffd57dc96", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "LT (@0x7fd4c0478ee0)" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "lt_aber = Aberration(\"LT\") # This is the same as the LT enum in SPICE.\n", - "lt_aber" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "935bf989-bf59-49ba-b31f-8b64267f26d1", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "unconverged light-time aberration\n" - ] - } - ], - "source": [ - "# If you want the details of what will be computed, using the string representation instead of the `__repr__`:\n", - "print(lt_aber)" - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "fba7ff18-4669-42d1-92c9-00e3568c736c", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\u001b[0;31mSignature:\u001b[0m \u001b[0malmanac\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtranslate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtarget_frame\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mobserver_frame\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mepoch\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mab_corr\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mDocstring:\u001b[0m\n", - "Returns the Cartesian state of the target frame as seen from the observer frame at the provided epoch, and optionally given the aberration correction.\n", - "\n", - "# SPICE Compatibility\n", - "This function is the SPICE equivalent of spkezr: `spkezr(TARGET_ID, EPOCH_TDB_S, ORIENTATION_ID, ABERRATION, OBSERVER_ID)`\n", - "In ANISE, the TARGET_ID and ORIENTATION are provided in the first argument (TARGET_FRAME), as that frame includes BOTH\n", - "the target ID and the orientation of that target. The EPOCH_TDB_S is the epoch in the TDB time system, which is computed\n", - "in ANISE using Hifitime. THe ABERRATION is computed by providing the optional Aberration flag. Finally, the OBSERVER\n", - "argument is replaced by OBSERVER_FRAME: if the OBSERVER_FRAME argument has the same orientation as the TARGET_FRAME, then this call\n", - "will return exactly the same data as the spkerz SPICE call.\n", - "\n", - "# Warning\n", - "This function only performs the translation and no rotation whatsoever. Use the `transform` function instead to include rotations.\n", - "\n", - "# Note\n", - "This function performs a recursion of no more than twice the [MAX_TREE_DEPTH].\n", - "\u001b[0;31mType:\u001b[0m builtin_function_or_method" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Looking at the documentation here can be quite useful\n", - "almanac.translate?" - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "id": "a2c9b478-e8b0-411b-828a-0bf50cc82a6b", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\u001b[0;31mSignature:\u001b[0m \u001b[0malmanac\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtransform\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtarget_frame\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mobserver_frame\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mepoch\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mab_corr\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mDocstring:\u001b[0m\n", - "Returns the Cartesian state needed to transform the `from_frame` to the `to_frame`.\n", - "\n", - "# SPICE Compatibility\n", - "This function is the SPICE equivalent of spkezr: `spkezr(TARGET_ID, EPOCH_TDB_S, ORIENTATION_ID, ABERRATION, OBSERVER_ID)`\n", - "In ANISE, the TARGET_ID and ORIENTATION are provided in the first argument (TARGET_FRAME), as that frame includes BOTH\n", - "the target ID and the orientation of that target. The EPOCH_TDB_S is the epoch in the TDB time system, which is computed\n", - "in ANISE using Hifitime. THe ABERRATION is computed by providing the optional Aberration flag. Finally, the OBSERVER\n", - "argument is replaced by OBSERVER_FRAME: if the OBSERVER_FRAME argument has the same orientation as the TARGET_FRAME, then this call\n", - "will return exactly the same data as the spkerz SPICE call.\n", - "\n", - "# Note\n", - "The units will be those of the underlying ephemeris data (typically km and km/s)\n", - "\u001b[0;31mType:\u001b[0m builtin_function_or_method" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# The transform function will also compute the rotations where needed.\n", - "almanac.transform?" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "id": "aa3e6682-c0d0-4335-a488-69e9eef0dccf", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[Solar System Barycenter J2000] 2010-12-21T03:04:05 UTC\tposition = [2006351.284308, 135110969.586735, 58576035.963861] km\tvelocity = [-30.254436, 0.375013, 0.163924] km/s\n" - ] - } - ], - "source": [ - "state = almanac.translate(Frames.EARTH_J2000, Frames.SSB_J2000, epoch, None)\n", - "print(state)" - ] - }, - { - "cell_type": "markdown", - "id": "d8463e96-1b38-49f9-bdf2-36137e3b5212", - "metadata": {}, - "source": [ - "**TA DA!** We've just queried the almanac (i.e. the DE440s.bsp file) to get the position of the Earth in the J2000 frame as seen from the solar system barycenter (\"SSB\") in the J2000 frame, at the epoch we defined earlier, and when no aberration correction is performed." - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "id": "0ab91d31-1792-46ca-921f-e3320c226835", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "8 min 11 s 259 ms 84 μs 389 ns\n" - ] - } - ], - "source": [ - "state = almanac.translate(Frames.EARTH_J2000, Frames.SSB_J2000, epoch, Aberration(\"LT\"))\n", - "print(state.light_time())" - ] - }, - { - "cell_type": "markdown", - "id": "e9644e04-1f62-4694-9d27-a70e41ad9f43", - "metadata": {}, - "source": [ - "Calling \"light_time\" will return a Hifitime duration instance. You can grab the exact seconds by converting that high precision duration into the unit of your choice:" - ] - }, - { - "cell_type": "code", - "execution_count": 25, - "id": "7e50475e-d2a1-44f5-82bb-b721165c2625", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "0.005685869032280092\n", - "491.259084389\n", - "491.259084389\n" - ] - } - ], - "source": [ - "print(state.light_time().to_unit(Unit.Day))\n", - "print(state.light_time().to_unit(Unit.Second))\n", - "print(state.light_time().to_seconds())" - ] - }, - { - "cell_type": "markdown", - "id": "84d60190-cc88-4ab8-86b5-cad83297f787", - "metadata": {}, - "source": [ - "## Exerices\n", - "\n", - "1. Query the position of the Earth at different times using a time series. Note that the DE440s file is only valid for 200 years centered on 1950.\n", - "2. Query the position of Venus as seen from the Earth, both in the J2000 frame, and make a plot of the light time between these two planets throughout a given year." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "c6f54988-411b-456d-b3b6-0e1cb5902a95", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": ".venv" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.4" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/anise/tutorials/Tutorial 01 - Querying SPK files.md b/docs/anise/tutorials/Tutorial 01 - Querying SPK files.md new file mode 100644 index 0000000..4d5af84 --- /dev/null +++ b/docs/anise/tutorials/Tutorial 01 - Querying SPK files.md @@ -0,0 +1,592 @@ +# ANISE + +ANISE is a modern rewrite of NAIF SPICE, written in Rust and providing interfaces to other languages including Python. + +Evidently, this tutorial applies to the Python usage of ANISE. + +## Goal +By the end of this tutorial, you should be able to know how to load local files and query the Almanac for the position and velocity of different celestial objects at different times, expressed in different time systems. + +Let's start by installing ANISE: `pip install anise` + +## Introduction + + +```python +import anise +``` + +ANISE allows one to query SPICE files, like those created by JPL, for position and rotation information. A contrario to SPICE, ANISE is fully thread safe. To support this, ANISE has an object called the `Almanac` which stores all of the loaded files, whereas SPICE would have a single thread-unsafe shared context. + +Let's import that class. + + +```python +from anise import Almanac +Almanac? +``` + + + Init signature: Almanac(path) + Docstring: + An Almanac contains all of the loaded SPICE and ANISE data. + + # Limitations + The stack space required depends on the maximum number of each type that can be loaded. + File: ~/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages/anise/__init__.py + Type: type + Subclasses: + + +As much as possible, the functions and classes in ANISE are documented. In typical Python, you can do `help(Almanac)` and in Jupyter notebook, you can simply enter the name of what you're seeking help on followed by a question mark, and execute the cell (Ctrl+Enter). + +Let's load on the files provided in the ANISE repo. Note that ANISE supports automatically downloading remote files as well, but that is the topic of another tutorial. + + +```python +almanac = Almanac("../../data/de440s.bsp") +print(almanac) +``` + + Almanac: #SPK = 1 #BPC = 0 + + +You may also use the `describe` function to see the details of what is loaded, optionally showing only some of the loaded data. + + +```python +almanac.describe? +``` + + + Signature: + almanac.describe( +  spk=None, +  bpc=None, +  planetary=None, +  time_scale=None, +  round_time=None, + ) + Docstring: + Pretty prints the description of this Almanac, showing everything by default. Default time scale is TDB. + If any parameter is set to true, then nothing other than that will be printed. + Type: builtin_function_or_method + + + +```python +almanac.describe(bpc=True) # This will print nothing because no BPC is loaded +almanac.describe(spk=True) +``` + + === SPK #0 === + ┌────────────────┬─────────────────────────────┬───────────────────────────────┬───────────────────────────────────┬───────────────────────────────────┬─────────────┬────────────────────┐ + │ Name │ Target │ Center │ Start epoch │ End epoch │ Duration │ Interpolation kind │ + ├────────────────┼─────────────────────────────┼───────────────────────────────┼───────────────────────────────────┼───────────────────────────────────┼─────────────┼────────────────────┤ + │ DE-0440LE-0440 │ Mercury J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Venus J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Earth-Moon Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Mars Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Jupiter Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Saturn Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Uranus Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Neptune Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Pluto Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Sun J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Moon J2000 │ Earth-Moon Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Earth J2000 │ Earth-Moon Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ body 199 J2000 │ Mercury J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ body 299 J2000 │ Venus J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + └────────────────┴─────────────────────────────┴───────────────────────────────┴───────────────────────────────────┴───────────────────────────────────┴─────────────┴────────────────────┘ + + +As of version 0.2.2, in Python, one can only perform transformations, and simple translations, but not extract the rotation information like the DCM (it is only available in Rust for now). What this means in practice is that one can query for the state of the Moon in the Earth body fixed frame (IAU Earth for example), and ANISE will perform all of the required transformations, but you cannot only query for the rotation parameters. + +**To query something, we need several pieces of information:** + +1. The epoch (or "datetime") at which to compute the state; +2. The frames we want the observer and target to be in; +3. The aberration corrections desired. + + +## Time management +For time management, ANISE uses [Hifitime](https://nyxspace.com/hifitime/intro/), arguably the most precise time management library in the world. Let's create an epoch by importing the hifitime classes, which are re-exported from ANISE. + + +```python +from anise.time import Epoch + +epoch_utc = Epoch("2010-12-21T03:04:05") +print(epoch_utc) + +epoch_tai = Epoch("2010-12-21T03:04:05 TAI") +print(epoch_tai) +print(epoch_tai.timedelta(epoch_utc)) # This will print the number of leap seconds between UTC and TAI. +``` + + 2010-12-21T03:04:05 UTC + 2010-12-21T03:03:31 UTC + -34 s + + +We created an Epoch in the TAI and in the UTC time scales. UTC is a glitchy time scale, which is never used in astrodynamics or astronomy. If the notion of a time scale is new to you, that's fine, just please read the Hifitime link above which explains the concept of time scales and why they are so important. + +So if UTC is not used in astrodynamics, what is? Well that would be the GPST time scale for low Earth orbiters and the TDB time scale for most things out of LEO. + + +```python +epoch = epoch_utc + +print(f"{epoch} is {epoch.to_gpst_duration()} after the GPS reference time of {Epoch.init_from_gpst_nanoseconds(0)}") + +print(f"{epoch} is {epoch.to_tdb_duration()} after the Dynamical Barycentric Time reference time of {Epoch.init_from_tdb_seconds(0.0)}") +``` + + 2010-12-21T03:04:05 UTC is 11307 days 3 h 4 min 20 s after the GPS reference time of 1980-01-06T00:00:00 UTC + 2010-12-21T03:04:05 UTC is 4006 days 15 h 5 min 11 s 183 ms 602 μs 262 ns after the Dynamical Barycentric Time reference time of 2000-01-01T11:58:55.816072704 UTC + + +^^^ That is one of the most useful features of Hifitime: seamless conversion between time scales, all with nanosecond precision, and for ~65,000 centuries around 01 January 1900. + +At this point, we've got one epoch. More analysis requires us to check stuff at different times. + +To do so, Hifitime provides arithmetics on epochs. Let's try that out. + + +```python +from anise.time import Duration, Unit + +print(epoch + Duration("1.567 days 20 ns")) +print(epoch + Unit.Day*1.567 + Unit.Nanosecond*20) +``` + + 2010-12-22T16:40:33.800000020 UTC + 2010-12-22T16:40:33.800000020 UTC + + +Durations can be initialized using either their string representation using standard unit abbreviations. + +If we know exactly that we want to sample a certain number of times between two epoch, we can either write a for loop and add a duration, or we can use the `TimeSeries` class of Hifitime. + + +```python +from anise.time import TimeSeries + +start = Epoch("2010-12-21T03:04:05") + +for epoch_from_series in TimeSeries(start=start, end=start + Unit.Day*4, step=Unit.Hour*3.7, inclusive=True): + print(epoch_from_series) +``` + + 2010-12-21T03:04:05 UTC + 2010-12-21T06:46:05 UTC + 2010-12-21T10:28:05 UTC + 2010-12-21T14:10:05 UTC + 2010-12-21T17:52:05 UTC + 2010-12-21T21:34:05 UTC + 2010-12-22T01:16:05 UTC + 2010-12-22T04:58:05 UTC + 2010-12-22T08:40:05 UTC + 2010-12-22T12:22:05 UTC + 2010-12-22T16:04:05 UTC + 2010-12-22T19:46:05 UTC + 2010-12-22T23:28:05 UTC + 2010-12-23T03:10:05 UTC + 2010-12-23T06:52:05 UTC + 2010-12-23T10:34:05 UTC + 2010-12-23T14:16:05 UTC + 2010-12-23T17:58:05 UTC + 2010-12-23T21:40:05 UTC + 2010-12-24T01:22:05 UTC + 2010-12-24T05:04:05 UTC + 2010-12-24T08:46:05 UTC + 2010-12-24T12:28:05 UTC + 2010-12-24T16:10:05 UTC + 2010-12-24T19:52:05 UTC + 2010-12-24T23:34:05 UTC + + +Finally, as a last bit of this introduction on time, let's demonstrate how durations can be rounded in hifitime. +Most humans don't usually care about nanosecond precision, so we can round those away. + + +```python +orig = Duration("13 d 5.6 h 23 ns") +print(orig) + +print(orig.round(Unit.Day*1)) # Rounded to the nearest day + +print(orig.floor(Unit.Second*1)) # Floored to the nearest second + +print(orig.ceil(Unit.Century*0.25)) # Ceiled to the nearest quarter of a century +``` + + 13 days 5 h 36 min 23 ns + 13 days + 13 days 5 h 36 min + 9131 days 6 h + + +### Learning exercise + +Your exercise is simple: create two dates in different time systems, run a time series over them at your desired time step, and print the difference between the start date and "current" date of the iterator as the printing happens. Don't hesitate to look at the [Python user guide of Hifitime](https://nyxspace.com/hifitime/python/). + + +## Frame management + +Frames are absolutely crucial in astrodynamics. They defined the point of origin and the orientation of that frame for each state. + +In terms of orientation, one will typically use the J2000 orientation. This is an inertial frame (well, quasi-inertial) which is tilted by about 23 degrees compared to the Ecliptic frame. The Ecliptic frame can be thought of as the average plane in which all of the planets rotate around the Sun. As such, the J2000 frame can be thought of the frame where the Z axis points _on average_ in the direction of the axis of rotation of the Earth. + +If a state is communicated in the "J2000 frame" without any centeral object provided, it's safe to assume that's the "EME2000" frame, which is the Earth Mean Equator J2000 orientation frame. + +There are _lots_ of frames, since one can define any frame with any center and any orientation. ANISE tries to help out: you can create a frame simply specifying its central object (as it NAIF ID) and its orientation ID (also matches the NAIF IDs). ANISE also provides constants for most common frames. + + +```python +from anise.astro.constants import Frames +dir(Frames) # List of built-in frames +``` + + + + + ['EARTH_ECLIPJ2000', + 'EARTH_ITRF93', + 'EARTH_J2000', + 'EARTH_MOON_BARYCENTER_J2000', + 'EME2000', + 'IAU_EARTH_FRAME', + 'IAU_JUPITER_FRAME', + 'IAU_MARS_FRAME', + 'IAU_MERCURY_FRAME', + 'IAU_MOON_FRAME', + 'IAU_NEPTUNE_FRAME', + 'IAU_SATURN_FRAME', + 'IAU_URANUS_FRAME', + 'IAU_VENUS_FRAME', + 'JUPITER_BARYCENTER_J2000', + 'MARS_BARYCENTER_J2000', + 'MERCURY_J2000', + 'MOON_J2000', + 'NEPTUNE_BARYCENTER_J2000', + 'PLUTO_BARYCENTER_J2000', + 'SATURN_BARYCENTER_J2000', + 'SSB_J2000', + 'SUN_J2000', + 'URANUS_BARYCENTER_J2000', + 'VENUS_J2000', + '__class__', + '__delattr__', + '__dir__', + '__doc__', + '__eq__', + '__format__', + '__ge__', + '__getattribute__', + '__getstate__', + '__gt__', + '__hash__', + '__init__', + '__init_subclass__', + '__le__', + '__lt__', + '__module__', + '__ne__', + '__new__', + '__reduce__', + '__reduce_ex__', + '__repr__', + '__setattr__', + '__sizeof__', + '__str__', + '__subclasshook__'] + + + + +```python +# One can build a frame from its ephemeris and orientation IDs, which are the same as the NAIF codes. +# For example, ephemeris center 3 is the Earth Moon Barycenter, and orientation 1 is J2000 +from anise.astro import Frame +custom_frame = Frame(3, 1) +print(custom_frame) +print(custom_frame == Frames.EARTH_MOON_BARYCENTER_J2000) +``` + + Earth-Moon Barycenter J2000 + True + + + +```python +eme2k = Frames.EARTH_J2000 +dir(eme2k) +``` + + + + + ['__class__', + '__delattr__', + '__dir__', + '__doc__', + '__eq__', + '__format__', + '__ge__', + '__getattribute__', + '__getnewargs__', + '__getstate__', + '__gt__', + '__hash__', + '__init__', + '__init_subclass__', + '__le__', + '__lt__', + '__module__', + '__ne__', + '__new__', + '__reduce__', + '__reduce_ex__', + '__repr__', + '__setattr__', + '__sizeof__', + '__str__', + '__subclasshook__', + 'ephem_origin_id_match', + 'ephem_origin_match', + 'ephemeris_id', + 'flattening', + 'is_celestial', + 'is_geodetic', + 'mean_equatorial_radius_km', + 'mu_km3_s2', + 'orient_origin_id_match', + 'orient_origin_match', + 'orientation_id', + 'polar_radius_km', + 'semi_major_radius_km', + 'shape', + 'with_ephem', + 'with_orient'] + + + +Frames include _potentially_ a lot of information. This is because the frame data is used to compute orbital elements, which we'll cover later on. + +However, ANISE does not provide any defaults for this data. You must load a Planetary Constants ANISE ("PCA") file for ANISE to know that these exist. Otherwise, you'll get an exception: + + +```python +eme2k.mu_km3_s2() +``` + + + --------------------------------------------------------------------------- + + Exception Traceback (most recent call last) + + Cell In[14], line 1 + ----> 1 eme2k.mu_km3_s2() + + + Exception: retrieving gravitational parameter requires the frame Earth J2000 to have mu_km3_s2 defined + + + +```python +# Let's load the PCA +almanac = almanac.load("../../data/pck08.pca") +``` + +**VERY IMPORTANT:** Note how we've _reassigned_ the `almanac` variable in the `load` call. This is _also_ part of the thread safety: the almanac structure is _read only_ and the reassignment is what allows us to grab a reference to the almanac that has the newly loaded file. + +(Note that this also means you can have _multiple_ Almanac objects and compare the data loaded into them, similar to a "BOA" for those familiar with JPL MONTE.) + + +```python +print(eme2k.mu_km3_s2()) +``` + + + --------------------------------------------------------------------------- + + Exception Traceback (most recent call last) + + Cell In[16], line 1 + ----> 1 print(eme2k.mu_km3_s2()) + + + Exception: retrieving gravitational parameter requires the frame Earth J2000 to have mu_km3_s2 defined + + +Uh! One way for ANISE to be thread safe without mutexes is for the data to be stored solely in the almanac structure. So loading the data in the almanac is not sufficient: we need to retrieve it. + + + +```python +loaded_eme2k = almanac.frame_info(eme2k) +print(loaded_eme2k) +``` + + Earth J2000 (μ = 398600.435436096 km^3/s^2, eq. radius = 6378.14 km, polar radius = 6356.75 km, f = 0.0033536422844278) + + +All of this data was created from the KPL/TPC files that NAIF provides. Another tutorial will guide you through modifying these. + +### Exercise #1 + +1. Investigate what are some of the available fields in this loaded EME2000 frame. +2. Load the Venus J2000 frame and repeat point 1. + +## Querying the almanac + +So we've already seen how to build an epoch, and how to find a frame. Let's now quickly look into aberration corrections. The best explanation you'll find is that from NAIF itself: . + + +```python +from anise import Aberration +Aberration? +``` + + + Init signature: Aberration(name) + Docstring: + Represents the aberration correction options in ANISE. + + In space science and engineering, accurately pointing instruments (like optical cameras or radio antennas) at a target is crucial. This task is complicated by the finite speed of light, necessitating corrections for the apparent position of the target. + + This structure holds parameters for aberration corrections applied to a target's position or state vector. These corrections account for the difference between the target's geometric (true) position and its apparent position as observed. + + # Rule of tumb + In most Earth orbits, one does _not_ need to provide any aberration corrections. Light time to the target is less than one second (the Moon is about one second away). + In near Earth orbits, e.g. inner solar system, preliminary analysis can benefit from enabling unconverged light time correction. Stellar aberration is probably not required. + For deep space missions, preliminary analysis would likely require both light time correction and stellar aberration. Mission planning and operations will definitely need converged light-time calculations. + + For more details, . + + # SPICE Validation + + The validation test `validate_jplde_de440s_aberration_lt` checks 101,000 pairs of ephemeris computations and shows that the unconverged Light Time computation matches the SPICE computations almost all the time. + More specifically, the 99th percentile of error is less than 5 meters, the 75th percentile is less than one meter, and the median error is less than 2 millimeters. + File: ~/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages/anise/__init__.py + Type: type + Subclasses: + + + +```python +lt_aber = Aberration("LT") # This is the same as the LT enum in SPICE. +lt_aber +``` + + + + + LT (@0x7fd4c0478ee0) + + + + +```python +# If you want the details of what will be computed, using the string representation instead of the `__repr__`: +print(lt_aber) +``` + + unconverged light-time aberration + + + +```python +# Looking at the documentation here can be quite useful +almanac.translate? +``` + + + Signature: almanac.translate(target_frame, observer_frame, epoch, ab_corr=None) + Docstring: + Returns the Cartesian state of the target frame as seen from the observer frame at the provided epoch, and optionally given the aberration correction. + + # SPICE Compatibility + This function is the SPICE equivalent of spkezr: `spkezr(TARGET_ID, EPOCH_TDB_S, ORIENTATION_ID, ABERRATION, OBSERVER_ID)` + In ANISE, the TARGET_ID and ORIENTATION are provided in the first argument (TARGET_FRAME), as that frame includes BOTH + the target ID and the orientation of that target. The EPOCH_TDB_S is the epoch in the TDB time system, which is computed + in ANISE using Hifitime. THe ABERRATION is computed by providing the optional Aberration flag. Finally, the OBSERVER + argument is replaced by OBSERVER_FRAME: if the OBSERVER_FRAME argument has the same orientation as the TARGET_FRAME, then this call + will return exactly the same data as the spkerz SPICE call. + + # Warning + This function only performs the translation and no rotation whatsoever. Use the `transform` function instead to include rotations. + + # Note + This function performs a recursion of no more than twice the [MAX_TREE_DEPTH]. + Type: builtin_function_or_method + + + +```python +# The transform function will also compute the rotations where needed. +almanac.transform? +``` + + + Signature: almanac.transform(target_frame, observer_frame, epoch, ab_corr=None) + Docstring: + Returns the Cartesian state needed to transform the `from_frame` to the `to_frame`. + + # SPICE Compatibility + This function is the SPICE equivalent of spkezr: `spkezr(TARGET_ID, EPOCH_TDB_S, ORIENTATION_ID, ABERRATION, OBSERVER_ID)` + In ANISE, the TARGET_ID and ORIENTATION are provided in the first argument (TARGET_FRAME), as that frame includes BOTH + the target ID and the orientation of that target. The EPOCH_TDB_S is the epoch in the TDB time system, which is computed + in ANISE using Hifitime. THe ABERRATION is computed by providing the optional Aberration flag. Finally, the OBSERVER + argument is replaced by OBSERVER_FRAME: if the OBSERVER_FRAME argument has the same orientation as the TARGET_FRAME, then this call + will return exactly the same data as the spkerz SPICE call. + + # Note + The units will be those of the underlying ephemeris data (typically km and km/s) + Type: builtin_function_or_method + + + +```python +state = almanac.translate(Frames.EARTH_J2000, Frames.SSB_J2000, epoch, None) +print(state) +``` + + [Solar System Barycenter J2000] 2010-12-21T03:04:05 UTC position = [2006351.284308, 135110969.586735, 58576035.963861] km velocity = [-30.254436, 0.375013, 0.163924] km/s + + +**TA DA!** We've just queried the almanac (i.e. the DE440s.bsp file) to get the position of the Earth in the J2000 frame as seen from the solar system barycenter ("SSB") in the J2000 frame, at the epoch we defined earlier, and when no aberration correction is performed. + + +```python +state = almanac.translate(Frames.EARTH_J2000, Frames.SSB_J2000, epoch, Aberration("LT")) +print(state.light_time()) +``` + + 8 min 11 s 259 ms 84 μs 389 ns + + +Calling "light_time" will return a Hifitime duration instance. You can grab the exact seconds by converting that high precision duration into the unit of your choice: + + +```python +print(state.light_time().to_unit(Unit.Day)) +print(state.light_time().to_unit(Unit.Second)) +print(state.light_time().to_seconds()) +``` + + 0.005685869032280092 + 491.259084389 + 491.259084389 + + +## Exerices + +1. Query the position of the Earth at different times using a time series. Note that the DE440s file is only valid for 200 years centered on 1950. +2. Query the position of Venus as seen from the Earth, both in the J2000 frame, and make a plot of the light time between these two planets throughout a given year. + + +```python + +``` diff --git a/docs/anise/tutorials/Tutorial 02 - Loading remote SPICE and ANISE files (meta almanac).ipynb b/docs/anise/tutorials/Tutorial 02 - Loading remote SPICE and ANISE files (meta almanac).ipynb deleted file mode 100644 index bc79e41..0000000 --- a/docs/anise/tutorials/Tutorial 02 - Loading remote SPICE and ANISE files (meta almanac).ipynb +++ /dev/null @@ -1,589 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "9055aeed-f362-402e-a504-3b20e38feff4", - "metadata": {}, - "source": [ - "# ANISE\n", - "\n", - "ANISE is a modern rewrite of NAIF SPICE, written in Rust and providing interfaces to other languages include Python.\n", - "\n", - "Evidently, this tutorial applies to the Python usage of ANISE.\n", - "\n", - "## Goal\n", - "By the end of this tutorial, you should be able to know how to load local and remotes files using the `MetaAlmanac` structure, and know how to save and reload that meta configuration.\n", - "\n", - "Let's start by installing ANISE: `pip install anise`" - ] - }, - { - "cell_type": "markdown", - "id": "0ac23885-e0ff-4e38-94a3-85c247befd09", - "metadata": {}, - "source": [ - "## Introduction\n", - "\n", - "SPICE files, such as development ephemerides, are often substantial in size. Typically, they are stored on shared resources, with links circulated among teams who require access. This process can be cumbersome for end-users, who must ensure they are using the correct file and that it loads properly, irrespective of the script’s execution path.\n", - "\n", - "The `MetaAlmanac` addresses this challenge by facilitating the initialization of an Almanac using both local and remote files. Remote files are automatically downloaded to the user's application data cache folder (`AppData` on Windows, `~/.local/share/cache` on Linux). For these files, the MetaAlmanac verifies the integrity of any local copy by comparing its CRC32 checksum with that of the remote file.\n", - "\n", - "Furthermore, the MetaAlmanac guarantees the use of the most up-to-date versions of these files. An example is the daily Earth Orientation Parameters published by JPL, termed the \"high precision Earth rotation\" kernel. The MetaAlmanac enables users to seamlessly access the latest version of these files." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "28340bbd-490f-4aef-9ada-30c1d4017bcf", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\u001b[0;31mInit signature:\u001b[0m \u001b[0mMetaAlmanac\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmaybe_path\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mDocstring:\u001b[0m \n", - "A structure to set up an Almanac, with automatic downloading, local storage, checksum checking, and more.\n", - "\n", - "# Behavior\n", - "If the URI is a local path, relative or absolute, nothing will be fetched from a remote. Relative paths are relative to the execution folder (i.e. the current working directory).\n", - "If the URI is a remote path, the MetaAlmanac will first check if the file exists locally. If it exists, it will check that the CRC32 checksum of this file matches that of the specs.\n", - "If it does not match, the file will be downloaded again. If no CRC32 is provided but the file exists, then the MetaAlmanac will fetch the remote file and overwrite the existing file.\n", - "The downloaded path will be stored in the \"AppData\" folder.\n", - "\u001b[0;31mFile:\u001b[0m ~/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages/anise/__init__.py\n", - "\u001b[0;31mType:\u001b[0m type\n", - "\u001b[0;31mSubclasses:\u001b[0m " - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from anise import MetaAlmanac\n", - "\n", - "MetaAlmanac?" - ] - }, - { - "cell_type": "markdown", - "id": "f1934f96-030f-4008-bea5-59ba1fed60de", - "metadata": {}, - "source": [ - "## MetaAlmanac configuration\n", - "\n", - "As seen in the previous cell, a MetaAlmanac is typically initialized using a path to a configuration file (the `maybe_path` argument).\n", - "\n", - "The configuration file is written in [`Dhall`](https://dhall-lang.org/), an exceptional configuration language known for its safety, expressiveness, and maintainability. Dhall's design simplifies the process of configuring complex systems, making it a standout choice for tasks like initializing the MetaAlmanac.\n", - "\n", - "Let's see what this looks like:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "516ce346-1285-48cc-9940-8bb90620e993", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "-- Latest planetary ephemerides, planetary constants, high precision Moon rotation, and daily Earth orientation parameter\n", - "{ files =\n", - "[ { crc32 = Some 1921414410\n", - ", uri = \"http://public-data.nyxspace.com/anise/de440s.bsp\"\n", - "}\n", - ", { crc32 = Some 2899443223\n", - ", uri = \"http://public-data.nyxspace.com/anise/v0.4/pck11.pca\"\n", - "}\n", - ", { crc32 = Some 2133296540\n", - ", uri = \"http://public-data.nyxspace.com/anise/v0.4/moon_fk.epa\"\n", - "}\n", - ", { crc32 = Some 1817759242\n", - ", uri = \"http://public-data.nyxspace.com/anise/moon_pa_de440_200625.bpc\"\n", - "}\n", - ", { crc32 = None Natural\n", - ", uri =\n", - "\"https://naif.jpl.nasa.gov/pub/naif/generic_kernels/pck/earth_latest_high_prec.bpc\"\n", - "}\n", - "]\n", - "}\n" - ] - } - ], - "source": [ - "with open(\"../../data/latest.dhall\") as f:\n", - " for line in f.readlines():\n", - " print(line.strip())" - ] - }, - { - "cell_type": "markdown", - "id": "6076d240-713d-4a42-8dfa-a718c222e3a7", - "metadata": {}, - "source": [ - "This is the default MetaAlmanac: it grabs the development ephemerides `DE440s` from the public cloud of Nyx Space, grab a copy of the high fidelity Moon Principal Axes frame, grabs the planetary constant ANISE file `pck08.pca`. It also downloads the latest high precision Earth rotation parameters from JPL.\n", - "\n", - "The MetaAlmanac comes with a shortcut to download the latest info above without needing a copy of the Dhall file." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "97824317-42a6-4a5b-8424-7411a2d62ab7", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "=== BPC #0 ===\n", - "┌─────────────────────────┬───────────────────────────────────┬───────────────────────────────────┬───────────────────────────┬────────────────────┬───────┬────────────────┐\n", - "│ Name │ Start epoch │ End epoch │ Duration │ Interpolation kind │ Frame │ Inertial frame │\n", - "├─────────────────────────┼───────────────────────────────────┼────────────────────────────��──────┼───────────────────────────┼────────────────────┼───────┼────────────────┤\n", - "│ Earth PCK, ITRF93 Frame │ 2000-01-01T00:01:04.183912847 TDB │ 2002-09-26T21:18:50.632952778 TDB │ 999 days 21 h 17 min 46 s │ Chebyshev Triplet │ 3000 │ 17 │\n", - "├─────────────────────────┼───────────────────────────────────┼───────────────────────────────────┼───────────────────────────┼────────────────────┼───────┼────────────────┤\n", - "│ Earth PCK, ITRF93 Frame │ 2002-09-26T21:18:50.632952778 TDB │ 2005-06-22T18:36:37.081996238 TDB │ 999 days 21 h 17 min 46 s │ Chebyshev Triplet │ 3000 │ 17 │\n", - "├─────────────────────────┼───────────────────────────────────┼───────────────────────────────────┼───────────────────────────┼────────────────────┼───────┼────────────────┤\n", - "│ Earth PCK, ITRF93 Frame │ 2005-06-22T18:36:37.081996238 TDB │ 2008-03-18T15:54:23.531035669 TDB │ 999 days 21 h 17 min 46 s │ Chebyshev Triplet │ 3000 │ 17 │\n", - "├─────────────────────────┼─────────────────────���─────────────┼───────────────────────────────────┼───────────────────────────┼────────────────────┼───────┼────────────────┤\n", - "│ Earth PCK, ITRF93 Frame │ 2008-03-18T15:54:23.531035669 TDB │ 2010-12-13T13:12:09.980072814 TDB │ 999 days 21 h 17 min 46 s │ Chebyshev Triplet │ 3000 │ 17 │\n", - "├─────────────────────────┼───────────────────────────────────┼───────────────────────────────────┼───────────────────────────┼────────────────────┼───────┼────────────────┤\n", - "│ Earth PCK, ITRF93 Frame │ 2010-12-13T13:12:09.980072814 TDB │ 2013-09-08T10:29:56.429117874 TDB │ 999 days 21 h 17 min 46 s │ Chebyshev Triplet │ 3000 │ 17 │\n", - "├─────────────────────────┼───────────────────────────────────┼───────────────────────────────────┼───────────────────────────┼────────────────────┼───────┼────────────────┤\n", - "│ Earth PCK, ITRF93 Frame │ 2013-09-08T10:29:56.429117874 TDB │ 2016-06-04T07:47:42.878162558 TDB │ 999 days 21 h 17 min 46 s │ Chebyshev Triplet │ 3000 │ 17 │\n", - "├─────────────────────────┼───────────────────────────────────┼───────────────────────────────────┼───────────────────────────┼────────────────────┼───────┼────────────────┤\n", - "│ Earth PCK, ITRF93 Frame │ 2016-06-04T07:47:42.878162558 TDB │ 2019-03-01T05:05:29.327196302 TDB │ 999 days 21 h 17 min 46 s │ Chebyshev Triplet │ 3000 │ 17 │\n", - "├─────────────────────────┼───────────────────────────────────┼───────────────────────────────────┼──��────────────────────────┼────────────────────┼───────┼────────────────┤\n", - "│ Earth PCK, ITRF93 Frame │ 2019-03-01T05:05:29.327196302 TDB │ 2021-11-25T02:23:15.776233885 TDB │ 999 days 21 h 17 min 46 s │ Chebyshev Triplet │ 3000 │ 17 │\n", - "├─────────────────────────┼───────────────────────────────────┼───────────────────────────────────┼───────────────────────────┼────────────────────┼───────┼────────────────┤\n", - "│ Earth PCK, ITRF93 Frame │ 2021-11-25T02:23:15.776233885 TDB │ 2024-04-19T00:01:09.185602312 TDB │ 875 days 21 h 37 min 53 s │ Chebyshev Triplet │ 3000 │ 17 │\n", - "└─────────────────────────┴───────────────────────────────────┴───────────────────────────────────┴───────────────────────────┴────────────────────┴───────┴────────────────┘\n", - "=== BPC #1 ===\n", - "┌───────────┬───────────────────────────────────┬───────────────────────────────────┬─────────────┬─────────────────��──┬───────┬────────────────┐\n", - "│ Name │ Start epoch │ End epoch │ Duration │ Interpolation kind │ Frame │ Inertial frame │\n", - "├───────────┼───────────────────────────────────┼───────────────────────────────────┼─────────────┼────────────────────┼───────┼────────────────┤\n", - "│ de440.nio │ 1449-12-26T23:59:59.999860483 TDB │ 2426-02-15T23:59:59.999891009 TDB │ 320000 days │ Chebyshev Triplet │ 31008 │ 1 │\n", - "├───────────┼───────────────────────────────────┼──���────────────────────────────────┼─────────────┼────────────────────┼───────┼────────────────┤\n", - "│ de440.nio │ 2426-02-15T23:59:59.999891009 TDB │ 2650-01-24T23:59:59.999798018 TDB │ 81792 days │ Chebyshev Triplet │ 31008 │ 1 │\n", - "└───────────┴───────────────────────────────────┴───────────────────────────────────┴─────────────┴────────────────────┴───────┴────────────────┘\n" - ] - } - ], - "source": [ - "almanac = MetaAlmanac.latest()\n", - "almanac.describe(bpc=True)" - ] - }, - { - "cell_type": "markdown", - "id": "c1f6515d-3018-4a90-a617-dfe98aae1add", - "metadata": {}, - "source": [ - "The data downloaded from Nyx Space cloud has a checksum in the configuration file: that's because we know exactly what this data should be. Hence, if the data is modified in your local copy, the MetaAlmanac will download it again and replace your local copy. However, the JPL data changes daily, so we don't store a checksum in the config file, ensuring that the latest data is always downloaded.\n", - "\n", - "ANISE also provides a local config file to use the data stored in a copy of the repo." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "f748993a-744f-4e9a-be4f-0e637c605486", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "-- Default Almanac\n", - "{ files =\n", - "[ { crc32 = None Natural, uri = \"../../data/de440s.bsp\" }\n", - ", { crc32 = None Natural, uri = \"../../data/pck08.pca\" }\n", - "]\n", - "}\n" - ] - } - ], - "source": [ - "with open(\"../../data/local.dhall\") as f:\n", - " for line in f.readlines():\n", - " print(line.strip())" - ] - }, - { - "cell_type": "markdown", - "id": "44d856cb-10c2-42d0-ab15-614b84538b4a", - "metadata": {}, - "source": [ - "The CRC32 integrity number is not set for local paths because in any case, the MetaAlmanac does not know where to fetch another version where the checksum should match." - ] - }, - { - "cell_type": "markdown", - "id": "67f031fe-6a51-4aa0-a0c4-619669d04b0e", - "metadata": {}, - "source": [ - "## Using the MetaAlmanac\n", - "\n", - "The MetaAlmanac is designed to work seamlessly with the Alamac itself. In the following example, we'll use the latest MetaAlmanac (the same that's in `latest.dhall`) and see that it can be used to return the Almanac directly." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "5d07fd6b-b498-4f33-85f1-9e243a4f0ab1", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "MetaAlmanac { files: [MetaFile { uri: \"http://public-data.nyxspace.com/anise/de440s.bsp\", crc32: Some(1921414410) }, MetaFile { uri: \"http://public-data.nyxspace.com/anise/v0.4/pck11.pca\", crc32: Some(2899443223) }, MetaFile { uri: \"http://public-data.nyxspace.com/anise/v0.4/moon_fk.epa\", crc32: Some(2133296540) }, MetaFile { uri: \"http://public-data.nyxspace.com/anise/moon_pa_de440_200625.bpc\", crc32: Some(1817759242) }, MetaFile { uri: \"https://naif.jpl.nasa.gov/pub/naif/generic_kernels/pck/earth_latest_high_prec.bpc\", crc32: None }] }\n" - ] - } - ], - "source": [ - "meta = MetaAlmanac.load(\"../../data/latest.dhall\")\n", - "print(meta)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "550ec288-15bb-4652-a50c-278d59492bff", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Almanac: #SPK = 1\t#BPC = 2\tPlanetaryData with 54 ID mappings and 0 name mappings\tEulerParameterData with 3 ID mappings and 3 name mappings\n" - ] - } - ], - "source": [ - "almanac = meta.process()\n", - "print(almanac)" - ] - }, - { - "cell_type": "markdown", - "id": "79813cf7-b7ad-4d56-b315-508a438035f9", - "metadata": {}, - "source": [ - "Trivial! We now see that the loaded Almanac has one loaded SPK file (the de440s file), one BPC file (the latest Earth high precision rotation), and 49 planetary data mappings, loaded from the `pck08.pca`.\n", - "\n", - "Even simpler, you can just call the `latest()` class method which will call the `latest.dhall` equivalent, without requiring a local configuration file." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "8826c9a5-a50d-4c93-90b8-1ae45074d058", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Almanac: #SPK = 1\t#BPC = 2\tPlanetaryData with 54 ID mappings and 0 name mappings\tEulerParameterData with 3 ID mappings and 3 name mappings\n" - ] - } - ], - "source": [ - "print(MetaAlmanac.latest())" - ] - }, - { - "cell_type": "markdown", - "id": "3cb4db3f-09a4-4ad6-b32d-a7c2c9ba8e3c", - "metadata": {}, - "source": [ - "## Building a MetaAlmanac config\n", - "\n", - "Building a Dhall configuration for ANISE can be approached in two ways. The most direct method is to craft a Dhall file manually. However, given the complexity often associated with writing in Dhall, ANISE offers a more user-friendly alternative through the `MetaFile` class. This option simplifies the process of creating the necessary data, catering to users who may find direct Dhall scripting challenging." - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "fae8a0c8-15e7-4e98-aaf3-ba0859c64121", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\u001b[0;31mInit signature:\u001b[0m \u001b[0mMetaFile\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0muri\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcrc32\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mFile:\u001b[0m ~/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages/anise/__init__.py\n", - "\u001b[0;31mType:\u001b[0m type\n", - "\u001b[0;31mSubclasses:\u001b[0m " - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from anise import MetaFile\n", - "MetaFile?" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "423b2032-a36c-4863-ba6e-ebd09ed90508", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Almanac: #SPK = 0\t#BPC = 0 (@0x5582525a9d10)" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Let's initialize a new empty MetaAlmanac.\n", - "new_meta = MetaAlmanac()\n", - "new_meta.process() # Note that you can always initialize an empty MetaAlmanac because you can initialize an empty Almanac" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "88479168-6c60-438d-958e-86c757a59f04", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'{ files = [{ crc32 = None Natural, uri = \"../../data/de440s.bsp\" }, { crc32 = None Natural, uri = \"https://naif.jpl.nasa.gov/pub/naif/generic_kernels/pck/moon_pa_de440_200625.bpc\" }, { crc32 = None Natural, uri = \"https://google.com/non/existing/pck08.pca\" }] }'" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# Create MetaFile instances\n", - "local_de = MetaFile(\"../../data/de440s.bsp\")\n", - "jpl_moon_rotation = MetaFile(\"https://naif.jpl.nasa.gov/pub/naif/generic_kernels/pck/moon_pa_de440_200625.bpc\")\n", - "non_existing = MetaFile(\"https://google.com/non/existing/pck08.pca\")\n", - "# Add them to the meta almanac\n", - "new_meta.files = [local_de, jpl_moon_rotation, non_existing]\n", - "# And print what this configuration would be:\n", - "new_meta.dumps()" - ] - }, - { - "cell_type": "markdown", - "id": "a8e243d7-1be5-401d-a732-46273e23327e", - "metadata": {}, - "source": [ - "Note that the MetaAlmanac will raise an error in case it cannot download the files. Let's try to load the configuration we just specified." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "b2f5c44c-473c-4ed3-8d92-18ac3eaa8dca", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\u001b[0;31mSignature:\u001b[0m \u001b[0mnon_existing\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mprocess\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mDocstring:\u001b[0m\n", - "Processes this MetaFile by downloading it if it's a URL.\n", - "\n", - "This function modified `self` and changes the URI to be the path to the downloaded file.\n", - "\u001b[0;31mType:\u001b[0m builtin_function_or_method" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# You can call a specific file's `process` method to handle this specific file\n", - "non_existing.process?" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "7d58dc71-0a96-477d-9a41-c7e201704042", - "metadata": {}, - "outputs": [], - "source": [ - "# This does nothing because it's a local file\n", - "local_de.process()" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "83437378-9b44-4fcd-a6e8-e774bf098534", - "metadata": {}, - "outputs": [ - { - "ename": "Exception", - "evalue": "fetching https://google.com/non/existing/pck08.pca returned 404 Not Found", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mException\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[13], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;66;03m# Trying to download the non existing file will throw an exception\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m \u001b[43mnon_existing\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mprocess\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[0;31mException\u001b[0m: fetching https://google.com/non/existing/pck08.pca returned 404 Not Found" - ] - } - ], - "source": [ - "# Trying to download the non existing file will throw an exception\n", - "non_existing.process()" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "e2c35dc9-4efe-4348-ac81-cb7384cf8faf", - "metadata": {}, - "outputs": [ - { - "ename": "Exception", - "evalue": "processing file #2 (MetaFile { uri: \"https://google.com/non/existing/pck08.pca\", crc32: None }) caused an error: fetching https://google.com/non/existing/pck08.pca returned 404 Not Found", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mException\u001b[0m Traceback (most recent call last)", - "Cell \u001b[0;32mIn[14], line 2\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;66;03m# Trying to process the full meta almanac with an erroneous meta file will also throw an exception.\u001b[39;00m\n\u001b[0;32m----> 2\u001b[0m \u001b[43mnew_meta\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mprocess\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[0;31mException\u001b[0m: processing file #2 (MetaFile { uri: \"https://google.com/non/existing/pck08.pca\", crc32: None }) caused an error: fetching https://google.com/non/existing/pck08.pca returned 404 Not Found" - ] - } - ], - "source": [ - "# Trying to process the full meta almanac with an erroneous meta file will also throw an exception.\n", - "new_meta.process()" - ] - }, - { - "cell_type": "markdown", - "id": "bbd9e752-a045-4a50-bf65-0fe455b84f0a", - "metadata": {}, - "source": [ - "## Directly loading a MetaFile\n", - "\n", - "Say you have an `Almanac` loaded with some of your files. And then you'd like to load one file that is stored remotely. You may do so directly with the `load_from_metafile` method of the Almanac.\n", - "\n", - "**Note:** an Almanac may only load _one_ planetary constants kernel and _one_ Euler parameters kernel. As such, if your Almanac already includes one of these, loading another one will _replace it_. Refer to tutorial #05 on how to build your own PCA and EPA files that include everything you need." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "577dde0e-9dd5-41b5-9263-0f97fdb78628", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Almanac: #SPK = 1\t#BPC = 0\tPlanetaryData with 54 ID mappings and 0 name mappings\n", - "Almanac: #SPK = 1\t#BPC = 0\tPlanetaryData with 54 ID mappings and 0 name mappings\tEulerParameterData with 3 ID mappings and 3 name mappings\n" - ] - } - ], - "source": [ - "from anise import Almanac\n", - "only_de440s = Almanac(\"../../data/de440s.bsp\").load(\"../../data/pck11.pca\")\n", - "print(only_de440s)\n", - "# Now load a PCA from the Nyx Space cloud\n", - "de440s_and_moon = only_de440s.load_from_metafile(MetaFile(\"http://public-data.nyxspace.com/anise/v0.4/moon_fk.epa\", 2133296540))\n", - "print(de440s_and_moon)" - ] - }, - { - "cell_type": "markdown", - "id": "22637388-6a44-42cb-8722-38bf0d570a82", - "metadata": {}, - "source": [ - "To confirm that we've loaded the Moon FK file, let's grab the frame info from the Moon ME and Moon PA frames." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "3c0466bd-fca6-49e8-9aca-33032bbd4306", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Moon MOON_ME (μ = 4902.800066163796 km^3/s^2, radius = 1737.4 km)\n", - "Moon MOON_PA (μ = 4902.800066163796 km^3/s^2, radius = 1737.4 km)\n" - ] - } - ], - "source": [ - "from anise.astro.constants import Frames\n", - "print(de440s_and_moon.frame_info(Frames.MOON_ME_FRAME))\n", - "print(de440s_and_moon.frame_info(Frames.MOON_PA_FRAME))" - ] - }, - { - "cell_type": "markdown", - "id": "a8f2d315-6956-4236-bbf9-67b216ea16bf", - "metadata": {}, - "source": [ - "## Exercises\n", - "\n", - "### Learning Goals:\n", - "\n", - "1. Understand the structure and syntax of Dhall configuration files.\n", - "1. Learn how to use the MetaFile tool for easier configuration creation.\n", - "1. Gain insight into how different configurations affect the MetaAlmanac's operation.\n", - "\n", - "\n", - "### 1. Manual Dhall Configuration:\n", - "\n", - "+ Create a simple Dhall configuration file.\n", - "+ Include basic elements like a specific ephemerides file and a custom planetary constant file.\n", - "+ Load this configuration into the MetaAlmanac, observe the behavior, and query the loaded Almanac itself.\n", - "\n", - "### 2. Using MetaFile:\n", - "\n", - "+ Use the `MetaFile` class to generate the same configuration.\n", - "+ Compare the process of using MetaFile with manual Dhall file creation.\n", - "+ Load the generated configuration into the MetaAlmanac.\n", - "\n", - "_Note:_ Almanac, MetaFile, and MetaAlmana all support the equality operation in Python.\n" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": ".venv" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.4" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} \ No newline at end of file diff --git a/docs/anise/tutorials/Tutorial 02 - Loading remote SPICE and ANISE files (meta almanac).md b/docs/anise/tutorials/Tutorial 02 - Loading remote SPICE and ANISE files (meta almanac).md new file mode 100644 index 0000000..ad590b9 --- /dev/null +++ b/docs/anise/tutorials/Tutorial 02 - Loading remote SPICE and ANISE files (meta almanac).md @@ -0,0 +1,341 @@ +# ANISE + +ANISE is a modern rewrite of NAIF SPICE, written in Rust and providing interfaces to other languages include Python. + +Evidently, this tutorial applies to the Python usage of ANISE. + +## Goal +By the end of this tutorial, you should be able to know how to load local and remotes files using the `MetaAlmanac` structure, and know how to save and reload that meta configuration. + +Let's start by installing ANISE: `pip install anise` + +## Introduction + +SPICE files, such as development ephemerides, are often substantial in size. Typically, they are stored on shared resources, with links circulated among teams who require access. This process can be cumbersome for end-users, who must ensure they are using the correct file and that it loads properly, irrespective of the script’s execution path. + +The `MetaAlmanac` addresses this challenge by facilitating the initialization of an Almanac using both local and remote files. Remote files are automatically downloaded to the user's application data cache folder (`AppData` on Windows, `~/.local/share/cache` on Linux). For these files, the MetaAlmanac verifies the integrity of any local copy by comparing its CRC32 checksum with that of the remote file. + +Furthermore, the MetaAlmanac guarantees the use of the most up-to-date versions of these files. An example is the daily Earth Orientation Parameters published by JPL, termed the "high precision Earth rotation" kernel. The MetaAlmanac enables users to seamlessly access the latest version of these files. + + +```python +from anise import MetaAlmanac + +MetaAlmanac? +``` + + + Init signature: MetaAlmanac(maybe_path=None) + Docstring: + A structure to set up an Almanac, with automatic downloading, local storage, checksum checking, and more. + + # Behavior + If the URI is a local path, relative or absolute, nothing will be fetched from a remote. Relative paths are relative to the execution folder (i.e. the current working directory). + If the URI is a remote path, the MetaAlmanac will first check if the file exists locally. If it exists, it will check that the CRC32 checksum of this file matches that of the specs. + If it does not match, the file will be downloaded again. If no CRC32 is provided but the file exists, then the MetaAlmanac will fetch the remote file and overwrite the existing file. + The downloaded path will be stored in the "AppData" folder. + File: ~/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages/anise/__init__.py + Type: type + Subclasses: + + +## MetaAlmanac configuration + +As seen in the previous cell, a MetaAlmanac is typically initialized using a path to a configuration file (the `maybe_path` argument). + +The configuration file is written in [`Dhall`](https://dhall-lang.org/), an exceptional configuration language known for its safety, expressiveness, and maintainability. Dhall's design simplifies the process of configuring complex systems, making it a standout choice for tasks like initializing the MetaAlmanac. + +Let's see what this looks like: + + +```python +with open("../../data/latest.dhall") as f: + for line in f.readlines(): + print(line.strip()) +``` + + -- Latest planetary ephemerides, planetary constants, high precision Moon rotation, and daily Earth orientation parameter + { files = + [ { crc32 = Some 1921414410 + , uri = "http://public-data.nyxspace.com/anise/de440s.bsp" + } + , { crc32 = Some 2899443223 + , uri = "http://public-data.nyxspace.com/anise/v0.4/pck11.pca" + } + , { crc32 = Some 2133296540 + , uri = "http://public-data.nyxspace.com/anise/v0.4/moon_fk.epa" + } + , { crc32 = Some 1817759242 + , uri = "http://public-data.nyxspace.com/anise/moon_pa_de440_200625.bpc" + } + , { crc32 = None Natural + , uri = + "https://naif.jpl.nasa.gov/pub/naif/generic_kernels/pck/earth_latest_high_prec.bpc" + } + ] + } + + +This is the default MetaAlmanac: it grabs the development ephemerides `DE440s` from the public cloud of Nyx Space, grab a copy of the high fidelity Moon Principal Axes frame, grabs the planetary constant ANISE file `pck08.pca`. It also downloads the latest high precision Earth rotation parameters from JPL. + +The MetaAlmanac comes with a shortcut to download the latest info above without needing a copy of the Dhall file. + + +```python +almanac = MetaAlmanac.latest() +almanac.describe(bpc=True) +``` + + === BPC #0 === + ┌─────────────────────────┬───────────────────────────────────┬───────────────────────────────────┬───────────────────────────┬────────────────────┬───────┬────────────────┐ + │ Name │ Start epoch │ End epoch │ Duration │ Interpolation kind │ Frame │ Inertial frame │ + ├─────────────────────────┼───────────────────────────────────┼────────────────────────────��──────┼───────────────────────────┼────────────────────┼───────┼────────────────┤ + │ Earth PCK, ITRF93 Frame │ 2000-01-01T00:01:04.183912847 TDB │ 2002-09-26T21:18:50.632952778 TDB │ 999 days 21 h 17 min 46 s │ Chebyshev Triplet │ 3000 │ 17 │ + ├─────────────────────────┼───────────────────────────────────┼───────────────────────────────────┼───────────────────────────┼────────────────────┼───────┼────────────────┤ + │ Earth PCK, ITRF93 Frame │ 2002-09-26T21:18:50.632952778 TDB │ 2005-06-22T18:36:37.081996238 TDB │ 999 days 21 h 17 min 46 s │ Chebyshev Triplet │ 3000 │ 17 │ + ├─────────────────────────┼───────────────────────────────────┼───────────────────────────────────┼───────────────────────────┼────────────────────┼───────┼────────────────┤ + │ Earth PCK, ITRF93 Frame │ 2005-06-22T18:36:37.081996238 TDB │ 2008-03-18T15:54:23.531035669 TDB │ 999 days 21 h 17 min 46 s │ Chebyshev Triplet │ 3000 │ 17 │ + ├─────────────────────────┼─────────────────────���─────────────┼───────────────────────────────────┼───────────────────────────┼────────────────────┼───────┼────────────────┤ + │ Earth PCK, ITRF93 Frame │ 2008-03-18T15:54:23.531035669 TDB │ 2010-12-13T13:12:09.980072814 TDB │ 999 days 21 h 17 min 46 s │ Chebyshev Triplet │ 3000 │ 17 │ + ├─────────────────────────┼───────────────────────────────────┼───────────────────────────────────┼───────────────────────────┼────────────────────┼───────┼────────────────┤ + │ Earth PCK, ITRF93 Frame │ 2010-12-13T13:12:09.980072814 TDB │ 2013-09-08T10:29:56.429117874 TDB │ 999 days 21 h 17 min 46 s │ Chebyshev Triplet │ 3000 │ 17 │ + ├─────────────────────────┼───────────────────────────────────┼───────────────────────────────────┼───────────────────────────┼────────────────────┼───────┼────────────────┤ + │ Earth PCK, ITRF93 Frame │ 2013-09-08T10:29:56.429117874 TDB │ 2016-06-04T07:47:42.878162558 TDB │ 999 days 21 h 17 min 46 s │ Chebyshev Triplet │ 3000 │ 17 │ + ├─────────────────────────┼───────────────────────────────────┼───────────────────────────────────┼───────────────────────────┼────────────────────┼───────┼────────────────┤ + │ Earth PCK, ITRF93 Frame │ 2016-06-04T07:47:42.878162558 TDB │ 2019-03-01T05:05:29.327196302 TDB │ 999 days 21 h 17 min 46 s │ Chebyshev Triplet │ 3000 │ 17 │ + ├─────────────────────────┼───────────────────────────────────┼───────────────────────────────────┼──��────────────────────────┼────────────────────┼───────┼────────────────┤ + │ Earth PCK, ITRF93 Frame │ 2019-03-01T05:05:29.327196302 TDB │ 2021-11-25T02:23:15.776233885 TDB │ 999 days 21 h 17 min 46 s │ Chebyshev Triplet │ 3000 │ 17 │ + ├─────────────────────────┼───────────────────────────────────┼───────────────────────────────────┼───────────────────────────┼────────────────────┼───────┼────────────────┤ + │ Earth PCK, ITRF93 Frame │ 2021-11-25T02:23:15.776233885 TDB │ 2024-04-19T00:01:09.185602312 TDB │ 875 days 21 h 37 min 53 s │ Chebyshev Triplet │ 3000 │ 17 │ + └─────────────────────────┴───────────────────────────────────┴───────────────────────────────────┴───────────────────────────┴────────────────────┴───────┴────────────────┘ + === BPC #1 === + ┌───────────┬───────────────────────────────────┬───────────────────────────────────┬─────────────┬─────────────────��──┬───────┬────────────────┐ + │ Name │ Start epoch │ End epoch │ Duration │ Interpolation kind │ Frame │ Inertial frame │ + ├───────────┼───────────────────────────────────┼───────────────────────────────────┼─────────────┼────────────────────┼───────┼────────────────┤ + │ de440.nio │ 1449-12-26T23:59:59.999860483 TDB │ 2426-02-15T23:59:59.999891009 TDB │ 320000 days │ Chebyshev Triplet │ 31008 │ 1 │ + ├───────────┼───────────────────────────────────┼──���────────────────────────────────┼─────────────┼────────────────────┼───────┼────────────────┤ + │ de440.nio │ 2426-02-15T23:59:59.999891009 TDB │ 2650-01-24T23:59:59.999798018 TDB │ 81792 days │ Chebyshev Triplet │ 31008 │ 1 │ + └───────────┴───────────────────────────────────┴───────────────────────────────────┴─────────────┴────────────────────┴───────┴────────────────┘ + + +The data downloaded from Nyx Space cloud has a checksum in the configuration file: that's because we know exactly what this data should be. Hence, if the data is modified in your local copy, the MetaAlmanac will download it again and replace your local copy. However, the JPL data changes daily, so we don't store a checksum in the config file, ensuring that the latest data is always downloaded. + +ANISE also provides a local config file to use the data stored in a copy of the repo. + + +```python +with open("../../data/local.dhall") as f: + for line in f.readlines(): + print(line.strip()) +``` + + -- Default Almanac + { files = + [ { crc32 = None Natural, uri = "../../data/de440s.bsp" } + , { crc32 = None Natural, uri = "../../data/pck08.pca" } + ] + } + + +The CRC32 integrity number is not set for local paths because in any case, the MetaAlmanac does not know where to fetch another version where the checksum should match. + +## Using the MetaAlmanac + +The MetaAlmanac is designed to work seamlessly with the Alamac itself. In the following example, we'll use the latest MetaAlmanac (the same that's in `latest.dhall`) and see that it can be used to return the Almanac directly. + + +```python +meta = MetaAlmanac.load("../../data/latest.dhall") +print(meta) +``` + + MetaAlmanac { files: [MetaFile { uri: "http://public-data.nyxspace.com/anise/de440s.bsp", crc32: Some(1921414410) }, MetaFile { uri: "http://public-data.nyxspace.com/anise/v0.4/pck11.pca", crc32: Some(2899443223) }, MetaFile { uri: "http://public-data.nyxspace.com/anise/v0.4/moon_fk.epa", crc32: Some(2133296540) }, MetaFile { uri: "http://public-data.nyxspace.com/anise/moon_pa_de440_200625.bpc", crc32: Some(1817759242) }, MetaFile { uri: "https://naif.jpl.nasa.gov/pub/naif/generic_kernels/pck/earth_latest_high_prec.bpc", crc32: None }] } + + + +```python +almanac = meta.process() +print(almanac) +``` + + Almanac: #SPK = 1 #BPC = 2 PlanetaryData with 54 ID mappings and 0 name mappings EulerParameterData with 3 ID mappings and 3 name mappings + + +Trivial! We now see that the loaded Almanac has one loaded SPK file (the de440s file), one BPC file (the latest Earth high precision rotation), and 49 planetary data mappings, loaded from the `pck08.pca`. + +Even simpler, you can just call the `latest()` class method which will call the `latest.dhall` equivalent, without requiring a local configuration file. + + +```python +print(MetaAlmanac.latest()) +``` + + Almanac: #SPK = 1 #BPC = 2 PlanetaryData with 54 ID mappings and 0 name mappings EulerParameterData with 3 ID mappings and 3 name mappings + + +## Building a MetaAlmanac config + +Building a Dhall configuration for ANISE can be approached in two ways. The most direct method is to craft a Dhall file manually. However, given the complexity often associated with writing in Dhall, ANISE offers a more user-friendly alternative through the `MetaFile` class. This option simplifies the process of creating the necessary data, catering to users who may find direct Dhall scripting challenging. + + +```python +from anise import MetaFile +MetaFile? +``` + + + Init signature: MetaFile(uri, crc32=None) + File: ~/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages/anise/__init__.py + Type: type + Subclasses: + + + +```python +# Let's initialize a new empty MetaAlmanac. +new_meta = MetaAlmanac() +new_meta.process() # Note that you can always initialize an empty MetaAlmanac because you can initialize an empty Almanac +``` + + + + + Almanac: #SPK = 0 #BPC = 0 (@0x5582525a9d10) + + + + +```python +# Create MetaFile instances +local_de = MetaFile("../../data/de440s.bsp") +jpl_moon_rotation = MetaFile("https://naif.jpl.nasa.gov/pub/naif/generic_kernels/pck/moon_pa_de440_200625.bpc") +non_existing = MetaFile("https://google.com/non/existing/pck08.pca") +# Add them to the meta almanac +new_meta.files = [local_de, jpl_moon_rotation, non_existing] +# And print what this configuration would be: +new_meta.dumps() +``` + + + + + '{ files = [{ crc32 = None Natural, uri = "../../data/de440s.bsp" }, { crc32 = None Natural, uri = "https://naif.jpl.nasa.gov/pub/naif/generic_kernels/pck/moon_pa_de440_200625.bpc" }, { crc32 = None Natural, uri = "https://google.com/non/existing/pck08.pca" }] }' + + + +Note that the MetaAlmanac will raise an error in case it cannot download the files. Let's try to load the configuration we just specified. + + +```python +# You can call a specific file's `process` method to handle this specific file +non_existing.process? +``` + + + Signature: non_existing.process() + Docstring: + Processes this MetaFile by downloading it if it's a URL. + + This function modified `self` and changes the URI to be the path to the downloaded file. + Type: builtin_function_or_method + + + +```python +# This does nothing because it's a local file +local_de.process() +``` + + +```python +# Trying to download the non existing file will throw an exception +non_existing.process() +``` + + + --------------------------------------------------------------------------- + + Exception Traceback (most recent call last) + + Cell In[13], line 2 + 1 # Trying to download the non existing file will throw an exception + ----> 2 non_existing.process() + + + Exception: fetching https://google.com/non/existing/pck08.pca returned 404 Not Found + + + +```python +# Trying to process the full meta almanac with an erroneous meta file will also throw an exception. +new_meta.process() +``` + + + --------------------------------------------------------------------------- + + Exception Traceback (most recent call last) + + Cell In[14], line 2 + 1 # Trying to process the full meta almanac with an erroneous meta file will also throw an exception. + ----> 2 new_meta.process() + + + Exception: processing file #2 (MetaFile { uri: "https://google.com/non/existing/pck08.pca", crc32: None }) caused an error: fetching https://google.com/non/existing/pck08.pca returned 404 Not Found + + +## Directly loading a MetaFile + +Say you have an `Almanac` loaded with some of your files. And then you'd like to load one file that is stored remotely. You may do so directly with the `load_from_metafile` method of the Almanac. + +**Note:** an Almanac may only load _one_ planetary constants kernel and _one_ Euler parameters kernel. As such, if your Almanac already includes one of these, loading another one will _replace it_. Refer to tutorial #05 on how to build your own PCA and EPA files that include everything you need. + + +```python +from anise import Almanac +only_de440s = Almanac("../../data/de440s.bsp").load("../../data/pck11.pca") +print(only_de440s) +# Now load a PCA from the Nyx Space cloud +de440s_and_moon = only_de440s.load_from_metafile(MetaFile("http://public-data.nyxspace.com/anise/v0.4/moon_fk.epa", 2133296540)) +print(de440s_and_moon) +``` + + Almanac: #SPK = 1 #BPC = 0 PlanetaryData with 54 ID mappings and 0 name mappings + Almanac: #SPK = 1 #BPC = 0 PlanetaryData with 54 ID mappings and 0 name mappings EulerParameterData with 3 ID mappings and 3 name mappings + + +To confirm that we've loaded the Moon FK file, let's grab the frame info from the Moon ME and Moon PA frames. + + +```python +from anise.astro.constants import Frames +print(de440s_and_moon.frame_info(Frames.MOON_ME_FRAME)) +print(de440s_and_moon.frame_info(Frames.MOON_PA_FRAME)) +``` + + Moon MOON_ME (μ = 4902.800066163796 km^3/s^2, radius = 1737.4 km) + Moon MOON_PA (μ = 4902.800066163796 km^3/s^2, radius = 1737.4 km) + + +## Exercises + +### Learning Goals: + +1. Understand the structure and syntax of Dhall configuration files. +1. Learn how to use the MetaFile tool for easier configuration creation. +1. Gain insight into how different configurations affect the MetaAlmanac's operation. + + +### 1. Manual Dhall Configuration: + ++ Create a simple Dhall configuration file. ++ Include basic elements like a specific ephemerides file and a custom planetary constant file. ++ Load this configuration into the MetaAlmanac, observe the behavior, and query the loaded Almanac itself. + +### 2. Using MetaFile: + ++ Use the `MetaFile` class to generate the same configuration. ++ Compare the process of using MetaFile with manual Dhall file creation. ++ Load the generated configuration into the MetaAlmanac. + +_Note:_ Almanac, MetaFile, and MetaAlmana all support the equality operation in Python. diff --git a/docs/anise/tutorials/Tutorial 03 - Defining and working with the Orbit structure.ipynb b/docs/anise/tutorials/Tutorial 03 - Defining and working with the Orbit structure.ipynb deleted file mode 100644 index b65a395..0000000 --- a/docs/anise/tutorials/Tutorial 03 - Defining and working with the Orbit structure.ipynb +++ /dev/null @@ -1,495 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "e27f64b8-2625-40a7-a6b9-c4b9a9281530", - "metadata": {}, - "source": [ - "# ANISE\n", - "\n", - "ANISE is a modern rewrite of NAIF SPICE, written in Rust and providing interfaces to other languages include Python.\n", - "\n", - "Evidently, this tutorial applies to the Python usage of ANISE.\n", - "\n", - "## Goal\n", - "By the end of this tutorial, you should know how to initialize an `Orbit` structure, how to retrieve any of the available orbital elements, and compute the future state of that orbit in the future assuming two body propagation (i.e. _no perturbation of any kind_).\n", - "\n", - "For comprehensive and high-fidelity astrodynamics, orbit estimation, and other spaceflight dynamics functionalities essential in real-world missions, consider using [Nyx](https://github.com/nyx-space/nyx/). Nyx has been the choice for several cislunar missions and lunar landers, and, like ANISE, is also open-source and free to use.\n", - "\n", - "Let's start by installing ANISE: `pip install anise`\n", - "\n", - "## Load the latest Almanac\n", - "\n", - "As seen the tutorial #2, we'll just download the latest data using the MetaAlmanac." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "1787d97b-d706-4a29-8666-4addf8a4d95d", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Almanac: #SPK = 1\t#BPC = 2\tPlanetaryData with 49 ID mappings and 0 name mappings (@0x5587ec685530)" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from anise import MetaAlmanac\n", - "\n", - "almanac = MetaAlmanac(\"../../data/latest.dhall\").process()\n", - "\n", - "almanac" - ] - }, - { - "cell_type": "markdown", - "id": "3d95d9a6-c231-4326-b5b7-c7a19a5ff617", - "metadata": {}, - "source": [ - "## Orbit structure\n", - "\n", - "The `Orbit` structure is an alias for `CartesianState`, which is only available under that name in Rust. In fact, `Orbit` will _always_ store the information in its Cartesian form because it's a non-singular representation of an orbit, regardless of whether it's elliptical, hyperbolic, equatorial and circular, etc.\n", - "\n", - "An orbit is defined by its position, velocity, an epoch, and a frame.\n", - "\n", - "As we saw in tutorial #1, we can import the frames as they're defined with their ephemeris and orientation IDs. However, recall that ANISE does not provide _any_ default values for the gravitational parameter or shape of the geoid in these frames and they need to be loaded from the Almanac." - ] - }, - { - "cell_type": "markdown", - "id": "6f4de9b9-c2fb-4774-837d-e70da5afc84a", - "metadata": {}, - "source": [ - "Let's grab the frame information from the almanac." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "cbe97591-9aec-48cb-b63f-1f608b10b405", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Earth J2000\n", - "Earth J2000 (μ = 398600.435436096 km^3/s^2, eq. radius = 6378.14 km, polar radius = 6356.75 km, f = 0.0033536422844278)\n" - ] - } - ], - "source": [ - "from anise.astro.constants import Frames\n", - "eme2k = almanac.frame_info(Frames.EME2000)\n", - "print(Frames.EME2000)\n", - "# Only the loaded frame has the gravitational parameter and shape information\n", - "print(eme2k)" - ] - }, - { - "cell_type": "markdown", - "id": "c20bfa85-da31-4dda-a9b1-881bf5c0186c", - "metadata": {}, - "source": [ - "Orbits can be initialized in several ways. The simplest is to initialize it from its cartesian data, provided in kilometers and kilometers per second." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "3c457886-95ed-40e4-8b49-9ca8e08b1e7f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\u001b[0;31mInit signature:\u001b[0m \u001b[0mOrbit\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx_km\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0my_km\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mz_km\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvx_km_s\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvy_km_s\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvz_km_s\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mepoch\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mframe\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mDocstring:\u001b[0m \n", - "Defines a Cartesian state in a given frame at a given epoch in a given time scale. Radius data is expressed in kilometers. Velocity data is expressed in kilometers per second.\n", - "Regardless of the constructor used, this struct stores all the state information in Cartesian coordinates as these are always non singular.\n", - "\n", - "Unless noted otherwise, algorithms are from GMAT 2016a [StateConversionUtil.cpp](https://github.com/ChristopherRabotin/GMAT/blob/37201a6290e7f7b941bc98ee973a527a5857104b/src/base/util/StateConversionUtil.cpp).\n", - "\u001b[0;31mType:\u001b[0m type\n", - "\u001b[0;31mSubclasses:\u001b[0m " - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from anise.astro import Orbit\n", - "Orbit?" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "b612912a-9f93-472a-957a-ecd4d0e70bb9", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[Earth J2000] 2010-12-21T02:01:57.816398952 UTC\tposition = [5946.673548, 1656.154606, 2259.012130] km\tvelocity = [-3.098683, 4.579534, 6.246542] km/s\n" - ] - } - ], - "source": [ - "from anise.time import Epoch\n", - "\n", - "epoch = Epoch(\"2010-12-21 02:03:04 TDB\") # Let's create an epoch in the TDB time system\n", - "\n", - "# And initialize the orbit from an arbitrary position and velocity.\n", - "\n", - "from_cartesian = Orbit.from_cartesian(\n", - " 5_946.673548288958,\n", - " 1_656.154606023661,\n", - " 2_259.012129598249,\n", - " -3.098683050943824,\n", - " 4.579534132135011,\n", - " 6.246541551539432,\n", - " epoch,\n", - " eme2k\n", - " )\n", - "print(from_cartesian)" - ] - }, - { - "cell_type": "markdown", - "id": "5431a376-8e85-4573-9b1d-9514ee24c78e", - "metadata": {}, - "source": [ - "Oftentimes, we know the scientific mission of a spacecraft, so we initialize it from its Keplerian parameters and not its Cartesian state.\n", - "\n", - "All of the orbit initializers start with `from_...`, making it easy to spot if you have an autocompletion enabled (you should!)." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "fc94db3b-9ce3-4113-a96c-5e3c4318bdfb", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\u001b[0;31mSignature:\u001b[0m \u001b[0mOrbit\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfrom_keplerian\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0msma\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mecc\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0minc\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mraan\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0maop\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mta\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mepoch\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mframe\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mDocstring:\u001b[0m\n", - "Creates a new Orbit around the provided Celestial or Geoid frame from the Keplerian orbital elements.\n", - "\n", - "**Units:** km, none, degrees, degrees, degrees, degrees\n", - "\n", - "NOTE: The state is defined in Cartesian coordinates as they are non-singular. This causes rounding\n", - "errors when creating a state from its Keplerian orbital elements (cf. the state tests).\n", - "One should expect these errors to be on the order of 1e-12.\n", - "\u001b[0;31mType:\u001b[0m builtin_function_or_method" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "Orbit.from_keplerian?" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "4542fa57-7473-4f3f-ba67-d381a94f7c7e", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "[Earth J2000] 2010-12-21T02:01:57.816398952 UTC\tposition = [5946.673549, 1656.154605, 2259.012130] km\tvelocity = [-3.098683, 4.579534, 6.246542] km/s\n" - ] - } - ], - "source": [ - "from_keplerian = Orbit.from_keplerian(\n", - " 7_712.186_117_895_041,\n", - " 0.158_999_999_999_999_95,\n", - " 53.75369,\n", - " 1.998_632_864_211_17e-5,\n", - " 359.787_880_000_004,\n", - " 25.434_003_407_751_188,\n", - " epoch,\n", - " eme2k\n", - ")\n", - "print(from_keplerian)" - ] - }, - { - "cell_type": "markdown", - "id": "ef0d35eb-d1a3-4a4b-b35f-eebd4919ae7f", - "metadata": {}, - "source": [ - "### Exercise #1\n", - "\n", - "We won't go through all of the initializers, so use your autocompletion to try the following ones out:\n", - "\n", - "+ `from_keplerian_mean_anomaly`\n", - "+ `from_keplerian_apsis_radii`\n", - "+ `from_keplerian_altitude`\n", - "+ `from_latlongalt`\n", - "\n", - "**Objective:** Create and initialize an Orbit structure with given orbital elements.\n", - "\n", - "**Tasks:**\n", - " 1. Input specific orbital elements to define an orbit, including hyperbolic orbits (eccentricity greater than 1)\n", - " 2. Display the initialized orbit’s parameters and notice how initializing from the Keplerian parameters won't return _exactly_ the same Keplerian data, as it's converted back and forth from its Keplerian representation to its Cartesian representation.\n", - "\n", - "**Learning Goal:** Understand how to define an orbit in ANISE and interpret its parameters." - ] - }, - { - "cell_type": "markdown", - "id": "6f0bbad6-ea5c-464f-8907-22acbf569d74", - "metadata": {}, - "source": [ - "## Fields of the orbit class\n", - "\n", - "The orbit class provides direct access to a large range of orbital parameters, including the semi major axis, the true longitude, the orbital period, and so on. All of these accessors include the parameter along with the unit in which it is returned. Let's look at a few more common ones." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "202b81e4-9a6e-45a6-a4f7-aa99c90cd2c1", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "(1334.0462758738768,\n", - " 25.221883407752955,\n", - " -25.842246360350657,\n", - " 107.80853340089652)" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from_cartesian.sma_altitude_km(), from_cartesian.aol_deg(), from_cartesian.energy_km2_s2(), from_cartesian.periapsis_altitude_km()" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "1377c6ce-ad67-4a60-9f2a-80b2d50b717d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "1 h 52 min 20 s 269 ms 114 μs 912 ns\n", - "1 h 52 min 20 s\n", - "6740.269114912\n", - "21 ms 926 μs 330 ns\n" - ] - } - ], - "source": [ - "# The orbital period (and the light time) is returned as a Hifitime Duration, enabling trivial computations\n", - "from anise.time import Unit\n", - "\n", - "print(from_keplerian.period())\n", - "print(from_keplerian.period().round(Unit.Second*1))\n", - "print(from_keplerian.period().to_seconds())\n", - "\n", - "print(from_keplerian.light_time())" - ] - }, - { - "cell_type": "markdown", - "id": "da8c8062-47f8-45d1-8b7e-3a112f7f0ee7", - "metadata": {}, - "source": [ - "**Importantly**, all of the orbital parameter computations require the frame data to be initialized, otherwise an exception will be raised specifying what data was not available. In almost all cases, this happens because you use the constant frame definition without fetching the frame information from the Almanac, or your Almanac does not include any planetary constants.\n", - "\n", - "The Orbit structure supports the equality operation, and it'll check that the epoch, frame, and Cartesian state match to within the centimeter level in position and centimeter per second level in velocity.\n", - "\n", - "If that precision is different than the one you wish, you can use the `eq_within` function providing a tolerance in kilometers and kilometers per second." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "c915cdc7-1932-438b-87a9-c7c66591822d", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from_keplerian == from_cartesian" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "ca0931d5-4df0-46e7-97f3-29d5f180d2a2", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\u001b[0;31mSignature:\u001b[0m \u001b[0mfrom_keplerian\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0meq_within\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mother\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mradial_tol_km\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mvelocity_tol_km_s\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mDocstring:\u001b[0m Returns whether this orbit and another are equal within the specified radial and velocity absolute tolerances\n", - "\u001b[0;31mType:\u001b[0m builtin_function_or_method" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from_keplerian.eq_within?" - ] - }, - { - "cell_type": "markdown", - "id": "1546978b-d1cd-41bd-bab3-23bb8fe27f0e", - "metadata": {}, - "source": [ - "### Exercise #2\n", - "\n", - "**Objective:** Convert between different sets of orbital elements.\n", - "\n", - "**Tasks:**\n", - " 1. Using the `add_...` methods (e.g. `add_sma`), see how the Cartesian elements change (accessible as getters like `x_km`).\n", - " 2. Analyze how changes in one element affect others.\n", - "\n", - "**Learning Goal:** Gain proficiency in working with various orbital element formats." - ] - }, - { - "cell_type": "markdown", - "id": "536338b1-392a-4884-8c14-c62c14bbfdf4", - "metadata": {}, - "source": [ - "## Two body propagation\n", - "\n", - "### Important disclaimer\n", - "\n", - "Two body propagation is a _tool_ and should not be mistaken for a complete representation of real-world scenarios. In actual mission design, the exclusive use of two-body propagation is _physically incorrect and the wrong approach_. This is because a spacecraft or celestial body is invariably influenced by a multitude of factors. These include gravitational forces from other celestial bodies, as well as subtler forces like solar radiation pressure, atmospheric drag, and gravity fields (as, e.g., spherical harmonic modeling), which account for the uneven distribution of a celestial object's mass. **ANISE will only ever support two-body propagation. For high-fidelity astrodynamics, including orbit estimation, the recommended resource is Nyx. Nyx has been integral to several high-profile lunar missions, offering the necessary models for accurate space mission analysis, planning, and operations.**\n", - "\n", - "### Usage\n", - "\n", - "Using the two body propagator in ANISE is super straight-forward: simply call `at_epoch` on an `Orbit` instance. This uses the current mean anomaly of this instance to compute its future mean anomaly using only the time difference between the current epoch and the future epoch.\n", - "\n", - "Of course, this converts from the internal Cartesian representation to its Keplerian representation, and back again, so some level of error in the Keplerian orbital elements will be seen, but it should be minimal." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "98d91960-34cb-4856-986b-0bb9c0116581", - "metadata": {}, - "outputs": [], - "source": [ - "current_sma_km = from_keplerian.sma_km()\n", - "current_true_anomaly_deg = from_keplerian.ta_deg()\n", - "current_mean_anomaly_deg = from_keplerian.ma_deg()\n", - "future_state = from_keplerian.at_epoch(from_keplerian.epoch + Unit.Day*1.7565)\n", - "# Let's see how much these two Keplerian orbital elements have changed\n", - "future_sma_km = future_state.sma_km()\n", - "future_true_anomaly_deg = future_state.ta_deg()\n", - "future_mean_anomaly_deg = future_state.ma_deg()" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "0e5f6e9f-2349-4b11-86f2-66e0aa1f0349", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Error in SMA:\t9.094947017729282e-13 km\n", - "Propagated true anomaly:\t172.3281610410407 deg\n", - "Propagated mean anomaly:\t185.63718609798167 deg\n" - ] - } - ], - "source": [ - "print(f\"Error in SMA:\\t{abs(current_sma_km - future_sma_km)} km\")\n", - "print(f\"Propagated true anomaly:\\t{abs(current_true_anomaly_deg - future_true_anomaly_deg)} deg\")\n", - "print(f\"Propagated mean anomaly:\\t{abs(current_mean_anomaly_deg - future_mean_anomaly_deg)} deg\")" - ] - }, - { - "cell_type": "markdown", - "id": "9b25ddb5-1d23-4b8a-94f7-fd81f667b3f1", - "metadata": {}, - "source": [ - "### Exercise #3\n", - "\n", - "All Almanac translation/transform queries return an instance of `Orbit`, populated with the relevant frame information from the Almanac, so gaining familiarity with this structure is important.\n", - "\n", - "**Objective:** Simulate the orbit of a body over time using two-body propagation.\n", - "\n", - "**Tasks:**\n", - " 1. Fetch the state of a celestial object, like the Moon, at the epoch of your choice.\n", - " 3. Use the Orbit instance's `at_epoch` to propagate the position of that object forward in time, assuming it was subjected only to two body dynamics.\n", - " 4. Fetch the state of that same object from the Almanac at that other time, and look at the difference in Cartesian state and orbital elements.\n", - "\n", - "**Learning Goal:** Understand the basics of orbital propagation and its limitations." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "002aa86f-9ae2-484a-a6cb-441a9bd26b40", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": ".venv" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.4" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/anise/tutorials/Tutorial 03 - Defining and working with the Orbit structure.md b/docs/anise/tutorials/Tutorial 03 - Defining and working with the Orbit structure.md new file mode 100644 index 0000000..2d9f438 --- /dev/null +++ b/docs/anise/tutorials/Tutorial 03 - Defining and working with the Orbit structure.md @@ -0,0 +1,285 @@ +# ANISE + +ANISE is a modern rewrite of NAIF SPICE, written in Rust and providing interfaces to other languages include Python. + +Evidently, this tutorial applies to the Python usage of ANISE. + +## Goal +By the end of this tutorial, you should know how to initialize an `Orbit` structure, how to retrieve any of the available orbital elements, and compute the future state of that orbit in the future assuming two body propagation (i.e. _no perturbation of any kind_). + +For comprehensive and high-fidelity astrodynamics, orbit estimation, and other spaceflight dynamics functionalities essential in real-world missions, consider using [Nyx](https://github.com/nyx-space/nyx/). Nyx has been the choice for several cislunar missions and lunar landers, and, like ANISE, is also open-source and free to use. + +Let's start by installing ANISE: `pip install anise` + +## Load the latest Almanac + +As seen the tutorial #2, we'll just download the latest data using the MetaAlmanac. + + +```python +from anise import MetaAlmanac + +almanac = MetaAlmanac("../../data/latest.dhall").process() + +almanac +``` + + + + + Almanac: #SPK = 1 #BPC = 2 PlanetaryData with 49 ID mappings and 0 name mappings (@0x5587ec685530) + + + +## Orbit structure + +The `Orbit` structure is an alias for `CartesianState`, which is only available under that name in Rust. In fact, `Orbit` will _always_ store the information in its Cartesian form because it's a non-singular representation of an orbit, regardless of whether it's elliptical, hyperbolic, equatorial and circular, etc. + +An orbit is defined by its position, velocity, an epoch, and a frame. + +As we saw in tutorial #1, we can import the frames as they're defined with their ephemeris and orientation IDs. However, recall that ANISE does not provide _any_ default values for the gravitational parameter or shape of the geoid in these frames and they need to be loaded from the Almanac. + +Let's grab the frame information from the almanac. + + +```python +from anise.astro.constants import Frames +eme2k = almanac.frame_info(Frames.EME2000) +print(Frames.EME2000) +# Only the loaded frame has the gravitational parameter and shape information +print(eme2k) +``` + + Earth J2000 + Earth J2000 (μ = 398600.435436096 km^3/s^2, eq. radius = 6378.14 km, polar radius = 6356.75 km, f = 0.0033536422844278) + + +Orbits can be initialized in several ways. The simplest is to initialize it from its cartesian data, provided in kilometers and kilometers per second. + + +```python +from anise.astro import Orbit +Orbit? +``` + + + Init signature: Orbit(x_km, y_km, z_km, vx_km_s, vy_km_s, vz_km_s, epoch, frame) + Docstring: + Defines a Cartesian state in a given frame at a given epoch in a given time scale. Radius data is expressed in kilometers. Velocity data is expressed in kilometers per second. + Regardless of the constructor used, this struct stores all the state information in Cartesian coordinates as these are always non singular. + + Unless noted otherwise, algorithms are from GMAT 2016a [StateConversionUtil.cpp](https://github.com/ChristopherRabotin/GMAT/blob/37201a6290e7f7b941bc98ee973a527a5857104b/src/base/util/StateConversionUtil.cpp). + Type: type + Subclasses: + + + +```python +from anise.time import Epoch + +epoch = Epoch("2010-12-21 02:03:04 TDB") # Let's create an epoch in the TDB time system + +# And initialize the orbit from an arbitrary position and velocity. + +from_cartesian = Orbit.from_cartesian( + 5_946.673548288958, + 1_656.154606023661, + 2_259.012129598249, + -3.098683050943824, + 4.579534132135011, + 6.246541551539432, + epoch, + eme2k + ) +print(from_cartesian) +``` + + [Earth J2000] 2010-12-21T02:01:57.816398952 UTC position = [5946.673548, 1656.154606, 2259.012130] km velocity = [-3.098683, 4.579534, 6.246542] km/s + + +Oftentimes, we know the scientific mission of a spacecraft, so we initialize it from its Keplerian parameters and not its Cartesian state. + +All of the orbit initializers start with `from_...`, making it easy to spot if you have an autocompletion enabled (you should!). + + +```python +Orbit.from_keplerian? +``` + + + Signature: Orbit.from_keplerian(sma, ecc, inc, raan, aop, ta, epoch, frame) + Docstring: + Creates a new Orbit around the provided Celestial or Geoid frame from the Keplerian orbital elements. + + **Units:** km, none, degrees, degrees, degrees, degrees + + NOTE: The state is defined in Cartesian coordinates as they are non-singular. This causes rounding + errors when creating a state from its Keplerian orbital elements (cf. the state tests). + One should expect these errors to be on the order of 1e-12. + Type: builtin_function_or_method + + + +```python +from_keplerian = Orbit.from_keplerian( + 7_712.186_117_895_041, + 0.158_999_999_999_999_95, + 53.75369, + 1.998_632_864_211_17e-5, + 359.787_880_000_004, + 25.434_003_407_751_188, + epoch, + eme2k +) +print(from_keplerian) +``` + + [Earth J2000] 2010-12-21T02:01:57.816398952 UTC position = [5946.673549, 1656.154605, 2259.012130] km velocity = [-3.098683, 4.579534, 6.246542] km/s + + +### Exercise #1 + +We won't go through all of the initializers, so use your autocompletion to try the following ones out: + ++ `from_keplerian_mean_anomaly` ++ `from_keplerian_apsis_radii` ++ `from_keplerian_altitude` ++ `from_latlongalt` + +**Objective:** Create and initialize an Orbit structure with given orbital elements. + +**Tasks:** + 1. Input specific orbital elements to define an orbit, including hyperbolic orbits (eccentricity greater than 1) + 2. Display the initialized orbit’s parameters and notice how initializing from the Keplerian parameters won't return _exactly_ the same Keplerian data, as it's converted back and forth from its Keplerian representation to its Cartesian representation. + +**Learning Goal:** Understand how to define an orbit in ANISE and interpret its parameters. + +## Fields of the orbit class + +The orbit class provides direct access to a large range of orbital parameters, including the semi major axis, the true longitude, the orbital period, and so on. All of these accessors include the parameter along with the unit in which it is returned. Let's look at a few more common ones. + + +```python +from_cartesian.sma_altitude_km(), from_cartesian.aol_deg(), from_cartesian.energy_km2_s2(), from_cartesian.periapsis_altitude_km() +``` + + + + + (1334.0462758738768, + 25.221883407752955, + -25.842246360350657, + 107.80853340089652) + + + + +```python +# The orbital period (and the light time) is returned as a Hifitime Duration, enabling trivial computations +from anise.time import Unit + +print(from_keplerian.period()) +print(from_keplerian.period().round(Unit.Second*1)) +print(from_keplerian.period().to_seconds()) + +print(from_keplerian.light_time()) +``` + + 1 h 52 min 20 s 269 ms 114 μs 912 ns + 1 h 52 min 20 s + 6740.269114912 + 21 ms 926 μs 330 ns + + +**Importantly**, all of the orbital parameter computations require the frame data to be initialized, otherwise an exception will be raised specifying what data was not available. In almost all cases, this happens because you use the constant frame definition without fetching the frame information from the Almanac, or your Almanac does not include any planetary constants. + +The Orbit structure supports the equality operation, and it'll check that the epoch, frame, and Cartesian state match to within the centimeter level in position and centimeter per second level in velocity. + +If that precision is different than the one you wish, you can use the `eq_within` function providing a tolerance in kilometers and kilometers per second. + + +```python +from_keplerian == from_cartesian +``` + + + + + True + + + + +```python +from_keplerian.eq_within? +``` + + + Signature: from_keplerian.eq_within(other, radial_tol_km, velocity_tol_km_s) + Docstring: Returns whether this orbit and another are equal within the specified radial and velocity absolute tolerances + Type: builtin_function_or_method + + +### Exercise #2 + +**Objective:** Convert between different sets of orbital elements. + +**Tasks:** + 1. Using the `add_...` methods (e.g. `add_sma`), see how the Cartesian elements change (accessible as getters like `x_km`). + 2. Analyze how changes in one element affect others. + +**Learning Goal:** Gain proficiency in working with various orbital element formats. + +## Two body propagation + +### Important disclaimer + +Two body propagation is a _tool_ and should not be mistaken for a complete representation of real-world scenarios. In actual mission design, the exclusive use of two-body propagation is _physically incorrect and the wrong approach_. This is because a spacecraft or celestial body is invariably influenced by a multitude of factors. These include gravitational forces from other celestial bodies, as well as subtler forces like solar radiation pressure, atmospheric drag, and gravity fields (as, e.g., spherical harmonic modeling), which account for the uneven distribution of a celestial object's mass. **ANISE will only ever support two-body propagation. For high-fidelity astrodynamics, including orbit estimation, the recommended resource is Nyx. Nyx has been integral to several high-profile lunar missions, offering the necessary models for accurate space mission analysis, planning, and operations.** + +### Usage + +Using the two body propagator in ANISE is super straight-forward: simply call `at_epoch` on an `Orbit` instance. This uses the current mean anomaly of this instance to compute its future mean anomaly using only the time difference between the current epoch and the future epoch. + +Of course, this converts from the internal Cartesian representation to its Keplerian representation, and back again, so some level of error in the Keplerian orbital elements will be seen, but it should be minimal. + + +```python +current_sma_km = from_keplerian.sma_km() +current_true_anomaly_deg = from_keplerian.ta_deg() +current_mean_anomaly_deg = from_keplerian.ma_deg() +future_state = from_keplerian.at_epoch(from_keplerian.epoch + Unit.Day*1.7565) +# Let's see how much these two Keplerian orbital elements have changed +future_sma_km = future_state.sma_km() +future_true_anomaly_deg = future_state.ta_deg() +future_mean_anomaly_deg = future_state.ma_deg() +``` + + +```python +print(f"Error in SMA:\t{abs(current_sma_km - future_sma_km)} km") +print(f"Propagated true anomaly:\t{abs(current_true_anomaly_deg - future_true_anomaly_deg)} deg") +print(f"Propagated mean anomaly:\t{abs(current_mean_anomaly_deg - future_mean_anomaly_deg)} deg") +``` + + Error in SMA: 9.094947017729282e-13 km + Propagated true anomaly: 172.3281610410407 deg + Propagated mean anomaly: 185.63718609798167 deg + + +### Exercise #3 + +All Almanac translation/transform queries return an instance of `Orbit`, populated with the relevant frame information from the Almanac, so gaining familiarity with this structure is important. + +**Objective:** Simulate the orbit of a body over time using two-body propagation. + +**Tasks:** + 1. Fetch the state of a celestial object, like the Moon, at the epoch of your choice. + 3. Use the Orbit instance's `at_epoch` to propagate the position of that object forward in time, assuming it was subjected only to two body dynamics. + 4. Fetch the state of that same object from the Almanac at that other time, and look at the difference in Cartesian state and orbital elements. + +**Learning Goal:** Understand the basics of orbital propagation and its limitations. + + +```python + +``` diff --git a/docs/anise/tutorials/Tutorial 04 - Computing Azimuth Elevation and Range data.ipynb b/docs/anise/tutorials/Tutorial 04 - Computing Azimuth Elevation and Range data.ipynb deleted file mode 100644 index b654f85..0000000 --- a/docs/anise/tutorials/Tutorial 04 - Computing Azimuth Elevation and Range data.ipynb +++ /dev/null @@ -1,1550 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "f20d04c8-b67e-49f6-8236-df75f52049af", - "metadata": {}, - "source": [ - "# ANISE\n", - "\n", - "ANISE is a modern rewrite of NAIF SPICE, written in Rust and providing interfaces to other languages including Python.\n", - "\n", - "Evidently, this tutorial applies to the Python usage of ANISE.\n", - "\n", - "## Goal\n", - "By the end of this tutorial, you should know how to build a data frame containing the Azimuth, Elevation, and Range data from a location from any body fixed object (like an Earth ground station) and another other object stored in the almanac.\n", - "\n", - "Let's start by installing ANISE: `pip install anise`" - ] - }, - { - "cell_type": "markdown", - "id": "645bb375-58da-42a2-8107-2af6bbb38b5f", - "metadata": {}, - "source": [ - "## Loading the latest orientation and planetary data\n", - "\n", - "In this tutorial, we're using the latest Earth orientation parameters from JPL, and the latest planetary ephemerides, constants, and high precision Moon rotation parameters. These can be downloaded automatically by ANISE using the MetaAlmanac class (refer to tutorial #02 for details)." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "b594d869-f456-4f8a-a6e5-72a0e3050f5e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Almanac: #SPK = 1\t#BPC = 2\tPlanetaryData with 54 ID mappings and 0 name mappings\tEulerParameterData with 3 ID mappings and 3 name mappings (@0x5638ac745070)" - ] - }, - "execution_count": 1, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from anise import MetaAlmanac\n", - "\n", - "almanac = MetaAlmanac.latest()\n", - "\n", - "almanac" - ] - }, - { - "cell_type": "markdown", - "id": "8933c0a3-0530-43ec-a332-39b5f35869b0", - "metadata": {}, - "source": [ - "Superb! We've got two BPCs loaded (the latest Earth and the long-term Moon high precision rotation), the DE440s planetary ephemerides, and planetary constants for 49 objects in the solar system." - ] - }, - { - "cell_type": "markdown", - "id": "38ce5bee-bb5b-4bbc-9e9e-125faf6aa1e7", - "metadata": {}, - "source": [ - "## Defining a ground station\n", - "\n", - "Let's define a ground station in Madrid, Spain (for example). We start by importing the Orbit structure, and figure out how it could be initialized for a ground asset." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "788f43ca-b56a-4737-9088-b40424b81fe5", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\u001b[0;31mSignature:\u001b[0m\n", - "\u001b[0mOrbit\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfrom_latlongalt\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mlatitude_deg\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mlongitude_deg\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mheight_km\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mangular_velocity\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mepoch\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mframe\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mDocstring:\u001b[0m\n", - "Creates a new Orbit from the latitude (φ), longitude (λ) and height (in km) with respect to the frame's ellipsoid given the angular velocity.\n", - "\n", - "**Units:** degrees, degrees, km, rad/s\n", - "NOTE: This computation differs from the spherical coordinates because we consider the flattening of body.\n", - "Reference: G. Xu and Y. Xu, \"GPS\", DOI 10.1007/978-3-662-50367-6_2, 2016\n", - "\u001b[0;31mType:\u001b[0m builtin_function_or_method" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from anise.astro import Orbit\n", - "Orbit.from_latlongalt?" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "8ca5609a-2751-4f8e-8182-09f8769d0037", - "metadata": {}, - "outputs": [], - "source": [ - "# Define the location of the asset\n", - "# Build the Madrid DSN gound station\n", - "latitude_deg = 40.427_222\n", - "longitude_deg = 4.250_556\n", - "height_km = 0.834_939\n", - "\n", - "# As noted in the documentation, this call also requires the mean angular velocity of the reference frame.\n", - "# Source: G. Xu and Y. Xu, \"GPS\", DOI 10.1007/978-3-662-50367-6_2, 2016 (confirmed by https://hpiers.obspm.fr/eop-pc/models/constants.html)\n", - "MEAN_EARTH_ANGULAR_VELOCITY_DEG_S = 0.004178079012116429" - ] - }, - { - "cell_type": "markdown", - "id": "f82e4205-8a5f-4ec6-8b55-d6e4df854631", - "metadata": {}, - "source": [ - "## Defining the frame and epoch" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "13fd331a-f32b-4e23-a326-1dcd0a671586", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Earth ITRF93 (μ = 398600.435436096 km^3/s^2, eq. radius = 6378.1366 km, polar radius = 6356.7519 km, f = 0.0033528131084554717) (@0x7f64028c6e80)" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from anise.astro.constants import Frames\n", - "from anise.time import *\n", - "\n", - "# Grab the high precision Earth rotation frame (ITRF93)\n", - "itrf93 = almanac.frame_info(Frames.EARTH_ITRF93)\n", - "itrf93" - ] - }, - { - "cell_type": "markdown", - "id": "f632778f-4792-462f-bb83-88abba8f788a", - "metadata": {}, - "source": [ - "We now have everything to initialize the ground asset, apart from the epoch. So let's define a time series to compute the AER of the (center) of the Moon throughout a month. " - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "0b0c35b5-ee36-4313-8b81-6194e8ede907", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "TimeSeries { start: 2023-01-01T20:00:00 UTC, duration: Duration { centuries: 0, nanoseconds: 2592000000000000 }, step: Duration { centuries: 0, nanoseconds: 60000000000 }, cur: 0, incl: true } @ 0x7f64032ba1a0" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "start = Epoch(\"2023-01-01 20:00:00\")\n", - "end = start + Unit.Day*30\n", - "step = Unit.Minute*1\n", - "\n", - "time_series = TimeSeries(start, end, step, inclusive=True)\n", - "time_series" - ] - }, - { - "cell_type": "markdown", - "id": "54310bd7-f573-4436-b135-18a18baba32f", - "metadata": {}, - "source": [ - "## Generate the AER data\n", - "Now, we just need to iterate over this time series, storing the AER result in an array for each computation.\n", - "\n", - "Note that the nomenclature used in the AER computation is \"receiver\" and \"transmitter\", which is the typical convention for orbit determination, a capability enabled by the Nyx astrodynamics package, a superset of ANISE." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "fe9a2311-eb16-43f6-8e83-f9daacd13ec2", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\u001b[0;31mSignature:\u001b[0m \u001b[0malmanac\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mazimuth_elevation_range_sez\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mrx\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mDocstring:\u001b[0m\n", - "Computes the azimuth (in degrees), elevation (in degrees), and range (in kilometers) of the\n", - "receiver state (`rx`) seen from the transmitter state (`tx`), once converted into the SEZ frame of the transmitter.\n", - "\n", - "# Algorithm\n", - "1. Compute the SEZ (South East Zenith) frame of the transmitter.\n", - "2. Rotate the receiver position vector into the transmitter SEZ frame.\n", - "3. Rotate the transmitter position vector into that same SEZ frame.\n", - "4. Compute the range as the norm of the difference between these two position vectors.\n", - "5. Compute the elevation, and ensure it is between +/- 180 degrees.\n", - "6. Compute the azimuth with a quadrant check, and ensure it is between 0 and 360 degrees.\n", - "\u001b[0;31mType:\u001b[0m builtin_function_or_method" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "almanac.azimuth_elevation_range_sez?" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "a394f90a-be4a-412b-8fb1-ac7d7f8347f0", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\u001b[0;31mSignature:\u001b[0m \u001b[0malmanac\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtransform\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtarget_frame\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mobserver_frame\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mepoch\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mab_corr\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mDocstring:\u001b[0m\n", - "Returns the Cartesian state needed to transform the `from_frame` to the `to_frame`.\n", - "\n", - "# SPICE Compatibility\n", - "This function is the SPICE equivalent of spkezr: `spkezr(TARGET_ID, EPOCH_TDB_S, ORIENTATION_ID, ABERRATION, OBSERVER_ID)`\n", - "In ANISE, the TARGET_ID and ORIENTATION are provided in the first argument (TARGET_FRAME), as that frame includes BOTH\n", - "the target ID and the orientation of that target. The EPOCH_TDB_S is the epoch in the TDB time system, which is computed\n", - "in ANISE using Hifitime. THe ABERRATION is computed by providing the optional Aberration flag. Finally, the OBSERVER\n", - "argument is replaced by OBSERVER_FRAME: if the OBSERVER_FRAME argument has the same orientation as the TARGET_FRAME, then this call\n", - "will return exactly the same data as the spkerz SPICE call.\n", - "\n", - "# Note\n", - "The units will be those of the underlying ephemeris data (typically km and km/s)\n", - "\u001b[0;31mType:\u001b[0m builtin_function_or_method" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "# Import the aberration correction because we want to correct for the light time.\n", - "from anise import Aberration\n", - "# Checking the doc is always helpful!\n", - "almanac.transform?" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "b93ab05e-a27f-4b18-9ad9-d74746609e45", - "metadata": {}, - "outputs": [], - "source": [ - "aer_data = []\n", - "\n", - "for epoch in time_series:\n", - " # Build the observer (\"transmitter\") at this epoch\n", - " tx = Orbit.from_latlongalt(latitude_deg, longitude_deg, height_km, MEAN_EARTH_ANGULAR_VELOCITY_DEG_S, epoch, itrf93)\n", - " # Grab the state of the Moon at this epoch (\"receiver\")\n", - " rx = almanac.transform(Frames.MOON_J2000, Frames.EARTH_J2000, epoch, Aberration(\"LT+S\"))\n", - " # NOTE: The rx state here is in the Earth J2000 frame, but that's OK because the Almanac will compute the translations needed.\n", - " this_aer = almanac.azimuth_elevation_range_sez(rx, tx)\n", - " aer_data += [this_aer]" - ] - }, - { - "cell_type": "markdown", - "id": "cfb78d25-d1d5-47b8-8272-9c1081917bb9", - "metadata": {}, - "source": [ - "## Plotting the data\n", - "\n", - "Let's build a Polars dataframe with the AER data and plot it using `hvplot` (the recommended tool by Polars). We're using Polars here because it's dozens of times faster than Pandas, and we've got ~43k rows in our data set, so we might as well filter through it quickly.\n", - "\n", - "Let's install Polars (with the plotting option) and hvplot." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "302ef0b8-370b-43ad-92e5-3e77431dd63d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Requirement already satisfied: polars[plot] in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (0.20.4)\n", - "Requirement already satisfied: hvplot in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (0.9.1)\n", - "Requirement already satisfied: bokeh>=1.0.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (3.3.3)\n", - "Requirement already satisfied: colorcet>=2 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (3.0.1)\n", - "Requirement already satisfied: holoviews>=1.11.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (1.18.1)\n", - "Requirement already satisfied: pandas in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (2.1.4)\n", - "Requirement already satisfied: numpy>=1.15 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (1.26.3)\n", - "Requirement already satisfied: packaging in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (23.2)\n", - "Requirement already satisfied: panel>=0.11.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (1.3.6)\n", - "Requirement already satisfied: param<3.0,>=1.12.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (2.0.1)\n", - "Requirement already satisfied: Jinja2>=2.9 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from bokeh>=1.0.0->hvplot) (3.1.2)\n", - "Requirement already satisfied: contourpy>=1 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from bokeh>=1.0.0->hvplot) (1.2.0)\n", - "Requirement already satisfied: pillow>=7.1.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from bokeh>=1.0.0->hvplot) (10.2.0)\n", - "Requirement already satisfied: PyYAML>=3.10 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from bokeh>=1.0.0->hvplot) (6.0.1)\n", - "Requirement already satisfied: tornado>=5.1 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from bokeh>=1.0.0->hvplot) (6.4)\n", - "Requirement already satisfied: xyzservices>=2021.09.1 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from bokeh>=1.0.0->hvplot) (2023.10.1)\n", - "Requirement already satisfied: pyct>=0.4.4 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from colorcet>=2->hvplot) (0.5.0)\n", - "Requirement already satisfied: pyviz-comms>=0.7.4 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from holoviews>=1.11.0->hvplot) (3.0.0)\n", - "Requirement already satisfied: python-dateutil>=2.8.2 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from pandas->hvplot) (2.8.2)\n", - "Requirement already satisfied: pytz>=2020.1 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from pandas->hvplot) (2023.3.post1)\n", - "Requirement already satisfied: tzdata>=2022.1 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from pandas->hvplot) (2023.4)\n", - "Requirement already satisfied: markdown in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (3.5.2)\n", - "Requirement already satisfied: markdown-it-py in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (3.0.0)\n", - "Requirement already satisfied: linkify-it-py in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (2.0.2)\n", - "Requirement already satisfied: mdit-py-plugins in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (0.4.0)\n", - "Requirement already satisfied: requests in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (2.31.0)\n", - "Requirement already satisfied: tqdm>=4.48.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (4.66.1)\n", - "Requirement already satisfied: bleach in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (6.1.0)\n", - "Requirement already satisfied: typing-extensions in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (4.6.3)\n", - "Requirement already satisfied: MarkupSafe>=2.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from Jinja2>=2.9->bokeh>=1.0.0->hvplot) (2.1.3)\n", - "Requirement already satisfied: six>=1.5 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from python-dateutil>=2.8.2->pandas->hvplot) (1.16.0)\n", - "Requirement already satisfied: webencodings in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from bleach->panel>=0.11.0->hvplot) (0.5.1)\n", - "Requirement already satisfied: uc-micro-py in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from linkify-it-py->panel>=0.11.0->hvplot) (1.0.2)\n", - "Requirement already satisfied: mdurl~=0.1 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from markdown-it-py->panel>=0.11.0->hvplot) (0.1.2)\n", - "Requirement already satisfied: charset-normalizer<4,>=2 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from requests->panel>=0.11.0->hvplot) (3.3.2)\n", - "Requirement already satisfied: idna<4,>=2.5 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from requests->panel>=0.11.0->hvplot) (3.6)\n", - "Requirement already satisfied: urllib3<3,>=1.21.1 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from requests->panel>=0.11.0->hvplot) (2.1.0)\n", - "Requirement already satisfied: certifi>=2017.4.17 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from requests->panel>=0.11.0->hvplot) (2023.11.17)\n", - "\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m23.1.2\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m24.0\u001b[0m\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n", - "Note: you may need to restart the kernel to use updated packages.\n" - ] - } - ], - "source": [ - "%pip install \"polars[plot]\" hvplot" - ] - }, - { - "cell_type": "markdown", - "id": "2400ac97-6a46-47cf-a3ac-ad85672469ee", - "metadata": {}, - "source": [ - "Now, let's build a data frame. As of January 2024, we need some custom handling of the epochs in Hifitime to make them compatible with Python's lower precision datetime object, but this will be fixed in [this hifitime issue](https://github.com/nyx-space/hifitime/issues/185)." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "2de60a1a-27c9-461d-8bf6-c430b951ca8a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "shape: (9, 6)
describeepochselevation_degazimuth_degrange_kmrange_rate_km_s
strstrf64f64f64f64
"count""43201"43201.043201.043201.043201.0
"null_count""0"0.00.00.00.0
"mean"null1.920361179.943419385666.0888740.009381
"std"null36.57106398.44334216472.23810613.798014
"min""2023-01-01 20:…-77.1666770.005663354094.280434-20.694402
"25%"null-25.14492.887676370637.2144-13.742085
"50%"null1.205067180.070913390142.67677-0.0035
"75%"null29.304787266.940744399983.93130713.763225
"max""2023-01-31 20:…76.760723359.975421409264.59034220.698013
" - ], - "text/plain": [ - "shape: (9, 6)\n", - "┌────────────┬─────────────────────┬───────────────┬─────────────┬───────────────┬─────────────────┐\n", - "│ describe ┆ epochs ┆ elevation_deg ┆ azimuth_deg ┆ range_km ┆ range_rate_km_s │\n", - "│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │\n", - "│ str ┆ str ┆ f64 ┆ f64 ┆ f64 ┆ f64 │\n", - "╞════════════╪═════════════════════╪═══════════════╪═════════════╪═══════════════╪═════════════════╡\n", - "│ count ┆ 43201 ┆ 43201.0 ┆ 43201.0 ┆ 43201.0 ┆ 43201.0 │\n", - "│ null_count ┆ 0 ┆ 0.0 ┆ 0.0 ┆ 0.0 ┆ 0.0 │\n", - "│ mean ┆ null ┆ 1.920361 ┆ 179.943419 ┆ 385666.088874 ┆ 0.009381 │\n", - "│ std ┆ null ┆ 36.571063 ┆ 98.443342 ┆ 16472.238106 ┆ 13.798014 │\n", - "│ min ┆ 2023-01-01 20:00:00 ┆ -77.166677 ┆ 0.005663 ┆ 354094.280434 ┆ -20.694402 │\n", - "│ 25% ┆ null ┆ -25.144 ┆ 92.887676 ┆ 370637.2144 ┆ -13.742085 │\n", - "│ 50% ┆ null ┆ 1.205067 ┆ 180.070913 ┆ 390142.67677 ┆ -0.0035 │\n", - "│ 75% ┆ null ┆ 29.304787 ┆ 266.940744 ┆ 399983.931307 ┆ 13.763225 │\n", - "│ max ┆ 2023-01-31 20:00:00 ┆ 76.760723 ┆ 359.975421 ┆ 409264.590342 ┆ 20.698013 │\n", - "└────────────┴─────────────────────┴───────────────┴─────────────┴───────────────┴─────────────────┘" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import polars as pl\n", - "from datetime import datetime\n", - "\n", - "def hifitime_to_datetime(e: Epoch) -> datetime:\n", - " return datetime.fromisoformat(str(e).replace(\" UTC\", \"\")[:23])\n", - "\n", - "# Build the arrays of data\n", - "epochs = []\n", - "elevations = []\n", - "azimuths = []\n", - "ranges = []\n", - "range_rates = []\n", - "\n", - "for aer in aer_data:\n", - " epochs += [hifitime_to_datetime(aer.epoch)]\n", - " elevations += [aer.elevation_deg]\n", - " azimuths += [aer.azimuth_deg]\n", - " ranges += [aer.range_km]\n", - " range_rates += [aer.range_rate_km_s]\n", - "\n", - "# Build the data frame\n", - "df = pl.DataFrame(\n", - " {\n", - " 'epochs': epochs,\n", - " 'elevation_deg': elevations,\n", - " 'azimuth_deg': azimuths,\n", - " 'range_km': ranges,\n", - " 'range_rate_km_s': range_rates\n", - " }\n", - ")\n", - "\n", - "df.describe()" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "d4543961-cef4-41b0-8c80-ffcc3b369adb", - "metadata": {}, - "outputs": [ - { - "data": { - "application/javascript": [ - "(function(root) {\n", - " function now() {\n", - " return new Date();\n", - " }\n", - "\n", - " var force = true;\n", - " var py_version = '3.3.3'.replace('rc', '-rc.').replace('.dev', '-dev.');\n", - " var reloading = false;\n", - " var Bokeh = root.Bokeh;\n", - "\n", - " if (typeof (root._bokeh_timeout) === \"undefined\" || force) {\n", - " root._bokeh_timeout = Date.now() + 5000;\n", - " root._bokeh_failed_load = false;\n", - " }\n", - "\n", - " function run_callbacks() {\n", - " try {\n", - " root._bokeh_onload_callbacks.forEach(function(callback) {\n", - " if (callback != null)\n", - " callback();\n", - " });\n", - " } finally {\n", - " delete root._bokeh_onload_callbacks;\n", - " }\n", - " console.debug(\"Bokeh: all callbacks have finished\");\n", - " }\n", - "\n", - " function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {\n", - " if (css_urls == null) css_urls = [];\n", - " if (js_urls == null) js_urls = [];\n", - " if (js_modules == null) js_modules = [];\n", - " if (js_exports == null) js_exports = {};\n", - "\n", - " root._bokeh_onload_callbacks.push(callback);\n", - "\n", - " if (root._bokeh_is_loading > 0) {\n", - " console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n", - " return null;\n", - " }\n", - " if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n", - " run_callbacks();\n", - " return null;\n", - " }\n", - " if (!reloading) {\n", - " console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n", - " }\n", - "\n", - " function on_load() {\n", - " root._bokeh_is_loading--;\n", - " if (root._bokeh_is_loading === 0) {\n", - " console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n", - " run_callbacks()\n", - " }\n", - " }\n", - " window._bokeh_on_load = on_load\n", - "\n", - " function on_error() {\n", - " console.error(\"failed to load \" + url);\n", - " }\n", - "\n", - " var skip = [];\n", - " if (window.requirejs) {\n", - " window.requirejs.config({'packages': {}, 'paths': {'jspanel': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/jspanel', 'jspanel-modal': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal', 'jspanel-tooltip': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip', 'jspanel-hint': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint', 'jspanel-layout': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout', 'jspanel-contextmenu': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu', 'jspanel-dock': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/dock/jspanel.dock', 'gridstack': 'https://cdn.jsdelivr.net/npm/gridstack@7.2.3/dist/gridstack-all', 'notyf': 'https://cdn.jsdelivr.net/npm/notyf@3/notyf.min'}, 'shim': {'jspanel': {'exports': 'jsPanel'}, 'gridstack': {'exports': 'GridStack'}}});\n", - " require([\"jspanel\"], function(jsPanel) {\n", - "\twindow.jsPanel = jsPanel\n", - "\ton_load()\n", - " })\n", - " require([\"jspanel-modal\"], function() {\n", - "\ton_load()\n", - " })\n", - " require([\"jspanel-tooltip\"], function() {\n", - "\ton_load()\n", - " })\n", - " require([\"jspanel-hint\"], function() {\n", - "\ton_load()\n", - " })\n", - " require([\"jspanel-layout\"], function() {\n", - "\ton_load()\n", - " })\n", - " require([\"jspanel-contextmenu\"], function() {\n", - "\ton_load()\n", - " })\n", - " require([\"jspanel-dock\"], function() {\n", - "\ton_load()\n", - " })\n", - " require([\"gridstack\"], function(GridStack) {\n", - "\twindow.GridStack = GridStack\n", - "\ton_load()\n", - " })\n", - " require([\"notyf\"], function() {\n", - "\ton_load()\n", - " })\n", - " root._bokeh_is_loading = css_urls.length + 9;\n", - " } else {\n", - " root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n", - " }\n", - "\n", - " var existing_stylesheets = []\n", - " var links = document.getElementsByTagName('link')\n", - " for (var i = 0; i < links.length; i++) {\n", - " var link = links[i]\n", - " if (link.href != null) {\n", - "\texisting_stylesheets.push(link.href)\n", - " }\n", - " }\n", - " for (var i = 0; i < css_urls.length; i++) {\n", - " var url = css_urls[i];\n", - " if (existing_stylesheets.indexOf(url) !== -1) {\n", - "\ton_load()\n", - "\tcontinue;\n", - " }\n", - " const element = document.createElement(\"link\");\n", - " element.onload = on_load;\n", - " element.onerror = on_error;\n", - " element.rel = \"stylesheet\";\n", - " element.type = \"text/css\";\n", - " element.href = url;\n", - " console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n", - " document.body.appendChild(element);\n", - " } if (((window['jsPanel'] !== undefined) && (!(window['jsPanel'] instanceof HTMLElement))) || window.requirejs) {\n", - " var urls = ['https://cdn.holoviz.org/panel/1.3.6/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.6/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.6/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.6/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.6/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.6/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.6/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/dock/jspanel.dock.js'];\n", - " for (var i = 0; i < urls.length; i++) {\n", - " skip.push(urls[i])\n", - " }\n", - " } if (((window['GridStack'] !== undefined) && (!(window['GridStack'] instanceof HTMLElement))) || window.requirejs) {\n", - " var urls = ['https://cdn.holoviz.org/panel/1.3.6/dist/bundled/gridstack/gridstack@7.2.3/dist/gridstack-all.js'];\n", - " for (var i = 0; i < urls.length; i++) {\n", - " skip.push(urls[i])\n", - " }\n", - " } if (((window['Notyf'] !== undefined) && (!(window['Notyf'] instanceof HTMLElement))) || window.requirejs) {\n", - " var urls = ['https://cdn.holoviz.org/panel/1.3.6/dist/bundled/notificationarea/notyf@3/notyf.min.js'];\n", - " for (var i = 0; i < urls.length; i++) {\n", - " skip.push(urls[i])\n", - " }\n", - " } var existing_scripts = []\n", - " var scripts = document.getElementsByTagName('script')\n", - " for (var i = 0; i < scripts.length; i++) {\n", - " var script = scripts[i]\n", - " if (script.src != null) {\n", - "\texisting_scripts.push(script.src)\n", - " }\n", - " }\n", - " for (var i = 0; i < js_urls.length; i++) {\n", - " var url = js_urls[i];\n", - " if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n", - "\tif (!window.requirejs) {\n", - "\t on_load();\n", - "\t}\n", - "\tcontinue;\n", - " }\n", - " var element = document.createElement('script');\n", - " element.onload = on_load;\n", - " element.onerror = on_error;\n", - " element.async = false;\n", - " element.src = url;\n", - " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", - " document.head.appendChild(element);\n", - " }\n", - " for (var i = 0; i < js_modules.length; i++) {\n", - " var url = js_modules[i];\n", - " if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n", - "\tif (!window.requirejs) {\n", - "\t on_load();\n", - "\t}\n", - "\tcontinue;\n", - " }\n", - " var element = document.createElement('script');\n", - " element.onload = on_load;\n", - " element.onerror = on_error;\n", - " element.async = false;\n", - " element.src = url;\n", - " element.type = \"module\";\n", - " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", - " document.head.appendChild(element);\n", - " }\n", - " for (const name in js_exports) {\n", - " var url = js_exports[name];\n", - " if (skip.indexOf(url) >= 0 || root[name] != null) {\n", - "\tif (!window.requirejs) {\n", - "\t on_load();\n", - "\t}\n", - "\tcontinue;\n", - " }\n", - " var element = document.createElement('script');\n", - " element.onerror = on_error;\n", - " element.async = false;\n", - " element.type = \"module\";\n", - " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n", - " element.textContent = `\n", - " import ${name} from \"${url}\"\n", - " window.${name} = ${name}\n", - " window._bokeh_on_load()\n", - " `\n", - " document.head.appendChild(element);\n", - " }\n", - " if (!js_urls.length && !js_modules.length) {\n", - " on_load()\n", - " }\n", - " };\n", - "\n", - " function inject_raw_css(css) {\n", - " const element = document.createElement(\"style\");\n", - " element.appendChild(document.createTextNode(css));\n", - " document.body.appendChild(element);\n", - " }\n", - "\n", - " var js_urls = [\"https://cdn.bokeh.org/bokeh/release/bokeh-3.3.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.3.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.3.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.3.3.min.js\", \"https://cdn.holoviz.org/panel/1.3.6/dist/panel.min.js\"];\n", - " var js_modules = [];\n", - " var js_exports = {};\n", - " var css_urls = [];\n", - " var inline_js = [ function(Bokeh) {\n", - " Bokeh.set_log_level(\"info\");\n", - " },\n", - "function(Bokeh) {} // ensure no trailing comma for IE\n", - " ];\n", - "\n", - " function run_inline_js() {\n", - " if ((root.Bokeh !== undefined) || (force === true)) {\n", - " for (var i = 0; i < inline_js.length; i++) {\n", - "\ttry {\n", - " inline_js[i].call(root, root.Bokeh);\n", - "\t} catch(e) {\n", - "\t if (!reloading) {\n", - "\t throw e;\n", - "\t }\n", - "\t}\n", - " }\n", - " // Cache old bokeh versions\n", - " if (Bokeh != undefined && !reloading) {\n", - "\tvar NewBokeh = root.Bokeh;\n", - "\tif (Bokeh.versions === undefined) {\n", - "\t Bokeh.versions = new Map();\n", - "\t}\n", - "\tif (NewBokeh.version !== Bokeh.version) {\n", - "\t Bokeh.versions.set(NewBokeh.version, NewBokeh)\n", - "\t}\n", - "\troot.Bokeh = Bokeh;\n", - " }} else if (Date.now() < root._bokeh_timeout) {\n", - " setTimeout(run_inline_js, 100);\n", - " } else if (!root._bokeh_failed_load) {\n", - " console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n", - " root._bokeh_failed_load = true;\n", - " }\n", - " root._bokeh_is_initializing = false\n", - " }\n", - "\n", - " function load_or_wait() {\n", - " // Implement a backoff loop that tries to ensure we do not load multiple\n", - " // versions of Bokeh and its dependencies at the same time.\n", - " // In recent versions we use the root._bokeh_is_initializing flag\n", - " // to determine whether there is an ongoing attempt to initialize\n", - " // bokeh, however for backward compatibility we also try to ensure\n", - " // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n", - " // before older versions are fully initialized.\n", - " if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n", - " root._bokeh_is_initializing = false;\n", - " root._bokeh_onload_callbacks = undefined;\n", - " console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n", - " load_or_wait();\n", - " } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n", - " setTimeout(load_or_wait, 100);\n", - " } else {\n", - " root._bokeh_is_initializing = true\n", - " root._bokeh_onload_callbacks = []\n", - " var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n", - " if (!reloading && !bokeh_loaded) {\n", - "\troot.Bokeh = undefined;\n", - " }\n", - " load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n", - "\tconsole.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n", - "\trun_inline_js();\n", - " });\n", - " }\n", - " }\n", - " // Give older versions of the autoload script a head-start to ensure\n", - " // they initialize before we start loading newer version.\n", - " setTimeout(load_or_wait, 100)\n", - "}(window));" - ], - "application/vnd.holoviews_load.v0+json": "(function(root) {\n function now() {\n return new Date();\n }\n\n var force = true;\n var py_version = '3.3.3'.replace('rc', '-rc.').replace('.dev', '-dev.');\n var reloading = false;\n var Bokeh = root.Bokeh;\n\n if (typeof (root._bokeh_timeout) === \"undefined\" || force) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks;\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, js_modules, js_exports, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n if (js_modules == null) js_modules = [];\n if (js_exports == null) js_exports = {};\n\n root._bokeh_onload_callbacks.push(callback);\n\n if (root._bokeh_is_loading > 0) {\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n }\n if (js_urls.length === 0 && js_modules.length === 0 && Object.keys(js_exports).length === 0) {\n run_callbacks();\n return null;\n }\n if (!reloading) {\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n }\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n window._bokeh_on_load = on_load\n\n function on_error() {\n console.error(\"failed to load \" + url);\n }\n\n var skip = [];\n if (window.requirejs) {\n window.requirejs.config({'packages': {}, 'paths': {'jspanel': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/jspanel', 'jspanel-modal': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal', 'jspanel-tooltip': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip', 'jspanel-hint': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint', 'jspanel-layout': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout', 'jspanel-contextmenu': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu', 'jspanel-dock': 'https://cdn.jsdelivr.net/npm/jspanel4@4.12.0/dist/extensions/dock/jspanel.dock', 'gridstack': 'https://cdn.jsdelivr.net/npm/gridstack@7.2.3/dist/gridstack-all', 'notyf': 'https://cdn.jsdelivr.net/npm/notyf@3/notyf.min'}, 'shim': {'jspanel': {'exports': 'jsPanel'}, 'gridstack': {'exports': 'GridStack'}}});\n require([\"jspanel\"], function(jsPanel) {\n\twindow.jsPanel = jsPanel\n\ton_load()\n })\n require([\"jspanel-modal\"], function() {\n\ton_load()\n })\n require([\"jspanel-tooltip\"], function() {\n\ton_load()\n })\n require([\"jspanel-hint\"], function() {\n\ton_load()\n })\n require([\"jspanel-layout\"], function() {\n\ton_load()\n })\n require([\"jspanel-contextmenu\"], function() {\n\ton_load()\n })\n require([\"jspanel-dock\"], function() {\n\ton_load()\n })\n require([\"gridstack\"], function(GridStack) {\n\twindow.GridStack = GridStack\n\ton_load()\n })\n require([\"notyf\"], function() {\n\ton_load()\n })\n root._bokeh_is_loading = css_urls.length + 9;\n } else {\n root._bokeh_is_loading = css_urls.length + js_urls.length + js_modules.length + Object.keys(js_exports).length;\n }\n\n var existing_stylesheets = []\n var links = document.getElementsByTagName('link')\n for (var i = 0; i < links.length; i++) {\n var link = links[i]\n if (link.href != null) {\n\texisting_stylesheets.push(link.href)\n }\n }\n for (var i = 0; i < css_urls.length; i++) {\n var url = css_urls[i];\n if (existing_stylesheets.indexOf(url) !== -1) {\n\ton_load()\n\tcontinue;\n }\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error;\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n } if (((window['jsPanel'] !== undefined) && (!(window['jsPanel'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.3.6/dist/bundled/floatpanel/jspanel4@4.12.0/dist/jspanel.js', 'https://cdn.holoviz.org/panel/1.3.6/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/modal/jspanel.modal.js', 'https://cdn.holoviz.org/panel/1.3.6/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/tooltip/jspanel.tooltip.js', 'https://cdn.holoviz.org/panel/1.3.6/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/hint/jspanel.hint.js', 'https://cdn.holoviz.org/panel/1.3.6/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/layout/jspanel.layout.js', 'https://cdn.holoviz.org/panel/1.3.6/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/contextmenu/jspanel.contextmenu.js', 'https://cdn.holoviz.org/panel/1.3.6/dist/bundled/floatpanel/jspanel4@4.12.0/dist/extensions/dock/jspanel.dock.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window['GridStack'] !== undefined) && (!(window['GridStack'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.3.6/dist/bundled/gridstack/gridstack@7.2.3/dist/gridstack-all.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } if (((window['Notyf'] !== undefined) && (!(window['Notyf'] instanceof HTMLElement))) || window.requirejs) {\n var urls = ['https://cdn.holoviz.org/panel/1.3.6/dist/bundled/notificationarea/notyf@3/notyf.min.js'];\n for (var i = 0; i < urls.length; i++) {\n skip.push(urls[i])\n }\n } var existing_scripts = []\n var scripts = document.getElementsByTagName('script')\n for (var i = 0; i < scripts.length; i++) {\n var script = scripts[i]\n if (script.src != null) {\n\texisting_scripts.push(script.src)\n }\n }\n for (var i = 0; i < js_urls.length; i++) {\n var url = js_urls[i];\n if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (var i = 0; i < js_modules.length; i++) {\n var url = js_modules[i];\n if (skip.indexOf(url) !== -1 || existing_scripts.indexOf(url) !== -1) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error;\n element.async = false;\n element.src = url;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n for (const name in js_exports) {\n var url = js_exports[name];\n if (skip.indexOf(url) >= 0 || root[name] != null) {\n\tif (!window.requirejs) {\n\t on_load();\n\t}\n\tcontinue;\n }\n var element = document.createElement('script');\n element.onerror = on_error;\n element.async = false;\n element.type = \"module\";\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n element.textContent = `\n import ${name} from \"${url}\"\n window.${name} = ${name}\n window._bokeh_on_load()\n `\n document.head.appendChild(element);\n }\n if (!js_urls.length && !js_modules.length) {\n on_load()\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n var js_urls = [\"https://cdn.bokeh.org/bokeh/release/bokeh-3.3.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-3.3.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-3.3.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-3.3.3.min.js\", \"https://cdn.holoviz.org/panel/1.3.6/dist/panel.min.js\"];\n var js_modules = [];\n var js_exports = {};\n var css_urls = [];\n var inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {} // ensure no trailing comma for IE\n ];\n\n function run_inline_js() {\n if ((root.Bokeh !== undefined) || (force === true)) {\n for (var i = 0; i < inline_js.length; i++) {\n\ttry {\n inline_js[i].call(root, root.Bokeh);\n\t} catch(e) {\n\t if (!reloading) {\n\t throw e;\n\t }\n\t}\n }\n // Cache old bokeh versions\n if (Bokeh != undefined && !reloading) {\n\tvar NewBokeh = root.Bokeh;\n\tif (Bokeh.versions === undefined) {\n\t Bokeh.versions = new Map();\n\t}\n\tif (NewBokeh.version !== Bokeh.version) {\n\t Bokeh.versions.set(NewBokeh.version, NewBokeh)\n\t}\n\troot.Bokeh = Bokeh;\n }} else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n }\n root._bokeh_is_initializing = false\n }\n\n function load_or_wait() {\n // Implement a backoff loop that tries to ensure we do not load multiple\n // versions of Bokeh and its dependencies at the same time.\n // In recent versions we use the root._bokeh_is_initializing flag\n // to determine whether there is an ongoing attempt to initialize\n // bokeh, however for backward compatibility we also try to ensure\n // that we do not start loading a newer (Panel>=1.0 and Bokeh>3) version\n // before older versions are fully initialized.\n if (root._bokeh_is_initializing && Date.now() > root._bokeh_timeout) {\n root._bokeh_is_initializing = false;\n root._bokeh_onload_callbacks = undefined;\n console.log(\"Bokeh: BokehJS was loaded multiple times but one version failed to initialize.\");\n load_or_wait();\n } else if (root._bokeh_is_initializing || (typeof root._bokeh_is_initializing === \"undefined\" && root._bokeh_onload_callbacks !== undefined)) {\n setTimeout(load_or_wait, 100);\n } else {\n root._bokeh_is_initializing = true\n root._bokeh_onload_callbacks = []\n var bokeh_loaded = Bokeh != null && (Bokeh.version === py_version || (Bokeh.versions !== undefined && Bokeh.versions.has(py_version)));\n if (!reloading && !bokeh_loaded) {\n\troot.Bokeh = undefined;\n }\n load_libs(css_urls, js_urls, js_modules, js_exports, function() {\n\tconsole.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n\trun_inline_js();\n });\n }\n }\n // Give older versions of the autoload script a head-start to ensure\n // they initialize before we start loading newer version.\n setTimeout(load_or_wait, 100)\n}(window));" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/javascript": [ - "\n", - "if ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n", - " window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n", - "}\n", - "\n", - "\n", - " function JupyterCommManager() {\n", - " }\n", - "\n", - " JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n", - " if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n", - " var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n", - " comm_manager.register_target(comm_id, function(comm) {\n", - " comm.on_msg(msg_handler);\n", - " });\n", - " } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n", - " window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n", - " comm.onMsg = msg_handler;\n", - " });\n", - " } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n", - " google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n", - " var messages = comm.messages[Symbol.asyncIterator]();\n", - " function processIteratorResult(result) {\n", - " var message = result.value;\n", - " console.log(message)\n", - " var content = {data: message.data, comm_id};\n", - " var buffers = []\n", - " for (var buffer of message.buffers || []) {\n", - " buffers.push(new DataView(buffer))\n", - " }\n", - " var metadata = message.metadata || {};\n", - " var msg = {content, buffers, metadata}\n", - " msg_handler(msg);\n", - " return messages.next().then(processIteratorResult);\n", - " }\n", - " return messages.next().then(processIteratorResult);\n", - " })\n", - " }\n", - " }\n", - "\n", - " JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n", - " if (comm_id in window.PyViz.comms) {\n", - " return window.PyViz.comms[comm_id];\n", - " } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n", - " var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n", - " var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n", - " if (msg_handler) {\n", - " comm.on_msg(msg_handler);\n", - " }\n", - " } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n", - " var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n", - " comm.open();\n", - " if (msg_handler) {\n", - " comm.onMsg = msg_handler;\n", - " }\n", - " } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n", - " var comm_promise = google.colab.kernel.comms.open(comm_id)\n", - " comm_promise.then((comm) => {\n", - " window.PyViz.comms[comm_id] = comm;\n", - " if (msg_handler) {\n", - " var messages = comm.messages[Symbol.asyncIterator]();\n", - " function processIteratorResult(result) {\n", - " var message = result.value;\n", - " var content = {data: message.data};\n", - " var metadata = message.metadata || {comm_id};\n", - " var msg = {content, metadata}\n", - " msg_handler(msg);\n", - " return messages.next().then(processIteratorResult);\n", - " }\n", - " return messages.next().then(processIteratorResult);\n", - " }\n", - " }) \n", - " var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n", - " return comm_promise.then((comm) => {\n", - " comm.send(data, metadata, buffers, disposeOnDone);\n", - " });\n", - " };\n", - " var comm = {\n", - " send: sendClosure\n", - " };\n", - " }\n", - " window.PyViz.comms[comm_id] = comm;\n", - " return comm;\n", - " }\n", - " window.PyViz.comm_manager = new JupyterCommManager();\n", - " \n", - "\n", - "\n", - "var JS_MIME_TYPE = 'application/javascript';\n", - "var HTML_MIME_TYPE = 'text/html';\n", - "var EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\n", - "var CLASS_NAME = 'output';\n", - "\n", - "/**\n", - " * Render data to the DOM node\n", - " */\n", - "function render(props, node) {\n", - " var div = document.createElement(\"div\");\n", - " var script = document.createElement(\"script\");\n", - " node.appendChild(div);\n", - " node.appendChild(script);\n", - "}\n", - "\n", - "/**\n", - " * Handle when a new output is added\n", - " */\n", - "function handle_add_output(event, handle) {\n", - " var output_area = handle.output_area;\n", - " var output = handle.output;\n", - " if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n", - " return\n", - " }\n", - " var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n", - " var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n", - " if (id !== undefined) {\n", - " var nchildren = toinsert.length;\n", - " var html_node = toinsert[nchildren-1].children[0];\n", - " html_node.innerHTML = output.data[HTML_MIME_TYPE];\n", - " var scripts = [];\n", - " var nodelist = html_node.querySelectorAll(\"script\");\n", - " for (var i in nodelist) {\n", - " if (nodelist.hasOwnProperty(i)) {\n", - " scripts.push(nodelist[i])\n", - " }\n", - " }\n", - "\n", - " scripts.forEach( function (oldScript) {\n", - " var newScript = document.createElement(\"script\");\n", - " var attrs = [];\n", - " var nodemap = oldScript.attributes;\n", - " for (var j in nodemap) {\n", - " if (nodemap.hasOwnProperty(j)) {\n", - " attrs.push(nodemap[j])\n", - " }\n", - " }\n", - " attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n", - " newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n", - " oldScript.parentNode.replaceChild(newScript, oldScript);\n", - " });\n", - " if (JS_MIME_TYPE in output.data) {\n", - " toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n", - " }\n", - " output_area._hv_plot_id = id;\n", - " if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n", - " window.PyViz.plot_index[id] = Bokeh.index[id];\n", - " } else {\n", - " window.PyViz.plot_index[id] = null;\n", - " }\n", - " } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n", - " var bk_div = document.createElement(\"div\");\n", - " bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n", - " var script_attrs = bk_div.children[0].attributes;\n", - " for (var i = 0; i < script_attrs.length; i++) {\n", - " toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n", - " }\n", - " // store reference to server id on output_area\n", - " output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n", - " }\n", - "}\n", - "\n", - "/**\n", - " * Handle when an output is cleared or removed\n", - " */\n", - "function handle_clear_output(event, handle) {\n", - " var id = handle.cell.output_area._hv_plot_id;\n", - " var server_id = handle.cell.output_area._bokeh_server_id;\n", - " if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n", - " var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n", - " if (server_id !== null) {\n", - " comm.send({event_type: 'server_delete', 'id': server_id});\n", - " return;\n", - " } else if (comm !== null) {\n", - " comm.send({event_type: 'delete', 'id': id});\n", - " }\n", - " delete PyViz.plot_index[id];\n", - " if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n", - " var doc = window.Bokeh.index[id].model.document\n", - " doc.clear();\n", - " const i = window.Bokeh.documents.indexOf(doc);\n", - " if (i > -1) {\n", - " window.Bokeh.documents.splice(i, 1);\n", - " }\n", - " }\n", - "}\n", - "\n", - "/**\n", - " * Handle kernel restart event\n", - " */\n", - "function handle_kernel_cleanup(event, handle) {\n", - " delete PyViz.comms[\"hv-extension-comm\"];\n", - " window.PyViz.plot_index = {}\n", - "}\n", - "\n", - "/**\n", - " * Handle update_display_data messages\n", - " */\n", - "function handle_update_output(event, handle) {\n", - " handle_clear_output(event, {cell: {output_area: handle.output_area}})\n", - " handle_add_output(event, handle)\n", - "}\n", - "\n", - "function register_renderer(events, OutputArea) {\n", - " function append_mime(data, metadata, element) {\n", - " // create a DOM node to render to\n", - " var toinsert = this.create_output_subarea(\n", - " metadata,\n", - " CLASS_NAME,\n", - " EXEC_MIME_TYPE\n", - " );\n", - " this.keyboard_manager.register_events(toinsert);\n", - " // Render to node\n", - " var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n", - " render(props, toinsert[0]);\n", - " element.append(toinsert);\n", - " return toinsert\n", - " }\n", - "\n", - " events.on('output_added.OutputArea', handle_add_output);\n", - " events.on('output_updated.OutputArea', handle_update_output);\n", - " events.on('clear_output.CodeCell', handle_clear_output);\n", - " events.on('delete.Cell', handle_clear_output);\n", - " events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n", - "\n", - " OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n", - " safe: true,\n", - " index: 0\n", - " });\n", - "}\n", - "\n", - "if (window.Jupyter !== undefined) {\n", - " try {\n", - " var events = require('base/js/events');\n", - " var OutputArea = require('notebook/js/outputarea').OutputArea;\n", - " if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n", - " register_renderer(events, OutputArea);\n", - " }\n", - " } catch(err) {\n", - " }\n", - "}\n" - ], - "application/vnd.holoviews_load.v0+json": "\nif ((window.PyViz === undefined) || (window.PyViz instanceof HTMLElement)) {\n window.PyViz = {comms: {}, comm_status:{}, kernels:{}, receivers: {}, plot_index: []}\n}\n\n\n function JupyterCommManager() {\n }\n\n JupyterCommManager.prototype.register_target = function(plot_id, comm_id, msg_handler) {\n if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n comm_manager.register_target(comm_id, function(comm) {\n comm.on_msg(msg_handler);\n });\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n window.PyViz.kernels[plot_id].registerCommTarget(comm_id, function(comm) {\n comm.onMsg = msg_handler;\n });\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n google.colab.kernel.comms.registerTarget(comm_id, (comm) => {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n console.log(message)\n var content = {data: message.data, comm_id};\n var buffers = []\n for (var buffer of message.buffers || []) {\n buffers.push(new DataView(buffer))\n }\n var metadata = message.metadata || {};\n var msg = {content, buffers, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n })\n }\n }\n\n JupyterCommManager.prototype.get_client_comm = function(plot_id, comm_id, msg_handler) {\n if (comm_id in window.PyViz.comms) {\n return window.PyViz.comms[comm_id];\n } else if (window.comm_manager || ((window.Jupyter !== undefined) && (Jupyter.notebook.kernel != null))) {\n var comm_manager = window.comm_manager || Jupyter.notebook.kernel.comm_manager;\n var comm = comm_manager.new_comm(comm_id, {}, {}, {}, comm_id);\n if (msg_handler) {\n comm.on_msg(msg_handler);\n }\n } else if ((plot_id in window.PyViz.kernels) && (window.PyViz.kernels[plot_id])) {\n var comm = window.PyViz.kernels[plot_id].connectToComm(comm_id);\n comm.open();\n if (msg_handler) {\n comm.onMsg = msg_handler;\n }\n } else if (typeof google != 'undefined' && google.colab.kernel != null) {\n var comm_promise = google.colab.kernel.comms.open(comm_id)\n comm_promise.then((comm) => {\n window.PyViz.comms[comm_id] = comm;\n if (msg_handler) {\n var messages = comm.messages[Symbol.asyncIterator]();\n function processIteratorResult(result) {\n var message = result.value;\n var content = {data: message.data};\n var metadata = message.metadata || {comm_id};\n var msg = {content, metadata}\n msg_handler(msg);\n return messages.next().then(processIteratorResult);\n }\n return messages.next().then(processIteratorResult);\n }\n }) \n var sendClosure = (data, metadata, buffers, disposeOnDone) => {\n return comm_promise.then((comm) => {\n comm.send(data, metadata, buffers, disposeOnDone);\n });\n };\n var comm = {\n send: sendClosure\n };\n }\n window.PyViz.comms[comm_id] = comm;\n return comm;\n }\n window.PyViz.comm_manager = new JupyterCommManager();\n \n\n\nvar JS_MIME_TYPE = 'application/javascript';\nvar HTML_MIME_TYPE = 'text/html';\nvar EXEC_MIME_TYPE = 'application/vnd.holoviews_exec.v0+json';\nvar CLASS_NAME = 'output';\n\n/**\n * Render data to the DOM node\n */\nfunction render(props, node) {\n var div = document.createElement(\"div\");\n var script = document.createElement(\"script\");\n node.appendChild(div);\n node.appendChild(script);\n}\n\n/**\n * Handle when a new output is added\n */\nfunction handle_add_output(event, handle) {\n var output_area = handle.output_area;\n var output = handle.output;\n if ((output.data == undefined) || (!output.data.hasOwnProperty(EXEC_MIME_TYPE))) {\n return\n }\n var id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n if (id !== undefined) {\n var nchildren = toinsert.length;\n var html_node = toinsert[nchildren-1].children[0];\n html_node.innerHTML = output.data[HTML_MIME_TYPE];\n var scripts = [];\n var nodelist = html_node.querySelectorAll(\"script\");\n for (var i in nodelist) {\n if (nodelist.hasOwnProperty(i)) {\n scripts.push(nodelist[i])\n }\n }\n\n scripts.forEach( function (oldScript) {\n var newScript = document.createElement(\"script\");\n var attrs = [];\n var nodemap = oldScript.attributes;\n for (var j in nodemap) {\n if (nodemap.hasOwnProperty(j)) {\n attrs.push(nodemap[j])\n }\n }\n attrs.forEach(function(attr) { newScript.setAttribute(attr.name, attr.value) });\n newScript.appendChild(document.createTextNode(oldScript.innerHTML));\n oldScript.parentNode.replaceChild(newScript, oldScript);\n });\n if (JS_MIME_TYPE in output.data) {\n toinsert[nchildren-1].children[1].textContent = output.data[JS_MIME_TYPE];\n }\n output_area._hv_plot_id = id;\n if ((window.Bokeh !== undefined) && (id in Bokeh.index)) {\n window.PyViz.plot_index[id] = Bokeh.index[id];\n } else {\n window.PyViz.plot_index[id] = null;\n }\n } else if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n var bk_div = document.createElement(\"div\");\n bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n var script_attrs = bk_div.children[0].attributes;\n for (var i = 0; i < script_attrs.length; i++) {\n toinsert[toinsert.length - 1].childNodes[1].setAttribute(script_attrs[i].name, script_attrs[i].value);\n }\n // store reference to server id on output_area\n output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n }\n}\n\n/**\n * Handle when an output is cleared or removed\n */\nfunction handle_clear_output(event, handle) {\n var id = handle.cell.output_area._hv_plot_id;\n var server_id = handle.cell.output_area._bokeh_server_id;\n if (((id === undefined) || !(id in PyViz.plot_index)) && (server_id !== undefined)) { return; }\n var comm = window.PyViz.comm_manager.get_client_comm(\"hv-extension-comm\", \"hv-extension-comm\", function () {});\n if (server_id !== null) {\n comm.send({event_type: 'server_delete', 'id': server_id});\n return;\n } else if (comm !== null) {\n comm.send({event_type: 'delete', 'id': id});\n }\n delete PyViz.plot_index[id];\n if ((window.Bokeh !== undefined) & (id in window.Bokeh.index)) {\n var doc = window.Bokeh.index[id].model.document\n doc.clear();\n const i = window.Bokeh.documents.indexOf(doc);\n if (i > -1) {\n window.Bokeh.documents.splice(i, 1);\n }\n }\n}\n\n/**\n * Handle kernel restart event\n */\nfunction handle_kernel_cleanup(event, handle) {\n delete PyViz.comms[\"hv-extension-comm\"];\n window.PyViz.plot_index = {}\n}\n\n/**\n * Handle update_display_data messages\n */\nfunction handle_update_output(event, handle) {\n handle_clear_output(event, {cell: {output_area: handle.output_area}})\n handle_add_output(event, handle)\n}\n\nfunction register_renderer(events, OutputArea) {\n function append_mime(data, metadata, element) {\n // create a DOM node to render to\n var toinsert = this.create_output_subarea(\n metadata,\n CLASS_NAME,\n EXEC_MIME_TYPE\n );\n this.keyboard_manager.register_events(toinsert);\n // Render to node\n var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n render(props, toinsert[0]);\n element.append(toinsert);\n return toinsert\n }\n\n events.on('output_added.OutputArea', handle_add_output);\n events.on('output_updated.OutputArea', handle_update_output);\n events.on('clear_output.CodeCell', handle_clear_output);\n events.on('delete.Cell', handle_clear_output);\n events.on('kernel_ready.Kernel', handle_kernel_cleanup);\n\n OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n safe: true,\n index: 0\n });\n}\n\nif (window.Jupyter !== undefined) {\n try {\n var events = require('base/js/events');\n var OutputArea = require('notebook/js/outputarea').OutputArea;\n if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n register_renderer(events, OutputArea);\n }\n } catch(err) {\n }\n}\n" - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "
\n", - "
\n", - "" - ] - }, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "p1002" - } - }, - "output_type": "display_data" - }, - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":Curve [epochs] (elevation_deg)" - ] - }, - "execution_count": 14, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "p1005" - } - }, - "output_type": "execute_result" - } - ], - "source": [ - "import hvplot.polars\n", - "from bokeh.models.formatters import DatetimeTickFormatter\n", - "\n", - "formatter = DatetimeTickFormatter(days='%d/%m') # Make the world a better place\n", - "\n", - "df.hvplot(x=\"epochs\", y=\"elevation_deg\", xformatter=formatter, title=\"Elevation of the Moon throughout Jan 2023 from Madrid, Spain\")" - ] - }, - { - "cell_type": "markdown", - "id": "967f1d5c-6fff-44c2-aaa3-25828fa15617", - "metadata": {}, - "source": [ - "Great! We've plotted the elevation of the Moon over the month of January 2023. Let's now plot the azimuth angle only when the elevation is greater than 5 degrees." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "80c6233c-285f-4e9c-8c5d-6014e3103c55", - "metadata": {}, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":Curve [epochs] (azimuth_deg)" - ] - }, - "execution_count": 15, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "p1080" - } - }, - "output_type": "execute_result" - } - ], - "source": [ - "elevation_gt_5 = df.filter(pl.col('elevation_deg') > 5)\n", - "elevation_gt_5.hvplot(x=\"epochs\", y=\"azimuth_deg\", xformatter=formatter, title=\"Azimuth of the Moon when above horizon from Madrid, Spain\")" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "0f3c0b4f-0b06-494a-9543-6a4e9365efa5", - "metadata": {}, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":Curve [epochs] (range_km)" - ] - }, - "execution_count": 16, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "p1155" - } - }, - "output_type": "execute_result" - } - ], - "source": [ - "df.hvplot(x=\"epochs\", y=\"range_km\", xformatter=formatter, title=\"Range of the Moon from Madrid, Spain throughout Jan 2023\")" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "e21f128a-cfd8-4ad8-81c1-6512db28c3a1", - "metadata": {}, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":Curve [epochs] (range_rate_km_s)" - ] - }, - "execution_count": 17, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "p1230" - } - }, - "output_type": "execute_result" - } - ], - "source": [ - "df.hvplot(x=\"epochs\", y=\"range_rate_km_s\", xformatter=formatter, title=\"Range rate of the Moon from Madrid, Spain throughout Jan 2023\")" - ] - }, - { - "cell_type": "markdown", - "id": "2d03907b-52ec-413b-a19d-fdf6695260a1", - "metadata": {}, - "source": [ - "## Exercises\n", - "\n", - "### Learning objective\n", - "\n", - "1. Become more familiar with TimeSeries\n", - "2. Become more familiar with building assets on the surface of the Moon and Earth\n", - "3. Learn how to convert between units using Hifitime's `Unit` structure, and plotting those results.\n", - "\n", - "### 1. Range and light-time from Earth asset to Pluto until 2045\n", - "\n", - "+ Build a time series from today (`Epoch.system_now()`) until a date of your choosing in 2045, with a daily time step.\n", - "+ Build the AER data for the whole time series.\n", - "+ Using the speed of light constant defined in `anise.astro.constants` in the `UsualConstants` class, compute the light time delay from the range data.\n", - "+ Plot the range over time and plot the light time in hours over time (by multiplying the light time in seconds by `Unit.Second` and calling `to_unit(Unit.Hour)` on the result).\n", - "\n", - "### 2. Elevation from a lunar asset to an Earth asset\n", - "\n", - "+ Using the mean Lunar angular velocity constant defined in `anise.astro.constants` in the `UsualConstants` class, compute the elevation of the Earth as seen from Shackleton Crater on the Lunar South pole.\n", - "\n", - "Hints: You will need to build the Moon frame with either data from the PCK08 Planetary Constants or from the high precision Moon orientation parameters. Use `almanac.describe()` to see a pretty printed list of the loaded data to know which frame to build. Or you may use the MOON_PA frame from the planetary constants." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "f4a0eafd-a9d5-4100-ac0b-c859d0fb589d", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": ".venv" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.8" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/anise/tutorials/Tutorial 04 - Computing Azimuth Elevation and Range data.md b/docs/anise/tutorials/Tutorial 04 - Computing Azimuth Elevation and Range data.md new file mode 100644 index 0000000..91e73c3 --- /dev/null +++ b/docs/anise/tutorials/Tutorial 04 - Computing Azimuth Elevation and Range data.md @@ -0,0 +1,722 @@ +# ANISE + +ANISE is a modern rewrite of NAIF SPICE, written in Rust and providing interfaces to other languages including Python. + +Evidently, this tutorial applies to the Python usage of ANISE. + +## Goal +By the end of this tutorial, you should know how to build a data frame containing the Azimuth, Elevation, and Range data from a location from any body fixed object (like an Earth ground station) and another other object stored in the almanac. + +Let's start by installing ANISE: `pip install anise` + +## Loading the latest orientation and planetary data + +In this tutorial, we're using the latest Earth orientation parameters from JPL, and the latest planetary ephemerides, constants, and high precision Moon rotation parameters. These can be downloaded automatically by ANISE using the MetaAlmanac class (refer to tutorial #02 for details). + + +```python +from anise import MetaAlmanac + +almanac = MetaAlmanac.latest() + +almanac +``` + + + + + Almanac: #SPK = 1 #BPC = 2 PlanetaryData with 54 ID mappings and 0 name mappings EulerParameterData with 3 ID mappings and 3 name mappings (@0x5638ac745070) + + + +Superb! We've got two BPCs loaded (the latest Earth and the long-term Moon high precision rotation), the DE440s planetary ephemerides, and planetary constants for 49 objects in the solar system. + +## Defining a ground station + +Let's define a ground station in Madrid, Spain (for example). We start by importing the Orbit structure, and figure out how it could be initialized for a ground asset. + + +```python +from anise.astro import Orbit +Orbit.from_latlongalt? +``` + + + Signature: + Orbit.from_latlongalt( +  latitude_deg, +  longitude_deg, +  height_km, +  angular_velocity, +  epoch, +  frame, + ) + Docstring: + Creates a new Orbit from the latitude (φ), longitude (λ) and height (in km) with respect to the frame's ellipsoid given the angular velocity. + + **Units:** degrees, degrees, km, rad/s + NOTE: This computation differs from the spherical coordinates because we consider the flattening of body. + Reference: G. Xu and Y. Xu, "GPS", DOI 10.1007/978-3-662-50367-6_2, 2016 + Type: builtin_function_or_method + + + +```python +# Define the location of the asset +# Build the Madrid DSN gound station +latitude_deg = 40.427_222 +longitude_deg = 4.250_556 +height_km = 0.834_939 + +# As noted in the documentation, this call also requires the mean angular velocity of the reference frame. +# Source: G. Xu and Y. Xu, "GPS", DOI 10.1007/978-3-662-50367-6_2, 2016 (confirmed by https://hpiers.obspm.fr/eop-pc/models/constants.html) +MEAN_EARTH_ANGULAR_VELOCITY_DEG_S = 0.004178079012116429 +``` + +## Defining the frame and epoch + + +```python +from anise.astro.constants import Frames +from anise.time import * + +# Grab the high precision Earth rotation frame (ITRF93) +itrf93 = almanac.frame_info(Frames.EARTH_ITRF93) +itrf93 +``` + + + + + Earth ITRF93 (μ = 398600.435436096 km^3/s^2, eq. radius = 6378.1366 km, polar radius = 6356.7519 km, f = 0.0033528131084554717) (@0x7f64028c6e80) + + + +We now have everything to initialize the ground asset, apart from the epoch. So let's define a time series to compute the AER of the (center) of the Moon throughout a month. + + +```python +start = Epoch("2023-01-01 20:00:00") +end = start + Unit.Day*30 +step = Unit.Minute*1 + +time_series = TimeSeries(start, end, step, inclusive=True) +time_series +``` + + + + + TimeSeries { start: 2023-01-01T20:00:00 UTC, duration: Duration { centuries: 0, nanoseconds: 2592000000000000 }, step: Duration { centuries: 0, nanoseconds: 60000000000 }, cur: 0, incl: true } @ 0x7f64032ba1a0 + + + +## Generate the AER data +Now, we just need to iterate over this time series, storing the AER result in an array for each computation. + +Note that the nomenclature used in the AER computation is "receiver" and "transmitter", which is the typical convention for orbit determination, a capability enabled by the Nyx astrodynamics package, a superset of ANISE. + + +```python +almanac.azimuth_elevation_range_sez? +``` + + + Signature: almanac.azimuth_elevation_range_sez(rx, tx) + Docstring: + Computes the azimuth (in degrees), elevation (in degrees), and range (in kilometers) of the + receiver state (`rx`) seen from the transmitter state (`tx`), once converted into the SEZ frame of the transmitter. + + # Algorithm + 1. Compute the SEZ (South East Zenith) frame of the transmitter. + 2. Rotate the receiver position vector into the transmitter SEZ frame. + 3. Rotate the transmitter position vector into that same SEZ frame. + 4. Compute the range as the norm of the difference between these two position vectors. + 5. Compute the elevation, and ensure it is between +/- 180 degrees. + 6. Compute the azimuth with a quadrant check, and ensure it is between 0 and 360 degrees. + Type: builtin_function_or_method + + + +```python +# Import the aberration correction because we want to correct for the light time. +from anise import Aberration +# Checking the doc is always helpful! +almanac.transform? +``` + + + Signature: almanac.transform(target_frame, observer_frame, epoch, ab_corr=None) + Docstring: + Returns the Cartesian state needed to transform the `from_frame` to the `to_frame`. + + # SPICE Compatibility + This function is the SPICE equivalent of spkezr: `spkezr(TARGET_ID, EPOCH_TDB_S, ORIENTATION_ID, ABERRATION, OBSERVER_ID)` + In ANISE, the TARGET_ID and ORIENTATION are provided in the first argument (TARGET_FRAME), as that frame includes BOTH + the target ID and the orientation of that target. The EPOCH_TDB_S is the epoch in the TDB time system, which is computed + in ANISE using Hifitime. THe ABERRATION is computed by providing the optional Aberration flag. Finally, the OBSERVER + argument is replaced by OBSERVER_FRAME: if the OBSERVER_FRAME argument has the same orientation as the TARGET_FRAME, then this call + will return exactly the same data as the spkerz SPICE call. + + # Note + The units will be those of the underlying ephemeris data (typically km and km/s) + Type: builtin_function_or_method + + + +```python +aer_data = [] + +for epoch in time_series: + # Build the observer ("transmitter") at this epoch + tx = Orbit.from_latlongalt(latitude_deg, longitude_deg, height_km, MEAN_EARTH_ANGULAR_VELOCITY_DEG_S, epoch, itrf93) + # Grab the state of the Moon at this epoch ("receiver") + rx = almanac.transform(Frames.MOON_J2000, Frames.EARTH_J2000, epoch, Aberration("LT+S")) + # NOTE: The rx state here is in the Earth J2000 frame, but that's OK because the Almanac will compute the translations needed. + this_aer = almanac.azimuth_elevation_range_sez(rx, tx) + aer_data += [this_aer] +``` + +## Plotting the data + +Let's build a Polars dataframe with the AER data and plot it using `hvplot` (the recommended tool by Polars). We're using Polars here because it's dozens of times faster than Pandas, and we've got ~43k rows in our data set, so we might as well filter through it quickly. + +Let's install Polars (with the plotting option) and hvplot. + + +```python +%pip install "polars[plot]" hvplot +``` + + Requirement already satisfied: polars[plot] in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (0.20.4) + Requirement already satisfied: hvplot in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (0.9.1) + Requirement already satisfied: bokeh>=1.0.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (3.3.3) + Requirement already satisfied: colorcet>=2 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (3.0.1) + Requirement already satisfied: holoviews>=1.11.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (1.18.1) + Requirement already satisfied: pandas in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (2.1.4) + Requirement already satisfied: numpy>=1.15 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (1.26.3) + Requirement already satisfied: packaging in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (23.2) + Requirement already satisfied: panel>=0.11.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (1.3.6) + Requirement already satisfied: param<3.0,>=1.12.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (2.0.1) + Requirement already satisfied: Jinja2>=2.9 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from bokeh>=1.0.0->hvplot) (3.1.2) + Requirement already satisfied: contourpy>=1 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from bokeh>=1.0.0->hvplot) (1.2.0) + Requirement already satisfied: pillow>=7.1.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from bokeh>=1.0.0->hvplot) (10.2.0) + Requirement already satisfied: PyYAML>=3.10 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from bokeh>=1.0.0->hvplot) (6.0.1) + Requirement already satisfied: tornado>=5.1 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from bokeh>=1.0.0->hvplot) (6.4) + Requirement already satisfied: xyzservices>=2021.09.1 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from bokeh>=1.0.0->hvplot) (2023.10.1) + Requirement already satisfied: pyct>=0.4.4 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from colorcet>=2->hvplot) (0.5.0) + Requirement already satisfied: pyviz-comms>=0.7.4 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from holoviews>=1.11.0->hvplot) (3.0.0) + Requirement already satisfied: python-dateutil>=2.8.2 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from pandas->hvplot) (2.8.2) + Requirement already satisfied: pytz>=2020.1 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from pandas->hvplot) (2023.3.post1) + Requirement already satisfied: tzdata>=2022.1 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from pandas->hvplot) (2023.4) + Requirement already satisfied: markdown in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (3.5.2) + Requirement already satisfied: markdown-it-py in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (3.0.0) + Requirement already satisfied: linkify-it-py in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (2.0.2) + Requirement already satisfied: mdit-py-plugins in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (0.4.0) + Requirement already satisfied: requests in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (2.31.0) + Requirement already satisfied: tqdm>=4.48.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (4.66.1) + Requirement already satisfied: bleach in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (6.1.0) + Requirement already satisfied: typing-extensions in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (4.6.3) + Requirement already satisfied: MarkupSafe>=2.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from Jinja2>=2.9->bokeh>=1.0.0->hvplot) (2.1.3) + Requirement already satisfied: six>=1.5 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from python-dateutil>=2.8.2->pandas->hvplot) (1.16.0) + Requirement already satisfied: webencodings in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from bleach->panel>=0.11.0->hvplot) (0.5.1) + Requirement already satisfied: uc-micro-py in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from linkify-it-py->panel>=0.11.0->hvplot) (1.0.2) + Requirement already satisfied: mdurl~=0.1 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from markdown-it-py->panel>=0.11.0->hvplot) (0.1.2) + Requirement already satisfied: charset-normalizer<4,>=2 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from requests->panel>=0.11.0->hvplot) (3.3.2) + Requirement already satisfied: idna<4,>=2.5 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from requests->panel>=0.11.0->hvplot) (3.6) + Requirement already satisfied: urllib3<3,>=1.21.1 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from requests->panel>=0.11.0->hvplot) (2.1.0) + Requirement already satisfied: certifi>=2017.4.17 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from requests->panel>=0.11.0->hvplot) (2023.11.17) + + [notice] A new release of pip is available: 23.1.2 -> 24.0 + [notice] To update, run: pip install --upgrade pip + Note: you may need to restart the kernel to use updated packages. + + +Now, let's build a data frame. As of January 2024, we need some custom handling of the epochs in Hifitime to make them compatible with Python's lower precision datetime object, but this will be fixed in [this hifitime issue](https://github.com/nyx-space/hifitime/issues/185). + + +```python +import polars as pl +from datetime import datetime + +def hifitime_to_datetime(e: Epoch) -> datetime: + return datetime.fromisoformat(str(e).replace(" UTC", "")[:23]) + +# Build the arrays of data +epochs = [] +elevations = [] +azimuths = [] +ranges = [] +range_rates = [] + +for aer in aer_data: + epochs += [hifitime_to_datetime(aer.epoch)] + elevations += [aer.elevation_deg] + azimuths += [aer.azimuth_deg] + ranges += [aer.range_km] + range_rates += [aer.range_rate_km_s] + +# Build the data frame +df = pl.DataFrame( + { + 'epochs': epochs, + 'elevation_deg': elevations, + 'azimuth_deg': azimuths, + 'range_km': ranges, + 'range_rate_km_s': range_rates + } +) + +df.describe() +``` + + + + +
+shape: (9, 6)
describeepochselevation_degazimuth_degrange_kmrange_rate_km_s
strstrf64f64f64f64
"count""43201"43201.043201.043201.043201.0
"null_count""0"0.00.00.00.0
"mean"null1.920361179.943419385666.0888740.009381
"std"null36.57106398.44334216472.23810613.798014
"min""2023-01-01 20:…-77.1666770.005663354094.280434-20.694402
"25%"null-25.14492.887676370637.2144-13.742085
"50%"null1.205067180.070913390142.67677-0.0035
"75%"null29.304787266.940744399983.93130713.763225
"max""2023-01-31 20:…76.760723359.975421409264.59034220.698013
+ + + + +```python +import hvplot.polars +from bokeh.models.formatters import DatetimeTickFormatter + +formatter = DatetimeTickFormatter(days='%d/%m') # Make the world a better place + +df.hvplot(x="epochs", y="elevation_deg", xformatter=formatter, title="Elevation of the Moon throughout Jan 2023 from Madrid, Spain") +``` + + + + + + + + + + +
+
+
+ + + + + + + + +
+
+
+ + + + +Great! We've plotted the elevation of the Moon over the month of January 2023. Let's now plot the azimuth angle only when the elevation is greater than 5 degrees. + + +```python +elevation_gt_5 = df.filter(pl.col('elevation_deg') > 5) +elevation_gt_5.hvplot(x="epochs", y="azimuth_deg", xformatter=formatter, title="Azimuth of the Moon when above horizon from Madrid, Spain") +``` + + + + + + +
+
+
+ + + + + +```python +df.hvplot(x="epochs", y="range_km", xformatter=formatter, title="Range of the Moon from Madrid, Spain throughout Jan 2023") +``` + + + + + + +
+
+
+ + + + + +```python +df.hvplot(x="epochs", y="range_rate_km_s", xformatter=formatter, title="Range rate of the Moon from Madrid, Spain throughout Jan 2023") +``` + + + + + + +
+
+
+ + + + +## Exercises + +### Learning objective + +1. Become more familiar with TimeSeries +2. Become more familiar with building assets on the surface of the Moon and Earth +3. Learn how to convert between units using Hifitime's `Unit` structure, and plotting those results. + +### 1. Range and light-time from Earth asset to Pluto until 2045 + ++ Build a time series from today (`Epoch.system_now()`) until a date of your choosing in 2045, with a daily time step. ++ Build the AER data for the whole time series. ++ Using the speed of light constant defined in `anise.astro.constants` in the `UsualConstants` class, compute the light time delay from the range data. ++ Plot the range over time and plot the light time in hours over time (by multiplying the light time in seconds by `Unit.Second` and calling `to_unit(Unit.Hour)` on the result). + +### 2. Elevation from a lunar asset to an Earth asset + ++ Using the mean Lunar angular velocity constant defined in `anise.astro.constants` in the `UsualConstants` class, compute the elevation of the Earth as seen from Shackleton Crater on the Lunar South pole. + +Hints: You will need to build the Moon frame with either data from the PCK08 Planetary Constants or from the high precision Moon orientation parameters. Use `almanac.describe()` to see a pretty printed list of the loaded data to know which frame to build. Or you may use the MOON_PA frame from the planetary constants. + + +```python + +``` diff --git a/docs/anise/tutorials/Tutorial 05 - Using frame kernels and text planetary kernels.ipynb b/docs/anise/tutorials/Tutorial 05 - Using frame kernels and text planetary kernels.ipynb deleted file mode 100644 index 1a7a6f4..0000000 --- a/docs/anise/tutorials/Tutorial 05 - Using frame kernels and text planetary kernels.ipynb +++ /dev/null @@ -1,341 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "89f0eb3a-044d-4337-9cef-43a41cbef37c", - "metadata": {}, - "source": [ - "# ANISE\n", - "\n", - "ANISE is a modern rewrite of NAIF SPICE, written in Rust and providing interfaces to other languages including Python.\n", - "\n", - "## Goal\n", - "\n", - "By the end of this tutorial, you will know how to use your custom frame kernels (FK) and text planetary constant kernels (TPC) in ANISE.\n", - "\n", - "Let's start by installing ANISE: `pip install anise`" - ] - }, - { - "cell_type": "markdown", - "id": "1ee5f04e-762a-41d7-8d3b-ac537ae7e10c", - "metadata": {}, - "source": [ - "## Data structure\n", - "\n", - "SPICE supports text-based kernels. These allow for easy modification and inclusion of documentation directly in the text file. The way SPICE handles these is by keeping a file handler open on the file while it's loaded. This causes a number of issues, such as potential file locking problems, increased risk of data corruption in cases of unexpected software termination, and increased difficulty in managing concurrent access, especially pertinent in the context of onboard flight software. To prevent these issues, ANISE maintains a pointer to the data contained in these files in memory (via a memory mapping or a heap allocation). Moreover, ANISE ensures that the data it uses allows for immediate random access, which is a method of accessing data at any point in memory with equal speed, independent of its location, thereby speeding up searches within the kernel data. To facilitate this, the data is stored in a platform-independent binary structure using the ASN.1 DER specification (the telecommunications industry standard for 20+ years). ASN.1 offers advantages like well-defined data structures, robust encoding schemes, and ease of interoperability across different platforms, making it a reliable choice for storing kernel data.\n", - "\n", - "This approach allows ANISE to parse through the FK and TPC equivalents significantly faster than SPICE, while storing the same information.\n", - "\n", - "However, this also means that ANISE cannot load the text files directly. Instead, these must be converted into the ANISE format, respectively PCA and EPA for \"Planetary Constants ANISE\" kernel and \"Euler Parameter ANISE\" kernel. Euler parameters (also known as quaternion parameters) offer a compact and non-redundant representation of orientations in three dimensions. They differ from quaternions in their normalization constraint and are slightly more robust to numerical errors in some computational scenarios.\n", - "\n", - "For details about the data set structure, refer to the API documentation: [`DataSet`](https://docs.rs/anise/latest/anise/structure/dataset/struct.DataSet.html). The `EulerParameterDataSet` and the `PlanetaryDataSet` are concrete implementations of this `DataSet` structure.\n", - "\n", - "### Version compatibility\n", - "\n", - "ANISE guarantees not to change the structure of these kernels between patch versions (e.g., version `0.3.0` and version `0.3.99` are guaranteed to have compatible kernels). However, until version `1.0.0`, the structure _may_ change and if so, the updated version of the default PCA and EPA files will be added to the Nyx Space cloud.\n", - "\n", - "Since version `0.1.0`, the structure of the kernels has _not_ changed. However, the ANISE version is encoded at the start of each kernel. This is only used if the data set cannot be properly decoded to inform the user of the expected ANISE version and the one that they're trying to load. In other words, although there is a version `0.3` of the PCK08 and PCK11 kernels, the files used in version `0.1.0` are still compatible." - ] - }, - { - "cell_type": "markdown", - "id": "c0233d36-b01b-472b-9880-d1119099cc7c", - "metadata": {}, - "source": [ - "## Planetary Constant ANISE kernels\n", - "\n", - "Planetary Constant ANISE (PCA) kernels (or a \"data set\") include a lookup table for random access via a name or an ID, metadata, and, more importantly, the actual planetary data itself. This data includes gravitational parameters, the shape of the triaxial ellipsoid, phase angle polynomials for the prime meridian, pole right ascension and declination, and more. You'll find all of the specifications in the API documentation: [`PlanetaryData`](https://docs.rs/anise/latest/anise/structure/planetocentric/struct.PlanetaryData.html).\n", - "\n", - "In the previous tutorials, we focused on fetching the frame information from the Almanac. This operation reads the PCA to return a copy of this information. This is why a PCA is provided in the `latest.dhall` configuration file for an Almanac.\n", - "\n", - "The Planetary Data structure includes the gravitational data. _However_, the SPICE TPC files contain _either_ ellipsoid definition information _or_ gravitational data. Therefore, to build a PCA, ANISE requires _both_ versions of the TPCs.\n", - "\n", - "Let's go ahead and build a PCA file from the NAIF `pck0008.tpc` and the `gm_de431.tpc` files." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "d8f0c21e-5075-4179-a3da-d85d35078709", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\u001b[0;31mSignature:\u001b[0m \u001b[0mconvert_tpc\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpck_file_path\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mgm_file_path\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0manise_output_path\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0moverwrite\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mDocstring:\u001b[0m\n", - "Converts two KPL/TPC files, one defining the planetary constants as text, and the other defining the gravity parameters, into the PlanetaryDataSet equivalent ANISE file.\n", - "KPL/TPC files must be converted into \"PCA\" (Planetary Constant ANISE) files before being loaded into ANISE.\n", - "\u001b[0;31mType:\u001b[0m builtin_function_or_method" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from anise.utils import convert_tpc\n", - "convert_tpc?" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "4f28e5f6-a768-4798-be37-a922948423d8", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "Skipping 802: no gravity data\n", - "Skipping 806: no gravity data\n", - "Skipping 9511010: no gravity data\n", - "Skipping 514: no gravity data\n", - "Skipping 714: no gravity data\n", - "Skipping 509: no gravity data\n", - "Skipping 707: no gravity data\n", - "Skipping 711: no gravity data\n", - "Skipping 804: no gravity data\n", - "Skipping 2431010: no gravity data\n", - "Skipping 506: no gravity data\n", - "Skipping 710: no gravity data\n", - "Skipping 712: no gravity data\n", - "Skipping 612: no gravity data\n", - "Skipping 618: no gravity data\n", - "Skipping 713: no gravity data\n", - "Skipping 803: no gravity data\n", - "Skipping 508: no gravity data\n", - "Skipping 515: no gravity data\n", - "Skipping 715: no gravity data\n", - "Skipping 511: no gravity data\n", - "Skipping 706: no gravity data\n", - "Skipping 516: no gravity data\n", - "Skipping 507: no gravity data\n", - "Skipping 614: no gravity data\n", - "Skipping 808: no gravity data\n", - "Skipping 805: no gravity data\n", - "Skipping 807: no gravity data\n", - "Skipping 513: no gravity data\n", - "Skipping 2000216: no gravity data\n", - "Skipping 613: no gravity data\n", - "Skipping 708: no gravity data\n", - "Skipping 512: no gravity data\n", - "Skipping 709: no gravity data\n", - "Skipping 510: no gravity data\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Added 49 items\n" - ] - } - ], - "source": [ - "convert_tpc(\"../../data/pck00008.tpc\", \"../../data/gm_de431.tpc\", \"demo08.pca\", True)" - ] - }, - { - "cell_type": "markdown", - "id": "443e0da7-cebe-4dd6-97e6-bee3b8f0450c", - "metadata": {}, - "source": [ - "We now have a PCA called `demo08.pca` which includes 49 entries. This file is compatible with _any_ machine you run ANISE on, little or big endian (which is _not_ the case of the DAF/BSP or DAF/BPC files).\n", - "\n", - "Let's load this file in an Almanac." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "24fa8803-6aa6-493b-a887-89f6e5e2f29a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Almanac: #SPK = 0\t#BPC = 0\tPlanetaryData with 49 ID mappings and 0 name mappings (@0x559b4af30800)" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from anise import Almanac\n", - "almanac = Almanac(\"demo08.pca\")\n", - "almanac" - ] - }, - { - "cell_type": "markdown", - "id": "4524157c-6d4d-4acb-93d0-ed0c02fcc883", - "metadata": {}, - "source": [ - "Since we don't have anything loaded other than these planetary constants, we can't do a whole lot, but we can query the Almanac for the shape and gravitational data by using `frame_info`." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "ab2d1579-411e-4cdb-ab2c-19fb15292cb0", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "body 599 IAU_JUPITER (μ = 126686534.9218008 km^3/s^2, eq. radius = 71492 km, polar radius = 66854 km, f = 0.0648743915403122) (@0x7fe814070310)" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from anise.astro.constants import Frames\n", - "iau_jupiter_frame = almanac.frame_info(Frames.IAU_JUPITER_FRAME)\n", - "iau_jupiter_frame" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "0a9b1b53-dea9-4e91-939b-15c310b0a92f", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "126686534.9218008" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "iau_jupiter_frame.mu_km3_s2()" - ] - }, - { - "cell_type": "markdown", - "id": "1c627e1c-0e9a-4965-b4b6-afa83be211b0", - "metadata": {}, - "source": [ - "### Exercise: Modifying the gravitational parameter of the Earth frame\n", - "\n", - "+ Make a copy of the `gm_de431.tpc` file and remove everything but the Earth GM.\n", - "+ Set the Earth GM to the one used by GMAT: `398600.4415` km^3/s^2\n", - "+ Make a copy of the `pck00008.tpc` file, removing everything except Earth related data.\n", - "+ Build a new PCA file using these two new files\n", - "+ Load a default Almanac and an empty Almanac where you'll load these two files into, along with the `DE440s.bsp` file.\n", - "+ Query the Cartesian state of the Earth at any time of your choosing from both of these Almanac. The state should be the same, since it'll be from the DE440s.bsp.\n", - "+ Observe how the frame graviational parameter information of both will differ.\n", - "+ Finally, using the `at_epoch` function on both of these state, perform a two-body propagation for both and notice how the graviational parameter affects the result." - ] - }, - { - "cell_type": "markdown", - "id": "652143bf-5066-4291-aac7-a663982333de", - "metadata": {}, - "source": [ - "## Euler Parameter ANISE kernel\n", - "\n", - "Euler parameters are a normalized quaternion. In fact, in the ANISE code in Rust, `EulerParameter` is an alias for `Quaternion`. Euler parameters are useful for defining fixed rotations, e.g. the rotation from the Moon Principal Axes frame to the Moon Mean Earth frame.\n", - "\n", - "Euler Parameter ANISE kernels (EPA) can be used to store these fixed rotations between frames, and reference them either by their ID or by their name. **They are the equivalent of SPICE's `FK` text files.**\n", - "\n", - "Until [#175](https://github.com/nyx-space/anise/issues/175), rotation data is _not_ exposed to Python. \n", - "\n", - "### How to build EPA files\n", - "\n", - "However, it's possible to convert an FK file into the EPA file using the following function." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "263507b1-8f58-44e5-b0b3-4abcf0a00b92", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\u001b[0;31mSignature:\u001b[0m\n", - "\u001b[0mconvert_fk\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mfk_file_path\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0manise_output_path\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0mshow_comments\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m \u001b[0moverwrite\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0;32mNone\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0;34m\u001b[0m\n", - "\u001b[0;34m\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mDocstring:\u001b[0m\n", - "Converts a KPL/FK file, that defines frame constants like fixed rotations, and frame name to ID mappings into the EulerParameterDataSet equivalent ANISE file.\n", - "KPL/FK files must be converted into \"PCA\" (Planetary Constant ANISE) files before being loaded into ANISE.\n", - "\u001b[0;31mType:\u001b[0m builtin_function_or_method" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from anise.utils import convert_fk\n", - "convert_fk?" - ] - }, - { - "cell_type": "markdown", - "id": "e26f92f9-9fa6-4fb7-b142-8bcaf0451b28", - "metadata": {}, - "source": [ - "On the Nyx Space Cloud, you'll find the `moon.fk` file, which includes the mapping between the Moon PA and Moon ME frame. The latest Almanac also includes the high precision Moon ME frame. Hence, with default data, you can rotate an object from any frame into the high precision Moon ME frame.\n", - "\n", - "## Exercise\n", - "\n", - "Using tutorial 04, compute the azimuth, elevation, and range of the [Shackleton](https://en.wikipedia.org/wiki/Shackleton_(crater)) crater on the Moon to the city of Paris, France.\n", - "\n", - "Here are the general steps:\n", - "\n", - "1. Load the latest Almanac, and check (by printing it) that it includes both EPA and PCA data. Else, load the `moon_fk.epa` file from the Nyx Space Cloud using a `MetaFile` with the URL .\n", - "2. Define a time series over a year with a granularity of 12 hours. This crater is on the South Pole of the Moon, and its visibility is often below the horizon of an object as far north as Paris.\n", - "3. For each epoch, define Paris as an `Orbit` instance from its longitude and latitude (recall that the constants include the mean Earth angular rotation rate), in the IAU_EARTH frame. Also, build the crater in the IAU_MOON frame.\n", - "4. Finally, call the `AER` function of the Almanac with each epoch to compute the AER data. Plot it!" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "0a91851b-188a-4dc8-9e8c-40276012dbd1", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": ".venv" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.4" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} \ No newline at end of file diff --git a/docs/anise/tutorials/Tutorial 05 - Using frame kernels and text planetary kernels.md b/docs/anise/tutorials/Tutorial 05 - Using frame kernels and text planetary kernels.md new file mode 100644 index 0000000..d3e1dcf --- /dev/null +++ b/docs/anise/tutorials/Tutorial 05 - Using frame kernels and text planetary kernels.md @@ -0,0 +1,201 @@ +# ANISE + +ANISE is a modern rewrite of NAIF SPICE, written in Rust and providing interfaces to other languages including Python. + +## Goal + +By the end of this tutorial, you will know how to use your custom frame kernels (FK) and text planetary constant kernels (TPC) in ANISE. + +Let's start by installing ANISE: `pip install anise` + +## Data structure + +SPICE supports text-based kernels. These allow for easy modification and inclusion of documentation directly in the text file. The way SPICE handles these is by keeping a file handler open on the file while it's loaded. This causes a number of issues, such as potential file locking problems, increased risk of data corruption in cases of unexpected software termination, and increased difficulty in managing concurrent access, especially pertinent in the context of onboard flight software. To prevent these issues, ANISE maintains a pointer to the data contained in these files in memory (via a memory mapping or a heap allocation). Moreover, ANISE ensures that the data it uses allows for immediate random access, which is a method of accessing data at any point in memory with equal speed, independent of its location, thereby speeding up searches within the kernel data. To facilitate this, the data is stored in a platform-independent binary structure using the ASN.1 DER specification (the telecommunications industry standard for 20+ years). ASN.1 offers advantages like well-defined data structures, robust encoding schemes, and ease of interoperability across different platforms, making it a reliable choice for storing kernel data. + +This approach allows ANISE to parse through the FK and TPC equivalents significantly faster than SPICE, while storing the same information. + +However, this also means that ANISE cannot load the text files directly. Instead, these must be converted into the ANISE format, respectively PCA and EPA for "Planetary Constants ANISE" kernel and "Euler Parameter ANISE" kernel. Euler parameters (also known as quaternion parameters) offer a compact and non-redundant representation of orientations in three dimensions. They differ from quaternions in their normalization constraint and are slightly more robust to numerical errors in some computational scenarios. + +For details about the data set structure, refer to the API documentation: [`DataSet`](https://docs.rs/anise/latest/anise/structure/dataset/struct.DataSet.html). The `EulerParameterDataSet` and the `PlanetaryDataSet` are concrete implementations of this `DataSet` structure. + +### Version compatibility + +ANISE guarantees not to change the structure of these kernels between patch versions (e.g., version `0.3.0` and version `0.3.99` are guaranteed to have compatible kernels). However, until version `1.0.0`, the structure _may_ change and if so, the updated version of the default PCA and EPA files will be added to the Nyx Space cloud. + +Since version `0.1.0`, the structure of the kernels has _not_ changed. However, the ANISE version is encoded at the start of each kernel. This is only used if the data set cannot be properly decoded to inform the user of the expected ANISE version and the one that they're trying to load. In other words, although there is a version `0.3` of the PCK08 and PCK11 kernels, the files used in version `0.1.0` are still compatible. + +## Planetary Constant ANISE kernels + +Planetary Constant ANISE (PCA) kernels (or a "data set") include a lookup table for random access via a name or an ID, metadata, and, more importantly, the actual planetary data itself. This data includes gravitational parameters, the shape of the triaxial ellipsoid, phase angle polynomials for the prime meridian, pole right ascension and declination, and more. You'll find all of the specifications in the API documentation: [`PlanetaryData`](https://docs.rs/anise/latest/anise/structure/planetocentric/struct.PlanetaryData.html). + +In the previous tutorials, we focused on fetching the frame information from the Almanac. This operation reads the PCA to return a copy of this information. This is why a PCA is provided in the `latest.dhall` configuration file for an Almanac. + +The Planetary Data structure includes the gravitational data. _However_, the SPICE TPC files contain _either_ ellipsoid definition information _or_ gravitational data. Therefore, to build a PCA, ANISE requires _both_ versions of the TPCs. + +Let's go ahead and build a PCA file from the NAIF `pck0008.tpc` and the `gm_de431.tpc` files. + + +```python +from anise.utils import convert_tpc +convert_tpc? +``` + + + Signature: convert_tpc(pck_file_path, gm_file_path, anise_output_path, overwrite=None) + Docstring: + Converts two KPL/TPC files, one defining the planetary constants as text, and the other defining the gravity parameters, into the PlanetaryDataSet equivalent ANISE file. + KPL/TPC files must be converted into "PCA" (Planetary Constant ANISE) files before being loaded into ANISE. + Type: builtin_function_or_method + + + +```python +convert_tpc("../../data/pck00008.tpc", "../../data/gm_de431.tpc", "demo08.pca", True) +``` + + Skipping 802: no gravity data + Skipping 806: no gravity data + Skipping 9511010: no gravity data + Skipping 514: no gravity data + Skipping 714: no gravity data + Skipping 509: no gravity data + Skipping 707: no gravity data + Skipping 711: no gravity data + Skipping 804: no gravity data + Skipping 2431010: no gravity data + Skipping 506: no gravity data + Skipping 710: no gravity data + Skipping 712: no gravity data + Skipping 612: no gravity data + Skipping 618: no gravity data + Skipping 713: no gravity data + Skipping 803: no gravity data + Skipping 508: no gravity data + Skipping 515: no gravity data + Skipping 715: no gravity data + Skipping 511: no gravity data + Skipping 706: no gravity data + Skipping 516: no gravity data + Skipping 507: no gravity data + Skipping 614: no gravity data + Skipping 808: no gravity data + Skipping 805: no gravity data + Skipping 807: no gravity data + Skipping 513: no gravity data + Skipping 2000216: no gravity data + Skipping 613: no gravity data + Skipping 708: no gravity data + Skipping 512: no gravity data + Skipping 709: no gravity data + Skipping 510: no gravity data + + + Added 49 items + + +We now have a PCA called `demo08.pca` which includes 49 entries. This file is compatible with _any_ machine you run ANISE on, little or big endian (which is _not_ the case of the DAF/BSP or DAF/BPC files). + +Let's load this file in an Almanac. + + +```python +from anise import Almanac +almanac = Almanac("demo08.pca") +almanac +``` + + + + + Almanac: #SPK = 0 #BPC = 0 PlanetaryData with 49 ID mappings and 0 name mappings (@0x559b4af30800) + + + +Since we don't have anything loaded other than these planetary constants, we can't do a whole lot, but we can query the Almanac for the shape and gravitational data by using `frame_info`. + + +```python +from anise.astro.constants import Frames +iau_jupiter_frame = almanac.frame_info(Frames.IAU_JUPITER_FRAME) +iau_jupiter_frame +``` + + + + + body 599 IAU_JUPITER (μ = 126686534.9218008 km^3/s^2, eq. radius = 71492 km, polar radius = 66854 km, f = 0.0648743915403122) (@0x7fe814070310) + + + + +```python +iau_jupiter_frame.mu_km3_s2() +``` + + + + + 126686534.9218008 + + + +### Exercise: Modifying the gravitational parameter of the Earth frame + ++ Make a copy of the `gm_de431.tpc` file and remove everything but the Earth GM. ++ Set the Earth GM to the one used by GMAT: `398600.4415` km^3/s^2 ++ Make a copy of the `pck00008.tpc` file, removing everything except Earth related data. ++ Build a new PCA file using these two new files ++ Load a default Almanac and an empty Almanac where you'll load these two files into, along with the `DE440s.bsp` file. ++ Query the Cartesian state of the Earth at any time of your choosing from both of these Almanac. The state should be the same, since it'll be from the DE440s.bsp. ++ Observe how the frame graviational parameter information of both will differ. ++ Finally, using the `at_epoch` function on both of these state, perform a two-body propagation for both and notice how the graviational parameter affects the result. + +## Euler Parameter ANISE kernel + +Euler parameters are a normalized quaternion. In fact, in the ANISE code in Rust, `EulerParameter` is an alias for `Quaternion`. Euler parameters are useful for defining fixed rotations, e.g. the rotation from the Moon Principal Axes frame to the Moon Mean Earth frame. + +Euler Parameter ANISE kernels (EPA) can be used to store these fixed rotations between frames, and reference them either by their ID or by their name. **They are the equivalent of SPICE's `FK` text files.** + +Until [#175](https://github.com/nyx-space/anise/issues/175), rotation data is _not_ exposed to Python. + +### How to build EPA files + +However, it's possible to convert an FK file into the EPA file using the following function. + + +```python +from anise.utils import convert_fk +convert_fk? +``` + + + Signature: + convert_fk( +  fk_file_path, +  anise_output_path, +  show_comments=None, +  overwrite=None, + ) + Docstring: + Converts a KPL/FK file, that defines frame constants like fixed rotations, and frame name to ID mappings into the EulerParameterDataSet equivalent ANISE file. + KPL/FK files must be converted into "PCA" (Planetary Constant ANISE) files before being loaded into ANISE. + Type: builtin_function_or_method + + +On the Nyx Space Cloud, you'll find the `moon.fk` file, which includes the mapping between the Moon PA and Moon ME frame. The latest Almanac also includes the high precision Moon ME frame. Hence, with default data, you can rotate an object from any frame into the high precision Moon ME frame. + +## Exercise + +Using tutorial 04, compute the azimuth, elevation, and range of the [Shackleton](https://en.wikipedia.org/wiki/Shackleton_(crater)) crater on the Moon to the city of Paris, France. + +Here are the general steps: + +1. Load the latest Almanac, and check (by printing it) that it includes both EPA and PCA data. Else, load the `moon_fk.epa` file from the Nyx Space Cloud using a `MetaFile` with the URL . +2. Define a time series over a year with a granularity of 12 hours. This crater is on the South Pole of the Moon, and its visibility is often below the horizon of an object as far north as Paris. +3. For each epoch, define Paris as an `Orbit` instance from its longitude and latitude (recall that the constants include the mean Earth angular rotation rate), in the IAU_EARTH frame. Also, build the crater in the IAU_MOON frame. +4. Finally, call the `AER` function of the Almanac with each epoch to compute the AER data. Plot it! + + +```python + +``` diff --git a/docs/anise/tutorials/Tutorial 06 - Sun probe Earth angle.ipynb b/docs/anise/tutorials/Tutorial 06 - Sun probe Earth angle.ipynb deleted file mode 100644 index 0ec4f33..0000000 --- a/docs/anise/tutorials/Tutorial 06 - Sun probe Earth angle.ipynb +++ /dev/null @@ -1,620 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "142de0bb-6053-4122-9777-ea9db5dd61ce", - "metadata": {}, - "source": [ - "# ANISE\n", - "\n", - "ANISE is a modern rewrite of NAIF SPICE, written in Rust and providing interfaces to other languages including Python.\n", - "\n", - "Evidently, this tutorial applies to the Python usage of ANISE.\n", - "\n", - "## Goal\n", - "By the end of this tutorial, you should know how to build a data frame containing the Sun probe Earth angle of a given spacecraft BSP and plot that information. Your exercise will be to confirm that this calculation is correct by computing the Sun elevation at nadir below the spacecraft as detailed in tutorial 04.## Loading the latest orientation and planetary data\n", - "\n", - "Let's start by installing ANISE: `pip install anise`" - ] - }, - { - "cell_type": "markdown", - "id": "20351a7f-e6b0-4d0c-ac6b-0cfabb6d76ca", - "metadata": {}, - "source": [ - "## Load a BSP containing a spacecraft trajectory\n", - "\n", - "In this tutorial, we will use the `gmat-hermite.bsp` file which contains some trajectory data build in GMAT and used in ANISE for validation purposes. Although the Sun probe Earth angle _typically_ applies to spacecraft trajectories, the calculation works for any two objects." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "59c15415-c54d-465f-962b-585e870d296b", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "=== SPK #0 ===\n", - "┌─────────────┬──────────────────────┬─────────────┬───────────────────────────────────┬───────────────────────────────────┬────────────┬──────────────────────┐\n", - "│ Name │ Target │ Center │ Start epoch │ End epoch │ Duration │ Interpolation kind │\n", - "├─────────────┼──────────────────────┼─────────────┼───────────────────────────────────┼───────────────────────────────────┼────────────┼──────────────────────┤\n", - "│ SPK_SEGMENT │ body -10000001 J2000 │ Earth J2000 │ 2000-01-01T12:00:32.183927328 TDB │ 2000-01-01T15:20:32.183931556 TDB │ 3 h 20 min │ Hermite Unequal Step │\n", - "└─────────────┴──────────────────────┴─────────────┴───────────────────────────────────┴───────────────────────────────────┴────────────┴──────────────────────┘\n", - "=== SPK #1 ===\n", - "┌────────────────┬─────────────────────────────┬─────────��─────────────────────┬───────────────────────────────────┬───────────────────────────────────┬─────────────┬────────────────────┐\n", - "│ Name │ Target │ Center │ Start epoch │ End epoch │ Duration │ Interpolation kind │\n", - "├────────────────┼─────────────────────────────┼───────────────────────────────┼───────────────────────────────────┼───────────────────��───────────────┼─────────────┼────────────────────┤\n", - "│ DE-0440LE-0440 │ Mercury J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Venus J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Earth-Moon Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Mars Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Jupiter Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Saturn Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Uranus Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Neptune Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Pluto Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Sun J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Moon J2000 │ Earth-Moon Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Earth J2000 │ Earth-Moon Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ body 199 J2000 │ Mercury J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ body 299 J2000 │ Venus J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │\n", - "└────────────────┴─────────────────────────────┴───────────────────────────────┴───────────────────────────────────┴───────────────────────────────────┴─────────────┴────────────────────┘\n" - ] - } - ], - "source": [ - "from anise import MetaAlmanac\n", - "almanac = MetaAlmanac.latest().load(\"../../data/gmat-hermite.bsp\")\n", - "almanac.describe(spk=True)" - ] - }, - { - "cell_type": "markdown", - "id": "9f1d744b-461a-4a38-9d24-b5e989747355", - "metadata": {}, - "source": [ - "We have successfully loaded two BSP files, one with the planetary data and the other with spacecraft data. We now know that this spacecraft has the ID `-10000001`. We can actually query the Almanac for the precise start and stop epochs (returned as hifitime `Epoch` objects) of this ID in our loaded files." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "c85fc5ec-7b54-4c00-9a1a-9722d7a80cd1", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "2000-01-01T11:59:28.000000021 UTC 2000-01-01T15:19:28.000000226 UTC\n" - ] - } - ], - "source": [ - "start_epoch, stop_epoch = almanac.spk_domain(-10000001)\n", - "print(start_epoch, stop_epoch)" - ] - }, - { - "cell_type": "markdown", - "id": "414d228b-1b69-4352-b415-0ebcd71a669c", - "metadata": {}, - "source": [ - "The Sun Probe Earth angle is the angle between a probe and the Sun and the probe the Earth. It allows one to know whether the point exactly nadir (i.e. below) the spacecraft is illuminated by the Sun or not. This is helpful information if the spacecraft carries a visible light camera and needs to take pictures when it's daytime below the spacecraft.\n", - "\n", - "Let's look at the signature of this function." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "40176b6b-dfb8-4d9d-a6c2-93cc5c8cd751", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "\u001b[0;31mSignature:\u001b[0m \u001b[0malmanac\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msun_angle_deg\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mtarget_id\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mobserver_id\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mepoch\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mDocstring:\u001b[0m\n", - "Returns the angle (between 0 and 180 degrees) between the observer and the Sun, and the observer and the target body ID.\n", - "This computes the Sun Probe Earth angle (SPE) if the probe is in a loaded, its ID is the \"observer_id\", and the target is set to its central body.\n", - "\n", - "# Geometry\n", - "If the SPE is greater than 90 degrees, then the celestial object below the probe is in sunlight.\n", - "\n", - "## Sunrise at nadir\n", - "```text\n", - "Sun\n", - " | \\ \n", - " | \\\n", - " | \\\n", - " Obs. -- Target\n", - "```\n", - "## Sun high at nadir\n", - "```text\n", - "Sun\n", - " \\ \n", - " \\ __ θ > 90\n", - " \\ \\\n", - " Obs. ---------- Target\n", - "```\n", - "\n", - "## Sunset at nadir\n", - "```text\n", - " Sun\n", - " / \n", - " / __ θ < 90\n", - " / /\n", - " Obs. -- Target\n", - "```\n", - "\n", - "# Algorithm\n", - "1. Compute the position of the Sun as seen from the observer\n", - "2. Compute the position of the target as seen from the observer\n", - "3. Return the arccosine of the dot product of the norms of these vectors.\n", - "\u001b[0;31mType:\u001b[0m builtin_function_or_method" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "almanac.sun_angle_deg?" - ] - }, - { - "cell_type": "markdown", - "id": "b36bb3ec-8f8a-4298-90ea-2579c3f74ea5", - "metadata": {}, - "source": [ - "One will note that this function is generic to what the \"probe\" SPK ID is (\"observer\") and what its central object should be instead of Earth (\"target\").\n", - "\n", - "The other crucial point here is that this is one of the few functions where the object _ID_ is required instead of a frame. This is because the Almanac will compute everything in the J2000 frame. Don't worry, if you have frame objects instead, you may use the `sun_angle_deg_from_frame` function instead.\n", - "\n", - "Let's see what is the SPE of our spacecraft at the start of the trajectory.\n" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "0366c5b9-cf4d-473e-9385-3a425fd554bf", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "83.87312777296376" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "from anise.astro.constants import CelestialObjects\n", - "almanac.sun_angle_deg(-10000001, CelestialObjects.EARTH, start_epoch)" - ] - }, - { - "cell_type": "markdown", - "id": "c18f00cf-fdc7-4050-b8b4-4f951e6e11e6", - "metadata": {}, - "source": [ - "A angle of less than 90 degrees means that the nadir point is in the darkness. Let's look at the evolution of the SPE over the duration of the trajectory." - ] - }, - { - "cell_type": "markdown", - "id": "8d8a8d44-b433-4955-a0e5-81988decafce", - "metadata": { - "jp-MarkdownHeadingCollapsed": true - }, - "source": [ - "## Package installation for plotting" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "b91b712a-3fe7-41a9-9e7d-78cabb756b9d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Requirement already satisfied: polars[plot] in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (0.20.4)\n", - "Requirement already satisfied: hvplot in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (0.9.1)\n", - "Collecting geoviews\n", - " Downloading geoviews-1.11.0-py2.py3-none-any.whl (511 kB)\n", - "\u001b[2K \u001b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m511.2/511.2 kB\u001b[0m \u001b[31m690.4 kB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m kB/s\u001b[0m eta \u001b[36m0:00:01\u001b[0m:01\u001b[0m\n", - "\u001b[?25hRequirement already satisfied: bokeh>=1.0.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (3.3.3)\n", - "Requirement already satisfied: colorcet>=2 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (3.0.1)\n", - "Requirement already satisfied: holoviews>=1.11.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (1.18.1)\n", - "Requirement already satisfied: pandas in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (2.1.4)\n", - "Requirement already satisfied: numpy>=1.15 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (1.26.3)\n", - "Requirement already satisfied: packaging in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (23.2)\n", - "Requirement already satisfied: panel>=0.11.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (1.3.6)\n", - "Requirement already satisfied: param<3.0,>=1.12.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (2.0.1)\n", - "Collecting cartopy>=0.18.0 (from geoviews)\n", - " Downloading Cartopy-0.22.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (11.9 MB)\n", - "\u001b[2K \u001b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m11.9/11.9 MB\u001b[0m \u001b[31m14.1 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0mm eta \u001b[36m0:00:01\u001b[0m[36m0:00:01\u001b[0m\n", - "\u001b[?25hCollecting shapely (from geoviews)\n", - " Downloading shapely-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.5 MB)\n", - "\u001b[2K \u001b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m2.5/2.5 MB\u001b[0m \u001b[31m54.5 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m31m72.7 MB/s\u001b[0m eta \u001b[36m0:00:01\u001b[0m\n", - "\u001b[?25hCollecting pyproj (from geoviews)\n", - " Downloading pyproj-3.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (8.6 MB)\n", - "\u001b[2K \u001b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m8.6/8.6 MB\u001b[0m \u001b[31m13.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0mm eta \u001b[36m0:00:01\u001b[0m0:01\u001b[0m:01\u001b[0m\n", - "\u001b[?25hRequirement already satisfied: xyzservices in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from geoviews) (2023.10.1)\n", - "Requirement already satisfied: Jinja2>=2.9 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from bokeh>=1.0.0->hvplot) (3.1.2)\n", - "Requirement already satisfied: contourpy>=1 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from bokeh>=1.0.0->hvplot) (1.2.0)\n", - "Requirement already satisfied: pillow>=7.1.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from bokeh>=1.0.0->hvplot) (10.2.0)\n", - "Requirement already satisfied: PyYAML>=3.10 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from bokeh>=1.0.0->hvplot) (6.0.1)\n", - "Requirement already satisfied: tornado>=5.1 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from bokeh>=1.0.0->hvplot) (6.4)\n", - "Collecting matplotlib>=3.4 (from cartopy>=0.18.0->geoviews)\n", - " Downloading matplotlib-3.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (11.6 MB)\n", - "\u001b[2K \u001b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m11.6/11.6 MB\u001b[0m \u001b[31m47.7 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0mm eta \u001b[36m0:00:01\u001b[0m[36m0:00:01\u001b[0m\n", - "\u001b[?25hCollecting pyshp>=2.1 (from cartopy>=0.18.0->geoviews)\n", - " Downloading pyshp-2.3.1-py2.py3-none-any.whl (46 kB)\n", - "\u001b[2K \u001b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m46.5/46.5 kB\u001b[0m \u001b[31m14.8 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hRequirement already satisfied: pyct>=0.4.4 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from colorcet>=2->hvplot) (0.5.0)\n", - "Requirement already satisfied: pyviz-comms>=0.7.4 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from holoviews>=1.11.0->hvplot) (3.0.0)\n", - "Requirement already satisfied: python-dateutil>=2.8.2 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from pandas->hvplot) (2.8.2)\n", - "Requirement already satisfied: pytz>=2020.1 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from pandas->hvplot) (2023.3.post1)\n", - "Requirement already satisfied: tzdata>=2022.1 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from pandas->hvplot) (2023.4)\n", - "Requirement already satisfied: markdown in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (3.5.2)\n", - "Requirement already satisfied: markdown-it-py in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (3.0.0)\n", - "Requirement already satisfied: linkify-it-py in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (2.0.2)\n", - "Requirement already satisfied: mdit-py-plugins in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (0.4.0)\n", - "Requirement already satisfied: requests in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (2.31.0)\n", - "Requirement already satisfied: tqdm>=4.48.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (4.66.1)\n", - "Requirement already satisfied: bleach in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (6.1.0)\n", - "Requirement already satisfied: typing-extensions in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (4.9.0)\n", - "Requirement already satisfied: certifi in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from pyproj->geoviews) (2023.11.17)\n", - "Requirement already satisfied: MarkupSafe>=2.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from Jinja2>=2.9->bokeh>=1.0.0->hvplot) (2.1.3)\n", - "Collecting cycler>=0.10 (from matplotlib>=3.4->cartopy>=0.18.0->geoviews)\n", - " Downloading cycler-0.12.1-py3-none-any.whl (8.3 kB)\n", - "Collecting fonttools>=4.22.0 (from matplotlib>=3.4->cartopy>=0.18.0->geoviews)\n", - " Downloading fonttools-4.47.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.9 MB)\n", - "\u001b[2K \u001b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m4.9/4.9 MB\u001b[0m \u001b[31m49.5 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m31m56.4 MB/s\u001b[0m eta \u001b[36m0:00:01\u001b[0m\n", - "\u001b[?25hCollecting kiwisolver>=1.3.1 (from matplotlib>=3.4->cartopy>=0.18.0->geoviews)\n", - " Downloading kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.4 MB)\n", - "\u001b[2K \u001b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m1.4/1.4 MB\u001b[0m \u001b[31m47.4 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hCollecting pyparsing>=2.3.1 (from matplotlib>=3.4->cartopy>=0.18.0->geoviews)\n", - " Downloading pyparsing-3.1.1-py3-none-any.whl (103 kB)\n", - "\u001b[2K \u001b[38;2;114;156;31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m103.1/103.1 kB\u001b[0m \u001b[31m42.5 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n", - "\u001b[?25hRequirement already satisfied: six>=1.5 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from python-dateutil>=2.8.2->pandas->hvplot) (1.16.0)\n", - "Requirement already satisfied: webencodings in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from bleach->panel>=0.11.0->hvplot) (0.5.1)\n", - "Requirement already satisfied: uc-micro-py in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from linkify-it-py->panel>=0.11.0->hvplot) (1.0.2)\n", - "Requirement already satisfied: mdurl~=0.1 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from markdown-it-py->panel>=0.11.0->hvplot) (0.1.2)\n", - "Requirement already satisfied: charset-normalizer<4,>=2 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from requests->panel>=0.11.0->hvplot) (3.3.2)\n", - "Requirement already satisfied: idna<4,>=2.5 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from requests->panel>=0.11.0->hvplot) (3.6)\n", - "Requirement already satisfied: urllib3<3,>=1.21.1 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from requests->panel>=0.11.0->hvplot) (2.1.0)\n", - "Installing collected packages: shapely, pyshp, pyproj, pyparsing, kiwisolver, fonttools, cycler, matplotlib, cartopy, geoviews\n", - "Successfully installed cartopy-0.22.0 cycler-0.12.1 fonttools-4.47.2 geoviews-1.11.0 kiwisolver-1.4.5 matplotlib-3.8.2 pyparsing-3.1.1 pyproj-3.6.1 pyshp-2.3.1 shapely-2.0.2\n", - "\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m A new release of pip is available: \u001b[0m\u001b[31;49m23.1.2\u001b[0m\u001b[39;49m -> \u001b[0m\u001b[32;49m23.3.2\u001b[0m\n", - "\u001b[1m[\u001b[0m\u001b[34;49mnotice\u001b[0m\u001b[1;39;49m]\u001b[0m\u001b[39;49m To update, run: \u001b[0m\u001b[32;49mpip install --upgrade pip\u001b[0m\n", - "Note: you may need to restart the kernel to use updated packages.\n" - ] - } - ], - "source": [ - "%pip install \"polars[plot]\" hvplot geoviews # geoviews for geographic data" - ] - }, - { - "cell_type": "markdown", - "id": "70a3ddf6-18d7-4f94-94c4-14d73018b2ea", - "metadata": {}, - "source": [ - "## Evolution of SPE over time\n", - "\n", - "We'll plot the change in Sun probe Earth angle over time. We'll also grab the latitude and longitude data so we can plot the position of the spacecraft above the Earth at those times (as a separate plot)." - ] - }, - { - "cell_type": "code", - "execution_count": 38, - "id": "55ec0bf7-2ec8-480f-89be-cd698d0d1e55", - "metadata": {}, - "outputs": [], - "source": [ - "from anise.time import TimeSeries, Unit, Epoch\n", - "from anise.astro import Frame\n", - "from anise.astro.constants import Frames, Orientations\n", - "\n", - "import polars as pl\n", - "from datetime import datetime\n", - "\n", - "def hifitime_to_datetime(e: Epoch) -> datetime:\n", - " return datetime.fromisoformat(str(e).replace(\" UTC\", \"\")[:23])\n", - "\n", - "epochs = []\n", - "spe_deg = []\n", - "lat_deg = []\n", - "long_deg = []\n", - "\n", - "# Let's be sure to load the PCK data, which includes the frame information such as the shape of\n", - "# the ellipsoid and how to compute the rotation of the body fixed frames\n", - "almanac = almanac.load(\"../../data/pck08.pca\")\n", - "\n", - "SC_ID = -10000001\n", - "SC_J2K = Frame(SC_ID, Orientations.J2000)\n", - "\n", - "for epoch in TimeSeries(start_epoch, stop_epoch, Unit.Minute*1, inclusive=True):\n", - " epochs += [hifitime_to_datetime(epoch)]\n", - " spe_deg += [almanac.sun_angle_deg(CelestialObjects.EARTH, SC_ID, epoch)]\n", - " # Grab position of the spacecraft in the IAU Earth frame\n", - " sc_iau_earth = almanac.transform(SC_J2K, Frames.IAU_EARTH_FRAME, epoch)\n", - " lat_deg += [sc_iau_earth.latitude_deg()]\n", - " long_deg += [sc_iau_earth.longitude_deg()]" - ] - }, - { - "cell_type": "code", - "execution_count": 39, - "id": "4f6a34a7-ddae-430d-a138-e3111e7b696c", - "metadata": {}, - "outputs": [], - "source": [ - "# Build the data frame\n", - "df = pl.DataFrame(\n", - " {\n", - " 'Epoch': epochs,\n", - " 'Sun Probe Earth angle (deg)': spe_deg,\n", - " 'Latitude (deg)': lat_deg,\n", - " 'Longitude (deg)': long_deg\n", - " }\n", - ")" - ] - }, - { - "cell_type": "code", - "execution_count": 43, - "id": "614ff2ee-593c-41a6-8932-72671c430ac7", - "metadata": {}, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":Curve [Epoch] (Sun Probe Earth angle (deg),Latitude (deg),Longitude (deg))" - ] - }, - "execution_count": 43, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "p2696" - } - }, - "output_type": "execute_result" - } - ], - "source": [ - "import hvplot.polars\n", - "from bokeh.models.formatters import DatetimeTickFormatter\n", - "\n", - "formatter = DatetimeTickFormatter(days='%d/%m') # Make the world a better place\n", - "\n", - "df.hvplot(x=\"Epoch\", y=\"Sun Probe Earth angle (deg)\", xformatter=formatter, \n", - " title=\"Sun probe Earth angle over time\", hover_cols=[\"Latitude (deg)\", \"Longitude (deg)\"])" - ] - }, - { - "cell_type": "code", - "execution_count": 41, - "id": "abb43538-5c41-4481-96dc-eefcb36764a2", - "metadata": {}, - "outputs": [ - { - "data": {}, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.holoviews_exec.v0+json": "", - "text/html": [ - "
\n", - "
\n", - "
\n", - "" - ], - "text/plain": [ - ":Overlay\n", - " .WMTS.I :WMTS [Longitude,Latitude]\n", - " .Points.I :Points [Longitude (deg),Latitude (deg)] (Epoch,Sun Probe Earth angle (deg))" - ] - }, - "execution_count": 41, - "metadata": { - "application/vnd.holoviews_exec.v0+json": { - "id": "p2520" - } - }, - "output_type": "execute_result" - } - ], - "source": [ - "df.hvplot.points('Longitude (deg)', 'Latitude (deg)', geo=True, color='red', tiles='ESRI', xlim=(0, 360), ylim=(-60, 60), hover_cols=[\"Epoch\", \"Sun Probe Earth angle (deg)\"])" - ] - }, - { - "cell_type": "markdown", - "id": "4e64369b-ae0e-4977-aec7-a0722e9a06a5", - "metadata": {}, - "source": [ - "## Exercise\n", - "\n", - "The goal of this exercise is to show that the SPE is essentially the Sun elevation angle from the position exactly nadir of the vehicle plus 90 degrees (or so, since the Earth isn't a perfect sphere). It also shows you how to combine the different functionality you've seen with the other tutorials to solve similar problem.\n", - "\n", - "_Note:_ this is the `verify_geometry` Rust test, but rebuilt in Python.\n", - "\n", - "1. For the whole spacecraft trajectory, build the locality exactly nadir of it at each epoch (remember that a `TimeSeries` instance can only be used once, so you'll need to rebuild a new one). For this step, initialize a new `Orbit` instance from the latitude and longitude constructor using the position of the spacecraft in the IAU Earth frame as we did above.\n", - "2. At each epoch, grab the state of the Sun as seen from the Earth (both can be in the J2000 frame since the AER computation will transform the states into the correct frames anyway).\n", - "3. Call the azimuth, elevtion, and range function of your loaded Almanac, and store the elevation of the Sun as seen from the point exactly nadir of the spacecraft.\n", - "4. Plot the elevation data in degrees compared to the SPE angle calculated above." - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": ".venv", - "language": "python", - "name": ".venv" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.4" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/anise/tutorials/Tutorial 06 - Sun probe Earth angle.md b/docs/anise/tutorials/Tutorial 06 - Sun probe Earth angle.md new file mode 100644 index 0000000..f020b49 --- /dev/null +++ b/docs/anise/tutorials/Tutorial 06 - Sun probe Earth angle.md @@ -0,0 +1,437 @@ +# ANISE + +ANISE is a modern rewrite of NAIF SPICE, written in Rust and providing interfaces to other languages including Python. + +Evidently, this tutorial applies to the Python usage of ANISE. + +## Goal +By the end of this tutorial, you should know how to build a data frame containing the Sun probe Earth angle of a given spacecraft BSP and plot that information. Your exercise will be to confirm that this calculation is correct by computing the Sun elevation at nadir below the spacecraft as detailed in tutorial 04.## Loading the latest orientation and planetary data + +Let's start by installing ANISE: `pip install anise` + +## Load a BSP containing a spacecraft trajectory + +In this tutorial, we will use the `gmat-hermite.bsp` file which contains some trajectory data build in GMAT and used in ANISE for validation purposes. Although the Sun probe Earth angle _typically_ applies to spacecraft trajectories, the calculation works for any two objects. + + +```python +from anise import MetaAlmanac +almanac = MetaAlmanac.latest().load("../../data/gmat-hermite.bsp") +almanac.describe(spk=True) +``` + + === SPK #0 === + ┌─────────────┬──────────────────────┬─────────────┬───────────────────────────────────┬───────────────────────────────────┬────────────┬──────────────────────┐ + │ Name │ Target │ Center │ Start epoch │ End epoch │ Duration │ Interpolation kind │ + ├─────────────┼──────────────────────┼─────────────┼───────────────────────────────────┼───────────────────────────────────┼────────────┼──────────────────────┤ + │ SPK_SEGMENT │ body -10000001 J2000 │ Earth J2000 │ 2000-01-01T12:00:32.183927328 TDB │ 2000-01-01T15:20:32.183931556 TDB │ 3 h 20 min │ Hermite Unequal Step │ + └─────────────┴──────────────────────┴─────────────┴───────────────────────────────────┴───────────────────────────────────┴────────────┴──────────────────────┘ + === SPK #1 === + ┌────────────────┬─────────────────────────────┬─────────��─────────────────────┬───────────────────────────────────┬───────────────────────────────────┬─────────────┬────────────────────┐ + │ Name │ Target │ Center │ Start epoch │ End epoch │ Duration │ Interpolation kind │ + ├────────────────┼─────────────────────────────┼───────────────────────────────┼───────────────────────────────────┼───────────────────��───────────────┼─────────────┼────────────────────┤ + │ DE-0440LE-0440 │ Mercury J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Venus J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Earth-Moon Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Mars Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Jupiter Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Saturn Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Uranus Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Neptune Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Pluto Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Sun J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Moon J2000 │ Earth-Moon Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Earth J2000 │ Earth-Moon Barycenter J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ body 199 J2000 │ Mercury J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ body 299 J2000 │ Venus J2000 │ 1849-12-26T00:00:00.000046818 TDB │ 2150-01-21T23:59:59.999955683 TDB │ 109600 days │ Chebyshev Triplet │ + └────────────────┴─────────────────────────────┴───────────────────────────────┴───────────────────────────────────┴───────────────────────────────────┴─────────────┴────────────────────┘ + + +We have successfully loaded two BSP files, one with the planetary data and the other with spacecraft data. We now know that this spacecraft has the ID `-10000001`. We can actually query the Almanac for the precise start and stop epochs (returned as hifitime `Epoch` objects) of this ID in our loaded files. + + +```python +start_epoch, stop_epoch = almanac.spk_domain(-10000001) +print(start_epoch, stop_epoch) +``` + + 2000-01-01T11:59:28.000000021 UTC 2000-01-01T15:19:28.000000226 UTC + + +The Sun Probe Earth angle is the angle between a probe and the Sun and the probe the Earth. It allows one to know whether the point exactly nadir (i.e. below) the spacecraft is illuminated by the Sun or not. This is helpful information if the spacecraft carries a visible light camera and needs to take pictures when it's daytime below the spacecraft. + +Let's look at the signature of this function. + + +```python +almanac.sun_angle_deg? +``` + + + Signature: almanac.sun_angle_deg(target_id, observer_id, epoch) + Docstring: + Returns the angle (between 0 and 180 degrees) between the observer and the Sun, and the observer and the target body ID. + This computes the Sun Probe Earth angle (SPE) if the probe is in a loaded, its ID is the "observer_id", and the target is set to its central body. + + # Geometry + If the SPE is greater than 90 degrees, then the celestial object below the probe is in sunlight. + + ## Sunrise at nadir + ```text + Sun + | \ + | \ + | \ + Obs. -- Target + ``` + ## Sun high at nadir + ```text + Sun + \ + \ __ θ > 90 + \ \ + Obs. ---------- Target + ``` + + ## Sunset at nadir + ```text + Sun + / + / __ θ < 90 + / / + Obs. -- Target + ``` + + # Algorithm + 1. Compute the position of the Sun as seen from the observer + 2. Compute the position of the target as seen from the observer + 3. Return the arccosine of the dot product of the norms of these vectors. + Type: builtin_function_or_method + + +One will note that this function is generic to what the "probe" SPK ID is ("observer") and what its central object should be instead of Earth ("target"). + +The other crucial point here is that this is one of the few functions where the object _ID_ is required instead of a frame. This is because the Almanac will compute everything in the J2000 frame. Don't worry, if you have frame objects instead, you may use the `sun_angle_deg_from_frame` function instead. + +Let's see what is the SPE of our spacecraft at the start of the trajectory. + + + +```python +from anise.astro.constants import CelestialObjects +almanac.sun_angle_deg(-10000001, CelestialObjects.EARTH, start_epoch) +``` + + + + + 83.87312777296376 + + + +A angle of less than 90 degrees means that the nadir point is in the darkness. Let's look at the evolution of the SPE over the duration of the trajectory. + +## Package installation for plotting + + +```python +%pip install "polars[plot]" hvplot geoviews # geoviews for geographic data +``` + + Requirement already satisfied: polars[plot] in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (0.20.4) + Requirement already satisfied: hvplot in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (0.9.1) + Collecting geoviews + Downloading geoviews-1.11.0-py2.py3-none-any.whl (511 kB) +  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 511.2/511.2 kB 690.4 kB/s eta 0:00:00 kB/s eta 0:00:01:01 + [?25hRequirement already satisfied: bokeh>=1.0.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (3.3.3) + Requirement already satisfied: colorcet>=2 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (3.0.1) + Requirement already satisfied: holoviews>=1.11.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (1.18.1) + Requirement already satisfied: pandas in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (2.1.4) + Requirement already satisfied: numpy>=1.15 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (1.26.3) + Requirement already satisfied: packaging in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (23.2) + Requirement already satisfied: panel>=0.11.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (1.3.6) + Requirement already satisfied: param<3.0,>=1.12.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from hvplot) (2.0.1) + Collecting cartopy>=0.18.0 (from geoviews) + Downloading Cartopy-0.22.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (11.9 MB) +  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 11.9/11.9 MB 14.1 MB/s eta 0:00:00m eta 0:00:01[36m0:00:01 + [?25hCollecting shapely (from geoviews) + Downloading shapely-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.5 MB) +  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 2.5/2.5 MB 54.5 MB/s eta 0:00:0031m72.7 MB/s eta 0:00:01 + [?25hCollecting pyproj (from geoviews) + Downloading pyproj-3.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (8.6 MB) +  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 8.6/8.6 MB 13.8 MB/s eta 0:00:00m eta 0:00:010:01:01 + [?25hRequirement already satisfied: xyzservices in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from geoviews) (2023.10.1) + Requirement already satisfied: Jinja2>=2.9 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from bokeh>=1.0.0->hvplot) (3.1.2) + Requirement already satisfied: contourpy>=1 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from bokeh>=1.0.0->hvplot) (1.2.0) + Requirement already satisfied: pillow>=7.1.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from bokeh>=1.0.0->hvplot) (10.2.0) + Requirement already satisfied: PyYAML>=3.10 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from bokeh>=1.0.0->hvplot) (6.0.1) + Requirement already satisfied: tornado>=5.1 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from bokeh>=1.0.0->hvplot) (6.4) + Collecting matplotlib>=3.4 (from cartopy>=0.18.0->geoviews) + Downloading matplotlib-3.8.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (11.6 MB) +  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 11.6/11.6 MB 47.7 MB/s eta 0:00:00m eta 0:00:01[36m0:00:01 + [?25hCollecting pyshp>=2.1 (from cartopy>=0.18.0->geoviews) + Downloading pyshp-2.3.1-py2.py3-none-any.whl (46 kB) +  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 46.5/46.5 kB 14.8 MB/s eta 0:00:00 + [?25hRequirement already satisfied: pyct>=0.4.4 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from colorcet>=2->hvplot) (0.5.0) + Requirement already satisfied: pyviz-comms>=0.7.4 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from holoviews>=1.11.0->hvplot) (3.0.0) + Requirement already satisfied: python-dateutil>=2.8.2 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from pandas->hvplot) (2.8.2) + Requirement already satisfied: pytz>=2020.1 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from pandas->hvplot) (2023.3.post1) + Requirement already satisfied: tzdata>=2022.1 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from pandas->hvplot) (2023.4) + Requirement already satisfied: markdown in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (3.5.2) + Requirement already satisfied: markdown-it-py in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (3.0.0) + Requirement already satisfied: linkify-it-py in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (2.0.2) + Requirement already satisfied: mdit-py-plugins in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (0.4.0) + Requirement already satisfied: requests in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (2.31.0) + Requirement already satisfied: tqdm>=4.48.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (4.66.1) + Requirement already satisfied: bleach in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (6.1.0) + Requirement already satisfied: typing-extensions in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from panel>=0.11.0->hvplot) (4.9.0) + Requirement already satisfied: certifi in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from pyproj->geoviews) (2023.11.17) + Requirement already satisfied: MarkupSafe>=2.0 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from Jinja2>=2.9->bokeh>=1.0.0->hvplot) (2.1.3) + Collecting cycler>=0.10 (from matplotlib>=3.4->cartopy>=0.18.0->geoviews) + Downloading cycler-0.12.1-py3-none-any.whl (8.3 kB) + Collecting fonttools>=4.22.0 (from matplotlib>=3.4->cartopy>=0.18.0->geoviews) + Downloading fonttools-4.47.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.9 MB) +  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.9/4.9 MB 49.5 MB/s eta 0:00:0031m56.4 MB/s eta 0:00:01 + [?25hCollecting kiwisolver>=1.3.1 (from matplotlib>=3.4->cartopy>=0.18.0->geoviews) + Downloading kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.4 MB) +  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.4/1.4 MB 47.4 MB/s eta 0:00:00 + [?25hCollecting pyparsing>=2.3.1 (from matplotlib>=3.4->cartopy>=0.18.0->geoviews) + Downloading pyparsing-3.1.1-py3-none-any.whl (103 kB) +  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 103.1/103.1 kB 42.5 MB/s eta 0:00:00 + [?25hRequirement already satisfied: six>=1.5 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from python-dateutil>=2.8.2->pandas->hvplot) (1.16.0) + Requirement already satisfied: webencodings in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from bleach->panel>=0.11.0->hvplot) (0.5.1) + Requirement already satisfied: uc-micro-py in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from linkify-it-py->panel>=0.11.0->hvplot) (1.0.2) + Requirement already satisfied: mdurl~=0.1 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from markdown-it-py->panel>=0.11.0->hvplot) (0.1.2) + Requirement already satisfied: charset-normalizer<4,>=2 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from requests->panel>=0.11.0->hvplot) (3.3.2) + Requirement already satisfied: idna<4,>=2.5 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from requests->panel>=0.11.0->hvplot) (3.6) + Requirement already satisfied: urllib3<3,>=1.21.1 in /home/chris/Workspace/nyx-space/anise/anise-py/.venv/lib64/python3.11/site-packages (from requests->panel>=0.11.0->hvplot) (2.1.0) + Installing collected packages: shapely, pyshp, pyproj, pyparsing, kiwisolver, fonttools, cycler, matplotlib, cartopy, geoviews + Successfully installed cartopy-0.22.0 cycler-0.12.1 fonttools-4.47.2 geoviews-1.11.0 kiwisolver-1.4.5 matplotlib-3.8.2 pyparsing-3.1.1 pyproj-3.6.1 pyshp-2.3.1 shapely-2.0.2 + + [notice] A new release of pip is available: 23.1.2 -> 23.3.2 + [notice] To update, run: pip install --upgrade pip + Note: you may need to restart the kernel to use updated packages. + + +## Evolution of SPE over time + +We'll plot the change in Sun probe Earth angle over time. We'll also grab the latitude and longitude data so we can plot the position of the spacecraft above the Earth at those times (as a separate plot). + + +```python +from anise.time import TimeSeries, Unit, Epoch +from anise.astro import Frame +from anise.astro.constants import Frames, Orientations + +import polars as pl +from datetime import datetime + +def hifitime_to_datetime(e: Epoch) -> datetime: + return datetime.fromisoformat(str(e).replace(" UTC", "")[:23]) + +epochs = [] +spe_deg = [] +lat_deg = [] +long_deg = [] + +# Let's be sure to load the PCK data, which includes the frame information such as the shape of +# the ellipsoid and how to compute the rotation of the body fixed frames +almanac = almanac.load("../../data/pck08.pca") + +SC_ID = -10000001 +SC_J2K = Frame(SC_ID, Orientations.J2000) + +for epoch in TimeSeries(start_epoch, stop_epoch, Unit.Minute*1, inclusive=True): + epochs += [hifitime_to_datetime(epoch)] + spe_deg += [almanac.sun_angle_deg(CelestialObjects.EARTH, SC_ID, epoch)] + # Grab position of the spacecraft in the IAU Earth frame + sc_iau_earth = almanac.transform(SC_J2K, Frames.IAU_EARTH_FRAME, epoch) + lat_deg += [sc_iau_earth.latitude_deg()] + long_deg += [sc_iau_earth.longitude_deg()] +``` + + +```python +# Build the data frame +df = pl.DataFrame( + { + 'Epoch': epochs, + 'Sun Probe Earth angle (deg)': spe_deg, + 'Latitude (deg)': lat_deg, + 'Longitude (deg)': long_deg + } +) +``` + + +```python +import hvplot.polars +from bokeh.models.formatters import DatetimeTickFormatter + +formatter = DatetimeTickFormatter(days='%d/%m') # Make the world a better place + +df.hvplot(x="Epoch", y="Sun Probe Earth angle (deg)", xformatter=formatter, + title="Sun probe Earth angle over time", hover_cols=["Latitude (deg)", "Longitude (deg)"]) +``` + + + + + + +
+
+
+ + + + + +```python +df.hvplot.points('Longitude (deg)', 'Latitude (deg)', geo=True, color='red', tiles='ESRI', xlim=(0, 360), ylim=(-60, 60), hover_cols=["Epoch", "Sun Probe Earth angle (deg)"]) +``` + + + + + + +
+
+
+ + + + +## Exercise + +The goal of this exercise is to show that the SPE is essentially the Sun elevation angle from the position exactly nadir of the vehicle plus 90 degrees (or so, since the Earth isn't a perfect sphere). It also shows you how to combine the different functionality you've seen with the other tutorials to solve similar problem. + +_Note:_ this is the `verify_geometry` Rust test, but rebuilt in Python. + +1. For the whole spacecraft trajectory, build the locality exactly nadir of it at each epoch (remember that a `TimeSeries` instance can only be used once, so you'll need to rebuild a new one). For this step, initialize a new `Orbit` instance from the latitude and longitude constructor using the position of the spacecraft in the IAU Earth frame as we did above. +2. At each epoch, grab the state of the Sun as seen from the Earth (both can be in the J2000 frame since the AER computation will transform the states into the correct frames anyway). +3. Call the azimuth, elevtion, and range function of your loaded Almanac, and store the elevation of the Sun as seen from the point exactly nadir of the spacecraft. +4. Plot the elevation data in degrees compared to the SPE angle calculated above. diff --git a/docs/anise/tutorials/Tutorial 07 - Analysis.ipynb b/docs/anise/tutorials/Tutorial 07 - Analysis.ipynb deleted file mode 100644 index 15e9ac7..0000000 --- a/docs/anise/tutorials/Tutorial 07 - Analysis.ipynb +++ /dev/null @@ -1,1629 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "e4f3a8b2-1c4d-4e5f-9a1b-0e8c7d6a5b3a", - "metadata": {}, - "source": [ - "# Tutorial 07 - Analysis Module\n", - "\n", - "This tutorial covers the new analysis module introduced in ANISE version 0.8.0. We will explore:\n", - "1. **Report Building:** Creating complex scalar reports, generating data, and visualizing it with Polars and Plotly.\n", - "2. **Event Finding:** Finding discrete events like apoapsis, periapsis, and eclipse entry/exit.\n", - "3. **Event Arc Finding:** Detecting continuous events (arcs) like eclipses and sunset periods.\n", - "4. **Ground Contact Finders:** Using `LocationDhallSet` to define ground stations and finding visibility/communication arcs." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "a1b2c3d4-e5f6-7890-1234-567890abcdef", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "=== SPK #0: `../../data/lro.bsp` ===\n", - "┌───────────────┬────────────────┬────────────┬───────────────────────────────────┬───────────────────────────────────┬──────────┬──────────────────────┐\n", - "│ Name │ Target │ Center │ Start epoch │ End epoch │ Duration │ Interpolation kind │\n", - "├───────────────┼────────────────┼────────────┼───────────────────────────────────┼───────────────────────────────────┼──────────┼──────────────────────┤\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-15T00:01:09.183418531 TDB │ 2023-12-16T00:01:09.183445764 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-16T00:01:09.183445766 TDB │ 2023-12-17T00:01:09.183473001 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-17T00:01:09.183473003 TDB │ 2023-12-18T00:01:09.183500753 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-18T00:01:09.183500755 TDB │ 2023-12-19T00:01:09.183528378 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-19T00:01:09.183528380 TDB │ 2023-12-20T00:01:09.183556262 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-20T00:01:09.183556264 TDB │ 2023-12-21T00:01:09.183584149 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-21T00:01:09.183584151 TDB │ 2023-12-22T00:01:09.183612293 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-22T00:01:09.183612295 TDB │ 2023-12-23T00:01:09.183640567 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-23T00:01:09.183640569 TDB │ 2023-12-24T00:01:09.183668972 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-24T00:01:09.183668974 TDB │ 2023-12-25T00:01:09.183697379 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-25T00:01:09.183697381 TDB │ 2023-12-26T00:01:09.183725917 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-26T00:01:09.183725919 TDB │ 2023-12-27T00:01:09.183754585 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-27T00:01:09.183754587 TDB │ 2023-12-28T00:01:09.183783255 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-28T00:01:09.183783257 TDB │ 2023-12-29T00:01:09.183812055 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-29T00:01:09.183812057 TDB │ 2023-12-30T00:01:09.183840858 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-30T00:01:09.183840860 TDB │ 2023-12-31T00:01:09.183869791 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-31T00:01:09.183869793 TDB │ 2024-01-01T00:01:09.183898599 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-01T00:01:09.183898601 TDB │ 2024-01-02T00:01:09.183927664 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-02T00:01:09.183927666 TDB │ 2024-01-03T00:01:09.183956604 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-03T00:01:09.183956606 TDB │ 2024-01-04T00:01:09.183985547 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-04T00:01:09.183985549 TDB │ 2024-01-05T00:01:09.184014491 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-05T00:01:09.184014493 TDB │ 2024-01-06T00:01:09.184043565 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-06T00:01:09.184043567 TDB │ 2024-01-07T00:01:09.184072515 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-07T00:01:09.184072517 TDB │ 2024-01-08T00:01:09.184101466 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-08T00:01:09.184101468 TDB │ 2024-01-09T00:01:09.184130420 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-09T00:01:09.184130422 TDB │ 2024-01-10T00:01:09.184159376 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-10T00:01:09.184159378 TDB │ 2024-01-11T00:01:09.184188206 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-11T00:01:09.184188208 TDB │ 2024-01-12T00:01:09.184216912 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-12T00:01:09.184216914 TDB │ 2024-01-13T00:01:09.184245618 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-13T00:01:09.184245620 TDB │ 2024-01-14T00:01:09.184274327 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-14T00:01:09.184274329 TDB │ 2024-01-15T00:01:09.184302911 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-15T00:01:09.184302913 TDB │ 2024-01-16T00:01:09.184331240 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-16T00:01:09.184331242 TDB │ 2024-01-17T00:01:09.184359700 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-17T00:01:09.184359702 TDB │ 2024-01-18T00:01:09.184387905 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-18T00:01:09.184387907 TDB │ 2024-01-19T00:01:09.184415986 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-19T00:01:09.184415988 TDB │ 2024-01-20T00:01:09.184443940 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-20T00:01:09.184443942 TDB │ 2024-01-21T00:01:09.184471769 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-21T00:01:09.184471771 TDB │ 2024-01-22T00:01:09.184499470 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-22T00:01:09.184499472 TDB │ 2024-01-23T00:01:09.184527175 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-23T00:01:09.184527177 TDB │ 2024-01-24T00:01:09.184554497 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-24T00:01:09.184554499 TDB │ 2024-01-25T00:01:09.184581694 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-25T00:01:09.184581696 TDB │ 2024-01-26T00:01:09.184608766 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-26T00:01:09.184608768 TDB │ 2024-01-27T00:01:09.184635583 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-27T00:01:09.184635585 TDB │ 2024-01-28T00:01:09.184662273 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-28T00:01:09.184662275 TDB │ 2024-01-29T00:01:09.184688710 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-29T00:01:09.184688712 TDB │ 2024-01-30T00:01:09.184714892 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 ��� 2024-01-30T00:01:09.184714894 TDB │ 2024-01-31T00:01:09.184740949 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-31T00:01:09.184740951 TDB │ 2024-02-01T00:01:09.184766751 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-01T00:01:09.184766753 TDB │ 2024-02-02T00:01:09.184792172 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-02T00:01:09.184792174 TDB │ 2024-02-03T00:01:09.184817595 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-03T00:01:09.184817597 TDB │ 2024-02-04T00:01:09.184842507 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-04T00:01:09.184842509 TDB │ 2024-02-05T00:01:09.184867292 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-05T00:01:09.184867294 TDB │ 2024-02-06T00:01:09.184891825 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-06T00:01:09.184891827 TDB │ 2024-02-07T00:01:09.184916102 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-07T00:01:09.184916104 TDB │ 2024-02-08T00:01:09.184939997 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-08T00:01:09.184939999 TDB │ 2024-02-09T00:01:09.184963766 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-09T00:01:09.184963768 TDB │ 2024-02-10T00:01:09.184987025 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-10T00:01:09.184987027 TDB │ 2024-02-11T00:01:09.185010157 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-11T00:01:09.185010159 TDB │ 2024-02-12T00:01:09.185032906 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-12T00:01:09.185032908 TDB │ 2024-02-13T00:01:09.185055402 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-13T00:01:09.185055404 TDB │ 2024-02-14T00:01:09.185077515 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-14T00:01:09.185077517 TDB │ 2024-02-15T00:01:09.185099245 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-15T00:01:09.185099247 TDB │ 2024-02-16T00:01:09.185120850 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-16T00:01:09.185120852 TDB │ 2024-02-17T00:01:09.185141943 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-17T00:01:09.185141945 TDB │ 2024-02-18T00:01:09.185162653 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-18T00:01:09.185162655 TDB │ 2024-02-19T00:01:09.185182982 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-19T00:01:09.185182984 TDB │ 2024-02-20T00:01:09.185203056 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-20T00:01:09.185203058 TDB │ 2024-02-21T00:01:09.185222746 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-21T00:01:09.185222748 TDB │ 2024-02-22T00:01:09.185241926 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-22T00:01:09.185241928 TDB │ 2024-02-23T00:01:09.185260851 TDB │ 1 day �� Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-23T00:01:09.185260853 TDB │ 2024-02-24T00:01:09.185279522 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-24T00:01:09.185279524 TDB │ 2024-02-25T00:01:09.185297554 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-25T00:01:09.185297556 TDB │ 2024-02-26T00:01:09.185315331 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-26T00:01:09.185315333 TDB │ 2024-02-27T00:01:09.185332596 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-27T00:01:09.185332598 TDB │ 2024-02-28T00:01:09.185349607 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-28T00:01:09.185349609 TDB │ 2024-02-29T00:01:09.185366107 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-29T00:01:09.185366109 TDB │ 2024-03-01T00:01:09.185382224 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-01T00:01:09.185382226 TDB │ 2024-03-02T00:01:09.185397958 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-02T00:01:09.185397960 TDB │ 2024-03-03T00:01:09.185413181 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-03T00:01:09.185413183 TDB │ 2024-03-04T00:01:09.185428021 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-04T00:01:09.185428023 TDB │ 2024-03-05T00:01:09.185442478 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-05T00:01:09.185442480 TDB │ 2024-03-06T00:01:09.185456422 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-06T00:01:09.185456424 TDB │ 2024-03-07T00:01:09.185469985 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-07T00:01:09.185469987 TDB │ 2024-03-08T00:01:09.185482908 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-08T00:01:09.185482910 TDB │ 2024-03-09T00:01:09.185495575 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-09T00:01:09.185495577 TDB │ 2024-03-10T00:01:09.185507731 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-10T00:01:09.185507733 TDB │ 2024-03-11T00:01:09.185519632 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-11T00:01:09.185519634 TDB │ 2024-03-12T00:01:09.185530765 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-12T00:01:09.185530767 TDB │ 2024-03-13T00:01:09.185541515 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-13T00:01:09.185541517 TDB │ 2024-03-14T00:01:09.185551881 TDB │ 1 day │ Hermite Unequal Step │\n", - "│ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-14T00:01:09.185551883 TDB │ 2024-03-15T00:01:09.185561735 TDB │ 1 day │ Hermite Unequal Step │\n", - "└───────────────┴────────────────┴────────────┴───────────────────────────────────┴───────────────────────────────────┴──────────┴────���─────────────────┘\n", - "=== SPK #1: `../../data/de440s.bsp` ===\n", - "┌────────────────┬─────────────────────────────┬───────────────────────────────┬───────────────────────────────────┬───────────────────────────────────┬─────────────┬────────────────────┐\n", - "│ Name │ Target │ Center │ Start epoch │ End epoch │ Duration │ Interpolation kind │\n", - "├────────────────┼───────────────────────────��─┼───────────────────────────────┼───────────────────────────────────┼───────────────────────────────────┼─────────────┼────────────────────┤\n", - "│ DE-0440LE-0440 │ Mercury Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Venus Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Earth-Moon Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Mars Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Jupiter Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Saturn Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Uranus Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Neptune Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Pluto Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Sun J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Moon J2000 │ Earth-Moon Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Earth J2000 │ Earth-Moon Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Mercury J2000 │ Mercury Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │\n", - "│ DE-0440LE-0440 │ Venus J2000 │ Venus Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │\n", - "└────────────────┴─────────────────────────────┴───────────────────────────────┴───────────────────────────────────┴───────────────────────────────────┴─────────────┴────────────────────┘\n" - ] - } - ], - "source": [ - "import polars as pl\n", - "import plotly.express as px\n", - "from anise import (\n", - " Almanac,\n", - " Aberration,\n", - " LocationDhallSet,\n", - " LocationDhallSetEntry,\n", - " LocationDataSet,\n", - ")\n", - "from anise.astro import Location, TerrainMask, FrameUid, Frame\n", - "from anise.analysis import Event, Condition\n", - "import anise.analysis as analysis\n", - "from anise.time import Duration, Epoch, TimeSeries, Unit\n", - "from anise.constants import Frames, Orientations\n", - "\n", - "# Load the Almanac with necessary kernels\n", - "almanac = (\n", - " Almanac(\"../../data/de440s.bsp\")\n", - " .load(\"../../data/pck08.pca\")\n", - " .load(\"../../data/lro.bsp\")\n", - ")\n", - "almanac.describe(spk=True)" - ] - }, - { - "cell_type": "markdown", - "id": "b2c3d4e5-f678-9012-3456-7890abcdef12", - "metadata": {}, - "source": [ - "## 1. Report Building\n", - "\n", - "We can define complex scalar expressions and generate reports over a time series. This is useful for analyzing orbital elements, geometric conditions, and custom calculations." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "c3d4e5f6-7890-1234-5678-90abcdef1234", - "metadata": {}, - "outputs": [], - "source": [ - "target_frame = analysis.FrameSpec.Loaded(Frame(-85, Orientations.J2000))\n", - "observer_frame = analysis.FrameSpec.Loaded(Frames.MOON_J2000)\n", - "\n", - "state = analysis.StateSpec(\n", - " target_frame=target_frame,\n", - " observer_frame=observer_frame,\n", - " ab_corr=None,\n", - ")\n", - "\n", - "# Define a VNC (Velocity-Normal-Co-normal) Frame\n", - "vnc = analysis.OrthogonalFrame.XY(\n", - " x=analysis.VectorExpr.Unit(analysis.VectorExpr.Velocity(state)),\n", - " y=analysis.VectorExpr.Unit(analysis.VectorExpr.OrbitalMomentum(state)),\n", - ")\n", - "\n", - "sun_state = analysis.StateSpec(\n", - " target_frame=target_frame,\n", - " observer_frame=analysis.FrameSpec.Loaded(Frames.SUN_J2000),\n", - " ab_corr=Aberration(\"LT\"),\n", - ")\n", - "\n", - "# Project the Earth->Sun vector onto the VNC frame\n", - "proj = analysis.VectorExpr.Project(\n", - " v=analysis.VectorExpr.Negate(\n", - " analysis.VectorExpr.Unit(analysis.VectorExpr.Radius(sun_state))\n", - " ),\n", - " frame=vnc,\n", - " plane=analysis.Plane.XY,\n", - ")\n", - "\n", - "# Custom Local Solar Time (LST) Calculation using fundamental expressions\n", - "earth_sun = analysis.StateSpec(\n", - " target_frame=analysis.FrameSpec.Loaded(Frames.SUN_J2000),\n", - " observer_frame=observer_frame,\n", - " ab_corr=Aberration(\"LT\"),\n", - ")\n", - "u = analysis.VectorExpr.Unit(\n", - " analysis.VectorExpr.CrossProduct(\n", - " a=analysis.VectorExpr.Unit(analysis.VectorExpr.Radius(earth_sun)),\n", - " b=analysis.VectorExpr.Unit(analysis.VectorExpr.OrbitalMomentum(state)),\n", - " )\n", - ")\n", - "v = analysis.VectorExpr.CrossProduct(\n", - " a=analysis.VectorExpr.Unit(analysis.VectorExpr.OrbitalMomentum(state)), b=u\n", - ")\n", - "r = analysis.VectorExpr.Radius(state)\n", - "sin_theta = analysis.ScalarExpr.DotProduct(a=v, b=r)\n", - "cos_theta = analysis.ScalarExpr.DotProduct(a=u, b=r)\n", - "theta = analysis.ScalarExpr.Atan2(y=sin_theta, x=cos_theta)\n", - "lst_prod = analysis.ScalarExpr.Mul(\n", - " a=analysis.ScalarExpr.Mul(a=theta, b=analysis.ScalarExpr.Constant(1.0 / 180.0)),\n", - " b=analysis.ScalarExpr.Constant(12.0),\n", - ")\n", - "lst_add = analysis.ScalarExpr.Add(a=lst_prod, b=analysis.ScalarExpr.Constant(6.0))\n", - "lst = analysis.ScalarExpr.Modulo(v=lst_add, m=analysis.ScalarExpr.Constant(24.0))\n", - "\n", - "# Define scalars to report\n", - "scalars = [\n", - " analysis.ScalarExpr.Element(analysis.OrbitalElement.SemiMajorAxis),\n", - " analysis.ScalarExpr.Element(analysis.OrbitalElement.Eccentricity),\n", - " analysis.ScalarExpr.SolarEclipsePercentage(eclipsing_frame=Frames.VENUS_J2000),\n", - " analysis.ScalarExpr.VectorX(proj),\n", - " analysis.ScalarExpr.VectorY(proj),\n", - " analysis.ScalarExpr.VectorZ(proj),\n", - " analysis.ScalarExpr.LocalSolarTime(),\n", - " lst,\n", - "]\n", - "\n", - "# Set aliases for readable columns\n", - "scalars_with_aliases = [(s, None) for s in scalars]\n", - "scalars_with_aliases[3] = (scalars_with_aliases[3][0], \"proj VNC X\")\n", - "scalars_with_aliases[4] = (scalars_with_aliases[4][0], \"proj VNC Y\")\n", - "scalars_with_aliases[5] = (scalars_with_aliases[5][0], \"proj VNC Z\")\n", - "scalars_with_aliases[7] = (scalars_with_aliases[7][0], \"Custom LST (h)\")\n", - "\n", - "report = analysis.ReportScalars(scalars_with_aliases, state)" - ] - }, - { - "cell_type": "markdown", - "id": "d4e5f678-9012-3456-7890-abcdef123456", - "metadata": {}, - "source": [ - "Now we generate the data over a time series and convert it to a Polars DataFrame." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "e5f67890-1234-5678-90ab-cdef12345678", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "LRO valid from 2023-12-15T00:01:09.183425793 ET to 2024-03-15T00:01:09.185563263 ET\n", - "shape: (5, 9)\n", - "┌───────────┬───────────┬───────────┬───────────┬───┬───────────┬───────────┬───────────┬──────────┐\n", - "│ Epoch ┆ proj VNC ┆ local ┆ solar ┆ … ┆ Custom ┆ proj VNC ┆ SemiMajor ┆ proj VNC │\n", - "│ --- ┆ X ┆ solar ┆ eclipse ┆ ┆ LST (h) ┆ Y ┆ Axis (km) ┆ Z │\n", - "│ datetime[ ┆ --- ┆ time (h) ┆ due to ┆ ┆ --- ┆ --- ┆ --- ┆ --- │\n", - "│ μs] ┆ f64 ┆ --- ┆ Venus ┆ ┆ f64 ┆ f64 ┆ f64 ┆ f64 │\n", - "│ ┆ ┆ f64 ┆ Bar… ┆ ┆ ┆ ┆ ┆ │\n", - "│ ┆ ┆ ┆ --- ┆ ┆ ┆ ┆ ┆ │\n", - "│ ┆ ┆ ┆ f64 ┆ ┆ ┆ ┆ ┆ │\n", - "╞═══════════╪═══════════╪═══════════╪═══════════╪═══╪═══════════╪═══════════╪═══════════╪══════════╡\n", - "│ 2024-03-1 ┆ -0.382568 ┆ 21.345665 ┆ 0.0 ┆ … ┆ 21.345665 ┆ 0.753565 ┆ 1827.7867 ┆ -0.53235 │\n", - "│ 3 12:01:0 ┆ ┆ ┆ ┆ ┆ ┆ ┆ 98 ┆ 5 │\n", - "│ 9.185563 ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ │\n", - "│ 2024-03-1 ┆ -0.055965 ┆ 23.400991 ┆ 0.0 ┆ … ┆ 23.400991 ┆ 0.75363 ┆ 1828.0145 ┆ -0.65309 │\n", - "│ 3 12:11:0 ┆ ┆ ┆ ┆ ┆ ┆ ┆ 17 ┆ 6 │\n", - "│ 9.185563 ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ │\n", - "│ 2024-03-1 ┆ 0.287843 ┆ 1.474358 ┆ 0.0 ┆ … ┆ 1.474358 ┆ 0.753692 ┆ 1827.7419 ┆ -0.58883 │\n", - "│ 3 12:21:0 ┆ ┆ ┆ ┆ ┆ ┆ ┆ 3 ┆ 2 │\n", - "│ 9.185563 ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ │\n", - "│ 2024-03-1 ┆ 0.550096 ┆ 3.559474 ┆ 0.0 ┆ … ┆ 3.559474 ┆ 0.753795 ┆ 1827.3555 ┆ -0.35611 │\n", - "│ 3 12:31:0 ┆ ┆ ┆ ┆ ┆ ┆ ┆ 14 ┆ 7 │\n", - "│ 9.185563 ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ │\n", - "│ 2024-03-1 ┆ 0.654768 ┆ 5.647546 ┆ 0.0 ┆ … ┆ 5.647546 ┆ 0.753964 ┆ 1827.3668 ┆ -0.02131 │\n", - "│ 3 12:41:0 ┆ ┆ ┆ ┆ ┆ ┆ ┆ 86 ┆ │\n", - "│ 9.185563 ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ │\n", - "└───────────┴───────────┴───────────┴───────────┴───┴───────────┴───────────┴───────────┴──────────┘\n" - ] - } - ], - "source": [ - "lro_start, lro_stop = almanac.spk_domain(-85)\n", - "print(f\"LRO valid from {lro_start} to {lro_stop}\")\n", - "series = TimeSeries(\n", - " lro_stop - Unit.Day * 1.5,\n", - " lro_stop,\n", - " Unit.Minute * 10, # Higher resolution for plotting\n", - " inclusive=True,\n", - ")\n", - "\n", - "data = almanac.report_scalars(report, series)\n", - "\n", - "# Convert to Polars DataFrame\n", - "rows = []\n", - "for epoch_str, val_dict in data.items():\n", - " row = {\"Epoch\": epoch_str}\n", - " row.update(val_dict)\n", - " rows.append(row)\n", - "\n", - "df = pl.DataFrame(rows)\n", - "# Convert Epoch string to datetime for better plotting\n", - "df = df.with_columns(pl.col(\"Epoch\").str.to_datetime()).sort(\"Epoch\")\n", - "print(df.head())" - ] - }, - { - "cell_type": "markdown", - "id": "f6789012-3456-7890-abcd-ef1234567890", - "metadata": {}, - "source": [ - "Let's visualize the Custom LST vs Time." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "78901234-5678-90ab-cdef-1234567890ab", - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.plotly.v1+json": { - "config": { - "plotlyServerURL": "https://plot.ly" - }, - "data": [ - { - "hovertemplate": "Epoch=%{x}
Custom LST (h)=%{y}", - "legendgroup": "", - "line": { - "color": "#636efa", - "dash": "solid" - }, - "marker": { - "symbol": "circle" - }, - "mode": "lines", - "name": "", - "orientation": "v", - "showlegend": false, - "type": "scatter", - "x": [ - "2024-03-13T12:01:09.185563", - "2024-03-13T12:11:09.185563", - "2024-03-13T12:21:09.185563", - "2024-03-13T12:31:09.185563", - "2024-03-13T12:41:09.185563", - "2024-03-13T12:51:09.185563", - "2024-03-13T13:01:09.185563", - "2024-03-13T13:11:09.185563", - "2024-03-13T13:21:09.185563", - "2024-03-13T13:31:09.185563", - "2024-03-13T13:41:09.185563", - "2024-03-13T13:51:09.185563", - "2024-03-13T14:01:09.185563", - "2024-03-13T14:11:09.185563", - "2024-03-13T14:21:09.185563", - "2024-03-13T14:31:09.185563", - "2024-03-13T14:41:09.185563", - "2024-03-13T14:51:09.185563", - "2024-03-13T15:01:09.185563", - "2024-03-13T15:11:09.185563", - "2024-03-13T15:21:09.185563", - "2024-03-13T15:31:09.185563", - "2024-03-13T15:41:09.185563", - "2024-03-13T15:51:09.185563", - "2024-03-13T16:01:09.185563", - "2024-03-13T16:11:09.185563", - "2024-03-13T16:21:09.185563", - "2024-03-13T16:31:09.185563", - "2024-03-13T16:41:09.185563", - "2024-03-13T16:51:09.185563", - "2024-03-13T17:01:09.185563", - "2024-03-13T17:11:09.185563", - "2024-03-13T17:21:09.185563", - "2024-03-13T17:31:09.185563", - "2024-03-13T17:41:09.185563", - "2024-03-13T17:51:09.185563", - "2024-03-13T18:01:09.185563", - "2024-03-13T18:11:09.185563", - "2024-03-13T18:21:09.185563", - "2024-03-13T18:31:09.185563", - "2024-03-13T18:41:09.185563", - "2024-03-13T18:51:09.185563", - "2024-03-13T19:01:09.185563", - "2024-03-13T19:11:09.185563", - "2024-03-13T19:21:09.185563", - "2024-03-13T19:31:09.185563", - "2024-03-13T19:41:09.185563", - "2024-03-13T19:51:09.185563", - "2024-03-13T20:01:09.185563", - "2024-03-13T20:11:09.185563", - "2024-03-13T20:21:09.185563", - "2024-03-13T20:31:09.185563", - "2024-03-13T20:41:09.185563", - "2024-03-13T20:51:09.185563", - "2024-03-13T21:01:09.185563", - "2024-03-13T21:11:09.185563", - "2024-03-13T21:21:09.185563", - "2024-03-13T21:31:09.185563", - "2024-03-13T21:41:09.185563", - "2024-03-13T21:51:09.185563", - "2024-03-13T22:01:09.185563", - "2024-03-13T22:11:09.185563", - "2024-03-13T22:21:09.185563", - "2024-03-13T22:31:09.185563", - "2024-03-13T22:41:09.185563", - "2024-03-13T22:51:09.185563", - "2024-03-13T23:01:09.185563", - "2024-03-13T23:11:09.185563", - "2024-03-13T23:21:09.185563", - "2024-03-13T23:31:09.185563", - "2024-03-13T23:41:09.185563", - "2024-03-13T23:51:09.185563", - "2024-03-14T00:01:09.185563", - "2024-03-14T00:11:09.185563", - "2024-03-14T00:21:09.185563", - "2024-03-14T00:31:09.185563", - "2024-03-14T00:41:09.185563", - "2024-03-14T00:51:09.185563", - "2024-03-14T01:01:09.185563", - "2024-03-14T01:11:09.185563", - "2024-03-14T01:21:09.185563", - "2024-03-14T01:31:09.185563", - "2024-03-14T01:41:09.185563", - "2024-03-14T01:51:09.185563", - "2024-03-14T02:01:09.185563", - "2024-03-14T02:11:09.185563", - "2024-03-14T02:21:09.185563", - "2024-03-14T02:31:09.185563", - "2024-03-14T02:41:09.185563", - "2024-03-14T02:51:09.185563", - "2024-03-14T03:01:09.185563", - "2024-03-14T03:11:09.185563", - "2024-03-14T03:21:09.185563", - "2024-03-14T03:31:09.185563", - "2024-03-14T03:41:09.185563", - "2024-03-14T03:51:09.185563", - "2024-03-14T04:01:09.185563", - "2024-03-14T04:11:09.185563", - "2024-03-14T04:21:09.185563", - "2024-03-14T04:31:09.185563", - "2024-03-14T04:41:09.185563", - "2024-03-14T04:51:09.185563", - "2024-03-14T05:01:09.185563", - "2024-03-14T05:11:09.185563", - "2024-03-14T05:21:09.185563", - "2024-03-14T05:31:09.185563", - "2024-03-14T05:41:09.185563", - "2024-03-14T05:51:09.185563", - "2024-03-14T06:01:09.185563", - "2024-03-14T06:11:09.185563", - "2024-03-14T06:21:09.185563", - "2024-03-14T06:31:09.185563", - "2024-03-14T06:41:09.185563", - "2024-03-14T06:51:09.185563", - "2024-03-14T07:01:09.185563", - "2024-03-14T07:11:09.185563", - "2024-03-14T07:21:09.185563", - "2024-03-14T07:31:09.185563", - "2024-03-14T07:41:09.185563", - "2024-03-14T07:51:09.185563", - "2024-03-14T08:01:09.185563", - "2024-03-14T08:11:09.185563", - "2024-03-14T08:21:09.185563", - "2024-03-14T08:31:09.185563", - "2024-03-14T08:41:09.185563", - "2024-03-14T08:51:09.185563", - "2024-03-14T09:01:09.185563", - "2024-03-14T09:11:09.185563", - "2024-03-14T09:21:09.185563", - "2024-03-14T09:31:09.185563", - "2024-03-14T09:41:09.185563", - "2024-03-14T09:51:09.185563", - "2024-03-14T10:01:09.185563", - "2024-03-14T10:11:09.185563", - "2024-03-14T10:21:09.185563", - "2024-03-14T10:31:09.185563", - "2024-03-14T10:41:09.185563", - "2024-03-14T10:51:09.185563", - "2024-03-14T11:01:09.185563", - "2024-03-14T11:11:09.185563", - "2024-03-14T11:21:09.185563", - "2024-03-14T11:31:09.185563", - "2024-03-14T11:41:09.185563", - "2024-03-14T11:51:09.185563", - "2024-03-14T12:01:09.185563", - "2024-03-14T12:11:09.185563", - "2024-03-14T12:21:09.185563", - "2024-03-14T12:31:09.185563", - "2024-03-14T12:41:09.185563", - "2024-03-14T12:51:09.185563", - "2024-03-14T13:01:09.185563", - "2024-03-14T13:11:09.185563", - "2024-03-14T13:21:09.185563", - "2024-03-14T13:31:09.185563", - "2024-03-14T13:41:09.185563", - "2024-03-14T13:51:09.185563", - "2024-03-14T14:01:09.185563", - "2024-03-14T14:11:09.185563", - "2024-03-14T14:21:09.185563", - "2024-03-14T14:31:09.185563", - "2024-03-14T14:41:09.185563", - "2024-03-14T14:51:09.185563", - "2024-03-14T15:01:09.185563", - "2024-03-14T15:11:09.185563", - "2024-03-14T15:21:09.185563", - "2024-03-14T15:31:09.185563", - "2024-03-14T15:41:09.185563", - "2024-03-14T15:51:09.185563", - "2024-03-14T16:01:09.185563", - "2024-03-14T16:11:09.185563", - "2024-03-14T16:21:09.185563", - "2024-03-14T16:31:09.185563", - "2024-03-14T16:41:09.185563", - "2024-03-14T16:51:09.185563", - "2024-03-14T17:01:09.185563", - "2024-03-14T17:11:09.185563", - "2024-03-14T17:21:09.185563", - "2024-03-14T17:31:09.185563", - "2024-03-14T17:41:09.185563", - "2024-03-14T17:51:09.185563", - "2024-03-14T18:01:09.185563", - "2024-03-14T18:11:09.185563", - "2024-03-14T18:21:09.185563", - "2024-03-14T18:31:09.185563", - "2024-03-14T18:41:09.185563", - "2024-03-14T18:51:09.185563", - "2024-03-14T19:01:09.185563", - "2024-03-14T19:11:09.185563", - "2024-03-14T19:21:09.185563", - "2024-03-14T19:31:09.185563", - "2024-03-14T19:41:09.185563", - "2024-03-14T19:51:09.185563", - "2024-03-14T20:01:09.185563", - "2024-03-14T20:11:09.185563", - "2024-03-14T20:21:09.185563", - "2024-03-14T20:31:09.185563", - "2024-03-14T20:41:09.185563", - "2024-03-14T20:51:09.185563", - "2024-03-14T21:01:09.185563", - "2024-03-14T21:11:09.185563", - "2024-03-14T21:21:09.185563", - "2024-03-14T21:31:09.185563", - "2024-03-14T21:41:09.185563", - "2024-03-14T21:51:09.185563", - "2024-03-14T22:01:09.185563", - "2024-03-14T22:11:09.185563", - "2024-03-14T22:21:09.185563", - "2024-03-14T22:31:09.185563", - "2024-03-14T22:41:09.185563", - "2024-03-14T22:51:09.185563", - "2024-03-14T23:01:09.185563", - "2024-03-14T23:11:09.185563", - "2024-03-14T23:21:09.185563", - "2024-03-14T23:31:09.185563", - "2024-03-14T23:41:09.185563", - "2024-03-14T23:51:09.185563", - "2024-03-15T00:01:09.185563" - ], - "xaxis": "x", - "y": { - "bdata": "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", - "dtype": "f8" - }, - "yaxis": "y" - } - ], - "layout": { - "legend": { - "tracegroupgap": 0 - }, - "template": { - "data": { - "bar": [ - { - "error_x": { - "color": "#2a3f5f" - }, - "error_y": { - "color": "#2a3f5f" - }, - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "bar" - } - ], - "barpolar": [ - { - "marker": { - "line": { - "color": "#E5ECF6", - "width": 0.5 - }, - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "barpolar" - } - ], - "carpet": [ - { - "aaxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "baxis": { - "endlinecolor": "#2a3f5f", - "gridcolor": "white", - "linecolor": "white", - "minorgridcolor": "white", - "startlinecolor": "#2a3f5f" - }, - "type": "carpet" - } - ], - "choropleth": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "choropleth" - } - ], - "contour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "contour" - } - ], - "contourcarpet": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "contourcarpet" - } - ], - "heatmap": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "heatmap" - } - ], - "histogram": [ - { - "marker": { - "pattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - } - }, - "type": "histogram" - } - ], - "histogram2d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2d" - } - ], - "histogram2dcontour": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "histogram2dcontour" - } - ], - "mesh3d": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "type": "mesh3d" - } - ], - "parcoords": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "parcoords" - } - ], - "pie": [ - { - "automargin": true, - "type": "pie" - } - ], - "scatter": [ - { - "fillpattern": { - "fillmode": "overlay", - "size": 10, - "solidity": 0.2 - }, - "type": "scatter" - } - ], - "scatter3d": [ - { - "line": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatter3d" - } - ], - "scattercarpet": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattercarpet" - } - ], - "scattergeo": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergeo" - } - ], - "scattergl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattergl" - } - ], - "scattermap": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattermap" - } - ], - "scattermapbox": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scattermapbox" - } - ], - "scatterpolar": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolar" - } - ], - "scatterpolargl": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterpolargl" - } - ], - "scatterternary": [ - { - "marker": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "type": "scatterternary" - } - ], - "surface": [ - { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - }, - "colorscale": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "type": "surface" - } - ], - "table": [ - { - "cells": { - "fill": { - "color": "#EBF0F8" - }, - "line": { - "color": "white" - } - }, - "header": { - "fill": { - "color": "#C8D4E3" - }, - "line": { - "color": "white" - } - }, - "type": "table" - } - ] - }, - "layout": { - "annotationdefaults": { - "arrowcolor": "#2a3f5f", - "arrowhead": 0, - "arrowwidth": 1 - }, - "autotypenumbers": "strict", - "coloraxis": { - "colorbar": { - "outlinewidth": 0, - "ticks": "" - } - }, - "colorscale": { - "diverging": [ - [ - 0, - "#8e0152" - ], - [ - 0.1, - "#c51b7d" - ], - [ - 0.2, - "#de77ae" - ], - [ - 0.3, - "#f1b6da" - ], - [ - 0.4, - "#fde0ef" - ], - [ - 0.5, - "#f7f7f7" - ], - [ - 0.6, - "#e6f5d0" - ], - [ - 0.7, - "#b8e186" - ], - [ - 0.8, - "#7fbc41" - ], - [ - 0.9, - "#4d9221" - ], - [ - 1, - "#276419" - ] - ], - "sequential": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ], - "sequentialminus": [ - [ - 0, - "#0d0887" - ], - [ - 0.1111111111111111, - "#46039f" - ], - [ - 0.2222222222222222, - "#7201a8" - ], - [ - 0.3333333333333333, - "#9c179e" - ], - [ - 0.4444444444444444, - "#bd3786" - ], - [ - 0.5555555555555556, - "#d8576b" - ], - [ - 0.6666666666666666, - "#ed7953" - ], - [ - 0.7777777777777778, - "#fb9f3a" - ], - [ - 0.8888888888888888, - "#fdca26" - ], - [ - 1, - "#f0f921" - ] - ] - }, - "colorway": [ - "#636efa", - "#EF553B", - "#00cc96", - "#ab63fa", - "#FFA15A", - "#19d3f3", - "#FF6692", - "#B6E880", - "#FF97FF", - "#FECB52" - ], - "font": { - "color": "#2a3f5f" - }, - "geo": { - "bgcolor": "white", - "lakecolor": "white", - "landcolor": "#E5ECF6", - "showlakes": true, - "showland": true, - "subunitcolor": "white" - }, - "hoverlabel": { - "align": "left" - }, - "hovermode": "closest", - "mapbox": { - "style": "light" - }, - "paper_bgcolor": "white", - "plot_bgcolor": "#E5ECF6", - "polar": { - "angularaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "radialaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "scene": { - "xaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "yaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - }, - "zaxis": { - "backgroundcolor": "#E5ECF6", - "gridcolor": "white", - "gridwidth": 2, - "linecolor": "white", - "showbackground": true, - "ticks": "", - "zerolinecolor": "white" - } - }, - "shapedefaults": { - "line": { - "color": "#2a3f5f" - } - }, - "ternary": { - "aaxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "baxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - }, - "bgcolor": "#E5ECF6", - "caxis": { - "gridcolor": "white", - "linecolor": "white", - "ticks": "" - } - }, - "title": { - "x": 0.05 - }, - "xaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - }, - "yaxis": { - "automargin": true, - "gridcolor": "white", - "linecolor": "white", - "ticks": "", - "title": { - "standoff": 15 - }, - "zerolinecolor": "white", - "zerolinewidth": 2 - } - } - }, - "title": { - "text": "Custom Local Solar Time over Time" - }, - "xaxis": { - "anchor": "y", - "domain": [ - 0, - 1 - ], - "title": { - "text": "Epoch" - } - }, - "yaxis": { - "anchor": "x", - "domain": [ - 0, - 1 - ], - "title": { - "text": "Custom LST (h)" - } - } - } - }, - "image/png": "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qSZAo0iqQ4DvJwESyAQC1JEESIAESIAESIAESIAESCCABGgUCWCgUCQSMJsApScBEiABEiABEiABEiABEiABMwjQKGJGOFHKoBKgXCRAAiRAAiRAAiRAAiRAAiRAAsYSoFHE2KCrfcH5RhIgARIgARIgARIgARIgARIgARJIJwI0iiQOTZ4lARIgARIgARIgARIgARIgARIgARJIcwJZImmuIdUjARIgARIgARIgARIgARIgARIgARIQEUKIJ8CRIvFEeEwCJEACJEACJEACJEACJEACJGA+AWpAAhUgQKNIBSDxEhIgARIgARIgARIgARIgARIIMgHKRgIkUDUCNIpUjRvvIgESIAESIAESIAESIAESSA0BvpUESIAEqo0AjSLVhpIPIgESIAESIAESIAESIIHqJsDnkQAJkAAJ1CQBGkVqki6fTQIkQAIkQAIkQAIkUHECvJIESIAESIAEapkAjSK1DJyvIwESIAESIAES2D0Cp4+eKO9++OXuPSQAd1MEEiABEiABEiCB1BOgUST1YUAJSIAESIAESMAoAstWrpGefYfv5J55eUp5evA8CZAACZAACZAACQSSAI0igQwWCkUCJEACJGAugcyR/Nupj8msj5723HmnD8gc5akpCZAACZAACZBAWhCgUSQtgpFKkAAJkECKCPC1JJCAwJ0PPS8YNTL55Sky6LdXSd9hl8p9j70i0WiJd/XaXzbK+Il/lcOGXCCDz71abnvwOdm46VfPHztTPvpKxlx5j/QZNFbOvfhWmTFnEU7bDveff9ntcuCA0XLy+dfJv979r32ePyRAAiRAAiRAAiRQGQI0ilSGFq8lARLIaAJUngRIoOIEHn7mddm+o1ief/gGuXfihTLloy/l2Ven2g8oLS2VUX+8Sxrm1pN/PXWL/OWWSwRGjrFX32v742eKZRC5edJkOePEY+WDlyfJiLNOkL32bAQv2z3x/Fvym5P7yQevTJLzzzxBrrv9cflx+Wrbjz8kQAIkQAIkQAIkUFECNIpUlBSvI4HMIkBtSYAESGCXBHofPypmXZH5i5Z593Tt2EZGnTNEGjdsIL336Sajf3ui/OsdZzTH/76ZJT+vWS/XX/o7adF8T+nUvpXcfOUImbvgR5k5b7H9jL9bBpThZw6Ufkf1ltwG9eTYww+QNi2b2X74GdC3jwzoe7D9/FMGHil5XdrJF9Nnw4uOBEiABEiABEiABCpMgEaRCqPihelLgJqRAAmQAAlUhcAjd1wuT066ynNtWzX3HtOhbQtvEMuPaQAABShJREFUHzs9uneUn1Y4IzkwoqNb57aSE86Gl+3q16srHdq0kIVLVtjHC5eukF55nez9RD/drfv959tb924t2u4/xX0SIAESIAESIAES2CUBGkV2iSjNLqA6JEACJEACJFBNBPockC+HHFDgufr19vCeHIlEvX3sYMqMuuLiCE4ldDnhsH2+tKTU3pb3AyNKeX48TwIkQAIkQAIkQAIVJZDWRpGKQuB1JEACJEACJEAC1Utg0Y8rYx44p3CpYCRJKBSSju1aSuGiZVLsM5xsLdomS5atkk7tW9r3dbCumV24xN7nDwmQAAmQAAmQAAnsikBV/WkUqSo53kcCJEACJEACJFAugUVLV8iTL7xtf1Fmy69F8tQL78iwIcfY1x96YE9p0jhX/nzfZHttkcWWAWXCnU/Kfj26SEG3DvY1Z5/az77n4/99L7h/+sxCwXW2J39IgARIgARIILMJUPtqJECjSDXC5KNIgARIgARIIJMIxC+0is/wqv4nDzxSvv2hUA4/6UI58bxrpP/RB8pvh/6f7Z0Tzpan77tGMJ3mjDETZfilt0udnBy5/6aLbX/8nHT8EXLpqNMFX5k54qSLZOxV98r6jVvgRUcCJEACJJBRBKgsCdQsARpFapYvn04CJEACJEACaUcA02BmffS0xLvzTh/g6VpaUiJ/ufUS+5oPX7lPxo0cJtnZZdWOvZs1kZuuGCEfv3a/7W67dpQ9esR7gLUzbPDRMvmBa+X795+QL9562P6KjXVaXn50ogw8tg92PXffTReJ//2eB3dIgARIwCQClJUESKDWCWTV+hv5QhIgARIgARIggbQnUJp8ndS0158KkgAJ7JoAryABEiCBIBCgUSQIoUAZSIAESIAESIAESIAE0pkAdSMBEiABEggoARpFAhowFIsESIAESIAESIAEzCRAqUmABEiABEjAHAI0ipgTVpSUBEiABEiABIwgcOWFZ8nEPw43QtbdFpIPIAESIAESIAESMJoAjSJGBx+FJwESIAESIIHaI8A3kQAJkAAJkAAJkEC6EaBRJN1ClPqQAAmQAAlUBwE+gwRIgARIgARIgARIIAMI0CiSAYFMFUmABEggOQH6kgAJkAAJkAAJkAAJkEBmEqBRJDPDnVqTQOYSoOYkQAIkQAIkQAIkQAIkQAIk4BKgUcQFwQ0JpCMB6kQCJEACJEACJEACJEACJEACJFA+ARpFymdDH7MIUFoSIAESIAESIAESIAESIAESIAESqBQBGkUqhSsoF1MOEiABEiABEiABEiABEiABEiABEiCB3SUQfKPI7mrI+0mABEiABEiABEiABEiABEiABEiABIJPIAUS0iiSAuh8JQmQAAmQAAmQAAmQAAmQAAmQQGYToPbBIECjSDDCgVKQAAmQAAmQAAmQAAmQAAmQQLoSoF4kEFgCNIoENmgoGAmQAAmQAAmQAAmQAAmQgHkEKDEJkIBJBGgUMSm0KCsJkAAJkAAJkAAJkAAJBIkAZSEBEiABwwnQKGJ4AFJ8EiABEiABEiABEiCB2iHAt5AACZAACaQfARpF0i9MqREJkAAJkAAJkAAJ7C4B3k8CJEACJEACGUGARpGMCGYqSQIkQAIkQAIkUD4B+pAACZAACZAACWQqARpFMjXkqTcJkAAJkEBmEqDWJEACJEACJEACJEACHgEaRTwU3CEBEiABEkg3AtSHBEiABEiABEiABEiABJIRoFEkGR36kQAJkIA5BCgpCZAACZAACZAACZAACZBAJQn8PwAAAP//hdP6RwAAAAZJREFUAwBBTMqwFj6s2gAAAABJRU5ErkJggg==" - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "fig = px.line(df, x=\"Epoch\", y=\"Custom LST (h)\", title=\"Custom Local Solar Time over Time\")\n", - "fig.show()" - ] - }, - { - "cell_type": "markdown", - "id": "89012345-6789-0abc-def1-234567890abc", - "metadata": {}, - "source": [ - "## 2. Event Finding\n", - "\n", - "We can search for discrete events such as apoapsis, periapsis, or specific geometric conditions." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "90123456-7890-abcd-ef12-34567890abcd", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Searching events from 2023-12-15T00:01:09.183425793 ET to 2023-12-17T00:01:09.183425793 ET\n", - "Found 25 apoapsis events.\n", - "Found 24 periapsis events.\n", - "Apoapsis 1: 2023-12-15T00:07:04.599486254 ET\n", - "Apoapsis 2: 2023-12-15T02:05:09.311922236 ET\n", - "Apoapsis 3: 2023-12-15T04:03:14.118872647 ET\n" - ] - } - ], - "source": [ - "lro_frame = Frame(-85, 1) # LRO NAIF ID\n", - "lro_state_spec = analysis.StateSpec(\n", - " target_frame=analysis.FrameSpec.Loaded(lro_frame),\n", - " observer_frame=analysis.FrameSpec.Loaded(Frames.MOON_J2000),\n", - " ab_corr=None,\n", - ")\n", - "\n", - "apolune = Event.apoapsis()\n", - "perilune = Event.periapsis()\n", - "\n", - "start_epoch, end_epoch = almanac.spk_domain(-85)\n", - "# Search over a shorter window\n", - "search_end = start_epoch + Unit.Day * 2\n", - "\n", - "print(f\"Searching events from {start_epoch} to {search_end}\")\n", - "\n", - "apo_events = almanac.report_events(lro_state_spec, apolune, start_epoch, search_end)\n", - "print(f\"Found {len(apo_events)} apoapsis events.\")\n", - "\n", - "peri_events = almanac.report_events(lro_state_spec, perilune, start_epoch, search_end)\n", - "print(f\"Found {len(peri_events)} periapsis events.\")\n", - "\n", - "# Print first few events\n", - "for i, event in enumerate(apo_events[:3]):\n", - " print(f\"Apoapsis {i+1}: {event.orbit.epoch}\")" - ] - }, - { - "cell_type": "markdown", - "id": "01234567-890a-bcde-f123-4567890abcde", - "metadata": {}, - "source": [ - "## 3. Event Arc Finding\n", - "\n", - "Event arcs represent continuous periods where a condition is met, such as being in eclipse or having the sun set." - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "12345678-90ab-cdef-1234-567890abcdef", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Found 11 sunset arcs.\n", - "Found 25 eclipse arcs.\n", - "First eclipse duration: 39 min 12 s 806 ms 171 μs 252 ns\n" - ] - } - ], - "source": [ - "sun_has_set = analysis.Event(\n", - " analysis.ScalarExpr.SunAngle(observer_id=-85),\n", - " Condition.LessThan(90.0),\n", - " Unit.Second * 0.5,\n", - " ab_corr=None,\n", - ")\n", - "eclipse = Event.total_eclipse(Frames.MOON_J2000)\n", - "\n", - "sunset_arcs = almanac.report_event_arcs(lro_state_spec, sun_has_set, start_epoch, search_end)\n", - "print(f\"Found {len(sunset_arcs)} sunset arcs.\")\n", - "\n", - "eclipse_arcs = almanac.report_event_arcs(lro_state_spec, eclipse, start_epoch, search_end)\n", - "print(f\"Found {len(eclipse_arcs)} eclipse arcs.\")\n", - "\n", - "if eclipse_arcs:\n", - " print(f\"First eclipse duration: {eclipse_arcs[0].duration()}\")" - ] - }, - { - "cell_type": "markdown", - "id": "23456789-0abc-def1-2345-67890abcdef1", - "metadata": {}, - "source": [ - "## 4. Ground Contact Finders\n", - "\n", - "We can simulate ground station visibility by defining locations and masks." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "34567890-abcd-ef12-3456-7890abcdef12", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "[save_as] overwriting tutorial_loc_kernel.lka\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Found 1 Comm arcs (1 day).\n", - "First pass duration: 8 h 56 min 55 s 251 ms 300 μs 749 ns\n", - "Pass location: DSS65 (#1)\n", - "Number of AER data points: 54\n" - ] - } - ], - "source": [ - "# Define a ground station (DSS65)\n", - "mask = [TerrainMask(0.0, 5.0), TerrainMask(35.0, 10.0), TerrainMask(270.0, 3.0)]\n", - "dss65 = Location(\n", - " 40.427_222,\n", - " 4.250_556,\n", - " 0.834_939,\n", - " FrameUid(399, 399),\n", - " mask,\n", - " terrain_mask_ignored=True,\n", - ")\n", - "\n", - "# Create and save an LKA (Location Kernel Anise) file\n", - "entry = LocationDhallSetEntry(dss65, id=1, alias=\"DSS65\")\n", - "dhallset = LocationDhallSet([entry])\n", - "dataset = dhallset.to_dataset()\n", - "lka_path = \"tutorial_loc_kernel.lka\"\n", - "dataset.save_as(lka_path, True)\n", - "\n", - "# Load the LKA into the Almanac\n", - "almanac = almanac.load(lka_path)\n", - "\n", - "# Find visibility arcs\n", - "horizon = Event.visible_from_location_id(1)\n", - "\n", - "comm_arcs = almanac.report_event_arcs(\n", - " lro_state_spec, horizon, start_epoch, start_epoch + Unit.Day * 1\n", - ")\n", - "print(f\"Found {len(comm_arcs)} Comm arcs (1 day).\")\n", - "\n", - "# More detailed visibility report\n", - "visibility_arcs = almanac.report_visibility_arcs(\n", - " lro_state_spec, 1, start_epoch, start_epoch + Unit.Day * 1, Unit.Minute * 10, None\n", - ")\n", - "\n", - "if visibility_arcs:\n", - " first_pass = visibility_arcs[0]\n", - " print(f\"First pass duration: {first_pass.duration()}\")\n", - " print(f\"Pass location: {first_pass.location_ref}\")\n", - " \n", - " # Access AER (Azimuth, Elevation, Range) data\n", - " print(f\"Number of AER data points: {len(first_pass.aer_data)}\")\n", - "else:\n", - " print(\"No visibility arcs found.\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8ce5aa82-06cf-4a13-8f33-198f652a7e18", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.14" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/docs/anise/tutorials/Tutorial 07 - Analysis.md b/docs/anise/tutorials/Tutorial 07 - Analysis.md new file mode 100644 index 0000000..3c187c4 --- /dev/null +++ b/docs/anise/tutorials/Tutorial 07 - Analysis.md @@ -0,0 +1,440 @@ +# Tutorial 07 - Analysis Module + +This tutorial covers the new analysis module introduced in ANISE version 0.8.0. We will explore: +1. **Report Building:** Creating complex scalar reports, generating data, and visualizing it with Polars and Plotly. +2. **Event Finding:** Finding discrete events like apoapsis, periapsis, and eclipse entry/exit. +3. **Event Arc Finding:** Detecting continuous events (arcs) like eclipses and sunset periods. +4. **Ground Contact Finders:** Using `LocationDhallSet` to define ground stations and finding visibility/communication arcs. + + +```python +import polars as pl +import plotly.express as px +from anise import ( + Almanac, + Aberration, + LocationDhallSet, + LocationDhallSetEntry, + LocationDataSet, +) +from anise.astro import Location, TerrainMask, FrameUid, Frame +from anise.analysis import Event, Condition +import anise.analysis as analysis +from anise.time import Duration, Epoch, TimeSeries, Unit +from anise.constants import Frames, Orientations + +# Load the Almanac with necessary kernels +almanac = ( + Almanac("../../data/de440s.bsp") + .load("../../data/pck08.pca") + .load("../../data/lro.bsp") +) +almanac.describe(spk=True) +``` + + === SPK #0: `../../data/lro.bsp` === + ┌───────────────┬────────────────┬────────────┬───────────────────────────────────┬───────────────────────────────────┬──────────┬──────────────────────┐ + │ Name │ Target │ Center │ Start epoch │ End epoch │ Duration │ Interpolation kind │ + ├───────────────┼────────────────┼────────────┼───────────────────────────────────┼───────────────────────────────────┼──────────┼──────────────────────┤ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-15T00:01:09.183418531 TDB │ 2023-12-16T00:01:09.183445764 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-16T00:01:09.183445766 TDB │ 2023-12-17T00:01:09.183473001 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-17T00:01:09.183473003 TDB │ 2023-12-18T00:01:09.183500753 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-18T00:01:09.183500755 TDB │ 2023-12-19T00:01:09.183528378 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-19T00:01:09.183528380 TDB │ 2023-12-20T00:01:09.183556262 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-20T00:01:09.183556264 TDB │ 2023-12-21T00:01:09.183584149 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-21T00:01:09.183584151 TDB │ 2023-12-22T00:01:09.183612293 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-22T00:01:09.183612295 TDB │ 2023-12-23T00:01:09.183640567 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-23T00:01:09.183640569 TDB │ 2023-12-24T00:01:09.183668972 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-24T00:01:09.183668974 TDB │ 2023-12-25T00:01:09.183697379 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-25T00:01:09.183697381 TDB │ 2023-12-26T00:01:09.183725917 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-26T00:01:09.183725919 TDB │ 2023-12-27T00:01:09.183754585 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-27T00:01:09.183754587 TDB │ 2023-12-28T00:01:09.183783255 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-28T00:01:09.183783257 TDB │ 2023-12-29T00:01:09.183812055 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-29T00:01:09.183812057 TDB │ 2023-12-30T00:01:09.183840858 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-30T00:01:09.183840860 TDB │ 2023-12-31T00:01:09.183869791 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2023-12-31T00:01:09.183869793 TDB │ 2024-01-01T00:01:09.183898599 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-01T00:01:09.183898601 TDB │ 2024-01-02T00:01:09.183927664 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-02T00:01:09.183927666 TDB │ 2024-01-03T00:01:09.183956604 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-03T00:01:09.183956606 TDB │ 2024-01-04T00:01:09.183985547 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-04T00:01:09.183985549 TDB │ 2024-01-05T00:01:09.184014491 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-05T00:01:09.184014493 TDB │ 2024-01-06T00:01:09.184043565 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-06T00:01:09.184043567 TDB │ 2024-01-07T00:01:09.184072515 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-07T00:01:09.184072517 TDB │ 2024-01-08T00:01:09.184101466 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-08T00:01:09.184101468 TDB │ 2024-01-09T00:01:09.184130420 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-09T00:01:09.184130422 TDB │ 2024-01-10T00:01:09.184159376 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-10T00:01:09.184159378 TDB │ 2024-01-11T00:01:09.184188206 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-11T00:01:09.184188208 TDB │ 2024-01-12T00:01:09.184216912 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-12T00:01:09.184216914 TDB │ 2024-01-13T00:01:09.184245618 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-13T00:01:09.184245620 TDB │ 2024-01-14T00:01:09.184274327 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-14T00:01:09.184274329 TDB │ 2024-01-15T00:01:09.184302911 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-15T00:01:09.184302913 TDB │ 2024-01-16T00:01:09.184331240 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-16T00:01:09.184331242 TDB │ 2024-01-17T00:01:09.184359700 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-17T00:01:09.184359702 TDB │ 2024-01-18T00:01:09.184387905 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-18T00:01:09.184387907 TDB │ 2024-01-19T00:01:09.184415986 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-19T00:01:09.184415988 TDB │ 2024-01-20T00:01:09.184443940 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-20T00:01:09.184443942 TDB │ 2024-01-21T00:01:09.184471769 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-21T00:01:09.184471771 TDB │ 2024-01-22T00:01:09.184499470 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-22T00:01:09.184499472 TDB │ 2024-01-23T00:01:09.184527175 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-23T00:01:09.184527177 TDB │ 2024-01-24T00:01:09.184554497 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-24T00:01:09.184554499 TDB │ 2024-01-25T00:01:09.184581694 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-25T00:01:09.184581696 TDB │ 2024-01-26T00:01:09.184608766 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-26T00:01:09.184608768 TDB │ 2024-01-27T00:01:09.184635583 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-27T00:01:09.184635585 TDB │ 2024-01-28T00:01:09.184662273 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-28T00:01:09.184662275 TDB │ 2024-01-29T00:01:09.184688710 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-29T00:01:09.184688712 TDB │ 2024-01-30T00:01:09.184714892 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 ��� 2024-01-30T00:01:09.184714894 TDB │ 2024-01-31T00:01:09.184740949 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-01-31T00:01:09.184740951 TDB │ 2024-02-01T00:01:09.184766751 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-01T00:01:09.184766753 TDB │ 2024-02-02T00:01:09.184792172 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-02T00:01:09.184792174 TDB │ 2024-02-03T00:01:09.184817595 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-03T00:01:09.184817597 TDB │ 2024-02-04T00:01:09.184842507 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-04T00:01:09.184842509 TDB │ 2024-02-05T00:01:09.184867292 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-05T00:01:09.184867294 TDB │ 2024-02-06T00:01:09.184891825 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-06T00:01:09.184891827 TDB │ 2024-02-07T00:01:09.184916102 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-07T00:01:09.184916104 TDB │ 2024-02-08T00:01:09.184939997 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-08T00:01:09.184939999 TDB │ 2024-02-09T00:01:09.184963766 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-09T00:01:09.184963768 TDB │ 2024-02-10T00:01:09.184987025 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-10T00:01:09.184987027 TDB │ 2024-02-11T00:01:09.185010157 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-11T00:01:09.185010159 TDB │ 2024-02-12T00:01:09.185032906 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-12T00:01:09.185032908 TDB │ 2024-02-13T00:01:09.185055402 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-13T00:01:09.185055404 TDB │ 2024-02-14T00:01:09.185077515 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-14T00:01:09.185077517 TDB │ 2024-02-15T00:01:09.185099245 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-15T00:01:09.185099247 TDB │ 2024-02-16T00:01:09.185120850 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-16T00:01:09.185120852 TDB │ 2024-02-17T00:01:09.185141943 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-17T00:01:09.185141945 TDB │ 2024-02-18T00:01:09.185162653 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-18T00:01:09.185162655 TDB │ 2024-02-19T00:01:09.185182982 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-19T00:01:09.185182984 TDB │ 2024-02-20T00:01:09.185203056 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-20T00:01:09.185203058 TDB │ 2024-02-21T00:01:09.185222746 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-21T00:01:09.185222748 TDB │ 2024-02-22T00:01:09.185241926 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-22T00:01:09.185241928 TDB │ 2024-02-23T00:01:09.185260851 TDB │ 1 day �� Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-23T00:01:09.185260853 TDB │ 2024-02-24T00:01:09.185279522 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-24T00:01:09.185279524 TDB │ 2024-02-25T00:01:09.185297554 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-25T00:01:09.185297556 TDB │ 2024-02-26T00:01:09.185315331 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-26T00:01:09.185315333 TDB │ 2024-02-27T00:01:09.185332596 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-27T00:01:09.185332598 TDB │ 2024-02-28T00:01:09.185349607 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-28T00:01:09.185349609 TDB │ 2024-02-29T00:01:09.185366107 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-02-29T00:01:09.185366109 TDB │ 2024-03-01T00:01:09.185382224 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-01T00:01:09.185382226 TDB │ 2024-03-02T00:01:09.185397958 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-02T00:01:09.185397960 TDB │ 2024-03-03T00:01:09.185413181 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-03T00:01:09.185413183 TDB │ 2024-03-04T00:01:09.185428021 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-04T00:01:09.185428023 TDB │ 2024-03-05T00:01:09.185442478 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-05T00:01:09.185442480 TDB │ 2024-03-06T00:01:09.185456422 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-06T00:01:09.185456424 TDB │ 2024-03-07T00:01:09.185469985 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-07T00:01:09.185469987 TDB │ 2024-03-08T00:01:09.185482908 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-08T00:01:09.185482910 TDB │ 2024-03-09T00:01:09.185495575 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-09T00:01:09.185495577 TDB │ 2024-03-10T00:01:09.185507731 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-10T00:01:09.185507733 TDB │ 2024-03-11T00:01:09.185519632 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-11T00:01:09.185519634 TDB │ 2024-03-12T00:01:09.185530765 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-12T00:01:09.185530767 TDB │ 2024-03-13T00:01:09.185541515 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-13T00:01:09.185541517 TDB │ 2024-03-14T00:01:09.185551881 TDB │ 1 day │ Hermite Unequal Step │ + │ SPK_STATES_13 │ body -85 J2000 │ Moon J2000 │ 2024-03-14T00:01:09.185551883 TDB │ 2024-03-15T00:01:09.185561735 TDB │ 1 day │ Hermite Unequal Step │ + └───────────────┴────────────────┴────────────┴───────────────────────────────────┴───────────────────────────────────┴──────────┴────���─────────────────┘ + === SPK #1: `../../data/de440s.bsp` === + ┌────────────────┬─────────────────────────────┬───────────────────────────────┬───────────────────────────────────┬───────────────────────────────────┬─────────────┬────────────────────┐ + │ Name │ Target │ Center │ Start epoch │ End epoch │ Duration │ Interpolation kind │ + ├────────────────┼───────────────────────────��─┼───────────────────────────────┼───────────────────────────────────┼───────────────────────────────────┼─────────────┼────────────────────┤ + │ DE-0440LE-0440 │ Mercury Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Venus Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Earth-Moon Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Mars Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Jupiter Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Saturn Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Uranus Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Neptune Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Pluto Barycenter J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Sun J2000 │ Solar System Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Moon J2000 │ Earth-Moon Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Earth J2000 │ Earth-Moon Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Mercury J2000 │ Mercury Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │ + │ DE-0440LE-0440 │ Venus J2000 │ Venus Barycenter J2000 │ 1849-12-26T00:00:00.000046619 TDB │ 2150-01-21T23:59:59.999955184 TDB │ 109600 days │ Chebyshev Triplet │ + └────────────────┴─────────────────────────────┴───────────────────────────────┴───────────────────────────────────┴───────────────────────────────────┴─────────────┴────────────────────┘ + + +## 1. Report Building + +We can define complex scalar expressions and generate reports over a time series. This is useful for analyzing orbital elements, geometric conditions, and custom calculations. + + +```python +target_frame = analysis.FrameSpec.Loaded(Frame(-85, Orientations.J2000)) +observer_frame = analysis.FrameSpec.Loaded(Frames.MOON_J2000) + +state = analysis.StateSpec( + target_frame=target_frame, + observer_frame=observer_frame, + ab_corr=None, +) + +# Define a VNC (Velocity-Normal-Co-normal) Frame +vnc = analysis.OrthogonalFrame.XY( + x=analysis.VectorExpr.Unit(analysis.VectorExpr.Velocity(state)), + y=analysis.VectorExpr.Unit(analysis.VectorExpr.OrbitalMomentum(state)), +) + +sun_state = analysis.StateSpec( + target_frame=target_frame, + observer_frame=analysis.FrameSpec.Loaded(Frames.SUN_J2000), + ab_corr=Aberration("LT"), +) + +# Project the Earth->Sun vector onto the VNC frame +proj = analysis.VectorExpr.Project( + v=analysis.VectorExpr.Negate( + analysis.VectorExpr.Unit(analysis.VectorExpr.Radius(sun_state)) + ), + frame=vnc, + plane=analysis.Plane.XY, +) + +# Custom Local Solar Time (LST) Calculation using fundamental expressions +earth_sun = analysis.StateSpec( + target_frame=analysis.FrameSpec.Loaded(Frames.SUN_J2000), + observer_frame=observer_frame, + ab_corr=Aberration("LT"), +) +u = analysis.VectorExpr.Unit( + analysis.VectorExpr.CrossProduct( + a=analysis.VectorExpr.Unit(analysis.VectorExpr.Radius(earth_sun)), + b=analysis.VectorExpr.Unit(analysis.VectorExpr.OrbitalMomentum(state)), + ) +) +v = analysis.VectorExpr.CrossProduct( + a=analysis.VectorExpr.Unit(analysis.VectorExpr.OrbitalMomentum(state)), b=u +) +r = analysis.VectorExpr.Radius(state) +sin_theta = analysis.ScalarExpr.DotProduct(a=v, b=r) +cos_theta = analysis.ScalarExpr.DotProduct(a=u, b=r) +theta = analysis.ScalarExpr.Atan2(y=sin_theta, x=cos_theta) +lst_prod = analysis.ScalarExpr.Mul( + a=analysis.ScalarExpr.Mul(a=theta, b=analysis.ScalarExpr.Constant(1.0 / 180.0)), + b=analysis.ScalarExpr.Constant(12.0), +) +lst_add = analysis.ScalarExpr.Add(a=lst_prod, b=analysis.ScalarExpr.Constant(6.0)) +lst = analysis.ScalarExpr.Modulo(v=lst_add, m=analysis.ScalarExpr.Constant(24.0)) + +# Define scalars to report +scalars = [ + analysis.ScalarExpr.Element(analysis.OrbitalElement.SemiMajorAxis), + analysis.ScalarExpr.Element(analysis.OrbitalElement.Eccentricity), + analysis.ScalarExpr.SolarEclipsePercentage(eclipsing_frame=Frames.VENUS_J2000), + analysis.ScalarExpr.VectorX(proj), + analysis.ScalarExpr.VectorY(proj), + analysis.ScalarExpr.VectorZ(proj), + analysis.ScalarExpr.LocalSolarTime(), + lst, +] + +# Set aliases for readable columns +scalars_with_aliases = [(s, None) for s in scalars] +scalars_with_aliases[3] = (scalars_with_aliases[3][0], "proj VNC X") +scalars_with_aliases[4] = (scalars_with_aliases[4][0], "proj VNC Y") +scalars_with_aliases[5] = (scalars_with_aliases[5][0], "proj VNC Z") +scalars_with_aliases[7] = (scalars_with_aliases[7][0], "Custom LST (h)") + +report = analysis.ReportScalars(scalars_with_aliases, state) +``` + +Now we generate the data over a time series and convert it to a Polars DataFrame. + + +```python +lro_start, lro_stop = almanac.spk_domain(-85) +print(f"LRO valid from {lro_start} to {lro_stop}") +series = TimeSeries( + lro_stop - Unit.Day * 1.5, + lro_stop, + Unit.Minute * 10, # Higher resolution for plotting + inclusive=True, +) + +data = almanac.report_scalars(report, series) + +# Convert to Polars DataFrame +rows = [] +for epoch_str, val_dict in data.items(): + row = {"Epoch": epoch_str} + row.update(val_dict) + rows.append(row) + +df = pl.DataFrame(rows) +# Convert Epoch string to datetime for better plotting +df = df.with_columns(pl.col("Epoch").str.to_datetime()).sort("Epoch") +print(df.head()) +``` + + LRO valid from 2023-12-15T00:01:09.183425793 ET to 2024-03-15T00:01:09.185563263 ET + shape: (5, 9) + ┌───────────┬───────────┬───────────┬───────────┬───┬───────────┬───────────┬───────────┬──────────┐ + │ Epoch ┆ proj VNC ┆ local ┆ solar ┆ … ┆ Custom ┆ proj VNC ┆ SemiMajor ┆ proj VNC │ + │ --- ┆ X ┆ solar ┆ eclipse ┆ ┆ LST (h) ┆ Y ┆ Axis (km) ┆ Z │ + │ datetime[ ┆ --- ┆ time (h) ┆ due to ┆ ┆ --- ┆ --- ┆ --- ┆ --- │ + │ μs] ┆ f64 ┆ --- ┆ Venus ┆ ┆ f64 ┆ f64 ┆ f64 ┆ f64 │ + │ ┆ ┆ f64 ┆ Bar… ┆ ┆ ┆ ┆ ┆ │ + │ ┆ ┆ ┆ --- ┆ ┆ ┆ ┆ ┆ │ + │ ┆ ┆ ┆ f64 ┆ ┆ ┆ ┆ ┆ │ + ╞═══════════╪═══════════╪═══════════╪═══════════╪═══╪═══════════╪═══════════╪═══════════╪══════════╡ + │ 2024-03-1 ┆ -0.382568 ┆ 21.345665 ┆ 0.0 ┆ … ┆ 21.345665 ┆ 0.753565 ┆ 1827.7867 ┆ -0.53235 │ + │ 3 12:01:0 ┆ ┆ ┆ ┆ ┆ ┆ ┆ 98 ┆ 5 │ + │ 9.185563 ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ │ + │ 2024-03-1 ┆ -0.055965 ┆ 23.400991 ┆ 0.0 ┆ … ┆ 23.400991 ┆ 0.75363 ┆ 1828.0145 ┆ -0.65309 │ + │ 3 12:11:0 ┆ ┆ ┆ ┆ ┆ ┆ ┆ 17 ┆ 6 │ + │ 9.185563 ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ │ + │ 2024-03-1 ┆ 0.287843 ┆ 1.474358 ┆ 0.0 ┆ … ┆ 1.474358 ┆ 0.753692 ┆ 1827.7419 ┆ -0.58883 │ + │ 3 12:21:0 ┆ ┆ ┆ ┆ ┆ ┆ ┆ 3 ┆ 2 │ + │ 9.185563 ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ │ + │ 2024-03-1 ┆ 0.550096 ┆ 3.559474 ┆ 0.0 ┆ … ┆ 3.559474 ┆ 0.753795 ┆ 1827.3555 ┆ -0.35611 │ + │ 3 12:31:0 ┆ ┆ ┆ ┆ ┆ ┆ ┆ 14 ┆ 7 │ + │ 9.185563 ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ │ + │ 2024-03-1 ┆ 0.654768 ┆ 5.647546 ┆ 0.0 ┆ … ┆ 5.647546 ┆ 0.753964 ┆ 1827.3668 ┆ -0.02131 │ + │ 3 12:41:0 ┆ ┆ ┆ ┆ ┆ ┆ ┆ 86 ┆ │ + │ 9.185563 ┆ ┆ ┆ ┆ ┆ ┆ ┆ ┆ │ + └───────────┴───────────┴───────────┴───────────┴───┴───────────┴───────────┴───────────┴──────────┘ + + +Let's visualize the Custom LST vs Time. + + +```python +fig = px.line(df, x="Epoch", y="Custom LST (h)", title="Custom Local Solar Time over Time") +fig.show() +``` + + + +![png](Tutorial%2007%20-%20Analysis_files/Tutorial%2007%20-%20Analysis_7_0.png) + + + +## 2. Event Finding + +We can search for discrete events such as apoapsis, periapsis, or specific geometric conditions. + + +```python +lro_frame = Frame(-85, 1) # LRO NAIF ID +lro_state_spec = analysis.StateSpec( + target_frame=analysis.FrameSpec.Loaded(lro_frame), + observer_frame=analysis.FrameSpec.Loaded(Frames.MOON_J2000), + ab_corr=None, +) + +apolune = Event.apoapsis() +perilune = Event.periapsis() + +start_epoch, end_epoch = almanac.spk_domain(-85) +# Search over a shorter window +search_end = start_epoch + Unit.Day * 2 + +print(f"Searching events from {start_epoch} to {search_end}") + +apo_events = almanac.report_events(lro_state_spec, apolune, start_epoch, search_end) +print(f"Found {len(apo_events)} apoapsis events.") + +peri_events = almanac.report_events(lro_state_spec, perilune, start_epoch, search_end) +print(f"Found {len(peri_events)} periapsis events.") + +# Print first few events +for i, event in enumerate(apo_events[:3]): + print(f"Apoapsis {i+1}: {event.orbit.epoch}") +``` + + Searching events from 2023-12-15T00:01:09.183425793 ET to 2023-12-17T00:01:09.183425793 ET + Found 25 apoapsis events. + Found 24 periapsis events. + Apoapsis 1: 2023-12-15T00:07:04.599486254 ET + Apoapsis 2: 2023-12-15T02:05:09.311922236 ET + Apoapsis 3: 2023-12-15T04:03:14.118872647 ET + + +## 3. Event Arc Finding + +Event arcs represent continuous periods where a condition is met, such as being in eclipse or having the sun set. + + +```python +sun_has_set = analysis.Event( + analysis.ScalarExpr.SunAngle(observer_id=-85), + Condition.LessThan(90.0), + Unit.Second * 0.5, + ab_corr=None, +) +eclipse = Event.total_eclipse(Frames.MOON_J2000) + +sunset_arcs = almanac.report_event_arcs(lro_state_spec, sun_has_set, start_epoch, search_end) +print(f"Found {len(sunset_arcs)} sunset arcs.") + +eclipse_arcs = almanac.report_event_arcs(lro_state_spec, eclipse, start_epoch, search_end) +print(f"Found {len(eclipse_arcs)} eclipse arcs.") + +if eclipse_arcs: + print(f"First eclipse duration: {eclipse_arcs[0].duration()}") +``` + + Found 11 sunset arcs. + Found 25 eclipse arcs. + First eclipse duration: 39 min 12 s 806 ms 171 μs 252 ns + + +## 4. Ground Contact Finders + +We can simulate ground station visibility by defining locations and masks. + + +```python +# Define a ground station (DSS65) +mask = [TerrainMask(0.0, 5.0), TerrainMask(35.0, 10.0), TerrainMask(270.0, 3.0)] +dss65 = Location( + 40.427_222, + 4.250_556, + 0.834_939, + FrameUid(399, 399), + mask, + terrain_mask_ignored=True, +) + +# Create and save an LKA (Location Kernel Anise) file +entry = LocationDhallSetEntry(dss65, id=1, alias="DSS65") +dhallset = LocationDhallSet([entry]) +dataset = dhallset.to_dataset() +lka_path = "tutorial_loc_kernel.lka" +dataset.save_as(lka_path, True) + +# Load the LKA into the Almanac +almanac = almanac.load(lka_path) + +# Find visibility arcs +horizon = Event.visible_from_location_id(1) + +comm_arcs = almanac.report_event_arcs( + lro_state_spec, horizon, start_epoch, start_epoch + Unit.Day * 1 +) +print(f"Found {len(comm_arcs)} Comm arcs (1 day).") + +# More detailed visibility report +visibility_arcs = almanac.report_visibility_arcs( + lro_state_spec, 1, start_epoch, start_epoch + Unit.Day * 1, Unit.Minute * 10, None +) + +if visibility_arcs: + first_pass = visibility_arcs[0] + print(f"First pass duration: {first_pass.duration()}") + print(f"Pass location: {first_pass.location_ref}") + + # Access AER (Azimuth, Elevation, Range) data + print(f"Number of AER data points: {len(first_pass.aer_data)}") +else: + print("No visibility arcs found.") +``` + + [save_as] overwriting tutorial_loc_kernel.lka + + + Found 1 Comm arcs (1 day). + First pass duration: 8 h 56 min 55 s 251 ms 300 μs 749 ns + Pass location: DSS65 (#1) + Number of AER data points: 54 + + + +```python + +``` diff --git a/docs/anise/tutorials/Tutorial 07 - Analysis_files/Tutorial 07 - Analysis_7_0.png b/docs/anise/tutorials/Tutorial 07 - Analysis_files/Tutorial 07 - Analysis_7_0.png new file mode 100644 index 0000000000000000000000000000000000000000..d0880195c172a54676797bee243365915d6dd242 GIT binary patch literal 67123 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%0AyE9Ao`0~tE+Xhik`c+= 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 Pe;_19 Date: Wed, 1 Jul 2026 13:16:08 -0700 Subject: [PATCH 2/3] Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- ...- Loading remote SPICE and ANISE files (meta almanac).md | 6 +++--- ...al 03 - Defining and working with the Orbit structure.md | 6 +++--- ...l 05 - Using frame kernels and text planetary kernels.md | 4 ++-- docs/anise/tutorials/Tutorial 06 - Sun probe Earth angle.md | 4 +++- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/anise/tutorials/Tutorial 02 - Loading remote SPICE and ANISE files (meta almanac).md b/docs/anise/tutorials/Tutorial 02 - Loading remote SPICE and ANISE files (meta almanac).md index ad590b9..0a6696c 100644 --- a/docs/anise/tutorials/Tutorial 02 - Loading remote SPICE and ANISE files (meta almanac).md +++ b/docs/anise/tutorials/Tutorial 02 - Loading remote SPICE and ANISE files (meta almanac).md @@ -1,6 +1,6 @@ # ANISE -ANISE is a modern rewrite of NAIF SPICE, written in Rust and providing interfaces to other languages include Python. +ANISE is a modern rewrite of NAIF SPICE, written in Rust and providing interfaces to other languages including Python. Evidently, this tutorial applies to the Python usage of ANISE. @@ -143,7 +143,7 @@ The CRC32 integrity number is not set for local paths because in any case, the M The MetaAlmanac is designed to work seamlessly with the Alamac itself. In the following example, we'll use the latest MetaAlmanac (the same that's in `latest.dhall`) and see that it can be used to return the Almanac directly. - +The MetaAlmanac is designed to work seamlessly with the Almanac itself. In the following example, we'll use the latest MetaAlmanac (the same that's in `latest.dhall`) and see that it can be used to return the Almanac directly. ```python meta = MetaAlmanac.load("../../data/latest.dhall") print(meta) @@ -338,4 +338,4 @@ print(de440s_and_moon.frame_info(Frames.MOON_PA_FRAME)) + Compare the process of using MetaFile with manual Dhall file creation. + Load the generated configuration into the MetaAlmanac. -_Note:_ Almanac, MetaFile, and MetaAlmana all support the equality operation in Python. +_Note:_ Almanac, MetaFile, and MetaAlmanac all support the equality operation in Python. diff --git a/docs/anise/tutorials/Tutorial 03 - Defining and working with the Orbit structure.md b/docs/anise/tutorials/Tutorial 03 - Defining and working with the Orbit structure.md index 2d9f438..284367f 100644 --- a/docs/anise/tutorials/Tutorial 03 - Defining and working with the Orbit structure.md +++ b/docs/anise/tutorials/Tutorial 03 - Defining and working with the Orbit structure.md @@ -1,6 +1,6 @@ # ANISE -ANISE is a modern rewrite of NAIF SPICE, written in Rust and providing interfaces to other languages include Python. +ANISE is a modern rewrite of NAIF SPICE, written in Rust and providing interfaces to other languages including Python. Evidently, this tutorial applies to the Python usage of ANISE. @@ -274,8 +274,8 @@ All Almanac translation/transform queries return an instance of `Orbit`, populat **Tasks:** 1. Fetch the state of a celestial object, like the Moon, at the epoch of your choice. - 3. Use the Orbit instance's `at_epoch` to propagate the position of that object forward in time, assuming it was subjected only to two body dynamics. - 4. Fetch the state of that same object from the Almanac at that other time, and look at the difference in Cartesian state and orbital elements. + 2. Use the Orbit instance's `at_epoch` to propagate the position of that object forward in time, assuming it was subjected only to two body dynamics. + 3. Fetch the state of that same object from the Almanac at that other time, and look at the difference in Cartesian state and orbital elements. **Learning Goal:** Understand the basics of orbital propagation and its limitations. diff --git a/docs/anise/tutorials/Tutorial 05 - Using frame kernels and text planetary kernels.md b/docs/anise/tutorials/Tutorial 05 - Using frame kernels and text planetary kernels.md index d3e1dcf..e419dbc 100644 --- a/docs/anise/tutorials/Tutorial 05 - Using frame kernels and text planetary kernels.md +++ b/docs/anise/tutorials/Tutorial 05 - Using frame kernels and text planetary kernels.md @@ -147,8 +147,8 @@ iau_jupiter_frame.mu_km3_s2() + Build a new PCA file using these two new files + Load a default Almanac and an empty Almanac where you'll load these two files into, along with the `DE440s.bsp` file. + Query the Cartesian state of the Earth at any time of your choosing from both of these Almanac. The state should be the same, since it'll be from the DE440s.bsp. -+ Observe how the frame graviational parameter information of both will differ. -+ Finally, using the `at_epoch` function on both of these state, perform a two-body propagation for both and notice how the graviational parameter affects the result. ++ Observe how the frame gravitational parameter information of both will differ. ++ Finally, using the `at_epoch` function on both of these states, perform a two-body propagation for both and notice how the gravitational parameter affects the result. ## Euler Parameter ANISE kernel diff --git a/docs/anise/tutorials/Tutorial 06 - Sun probe Earth angle.md b/docs/anise/tutorials/Tutorial 06 - Sun probe Earth angle.md index f020b49..c75fcfc 100644 --- a/docs/anise/tutorials/Tutorial 06 - Sun probe Earth angle.md +++ b/docs/anise/tutorials/Tutorial 06 - Sun probe Earth angle.md @@ -5,7 +5,9 @@ ANISE is a modern rewrite of NAIF SPICE, written in Rust and providing interface Evidently, this tutorial applies to the Python usage of ANISE. ## Goal -By the end of this tutorial, you should know how to build a data frame containing the Sun probe Earth angle of a given spacecraft BSP and plot that information. Your exercise will be to confirm that this calculation is correct by computing the Sun elevation at nadir below the spacecraft as detailed in tutorial 04.## Loading the latest orientation and planetary data +By the end of this tutorial, you should know how to build a data frame containing the Sun probe Earth angle of a given spacecraft BSP and plot that information. Your exercise will be to confirm that this calculation is correct by computing the Sun elevation at nadir below the spacecraft as detailed in tutorial 04. + +## Loading the latest orientation and planetary data Let's start by installing ANISE: `pip install anise` From dc3ed095d91f5ae86dd8ed35ff3dedf1de103137 Mon Sep 17 00:00:00 2001 From: Chris Date: Wed, 1 Jul 2026 13:43:41 -0700 Subject: [PATCH 3/3] Apply suggestions from code review Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- docs/anise/tutorials/Tutorial 01 - Querying SPK files.md | 2 +- ...orial 05 - Using frame kernels and text planetary kernels.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/anise/tutorials/Tutorial 01 - Querying SPK files.md b/docs/anise/tutorials/Tutorial 01 - Querying SPK files.md index 4d5af84..5069886 100644 --- a/docs/anise/tutorials/Tutorial 01 - Querying SPK files.md +++ b/docs/anise/tutorials/Tutorial 01 - Querying SPK files.md @@ -581,7 +581,7 @@ print(state.light_time().to_seconds()) 491.259084389 -## Exerices +## Exercises 1. Query the position of the Earth at different times using a time series. Note that the DE440s file is only valid for 200 years centered on 1950. 2. Query the position of Venus as seen from the Earth, both in the J2000 frame, and make a plot of the light time between these two planets throughout a given year. diff --git a/docs/anise/tutorials/Tutorial 05 - Using frame kernels and text planetary kernels.md b/docs/anise/tutorials/Tutorial 05 - Using frame kernels and text planetary kernels.md index e419dbc..7dd19b9 100644 --- a/docs/anise/tutorials/Tutorial 05 - Using frame kernels and text planetary kernels.md +++ b/docs/anise/tutorials/Tutorial 05 - Using frame kernels and text planetary kernels.md @@ -146,7 +146,7 @@ iau_jupiter_frame.mu_km3_s2() + Make a copy of the `pck00008.tpc` file, removing everything except Earth related data. + Build a new PCA file using these two new files + Load a default Almanac and an empty Almanac where you'll load these two files into, along with the `DE440s.bsp` file. -+ Query the Cartesian state of the Earth at any time of your choosing from both of these Almanac. The state should be the same, since it'll be from the DE440s.bsp. ++ Query the Cartesian state of the Earth at any time of your choosing from both of these Almanacs. The state should be the same, since it'll be from the DE440s.bsp. + Observe how the frame gravitational parameter information of both will differ. + Finally, using the `at_epoch` function on both of these states, perform a two-body propagation for both and notice how the gravitational parameter affects the result.