Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6737fd1
If hdate in tree use hdate instead of date
awarde96 Mar 17, 2026
287222e
Fix ruff
awarde96 Mar 17, 2026
19be370
Fix bounding box with hdate
awarde96 Mar 17, 2026
deeffa7
Add hdate for all features
awarde96 Mar 17, 2026
33bd292
test: add simple e2e regression test for TimeSeries encoder
andreas-grafberger Apr 8, 2026
67b3039
test: add simple first e2e test for an hdate-based reanalysis data input
andreas-grafberger Apr 8, 2026
d25fb01
test: add a few more hdate-specific tests
andreas-grafberger Apr 8, 2026
940622e
test: assume hdate already contains merged time component
andreas-grafberger Apr 8, 2026
bfd4c3f
test: rename test file
andreas-grafberger Apr 9, 2026
4b45e1a
chore: run pre-commit hooks
andreas-grafberger Apr 9, 2026
0e6129e
test: add further regression test for "normal" forecasts
andreas-grafberger Apr 9, 2026
d771982
chore: run pre-commit hooks
andreas-grafberger Apr 9, 2026
89682fd
Merge remote-tracking branch 'origin/main' into feature/hdate_reanaly…
andreas-grafberger Apr 10, 2026
6a39039
feat: add experimental from_polytope_reforecast to TimeSeries encoder
andreas-grafberger Apr 10, 2026
eaaa078
feat: add from_polytope_reforecast to all encoders
andreas-grafberger Apr 13, 2026
8c5eae5
refactor: remove duplication in TimeSeries.from_polytope_reforecast
andreas-grafberger Apr 13, 2026
df5fbea
tidy: simplify tests
andreas-grafberger Apr 13, 2026
a724b3a
test: clean up added tests a bit
andreas-grafberger Apr 13, 2026
c440ff3
chore: update type hints and docstrings
andreas-grafberger Apr 14, 2026
6916879
refactor: remove duplication in added tests
andreas-grafberger Apr 14, 2026
4a955d6
fix: typo in original code
andreas-grafberger Apr 14, 2026
6d12df2
fix: install polytope-python in CI
andreas-grafberger Apr 14, 2026
e67bd5a
Bump version
awarde96 Apr 16, 2026
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
7 changes: 3 additions & 4 deletions covjsonkit/encoder/BoundingBox.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ def from_xarray(self, dataset):
# Return the generated CoverageJSON
return self.covjson

def from_polytope(self, result):

def from_polytope(self, result, date_key: str = "date") -> dict:
"""Encode a polytope ``TensorIndexTree`` result into a MultiPoint (BoundingBox) CoverageJSON collection."""
coords = {}
mars_metadata = {}
range_dict = {}
Expand All @@ -130,8 +130,7 @@ def from_polytope(self, result):
fields["dates"] = []
fields["levels"] = [0]

self.walk_tree(result, fields, coords, mars_metadata, range_dict)

self.walk_tree(result, fields, coords, mars_metadata, range_dict, date_key=date_key)
logging.debug("The values returned from walking tree: %s", range_dict) # noqa: E501
logging.debug("The coordinates returned from walking tree: %s", coords) # noqa: E501

Expand Down
8 changes: 4 additions & 4 deletions covjsonkit/encoder/Circle.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,10 @@ def from_xarray(self, dataset):
self.add_coverage(mars_metadata, coords, dv_dict)

# Return the generated CoverageJSON
return self.covjsonå

def from_polytope(self, result):
return self.covjson

def from_polytope(self, result, date_key: str = "date") -> dict:
"""Encode a polytope ``TensorIndexTree`` result into a MultiPoint (Circle) CoverageJSON collection."""
coords = {}
mars_metadata = {}
range_dict = {}
Expand All @@ -127,7 +127,7 @@ def from_polytope(self, result):
fields["dates"] = []
fields["levels"] = [0]

self.walk_tree(result, fields, coords, mars_metadata, range_dict)
self.walk_tree(result, fields, coords, mars_metadata, range_dict, date_key=date_key)

logging.debug("The values returned from walking tree: %s", range_dict) # noqa: E501
logging.debug("The coordinates returned from walking tree: %s", coords) # noqa: E501
Expand Down
6 changes: 3 additions & 3 deletions covjsonkit/encoder/Frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ def from_xarray(self, dataset):
# Return the generated CoverageJSON
return self.covjson

def from_polytope(self, result):

def from_polytope(self, result, date_key: str = "date") -> dict:
"""Encode a polytope ``TensorIndexTree`` result into a MultiPoint (Frame) CoverageJSON collection."""
coords = {}
mars_metadata = {}
range_dict = {}
Expand All @@ -127,7 +127,7 @@ def from_polytope(self, result):
fields["dates"] = []
fields["levels"] = [0]

self.walk_tree(result, fields, coords, mars_metadata, range_dict)
self.walk_tree(result, fields, coords, mars_metadata, range_dict, date_key=date_key)

logging.debug("The values returned from walking tree: %s", range_dict) # noqa: E501
logging.debug("The coordinates returned from walking tree: %s", coords) # noqa: E501
Expand Down
7 changes: 3 additions & 4 deletions covjsonkit/encoder/Grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,8 @@ def from_xarray(self, dataset):
# Return the generated CoverageJSON
return self.covjson

def from_polytope(self, result):

def from_polytope(self, result, date_key: str = "date") -> dict:
"""Encode a polytope ``TensorIndexTree`` result into a Grid CoverageJSON collection."""
coords = {}
mars_metadata = {}
range_dict = {}
Expand All @@ -133,8 +133,7 @@ def from_polytope(self, result):
fields["dates"] = []
fields["levels"] = [0]

self.walk_tree(result, fields, coords, mars_metadata, range_dict)

self.walk_tree(result, fields, coords, mars_metadata, range_dict, date_key=date_key)
logging.debug("The values returned from walking tree: %s", range_dict) # noqa: E501
logging.debug("The coordinates returned from walking tree: %s", coords) # noqa: E501

Expand Down
6 changes: 3 additions & 3 deletions covjsonkit/encoder/Path.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ def from_xarray(self, dataset):
# Return the generated CoverageJSON
return self.covjson

def from_polytope(self, result):

def from_polytope(self, result, date_key: str = "date") -> dict:
"""Encode a polytope ``TensorIndexTree`` result into a Trajectory (Path) CoverageJSON collection."""
coords = {}
mars_metadata = {}
range_dict = {}
Expand All @@ -129,7 +129,7 @@ def from_polytope(self, result):
fields["s"] = []
fields["l"] = []

self.walk_tree(result, fields, coords, mars_metadata, range_dict)
self.walk_tree(result, fields, coords, mars_metadata, range_dict, date_key=date_key)

if len(fields["l"]) == 0:
fields["l"] = [0]
Expand Down
16 changes: 9 additions & 7 deletions covjsonkit/encoder/Position.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def from_xarray(self, datasets):
datasets (Union[xarray.Dataset, List[xarray.Dataset]]): An xarray dataset or a list of xarray datasets.

Returns:
dict: The CoverageJSON representation of the coverageCollection.
dict: The CoverageJSON representation of the coverage collection.
"""
if not isinstance(datasets, list):
datasets = [datasets]
Expand Down Expand Up @@ -123,13 +123,15 @@ def from_xarray(self, datasets):

return self.covjson

def from_polytope(self, result):
"""
Converts a Polytope result into an OGC CoverageJSON coverageCollection of type PointSeries
def from_polytope(self, result, date_key: str = "date") -> dict:
"""Encode a polytope ``TensorIndexTree`` result into a PointSeries (Position) CoverageJSON collection.

Args:
result (dict): The Polytope result containing the data to be converted.
result: The polytope ``TensorIndexTree`` containing the data to be converted.
date_key: Tree axis name to treat as the time dimension
(``"date"`` for forecasts, ``"hdate"`` for hindcast/reforecast).
Returns:
dict: The CoverageJSON representation of the coverageCollection.
dict: The CoverageJSON representation of the coverage collection.
"""
coords = {}
mars_metadata = {}
Expand All @@ -144,7 +146,7 @@ def from_polytope(self, result):

start = time.time()
logging.debug("Tree walking starts at: %s", start) # noqa: E501
self.walk_tree(result, fields, coords, mars_metadata, range_dict)
self.walk_tree(result, fields, coords, mars_metadata, range_dict, date_key=date_key)
end = time.time()
delta = end - start
logging.debug("Tree walking ends at: %s", end) # noqa: E501
Expand Down
6 changes: 3 additions & 3 deletions covjsonkit/encoder/Shapefile.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ def from_xarray(self, dataset):
# Return the generated CoverageJSON
return self.covjson

def from_polytope(self, result):

def from_polytope(self, result, date_key: str = "date") -> dict:
"""Encode a polytope ``TensorIndexTree`` result into a MultiPoint (Shapefile) CoverageJSON collection."""
coords = {}
mars_metadata = {}
range_dict = {}
Expand All @@ -127,7 +127,7 @@ def from_polytope(self, result):
fields["dates"] = []
fields["levels"] = [0]

self.walk_tree(result, fields, coords, mars_metadata, range_dict)
self.walk_tree(result, fields, coords, mars_metadata, range_dict, date_key=date_key)

logging.debug("The values returned from walking tree: %s", range_dict) # noqa: E501
logging.debug("The coordinates returned from walking tree: %s", coords) # noqa: E501
Expand Down
16 changes: 9 additions & 7 deletions covjsonkit/encoder/TimeSeries.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def from_xarray(self, datasets):
datasets (Union[xarray.Dataset, List[xarray.Dataset]]): An xarray dataset or a list of xarray datasets.

Returns:
dict: The CoverageJSON representation of the coverageCollection.
dict: The CoverageJSON representation of the coverage collection.
"""
if not isinstance(datasets, list):
datasets = [datasets]
Expand Down Expand Up @@ -123,13 +123,15 @@ def from_xarray(self, datasets):

return self.covjson

def from_polytope(self, result):
"""
Converts a Polytope result into an OGC CoverageJSON coverageCollection of type PointSeries
def from_polytope(self, result, date_key: str = "date") -> dict:
"""Encode a polytope ``TensorIndexTree`` result into a PointSeries CoverageJSON collection.

Args:
result (dict): The Polytope result containing the data to be converted.
result: The polytope ``TensorIndexTree`` containing the data to be converted.
date_key: Tree axis name to treat as the time dimension
(``"date"`` for forecasts, ``"hdate"`` for hindcast/reforecast).
Returns:
dict: The CoverageJSON representation of the coverageCollection.
dict: The CoverageJSON representation of the coverage collection.
"""
coords = {}
mars_metadata = {}
Expand All @@ -144,7 +146,7 @@ def from_polytope(self, result):

start = time.time()
logging.debug("Tree walking starts at: %s", start) # noqa: E501
self.walk_tree(result, fields, coords, mars_metadata, range_dict)
self.walk_tree(result, fields, coords, mars_metadata, range_dict, date_key=date_key)
end = time.time()
delta = end - start
logging.debug("Tree walking ends at: %s", end) # noqa: E501
Expand Down
5 changes: 3 additions & 2 deletions covjsonkit/encoder/VerticalProfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ def from_xarray(self, datasets):

return self.covjson

def from_polytope(self, result):
def from_polytope(self, result, date_key: str = "date") -> dict:
"""Encode a polytope ``TensorIndexTree`` result into a VerticalProfile CoverageJSON collection."""
coords = {}
mars_metadata = {}
range_dict = {}
Expand All @@ -135,7 +136,7 @@ def from_polytope(self, result):

start = time.time()
logging.debug("Tree walking starts at: %s", start) # noqa: E501
self.walk_tree(result, fields, coords, mars_metadata, range_dict)
self.walk_tree(result, fields, coords, mars_metadata, range_dict, date_key=date_key)
end = time.time()
delta = end - start
logging.debug("Tree walking ends at: %s", end) # noqa: E501
Expand Down
6 changes: 3 additions & 3 deletions covjsonkit/encoder/Wkt.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@ def from_xarray(self, dataset):
# Return the generated CoverageJSON
return self.covjson

def from_polytope(self, result):

def from_polytope(self, result, date_key: str = "date") -> dict:
"""Encode a polytope ``TensorIndexTree`` result into a MultiPoint (Wkt/Polygon) CoverageJSON collection."""
coords = {}
mars_metadata = {}
range_dict = {}
Expand All @@ -131,7 +131,7 @@ def from_polytope(self, result):
fields["dates"] = []
fields["levels"] = [0]

self.walk_tree(result, fields, coords, mars_metadata, range_dict)
self.walk_tree(result, fields, coords, mars_metadata, range_dict, date_key=date_key)

logging.debug("The values returned from walking tree: %s", range_dict) # noqa: E501
logging.debug("The coordinates returned from walking tree: %s", coords) # noqa: E501
Expand Down
41 changes: 35 additions & 6 deletions covjsonkit/encoder/encoder.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Any

import orjson
import pandas as pd
Expand Down Expand Up @@ -149,12 +152,29 @@ def get_json(self):
# self.covjson = self.pydantic_coverage.model_dump_json(exclude_none=True, indent=4)
return orjson.dumps(self.covjson)

def walk_tree(self, tree, fields, coords, mars_metadata, range_dict):
def walk_tree(
self,
tree,
fields: dict[str, Any],
coords: dict[str, dict[str, list]],
mars_metadata: dict[str, Any],
range_dict: dict[tuple, list],
date_key: str = "date",
) -> None:
"""Walk the polytope result tree, extracting data into fields, coords, and range_dict.

``date_key`` controls which tree axis is treated as the time dimension
(e.g. ``"date"`` for forecasts, ``"hdate"`` for hindcast/reforecast data).
Any other axis with the default name falls through to ``mars_metadata``
instead. Regardless of ``date_key``, values are always stored under
``fields["dates"]``.
"""

def create_composite_key(date, level, num, para, s):
return (date, level, num, para, s)

def handle_non_leaf_node(child):
non_leaf_axes = ["latitude", "longitude", "param", "date"]
non_leaf_axes = ["latitude", "longitude", "param", date_key]
if child.axis.name not in non_leaf_axes:
mars_metadata[child.axis.name] = child.values[0]

Expand All @@ -165,7 +185,7 @@ def handle_specific_axes(child):
return child.values
if child.axis.name == "param":
return child.values
if child.axis.name in ["date", "time"]:
if child.axis.name in [date_key, "time"]:
dates = [f"{date}Z" for date in child.values]
mars_metadata["Forecast date"] = str(child.values[0])
for date in dates:
Expand Down Expand Up @@ -202,7 +222,7 @@ def append_composite_coords(dates, tree_values, lat, coords):
fields["l"].extend(result)
elif child.axis.name == "param":
fields["param"] = result
elif child.axis.name in ["date", "time"]:
elif child.axis.name in [date_key, "time"]:
fields["dates"].extend(result)
elif child.axis.name == "number":
fields["number"] = result
Expand All @@ -211,7 +231,7 @@ def append_composite_coords(dates, tree_values, lat, coords):
if "s" in fields:
fields["s"].extend(result)

self.walk_tree(child, fields, coords, mars_metadata, range_dict)
self.walk_tree(child, fields, coords, mars_metadata, range_dict, date_key=date_key)
else:
tree.values = [float(val) for val in tree.values]
if all(val is None for val in tree.result):
Expand Down Expand Up @@ -529,5 +549,14 @@ def from_xarray(self, dataset):
pass

@abstractmethod
def from_polytope(self, result):
def from_polytope(self, result, date_key: str = "date") -> dict:
pass

def from_polytope_reforecast(self, result) -> dict:
"""Encode reforecast/reanalysis data that uses ``"hdate"`` as the time axis.

Delegates to :meth:`from_polytope` with ``date_key="hdate"``.
Each hdate produces a separate coverage; steps within a single
hdate become that coverage's t-axis values.
"""
return self.from_polytope(result, date_key="hdate")
2 changes: 1 addition & 1 deletion covjsonkit/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.2.15"
__version__ = "0.2.16"
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ markers = ["data: uses test data (deselect with '-m \"not data\"')",]

[project]
name = "covjsonkit"
version = "0.2.15"
version = "0.2.16"
dependencies = [
"pandas<3",
"orjson",
Expand All @@ -23,3 +23,7 @@ geo = [
"rasterio",
"shapely",
]
tests = [
"pytest",
"polytope-python",
]
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ covjson-pydantic
conflator
scipy
pre-commit
polytope-python
Loading
Loading