How to Use the ECCO Sea Ice Velocity Dataset¶
This notebook demonstrates how to work with the ECCO L4 Sea Ice Velocity dataset using TiTiler-CMR.
About the Dataset¶
The ECCO (Estimating the Circulation and Climate of the Ocean) L4 Sea Ice Velocity dataset provides monthly estimates of Arctic sea ice motion on a 0.5-degree latitude/longitude grid. The data spans from 1992 to 2018 and includes two velocity components:
- SIeice: Eastward sea ice velocity (m/s)
- SInice: Northward sea ice velocity (m/s)
This dataset is valuable for understanding Arctic sea ice dynamics, climate change impacts on polar regions, and ocean circulation patterns.
What You Will Learn¶
This notebook covers:
- Inspecting dataset metadata with the
/compatibilityendpoint - Visualizing velocity fields on interactive maps with
/tiles - Computing statistics for custom regions with
/xarray/statistics - Analyzing temporal variations with
/timeseries - Comparing multiple variables (eastward vs. northward components)
import os
import json
import httpx
from folium import LayerControl, Map, TileLayer
titiler_endpoint = os.getenv(
"TITILER_CMR_ENDPOINT", "https://openveda.cloud/api/titiler-cmr"
)
collection_concept_id = "C1990404790-POCLOUD"
temporal = "1992-01-16T18:00:00Z"
Step 1: Dataset Inspection with the Compatibility Endpoint¶
Before working with any dataset, use the /compatibility endpoint to understand:
- Available variables and their data types
- Coordinate dimensions (latitude, longitude, time)
- Temporal coverage and resolution
- Variable value ranges (min, max, mean, percentiles)
This information helps you craft appropriate requests and understand the data structure.
compatibility_response = httpx.get(
f"{titiler_endpoint}/compatibility",
params={"collection_concept_id": collection_concept_id},
timeout=None,
).json()
print(json.dumps(compatibility_response, indent=2))
{
"concept_id": "C1990404790-POCLOUD",
"backend": "xarray",
"datetime": [
{
"EndsAtPresentFlag": false,
"RangeDateTimes": [
{
"BeginningDateTime": "1992-01-01T00:00:00.000Z",
"EndingDateTime": "2018-01-01T00:00:00.000Z"
}
]
}
],
"variables": {
"SIeice": {
"shape": [
1,
360,
720
],
"dtype": "float32",
"min": -0.4000000059604645,
"max": 0.4000000059604645,
"mean": 0.01383803877979517,
"p01": -0.3568556708097458,
"p05": -0.2387306362390518,
"p95": 0.2615017294883728,
"p99": 0.3231524538993836
},
"SInice": {
"shape": [
1,
360,
720
],
"dtype": "float32",
"min": -0.3943014144897461,
"max": 0.35976916551589966,
"mean": 0.00021590900723822415,
"p01": -0.18944732397794725,
"p05": -0.1089107483625412,
"p95": 0.10234527289867401,
"p99": 0.17241160720586776
}
},
"dimensions": {
"time": 1,
"latitude": 360,
"longitude": 720,
"nv": 2
},
"coordinates": {
"time": {
"size": 1,
"dtype": "datetime64[ns]",
"min": null,
"max": null
},
"latitude": {
"size": 360,
"dtype": "float32",
"min": -89.75,
"max": 89.75
},
"longitude": {
"size": 720,
"dtype": "float32",
"min": -179.75,
"max": 179.75
},
"time_bnds": {
"size": 2,
"dtype": "datetime64[ns]",
"min": null,
"max": null
},
"latitude_bnds": {
"size": 720,
"dtype": "float32",
"min": -90.0,
"max": 90.0
},
"longitude_bnds": {
"size": 1440,
"dtype": "float32",
"min": -180.0,
"max": 180.0
}
},
"example_assets": "s3://podaac-ops-cumulus-protected/ECCO_L4_SEA_ICE_VELOCITY_05DEG_MONTHLY_V4R4/SEA_ICE_VELOCITY_mon_mean_1992-01_ECCO_V4r4_latlon_0p50deg.nc",
"sample_asset_raster_info": null,
"links": [
{
"rel": "tilejson",
"href": "https://openveda.cloud/api/titiler-cmr/xarray/WebMercatorQuad/tilejson.json?collection_concept_id=C1990404790-POCLOUD&variable=SIeice&temporal={temporal}",
"title": "TileJSON",
"type": "application/json"
},
{
"rel": "map",
"href": "https://openveda.cloud/api/titiler-cmr/xarray/WebMercatorQuad/map.html?collection_concept_id=C1990404790-POCLOUD&variable=SIeice&temporal={temporal}",
"title": "Map viewer",
"type": "text/html"
},
{
"rel": "tile",
"href": "https://openveda.cloud/api/titiler-cmr/xarray/tiles/WebMercatorQuad/{z}/{x}/{y}?collection_concept_id=C1990404790-POCLOUD&variable=SIeice&temporal={temporal}",
"title": "Map tile",
"type": "image/png"
}
]
}
The output shows:
SIeice: Eastward velocity component (shape: [1, 360, 720])SInice: Northward velocity component (shape: [1, 360, 720])- Temporal range: 1992-01-01 through 2018-01-01 (monthly snapshots)
- Value ranges: Both variables range from approximately -0.4 to +0.4 m/s
This tells us the dataset is monthly, global in coverage, and well-suited for tile generation and statistical analysis.
The compatibility metadata shows two variables: SIeice and SInice, which correspond to the eastward and northward sea ice velocity components.
It also reports a temporal range from 1992-01-01 through 2018-01-01.
Step 2: Tile-Based Visualization of Eastward Velocity¶
Use the /tiles/WebMercatorQuad endpoint to render the SIeice (eastward) variable as interactive map tiles.
Tiles enable efficient web-based visualization, allowing users to zoom and pan without downloading the entire dataset.
r = httpx.get(
f"{titiler_endpoint}/xarray/WebMercatorQuad/tilejson.json",
params=(
("collection_concept_id", collection_concept_id),
("temporal", temporal),
("variables", "SIeice"),
("rescale", "-0.4,0.4"),
("colormap_name", "rdbu"),
),
timeout=None,
).json()
m = Map(location=[72, -57], zoom_start=3)
TileLayer(
tiles=r["tiles"][0],
attr="NASA / TiTiler-CMR",
name="SIeice eastward velocity",
overlay=True,
control=True,
).add_to(m)
LayerControl().add_to(m)
m
Step 3: Regional Statistics for Eastward Velocity¶
Use the /xarray/statistics endpoint to compute summary statistics for the SIeice variable within a custom polygon region.
This is useful for:
- Comparing velocity magnitudes across different regions
- Detecting anomalies or extreme values
- Creating time series of regional averages
Compute statistics for a region¶
Use the /xarray/statistics endpoint to summarize the SIeice variable over a polygon.
geojson_dict = {
"type": "Feature",
"properties": {},
"geometry": {
"type": "Polygon",
"coordinates": [
[[-60.0, 70.0], [-60.0, 75.0], [-55.0, 75.0], [-55.0, 70.0], [-60.0, 70.0]]
],
},
}
stats_response = httpx.post(
f"{titiler_endpoint}/xarray/statistics",
params={
"collection_concept_id": collection_concept_id,
"temporal": temporal,
"variables": "SIeice",
},
json=geojson_dict,
timeout=None,
).json()
print(json.dumps(stats_response, indent=2))
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
-60.0,
70.0
],
[
-60.0,
75.0
],
[
-55.0,
75.0
],
[
-55.0,
70.0
],
[
-60.0,
70.0
]
]
]
},
"properties": {
"statistics": {
"b1": {
"min": -0.004872002638876438,
"max": 0.02276342175900936,
"mean": 0.009062288329005241,
"count": 78.0,
"sum": 0.7068585157394409,
"std": 0.007456335439217724,
"median": 0.00937563180923462,
"majority": 0.0017839688807725906,
"minority": -0.0030098063871264458,
"unique": 40.0,
"histogram": [
[
6,
7,
8,
11,
3,
10,
11,
8,
13,
1
],
[
-0.004872002638876438,
-0.002108460059389472,
0.0006550825200974941,
0.003418625332415104,
0.006182167679071426,
0.008945710025727749,
0.011709253303706646,
0.014472794719040394,
0.017236337065696716,
0.019999880343675613,
0.02276342175900936
]
],
"valid_percent": 78.0,
"masked_pixels": 22.0,
"valid_pixels": 78.0,
"description": "0",
"percentile_2": -0.004872002638876438,
"percentile_98": 0.019531458616256714
}
},
"used_assets": [
"SEA_ICE_VELOCITY_mon_mean_1992-01_ECCO_V4r4_latlon_0p50deg"
]
}
}
The statistics output includes detailed summaries:
min,max,mean: Central tendency and rangestd: Standard deviation (variability)median: 50th percentilehistogram: Distribution of values in the regionpercentile_2,percentile_98: Extreme percentilesvalid_percent: Coverage percentage in the polygonused_assets: The granule that was used to compute these statistics
Step 4: Northward Velocity Component (SInice)¶
Now let's examine the northward component of sea ice velocity. By comparing both components, we get a complete picture of sea ice motion.
Tile Visualization of Northward Velocity¶
Visualize SInice (northward velocity) on an interactive map.
r = httpx.get(
f"{titiler_endpoint}/xarray/WebMercatorQuad/tilejson.json",
params=(
("collection_concept_id", collection_concept_id),
("temporal", temporal),
("variables", "SInice"),
("rescale", "-0.4,0.4"),
("colormap_name", "rdbu"),
),
timeout=None,
).json()
m = Map(location=[72, -57], zoom_start=3)
TileLayer(
tiles=r["tiles"][0],
attr="NASA / TiTiler-CMR",
name="SInice northward velocity",
overlay=True,
control=True,
).add_to(m)
LayerControl().add_to(m)
m
Statistics for Northward Velocity¶
Compute regional statistics for the northward component in the same Arctic region.
stats_northward = httpx.post(
f"{titiler_endpoint}/xarray/statistics",
params={
"collection_concept_id": collection_concept_id,
"temporal": temporal,
"variables": "SInice",
},
json=geojson_dict,
timeout=None,
).json()
print(json.dumps(stats_northward, indent=2))
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
-60.0,
70.0
],
[
-60.0,
75.0
],
[
-55.0,
75.0
],
[
-55.0,
70.0
],
[
-60.0,
70.0
]
]
]
},
"properties": {
"statistics": {
"b1": {
"min": -0.07271765172481537,
"max": 0.011994512751698494,
"mean": -0.02668500319123268,
"count": 78.0,
"sum": -2.081430196762085,
"std": 0.023763466094909955,
"median": -0.03142465278506279,
"majority": -0.0633004754781723,
"minority": -0.07271765172481537,
"unique": 40.0,
"histogram": [
[
4,
8,
4,
9,
14,
1,
12,
8,
5,
13
],
[
-0.07271765172481537,
-0.06424643844366074,
-0.05577521771192551,
-0.047304004430770874,
-0.03883278742432594,
-0.030361570417881012,
-0.02189035713672638,
-0.013419140130281448,
-0.004947923123836517,
0.0035232901573181152,
0.011994512751698494
]
],
"valid_percent": 78.0,
"masked_pixels": 22.0,
"valid_pixels": 78.0,
"description": "0",
"percentile_2": -0.07006342709064484,
"percentile_98": 0.011994512751698494
}
},
"used_assets": [
"SEA_ICE_VELOCITY_mon_mean_1992-01_ECCO_V4r4_latlon_0p50deg"
]
}
}
Comparison: Eastward vs. Northward¶
By comparing the statistical outputs from both velocity components, you can assess whether sea ice motion is dominated by meridional (north-south) or zonal (east-west) flow in different regions.
The statistics output is returned for the single band in the selected subset.
To explore the northward component instead, change variable=SIeice to variable=SInice and rerun the request.
Time series analysis¶
Use the /timeseries endpoint to get statistics over multiple time steps for the same region.
timeseries_response = httpx.get(
f"{titiler_endpoint}/timeseries",
params={
"collection_concept_id": collection_concept_id,
"temporal": "1992-01-01T00:00:00Z/1992-03-01T00:00:00Z",
"step": "P1M",
"temporal_mode": "point",
},
timeout=None,
).json()
print("Available time steps:")
for item in timeseries_response:
print(item["temporal"])
Available time steps: 1992-01-01T00:00:00+00:00/1992-01-01T00:00:00+00:00 1992-02-01T00:00:00+00:00/1992-02-01T00:00:00+00:00 1992-03-01T00:00:00+00:00/1992-03-01T00:00:00+00:00
# Get statistics for the first few months of the dataset
temporal_range = "1992-01-01T00:00:00Z/1992-03-01T00:00:00Z"
timeseries_stats = httpx.post(
f"{titiler_endpoint}/xarray/timeseries/statistics",
params={
"collection_concept_id": collection_concept_id,
"temporal": "1992-01-01T00:00:00Z/1992-03-01T00:00:00Z",
"variables": "SIeice",
"step": "P1M",
"temporal_mode": "point",
},
json=geojson_dict,
timeout=None,
).json()
print(f"Statistics for {temporal_range}:")
print(json.dumps(timeseries_stats, indent=2))
Statistics for 1992-01-01T00:00:00Z/1992-03-01T00:00:00Z:
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[
-60.0,
70.0
],
[
-60.0,
75.0
],
[
-55.0,
75.0
],
[
-55.0,
70.0
],
[
-60.0,
70.0
]
]
]
},
"properties": {
"statistics": {
"1992-02-01T00:00:00+00:00/1992-02-01T00:00:00+00:00": {
"b1": {
"min": -0.004872002638876438,
"max": 0.02276342175900936,
"mean": 0.009062288329005241,
"count": 78.0,
"sum": 0.7068585157394409,
"std": 0.007456335439217724,
"median": 0.00937563180923462,
"majority": 0.0017839688807725906,
"minority": -0.0030098063871264458,
"unique": 40.0,
"histogram": [
[
6,
7,
8,
11,
3,
10,
11,
8,
13,
1
],
[
-0.004872002638876438,
-0.002108460059389472,
0.0006550825200974941,
0.003418625332415104,
0.006182167679071426,
0.008945710025727749,
0.011709253303706646,
0.014472794719040394,
0.017236337065696716,
0.019999880343675613,
0.02276342175900936
]
],
"valid_percent": 78.0,
"masked_pixels": 22.0,
"valid_pixels": 78.0,
"description": "0",
"percentile_2": -0.004872002638876438,
"percentile_98": 0.019531458616256714
}
},
"1992-03-01T00:00:00+00:00/1992-03-01T00:00:00+00:00": {
"b1": {
"min": -0.0015085544437170029,
"max": 0.020138688385486603,
"mean": 0.006678348872810602,
"count": 78.0,
"sum": 0.5209112167358398,
"std": 0.005901751845784247,
"median": 0.005913831293582916,
"majority": 0.0021750852465629578,
"minority": -0.0012709565926343203,
"unique": 40.0,
"histogram": [
[
15,
8,
7,
21,
5,
6,
2,
7,
3,
4
],
[
-0.0015085544437170029,
0.0006561698392033577,
0.0028208941221237183,
0.004985618405044079,
0.007150342687964439,
0.0093150669708848,
0.01147979125380516,
0.013644515536725521,
0.01580923981964588,
0.017973965033888817,
0.020138688385486603
]
],
"valid_percent": 78.0,
"masked_pixels": 22.0,
"valid_pixels": 78.0,
"description": "0",
"percentile_2": -0.0015085544437170029,
"percentile_98": 0.01801844872534275
}
}
}
}
}
Particle Visualization¶
This dataset can be visualized as an animated particle flow showing sea ice motion direction and speed. The particle demo is implemented with MapLibre GL JS and a custom 2D canvas particle system that decodes the two-band (SIeice, SInice) PNG tiles from TiTiler-CMR to drive particle movement.
You can view the interactive demo at https://hanzila1.github.io/sea-ice-particle-demo/. The source code is available in its standalone repository.
Next steps¶
- Use the
/compatibilityendpoint to inspect additional temporal ranges and variable min/max values. - Change the
temporaltimestamp to view other monthly snapshots in the 1992–2018 series. - Combine both components to compute speed magnitude:
sqrt(SIeice² + SInice²).