covjsonkit 0.2.22__tar.gz → 0.2.23__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.
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/PKG-INFO +1 -1
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/encoder/encoder.py +163 -142
- covjsonkit-0.2.23/covjsonkit/version.py +1 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit.egg-info/PKG-INFO +1 -1
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit.egg-info/SOURCES.txt +1 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/pyproject.toml +1 -1
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/conftest.py +61 -5
- covjsonkit-0.2.23/tests/test_encoder_merged_node_parity.py +111 -0
- covjsonkit-0.2.22/covjsonkit/version.py +0 -1
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/.github/ci-hpc-config.yml +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/.github/workflows/ci.yaml +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/.github/workflows/label-public-pr.yml +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/.gitignore +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/.pre-commit-config.yaml +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/ACKNOWLEDGEMENTS.rst +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/LICENSE +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/MANIFEST.in +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/Makefile +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/README.md +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/Coverage.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/CoverageCollection.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/__init__.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/api.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/config.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/data/__init__.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/data/dwd/param.json +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/data/dwd/param_id.json +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/data/dwd/unit.json +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/data/ecmwf/param.json +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/data/ecmwf/param_id.json +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/data/ecmwf/unit.json +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/decoder/BoundingBox.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/decoder/Circle.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/decoder/Frame.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/decoder/Grid.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/decoder/Path.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/decoder/Position.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/decoder/Shapefile.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/decoder/TimeSeries.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/decoder/VerticalProfile.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/decoder/Wkt.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/decoder/__init__.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/decoder/decoder.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/encoder/BoundingBox.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/encoder/Circle.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/encoder/Frame.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/encoder/Grid.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/encoder/Path.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/encoder/Position.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/encoder/Shapefile.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/encoder/TimeSeries.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/encoder/VerticalProfile.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/encoder/Wkt.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/encoder/__init__.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/param_db.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit/utils.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit.egg-info/dependency_links.txt +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit.egg-info/not-zip-safe +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit.egg-info/requires.txt +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/covjsonkit.egg-info/top_level.txt +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/docs/images/ECMWF_logo.svg.png +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/docs/images/Logo_Destination_Earth_Colours.png +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/example_config.json +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/requirements.txt +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/scripts/generate_param_db.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/setup.cfg +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/setup.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/data/test_coverage1.json +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/data/test_coverage2.json +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/data/test_geotiff.json +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/data/test_multipoint_coverage.json +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/data/test_path_coverage.json +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/data/test_timeseries_coverage.json +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/data/test_timeseries_param_t.json +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/data/test_timeseries_xyz_coverage.json +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/data/test_verticalprofile_coverage.json +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/data/test_verticalprofile_param_t.json +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/data/test_verticalprofile_xyz_coverage.json +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/geojsontest.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/requirements_test.txt +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_decoder_bounding_box.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_decoder_circle.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_decoder_frame.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_decoder_grid.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_decoder_position.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_decoder_shapefile.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_decoder_time_series.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_decoder_vertical_profile.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_decoder_wkt.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_encoder_bounding_box.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_encoder_bounding_box_from_polytope.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_encoder_circle_from_polytope.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_encoder_frame.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_encoder_frame_from_polytope.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_encoder_grid.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_encoder_grid_from_polytope.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_encoder_path.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_encoder_path_from_polytope.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_encoder_position_from_polytope.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_encoder_shapefile.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_encoder_shapefile_from_polytope.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_encoder_time_series.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_encoder_time_series_from_polytope.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_encoder_vertical_profile_from_polytope.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_encoder_wkt.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_encoder_wkt_from_polytope.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_geojson.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_geotiff.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_util_merge.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_xarray_multipoint.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_xarray_path.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_xarray_ts.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_xarray_vp.py +0 -0
- {covjsonkit-0.2.22 → covjsonkit-0.2.23}/tox.ini +0 -0
|
@@ -12,6 +12,25 @@ from covjson_pydantic.domain import DomainType
|
|
|
12
12
|
|
|
13
13
|
from covjsonkit.param_db import get_param_ids, get_params, get_units
|
|
14
14
|
|
|
15
|
+
try:
|
|
16
|
+
# Polytope compacts unstructured-grid (e.g. ICON, Lambert LAM) leaves into a single
|
|
17
|
+
# MergedTensorIndexNode holding axes=(lat_axis, lon_axis) and values=(lat, lon).
|
|
18
|
+
from polytope_feature.datacube.tensor_index_tree import MergedTensorIndexNode
|
|
19
|
+
except ImportError: # older polytope without merged nodes
|
|
20
|
+
MergedTensorIndexNode = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def is_merged_node(node) -> bool:
|
|
24
|
+
"""True if ``node`` is a polytope ``MergedTensorIndexNode`` (a compacted lat/lon leaf).
|
|
25
|
+
|
|
26
|
+
Such nodes carry ``axes=(lat_axis, lon_axis)`` and ``values=(lat, lon)`` for a single
|
|
27
|
+
spatial point, and are always leaves. Falls back to duck-typing (``.axes`` present)
|
|
28
|
+
if the polytope import was unavailable.
|
|
29
|
+
"""
|
|
30
|
+
if MergedTensorIndexNode is not None:
|
|
31
|
+
return isinstance(node, MergedTensorIndexNode)
|
|
32
|
+
return hasattr(node, "axes") and getattr(node, "axes", None) is not None
|
|
33
|
+
|
|
15
34
|
|
|
16
35
|
def timedelta_to_step_string(td: timedelta) -> str:
|
|
17
36
|
"""
|
|
@@ -383,32 +402,15 @@ class Encoder(ABC):
|
|
|
383
402
|
for value in tree_values:
|
|
384
403
|
coords[dates]["composite"].append([lat, value])
|
|
385
404
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
handle_non_leaf_node(child)
|
|
389
|
-
result = handle_specific_axes(child)
|
|
390
|
-
if result is not None:
|
|
391
|
-
if child.axis.name == "latitude":
|
|
392
|
-
fields["lat"] = result
|
|
393
|
-
elif child.axis.name == "levelist":
|
|
394
|
-
fields["levels"] = result
|
|
395
|
-
if "l" in fields:
|
|
396
|
-
fields["l"].extend(result)
|
|
397
|
-
elif child.axis.name == "param":
|
|
398
|
-
fields["param"] = result
|
|
399
|
-
elif child.axis.name in [date_key, "time"]:
|
|
400
|
-
fields["dates"].extend(result)
|
|
401
|
-
elif child.axis.name == "number":
|
|
402
|
-
fields["number"] = result
|
|
403
|
-
elif child.axis.name == "step":
|
|
404
|
-
fields["step"] = result
|
|
405
|
-
if "s" in fields:
|
|
406
|
-
fields["s"].extend(result)
|
|
405
|
+
def emit_leaf(lat, lon_values, result):
|
|
406
|
+
"""Emit one spatial leaf: append [lat, lon] composite coords and slice results.
|
|
407
407
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
408
|
+
Shared by the legacy longitude-leaf path (``lat`` from the parent latitude
|
|
409
|
+
node, ``lon_values`` = the leaf's list of longitudes) and the compacted
|
|
410
|
+
``MergedTensorIndexNode`` path (single point: ``lon_values`` = [lon]).
|
|
411
|
+
"""
|
|
412
|
+
lon_values = [float(val) for val in lon_values]
|
|
413
|
+
if all(val is None for val in result):
|
|
412
414
|
fields["dates"] = fields["dates"][:-1]
|
|
413
415
|
for date in fields["dates"]:
|
|
414
416
|
for level in fields["levels"]:
|
|
@@ -419,13 +421,13 @@ class Encoder(ABC):
|
|
|
419
421
|
if key in range_dict:
|
|
420
422
|
del range_dict[key]
|
|
421
423
|
else:
|
|
422
|
-
|
|
423
|
-
level_len = len(
|
|
424
|
+
result = [float(val) if val is not None else val for val in result]
|
|
425
|
+
level_len = len(result) / len(fields["levels"])
|
|
424
426
|
num_len = level_len / len(fields["number"])
|
|
425
427
|
para_len = num_len / len(fields["param"])
|
|
426
428
|
step_len = para_len / len(fields["step"])
|
|
427
429
|
|
|
428
|
-
append_composite_coords(fields["dates"][-1],
|
|
430
|
+
append_composite_coords(fields["dates"][-1], lon_values, lat, coords)
|
|
429
431
|
|
|
430
432
|
for l, level in enumerate(fields["levels"]): # noqa: E741
|
|
431
433
|
for i, num in enumerate(fields["number"]):
|
|
@@ -437,7 +439,37 @@ class Encoder(ABC):
|
|
|
437
439
|
key = create_composite_key(fields["dates"][-1], level, num, para, s)
|
|
438
440
|
if key not in range_dict:
|
|
439
441
|
range_dict[key] = []
|
|
440
|
-
range_dict[key].extend(
|
|
442
|
+
range_dict[key].extend(result[start_index:end_index])
|
|
443
|
+
|
|
444
|
+
if len(tree.children) != 0:
|
|
445
|
+
for child in tree.children:
|
|
446
|
+
# Compacted unstructured leaf: values=(lat, lon), own result. Emit directly.
|
|
447
|
+
if is_merged_node(child):
|
|
448
|
+
emit_leaf(child.values[0], [child.values[1]], child.result)
|
|
449
|
+
continue
|
|
450
|
+
handle_non_leaf_node(child)
|
|
451
|
+
result = handle_specific_axes(child)
|
|
452
|
+
if result is not None:
|
|
453
|
+
if child.axis.name == "latitude":
|
|
454
|
+
fields["lat"] = result
|
|
455
|
+
elif child.axis.name == "levelist":
|
|
456
|
+
fields["levels"] = result
|
|
457
|
+
if "l" in fields:
|
|
458
|
+
fields["l"].extend(result)
|
|
459
|
+
elif child.axis.name == "param":
|
|
460
|
+
fields["param"] = result
|
|
461
|
+
elif child.axis.name in [date_key, "time"]:
|
|
462
|
+
fields["dates"].extend(result)
|
|
463
|
+
elif child.axis.name == "number":
|
|
464
|
+
fields["number"] = result
|
|
465
|
+
elif child.axis.name == "step":
|
|
466
|
+
fields["step"] = result
|
|
467
|
+
if "s" in fields:
|
|
468
|
+
fields["s"].extend(result)
|
|
469
|
+
|
|
470
|
+
self.walk_tree(child, fields, coords, mars_metadata, range_dict, date_key=date_key)
|
|
471
|
+
else:
|
|
472
|
+
emit_leaf(fields["lat"], tree.values, tree.result)
|
|
441
473
|
|
|
442
474
|
def walk_tree_step(self, tree, fields, coords, mars_metadata, range_dict):
|
|
443
475
|
def create_composite_key_step(date, level, num, para):
|
|
@@ -496,34 +528,10 @@ class Encoder(ABC):
|
|
|
496
528
|
for value in tree_values:
|
|
497
529
|
coords[dates]["composite"].append([lat, value])
|
|
498
530
|
|
|
499
|
-
|
|
500
|
-
for
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
if result is not None:
|
|
504
|
-
if child.axis.name == "latitude":
|
|
505
|
-
fields["lat"] = result
|
|
506
|
-
elif child.axis.name == "levelist":
|
|
507
|
-
fields["levels"] = result
|
|
508
|
-
if "l" in fields:
|
|
509
|
-
fields["l"].extend(result)
|
|
510
|
-
elif child.axis.name == "param":
|
|
511
|
-
fields["param"] = result
|
|
512
|
-
elif child.axis.name in ["date"]:
|
|
513
|
-
fields["dates"].extend(result)
|
|
514
|
-
elif child.axis.name == "number":
|
|
515
|
-
fields["number"] = result
|
|
516
|
-
elif child.axis.name == "step":
|
|
517
|
-
fields["step"] = result
|
|
518
|
-
if "s" in fields:
|
|
519
|
-
fields["s"].extend(result)
|
|
520
|
-
elif child.axis.name == "time":
|
|
521
|
-
fields["times"].extend(result)
|
|
522
|
-
|
|
523
|
-
self.walk_tree_step(child, fields, coords, mars_metadata, range_dict)
|
|
524
|
-
else:
|
|
525
|
-
tree.values = [float(val) for val in tree.values]
|
|
526
|
-
if all(val is None for val in tree.result):
|
|
531
|
+
def emit_leaf_step(lat, lon_values, result):
|
|
532
|
+
"""Emit one spatial leaf for the step walker (shared by legacy and merged)."""
|
|
533
|
+
lon_values = [float(val) for val in lon_values]
|
|
534
|
+
if all(val is None for val in result):
|
|
527
535
|
fields["dates"] = fields["dates"][:-1]
|
|
528
536
|
for date in fields["dates"]:
|
|
529
537
|
for level in fields["levels"]:
|
|
@@ -534,61 +542,61 @@ class Encoder(ABC):
|
|
|
534
542
|
if key in range_dict:
|
|
535
543
|
del range_dict[key]
|
|
536
544
|
else:
|
|
537
|
-
|
|
538
|
-
date_len = len(
|
|
545
|
+
result = [float(val) if val is not None else val for val in result]
|
|
546
|
+
date_len = len(result) / len(fields["dates"])
|
|
539
547
|
level_len = date_len / len(fields["levels"])
|
|
540
548
|
para_len = level_len / len(fields["param"])
|
|
541
|
-
# time_len = para_len / len(fields["times"])
|
|
542
|
-
# coords_len = len(tree.values)
|
|
543
549
|
|
|
544
550
|
for date in fields["dates"]:
|
|
545
|
-
append_composite_coords_step(date,
|
|
546
|
-
|
|
547
|
-
for ti, _ in enumerate(fields["times"]):
|
|
548
|
-
for d, date in enumerate(fields["dates"]):
|
|
549
|
-
for l, level in enumerate(fields["levels"]): # noqa: E741
|
|
550
|
-
for i, num in enumerate(fields["number"]):
|
|
551
|
-
for j, para in enumerate(fields["param"]):
|
|
552
|
-
# for k, t in enumerate(fields["times"]):
|
|
553
|
-
# start_index, end_index = calculate_index_bounds_step(
|
|
554
|
-
# level_len, num_len, para_len, time_len, l, i, j, k
|
|
555
|
-
# )
|
|
556
|
-
key = create_composite_key_step(date, level, num, para)
|
|
557
|
-
if key not in range_dict:
|
|
558
|
-
range_dict[key] = []
|
|
559
|
-
# range_dict[key].extend(tree.result[start_index:end_index])
|
|
560
|
-
# print(d, date_len,j, para_len)
|
|
561
|
-
# print(d*date_len+j*para_len)
|
|
562
|
-
# print(int(d*date_len+j*para_len+len(fields["times"])))
|
|
563
|
-
# print(tree.result[int(d*date_len+j*para_len+len)])
|
|
564
|
-
#print(tree.result)
|
|
565
|
-
range_dict[key].append(#tree.result
|
|
566
|
-
tree.result[
|
|
567
|
-
int(d * date_len + j * para_len + ti * time_len) : int(
|
|
568
|
-
d * date_len + j * para_len + ti * time_len + len(tree.values)
|
|
569
|
-
)
|
|
570
|
-
]
|
|
571
|
-
)
|
|
572
|
-
"""
|
|
551
|
+
append_composite_coords_step(date, lon_values, lat, coords)
|
|
552
|
+
|
|
573
553
|
for d, date in enumerate(fields["dates"]):
|
|
574
554
|
for l, level in enumerate(fields["levels"]): # noqa: E741
|
|
575
555
|
for i, num in enumerate(fields["number"]):
|
|
576
556
|
for j, para in enumerate(fields["param"]):
|
|
577
|
-
# for k, t in enumerate(fields["times"]):
|
|
578
|
-
# start_index, end_index = calculate_index_bounds_step(
|
|
579
|
-
# level_len, num_len, para_len, time_len, l, i, j, k
|
|
580
|
-
# )
|
|
581
557
|
key = create_composite_key_step(date, level, num, para)
|
|
582
558
|
if key not in range_dict:
|
|
583
559
|
range_dict[key] = []
|
|
584
|
-
range_dict[key].append(
|
|
585
|
-
|
|
560
|
+
range_dict[key].append(
|
|
561
|
+
result[
|
|
586
562
|
int(d * date_len + l * level_len + j * para_len) : int(
|
|
587
563
|
d * date_len + l * level_len + j * para_len + len(fields["times"])
|
|
588
564
|
)
|
|
589
565
|
]
|
|
590
566
|
)
|
|
591
567
|
|
|
568
|
+
if len(tree.children) != 0:
|
|
569
|
+
for child in tree.children:
|
|
570
|
+
# Compacted unstructured leaf: values=(lat, lon), own result. Emit directly.
|
|
571
|
+
if is_merged_node(child):
|
|
572
|
+
emit_leaf_step(child.values[0], [child.values[1]], child.result)
|
|
573
|
+
continue
|
|
574
|
+
handle_non_leaf_node_step(child)
|
|
575
|
+
result = handle_specific_axes_step(child)
|
|
576
|
+
if result is not None:
|
|
577
|
+
if child.axis.name == "latitude":
|
|
578
|
+
fields["lat"] = result
|
|
579
|
+
elif child.axis.name == "levelist":
|
|
580
|
+
fields["levels"] = result
|
|
581
|
+
if "l" in fields:
|
|
582
|
+
fields["l"].extend(result)
|
|
583
|
+
elif child.axis.name == "param":
|
|
584
|
+
fields["param"] = result
|
|
585
|
+
elif child.axis.name in ["date"]:
|
|
586
|
+
fields["dates"].extend(result)
|
|
587
|
+
elif child.axis.name == "number":
|
|
588
|
+
fields["number"] = result
|
|
589
|
+
elif child.axis.name == "step":
|
|
590
|
+
fields["step"] = result
|
|
591
|
+
if "s" in fields:
|
|
592
|
+
fields["s"].extend(result)
|
|
593
|
+
elif child.axis.name == "time":
|
|
594
|
+
fields["times"].extend(result)
|
|
595
|
+
|
|
596
|
+
self.walk_tree_step(child, fields, coords, mars_metadata, range_dict)
|
|
597
|
+
else:
|
|
598
|
+
emit_leaf_step(fields["lat"], tree.values, tree.result)
|
|
599
|
+
|
|
592
600
|
def walk_tree_month(self, tree, fields, coords, mars_metadata, range_dict, _ctx=None):
|
|
593
601
|
"""Walk the result tree for monthly-mean streams (e.g. clmn).
|
|
594
602
|
|
|
@@ -640,47 +648,13 @@ class Encoder(ABC):
|
|
|
640
648
|
for value in tree_values:
|
|
641
649
|
coords[date_key]["composite"].append([lat, value])
|
|
642
650
|
|
|
643
|
-
|
|
644
|
-
for
|
|
645
|
-
handle_non_leaf_node_month(child)
|
|
646
|
-
result = handle_specific_axes_month(child)
|
|
647
|
-
|
|
648
|
-
# Build a child context that inherits the current year/month
|
|
649
|
-
child_ctx = dict(_ctx)
|
|
651
|
+
def emit_leaf_month(lat, lon_values, result):
|
|
652
|
+
"""Emit one spatial leaf for the month walker (shared by legacy and merged).
|
|
650
653
|
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
fields["levels"] = result
|
|
656
|
-
if "l" in fields:
|
|
657
|
-
fields["l"].extend(result)
|
|
658
|
-
elif child.axis.name == "param":
|
|
659
|
-
fields["param"] = result
|
|
660
|
-
elif child.axis.name == "year":
|
|
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))
|
|
669
|
-
elif child.axis.name == "month":
|
|
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"]
|
|
679
|
-
elif child.axis.name == "number":
|
|
680
|
-
fields["number"] = result
|
|
681
|
-
|
|
682
|
-
self.walk_tree_month(child, fields, coords, mars_metadata, range_dict, _ctx=child_ctx)
|
|
683
|
-
else:
|
|
654
|
+
``lat`` is the latitude for this leaf, ``lon_values`` the leaf's list of
|
|
655
|
+
longitudes (length 1 for a compacted ``MergedTensorIndexNode``), and
|
|
656
|
+
``result`` its flat value array.
|
|
657
|
+
"""
|
|
684
658
|
# Leaf node — ensure all (year, month) combinations from context are registered.
|
|
685
659
|
ctx_years = _ctx.get("years", fields.get("years", []))
|
|
686
660
|
ctx_months = _ctx.get("months", fields.get("months", []))
|
|
@@ -708,8 +682,8 @@ class Encoder(ABC):
|
|
|
708
682
|
else:
|
|
709
683
|
leaf_dates = fields["dates"]
|
|
710
684
|
|
|
711
|
-
|
|
712
|
-
if all(val is None for val in
|
|
685
|
+
lon_values = [float(val) for val in lon_values]
|
|
686
|
+
if all(val is None for val in result):
|
|
713
687
|
# Remove date entries for this leaf that produced no data.
|
|
714
688
|
for key in leaf_dates:
|
|
715
689
|
if key in fields["dates"]:
|
|
@@ -721,20 +695,20 @@ class Encoder(ABC):
|
|
|
721
695
|
if rkey in range_dict:
|
|
722
696
|
del range_dict[rkey]
|
|
723
697
|
else:
|
|
724
|
-
|
|
698
|
+
result = [float(val) if val is not None else val for val in result]
|
|
725
699
|
|
|
726
700
|
n_dates = len(leaf_dates)
|
|
727
701
|
n_levels = len(fields["levels"])
|
|
728
702
|
n_params = len(fields["param"])
|
|
729
703
|
|
|
730
|
-
date_len = len(
|
|
704
|
+
date_len = len(result) / n_dates if n_dates else len(result)
|
|
731
705
|
level_len = date_len / n_levels if n_levels else date_len
|
|
732
706
|
para_len = level_len / n_params if n_params else level_len
|
|
733
707
|
|
|
734
708
|
# Append this leaf's longitude values to composite coords for
|
|
735
709
|
# every date key in scope.
|
|
736
710
|
for date in leaf_dates:
|
|
737
|
-
append_composite_coords_month(date,
|
|
711
|
+
append_composite_coords_month(date, lon_values, lat)
|
|
738
712
|
|
|
739
713
|
for d, date in enumerate(leaf_dates):
|
|
740
714
|
for l, level in enumerate(fields["levels"]): # noqa: E741
|
|
@@ -744,8 +718,55 @@ class Encoder(ABC):
|
|
|
744
718
|
if key not in range_dict:
|
|
745
719
|
range_dict[key] = []
|
|
746
720
|
start = int(d * date_len + l * level_len + j * para_len)
|
|
747
|
-
end = int(start + len(
|
|
748
|
-
range_dict[key].append(
|
|
721
|
+
end = int(start + len(lon_values))
|
|
722
|
+
range_dict[key].append(result[start:end])
|
|
723
|
+
|
|
724
|
+
if len(tree.children) != 0:
|
|
725
|
+
for child in tree.children:
|
|
726
|
+
# Compacted unstructured leaf: values=(lat, lon), own result. Emit directly.
|
|
727
|
+
if is_merged_node(child):
|
|
728
|
+
emit_leaf_month(child.values[0], [child.values[1]], child.result)
|
|
729
|
+
continue
|
|
730
|
+
handle_non_leaf_node_month(child)
|
|
731
|
+
result = handle_specific_axes_month(child)
|
|
732
|
+
|
|
733
|
+
# Build a child context that inherits the current year/month
|
|
734
|
+
child_ctx = dict(_ctx)
|
|
735
|
+
|
|
736
|
+
if result is not None:
|
|
737
|
+
if child.axis.name == "latitude":
|
|
738
|
+
fields["lat"] = result
|
|
739
|
+
elif child.axis.name == "levelist":
|
|
740
|
+
fields["levels"] = result
|
|
741
|
+
if "l" in fields:
|
|
742
|
+
fields["l"].extend(result)
|
|
743
|
+
elif child.axis.name == "param":
|
|
744
|
+
fields["param"] = result
|
|
745
|
+
elif child.axis.name == "year":
|
|
746
|
+
fields["years"] = list(result) if fields.get("years") == [] else fields["years"]
|
|
747
|
+
child_ctx["years"] = result
|
|
748
|
+
child_ctx["_axis_order"] = _ctx.get("_axis_order", []) + ["year"]
|
|
749
|
+
# If month is already fixed in context, register dates now.
|
|
750
|
+
if "months" in _ctx:
|
|
751
|
+
for y in result:
|
|
752
|
+
for m in _ctx["months"]:
|
|
753
|
+
_register_date_key(_year_month_key(y, m))
|
|
754
|
+
elif child.axis.name == "month":
|
|
755
|
+
fields["months"] = list(result) if fields.get("months") == [] else fields["months"]
|
|
756
|
+
child_ctx["months"] = result
|
|
757
|
+
# If year is already fixed in context, register dates now.
|
|
758
|
+
if "years" in _ctx:
|
|
759
|
+
for y in _ctx["years"]:
|
|
760
|
+
for m in result:
|
|
761
|
+
_register_date_key(_year_month_key(y, m))
|
|
762
|
+
# Track that month is the inner axis relative to year
|
|
763
|
+
child_ctx["_axis_order"] = _ctx.get("_axis_order", []) + ["month"]
|
|
764
|
+
elif child.axis.name == "number":
|
|
765
|
+
fields["number"] = result
|
|
766
|
+
|
|
767
|
+
self.walk_tree_month(child, fields, coords, mars_metadata, range_dict, _ctx=child_ctx)
|
|
768
|
+
else:
|
|
769
|
+
emit_leaf_month(fields["lat"], tree.values, tree.result)
|
|
749
770
|
|
|
750
771
|
@abstractmethod
|
|
751
772
|
def add_coverage(self, mars_metadata, coords, values):
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.23"
|
|
@@ -80,6 +80,7 @@ tests/test_encoder_frame.py
|
|
|
80
80
|
tests/test_encoder_frame_from_polytope.py
|
|
81
81
|
tests/test_encoder_grid.py
|
|
82
82
|
tests/test_encoder_grid_from_polytope.py
|
|
83
|
+
tests/test_encoder_merged_node_parity.py
|
|
83
84
|
tests/test_encoder_path.py
|
|
84
85
|
tests/test_encoder_path_from_polytope.py
|
|
85
86
|
tests/test_encoder_position_from_polytope.py
|
|
@@ -2,6 +2,15 @@ import numpy as np
|
|
|
2
2
|
from polytope_feature.datacube.datacube_axis import IntDatacubeAxis
|
|
3
3
|
from polytope_feature.datacube.tensor_index_tree import TensorIndexTree
|
|
4
4
|
|
|
5
|
+
try:
|
|
6
|
+
# Only available on polytope versions that support compacted unstructured
|
|
7
|
+
# (ICON, Lambert LAM) results. Absent on released polytope, in which case
|
|
8
|
+
# the merged-node fixtures/tests below are skipped rather than breaking
|
|
9
|
+
# collection of the whole test suite.
|
|
10
|
+
from polytope_feature.datacube.tensor_index_tree import MergedTensorIndexNode
|
|
11
|
+
except ImportError:
|
|
12
|
+
MergedTensorIndexNode = None
|
|
13
|
+
|
|
5
14
|
# -- Shared constants for reforecast tests --
|
|
6
15
|
|
|
7
16
|
REFORECAST_METADATA_BASE = {
|
|
@@ -61,14 +70,36 @@ def make_point(lat, lon, result):
|
|
|
61
70
|
return lat_n
|
|
62
71
|
|
|
63
72
|
|
|
64
|
-
def
|
|
73
|
+
def make_merged_point(lat, lon, result):
|
|
74
|
+
"""Create a compacted MergedTensorIndexNode for a single spatial point.
|
|
75
|
+
|
|
76
|
+
This is the unstructured-grid equivalent of :func:`make_point`: instead of a
|
|
77
|
+
latitude node with a longitude-leaf child, the (lat, lon) pair is compacted
|
|
78
|
+
into a single leaf node carrying its own ``result``.
|
|
79
|
+
"""
|
|
80
|
+
if MergedTensorIndexNode is None:
|
|
81
|
+
raise RuntimeError(
|
|
82
|
+
"MergedTensorIndexNode is not available in this polytope build; " "merged-node tests should be skipped."
|
|
83
|
+
)
|
|
84
|
+
lat_ax = IntDatacubeAxis()
|
|
85
|
+
lat_ax.name = "latitude"
|
|
86
|
+
lon_ax = IntDatacubeAxis()
|
|
87
|
+
lon_ax.name = "longitude"
|
|
88
|
+
merged = MergedTensorIndexNode(axes=(lat_ax, lon_ax), values=(lat, lon))
|
|
89
|
+
merged.result = [np.float64(r) for r in result]
|
|
90
|
+
return merged
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def forecast_tree(points, param="167", step=(0,), date=np.datetime64("2025-01-01T00:00:00"), point_factory=make_point):
|
|
65
94
|
"""Build a standard forecast TensorIndexTree with the given spatial points.
|
|
66
95
|
|
|
67
96
|
Args:
|
|
68
|
-
points: list of (lat, lon, result_list) tuples, passed to
|
|
97
|
+
points: list of (lat, lon, result_list) tuples, passed to point_factory().
|
|
69
98
|
param: MARS parameter code.
|
|
70
99
|
step: tuple of step values.
|
|
71
100
|
date: forecast date.
|
|
101
|
+
point_factory: callable(lat, lon, result) -> node. Use make_merged_point
|
|
102
|
+
to build a compacted unstructured (MergedTensorIndexNode) tree.
|
|
72
103
|
"""
|
|
73
104
|
tree = chain(
|
|
74
105
|
TensorIndexTree(),
|
|
@@ -84,11 +115,36 @@ def forecast_tree(points, param="167", step=(0,), date=np.datetime64("2025-01-01
|
|
|
84
115
|
)
|
|
85
116
|
parent = tip(tree)
|
|
86
117
|
for lat, lon, result in points:
|
|
87
|
-
parent.add_child(
|
|
118
|
+
parent.add_child(point_factory(lat, lon, result))
|
|
119
|
+
return tree
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def month_tree(points, param="167", years=(2020, 2021), months=(1,), point_factory=make_point):
|
|
123
|
+
"""Build a monthly-mean tree (year/month axes) for the walk_tree_month path.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
points: list of (lat, lon, result_list) tuples, passed to point_factory().
|
|
127
|
+
param: MARS parameter code.
|
|
128
|
+
years: tuple of year values.
|
|
129
|
+
months: tuple of month values.
|
|
130
|
+
point_factory: callable(lat, lon, result) -> node. Use make_merged_point
|
|
131
|
+
to build a compacted unstructured (MergedTensorIndexNode) tree.
|
|
132
|
+
"""
|
|
133
|
+
tree = chain(
|
|
134
|
+
TensorIndexTree(),
|
|
135
|
+
node("class", ("od",)),
|
|
136
|
+
node("levtype", ("sfc",)),
|
|
137
|
+
node("param", (param,)),
|
|
138
|
+
node("year", years),
|
|
139
|
+
node("month", months),
|
|
140
|
+
)
|
|
141
|
+
parent = tip(tree)
|
|
142
|
+
for lat, lon, result in points:
|
|
143
|
+
parent.add_child(point_factory(lat, lon, result))
|
|
88
144
|
return tree
|
|
89
145
|
|
|
90
146
|
|
|
91
|
-
def reforecast_branch(hdate, points, param="167", step=(0,)):
|
|
147
|
+
def reforecast_branch(hdate, points, param="167", step=(0,), point_factory=make_point):
|
|
92
148
|
"""Build a reforecast branch rooted at an hdate node.
|
|
93
149
|
|
|
94
150
|
Attaches spatial points at the leaf. Caller is responsible for
|
|
@@ -106,7 +162,7 @@ def reforecast_branch(hdate, points, param="167", step=(0,)):
|
|
|
106
162
|
)
|
|
107
163
|
parent = tip(branch)
|
|
108
164
|
for lat, lon, result in points:
|
|
109
|
-
parent.add_child(
|
|
165
|
+
parent.add_child(point_factory(lat, lon, result))
|
|
110
166
|
return branch
|
|
111
167
|
|
|
112
168
|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Parity tests for compacted unstructured-grid results.
|
|
2
|
+
|
|
3
|
+
Polytope emits ``MergedTensorIndexNode`` leaves for unstructured grids (ICON,
|
|
4
|
+
Lambert LAM), compacting each (latitude, longitude) point and its result into a
|
|
5
|
+
single leaf instead of a latitude -> longitude-leaf subtree. These tests assert
|
|
6
|
+
that covjsonkit produces byte-identical CoverageJSON whether the source tree
|
|
7
|
+
uses the legacy layout (``make_point``) or the compacted layout
|
|
8
|
+
(``make_merged_point``), exercising all tree walkers.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
import pytest
|
|
13
|
+
from conftest import (
|
|
14
|
+
MergedTensorIndexNode,
|
|
15
|
+
chain,
|
|
16
|
+
forecast_tree,
|
|
17
|
+
make_merged_point,
|
|
18
|
+
make_point,
|
|
19
|
+
month_tree,
|
|
20
|
+
node,
|
|
21
|
+
reforecast_branch,
|
|
22
|
+
reforecast_tree,
|
|
23
|
+
tip,
|
|
24
|
+
)
|
|
25
|
+
from polytope_feature.datacube.tensor_index_tree import TensorIndexTree
|
|
26
|
+
|
|
27
|
+
from covjsonkit.api import Covjsonkit
|
|
28
|
+
|
|
29
|
+
# These tests require a polytope build with compacted unstructured-grid support.
|
|
30
|
+
# On released polytope (no MergedTensorIndexNode) they are skipped entirely.
|
|
31
|
+
pytestmark = pytest.mark.skipif(
|
|
32
|
+
MergedTensorIndexNode is None,
|
|
33
|
+
reason="polytope build lacks MergedTensorIndexNode (compacted unstructured support)",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
TWO_POINTS = [(48.0, 11.0, [264.9]), (50.0, 12.0, [265.1])]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _encode(tree, feature="BoundingBox", reforecast=False):
|
|
40
|
+
api = Covjsonkit().encode("CoverageCollection", feature)
|
|
41
|
+
if reforecast:
|
|
42
|
+
return api.from_polytope_reforecast(tree)
|
|
43
|
+
return api.from_polytope(tree)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _encode_month(tree, feature="BoundingBox"):
|
|
47
|
+
return Covjsonkit().encode("CoverageCollection", feature).from_polytope_month(tree)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TestMergedNodeParity:
|
|
51
|
+
def test_walk_tree_single_date_single_step(self):
|
|
52
|
+
legacy = _encode(forecast_tree(TWO_POINTS, point_factory=make_point))
|
|
53
|
+
merged = _encode(forecast_tree(TWO_POINTS, point_factory=make_merged_point))
|
|
54
|
+
assert merged == legacy
|
|
55
|
+
|
|
56
|
+
def test_walk_tree_multi_step(self):
|
|
57
|
+
points = [(48.0, 11.0, [264.9, 270.1]), (50.0, 12.0, [265.1, 271.3])]
|
|
58
|
+
legacy = _encode(forecast_tree(points, step=(0, 6), point_factory=make_point))
|
|
59
|
+
merged = _encode(forecast_tree(points, step=(0, 6), point_factory=make_merged_point))
|
|
60
|
+
assert merged == legacy
|
|
61
|
+
|
|
62
|
+
def test_walk_tree_two_dates_two_steps(self):
|
|
63
|
+
def build(point_factory):
|
|
64
|
+
tree = chain(TensorIndexTree(), node("class", ("od",)))
|
|
65
|
+
cls = tip(tree)
|
|
66
|
+
for date_val, vals in [
|
|
67
|
+
(np.datetime64("2025-01-01T00:00:00"), [[264.9, 270.1], [265.1, 271.3]]),
|
|
68
|
+
(np.datetime64("2025-01-02T00:00:00"), [[266.0, 272.0], [267.0, 273.0]]),
|
|
69
|
+
]:
|
|
70
|
+
branch = chain(
|
|
71
|
+
node("date", (date_val,)),
|
|
72
|
+
node("domain", ("g",)),
|
|
73
|
+
node("expver", ("0001",)),
|
|
74
|
+
node("levtype", ("sfc",)),
|
|
75
|
+
node("param", ("167",)),
|
|
76
|
+
node("step", (0, 6)),
|
|
77
|
+
node("stream", ("oper",)),
|
|
78
|
+
node("type", ("fc",)),
|
|
79
|
+
)
|
|
80
|
+
fc = tip(branch)
|
|
81
|
+
fc.add_child(point_factory(48.0, 11.0, vals[0]))
|
|
82
|
+
fc.add_child(point_factory(50.0, 12.0, vals[1]))
|
|
83
|
+
cls.add_child(branch)
|
|
84
|
+
return tree
|
|
85
|
+
|
|
86
|
+
assert _encode(build(make_merged_point)) == _encode(build(make_point))
|
|
87
|
+
|
|
88
|
+
def test_reforecast_walker(self):
|
|
89
|
+
def build(point_factory):
|
|
90
|
+
return reforecast_tree(
|
|
91
|
+
[
|
|
92
|
+
reforecast_branch(np.datetime64("2025-07-14T06:00:00"), TWO_POINTS, point_factory=point_factory),
|
|
93
|
+
reforecast_branch(
|
|
94
|
+
np.datetime64("2025-07-15T06:00:00"),
|
|
95
|
+
[(48.0, 11.0, [266.0]), (50.0, 12.0, [267.0])],
|
|
96
|
+
point_factory=point_factory,
|
|
97
|
+
),
|
|
98
|
+
]
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
legacy = _encode(build(make_point), reforecast=True)
|
|
102
|
+
merged = _encode(build(make_merged_point), reforecast=True)
|
|
103
|
+
assert merged == legacy
|
|
104
|
+
|
|
105
|
+
def test_walk_tree_month(self):
|
|
106
|
+
# Monthly-mean (year/month axes) path: two years x one month, two points.
|
|
107
|
+
points = [(48.0, 11.0, [264.9, 265.9]), (50.0, 12.0, [266.1, 267.1])]
|
|
108
|
+
legacy = _encode_month(month_tree(points, point_factory=make_point))
|
|
109
|
+
merged = _encode_month(month_tree(points, point_factory=make_merged_point))
|
|
110
|
+
assert len(legacy["coverages"]) == 2
|
|
111
|
+
assert merged == legacy
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.22"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{covjsonkit-0.2.22 → covjsonkit-0.2.23}/tests/test_encoder_vertical_profile_from_polytope.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|