Tools to visualize searches performed by Iterative Precovery and Orbit Determination (IPOD) and precovery.
Goals:
- Visualize an orbit's trajectory on the sky. Propagate the orbit in a series of time steps between user defined start and end times, and display the orbit's position as a function of time.
- Visualize the frames table as a function of time. Display healpix frames being observed as a function of survey time. To start, lets create a single visualization for each observatory code.
- Combine 1 and 2, and display the orbit's position as a function of time, and overlay the frames table on top of the orbit highlighting the frames where a potential intersection may have occured.
- Create a multi-view dashboard that allows the user to interact the above visualizations.
- Create a visualization that displays the individual point-source measurements within each frame and shows summary statistics such as the astrometric position and uncertainty (if available), measurement time, magnitude and filter. Add this visualization to the dashboard.
- Devise a visualization to incorporate information about the residuals between the orbit's predicted position and the observations within each frame. Add this visualization to the dashboard.
- Stretch : Reference cutouts for each observation on disk and visualize them in the dashboard when clicking on a point-source measurement.
- Stretch : Select observations to be used in the orbit determination process and produce a list of the observations.
- Stretch : ...
Tools:
precovery
- A Python package to perform precovery searches on a set of observations
ipod
- A Python package to perform iterative precovery and orbit determination on a set of observations. IPOD performs iterative searches for new observations by mapping the orbit's approximate on-sky uncertainty to the observations. As new observations are found, the orbit is refined and the search is performed again.
adam_core
- A Python package that defines a common set of utilies underpinning Asteroid Institute's open source tools.
bokeh
- Current visualization package of choice for individual visualizations.
Unknowns: Possible visualization tools for individual visualizations and multi-view dashboards.
bokeh
- Bokeh server allows for interactive visualizations to be created and served.dash
- A Python framework for building analytical web applications.plotly
- A Python graphing library that makes interactive, publication-quality graphs online.panel
- A high-level app and dashboarding solution for Python.d3.js
- A JavaScript library for producing dynamic, interactive data visualizations in web browsers.- others?
To run these examples, you'll need to install the appropriate packages. Ideally, in a fresh virtual environment (such as one created by conda
or virtualenv
).
conda create -n precovis_py311 python=3.11
conda activate precovis_py311
pip install -r requirements.txt
from adam_core.orbits.query import query_sbdb
object_ids = ["2013 RR165", "Eros", "Apophis"]
orbits = query_sbdb(object_ids)
This returns an Orbits
object (which itself is a quivr
table). To convert the table to a pandas
dataframe, you can do the following:
orbits.to_dataframe()
quivr
tables can be sliced like numpy arrays.
orbit0 = orbits[0]
orbit1 = orbits[1:2]
orbit2 = orbits[-1]
quivr
tables also have several very useful functions. If you want to select the rows that match a particular value:
filtered_orbits = orbits.select("object_id", "(2013 RR165)")
You can also apply masks. These are a bit more trickier since they require pyarrow
compute functions. Here is an example:
import pyarrow.compute as pc
filtered_orbits = orbits.apply_mask(pc.equal(orbits.object_id, "(2013 RR165)")) # This is equivalent to the select statement above
Here is a more complex one:
filtered_orbits = orbits.apply_mask(
pc.and_(
pc.greater_equal(orbits.coordinates.x, 0.5),
pc.less_equal(orbits.coordinates.y, 0.0))
)
Let's take a look at the actual definition of the Orbits
object which we repeat here for convenience:
import quivr as qv
from adam_core.coordinates import CartesianCoordinates
class Orbits(qv.Table):
orbit_id = qv.LargeStringColumn(default=lambda: uuid.uuid4().hex)
object_id = qv.LargeStringColumn(nullable=True)
coordinates = CartesianCoordinates.as_column()
Notice how in adam_core, the default representation for all orbits is Cartesian. CartesianCoordinates are just another quivr
table that is nested within the Orbits
table.
Let's define an orbit without using external data.
from adam_core.orbits import Orbits
from adam_core.time import Timestamp
from adam_core.coordinates import KeplerianCoordinates, Origin
keplerian = KeplerianCoordinates.from_kwargs(
a=[2.0],
e=[0.1],
i=[0.1],
raan=[10.0],
ap=[30.0],
M=[23.0],
time=Timestamp.from_kwargs(
days=[59000],
nanos=[0],
scale="tdb"
),
origin=Origin.from_kwargs(code=["SUN"]),
frame="ecliptic",
)
orbits = Orbits.from_kwargs(
object_id=["Dummy Object"],
coordinates=keplerian.to_cartesian()
)
There is a lot going on here. Let's break it down:
-
We've defined a
KeplerianCoordinates
object. This object is just anotherquivr
table.KeplerianCoordinates
are a little easier to interpret geometrically thanCartesianCoordinates
so we use them this time to define an orbit. When we actually go to create the orbits class, we convert theKeplerianCoordinates
toCartesianCoordinates
. -
The
.from_kwargs
constructor is one way to a create aquivr
table from keywords representing the different columns of the underlying table. Notice how these keywords are lists, that is they represent columns and therefore are never scalars. Valid options are lists,~numpy.ndarrays
,~pandas.Series
,~pyarrow.Array
,~quivr.Table
, among others. -
The
KeplerianCoordinates
object has an associated time. The default class that represents time is the Timestamp class. This class is also aquivr
table. We've chosen to use our own custom class because under the hood it stores time as two integers (days and nanoseconds). The Timestamp class has convenience functions to convert to and from~astropy.time.Time
objects (Timestamp.from_astropy
andTimestamp.to_astropy
). -
Coordinates also need to have a defined Origin and Frame. The Origin is a
quivr
table that has a single columncode
that represents the origin of the coordinates. The Frame is a string that represents the frame of the coordinates. The frame is an attribute and not a column because it is the same for all coordinates in the table (this is why we can pass a single value and not a list of values).
Now, we get to the fun parts of dealing with orbits -- propagation!
adam_core has a Propagator class that wraps different propagators. For now we will stick with PYOORB:
from adam_core.propagator.adam_pyoorb import PYOORBPropagator
from adam_core.time import Timestamp
import numpy as np
propagator = PYOORBPropagator()
times = Timestamp.from_mjd(np.linspace(59000, 59100, 100))
propagated_orbits = propagator.propagate_orbits(orbits, times)
This returns an Orbits
object that has M x N rows where M is the number of times and N is the number of orbits. Again, you can use propagated_orbits.to_dataframe()
to convert to a pandas dataframe and inspect the results.
We can also generate predicted ephemerides for the orbits. This is useful for visualizing the orbits on the sky. Here, we need to define an observer or set of observers and pass them to the propagator. I41
is the Zwicky Transient Facility (ZTF) observatory code. To define multiple observers (different observatory codes with different times), you would use quivr.concatenate
to concatenate the observers into a single Observers
.
from adam_core.propagator.adam_pyoorb import PYOORBPropagator
from adam_core.time import Timestamp
from adam_core.observers import Observers
import numpy as np
propagator = PYOORBPropagator()
observers = Observers.from_code(
"I41",
Timestamp.from_mjd(np.linspace(59000, 59100, 100)),
)
ephemeris = propagator.generate_ephemeris(propagated_orbits, observers)