Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ The application will be available at this address: [http://localhost:8081/api.ht
To run the application directly in your local environment, configure the application to access data over `HTTP` then run it using `uvicorn`:

```bash
TITILER_CMR_S3_AUTH_ACCESS=external uvicorn titiler.cmr.main:app --reload
TITILER_CMR_S3_AUTH_ACCESS=external uv run uvicorn titiler.cmr.main:app --reload
```

The application will be available at this address: [http://localhost:8000/api.html](http://localhost:8000/api.html)
Expand All @@ -105,9 +105,9 @@ Environment variables for the `veda-deploy` deployment should be configured in t

The application-specific (`AppSettings`) environment variables which should be set in the `veda-deploy` AWS secret are:

* `TITILER_CMR_S3_AUTH_STRATEGY=iam`
* `TITILER_CMR_ROOT_PATH=/api/titiler-cmr`
* `TITILER_CMR_AWS_REQUEST_PAYER=requester`
- `TITILER_CMR_S3_AUTH_STRATEGY=iam`
- `TITILER_CMR_ROOT_PATH=/api/titiler-cmr`
- `TITILER_CMR_AWS_REQUEST_PAYER=requester`

### Deployment to a development/test instance

Expand Down
105 changes: 99 additions & 6 deletions docs/examples/time_series_example.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,16 @@
"source": [
"from IPython.display import IFrame\n",
"\n",
"# if running titiler-cmr in the docker network\n",
"# titiler_endpoint = \"http://localhost:8081\"\n",
"# if running titiler-cmr locally\n",
"# titiler_endpoint = \"http://localhost:8000\"\n",
"\n",
"# titiler_endpoint = \"http://localhost:8081\" # docker network endpoint\n",
"# titiler_endpoint = (\n",
"# \"https://staging.openveda.cloud/api/titiler-cmr\" # VEDA staging endpoint\n",
"# )\n",
"titiler_endpoint = (\n",
" \"https://staging.openveda.cloud/api/titiler-cmr\" # VEDA staging endpoint\n",
" \"https://v4jec6i5c0.execute-api.us-west-2.amazonaws.com\" # dev endpoint\n",
")\n",
"# titiler_endpoint = \"https://v4jec6i5c0.execute-api.us-west-2.amazonaws.com\" # dev endpoint\n",
"\n",
"IFrame(f\"{titiler_endpoint}/api.html#Timeseries\", 900, 500)"
]
Expand Down Expand Up @@ -406,9 +408,10 @@
"stds = []\n",
"\n",
"for date_str, values in data.items():\n",
" stats = list(values.values())[0]\n",
" dates.append(datetime.fromisoformat(date_str))\n",
" means.append(values[\"analysed_sst\"][\"mean\"])\n",
" stds.append(values[\"analysed_sst\"][\"std\"])\n",
" means.append(stats[\"mean\"])\n",
" stds.append(stats[\"std\"])\n",
"\n",
"plt.figure(figsize=(10, 6))\n",
"\n",
Expand All @@ -435,6 +438,96 @@
"plt.show()"
]
},
{
"cell_type": "markdown",
"id": "2b09bafc-b7c4-425a-b772-50be4cdad178",
"metadata": {},
"source": [
"## Example: {datetime} string interpolation with sel parameter\n",
"\n",
"Datasets with more than two dimensions (e.g. x, y, time) will require the use of the `sel` parameter to pick a particular level of a dimension. Here is an example that shows how to get time series statistics from the TROPESS O3 dataset which consists of annual granules each with dimensions for `time` (monthly) and `lev`.\n",
"\n",
"For this dataset, queries within a single year will return the same granule. If `{datetime}` is present in a `sel` query parameter value, titiler-cmr will pass the `datetime` query parameter value to the `sel` parameter by interpolating the string `\"time={datetime}\"`. "
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5a5ed6f0-adb9-4f5f-866e-8447a1a07cb4",
"metadata": {},
"outputs": [],
"source": [
"%%time\n",
"minx, miny, maxx, maxy = -98.676, 18.857, -81.623, 31.097\n",
"geojson = Feature(\n",
" type=\"Feature\",\n",
" geometry=Polygon.from_bounds(minx, miny, maxx, maxy),\n",
" properties={},\n",
")\n",
"request = httpx.post(\n",
" f\"{titiler_endpoint}/timeseries/statistics\",\n",
" params={\n",
" \"concept_id\": \"C2837626477-GES_DISC\",\n",
" \"datetime\": \"2010-01-01T00:00:01Z/2021-03-31T23:59:59Z\",\n",
" \"step\": \"P1M\",\n",
" \"temporal_mode\": \"point\",\n",
" \"variable\": \"o3\",\n",
" \"backend\": \"xarray\",\n",
" \"sel\": [\"time={datetime}\", \"lev=1000\"],\n",
" \"sel_method\": \"nearest\",\n",
" },\n",
" json=geojson.model_dump(exclude_none=True),\n",
" timeout=None,\n",
")\n",
"\n",
"request.raise_for_status()\n",
"response = request.json()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "68925555-81a1-416b-bbc5-6bd81f597f09",
"metadata": {},
"outputs": [],
"source": [
"data = response[\"properties\"][\"statistics\"]\n",
"\n",
"dates = []\n",
"means = []\n",
"stds = []\n",
"\n",
"for date_str, values in data.items():\n",
" stats = list(values.values())[0]\n",
" dates.append(datetime.fromisoformat(date_str))\n",
" means.append(stats[\"mean\"])\n",
" stds.append(stats[\"std\"])\n",
"\n",
"plt.figure(figsize=(10, 6))\n",
"\n",
"plt.plot(dates, means, \"b-\", label=\"Mean\")\n",
"\n",
"plt.fill_between(\n",
" dates,\n",
" np.array(means) - np.array(stds),\n",
" np.array(means) + np.array(stds),\n",
" alpha=0.2,\n",
" color=\"b\",\n",
" label=\"Standard Deviation\",\n",
")\n",
"\n",
"plt.xlabel(\"Date\")\n",
"plt.ylabel(\"O3\")\n",
"plt.title(\"Mean monthly O3 concentration in the Gulf of Mexico\")\n",
"plt.legend()\n",
"\n",
"plt.xticks(rotation=45)\n",
"\n",
"plt.tight_layout()\n",
"\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"id": "8b6faa38-3563-439c-af53-3f107bb1f09c",
Expand Down
106 changes: 102 additions & 4 deletions docs/examples/xarray_backend_example.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,12 @@
"from folium import Map, TileLayer\n",
"\n",
"# titiler_endpoint = \"http://localhost:8081\" # docker network endpoint\n",
"# titiler_endpoint = (\n",
"# \"https://staging.openveda.cloud/api/titiler-cmr\" # VEDA staging endpoint\n",
"# )\n",
"titiler_endpoint = (\n",
" \"https://staging.openveda.cloud/api/titiler-cmr\" # VEDA staging endpoint\n",
")\n",
"# titiler_endpoint = \"https://v4jec6i5c0.execute-api.us-west-2.amazonaws.com\" # dev endpoint"
" \"https://v4jec6i5c0.execute-api.us-west-2.amazonaws.com\" # dev endpoint\n",
")"
]
},
{
Expand Down Expand Up @@ -262,10 +264,106 @@
"print(json.dumps(r, indent=2))"
]
},
{
"cell_type": "markdown",
"id": "3eb898d1-8d02-4943-a17f-c11a21eb0124",
"metadata": {},
"source": [
"## Datetime string interpolation with the sel parameter\n",
"\n",
"Datasets with more than two dimensions (e.g. x, y, time) will require the use of the `sel` parameter to pick a particular level of a dimension. Here is an example that shows how to get statistics for a single time slice of a granule from the TROPESS O3 dataset. This dataset has annual granules each with dimensions for `time` (monthly) and `lev`.\n",
"\n",
"For this dataset, queries within a single year will return the same granule. If `{datetime}` is present in a `sel` query parameter value, titiler-cmr will pass the `datetime` query parameter value to the `sel` parameter by interpolating the string `\"time={datetime}\"`. "
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "462fa885-b3bd-4406-a360-c69dab0e44e6",
"metadata": {},
"outputs": [],
"source": [
"geojson_dict = {\n",
" \"type\": \"FeatureCollection\",\n",
" \"features\": [\n",
" {\n",
" \"type\": \"Feature\",\n",
" \"properties\": {},\n",
" \"geometry\": {\n",
" \"coordinates\": [\n",
" [\n",
" [-20.79973248834736, 83.55979308678764],\n",
" [-20.79973248834736, 75.0115425216471],\n",
" [14.483337068956956, 75.0115425216471],\n",
" [14.483337068956956, 83.55979308678764],\n",
" [-20.79973248834736, 83.55979308678764],\n",
" ]\n",
" ],\n",
" \"type\": \"Polygon\",\n",
" },\n",
" }\n",
" ],\n",
"}\n",
"\n",
"r = httpx.post(\n",
" f\"{titiler_endpoint}/statistics\",\n",
" params=(\n",
" (\"concept_id\", \"C2837626477-GES_DISC\"),\n",
" # Datetime for CMR granule query\n",
" (\"datetime\", datetime(2021, 10, 10, tzinfo=timezone.utc).isoformat()),\n",
" # xarray backend query parameters\n",
" (\"backend\", \"xarray\"),\n",
" (\"variable\", \"o3\"),\n",
" (\"sel\", \"time={datetime}\"), #\n",
" (\"sel\", \"lev=1000\"),\n",
" (\"sel_method\", \"nearest\"),\n",
" ),\n",
" json=geojson_dict,\n",
" timeout=60,\n",
").json()\n",
"\n",
"print(json.dumps(r, indent=2))"
]
},
{
"cell_type": "markdown",
"id": "50178b80-0a84-47bc-ac74-50d0a1f6f489",
"metadata": {},
"source": [
"You can chose a different time slice from the same granule simply by updating the `datetime` query parameter."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "136ef791-a5b6-42d7-bce3-d07e108d940f",
"metadata": {},
"outputs": [],
"source": [
"r = httpx.post(\n",
" f\"{titiler_endpoint}/statistics\",\n",
" params=(\n",
" (\"concept_id\", \"C2837626477-GES_DISC\"),\n",
" # Datetime for CMR granule query\n",
" (\"datetime\", datetime(2021, 12, 10, tzinfo=timezone.utc).isoformat()),\n",
" # xarray backend query parameters\n",
" (\"backend\", \"xarray\"),\n",
" (\"variable\", \"o3\"),\n",
" (\"sel\", \"time={datetime}\"), #\n",
" (\"sel\", \"lev=1000\"),\n",
" (\"sel_method\", \"nearest\"),\n",
" ),\n",
" json=geojson_dict,\n",
" timeout=60,\n",
").json()\n",
"\n",
"print(json.dumps(r, indent=2))"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7a38b762-ff68-40d3-84eb-0e37957381e0",
"id": "3d2b199f-8448-4177-b079-94b45b988d7c",
"metadata": {},
"outputs": [],
"source": []
Expand Down
87 changes: 87 additions & 0 deletions tests/test_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from titiler.cmr import dependencies
from titiler.cmr.enums import MediaType
from titiler.cmr.errors import InvalidDatetime
from titiler.xarray.dependencies import CompatXarrayParams


def test_media_type():
Expand Down Expand Up @@ -167,3 +168,89 @@ def test_cmr_query_more():
concept_id="something",
datetime="2019-02-12T09:00:00Z/2019-02-12",
)


def test_interpolated_xarray_params_single_datetime():
"""Test InterpolatedXarrayParams with single datetime interpolation."""
xarray_params = CompatXarrayParams(
variable="temperature", sel=["time={datetime}", "lev=1000"], method="nearest"
)

single_datetime = datetime(2025, 9, 23, 0, 0, 0, tzinfo=timezone.utc)
cmr_query_params = {"concept_id": "test_concept", "temporal": single_datetime}

result = dependencies.interpolated_xarray_ds_params(xarray_params, cmr_query_params)

assert result.sel == [f"time={single_datetime.isoformat()}", "lev=1000"]
assert result.variable == "temperature"
assert result.method == "nearest"


def test_interpolated_xarray_params_datetime_range():
"""Test InterpolatedXarrayParams with datetime range (uses start datetime)."""
xarray_params = CompatXarrayParams(
variable="temperature", sel=["time={datetime}"], method="nearest"
)

start_datetime = datetime(2025, 9, 23, 0, 0, 0, tzinfo=timezone.utc)
end_datetime = datetime(2025, 9, 24, 0, 0, 0, tzinfo=timezone.utc)
cmr_query_params = {
"concept_id": "test_concept",
"temporal": (start_datetime, end_datetime),
}

result = dependencies.interpolated_xarray_ds_params(xarray_params, cmr_query_params)

assert result.sel == [f"time={start_datetime.isoformat()}"]


def test_interpolated_xarray_params_no_datetime_template():
"""Test InterpolatedXarrayParams when sel doesn't contain datetime template."""
xarray_params = CompatXarrayParams(
variable="temperature",
sel=["time=2025-01-01T00:00:00Z", "lev=1000"],
method="nearest",
)

single_datetime = datetime(2025, 9, 23, 0, 0, 0, tzinfo=timezone.utc)
cmr_query_params = {"concept_id": "test_concept", "temporal": single_datetime}

result = dependencies.interpolated_xarray_ds_params(xarray_params, cmr_query_params)

assert result.sel == ["time=2025-01-01T00:00:00Z", "lev=1000"]


def test_interpolated_xarray_params_no_sel():
"""Test InterpolatedXarrayParams when sel is None or empty."""
xarray_params = CompatXarrayParams(
variable="temperature", sel=None, method="nearest"
)

single_datetime = datetime(2025, 9, 23, 0, 0, 0, tzinfo=timezone.utc)
cmr_query_params = {"concept_id": "test_concept", "temporal": single_datetime}

result = dependencies.interpolated_xarray_ds_params(xarray_params, cmr_query_params)

assert result.sel is None
assert result.variable == "temperature"


def test_interpolated_xarray_params_multiple_templates():
"""Test InterpolatedXarrayParams with multiple datetime templates."""
xarray_params = CompatXarrayParams(
variable="temperature",
sel=["time={datetime}", "start_time={datetime}", "lev=1000"],
method="nearest",
)

single_datetime = datetime(2025, 9, 23, 12, 30, 45, tzinfo=timezone.utc)
cmr_query_params = {"concept_id": "test_concept", "temporal": single_datetime}

result = dependencies.interpolated_xarray_ds_params(xarray_params, cmr_query_params)

expected = [
f"time={single_datetime.isoformat()}",
f"start_time={single_datetime.isoformat()}",
"lev=1000",
]
assert result.sel == expected
Loading
Loading