Mars Rover & Heli DemoΒΆ
This tutorial uses data published by NASA:
- https://mars.nasa.gov/mmgis-maps/M20/Layers/json/M20_waypoints.json
- https://mars.nasa.gov/mmgis-maps/M20/Layers/json/M20_traverse.json
- https://mars.nasa.gov/mmgis-maps/M20/Layers/json/m20_heli_waypoints.json
- https://mars.nasa.gov/mmgis-maps/M20/Layers/json/m20_heli_flight_path.json
Hat tip to https://fosstodon.org/@65dBnoise/108251277108722231 for providing the pointers
Known issues:
- MovingPandas will calculate movement speeds based on Earth's WGS84 ellipsoid by default
InΒ [1]:
import numpy as np
import pandas as pd
import geopandas as gpd
import movingpandas as mpd
import shapely as shp
import hvplot.pandas
import matplotlib.pyplot as plt
from geopandas import GeoDataFrame, read_file
from shapely.geometry import Point, LineString, Polygon
from datetime import datetime, timedelta
from holoviews import opts, dim
from os.path import exists
from urllib.request import urlretrieve
import warnings
warnings.filterwarnings("ignore")
plot_defaults = {"linewidth": 5, "capstyle": "round", "figsize": (9, 3), "legend": True}
opts.defaults(
opts.Overlay(active_tools=["wheel_zoom"], frame_width=500, frame_height=400)
)
hvplot_defaults = {"tiles": None, "cmap": "Viridis", "colorbar": True}
mpd.show_versions()
MovingPandas 0.20.0 SYSTEM INFO ----------- python : 3.10.15 | packaged by conda-forge | (main, Oct 16 2024, 01:15:49) [MSC v.1941 64 bit (AMD64)] executable : c:\Users\Agarkovam\AppData\Local\miniforge3\envs\mpd-ex\python.exe machine : Windows-10-10.0.19045-SP0 GEOS, GDAL, PROJ INFO --------------------- GEOS : None GEOS lib : None GDAL : None GDAL data dir: None PROJ : 9.5.0 PROJ data dir: C:\Users\Agarkovam\AppData\Local\miniforge3\envs\mpd-ex\Library\share\proj PYTHON DEPENDENCIES ------------------- geopandas : 1.0.1 pandas : 2.2.3 fiona : None numpy : 1.23.1 shapely : 2.0.6 pyproj : 3.7.0 matplotlib : 3.9.2 mapclassify: 2.8.1 geopy : 2.4.1 holoviews : 1.20.0 hvplot : 0.11.1 geoviews : 1.13.0 stonesoup : 1.4
Loading the rover & heli dataΒΆ
"The car-sized Perseverance and its little helicopter buddy Ingenuity landed together inside Mars' Jezero Crater on Feb. 18." https://www.space.com/perseverance-rover-100-mars-days (by Mike Wall published June 02, 2021)
"One sol lasts about 24 hours and 40 minutes, slightly longer than an Earth day." https://www.space.com/perseverance-rover-100-mars-days
InΒ [2]:
def to_timestamp(row):
start_time = datetime(2021, 2, 18, 0, 0, 0) # sol 0
try:
sol = row["sol"] # rover
except KeyError:
sol = row["Sol"] # heli
td = timedelta(hours=24 * sol, minutes=40 * sol)
return start_time + td
def get_df_from_url(url):
file = url.split("/")[-1]
if not exists(file):
urlretrieve(url, file)
gdf = read_file(file)
gdf["time"] = gdf.apply(to_timestamp, axis=1)
gdf.set_index("time", inplace=True)
return gdf
m20_waypoints_json = (
"https://mars.nasa.gov/mmgis-maps/M20/Layers/json/M20_waypoints.json"
)
heli_waypoints_json = (
"https://mars.nasa.gov/mmgis-maps/M20/Layers/json/m20_heli_waypoints.json"
)
m20_df = get_df_from_url(m20_waypoints_json)
heli_df = get_df_from_url(heli_waypoints_json)
print(f"M20 records: {len(m20_df)}")
print(f"Heli records: {len(heli_df)}")
M20 records: 497 Heli records: 73
InΒ [3]:
m20_df.describe()
Out[3]:
site | drive | sol | easting | northing | elev_geoid | elev_radii | radius | lon | lat | roll | pitch | yaw | yaw_rad | tilt | dist_m | dist_total_m | dist_km | dist_mi | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
count | 497.000000 | 497.000000 | 497.000000 | 4.970000e+02 | 4.970000e+02 | 497.000000 | 497.000000 | 4.970000e+02 | 497.000000 | 497.000000 | 497.000000 | 497.000000 | 497.000000 | 497.000000 | 497.000000 | 497.000000 | 497.000000 | 497.000000 | 497.000000 |
mean | 33.265594 | 1667.835010 | 714.156942 | 4.350538e+06 | 1.094584e+06 | -2453.160767 | -4138.887966 | 3.392051e+06 | 77.380516 | 18.466302 | 0.069158 | 2.304528 | -43.397976 | -0.757437 | 5.719657 | 61.664979 | 12020.495105 | 12.012773 | 7.464414 |
std | 18.341130 | 1629.013945 | 384.867289 | 3.055007e+03 | 1.266350e+03 | 126.795920 | 125.520535 | 1.255206e+02 | 0.054338 | 0.021364 | 5.060485 | 5.518405 | 99.027564 | 1.728359 | 8.501640 | 77.437150 | 10764.778606 | 10.752641 | 6.681458 |
min | 3.000000 | 0.000000 | 13.000000 | 4.345509e+06 | 1.092270e+06 | -2585.869629 | -4266.522461 | 3.391923e+06 | 77.291067 | 18.427253 | -16.503758 | -15.895000 | -179.864359 | -3.139225 | -148.482000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
25% | 20.000000 | 400.000000 | 404.000000 | 4.347451e+06 | 1.093661e+06 | -2551.550537 | -4236.529000 | 3.391953e+06 | 77.325614 | 18.450726 | -2.097687 | -0.512600 | -119.315800 | -2.082500 | 1.621186 | 2.401000 | 288.274000 | 0.288000 | 0.179000 |
50% | 33.000000 | 1222.000000 | 707.000000 | 4.351669e+06 | 1.094613e+06 | -2517.544189 | -4201.832031 | 3.391988e+06 | 77.400638 | 18.466787 | 0.007872 | 1.088109 | -77.732483 | -1.356688 | 4.661634 | 27.434000 | 11688.190000 | 11.688000 | 7.263000 |
75% | 51.000000 | 2448.000000 | 1101.000000 | 4.353081e+06 | 1.095915e+06 | -2375.036133 | -4063.509521 | 3.392126e+06 | 77.425757 | 18.488754 | 2.382374 | 4.346000 | 22.324000 | 0.389600 | 9.242036 | 98.099000 | 22590.684000 | 22.591000 | 14.037000 |
max | 61.000000 | 9436.000000 | 1318.000000 | 4.355183e+06 | 1.096562e+06 | -2033.547363 | -3717.454102 | 3.392473e+06 | 77.463133 | 18.499663 | 17.605792 | 19.742318 | 179.950349 | 3.140726 | 20.334536 | 473.921000 | 30414.464000 | 30.410000 | 18.900000 |
InΒ [4]:
m20_df.hvplot(
title="M20 & heli waypoints", hover_cols=["sol"], **hvplot_defaults
) * heli_df.hvplot()
Out[4]:
InΒ [5]:
m20_traj = mpd.Trajectory(m20_df, "m20")
heli_traj = mpd.Trajectory(heli_df, "heli")
InΒ [6]:
traj_plot = m20_traj.hvplot(
title="M20 & heli trajectories", line_width=3, **hvplot_defaults
) * heli_traj.hvplot(line_width=3, color="red", **hvplot_defaults)
traj_plot
Out[6]:
InΒ [7]:
m20_traj.hvplot(
title="Rover speed (only suitable for relative comparison)",
c="speed",
line_width=7,
**hvplot_defaults
)
Out[7]:
InΒ [8]:
m20_detector = mpd.TrajectoryStopDetector(m20_traj)
stop_points = m20_detector.get_stop_points(
min_duration=timedelta(seconds=60), max_diameter=100
)
stop_points["duration_days"] = stop_points["duration_s"] / (60 * 60 * 24)
stop_points.head()
Out[8]:
geometry | start_time | end_time | traj_id | duration_s | duration_days | |
---|---|---|---|---|---|---|
stop_id | ||||||
m20_2021-03-03 08:40:00 | POINT (77.45095 18.44463) | 2021-03-03 08:40:00 | 2021-03-05 10:00:00 | m20 | 177600.0 | 2.055556 |
m20_2021-03-06 10:40:00 | POINT (77.45164 18.44517) | 2021-03-06 10:40:00 | 2021-03-22 21:20:00 | m20 | 1420800.0 | 16.444444 |
m20_2021-03-23 22:00:00 | POINT (77.45102 18.44487) | 2021-03-23 22:00:00 | 2021-04-08 08:00:00 | m20 | 1332000.0 | 15.416667 |
m20_2021-04-09 08:40:00 | POINT (77.45228 18.44453) | 2021-04-09 08:40:00 | 2021-05-15 08:00:00 | m20 | 3108000.0 | 35.972222 |
m20_2021-05-17 09:20:00 | POINT (77.45221 18.44388) | 2021-05-17 09:20:00 | 2021-05-31 18:40:00 | m20 | 1243200.0 | 14.388889 |
InΒ [9]:
heli_detector = mpd.TrajectoryStopDetector(heli_traj)
heli_stop_points = heli_detector.get_stop_points(
min_duration=timedelta(seconds=60), max_diameter=100
)
heli_stop_points["duration_days"] = heli_stop_points["duration_s"] / (60 * 60 * 24)
heli_stop_points.head()
Out[9]:
geometry | start_time | end_time | traj_id | duration_s | duration_days | |
---|---|---|---|---|---|---|
stop_id | ||||||
heli_2021-04-03 04:40:00 | POINT (77.45102 18.44486) | 2021-04-03 04:40:00 | 2021-04-29 22:00:00 | heli | 2308800.0 | 26.722222 |
heli_2021-08-04 12:40:00 | POINT (77.43916 18.43277) | 2021-08-04 12:40:00 | 2021-10-23 16:40:00 | heli | 6926400.0 | 80.166667 |
heli_2022-03-23 18:40:00 | POINT (77.44288 18.45068) | 2022-03-23 18:40:00 | 2022-04-03 01:20:00 | heli | 888000.0 | 10.277778 |
heli_2022-06-10 22:00:00 | POINT (77.41766 18.45597) | 2022-06-10 22:00:00 | 2022-08-19 19:20:00 | heli | 6038400.0 | 69.888889 |
heli_2022-09-23 18:00:00 | POINT (77.41218 18.45572) | 2022-09-23 18:00:00 | 2022-12-09 20:00:00 | heli | 6660000.0 | 77.083333 |
InΒ [10]:
stop_point_plot = stop_points.hvplot(
title="M20 & heli stops ",
geo=True,
size=np.log(dim("duration_days")) * 10,
hover_cols=["duration_days"],
color="blue",
alpha=0.5,
)
heli_stop_plot = heli_stop_points.hvplot(
geo=True,
size=np.log(dim("duration_days")) * 10,
hover_cols=["duration_days"],
color="red",
alpha=0.5,
)
stop_point_plot * heli_stop_plot * traj_plot
Out[10]:
Mars background mapΒΆ
Compare to https://mars.nasa.gov/mars2020/mission/where-is-the-rover/
InΒ [11]:
from bokeh.models import TMSTileSource
tile_url = "http://s3-eu-west-1.amazonaws.com/whereonmars.cartodb.net/celestia_mars-shaded-16k_global/{Z}/{X}/{Y}.png"
def mars_tiles(plot, element):
plot.state.add_tile(TMSTileSource(url=tile_url), level="underlay")
traj_map = m20_traj.hvplot(
title="M20 & heli trajectories", tiles=None
) * heli_traj.hvplot(color="red", **hvplot_defaults)
traj_map.opts(hooks=[mars_tiles])
Out[11]:
Work in progress:ΒΆ
InΒ [12]:
from geoviews.element import WMTS
MarsImagery = WMTS(
"https://trek.nasa.gov/tiles/Mars/EQ/Mars_MGS_MOLA_ClrShade_merge_global_463m/1.0.0/default/default028mm/{Z}/{Y}/{X}.jpg",
name="Mars",
)
m20_traj.hvplot(title="M20 & heli trajectories", tiles=MarsImagery) * heli_traj.hvplot(
color="red", **hvplot_defaults
)
Out[12]: