covjsonkit 0.2.18__tar.gz → 0.2.20__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/PKG-INFO +1 -1
  2. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/decoder/Position.py +58 -58
  3. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/decoder/TimeSeries.py +74 -65
  4. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/encoder/encoder.py +70 -36
  5. covjsonkit-0.2.20/covjsonkit/version.py +1 -0
  6. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit.egg-info/PKG-INFO +1 -1
  7. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit.egg-info/SOURCES.txt +4 -0
  8. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/pyproject.toml +1 -1
  9. covjsonkit-0.2.20/tests/data/test_timeseries_param_t.json +47 -0
  10. covjsonkit-0.2.20/tests/data/test_verticalprofile_param_t.json +94 -0
  11. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_decoder_bounding_box.py +12 -6
  12. covjsonkit-0.2.20/tests/test_decoder_circle.py +71 -0
  13. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_decoder_frame.py +10 -0
  14. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_decoder_grid.py +23 -0
  15. covjsonkit-0.2.20/tests/test_decoder_position.py +25 -0
  16. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_decoder_shapefile.py +10 -0
  17. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_decoder_time_series.py +20 -2
  18. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_decoder_vertical_profile.py +15 -6
  19. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_decoder_wkt.py +10 -0
  20. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_xarray_multipoint.py +20 -0
  21. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_xarray_path.py +20 -0
  22. covjsonkit-0.2.18/covjsonkit/version.py +0 -1
  23. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/.github/ci-hpc-config.yml +0 -0
  24. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/.github/workflows/ci.yaml +0 -0
  25. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/.github/workflows/label-public-pr.yml +0 -0
  26. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/.gitignore +0 -0
  27. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/.pre-commit-config.yaml +0 -0
  28. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/ACKNOWLEDGEMENTS.rst +0 -0
  29. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/LICENSE +0 -0
  30. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/MANIFEST.in +0 -0
  31. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/Makefile +0 -0
  32. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/README.md +0 -0
  33. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/Coverage.py +0 -0
  34. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/CoverageCollection.py +0 -0
  35. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/__init__.py +0 -0
  36. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/api.py +0 -0
  37. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/config.py +0 -0
  38. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/data/__init__.py +0 -0
  39. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/data/dwd/param.json +0 -0
  40. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/data/dwd/param_id.json +0 -0
  41. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/data/dwd/unit.json +0 -0
  42. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/data/ecmwf/param.json +0 -0
  43. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/data/ecmwf/param_id.json +0 -0
  44. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/data/ecmwf/unit.json +0 -0
  45. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/decoder/BoundingBox.py +0 -0
  46. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/decoder/Circle.py +0 -0
  47. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/decoder/Frame.py +0 -0
  48. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/decoder/Grid.py +0 -0
  49. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/decoder/Path.py +0 -0
  50. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/decoder/Shapefile.py +0 -0
  51. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/decoder/VerticalProfile.py +0 -0
  52. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/decoder/Wkt.py +0 -0
  53. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/decoder/__init__.py +0 -0
  54. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/decoder/decoder.py +0 -0
  55. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/encoder/BoundingBox.py +0 -0
  56. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/encoder/Circle.py +0 -0
  57. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/encoder/Frame.py +0 -0
  58. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/encoder/Grid.py +0 -0
  59. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/encoder/Path.py +0 -0
  60. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/encoder/Position.py +0 -0
  61. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/encoder/Shapefile.py +0 -0
  62. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/encoder/TimeSeries.py +0 -0
  63. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/encoder/VerticalProfile.py +0 -0
  64. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/encoder/Wkt.py +0 -0
  65. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/encoder/__init__.py +0 -0
  66. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/param_db.py +0 -0
  67. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit/utils.py +0 -0
  68. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit.egg-info/dependency_links.txt +0 -0
  69. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit.egg-info/not-zip-safe +0 -0
  70. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit.egg-info/requires.txt +0 -0
  71. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/covjsonkit.egg-info/top_level.txt +0 -0
  72. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/docs/images/ECMWF_logo.svg.png +0 -0
  73. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/docs/images/Logo_Destination_Earth_Colours.png +0 -0
  74. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/example_config.json +0 -0
  75. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/requirements.txt +0 -0
  76. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/scripts/generate_param_db.py +0 -0
  77. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/setup.cfg +0 -0
  78. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/setup.py +0 -0
  79. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/conftest.py +0 -0
  80. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/data/test_coverage1.json +0 -0
  81. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/data/test_coverage2.json +0 -0
  82. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/data/test_geotiff.json +0 -0
  83. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/data/test_multipoint_coverage.json +0 -0
  84. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/data/test_path_coverage.json +0 -0
  85. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/data/test_timeseries_coverage.json +0 -0
  86. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/data/test_timeseries_xyz_coverage.json +0 -0
  87. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/data/test_verticalprofile_coverage.json +0 -0
  88. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/data/test_verticalprofile_xyz_coverage.json +0 -0
  89. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/geojsontest.py +0 -0
  90. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/requirements_test.txt +0 -0
  91. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_encoder_bounding_box.py +0 -0
  92. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_encoder_bounding_box_from_polytope.py +0 -0
  93. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_encoder_circle_from_polytope.py +0 -0
  94. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_encoder_frame.py +0 -0
  95. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_encoder_frame_from_polytope.py +0 -0
  96. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_encoder_grid.py +0 -0
  97. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_encoder_grid_from_polytope.py +0 -0
  98. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_encoder_path.py +0 -0
  99. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_encoder_path_from_polytope.py +0 -0
  100. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_encoder_position_from_polytope.py +0 -0
  101. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_encoder_shapefile.py +0 -0
  102. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_encoder_shapefile_from_polytope.py +0 -0
  103. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_encoder_time_series.py +0 -0
  104. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_encoder_time_series_from_polytope.py +0 -0
  105. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_encoder_vertical_profile_from_polytope.py +0 -0
  106. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_encoder_wkt.py +0 -0
  107. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_encoder_wkt_from_polytope.py +0 -0
  108. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_geojson.py +0 -0
  109. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_geotiff.py +0 -0
  110. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_util_merge.py +0 -0
  111. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_xarray_ts.py +0 -0
  112. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tests/test_xarray_vp.py +0 -0
  113. {covjsonkit-0.2.18 → covjsonkit-0.2.20}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: covjsonkit
3
- Version: 0.2.18
3
+ Version: 0.2.20
4
4
  Home-page: https://github.com/ecmwf/covjsonkit
5
5
  Author: ECMWF
6
6
  Author-email: James.Hawkes@ecmwf.int, Adam.Warde@ecmwf.int, Mathilde.Leuridan@ecmwf.int
@@ -109,9 +109,6 @@ class Position(Decoder):
109
109
  dims = ["latitude", "longitude", "levelist", "number", "datetime", "t"]
110
110
  ds = []
111
111
 
112
- # Get coordinates for all domains
113
- all_coords = self.get_domains()
114
-
115
112
  unique_coords = set() # To track unique coordinate tuples
116
113
  unique_domains = [] # To store unique domains
117
114
 
@@ -131,14 +128,17 @@ class Position(Decoder):
131
128
  unique_domains.append(domain) # Add to unique domains
132
129
 
133
130
  all_coords = unique_domains
134
- param_values = {}
135
131
 
136
- # Initialize parameter values for all parameters
137
- for parameter in self.parameters:
138
- param_values[parameter] = []
132
+ num = []
133
+ datetime = []
134
+ for coverage in self.covjson["coverages"]:
135
+ num.append(coverage["mars:metadata"]["number"])
136
+ datetime.append(coverage["mars:metadata"]["Forecast date"])
137
+ nums = list(set(num))
138
+ datetime = list(set(datetime))
139
139
 
140
140
  # Process each coordinate domain
141
- for domain_idx, coords in enumerate(all_coords):
141
+ for coords in all_coords:
142
142
  dataarraydict = {}
143
143
  x = coords["axes"][self.x_name]["values"]
144
144
  y = coords["axes"][self.y_name]["values"]
@@ -147,69 +147,69 @@ class Position(Decoder):
147
147
  steps = [step.replace("Z", "") for step in steps]
148
148
  steps = pd.to_datetime(steps)
149
149
 
150
- num = []
151
- datetime = []
152
- for coverage in self.covjson["coverages"]:
153
- num.append(coverage["mars:metadata"]["number"])
154
- datetime.append(coverage["mars:metadata"]["Forecast date"])
150
+ cov_idx_list = self._find_coverages(nums, datetime, x, y, z)
155
151
 
156
- nums = list(set(num))
157
- datetime = list(set(datetime))
152
+ coords = {
153
+ "latitude": x,
154
+ "longitude": y,
155
+ "levelist": z,
156
+ "number": nums,
157
+ "datetime": datetime,
158
+ "t": steps,
159
+ }
158
160
 
159
- # Extract parameter values for the current domain
160
161
  for parameter in self.parameters:
161
- if len(param_values[parameter]) <= domain_idx:
162
- param_values[parameter].append([])
163
-
164
- for i, num in enumerate(nums):
165
- if len(param_values[parameter][domain_idx]) <= i:
166
- param_values[parameter][domain_idx].append([])
167
-
168
- for j, date in enumerate(datetime):
169
- if len(param_values[parameter][domain_idx][i]) <= j:
170
- param_values[parameter][domain_idx][i].append([])
171
-
172
- for k, step in enumerate(steps):
173
- for coverage in self.covjson["coverages"]:
174
- if (
175
- coverage["mars:metadata"]["number"] == num
176
- and coverage["mars:metadata"]["Forecast date"] == date
177
- and coverage["domain"]["axes"][self.x_name]["values"] == x
178
- and coverage["domain"]["axes"][self.y_name]["values"] == y
179
- and coverage["domain"]["axes"][self.z_name]["values"] == z
180
- ):
181
- param_values[parameter][domain_idx][i][j] = coverage["ranges"][parameter]["values"]
162
+ param_values = [[[] for _ in range(len(datetime))] for _ in range(len(nums))]
182
163
 
183
- for parameter in self.parameters:
184
- param_coords = {
185
- "latitude": x,
186
- "longitude": y,
187
- "levelist": z,
188
- "number": nums,
189
- "datetime": datetime,
190
- "t": steps,
164
+ # Extract parameter values for the current domain
165
+ for i, j, cov in cov_idx_list:
166
+ param_values[i][j] = cov["ranges"][parameter]["values"]
167
+
168
+ long_name = self.get_parameter_metadata(parameter)["observedProperty"]["id"]
169
+
170
+ if long_name == "t":
171
+ long_name = "T" # Avoid collision with time dimension 't'
172
+
173
+ attrs = {
174
+ "type": self.get_parameter_metadata(parameter)["type"],
175
+ "units": self.get_parameter_metadata(parameter)["unit"]["symbol"],
176
+ "long_name": long_name,
191
177
  }
192
- dataarray = xr.DataArray(
193
- [[[param_values[parameter][domain_idx]]]],
194
- dims=dims,
195
- coords=param_coords,
196
- name=f"{parameter}_domain_{domain_idx}",
178
+ dataarraydict[long_name] = (
179
+ dims,
180
+ [[[param_values]]],
181
+ attrs,
197
182
  )
198
183
 
199
- dataarray.attrs["type"] = self.get_parameter_metadata(parameter)["type"]
200
- dataarray.attrs["units"] = self.get_parameter_metadata(parameter)["unit"]["symbol"]
201
- dataarray.attrs["long_name"] = self.get_parameter_metadata(parameter)["observedProperty"]["id"]
202
- dataarraydict[dataarray.attrs["long_name"]] = dataarray
203
-
204
- ds.append(xr.Dataset(dataarraydict))
184
+ ds.append(xr.Dataset(data_vars=dataarraydict, coords=coords))
205
185
 
206
186
  # Combine all DataArrays into a Dataset
207
187
  for mars_metadata in self.mars_metadata[0]:
208
- for dss in ds:
209
- if mars_metadata != "date" and mars_metadata != "step":
188
+ if mars_metadata != "date" and mars_metadata != "step":
189
+ for dss in ds:
210
190
  dss.attrs[mars_metadata] = self.mars_metadata[0][mars_metadata]
211
191
 
212
192
  if len(ds) == 1:
213
193
  return ds[0]
214
194
 
215
195
  return ds
196
+
197
+ def _find_coverages(self, nums, datetime, x, y, z):
198
+ """Find coverages matching domain parameters and return with indices."""
199
+ result = []
200
+ for i, num in enumerate(nums):
201
+ for j, date in enumerate(datetime):
202
+ for coverage in self.covjson["coverages"]:
203
+ if self._covers_domain(coverage, num, date, x, y, z):
204
+ result.append((i, j, coverage))
205
+ return result
206
+
207
+ def _covers_domain(self, coverage, num, date, x, y, z):
208
+ """Check if coverage matches the given domain parameters."""
209
+ return (
210
+ coverage["mars:metadata"]["number"] == num
211
+ and coverage["mars:metadata"]["Forecast date"] == date
212
+ and coverage["domain"]["axes"][self.x_name]["values"] == x
213
+ and coverage["domain"]["axes"][self.y_name]["values"] == y
214
+ and coverage["domain"]["axes"][self.z_name]["values"] == z
215
+ )
@@ -139,14 +139,17 @@ class TimeSeries(Decoder):
139
139
  unique_domains.append(domain) # Add to unique domains
140
140
 
141
141
  all_coords = unique_domains
142
- param_values = {}
143
142
 
144
- # Initialize parameter values for all parameters
145
- for parameter in self.parameters:
146
- param_values[parameter] = []
143
+ num = []
144
+ datetime = []
145
+ for coverage in self.covjson["coverages"]:
146
+ num.append(coverage["mars:metadata"]["number"])
147
+ datetime.append(coverage["mars:metadata"]["Forecast date"])
148
+ nums = list(set(num))
149
+ datetime = list(set(datetime))
147
150
 
148
151
  # Process each coordinate domain
149
- for domain_idx, coords in enumerate(all_coords):
152
+ for coords in all_coords:
150
153
  dataarraydict = {}
151
154
  x = coords["axes"][self.x_name]["values"]
152
155
  y = coords["axes"][self.y_name]["values"]
@@ -155,66 +158,46 @@ class TimeSeries(Decoder):
155
158
  steps = [step.replace("Z", "") for step in steps]
156
159
  steps = pd.to_datetime(steps)
157
160
 
158
- num = []
159
- datetime = []
160
- for coverage in self.covjson["coverages"]:
161
- num.append(coverage["mars:metadata"]["number"])
162
- datetime.append(coverage["mars:metadata"]["Forecast date"])
161
+ cov_idx_list = self._find_coverages(nums, datetime, x, y, z)
163
162
 
164
- nums = list(set(num))
165
- datetime = list(set(datetime))
163
+ coords = {
164
+ "latitude": x,
165
+ "longitude": y,
166
+ "levelist": z,
167
+ "number": nums,
168
+ "datetime": datetime,
169
+ "t": steps,
170
+ }
166
171
 
167
- # Extract parameter values for the current domain
168
172
  for parameter in self.parameters:
169
- if len(param_values[parameter]) <= domain_idx:
170
- param_values[parameter].append([])
171
-
172
- for i, num in enumerate(nums):
173
- if len(param_values[parameter][domain_idx]) <= i:
174
- param_values[parameter][domain_idx].append([])
175
-
176
- for j, date in enumerate(datetime):
177
- if len(param_values[parameter][domain_idx][i]) <= j:
178
- param_values[parameter][domain_idx][i].append([])
179
-
180
- for k, step in enumerate(steps):
181
- for coverage in self.covjson["coverages"]:
182
- if (
183
- coverage["mars:metadata"]["number"] == num
184
- and coverage["mars:metadata"]["Forecast date"] == date
185
- and coverage["domain"]["axes"][self.x_name]["values"] == x
186
- and coverage["domain"]["axes"][self.y_name]["values"] == y
187
- and coverage["domain"]["axes"][self.z_name]["values"] == z
188
- ):
189
- param_values[parameter][domain_idx][i][j] = coverage["ranges"][parameter]["values"]
173
+ param_values = [[[] for _ in range(len(datetime))] for _ in range(len(nums))]
190
174
 
191
- for parameter in self.parameters:
192
- param_coords = {
193
- "latitude": x,
194
- "longitude": y,
195
- "levelist": z,
196
- "number": nums,
197
- "datetime": datetime,
198
- "t": steps,
175
+ # Extract parameter values for the current domain
176
+ for i, j, cov in cov_idx_list:
177
+ param_values[i][j] = cov["ranges"][parameter]["values"]
178
+
179
+ long_name = self.get_parameter_metadata(parameter)["observedProperty"]["id"]
180
+
181
+ if long_name == "t":
182
+ long_name = "T" # Avoid collision with time dimension 't'
183
+
184
+ attrs = {
185
+ "type": self.get_parameter_metadata(parameter)["type"],
186
+ "units": self.get_parameter_metadata(parameter)["unit"]["symbol"],
187
+ "long_name": long_name,
199
188
  }
200
- dataarray = xr.DataArray(
201
- [[[param_values[parameter][domain_idx]]]],
202
- dims=dims,
203
- coords=param_coords,
204
- name=f"{parameter}_domain_{domain_idx}",
189
+ dataarraydict[long_name] = (
190
+ dims,
191
+ [[[param_values]]],
192
+ attrs,
205
193
  )
206
194
 
207
- dataarray.attrs["type"] = self.get_parameter_metadata(parameter)["type"]
208
- dataarray.attrs["units"] = self.get_parameter_metadata(parameter)["unit"]["symbol"]
209
- dataarray.attrs["long_name"] = self.get_parameter_metadata(parameter)["observedProperty"]["id"]
210
- dataarraydict[dataarray.attrs["long_name"]] = dataarray
211
-
212
- ds.append(xr.Dataset(dataarraydict))
195
+ ds.append(xr.Dataset(data_vars=dataarraydict, coords=coords))
213
196
 
214
197
  # Combine all DataArrays into a Dataset
215
198
  for mars_metadata in self.mars_metadata[0]:
216
- for dss in ds:
217
- if mars_metadata != "date" and mars_metadata != "step":
199
+ if mars_metadata != "date" and mars_metadata != "step":
200
+ for dss in ds:
218
201
  dss.attrs[mars_metadata] = self.mars_metadata[0][mars_metadata]
219
202
 
220
203
  if len(ds) == 1:
@@ -222,6 +205,27 @@ class TimeSeries(Decoder):
222
205
 
223
206
  return ds
224
207
 
208
+ def _find_coverages(self, nums, datetime, x, y, z):
209
+ """Find coverages that match the given domain parameters (num, date, x, y, z)
210
+ and return them along with domain parameter indices."""
211
+ result = []
212
+ for i, num in enumerate(nums):
213
+ for j, date in enumerate(datetime):
214
+ for coverage in self.covjson["coverages"]:
215
+ if self._covers_domain(coverage, num, date, x, y, z):
216
+ result.append((i, j, coverage))
217
+ return result
218
+
219
+ def _covers_domain(self, coverage, num, date, x, y, z):
220
+ """check if coverage matches the given domain parameters (num, date, x, y, z)"""
221
+ return (
222
+ coverage["mars:metadata"]["number"] == num
223
+ and coverage["mars:metadata"]["Forecast date"] == date
224
+ and coverage["domain"]["axes"][self.x_name]["values"] == x
225
+ and coverage["domain"]["axes"][self.y_name]["values"] == y
226
+ and coverage["domain"]["axes"][self.z_name]["values"] == z
227
+ )
228
+
225
229
  def _to_xarray_no_forecast_date(self):
226
230
  """Convert monthly-means CovJSON (no 'Forecast date' in metadata) to xarray.
227
231
 
@@ -233,7 +237,7 @@ class TimeSeries(Decoder):
233
237
  """
234
238
  ds_list = []
235
239
 
236
- for cov_idx, coverage in enumerate(self.covjson["coverages"]):
240
+ for coverage in self.covjson["coverages"]:
237
241
  domain = coverage["domain"]["axes"]
238
242
  x = domain[self.x_name]["values"]
239
243
  y = domain[self.y_name]["values"]
@@ -246,15 +250,20 @@ class TimeSeries(Decoder):
246
250
  dataarraydict = {}
247
251
  for parameter in self.parameters:
248
252
  values = coverage["ranges"][parameter]["values"]
249
- dataarray = xr.DataArray(
250
- values,
251
- dims=["t"],
252
- coords={"t": steps},
253
- )
254
- dataarray.attrs["type"] = self.get_parameter_metadata(parameter)["type"]
255
- dataarray.attrs["units"] = self.get_parameter_metadata(parameter)["unit"]["symbol"]
256
- dataarray.attrs["long_name"] = self.get_parameter_metadata(parameter)["observedProperty"]["id"]
257
- dataarraydict[dataarray.attrs["long_name"]] = dataarray
253
+ long_name = self.get_parameter_metadata(parameter)["observedProperty"]["id"]
254
+
255
+ if long_name == "t":
256
+ long_name = "T" # Avoid collision with time dimension 't'
257
+
258
+ attrs = {
259
+ "type": self.get_parameter_metadata(parameter)["type"],
260
+ "units": self.get_parameter_metadata(parameter)["unit"]["symbol"],
261
+ "long_name": long_name,
262
+ }
263
+
264
+ dataarray = xr.DataArray(values, dims=["t"], coords={"t": steps}, attrs=attrs)
265
+
266
+ dataarraydict[long_name] = dataarray
258
267
 
259
268
  coord_dict = dict(
260
269
  latitude=(["latitude"], x),
@@ -589,7 +589,7 @@ class Encoder(ABC):
589
589
  ]
590
590
  )
591
591
 
592
- def walk_tree_month(self, tree, fields, coords, mars_metadata, range_dict):
592
+ def walk_tree_month(self, tree, fields, coords, mars_metadata, range_dict, _ctx=None):
593
593
  """Walk the result tree for monthly-mean streams (e.g. clmn).
594
594
 
595
595
  These streams use ``year`` and ``month`` axes instead of ``date``/``time``/``step``.
@@ -597,28 +597,21 @@ class Encoder(ABC):
597
597
  ``"YYYY-MM"`` that plays the same role as ``date`` does in the standard
598
598
  step-based tree walker.
599
599
 
600
- Because the tree ordering can place ``month`` before ``year`` (or vice
601
- versa), both axes are collected independently into ``fields["months"]``
602
- and ``fields["years"]``. The combined "YYYY-MM" keys are only built
603
- once both are available — either when the second axis is encountered, or
604
- at the leaf node.
600
+ Dates are accumulated in the order they are actually encountered during
601
+ tree traversal, so the ordering correctly reflects the tree structure
602
+ (e.g. month-major when the tree has month as the outer axis).
605
603
  """
604
+ if _ctx is None:
605
+ _ctx = {}
606
606
 
607
607
  def _year_month_key(year, month):
608
608
  return f"{int(year):04d}-{int(month):02d}"
609
609
 
610
- def _ensure_date_keys():
611
- """Populate ``fields["dates"]`` and ``coords`` once both year and
612
- month values are known."""
613
- if not fields.get("years") or not fields.get("months"):
614
- return
615
- for year in fields["years"]:
616
- for month in fields["months"]:
617
- key = _year_month_key(year, month)
618
- if key not in fields["dates"]:
619
- fields["dates"].append(key)
620
- if key not in coords:
621
- coords[key] = {"composite": [], "t": [key]}
610
+ def _register_date_key(key):
611
+ if key not in fields["dates"]:
612
+ fields["dates"].append(key)
613
+ if key not in coords:
614
+ coords[key] = {"composite": [], "t": [key]}
622
615
 
623
616
  def handle_non_leaf_node_month(child):
624
617
  non_leaf_axes = ["latitude", "longitude", "param", "year", "month"]
@@ -651,6 +644,10 @@ class Encoder(ABC):
651
644
  for child in tree.children:
652
645
  handle_non_leaf_node_month(child)
653
646
  result = handle_specific_axes_month(child)
647
+
648
+ # Build a child context that inherits the current year/month
649
+ child_ctx = dict(_ctx)
650
+
654
651
  if result is not None:
655
652
  if child.axis.name == "latitude":
656
653
  fields["lat"] = result
@@ -661,35 +658,72 @@ class Encoder(ABC):
661
658
  elif child.axis.name == "param":
662
659
  fields["param"] = result
663
660
  elif child.axis.name == "year":
664
- fields["years"] = result
665
- _ensure_date_keys()
661
+ fields["years"] = list(result) if fields.get("years") == [] else fields["years"]
662
+ child_ctx["years"] = result
663
+ child_ctx["_axis_order"] = _ctx.get("_axis_order", []) + ["year"]
664
+ # If month is already fixed in context, register dates now.
665
+ if "months" in _ctx:
666
+ for y in result:
667
+ for m in _ctx["months"]:
668
+ _register_date_key(_year_month_key(y, m))
666
669
  elif child.axis.name == "month":
667
- fields["months"] = result
668
- _ensure_date_keys()
670
+ fields["months"] = list(result) if fields.get("months") == [] else fields["months"]
671
+ child_ctx["months"] = result
672
+ # If year is already fixed in context, register dates now.
673
+ if "years" in _ctx:
674
+ for y in _ctx["years"]:
675
+ for m in result:
676
+ _register_date_key(_year_month_key(y, m))
677
+ # Track that month is the inner axis relative to year
678
+ child_ctx["_axis_order"] = _ctx.get("_axis_order", []) + ["month"]
669
679
  elif child.axis.name == "number":
670
680
  fields["number"] = result
671
681
 
672
- self.walk_tree_month(child, fields, coords, mars_metadata, range_dict)
682
+ self.walk_tree_month(child, fields, coords, mars_metadata, range_dict, _ctx=child_ctx)
673
683
  else:
674
- # Leaf node — make sure date keys are built before processing.
675
- _ensure_date_keys()
684
+ # Leaf node — ensure all (year, month) combinations from context are registered.
685
+ ctx_years = _ctx.get("years", fields.get("years", []))
686
+ ctx_months = _ctx.get("months", fields.get("months", []))
687
+ for y in ctx_years:
688
+ for m in ctx_months:
689
+ _register_date_key(_year_month_key(y, m))
690
+
691
+ # Determine the dates in scope for this specific leaf. The loop order
692
+ # must match the actual tree axis order (outermost axis first) so that
693
+ # the flat result array is sliced correctly.
694
+ # _axis_order records axes in the order they were encountered top-down;
695
+ # the first entry is the outer axis at this leaf.
696
+ if ctx_years and ctx_months:
697
+ axis_order = _ctx.get("_axis_order", [])
698
+ # Default: if year was seen before month in the tree, year is outer.
699
+ year_is_outer = (
700
+ axis_order.index("year") < axis_order.index("month")
701
+ if ("year" in axis_order and "month" in axis_order)
702
+ else True
703
+ )
704
+ if year_is_outer:
705
+ leaf_dates = [_year_month_key(y, m) for y in ctx_years for m in ctx_months]
706
+ else:
707
+ leaf_dates = [_year_month_key(y, m) for m in ctx_months for y in ctx_years]
708
+ else:
709
+ leaf_dates = fields["dates"]
676
710
 
677
711
  tree.values = [float(val) for val in tree.values]
678
712
  if all(val is None for val in tree.result):
679
- # Remove the last date entry that produced no data.
680
- if fields["dates"]:
681
- last_date = fields["dates"][-1]
682
- fields["dates"] = fields["dates"][:-1]
713
+ # Remove date entries for this leaf that produced no data.
714
+ for key in leaf_dates:
715
+ if key in fields["dates"]:
716
+ fields["dates"].remove(key)
683
717
  for level in fields["levels"]:
684
718
  for num in fields["number"]:
685
719
  for para in fields["param"]:
686
- key = (last_date, level, num, para)
687
- if key in range_dict:
688
- del range_dict[key]
720
+ rkey = (key, level, num, para)
721
+ if rkey in range_dict:
722
+ del range_dict[rkey]
689
723
  else:
690
724
  tree.result = [float(val) if val is not None else val for val in tree.result]
691
725
 
692
- n_dates = len(fields["dates"])
726
+ n_dates = len(leaf_dates)
693
727
  n_levels = len(fields["levels"])
694
728
  n_params = len(fields["param"])
695
729
 
@@ -698,11 +732,11 @@ class Encoder(ABC):
698
732
  para_len = level_len / n_params if n_params else level_len
699
733
 
700
734
  # Append this leaf's longitude values to composite coords for
701
- # every date key so spatial points are recorded.
702
- for date in fields["dates"]:
735
+ # every date key in scope.
736
+ for date in leaf_dates:
703
737
  append_composite_coords_month(date, tree.values, fields["lat"])
704
738
 
705
- for d, date in enumerate(fields["dates"]):
739
+ for d, date in enumerate(leaf_dates):
706
740
  for l, level in enumerate(fields["levels"]): # noqa: E741
707
741
  for i, num in enumerate(fields["number"]):
708
742
  for j, para in enumerate(fields["param"]):
@@ -0,0 +1 @@
1
+ __version__ = "0.2.20"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: covjsonkit
3
- Version: 0.2.18
3
+ Version: 0.2.20
4
4
  Home-page: https://github.com/ecmwf/covjsonkit
5
5
  Author: ECMWF
6
6
  Author-email: James.Hawkes@ecmwf.int, Adam.Warde@ecmwf.int, Mathilde.Leuridan@ecmwf.int
@@ -65,8 +65,10 @@ tests/conftest.py
65
65
  tests/geojsontest.py
66
66
  tests/requirements_test.txt
67
67
  tests/test_decoder_bounding_box.py
68
+ tests/test_decoder_circle.py
68
69
  tests/test_decoder_frame.py
69
70
  tests/test_decoder_grid.py
71
+ tests/test_decoder_position.py
70
72
  tests/test_decoder_shapefile.py
71
73
  tests/test_decoder_time_series.py
72
74
  tests/test_decoder_vertical_profile.py
@@ -101,6 +103,8 @@ tests/data/test_geotiff.json
101
103
  tests/data/test_multipoint_coverage.json
102
104
  tests/data/test_path_coverage.json
103
105
  tests/data/test_timeseries_coverage.json
106
+ tests/data/test_timeseries_param_t.json
104
107
  tests/data/test_timeseries_xyz_coverage.json
105
108
  tests/data/test_verticalprofile_coverage.json
109
+ tests/data/test_verticalprofile_param_t.json
106
110
  tests/data/test_verticalprofile_xyz_coverage.json
@@ -8,7 +8,7 @@ markers = ["data: uses test data (deselect with '-m \"not data\"')",]
8
8
 
9
9
  [project]
10
10
  name = "covjsonkit"
11
- version = "0.2.18"
11
+ version = "0.2.20"
12
12
  dependencies = [
13
13
  "pandas<3",
14
14
  "orjson",
@@ -0,0 +1,47 @@
1
+ {
2
+ "type": "CoverageCollection",
3
+ "domainType": "PointSeries",
4
+ "coverages": [
5
+ {
6
+ "mars:metadata": {
7
+ "Forecast date": "2026-05-04T18:00:00Z",
8
+ "number": 1
9
+ },
10
+ "type": "Coverage",
11
+ "domain": {
12
+ "type": "Domain",
13
+ "axes": {
14
+ "latitude": {"values": [47.5]},
15
+ "longitude": {"values": [8.5]},
16
+ "levelist": {"values": [74]},
17
+ "t": {"values": ["2026-05-04T18:00:00Z"]}
18
+ }
19
+ },
20
+ "ranges": {
21
+ "t": {
22
+ "type": "NdArray",
23
+ "dataType": "float",
24
+ "shape": [1],
25
+ "axisNames": ["t"],
26
+ "values": [285.6]
27
+ }
28
+ }
29
+ }
30
+ ],
31
+ "referencing": [
32
+ {
33
+ "coordinates": ["latitude", "longitude", "levelist"],
34
+ "system": {"type": "GeographicCRS"}
35
+ }
36
+ ],
37
+ "parameters": {
38
+ "t": {
39
+ "type": "Parameter",
40
+ "unit": {"symbol": "K"},
41
+ "observedProperty": {
42
+ "id": "t",
43
+ "label": {"en": "Temperature"}
44
+ }
45
+ }
46
+ }
47
+ }