Skip to content

Commit ef8c494

Browse files
authored
feat: interpolate {datetime} in if sel includes {dim}={datetime} (#78)
1 parent 39abebc commit ef8c494

File tree

6 files changed

+355
-24
lines changed

6 files changed

+355
-24
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ The application will be available at this address: [http://localhost:8081/api.ht
9090
To run the application directly in your local environment, configure the application to access data over `HTTP` then run it using `uvicorn`:
9191

9292
```bash
93-
TITILER_CMR_S3_AUTH_ACCESS=external uvicorn titiler.cmr.main:app --reload
93+
TITILER_CMR_S3_AUTH_ACCESS=external uv run uvicorn titiler.cmr.main:app --reload
9494
```
9595

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

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

108-
* `TITILER_CMR_S3_AUTH_STRATEGY=iam`
109-
* `TITILER_CMR_ROOT_PATH=/api/titiler-cmr`
110-
* `TITILER_CMR_AWS_REQUEST_PAYER=requester`
108+
- `TITILER_CMR_S3_AUTH_STRATEGY=iam`
109+
- `TITILER_CMR_ROOT_PATH=/api/titiler-cmr`
110+
- `TITILER_CMR_AWS_REQUEST_PAYER=requester`
111111

112112
### Deployment to a development/test instance
113113

docs/examples/time_series_example.ipynb

Lines changed: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,16 @@
3434
"source": [
3535
"from IPython.display import IFrame\n",
3636
"\n",
37-
"# if running titiler-cmr in the docker network\n",
38-
"# titiler_endpoint = \"http://localhost:8081\"\n",
37+
"# if running titiler-cmr locally\n",
38+
"# titiler_endpoint = \"http://localhost:8000\"\n",
3939
"\n",
4040
"# titiler_endpoint = \"http://localhost:8081\" # docker network endpoint\n",
41+
"# titiler_endpoint = (\n",
42+
"# \"https://staging.openveda.cloud/api/titiler-cmr\" # VEDA staging endpoint\n",
43+
"# )\n",
4144
"titiler_endpoint = (\n",
42-
" \"https://staging.openveda.cloud/api/titiler-cmr\" # VEDA staging endpoint\n",
45+
" \"https://v4jec6i5c0.execute-api.us-west-2.amazonaws.com\" # dev endpoint\n",
4346
")\n",
44-
"# titiler_endpoint = \"https://v4jec6i5c0.execute-api.us-west-2.amazonaws.com\" # dev endpoint\n",
4547
"\n",
4648
"IFrame(f\"{titiler_endpoint}/api.html#Timeseries\", 900, 500)"
4749
]
@@ -406,9 +408,10 @@
406408
"stds = []\n",
407409
"\n",
408410
"for date_str, values in data.items():\n",
411+
" stats = list(values.values())[0]\n",
409412
" dates.append(datetime.fromisoformat(date_str))\n",
410-
" means.append(values[\"analysed_sst\"][\"mean\"])\n",
411-
" stds.append(values[\"analysed_sst\"][\"std\"])\n",
413+
" means.append(stats[\"mean\"])\n",
414+
" stds.append(stats[\"std\"])\n",
412415
"\n",
413416
"plt.figure(figsize=(10, 6))\n",
414417
"\n",
@@ -435,6 +438,96 @@
435438
"plt.show()"
436439
]
437440
},
441+
{
442+
"cell_type": "markdown",
443+
"id": "2b09bafc-b7c4-425a-b772-50be4cdad178",
444+
"metadata": {},
445+
"source": [
446+
"## Example: {datetime} string interpolation with sel parameter\n",
447+
"\n",
448+
"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",
449+
"\n",
450+
"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}\"`. "
451+
]
452+
},
453+
{
454+
"cell_type": "code",
455+
"execution_count": null,
456+
"id": "5a5ed6f0-adb9-4f5f-866e-8447a1a07cb4",
457+
"metadata": {},
458+
"outputs": [],
459+
"source": [
460+
"%%time\n",
461+
"minx, miny, maxx, maxy = -98.676, 18.857, -81.623, 31.097\n",
462+
"geojson = Feature(\n",
463+
" type=\"Feature\",\n",
464+
" geometry=Polygon.from_bounds(minx, miny, maxx, maxy),\n",
465+
" properties={},\n",
466+
")\n",
467+
"request = httpx.post(\n",
468+
" f\"{titiler_endpoint}/timeseries/statistics\",\n",
469+
" params={\n",
470+
" \"concept_id\": \"C2837626477-GES_DISC\",\n",
471+
" \"datetime\": \"2010-01-01T00:00:01Z/2021-03-31T23:59:59Z\",\n",
472+
" \"step\": \"P1M\",\n",
473+
" \"temporal_mode\": \"point\",\n",
474+
" \"variable\": \"o3\",\n",
475+
" \"backend\": \"xarray\",\n",
476+
" \"sel\": [\"time={datetime}\", \"lev=1000\"],\n",
477+
" \"sel_method\": \"nearest\",\n",
478+
" },\n",
479+
" json=geojson.model_dump(exclude_none=True),\n",
480+
" timeout=None,\n",
481+
")\n",
482+
"\n",
483+
"request.raise_for_status()\n",
484+
"response = request.json()"
485+
]
486+
},
487+
{
488+
"cell_type": "code",
489+
"execution_count": null,
490+
"id": "68925555-81a1-416b-bbc5-6bd81f597f09",
491+
"metadata": {},
492+
"outputs": [],
493+
"source": [
494+
"data = response[\"properties\"][\"statistics\"]\n",
495+
"\n",
496+
"dates = []\n",
497+
"means = []\n",
498+
"stds = []\n",
499+
"\n",
500+
"for date_str, values in data.items():\n",
501+
" stats = list(values.values())[0]\n",
502+
" dates.append(datetime.fromisoformat(date_str))\n",
503+
" means.append(stats[\"mean\"])\n",
504+
" stds.append(stats[\"std\"])\n",
505+
"\n",
506+
"plt.figure(figsize=(10, 6))\n",
507+
"\n",
508+
"plt.plot(dates, means, \"b-\", label=\"Mean\")\n",
509+
"\n",
510+
"plt.fill_between(\n",
511+
" dates,\n",
512+
" np.array(means) - np.array(stds),\n",
513+
" np.array(means) + np.array(stds),\n",
514+
" alpha=0.2,\n",
515+
" color=\"b\",\n",
516+
" label=\"Standard Deviation\",\n",
517+
")\n",
518+
"\n",
519+
"plt.xlabel(\"Date\")\n",
520+
"plt.ylabel(\"O3\")\n",
521+
"plt.title(\"Mean monthly O3 concentration in the Gulf of Mexico\")\n",
522+
"plt.legend()\n",
523+
"\n",
524+
"plt.xticks(rotation=45)\n",
525+
"\n",
526+
"plt.tight_layout()\n",
527+
"\n",
528+
"plt.show()"
529+
]
530+
},
438531
{
439532
"cell_type": "markdown",
440533
"id": "8b6faa38-3563-439c-af53-3f107bb1f09c",

docs/examples/xarray_backend_example.ipynb

Lines changed: 102 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,12 @@
3434
"from folium import Map, TileLayer\n",
3535
"\n",
3636
"# titiler_endpoint = \"http://localhost:8081\" # docker network endpoint\n",
37+
"# titiler_endpoint = (\n",
38+
"# \"https://staging.openveda.cloud/api/titiler-cmr\" # VEDA staging endpoint\n",
39+
"# )\n",
3740
"titiler_endpoint = (\n",
38-
" \"https://staging.openveda.cloud/api/titiler-cmr\" # VEDA staging endpoint\n",
39-
")\n",
40-
"# titiler_endpoint = \"https://v4jec6i5c0.execute-api.us-west-2.amazonaws.com\" # dev endpoint"
41+
" \"https://v4jec6i5c0.execute-api.us-west-2.amazonaws.com\" # dev endpoint\n",
42+
")"
4143
]
4244
},
4345
{
@@ -262,10 +264,106 @@
262264
"print(json.dumps(r, indent=2))"
263265
]
264266
},
267+
{
268+
"cell_type": "markdown",
269+
"id": "3eb898d1-8d02-4943-a17f-c11a21eb0124",
270+
"metadata": {},
271+
"source": [
272+
"## Datetime string interpolation with the sel parameter\n",
273+
"\n",
274+
"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",
275+
"\n",
276+
"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}\"`. "
277+
]
278+
},
279+
{
280+
"cell_type": "code",
281+
"execution_count": null,
282+
"id": "462fa885-b3bd-4406-a360-c69dab0e44e6",
283+
"metadata": {},
284+
"outputs": [],
285+
"source": [
286+
"geojson_dict = {\n",
287+
" \"type\": \"FeatureCollection\",\n",
288+
" \"features\": [\n",
289+
" {\n",
290+
" \"type\": \"Feature\",\n",
291+
" \"properties\": {},\n",
292+
" \"geometry\": {\n",
293+
" \"coordinates\": [\n",
294+
" [\n",
295+
" [-20.79973248834736, 83.55979308678764],\n",
296+
" [-20.79973248834736, 75.0115425216471],\n",
297+
" [14.483337068956956, 75.0115425216471],\n",
298+
" [14.483337068956956, 83.55979308678764],\n",
299+
" [-20.79973248834736, 83.55979308678764],\n",
300+
" ]\n",
301+
" ],\n",
302+
" \"type\": \"Polygon\",\n",
303+
" },\n",
304+
" }\n",
305+
" ],\n",
306+
"}\n",
307+
"\n",
308+
"r = httpx.post(\n",
309+
" f\"{titiler_endpoint}/statistics\",\n",
310+
" params=(\n",
311+
" (\"concept_id\", \"C2837626477-GES_DISC\"),\n",
312+
" # Datetime for CMR granule query\n",
313+
" (\"datetime\", datetime(2021, 10, 10, tzinfo=timezone.utc).isoformat()),\n",
314+
" # xarray backend query parameters\n",
315+
" (\"backend\", \"xarray\"),\n",
316+
" (\"variable\", \"o3\"),\n",
317+
" (\"sel\", \"time={datetime}\"), #\n",
318+
" (\"sel\", \"lev=1000\"),\n",
319+
" (\"sel_method\", \"nearest\"),\n",
320+
" ),\n",
321+
" json=geojson_dict,\n",
322+
" timeout=60,\n",
323+
").json()\n",
324+
"\n",
325+
"print(json.dumps(r, indent=2))"
326+
]
327+
},
328+
{
329+
"cell_type": "markdown",
330+
"id": "50178b80-0a84-47bc-ac74-50d0a1f6f489",
331+
"metadata": {},
332+
"source": [
333+
"You can chose a different time slice from the same granule simply by updating the `datetime` query parameter."
334+
]
335+
},
336+
{
337+
"cell_type": "code",
338+
"execution_count": null,
339+
"id": "136ef791-a5b6-42d7-bce3-d07e108d940f",
340+
"metadata": {},
341+
"outputs": [],
342+
"source": [
343+
"r = httpx.post(\n",
344+
" f\"{titiler_endpoint}/statistics\",\n",
345+
" params=(\n",
346+
" (\"concept_id\", \"C2837626477-GES_DISC\"),\n",
347+
" # Datetime for CMR granule query\n",
348+
" (\"datetime\", datetime(2021, 12, 10, tzinfo=timezone.utc).isoformat()),\n",
349+
" # xarray backend query parameters\n",
350+
" (\"backend\", \"xarray\"),\n",
351+
" (\"variable\", \"o3\"),\n",
352+
" (\"sel\", \"time={datetime}\"), #\n",
353+
" (\"sel\", \"lev=1000\"),\n",
354+
" (\"sel_method\", \"nearest\"),\n",
355+
" ),\n",
356+
" json=geojson_dict,\n",
357+
" timeout=60,\n",
358+
").json()\n",
359+
"\n",
360+
"print(json.dumps(r, indent=2))"
361+
]
362+
},
265363
{
266364
"cell_type": "code",
267365
"execution_count": null,
268-
"id": "7a38b762-ff68-40d3-84eb-0e37957381e0",
366+
"id": "3d2b199f-8448-4177-b079-94b45b988d7c",
269367
"metadata": {},
270368
"outputs": [],
271369
"source": []

tests/test_dependencies.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from titiler.cmr import dependencies
99
from titiler.cmr.enums import MediaType
1010
from titiler.cmr.errors import InvalidDatetime
11+
from titiler.xarray.dependencies import CompatXarrayParams
1112

1213

1314
def test_media_type():
@@ -167,3 +168,89 @@ def test_cmr_query_more():
167168
concept_id="something",
168169
datetime="2019-02-12T09:00:00Z/2019-02-12",
169170
)
171+
172+
173+
def test_interpolated_xarray_params_single_datetime():
174+
"""Test InterpolatedXarrayParams with single datetime interpolation."""
175+
xarray_params = CompatXarrayParams(
176+
variable="temperature", sel=["time={datetime}", "lev=1000"], method="nearest"
177+
)
178+
179+
single_datetime = datetime(2025, 9, 23, 0, 0, 0, tzinfo=timezone.utc)
180+
cmr_query_params = {"concept_id": "test_concept", "temporal": single_datetime}
181+
182+
result = dependencies.interpolated_xarray_ds_params(xarray_params, cmr_query_params)
183+
184+
assert result.sel == [f"time={single_datetime.isoformat()}", "lev=1000"]
185+
assert result.variable == "temperature"
186+
assert result.method == "nearest"
187+
188+
189+
def test_interpolated_xarray_params_datetime_range():
190+
"""Test InterpolatedXarrayParams with datetime range (uses start datetime)."""
191+
xarray_params = CompatXarrayParams(
192+
variable="temperature", sel=["time={datetime}"], method="nearest"
193+
)
194+
195+
start_datetime = datetime(2025, 9, 23, 0, 0, 0, tzinfo=timezone.utc)
196+
end_datetime = datetime(2025, 9, 24, 0, 0, 0, tzinfo=timezone.utc)
197+
cmr_query_params = {
198+
"concept_id": "test_concept",
199+
"temporal": (start_datetime, end_datetime),
200+
}
201+
202+
result = dependencies.interpolated_xarray_ds_params(xarray_params, cmr_query_params)
203+
204+
assert result.sel == [f"time={start_datetime.isoformat()}"]
205+
206+
207+
def test_interpolated_xarray_params_no_datetime_template():
208+
"""Test InterpolatedXarrayParams when sel doesn't contain datetime template."""
209+
xarray_params = CompatXarrayParams(
210+
variable="temperature",
211+
sel=["time=2025-01-01T00:00:00Z", "lev=1000"],
212+
method="nearest",
213+
)
214+
215+
single_datetime = datetime(2025, 9, 23, 0, 0, 0, tzinfo=timezone.utc)
216+
cmr_query_params = {"concept_id": "test_concept", "temporal": single_datetime}
217+
218+
result = dependencies.interpolated_xarray_ds_params(xarray_params, cmr_query_params)
219+
220+
assert result.sel == ["time=2025-01-01T00:00:00Z", "lev=1000"]
221+
222+
223+
def test_interpolated_xarray_params_no_sel():
224+
"""Test InterpolatedXarrayParams when sel is None or empty."""
225+
xarray_params = CompatXarrayParams(
226+
variable="temperature", sel=None, method="nearest"
227+
)
228+
229+
single_datetime = datetime(2025, 9, 23, 0, 0, 0, tzinfo=timezone.utc)
230+
cmr_query_params = {"concept_id": "test_concept", "temporal": single_datetime}
231+
232+
result = dependencies.interpolated_xarray_ds_params(xarray_params, cmr_query_params)
233+
234+
assert result.sel is None
235+
assert result.variable == "temperature"
236+
237+
238+
def test_interpolated_xarray_params_multiple_templates():
239+
"""Test InterpolatedXarrayParams with multiple datetime templates."""
240+
xarray_params = CompatXarrayParams(
241+
variable="temperature",
242+
sel=["time={datetime}", "start_time={datetime}", "lev=1000"],
243+
method="nearest",
244+
)
245+
246+
single_datetime = datetime(2025, 9, 23, 12, 30, 45, tzinfo=timezone.utc)
247+
cmr_query_params = {"concept_id": "test_concept", "temporal": single_datetime}
248+
249+
result = dependencies.interpolated_xarray_ds_params(xarray_params, cmr_query_params)
250+
251+
expected = [
252+
f"time={single_datetime.isoformat()}",
253+
f"start_time={single_datetime.isoformat()}",
254+
"lev=1000",
255+
]
256+
assert result.sel == expected

0 commit comments

Comments
 (0)