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.23.0

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-134-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: 691
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 691.000000 691.000000 691.000000 6.910000e+02 6.910000e+02 691.000000 691.000000 6.910000e+02 691.000000 691.000000 691.000000 691.000000 691.000000 691.000000 691.000000 691.000000 691.000000 691.000000 691.000000
mean 45.609262 1699.693198 967.881331 4.348491e+06 1.094226e+06 -2309.119631 -4719.877993 3.003194e+06 77.344107 18.460250 0.419576 2.346583 -33.414066 -0.583185 7.442145 64.201207 17063.059039 17.062996 10.602431
std 25.797273 1618.752874 535.152015 4.265121e+03 1.303901e+03 259.029506 1911.790268 1.083376e+06 0.075861 0.021998 5.868255 6.525917 102.699329 1.792441 5.204127 81.545640 14685.660927 14.685633 9.125303
min 3.000000 0.000000 13.000000 4.341395e+06 1.091838e+06 -2585.869629 -9999.000000 -9.999000e+03 77.217900 18.419970 -18.770257 -16.937142 -179.864359 -3.139225 0.121048 0.000000 0.000000 0.000000 0.000000
25% 26.000000 401.000000 460.500000 4.344520e+06 1.093301e+06 -2543.984863 -4252.142578 3.391938e+06 77.273490 18.444647 -2.568989 -0.903244 -113.549552 -1.981814 2.717636 2.501000 437.739000 0.440000 0.270000
50% 47.000000 1280.000000 999.000000 4.348646e+06 1.094090e+06 -2407.488281 -4142.341309 3.392048e+06 77.346877 18.457958 0.223273 1.355454 -71.401508 -1.246191 6.802178 24.853000 15651.270000 15.650000 9.730000
75% 63.000000 2544.000000 1350.000000 4.352099e+06 1.095510e+06 -1977.822754 -4040.725220 3.392149e+06 77.408293 18.481917 3.498807 6.348238 34.836224 0.608007 11.489210 104.952500 29721.303000 29.720000 18.465000
max 90.000000 9436.000000 1941.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 44363.034000 44.360000 27.570000
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