Mars Rover & Heli DemoΒΆ

No description has been provided for this image

Binder IPYNB HTML

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:

  1. 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.22.4

SYSTEM INFO
-----------
python     : 3.10.19 | packaged by conda-forge | (main, Jan 26 2026, 23:45:08) [GCC 14.3.0]
executable : /home/anita/miniforge3/envs/mpd-ex/bin/python
machine    : Linux-6.8.0-107-generic-x86_64-with-glibc2.39

PROJ INFO
-----------
PROJ       : 9.6.2
PROJ data dir: /home/anita/miniforge3/envs/mpd-ex/share/proj

PYTHON DEPENDENCIES
-------------------
numpy      : 1.23.1
geopandas  : 1.0.1
geopy      : 2.4.1
geoviews   : 1.15.1
holoviews  : 1.22.1
hvplot     : 0.12.2
mapclassify: 2.8.1
matplotlib : 3.10.8
pandas     : 2.3.3
pyproj     : 3.7.1
shapely    : 2.1.2
stonesoup  : 1.8

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: 652
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 652.000000 652.000000 652.000000 6.520000e+02 6.520000e+02 652.000000 652.000000 6.520000e+02 652.000000 652.000000 652.000000 652.000000 652.000000 652.000000 652.000000 652.000000 652.000000 652.000000 652.000000
mean 43.050613 1662.193252 912.934049 4.348907e+06 1.094340e+06 -2329.706883 -4404.102290 3.183431e+06 77.351517 18.462181 0.313468 2.358164 -37.811160 -0.659929 7.372137 63.756023 16183.142138 16.183098 10.055675
std 24.272497 1608.145504 499.907091 4.024937e+03 1.251388e+03 252.171655 1450.674001 8.170431e+05 0.071589 0.021112 5.848994 6.544248 100.374611 1.751867 5.301634 81.352398 13926.989229 13.927000 8.653890
min 3.000000 0.000000 13.000000 4.341418e+06 1.092257e+06 -2585.869629 -9999.000000 -9.999000e+03 77.218302 18.427034 -18.770257 -16.937142 -179.864359 -3.139225 0.121048 0.000000 0.000000 0.000000 0.000000
25% 25.000000 371.500000 442.500000 4.345620e+06 1.093481e+06 -2545.003250 -4238.141724 3.391952e+06 77.293053 18.447689 -2.575188 -0.866872 -115.867747 -2.022274 2.503709 2.519000 443.786500 0.445000 0.275000
50% 45.000000 1250.000000 925.500000 4.348898e+06 1.094136e+06 -2414.716675 -4109.816162 3.392080e+06 77.351360 18.458740 0.165101 1.302947 -74.503900 -1.300338 6.449319 24.705000 14299.754500 14.300000 8.885000
75% 61.000000 2485.000000 1301.000000 4.352108e+06 1.095552e+06 -2056.029541 -4038.487183 3.392152e+06 77.408450 18.482622 3.176293 5.948900 29.995488 0.523520 11.635057 104.293500 28316.740500 28.312500 17.597500
max 87.000000 9436.000000 1830.000000 4.355183e+06 1.096562e+06 -1785.999023 -3468.034180 3.392722e+06 77.463133 18.499663 17.605792 20.135651 179.950349 3.140726 20.676529 488.525000 41568.927000 41.570000 25.830000
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-30 18:00:00 m20 1154400.0 13.361111
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.44287 18.45067) 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.45598) 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.45571) 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]:

Continue exploring MovingPandasΒΆ

  1. Bird migration analysis
  2. Ship data analysis
  3. Horse collar data exploration
  4. OSM traces
  5. Soccer game
  6. Mars rover & heli
  7. Ever Given
  8. Iceberg
  9. Pollution data