From 71e91e7bdb0edc4d2720384753219d10787601a0 Mon Sep 17 00:00:00 2001 From: Lasse Meinen Date: Wed, 6 May 2026 13:22:19 +0200 Subject: [PATCH 01/22] added test demonstrating param 't' issue --- tests/test_decoder_time_series.py | 46 +++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/tests/test_decoder_time_series.py b/tests/test_decoder_time_series.py index f5289d4..6fe13ef 100644 --- a/tests/test_decoder_time_series.py +++ b/tests/test_decoder_time_series.py @@ -287,8 +287,50 @@ def test_timeseries_coordinates(self): assert decoder.get_coordinates() == coordinates def test_timeseries_to_xarray(self): - # decoder = Covjsonkit().decode(self.covjson) - # ds = decoder.to_xarray() + covjson = { + "type": "CoverageCollection", + "domainType": "PointSeries", + "coverages": [ + { + "type": "Coverage", + "domain": { + "type": "Domain", + "axes": { + "latitude": {"values": [47.5]}, + "longitude": {"values": [8.5]}, + "levelist": {"values": [74]}, + "t": {"values": ["2026-05-04T18:00:00Z"]}, + }, + }, + "ranges": { + "t": { + "type": "NdArray", + "dataType": "float", + "shape": [1], + "axisNames": ["t"], + "values": [285.6], + } + }, + } + ], + "referencing": [ + { + "coordinates": ["latitude", "longitude", "levelist"], + "system": {"type": "GeographicCRS"}, + } + ], + "parameters": { + "t": { + "type": "Parameter", + "unit": {"symbol": "K"}, + "observedProperty": {"id": "t", "label": {"en": "Temperature"}}, + } + }, + } + ds = Covjsonkit().decode(covjson).to_xarray() + data_vars = ["t"] + assert all(var in ds.data_vars for var in data_vars) + # print(ds) # print(ds["Temperature"]) # xrds.to_netcdf("timeseries.nc") From 806070d9538f6a2c88a24e012d0229c99146ee29 Mon Sep 17 00:00:00 2001 From: Lasse Meinen Date: Wed, 6 May 2026 13:40:43 +0200 Subject: [PATCH 02/22] add second test for mars metadata path --- tests/test_decoder_time_series.py | 52 ++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/tests/test_decoder_time_series.py b/tests/test_decoder_time_series.py index 6fe13ef..2c61c71 100644 --- a/tests/test_decoder_time_series.py +++ b/tests/test_decoder_time_series.py @@ -286,7 +286,56 @@ def test_timeseries_coordinates(self): print(decoder.get_coordinates()) assert decoder.get_coordinates() == coordinates - def test_timeseries_to_xarray(self): + def test_timeseries_to_xarray_param_t(self): + covjson = { + "type": "CoverageCollection", + "domainType": "PointSeries", + "coverages": [ + { + "mars:metadata": { + "Forecast date": "2026-05-04T18:00:00Z", + "number": 1, + }, + "type": "Coverage", + "domain": { + "type": "Domain", + "axes": { + "latitude": {"values": [47.5]}, + "longitude": {"values": [8.5]}, + "levelist": {"values": [74]}, + "t": {"values": ["2026-05-04T18:00:00Z"]}, + }, + }, + "ranges": { + "t": { + "type": "NdArray", + "dataType": "float", + "shape": [1], + "axisNames": ["t"], + "values": [285.6], + } + }, + } + ], + "referencing": [ + { + "coordinates": ["latitude", "longitude", "levelist"], + "system": {"type": "GeographicCRS"}, + } + ], + "parameters": { + "t": { + "type": "Parameter", + "unit": {"symbol": "K"}, + "observedProperty": {"id": "t", "label": {"en": "Temperature"}}, + } + }, + } + ds = Covjsonkit().decode(covjson).to_xarray() + data_vars = ["t"] + assert all(var in ds.data_vars for var in data_vars) + + def test_timeseries_to_xarray_param_t_no_forecast_date(self): covjson = { "type": "CoverageCollection", "domainType": "PointSeries", @@ -331,6 +380,7 @@ def test_timeseries_to_xarray(self): data_vars = ["t"] assert all(var in ds.data_vars for var in data_vars) + def test_timeseries_to_xarray(self): # print(ds) # print(ds["Temperature"]) # xrds.to_netcdf("timeseries.nc") From b65917290ae933a4a152b4e96bb36dafdf6e6a69 Mon Sep 17 00:00:00 2001 From: Lasse Meinen Date: Wed, 6 May 2026 13:47:51 +0200 Subject: [PATCH 03/22] added param_t test data as separate json --- tests/data/test_timeseries_param_t.json | 47 ++++++++++++ tests/test_decoder_time_series.py | 96 +++---------------------- 2 files changed, 58 insertions(+), 85 deletions(-) create mode 100644 tests/data/test_timeseries_param_t.json diff --git a/tests/data/test_timeseries_param_t.json b/tests/data/test_timeseries_param_t.json new file mode 100644 index 0000000..99b7f47 --- /dev/null +++ b/tests/data/test_timeseries_param_t.json @@ -0,0 +1,47 @@ +{ + "type": "CoverageCollection", + "domainType": "PointSeries", + "coverages": [ + { + "mars:metadata": { + "Forecast date": "2026-05-04T18:00:00Z", + "number": 1 + }, + "type": "Coverage", + "domain": { + "type": "Domain", + "axes": { + "latitude": {"values": [47.5]}, + "longitude": {"values": [8.5]}, + "levelist": {"values": [74]}, + "t": {"values": ["2026-05-04T18:00:00Z"]} + } + }, + "ranges": { + "t": { + "type": "NdArray", + "dataType": "float", + "shape": [1], + "axisNames": ["t"], + "values": [285.6] + } + } + } + ], + "referencing": [ + { + "coordinates": ["latitude", "longitude", "levelist"], + "system": {"type": "GeographicCRS"} + } + ], + "parameters": { + "t": { + "type": "Parameter", + "unit": {"symbol": "K"}, + "observedProperty": { + "id": "t", + "label": {"en": "Temperature"} + } + } + } +} diff --git a/tests/test_decoder_time_series.py b/tests/test_decoder_time_series.py index 2c61c71..8bc7975 100644 --- a/tests/test_decoder_time_series.py +++ b/tests/test_decoder_time_series.py @@ -1,6 +1,9 @@ # from earthkit import data +import json + from covjsonkit.api import Covjsonkit +from pathlib import Path class TestDecoder: @@ -287,95 +290,18 @@ def test_timeseries_coordinates(self): assert decoder.get_coordinates() == coordinates def test_timeseries_to_xarray_param_t(self): - covjson = { - "type": "CoverageCollection", - "domainType": "PointSeries", - "coverages": [ - { - "mars:metadata": { - "Forecast date": "2026-05-04T18:00:00Z", - "number": 1, - }, - "type": "Coverage", - "domain": { - "type": "Domain", - "axes": { - "latitude": {"values": [47.5]}, - "longitude": {"values": [8.5]}, - "levelist": {"values": [74]}, - "t": {"values": ["2026-05-04T18:00:00Z"]}, - }, - }, - "ranges": { - "t": { - "type": "NdArray", - "dataType": "float", - "shape": [1], - "axisNames": ["t"], - "values": [285.6], - } - }, - } - ], - "referencing": [ - { - "coordinates": ["latitude", "longitude", "levelist"], - "system": {"type": "GeographicCRS"}, - } - ], - "parameters": { - "t": { - "type": "Parameter", - "unit": {"symbol": "K"}, - "observedProperty": {"id": "t", "label": {"en": "Temperature"}}, - } - }, - } + path = Path(__file__).parent / "data/test_timeseries_param_t.json" + with open(path, "r") as f: + covjson = json.load(f) ds = Covjsonkit().decode(covjson).to_xarray() data_vars = ["t"] assert all(var in ds.data_vars for var in data_vars) - def test_timeseries_to_xarray_param_t_no_forecast_date(self): - covjson = { - "type": "CoverageCollection", - "domainType": "PointSeries", - "coverages": [ - { - "type": "Coverage", - "domain": { - "type": "Domain", - "axes": { - "latitude": {"values": [47.5]}, - "longitude": {"values": [8.5]}, - "levelist": {"values": [74]}, - "t": {"values": ["2026-05-04T18:00:00Z"]}, - }, - }, - "ranges": { - "t": { - "type": "NdArray", - "dataType": "float", - "shape": [1], - "axisNames": ["t"], - "values": [285.6], - } - }, - } - ], - "referencing": [ - { - "coordinates": ["latitude", "longitude", "levelist"], - "system": {"type": "GeographicCRS"}, - } - ], - "parameters": { - "t": { - "type": "Parameter", - "unit": {"symbol": "K"}, - "observedProperty": {"id": "t", "label": {"en": "Temperature"}}, - } - }, - } + def test_timeseries_to_xarray_no_forecast_date_param_t(self): + path = Path(__file__).parent / "data/test_timeseries_param_t.json" + with open(path, "r") as f: + covjson = json.load(f) + covjson["coverages"][0].pop("mars:metadata") ds = Covjsonkit().decode(covjson).to_xarray() data_vars = ["t"] assert all(var in ds.data_vars for var in data_vars) From 466c5348843e3877612ad2817c13cd5b3e69d53f Mon Sep 17 00:00:00 2001 From: Lasse Meinen Date: Wed, 6 May 2026 14:22:35 +0200 Subject: [PATCH 04/22] extract loop-independent operations --- covjsonkit/decoder/TimeSeries.py | 79 +++++++++++++++++++------------- 1 file changed, 48 insertions(+), 31 deletions(-) diff --git a/covjsonkit/decoder/TimeSeries.py b/covjsonkit/decoder/TimeSeries.py index a4ecfc4..70fb1c2 100644 --- a/covjsonkit/decoder/TimeSeries.py +++ b/covjsonkit/decoder/TimeSeries.py @@ -110,7 +110,10 @@ def to_xarray(self): # time steps into a single coverage's t-axis and do NOT write "Forecast date" # into mars:metadata. Detect this case and use a simpler (time, point) layout # that mirrors the Wkt/Frame/Shapefile decoders. - has_forecast_date = any("Forecast date" in cov.get("mars:metadata", {}) for cov in self.covjson["coverages"]) + has_forecast_date = any( + "Forecast date" in cov.get("mars:metadata", {}) + for cov in self.covjson["coverages"] + ) if not has_forecast_date: return self._to_xarray_no_forecast_date() @@ -139,11 +142,23 @@ def to_xarray(self): unique_domains.append(domain) # Add to unique domains all_coords = unique_domains - param_values = {} + + num = [] + datetime = [] + for coverage in self.covjson["coverages"]: + num.append(coverage["mars:metadata"]["number"]) + datetime.append(coverage["mars:metadata"]["Forecast date"]) + nums = list(set(num)) + datetime = list(set(datetime)) # Initialize parameter values for all parameters - for parameter in self.parameters: - param_values[parameter] = [] + param_values = { + parameter: [ + [[[] for _ in range(len(datetime))] for _ in range(len(nums))] + for _ in range(len(all_coords)) + ] + for parameter in self.parameters + } # Process each coordinate domain for domain_idx, coords in enumerate(all_coords): @@ -155,38 +170,32 @@ def to_xarray(self): steps = [step.replace("Z", "") for step in steps] steps = pd.to_datetime(steps) - num = [] - datetime = [] - for coverage in self.covjson["coverages"]: - num.append(coverage["mars:metadata"]["number"]) - datetime.append(coverage["mars:metadata"]["Forecast date"]) - - nums = list(set(num)) - datetime = list(set(datetime)) - # Extract parameter values for the current domain for parameter in self.parameters: - if len(param_values[parameter]) <= domain_idx: - param_values[parameter].append([]) - for i, num in enumerate(nums): - if len(param_values[parameter][domain_idx]) <= i: - param_values[parameter][domain_idx].append([]) - for j, date in enumerate(datetime): - if len(param_values[parameter][domain_idx][i]) <= j: - param_values[parameter][domain_idx][i].append([]) - for k, step in enumerate(steps): for coverage in self.covjson["coverages"]: if ( coverage["mars:metadata"]["number"] == num - and coverage["mars:metadata"]["Forecast date"] == date - and coverage["domain"]["axes"][self.x_name]["values"] == x - and coverage["domain"]["axes"][self.y_name]["values"] == y - and coverage["domain"]["axes"][self.z_name]["values"] == z + and coverage["mars:metadata"]["Forecast date"] + == date + and coverage["domain"]["axes"][self.x_name][ + "values" + ] + == x + and coverage["domain"]["axes"][self.y_name][ + "values" + ] + == y + and coverage["domain"]["axes"][self.z_name][ + "values" + ] + == z ): - param_values[parameter][domain_idx][i][j] = coverage["ranges"][parameter]["values"] + param_values[parameter][domain_idx][i][j] = ( + coverage["ranges"][parameter]["values"] + ) for parameter in self.parameters: param_coords = { @@ -205,8 +214,12 @@ def to_xarray(self): ) dataarray.attrs["type"] = self.get_parameter_metadata(parameter)["type"] - dataarray.attrs["units"] = self.get_parameter_metadata(parameter)["unit"]["symbol"] - dataarray.attrs["long_name"] = self.get_parameter_metadata(parameter)["observedProperty"]["id"] + dataarray.attrs["units"] = self.get_parameter_metadata(parameter)[ + "unit" + ]["symbol"] + dataarray.attrs["long_name"] = self.get_parameter_metadata(parameter)[ + "observedProperty" + ]["id"] dataarraydict[dataarray.attrs["long_name"]] = dataarray ds.append(xr.Dataset(dataarraydict)) @@ -252,8 +265,12 @@ def _to_xarray_no_forecast_date(self): coords={"t": steps}, ) dataarray.attrs["type"] = self.get_parameter_metadata(parameter)["type"] - dataarray.attrs["units"] = self.get_parameter_metadata(parameter)["unit"]["symbol"] - dataarray.attrs["long_name"] = self.get_parameter_metadata(parameter)["observedProperty"]["id"] + dataarray.attrs["units"] = self.get_parameter_metadata(parameter)[ + "unit" + ]["symbol"] + dataarray.attrs["long_name"] = self.get_parameter_metadata(parameter)[ + "observedProperty" + ]["id"] dataarraydict[dataarray.attrs["long_name"]] = dataarray coord_dict = dict( From a3f4dca2e9b857a9ab2eb3aaceffc11787bfde42 Mon Sep 17 00:00:00 2001 From: Lasse Meinen Date: Wed, 6 May 2026 15:15:07 +0200 Subject: [PATCH 05/22] use tuple-based construction for Dataset - surfaces 't' collision explicitly --- covjsonkit/decoder/TimeSeries.py | 69 ++++++++++++++------------------ 1 file changed, 31 insertions(+), 38 deletions(-) diff --git a/covjsonkit/decoder/TimeSeries.py b/covjsonkit/decoder/TimeSeries.py index 70fb1c2..7225ab0 100644 --- a/covjsonkit/decoder/TimeSeries.py +++ b/covjsonkit/decoder/TimeSeries.py @@ -176,53 +176,36 @@ def to_xarray(self): for j, date in enumerate(datetime): for k, step in enumerate(steps): for coverage in self.covjson["coverages"]: - if ( - coverage["mars:metadata"]["number"] == num - and coverage["mars:metadata"]["Forecast date"] - == date - and coverage["domain"]["axes"][self.x_name][ - "values" - ] - == x - and coverage["domain"]["axes"][self.y_name][ - "values" - ] - == y - and coverage["domain"]["axes"][self.z_name][ - "values" - ] - == z - ): + if self._covers_domain(coverage, num, date, x, y, z): param_values[parameter][domain_idx][i][j] = ( coverage["ranges"][parameter]["values"] ) + coords = { + "latitude": x, + "longitude": y, + "levelist": z, + "number": nums, + "datetime": datetime, + "t": steps, + } + for parameter in self.parameters: - param_coords = { - "latitude": x, - "longitude": y, - "levelist": z, - "number": nums, - "datetime": datetime, - "t": steps, + long_name = self.get_parameter_metadata(parameter)["observedProperty"][ + "id" + ] + attrs = { + "type": self.get_parameter_metadata(parameter)["type"], + "units": self.get_parameter_metadata(parameter)["unit"]["symbol"], + "long_name": long_name, } - dataarray = xr.DataArray( + dataarraydict[long_name] = ( + dims, [[[param_values[parameter][domain_idx]]]], - dims=dims, - coords=param_coords, - name=f"{parameter}_domain_{domain_idx}", + attrs, ) - dataarray.attrs["type"] = self.get_parameter_metadata(parameter)["type"] - dataarray.attrs["units"] = self.get_parameter_metadata(parameter)[ - "unit" - ]["symbol"] - dataarray.attrs["long_name"] = self.get_parameter_metadata(parameter)[ - "observedProperty" - ]["id"] - dataarraydict[dataarray.attrs["long_name"]] = dataarray - - ds.append(xr.Dataset(dataarraydict)) + ds.append(xr.Dataset(data_vars=dataarraydict, coords=coords)) # Combine all DataArrays into a Dataset for mars_metadata in self.mars_metadata[0]: @@ -235,6 +218,16 @@ def to_xarray(self): return ds + def _covers_domain(self, coverage, num, date, x, y, z): + """check if coverage matches the given domain parameters (num, date, x, y, z)""" + return ( + coverage["mars:metadata"]["number"] == num + and coverage["mars:metadata"]["Forecast date"] == date + and coverage["domain"]["axes"][self.x_name]["values"] == x + and coverage["domain"]["axes"][self.y_name]["values"] == y + and coverage["domain"]["axes"][self.z_name]["values"] == z + ) + def _to_xarray_no_forecast_date(self): """Convert monthly-means CovJSON (no 'Forecast date' in metadata) to xarray. From a7ff95df30c0d1b98f3e50750ad73e4221f05c8c Mon Sep 17 00:00:00 2001 From: Lasse Meinen Date: Wed, 6 May 2026 15:42:57 +0200 Subject: [PATCH 06/22] reorder inner loops to avoid unnecessary iterations --- covjsonkit/decoder/TimeSeries.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/covjsonkit/decoder/TimeSeries.py b/covjsonkit/decoder/TimeSeries.py index 7225ab0..5149409 100644 --- a/covjsonkit/decoder/TimeSeries.py +++ b/covjsonkit/decoder/TimeSeries.py @@ -171,15 +171,14 @@ def to_xarray(self): steps = pd.to_datetime(steps) # Extract parameter values for the current domain - for parameter in self.parameters: - for i, num in enumerate(nums): - for j, date in enumerate(datetime): - for k, step in enumerate(steps): - for coverage in self.covjson["coverages"]: - if self._covers_domain(coverage, num, date, x, y, z): - param_values[parameter][domain_idx][i][j] = ( - coverage["ranges"][parameter]["values"] - ) + for i, num in enumerate(nums): + for j, date in enumerate(datetime): + for coverage in self.covjson["coverages"]: + if self._covers_domain(coverage, num, date, x, y, z): + for parameter in self.parameters: + param_values[parameter][domain_idx][i][j] = ( + coverage["ranges"][parameter]["values"] + ) coords = { "latitude": x, From 14cf7aba3f649f31f5c1f35bcc5640638785d2d5 Mon Sep 17 00:00:00 2001 From: Lasse Meinen Date: Wed, 6 May 2026 15:55:07 +0200 Subject: [PATCH 07/22] merge loops and drop unneeded nested list --- covjsonkit/decoder/TimeSeries.py | 41 ++++++++++++++++---------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/covjsonkit/decoder/TimeSeries.py b/covjsonkit/decoder/TimeSeries.py index 5149409..1123a8d 100644 --- a/covjsonkit/decoder/TimeSeries.py +++ b/covjsonkit/decoder/TimeSeries.py @@ -151,17 +151,8 @@ def to_xarray(self): nums = list(set(num)) datetime = list(set(datetime)) - # Initialize parameter values for all parameters - param_values = { - parameter: [ - [[[] for _ in range(len(datetime))] for _ in range(len(nums))] - for _ in range(len(all_coords)) - ] - for parameter in self.parameters - } - # Process each coordinate domain - for domain_idx, coords in enumerate(all_coords): + for coords in all_coords: dataarraydict = {} x = coords["axes"][self.x_name]["values"] y = coords["axes"][self.y_name]["values"] @@ -170,15 +161,7 @@ def to_xarray(self): steps = [step.replace("Z", "") for step in steps] steps = pd.to_datetime(steps) - # Extract parameter values for the current domain - for i, num in enumerate(nums): - for j, date in enumerate(datetime): - for coverage in self.covjson["coverages"]: - if self._covers_domain(coverage, num, date, x, y, z): - for parameter in self.parameters: - param_values[parameter][domain_idx][i][j] = ( - coverage["ranges"][parameter]["values"] - ) + cov_idx_list = self._find_coverages(nums, datetime, x, y, z) coords = { "latitude": x, @@ -190,6 +173,14 @@ def to_xarray(self): } for parameter in self.parameters: + param_values = [ + [[] for _ in range(len(datetime))] for _ in range(len(nums)) + ] + + # Extract parameter values for the current domain + for i, j, cov in cov_idx_list: + param_values[i][j] = cov["ranges"][parameter]["values"] + long_name = self.get_parameter_metadata(parameter)["observedProperty"][ "id" ] @@ -200,7 +191,7 @@ def to_xarray(self): } dataarraydict[long_name] = ( dims, - [[[param_values[parameter][domain_idx]]]], + [[[param_values]]], attrs, ) @@ -217,6 +208,16 @@ def to_xarray(self): return ds + def _find_coverages(self, nums, datetime, x, y, z): + """Find coverages that match the given domain parameters (num, date, x, y, z) and return them along with domain parameter indices.""" + result = [] + for i, num in enumerate(nums): + for j, date in enumerate(datetime): + for coverage in self.covjson["coverages"]: + if self._covers_domain(coverage, num, date, x, y, z): + result.append((i, j, coverage)) + return result + def _covers_domain(self, coverage, num, date, x, y, z): """check if coverage matches the given domain parameters (num, date, x, y, z)""" return ( From 6c605b7fa528248086a1048d271435e8b81a4406 Mon Sep 17 00:00:00 2001 From: Lasse Meinen Date: Wed, 6 May 2026 16:05:02 +0200 Subject: [PATCH 08/22] circumvent bug: capitalize param name if 't' --- covjsonkit/decoder/TimeSeries.py | 4 ++++ tests/test_decoder_time_series.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/covjsonkit/decoder/TimeSeries.py b/covjsonkit/decoder/TimeSeries.py index 1123a8d..a640960 100644 --- a/covjsonkit/decoder/TimeSeries.py +++ b/covjsonkit/decoder/TimeSeries.py @@ -184,6 +184,10 @@ def to_xarray(self): long_name = self.get_parameter_metadata(parameter)["observedProperty"][ "id" ] + + if long_name == "t": + long_name = "T" # Avoid collision with time dimension 't' + attrs = { "type": self.get_parameter_metadata(parameter)["type"], "units": self.get_parameter_metadata(parameter)["unit"]["symbol"], diff --git a/tests/test_decoder_time_series.py b/tests/test_decoder_time_series.py index 8bc7975..a96064e 100644 --- a/tests/test_decoder_time_series.py +++ b/tests/test_decoder_time_series.py @@ -294,7 +294,7 @@ def test_timeseries_to_xarray_param_t(self): with open(path, "r") as f: covjson = json.load(f) ds = Covjsonkit().decode(covjson).to_xarray() - data_vars = ["t"] + data_vars = ["T"] assert all(var in ds.data_vars for var in data_vars) def test_timeseries_to_xarray_no_forecast_date_param_t(self): @@ -303,7 +303,7 @@ def test_timeseries_to_xarray_no_forecast_date_param_t(self): covjson = json.load(f) covjson["coverages"][0].pop("mars:metadata") ds = Covjsonkit().decode(covjson).to_xarray() - data_vars = ["t"] + data_vars = ["T"] assert all(var in ds.data_vars for var in data_vars) def test_timeseries_to_xarray(self): From 45c53e3a8a7e63c1d59dc6be0652eb23708bb3e0 Mon Sep 17 00:00:00 2001 From: Lasse Meinen Date: Wed, 6 May 2026 16:05:39 +0200 Subject: [PATCH 09/22] reorder if statement with loop to avoid unnecessary iterations --- covjsonkit/decoder/TimeSeries.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/covjsonkit/decoder/TimeSeries.py b/covjsonkit/decoder/TimeSeries.py index a640960..9c7bf07 100644 --- a/covjsonkit/decoder/TimeSeries.py +++ b/covjsonkit/decoder/TimeSeries.py @@ -203,8 +203,8 @@ def to_xarray(self): # Combine all DataArrays into a Dataset for mars_metadata in self.mars_metadata[0]: - for dss in ds: - if mars_metadata != "date" and mars_metadata != "step": + if mars_metadata != "date" and mars_metadata != "step": + for dss in ds: dss.attrs[mars_metadata] = self.mars_metadata[0][mars_metadata] if len(ds) == 1: From 1052ea26c2bb1f1751176b152cf80fa557da454b Mon Sep 17 00:00:00 2001 From: Lasse Meinen Date: Wed, 6 May 2026 16:12:47 +0200 Subject: [PATCH 10/22] circumvent bug: capitalize param name if 't' in no_forecast_date path --- covjsonkit/decoder/TimeSeries.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/covjsonkit/decoder/TimeSeries.py b/covjsonkit/decoder/TimeSeries.py index 9c7bf07..f3aa661 100644 --- a/covjsonkit/decoder/TimeSeries.py +++ b/covjsonkit/decoder/TimeSeries.py @@ -243,7 +243,7 @@ def _to_xarray_no_forecast_date(self): """ ds_list = [] - for cov_idx, coverage in enumerate(self.covjson["coverages"]): + for coverage in self.covjson["coverages"]: domain = coverage["domain"]["axes"] x = domain[self.x_name]["values"] y = domain[self.y_name]["values"] @@ -256,19 +256,24 @@ def _to_xarray_no_forecast_date(self): dataarraydict = {} for parameter in self.parameters: values = coverage["ranges"][parameter]["values"] + long_name = self.get_parameter_metadata(parameter)["observedProperty"][ + "id" + ] + + if long_name == "t": + long_name = "T" # Avoid collision with time dimension 't' + + attrs = { + "type": self.get_parameter_metadata(parameter)["type"], + "units": self.get_parameter_metadata(parameter)["unit"]["symbol"], + "long_name": long_name, + } + dataarray = xr.DataArray( - values, - dims=["t"], - coords={"t": steps}, + values, dims=["t"], coords={"t": steps}, attrs=attrs ) - dataarray.attrs["type"] = self.get_parameter_metadata(parameter)["type"] - dataarray.attrs["units"] = self.get_parameter_metadata(parameter)[ - "unit" - ]["symbol"] - dataarray.attrs["long_name"] = self.get_parameter_metadata(parameter)[ - "observedProperty" - ]["id"] - dataarraydict[dataarray.attrs["long_name"]] = dataarray + + dataarraydict[long_name] = dataarray coord_dict = dict( latitude=(["latitude"], x), From 0b591d7832b44abae4676b64a00ead0e8a7d4e06 Mon Sep 17 00:00:00 2001 From: Lasse Meinen Date: Thu, 7 May 2026 10:43:23 +0200 Subject: [PATCH 11/22] add param 't' test and fix failing to_xarray decoding --- tests/test_decoder_bounding_box.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/test_decoder_bounding_box.py b/tests/test_decoder_bounding_box.py index d231626..ec23f43 100644 --- a/tests/test_decoder_bounding_box.py +++ b/tests/test_decoder_bounding_box.py @@ -68,7 +68,7 @@ def setup_method(self, method): "domain": { "type": "Domain", "axes": { - "t": {"values": ["2017-01-01T01:00:00"]}, + "t": {"values": ["2017-01-01T00:00:00"]}, "composite": { "dataType": "tuple", "coordinates": ["x", "y", "z"], @@ -190,7 +190,7 @@ def test_bounding_box_domains(self): domain2 = { "type": "Domain", "axes": { - "t": {"values": ["2017-01-01T01:00:00"]}, + "t": {"values": ["2017-01-01T00:00:00"]}, "composite": { "dataType": "tuple", "coordinates": ["x", "y", "z"], @@ -263,7 +263,13 @@ def test_bounding_box_get_values(self): } assert decoder.get_values() == values - # def test_bounding_box_to_xarray(self): - # decoder = BoundingBox.BoundingBox(self.covjson) - # dataset = decoder.to_xarray() - # print(dataset) + def test_bounding_box_to_xarray_param_t(self): + """to_xarray works with param 't' - no collision since dims use 'datetimes'.""" + decoder = BoundingBox.BoundingBox(self.covjson) + ds = decoder.to_xarray() + + # Param 't' should be in data_vars (no collision with 'datetimes' dim) + assert "t" in ds.data_vars, f"Expected 't' in data_vars, got {list(ds.data_vars)}" + assert "p" in ds.data_vars + # Time dimension is 'datetimes', not 't' + assert "datetimes" in ds.dims From 1dcb3f9036bf520f8a3e8cffd0059f5b83ec403a Mon Sep 17 00:00:00 2001 From: Lasse Meinen Date: Thu, 7 May 2026 10:45:27 +0200 Subject: [PATCH 12/22] add param 't' test for Position and clean up to_xarray --- covjsonkit/decoder/Position.py | 120 +++++++++++++++++---------------- tests/test_decoder_position.py | 25 +++++++ 2 files changed, 87 insertions(+), 58 deletions(-) create mode 100644 tests/test_decoder_position.py diff --git a/covjsonkit/decoder/Position.py b/covjsonkit/decoder/Position.py index 1fc5a5f..0957bc5 100644 --- a/covjsonkit/decoder/Position.py +++ b/covjsonkit/decoder/Position.py @@ -109,9 +109,6 @@ def to_xarray(self): dims = ["latitude", "longitude", "levelist", "number", "datetime", "t"] ds = [] - # Get coordinates for all domains - all_coords = self.get_domains() - unique_coords = set() # To track unique coordinate tuples unique_domains = [] # To store unique domains @@ -131,14 +128,17 @@ def to_xarray(self): unique_domains.append(domain) # Add to unique domains all_coords = unique_domains - param_values = {} - # Initialize parameter values for all parameters - for parameter in self.parameters: - param_values[parameter] = [] + num = [] + datetime = [] + for coverage in self.covjson["coverages"]: + num.append(coverage["mars:metadata"]["number"]) + datetime.append(coverage["mars:metadata"]["Forecast date"]) + nums = list(set(num)) + datetime = list(set(datetime)) # Process each coordinate domain - for domain_idx, coords in enumerate(all_coords): + for coords in all_coords: dataarraydict = {} x = coords["axes"][self.x_name]["values"] y = coords["axes"][self.y_name]["values"] @@ -147,69 +147,73 @@ def to_xarray(self): steps = [step.replace("Z", "") for step in steps] steps = pd.to_datetime(steps) - num = [] - datetime = [] - for coverage in self.covjson["coverages"]: - num.append(coverage["mars:metadata"]["number"]) - datetime.append(coverage["mars:metadata"]["Forecast date"]) + cov_idx_list = self._find_coverages(nums, datetime, x, y, z) - nums = list(set(num)) - datetime = list(set(datetime)) + coords = { + "latitude": x, + "longitude": y, + "levelist": z, + "number": nums, + "datetime": datetime, + "t": steps, + } - # Extract parameter values for the current domain for parameter in self.parameters: - if len(param_values[parameter]) <= domain_idx: - param_values[parameter].append([]) - - for i, num in enumerate(nums): - if len(param_values[parameter][domain_idx]) <= i: - param_values[parameter][domain_idx].append([]) - - for j, date in enumerate(datetime): - if len(param_values[parameter][domain_idx][i]) <= j: - param_values[parameter][domain_idx][i].append([]) - - for k, step in enumerate(steps): - for coverage in self.covjson["coverages"]: - if ( - coverage["mars:metadata"]["number"] == num - and coverage["mars:metadata"]["Forecast date"] == date - and coverage["domain"]["axes"][self.x_name]["values"] == x - and coverage["domain"]["axes"][self.y_name]["values"] == y - and coverage["domain"]["axes"][self.z_name]["values"] == z - ): - param_values[parameter][domain_idx][i][j] = coverage["ranges"][parameter]["values"] + param_values = [ + [[] for _ in range(len(datetime))] for _ in range(len(nums)) + ] - for parameter in self.parameters: - param_coords = { - "latitude": x, - "longitude": y, - "levelist": z, - "number": nums, - "datetime": datetime, - "t": steps, + # Extract parameter values for the current domain + for i, j, cov in cov_idx_list: + param_values[i][j] = cov["ranges"][parameter]["values"] + + long_name = self.get_parameter_metadata(parameter)["observedProperty"][ + "id" + ] + + if long_name == "t": + long_name = "T" # Avoid collision with time dimension 't' + + attrs = { + "type": self.get_parameter_metadata(parameter)["type"], + "units": self.get_parameter_metadata(parameter)["unit"]["symbol"], + "long_name": long_name, } - dataarray = xr.DataArray( - [[[param_values[parameter][domain_idx]]]], - dims=dims, - coords=param_coords, - name=f"{parameter}_domain_{domain_idx}", + dataarraydict[long_name] = ( + dims, + [[[param_values]]], + attrs, ) - dataarray.attrs["type"] = self.get_parameter_metadata(parameter)["type"] - dataarray.attrs["units"] = self.get_parameter_metadata(parameter)["unit"]["symbol"] - dataarray.attrs["long_name"] = self.get_parameter_metadata(parameter)["observedProperty"]["id"] - dataarraydict[dataarray.attrs["long_name"]] = dataarray - - ds.append(xr.Dataset(dataarraydict)) + ds.append(xr.Dataset(data_vars=dataarraydict, coords=coords)) # Combine all DataArrays into a Dataset for mars_metadata in self.mars_metadata[0]: - for dss in ds: - if mars_metadata != "date" and mars_metadata != "step": + if mars_metadata != "date" and mars_metadata != "step": + for dss in ds: dss.attrs[mars_metadata] = self.mars_metadata[0][mars_metadata] if len(ds) == 1: return ds[0] return ds + + def _find_coverages(self, nums, datetime, x, y, z): + """Find coverages matching domain parameters and return with indices.""" + result = [] + for i, num in enumerate(nums): + for j, date in enumerate(datetime): + for coverage in self.covjson["coverages"]: + if self._covers_domain(coverage, num, date, x, y, z): + result.append((i, j, coverage)) + return result + + def _covers_domain(self, coverage, num, date, x, y, z): + """Check if coverage matches the given domain parameters.""" + return ( + coverage["mars:metadata"]["number"] == num + and coverage["mars:metadata"]["Forecast date"] == date + and coverage["domain"]["axes"][self.x_name]["values"] == x + and coverage["domain"]["axes"][self.y_name]["values"] == y + and coverage["domain"]["axes"][self.z_name]["values"] == z + ) diff --git a/tests/test_decoder_position.py b/tests/test_decoder_position.py new file mode 100644 index 0000000..2f7045a --- /dev/null +++ b/tests/test_decoder_position.py @@ -0,0 +1,25 @@ +"""Tests for Position decoder's to_xarray method.""" + +import json +from pathlib import Path + +from covjsonkit.api import Covjsonkit + + +class TestPositionDecoder: + """Tests for Position decoder to_xarray with param 't' collision.""" + + def test_position_to_xarray_param_t(self): + """to_xarray must not fail when parameter is named 't' (collides with time dim).""" + path = Path(__file__).parent / "data/test_timeseries_param_t.json" + with open(path, "r") as f: + covjson = json.load(f) + + # Change domainType to position to use Position decoder + covjson["domainType"] = "position" + + ds = Covjsonkit().decode(covjson).to_xarray() + + # Parameter 't' should be renamed to 'T' to avoid collision with time dimension + assert "T" in ds.data_vars, f"Expected 'T' in data_vars, got {list(ds.data_vars)}" + assert "t" in ds.dims or "t" in ds.coords, "Time dimension/coord 't' should still exist" From 4ec7fc1f96cbdfa01ca6ce07ee7ff44bf0772b1e Mon Sep 17 00:00:00 2001 From: Lasse Meinen Date: Thu, 7 May 2026 10:49:00 +0200 Subject: [PATCH 13/22] add param 't' test for Circle --- tests/test_decoder_circle.py | 71 ++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 tests/test_decoder_circle.py diff --git a/tests/test_decoder_circle.py b/tests/test_decoder_circle.py new file mode 100644 index 0000000..b064793 --- /dev/null +++ b/tests/test_decoder_circle.py @@ -0,0 +1,71 @@ +"""Tests for Circle decoder's to_xarray method.""" + +from covjsonkit.decoder import Circle + + +class TestCircleDecoder: + def setup_method(self): + self.covjson = { + "type": "CoverageCollection", + "domainType": "circle", + "coverages": [ + { + "mars:metadata": { + "class": "od", + "stream": "oper", + "levtype": "pl", + "date": "20170101", + "step": "0", + "number": "0", + }, + "type": "Coverage", + "domain": { + "type": "Domain", + "axes": { + "t": {"values": ["2017-01-01T00:00:00"]}, + "composite": { + "dataType": "tuple", + "coordinates": ["x", "y", "z"], + "values": [[1, 20, 1], [2, 21, 3]], + }, + }, + }, + "ranges": { + "t": { + "type": "NdArray", + "dataType": "float", + "shape": [2], + "axisNames": ["t"], + "values": [264.9, 263.8], + }, + }, + }, + ], + "referencing": [ + { + "coordinates": ["x", "y", "z"], + "system": { + "type": "GeographicCRS", + "id": "http://www.opengis.net/def/crs/OGC/1.3/CRS84", + }, + } + ], + "parameters": { + "t": { + "type": "Parameter", + "description": "Temperature", + "unit": {"symbol": "K"}, + "observedProperty": {"id": "t", "label": {"en": "Temperature"}}, + }, + }, + } + + def test_circle_to_xarray_param_t_no_collision(self): + """to_xarray works with param 't' - no collision since dims use 'datetimes'.""" + decoder = Circle.Circle(self.covjson) + ds = decoder.to_xarray() + + # Param 't' should be in data_vars (no collision with 'datetimes' dim) + assert "t" in ds.data_vars, f"Expected 't' in data_vars, got {list(ds.data_vars)}" + # Time dimension is 'datetimes', not 't' + assert "datetimes" in ds.dims From 8229653d9487c7dba6eb55af4965677650e5c554 Mon Sep 17 00:00:00 2001 From: Lasse Meinen Date: Thu, 7 May 2026 11:04:39 +0200 Subject: [PATCH 14/22] add param 't' test for Grid --- tests/test_decoder_grid.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/test_decoder_grid.py b/tests/test_decoder_grid.py index 49e4a99..9444253 100644 --- a/tests/test_decoder_grid.py +++ b/tests/test_decoder_grid.py @@ -464,3 +464,26 @@ def test_grid_get_values(self): ], } assert decoder.get_values() == values + + def test_grid_to_xarray_param_t_no_collision(self): + """to_xarray works with param 't' - no collision since dims use 'datetimes'.""" + # Add a param named 't' to verify no collision + self.covjson["parameters"]["t"] = { + "type": "Parameter", + "description": "Temperature", + "unit": {"symbol": "K"}, + "observedProperty": {"id": "t", "label": {"en": "Temperature"}}, + } + for cov in self.covjson["coverages"]: + cov["ranges"]["t"] = cov["ranges"]["2t"].copy() + + for cov in self.covjson["coverages"]: + cov["mars:metadata"]["Forecast date"] = cov["mars:metadata"]["date"] + + decoder = Grid.Grid(self.covjson) + ds = decoder.to_xarray() + + # Param 't' should be in data_vars (no collision with 'datetimes' dim) + assert "t" in ds.data_vars, f"Expected 't' in data_vars, got {list(ds.data_vars)}" + # Time dimension is 'datetimes', not 't' + assert "datetimes" in ds.dims From 0b1d988ef32c4fefb5d1660c4aec1ae2eee47623 Mon Sep 17 00:00:00 2001 From: Lasse Meinen Date: Thu, 7 May 2026 11:40:06 +0200 Subject: [PATCH 15/22] add json file for verticalprofile test --- tests/data/test_verticalprofile_param_t.json | 94 ++++++++++++++++++++ tests/test_decoder_vertical_profile.py | 31 +++++-- 2 files changed, 117 insertions(+), 8 deletions(-) create mode 100644 tests/data/test_verticalprofile_param_t.json diff --git a/tests/data/test_verticalprofile_param_t.json b/tests/data/test_verticalprofile_param_t.json new file mode 100644 index 0000000..388f08d --- /dev/null +++ b/tests/data/test_verticalprofile_param_t.json @@ -0,0 +1,94 @@ +{ + "type": "CoverageCollection", + "domainType": "VerticalProfile", + "coverages": [ + { + "mars:metadata": { + "class": "od", + "Forecast date": "2026-05-06T18:00:00Z", + "expver": "0001", + "levelist": 100, + "levtype": "pl", + "model": "icon-ch1-eps", + "step": 0, + "stream": "enfo", + "timespan": "none", + "type": "cf", + "number": 0 + }, + "type": "Coverage", + "domain": { + "type": "Domain", + "axes": { + "latitude": { + "values": [ + 47.45541763305664 + ] + }, + "longitude": { + "values": [ + 8.560781121253967 + ] + }, + "levelist": { + "values": [ + 100, + 150 + ] + }, + "t": { + "values": [ + "2026-05-06T18:00:00Z" + ] + } + } + }, + "ranges": { + "t": { + "type": "NdArray", + "dataType": "float", + "shape": [ + 2 + ], + "axisNames": [ + "levelist" + ], + "values": [ + 218.3372802734375, + 219.89144897460938 + ] + } + } + } + ], + "referencing": [ + { + "coordinates": [ + "latitude", + "longitude", + "levelist" + ], + "system": { + "type": "GeographicCRS", + "id": "http://www.opengis.net/def/crs/OGC/1.3/CRS84" + } + } + ], + "parameters": { + "t": { + "type": "Parameter", + "description": { + "en": null + }, + "unit": { + "symbol": "K" + }, + "observedProperty": { + "id": "t", + "label": { + "en": "Temperature" + } + } + } + } +} diff --git a/tests/test_decoder_vertical_profile.py b/tests/test_decoder_vertical_profile.py index c543cf9..d1d04ce 100644 --- a/tests/test_decoder_vertical_profile.py +++ b/tests/test_decoder_vertical_profile.py @@ -1,4 +1,7 @@ +import json + from covjsonkit.api import Covjsonkit +from pathlib import Path class TestDecoder: @@ -11,6 +14,7 @@ def setup_method(self, method): "mars:metadata": { "class": "ea", "date": "2017-01-01 12:00:00", + "Forecast date": "2017-01-01T12:00:00Z", "levtype": "pl", "step": "0", "stream": "enda", @@ -47,6 +51,7 @@ def setup_method(self, method): "mars:metadata": { "class": "ea", "date": "2017-01-01 12:00:00", + "Forecast date": "2017-01-01T12:00:00Z", "levtype": "pl", "step": "0", "stream": "enda", @@ -59,7 +64,7 @@ def setup_method(self, method): "x": {"values": ["0.0"]}, "y": {"values": ["0.0"]}, "z": {"values": ["500", "850"]}, - "t": {"values": ["2017-01-01 12:00:00"]}, + "t": {"values": ["2017-01-01T12:00:00Z"]}, }, }, "ranges": { @@ -92,7 +97,9 @@ def setup_method(self, method): "coordinates": ["z"], "system": { "type": "VerticalCRS", - "cs": {"csAxes": [{"name": {"en": "level"}, "direction": "down"}]}, + "cs": { + "csAxes": [{"name": {"en": "level"}, "direction": "down"}] + }, }, }, ], @@ -241,9 +248,17 @@ def test_verticalprofile_values(self): } assert decoder.get_values() == values - # def test_verticalprofile_to_xarray(self): - # decoder = Covjsonkit().decode(self.covjson) - # dataset = decoder.to_xarray() - # encoder = Covjsonkit.encoder.VerticalProfile.VerticalProfile("CoverageCollection", "VerticalProfile") - # cov = encoder.from_xarray(dataset) - # print(cov) + def test_verticalprofile_to_xarray_param_t(self): + """to_xarray works with param 't' - no collision since dims use 'time'.""" + path = Path(__file__).parent / "data/test_verticalprofile_param_t.json" + with open(path, "r") as f: + covjson = json.load(f) + decoder = Covjsonkit().decode(covjson) + ds = decoder.to_xarray() + + # Param 't' should be in data_vars (no collision with 'time' dim) + assert "t" in ds.data_vars, ( + f"Expected 't' in data_vars, got {list(ds.data_vars)}" + ) + # Time dimension is 'time', not 't' + assert "time" in ds.dims From 5617d97e96dbb1455c6ce078133c60c0b211315f Mon Sep 17 00:00:00 2001 From: Lasse Meinen Date: Thu, 7 May 2026 11:41:04 +0200 Subject: [PATCH 16/22] add param 't' test for Frame --- tests/test_decoder_frame.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_decoder_frame.py b/tests/test_decoder_frame.py index 11b4e94..23ffa25 100644 --- a/tests/test_decoder_frame.py +++ b/tests/test_decoder_frame.py @@ -314,3 +314,13 @@ def test_to_xarray_multi_coverage(self): # MARS metadata from the first coverage is attached as dataset attrs assert ds.attrs["step"] == "0" assert ds.attrs["class"] == "od" + + def test_to_xarray_param_t_no_collision(self): + """to_xarray works with param 't' - no collision since dims use 'time'.""" + decoder = Frame.Frame(self.covjson) + ds = decoder.to_xarray() + + # Param 't' should be in data_vars (no collision with 'time' dim) + assert "t" in ds.data_vars, f"Expected 't' in data_vars, got {list(ds.data_vars)}" + # Time dimension is 'time', not 't' + assert "time" in ds.dims From 81fe543f469720401328c1732840480f1bd3af3a Mon Sep 17 00:00:00 2001 From: Lasse Meinen Date: Thu, 7 May 2026 11:41:25 +0200 Subject: [PATCH 17/22] add param 't' test for Shapefile --- tests/test_decoder_shapefile.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_decoder_shapefile.py b/tests/test_decoder_shapefile.py index b08e4bd..5410d4f 100644 --- a/tests/test_decoder_shapefile.py +++ b/tests/test_decoder_shapefile.py @@ -310,3 +310,13 @@ def test_to_xarray_multi_coverage(self): # MARS metadata from the first coverage is attached as dataset attrs assert ds.attrs["step"] == "0" assert ds.attrs["class"] == "od" + + def test_to_xarray_param_t_no_collision(self): + """to_xarray works with param 't' - no collision since dims use 'time'.""" + decoder = Shapefile.Shapefile(self.covjson) + ds = decoder.to_xarray() + + # Param 't' should be in data_vars (no collision with 'time' dim) + assert "t" in ds.data_vars, f"Expected 't' in data_vars, got {list(ds.data_vars)}" + # Time dimension is 'time', not 't' + assert "time" in ds.dims From 4d298104f592fbdb6f7498e057eabb370f16378a Mon Sep 17 00:00:00 2001 From: Lasse Meinen Date: Thu, 7 May 2026 11:41:40 +0200 Subject: [PATCH 18/22] add param 't' test for Wkt --- tests/test_decoder_wkt.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_decoder_wkt.py b/tests/test_decoder_wkt.py index 8e5776f..280bdb2 100644 --- a/tests/test_decoder_wkt.py +++ b/tests/test_decoder_wkt.py @@ -310,3 +310,13 @@ def test_to_xarray_multi_coverage(self): # MARS metadata from the first coverage is attached as dataset attrs assert ds.attrs["step"] == "0" assert ds.attrs["class"] == "od" + + def test_to_xarray_param_t_no_collision(self): + """to_xarray works with param 't' - no collision since dims use 'time'.""" + decoder = Wkt.Wkt(self.covjson) + ds = decoder.to_xarray() + + # Param 't' should be in data_vars (no collision with 'time' dim) + assert "t" in ds.data_vars, f"Expected 't' in data_vars, got {list(ds.data_vars)}" + # Time dimension is 'time', not 't' + assert "time" in ds.dims From ede8551d324f9e4987cc767bb564b05fe2018c18 Mon Sep 17 00:00:00 2001 From: Lasse Meinen Date: Thu, 7 May 2026 11:42:13 +0200 Subject: [PATCH 19/22] add param 't' test for Multipoint --- tests/test_xarray_multipoint.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_xarray_multipoint.py b/tests/test_xarray_multipoint.py index 3373000..4cf8cee 100644 --- a/tests/test_xarray_multipoint.py +++ b/tests/test_xarray_multipoint.py @@ -44,3 +44,23 @@ def test_from_xarray(self): covjson_result["coverages"][0]["ranges"]["2t"]["values"][0] == self.test_covjson["coverages"][0]["ranges"]["2t"]["values"][0] ) + + def test_to_xarray_param_t_no_collision(self): + """to_xarray works with param 't' - no collision since dims use 'datetimes'.""" + # Add a param named 't' to verify no collision + self.test_covjson["parameters"]["t"] = { + "type": "Parameter", + "description": {"en": "Temperature"}, + "unit": {"symbol": "K"}, + "observedProperty": {"id": "t", "label": {"en": "Temperature"}}, + } + for cov in self.test_covjson["coverages"]: + cov["ranges"]["t"] = cov["ranges"]["2t"].copy() + + decoder_obj = Covjsonkit().decode(self.test_covjson) + ds = decoder_obj.to_xarray() + + # Param 't' should be in data_vars (no collision with 'datetimes' dim) + assert "t" in ds.data_vars, f"Expected 't' in data_vars, got {list(ds.data_vars)}" + # Time dimension is 'datetimes', not 't' + assert "datetimes" in ds.dims From 37b28afc5daf6d13f1194654763acf2faaf26b0a Mon Sep 17 00:00:00 2001 From: Lasse Meinen Date: Thu, 7 May 2026 11:42:35 +0200 Subject: [PATCH 20/22] add param 't' test for Path --- tests/test_xarray_path.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_xarray_path.py b/tests/test_xarray_path.py index de413d7..8717648 100644 --- a/tests/test_xarray_path.py +++ b/tests/test_xarray_path.py @@ -44,3 +44,23 @@ def test_from_xarray(self): covjson_result["coverages"][0]["ranges"]["2t"]["values"][0] == self.test_covjson["coverages"][0]["ranges"]["2t"]["values"][0] ) + + def test_to_xarray_param_t_no_collision(self): + """to_xarray works with param 't' - no collision since dims use 'datetimes'.""" + # Add a param named 't' to verify no collision + self.test_covjson["parameters"]["t"] = { + "type": "Parameter", + "description": {"en": "Temperature"}, + "unit": {"symbol": "K"}, + "observedProperty": {"id": "t", "label": {"en": "Temperature"}}, + } + for cov in self.test_covjson["coverages"]: + cov["ranges"]["t"] = cov["ranges"]["2t"].copy() + + decoder_obj = Covjsonkit().decode(self.test_covjson) + ds = decoder_obj.to_xarray() + + # Param 't' should be in data_vars (no collision with 'datetimes' dim) + assert "t" in ds.data_vars, f"Expected 't' in data_vars, got {list(ds.data_vars)}" + # Time dimension is 'datetimes', not 't' + assert "datetimes" in ds.dims From 86dd20fbd4b951e21477356274ba2599c5e5e38a Mon Sep 17 00:00:00 2001 From: Lasse Meinen Date: Thu, 7 May 2026 11:43:58 +0200 Subject: [PATCH 21/22] fix failing test: incorrect formatting for axis 't' --- tests/test_decoder_vertical_profile.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_decoder_vertical_profile.py b/tests/test_decoder_vertical_profile.py index d1d04ce..efe7ad4 100644 --- a/tests/test_decoder_vertical_profile.py +++ b/tests/test_decoder_vertical_profile.py @@ -14,7 +14,6 @@ def setup_method(self, method): "mars:metadata": { "class": "ea", "date": "2017-01-01 12:00:00", - "Forecast date": "2017-01-01T12:00:00Z", "levtype": "pl", "step": "0", "stream": "enda", @@ -51,7 +50,6 @@ def setup_method(self, method): "mars:metadata": { "class": "ea", "date": "2017-01-01 12:00:00", - "Forecast date": "2017-01-01T12:00:00Z", "levtype": "pl", "step": "0", "stream": "enda", @@ -64,7 +62,7 @@ def setup_method(self, method): "x": {"values": ["0.0"]}, "y": {"values": ["0.0"]}, "z": {"values": ["500", "850"]}, - "t": {"values": ["2017-01-01T12:00:00Z"]}, + "t": {"values": ["2017-01-01 12:00:00"]}, }, }, "ranges": { From b116098c012e397d35684c44ae8d9204a254753d Mon Sep 17 00:00:00 2001 From: awarde96 Date: Thu, 7 May 2026 13:04:24 +0000 Subject: [PATCH 22/22] Fix formatting --- covjsonkit/decoder/Position.py | 8 ++------ covjsonkit/decoder/TimeSeries.py | 24 +++++++----------------- tests/test_decoder_time_series.py | 6 +++--- tests/test_decoder_vertical_profile.py | 12 ++++-------- 4 files changed, 16 insertions(+), 34 deletions(-) diff --git a/covjsonkit/decoder/Position.py b/covjsonkit/decoder/Position.py index 0957bc5..4331582 100644 --- a/covjsonkit/decoder/Position.py +++ b/covjsonkit/decoder/Position.py @@ -159,17 +159,13 @@ def to_xarray(self): } for parameter in self.parameters: - param_values = [ - [[] for _ in range(len(datetime))] for _ in range(len(nums)) - ] + param_values = [[[] for _ in range(len(datetime))] for _ in range(len(nums))] # Extract parameter values for the current domain for i, j, cov in cov_idx_list: param_values[i][j] = cov["ranges"][parameter]["values"] - long_name = self.get_parameter_metadata(parameter)["observedProperty"][ - "id" - ] + long_name = self.get_parameter_metadata(parameter)["observedProperty"]["id"] if long_name == "t": long_name = "T" # Avoid collision with time dimension 't' diff --git a/covjsonkit/decoder/TimeSeries.py b/covjsonkit/decoder/TimeSeries.py index f3aa661..28f5702 100644 --- a/covjsonkit/decoder/TimeSeries.py +++ b/covjsonkit/decoder/TimeSeries.py @@ -110,10 +110,7 @@ def to_xarray(self): # time steps into a single coverage's t-axis and do NOT write "Forecast date" # into mars:metadata. Detect this case and use a simpler (time, point) layout # that mirrors the Wkt/Frame/Shapefile decoders. - has_forecast_date = any( - "Forecast date" in cov.get("mars:metadata", {}) - for cov in self.covjson["coverages"] - ) + has_forecast_date = any("Forecast date" in cov.get("mars:metadata", {}) for cov in self.covjson["coverages"]) if not has_forecast_date: return self._to_xarray_no_forecast_date() @@ -173,17 +170,13 @@ def to_xarray(self): } for parameter in self.parameters: - param_values = [ - [[] for _ in range(len(datetime))] for _ in range(len(nums)) - ] + param_values = [[[] for _ in range(len(datetime))] for _ in range(len(nums))] # Extract parameter values for the current domain for i, j, cov in cov_idx_list: param_values[i][j] = cov["ranges"][parameter]["values"] - long_name = self.get_parameter_metadata(parameter)["observedProperty"][ - "id" - ] + long_name = self.get_parameter_metadata(parameter)["observedProperty"]["id"] if long_name == "t": long_name = "T" # Avoid collision with time dimension 't' @@ -213,7 +206,8 @@ def to_xarray(self): return ds def _find_coverages(self, nums, datetime, x, y, z): - """Find coverages that match the given domain parameters (num, date, x, y, z) and return them along with domain parameter indices.""" + """Find coverages that match the given domain parameters (num, date, x, y, z) + and return them along with domain parameter indices.""" result = [] for i, num in enumerate(nums): for j, date in enumerate(datetime): @@ -256,9 +250,7 @@ def _to_xarray_no_forecast_date(self): dataarraydict = {} for parameter in self.parameters: values = coverage["ranges"][parameter]["values"] - long_name = self.get_parameter_metadata(parameter)["observedProperty"][ - "id" - ] + long_name = self.get_parameter_metadata(parameter)["observedProperty"]["id"] if long_name == "t": long_name = "T" # Avoid collision with time dimension 't' @@ -269,9 +261,7 @@ def _to_xarray_no_forecast_date(self): "long_name": long_name, } - dataarray = xr.DataArray( - values, dims=["t"], coords={"t": steps}, attrs=attrs - ) + dataarray = xr.DataArray(values, dims=["t"], coords={"t": steps}, attrs=attrs) dataarraydict[long_name] = dataarray diff --git a/tests/test_decoder_time_series.py b/tests/test_decoder_time_series.py index a96064e..80cb0a2 100644 --- a/tests/test_decoder_time_series.py +++ b/tests/test_decoder_time_series.py @@ -1,9 +1,9 @@ # from earthkit import data import json +from pathlib import Path from covjsonkit.api import Covjsonkit -from pathlib import Path class TestDecoder: @@ -290,7 +290,7 @@ def test_timeseries_coordinates(self): assert decoder.get_coordinates() == coordinates def test_timeseries_to_xarray_param_t(self): - path = Path(__file__).parent / "data/test_timeseries_param_t.json" + path = Path(__file__).parent / "data/test_timeseries_param_t.json" with open(path, "r") as f: covjson = json.load(f) ds = Covjsonkit().decode(covjson).to_xarray() @@ -298,7 +298,7 @@ def test_timeseries_to_xarray_param_t(self): assert all(var in ds.data_vars for var in data_vars) def test_timeseries_to_xarray_no_forecast_date_param_t(self): - path = Path(__file__).parent / "data/test_timeseries_param_t.json" + path = Path(__file__).parent / "data/test_timeseries_param_t.json" with open(path, "r") as f: covjson = json.load(f) covjson["coverages"][0].pop("mars:metadata") diff --git a/tests/test_decoder_vertical_profile.py b/tests/test_decoder_vertical_profile.py index efe7ad4..09ef4bc 100644 --- a/tests/test_decoder_vertical_profile.py +++ b/tests/test_decoder_vertical_profile.py @@ -1,7 +1,7 @@ import json +from pathlib import Path from covjsonkit.api import Covjsonkit -from pathlib import Path class TestDecoder: @@ -95,9 +95,7 @@ def setup_method(self, method): "coordinates": ["z"], "system": { "type": "VerticalCRS", - "cs": { - "csAxes": [{"name": {"en": "level"}, "direction": "down"}] - }, + "cs": {"csAxes": [{"name": {"en": "level"}, "direction": "down"}]}, }, }, ], @@ -248,15 +246,13 @@ def test_verticalprofile_values(self): def test_verticalprofile_to_xarray_param_t(self): """to_xarray works with param 't' - no collision since dims use 'time'.""" - path = Path(__file__).parent / "data/test_verticalprofile_param_t.json" + path = Path(__file__).parent / "data/test_verticalprofile_param_t.json" with open(path, "r") as f: covjson = json.load(f) decoder = Covjsonkit().decode(covjson) ds = decoder.to_xarray() # Param 't' should be in data_vars (no collision with 'time' dim) - assert "t" in ds.data_vars, ( - f"Expected 't' in data_vars, got {list(ds.data_vars)}" - ) + assert "t" in ds.data_vars, f"Expected 't' in data_vars, got {list(ds.data_vars)}" # Time dimension is 'time', not 't' assert "time" in ds.dims