From d775354679b1a69a93df1b16fb9d4653d78ceb3e Mon Sep 17 00:00:00 2001 From: Nikeeth Ramanathan Date: Mon, 9 Feb 2026 14:17:53 +1100 Subject: [PATCH 01/16] [skip ci] squash and rebaase initial persistence boiler plate --- .../bundled_models/persistence/.gitattributes | 2 + .../bundled_models/persistence/.gitignore | 4 + packages/bundled_models/persistence/README.md | 45 + packages/bundled_models/persistence/pixi.lock | 3369 +++++++++++++++++ .../bundled_models/persistence/pyproject.toml | 91 + .../persistence/src/persistence/__init__.py | 0 .../src/persistence/config/dask.py | 41 + .../src/persistence/interface/__init__.py | 0 .../src/persistence/interface/_backend.py | 65 + .../src/persistence/interface/_chunker.py | 415 ++ .../src/persistence/interface/_compute.py | 272 ++ .../src/persistence/interface/_interface.py | 6 + .../src/persistence/interface/_metadata.py | 85 + .../src/persistence/interface/_method.py | 53 + .../src/persistence/interface/types.py | 194 + .../src/persistence/methods/__init__.py | 0 .../src/persistence/methods/_impute.py | 31 + .../src/persistence/methods/_median.py | 47 + .../src/persistence/methods/_mostrecent.py | 0 .../src/persistence/persistence_impl.py | 266 ++ .../src/persistence/registered_model.py | 59 + .../tests/interface/test__chunker.py | 138 + .../tests/interface/test__compute.py | 198 + .../persistence/tests/test__daskconfig.py | 137 + .../persistence/tests/test__datatypes.py | 140 + .../persistence/tests/test__impute.py | 41 + .../persistence/tests/test__interface.py | 159 + .../persistence/tests/test__median.py | 51 + 28 files changed, 5909 insertions(+) create mode 100644 packages/bundled_models/persistence/.gitattributes create mode 100644 packages/bundled_models/persistence/.gitignore create mode 100644 packages/bundled_models/persistence/README.md create mode 100644 packages/bundled_models/persistence/pixi.lock create mode 100644 packages/bundled_models/persistence/pyproject.toml create mode 100644 packages/bundled_models/persistence/src/persistence/__init__.py create mode 100644 packages/bundled_models/persistence/src/persistence/config/dask.py create mode 100644 packages/bundled_models/persistence/src/persistence/interface/__init__.py create mode 100644 packages/bundled_models/persistence/src/persistence/interface/_backend.py create mode 100644 packages/bundled_models/persistence/src/persistence/interface/_chunker.py create mode 100644 packages/bundled_models/persistence/src/persistence/interface/_compute.py create mode 100644 packages/bundled_models/persistence/src/persistence/interface/_interface.py create mode 100644 packages/bundled_models/persistence/src/persistence/interface/_metadata.py create mode 100644 packages/bundled_models/persistence/src/persistence/interface/_method.py create mode 100644 packages/bundled_models/persistence/src/persistence/interface/types.py create mode 100644 packages/bundled_models/persistence/src/persistence/methods/__init__.py create mode 100644 packages/bundled_models/persistence/src/persistence/methods/_impute.py create mode 100644 packages/bundled_models/persistence/src/persistence/methods/_median.py create mode 100644 packages/bundled_models/persistence/src/persistence/methods/_mostrecent.py create mode 100644 packages/bundled_models/persistence/src/persistence/persistence_impl.py create mode 100644 packages/bundled_models/persistence/src/persistence/registered_model.py create mode 100644 packages/bundled_models/persistence/tests/interface/test__chunker.py create mode 100644 packages/bundled_models/persistence/tests/interface/test__compute.py create mode 100644 packages/bundled_models/persistence/tests/test__daskconfig.py create mode 100644 packages/bundled_models/persistence/tests/test__datatypes.py create mode 100644 packages/bundled_models/persistence/tests/test__impute.py create mode 100644 packages/bundled_models/persistence/tests/test__interface.py create mode 100644 packages/bundled_models/persistence/tests/test__median.py diff --git a/packages/bundled_models/persistence/.gitattributes b/packages/bundled_models/persistence/.gitattributes new file mode 100644 index 00000000..997504b4 --- /dev/null +++ b/packages/bundled_models/persistence/.gitattributes @@ -0,0 +1,2 @@ +# SCM syntax highlighting & preventing 3-way merges +pixi.lock merge=binary linguist-language=YAML linguist-generated=true -diff diff --git a/packages/bundled_models/persistence/.gitignore b/packages/bundled_models/persistence/.gitignore new file mode 100644 index 00000000..75bc4918 --- /dev/null +++ b/packages/bundled_models/persistence/.gitignore @@ -0,0 +1,4 @@ +# pixi environments +.pixi/* +!.pixi/config.toml +report.xml diff --git a/packages/bundled_models/persistence/README.md b/packages/bundled_models/persistence/README.md new file mode 100644 index 00000000..b61c4a03 --- /dev/null +++ b/packages/bundled_models/persistence/README.md @@ -0,0 +1,45 @@ +# Persistence Model for use with the PyEarthTools Package + +**TODO: description** + +## Installation + +Clone the repository, then run +```shell +pip install -e . +``` + +## Training + +No training is required for this model. It computes persistence on-the-fly using historical data loaded via the PET pipeline. + +## Predictions / Inference + +You can generate persistence values out of the box using the `pet predict` command line API, or by using a Jupyter Notebook as demonstrated in the tutorial gallery. + +```shell +pet predict +``` + +and `Development/Persistence` should be visible. + +If so, you can now run some inference. + +```shell +pet predict --model Development/Persistence +``` + +When running the command, it will prompt for other required arguments. + +**TODO: description of required arguments** + + +#### Example + +```shell +pet predict --model Development/Persistence # TODO +``` + +## Acknowledgments + +Not applicable. Heuristically developed. diff --git a/packages/bundled_models/persistence/pixi.lock b/packages/bundled_models/persistence/pixi.lock new file mode 100644 index 00000000..6fad0157 --- /dev/null +++ b/packages/bundled_models/persistence/pixi.lock @@ -0,0 +1,3369 @@ +version: 6 +environments: + dask: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + options: + pypi-prerelease-mode: if-necessary-or-explicit + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.9.3-hef928c7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.9.13-h2c9d079_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.12.6-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.3.1-h8b1a151_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.5.7-h28f887f_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.10.7-ha8fc4e3_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.23.3-hdaf4b65_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.13.3-hc63082f_11.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.11.3-h06ab39a_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.2.4-h8b1a151_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.2.7-h8b1a151_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.35.4-h8824e59_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.606-h20b40b1_10.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.16.2-h206d751_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.13.3-hed0cdb0_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.16.0-hdd73cc9_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.12.0-ha7a2c86_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.14.0-h52c5a47_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.3.0-py313h18e8e13_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py313hf159716_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.1-pyh8f84b5b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cytoolz-1.1.0-py313h07c4f96_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dask-core-2026.1.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/distributed-2026.1.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fsspec-2026.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gflags-2.2.2-h5888daf_1005.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hbabe93e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.2-h33c6efd_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260107.1-cxx17_h7b12aa8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-23.0.0-h2603568_3_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-23.0.0-h635bf11_3_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-compute-23.0.0-h53684a4_3_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-23.0.0-h635bf11_3_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-23.0.0-hb4dd7c2_3_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-5_h4a7cf45_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-5_h0358290_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.18.0-hcf29cc6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.39.0-h9d11ab5_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-2.39.0-hdbdcf42_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.78.0-h1d1128b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-5_h47877c9_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.2-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-1.21.0-h9692893_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-headers-1.21.0-ha770c72_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libparquet-23.0.0-h7376487_3_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-6.33.5-h2b00c02_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2025.11.05-h0dc7533_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.2-hf4e2dac_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.22.0-h454ac66_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.11.3-hfe17d71_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.3-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.1-hca6bf5a_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.1-he237659_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py313h3dea7bd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.1.2-py313h7037e92_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h54a6638_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.4.2-py313hf6604e3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.2.2-hbb90d81_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.0-py313hbfd7664_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/prometheus-cpp-1.3.0-ha5d0236_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py313h54dd161_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-23.0.0-py313h78bf25f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-23.0.0-py313h98bfbea_0_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.12-hc97d973_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py313h3dea7bd_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/re2-2025.11.05-h5301d42_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.6.2-he8a4886_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tblib-3.2.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.3-py313h07c4f96_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zict-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - pypi: https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz + - pypi: https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/35/a8/365059bbcd4572cbc41de17fd5b682be5868b218c3c5479071865cab9078/entrypoints-0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/e4/fac19dc34cb686c96011388b813ff7b858a70681e5ce6ce7698e5021b0f4/geopandas-1.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/50/e0edd38dcd63fb26a8547f13d28f7a008bc4a3fd4eb4ff030673f22ad41a/hydra_core-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/cf/be4e93afbfa0def2cd6fac9302071db0bd6d0617999ecbf53f92b9398de3/multiurl-0.3.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/f8/f47b90fbeaf36e112b1a93fc313d5f0bc9f0051ae8be734173787a00271a/pyearthtools_data-0.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/f8/beda8582d430075031ac8835aced207d7bc639469451c932fdf1c0b2ed5c/pyearthtools_pipeline-0.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/06/7ed1c4fad0195d7700b77df09dae83ce6658fa6e2d5bb0c92bec79d766d3/pyearthtools_training-0.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cf/fc/c774d872abe5ae0c4381c5cb1ed61240e682c44ed019f807e18be26a7882/pyearthtools_utils-0.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/45/1cb45ccac7c5f728a363d17a145443ed1f66962d3224b8e1166a4fd7bae1/pyearthtools_zoo-0.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/35/b874f79d03e9f900012cf609f7fff97b77164f2e14ee5aac282f8a999c1b/pyogrio-0.12.1-cp313-cp313-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f8/85/c2b1706e51942de19076eff082f8495e57d5151364e78b5bef4af4a1d94a/pyproj-3.7.2-cp313-cp313-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/63/1e/12fbf2a3bb240161651c94bb5cdd0eae5d4e8cc6eaeceb74ab07b12a753d/scipy-1.17.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl + - pypi: ./ + default: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + options: + pypi-prerelease-mode: if-necessary-or-explicit + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.2-h33c6efd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-5_h4a7cf45_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-5_h0358290_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-5_h47877c9_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.2-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.2-hf4e2dac_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.3-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.4.2-py313hf6604e3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.0-py313hbfd7664_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.12-hc97d973_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - pypi: https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz + - pypi: https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/35/a8/365059bbcd4572cbc41de17fd5b682be5868b218c3c5479071865cab9078/entrypoints-0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/e4/fac19dc34cb686c96011388b813ff7b858a70681e5ce6ce7698e5021b0f4/geopandas-1.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/50/e0edd38dcd63fb26a8547f13d28f7a008bc4a3fd4eb4ff030673f22ad41a/hydra_core-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/cf/be4e93afbfa0def2cd6fac9302071db0bd6d0617999ecbf53f92b9398de3/multiurl-0.3.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/f8/f47b90fbeaf36e112b1a93fc313d5f0bc9f0051ae8be734173787a00271a/pyearthtools_data-0.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/f8/beda8582d430075031ac8835aced207d7bc639469451c932fdf1c0b2ed5c/pyearthtools_pipeline-0.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/06/7ed1c4fad0195d7700b77df09dae83ce6658fa6e2d5bb0c92bec79d766d3/pyearthtools_training-0.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cf/fc/c774d872abe5ae0c4381c5cb1ed61240e682c44ed019f807e18be26a7882/pyearthtools_utils-0.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/45/1cb45ccac7c5f728a363d17a145443ed1f66962d3224b8e1166a4fd7bae1/pyearthtools_zoo-0.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/35/b874f79d03e9f900012cf609f7fff97b77164f2e14ee5aac282f8a999c1b/pyogrio-0.12.1-cp313-cp313-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f8/85/c2b1706e51942de19076eff082f8495e57d5151364e78b5bef4af4a1d94a/pyproj-3.7.2-cp313-cp313-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/63/1e/12fbf2a3bb240161651c94bb5cdd0eae5d4e8cc6eaeceb74ab07b12a753d/scipy-1.17.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl + - pypi: ./ + dev: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + options: + pypi-prerelease-mode: if-necessary-or-explicit + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.9.3-hef928c7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.9.13-h2c9d079_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.12.6-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.3.1-h8b1a151_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.5.7-h28f887f_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.10.7-ha8fc4e3_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.23.3-hdaf4b65_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.13.3-hc63082f_11.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.11.3-h06ab39a_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.2.4-h8b1a151_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.2.7-h8b1a151_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.35.4-h8824e59_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.606-h20b40b1_10.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.16.2-h206d751_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.13.3-hed0cdb0_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.16.0-hdd73cc9_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.12.0-ha7a2c86_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.14.0-h52c5a47_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.3.0-py313h18e8e13_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py313hf159716_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.1-pyh8f84b5b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.13.4-py313h3dea7bd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cytoolz-1.1.0-py313h07c4f96_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dask-core-2026.1.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/distributed-2026.1.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fsspec-2026.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gflags-2.2.2-h5888daf_1005.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hbabe93e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.2-h33c6efd_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.10.0-pyh53cf698_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260107.1-cxx17_h7b12aa8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-23.0.0-h2603568_3_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-23.0.0-h635bf11_3_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-compute-23.0.0-h53684a4_3_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-23.0.0-h635bf11_3_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-23.0.0-hb4dd7c2_3_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-5_h4a7cf45_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-5_h0358290_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.18.0-hcf29cc6_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.39.0-h9d11ab5_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-2.39.0-hdbdcf42_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.78.0-h1d1128b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-5_h47877c9_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.2-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-1.21.0-h9692893_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-headers-1.21.0-ha770c72_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libparquet-23.0.0-h7376487_3_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-6.33.5-h2b00c02_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2025.11.05-h0dc7533_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.2-hf4e2dac_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.22.0-h454ac66_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.11.3-hfe17d71_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.3-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.1-hca6bf5a_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.1-he237659_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py313h3dea7bd_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.1.2-py313h7037e92_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h54a6638_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.4.2-py313hf6604e3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.2.2-hbb90d81_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.0-py313hbfd7664_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.6-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/prometheus-cpp-1.3.0-ha5d0236_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py313h54dd161_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-23.0.0-py313h78bf25f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-23.0.0-py313h98bfbea_0_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.0.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.12-hc97d973_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py313h3dea7bd_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/re2-2025.11.05-h5301d42_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.15.0-h40fa522_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.6.2-he8a4886_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tblib-3.2.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.3-py313h07c4f96_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.1.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zict-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - pypi: https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz + - pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/35/a8/365059bbcd4572cbc41de17fd5b682be5868b218c3c5479071865cab9078/entrypoints-0.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/e4/fac19dc34cb686c96011388b813ff7b858a70681e5ce6ce7698e5021b0f4/geopandas-1.1.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c6/50/e0edd38dcd63fb26a8547f13d28f7a008bc4a3fd4eb4ff030673f22ad41a/hydra_core-1.3.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/93/cf/be4e93afbfa0def2cd6fac9302071db0bd6d0617999ecbf53f92b9398de3/multiurl-0.3.7-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/b3/f8/f47b90fbeaf36e112b1a93fc313d5f0bc9f0051ae8be734173787a00271a/pyearthtools_data-0.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f2/f8/beda8582d430075031ac8835aced207d7bc639469451c932fdf1c0b2ed5c/pyearthtools_pipeline-0.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/06/7ed1c4fad0195d7700b77df09dae83ce6658fa6e2d5bb0c92bec79d766d3/pyearthtools_training-0.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cf/fc/c774d872abe5ae0c4381c5cb1ed61240e682c44ed019f807e18be26a7882/pyearthtools_utils-0.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a4/45/1cb45ccac7c5f728a363d17a145443ed1f66962d3224b8e1166a4fd7bae1/pyearthtools_zoo-0.5.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/46/35/b874f79d03e9f900012cf609f7fff97b77164f2e14ee5aac282f8a999c1b/pyogrio-0.12.1-cp313-cp313-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f8/85/c2b1706e51942de19076eff082f8495e57d5151364e78b5bef4af4a1d94a/pyproj-3.7.2-cp313-cp313-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/63/1e/12fbf2a3bb240161651c94bb5cdd0eae5d4e8cc6eaeceb74ab07b12a753d/scipy-1.17.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl + - pypi: ./ +packages: +- conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 + md5: d7c89558ba9fa0495403155b64376d81 + license: None + purls: [] + size: 2562 + timestamp: 1578324546067 +- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + build_number: 16 + sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22 + md5: 73aaf86a425cc6e73fcf236a5a46396d + depends: + - _libgcc_mutex 0.1 conda_forge + - libgomp >=7.5.0 + constrains: + - openmp_impl 9999 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 23621 + timestamp: 1650670423406 +- pypi: https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz + name: antlr4-python3-runtime + version: 4.9.3 + sha256: f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b + requires_dist: + - typing ; python_full_version < '3.5' +- pypi: https://files.pythonhosted.org/packages/d2/39/e7eaf1799466a4aef85b6a4fe7bd175ad2b1c6345066aa33f1f58d4b18d0/asttokens-3.0.1-py3-none-any.whl + name: asttokens + version: 3.0.1 + sha256: 15a3ebc0f43c2d0a50eeafea25e19046c68398e487b9f1f5b517f7c0f40f976a + requires_dist: + - astroid>=2,<5 ; extra == 'astroid' + - astroid>=2,<5 ; extra == 'test' + - pytest<9.0 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-xdist ; extra == 'test' + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + sha256: ee4da0f3fe9d59439798ee399ef3e482791e48784873d546e706d0935f9ff010 + md5: 9673a61a297b00016442e022d689faa6 + depends: + - python >=3.10 + constrains: + - astroid >=2,<5 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/asttokens?source=hash-mapping + size: 28797 + timestamp: 1763410017955 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.9.3-hef928c7_0.conda + sha256: d9c5babed03371448bb0dc91a1573c80d278d1222a3b0accef079ed112e584f9 + md5: bdd464b33f6540ed70845b946c11a7b8 + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - aws-c-http >=0.10.7,<0.10.8.0a0 + - aws-c-sdkutils >=0.2.4,<0.2.5.0a0 + - aws-c-cal >=0.9.13,<0.9.14.0a0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 133443 + timestamp: 1764765235190 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.9.13-h2c9d079_1.conda + sha256: f21d648349a318f4ae457ea5403d542ba6c0e0343b8642038523dd612b2a5064 + md5: 3c3d02681058c3d206b562b2e3bc337f + depends: + - __glibc >=2.17,<3.0.a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - libgcc >=14 + - openssl >=3.5.4,<4.0a0 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 56230 + timestamp: 1764593147526 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.12.6-hb03c661_0.conda + sha256: 926a5b9de0a586e88669d81de717c8dd3218c51ce55658e8a16af7e7fe87c833 + md5: e36ad70a7e0b48f091ed6902f04c23b8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 239605 + timestamp: 1763585595898 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.3.1-h8b1a151_9.conda + sha256: 96edccb326b8c653c8eb95a356e01d4aba159da1a97999577b7dd74461b040b4 + md5: f7ec84186dfe7a9e3a9f9e5a4d023e75 + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 22272 + timestamp: 1764593718823 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.5.7-h28f887f_1.conda + sha256: a5b151db1c8373b6ca2dacea65bc8bda02791a43685eebfa4ea987bb1a758ca9 + md5: 7b8e3f846353b75db163ad93248e5f9d + depends: + - libgcc >=14 + - libstdcxx >=14 + - __glibc >=2.17,<3.0.a0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - aws-checksums >=0.2.7,<0.2.8.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 58806 + timestamp: 1764675439822 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.10.7-ha8fc4e3_5.conda + sha256: 5527224d6e0813e37426557d38cb04fed3753d6b1e544026cfbe2654f5e556be + md5: 3028f20dacafc00b22b88b324c8956cc + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - aws-c-cal >=0.9.13,<0.9.14.0a0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + - aws-c-compression >=0.3.1,<0.3.2.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 224580 + timestamp: 1764675497060 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.23.3-hdaf4b65_5.conda + sha256: 07d7f2a4493ada676084c3f4313da1fab586cf0a7302572c5d8dde6606113bf4 + md5: 132e8f8f40f0ffc0bbde12bb4e8dd1a1 + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - s2n >=1.6.2,<1.6.3.0a0 + - aws-c-cal >=0.9.13,<0.9.14.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 181361 + timestamp: 1765168239856 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.13.3-hc63082f_11.conda + sha256: fb102b0346a1f5c4f3bb680ec863c529b0333fa4119d78768c3e8a5d1cc2c812 + md5: 6a653aefdc5d83a4f959869d1759e6e3 + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + - aws-c-http >=0.10.7,<0.10.8.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 216454 + timestamp: 1764681745427 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.11.3-h06ab39a_1.conda + sha256: 8de2292329dce2fd512413d83988584d616582442a07990f67670f9bc793a98b + md5: 3689a4290319587e3b54a4f9e68f70c8 + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - openssl >=3.5.4,<4.0a0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + - aws-c-http >=0.10.7,<0.10.8.0a0 + - aws-c-auth >=0.9.3,<0.9.4.0a0 + - aws-checksums >=0.2.7,<0.2.8.0a0 + - aws-c-cal >=0.9.13,<0.9.14.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 151382 + timestamp: 1765174166541 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.2.4-h8b1a151_4.conda + sha256: 9d62c5029f6f8219368a8665f0a549da572dc777f52413b7d75609cacdbc02cc + md5: c7e3e08b7b1b285524ab9d74162ce40b + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 59383 + timestamp: 1764610113765 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.2.7-h8b1a151_5.conda + sha256: a8693d2e06903a09e98fe724ed5ec32e7cd1b25c405d754f0ab7efb299046f19 + md5: 68da5b56dde41e172b7b24f071c4b392 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - aws-c-common >=0.12.6,<0.12.7.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 76915 + timestamp: 1764593731486 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.35.4-h8824e59_0.conda + sha256: 524fc8aa2645e5701308b865bf5c523257feabc6dfa7000cb8207ccfbb1452a1 + md5: 113b9d9913280474c0868b0e290c0326 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - aws-c-event-stream >=0.5.7,<0.5.8.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - aws-c-cal >=0.9.13,<0.9.14.0a0 + - aws-c-sdkutils >=0.2.4,<0.2.5.0a0 + - aws-c-io >=0.23.3,<0.23.4.0a0 + - aws-c-auth >=0.9.3,<0.9.4.0a0 + - aws-c-http >=0.10.7,<0.10.8.0a0 + - aws-c-mqtt >=0.13.3,<0.13.4.0a0 + - aws-c-s3 >=0.11.3,<0.11.4.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 408804 + timestamp: 1765200263609 +- conda: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.606-h20b40b1_10.conda + sha256: e0d81b7dd6d054d457a1c54d17733d430d96dc5ca9b2ca69a72eb41c3fc8c9bf + md5: 937d1d4c233adc6eeb2ac3d6e9a73e53 + depends: + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libcurl >=8.17.0,<9.0a0 + - aws-c-common >=0.12.6,<0.12.7.0a0 + - aws-crt-cpp >=0.35.4,<0.35.5.0a0 + - libzlib >=1.3.1,<2.0a0 + - aws-c-event-stream >=0.5.7,<0.5.8.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 3472674 + timestamp: 1765257107074 +- conda: https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.16.2-h206d751_0.conda + sha256: 321d1070905e467b6bc6f5067b97c1868d7345c272add82b82e08a0224e326f0 + md5: 5492abf806c45298ae642831c670bba0 + depends: + - __glibc >=2.17,<3.0.a0 + - libcurl >=8.18.0,<9.0a0 + - libgcc >=14 + - libstdcxx >=14 + - openssl >=3.5.4,<4.0a0 + license: MIT + license_family: MIT + purls: [] + size: 348729 + timestamp: 1768837519361 +- conda: https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.13.3-hed0cdb0_1.conda + sha256: 2beb6ae8406f946b8963a67e72fe74453e1411c5ae7e992978340de6c512d13c + md5: 68bfb556bdf56d56e9f38da696e752ca + depends: + - __glibc >=2.17,<3.0.a0 + - azure-core-cpp >=1.16.2,<1.16.3.0a0 + - libgcc >=14 + - libstdcxx >=14 + - openssl >=3.5.5,<4.0a0 + license: MIT + license_family: MIT + purls: [] + size: 250511 + timestamp: 1770344967948 +- conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.16.0-hdd73cc9_1.conda + sha256: cef75b91bdd5a65c560b501df78905437cc2090a64b4c5ecd7da9e08e9e9af7c + md5: 939d9ce324e51961c7c4c0046733dbb7 + depends: + - __glibc >=2.17,<3.0.a0 + - azure-core-cpp >=1.16.2,<1.16.3.0a0 + - azure-storage-common-cpp >=12.12.0,<12.12.1.0a0 + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT + purls: [] + size: 579825 + timestamp: 1770321459546 +- conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.12.0-ha7a2c86_1.conda + sha256: ef7d1cae36910b21385d0816f8524a84dee1513e0306927e41a6bd32b5b9a0d0 + md5: 6400f73fe5ebe19fe7aca3616f1f1de7 + depends: + - __glibc >=2.17,<3.0.a0 + - azure-core-cpp >=1.16.2,<1.16.3.0a0 + - libgcc >=14 + - libstdcxx >=14 + - libxml2 + - libxml2-16 >=2.14.6 + - openssl >=3.5.5,<4.0a0 + license: MIT + license_family: MIT + purls: [] + size: 150405 + timestamp: 1770240307002 +- conda: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.14.0-h52c5a47_1.conda + sha256: 55aa8ad5217d358e0ccf4a715bd1f9bafef3cd1c2ea4021f0e916f174c20f8e3 + md5: 6d10339800840562b7dad7775f5d2c16 + depends: + - __glibc >=2.17,<3.0.a0 + - azure-core-cpp >=1.16.2,<1.16.3.0a0 + - azure-storage-blobs-cpp >=12.16.0,<12.16.1.0a0 + - azure-storage-common-cpp >=12.12.0,<12.12.1.0a0 + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT + purls: [] + size: 302524 + timestamp: 1770384269834 +- conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.3.0-py313h18e8e13_0.conda + sha256: 9552afbec37c4d8d0e83a5c4c6b3c7f4b8785f935094ce3881e0a249045909ce + md5: d9e90792551a527200637e23a915dd79 + depends: + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.13.* *_cp313 + - zstd >=1.5.7,<1.6.0a0 + license: BSD-3-Clause AND MIT AND EPL-2.0 + purls: + - pkg:pypi/backports-zstd?source=hash-mapping + size: 240943 + timestamp: 1767044981366 +- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py313hf159716_1.conda + sha256: dadec2879492adede0a9af0191203f9b023f788c18efd45ecac676d424c458ae + md5: 6c4d3597cf43f3439a51b2b13e29a4ba + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + constrains: + - libbrotlicommon 1.2.0 hb03c661_1 + license: MIT + license_family: MIT + purls: + - pkg:pypi/brotli?source=hash-mapping + size: 367721 + timestamp: 1764017371123 +- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda + sha256: c30daba32ddebbb7ded490f0e371eae90f51e72db620554089103b4a6934b0d5 + md5: 51a19bba1b8ebfb60df25cde030b7ebc + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 260341 + timestamp: 1757437258798 +- conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda + sha256: cc9accf72fa028d31c2a038460787751127317dcfa991f8d1f1babf216bb454e + md5: 920bb03579f15389b9e512095ad995b7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 207882 + timestamp: 1765214722852 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + sha256: b5974ec9b50e3c514a382335efa81ed02b05906849827a34061c496f4defa0b2 + md5: bddacf101bb4dd0e51811cb69c7790e2 + depends: + - __unix + license: ISC + purls: [] + size: 146519 + timestamp: 1767500828366 +- pypi: https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl + name: certifi + version: 2026.1.4 + sha256: 9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: charset-normalizer + version: 3.4.4 + sha256: a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl + name: click + version: 8.3.1 + sha256: 981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6 + requires_dist: + - colorama ; sys_platform == 'win32' + requires_python: '>=3.10' +- conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.1-pyh8f84b5b_1.conda + sha256: 38cfe1ee75b21a8361c8824f5544c3866f303af1762693a178266d7f198e8715 + md5: ea8a6c3256897cc31263de9f455e25d9 + depends: + - python >=3.10 + - __unix + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/click?source=hash-mapping + size: 97676 + timestamp: 1764518652276 +- conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda + sha256: 4c287c2721d8a34c94928be8fe0e9a85754e90189dd4384a31b1806856b50a67 + md5: 61b8078a0905b12529abc622406cb62c + depends: + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/cloudpickle?source=compressed-mapping + size: 27353 + timestamp: 1765303462831 +- conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + sha256: ab29d57dc70786c1269633ba3dff20288b81664d3ff8d21af995742e2bb03287 + md5: 962b9857ee8e7018c22f2776ffa0b2d7 + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/colorama?source=hash-mapping + size: 27011 + timestamp: 1733218222191 +- conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.13.4-py313h3dea7bd_0.conda + sha256: 5b88b351c6a61ac25ed02e23cd37b25cc90e071f5cdfbc375b656356fb04ca5c + md5: 77e1fc7133e03ccd62070f2405c82ea9 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - tomli + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/coverage?source=hash-mapping + size: 394748 + timestamp: 1770720450191 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cytoolz-1.1.0-py313h07c4f96_1.conda + sha256: a8ffc7cf31a698a57a46bf7977185ed1e644c5e35d4e166d8f260dca93af6ffb + md5: bcca9afd203fe05d9582249ac12762da + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - toolz >=0.10.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/cytoolz?source=hash-mapping + size: 590435 + timestamp: 1760905824293 +- conda: https://conda.anaconda.org/conda-forge/noarch/dask-core-2026.1.2-pyhcf101f3_0.conda + sha256: c8500be32e2c75b10fd7a0664b0e5abc956dece18a54774a53f357aeabe9e1b6 + md5: b20e7ce9afd59036ab194f3d1e27edf5 + depends: + - python >=3.10 + - click >=8.1 + - cloudpickle >=3.0.0 + - fsspec >=2021.9.0 + - packaging >=20.0 + - partd >=1.4.0 + - pyyaml >=5.3.1 + - toolz >=0.12.0 + - importlib-metadata >=4.13.0 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/dask?source=hash-mapping + size: 1063599 + timestamp: 1769829714443 +- pypi: https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl + name: decorator + version: 5.2.1 + sha256: d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + sha256: c17c6b9937c08ad63cb20a26f403a3234088e57d4455600974a0ce865cb14017 + md5: 9ce473d1d1be1cc3810856a48b3fab32 + depends: + - python >=3.9 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/decorator?source=hash-mapping + size: 14129 + timestamp: 1740385067843 +- conda: https://conda.anaconda.org/conda-forge/noarch/distributed-2026.1.2-pyhcf101f3_0.conda + sha256: 1cbc2ffaef515c43f37d4684942850e1184956a89b1c0651bb656c81bc11aaa1 + md5: 1eac93a6257796dd348d366a85f7f283 + depends: + - python >=3.10 + - click >=8.0 + - cloudpickle >=3.0.0 + - cytoolz >=0.12.0 + - dask-core >=2026.1.2,<2026.1.3.0a0 + - jinja2 >=2.10.3 + - locket >=1.0.0 + - msgpack-python >=1.0.2 + - packaging >=20.0 + - psutil >=5.8.0 + - pyyaml >=5.4.1 + - sortedcontainers >=2.0.5 + - tblib >=1.6.0 + - toolz >=0.12.0 + - tornado >=6.2.0 + - urllib3 >=1.26.5 + - zict >=3.0.0 + - python + constrains: + - openssl !=1.1.1e + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/distributed?source=hash-mapping + size: 844862 + timestamp: 1769888496327 +- pypi: https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl + name: einops + version: 0.8.2 + sha256: 54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/35/a8/365059bbcd4572cbc41de17fd5b682be5868b218c3c5479071865cab9078/entrypoints-0.4-py3-none-any.whl + name: entrypoints + version: '0.4' + sha256: f174b5ff827504fd3cd97cc3f8649f3693f51538c7e4bdf3ef002c8429d42f9f + requires_python: '>=3.6' +- conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + sha256: ee6cf346d017d954255bbcbdb424cddea4d14e4ed7e9813e429db1d795d01144 + md5: 8e662bd460bda79b1ea39194e3c4c9ab + depends: + - python >=3.10 + - typing_extensions >=4.6.0 + license: MIT and PSF-2.0 + purls: + - pkg:pypi/exceptiongroup?source=hash-mapping + size: 21333 + timestamp: 1763918099466 +- conda: https://conda.anaconda.org/conda-forge/noarch/execnet-2.1.2-pyhd8ed1ab_0.conda + sha256: 1acc6a420efc5b64c384c1f35f49129966f8a12c93b4bb2bdc30079e5dc9d8a8 + md5: a57b4be42619213a94f31d2c69c5dda7 + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/execnet?source=hash-mapping + size: 39499 + timestamp: 1762974150770 +- pypi: https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl + name: executing + version: 2.2.1 + sha256: 760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017 + requires_dist: + - asttokens>=2.1.0 ; extra == 'tests' + - ipython ; extra == 'tests' + - pytest ; extra == 'tests' + - coverage ; extra == 'tests' + - coverage-enable-subprocess ; extra == 'tests' + - littleutils ; extra == 'tests' + - rich ; python_full_version >= '3.11' and extra == 'tests' + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda + sha256: 210c8165a58fdbf16e626aac93cc4c14dbd551a01d1516be5ecad795d2422cad + md5: ff9efb7f7469aed3c4a8106ffa29593c + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/executing?source=hash-mapping + size: 30753 + timestamp: 1756729456476 +- pypi: https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl + name: filelock + version: 3.20.3 + sha256: 4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1 + requires_python: '>=3.10' +- conda: https://conda.anaconda.org/conda-forge/noarch/fsspec-2026.2.0-pyhd8ed1ab_0.conda + sha256: 239b67edf1c5e5caed52cf36e9bed47cb21b37721779828c130e6b3fd9793c1b + md5: 496c6c9411a6284addf55c898d6ed8d7 + depends: + - python >=3.10 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/fsspec?source=compressed-mapping + size: 148757 + timestamp: 1770387898414 +- pypi: https://files.pythonhosted.org/packages/54/e4/fac19dc34cb686c96011388b813ff7b858a70681e5ce6ce7698e5021b0f4/geopandas-1.1.2-py3-none-any.whl + name: geopandas + version: 1.1.2 + sha256: 2bb0b1052cb47378addb4ba54c47f8d4642dcbda9b61375638274f49d9f0bb0d + requires_dist: + - numpy>=1.24 + - pyogrio>=0.7.2 + - packaging + - pandas>=2.0.0 + - pyproj>=3.5.0 + - shapely>=2.0.0 + - psycopg[binary]>=3.1.0 ; extra == 'all' + - sqlalchemy>=2.0 ; extra == 'all' + - geopy ; extra == 'all' + - matplotlib>=3.7 ; extra == 'all' + - mapclassify>=2.5 ; extra == 'all' + - xyzservices ; extra == 'all' + - folium ; extra == 'all' + - geoalchemy2 ; extra == 'all' + - pyarrow>=10.0.0 ; extra == 'all' + - scipy ; extra == 'all' + - pytest>=3.1.0 ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - pytest-xdist ; extra == 'dev' + - codecov ; extra == 'dev' + - pre-commit ; extra == 'dev' + - ruff ; extra == 'dev' + requires_python: '>=3.10' +- conda: https://conda.anaconda.org/conda-forge/linux-64/gflags-2.2.2-h5888daf_1005.conda + sha256: 6c33bf0c4d8f418546ba9c250db4e4221040936aef8956353bc764d4877bc39a + md5: d411fc29e338efb48c5fd4576d71d881 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 119654 + timestamp: 1726600001928 +- conda: https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hbabe93e_0.conda + sha256: dc824dc1d0aa358e28da2ecbbb9f03d932d976c8dca11214aa1dcdfcbd054ba2 + md5: ff862eebdfeb2fd048ae9dc92510baca + depends: + - gflags >=2.2.2,<2.3.0a0 + - libgcc-ng >=12 + - libstdcxx-ng >=12 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 143452 + timestamp: 1718284177264 +- pypi: https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl + name: graphviz + version: '0.21' + sha256: 54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42 + requires_dist: + - build ; extra == 'dev' + - wheel ; extra == 'dev' + - twine ; extra == 'dev' + - flake8 ; extra == 'dev' + - flake8-pyproject ; extra == 'dev' + - pep8-naming ; extra == 'dev' + - tox>=3 ; extra == 'dev' + - pytest>=7,<8.1 ; extra == 'test' + - pytest-mock>=3 ; extra == 'test' + - pytest-cov ; extra == 'test' + - coverage ; extra == 'test' + - sphinx>=5,<7 ; extra == 'docs' + - sphinx-autodoc-typehints ; extra == 'docs' + - sphinx-rtd-theme>=0.2.5 ; extra == 'docs' + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + sha256: 84c64443368f84b600bfecc529a1194a3b14c3656ee2e832d15a20e0329b6da3 + md5: 164fc43f0b53b6e3a7bc7dce5e4f1dc9 + depends: + - python >=3.10 + - hyperframe >=6.1,<7 + - hpack >=4.1,<5 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/h2?source=hash-mapping + size: 95967 + timestamp: 1756364871835 +- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + sha256: 6ad78a180576c706aabeb5b4c8ceb97c0cb25f1e112d76495bff23e3779948ba + md5: 0a802cb9888dd14eeefc611f05c40b6e + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/hpack?source=hash-mapping + size: 30731 + timestamp: 1737618390337 +- pypi: https://files.pythonhosted.org/packages/c6/50/e0edd38dcd63fb26a8547f13d28f7a008bc4a3fd4eb4ff030673f22ad41a/hydra_core-1.3.2-py3-none-any.whl + name: hydra-core + version: 1.3.2 + sha256: fa0238a9e31df3373b35b0bfb672c34cc92718d21f81311d8996a16de1141d8b + requires_dist: + - omegaconf>=2.2,<2.4 + - antlr4-python3-runtime==4.9.* + - packaging + - importlib-resources ; python_full_version < '3.9' +- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + sha256: 77af6f5fe8b62ca07d09ac60127a30d9069fdc3c68d6b256754d0ffb1f7779f8 + md5: 8e6923fc12f1fe8f8c4e5c9f343256ac + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/hyperframe?source=hash-mapping + size: 17397 + timestamp: 1737618427549 +- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.2-h33c6efd_0.conda + sha256: 142a722072fa96cf16ff98eaaf641f54ab84744af81754c292cb81e0881c0329 + md5: 186a18e3ba246eccfc7cff00cd19a870 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT + purls: [] + size: 12728445 + timestamp: 1767969922681 +- pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl + name: idna + version: '3.11' + sha256: 771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea + requires_dist: + - ruff>=0.6.2 ; extra == 'all' + - mypy>=1.11.2 ; extra == 'all' + - pytest>=8.3.2 ; extra == 'all' + - flake8>=7.1.1 ; extra == 'all' + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + sha256: c18ab120a0613ada4391b15981d86ff777b5690ca461ea7e9e49531e8f374745 + md5: 63ccfdc3a3ce25b027b8767eb722fca8 + depends: + - python >=3.9 + - zipp >=3.20 + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/importlib-metadata?source=hash-mapping + size: 34641 + timestamp: 1747934053147 +- conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + sha256: e1a9e3b1c8fe62dc3932a616c284b5d8cbe3124bbfbedcf4ce5c828cb166ee19 + md5: 9614359868482abba1bd15ce465e3c42 + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/iniconfig?source=compressed-mapping + size: 13387 + timestamp: 1760831448842 +- pypi: https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl + name: ipython + version: 9.10.0 + sha256: c6ab68cc23bba8c7e18e9b932797014cc61ea7fd6f19de180ab9ba73e65ee58d + requires_dist: + - colorama>=0.4.4 ; sys_platform == 'win32' + - decorator>=4.3.2 + - ipython-pygments-lexers>=1.0.0 + - jedi>=0.18.1 + - matplotlib-inline>=0.1.5 + - pexpect>4.3 ; sys_platform != 'emscripten' and sys_platform != 'win32' + - prompt-toolkit>=3.0.41,<3.1.0 + - pygments>=2.11.0 + - stack-data>=0.6.0 + - traitlets>=5.13.0 + - typing-extensions>=4.6 ; python_full_version < '3.12' + - black ; extra == 'black' + - docrepr ; extra == 'doc' + - exceptiongroup ; extra == 'doc' + - intersphinx-registry ; extra == 'doc' + - ipykernel ; extra == 'doc' + - ipython[matplotlib,test] ; extra == 'doc' + - setuptools>=70.0 ; extra == 'doc' + - sphinx-toml==0.0.4 ; extra == 'doc' + - sphinx-rtd-theme>=0.1.8 ; extra == 'doc' + - sphinx>=8.0 ; extra == 'doc' + - typing-extensions ; extra == 'doc' + - pytest>=7.0.0 ; extra == 'test' + - pytest-asyncio>=1.0.0 ; extra == 'test' + - testpath>=0.2 ; extra == 'test' + - packaging>=20.1.0 ; extra == 'test' + - setuptools>=61.2 ; extra == 'test' + - ipython[test] ; extra == 'test-extra' + - curio ; extra == 'test-extra' + - jupyter-ai ; extra == 'test-extra' + - ipython[matplotlib] ; extra == 'test-extra' + - nbformat ; extra == 'test-extra' + - nbclient ; extra == 'test-extra' + - ipykernel>6.30 ; extra == 'test-extra' + - numpy>=1.27 ; extra == 'test-extra' + - pandas>2.1 ; extra == 'test-extra' + - trio>=0.1.0 ; extra == 'test-extra' + - matplotlib>3.9 ; extra == 'matplotlib' + - ipython[doc,matplotlib,terminal,test,test-extra] ; extra == 'all' + - argcomplete>=3.0 ; extra == 'all' + requires_python: '>=3.11' +- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.10.0-pyh53cf698_0.conda + sha256: 12cb4db242ea1a2e5e60a51b20f16e9c8120a9eb5d013c641cbf827bf3bb78e1 + md5: 441ca4e203a62f7db2f29f190c02b9cf + depends: + - __unix + - pexpect >4.3 + - decorator >=4.3.2 + - ipython_pygments_lexers >=1.0.0 + - jedi >=0.18.1 + - matplotlib-inline >=0.1.5 + - prompt-toolkit >=3.0.41,<3.1.0 + - pygments >=2.11.0 + - python >=3.11 + - stack_data >=0.6.0 + - traitlets >=5.13.0 + - typing_extensions >=4.6 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipython?source=compressed-mapping + size: 647436 + timestamp: 1770040907512 +- pypi: https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl + name: ipython-pygments-lexers + version: 1.1.1 + sha256: a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c + requires_dist: + - pygments + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + sha256: 894682a42a7d659ae12878dbcb274516a7031bbea9104e92f8e88c1f2765a104 + md5: bd80ba060603cc228d9d81c257093119 + depends: + - pygments + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipython-pygments-lexers?source=hash-mapping + size: 13993 + timestamp: 1737123723464 +- pypi: https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl + name: jedi + version: 0.19.2 + sha256: a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9 + requires_dist: + - parso>=0.8.4,<0.9.0 + - jinja2==2.11.3 ; extra == 'docs' + - markupsafe==1.1.1 ; extra == 'docs' + - pygments==2.8.1 ; extra == 'docs' + - alabaster==0.7.12 ; extra == 'docs' + - babel==2.9.1 ; extra == 'docs' + - chardet==4.0.0 ; extra == 'docs' + - commonmark==0.8.1 ; extra == 'docs' + - docutils==0.17.1 ; extra == 'docs' + - future==0.18.2 ; extra == 'docs' + - idna==2.10 ; extra == 'docs' + - imagesize==1.2.0 ; extra == 'docs' + - mock==1.0.1 ; extra == 'docs' + - packaging==20.9 ; extra == 'docs' + - pyparsing==2.4.7 ; extra == 'docs' + - pytz==2021.1 ; extra == 'docs' + - readthedocs-sphinx-ext==2.1.4 ; extra == 'docs' + - recommonmark==0.5.0 ; extra == 'docs' + - requests==2.25.1 ; extra == 'docs' + - six==1.15.0 ; extra == 'docs' + - snowballstemmer==2.1.0 ; extra == 'docs' + - sphinx-rtd-theme==0.4.3 ; extra == 'docs' + - sphinx==1.8.5 ; extra == 'docs' + - sphinxcontrib-serializinghtml==1.1.4 ; extra == 'docs' + - sphinxcontrib-websupport==1.2.4 ; extra == 'docs' + - urllib3==1.26.4 ; extra == 'docs' + - flake8==5.0.4 ; extra == 'qa' + - mypy==0.971 ; extra == 'qa' + - types-setuptools==67.2.0.1 ; extra == 'qa' + - django ; extra == 'testing' + - attrs ; extra == 'testing' + - colorama ; extra == 'testing' + - docopt ; extra == 'testing' + - pytest<9.0.0 ; extra == 'testing' + requires_python: '>=3.6' +- conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + sha256: 92c4d217e2dc68983f724aa983cca5464dcb929c566627b26a2511159667dba8 + md5: a4f4c5dc9b80bc50e0d3dc4e6e8f1bd9 + depends: + - parso >=0.8.3,<0.9.0 + - python >=3.9 + license: Apache-2.0 AND MIT + purls: + - pkg:pypi/jedi?source=hash-mapping + size: 843646 + timestamp: 1733300981994 +- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + sha256: fc9ca7348a4f25fed2079f2153ecdcf5f9cf2a0bc36c4172420ca09e1849df7b + md5: 04558c96691bed63104678757beb4f8d + depends: + - markupsafe >=2.0 + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jinja2?source=compressed-mapping + size: 120685 + timestamp: 1764517220861 +- pypi: https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl + name: joblib + version: 1.5.3 + sha256: 5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713 + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + sha256: 0960d06048a7185d3542d850986d807c6e37ca2e644342dd0c72feefcf26c2a4 + md5: b38117a3c920364aff79f870c984b4a3 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: LGPL-2.1-or-later + purls: [] + size: 134088 + timestamp: 1754905959823 +- conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda + sha256: 3e307628ca3527448dd1cb14ad7bb9d04d1d28c7d4c5f97ba196ae984571dd25 + md5: fb53fb07ce46a575c5d004bbc96032c2 + depends: + - __glibc >=2.17,<3.0.a0 + - keyutils >=1.6.3,<2.0a0 + - libedit >=3.1.20250104,<3.2.0a0 + - libedit >=3.1.20250104,<4.0a0 + - libgcc >=14 + - libstdcxx >=14 + - openssl >=3.5.5,<4.0a0 + license: MIT + license_family: MIT + purls: [] + size: 1386730 + timestamp: 1769769569681 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_101.conda + sha256: 565941ac1f8b0d2f2e8f02827cbca648f4d18cd461afc31f15604cd291b5c5f3 + md5: 12bd9a3f089ee6c9266a37dab82afabd + depends: + - __glibc >=2.17,<3.0.a0 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - binutils_impl_linux-64 2.45.1 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 725507 + timestamp: 1770267139900 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260107.1-cxx17_h7b12aa8_0.conda + sha256: a7a4481a4d217a3eadea0ec489826a69070fcc3153f00443aa491ed21527d239 + md5: 6f7b4302263347698fd24565fbf11310 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + constrains: + - libabseil-static =20260107.1=cxx17* + - abseil-cpp =20260107.1 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 1384817 + timestamp: 1770863194876 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-23.0.0-h2603568_3_cpu.conda + build_number: 3 + sha256: 249572775ce68f418392b2e4fd08a6adcd1c1c75bf4c870145a96d61f71d08ff + md5: 4952208743759431df21f01aba7466dd + depends: + - __glibc >=2.17,<3.0.a0 + - aws-crt-cpp >=0.35.4,<0.35.5.0a0 + - aws-sdk-cpp >=1.11.606,<1.11.607.0a0 + - azure-core-cpp >=1.16.2,<1.16.3.0a0 + - azure-identity-cpp >=1.13.3,<1.13.4.0a0 + - azure-storage-blobs-cpp >=12.16.0,<12.16.1.0a0 + - azure-storage-files-datalake-cpp >=12.14.0,<12.14.1.0a0 + - bzip2 >=1.0.8,<2.0a0 + - glog >=0.7.1,<0.8.0a0 + - libabseil * cxx17* + - libabseil >=20260107.0,<20260108.0a0 + - libbrotlidec >=1.2.0,<1.3.0a0 + - libbrotlienc >=1.2.0,<1.3.0a0 + - libgcc >=14 + - libgoogle-cloud >=2.39.0,<2.40.0a0 + - libgoogle-cloud-storage >=2.39.0,<2.40.0a0 + - libopentelemetry-cpp >=1.21.0,<1.22.0a0 + - libprotobuf >=6.33.5,<6.33.6.0a0 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - orc >=2.2.2,<2.2.3.0a0 + - snappy >=1.2.2,<1.3.0a0 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - parquet-cpp <0.0a0 + - apache-arrow-proc =*=cpu + - arrow-cpp <0.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 6482745 + timestamp: 1770642318900 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-23.0.0-h635bf11_3_cpu.conda + build_number: 3 + sha256: 85104db18ecf79a5f2498434843fdd525fe77befe5cdb0a26950f542afe2f850 + md5: c2415c2264b6b5e4ef45019ce6aa9579 + depends: + - __glibc >=2.17,<3.0.a0 + - libarrow 23.0.0 h2603568_3_cpu + - libarrow-compute 23.0.0 h53684a4_3_cpu + - libgcc >=14 + - libstdcxx >=14 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 612674 + timestamp: 1770642525144 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-compute-23.0.0-h53684a4_3_cpu.conda + build_number: 3 + sha256: c3d47ea6e732c178d0d276b9e14578fbc4ec519baf9b47af1a4f7c9184787cd5 + md5: 8ffa55113b6ade32fe4a51d480f0b806 + depends: + - __glibc >=2.17,<3.0.a0 + - libarrow 23.0.0 h2603568_3_cpu + - libgcc >=14 + - libre2-11 >=2025.11.5 + - libstdcxx >=14 + - libutf8proc >=2.11.3,<2.12.0a0 + - re2 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 3007250 + timestamp: 1770642389976 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-23.0.0-h635bf11_3_cpu.conda + build_number: 3 + sha256: fb0de4d207633cdb9e1cb80c67b292eef04dde3d81c61741c825be2a6510ea1e + md5: 22beeb3b36026e14f509a8b62ca58f1a + depends: + - __glibc >=2.17,<3.0.a0 + - libarrow 23.0.0 h2603568_3_cpu + - libarrow-acero 23.0.0 h635bf11_3_cpu + - libarrow-compute 23.0.0 h53684a4_3_cpu + - libgcc >=14 + - libparquet 23.0.0 h7376487_3_cpu + - libstdcxx >=14 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 611552 + timestamp: 1770642619988 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-23.0.0-hb4dd7c2_3_cpu.conda + build_number: 3 + sha256: d91e8f99b17dcc1d9f387d5119163a34f7486daaac39f9e766c0890be8ad0826 + md5: c582146e900636a8db83955cc15eadd5 + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20260107.0,<20260108.0a0 + - libarrow 23.0.0 h2603568_3_cpu + - libarrow-acero 23.0.0 h635bf11_3_cpu + - libarrow-dataset 23.0.0 h635bf11_3_cpu + - libgcc >=14 + - libprotobuf >=6.33.5,<6.33.6.0a0 + - libstdcxx >=14 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 522978 + timestamp: 1770642651554 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-5_h4a7cf45_openblas.conda + build_number: 5 + sha256: 18c72545080b86739352482ba14ba2c4815e19e26a7417ca21a95b76ec8da24c + md5: c160954f7418d7b6e87eaf05a8913fa9 + depends: + - libopenblas >=0.3.30,<0.3.31.0a0 + - libopenblas >=0.3.30,<1.0a0 + constrains: + - mkl <2026 + - liblapack 3.11.0 5*_openblas + - libcblas 3.11.0 5*_openblas + - blas 2.305 openblas + - liblapacke 3.11.0 5*_openblas + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 18213 + timestamp: 1765818813880 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + sha256: 318f36bd49ca8ad85e6478bd8506c88d82454cc008c1ac1c6bf00a3c42fa610e + md5: 72c8fd1af66bd67bf580645b426513ed + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 79965 + timestamp: 1764017188531 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + sha256: 12fff21d38f98bc446d82baa890e01fd82e3b750378fedc720ff93522ffb752b + md5: 366b40a69f0ad6072561c1d09301c886 + depends: + - __glibc >=2.17,<3.0.a0 + - libbrotlicommon 1.2.0 hb03c661_1 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 34632 + timestamp: 1764017199083 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda + sha256: a0c15c79997820bbd3fbc8ecf146f4fe0eca36cc60b62b63ac6cf78857f1dd0d + md5: 4ffbb341c8b616aa2494b6afb26a0c5f + depends: + - __glibc >=2.17,<3.0.a0 + - libbrotlicommon 1.2.0 hb03c661_1 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 298378 + timestamp: 1764017210931 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-5_h0358290_openblas.conda + build_number: 5 + sha256: 0cbdcc67901e02dc17f1d19e1f9170610bd828100dc207de4d5b6b8ad1ae7ad8 + md5: 6636a2b6f1a87572df2970d3ebc87cc0 + depends: + - libblas 3.11.0 5_h4a7cf45_openblas + constrains: + - liblapacke 3.11.0 5*_openblas + - blas 2.305 openblas + - liblapack 3.11.0 5*_openblas + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 18194 + timestamp: 1765818837135 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcrc32c-1.1.2-h9c3ff4c_0.tar.bz2 + sha256: fd1d153962764433fe6233f34a72cdeed5dcf8a883a85769e8295ce940b5b0c5 + md5: c965a5aa0d5c1c37ffc62dff36e28400 + depends: + - libgcc-ng >=9.4.0 + - libstdcxx-ng >=9.4.0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 20440 + timestamp: 1633683576494 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.18.0-hcf29cc6_1.conda + sha256: c84e8dccb65ad5149c0121e4b54bdc47fa39303fd5f4979b8c44bb51b39a369b + md5: 1707cdd636af2ff697b53186572c9f77 + depends: + - __glibc >=2.17,<3.0.a0 + - krb5 >=1.22.2,<1.23.0a0 + - libgcc >=14 + - libnghttp2 >=1.67.0,<2.0a0 + - libssh2 >=1.11.1,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.5,<4.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: curl + license_family: MIT + purls: [] + size: 463621 + timestamp: 1770892808818 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + sha256: d789471216e7aba3c184cd054ed61ce3f6dac6f87a50ec69291b9297f8c18724 + md5: c277e0a4d549b03ac1e9d6cbbe3d017b + depends: + - ncurses + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - ncurses >=6.5,<7.0a0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 134676 + timestamp: 1738479519902 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda + sha256: 1cd6048169fa0395af74ed5d8f1716e22c19a81a8a36f934c110ca3ad4dd27b4 + md5: 172bf1cd1ff8629f2b1179945ed45055 + depends: + - libgcc-ng >=12 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 112766 + timestamp: 1702146165126 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda + sha256: 2e14399d81fb348e9d231a82ca4d816bf855206923759b69ad006ba482764131 + md5: a1cfcc585f0c42bf8d5546bb1dfb668d + depends: + - libgcc-ng >=12 + - openssl >=3.1.1,<4.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 427426 + timestamp: 1685725977222 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.3-hecca717_0.conda + sha256: 1e1b08f6211629cbc2efe7a5bca5953f8f6b3cae0eeb04ca4dacee1bd4e2db2f + md5: 8b09ae86839581147ef2e5c5e229d164 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - expat 2.7.3.* + license: MIT + license_family: MIT + purls: [] + size: 76643 + timestamp: 1763549731408 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + sha256: 31f19b6a88ce40ebc0d5a992c131f57d919f73c0b92cd1617a5bec83f6e961e6 + md5: a360c33a5abe61c07959e449fa1453eb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 58592 + timestamp: 1769456073053 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_17.conda + sha256: 43860222cf3abf04ded0cf24541a105aa388e0e1d4d6ca46258e186d4e87ae3e + md5: 3c281169ea25b987311400d7a7e28445 + depends: + - __glibc >=2.17,<3.0.a0 + - _openmp_mutex >=4.5 + constrains: + - libgcc-ng ==15.2.0=*_17 + - libgomp 15.2.0 he0feb66_17 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 1040478 + timestamp: 1770252533873 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_17.conda + sha256: bdfe50501e4a2d904a5eae65a7ae26e2b7a29b473ab084ad55d96080b966502e + md5: 1478bfa85224a65ab096d69ffd2af1e5 + depends: + - libgcc 15.2.0 he0feb66_17 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 27541 + timestamp: 1770252546553 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_17.conda + sha256: 1604c083dd65bc91e68b6cfe32c8610395088cb96af1acaf71f0dcaf83ac58f7 + md5: a6c682ac611cb1fa4d73478f9e6efb06 + depends: + - libgfortran5 15.2.0 h68bc16d_17 + constrains: + - libgfortran-ng ==15.2.0=*_17 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 27515 + timestamp: 1770252591906 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_17.conda + sha256: b1c77b85da9a3e204de986f59e262268805c6a35dffdf3953f1b98407db2aef3 + md5: 202fdf8cad9eea704c2b0d823d1732bf + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=15.2.0 + constrains: + - libgfortran 15.2.0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 2480824 + timestamp: 1770252563579 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_17.conda + sha256: b961b5dd9761907a7179678b58a69bb4fc16b940eb477f635aea3aec0a3f17a6 + md5: 51b78c6a757575c0d12f4401ffc67029 + depends: + - __glibc >=2.17,<3.0.a0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 603334 + timestamp: 1770252441199 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.39.0-h9d11ab5_1.conda + sha256: 44f8e354431d2336475465ec8d71df7f3dea1397e70df0718c2ac75137976c63 + md5: cd398eb8374fb626a710b7a35b7ffa98 + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20260107.0,<20260108.0a0 + - libcurl >=8.18.0,<9.0a0 + - libgcc >=14 + - libgrpc >=1.78.0,<1.79.0a0 + - libprotobuf >=6.33.5,<6.33.6.0a0 + - libstdcxx >=14 + - openssl >=3.5.5,<4.0a0 + constrains: + - libgoogle-cloud 2.39.0 *_1 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 1307253 + timestamp: 1770461665848 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-2.39.0-hdbdcf42_1.conda + sha256: 2cce946ebf40b0b5fdb3e82c8a9f90ca28cd62abd281b20713067cc69a75c441 + md5: 384a1730ea66a72692e377cb45996d61 + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil + - libcrc32c >=1.1.2,<1.2.0a0 + - libcurl + - libgcc >=14 + - libgoogle-cloud 2.39.0 h9d11ab5_1 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + - openssl + license: Apache-2.0 + license_family: Apache + purls: [] + size: 803453 + timestamp: 1770461856392 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.78.0-h1d1128b_1.conda + sha256: f6861217d6c4bf96283738ba8d55782fccb577513a6cd346abc60cf88d1795df + md5: 66055700c90b50c0405a4e515bb4fe3c + depends: + - __glibc >=2.17,<3.0.a0 + - c-ares >=1.34.6,<2.0a0 + - libabseil * cxx17* + - libabseil >=20260107.0,<20260108.0a0 + - libgcc >=14 + - libprotobuf >=6.33.5,<6.33.6.0a0 + - libre2-11 >=2025.11.5 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.5,<4.0a0 + - re2 + constrains: + - grpc-cpp =1.78.0 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 6992089 + timestamp: 1770260975908 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda + sha256: c467851a7312765447155e071752d7bf9bf44d610a5687e32706f480aad2833f + md5: 915f5995e94f60e9a4826e0b0920ee88 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: LGPL-2.1-only + purls: [] + size: 790176 + timestamp: 1754908768807 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-5_h47877c9_openblas.conda + build_number: 5 + sha256: c723b6599fcd4c6c75dee728359ef418307280fa3e2ee376e14e85e5bbdda053 + md5: b38076eb5c8e40d0106beda6f95d7609 + depends: + - libblas 3.11.0 5_h4a7cf45_openblas + constrains: + - blas 2.305 openblas + - liblapacke 3.11.0 5*_openblas + - libcblas 3.11.0 5*_openblas + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 18200 + timestamp: 1765818857876 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.2-hb03c661_0.conda + sha256: 755c55ebab181d678c12e49cced893598f2bab22d582fbbf4d8b83c18be207eb + md5: c7c83eecbb72d88b940c249af56c8b17 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - xz 5.8.2.* + license: 0BSD + purls: [] + size: 113207 + timestamp: 1768752626120 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + sha256: fe171ed5cf5959993d43ff72de7596e8ac2853e9021dec0344e583734f1e0843 + md5: 2c21e66f50753a083cbe6b80f38268fa + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 92400 + timestamp: 1769482286018 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.67.0-had1ee68_0.conda + sha256: a4a7dab8db4dc81c736e9a9b42bdfd97b087816e029e221380511960ac46c690 + md5: b499ce4b026493a13774bcf0f4c33849 + depends: + - __glibc >=2.17,<3.0.a0 + - c-ares >=1.34.5,<2.0a0 + - libev >=4.33,<4.34.0a0 + - libev >=4.33,<5.0a0 + - libgcc >=14 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.2,<4.0a0 + license: MIT + license_family: MIT + purls: [] + size: 666600 + timestamp: 1756834976695 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_4.conda + sha256: 199d79c237afb0d4780ccd2fbf829cea80743df60df4705202558675e07dd2c5 + md5: be43915efc66345cccb3c310b6ed0374 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libgfortran + - libgfortran5 >=14.3.0 + constrains: + - openblas >=0.3.30,<0.3.31.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 5927939 + timestamp: 1763114673331 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-1.21.0-h9692893_2.conda + sha256: 59663bdd97ac6d8ce8a83bf80e18c14c4ac5ca536ef1a2de4bc9080a45dc501a + md5: c3de1cc30bc11edbc98aed352381449d + depends: + - libabseil * cxx17* + - libabseil >=20260107.0,<20260108.0a0 + - libcurl >=8.18.0,<9.0a0 + - libgrpc >=1.78.0,<1.79.0a0 + - libopentelemetry-cpp-headers 1.21.0 ha770c72_2 + - libprotobuf >=6.33.5,<6.33.6.0a0 + - libzlib >=1.3.1,<2.0a0 + - nlohmann_json + - prometheus-cpp >=1.3.0,<1.4.0a0 + constrains: + - cpp-opentelemetry-sdk =1.21.0 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 896630 + timestamp: 1770452315175 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopentelemetry-cpp-headers-1.21.0-ha770c72_2.conda + sha256: b2b2122f214c417851ba280009aea040e546665c43de737690c2610055a255e3 + md5: 253e70376a8ae74f9d99d44712b3e087 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 362214 + timestamp: 1770452273268 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libparquet-23.0.0-h7376487_3_cpu.conda + build_number: 3 + sha256: 8f9f1885cbfb20de14c18d55cd69c8076e003f845658ad17a967eb28f8fb9bf1 + md5: e3eef5f398cccdd73d3ff2e3c8ec0793 + depends: + - __glibc >=2.17,<3.0.a0 + - libarrow 23.0.0 h2603568_3_cpu + - libgcc >=14 + - libstdcxx >=14 + - libthrift >=0.22.0,<0.22.1.0a0 + - openssl >=3.5.5,<4.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 1392223 + timestamp: 1770642492655 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-6.33.5-h2b00c02_0.conda + sha256: afbf195443269ae10a940372c1d37cda749355d2bd96ef9587a962abd87f2429 + md5: 11ac478fa72cf12c214199b8a96523f4 + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20260107.0,<20260108.0a0 + - libgcc >=14 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 3638698 + timestamp: 1769749419271 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2025.11.05-h0dc7533_1.conda + sha256: 138fc85321a8c0731c1715688b38e2be4fb71db349c9ab25f685315095ae70ff + md5: ced7f10b6cfb4389385556f47c0ad949 + depends: + - __glibc >=2.17,<3.0.a0 + - libabseil * cxx17* + - libabseil >=20260107.0,<20260108.0a0 + - libgcc >=14 + - libstdcxx >=14 + constrains: + - re2 2025.11.05.* + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 213122 + timestamp: 1768190028309 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.2-hf4e2dac_0.conda + sha256: 04596fcee262a870e4b7c9807224680ff48d4d0cc0dac076a602503d3dc6d217 + md5: da5be73701eecd0e8454423fd6ffcf30 + depends: + - __glibc >=2.17,<3.0.a0 + - icu >=78.2,<79.0a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + license: blessing + purls: [] + size: 942808 + timestamp: 1768147973361 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda + sha256: fa39bfd69228a13e553bd24601332b7cfeb30ca11a3ca50bb028108fe90a7661 + md5: eecce068c7e4eddeb169591baac20ac4 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.0,<4.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 304790 + timestamp: 1745608545575 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_17.conda + sha256: 50c48cd3716a2e58e8e2e02edc78fef2d08fffe1e3b1ed40eb5f87e7e2d07889 + md5: 24c2fe35fa45cd71214beba6f337c071 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc 15.2.0 he0feb66_17 + constrains: + - libstdcxx-ng ==15.2.0=*_17 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 5852406 + timestamp: 1770252584235 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_17.conda + sha256: ca3fb322dab3373946b1064da686ec076f5b1b9caf0a2823dad00d0b0f704928 + md5: ea12f5a6bf12c88c06750d9803e1a570 + depends: + - libstdcxx 15.2.0 h934c35e_17 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 27573 + timestamp: 1770252638797 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.22.0-h454ac66_1.conda + sha256: 4888b9ea2593c36ca587a5ebe38d0a56a0e6d6a9e4bb7da7d9a326aaaca7c336 + md5: 8ed82d90e6b1686f5e98f8b7825a15ef + depends: + - __glibc >=2.17,<3.0.a0 + - libevent >=2.1.12,<2.1.13.0a0 + - libgcc >=14 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.1,<4.0a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 424208 + timestamp: 1753277183984 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libutf8proc-2.11.3-hfe17d71_0.conda + sha256: ecbf4b7520296ed580498dc66a72508b8a79da5126e1d6dc650a7087171288f9 + md5: 1247168fe4a0b8912e3336bccdbf98a5 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 85969 + timestamp: 1768735071295 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.3-h5347b49_0.conda + sha256: 1a7539cfa7df00714e8943e18de0b06cceef6778e420a5ee3a2a145773758aee + md5: db409b7c1720428638e7c0d509d3e1b5 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 40311 + timestamp: 1766271528534 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.1-he237659_1.conda + sha256: 047be059033c394bd32ae5de66ce389824352120b3a7c0eff980195f7ed80357 + md5: 417955234eccd8f252b86a265ccdab7f + depends: + - __glibc >=2.17,<3.0.a0 + - icu >=78.1,<79.0a0 + - libgcc >=14 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libxml2-16 2.15.1 hca6bf5a_1 + - libzlib >=1.3.1,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 45402 + timestamp: 1766327161688 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.1-hca6bf5a_1.conda + sha256: 8331284bf9ae641b70cdc0e5866502dd80055fc3b9350979c74bb1d192e8e09e + md5: 3fdd8d99683da9fe279c2f4cecd1e048 + depends: + - __glibc >=2.17,<3.0.a0 + - icu >=78.1,<79.0a0 + - libgcc >=14 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libzlib >=1.3.1,<2.0a0 + constrains: + - libxml2 2.15.1 + license: MIT + license_family: MIT + purls: [] + size: 555747 + timestamp: 1766327145986 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + sha256: d4bfe88d7cb447768e31650f06257995601f89076080e76df55e3112d4e47dc4 + md5: edb0dca6bc32e4f4789199455a1dbeb8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 60963 + timestamp: 1727963148474 +- conda: https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2 + sha256: 9afe0b5cfa418e8bdb30d8917c5a6cec10372b037924916f1f85b9f4899a67a6 + md5: 91e27ef3d05cc772ce627e51cff111c4 + depends: + - python >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.* + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/locket?source=hash-mapping + size: 8250 + timestamp: 1650660473123 +- conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda + sha256: 47326f811392a5fd3055f0f773036c392d26fdb32e4d8e7a8197eed951489346 + md5: 9de5350a85c4a20c685259b889aa6393 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 167055 + timestamp: 1733741040117 +- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py313h3dea7bd_0.conda + sha256: a530a411bdaaf0b1e4de8869dfaca46cb07407bc7dc0702a9e231b0e5ce7ca85 + md5: c14389156310b8ed3520d84f854be1ee + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + constrains: + - jinja2 >=3.0.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/markupsafe?source=hash-mapping + size: 25909 + timestamp: 1759055357045 +- pypi: https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl + name: matplotlib-inline + version: 0.2.1 + sha256: d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76 + requires_dist: + - traitlets + - flake8 ; extra == 'test' + - nbdime ; extra == 'test' + - nbval ; extra == 'test' + - notebook ; extra == 'test' + - pytest ; extra == 'test' + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + sha256: 9d690334de0cd1d22c51bc28420663f4277cfa60d34fa5cad1ce284a13f1d603 + md5: 00e120ce3e40bad7bfc78861ce3c4a25 + depends: + - python >=3.10 + - traitlets + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/matplotlib-inline?source=hash-mapping + size: 15175 + timestamp: 1761214578417 +- conda: https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.1.2-py313h7037e92_1.conda + sha256: fac37e267dd1d07527f0b078ffe000916e80e8c89cfe69d466f5775b88e93df2 + md5: cd1cfde0ea3bca6c805c73ffa988b12a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/msgpack?source=hash-mapping + size: 103129 + timestamp: 1762504205590 +- pypi: https://files.pythonhosted.org/packages/93/cf/be4e93afbfa0def2cd6fac9302071db0bd6d0617999ecbf53f92b9398de3/multiurl-0.3.7-py3-none-any.whl + name: multiurl + version: 0.3.7 + sha256: 054f42974064f103be0ed55b43f0c32fc435a47dc7353a9adaffa643b99fa380 + requires_dist: + - requests + - tqdm + - pytz + - python-dateutil +- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586 + md5: 47e340acb35de30501a76c7c799c41d7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: X11 AND BSD-3-Clause + purls: [] + size: 891641 + timestamp: 1738195959188 +- conda: https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h54a6638_1.conda + sha256: fd2cbd8dfc006c72f45843672664a8e4b99b2f8137654eaae8c3d46dca776f63 + md5: 16c2a0e9c4a166e53632cfca4f68d020 + constrains: + - nlohmann_json-abi ==3.12.0 + license: MIT + license_family: MIT + purls: [] + size: 136216 + timestamp: 1758194284857 +- conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.4.2-py313hf6604e3_1.conda + sha256: 2eb8be25a7504f058a153a84be70471e0ebbf6bd0411ae2b6d34904b89d86fe3 + md5: ca9c6ba4beac38cb3d0a85afde27f94c + depends: + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + - liblapack >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - python_abi 3.13.* *_cp313 + - libblas >=3.9.0,<4.0a0 + constrains: + - numpy-base <0a0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/numpy?source=hash-mapping + size: 8857152 + timestamp: 1770098515258 +- pypi: https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl + name: omegaconf + version: 2.3.0 + sha256: 7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b + requires_dist: + - antlr4-python3-runtime==4.9.* + - pyyaml>=5.1.0 + - dataclasses ; python_full_version == '3.6.*' + requires_python: '>=3.6' +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda + sha256: 44c877f8af015332a5d12f5ff0fb20ca32f896526a7d0cdb30c769df1144fb5c + md5: f61eb8cd60ff9057122a3d338b99c00f + depends: + - __glibc >=2.17,<3.0.a0 + - ca-certificates + - libgcc >=14 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3164551 + timestamp: 1769555830639 +- conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.2.2-hbb90d81_1.conda + sha256: c59d22c4e555c09259c52da96f1576797fcb4fba5665073e9c1907393309172d + md5: 9269175175f18091b8844c8e9f213205 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libprotobuf >=6.33.5,<6.33.6.0a0 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - snappy >=1.2.2,<1.3.0a0 + - tzdata + - zstd >=1.5.7,<1.6.0a0 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 1319627 + timestamp: 1770452421607 +- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.0-pyhcf101f3_0.conda + sha256: c1fc0f953048f743385d31c468b4a678b3ad20caffdeaa94bed85ba63049fd58 + md5: b76541e68fea4d511b1ac46a28dcd2c6 + depends: + - python >=3.8 + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/packaging?source=compressed-mapping + size: 72010 + timestamp: 1769093650580 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.0-py313hbfd7664_0.conda + sha256: 05719fdfacdf97206a901621d79ab103c34905973ec8a18627825d5adab7a1b0 + md5: ab6d05e915ab2ae4c41d275b14592151 + depends: + - python + - numpy >=1.26.0 + - python-dateutil >=2.8.2 + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + - libgcc >=14 + - python_abi 3.13.* *_cp313 + - numpy >=1.23,<3 + constrains: + - adbc-driver-postgresql >=1.2.0 + - adbc-driver-sqlite >=1.2.0 + - beautifulsoup4 >=4.12.3 + - blosc >=1.21.3 + - bottleneck >=1.4.2 + - fastparquet >=2024.11.0 + - fsspec >=2024.10.0 + - gcsfs >=2024.10.0 + - html5lib >=1.1 + - hypothesis >=6.116.0 + - jinja2 >=3.1.5 + - lxml >=5.3.0 + - matplotlib >=3.9.3 + - numba >=0.60.0 + - numexpr >=2.10.2 + - odfpy >=1.4.1 + - openpyxl >=3.1.5 + - psycopg2 >=2.9.10 + - pyarrow >=13.0.0 + - pyiceberg >=0.8.1 + - pymysql >=1.1.1 + - pyqt5 >=5.15.9 + - pyreadstat >=1.2.8 + - pytables >=3.10.1 + - pytest >=8.3.4 + - pytest-xdist >=3.6.1 + - python-calamine >=0.3.0 + - pytz >=2024.2 + - pyxlsb >=1.0.10 + - qtpy >=2.4.2 + - scipy >=1.14.1 + - s3fs >=2024.10.0 + - sqlalchemy >=2.0.36 + - tabulate >=0.9.0 + - xarray >=2024.10.0 + - xlrd >=2.0.1 + - xlsxwriter >=3.2.0 + - zstandard >=0.23.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pandas?source=hash-mapping + size: 14952243 + timestamp: 1769076307505 +- pypi: https://files.pythonhosted.org/packages/b6/61/fae042894f4296ec49e3f193aff5d7c18440da9e48102c3315e1bc4519a7/parso-0.8.6-py2.py3-none-any.whl + name: parso + version: 0.8.6 + sha256: 2c549f800b70a5c4952197248825584cb00f033b29c692671d3bf08bf380baff + requires_dist: + - pytest ; extra == 'testing' + - docopt ; extra == 'testing' + - flake8==5.0.4 ; extra == 'qa' + - zuban==0.5.1 ; extra == 'qa' + - types-setuptools==67.2.0.1 ; extra == 'qa' + requires_python: '>=3.6' +- conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.6-pyhcf101f3_0.conda + sha256: 42b2d77ccea60752f3aa929a6413a7835aaacdbbde679f2f5870a744fa836b94 + md5: 97c1ce2fffa1209e7afb432810ec6e12 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/parso?source=compressed-mapping + size: 82287 + timestamp: 1770676243987 +- conda: https://conda.anaconda.org/conda-forge/noarch/partd-1.4.2-pyhd8ed1ab_0.conda + sha256: 472fc587c63ec4f6eba0cc0b06008a6371e0a08a5986de3cf4e8024a47b4fe6c + md5: 0badf9c54e24cecfb0ad2f99d680c163 + depends: + - locket + - python >=3.9 + - toolz + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/partd?source=hash-mapping + size: 20884 + timestamp: 1715026639309 +- pypi: https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl + name: pexpect + version: 4.9.0 + sha256: 7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523 + requires_dist: + - ptyprocess>=0.5 +- conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + sha256: 202af1de83b585d36445dc1fda94266697341994d1a3328fabde4989e1b3d07a + md5: d0d408b1f18883a944376da5cf8101ea + depends: + - ptyprocess >=0.5 + - python >=3.9 + license: ISC + purls: + - pkg:pypi/pexpect?source=hash-mapping + size: 53561 + timestamp: 1733302019362 +- pypi: https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: pillow + version: 12.1.1 + sha256: 47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717 + requires_dist: + - furo ; extra == 'docs' + - olefile ; extra == 'docs' + - sphinx>=8.2 ; extra == 'docs' + - sphinx-autobuild ; extra == 'docs' + - sphinx-copybutton ; extra == 'docs' + - sphinx-inline-tabs ; extra == 'docs' + - sphinxext-opengraph ; extra == 'docs' + - olefile ; extra == 'fpx' + - olefile ; extra == 'mic' + - arro3-compute ; extra == 'test-arrow' + - arro3-core ; extra == 'test-arrow' + - nanoarrow ; extra == 'test-arrow' + - pyarrow ; extra == 'test-arrow' + - check-manifest ; extra == 'tests' + - coverage>=7.4.2 ; extra == 'tests' + - defusedxml ; extra == 'tests' + - markdown2 ; extra == 'tests' + - olefile ; extra == 'tests' + - packaging ; extra == 'tests' + - pyroma>=5 ; extra == 'tests' + - pytest ; extra == 'tests' + - pytest-cov ; extra == 'tests' + - pytest-timeout ; extra == 'tests' + - pytest-xdist ; extra == 'tests' + - trove-classifiers>=2024.10.12 ; extra == 'tests' + - defusedxml ; extra == 'xmp' + requires_python: '>=3.10' +- conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + sha256: e14aafa63efa0528ca99ba568eaf506eb55a0371d12e6250aaaa61718d2eb62e + md5: d7585b6550ad04c8c5e21097ada2888e + depends: + - python >=3.9 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/pluggy?source=compressed-mapping + size: 25877 + timestamp: 1764896838868 +- conda: https://conda.anaconda.org/conda-forge/linux-64/prometheus-cpp-1.3.0-ha5d0236_0.conda + sha256: 013669433eb447548f21c3c6b16b2ed64356f726b5f77c1b39d5ba17a8a4b8bc + md5: a83f6a2fdc079e643237887a37460668 + depends: + - __glibc >=2.17,<3.0.a0 + - libcurl >=8.10.1,<9.0a0 + - libgcc >=13 + - libstdcxx >=13 + - libzlib >=1.3.1,<2.0a0 + - zlib + license: MIT + license_family: MIT + purls: [] + size: 199544 + timestamp: 1730769112346 +- pypi: https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl + name: prompt-toolkit + version: 3.0.52 + sha256: 9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955 + requires_dist: + - wcwidth + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.52-pyha770c72_0.conda + sha256: 4817651a276016f3838957bfdf963386438c70761e9faec7749d411635979bae + md5: edb16f14d920fb3faf17f5ce582942d6 + depends: + - python >=3.10 + - wcwidth + constrains: + - prompt_toolkit 3.0.52 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/prompt-toolkit?source=hash-mapping + size: 273927 + timestamp: 1756321848365 +- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py313h54dd161_0.conda + sha256: f19fd682d874689dfde20bf46d7ec1a28084af34583e0405685981363af47c91 + md5: 25fe6e02c2083497b3239e21b49d8093 + depends: + - python + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python_abi 3.13.* *_cp313 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/psutil?source=hash-mapping + size: 228663 + timestamp: 1769678153829 +- pypi: https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl + name: ptyprocess + version: 0.7.0 + sha256: 4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35 +- conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + sha256: a7713dfe30faf17508ec359e0bc7e0983f5d94682492469bd462cdaae9c64d83 + md5: 7d9daffbb8d8e0af0f769dbbcd173a54 + depends: + - python >=3.9 + license: ISC + purls: + - pkg:pypi/ptyprocess?source=hash-mapping + size: 19457 + timestamp: 1733302371990 +- pypi: https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl + name: pure-eval + version: 0.2.3 + sha256: 1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0 + requires_dist: + - pytest ; extra == 'tests' +- conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + sha256: 71bd24600d14bb171a6321d523486f6a06f855e75e547fa0cb2a0953b02047f0 + md5: 3bfdfb8dbcdc4af1ae3f9a8eb3948f04 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pure-eval?source=hash-mapping + size: 16668 + timestamp: 1733569518868 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-23.0.0-py313h78bf25f_0.conda + sha256: 43636b4ce58c57f3aeab182238b47cb8b860d2cc0544c184612c15ee294be154 + md5: a6e89cb214f318db9548b791ba27f862 + depends: + - libarrow-acero 23.0.0.* + - libarrow-dataset 23.0.0.* + - libarrow-substrait 23.0.0.* + - libparquet 23.0.0.* + - pyarrow-core 23.0.0 *_0_* + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 27332 + timestamp: 1769291558903 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-23.0.0-py313h98bfbea_0_cpu.conda + sha256: 30247f262175f7408c7856735c529a9402356f85b8f99cc54c86bbcd7600a2c0 + md5: c8d1ba76789588fdf7fddc213a25137e + depends: + - __glibc >=2.17,<3.0.a0 + - libarrow 23.0.0.* *cpu + - libarrow-compute 23.0.0.* *cpu + - libgcc >=14 + - libstdcxx >=14 + - libzlib >=1.3.1,<2.0a0 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + constrains: + - apache-arrow-proc * cpu + - numpy >=1.23,<3 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/pyarrow?source=hash-mapping + size: 4776275 + timestamp: 1770672664641 +- pypi: https://files.pythonhosted.org/packages/b3/f8/f47b90fbeaf36e112b1a93fc313d5f0bc9f0051ae8be734173787a00271a/pyearthtools_data-0.5.1-py3-none-any.whl + name: pyearthtools-data + version: 0.5.1 + sha256: f930e2ff804686d94699c0a6cdc5bf3675f9f8df0f8abb4494198fe6ab1a3fbc + requires_dist: + - click + - filelock + - geopandas + - pyearthtools-utils>=0.5.0 + - pyyaml + - shapely + - tqdm + - urllib3 + - xarray[complete] + - cdsapi ; extra == 'all' + - eccodes ; extra == 'all' + - ecmwf-opendata ; extra == 'all' + - gcsfs ; extra == 'all' + - intake ; extra == 'all' + - intake-esm ; extra == 'all' + - zarr==2.* ; extra == 'all' + - cdsapi ; extra == 'download' + - eccodes ; extra == 'download' + - ecmwf-opendata ; extra == 'download' + - gcsfs ; extra == 'download' + - zarr==2.* ; extra == 'download' + - intake ; extra == 'intake' + - intake-esm ; extra == 'intake' + requires_python: '>=3.11' +- pypi: ./ + name: pyearthtools-persistence + version: 0.6.0 + sha256: b1a739e368b0b6b224e7bd805870c2d5c9e66183a749d55c2f4ae7739e268bf6 + requires_dist: + - pyearthtools-zoo>=0.5.0 + - pyearthtools-data>=0.5.0 + - pyearthtools-pipeline>=0.5.0 + - hydra-core + requires_python: '>=3.11,<3.14' +- pypi: https://files.pythonhosted.org/packages/f2/f8/beda8582d430075031ac8835aced207d7bc639469451c932fdf1c0b2ed5c/pyearthtools_pipeline-0.5.1-py3-none-any.whl + name: pyearthtools-pipeline + version: 0.5.1 + sha256: 7a02dd6dd91226452ffbc71cf43d8ec16118cd3fb456f8e9180446bd72a4c417 + requires_dist: + - einops + - graphviz + - pandas + - pyearthtools-data>=0.5.0 + - pyearthtools-utils>=0.5.0 + - xarray + - dask ; extra == 'all' + - distributed ; extra == 'all' + - healpy ; extra == 'all' + - pyearthtools-data[all] ; extra == 'all' + - reproject ; extra == 'all' + - dask ; extra == 'distributed' + - distributed ; extra == 'distributed' + - healpy ; extra == 'remapping' + - reproject ; extra == 'remapping' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/38/06/7ed1c4fad0195d7700b77df09dae83ce6658fa6e2d5bb0c92bec79d766d3/pyearthtools_training-0.5.1-py3-none-any.whl + name: pyearthtools-training + version: 0.5.1 + sha256: 14a999fb404182615cfabf62e1279276178ef56e672b801cfa3e7f12049f9350 + requires_dist: + - einops + - pyearthtools-pipeline>=0.5.0 + - pyearthtools-utils>=0.5.0 + - scikit-learn + - scipy + - lightning ; extra == 'all' + - piqa ; extra == 'all' + - scikit-learn ; extra == 'all' + - tensorboard ; extra == 'all' + - tensorly ; extra == 'all' + - torch ; extra == 'all' + - xgboost ; extra == 'all' + - lightning ; extra == 'lightning' + - piqa ; extra == 'lightning' + - tensorboard ; extra == 'lightning' + - tensorly ; extra == 'lightning' + - torch ; extra == 'lightning' + - onnx ; extra == 'onnx' + - onnxruntime ; extra == 'onnx' + - onnxruntime-gpu ; extra == 'onnx-gpu' + - lightning ; extra == 'pytorch' + - piqa ; extra == 'pytorch' + - tensorboard ; extra == 'pytorch' + - tensorly ; extra == 'pytorch' + - torch ; extra == 'pytorch' + - scikit-learn ; extra == 'xgboost' + - xgboost ; extra == 'xgboost' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/cf/fc/c774d872abe5ae0c4381c5cb1ed61240e682c44ed019f807e18be26a7882/pyearthtools_utils-0.5.1-py3-none-any.whl + name: pyearthtools-utils + version: 0.5.1 + sha256: 17eb312fb26edc3d38d1e2da1b23a482b89383c84d7e10de83ff8940b8a701b2 + requires_dist: + - ipython + - numpy + - pillow + - pyyaml + - scikit-learn + - tqdm + - xarray + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/a4/45/1cb45ccac7c5f728a363d17a145443ed1f66962d3224b8e1166a4fd7bae1/pyearthtools_zoo-0.5.1-py3-none-any.whl + name: pyearthtools-zoo + version: 0.5.1 + sha256: fa6960043c621366aa020e85ab4d4b3097242f0a624cb603454f85c5d5563b9c + requires_dist: + - click + - entrypoints + - multiurl + - pyearthtools-data>=0.5.0 + - pyearthtools-pipeline>=0.5.0 + - pyearthtools-training>=0.5.0 + - pyearthtools-utils>=0.5.0 + - tqdm + - black ; extra == 'testing' + - coverage ; extra == 'testing' + - pytest ; extra == 'testing' + - pytest-cov ; extra == 'testing' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl + name: pygments + version: 2.19.2 + sha256: 86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b + requires_dist: + - colorama>=0.4.6 ; extra == 'windows-terminal' + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + sha256: 5577623b9f6685ece2697c6eb7511b4c9ac5fb607c9babc2646c811b428fd46a + md5: 6b6ece66ebcae2d5f326c77ef2c5a066 + depends: + - python >=3.9 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/pygments?source=hash-mapping + size: 889287 + timestamp: 1750615908735 +- pypi: https://files.pythonhosted.org/packages/46/35/b874f79d03e9f900012cf609f7fff97b77164f2e14ee5aac282f8a999c1b/pyogrio-0.12.1-cp313-cp313-manylinux_2_28_x86_64.whl + name: pyogrio + version: 0.12.1 + sha256: 0622bc1a186421547660271083079b38d42e6f868802936d8538c0b379f1ab6b + requires_dist: + - certifi + - numpy + - packaging + - cython>=3.1 ; extra == 'dev' + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-benchmark ; extra == 'benchmark' + - geopandas ; extra == 'geopandas' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/f8/85/c2b1706e51942de19076eff082f8495e57d5151364e78b5bef4af4a1d94a/pyproj-3.7.2-cp313-cp313-manylinux_2_28_x86_64.whl + name: pyproj + version: 3.7.2 + sha256: 5141a538ffdbe4bfd157421828bb2e07123a90a7a2d6f30fa1462abcfb5ce681 + requires_dist: + - certifi + requires_python: '>=3.11' +- conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + sha256: ba3b032fa52709ce0d9fd388f63d330a026754587a2f461117cac9ab73d8d0d8 + md5: 461219d1a5bd61342293efa2c0c90eac + depends: + - __unix + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pysocks?source=hash-mapping + size: 21085 + timestamp: 1733217331982 +- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.2-pyhcf101f3_0.conda + sha256: 9e749fb465a8bedf0184d8b8996992a38de351f7c64e967031944978de03a520 + md5: 2b694bad8a50dc2f712f5368de866480 + depends: + - pygments >=2.7.2 + - python >=3.10 + - iniconfig >=1.0.1 + - packaging >=22 + - pluggy >=1.5,<2 + - tomli >=1 + - colorama >=0.4 + - exceptiongroup >=1 + - python + constrains: + - pytest-faulthandler >=2 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pytest?source=hash-mapping + size: 299581 + timestamp: 1765062031645 +- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-cov-7.0.0-pyhcf101f3_1.conda + sha256: d0f45586aad48ef604590188c33c83d76e4fc6370ac569ba0900906b24fd6a26 + md5: 6891acad5e136cb62a8c2ed2679d6528 + depends: + - coverage >=7.10.6 + - pluggy >=1.2 + - pytest >=7 + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/pytest-cov?source=hash-mapping + size: 29016 + timestamp: 1757612051022 +- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-xdist-3.8.0-pyhd8ed1ab_0.conda + sha256: b7b58a5be090883198411337b99afb6404127809c3d1c9f96e99b59f36177a96 + md5: 8375cfbda7c57fbceeda18229be10417 + depends: + - execnet >=2.1 + - pytest >=7.0.0 + - python >=3.9 + constrains: + - psutil >=3.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pytest-xdist?source=hash-mapping + size: 39300 + timestamp: 1751452761594 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.12-hc97d973_100_cp313.conda + build_number: 100 + sha256: 8a08fe5b7cb5a28aa44e2994d18dbf77f443956990753a4ca8173153ffb6eb56 + md5: 4c875ed0e78c2d407ec55eadffb8cf3d + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.7.3,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - liblzma >=5.8.2,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.51.2,<4.0a0 + - libuuid >=2.41.3,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.5,<4.0a0 + - python_abi 3.13.* *_cp313 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + license: Python-2.0 + purls: [] + size: 37364553 + timestamp: 1770272309861 + python_site_packages_path: lib/python3.13/site-packages +- conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + sha256: d6a17ece93bbd5139e02d2bd7dbfa80bee1a4261dced63f65f679121686bf664 + md5: 5b8d21249ff20967101ffa321cab24e8 + depends: + - python >=3.9 + - six >=1.5 + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/python-dateutil?source=hash-mapping + size: 233310 + timestamp: 1751104122689 +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + build_number: 8 + sha256: 210bffe7b121e651419cb196a2a63687b087497595c9be9d20ebe97dd06060a7 + md5: 94305520c52a4aa3f6c2b1ff6008d9f8 + constrains: + - python 3.13.* *_cp313 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 7002 + timestamp: 1752805902938 +- pypi: https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl + name: pytz + version: '2025.2' + sha256: 5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00 +- pypi: https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: pyyaml + version: 6.0.3 + sha256: 0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py313h3dea7bd_1.conda + sha256: ef7df29b38ef04ec67a8888a4aa039973eaa377e8c4b59a7be0a1c50cd7e4ac6 + md5: f256753e840c3cd3766488c9437a8f8b + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + - yaml >=0.2.5,<0.3.0a0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyyaml?source=compressed-mapping + size: 201616 + timestamp: 1770223543730 +- conda: https://conda.anaconda.org/conda-forge/linux-64/re2-2025.11.05-h5301d42_1.conda + sha256: 3fc684b81631348540e9a42f6768b871dfeab532d3f47d5c341f1f83e2a2b2b2 + md5: 66a715bc01c77d43aca1f9fcb13dde3c + depends: + - libre2-11 2025.11.05 h0dc7533_1 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 27469 + timestamp: 1768190052132 +- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + sha256: 12ffde5a6f958e285aa22c191ca01bbd3d6e710aa852e00618fa6ddc59149002 + md5: d7d95fc8287ea7bf33e0e7116d2b95ec + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 345073 + timestamp: 1765813471974 +- pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + name: requests + version: 2.32.5 + sha256: 2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 + requires_dist: + - charset-normalizer>=2,<4 + - idna>=2.5,<4 + - urllib3>=1.21.1,<3 + - certifi>=2017.4.17 + - pysocks>=1.5.6,!=1.5.7 ; extra == 'socks' + - chardet>=3.0.2,<6 ; extra == 'use-chardet-on-py3' + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.15.0-h40fa522_0.conda + noarch: python + sha256: fc456645570586c798d2da12fe723b38ea0d0901373fd9959cab914cbb19518b + md5: fe90be2abf12b301dde984719a02ca0b + depends: + - python + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + purls: + - pkg:pypi/ruff?source=compressed-mapping + size: 9103793 + timestamp: 1770153712370 +- conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.6.2-he8a4886_1.conda + sha256: dec76e9faa3173579d34d226dbc91892417a80784911daf8e3f0eb9bad19d7a6 + md5: bade189a194e66b93c03021bd36c337b + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - openssl >=3.5.4,<4.0a0 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 394197 + timestamp: 1765160261434 +- pypi: https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: scikit-learn + version: 1.8.0 + sha256: 8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e + requires_dist: + - numpy>=1.24.1 + - scipy>=1.10.0 + - joblib>=1.3.0 + - threadpoolctl>=3.2.0 + - numpy>=1.24.1 ; extra == 'build' + - scipy>=1.10.0 ; extra == 'build' + - cython>=3.1.2 ; extra == 'build' + - meson-python>=0.17.1 ; extra == 'build' + - numpy>=1.24.1 ; extra == 'install' + - scipy>=1.10.0 ; extra == 'install' + - joblib>=1.3.0 ; extra == 'install' + - threadpoolctl>=3.2.0 ; extra == 'install' + - matplotlib>=3.6.1 ; extra == 'benchmark' + - pandas>=1.5.0 ; extra == 'benchmark' + - memory-profiler>=0.57.0 ; extra == 'benchmark' + - matplotlib>=3.6.1 ; extra == 'docs' + - scikit-image>=0.22.0 ; extra == 'docs' + - pandas>=1.5.0 ; extra == 'docs' + - seaborn>=0.13.0 ; extra == 'docs' + - memory-profiler>=0.57.0 ; extra == 'docs' + - sphinx>=7.3.7 ; extra == 'docs' + - sphinx-copybutton>=0.5.2 ; extra == 'docs' + - sphinx-gallery>=0.17.1 ; extra == 'docs' + - numpydoc>=1.2.0 ; extra == 'docs' + - pillow>=10.1.0 ; extra == 'docs' + - pooch>=1.8.0 ; extra == 'docs' + - sphinx-prompt>=1.4.0 ; extra == 'docs' + - sphinxext-opengraph>=0.9.1 ; extra == 'docs' + - plotly>=5.18.0 ; extra == 'docs' + - polars>=0.20.30 ; extra == 'docs' + - sphinx-design>=0.6.0 ; extra == 'docs' + - sphinxcontrib-sass>=0.3.4 ; extra == 'docs' + - pydata-sphinx-theme>=0.15.3 ; extra == 'docs' + - sphinx-remove-toctrees>=1.0.0.post1 ; extra == 'docs' + - towncrier>=24.8.0 ; extra == 'docs' + - matplotlib>=3.6.1 ; extra == 'examples' + - scikit-image>=0.22.0 ; extra == 'examples' + - pandas>=1.5.0 ; extra == 'examples' + - seaborn>=0.13.0 ; extra == 'examples' + - pooch>=1.8.0 ; extra == 'examples' + - plotly>=5.18.0 ; extra == 'examples' + - matplotlib>=3.6.1 ; extra == 'tests' + - pandas>=1.5.0 ; extra == 'tests' + - pytest>=7.1.2 ; extra == 'tests' + - pytest-cov>=2.9.0 ; extra == 'tests' + - ruff>=0.11.7 ; extra == 'tests' + - mypy>=1.15 ; extra == 'tests' + - pyamg>=5.0.0 ; extra == 'tests' + - polars>=0.20.30 ; extra == 'tests' + - pyarrow>=12.0.0 ; extra == 'tests' + - numpydoc>=1.2.0 ; extra == 'tests' + - pooch>=1.8.0 ; extra == 'tests' + - conda-lock==3.0.1 ; extra == 'maintenance' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/63/1e/12fbf2a3bb240161651c94bb5cdd0eae5d4e8cc6eaeceb74ab07b12a753d/scipy-1.17.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl + name: scipy + version: 1.17.0 + sha256: 6680f2dfd4f6182e7d6db161344537da644d1cf85cf293f015c60a17ecf08752 + requires_dist: + - numpy>=1.26.4,<2.7 + - pytest>=8.0.0 ; extra == 'test' + - pytest-cov ; extra == 'test' + - pytest-timeout ; extra == 'test' + - pytest-xdist ; extra == 'test' + - asv ; extra == 'test' + - mpmath ; extra == 'test' + - gmpy2 ; extra == 'test' + - threadpoolctl ; extra == 'test' + - scikit-umfpack ; extra == 'test' + - pooch ; extra == 'test' + - hypothesis>=6.30 ; extra == 'test' + - array-api-strict>=2.3.1 ; extra == 'test' + - cython ; extra == 'test' + - meson ; extra == 'test' + - ninja ; sys_platform != 'emscripten' and extra == 'test' + - sphinx>=5.0.0,<8.2.0 ; extra == 'doc' + - intersphinx-registry ; extra == 'doc' + - pydata-sphinx-theme>=0.15.2 ; extra == 'doc' + - sphinx-copybutton ; extra == 'doc' + - sphinx-design>=0.4.0 ; extra == 'doc' + - matplotlib>=3.5 ; extra == 'doc' + - numpydoc ; extra == 'doc' + - jupytext ; extra == 'doc' + - myst-nb>=1.2.0 ; extra == 'doc' + - pooch ; extra == 'doc' + - jupyterlite-sphinx>=0.19.1 ; extra == 'doc' + - jupyterlite-pyodide-kernel ; extra == 'doc' + - linkify-it-py ; extra == 'doc' + - tabulate ; extra == 'doc' + - click<8.3.0 ; extra == 'dev' + - spin ; extra == 'dev' + - mypy==1.10.0 ; extra == 'dev' + - typing-extensions ; extra == 'dev' + - types-psutil ; extra == 'dev' + - pycodestyle ; extra == 'dev' + - ruff>=0.12.0 ; extra == 'dev' + - cython-lint>=0.12.2 ; extra == 'dev' + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: shapely + version: 2.1.2 + sha256: 7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6 + requires_dist: + - numpy>=1.21 + - pytest ; extra == 'test' + - pytest-cov ; extra == 'test' + - scipy-doctest ; extra == 'test' + - numpydoc==1.1.* ; extra == 'docs' + - matplotlib ; extra == 'docs' + - sphinx ; extra == 'docs' + - sphinx-book-theme ; extra == 'docs' + - sphinx-remove-toctrees ; extra == 'docs' + requires_python: '>=3.10' +- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + sha256: 458227f759d5e3fcec5d9b7acce54e10c9e1f4f4b7ec978f3bfd54ce4ee9853d + md5: 3339e3b65d58accf4ca4fb8748ab16b3 + depends: + - python >=3.9 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/six?source=hash-mapping + size: 18455 + timestamp: 1753199211006 +- conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda + sha256: 48f3f6a76c34b2cfe80de9ce7f2283ecb55d5ed47367ba91e8bb8104e12b8f11 + md5: 98b6c9dc80eb87b2519b97bcf7e578dd + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + - libgcc >=14 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 45829 + timestamp: 1762948049098 +- conda: https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda + sha256: d1e3e06b5cf26093047e63c8cc77b70d970411c5cbc0cb1fad461a8a8df599f7 + md5: 0401a17ae845fa72c7210e206ec5647d + depends: + - python >=3.9 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/sortedcontainers?source=hash-mapping + size: 28657 + timestamp: 1738440459037 +- pypi: https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl + name: stack-data + version: 0.6.3 + sha256: d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695 + requires_dist: + - executing>=1.2.0 + - asttokens>=2.1.0 + - pure-eval + - pytest ; extra == 'tests' + - typeguard ; extra == 'tests' + - pygments ; extra == 'tests' + - littleutils ; extra == 'tests' + - cython ; extra == 'tests' +- conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + sha256: 570da295d421661af487f1595045760526964f41471021056e993e73089e9c41 + md5: b1b505328da7a6b246787df4b5a49fbc + depends: + - asttokens + - executing + - pure_eval + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/stack-data?source=hash-mapping + size: 26988 + timestamp: 1733569565672 +- conda: https://conda.anaconda.org/conda-forge/noarch/tblib-3.2.2-pyhcf101f3_0.conda + sha256: 6b549360f687ee4d11bf85a6d6a276a30f9333df1857adb0fe785f0f8e9bcd60 + md5: f88bb644823094f436792f80fba3207e + depends: + - python >=3.10 + - python + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/tblib?source=hash-mapping + size: 19397 + timestamp: 1762956379123 +- pypi: https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl + name: threadpoolctl + version: 3.6.0 + sha256: 43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + sha256: cafeec44494f842ffeca27e9c8b0c27ed714f93ac77ddadc6aaf726b5554ebac + md5: cffd3bdd58090148f4cfcd831f4b26ab + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + constrains: + - xorg-libx11 >=1.8.12,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3301196 + timestamp: 1769460227866 +- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.0-pyhcf101f3_0.conda + sha256: 62940c563de45790ba0f076b9f2085a842a65662268b02dd136a8e9b1eaf47a8 + md5: 72e780e9aa2d0a3295f59b1874e3768b + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/tomli?source=compressed-mapping + size: 21453 + timestamp: 1768146676791 +- conda: https://conda.anaconda.org/conda-forge/noarch/toolz-1.1.0-pyhd8ed1ab_1.conda + sha256: 4e379e1c18befb134247f56021fdf18e112fb35e64dd1691858b0a0f3bea9a45 + md5: c07a6153f8306e45794774cf9b13bd32 + depends: + - python >=3.10 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/toolz?source=hash-mapping + size: 53978 + timestamp: 1760707830681 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.3-py313h07c4f96_0.conda + sha256: 6006d4e5a6ff99be052c939e43adee844a38f2dc148f44a7c11aa0011fd3d811 + md5: 82da2dcf1ea3e298f2557b50459809e0 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/tornado?source=hash-mapping + size: 878109 + timestamp: 1765458900582 +- pypi: https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl + name: tqdm + version: 4.67.3 + sha256: ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf + requires_dist: + - colorama ; sys_platform == 'win32' + - importlib-metadata ; python_full_version < '3.8' + - pytest>=6 ; extra == 'dev' + - pytest-cov ; extra == 'dev' + - pytest-timeout ; extra == 'dev' + - pytest-asyncio>=0.24 ; extra == 'dev' + - nbval ; extra == 'dev' + - requests ; extra == 'discord' + - slack-sdk ; extra == 'slack' + - requests ; extra == 'telegram' + - ipywidgets>=6 ; extra == 'notebook' + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl + name: traitlets + version: 5.14.3 + sha256: b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f + requires_dist: + - myst-parser ; extra == 'docs' + - pydata-sphinx-theme ; extra == 'docs' + - sphinx ; extra == 'docs' + - argcomplete>=3.0.3 ; extra == 'test' + - mypy>=1.7.0 ; extra == 'test' + - pre-commit ; extra == 'test' + - pytest-mock ; extra == 'test' + - pytest-mypy-testing ; extra == 'test' + - pytest>=7.0,<8.2 ; extra == 'test' + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + sha256: f39a5620c6e8e9e98357507262a7869de2ae8cc07da8b7f84e517c9fd6c2b959 + md5: 019a7385be9af33791c989871317e1ed + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/traitlets?source=hash-mapping + size: 110051 + timestamp: 1733367480074 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + sha256: 032271135bca55aeb156cee361c81350c6f3fb203f57d024d7e5a1fc9ef18731 + md5: 0caa1af407ecff61170c9437a808404d + depends: + - python >=3.10 + - python + license: PSF-2.0 + license_family: PSF + purls: + - pkg:pypi/typing-extensions?source=hash-mapping + size: 51692 + timestamp: 1756220668932 +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + sha256: 1d30098909076af33a35017eed6f2953af1c769e273a0626a04722ac4acaba3c + md5: ad659d0a2b3e47e38d829aa8cad2d610 + license: LicenseRef-Public-Domain + purls: [] + size: 119135 + timestamp: 1767016325805 +- pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + name: urllib3 + version: 2.6.3 + sha256: bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 + requires_dist: + - brotli>=1.2.0 ; platform_python_implementation == 'CPython' and extra == 'brotli' + - brotlicffi>=1.2.0.0 ; platform_python_implementation != 'CPython' and extra == 'brotli' + - h2>=4,<5 ; extra == 'h2' + - pysocks>=1.5.6,!=1.5.7,<2.0 ; extra == 'socks' + - backports-zstd>=1.0.0 ; python_full_version < '3.14' and extra == 'zstd' + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + sha256: af641ca7ab0c64525a96fd9ad3081b0f5bcf5d1cbb091afb3f6ed5a9eee6111a + md5: 9272daa869e03efe68833e3dc7a02130 + depends: + - backports.zstd >=1.0.0 + - brotli-python >=1.2.0 + - h2 >=4,<5 + - pysocks >=1.5.6,<2.0,!=1.5.7 + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/urllib3?source=hash-mapping + size: 103172 + timestamp: 1767817860341 +- pypi: https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl + name: wcwidth + version: 0.6.0 + sha256: 1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.6.0-pyhd8ed1ab_0.conda + sha256: e298b508b2473c4227206800dfb14c39e4b14fd79d4636132e9e1e4244cdf4aa + md5: c3197f8c0d5b955c904616b716aca093 + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/wcwidth?source=compressed-mapping + size: 71550 + timestamp: 1770634638503 +- conda: https://conda.anaconda.org/conda-forge/noarch/xarray-2026.1.0-pyhcf101f3_0.conda + sha256: 878d190db1a78f1e3fe90497e053a0dc0941937e82378cc990f43115ffe2bee6 + md5: 397276eff153e81b0e7128acc56deb32 + depends: + - python >=3.11 + - numpy >=1.26 + - packaging >=24.1 + - pandas >=2.2 + - python + constrains: + - bottleneck >=1.4 + - cartopy >=0.23 + - cftime >=1.6 + - dask-core >=2024.6 + - distributed >=2024.6 + - flox >=0.9 + - h5netcdf >=1.3 + - h5py >=3.11 + - hdf5 >=1.14 + - iris >=3.9 + - matplotlib-base >=3.8 + - nc-time-axis >=1.4 + - netcdf4 >=1.6.0 + - numba >=0.60 + - numbagg >=0.8 + - pint >=0.24 + - pydap >=3.5.0 + - scipy >=1.13 + - seaborn-base >=0.13 + - sparse >=0.15 + - toolz >=0.12 + - zarr >=2.18 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/xarray?source=compressed-mapping + size: 1010206 + timestamp: 1769665430320 +- conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + sha256: 6d9ea2f731e284e9316d95fa61869fe7bbba33df7929f82693c121022810f4ad + md5: a77f85f77be52ff59391544bfe73390a + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + license: MIT + license_family: MIT + purls: [] + size: 85189 + timestamp: 1753484064210 +- conda: https://conda.anaconda.org/conda-forge/noarch/zict-3.0.0-pyhd8ed1ab_1.conda + sha256: 5488542dceeb9f2874e726646548ecc5608060934d6f9ceaa7c6a48c61f9cc8d + md5: e52c2ef711ccf31bb7f70ca87d144b9e + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/zict?source=hash-mapping + size: 36341 + timestamp: 1733261642963 +- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhcf101f3_1.conda + sha256: b4533f7d9efc976511a73ef7d4a2473406d7f4c750884be8e8620b0ce70f4dae + md5: 30cd29cb87d819caead4d55184c1d115 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/zipp?source=hash-mapping + size: 24194 + timestamp: 1764460141901 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.1-hb9d3cd8_2.conda + sha256: 5d7c0e5f0005f74112a34a7425179f4eb6e73c92f5d109e6af4ddeca407c92ab + md5: c9f075ab2f33b3bbee9e62d4ad0a6cd8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libzlib 1.3.1 hb9d3cd8_2 + license: Zlib + license_family: Other + purls: [] + size: 92286 + timestamp: 1727963153079 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + sha256: 68f0206ca6e98fea941e5717cec780ed2873ffabc0e1ed34428c061e2c6268c7 + md5: 4a13eeac0b5c8e5b8ab496e6c4ddd829 + depends: + - __glibc >=2.17,<3.0.a0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 601375 + timestamp: 1764777111296 diff --git a/packages/bundled_models/persistence/pyproject.toml b/packages/bundled_models/persistence/pyproject.toml new file mode 100644 index 00000000..0e487cea --- /dev/null +++ b/packages/bundled_models/persistence/pyproject.toml @@ -0,0 +1,91 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "pyearthtools-persistence" +version = "0.6.0" +description = "Persistence Bundled Model" +readme = "README.md" +requires-python = ">=3.11, <3.14" +keywords = ["persistence", "pyearthtools", "models"] +maintainers = [ + {name = "Tennessee Leeuwenburg", email = "tennessee.leeuwenburg@bom.gov.au"}, + {name = "Nikeeth Ramanathan", email = "nikeeth.ramanathan@gmail.com"}, +] +classifiers = [ + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + 'pyearthtools.zoo>=0.5.0', + 'pyearthtools.data>=0.5.0', + 'pyearthtools.pipeline>=0.5.0', + 'hydra-core', +] +[dependency-groups] +dev = [ + "pytest>=8.4.2", + "ruff", + "pytest-cov", + "pytest-xdist", +] + +[project.urls] +homepage = "https://pyearthtools.readthedocs.io/" +documentation = "https://pyearthtools.readthedocs.io/" +repository = "https://github.com/ACCESS-Community-Hub/PyEarthTools" + +[project.entry-points."pyearthtools.zoo.model"] +Global_PERSIST = "persistence.registered_model:Persistence" + +[tool.isort] +profile = "black" + +[tool.black] +line-length = 120 + +[tool.mypy] +warn_return_any = true +warn_unused_configs = true + +[[tool.mypy.overrides]] +ignore_missing_imports = true + +[tool.hatch.version] +path = "src/persistence/__init__.py" + +[tool.hatch.build.targets.wheel] +packages = ["src/pyearthtools/"] + +[tool.pixi.workspace] +channels = ["conda-forge"] +platforms = ["linux-64"] + +[tool.pixi.pypi-dependencies] +pyearthtools-persistence = { path = ".", editable = true } + +[tool.pixi.tasks] + +[tool.pixi.dependencies] +python = ">=3.11,<3.14" +xarray = ">=2026.1.0,<2027" + +[tool.pixi.feature.testing.dependencies] +pytest = ">=9.0.2,<10" +pytest-cov = ">=7.0.0,<8" +pytest-xdist = ">=3.8.0,<4" +ruff = ">=0.15.0,<0.16" +ipython = ">=9.10.0,<10" + +[tool.pixi.feature.dask.dependencies] +dask-core = "*" +distributed = "*" +pyarrow = ">=23.0.0,<24" + +[tool.pixi.environments] +dask = ["dask"] +dev = ["dask", "testing"] diff --git a/packages/bundled_models/persistence/src/persistence/__init__.py b/packages/bundled_models/persistence/src/persistence/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/packages/bundled_models/persistence/src/persistence/config/dask.py b/packages/bundled_models/persistence/src/persistence/config/dask.py new file mode 100644 index 00000000..ea6a751d --- /dev/null +++ b/packages/bundled_models/persistence/src/persistence/config/dask.py @@ -0,0 +1,41 @@ +from contextlib import contextmanager + + +# default scheduler string to set "single-threaded" mode. +_STR_DASK_SYNC_SCHEDULER = "synchronous" + + +@contextmanager +def _set_synchronous_dask(): + """ + Wrapper to set `dask` to single-threaded mode. Note: "single-threaded" in `dask`-land + (specifically) is the same as "synchronous". + + This handles the case where dask is _not_ installed. In which case it does a pass-through. + + IMPORTANT: never nest this context manager or call dask.config.reset() or attempt to update any + configs inside this context. Doing so may invalidate the "synchronous" setting. + + Example: + def do_stuff(...): + # I can now (optionally) fork other processes here - without confusing dask. + # IMPORTANT: I shouldn't try to reintroduce parallelism using dask here + ... + + with _set_synchronous_dask(): + do_stuff(...) + """ + try: + # this import order is important for the "distributed" configs to be recognized + import dask + import dask.config + + # NOTE: if you don't have dask.distributed, this setting may not work as intended. + # so you will have to manually deal with it in the compute level. + import dask.distributed + + # set state to desired config + with dask.config.set(scheduler=_STR_DASK_SYNC_SCHEDULER): + yield + except ImportError: + yield diff --git a/packages/bundled_models/persistence/src/persistence/interface/__init__.py b/packages/bundled_models/persistence/src/persistence/interface/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/packages/bundled_models/persistence/src/persistence/interface/_backend.py b/packages/bundled_models/persistence/src/persistence/interface/_backend.py new file mode 100644 index 00000000..997c6149 --- /dev/null +++ b/packages/bundled_models/persistence/src/persistence/interface/_backend.py @@ -0,0 +1,65 @@ +from enum import StrEnum, auto + + +class PersistenceBackendType(StrEnum): + """ + Enumeration of supported compute backends for persistence computations. + + --- + + SUPPORTED BACKENDS (as of 2026-02-28): + - NUMPY (20260228) + - others are WIP + + Note: "supported" implies that the backend is supported by the build system, it does not imply + that the particular persistence method itself is supported for that backend. + + --- + + Backends are configured at the "build" level in pyproject.toml, e.g. for rust this may be + maturin/pyO3, which usually handles most of the heavy lifting. + + numba might require certain system dependencies - e.g. llvm, to function since it requires + building on the fly. + + For C/zig this would involve using: + a. ziglang/zig-pypi to build the zig packages into wheels and running them on the fly using + sys.execute to execute the wheel as a module, building/running zig on-the-fly. Avoids + having to distribute the pre-built dependencies, but may not work well with specific + interfaces like `numpy`. + b. using setuptools-zig to build them into a "integrated" library and packaging the build + into the wheel/distribution + c. using cffi or ctypes. + + Methods a. and b. would require extending Python.h directly, and hence are preferrable, since + they don't involve foreign calls. Unlike numba, method a. exists for zig where jit compilation + can happen without dependency on additional system libraries. + + All of the above methods generally avoid (or at least have the ability to avoid) the need for + conda environments and are pretty light weight. + """ + + C = "c" + C_ZIG = "zig" + NUMBA = "numba" + NUMPY = "numpy" + RUST = "rust" + UNKNOWN = auto() + + def check_support(self): + """ + As per the module documentation, this method only tells you if a particular backend is + supported by the *build system*, it doesn't imply that the backend is useable for any given + method. + + Therefore, this check can and should be done as early as possible. Whereas method + compatiblilty will be checked later into the runtime but still early enough point in the + code, before attempting the computation. (see `PersistenceCompute` for more details) + """ + match self: + case PersistenceBackendType.NUMPY: + return + case _: + raise NotImplementedError( + f"PersistenceBackendType: {self} is not supported" + ) diff --git a/packages/bundled_models/persistence/src/persistence/interface/_chunker.py b/packages/bundled_models/persistence/src/persistence/interface/_chunker.py new file mode 100644 index 00000000..c1a5979d --- /dev/null +++ b/packages/bundled_models/persistence/src/persistence/interface/_chunker.py @@ -0,0 +1,415 @@ +from dataclasses import dataclass +import math +import numpy as np +import xarray as xr +import functools + +from typing import Generator + +from persistence.interface._metadata import PersistenceMetadata +from persistence.types import PetDataArrayLike + +# --- +# 1000 chunks is more than enough for most usecases. Persistence methods should not be using large +# amounts of historical data, and therefore should not need heavy chunking for data to fit in +# memory. +# +# If memory is an issue, this needs to be solved at a higher level where properties of the chunk +# strategy at the storage level are known and data can be optimally bounded (spatially or otherwise) +# before reaching the persistence chunker. +# +# Further the minimum memory usage is lower bounded by an entire single time slice of the of the +# data being processed since that is the output, and also is affected by the number of parallel +# workers used. +# +# Increasing chunk counts past a certain certain amount is therefore counter-productive. +_MAX_NUM_CHUNKS = 1000 +# --- + + +@dataclass +class PersistenceChunkInfo: + # --- + # least significant chunk index (fastest varying), most significant is 0, indices are + # incremented from least significant (fast) to most significant (slow) + lsi_chunk: int + # --- + num_chunks: int + size_chunk: int + dim_names: list[str] + shape_full: list[int] + + +@dataclass +class PersistenceDataChunk: + """ + The reason this is a class is that, there could be more useful info here in the future such as + start/end slices, and the chunk identifier, but for now its just a shallow wrapper and + effectively a type alias. + """ + + arr_chunk: np.ndarray + + # chunks are calculated independently and in different workers so a reference + # to the metadata is convenient. This is a small over-head. + metadata: PersistenceMetadata + + # list containing slices of each dimension that make up the chunk + slice_dims: list[slice] + + +@dataclass +class PersistenceChunker: + """ + The persistence chunker chunks a xarray dataarray and relays them using a generator (lazy). + + Important: + + This is not a general purpose chunker. It is tailored for persistence and has a critical + assumption that the time dimension will not be chunked during the computation (it may still + be chunked in storage - this is fine). The chunking strategy is also intentionally + simplistic and greedy. + + Depending on the method we could require 1 historical entry or 200. Therefore, there is no + "optimal" choice of chunks and workers here, since the data is not guaranteed to be stored + optimally for every choice of persistence method. The reason why persistence is so much + different to other models, is because we aren't storing any weights everything is done + on-the-fly. + + Hence, if there are issues with memory, the solution should be at a higher level where the + chunking strategy of the stored data is known, and appropriately bounded or alternatively + prepared offline with a storage strategy conducive to persistence calculations, BEFORE being + passed into this chunker. This may introduce a storage burden, but is imperative for any + sort of baseline model that cannot rely on stored weights, to function. + + The chunking algorithm is as follows: + + Divide the total size (product of the data shape) by the desired number of chunks (rounded + up, min chunk size = 1). This is the desired chunk size. + + Working backwards from the fastest varying index/axis/dimension (len - 1), find if the + desired chunk size is greater tha the product of the cardinality any slower varying indices. + (natural element = 1) e.g. + + product[len - 1] = 1 + product[len - 2] = shape[len - 1] * product[len - 1] + product[len - 3] = shape[len - 2] * product[len - 2] + ... + + If the chunk size is smaller than the product, stop. Create a marker at this index - call it + the "stop" index (i.e. the most significant index used for the chunk size calculation). The + product at the given iteration is the _actual_ chunk size. + + Then, for all indices that are more significant the "stop" index, increment it as a multi + index ring to find the start and end indices of the hyperslab. + + In other words, the chunks are designed in such a way that indices that are faster varying + than the "stop" index are always at their cardinality (max size), and slower varying indices + are incremented and used for selection. Increments are over the fastest of the slowest + varying index (i.e. fastest most significant index). + + Note: the time index is a special case and should be ignored. + + Note: the most significant index is the slowest varying index and the least significant + index is the fastest varying index. i.e. + + x[i0,...] v.s. x[...,iN] => i0 is slow varying, iN is fast varying. + + Note: It is *usually* more efficient to to increment chunks by the slower varying indices - + as this *usually* guarentees that the chunks are contiguous in memory (C-style). But + for updating individual values in a chunk the opposite is true. i.e. traversing chunks + v.s. traversing elements. Here we want the former for chunking, and the latter for + computation. Which is why we chunk with slower varying indices and compute with faster + varying indices (with whatever backend of choice). + + Note: The reason why dask isn't used (or at least forced into synchronous mode), + is because its configuration in PET (but possibly in general) is hard to pin down. + + Note: we could have used numpy.nditer with a external loop, but we would like to keep the + structure of the array and not flatten it. Further, we are only dealing with a max of + 1000 so any benefit would be minimal. + + FUTUREWORK: The loaders should present options to use direct mechanisms to load particular + types of data rather than xarray. For now this class has no control over the data loader. + """ + + da: xr.DataArray + metadata: PersistenceMetadata + chunk_info: PersistenceChunkInfo | None = None + + # TODO: + # add data shape as an explicit input, as even da.shape may trigger a computation depending on + # the underlying storage type. + + @staticmethod + def _b10_to_mi(b10: int, mi_size: list[int]) -> list[int]: + """ + Given: + 1. a base10 (integer) representation of the product of a multiindex + 2. a list of the cardinality of each index (size of each index) + convert the base10 representation of a multiindex back to a multiindex. + """ + assert b10 >= 0 + assert all([x is not None and x >= 0 for x in mi_size]) + + rem = b10 # set remainder to the orignal base10 value + + # incrementing the most significant shifts the hyperslab by the product of the size of every + # other index after it. This is a running product that is the "base" of a given multi index. + # the least significant index will have a base of 1. + mi_sizeshift = mi_size[1:] + [1] + prod = functools.reduce(lambda x, y: x * y, mi_sizeshift) + + num_idx = len(mi_size) # number of indices + mi = [None for i in range(num_idx)] # initialize multi index to return + + for i, s in enumerate(mi_sizeshift): + # calculate quotient/remainder + quo, rem = divmod(rem, prod) + + # update multi-index forwards (most-significant first) + mi[i] = quo + + # update product by reverting the most recent size (i.e. divide) the minimum product + # must be one. + prod = max(prod // s, 1) + + assert all([x is not None and x >= 0 for x in mi]) + assert len(mi) == len(mi_size) + return mi + + @staticmethod + def _mi_to_b10(mi: list[int], mi_size: list[int]) -> int: + """ + Given: + 1. a list of indices (for each dimension) + 2. a list of the cardinality of each index (size of each index) + convert the multiindex (1.) into a base10 (integer) representation. + """ + assert len(mi) == len(mi_size) + assert all([x is not None and x >= 0 for x in mi]) + assert all([x is not None and x >= 0 for x in mi_size]) + + prodscan = 1 # running accumulation of product + b10 = 0 # calculated using prodsum + + # need to reverse arrays since least significant needs to be computed first + for i, v in enumerate(zip(mi[::-1], mi_size[::-1])): + ix, s = v + b10 += ix * prodscan + # update product with latest size + prodscan *= s + + assert b10 >= 0 + return b10 + + @staticmethod + def _inc_mi(mi: list[int], mi_size: list[int], inc=1) -> list[int]: + """ + Increments a multindex by 1, note: this is the inefficient way, but it doesn't need to be + efficient - chunk sizes are hard capped to 1000. Note: the fastest varying index (last + index) is incremented first since that minimizes cache misses. + + Algorithm: + + Convert multi index to base10, then + add 1 to base10 value (or inc if specified) - trivial increment, then + convert back to multiindex + """ + assert inc > 0 + assert len(mi) == len(mi_size) + assert all([x is not None and x >= 0 for x in mi]) + assert all([x is not None and x >= 0 for x in mi_size]) + + fn_b10 = functools.partial(PersistenceChunker._mi_to_b10, mi_size=mi_size) + fn_b10_inv = functools.partial(PersistenceChunker._b10_to_mi, mi_size=mi_size) + mi_next = fn_b10_inv(fn_b10(mi) + inc) + + if mi_next[0] >= mi_size[0]: + raise OverflowError( + f"PersistenceChunker: increment multindex - overflow {mi} + {inc} goes past the" + f" maximum sizes: {mi_size}." + ) + + assert all( + [x is not None and x >= 0 and x < s for x, s in zip(mi_next, mi_size)] + ) + return mi_next + + @staticmethod + def _compute_chunkinfo_greedy( + desired_numchunks: int, + mi_size: list[int], + dim_names: list[str], + ) -> PersistenceChunkInfo: + """ + This is a greedy chunksize calculation, because it prefers having entire dimensions as part + of a chunk rather than partial extents in a dimension. Although this is the only chunking + strategy that will be conceivably used in the near future. + + Returns a structure (PersistenceChunkInfo) containing + 1. actual chunk size + 2. actual chunk count + 3. the position (least significant) of the first index that should be be used for + incrementing chunks (using multi-indexing) + 4. dimension names (passed through) + """ + assert desired_numchunks >= 1 + + if isinstance(mi_size, tuple): + mi_size = list(mi_size) + + total_size = functools.reduce(lambda x, y: x * y, mi_size) + desired_chunksize = int(max(1, math.ceil(total_size / desired_numchunks))) + + num_idx = len(mi_size) + prodsize = 1 + actual_chunksize = None + first_chunkindex = None + + for i, s in enumerate(mi_size[::-1]): + if prodsize >= desired_chunksize and s != 1: + first_chunkindex = num_idx - i - 1 + actual_chunksize = prodsize + break + prodsize *= s + + # single chunk + if first_chunkindex is None or actual_chunksize is None: + actual_chunksize = prodsize + actual_numchunks = 1 + first_chunkindex = 0 + + actual_numchunks = total_size // actual_chunksize + + assert actual_chunksize >= desired_chunksize + assert actual_numchunks <= desired_numchunks + + return PersistenceChunkInfo( + num_chunks=actual_numchunks, + size_chunk=actual_chunksize, + lsi_chunk=first_chunkindex, + dim_names=dim_names, + shape_full=mi_size, + ) + + def __post_init__(self): + # safety: don't want assume sets or dict keys because they may be unordered (depending on + # the version of python). However, most likely, dict is okay as long as we don't support + # python<=3.7 + assert isinstance(self.da.dims, tuple) or isinstance(self.da.dims, list) + + # check for chunks + if ( + self.metadata.num_chunks_desired < 1 + or self.metadata.num_chunks_desired > _MAX_NUM_CHUNKS + ): + err_msg = f"specified num chunks is invalid, valid range: 0 < num chunks <= {_MAX_NUM_CHUNKS}" + raise ValueError(err_msg) + + # --- + # Suppress time index for calculations. + # + # NOTE: + # + # Expanding an array by one dimension with a dimensionality 1, for example, has no impact + # on the chunk size, since the retraction operation of squeezing out the dimension, of + # size 1, also does not affect chunk size. Therefore, to suppress a dimension we set its + # size to 1 or drop it. Forcing to 0 is not right here, since that'd result in a empty array. + # + # Since we want to preserve structure, we can't drop it so our only remaining option is to + # force the size to 1. + shape_notime = list(self.da.shape) + shape_notime[self.metadata.idx_time_dim] = 1 + # --- + + self.chunk_info = self._compute_chunkinfo_greedy( + self.metadata.num_chunks_desired, + shape_notime, + self.da.dims, + ) + + # check that the input data shape has enough time indices to support the persistence + # calculation (including preprocessing). + len_time_max = self.da.shape[self.metadata.idx_time_dim] + len_time_prp = self.metadata.len_time_preprocess() + if len_time_prp > len_time_max: + raise ValueError( + "PersistenceChunker: input DataArray does have enough time indices for this" + " persistence method." + ) + + def _get_dim_slices(self, mi: list[int]) -> dict[str, slice]: + """ + maps slices to dimension names. + + 1. slices time based on required number of historical data for imputation/persistence + calculations. + + NOTE: + + This is an added safety, since it is expected that something higher level would have + sliced this by now. But, in case the data-array points (lazily) to the entire history + (for example), this slicing makes certain that the data that is loaded into memory is + still reasonably bounded. + + 2. slices other indices based on required chunk sizes + """ + assert self.chunk_info is not None and self.chunk_info.lsi_chunk is not None + assert all([x is not None and x >= 0 for x in mi]) + + dict_slice_dims = {} + len_time_max = self.da.shape[self.metadata.idx_time_dim] + len_time_prp = self.metadata.len_time_preprocess() + # this is static for all chunks + slice_time = slice(len_time_max - len_time_prp, len_time_max) + + for idx, name in enumerate(self.da.dims): + dim_size = self.da.shape[idx] + + # time dimension => use special time slicing + if idx == self.metadata.idx_time_dim: + # assert time dimension name is stored correctly - random safety check + assert name == self.chunk_info.dim_names[self.metadata.idx_time_dim] + dict_slice_dims[name] = slice_time + + # multi-indexer dimension => 1^m slice => incremental chunk of size 1 + elif idx < self.chunk_info.lsi_chunk + 1: + dict_slice_dims[name] = slice(mi[idx], mi[idx] + 1) + + # chunk dimension => N_i^(n-m) slice => use the entire dimension as a chunk (N_i) + else: + dict_slice_dims[name] = slice(0, dim_size) + + assert all(n in dict_slice_dims for n in self.chunk_info.dim_names) + return dict_slice_dims + + def generate_chunks(self) -> Generator[PersistenceDataChunk]: + """ + Evaluate chunks by loading each chunk into memory, the chunks are lazily loaded but eagerly + evaluated in memory in the backend. Chunks should ideally be contiguous in memory. (Except + for time). + + This generator generally would be fed into a multiprocessing worker pool in conjunction with + a method to process each chunk. + """ + # TODO: add a fast return for the special case when time is the only dimension. + shape_notime = list(self.da.shape) + shape_notime[self.metadata.idx_time_dim] = 1 + shape_notime_trimmed = shape_notime[: (self.chunk_info.lsi_chunk + 1)] + mi_inc = [0 for _ in shape_notime_trimmed] + + for _ in range(self.chunk_info.num_chunks): + dict_slice_dims = self._get_dim_slices(mi_inc) + arr_chunk = self.da.isel(dict_slice_dims) + + # pass chunk to caller + yield PersistenceDataChunk( + arr_chunk, self.metadata, list(dict_slice_dims.values()) + ) + + # increment index and break if overflow is detected. + try: + mi_inc = self._inc_mi(mi_inc, mi_size=shape_notime_trimmed) + except OverflowError: + return diff --git a/packages/bundled_models/persistence/src/persistence/interface/_compute.py b/packages/bundled_models/persistence/src/persistence/interface/_compute.py new file mode 100644 index 00000000..f1ba2602 --- /dev/null +++ b/packages/bundled_models/persistence/src/persistence/interface/_compute.py @@ -0,0 +1,272 @@ +import concurrent.futures +import multiprocessing +from enum import StrEnum, auto +from dataclasses import dataclass, field +from collections.abc import Callable +from contextlib import contextmanager +from typing import Union, Generator +from collections import namedtuple + +import numpy as np +import xarray as xr + +from persistence.types import PetDataArrayLike +from persistence.methods._impute import SimpleImpute +from persistence.methods._median import _median_of_three_numpy +from persistence.interface._metadata import PersistenceMetadata +from persistence.interface._method import PersistenceMethod +from persistence.interface._chunker import ( + PersistenceDataChunk, + PersistenceChunker, + PersistenceChunkInfo, +) +from persistence.interface._backend import PersistenceBackendType + + +ChunkResult = namedtuple("ChunkResult", ["array", "slice_dims"]) + + +@dataclass +class PersistenceComputePool: + """ + Generates a compute pool and uses the given chunk genarator along with the configured method to + perform the computations. + + Joins the chunks back together at completion according to the FIFO order. + + Computation here happens at a lower structural level (numpy or chosen system backend). + + --- + + Algorithm (see `compute_chunks`): + + 1. retrieve chunks (numpy arrays) + 2. perform compute on each chunk depending on the persistence method + 3. join numy arrays -> will be of the form + + for i in nd-index: + + arr[x0, x1, x2, ..., t, ...] + = arr[x0, x1, x2, ...] + = slab + + OR + + arr[x0, x1, t, x2, ...] + = arr[x0, x1, 1, x2, ...] + = slab + + here, x0, x1, x2 are the multi-indices that are incremented when filling in the slabs. + + Because the persistence methods all reduce the time index to a cardinality of 1, both of + these scenarios are equally efficient. + 4. use the stored data-array information (shapes/dimnames) + + --- + + Further, to reiterate the assumption, in persistence methods chunks are loaded lazily, but + evaluated eagerly, in otherwords the computation itself should not use `dask`. And loading is + forced to be synchronous e.g. + + load chunk 1 ---> compute [worker 1] + | finish compute + *>>> load chunk 2 ---> compute [worker 2] + | + *>>> load chunk 3 ---> compute [worker 3] + |---> at this point, we should only have: + - two chunks in memory with multiple time indices + - one "result" chunk with the reduced time dimension + + *>>> the time taken to load a chunk into memory + + Keep the above in mind when running this program, as it may help to debug issues. + Any scheduling/wait time implementation is out of scope here, and in fact is an anti-pattern. + + (This does not mean scheduling cannot be used - it just needs to be used at a higher level and + at a distributed compute level - NOT at a single node compute level) + + --- + + Important: + + - As per the rest of the persistence structures, the time dimension existing is crucial, and the + time dimension is what is aggregated over, and therfore not chunked. It is instead simpler to + act on, and chunk the embarassingly parallel independent dimensions (e.g. spatial dimensions). + + - Persistence computation is single-variate, it may in the future infer something from the + dimensionality, but it may not infer information from other variables. + + - In other words, coordinate information may be considered, but not other variables in a + provided dataset. Therefore, the absolute highest level structure returnable by this + computation a DataArray. + + - The reason for this is that multi-variate persistence models are an anti-pattern, since + persistence models inherently shouldn't do any inference, physics, or _parametric_ statistical + learning. Unparamaterized methods, i.e. methods that do NOT use knowledge of what the + coordinates or other variables represent - other than the trivial inference that they are + different dimensions and have a certain shape, are okay. + + --- + + Future considerations: + + - There could be methods in the future that aggregate based on neighbouring dimensions, in such + a scenario, the computation is still parameterless, but the methods could derive additional + statistical patterns and "state" parameters that could improve performance. This may cause + some non-determinism based on how chunks are chosen. + + - However, as long as these filters are semi bounded - e.g. "9 parameter savitzky golay filter", + then there is a guarantee that despte how large the chunks are the maximum number of + neighbouring parameters used in any "smarts" is 9 - spatially this could be a convolutional + 3x3 grid for example doing some smoothing or noise inference. And therefore, maintain some + level of determinism as long as the chunk sizes don't fall below this criteria. + + - Regardless, `PersistenceMetadata` and `PersistenceChunkInfo` are easily serialisable + structures that can be logged as part as experiments. + + - For now the only independent parameter that is known by the algorithms, is the time dimension. + """ + + chunk_generator: Generator[PersistenceDataChunk] # the chunks used for computation + chunk_info: PersistenceChunkInfo + metadata: PersistenceMetadata + + @staticmethod + def _job_wrapper(chunk: PersistenceDataChunk) -> ChunkResult: + """ + This wrapper needs to be static, as we may not want the state info of + this class to propagate. + """ + return ChunkResult( + array=PersistenceCompute(chunk.arr_chunk, chunk.metadata).compute(), + slice_dims=chunk.slice_dims, + ) + + def map_and_join_chunks(self) -> xr.DataArray: + """ + 1. Send chunks to workers + 2. Each worker runs the jobwrapper which invokes the configured persistence method + 3. Join the resulting list of numpy results along the time dimension + 4. Re-insert dimension names from chunk_info + + TODO: this should only be called via a main guard or entrypoint + + Calling forkserver preload and early inheriting any modules that may be forked is a + desirable way to call this, if multi-platform compatiblity is needed: + + e.g. + + if __name__ == "__main__": + ctx = multiprocessing.get_context("fork_server") + ctx.set_forkserver_preload(["module_name", "__main__"]) + args = parse_args(...) + generator = build_generator(args) + + with concurrent.futures.ProcessPoolExecutor(..., mp_context=ctx) as exec: + res = exec.map(fn, iter(generator)) + # do stuff with result + """ + # compute result shape by suppressing the time dimension + shape_res = [ + v if i != self.metadata.idx_time_dim else 1 + for i, v in enumerate(self.chunk_info.shape_full) + ] + arr_res = np.empty(shape_res) + + if self.metadata.num_workers <= 1: + # loop through instead + for chunk in iter(self.chunk_generator): + arr_res_chunk = PersistenceComputePool._job_wrapper(chunk) + arr_res[chunk.slice_dims] = arr_res_chunk + else: + # dispatch chunks to workers + # TODO: forkserver does/may not work with windows/mac, unless main-guarded + with concurrent.futures.ProcessPoolExecutor( + self.metadata.num_workers, + mp_context=multiprocessing.get_context("forkserver"), + ) as pp_exec: + results = pp_exec.map( + PersistenceComputePool._job_wrapper, iter(self.chunk_generator) + ) + for res_chunk in iter(results): + arr_res[*res_chunk.slice_dims] = res_chunk.array + + da_res = xr.DataArray(arr_res, dims=self.chunk_info.dim_names) + + return da_res + + +# TODO: the variable references are not right - need to use self.metadata +@dataclass +class PersistenceCompute: + arr: PetDataArrayLike + metadata: PersistenceMetadata + + def _method_impl(self, arr: np.ndarray) -> np.ndarray: + match self.metadata.backend: + case PersistenceBackendType.NUMPY: + return self._method_impl_numpy(arr) + case PersistenceBackendType.NUMBA: + return self._method_impl_numba(arr) + case PersistenceBackendType.RUST: + return self._method_impl_rust(arr) + case _: + raise NotImplementedError("PersistenceCompute: Unknown backend") + + def _method_impl_numpy(self, arr: np.ndarray) -> np.ndarray: + match self.metadata.method: + case PersistenceMethod.MEDIAN_OF_THREE: + return _median_of_three_numpy(arr, self.metadata.idx_time_dim) + case PersistenceMethod.MOST_RECENT: + raise NotImplementedError("TODO") + case _: + raise NotImplementedError( + f"PersistenceCompute: compute method {self.method} has not been implemented" + ) + + def _method_impl_numba(self, arr: np.ndarray) -> np.ndarray: + raise NotImplementedError("numba backend is not supported") + + def _method_impl_rust(self, arr: np.ndarray) -> np.ndarray: + raise NotImplementedError("rust backend is not supported") + + def _slice_time(self, arr: np.ndarray) -> np.ndarray: + """ + Further slices the data chunk into a smaller chunk required for the computation (usually + after imputation. + """ + # slice out data required for the computation + len_time_max = arr.shape[self.metadata.idx_time_dim] + len_time_cmp = self.metadata.len_time_compute() + arr_sliced = np.take( + arr, + range(len_time_max - len_time_cmp, len_time_max), + axis=self.metadata.idx_time_dim, + ) + + return arr_sliced + + def _impute(self, arr: np.ndarray) -> np.ndarray: + # default to pass-through + arr_imputed = arr + + if self.metadata.do_impute: + imputer = SimpleImpute(arr) + arr_imputed = imputer.impute_mean() + + return arr_imputed + + def compute(self) -> np.ndarray: + # check backend support + self.metadata.backend.check_support() + + # slice: to num_lookback indices + arr_sliced: np.ndarray = self._slice_time(self.arr) + + # impute: fill missing values + arr_imputed: np.ndarray = self._impute(arr_sliced) + + # compute: using specified persistence method and preprocessed array + arr_persist: np.ndarray = self._method_impl(arr_imputed) + + return arr_persist diff --git a/packages/bundled_models/persistence/src/persistence/interface/_interface.py b/packages/bundled_models/persistence/src/persistence/interface/_interface.py new file mode 100644 index 00000000..e60b9208 --- /dev/null +++ b/packages/bundled_models/persistence/src/persistence/interface/_interface.py @@ -0,0 +1,6 @@ +""" +Module that contains the interface required to "hook" into other pipeline methods in order to run +Persistence as a model. +""" +# TODO: this is no longer required, as it has been disected into separate modules. +# "persistence_impl.py" will instead be the actual interface into the computation. diff --git a/packages/bundled_models/persistence/src/persistence/interface/_metadata.py b/packages/bundled_models/persistence/src/persistence/interface/_metadata.py new file mode 100644 index 00000000..50b19cf2 --- /dev/null +++ b/packages/bundled_models/persistence/src/persistence/interface/_metadata.py @@ -0,0 +1,85 @@ +from dataclasses import dataclass, field +from multiprocessing import cpu_count +from persistence.interface._backend import PersistenceBackendType +from persistence.interface._method import ( + PersistenceMethod, + _DEFAULT_PERSISTENCE_SPARSITY_MULTIPLIER, +) + + +@dataclass +class PersistenceMetadata: + """ + Reference to common data that is passed around during persistence computations. + """ + + idx_time_dim: int # index of time dimension + method: PersistenceMethod # persistence method to use + + # --- (kw)args with defaults --- + # IMPORTANT: These are essentially tuning parameters that affect performance. The defaults are + # usually okay, but they need to be considered carefully for certain systems with limited + # computational power. + num_workers: int = field(default_factory=cpu_count) + + # --- + # NOTE: + # + # A hyperslab/cube is bound by orthogonal hyperplanes, each with its surface parallel to + # a unique axis or dimension. In our case a hyperslab is a chunk. + # + # The above constraint simplifies retrieval of chunks, without needing to flatten or change + # the underlying data structure. On the other hand, the constraint makes it harder to + # accomodate every possible chunk size/count. + # + # Therefore, the number of chunks requested by the user is a desire, not a guarentee. + # The actual chunksize is computed at runtime, and depends on the data shape. + # + # The runtime algorithm must abide by the constraints of hyperslab selection while choosing a + # chunk size that is close to the desired chunk size. + num_chunks_desired: int = 1 + # --- + + do_impute: bool = True + backend: PersistenceBackendType = PersistenceBackendType.NUMPY + + # --- + # multiplier to determine how much data to load, essentially + # + # S * N, where, + # N = Minimum amount of data required for computing a method + # S = this multiplier. + # + # The default is conservatively set at 2 so that it is capable of treating missing values, while + # not overzealously loading things into memory. + # + # If a dataset does not have missing values this can be set to 1, to minimize the load on memory. + # + # On the other hand some datasets may need a much larger sparsity multiplier as they are mostly + # sparse - this can be useful when values from historical observations quite far into the past + # can still be useful for persistence. + sparsity_multiplier: int = _DEFAULT_PERSISTENCE_SPARSITY_MULTIPLIER + # --- + + def len_time_preprocess(self) -> int: + """ + number of historical time indices required for preprocessing, e.g. imputation to fill + missing values. + + This is used during the chunking and pre-processing phase. + """ + _len = int(self.method.min_lookback(self.sparsity_multiplier)) + assert _len >= 1 + return _len + + def len_time_compute(self) -> int: + """ + number of historical time indices required for the persistence computation. + + This is used during the compute phase. + """ + _len = int(self.method.num_time_indices_required()) + # safety: this must always be smaller than or equal to the pre-processing length + assert _len <= self.len_time_preprocess() + assert _len >= 1 + return _len diff --git a/packages/bundled_models/persistence/src/persistence/interface/_method.py b/packages/bundled_models/persistence/src/persistence/interface/_method.py new file mode 100644 index 00000000..94b47b6d --- /dev/null +++ b/packages/bundled_models/persistence/src/persistence/interface/_method.py @@ -0,0 +1,53 @@ +from enum import StrEnum, auto + +# 50% sparsity is reasonable, though some data like precipitation may be more sparse than this +_DEFAULT_PERSISTENCE_SPARSITY_MULTIPLIER = 2 + + +class PersistenceMethod(StrEnum): + """ + Methods to use for persistence. + + MEDIAN_OF_THREE: + computes the median of the three most recent observations. + + MOST_RECENT: + uses the most-recent value as persistence. + + Additionally, num_lookback is used to determine how many indices in the past are required from a + dataslab in order to compute a persistence method. + + This is determined by the actual number of indices required multiplied by a sparsity factor to + account for missing values. Missing values will optionally be imputed. + """ + + MOST_RECENT = "most_recent" + MEDIAN_OF_THREE = "median_of_three" + UNKNOWN = auto() + + def num_time_indices_required(self) -> int: + """ + number of time indices required for computing a particular method + """ + match self: + case PersistenceMethod.MOST_RECENT: + return 1 + case PersistenceMethod.MEDIAN_OF_THREE: + return 3 + case _: + raise NotImplementedError( + "PersistenceMethod: Invalid persistence method." + ) + + def min_lookback( + self, sparsity_multiplier=_DEFAULT_PERSISTENCE_SPARSITY_MULTIPLIER + ) -> int: + """ + The minimum amount of lookback required to compute the corresponding metric. + By default we assume a 50% sparsity and require at least double the number of values + required for the compuation. + """ + if sparsity_multiplier < 1: + raise ValueError("PersistenceMethod: Sparsity multiplier must be >= 1") + + return int(self.num_time_indices_required() * sparsity_multiplier) diff --git a/packages/bundled_models/persistence/src/persistence/interface/types.py b/packages/bundled_models/persistence/src/persistence/interface/types.py new file mode 100644 index 00000000..e91ba438 --- /dev/null +++ b/packages/bundled_models/persistence/src/persistence/interface/types.py @@ -0,0 +1,194 @@ +""" +Common data array/set transformations supported by the persistence model, the main usecase is to map +a function to each data variable independently. This is a common pattern as more often than not we +wouldn't be intermixing variables in basic pre-processing steps. + +TODO: this should be somewhere more common +""" + +from typing import Union, Generic +from collections.abc import Callable +from enum import StrEnum, auto +import xarray as xr +import numpy as np +import numpy.typing as npt + +PetDataArrayLike = Union[xr.DataArray, xr.Dataset, npt.ArrayLike] + + +class PetInputDataType(StrEnum): + XR_DATAARRAY = "xr_dataarray" + XR_DATASET = "xr_dataset" + NP_ARRAY = "np_array" + UNKNOWN = auto() + + +class PetDataset: + def __init__( + self, + arraylike: PetDataArrayLike, + dummy_varname="_dummyvarname", # used for xarray dataarrays and numpy arrays + dimnames: list[str] = None, # used only for numpy arrays + ): + """ + Takes a PetDataArrayLike and converts it to a PetDataset which is compatible with the + `map_each_var` computation. + + `dimnames` is only relevant for numpy - and only if using name-based indexing for retrieving + e.g. time dimension + """ + self.raw_type = PetInputDataType.UNKNOWN + self.ds = self.from_arrlike(arraylike, dummy_varname, dimnames) + self.return_raw_result = True + + def with_return_raw_result(self, return_raw_result: bool = True): + """ + Optionally set this to return raw array from `map_each_var` + + NOTE: this is a special purpose function. It is useful when multiple operations that take in + PetDataArrayLike are chained. In which case self.return_raw_result = False will have some + slight performance benefit, otherwise you'd have to do: + + ``` + pd1 = PetDataset(arr) + res1 = pd1.map_each_var(fn1) + pd2 = PetDataset(res1) # each of this call incurs a overhead. + res2 = pd2.map_each_var(fn2) + ``` + + Instead, setting `with_return_raw_result(False)` we can chain methods: + + ``` + pet_ds = PetDataset(arr) + # no over head since the return type of each method is already a PetDataset + result = pet_ds.map_each_var(fn1).map_each_var(fn2)... + ``` + + Finally we can set: + + ``` + raw_result = + pet_ds.map_each_var(fn1) + .map_each_var(fn2) + ... + .with_return_raw_result() + .map_each_var(final_fn) + ``` + + if we explicitly need the raw result at the end. + + The default (True) is always to return the original array type. This would be the case for + most one-off computations. + """ + self.return_raw_result = return_raw_result + + def from_np_array( + self, arraylike: npt.ArrayLike, dummy_varname, dimnames + ) -> xr.Dataset: + self.raw_type = PetInputDataType.NP_ARRAY + return self.from_xr_dataarray( + xr.DataArray(arraylike, dims=dimnames), dummy_varname + ) + + def from_xr_dataarray(self, arraylike: xr.DataArray, dummy_varname) -> xr.Dataset: + self.raw_type = PetInputDataType.XR_DATAARRAY + return xr.Dataset({dummy_varname: arraylike}) + + def from_xr_dataset(self, arraylike: xr.Dataset) -> xr.Dataset: + self.raw_type = PetInputDataType.XR_DATASET + return arraylike + + def from_arrlike(self, arraylike, dummy_varname, dimnames) -> xr.Dataset: + # Order is important here, For example: + # xr.DataArray may be a npt.ArrayLike, but not the other way around. If we swap the order, + # the xr.DataArray constructor will never be reached. + + msg_type_error = """ + The provided data does not have a supported array type, supported array types are: + xr.DataArray, xr.Dataset and np.ndarray. + """ + + if isinstance(arraylike, xr.Dataset): + return self.from_xr_dataset(arraylike) + + if isinstance(arraylike, xr.DataArray): + return self.from_xr_dataarray(arraylike, dummy_varname) + + if isinstance(arraylike, (np.ndarray, list, tuple)): + arraylike = np.asarray(arraylike) # force convert just in case + return self.from_np_array(arraylike, dummy_varname, dimnames) + + # unsupported type + raise TypeError(msg_type_error) + + def map_each_var( + self, + _fn: Callable[[xr.DataArray, ...], xr.DataArray], + *_fn_args, + **_fn_kwargs, + ) -> PetDataArrayLike: + """ + Applies a function over each data array in the dataset. The return type will be dataset. + + The return type of each function operation itself will be per variable (dataarray). + + Only functions that have common structure associated to the variables in the Dataset will + work properly. + + IMPORTANT: global attributes and special variables may not be preserved. This operation is + destructive and for intermediate computation purposes only. + + Args: + _fn: takes a DataArray as its first input arg and produces a DataArray as output + _fn_args: additional positional arguments to provide to _fn + _fn_kwargs: additional keyword arguments to provide to _fn + """ + errmsg_badinputtype = "PetDataset.map_each_var: invalid input type detected" + errmsg_singlearrayret = ( + "PetDataset.map_each_var: Expect function to return a single xr.DataArray" + ) + + if self.raw_type == PetInputDataType.UNKNOWN: + raise RuntimeError(errmsg_badinputtype) + + dict_res = {} + + for k_var, v_da in self.ds.data_vars.items(): + # sense check + assert isinstance(v_da, xr.DataArray) + + da_res = _fn(v_da, *_fn_args, **_fn_kwargs) + + if not isinstance(da_res, xr.DataArray): + raise RuntimeError(errmsg_singlearrayret) + + dict_res[k_var] = da_res + + ds_res = xr.Dataset(dict_res) + + if self.return_raw_result: + return self._raw_result(ds_res) + + # return upgraded dataset by default + return ds_res + + def _raw_result(self, ds: xr.Dataset) -> PetDataArrayLike: + """ + Converts a result back into the original data structure. Down-converting is a lot safer and + so less checks required. + + NOTE: the returned datatype may have dummy names attached, as such these results are for + intermediate computation purposes only, not for operational outputs. + """ + if self.raw_type == PetDataArrayLike.UNKNOWN: + # this should not happen - _raw_result should not be called externally + raise RuntimeError("PetDataset._raw_result: Invalid raw type encountered") + elif self.raw_type == PetDataArrayLike.XR_DATASET: + # nothing to do + return ds + elif self.raw_type == PetDataArrayLike.XR_DATAARRAY: + # extract the dataarray + return ds[self._dummyvarname] + elif self.raw_type == PetDataArrayLike.NP_ARRAY: + # extract the numpy array - note this may force a memory load. + return ds[self._dummyvarname].values diff --git a/packages/bundled_models/persistence/src/persistence/methods/__init__.py b/packages/bundled_models/persistence/src/persistence/methods/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/packages/bundled_models/persistence/src/persistence/methods/_impute.py b/packages/bundled_models/persistence/src/persistence/methods/_impute.py new file mode 100644 index 00000000..2896f0c1 --- /dev/null +++ b/packages/bundled_models/persistence/src/persistence/methods/_impute.py @@ -0,0 +1,31 @@ +""" +This module handles imputation of missing data using very simple techniques. + +Only mean is currently supported. +""" + +from dataclasses import dataclass +import numpy as np + + +@dataclass(frozen=True) +class SimpleImpute: + arr: np.ndarray + + def impute_mean(self) -> np.ndarray: + """ + To keep the imputation representative of the data but yet simple we can do a simple + mean interpolation over the data slab. + + NOTE: This is non-deterministic depending on the data chunking strategy. + """ + nanmask = np.isnan(self.arr) + if not nanmask.any() or nanmask.all(): + # if nothing is missing or everything is missing, return the original array as-is + return self.arr + else: + # otherwise, replace missing values with the mean of the slab + # NOTE: the following flattens the array by default if axis isn't specified + fillval = np.nanmean(self.arr) + arr_imputed = np.where(nanmask, fillval, self.arr) + return arr_imputed diff --git a/packages/bundled_models/persistence/src/persistence/methods/_median.py b/packages/bundled_models/persistence/src/persistence/methods/_median.py new file mode 100644 index 00000000..015d4dfc --- /dev/null +++ b/packages/bundled_models/persistence/src/persistence/methods/_median.py @@ -0,0 +1,47 @@ +import numpy as np +import warnings + +# TODO: get this from common definition - requires refactor +_LOOKBACK = 3 + + +def _median_of_three_numpy(arr: np.ndarray, idx_time: int) -> np.ndarray: + """ + Computes median of three along the time index, ignores nans; if a + particular coordinate is all nan for the required time indices, the + output is nan for that entry. + """ + # safety: this should have been handled at the top level + len_time = arr.shape[idx_time] + assert len_time >= _LOOKBACK + + # --- + # select relenvant array indices by time, based on lookback + # + # TODO: this should happen someplace higher up assumes latest obs is at the end, similar to + # _LOOKBACK. + idx_end = len_time + idx_start = idx_end - _LOOKBACK + idx_slice = slice(idx_start, idx_end, 1) # start, end, step + # generator for nd-index slicing + idx_all = slice(None, None, None) + nd_slice = (idx_slice if i == idx_time else idx_all for i in range(len(arr.shape))) + # sliced array that only has the latest 3 values + arr_slice = arr[*tuple(nd_slice)] + # --- + + # --- + # calculate the median along the time axis + # + # NOTE: ignore numpy warnings as allowing all `nan` is intentional + # + # NOTE: `keepdims=True` because we want to keep the dimensional structure of the variable + # being computed at a higher level. + # + # TODO: this should be replaced by a fast median of three algorithm using if/else statements + # or a ternary operator equivilent. + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + arr_median = np.nanmedian(arr_slice, axis=idx_time, keepdims=True) + return arr_median + # --- diff --git a/packages/bundled_models/persistence/src/persistence/methods/_mostrecent.py b/packages/bundled_models/persistence/src/persistence/methods/_mostrecent.py new file mode 100644 index 00000000..e69de29b diff --git a/packages/bundled_models/persistence/src/persistence/persistence_impl.py b/packages/bundled_models/persistence/src/persistence/persistence_impl.py new file mode 100644 index 00000000..3efd7010 --- /dev/null +++ b/packages/bundled_models/persistence/src/persistence/persistence_impl.py @@ -0,0 +1,266 @@ +""" +Runs persistence model on the data loaded from the pipeline. Chunks the input data from the pipeline +and uses multiprocessing (if specified to do so). + +Persistence potentially needs to be computed on the fly. Depending on the persistence method, and +model it is being compared against, the computation may require ingestion of a reasonable amount of +historical data. + +The common use-case is to offload the data loading to something at a higher level (pet-pipeline). + +This module can't control the loading process, instead what it controls is the way in which the +chunks are indexed, so that they can be _processed_ (CPU not IO) efficiently. + +Examples of what can be done: + - choice of backend (e.g. numba/rust etc., defaulting to numpy) - wip currently only numpy is + supported + + - choice of number of chunks and workers for python to slice data into multiple workers. + (embarassingly parallel) + + - choice of persistence method + + - flexiblity in how the input array/slab is provided, currently supports: + - numpy array (<--- almost any hypercube datastructure can be converted to this) + - xarray dataset + - xarray dataarray + +CAUTION: + + Due to the way data is stored and loaded, multiprocessing may sometimes be necessary but should + be used with caution. Some tips, when in doubt just set the workers to 1, but you may still + chunk the data if required due to memory issues. + + Again, the chunking here is not to do with loading, its to do with efficient processing. + Assumedly the data is already chunked as it is loaded via some other framework. The chunking + applied here is on top of that to further sub-slice things to take into account the need to + ingest a large amount of data for aggregation computations. + +ANTIPATTERNS (for developers): + + - do not chunk over time (except for specific exceptions) + + - do not use external multiprocessing/threading like dask + + - do not use multiprocessing IF the compute backend already does it efficiently, UNLESS we are + IO bound. + + - do not use threading. IO bound issues should be resolved at a higher level because persistence + methods (currently) have no control over how the data is loaded - actually this is the same + for everything in PET that delegates data loading to the pipeline. + + - do not implemnent methods with heavy parametric statical inference or methods that are aware + of the "meaning" of orthogonal dimensions in the hypercube other than "time". + + - do not do any overly clever chunk/worker optimization - this is the user's responsiblity + + - do not assume this will be called as a library (but can be if the OS allows it and its been + tested sufficiently). + +IMPORTANT: + + The "proper" way to run this module is a standalone process/script. But it _may_ work as part of + a script/pipeline _if_ the underling OS supports it. See the executor pool defined in + `interface._compute` and the main guard at the bottom. + +FUTUREWORK: + + - Add the ability to bypass python completely for data loading. + + - Current architecture expects data to be lazily loaded from python but eagerly computed by + the backend, which may still be python or could be something like rust or C. + + - The target alternative or toggle is for this to be inverted in a way that the data loading + itself is done by the backend, allowing for even better control over the processing. + + - Persistence computation is relatively isolated enough from "frameworks" to be a perfect + candidate to do this. +""" + +from persistence.interface import ( + PetDataArrayLike, + PersistenceComputePool, + PersistenceBackend, + PersistenceMethod, + PetDataset, +) + + +def predict( + arr: PetDataArrayLike, + idx_time_dim: int, + num_workers: int = None, + num_chunks: int = None, + method: PersistenceMethod | str = PersistenceMethod.MEDIAN_OF_THREE, + simple_impute: bool = True, + backend_type: PersistenceBackendType = PersistenceBackendType.NUMPY, +) -> pet_persist.PetDataArrayLike: + """ + Calculate the persistence of historical observations, to be used as a baseline for other models. + + Persistence methods essentially compute either: + + a. reduce an array with multiple time indices into 1 time index, given the input with + multiple time indices (the number of time indices required, depends on the perisistence + method). ---> single time index + + b. A stochastic signal that has the maximum likelihood (depending on method) of representing + the data at the leadtime given the short amount of contextual history. E.g. this could be + the starting context using a. followed by some behaviour inferred from day cycles inferred + from the historical data. ---> multi-time indices, maybe autoregressive + + Only a. is currently supported. + + What persistence tries to answer is the following: + + Given some trivial, human comprehendable methods, am "I" - this program - able to apply the + method(s) according to the user configuration on some limited amount of historical data to + produce output that is competitive (speed, memory usage, accuracy, skill etc.) to the model + that I'm compared against. + + Because if the answer is "yes I can match this complex algorithm, then that invalidates the + need for the complex algorithm, especially since persistence is explainable and bounded to + the observations by definition. + + If the answer is "no" then the follow up is, how does this compare with other competitive + models, which essentially paves grounds for verfication and ranking models. + + + The general idea is that we are transforming a set of user requirements and a nd-dataarray into + a time reduced (single time index) nd-dataarray if n > 1 (otherwise we'd just get back a single + scalar). In this process we would also be doing chunking, multiprocessing, and offloading to + a different compute backend, if requested. By default no data splicing occurs and the backend is + chosen to be numpy. + + The above is repeated for each "variable" in the input data structure independently, where the + concept of a "variable" only applies in the case that the input is a `xr.Dataset` _or_ if the + underling `xr.DataArray` has a "name". The results are recomposed back into the original data + structure with/or without variables - depending. + + (C, M, D_(TxN), I) -> D_(T'xN) + + where: + D = data provided - usually observations + (must include time dimension, may have multiple dimensions) + C = chunk strategy (index, number of chunks) + (or none if doing it all in one go) + M = persistence method + (defaults to most recent observation) + I = simple imputation of missing values + (optional) + T = time dimension + T' = forecast time/lead time + N = other dimensions + D_(T'xN) = data collapsed to persistence output + + Use imputation only if data is sparse and predictable. + + Args: + + arr (array-like) - required: + ArrayLike - supports numpy and xarray + + idx_time (int) - required: + the dimension for time index + + num_workers (int): + number of workers to use for processing persistence, defaults to number of cpus. + + num_chunks (int): + number of chunks to use, defaults to `min(num_cpu, len(chunk_dimension))` + + method (str | StrEnum): + The method to use to compute persistence. see `PersistenceMethod`. + Supports: + - "median_of_three" + - "most_recent" + + simple_impute (bool): + defaults to True. Set to False if nan needs to be preserved. + NOTE: methods that require multiple non-nan datapoints to function may be forced to nan. + + backend_type (str | StrEnum): + see `PersistenceBackendType`. The backend compute engine to use. + Supports: + - "numpy" + + Returns: + + an array (PetDataArrayLike) matched to the same specific input type in + (PetDataArrayLike), i.e. output is guaranteed to have the same type as + the input array. + + FUTUREWORK: + + Optionally also return and/or cache a stochastic signal (autoregressive function) that + can be applied onto the persistence output (if the given method supports it). This + allows for persistence guided by some simple derived trend (like day cycles). + + Again, its important that this stochastic trend isn't derived using complicated methods, + and hence the user cannot provide this signal - it has to be pre-derived and cached by + one of the persistence methods dynamically. + """ + if isinstance(method, str): + # force it to EnumStr - auto raises error if not compatible. + method = PersistenceMethod(method) + + # --- DEPRECATED --- + # TODO: remove with_return_raw_result from PetDataset, there's no reason to + # keep the lifted structure when the caller likely only requires the + # original structure back. + # pet_ds = pet_persist.PetDataset(arr).with_return_raw_result(return_raw_result) + # --- + + # lift structure to dataset representation (higher order) + # structural order (highest to lowest) + # - xr.Dataset + # - xr.DataArray + # - np.ndarray + pet_ds = PetDataset(arr) + + raise NotImplementedError("TODO: map to persistence metadata") + + metadta = PersistenceMetadata(...) + + # apply function (ALWAYS) and destruct result (ONLYIF original array was lower order) + arr_result = pet_ds.map_each_var( + _predict_single_var, + metadata, + ) + + # safety capture for dev/test + assert type(arr) == type(arr_result) + + return arr_result + + +# TODO: make this ingest PersistenceMetadata instead... +def _predict_single_var( + da: xr.DataArray, + idx_time_dim: int, + num_chunks: int = None, + method: PersistenceMethod = PersistenceMethod.MEDIAN_OF_THREE, + simple_impute: bool = True, +): + """ + Computes persistence for a single data array, has the same interface as _compute_persistence + except that the first argument is a data array. + """ + # create metadata + + # input dataarray -> chunk -> impute -> compute persistence -> merge chunks + chunker = PersistenceChunker( + da_lazy=da, + method=method, + num_chunks=num_chunks, + idx_time_dim=idx_time, + ) + + # TODO: worker pool + # TODO: work chain i.e. slice -> impute -> compute + # TODO: merge result + raise NotImplementedError("TODO - some missing parts") + + +if __name__ == "__main__": + raise NotImplementedError("TODO - standalone call") diff --git a/packages/bundled_models/persistence/src/persistence/registered_model.py b/packages/bundled_models/persistence/src/persistence/registered_model.py new file mode 100644 index 00000000..1eeebc0b --- /dev/null +++ b/packages/bundled_models/persistence/src/persistence/registered_model.py @@ -0,0 +1,59 @@ +""" +Register persistence model in zoo + +NOTE: + +- this is temproary compatibility with pipeline ingest to fit in with the paradigm similar to + FourCastNeXT. + +- zoo may get deprecated in favour of direct implementations in bundled models, so any interfacing + is intentionally lightweight, with some shortcuts. +""" + + +@pyearthtools.zoo.register("Development/Persistence", exists="ignore") +class PersistenceRM(pyearthtools.zoo.BaseForecastModel): + _name = "Development/Persistence" + + def __init__( + self, + *, + pipeline_name: str = None, + output: Optional[os.PathLike] = None, + pipeline=None, + lead_time: int | str, + **kwargs, + ) -> None: + """ + TODO initialize persistence class with appropriate arguments + """ + raise NotImplementedError("TODO") + super().__init__( + pipeline_name=pipeline_name, pipeline=pipeline, output=output, **kwargs + ) + + def load(self, **kwargs) -> tuple[Any, dict[str, Any]]: + """ + TODO + + - check pipeline was constructed with a TemporalWindow or equivilent Temporal* index + extraction methods. + - pass the merged indices into the persistence algorithm + - the return type should be a "Predictor" that accepts some kwargs + - for a simplistic persistence model we don't want the recurrent predictor, as the + internal methods already handle any splitting and stacking. + - instead use the TimeWindow directly + - I'm not sure how this handles data sets + + The easiest way to do this is to: + + - look at a sample pipleline with a TemporalWindow method + - determine how to translate the variables into an output + - standardise the output to look like the original example + + FUTUREWORK + + while predictors in other cases e.g. fourcastnext have caching implemented. The strategy + needs to be considered carefully. So it will be bypassed for the initial implementation + """ + raise NotImplementedError("TODO") diff --git a/packages/bundled_models/persistence/tests/interface/test__chunker.py b/packages/bundled_models/persistence/tests/interface/test__chunker.py new file mode 100644 index 00000000..90164b06 --- /dev/null +++ b/packages/bundled_models/persistence/tests/interface/test__chunker.py @@ -0,0 +1,138 @@ +import functools +import xarray as xr +import numpy as np + +import persistence.interface._chunker as _chunker +import persistence.interface._metadata as _metadata +import persistence.interface._method as _method + +_pcr = _chunker.PersistenceChunker +_pci = _chunker.PersistenceChunkInfo +_pdc = _chunker.PersistenceDataChunk +_pma = _metadata.PersistenceMetadata +_pmd = _method.PersistenceMethod + + +def test_generate_chunks_default(): + """ + default chunk count is 1, i.e. no chunks or the entire dataset is a single chunk, this should + give the same result as ..._single_large_chunk. + + This is a separate test because the default may change, but we still want to retain the test + below for a single large chunk + """ + + +def test_generate_chunks_common_usecases(): + """ + common usecases for chunking + + Assume a reasonable number of dimensions for this test. + (3, 8, 10*, 5, 4) + + 10* => is the time dimension and should be ignored by the chunking strategy. + + total size = 3 * 8 * 5 * 4 = 480 + + we test the following chunk sizes: + - chunk start index = 3, chunksize = 4, chunkshape = (1, 1, 10, 1, 4) + - chunk start index = 1, chunksize = 20, chunkshape = (1, 1, 10, 5, 4) + - chunk start index = 0, chunksize = 160, chunkshape = (1, 8, 10, 5, 4) + + the desired chunks that can result in the above results are: + - 4 >= chunksize > 1, 120 <= numchunks < 480, choose 479 arbitrarily + - 20 >= chunksize > 4, 24 <= numchunks < 120, choose 24 arbitrarily + - 160 >= chunksize > 20, 3 <= numchunks < 24, choose 11 arbitrarily + + NOTE: + The first two cases above are intentionally edge cases and sit at the boundaries. + More edge cases such as: + - intentionally bad settings of chunks, + - impact of chunking along the first/last index, + - the position of the time index, + - testing defaults, + are covered in other tests. + """ + arr_shape = [3, 8, 10, 5, 4] + arr_shape_notime = [v if i != 2 else 1 for i, v in enumerate(arr_shape)] + size_total = functools.reduce(lambda x, y: x * y, arr_shape_notime) + num_chunks = [479, 24, 11] + # with MEDIAN_OF_THREE we expect 2 * 3 = 6 indices for time + method = _pmd.MEDIAN_OF_THREE + exp_result = [ + (3, 4, [1, 1, 6, 1, 4]), + (1, 20, [1, 1, 6, 5, 4]), + (0, 160, [1, 8, 6, 5, 4]), + ] + idx_time_dim = 2 + test_data = xr.DataArray(np.ones(arr_shape), dims=["x0", "x1", "t", "x2", "x3"]) + + for i, nchk in enumerate(num_chunks): + metadata = _pma( + idx_time_dim=idx_time_dim, num_chunks_desired=nchk, method=method + ) + chunker = _pcr(da=test_data, metadata=metadata) + assert chunker.chunk_info.lsi_chunk == exp_result[i][0] + assert chunker.chunk_info.size_chunk == exp_result[i][1] + assert chunker.chunk_info.num_chunks == size_total // exp_result[i][1] + for data_chunk in chunker.generate_chunks(): + assert list(data_chunk.arr_chunk.shape) == exp_result[i][2] + + +def test_generate_chunks_single_large_chunk(): + """ + explicitly set chunk sizes = 1 + """ + pass + + +def test_generate_chunks_each_element_is_a_chunk(): + """ + exlicitly set num_chunks = total size + """ + pass + + +def test_generate_chunks_edge_cases(): + """ + - desired num chunks is less than 1 + - desired num chunks is greater than the max supported chunk size + """ + pass + + +def test_chunk_caculation_single_worker(): + """ + basic test of multiprocessing pool processing the generated chunks, but with a single worker. + This should work in most setups. + + TODO: copy the notes below to the compute pool - this is a temporary location + + NOTE: chunking only saves memory if num_chunks > num_workers. And that too only during + processing since we only load a fraction of the input array at a given time. + + NOTE: regardless, the final array will be joined in-memory, this is unavoidable unless each + worker writes straight to disk - which is out of scope. So the minimum memory usage will always + be greater than the size of the entire hypercube for a single time instance (persistence returns + 1 time point) + + """ + + +# TODO: +# --- optional tests that are run only if the system can handle it --- +# @pytest.mark.skipif( +# mem < "1GiB", reason="system memory is not large enough to run test" +# ) +# def test_chunking_large_data_large_chunks(): +# """ +# skip if system does not have enough memory +# """ +# pass +# +# +# def test_multiprocessing_pool_ingest(): +# """ +# skip if system only has a single worker +# """ +# pass diff --git a/packages/bundled_models/persistence/tests/interface/test__compute.py b/packages/bundled_models/persistence/tests/interface/test__compute.py new file mode 100644 index 00000000..3daf05fa --- /dev/null +++ b/packages/bundled_models/persistence/tests/interface/test__compute.py @@ -0,0 +1,198 @@ +""" +Tests various compute methods and backends at a high level. The focus is on structural preservation +of the various computations that are dispatched into multiprocessing workers. Also ensuring correct +mapping to the method/backend given the user input. + +NOTE: this only does a very basic test of the method itself. Actual implementation and computational +accuracy of the method, and any edge cases are tested elsewhere. +""" + +import numpy as np +import xarray as xr +import functools + +from persistence.interface._backend import PersistenceBackendType +from persistence.interface._chunker import PersistenceChunker +from persistence.interface._compute import PersistenceCompute, PersistenceComputePool +from persistence.interface._metadata import PersistenceMetadata +from persistence.interface._method import PersistenceMethod + + +def _compute_single( + method: PersistenceMethod, + backend: PersistenceBackendType, + random=False, # defaults to "arange" i.e. value = 1-d index reshaped into nd-array + shape_input=(4, 5, 2, 6, 10), + numchunks=21, + time_index=3, +) -> (PersistenceMetadata, np.ndarray, np.ndarray): + """ + Helper function to create example data for a single computation. + + Useful for comparison of single workers vs pools, for various persistence methods and backends + + Returns references to: + - metadata + - input array (np.ndarray) + - output array (np.ndarray) + """ + # repeatability - re-seed rng state and bind it to `rng` variable + rng = np.random.default_rng(seed=42) + + # derive array shape + shape_input = list(shape_input) + total_size = functools.reduce(lambda x, y: x * y, shape_input) + + # choose whether to use linear increments (essentially the equivilent 1d index as the value or a + # random number as the value + arr_in = None + if random: + arr_in = np.arange(total_size).reshape(shape_input) + else: + arr_in = rng.random(shape_input) + + # specify metadata (mocked user input) + metadata = PersistenceMetadata( + idx_time_dim=time_index, + method=method, + num_chunks_desired=numchunks, + do_impute=True, + backend=backend, + ) + + # compute output + pc = PersistenceCompute(arr=arr_in, metadata=metadata) + arr_out = pc.compute() + + # expect the array shape to be the same except for time dimension which should be reduced to 1 + expect_shape = [ + s if i != metadata.idx_time_dim else 1 for i, s in enumerate(arr_in.shape) + ] + + # simple shape assert + assert expect_shape == list(arr_out.shape) + # return meta information for further tests in caller + return metadata, arr_in, arr_out + + +def _compute_pool( + method: PersistenceMethod, + backend: PersistenceBackendType, + _fn_compute_single=_compute_single, + *_fn_extra_args, + **_fn_extra_kwargs, +) -> (PersistenceMetadata, xr.DataArray, xr.DataArray): + """ + Same as _compute_single but for xarrays and using chunked pools. + + Cheats a bit by using _compute_single as a default to avoid repetition for basic tests. + + Returns references to: + - metadata + - input array (xr.DataArray) + - output array (xr.DataArray) + """ + metadata, arr_in, arr_out = _fn_compute_single( + method, backend, *_fn_extra_args, **_fn_extra_kwargs + ) + + # upgrade to data arrays with dummy names, except for the time index which will be 't' + dim_names = [ + "x" + str(i) if i != metadata.idx_time_dim else "t" + for i in range(len(arr_in.shape)) + ] + + # upgrade to dataarray + da_in = xr.DataArray(arr_in, dims=dim_names) + + # chunk generator + chunker = PersistenceChunker(da=da_in, metadata=metadata) + + # propagate information to compute pool + pcp = PersistenceComputePool( + chunk_generator=chunker.generate_chunks(), + chunk_info=chunker.chunk_info, + metadata=metadata, + ) + + # compute and retrieve chunks (joined back into data array) + da_out = pcp.map_and_join_chunks() + + # expect the array shape to be the same except for time dimension which should be reduced to 1 + expect_shape = [ + s if i != metadata.idx_time_dim else 1 for i, s in enumerate(arr_in.shape) + ] + + # simple shape assert + assert list(da_out.shape) == expect_shape + # dimnames should not have changed - NOTE: this may regress if xarray decides to deprecate dims + # in favour of sizes, in which case we should be extracting the "keys" as an ordered tuple. + assert dim_names == list(da_out.dims) + # single worker and pool should have the same values + assert np.allclose(da_out.values, arr_out) + # return meta information for further tests in caller + return metadata, da_in, da_out + + +def test_compute_medianofthree_workerpool_numpy(): + """ + method: median of three + backend: numpy + + expect lookback of 6 used for imputation (default) + expect lookback of 3 used for median of three computation (definition) + expect dimension shape to be preserved and only the time dimension to be reduced to 1 + expect dimension names to be mapped to the right shape + expected array can be easily constructed using a manual equivilent numpy operation e.g.: + 1. create a range of numbers + 2. compute median the trivial way over the axis + 3. sense check a few cherrypicked numbers + 4. compare the output against the output of the worker pool + 5. repeat the above, but for a random array (in which case 3. is not necessary - and in fact + cannot be done deterministically) + + Most of the same above strategy can be repeated for most of the other tests. + + ([numpy array], metadata) -> xarray dataarray + """ + # values = 1-d index + _, da_in, da_out = _compute_pool( + PersistenceMethod.MEDIAN_OF_THREE, + PersistenceBackendType.NUMPY, + ) + + # cherry picked tests (TODO) + + # values = random (TODO) + + +def test_compute_mostrecent_workerpool_numpy(): + """ + Sense check for most recent computation method + """ + pass + + +def test_no_impute_workerpool_numpy(): + """ + Check when imputation is disabled - should preserve nans + """ + pass + + +def test_compute_backend_supported(): + """ + Sense check for supported backends - should succeed + + NOTE: individual backend support themselves are done in tests of form _ + e.g. test_compute_medianofthree_workerpool_numpy tests the median of three computation on the + `numpy` backend pool + """ + pass + + +def test_compute_backend_unsupported(): + """ + Sense check for unsupported backends - should error out + """ + pass diff --git a/packages/bundled_models/persistence/tests/test__daskconfig.py b/packages/bundled_models/persistence/tests/test__daskconfig.py new file mode 100644 index 00000000..f7472e31 --- /dev/null +++ b/packages/bundled_models/persistence/tests/test__daskconfig.py @@ -0,0 +1,137 @@ +""" +Tests that dask is actually in synchronous/signle-threaded mode +""" + +from dataclasses import dataclass +import numpy as np +import persistence as pet_persist +import persistence.daskconfig as pet_daskconfig + + +@dataclass +class _PyTestThreadInfo: + id_thread_kern: int # usually same as process id + id_thread_py: int # python read id + id_process: int # process id for current worker + num_cpus: int # number of cpus + + +def _fn_dask_get_thread_info(count): + return _make_thread_info() + + +def _cmp_thread_info( + thread_info_a: _PyTestThreadInfo, thread_info_b: _PyTestThreadInfo +) -> int: + """ + Works like strcmp, thread info is the same => return 0, otherwise they are different. + """ + # Each critera will return 0 if they are equal or 1 if they are not. A larger number implies + # that there is larger discrepency. + # NOTE: cpu checks is not strictly required, but helpful to know, since it is not an expected + # scenario unless running multi-node. + count_diff = ( + int(thread_info_a.id_thread_kern != thread_info_b.id_thread_kern) + + int(thread_info_a.id_thread_py != thread_info_b.id_thread_py) + + int(thread_info_a.id_process != thread_info_b.id_process) + + int(thread_info_a.num_cpus != thread_info_b.num_cpus) + ) + return count_diff + + +def _is_multithreaded_compute(list_thread_info) -> bool: + """ + Returns true if the list of thread_info have different threads or processes. + """ + ref_thread_info = list_thread_info[0] + flag_has_different_threads = False + for i, v in enumerate(list_thread_info): + # ignore reference (i == 0) and update flag if a difference is spotted + if i != 0 and _cmp_thread_info(v, ref_thread_info) != 0: + flag_has_different_threads = True + break + return flag_has_different_threads + + +def _make_thread_info(): + """ + Creates the current thread info for the given context. This shouldn't be a fixture, it needs to + be called internally by a worker in the test. + """ + import threading + import os + + obj_thread_py: threading.Thread = threading.current_thread() + return _PyTestThreadInfo( + id_thread_kern=obj_thread_py.native_id, + id_thread_py=obj_thread_py.ident, + id_process=os.getpid(), + num_cpus=os.cpu_count(), + ) + + +def test_dask_single_threaded(): + """ + Set single threaded mode and check that the thread ids are the same for each worker. + """ + import dask + import dask.config + import dask.distributed + import dask.dataframe as _dd + import dask.array as _da + + main_thread_info: _PyTestThreadInfo = _make_thread_info() + + # we still set multiprocess here to check if our context manager is working as expected. + dask.config.config["scheduler"] = "processes" + dask.config.refresh() + + # partition task of processing 100 items by number of ccpus + _chunks = (min(main_thread_info.num_cpus, 100),) + _dask_df = _dd.io.from_dask_array( + _da.from_array(np.arange(100), chunks=_chunks), + columns=["x"], + ) + + # run computation in context manager + with pet_daskconfig._set_synchronous_dask(): + results = _dask_df.apply( + _fn_dask_get_thread_info, axis=1, meta=(None, "object") + ).compute() + assert not _is_multithreaded_compute(results) + + +def test_dask_default_multithreaded(): + """ + Tests dask without singlethreaded context management. + """ + # NOTE: this namespacing does not guarentee dask is out of scope in other tests + import dask + import dask.config + import dask.distributed + import dask.dataframe as _dd + import dask.array as _da + + # intentionally set to multiprocess mode (which is usually the case with e.g. xarray) + + main_thread_info: _PyTestThreadInfo = _make_thread_info() + dask.config.config["scheduler"] = "processes" + dask.config.refresh() + + # partition task of processing 100 items by number of ccpus + _chunks = (min(main_thread_info.num_cpus, 100),) + _dask_df = _dd.io.from_dask_array( + _da.from_array(np.arange(100), chunks=_chunks), + columns=["x"], + ) + # get results + results = _dask_df.apply( + _fn_dask_get_thread_info, axis=1, meta=(None, "object") + ).compute() + + # --- check if there are sufficient threads on system + if len(results) <= 1: + print("Insufficient cores/threads to do multi-process tests") + return + + assert _is_multithreaded_compute(results) diff --git a/packages/bundled_models/persistence/tests/test__datatypes.py b/packages/bundled_models/persistence/tests/test__datatypes.py new file mode 100644 index 00000000..b179ad1c --- /dev/null +++ b/packages/bundled_models/persistence/tests/test__datatypes.py @@ -0,0 +1,140 @@ +""" +This test suite tests the use of PetDataset to create a common datatype construction for numpy and +xarray (dataarrays and datasets). + +NOTES: +- Since numpy and xarray dataarrays cannot be completely representable by datasets, they will either + be given dummy variables and dimension names, or user-specified variable and dimension names. + Creating a common interface to handle all this is tricky. +- While these dummy names are always options when creating a PetDataset, they should not affect + higher types - e.g. datasets will never be overwritten with the _dummyvarname or "dims()" (because + it may have several variables wtih different dimensions). +""" + +import xarray as xr +import numpy as np +import persistence as pet_persist + + +def _dummy_sum_fn(x: xr.DataArray, y: int, z: int = 5) -> xr.DataArray: + """ + Dummy function to test mapping, should return a data array, first argument must be a data array. + Can take other arguments that may be required for the computation + """ + return x.sum() + y - z + + +def test_petdataset_type_homomorphism_numpy(): + """ + Test type mapping with numpy arrays + """ + # defaults + test_data = np.ones((5, 2, 3)) + pet_ds = pet_persist.PetDataset(test_data) + res_ds = pet_ds.map_each_var(_dummy_sum_fn, 5) + assert "_dummyvarname" in pet_ds.ds.data_vars + # y = 5 + # z = 5 (default) + # sum = 5 * 2 * 3 = 30 + assert res_ds["_dummyvarname"] == 30 + + # with dummy array naming + pet_ds = pet_persist.PetDataset(test_data, dummy_varname="new_dummy_name") + res_ds = pet_ds.map_each_var(_dummy_sum_fn, 5, z=2) + assert "new_dummy_name" in pet_ds.ds.data_vars + # y = 5 + # z = 2 + # sum = 5 * 2 * 3 = 30 + # res = sum + 5 - 2 = 33 + assert res_ds["new_dummy_name"] == 33 + + # with dimension naming + pet_ds = pet_persist.PetDataset(test_data, dimnames=["x", "time", "y"]) + res_ds = pet_ds.map_each_var(_dummy_sum_fn, y=-10, z=-15) + # y = 5 + # z = 2 + # sum = 5 * 2 * 3 = 30 + # res = sum - 10 - (-15) = 35 + assert res_ds["_dummyvarname"] == 35 + assert set(pet_ds.ds.dims) == set(["x", "time", "y"]) + + +def test_petdataset_type_homomorphism_da(): + """ + Test type mapping with data arrays + """ + # defaults + test_data = xr.DataArray(np.ones((5, 2, 3)), dims=["the", "last", "resort"]) + pet_ds = pet_persist.PetDataset(test_data) + res_ds = pet_ds.map_each_var(_dummy_sum_fn, 5) + assert "_dummyvarname" in pet_ds.ds.data_vars + # y = 5 + # z = 5 (default) + # sum = 5 * 2 * 3 = 30 + assert res_ds["_dummyvarname"] == 30 + + # with dummy array naming + pet_ds = pet_persist.PetDataset(test_data, dummy_varname="new_dummy_name") + res_ds = pet_ds.map_each_var(_dummy_sum_fn, 5, z=2) + assert "new_dummy_name" in pet_ds.ds.data_vars + # y = 5 + # z = 2 + # sum = 5 * 2 * 3 = 30 + # res = sum + 5 - 2 = 33 + assert res_ds["new_dummy_name"] == 33 + + # with dimension naming + pet_ds = pet_persist.PetDataset(test_data, dimnames=["x", "time", "y"]) + res_ds = pet_ds.map_each_var(_dummy_sum_fn, y=-10, z=-15) + # y = 5 + # z = 2 + # sum = 5 * 2 * 3 = 30 + # res = sum - 10 - (-15) = 35 + assert res_ds["_dummyvarname"] == 35 + # dimnames should have no effect on dataarrays + assert set(pet_ds.ds.dims) == set(["the", "last", "resort"]) + + +def test_petdataset_type_homomorphism_ds(): + """ + Test type mapping with datasets + """ + # defaults + test_data = xr.Dataset( + { + "potato": xr.DataArray( + np.ones((5, 2, 3)), + dims=["the", "last", "resort"], + ), + "tomato": xr.DataArray( + np.ones((2, 1, 2)), + dims=["x", "y", "z"], + ), + } + ) + pet_ds = pet_persist.PetDataset(test_data) + res_ds = pet_ds.map_each_var(_dummy_sum_fn, 5) + + # _dummyvarname should be ignored for datasets by default + assert "_dummyvarname" not in pet_ds.ds.data_vars + assert res_ds["potato"] == 30 + assert res_ds["tomato"] == 4 + + # with dummy array naming + pet_ds = pet_persist.PetDataset(test_data, dummy_varname="new_dummy_name") + res_ds = pet_ds.map_each_var(_dummy_sum_fn, 5, z=2) + + # _dummyvarname should be ignored for datasets even when forced + assert "new_dummy_name" not in pet_ds.ds.data_vars + assert res_ds["potato"] == 33 + assert res_ds["tomato"] == 7 + + # with dimension naming + pet_ds = pet_persist.PetDataset(test_data, dimnames=["x", "time", "y"]) + res_ds = pet_ds.map_each_var(_dummy_sum_fn, y=-10, z=-15) + assert res_ds["potato"] == 35 + assert res_ds["tomato"] == 9 + + # dimnames should have no effect on dataarrays within the dataset + assert set(pet_ds.ds["potato"].dims) == set(["the", "last", "resort"]) + assert set(pet_ds.ds["tomato"].dims) == set(["x", "y", "z"]) diff --git a/packages/bundled_models/persistence/tests/test__impute.py b/packages/bundled_models/persistence/tests/test__impute.py new file mode 100644 index 00000000..64675d5e --- /dev/null +++ b/packages/bundled_models/persistence/tests/test__impute.py @@ -0,0 +1,41 @@ +""" +This suite tests the simple imputer +""" + +import persistence as pet_persist +import numpy as np + + +def test_temporal_imputation_no_missing(): + """ + Nothing should change if there's no missing value + """ + arr_no_missing = np.full((5, 4, 3), 1, dtype=np.float64) + imputer = pet_persist.SimpleImpute(arr_no_missing) + arr_ret = imputer.impute_mean() + assert np.allclose(arr_ret, arr_no_missing, equal_nan=True) + + +def test_temporal_imputation_some_missing(): + """ + if some missing, then the nanmean is used to impute. + """ + # have no missing array for reference + arr_no_missing = np.full((5, 4, 3), 1, dtype=np.float64) + # put some nans in a random slab + arr_some_missing = np.full((5, 4, 3), 1, dtype=np.float64) + arr_some_missing[1:3, 0:3, 0] = np.nan + imputer = pet_persist.SimpleImpute(arr_some_missing) + arr_ret = imputer.impute_mean() + assert np.allclose(arr_ret, arr_no_missing, equal_nan=True) + assert np.sum(arr_ret) == 5 * 4 * 3 # (all ones) + + +def test_temporal_imputation_all_nans(): + """ + If all nan => don't alter original array. + """ + arr_all_missing = np.full((5, 4, 3), np.nan, dtype=np.float64) + imputer = pet_persist.SimpleImpute(arr_all_missing) + arr_ret = imputer.impute_mean() + assert np.allclose(arr_ret, arr_all_missing, equal_nan=True) diff --git a/packages/bundled_models/persistence/tests/test__interface.py b/packages/bundled_models/persistence/tests/test__interface.py new file mode 100644 index 00000000..8763f4a0 --- /dev/null +++ b/packages/bundled_models/persistence/tests/test__interface.py @@ -0,0 +1,159 @@ +""" +Basic suite of tests that make sure that the interface objects work as expected. +""" + +import numpy as np +import xarray as xr +import persistence as pet_persist + + +def test_persistence_method_obj(): + """ + Basic test to check object creation: PersistenceMethod + """ + persistence_mostrecent = pet_persist.PersistenceMethod.MOST_RECENT + persistence_median = pet_persist.PersistenceMethod.MEDIAN_OF_THREE + + # sense checks - mostrecent + assert persistence_mostrecent.num_time_indices_required() == 1 + assert persistence_mostrecent.min_lookback() == 2 + assert persistence_mostrecent.min_lookback(3) == 3 # 3 * 1 + + # sense checks - median + assert persistence_median.num_time_indices_required() == 3 + assert persistence_median.min_lookback() == 6 + assert persistence_median.min_lookback(50) == 150 # 3 * 50 + + +def test_persistence_data_chunk_obj(): + arr_chunk = np.random.randint(0, 10, (2, 5, 8)) + persistence_method = pet_persist.PersistenceMethod.MOST_RECENT + idx_time: int = 1 # len = 5 + + metadata = pet_persist.PersistenceMetadata( + idx_time_dim=idx_time, + method=persistence_method, + ) + + datachunk = pet_persist.PersistenceDataChunk( + arr_chunk=arr_chunk, + metadata=metadata, + ) + + assert datachunk.arr_chunk.shape.index(5) == datachunk.metadata.idx_time_dim + assert datachunk.metadata.method.min_lookback() == 2 + + +def test_persistence_chunker_obj(): + """ + Basic test to check object creation: PersistenceChunker + """ + da = xr.DataArray( + np.random.randint(0, 10, (2, 5, 8)), + dims=["x0", "time", "x2"], + ) + idx_time: int = 1 # len = 5 + num_chunks: int = 4 # each chunk is 2x5x2 + persistence_method = pet_persist.PersistenceMethod.MOST_RECENT + metadata = pet_persist.PersistenceMetadata( + idx_time_dim=idx_time, + method=persistence_method, + num_chunks=num_chunks, + ) + chunker = pet_persist.PersistenceChunker( + da=da, + metadata=metadata, + ) + + # sense checks + assert da.shape.index(5) == chunker.metadata.idx_time_dim + assert chunker.metadata.num_chunks == 4 + assert chunker.metadata.method.num_time_indices_required() == 1 + + +def test_chunker_multi_index_increment(): + """ + Tests the scenario in the docstrings for mult index increment + + i.e. + shape = (2, 4, 10, 2) + chunk_size = 47 (or increment size) + + Also does a double increment and a manual isel on the dataarray to make sure the sizes are as + expected. + + For this particular purpose we shall include a dummy dimension - time and it should be ignored. + + (2, 4, 5*, 10, 2) + + * time dimension + + as per the doc string example we expect giving a start index of all zeros and a increment (chunk + size) of 47, the next index we should receive is: + + (0, 2, 5*, 3, 1) + """ + da = xr.DataArray( + np.random.randint(0, 10, (2, 4, 5, 10, 2)), + dims=["x0", "x1", "time", "x3", "x4"], + ) + idx_time: int = 2 + chunk_size: int = 47 + + # NOTE: num_chunks is a dummy and not used since we want to explicitly test "47" + # still we set it abnormally high here to check that it is clipped to the data cardinality + # appropriately. + num_chunks: int = 999 + + persistence_method = pet_persist.PersistenceMethod.MOST_RECENT + metadata = pet_persist.PersistenceMetadata( + idx_time_dim=idx_time, + method=persistence_method, + num_chunks=999, + ) + chunker = pet_persist.PersistenceChunker( + da=da, + metadata=metadata, + ) + + assert chunker.metadata.num_chunks == 2 * 4 * 10 * 2 + + start_index = (0, 0, 0, 0, 0) + end_index = chunker.increment_multi_index(start_index, chunk_size) + + assert end_index == [0, 2, 5, 3, 1] + + # check slicing + np_start_index = np.asarray(list(start_index)) + np_end_index = np.asarray(end_index) + 1 + + # assert xarray dataarray dims returns a tuple (since tuples are ordered sets) + assert isinstance(da.dims, tuple) + + dim_names = list(da.dims) + multi_slice = { + dim_names[i]: slice(v[0], v[1], 1) + for v, i in enumerate(zip(np_start_index, np_end_index)) + } + da_slice = da.isel(**multi_slice) + da_slice.shape + + +def test_chunker_multi_index_increment_with_single_dim(): + """ + Tests multi index increment for the case where there is only a single dimension This should + return the entire array back as-is since there can only be one dimension in this case and that + dimension cannot be chunked - i.e. time + """ + pass + + +def test_chunker_multi_index_increment_unit_cardinality(): + """ + Tests multi index increment for the case where there are multiple indices but the indices all + have a cardinality of 1 => we can only have one chunk, regardless of what we set num_chunks to. + """ + # set num_chunks to 10 arbitrarily + + # chunks should be trimmed to min(10, np.prod(all_dims_except_time) => 1) = 1 + pass diff --git a/packages/bundled_models/persistence/tests/test__median.py b/packages/bundled_models/persistence/tests/test__median.py new file mode 100644 index 00000000..801f06a6 --- /dev/null +++ b/packages/bundled_models/persistence/tests/test__median.py @@ -0,0 +1,51 @@ +import numpy as np +from persistence.methods._median import _median_of_three_numpy + + +def test_median_of_three_numpy_basic(): + """ + Tests that the dimensions are preserved except the time dimension which is + reduced (but not squeezed) to one + """ + + # --- case 1 --- + # create a simple array and throw in an outlier for sense check + input_arr = np.array([[1, 2, 3], [5, 2, 6], [0, 191, 4]]) + expect_arr = np.array([[2], [5], [4]]) + idx_time = 1 # second dimension (idx=1) is time + result_arr = _median_of_three_numpy(input_arr, idx_time) + assert np.allclose(result_arr, expect_arr) + + # --- case 2 --- + # check dimensionality is preserved for >2 dimensions + # the values actually don't matter here. + input_arr = np.full((5, 4, 3, 4, 5), 1, dtype=np.float64) + idx_time = 3 # arbitrarily make fourth dimension time (idx_time = 3) + expect_shape = (5, 4, 3, 1, 5) + result_arr = _median_of_three_numpy(input_arr, idx_time) + result_shape = result_arr.shape + assert expect_shape == result_shape + + +def test_median_of_three_numpy_all_nans(): + """ + Test that all nans doesn't spit out a warning and that the associated + dimension is filled with a `nan` + """ + input_arr = np.array([[1, 2, 3], [5, 2, 6], [np.nan, np.nan, np.nan]]) + expect_arr = np.array([[2], [5], [np.nan]]) + idx_time = 1 # second dimension (idx=1) is time + result_arr = _median_of_three_numpy(input_arr, idx_time) + assert np.allclose(result_arr, expect_arr, equal_nan=True) + + +def test_median_of_three_numpy_partial_nan(): + """ + Test that partial nans are still handled. i.e. median of two numbers will + just be their mean and median of one number will just be itself. + """ + input_arr = np.array([[1, 2, 3], [5, 2, np.nan], [5, np.nan, np.nan]]) + expect_arr = np.array([[2], [3.5], [5]]) + idx_time = 1 # second dimension (idx=1) is time + result_arr = _median_of_three_numpy(input_arr, idx_time) + assert np.allclose(result_arr, expect_arr) From 6d27def8aaa44509774d618bbf7585685e01cd22 Mon Sep 17 00:00:00 2001 From: Nikeeth Ramanathan Date: Tue, 10 Mar 2026 11:18:11 +1100 Subject: [PATCH 02/16] [skip ci] readme for examples --- .../persistence/examples/README.md | 80 +++++++++++++++++++ .../persistence/examples/nci_multiprocess.py | 28 +++++++ 2 files changed, 108 insertions(+) create mode 100644 packages/bundled_models/persistence/examples/README.md create mode 100644 packages/bundled_models/persistence/examples/nci_multiprocess.py diff --git a/packages/bundled_models/persistence/examples/README.md b/packages/bundled_models/persistence/examples/README.md new file mode 100644 index 00000000..642db0f7 --- /dev/null +++ b/packages/bundled_models/persistence/examples/README.md @@ -0,0 +1,80 @@ +# Examples + +## Rationale + +Examples serve as patterns to demonstrate library pathways. While examples are often boilerplate, they provide entry points for optimization. Users are encouraged to scrutinize these examples to find more efficient implementations for their specific use-cases. If a superior method is discovered, it should be committed to the codebase, with the example updated to reflect this improvement. + +## Technical Context: Persistence Models + +Persistence models are designed for memory efficiency and speed, distinct from inference models which rely on pre-encoded weights and GPU acceleration. + +```mermaid +mindmap + root((Persistence Models)) + Performance + No GPU usage + Faster than inference + Memory efficient + Data Requirements + Requires historical data + Spatial chunking preference + Constraints + Avoids Dask parallelism + Limited by underlying storage I/O +``` + +### Storage & Performance Goals + +The library aims to provide functional and efficient code. However, persistence computations are currently limited by underlying hardware and storage paradigms, rather than software efficiency alone. + +* **Current Limitations:** Computation of simple statistics on weather data (HPC) can take hours due to data loading inefficiencies and inconsistent storage. +* **Library Scope:** PET provides access patterns and loaders (`pet-pipeline`) to mitigate data loading, but it does not universally solve I/O bottlenecks or hardware latency. +* **Acceptable Performance:** A runtime of 5 minutes for 8 time instances is considered unacceptable and requires optimization of storage/processing pipelines. + +## Configuration Architecture + +The examples below demonstrate different approaches to data loading (`pet-pipeline` vs `standalone`) and computation (`mp`, `py`, `backend`). + +### Core Configuration Options + +| Configuration | Description | Use Case | +| :--- | :--- | :--- | +| **Default** | Uses `pet-pipeline` for data retrieval and indexing. | Standard workflow. | +| **Standalone** | Uses `pet-pipeline` for indexing only; user implements custom loader. | Fine-grained control, non-multiprocessing platforms. | +| **MP/Py** | Python multiprocessing (`mp`) disabling Dask. | Linux/Forkserver contexts, maximum PET compatibility. | +| **1P** | Single worker process. | Stability testing, debugging. | +| **Backend** (e.g., `zig`) | Backend-specific computation (e.g., SIMD, Rust). | Specific hardware optimization or custom C-library integration. | + +### Usage Guidelines + +```mermaid +graph TD + A[User Requirement] --> B{Select Mode}; + + B -->|Testing/Simple| C[1P + Py]; + B -->|SIMD/Quantization| D[1P + Backend + Standalone]; + B -->|Balanced Computation| E[MP + Backend]; + + C --> C1[Portable, Sequential]; + D --> D1[Memory efficient, Custom Loader]; + E --> E1[Platform compatible, Custom Computation]; + + subgraph "Data Access" + F[Pet-Pipeline Index] --> G[Custom Loader]; + end +``` + +*Note: Not all combinations of the above options are exhaustive, but they provide sufficient patterns to construct custom requirements.* + +## Example Files + +The following table maps specific examples to their environments, modes, and purposes. + +| Filename | Environment | Mode | Description | +| :--- | :--- | :--- | :--- | +| `nci_py_mp.py` | NCI Linux (RHEL8-like) | MP + PET Pipeline | Multiprocessing with satellite data using the standard pipeline. | +| `nci_py_mp_standalone.py` | NCI Linux (RHEL8-like) | MP + Standalone | Ad-hoc data loading on NCI, bypassing the standard pipeline. | +| `anylinux_py_mp.py` | Any Linux (Arch tested) | MP + PET Pipeline | Multiprocessing on general Linux environments with PET pipeline. | +| `anylinux_py_standalone.py` | NCI Linux (RHEL8-like) | MP + Standalone | Ad-hoc loading on NCI. | +| `any_py_1p.py` | Local Machine (Win/Mac/Linux) | 1P + Py | Single-threaded processing for portability. May be slower. | +| `zigc.py` | Linux Only | Zig Backend | Computation examples using Zig. Includes parallel HDF5 loader examples and single-threaded variants. *Note: Linux required; not tested on other platforms.* | diff --git a/packages/bundled_models/persistence/examples/nci_multiprocess.py b/packages/bundled_models/persistence/examples/nci_multiprocess.py new file mode 100644 index 00000000..8cb42416 --- /dev/null +++ b/packages/bundled_models/persistence/examples/nci_multiprocess.py @@ -0,0 +1,28 @@ +""" +This example is a WORK IN PROGRESS. + +Use multiprocessing to delegate chunks to various processes, in order to compute the persistence +method in an embarassingly parallel fashion. + +This run specifically targets satellite data on NCI, but is also the most common usecase for +parallel compute. + +TODO: + [ ] example with PersistenceRM wrapper + [ ] example with plain persistence execution on a separate spawned process (separate GIL) + +NOTE: + These examples will eventually also go into the global tutorials. +""" + +def persistence_pipeline_library(): + # TODO: use library directly under main guard + pass + +def persistence_pipeline_spawnproc(): + # TODO: spawn process as separate python command + pass + + +if __name__ == "__main__": + pass From fa393ce18ccc901253ea1c3fc6d78439a51304d2 Mon Sep 17 00:00:00 2001 From: Nikeeth Ramanathan Date: Tue, 10 Mar 2026 12:00:27 +1100 Subject: [PATCH 03/16] [skip ci] change theme --- packages/bundled_models/persistence/examples/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/bundled_models/persistence/examples/README.md b/packages/bundled_models/persistence/examples/README.md index 642db0f7..4cda5ce2 100644 --- a/packages/bundled_models/persistence/examples/README.md +++ b/packages/bundled_models/persistence/examples/README.md @@ -6,9 +6,10 @@ Examples serve as patterns to demonstrate library pathways. While examples are o ## Technical Context: Persistence Models -Persistence models are designed for memory efficiency and speed, distinct from inference models which rely on pre-encoded weights and GPU acceleration. +Persistence models are designed for memory efficiency and speed, as they currently only built for CPU; distinct from inference models which rely on pre-encoded weights and GPU acceleration. ```mermaid +%%{init: theme: 'neutral'}%% mindmap root((Persistence Models)) Performance From 02f3b57d2de60c2fbb33608c0ff1980d8655bac3 Mon Sep 17 00:00:00 2001 From: Nikeeth Ramanathan Date: Tue, 10 Mar 2026 12:36:24 +1100 Subject: [PATCH 04/16] [skip ci] improved readme.md --- .../persistence/examples/README.md | 144 +++++++++++------- 1 file changed, 86 insertions(+), 58 deletions(-) diff --git a/packages/bundled_models/persistence/examples/README.md b/packages/bundled_models/persistence/examples/README.md index 4cda5ce2..6ec804bb 100644 --- a/packages/bundled_models/persistence/examples/README.md +++ b/packages/bundled_models/persistence/examples/README.md @@ -1,81 +1,109 @@ -# Examples +## Overview & Philosophy -## Rationale +Examples in this folder serve as patterns and architectural blueprints for library usage. They are intended to provide a starting point rather than production-ready, optimized code. -Examples serve as patterns to demonstrate library pathways. While examples are often boilerplate, they provide entry points for optimization. Users are encouraged to scrutinize these examples to find more efficient implementations for their specific use-cases. If a superior method is discovered, it should be committed to the codebase, with the example updated to reflect this improvement. +* **Not Optimal:** These examples represent "worst-case scenarios" or basic implementations. Assume they are inefficient. +* **Iterative Improvement:** If you find a better way to perform a task, commit it to the codebase and use it to forge a new, improved example for future users. +* **Goal:** The objective is functional and *functioning* code. Current benchmarks take 5 minutes for 8 time instances; verification requires better performance. ## Technical Context: Persistence Models -Persistence models are designed for memory efficiency and speed, as they currently only built for CPU; distinct from inference models which rely on pre-encoded weights and GPU acceleration. +Persistence models (statistical methods like mean, median, etc.) differ significantly from inference models (pre-trained weights) in computational requirements. -```mermaid -%%{init: theme: 'neutral'}%% -mindmap - root((Persistence Models)) - Performance - No GPU usage - Faster than inference - Memory efficient - Data Requirements - Requires historical data - Spatial chunking preference - Constraints - Avoids Dask parallelism - Limited by underlying storage I/O -``` - -### Storage & Performance Goals +### Comparison: Persistence vs. Inference -The library aims to provide functional and efficient code. However, persistence computations are currently limited by underlying hardware and storage paradigms, rather than software efficiency alone. +| Attribute | Persistence Models | Inference Models | +| :--- | :--- | :--- | +| **Hardware** | CPU only (No GPU usage) | GPU Accelerated (Tensor calculations) | +| **Data Requirement** | Requires extensive historical data | Weights encode historical data | +| **Performance** | Slower than GPU inference | Faster due to weight encoding | +| **Parallelism** | Avoids existing paradigms (e.g., Dask) if data is associated with them | Utilizes standard parallel paradigms | +| **Chunking** | Spatial (2D) preferred | Temporal (Time) preferred | -* **Current Limitations:** Computation of simple statistics on weather data (HPC) can take hours due to data loading inefficiencies and inconsistent storage. -* **Library Scope:** PET provides access patterns and loaders (`pet-pipeline`) to mitigate data loading, but it does not universally solve I/O bottlenecks or hardware latency. -* **Acceptable Performance:** A runtime of 5 minutes for 8 time instances is considered unacceptable and requires optimization of storage/processing pipelines. +**Why this is a pain point:** Software cannot solve all storage and loading inefficiencies. Hardware and platform-specific storage paradigms are often the root cause. While libraries can improve data processing predictability, they cannot universally solve nuanced data loading issues. -## Configuration Architecture +## Execution Modes -The examples below demonstrate different approaches to data loading (`pet-pipeline` vs `standalone`) and computation (`mp`, `py`, `backend`). +The examples are organized around specific execution paradigms. Understanding these modes is critical to selecting the correct example for your environment. -### Core Configuration Options +### Core Concepts -| Configuration | Description | Use Case | -| :--- | :--- | :--- | -| **Default** | Uses `pet-pipeline` for data retrieval and indexing. | Standard workflow. | -| **Standalone** | Uses `pet-pipeline` for indexing only; user implements custom loader. | Fine-grained control, non-multiprocessing platforms. | -| **MP/Py** | Python multiprocessing (`mp`) disabling Dask. | Linux/Forkserver contexts, maximum PET compatibility. | -| **1P** | Single worker process. | Stability testing, debugging. | -| **Backend** (e.g., `zig`) | Backend-specific computation (e.g., SIMD, Rust). | Specific hardware optimization or custom C-library integration. | +* **`pet-pipeline` (Default):** The library pipeline retrieves file information (indexing). + * *Note:* Retrieving file metadata is less costly than loading raw data for arbitrarily chunked files. +* **`standalone` (Custom Loader):** The user is responsible for data loading. + * The `pet-pipeline` provides the indexing/accessor, but the actual data is fetched via custom logic. +* **`mp` (Multiprocessing):** + * `py`: Uses Python processes (disables Dask). + * `1p`: Single worker (serial processing). +* **`` (e.g., `zig`):** Backend-specific computation. + * Assumption: Backend ingests chunks from the `pet-pipeline` and chunking is done on-the-fly. + * *Note:* This differs from expensive Xarray rechunking operations. -### Usage Guidelines +### Execution Matrix ```mermaid -graph TD - A[User Requirement] --> B{Select Mode}; +flowchart TD + A[Start] --> B{Data Loading Strategy}; + + B -- Standard --> C[pet-pipeline
Retrieves Indexing]; + B -- Custom --> D[standalone
User loads Data]; - B -->|Testing/Simple| C[1P + Py]; - B -->|SIMD/Quantization| D[1P + Backend + Standalone]; - B -->|Balanced Computation| E[MP + Backend]; + C --> E{Computation Strategy}; + D --> E; - C --> C1[Portable, Sequential]; - D --> D1[Memory efficient, Custom Loader]; - E --> E1[Platform compatible, Custom Computation]; + E -- Max Python Compatibility --> F[py
Processes]; + E -- Max Performance/Quantized --> G[1p+zig
Custom Backend]; + E -- Hybrid/Parallel --> H[mp+rust
Rust Backend]; + E -- Stability Testing --> I[1p
Single Worker]; - subgraph "Data Access" - F[Pet-Pipeline Index] --> G[Custom Loader]; - end + F --> J[Use Case: Standard ML workflows]; + G --> K[Use Case: Quantized/In-memory]; + H --> L[Use Case: Hybrid Compute]; + I --> M[Use Case: Testing/Debugging]; ``` -*Note: Not all combinations of the above options are exhaustive, but they provide sufficient patterns to construct custom requirements.* +### Selection Guide + +> **NOTE:** Not all combinations are implemented. Use the following logic to select the correct example: + +| Scenario | Recommended Configuration | Reasoning | +| :--- | :--- | :--- | +| **Testing / Simple Methods** | `1p + py` | Minimal overhead, high compatibility. | +| **High Perf / Quantized / In-Memory** | `1p + zig + standalone` | Enables SIMD/efficient code and quantization (e.g., 4-bit representation). | +| **Hybrid Compute** | `mp + rust` | PET pipeline for data retrieval, Rust for computation. | +| **Platform Constraints** | `standalone` | Required if you need fine-grained control or if the platform lacks multiprocessing support (e.g., restricted environments). | +| **Backend Control** | `` | Required if you need custom computation logic (e.g., Numpy vs. Zig). | + +## Available Examples + +### Linux / HPC Environment -## Example Files +These examples are optimized for Linux systems (e.g., RHEL8, Arch Linux) typically running on HPC nodes. + +| Filename | Description | Execution Context | +| :--- | :--- | :--- | +| `nci_py_mp.py` | Multiprocessing with Python on NCI. Uses **PET pipeline**. | HPC / Linux | +| `nci_py_mp_standalone.py` | Multiprocessing with Python on NCI. Uses **adhoc loading**. | HPC / Linux | +| `anylinux_py_mp.py` | Multiprocessing with Python. Uses **PET pipeline**. | Any Linux (tested Arch) | +| `anylinux_py_standalone.py` | Multiprocessing with Python. Uses **adhoc loading**. | Any Linux | + +### General / Local Environment + +These examples focus on portability across different architectures and operating systems. + +| Filename | Description | Execution Context | +| :--- | :--- | :--- | +| `any_py_1p.py` | Sequential processing with Python. Best for **portability** (Windows/Mac/Linux). | Any OS / Architecture | + +### Experimental / Backend Specific + +These examples utilize specific backends (e.g., Zig) and may require additional C libraries or specific OS support. + +| Filename | Description | Notes | +| :--- | :--- | :--- | +| `zigc.py` | Contains various approaches using the **Zig backend** for computation. | **Linux only**. Tested with parallel HDF5 loader, single-threaded, and NCI contexts. Use at your own risk. | -The following table maps specific examples to their environments, modes, and purposes. +> **Resources:** Refer to the following technical documentation for deeper understanding of data storage and loading nuances: +> * [ATPESC 2023: Principles of HPC I/O](https://extremecomputingtraining.anl.gov/wp-content/uploads/sites/96/2023/08/ATPESC-2023-Track-7-Talk-2-carns-io-principles.pdf) +> * [NCSA HDF5 Introduction](https://learn.ncsa.illinois.edu/pluginfile.php/20067/mod_label/intro/HDF_NCSA_3_2024.pdf) -| Filename | Environment | Mode | Description | -| :--- | :--- | :--- | :--- | -| `nci_py_mp.py` | NCI Linux (RHEL8-like) | MP + PET Pipeline | Multiprocessing with satellite data using the standard pipeline. | -| `nci_py_mp_standalone.py` | NCI Linux (RHEL8-like) | MP + Standalone | Ad-hoc data loading on NCI, bypassing the standard pipeline. | -| `anylinux_py_mp.py` | Any Linux (Arch tested) | MP + PET Pipeline | Multiprocessing on general Linux environments with PET pipeline. | -| `anylinux_py_standalone.py` | NCI Linux (RHEL8-like) | MP + Standalone | Ad-hoc loading on NCI. | -| `any_py_1p.py` | Local Machine (Win/Mac/Linux) | 1P + Py | Single-threaded processing for portability. May be slower. | -| `zigc.py` | Linux Only | Zig Backend | Computation examples using Zig. Includes parallel HDF5 loader examples and single-threaded variants. *Note: Linux required; not tested on other platforms.* | From 588f9e6df117c47f9df585f2d4c513604b99e395 Mon Sep 17 00:00:00 2001 From: Nikeeth Ramanathan Date: Thu, 12 Mar 2026 17:30:22 +1100 Subject: [PATCH 05/16] [skip ci] nci example and full persistence flow --- .../persistence/examples/nci_multiprocess.py | 28 ---- .../persistence/examples/nci_py_mp.py | 149 ++++++++++++++++++ .../src/persistence/interface/__init__.py | 18 +++ .../src/persistence/interface/_chunker.py | 2 +- .../src/persistence/interface/_compute.py | 6 +- .../src/persistence/interface/types.py | 10 +- .../src/persistence/methods/_median.py | 46 ++---- .../src/persistence/persistence_impl.py | 112 +++++++------ 8 files changed, 247 insertions(+), 124 deletions(-) delete mode 100644 packages/bundled_models/persistence/examples/nci_multiprocess.py create mode 100644 packages/bundled_models/persistence/examples/nci_py_mp.py diff --git a/packages/bundled_models/persistence/examples/nci_multiprocess.py b/packages/bundled_models/persistence/examples/nci_multiprocess.py deleted file mode 100644 index 8cb42416..00000000 --- a/packages/bundled_models/persistence/examples/nci_multiprocess.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -This example is a WORK IN PROGRESS. - -Use multiprocessing to delegate chunks to various processes, in order to compute the persistence -method in an embarassingly parallel fashion. - -This run specifically targets satellite data on NCI, but is also the most common usecase for -parallel compute. - -TODO: - [ ] example with PersistenceRM wrapper - [ ] example with plain persistence execution on a separate spawned process (separate GIL) - -NOTE: - These examples will eventually also go into the global tutorials. -""" - -def persistence_pipeline_library(): - # TODO: use library directly under main guard - pass - -def persistence_pipeline_spawnproc(): - # TODO: spawn process as separate python command - pass - - -if __name__ == "__main__": - pass diff --git a/packages/bundled_models/persistence/examples/nci_py_mp.py b/packages/bundled_models/persistence/examples/nci_py_mp.py new file mode 100644 index 00000000..3c58d3c2 --- /dev/null +++ b/packages/bundled_models/persistence/examples/nci_py_mp.py @@ -0,0 +1,149 @@ +""" +This example is a WORK IN PROGRESS. + +Use multiprocessing to delegate chunks to various processes, in order to compute the persistence +method in an embarassingly parallel fashion. + +PROS: + - Hooks up to PET pipelines easily. + - Good for small-medium datasets ( typical of hourly data + - 3 ensembles + - 3 levels + """ + # these could also be in main guard, but just being explicit + import numpy as np + import xarray as xr + + # --- Uh Oh! --- + # shape_input1 = (500, 500, 24, 99, 168) + # --- + # NOTE: setting the above would give you this impressive warning which is worth understanding: + # ``` + # numpy._core._exceptions._ArrayMemoryError: Unable to allocate 744. GiB for an array with + # shape (500, 500, 24, 99, 128) and data type float64 + # ``` + # The reason why is important is that you may _think_ this is a reasonably small dataset, and on + # disk it may actually just be stored as 1GB or even less maybe 20MB the reason is: + # 1. bit packing + # 2. compression + # 3. np.nan is not the same as "nothing", otherwise the structural integrity of the array will + # collapse. Sparse arrays will need a sparse array paradigm, but that will also make things + # complicated in the backend. + # 4. wait but this is mocking it in memory, my dataset will be chunked! + # --- + shape_input1 = (500, 500, 24, 10, 24) + dimnames1 = ("x1", "y1", "time", "n_ens", "levels") + dimnames2 = ("x2", "y2", "time") + + # without loss of generality specify two variables, they can have varying dimensions. + # for arguments sake, change the shape of the second. + shape_input2 = (400, 400, 24) + name_varA = "varA" + name_varB = "varB" + + # set unique rng context and constant seed for reprodicibility + rng_context = np.random.default_rng(seed=42) + arr1 = rng_context.random(list(shape_input1)) + arr2 = rng_context.random(list(shape_input2)) + + # make dataset from numpy data above and dims, assume the dim names are common and taken from left + # to right, i.e. either A in B and/or B in A without loss of generality. + ds_mock = xr.Dataset( + { + name_varA: xr.DataArray(arr1, dims=dimnames1), + name_varB: xr.DataArray(arr2, dims=dimnames2), + } + ) + + return ds_mock + + +def run_example(ds_input, use_real=True): + # TODO: use library directly under main guard + print("Example: python multiprocessing on nci.") + print("---") + print("NOTE: this example requires appropriate project data accessible") + print(" it currently uses the satellite data (TODO: which nci group?)") + + # NOTE: scoped import so that context isn't leaked - being safe here though it is likely okay + # for this to be on the global scope or at the very least main guarded is sufficient. + from persistence import persistence_impl + + if use_real: + NotImplementedError("mechanism to run real satellite data not yet implemented") + else: + # --- + # some printing logic for display/debugging + print("using mock data... use_real=False") + print("\n--- mocking data ---") + for v, da in ds_input.data_vars.items(): + print("...") + for i, (n, s) in enumerate(zip(da.dims, da.shape)): + print(f"{v}:shape={n}={s}") + print("---") + # --- + + # --- + # TODO: + # There is a flaw here if time index is not always the first index, since there is no + # guarantee that the datasets share the array dimensions - this needs to be rectified. + # + # This can be done by requesting named index for time at the higher level api instead of the + # integer directly. This is only really necessary for datasets and is infact insufficient + # for numpy. + # + # We still need `idx_time_dim` for `numpy` support, so it'll have to be a mutually + # exclusive argument. + # + # For testing purposes, this is a lower priority since the user can always just stick to + # data arrays and computing each variable separately in a for loop wrapper with minimal loss + # to performance, since the variable count is not likely to be very large. + import time + + ts = time.time() + print(f"ts={ts}") + ds_output = persistence_impl.predict( + ds_input, + idx_time_dim=list(ds_input.dims).index("time"), + num_workers=1, + # 20 chunks/2 workers => 10% of the data is loaded at any given time (assuming optimal chunking) + num_chunks=1, + method="median_of_three", + simple_impute=False, + backend_type="numpy", + ) + + # --- + te = time.time() + print(f"te={te}") + print(f"total={te - ts}s") + print(f"size={ds_output.sizes}") + print("---") + print(ds_output) + + +if __name__ == "__main__": + import multiprocessing + + # For windows/mac you may need to change this to spawn but not guarenteed to work + # NOTE: the inner functions in the package already set the context, but its good to do + # regardless. + # run this in mainguard so it doesn't get regenerated. + ds_input = _mock_dataset() + run_example(ds_input, use_real=False) diff --git a/packages/bundled_models/persistence/src/persistence/interface/__init__.py b/packages/bundled_models/persistence/src/persistence/interface/__init__.py index e69de29b..02584428 100644 --- a/packages/bundled_models/persistence/src/persistence/interface/__init__.py +++ b/packages/bundled_models/persistence/src/persistence/interface/__init__.py @@ -0,0 +1,18 @@ +from persistence.interface._backend import PersistenceBackendType +from persistence.interface._method import PersistenceMethod +from persistence.interface._metadata import PersistenceMetadata +from persistence.interface._compute import PersistenceCompute, PersistenceComputePool +from persistence.interface._chunker import PersistenceChunker, PersistenceChunkInfo +from persistence.interface.types import PetDataArrayLike, PetDataset + +__all__ = [ + "PersistenceBackendType", + "PersistenceMethod", + "PersistenceMetadata", + "PersistenceCompute", + "PersistenceComputePool", + "PersistenceChunker", + "PersistenceChunkInfo", + "PetDataArrayLike", + "PetDataset", +] diff --git a/packages/bundled_models/persistence/src/persistence/interface/_chunker.py b/packages/bundled_models/persistence/src/persistence/interface/_chunker.py index c1a5979d..ad24f9b2 100644 --- a/packages/bundled_models/persistence/src/persistence/interface/_chunker.py +++ b/packages/bundled_models/persistence/src/persistence/interface/_chunker.py @@ -7,7 +7,7 @@ from typing import Generator from persistence.interface._metadata import PersistenceMetadata -from persistence.types import PetDataArrayLike +from persistence.interface.types import PetDataArrayLike # --- # 1000 chunks is more than enough for most usecases. Persistence methods should not be using large diff --git a/packages/bundled_models/persistence/src/persistence/interface/_compute.py b/packages/bundled_models/persistence/src/persistence/interface/_compute.py index f1ba2602..af1e191b 100644 --- a/packages/bundled_models/persistence/src/persistence/interface/_compute.py +++ b/packages/bundled_models/persistence/src/persistence/interface/_compute.py @@ -10,7 +10,7 @@ import numpy as np import xarray as xr -from persistence.types import PetDataArrayLike +from persistence.interface.types import PetDataArrayLike from persistence.methods._impute import SimpleImpute from persistence.methods._median import _median_of_three_numpy from persistence.interface._metadata import PersistenceMetadata @@ -176,8 +176,8 @@ def map_and_join_chunks(self) -> xr.DataArray: if self.metadata.num_workers <= 1: # loop through instead for chunk in iter(self.chunk_generator): - arr_res_chunk = PersistenceComputePool._job_wrapper(chunk) - arr_res[chunk.slice_dims] = arr_res_chunk + res_chunk = PersistenceComputePool._job_wrapper(chunk) + arr_res[*res_chunk.slice_dims] = res_chunk.array else: # dispatch chunks to workers # TODO: forkserver does/may not work with windows/mac, unless main-guarded diff --git a/packages/bundled_models/persistence/src/persistence/interface/types.py b/packages/bundled_models/persistence/src/persistence/interface/types.py index e91ba438..b0947556 100644 --- a/packages/bundled_models/persistence/src/persistence/interface/types.py +++ b/packages/bundled_models/persistence/src/persistence/interface/types.py @@ -153,6 +153,7 @@ def map_each_var( dict_res = {} + # strip to lowest level and compute. for k_var, v_da in self.ds.data_vars.items(): # sense check assert isinstance(v_da, xr.DataArray) @@ -167,6 +168,7 @@ def map_each_var( ds_res = xr.Dataset(dict_res) if self.return_raw_result: + # if returning a raw result compare original type and strip as necessary return self._raw_result(ds_res) # return upgraded dataset by default @@ -180,15 +182,15 @@ def _raw_result(self, ds: xr.Dataset) -> PetDataArrayLike: NOTE: the returned datatype may have dummy names attached, as such these results are for intermediate computation purposes only, not for operational outputs. """ - if self.raw_type == PetDataArrayLike.UNKNOWN: + if self.raw_type == PetInputDataType.UNKNOWN: # this should not happen - _raw_result should not be called externally raise RuntimeError("PetDataset._raw_result: Invalid raw type encountered") - elif self.raw_type == PetDataArrayLike.XR_DATASET: + elif self.raw_type == PetInputDataType.XR_DATASET: # nothing to do return ds - elif self.raw_type == PetDataArrayLike.XR_DATAARRAY: + elif self.raw_type == PetInputDataType.XR_DATAARRAY: # extract the dataarray return ds[self._dummyvarname] - elif self.raw_type == PetDataArrayLike.NP_ARRAY: + elif self.raw_type == PetInputDataType.NP_ARRAY: # extract the numpy array - note this may force a memory load. return ds[self._dummyvarname].values diff --git a/packages/bundled_models/persistence/src/persistence/methods/_median.py b/packages/bundled_models/persistence/src/persistence/methods/_median.py index 015d4dfc..b35edacf 100644 --- a/packages/bundled_models/persistence/src/persistence/methods/_median.py +++ b/packages/bundled_models/persistence/src/persistence/methods/_median.py @@ -1,47 +1,21 @@ import numpy as np import warnings -# TODO: get this from common definition - requires refactor -_LOOKBACK = 3 - def _median_of_three_numpy(arr: np.ndarray, idx_time: int) -> np.ndarray: """ - Computes median of three along the time index, ignores nans; if a - particular coordinate is all nan for the required time indices, the - output is nan for that entry. + Computes median of three along the time index, preserves `nan`. IF a particular coordinate is all + `nan` along the time dimension, THEN the output is `nan` for that entry. """ - # safety: this should have been handled at the top level - len_time = arr.shape[idx_time] - assert len_time >= _LOOKBACK - - # --- - # select relenvant array indices by time, based on lookback - # - # TODO: this should happen someplace higher up assumes latest obs is at the end, similar to - # _LOOKBACK. - idx_end = len_time - idx_start = idx_end - _LOOKBACK - idx_slice = slice(idx_start, idx_end, 1) # start, end, step - # generator for nd-index slicing - idx_all = slice(None, None, None) - nd_slice = (idx_slice if i == idx_time else idx_all for i in range(len(arr.shape))) - # sliced array that only has the latest 3 values - arr_slice = arr[*tuple(nd_slice)] - # --- - - # --- - # calculate the median along the time axis - # - # NOTE: ignore numpy warnings as allowing all `nan` is intentional - # - # NOTE: `keepdims=True` because we want to keep the dimensional structure of the variable - # being computed at a higher level. + # NOTE: + # - ignore numpy warnings as allowing all `nan` is intentional + # - `keepdims=True` because we want to keep the dimensional structure of the variable being + # computed at a higher level. # - # TODO: this should be replaced by a fast median of three algorithm using if/else statements - # or a ternary operator equivilent. + # FUTUREWORK: + # This should be replaced by a fast median of three algorithm using if/else statements or a + # ternary operator equivilent. with warnings.catch_warnings(): warnings.simplefilter("ignore") - arr_median = np.nanmedian(arr_slice, axis=idx_time, keepdims=True) + arr_median = np.nanmedian(arr, axis=idx_time, keepdims=True) return arr_median - # --- diff --git a/packages/bundled_models/persistence/src/persistence/persistence_impl.py b/packages/bundled_models/persistence/src/persistence/persistence_impl.py index 3efd7010..fe66dfc0 100644 --- a/packages/bundled_models/persistence/src/persistence/persistence_impl.py +++ b/packages/bundled_models/persistence/src/persistence/persistence_impl.py @@ -77,14 +77,21 @@ candidate to do this. """ +import xarray as xr + from persistence.interface import ( PetDataArrayLike, PersistenceComputePool, - PersistenceBackend, + PersistenceBackendType, PersistenceMethod, + PersistenceMetadata, + PersistenceChunker, + PersistenceChunkInfo, PetDataset, ) +from persistence.config.dask import _set_synchronous_dask + def predict( arr: PetDataArrayLike, @@ -94,7 +101,7 @@ def predict( method: PersistenceMethod | str = PersistenceMethod.MEDIAN_OF_THREE, simple_impute: bool = True, backend_type: PersistenceBackendType = PersistenceBackendType.NUMPY, -) -> pet_persist.PetDataArrayLike: +) -> PetDataArrayLike: """ Calculate the persistence of historical observations, to be used as a baseline for other models. @@ -200,66 +207,67 @@ def predict( and hence the user cannot provide this signal - it has to be pre-derived and cached by one of the persistence methods dynamically. """ + # force it to EnumStr - auto raises error if not compatible. if isinstance(method, str): - # force it to EnumStr - auto raises error if not compatible. method = PersistenceMethod(method) + if isinstance(backend_type, str): + backend_type = PersistenceBackendType(backend_type) + + # Force to sync dask as early as possible + with _set_synchronous_dask(): + # lift structure to dataset representation (higher order) + # structural order (highest to lowest) + # - xr.Dataset + # - xr.DataArray + # - np.ndarray + pet_ds = PetDataset(arr) + + # construct metadata + metadata = PersistenceMetadata( + idx_time_dim=idx_time_dim, + method=method, + num_workers=num_workers, + num_chunks_desired=num_chunks, + do_impute=simple_impute, + backend=backend_type, + ) + + # apply function on each variable and destruct result + # destructurize ONLYIF original array was lower order + arr_result = pet_ds.map_each_var(_predict_single_var, metadata) + + # safety capture for dev/test + assert isinstance(arr_result, type(arr)) + + return arr_result - # --- DEPRECATED --- - # TODO: remove with_return_raw_result from PetDataset, there's no reason to - # keep the lifted structure when the caller likely only requires the - # original structure back. - # pet_ds = pet_persist.PetDataset(arr).with_return_raw_result(return_raw_result) - # --- - - # lift structure to dataset representation (higher order) - # structural order (highest to lowest) - # - xr.Dataset - # - xr.DataArray - # - np.ndarray - pet_ds = PetDataset(arr) - - raise NotImplementedError("TODO: map to persistence metadata") - - metadta = PersistenceMetadata(...) - - # apply function (ALWAYS) and destruct result (ONLYIF original array was lower order) - arr_result = pet_ds.map_each_var( - _predict_single_var, - metadata, - ) - - # safety capture for dev/test - assert type(arr) == type(arr_result) - return arr_result - - -# TODO: make this ingest PersistenceMetadata instead... def _predict_single_var( da: xr.DataArray, - idx_time_dim: int, - num_chunks: int = None, - method: PersistenceMethod = PersistenceMethod.MEDIAN_OF_THREE, - simple_impute: bool = True, -): + metadata: PersistenceMetadata, +) -> xr.DataArray: """ Computes persistence for a single data array, has the same interface as _compute_persistence except that the first argument is a data array. + + input: dataarray -> chunk -> impute -> compute persistence -> merge chunks -> dataarray :output """ - # create metadata - - # input dataarray -> chunk -> impute -> compute persistence -> merge chunks - chunker = PersistenceChunker( - da_lazy=da, - method=method, - num_chunks=num_chunks, - idx_time_dim=idx_time, - ) - - # TODO: worker pool - # TODO: work chain i.e. slice -> impute -> compute - # TODO: merge result - raise NotImplementedError("TODO - some missing parts") + # --- simple chunk strategy (split) --- + # build chunker struct + chunker = PersistenceChunker(da=da, metadata=metadata) + # this would have been filled up post-init or an error would have been raised. + chunk_info = chunker.chunk_info + # lazy - returns generator only. + chunk_generator = chunker.generate_chunks() + + # --- launch compute pool and run method against chunks (apply and join)--- + # build compute struct + # - this registers things from the metadata such as method and backend etc. + # - uses chunk info to determine how to re-join the chunks. + worker_pool = PersistenceComputePool(chunk_generator, chunk_info, metadata) + da_result = worker_pool.map_and_join_chunks() + + return da_result if __name__ == "__main__": From 0e9c7551ba0e03ad410c1c687c78e9d6136817fe Mon Sep 17 00:00:00 2001 From: Nikeeth Ramanathan Date: Thu, 12 Mar 2026 18:10:50 +1100 Subject: [PATCH 06/16] [skip ci] adjust description to reflect the cons of using multiprocessing without sufficient planning --- packages/bundled_models/persistence/examples/nci_py_mp.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/bundled_models/persistence/examples/nci_py_mp.py b/packages/bundled_models/persistence/examples/nci_py_mp.py index 3c58d3c2..826f7ec4 100644 --- a/packages/bundled_models/persistence/examples/nci_py_mp.py +++ b/packages/bundled_models/persistence/examples/nci_py_mp.py @@ -10,7 +10,11 @@ CONS: - Not good for largescale operational use. - Taylored for NCI use only. - - windows/mac support not guarenteed. + - Windows/mac support not guarenteed. + - Data MUST be stored in a chunked fashion for multiprocessing to have any benefit, otherwise + everything will be loaded in memory anyway, and its better to let numpy handle everything (set + workers=1 and num_chunks=1). TODO: there likely should be a smart way to detect this. + - Slow. """ From 4a62c3e2e5d8335953d4449f9963067f909d397b Mon Sep 17 00:00:00 2001 From: Nikeeth Ramanathan Date: Fri, 13 Mar 2026 00:14:17 +1100 Subject: [PATCH 07/16] [skip ci] zig wip --- .../persistence/src/zig/lib.zig | 1 + .../persistence/src/zig/median.zig | 180 ++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 packages/bundled_models/persistence/src/zig/lib.zig create mode 100644 packages/bundled_models/persistence/src/zig/median.zig diff --git a/packages/bundled_models/persistence/src/zig/lib.zig b/packages/bundled_models/persistence/src/zig/lib.zig new file mode 100644 index 00000000..95a0b682 --- /dev/null +++ b/packages/bundled_models/persistence/src/zig/lib.zig @@ -0,0 +1 @@ +const std = @import("std"); diff --git a/packages/bundled_models/persistence/src/zig/median.zig b/packages/bundled_models/persistence/src/zig/median.zig new file mode 100644 index 00000000..5b530e03 --- /dev/null +++ b/packages/bundled_models/persistence/src/zig/median.zig @@ -0,0 +1,180 @@ +const std = @import("std"); +const nanf32 = std.math.nan(f32); + +// ---------------------------------------------------------------------------- +// Description: +// Calculate median of three of an n-d array. Memory is allocated by numpy +// (python) and passed in. +// ---------------------------------------------------------------------------- +// Args: +// idx_time: time index +// shape: shape of input array +// len_shape: shape of input array +// arr_in: pointer to n-dimensional array +// len_in: length of input array +// arr_out: pointer to n-dimensional pre-allocated output +// len_out: length of output array +// ---------------------------------------------------------------------------- +fn medianofthree_numpy( + idx_time: i32, + shape: [*]i32, + len_shape: i32, + arr_in: [*]f32, + len_in: i32, + arr_out: [*]f32, + len_out: i32, +) void { + // UNIMPLEMENTED + _ = &idx_time; + _ = &shape; + _ = &len_shape; + _ = &arr_in; + _ = &len_in; + _ = &arr_out; + _ = &len_out; +} + +// ---------------------------------------------------------------------------- +// Description: +// Calculate median of three of scalars. +// TODO: there may be a more efficient way. +// ---------------------------------------------------------------------------- +// Alg: +// input: (f32, f32, f32) +// output: f32 +// +// {function state} +// state: +// - array[3]: container for valid inputs +// - count: number of valid inputs (non-nan) +// +// {nan filtering} +// traverse inputs: +// input is nan => skip +// else => store in array and increment +// +// {switch statement - NOTE: can be comptime} +// compute median: +// valid count = 0 => return NaN +// valid count = 1 => return x[0] +// valid count = 2 => return (x[0] + x[1]) / 2 +// valid count = 3 => return max(min(x[0], x[1]), x[2]) or similar +// ---------------------------------------------------------------------------- +// Args: +// x1, x2, x3: values to compute the median against +// ---------------------------------------------------------------------------- +fn medianofthree_scalar_nanfiltered(x1: f32, x2: f32, x3: f32) f32 { + var valid = [3]f32{ nanf32, nanf32, nanf32 }; + const xs = [3]f32{ x1, x2, x3 }; + var num_valid: u4 = 0; + for (xs) |x| { + if (!std.math.isNan(x)) { + valid[num_valid] = x; + num_valid += 1; + } + } + + std.debug.print("x1={},x2={},x3={},num_valid={}\n", .{ x1, x2, x3, num_valid }); + + return medianofthree_scalar(num_valid, valid); +} + +// ---------------------------------------------------------------------------- +// Description: +// Calculate median of a 3 element array, nans are masked. Unless the array +// is all-nan in which case nan is returned. The switch prongs are comptime +// resolvable since the choices are limited. Hopefully that makes it fast. +// ---------------------------------------------------------------------------- +// Alg: +// given [3]f32 array, x0, x1, x2 being the elements we need to compute the +// median: +// +// 1. choosing x0' = min(x0, x1), x1' = min(x1, x2), x2' = min(x0, x2), +// - {x0', x1', x2'} is guarenteed to have exactly two unique variables +// +// (NOTE: variables, NOT values e.g. {x0, x1, x0} has two unique variables, +// {x0, x1, x2} and {x0, x0, x0} do not.) +// +// - therefore, one of them must be the median. +// +// 2. the median has to be greater than the minimum of x1, x2, x3 so the +// only guarenteed choice is to take the max of all three min-pairs: +// +// median = max(max(x1', x2'), x0') +// +// 3. the expanded formula is given as: +// +// max(max(min(x1, x2), min(x0, x2)), min(x0, x1)) +// +// 4. note that max(min(x1, x2), min(x0, x2)): +// +// if x2 < x1, x0 => x2 +// if x1 < x2 < x0 (or x0 < x2 < x1) => x2 +// if x0 < x1 < x2 (or x1 < x1 < x2) => max(x0, x1) +// +// which is equivilent to: +// +// min(max(x0, x1), x2) +// +// i.e. I only choose x0 or x1 if x2 is an upper bound of {x0, x1} +// +// 5. substituing 4. into 3. we can now contract the number of operations +// from 5 binary operations to 4. (though the compiler likely may have +// done this anyway.) +// +// median = max(min(max(x0, x1), x2), min(x0, x1)) +// ---------------------------------------------------------------------------- +// NOTE: the above describe the scenario where x0, x1, x2 are unique, without +// loss of generality. Duplicate entries do not change the outcome. +// ---------------------------------------------------------------------------- +// Args: +// num_valid: valid count to determine which operation to use for median +// valid: the state array containing valid values +// ---------------------------------------------------------------------------- +fn medianofthree_scalar(num_valid: u4, valid: [3]f32) f32 { + return switch (num_valid) { + 0 => nanf32, + 1 => valid[0], + 2 => @as(f32, 0.5) * (valid[0] + valid[1]), + 3 => blk: { + const x0: f32, const x1: f32, const x2: f32 = valid; + const median = @max(@max(@min(x0, x1), @min(x1, x2)), @min(x0, x2)); + break :blk median; + }, + else => nanf32, + }; +} + +test "median of three test fleet" { + // 0. median of all nan + var x1: f32 = nanf32; + var x2: f32 = nanf32; + var x3: f32 = nanf32; + var expect: f32 = nanf32; + var result = medianofthree_scalar_nanfiltered(x1, x2, x3); + try std.testing.expectEqual(std.math.isNan(expect), std.math.isNan(result)); + + // 1. median of one + x1 = nanf32; + x2 = nanf32; + x3 = 0.5; + expect = 0.5; + result = medianofthree_scalar_nanfiltered(x1, x2, x3); + try std.testing.expectEqual(expect, result); + + // 2. median of two (mean) + x1 = 5.0; + x2 = nanf32; + x3 = -10.0; + expect = -2.5; + result = medianofthree_scalar_nanfiltered(x1, x2, x3); + try std.testing.expectEqual(expect, result); + + // 3. median of three (actually median) + x1 = -5.0; + x2 = 20.0; + x3 = -10.0; + expect = -5.0; + result = medianofthree_scalar_nanfiltered(x1, x2, x3); + try std.testing.expectEqual(expect, result); +} From f1117407d167b5f2357cf52e4f974fa676647bbc Mon Sep 17 00:00:00 2001 From: Nikeeth Ramanathan Date: Fri, 13 Mar 2026 00:15:50 +1100 Subject: [PATCH 08/16] [skip ci] simplifiy median operation a little. --- packages/bundled_models/persistence/src/zig/median.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bundled_models/persistence/src/zig/median.zig b/packages/bundled_models/persistence/src/zig/median.zig index 5b530e03..edd0e3c2 100644 --- a/packages/bundled_models/persistence/src/zig/median.zig +++ b/packages/bundled_models/persistence/src/zig/median.zig @@ -138,7 +138,7 @@ fn medianofthree_scalar(num_valid: u4, valid: [3]f32) f32 { 2 => @as(f32, 0.5) * (valid[0] + valid[1]), 3 => blk: { const x0: f32, const x1: f32, const x2: f32 = valid; - const median = @max(@max(@min(x0, x1), @min(x1, x2)), @min(x0, x2)); + const median = @max(@max(@min(x0, x1), x2), @min(x0, x2)); break :blk median; }, else => nanf32, From 538227cab8b685d220967f2e6b95d4377dff40b3 Mon Sep 17 00:00:00 2001 From: Nikeeth Ramanathan Date: Sun, 15 Mar 2026 18:30:41 +1100 Subject: [PATCH 09/16] [skip ci] wip external call example with zig --- .../persistence/src/lib/zig/lib.zig | 6 + .../persistence/src/{ => lib}/zig/median.zig | 112 +++++++++++++++--- .../persistence/src/zig/lib.zig | 1 - 3 files changed, 104 insertions(+), 15 deletions(-) create mode 100644 packages/bundled_models/persistence/src/lib/zig/lib.zig rename packages/bundled_models/persistence/src/{ => lib}/zig/median.zig (62%) delete mode 100644 packages/bundled_models/persistence/src/zig/lib.zig diff --git a/packages/bundled_models/persistence/src/lib/zig/lib.zig b/packages/bundled_models/persistence/src/lib/zig/lib.zig new file mode 100644 index 00000000..c584c43d --- /dev/null +++ b/packages/bundled_models/persistence/src/lib/zig/lib.zig @@ -0,0 +1,6 @@ +const std = @import("std"); +const median = @import("./median.zig"); + +export fn median_of_three(x1: f32, x2: f32, x3: f32) f32 { + return median.medianofthree_scalar_nanfiltered(x1, x2, x3); +} diff --git a/packages/bundled_models/persistence/src/zig/median.zig b/packages/bundled_models/persistence/src/lib/zig/median.zig similarity index 62% rename from packages/bundled_models/persistence/src/zig/median.zig rename to packages/bundled_models/persistence/src/lib/zig/median.zig index edd0e3c2..a8fbacfe 100644 --- a/packages/bundled_models/persistence/src/zig/median.zig +++ b/packages/bundled_models/persistence/src/lib/zig/median.zig @@ -15,23 +15,63 @@ const nanf32 = std.math.nan(f32); // arr_out: pointer to n-dimensional pre-allocated output // len_out: length of output array // ---------------------------------------------------------------------------- -fn medianofthree_numpy( +fn medianofthree_split_nd( idx_time: i32, shape: [*]i32, len_shape: i32, arr_in: [*]f32, - len_in: i32, arr_out: [*]f32, len_out: i32, ) void { - // UNIMPLEMENTED - _ = &idx_time; - _ = &shape; - _ = &len_shape; - _ = &arr_in; - _ = &len_in; - _ = &arr_out; - _ = &len_out; + // --- probably not optimal - for simplicity --- + // var arena = std.heap.ArenaAllocator.init(std.heap.c_allocator); + // defer arena.deinit(); + // const allocator = arena.allocator(); + // --- + const shape_arr: []i32 = shape[0..@as(usize, @intCast(len_shape))]; + const len_chunk: usize, const len_outer: usize = blk: { + var _prod_inner: usize = 1; + var _prod_outer: usize = 1; + for (shape_arr, 0..) |s, i| { + const s_usize: usize = @intCast(s); + if (i > idx_time) _prod_inner *= s_usize; + if (i < idx_time) _prod_outer *= s_usize; + } + break :blk .{ _prod_inner, _prod_outer }; + }; + + // safety + std.debug.assert(@as(usize, @intCast(len_out)) == len_chunk * len_outer); + + for (0..len_outer) |i| { + // --- + // start + const chunk_idxs = len_chunk * i; + // --- 3 equal length chunks representing time indices --- + // TODO: a more generic strategy required for historically lengthier metrics + const chunk_idx1 = 3 * chunk_idxs; + const chunk_idx2 = chunk_idx1 + len_chunk; + const chunk_idx3 = chunk_idx2 + len_chunk; + // --- + // end + const chunk_idxe = chunk_idx3 + len_chunk; + // --- + + // get chunks that are contiguous, in one go to avoid jumps + // slice view of contiguous cuhnks, so memory allocation not required. + const cntg_chunk1 = arr_in[chunk_idx1..chunk_idx2]; + const cntg_chunk2 = arr_in[chunk_idx2..chunk_idx3]; + const cntg_chunk3 = arr_in[chunk_idx3..chunk_idxe]; + + // fill output array + for (0..len_chunk) |j| { + arr_out[chunk_idxs + j] = medianofthree_scalar_nanfiltered( + cntg_chunk1[j], + cntg_chunk2[j], + cntg_chunk3[j], + ); + } + } } // ---------------------------------------------------------------------------- @@ -63,7 +103,7 @@ fn medianofthree_numpy( // Args: // x1, x2, x3: values to compute the median against // ---------------------------------------------------------------------------- -fn medianofthree_scalar_nanfiltered(x1: f32, x2: f32, x3: f32) f32 { +pub fn medianofthree_scalar_nanfiltered(x1: f32, x2: f32, x3: f32) f32 { var valid = [3]f32{ nanf32, nanf32, nanf32 }; const xs = [3]f32{ x1, x2, x3 }; var num_valid: u4 = 0; @@ -74,8 +114,6 @@ fn medianofthree_scalar_nanfiltered(x1: f32, x2: f32, x3: f32) f32 { } } - std.debug.print("x1={},x2={},x3={},num_valid={}\n", .{ x1, x2, x3, num_valid }); - return medianofthree_scalar(num_valid, valid); } @@ -138,7 +176,7 @@ fn medianofthree_scalar(num_valid: u4, valid: [3]f32) f32 { 2 => @as(f32, 0.5) * (valid[0] + valid[1]), 3 => blk: { const x0: f32, const x1: f32, const x2: f32 = valid; - const median = @max(@max(@min(x0, x1), x2), @min(x0, x2)); + const median = @max(@min(@max(x0, x1), x2), @min(x0, x1)); break :blk median; }, else => nanf32, @@ -178,3 +216,49 @@ test "median of three test fleet" { result = medianofthree_scalar_nanfiltered(x1, x2, x3); try std.testing.expectEqual(expect, result); } + +test "median of three nd" { + { + var test_arr_in: [5][4][3][6][3]f32 = undefined; + var test_arr_out: [5][4][1][6][3]f32 = undefined; + const total_len = 5 * 4 * 3 * 6 * 3; + for (0..total_len) |i| { + const arr_ptr: [*]f32 = @ptrCast(&test_arr_in); + arr_ptr[i] = @as(f32, @floatFromInt(i)); + } + var shape = [_]i32{ 5, 4, 3, 6, 3 }; + medianofthree_split_nd( + 2, + &shape, + 5, + @ptrCast(&test_arr_in), + @ptrCast(&test_arr_out), + total_len / 3, + ); + const arr_out_ptr: [*]f32 = @ptrCast(&test_arr_out); + const arr_in_ptr: [*]f32 = @ptrCast(&test_arr_in); + std.debug.print("{any}\n", .{arr_in_ptr[0..total_len]}); + std.debug.print("{any}\n", .{arr_out_ptr[0..(total_len / 3)]}); + } + + { + var test_arr_in = [2][3]f32{ + [_]f32{ 2, -5, 4 }, + [_]f32{ 5, 100, -2 }, + }; + var test_arr_out: [2][1]f32 = undefined; + var shape = [_]i32{ 2, 3 }; + const out = medianofthree_scalar_nanfiltered(2, -5, 4); + medianofthree_split_nd( + 1, + &shape, + 2, + @ptrCast(&test_arr_in), + @ptrCast(&test_arr_out), + 2, + ); + std.debug.print("\n{any}\n", .{out}); + std.debug.print("\n{any}\n", .{test_arr_in}); + std.debug.print("\n{any}\n", .{test_arr_out}); + } +} diff --git a/packages/bundled_models/persistence/src/zig/lib.zig b/packages/bundled_models/persistence/src/zig/lib.zig deleted file mode 100644 index 95a0b682..00000000 --- a/packages/bundled_models/persistence/src/zig/lib.zig +++ /dev/null @@ -1 +0,0 @@ -const std = @import("std"); From a81d65ace43ab2fb051fc03c0c8f5696996e0fcd Mon Sep 17 00:00:00 2001 From: Nikeeth Ramanathan Date: Sun, 15 Mar 2026 18:31:59 +1100 Subject: [PATCH 10/16] [skip ci] wip third party cffi --- .../bundled_models/persistence/.gitignore | 1 + packages/bundled_models/persistence/build.zig | 17 ++++ .../persistence/examples/zigc.py | 5 ++ packages/bundled_models/persistence/pixi.lock | 81 ++++++++++++++++++- .../bundled_models/persistence/pyproject.toml | 3 + .../bundled_models/persistence/setup_dev.sh | 23 ++++++ .../src/persistence/include/__init__.py | 0 .../src/persistence/include/_cffi.py | 33 ++++++++ .../src/persistence/methods/_median.py | 9 +++ 9 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 packages/bundled_models/persistence/build.zig create mode 100644 packages/bundled_models/persistence/examples/zigc.py create mode 100755 packages/bundled_models/persistence/setup_dev.sh create mode 100644 packages/bundled_models/persistence/src/persistence/include/__init__.py create mode 100644 packages/bundled_models/persistence/src/persistence/include/_cffi.py diff --git a/packages/bundled_models/persistence/.gitignore b/packages/bundled_models/persistence/.gitignore index 75bc4918..8eb575f6 100644 --- a/packages/bundled_models/persistence/.gitignore +++ b/packages/bundled_models/persistence/.gitignore @@ -1,4 +1,5 @@ # pixi environments +.zig-cache .pixi/* !.pixi/config.toml report.xml diff --git a/packages/bundled_models/persistence/build.zig b/packages/bundled_models/persistence/build.zig new file mode 100644 index 00000000..a8860c5d --- /dev/null +++ b/packages/bundled_models/persistence/build.zig @@ -0,0 +1,17 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + const lib_persistence_zig = b.addLibrary(.{ + .name = "persistence_zig", + .linkage = .dynamic, + .root_module = b.createModule(.{ + .root_source_file = b.path("src/lib/zig/lib.zig"), + .target = target, + .optimize = optimize, + }), + }); + lib_persistence_zig.linkLibC(); + b.installArtifact(lib_persistence_zig); +} diff --git a/packages/bundled_models/persistence/examples/zigc.py b/packages/bundled_models/persistence/examples/zigc.py new file mode 100644 index 00000000..1fda47e7 --- /dev/null +++ b/packages/bundled_models/persistence/examples/zigc.py @@ -0,0 +1,5 @@ +from persistence.methods._median import _median_of_three_zig + +if __name__ == "__main__": + m =_median_of_three_zig(5,-2, 3) + print(m) diff --git a/packages/bundled_models/persistence/pixi.lock b/packages/bundled_models/persistence/pixi.lock index 6fad0157..1b8cd7b6 100644 --- a/packages/bundled_models/persistence/pixi.lock +++ b/packages/bundled_models/persistence/pixi.lock @@ -34,6 +34,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py313hf46b229_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.1-pyh8f84b5b_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/cytoolz-1.1.0-py313h07c4f96_1.conda @@ -101,8 +102,10 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/locket-1.0.0-pyhd8ed1ab_0.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py313h3dea7bd_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/meson-1.10.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.1.2-py313h7037e92_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ninja-1.13.2-h171cf75_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h54a6638_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.4.2-py313hf6604e3_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda @@ -114,6 +117,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.2.2-py313h54dd161_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-23.0.0-py313h78bf25f_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-23.0.0-py313h98bfbea_0_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.12-hc97d973_100_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda @@ -122,6 +126,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/re2-2025.11.05-h5301d42_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.6.2-he8a4886_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda @@ -195,6 +200,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py313hf46b229_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.2-h33c6efd_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_101.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-5_h4a7cf45_openblas.conda @@ -213,15 +219,19 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_17.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.3-h5347b49_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/meson-1.10.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ninja-1.13.2-h171cf75_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.4.2-py313hf6604e3_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-3.0.0-py313hbfd7664_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.12-hc97d973_100_cp313.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda @@ -310,6 +320,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py313hf46b229_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.1-pyh8f84b5b_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda @@ -388,8 +399,10 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py313h3dea7bd_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/meson-1.10.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.1.2-py313h7037e92_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ninja-1.13.2-h171cf75_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h54a6638_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.4.2-py313hf6604e3_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda @@ -407,6 +420,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-23.0.0-py313h78bf25f_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-23.0.0-py313h98bfbea_0_cpu.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.2-pyhcf101f3_0.conda @@ -420,6 +434,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.15.0-h40fa522_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.6.2-he8a4886_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda @@ -851,6 +866,22 @@ packages: version: 2026.1.4 sha256: 9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c requires_python: '>=3.7' +- conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-2.0.0-py313hf46b229_1.conda + sha256: 2162a91819945c826c6ef5efe379e88b1df0fe9a387eeba23ddcf7ebeacd5bd6 + md5: d0616e7935acab407d1543b28c446f6f + depends: + - __glibc >=2.17,<3.0.a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - pycparser + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + license: MIT + license_family: MIT + purls: + - pkg:pypi/cffi?source=hash-mapping + size: 298357 + timestamp: 1761202966461 - pypi: https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl name: charset-normalizer version: 3.4.4 @@ -2169,6 +2200,19 @@ packages: - pkg:pypi/matplotlib-inline?source=hash-mapping size: 15175 timestamp: 1761214578417 +- conda: https://conda.anaconda.org/conda-forge/noarch/meson-1.10.1-pyhcf101f3_0.conda + sha256: c97f42730fcab178be043f7de3093f419b5ad179370c00494d46a472971f7bf7 + md5: 6c07238c531b1f93603c6908d1a4ef4f + depends: + - python >=3.10 + - ninja >=1.8.2 + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/meson?source=hash-mapping + size: 760481 + timestamp: 1768994208765 - conda: https://conda.anaconda.org/conda-forge/linux-64/msgpack-python-1.1.2-py313h7037e92_1.conda sha256: fac37e267dd1d07527f0b078ffe000916e80e8c89cfe69d466f5775b88e93df2 md5: cd1cfde0ea3bca6c805c73ffa988b12a @@ -2203,6 +2247,18 @@ packages: purls: [] size: 891641 timestamp: 1738195959188 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ninja-1.13.2-h171cf75_0.conda + sha256: 6f7d59dbec0a7b00bf5d103a4306e8886678b796ff2151b62452d4582b2a53fb + md5: b518e9e92493721281a60fa975bddc65 + depends: + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 186323 + timestamp: 1763688260928 - conda: https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.12.0-h54a6638_1.conda sha256: fd2cbd8dfc006c72f45843672664a8e4b99b2f8137654eaae8c3d46dca776f63 md5: 16c2a0e9c4a166e53632cfca4f68d020 @@ -2556,6 +2612,18 @@ packages: - pkg:pypi/pyarrow?source=hash-mapping size: 4776275 timestamp: 1770672664641 +- conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + sha256: 79db7928d13fab2d892592223d7570f5061c192f27b9febd1a418427b719acc6 + md5: 12c566707c80111f9799308d9e265aef + depends: + - python >=3.9 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pycparser?source=hash-mapping + size: 110100 + timestamp: 1733195786147 - pypi: https://files.pythonhosted.org/packages/b3/f8/f47b90fbeaf36e112b1a93fc313d5f0bc9f0051ae8be734173787a00271a/pyearthtools_data-0.5.1-py3-none-any.whl name: pyearthtools-data version: 0.5.1 @@ -2588,7 +2656,7 @@ packages: - pypi: ./ name: pyearthtools-persistence version: 0.6.0 - sha256: b1a739e368b0b6b224e7bd805870c2d5c9e66183a749d55c2f4ae7739e268bf6 + sha256: 5bfc864f36b9852afbf5135847de28dcfd7087cfc682b360f2639cd84d1aa4dd requires_dist: - pyearthtools-zoo>=0.5.0 - pyearthtools-data>=0.5.0 @@ -3022,6 +3090,17 @@ packages: - ruff>=0.12.0 ; extra == 'dev' - cython-lint>=0.12.2 ; extra == 'dev' requires_python: '>=3.11' +- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + sha256: 82088a6e4daa33329a30bc26dc19a98c7c1d3f05c0f73ce9845d4eab4924e9e1 + md5: 8e194e7b992f99a5015edbd4ebd38efd + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/setuptools?source=compressed-mapping + size: 639697 + timestamp: 1773074868565 - pypi: https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl name: shapely version: 2.1.2 diff --git a/packages/bundled_models/persistence/pyproject.toml b/packages/bundled_models/persistence/pyproject.toml index 0e487cea..dbe3d989 100644 --- a/packages/bundled_models/persistence/pyproject.toml +++ b/packages/bundled_models/persistence/pyproject.toml @@ -73,6 +73,9 @@ pyearthtools-persistence = { path = ".", editable = true } [tool.pixi.dependencies] python = ">=3.11,<3.14" xarray = ">=2026.1.0,<2027" +meson = ">=1.10.1,<2" +cffi = ">=2.0.0,<3" +setuptools = ">=82.0.1,<83" [tool.pixi.feature.testing.dependencies] pytest = ">=9.0.2,<10" diff --git a/packages/bundled_models/persistence/setup_dev.sh b/packages/bundled_models/persistence/setup_dev.sh new file mode 100755 index 00000000..2f08e4b2 --- /dev/null +++ b/packages/bundled_models/persistence/setup_dev.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +# setup build in include folder +( +cd src/persistence/include +rm -r lib/* +rm -r lib/*.a +rm -r __pycache__/ +rm *.c +rm *.so +rm *.o +rm *.a +) + +zig build --prefix src/persistence/include + +# run cffi +( +cd src/persistence/include +# move shared libraries to same directory, required for runs +cp ./lib/* . +python _cffi.py +) diff --git a/packages/bundled_models/persistence/src/persistence/include/__init__.py b/packages/bundled_models/persistence/src/persistence/include/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/packages/bundled_models/persistence/src/persistence/include/_cffi.py b/packages/bundled_models/persistence/src/persistence/include/_cffi.py new file mode 100644 index 00000000..59c37e4b --- /dev/null +++ b/packages/bundled_models/persistence/src/persistence/include/_cffi.py @@ -0,0 +1,33 @@ +""" +Compile cffi code and put them in the include directory +""" +from cffi import FFI +import sys +import os + + +_zig_c_declarations = """ +float median_of_three(float, float, float); +""" +_zig_c_libname="libpersistence_zig" +_include_libdir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "lib") + +def compile_zig(): + # cffi + ffibuilder = FFI() + # this is for python to know about + ffibuilder.cdef(_zig_c_declarations) + # NOTE: this is needed for API mode (recommended) + # no header here so declaration is repeated. + ffibuilder.set_source( + "_persistence_zig", + _zig_c_declarations, + libraries=["persistence_zig"], + library_dirs=[_include_libdir], + extra_link_args=[f"-Wl,-rpath={_include_libdir}"] + ) + ffibuilder.compile(verbose=True) + + +if __name__ == "__main__": + compile_zig() diff --git a/packages/bundled_models/persistence/src/persistence/methods/_median.py b/packages/bundled_models/persistence/src/persistence/methods/_median.py index b35edacf..5b1b5699 100644 --- a/packages/bundled_models/persistence/src/persistence/methods/_median.py +++ b/packages/bundled_models/persistence/src/persistence/methods/_median.py @@ -1,6 +1,8 @@ import numpy as np import warnings +import persistence.include._persistence_zig +from persistence.include._persistence_zig import ffi, lib def _median_of_three_numpy(arr: np.ndarray, idx_time: int) -> np.ndarray: """ @@ -19,3 +21,10 @@ def _median_of_three_numpy(arr: np.ndarray, idx_time: int) -> np.ndarray: warnings.simplefilter("ignore") arr_median = np.nanmedian(arr, axis=idx_time, keepdims=True) return arr_median + + +# --- TODO: for testing only +def _median_of_three_zig(x1: int, x2: int, x3: int) -> int: + y = lib.median_of_three(x1, x2, x3) + return y +# --- From da0e6033dacce6f97283d5fc085c33b4ca8b7798 Mon Sep 17 00:00:00 2001 From: Nikeeth Ramanathan Date: Mon, 16 Mar 2026 13:48:21 +1100 Subject: [PATCH 11/16] [skip ci] zig - cffi pattern (method2) --- .../persistence/examples/nci_py_mp.py | 19 +- .../persistence/examples/zigc.py | 216 +++++++++++++++++- .../persistence/src/lib/zig/lib.zig | 20 ++ .../persistence/src/lib/zig/median.zig | 4 +- .../src/persistence/include/.gitignore | 5 + .../src/persistence/include/_cffi.py | 7 +- .../src/persistence/interface/_backend.py | 4 +- .../src/persistence/interface/_chunker.py | 25 +- .../src/persistence/interface/_compute.py | 88 +++++-- .../src/persistence/interface/types.py | 5 +- .../src/persistence/methods/_median.py | 115 +++++++++- 11 files changed, 466 insertions(+), 42 deletions(-) create mode 100644 packages/bundled_models/persistence/src/persistence/include/.gitignore diff --git a/packages/bundled_models/persistence/examples/nci_py_mp.py b/packages/bundled_models/persistence/examples/nci_py_mp.py index 826f7ec4..d9b499bc 100644 --- a/packages/bundled_models/persistence/examples/nci_py_mp.py +++ b/packages/bundled_models/persistence/examples/nci_py_mp.py @@ -78,7 +78,7 @@ def _mock_dataset(): return ds_mock -def run_example(ds_input, use_real=True): +def run_example(ds_input, use_real=True, num_workers=1, num_chunks=1): # TODO: use library directly under main guard print("Example: python multiprocessing on nci.") print("---") @@ -125,9 +125,9 @@ def run_example(ds_input, use_real=True): ds_output = persistence_impl.predict( ds_input, idx_time_dim=list(ds_input.dims).index("time"), - num_workers=1, + num_workers=num_workers, # 20 chunks/2 workers => 10% of the data is loaded at any given time (assuming optimal chunking) - num_chunks=1, + num_chunks=num_chunks, method="median_of_three", simple_impute=False, backend_type="numpy", @@ -145,9 +145,12 @@ def run_example(ds_input, use_real=True): if __name__ == "__main__": import multiprocessing - # For windows/mac you may need to change this to spawn but not guarenteed to work - # NOTE: the inner functions in the package already set the context, but its good to do - # regardless. - # run this in mainguard so it doesn't get regenerated. + # CAUTION: windows/mac - this may not work, use num_workers=1 instead + try: + multiprocessing.set_start_method("forkserver") + print("Start method set to 'forkserver'") + except RuntimeError as e: + print(f"Could not set start method: {e}") + ds_input = _mock_dataset() - run_example(ds_input, use_real=False) + run_example(ds_input, use_real=False, num_workers=1, num_chunks=1) diff --git a/packages/bundled_models/persistence/examples/zigc.py b/packages/bundled_models/persistence/examples/zigc.py index 1fda47e7..b0a37308 100644 --- a/packages/bundled_models/persistence/examples/zigc.py +++ b/packages/bundled_models/persistence/examples/zigc.py @@ -1,5 +1,217 @@ +""" +This example is a WORK IN PROGRESS. + +Uses zig to process chunks instead of numpy. Optionally uses multiprocessing to delegate chunks. + +SETUP: + - run setup_dev.sh or appropriate pixi command (TODO) + - this will build the zig shared library + - NOTE: the shared library interface is always going to be slower than calling zig directly, see + FUTUREWORK for target state. + +PROS: + - Hooks up to PET pipelines easily. + - Good for small-medium datasets ( typical of hourly data + - 3 ensembles + - 3 levels + """ + # these could also be in main guard, but just being explicit + import numpy as np + import xarray as xr + + # --- Uh Oh! --- + # shape_input1 = (500, 500, 24, 99, 168) + # --- + # NOTE: setting the above would give you this impressive warning which is worth understanding: + # ``` + # numpy._core._exceptions._ArrayMemoryError: Unable to allocate 744. GiB for an array with + # shape (500, 500, 24, 99, 128) and data type float64 + # ``` + # The reason why is important is that you may _think_ this is a reasonably small dataset, and on + # disk it may actually just be stored as 1GB or even less maybe 20MB the reason is: + # 1. bit packing + # 2. compression + # 3. np.nan is not the same as "nothing", otherwise the structural integrity of the array will + # collapse. Sparse arrays will need a sparse array paradigm, but that will also make things + # complicated in the backend. + # 4. wait but this is mocking it in memory, my dataset will be chunked! + # --- + shape_input1 = (500, 500, 9, 24, 6) + shape_input2 = (400, 400, 9) + shape_input3 = (5, 2, 9, 2) # for manual inspection + dimnames1 = ("x1", "y1", "time", "n_ens", "levels") + dimnames2 = ("x2", "y2", "time") + dimnames3 = ("k", "x3", "time", "y3") + name_varA = "varA" + name_varB = "varB" + name_varC = "varC" + + # set unique rng context and constant seed for reprodicibility + rng_context = np.random.default_rng(seed=42) + arr1 = rng_context.random(list(shape_input1)) + arr2 = rng_context.random(list(shape_input2)) + arr3 = np.array( + [ + [ + [[1.0, -100.0], [4.0, -20.0], [-1.0, -200.0]] * 3, + [[1.0, 20.0], [1.0, 5.0], [1.0, 4.5]] * 3, + ], + [ + [[1.0, -100.0], [4.0, -20.0], [-1.0, -200.0]] * 3, + [[3.0, 20.0], [1.0, 5.0], [5.0, 4.5]] * 3, + ], + [ + [[-4.0, -100.0], [4.0, -20.0], [-1.0, -200.0]] * 3, + [[3.0, 2.0], [1.0, 5.0], [5.0, 4.5]] * 3, + ], + [ + [[1.0, -100.0], [1.0, -75.0], [1.0, -100.0]] * 3, + [[-4.0, -147.0], [4.0, -20.0], [-1.0, -200.0]] * 3, + ], + [ + [[30.0, -100.0], [1.0, -75.0], [1.0, -100.0]] * 3, + [[-4.0, -147.0], [-17.0, -20.0], [-1.0, -68.0]] * 3, + ], + ] + ) + print(arr3.shape) + + # make dataset from numpy data above and dims, assume the dim names are common and taken from left + # to right, i.e. either A in B and/or B in A without loss of generality. + ds_mock = xr.Dataset( + { + name_varA: xr.DataArray(arr1, dims=dimnames1), + name_varB: xr.DataArray(arr2, dims=dimnames2), + name_varC: xr.DataArray(arr3, dims=dimnames3), + } + ) + + return ds_mock + + +def run_example(ds_input, use_real=True, backend="zig", num_workers=1, num_chunks=1): + # TODO: use library directly under main guard + print("Example: python with zig.") + print("---") + print("NOTE: Optionally uses satellite data if appropriate nci group") + + # NOTE: scoped import so that context isn't leaked - being safe here though it is likely okay + # for this to be on the global scope or at the very least main guarded is sufficient. + from persistence import persistence_impl + + if use_real: + NotImplementedError("mechanism to run real satellite data not yet implemented") + else: + # --- + # some printing logic for display/debugging + print("using mock data... use_real=False") + print("\n--- mocking data ---") + for v, da in ds_input.data_vars.items(): + print("...") + for i, (n, s) in enumerate(zip(da.dims, da.shape)): + print(f"{v}:shape={n}={s}") + print("---") + # --- + + # --- + # TODO: + # There is a flaw here if time index is not always the first index, since there is no + # guarantee that the datasets share the array dimensions - this needs to be rectified. + # + # This can be done by requesting named index for time at the higher level api instead of the + # integer directly. This is only really necessary for datasets and is infact insufficient + # for numpy. + # + # We still need `idx_time_dim` for `numpy` support, so it'll have to be a mutually + # exclusive argument. + # + # For testing purposes, this is a lower priority since the user can always just stick to + # data arrays and computing each variable separately in a for loop wrapper with minimal loss + # to performance, since the variable count is not likely to be very large. + import time + + ts = time.time() + print(f"ts={ts}") + ds_output = persistence_impl.predict( + ds_input, + idx_time_dim=list(ds_input.dims).index("time"), + num_workers=num_workers, + # 20 chunks/2 workers => 10% of the data is loaded at any given time (assuming optimal chunking) + num_chunks=num_chunks, + method="median_of_three", + simple_impute=False, + backend_type=backend, + ) + + # --- + te = time.time() + print(f"te={te}") + print(f"total={te - ts}s") + print(f"size={ds_output.sizes}") + print("---") + print(ds_output) + return ds_output + + if __name__ == "__main__": - m =_median_of_three_zig(5,-2, 3) - print(m) + import multiprocessing + + # CAUTION: windows/mac - this may not work, use num_workers=1 instead + try: + multiprocessing.set_start_method("forkserver") + print("Start method set to 'forkserver'") + except RuntimeError as e: + print(f"Could not set start method: {e}") + + # For windows/mac you may need to change this to spawn but not guarenteed to work + # NOTE: the inner functions in the package already set the context, but its good to do + # regardless. + # run this in mainguard so it doesn't get regenerated. + ds_input = _mock_dataset() + ds_output1 = run_example( + ds_input, use_real=False, backend="zig", num_workers=1, num_chunks=5 + ) + ds_output2 = run_example( + ds_input, use_real=False, backend="numpy", num_workers=1, num_chunks=5 + ) + import numpy as np + + print(np.allclose(ds_output1.varA, ds_output2.varA)) + print(np.allclose(ds_output1.varB, ds_output2.varB)) + print(np.allclose(ds_output1.varC, ds_output2.varC)) + print(ds_output1.varC) + print(ds_output2.varC) diff --git a/packages/bundled_models/persistence/src/lib/zig/lib.zig b/packages/bundled_models/persistence/src/lib/zig/lib.zig index c584c43d..ad7ac38a 100644 --- a/packages/bundled_models/persistence/src/lib/zig/lib.zig +++ b/packages/bundled_models/persistence/src/lib/zig/lib.zig @@ -4,3 +4,23 @@ const median = @import("./median.zig"); export fn median_of_three(x1: f32, x2: f32, x3: f32) f32 { return median.medianofthree_scalar_nanfiltered(x1, x2, x3); } + +export fn median_of_three_nd( + idx_time: i32, + shape: [*]i32, + len_shape: i32, + arr_in: [*]f32, + len_in: i32, + arr_out: [*]f32, + len_out: i32, +) void { + median.medianofthree_split_nd( + idx_time, + shape, + len_shape, + arr_in, + len_in, + arr_out, + len_out, + ); +} diff --git a/packages/bundled_models/persistence/src/lib/zig/median.zig b/packages/bundled_models/persistence/src/lib/zig/median.zig index a8fbacfe..50dd8644 100644 --- a/packages/bundled_models/persistence/src/lib/zig/median.zig +++ b/packages/bundled_models/persistence/src/lib/zig/median.zig @@ -15,11 +15,12 @@ const nanf32 = std.math.nan(f32); // arr_out: pointer to n-dimensional pre-allocated output // len_out: length of output array // ---------------------------------------------------------------------------- -fn medianofthree_split_nd( +pub fn medianofthree_split_nd( idx_time: i32, shape: [*]i32, len_shape: i32, arr_in: [*]f32, + len_in: i32, arr_out: [*]f32, len_out: i32, ) void { @@ -42,6 +43,7 @@ fn medianofthree_split_nd( // safety std.debug.assert(@as(usize, @intCast(len_out)) == len_chunk * len_outer); + std.debug.assert(@as(usize, @intCast(len_in)) == shape[@as(usize, @intCast(idx_time))] * len_out); for (0..len_outer) |i| { // --- diff --git a/packages/bundled_models/persistence/src/persistence/include/.gitignore b/packages/bundled_models/persistence/src/persistence/include/.gitignore new file mode 100644 index 00000000..96c40cef --- /dev/null +++ b/packages/bundled_models/persistence/src/persistence/include/.gitignore @@ -0,0 +1,5 @@ +# these are autogenerated using cffi +*.a +*.so +*.c +*.o diff --git a/packages/bundled_models/persistence/src/persistence/include/_cffi.py b/packages/bundled_models/persistence/src/persistence/include/_cffi.py index 59c37e4b..73e3b42f 100644 --- a/packages/bundled_models/persistence/src/persistence/include/_cffi.py +++ b/packages/bundled_models/persistence/src/persistence/include/_cffi.py @@ -1,6 +1,7 @@ """ Compile cffi code and put them in the include directory """ + from cffi import FFI import sys import os @@ -8,10 +9,12 @@ _zig_c_declarations = """ float median_of_three(float, float, float); +void median_of_three_nd(int, int[], int, float[], int, float[], int); """ -_zig_c_libname="libpersistence_zig" +_zig_c_libname = "libpersistence_zig" _include_libdir = os.path.join(os.path.dirname(os.path.realpath(__file__)), "lib") + def compile_zig(): # cffi ffibuilder = FFI() @@ -24,7 +27,7 @@ def compile_zig(): _zig_c_declarations, libraries=["persistence_zig"], library_dirs=[_include_libdir], - extra_link_args=[f"-Wl,-rpath={_include_libdir}"] + extra_link_args=[f"-Wl,-rpath={_include_libdir}"], ) ffibuilder.compile(verbose=True) diff --git a/packages/bundled_models/persistence/src/persistence/interface/_backend.py b/packages/bundled_models/persistence/src/persistence/interface/_backend.py index 997c6149..a4377cc7 100644 --- a/packages/bundled_models/persistence/src/persistence/interface/_backend.py +++ b/packages/bundled_models/persistence/src/persistence/interface/_backend.py @@ -40,10 +40,10 @@ class PersistenceBackendType(StrEnum): """ C = "c" - C_ZIG = "zig" NUMBA = "numba" NUMPY = "numpy" RUST = "rust" + ZIG = "zig" UNKNOWN = auto() def check_support(self): @@ -59,6 +59,8 @@ def check_support(self): match self: case PersistenceBackendType.NUMPY: return + case PersistenceBackendType.ZIG: + return case _: raise NotImplementedError( f"PersistenceBackendType: {self} is not supported" diff --git a/packages/bundled_models/persistence/src/persistence/interface/_chunker.py b/packages/bundled_models/persistence/src/persistence/interface/_chunker.py index ad24f9b2..1319b707 100644 --- a/packages/bundled_models/persistence/src/persistence/interface/_chunker.py +++ b/packages/bundled_models/persistence/src/persistence/interface/_chunker.py @@ -1,4 +1,5 @@ from dataclasses import dataclass +import copy import math import numpy as np import xarray as xr @@ -57,6 +58,9 @@ class PersistenceDataChunk: # list containing slices of each dimension that make up the chunk slice_dims: list[slice] + # reduced dimensions expected from the output (reduced) + slice_dims_reduced: list[slice] + @dataclass class PersistenceChunker: @@ -393,6 +397,19 @@ def generate_chunks(self) -> Generator[PersistenceDataChunk]: This generator generally would be fed into a multiprocessing worker pool in conjunction with a method to process each chunk. """ + # chunksize = 1, early return + if ( + self.chunk_info.num_chunks == 1 + or self.chunk_info.size_chunk >= self.da.size + ): + # select everything for both input and result + slice_dims = [slice(None)] * len(self.da.shape) + slice_dims_reduced = slice_dims + yield PersistenceDataChunk( + self.da, self.metadata, slice_dims, slice_dims_reduced + ) + return + # TODO: add a fast return for the special case when time is the only dimension. shape_notime = list(self.da.shape) shape_notime[self.metadata.idx_time_dim] = 1 @@ -404,8 +421,14 @@ def generate_chunks(self) -> Generator[PersistenceDataChunk]: arr_chunk = self.da.isel(dict_slice_dims) # pass chunk to caller + slice_dims = list(dict_slice_dims.values()) + slice_dims_reduced = copy.deepcopy(slice_dims) + slice_dims_reduced[self.metadata.idx_time_dim] = slice(None, None, None) yield PersistenceDataChunk( - arr_chunk, self.metadata, list(dict_slice_dims.values()) + arr_chunk, + self.metadata, + slice_dims, + slice_dims_reduced, ) # increment index and break if overflow is detected. diff --git a/packages/bundled_models/persistence/src/persistence/interface/_compute.py b/packages/bundled_models/persistence/src/persistence/interface/_compute.py index af1e191b..518ebcba 100644 --- a/packages/bundled_models/persistence/src/persistence/interface/_compute.py +++ b/packages/bundled_models/persistence/src/persistence/interface/_compute.py @@ -7,12 +7,13 @@ from typing import Union, Generator from collections import namedtuple +import warnings import numpy as np import xarray as xr from persistence.interface.types import PetDataArrayLike from persistence.methods._impute import SimpleImpute -from persistence.methods._median import _median_of_three_numpy +from persistence.methods._median import _median_of_three_numpy, _median_of_three_zig from persistence.interface._metadata import PersistenceMetadata from persistence.interface._method import PersistenceMethod from persistence.interface._chunker import ( @@ -134,14 +135,32 @@ class PersistenceComputePool: @staticmethod def _job_wrapper(chunk: PersistenceDataChunk) -> ChunkResult: """ - This wrapper needs to be static, as we may not want the state info of - this class to propagate. + This wrapper needs to be static, as we may not want the state info of this class to + propagate. + + NOTE: multiprocessing is actually quite heavy it requires: + 1. passing the heavy pointer to the input chunk + 2. passing the entire data via shared memory back to the main thread. + + FUTUREWORK: + A lighter weight way of doing this is to write to disk directly: + - This needs to happen anyway and workers can write independently of one another. + - The joining process is not strictly required, because... + - The purpose of persistence models is to compare arrays at particular time instances. + - The arrays being stored in separate chunked files, does not go against the above + requirement, especially with an efficient loader. Meaning joins can be avoided. """ - return ChunkResult( - array=PersistenceCompute(chunk.arr_chunk, chunk.metadata).compute(), - slice_dims=chunk.slice_dims, + # force load with arr_chunk.values + arr_persist = chunk.arr_chunk.values + + # using reduced slice dims here since its the result + result = ChunkResult( + array=PersistenceCompute(arr_persist, chunk.metadata).compute(), + slice_dims=chunk.slice_dims_reduced, ) + return result + def map_and_join_chunks(self) -> xr.DataArray: """ 1. Send chunks to workers @@ -173,7 +192,19 @@ def map_and_join_chunks(self) -> xr.DataArray: ] arr_res = np.empty(shape_res) - if self.metadata.num_workers <= 1: + # workers must be less than or equal to chunks and must not oversubscribe to cpu + num_workers = min(self.metadata.num_workers, self.chunk_info.num_chunks) + num_workers = min(num_workers, multiprocessing.cpu_count()) + if num_workers != self.metadata.num_workers: + warnings.warn( + UserWarning( + f"Changed requested workers to: num_workers={num_workers}, " + "either insufficient threads on this machine OR num_chunks is too small." + "This is done to prevent oversubscription of CPU." + ) + ) + + if num_workers <= 1: # loop through instead for chunk in iter(self.chunk_generator): res_chunk = PersistenceComputePool._job_wrapper(chunk) @@ -182,7 +213,7 @@ def map_and_join_chunks(self) -> xr.DataArray: # dispatch chunks to workers # TODO: forkserver does/may not work with windows/mac, unless main-guarded with concurrent.futures.ProcessPoolExecutor( - self.metadata.num_workers, + num_workers, mp_context=multiprocessing.get_context("forkserver"), ) as pp_exec: results = pp_exec.map( @@ -202,16 +233,30 @@ class PersistenceCompute: arr: PetDataArrayLike metadata: PersistenceMetadata + def _raise_unimplemented_method(self): + """ + Specific error: method has not been implemented for a specific backend + """ + raise NotImplementedError( + f"PersistenceCompute: compute method {self.metadata.method} not implemented (backend={self.metadata.backend})" + ) + + def _raise_unimplemented_backend(self): + """ + Generic error: method has not been implemented for any backend + """ + raise NotImplementedError( + f"PersistenceCompute: backend type {self.metadata.backend} not implemented" + ) + def _method_impl(self, arr: np.ndarray) -> np.ndarray: match self.metadata.backend: case PersistenceBackendType.NUMPY: return self._method_impl_numpy(arr) - case PersistenceBackendType.NUMBA: - return self._method_impl_numba(arr) - case PersistenceBackendType.RUST: - return self._method_impl_rust(arr) + case PersistenceBackendType.ZIG: + return self._method_impl_zig(arr) case _: - raise NotImplementedError("PersistenceCompute: Unknown backend") + self._raise_unimplemented_backend() def _method_impl_numpy(self, arr: np.ndarray) -> np.ndarray: match self.metadata.method: @@ -220,15 +265,16 @@ def _method_impl_numpy(self, arr: np.ndarray) -> np.ndarray: case PersistenceMethod.MOST_RECENT: raise NotImplementedError("TODO") case _: - raise NotImplementedError( - f"PersistenceCompute: compute method {self.method} has not been implemented" - ) + self._raise_unimplemented_method() - def _method_impl_numba(self, arr: np.ndarray) -> np.ndarray: - raise NotImplementedError("numba backend is not supported") - - def _method_impl_rust(self, arr: np.ndarray) -> np.ndarray: - raise NotImplementedError("rust backend is not supported") + def _method_impl_zig(self, arr: np.ndarray) -> np.ndarray: + match self.metadata.method: + case PersistenceMethod.MEDIAN_OF_THREE: + return _median_of_three_zig(arr, self.metadata.idx_time_dim) + case PersistenceMethod.MOST_RECENT: + raise NotImplementedError("TODO") + case _: + self._raise_unimplemented_method() def _slice_time(self, arr: np.ndarray) -> np.ndarray: """ diff --git a/packages/bundled_models/persistence/src/persistence/interface/types.py b/packages/bundled_models/persistence/src/persistence/interface/types.py index b0947556..8e674538 100644 --- a/packages/bundled_models/persistence/src/persistence/interface/types.py +++ b/packages/bundled_models/persistence/src/persistence/interface/types.py @@ -24,10 +24,12 @@ class PetInputDataType(StrEnum): class PetDataset: + _dummyvarname = "dummy_varname" + def __init__( self, arraylike: PetDataArrayLike, - dummy_varname="_dummyvarname", # used for xarray dataarrays and numpy arrays + dummy_varname="dummy_varname", # used for xarray dataarrays and numpy arrays dimnames: list[str] = None, # used only for numpy arrays ): """ @@ -37,6 +39,7 @@ def __init__( `dimnames` is only relevant for numpy - and only if using name-based indexing for retrieving e.g. time dimension """ + self._dummyvarname = dummy_varname self.raw_type = PetInputDataType.UNKNOWN self.ds = self.from_arrlike(arraylike, dummy_varname, dimnames) self.return_raw_result = True diff --git a/packages/bundled_models/persistence/src/persistence/methods/_median.py b/packages/bundled_models/persistence/src/persistence/methods/_median.py index 5b1b5699..3c79ec83 100644 --- a/packages/bundled_models/persistence/src/persistence/methods/_median.py +++ b/packages/bundled_models/persistence/src/persistence/methods/_median.py @@ -1,14 +1,60 @@ +from dataclasses import dataclass +import copy import numpy as np import warnings import persistence.include._persistence_zig from persistence.include._persistence_zig import ffi, lib + +@dataclass(frozen=True) +class _MedianCommon: + """ + This is a private namespace containing utility functions + """ + + def check_shape(arr_shape, idx_time): + arr_shape = list(arr_shape) + if arr_shape[idx_time] != 3: + raise ValueError( + "_median_of_three_numpy: the time dimension MUST only have 3 entries" + ) + + def get_output_shape(arr_shape, idx_time): + arr_shape = list(arr_shape) + arr_shape_out = copy.deepcopy(arr_shape) + arr_shape_out[idx_time] = 1 + return arr_shape_out + + def check_and_convert_contiguousf32(arr: np.ndarray) -> np.ndarray: + if not arr.flags["C_CONTIGUOUS"]: + warnings.warn( + UserWarning( + "_median_of_three_zig: input numpy array is not C contiguous! " + "Make sure to load the array using C contiguous settings. Focing to contiguous array." + ) + ) + return np.ascontiguousarray(arr, dtype=np.float32, order="C") + return arr.astype(np.float32) + + def _median_of_three_numpy(arr: np.ndarray, idx_time: int) -> np.ndarray: """ Computes median of three along the time index, preserves `nan`. IF a particular coordinate is all `nan` along the time dimension, THEN the output is `nan` for that entry. + + Uses numpy backend + + Returns the median of three applied along time dimension. + + IMPORTANT: + - time dimension cardinality must equal 3 + + Raises: + ValueError: if time dimension does not have 3 entries """ + _MedianCommon.check_shape(arr.shape, idx_time) + shape_out = _MedianCommon.get_output_shape(arr.shape, idx_time) # NOTE: # - ignore numpy warnings as allowing all `nan` is intentional # - `keepdims=True` because we want to keep the dimensional structure of the variable being @@ -20,11 +66,70 @@ def _median_of_three_numpy(arr: np.ndarray, idx_time: int) -> np.ndarray: with warnings.catch_warnings(): warnings.simplefilter("ignore") arr_median = np.nanmedian(arr, axis=idx_time, keepdims=True) + assert list(shape_out) == list(arr_median.shape) return arr_median -# --- TODO: for testing only -def _median_of_three_zig(x1: int, x2: int, x3: int) -> int: - y = lib.median_of_three(x1, x2, x3) - return y -# --- +def _median_of_three_zig(arr: np.ndarray, idx_time: int) -> np.ndarray: + """ + Computes median of three along the time index, preserves `nan`. IF a particular coordinate is all + `nan` along the time dimension, THEN the output is `nan` for that entry. + + Uses zig backend + + Returns the median of three applied time dimension. + + IMPORTANT: + - input array (assumed to be a chunk) should be reasonably sized so that it doesn't need to + call the FFI multiple times per chunk. + - the input array must be C-contiguous, otherwise it'll be forced to be C-contiguous, + requiring an extra copy operation. + - time dimension cardinality must equal 3 + + PERFORMANCE: + This function performs best if: + - the input array/chunk it deals with is large + - the array is already in float32, most of the time xarray presents in float64 + (aside: float32 is more than enough for median calculations given that it's mainly a + sorting algorithm, and doesn't introduce additional error.) + - the array is already C-contiguous + - the above would mean that most of the work is done in zig, and not in converting the array + to conform. + + Raises: + ValueError: if time dimension has more than 3 entries + UserWarning: if array is not C contiguous + """ + # check/transform input to conform to c-types + _MedianCommon.check_shape(arr.shape, idx_time) + arr32_in = _MedianCommon.check_and_convert_contiguousf32(arr) + shape_out = _MedianCommon.get_output_shape(arr.shape, idx_time) + shape_in = np.array(arr.shape, dtype=np.int32, order="C") + arr32_out = np.empty(shape_out, dtype=np.float32, order="C") + + # gather inputs to pass to cffi + # --- not sure if this is optimal --- + ptr_arr32_in = ffi.from_buffer("float[]", arr32_in) + ptr_arr32_out = ffi.from_buffer("float[]", arr32_out) + ptr_shape_in = ffi.from_buffer("int[]", shape_in) + # --- + len_shape_in = len(shape_in) + len_in = arr32_in.size + len_out = arr32_out.size + + # safety + assert isinstance(len_in, int) + assert isinstance(len_out, int) + + lib.median_of_three_nd( + int(idx_time), + ptr_shape_in, + len_shape_in, + ptr_arr32_in, + int(len_in), + ptr_arr32_out, + int(len_out), + ) + + # revert to original array type + return arr32_out.astype(arr.dtype) From 5f0dede63cc92a75db5b3adaaef1228f4e3511af Mon Sep 17 00:00:00 2001 From: Nikeeth Ramanathan Date: Mon, 16 Mar 2026 14:03:59 +1100 Subject: [PATCH 12/16] [skip ci] additional comments for zigc example --- .../persistence/examples/zigc.py | 48 ++++++++++++++++--- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/packages/bundled_models/persistence/examples/zigc.py b/packages/bundled_models/persistence/examples/zigc.py index b0a37308..7c1038d8 100644 --- a/packages/bundled_models/persistence/examples/zigc.py +++ b/packages/bundled_models/persistence/examples/zigc.py @@ -190,28 +190,62 @@ def run_example(ds_input, use_real=True, backend="zig", num_workers=1, num_chunk if __name__ == "__main__": import multiprocessing - # CAUTION: windows/mac - this may not work, use num_workers=1 instead + # --- + # CAUTION: windows/mac - see WHEN IN DOUBT below, except it applies ALMOST ALWAYS. + # --- + # Notes: + # - WHEN IN DOUBT: set num_chunks = 1 and num_workers = 1 + # + # - IF USING DATASETS: chunk strategy is the SAME between variables. This could be very slow for + # certain variables that are very small in data size. + # + # - Support for datasets is for convenience only NOT SPEED. Supported settings != optimal settings + # + # - FASTER: use dataarrays or numpy arrays as inputs and combine later. This also allows the + # user to invoke embarassing parallelism at a higher level, and also choose different + # backend/computations for different variables. + # + # - (Not yet implemented) EVEN FASTER: data loading is also externally performed (FUTUREWORK). + # This allows for chunks to be stored on disk and retrieved by any compute engine, either + # using the same backend, a different backend, or using PET's existing computational stack + # (xarray + dask + numpy). The important take-away here is the separation of concern allows + # for flexiblity and portability. + # --- + NUM_WORKERS = 1 + NUM_CHUNKS = 5 + try: multiprocessing.set_start_method("forkserver") print("Start method set to 'forkserver'") except RuntimeError as e: print(f"Could not set start method: {e}") - # For windows/mac you may need to change this to spawn but not guarenteed to work - # NOTE: the inner functions in the package already set the context, but its good to do - # regardless. - # run this in mainguard so it doesn't get regenerated. ds_input = _mock_dataset() ds_output1 = run_example( - ds_input, use_real=False, backend="zig", num_workers=1, num_chunks=5 + ds_input, + use_real=False, + backend="zig", + num_workers=NUM_CHUNKS, + num_chunks=NUM_CHUNKS, ) + # NOTE: second run can be a bit faster as it likely does some caching, so actual times (not + # shown) can be much slower (depends). This part isn't for speed/memory benchmarking reasons + # rather for comparing outputs are equal. ds_output2 = run_example( - ds_input, use_real=False, backend="numpy", num_workers=1, num_chunks=5 + ds_input, + use_real=False, + backend="numpy", + num_workers=NUM_WORKERS, + num_chunks=NUM_CHUNKS, ) + import numpy as np + # to check equivilence mostly for random print(np.allclose(ds_output1.varA, ds_output2.varA)) print(np.allclose(ds_output1.varB, ds_output2.varB)) print(np.allclose(ds_output1.varC, ds_output2.varC)) + + # for manual inspection print(ds_output1.varC) print(ds_output2.varC) From 8640951e8e7abca4d5ccfc8765d33f7b75175c4a Mon Sep 17 00:00:00 2001 From: Nikeeth Ramanathan Date: Mon, 16 Mar 2026 14:04:46 +1100 Subject: [PATCH 13/16] [skip ci] additional comments for zigc example --- packages/bundled_models/persistence/examples/zigc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/bundled_models/persistence/examples/zigc.py b/packages/bundled_models/persistence/examples/zigc.py index 7c1038d8..31ff8401 100644 --- a/packages/bundled_models/persistence/examples/zigc.py +++ b/packages/bundled_models/persistence/examples/zigc.py @@ -190,8 +190,6 @@ def run_example(ds_input, use_real=True, backend="zig", num_workers=1, num_chunk if __name__ == "__main__": import multiprocessing - # --- - # CAUTION: windows/mac - see WHEN IN DOUBT below, except it applies ALMOST ALWAYS. # --- # Notes: # - WHEN IN DOUBT: set num_chunks = 1 and num_workers = 1 @@ -210,6 +208,8 @@ def run_example(ds_input, use_real=True, backend="zig", num_workers=1, num_chunk # using the same backend, a different backend, or using PET's existing computational stack # (xarray + dask + numpy). The important take-away here is the separation of concern allows # for flexiblity and portability. + # + # CAUTION: windows/mac - see WHEN IN DOUBT above, except it applies ALMOST ALWAYS. # --- NUM_WORKERS = 1 NUM_CHUNKS = 5 From 9f948c37c41518115da0d701fe52f7bd037389e0 Mon Sep 17 00:00:00 2001 From: Nikeeth Ramanathan Date: Tue, 17 Mar 2026 10:44:29 +1100 Subject: [PATCH 14/16] [skip ci][wip] notebook demo - with accessor bug? --- .../notebooks/FourCastMini_Demo.ipynb | 552 ++++++++++++++++++ .../notebooks/LUCIE-Inference.ipynb | 165 ++++++ .../persistence/notebooks/README.md | 3 + .../notebooks/pipeline_example.ipynb | 282 +++++++++ 4 files changed, 1002 insertions(+) create mode 100644 packages/bundled_models/persistence/notebooks/FourCastMini_Demo.ipynb create mode 100644 packages/bundled_models/persistence/notebooks/LUCIE-Inference.ipynb create mode 100644 packages/bundled_models/persistence/notebooks/README.md create mode 100644 packages/bundled_models/persistence/notebooks/pipeline_example.ipynb diff --git a/packages/bundled_models/persistence/notebooks/FourCastMini_Demo.ipynb b/packages/bundled_models/persistence/notebooks/FourCastMini_Demo.ipynb new file mode 100644 index 00000000..294eda71 --- /dev/null +++ b/packages/bundled_models/persistence/notebooks/FourCastMini_Demo.ipynb @@ -0,0 +1,552 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "7d499237-9410-4f01-b517-93100f47a0bf", + "metadata": {}, + "source": [ + "# Train and run a simplified global weather model (low hardware and data requirements)\n", + "\n", + "This notebook has been tested on a 4GB GPU in a Linux environment and uses less than 3GB of training data. This notebook has also been tested in an HPC facility. There is currently a known intermittent issue on Mac hardware.\n", + "\n", + "Overview:\n", + "- Downloading training data (takes a few minutes)\n", + "- Training a neural network to predict global weather conditions (takes around 30-60 minutes per epoch)\n", + "- Inferencing the network on unseen data (takes only a moment)\n", + "- This tutorial uses a simplied model to allow users to explore how PyEarthTools works, with comparatively low data and hardware requirements.\n", + "\n", + "## Summary\n", + "\n", + "### Choice of Data\n", + "This tutorial allows the user to download a 2.8GB file (or 6.4GB if you choose to use additional variables). The data contains around 60 years of global Earth system analysis data. The term \"analysis\" means the science community's best estimate of historical weather conditions based on available observations. The analysis data set used here was originally produced by the [European Centre for Medium Range Weather Forecasting (ECMWF)](https://www.ecmwf.int/en/forecasts/dataset/ecmwf-reanalysis-v5). This is a standard data set used in the field, however most research is done on a higher-resolution version of the data. That said, valuable research is also done using the lower resolution data. The spatial (latitude and longitude) resolution of this data is 64 pixels by 32 pixels, but the time series is very long. Only a few of the most interesting variables are downloaded in this notebook, to reduce how much data must be downloaded and stored.\n", + "\n", + "The data is made available by the ECMWF under license, and the conditions are described here: https://www.ecmwf.int/en/forecasts/accessing-forecasts/licences-available . Please review this before making use of the data for anything. In this tutorial, the data is downloaded using instructions from the WeatherBench 2 data guide. Please see https://weatherbench2.readthedocs.io/en/latest/data-guide.html for more information on the data, open access, and accessing other resolutions of the data.\n", + "\n", + "### Choice of Model (and Caveats)\n", + "\n", + "This tutorial uses a simplified version of FourCastNeXt. FourCastNeXt ([Guo et al. 2024](https://doi.org/10.48550/arXiv.2401.05584)) is a high-resolution global weather model (https://doi.org/10.48550/arXiv.2401.05584). It was originally trained using data with a spatial resolution of 1440x720. It was trained using four NVidia V100 GPUs (40GB cards) for 35 hours. \n", + "\n", + "This tutorial adapts and simplifies the original FourCastNeXt architecture. The model has been simplified so that this tutorial can be run with much lower requirements for hardware, data volumes and time. As the model has been simplied, its outputs are not as accurate as the original model. However, the purpose of this tutorial is to allow users to explore how PyEarthTools works, with comparatively low data and hardware requirements. \n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "c7e44d63-956f-41fd-95a6-6441ded61a3e", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "\n", + "# IMPORTANT! Set this to where you want to store your copy of the data!\n", + "# os.environ['ERA5LOWRESDEMO'] = os.path.expanduser(\"~\") # to use your home directory\n", + "# os.environ['ERA5LOWRESDEMO'] = os.path.abspath('./') # to use the current working directory\n", + "# os.environ['ERA5LOWRESDEMO'] = os.environ['PBS_JOBFS'] # to use a job-specific temporary directory\n", + "# os.environ['ERA5LOWRESDEMO'] = os.path.abspath('/tmp') # to use the /tmp directory\n", + "\n", + "# Most users should change this to the current directory.\n", + "os.environ['ERA5LOWRESDEMO'] = os.path.abspath('/tmp/') \n", + "EXPERIMENT_VERSION='v1'\n", + "\n", + "import hydra\n", + "import pathlib\n", + "import xarray as xr\n", + "from pathlib import Path\n", + "\n", + "from omegaconf import OmegaConf\n", + "\n", + "import pyearthtools.data.archive\n", + "import pyearthtools.tutorial\n", + "import pyearthtools.training\n", + "import pyearthtools.pipeline\n", + "\n", + "import fourcastnext\n", + "\n", + "# import torch; torch.set_default_device() # Uncomment and set this if you need to configure a non-default device." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "37828ffb-c6c7-45fb-8315-16e2d9e57466", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "This tutorial will download a copy of the input data to /tmp. It will also create model checkpoint files and other data here.\n" + ] + } + ], + "source": [ + "workdir = os.environ['ERA5LOWRESDEMO']\n", + "print(f'This tutorial will download a copy of the input data to {workdir}. It will also create model checkpoint files and other data here.')\n", + "\n", + "if pyearthtools.data.archive.ROOT_DIRECTORIES['era5lowresdemo'] != workdir:\n", + " print(\"There is some misconfiguration of your working directory, please review the commented out cells at the start of the notebook\")\n", + "\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "1719542b-bfba-4e6a-8426-5f22edc2f11e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "File already downloaded, skipping ...\n" + ] + } + ], + "source": [ + "file_location = workdir + '/mini.nc'\n", + "\n", + "if not os.path.exists(file_location):\n", + " print(\"Training data not found, downloading around 2.8GB of data\")\n", + " era5_lowres = xr.open_zarr('gs://weatherbench2/datasets/era5/1959-2022-6h-64x32_equiangular_conservative.zarr')\n", + " subset = era5_lowres[['10m_u_component_of_wind', \n", + " '10m_v_component_of_wind', \n", + " '2m_temperature', \n", + " 'mean_sea_level_pressure',\n", + " #'geopotential', # Uncomment this to fetch additional data\n", + " #'toa_incident_solar_radiation_6hr', # Uncomment this to fetch additional data\n", + " #'temperature' # Uncomment this to fetch additional data\n", + " ]]\n", + "\n", + " # bilevel = subset.sel({'level': [50, 500]}) Uncomment if fetching addtional data \n", + " # bilevel.to_netcdf(file_location)\n", + "\n", + " subset.to_netcdf(file_location) # Comment this out if using the bilevel data instead\n", + " print(\"Wrote file to {file_location}\")\n", + " assert os.path.exists(file_location)\n", + "else:\n", + " print(\"File already downloaded, skipping ...\")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "217eb07c-e789-4193-b78e-e9699f45da3c", + "metadata": {}, + "outputs": [], + "source": [ + "accessor = pyearthtools.tutorial.ERA5DataClass.ERA5LowResDemoIndex([\n", + " '10m_u_component_of_wind', \n", + " '10m_v_component_of_wind', \n", + " 'mean_sea_level_pressure',\n", + " '2m_temperature' \n", + "],\n", + "filename_override=file_location)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "6866d2b3-748c-4cbd-9279-4786d5ae9e31", + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAj4AAAHFCAYAAADyj/PrAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQAAd/RJREFUeJzt3Xl4FEXeB/Dv3LkHkpBLQhIPEAyiAitBlkMggCKXK7ruKwTRXZckcnrgxeFCEBVQFNAVA3jhqiB4cS0BzCIKERbwQFSOgAnhyp3M2e8fbEaGVA2ZyZDMMN/P8/QDqe6ururumVSqq+unUhRFAREREVEAUDd3AYiIiIiaChs+REREFDDY8CEiIqKAwYYPERERBQw2fIiIiChgsOFDREREAYMNHyIiIgoYbPgQERFRwGDDh4iIiAIGGz4UULZv347p06ejtLS03rrevXujd+/eTV6mpvDTTz9hypQp6Ny5M1q0aIHIyEjccsst+PDDD4Xbl5SUICMjA9HR0QgJCUFaWhr+/e9/19vu008/xahRo9CxY0fodDqoVCppGSwWC2bMmIHk5GQYDAZce+21WLhwoVv1uBTlcmXhwoW49tprYTAYkJKSghkzZsBisThtc+zYMUyYMAG9evVCixYtoFKpsGzZMo+OR0SXHhs+FFC2b9+OGTNmCBs+ixYtwqJFi5q+UE1gw4YN+Oyzz3DnnXfigw8+wDvvvINrrrkGd911F2bOnOm0rclkQt++ffHvf/8bL730EtasWYPY2FgMHDgQW7duddp29erV2LFjBzp06IBOnTq5LMO4ceOQk5ODzMxMrF+/HsOHD8f48eMxe/bsBtXhUpVLZtasWRg/fjxGjBiB9evXY9y4cZg9ezYyMzOdtvv555/xzjvvQK/X47bbbvPoWETUhBSiAPL8888rAJRDhw41d1Ga1MmTJxW73V4v/fbbb1dCQkKU2tpaR9qrr76qAFC2b9/uSLNYLEqHDh2UP/zhD07722w2x/8zMzMV2VfK/v37FZVKpcyePdsp/cEHH1SCg4OV06dPX7QOl6JcMqdOnVKCgoKUv/71r07ps2bNUlQqlfLdd98Jj7Vz504FgJKbm+vW8Yio6bDHhwLG9OnT8cgjjwAAUlJSoFKpoFKpsGXLFgD1H3UdPnwYKpUKzz//PJ577jkkJycjODgYvXv3xk8//QSLxYLHH38cCQkJMBqNGD58OEpKSuod9/3330daWhpCQ0MRFhaGAQMGYPfu3U1RZYfo6Gjh454//OEPqK6uxpkzZxxpq1evRrt27ZCWluZI02q1+L//+z988803OH78uCNdrW7YV8jHH38MRVEwZswYp/QxY8agpqYG69atu2gel6JcMuvWrUNtba2wvIqi4OOPP/basYioafETSwHjgQceQHZ2NgBg1apV+Oqrr/DVV1/hpptucrnfq6++iv/85z949dVX8cYbb+DHH3/EHXfcgbFjx+LkyZN48803MXfuXGzatAkPPPCA076zZ8/Gn//8Z3To0AH/+te/8NZbb6GiogJ//OMf8f3331+0zFartUGLoigenZO8vDy0atUKMTExjrT9+/fj+uuvr7dtXdp3333n9nH279+PVq1aIS4uTpjn/v37G5SHt8vl6lgA0LFjR6f0+Ph4REdHN6i8ROSbtM1dAKKm0rp1a7Rp0wYAcOONNyI5OblB+7Vo0QIff/yx4y/7U6dOYcKECbj22muxZs0ax3Y//vgjFixYgPLyckRERKCwsBDTpk1DVlYWXn75Zcd2/fv3xzXXXIMZM2bg/ffflx738OHDSElJaVAZ8/Ly3B6Y/cYbb2DLli146aWXoNFoHOmnT59GZGRkve3r0k6fPu3WcVzlGRoaCr1e36A8L0W5XB3LYDAgNDRUeDxvHouImhYbPkQXcdtttzk9zmjfvj0A4Pbbb3fari796NGjSE1Nxfr162G1WjFq1ChYrVbHdkFBQejVqxfy8vJcHjchIQE7d+5sUBnbtWvXoO3qfPHFF8jMzMSf/vQnRy/Y+Vy9BeXpG1INyVNRFNhsNqd1Wq223nbeKtf51wUANBqNI59LcQ6IqPmx4UN0ERf2Muj1epfptbW1AIATJ04AALp27SrM92JjQ/R6PW644YYGlfH8HpuLWb9+PUaMGIH+/fvjnXfeqfdLPCoqStijUTcOSNTrcjFRUVHYs2dPvfSqqiqYzWZHnsuXL683rqbuMd6lKJdOp3P6OTc3FxkZGYiKikJtbS2qq6sREhJS73idO3d2+1hE5BvY8CG6RKKjowEAH374IZKSktze/1I86lq/fj2GDRuGXr164aOPPnI01s7XsWNH7Nu3r156XVpqamqDynRhnitXrkRxcbHTOJ8L87zjjjukvVyXolwXHqvufNeN7dm3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5I852WfPIvn6lIWXAVyExZB8LqVhFTyo90lJOJwoTYV0H5EKRRzU15N6y8J7yD6v4ZKgpq7IwmXIzrlNcr1F92C13c3vrcYI0DE+33zzDTZv3oxPP/0U1113Xb3BzatWrXI7T49ejL///vtRUVH/w1JVVYX777/fkyyJiIiInLRo0QIjRoxAr169EB0dDaPR6LR4wqPBzcuXL8ecOXMQHu78F0RNTQ1WrFiBN99806PCEBERUX2BOsYnNzfX63m61fApLy+HoihQFAUVFRUICvo9sq7NZsPnn3+OmJgYrxeSiIgooAXooy4AsFqt2LJlC3755Rfce++9CA8Px2+//YaIiAiniZQbyq2GT4sWLaBSqaBSqdC2bdt661UqFWbMmOF2IYiIiIgudOTIEQwcOBBHjx6FyWRC//79ER4ejrlz56K2ttajSZPdavjk5eVBURTceuut+OijjxAZGelYp9frkZSUhISEBLcLQURERC544VGXP/b4jB8/Hl26dMF///tfREVFOdKHDx+OBx54wKM83Wr49OrVC8C5GZwTExO9GjSMiIiIJAL0UVd+fj7+85//1AuAnpSUhOPHj3uUp0eDm+sCflZXV+Po0aMwm81O66+//nqPCkNERERUx263w2arP23AsWPH6r1g1VAeNXxOnjyJMWPG4IsvvhCuFxWSiIiIPBSgPT79+/fHggUL8PrrrwM4N5a4srIS06ZNw2233eZRnh49q5owYQLOnj2LHTt2IDg4GOvWrcPy5ctxzTXXYO3atR4VhIiIiMTqXmdv7OJv5s+fj61bt6JDhw6ora3Fvffei+TkZBw/fhzPPfecR3l61OOzefNmrFmzBl27doVarUZSUhL69++PiIgI5OTk4Pbbb/eoMERERER1EhISsGfPHqxcuRIFBQWw2+0YO3Ys/vKXvyA4WDyL+MV41PCpqqpyzNcTGRmJkydPom3btujYsSO+/fZbjwpCREREdL5t27ahe/fuGDNmDMaMGeNIt1qt2LZtG3r27Ol2nh41fNq1a4cDBw4gOTkZN9xwA1577TUkJydjyZIliI/3Xnyq5nTWHopaW8NOj1kS86XELB541VInjicFAGrJQ9hoXaUwXRbTplYRx82pshukxw5SWaTrRGRllZPHEZLGJJI8jZXFT5KRxeMC5HGjZDG5QiXnSSPpRy5X9MJ0QF4PT2ISibiKzSYji7Fll8SHkubjYntpfC83Y1DJtpfFgALk9ZPdB+59KlyTXQ/Z57XaLr53TC7iYsnIPgPLTvQQpveP/F6YHqUVfxd5QhbfK05bKkzXS+pQ6+I+l55zybHdiadWYxfHlrskAnSMT58+fVBUVFRvcuSysjL06dPHozHFHjV8JkyYgKKiIgDAtGnTMGDAALz99tvQ6/VYvny5J1kSERGRRKCGrFAUBSpBAOHTp08jNDTUozw9avj85S9/cfz/xhtvxOHDh/Hjjz+iTZs2iI6O9qggRERERAAwYsQIAOfe4srIyIDB8PvTCpvNhr1796J79+4e5d3ghs+kSZManOm8efM8KgwRERFJ+GGPjafqIq8rioLw8HCngcx6vR7dunXDgw8+6FHeDW747N69u0HbibqkiIiIqBECbIxPXVT25ORkTJkyxePHWiINbvjk5eV57aBEREREFzNt2jSv5+nRGB8iIiJqOoE6uPlSYMOHiIjI1wXYo65LieHViYiIKGCwx4eIiMjH8VEXUFtbi6CgoEbnwx4fIiIiX6d4afEzdrsdzz77LK644gqEhYXh119/BQA8/fTTWLp0qUd5suFDREREPukf//gHli1bhrlz50Kv/z2ES8eOHfHGG294lCcfdUmYFS00F8RmkcXFKrOGCNOjdFXC9EgXsW5kcWVk+1whiWlTbDMK09Uu4mVZIK6fjCw+jizWjSuyOFCyWF2yayHjKlZXC434OrXTiM95tEZ8vdWSsh61VkiPXWgVz01RahcfI1RtEqbLzofs/AGuY2mJuIp/JeLqXpOR1U8jyUsWw8vduGKA/L6VxeKT1U8tKRMA6FTi2E6y6ye7b+0qcf1k8adcHeP6iGPC9E9OdhKmP5QgmdrE1SmXnJIzkhhlMrJz7urzbYMknpasvJKyiu5/2f13SQTo4OYVK1bg9ddfR9++ffHQQw850q+//nr8+OOPHuXJHh8iIiIfVzfGp7FLQ+Xk5KBr164IDw9HTEwMhg0bhgMHDtTb7ocffsCQIUNgNBoRHh6Obt264ejRo471JpMJ2dnZiI6ORmhoKIYMGYJjx8SNbZHjx4/j6quvrpdut9thsXgWQpgNHyIiIl/XxGN8tm7diszMTOzYsQMbN26E1WpFeno6qqp+7yH/5Zdf0KNHD1x77bXYsmUL/vvf/+Lpp592GoA8YcIErF69GitXrkR+fj4qKysxePDgBkdVv+666/Dll1/WS//ggw9w4403NrxC5+GjLiIiInKybt06p59zc3MRExODgoIC9OzZEwDw5JNP4rbbbsPcuXMd21155ZWO/5eVlWHp0qV466230K9fPwDA22+/jcTERGzatAkDBgy4aDmmTZuG++67D8ePH4fdbseqVatw4MABrFixAp9++qlHdWOPDxERka/zYo9PeXm502IyicfWna+srAwAEBkZCeDco6bPPvsMbdu2xYABAxATE4Obb74ZH3/8sWOfgoICWCwWpKenO9ISEhKQmpqK7du3N6jad9xxB95//318/vnnUKlUeOaZZ/DDDz/gk08+Qf/+/RuUx4XY8CEiIvJx3hzjk5iYCKPR6FhycnJcHltRFEyaNAk9evRAamoqAKCkpASVlZWYM2cOBg4ciA0bNmD48OEYMWIEtm7dCgAoLi6GXq9Hy5YtnfKLjY1FcXHxRetstVoxY8YMdOjQAVu3bkVlZSWqq6uRn5/v1JhyFx91ERERBZDCwkJEREQ4fjYYDC63z8rKwt69e5Gfn+9Is9vPvdE2dOhQTJw4EQBwww03YPv27ViyZAl69eolzU9RFKhUqouWU6vV4vnnn8fo0aMvuq072ONDRETk67z4qCsiIsJpcdXwyc7Oxtq1a5GXl4fWrVs70qOjo6HVatGhQwen7du3b+94qysuLg5msxlnz5512qakpASxsbENqna/fv2wZcuWBm3bUOzxISIi8nFNHbJCURRkZ2dj9erV2LJlC1JSUpzW6/V6dO3atd4r7j/99BOSkpIAAJ07d4ZOp8PGjRsxcuRIAEBRURH279/vNCDalUGDBmHq1KnYv38/OnfujNBQ57nPhgwZ0vBK/Q8bPkREROQkMzMT7777LtasWYPw8HDHmByj0Yjg4GAAwCOPPIK7774bPXv2RJ8+fbBu3Tp88sknjh4ao9GIsWPHYvLkyYiKikJkZCSmTJmCjh07Ot7yupi///3vAIB58+bVW6dSqRr8Wvz52PAhIiLydU08c/PixYsBAL1793ZKz83NRUZGBgBg+PDhWLJkCXJycvDwww+jXbt2+Oijj9CjRw/H9vPnz4dWq8XIkSNRU1ODvn37YtmyZdBoGjb7ft1YIm9iw0dCr7JCf8HYq2q7XryxRJBaPKtkuKZWus9vlhbC9BaaamF6ud29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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Print one sample data point from the accessor\n", + "accessor['2010-01-01T00']['2m_temperature'].plot(x='longitude', y='latitude')" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "c05733a4-e8e8-4b21-9b6d-5449dfdcc9dd", + "metadata": {}, + "outputs": [], + "source": [ + "data_pipeline = pyearthtools.pipeline.Pipeline(\n", + " accessor,\n", + " pyearthtools.data.transforms.coordinates.StandardLongitude(type=\"-180-180\"), \n", + " # Uncomment the line below if working with multi-level data\n", + " # pyearthtools.pipeline.operations.xarray.reshape.CoordinateFlatten(\"level\"),\n", + " pyearthtools.pipeline.modifications.TemporalRetrieval(\n", + " concat=True, samples=((0, 1), (6, 4, 6)) # Input = 1 sample from time T=0 hours. Output = T+6,+12,+18,+24\n", + " ), \n", + " pyearthtools.pipeline.operations.xarray.normalisation.MagicNorm(cache_dir=workdir), # Incremental normalisation calculator\n", + " pyearthtools.pipeline.operations.xarray.conversion.ToNumpy(),\n", + " pyearthtools.pipeline.operations.numpy.reshape.Rearrange('c t h w -> t c h w'), # channel time height width -> time channel height width\n", + " sampler=pyearthtools.pipeline.samplers.Default(),\n", + " iterator=pyearthtools.pipeline.iterators.DateRange(1980, 2016, interval='6 hours')\n", + ")\n", + "# data_pipeline" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "03c83595-e990-4162-b52f-a325f879f4a6", + "metadata": {}, + "outputs": [], + "source": [ + "# Uncomment this to view data from the pipeline.\n", + "# Experiment with skipping different steps to see the effect on the final data\n", + "# data_pipeline['2000-01-06T00']" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "9eae67cf-fe58-42d8-88b6-56a317ce9e5a", + "metadata": {}, + "outputs": [], + "source": [ + "splits = {\n", + " 'train_split': pyearthtools.pipeline.iterators.DateRange(1980, 2016, interval='6 hours'),\n", + " 'valid_split': pyearthtools.pipeline.iterators.DateRange(2018, 2020, interval = '6 hours'),\n", + "}\n", + "\n", + "# If you encounter memory problems, set the batch size to a lower number, or 1\n", + "# If you encounter CPU / multithreading issues, set num_workers to a lower number, or 0\n", + "# 0 is a special number which means 'use the main process', whereas '1' will spawn 1 worker\n", + "datamodule = pyearthtools.training.data.lightning.PipelineLightningDataModule(\n", + " data_pipeline,\n", + " **splits,\n", + " **{'num_workers': 11, 'batch_size': 8}\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "0c475fbf-f74f-42c7-a0b5-887047677f38", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Setting up PyTorch Lightning Model\n", + "{'img_size': (64, 32), 'in_channels': 4, 'out_channels': 4, 'embed_dim': 768, 'num_blocks': 4, 'patch_size': (2, 2), 'depth': 12}\n" + ] + } + ], + "source": [ + "# If this model doesn't fit on your GPU, reduce the size of the embed_dim to a much lower\n", + "# number. This will impact model accuracy but may still produce acceptable outputs.\n", + "model = fourcastnext.registered_model.FourCastNextRM(\n", + " pipeline=data_pipeline, \n", + " lightning_model_params = {'img_size': (64, 32), \n", + " 'in_channels': 4, # Increase this if using additional data\n", + " 'out_channels': 4, # Increase this if using additional data\n", + " 'embed_dim': 768, \n", + " 'num_blocks': 4, \n", + " 'patch_size': (2,2), # Change this to (4,4) if the GPU memory is exceeded\n", + " 'depth': 12,\n", + " },\n", + " output='.',\n", + " lead_time=6 # Time delta for autoregressive step\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "90012f87-735e-414a-9a04-8b6eb7bce0ab", + "metadata": {}, + "outputs": [], + "source": [ + "trainer_configuration = {'precision': '32', \n", + " 'checkpointing': [\n", + " {'monitor': 'train_loss', 'mode': 'min', \n", + " 'dirpath': '{path}/Checkpoints/Train', \n", + " 'filename': 'model-{epoch:02d}-{step:02d}', \n", + " 'every_n_train_steps': 1000, 'save_top_k': 10}, \n", + " {'monitor': 'valid_loss', 'mode': 'min', \n", + " 'dirpath': '{path}/Checkpoints/Valid', \n", + " 'filename': 'model-{epoch:02d}-{step:02d}-{valid_loss}', \n", + " 'every_n_train_steps': 5000, 'save_top_k': 10}, \n", + " {'monitor': 'epoch', 'mode': 'max', \n", + " 'dirpath': '{path}/Checkpoints/Epoch', \n", + " 'filename': 'model-{epoch:02d}', \n", + " 'save_on_train_epoch_end': True, \n", + " 'save_top_k': 50}]}\n", + "\n", + "checkpoint_path = workdir \n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "ed7de16d-31e2-4895-b971-c7cd19f06842", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "MAX_EPOCHS = 2 # Suggest training 1 or 2 epochs at first.\n", + " # It is worth experimenting with longer training\n", + "\n", + "trainer = pyearthtools.training.lightning.Train(\n", + " model.lightning_model, # We train the lightning model not the registered model?\n", + " datamodule,\n", + " path=checkpoint_path,\n", + " trainer_kwargs={'num_sanity_val_steps': 1, 'max_epochs': MAX_EPOCHS, },\n", + " **trainer_configuration\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "20efd398-37d5-4e02-b69a-6d5f9192840e", + "metadata": {}, + "outputs": [], + "source": [ + "# One epoch may take 30-60 minutes. This can be affected by changing the batch size or the patch size.\n", + "# trainer.fit()" + ] + }, + { + "cell_type": "markdown", + "id": "e01b77aa-4d3f-45e1-a0ff-35fad08e7c7b", + "metadata": {}, + "source": [ + "## Predicting and Evaluating\n", + "\n", + "Once the model has been trained for sufficient time, it can be used to produce predictions. The model must be trained for at least 1000 steps (about 20% of an epoch) before it will create a checkpoint file. This tutorial trains the model for two epochs, but you can interrupt it earlier and resume if you want to look at the performance of the model earlier in the training cycle.\n", + "\n", + "We reload the model using a specific checkpoint file from disk so that we see the same results each time. The models are very sensitive to the normalisation parameters used to train the model, so make sure those are also being used consistently.\n", + "\n", + "We will now compare predicted values to observed values over a specified 24 hour period." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "37207060-e830-4c7f-87f0-fe7e596e16c2", + "metadata": {}, + "outputs": [], + "source": [ + "COMPARISON_BASE_TIME = '2011-03-01T00'\n", + "COMPARISON_ANALYSIS_TIME = '2011-03-01T06'" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "1578cde1-a7ce-43a3-a9c7-7ef717add262", + "metadata": {}, + "outputs": [], + "source": [ + "# IMPORTANT! Your checkpoints will be in a unique location based on your training!\n", + "full_checkpoint_path = workdir + '/Checkpoints/Train/model-epoch=02-step=1000.ckpt'" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "53e74990-2dc0-4d0e-bdf0-2d42d59e5155", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Setting up PyTorch Lightning Model\n", + "{'img_size': (64, 32), 'in_channels': 9, 'out_channels': 9, 'embed_dim': 768, 'num_blocks': 4, 'patch_size': (2, 2), 'depth': 12}\n" + ] + } + ], + "source": [ + "# If this model doesn't fit on your GPU, reduce the size of the embed_dim to a much lower\n", + "# number. This will impact model accuracy but may still produce acceptable outputs.\n", + "pmodel = fourcastnext.registered_model.FourCastNextRM(\n", + " pipeline=data_pipeline, \n", + " ckpt_path = full_checkpoint_path,\n", + " lightning_model_params = {'img_size': (64, 32), \n", + " 'in_channels': 9, \n", + " 'out_channels': 9,\n", + " 'embed_dim': 768,\n", + " 'num_blocks': 4, \n", + " 'patch_size': (2,2),\n", + " 'depth': 12,\n", + " },\n", + " output='.',\n", + " lead_time=6\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "daf50654-db6c-4de2-8aa7-78ff967bd7b2", + "metadata": {}, + "outputs": [], + "source": [ + "# %%capture\n", + "# IMPORTANT! This cell must be run to produce prediction data. It produces a lot of debugging information, so it has been commented out for display purposes.\n", + "prediction = pmodel.run(COMPARISON_BASE_TIME)\n", + "\n", + "analysis_pipeline = pyearthtools.pipeline.Pipeline(\n", + " accessor,\n", + " pyearthtools.data.transforms.coordinates.StandardLongitude(type=\"-180-180\"),\n", + " # fourcastnext.CropToRectangleSmall(), \n", + " pyearthtools.pipeline.modifications.TemporalRetrieval(\n", + " concat=True, samples=((0, 4, 6))\n", + " ),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "f9695430-7463-4092-bc13-fcf6f8d2339b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABJkAAAEiCAYAAABa/wM6AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQAAsgJJREFUeJzsvXecXVW9/v/sffr0mUySyaQn1EhoiUKCQEBpIqh4VcpFgsqlRQiICF+uJHhp0kR6FcGfAl7KtSEGNIBIaCFIDy2NZCaTMpk+p67fH0PO/qzPmbPPzMmEZCbP+/WaV84+a++1V3vWWmdl7/U4xhgDQgghhBBCCCGEEEK2AHdbJ4AQQgghhBBCCCGEDH64yEQIIYQQQgghhBBCthguMhFCCCGEEEIIIYSQLYaLTIQQQgghhBBCCCFki+EiEyGEEEIIIYQQQgjZYrjIRAghhBBCCCGEEEK2GC4yEUIIIYQQQgghhJAthotMhBBCCCGEEEIIIWSL4SITIYQQQgghhBBCCNliuMi0lXnmmWfgOA42bdq0rZNCCBkgqGtChh7UNSFka8N+hhCyI8BFpgFk1qxZmDt3rvXdzJkz0dDQgMrKym2TqCJJJpP4yU9+gqlTp6K0tBT19fX47ne/izVr1ljnxeNx/PCHP0RtbS1KS0tx7LHH4pNPPrHOueKKKzBz5kyUlJSgqqqq1/ude+65mDZtGiKRCPbee+8+p/PZZ5/FtGnTEI1GMWnSJNxxxx1W+GOPPYbp06ejqqoKpaWl2HvvvfGb3/ymYLzGGMyfPx/19fWIxWKYNWsW3n77beucu+66C7NmzUJFRUXOhOHXv/41HMfx/XvmmWf6lAcA2LRpE84++2yMGjUK0WgUu+++O5544gnfPDQ3N+Pkk09GZWUlKisrcfLJJ+dMalauXIljjjkGpaWlqK2txTnnnINEIlGwfG677TZMnDgR0WgU06ZNwz//+c9+l99ggbqmrjfTV103NDTgxBNPxK677grXdXPaDwDcfffdOPDAA1FdXY3q6mp8+ctfxssvv1wwD9T1wEBdU9ebGUhdA8CNN96IXXfdFbFYDGPHjsV5552H7u5u3zxQ10MT9jM7Tj+zceNG/PCHP8Suu+6KkpISjBs3Dueccw5aWlr6nHZChhJcZNrKhMNh1NXVwXGcbZ2UftHZ2YnXXnsNP/3pT/Haa6/hsccew/vvv49jjz3WOm/u3Ll4/PHH8dBDD+H5559He3s7vvrVryKdTmfPSSQS+Na3voUzzzwz7/2MMfje976H73znO31O47Jly/CVr3wFBx54IJYsWYL/9//+H8455xw8+uij2XNqampwySWXYNGiRXjjjTdw6qmn4tRTT8Xf/vY337ivueYa3HDDDbjlllvwyiuvoK6uDocddhja2tqsMjryyCPx//7f/8u5/jvf+Q4aGhqyfzNmzMBpp51mfTdz5sw+5SGRSOCwww7D8uXL8cgjj2Dp0qW4++67MXr0aN88nHjiiXj99dfx5JNP4sknn8Trr7+Ok08+ORueTqdx9NFHo6OjA88//zweeughPProo/jRj37kG+/DDz+MuXPn4pJLLsGSJUtw4IEH4qijjsLKlSv7VX6DGeqauvbTdTwex/Dhw3HJJZdgr7326jUtzzzzDE444QQsXLgQixYtwrhx43D44Ydj9erVvnmgrrce1DV1vaW6/u1vf4uLLroI8+bNw7vvvot7770XDz/8MC6++GLfPFDXOw7sZ4ZmP7NmzRqsWbMG1113Hd588038+te/xpNPPonvf//7fU4/IUMKQwaEU045xQCw/pYtW2YWLlxoAJjm5mZjjDH33XefqaysNH/605/MLrvsYmKxmPnmN79p2tvbza9//Wszfvx4U1VVZebMmWNSqVQ2/ng8bn784x+b+vp6U1JSYr7whS+YhQsXfqZ5fPnllw0As2LFCmOMMZs2bTKhUMg89NBD2XNWr15tXNc1Tz75ZM71m/Pux7x588xee+3Vp/RceOGFZrfddrO+O/30083+++/ve90+++xj/vu//ztveCaTMXV1debqq6/Oftfd3W0qKyvNHXfckXO+ruPeOPjgg825555bVB5uv/12M2nSJJNIJHxyZfPOO+8YAObFF1/Mfrdo0SIDwLz33nvGGGOeeOIJ47quWb16dfacBx980EQiEdPS0pI37i984QvmjDPOsL7bbbfdzEUXXWSM6X/5bc9Q1z1Q172TT9f9PccYY1KplCkvLzf3339/3nOo64GBuu6Buu6dLdH12WefbQ499FDru/PPP9988YtfzBsXdT00YT/Tw47Yz2zm97//vQmHwyaZTPYp/YQMJfgk0wDxy1/+Mud/v8aOHdvruZ2dnbjpppvw0EMP4cknn8QzzzyD4447Dk88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P21ee+01c+ihh5q99trLcsBYsWKFWbJkibnssstMWVmZWbJkiVmyZIlpa2vLnvPBBx+YJUuWmNNPP93ssssu2XPkvTQff/yxKSkpMeedd5555513zL333mtCoZB55JFHsudceeWVZsGCBeajjz4y7777rrn++utNMBg0d999t2/+r776alNZWWkee+wx8+abb5oTTjjBjBo1yrS2tmbPaWhoMEuWLDF33323AWCee+45s2TJErNhw4ac+PI50fQlDytXrjRlZWVmzpw5ZunSpebPf/6zGTFihLn88suz5zz22GNm1113teI+8sgjzZ577mkWLVpkFi1aZKZOnWq++tWvZsNTqZTZY489zJe+9CXz2muvmaefftqMGTPGzJkzx4pn1113NY899lj2+KGHHjKhUMjce++95p133jFz5841paWlZvny5f0qv8ECdU1d91fXxphsXqdNm2ZOPPFEs2TJEvP2229nw3/+85+bcDhsHnnkEaseZPlR11sP6pq63hq6njdvnikvLzcPPvig+fjjj82CBQvM5MmTzbe//e3sOdT1jgP7mR2nn2ltbTX77befmTp1qvnwww+t8pF5J2RHgYtMA8jSpUvN/vvvb2KxWEGrUklfBpNEImEuvfRSM2HCBBMKhUxdXZ35xje+Yd54442tkpdly5blWK9u/pMWqV1dXWbOnDmmpqbGxGIx89WvftWsXLkyJy+F4jn44IN7PWfZsmW+6XzmmWfMPvvsY8LhsJkwYYK5/fbbrfBLLrnE7LTTTiYajZrq6mozY8YMy1o1H5lMxsybN8/U1dWZSCRiDjroIPPmm29a58ybN6/XNN9333058flNWgvlwRhjXnjhBbPffvuZSCRiJk2aZK644gpr0LrvvvuMXjPesGGDOemkk0x5ebkpLy83J510Uo5t84oVK8zRRx9tYrGYqampMXPmzDHd3d3WOb3l6dZbbzXjx4834XDY7LvvvubZZ5/td/kNFqhr6roYXfcWx/jx47Ph48eP7/WcefPmZc+hrrce1DV1vTV0nUwmzfz5883kyZNNNBo1Y8eONWeddZalUep6x4H9zI7Tz2yu12LSTMhQxDEmz/O7hBBCCCGEEEIIIYT0kdwdCAkhhBBCCCGEEEII6SdcZCKEEEIIIYQQQgghWwwXmQghhBBCCCGEEELIFsNFJkIIIYQQQgghhBCy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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# PLOT MODEL PREDICTIONS\n", + "# These predictions were from training step 17,000. \n", + "fcst_as_celcius = prediction['2m_temperature'] - 273.15\n", + "fcst_as_celcius.attrs[\"units\"] = \"deg C\"\n", + "fcst_as_celcius.plot(x='longitude', y='latitude', col='time', col_wrap=4)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "40d0e473-8990-4efe-89e6-10120227524c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABJkAAAEiCAYAAABa/wM6AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQAArMRJREFUeJzsnXeUHMW59p+emZ3ZnLTS7ioHQCBElGwQSQITjcE2vhcTbCPsywcmCowxXGwkMMlkG5PBBF8b8CVcJ4wFtgADIknCJJGVtau4OU2q749F02+9vdMTtAq7en7nzDnTUzXVlZ6qmprufhxjjAEhhBBCCCGEEEIIIZtBYFtngBBCCCGEEEIIIYQMfLjJRAghhBBCCCGEEEI2G24yEUIIIYQQQgghhJDNhptMhBBCCCGEEEIIIWSz4SYTIYQQQgghhBBCCNlsuMlECCGEEEIIIYQQQjYbbjIRQgghhBBCCCGEkM2Gm0yEEEIIIYQQQgghZLPhJhMhhBBCCCGEEEII2Wy4ybSFeeGFF+A4Dpqbm7d1Vggh/QR1Tcjgg7omhGxpOM4QQnYEuMnUj8yYMQOzZs2yPjvggAPQ0NCAioqKbZOpPInFYvjJT36CPfbYAyUlJRg+fDi+973vYfXq1Va8np4enHfeeaipqUFJSQmOP/54rFy50opzzTXX4IADDkBxcTEqKyv7PN8FF1yAKVOmIBKJYO+99846ny+++CKmTJmCwsJCjB8/HnfffbcV/tRTT2Hq1KmorKxESUkJ9t57b/z2t7/NmK4xBnPmzMHw4cNRVFSEGTNm4P3337fi3HvvvZgxYwbKy8s9C4aHHnoIjuP4vl544YWsygAAzc3NOOecc1BfX4/CwkLstttueOaZZ3zL0NTUhO9+97uoqKhARUUFvvvd73oWNcuXL8dxxx2HkpIS1NTU4Pzzz0c0Gs1YP3feeSfGjRuHwsJCTJkyBf/6179yrr+BAnVNXW8iW103NDTglFNOwcSJExEIBDz9BwDuu+8+HHzwwaiqqkJVVRUOP/xwvPHGGxnLQF33D9Q1db2J/tQ1ANx2222YOHEiioqKMGrUKFx44YXo7u72LQN1PTjhOLPjjDMbN27Eeeedh4kTJ6K4uBijR4/G+eefj5aWlqzzTshggptMW5hwOIy6ujo4jrOts5ITnZ2dWLhwIX72s59h4cKFeOqpp/Dxxx/j+OOPt+LNmjULTz/9NB577DG8/PLLaG9vx9e+9jUkEolUnGg0iv/8z//ED3/4w7TnM8bg+9//Pr797W9nncclS5bgq1/9Kg4++GAsWrQI//3f/43zzz8fTz75ZCpOdXU1Lr/8csyfPx/vvPMOTj/9dJx++un4+9//7pv2DTfcgFtuuQW//vWv8eabb6Kurg5HHHEE2trarDo6+uij8d///d+e73/7299GQ0ND6jVt2jScccYZ1mcHHHBAVmWIRqM44ogjsHTpUjzxxBP46KOPcN9992HEiBG+ZTjllFPw9ttv49lnn8Wzzz6Lt99+G9/97ndT4YlEAsceeyw6Ojrw8ssv47HHHsOTTz6JH/3oR77pPv7445g1axYuv/xyLFq0CAcffDCOOeYYLF++PKf6G8hQ19S1n657enowdOhQXH755dhrr736zMsLL7yAk08+GfPmzcP8+fMxevRoHHnkkVi1apVvGajrLQd1TV1vrq5/97vf4dJLL8Xs2bOxePFiPPDAA3j88cdx2WWX+ZaBut5x4DgzOMeZ1atXY/Xq1bjpppvw7rvv4qGHHsKzzz6LH/zgB1nnn5BBhSH9wmmnnWYAWK8lS5aYefPmGQCmqanJGGPMgw8+aCoqKsyf//xns8suu5iioiLzrW99y7S3t5uHHnrIjBkzxlRWVppzzz3XxOPxVPo9PT3mxz/+sRk+fLgpLi42X/7yl828efO2ahnfeOMNA8AsW7bMGGNMc3OzKSgoMI899lgqzqpVq0wgEDDPPvus5/ubyu7H7NmzzV577ZVVfi655BKz6667Wp+deeaZZv/99/f93j777GN++tOfpg1PJpOmrq7OXH/99anPuru7TUVFhbn77rs98XUb98X06dPNBRdckFcZ7rrrLjN+/HgTjUZ9SmXzwQcfGADmtddeS302f/58A8B8+OGHxhhjnnnmGRMIBMyqVatScR599FETiURMS0tL2rS//OUvm7POOsv6bNdddzWXXnqpMSb3+tueoa57oa77Jp2uc41jjDHxeNyUlZWZhx9+OG0c6rp/oK57oa77ZnN0fc4555jDDjvM+uyiiy4yBx10UNq0qOvBCceZXnbEcWYTf/jDH0w4HDaxWCyr/BMymOCVTP3EL3/5S8+/X6NGjeozbmdnJ371q1/hsccew7PPPosXXngBJ5xwAp555hk888wz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brkFDz30EHbZZRdcffXVOOKII/DRRx+hrKysr+QIIYQQQgghhBBCPDiBAJyA/3U5mcK3N+bNm5d6v3TpUlx66aWYOXNmyk1u/vz5ePjhh3HdddflfY4BtckUCoWsq5c2YYzBbbfdhssvvxwnnHACAODhhx9GbW0tfv/73+PMM8/c2lklhBBCCCGEEELIACUQzOJ2uQzh2xvTp09Pvb/qqqtwyy234OSTT059dvzxx2OPPfbAvffei9NOOy2vcwyoGvnkk08wfPhwjBs3DieddBI+//xzAMCSJUvQ2NiII488MhU3Eolg+vTpePXVV33T7OnpQWtrq/UihAxsqGtCBh/UNSGDD+qaELJdEwzAyfDCANtkksyfPx9Tp071fD516lS88cYbeac7YK5k2m+//fDII49gl112wZo1a3D11VfjgAMOwPvvv4/Gxl4r8NraWus7tbW1WLZsmW+61113Ha688krP506sC0405LHVdoQdtlGWx3DsDmbEcUBZ+koDWW3NOj5sp3vmPm652uL20+uXNrvpRpRtqraOD/k8+X59p5uHWMK2sqwu0jbW7nmqlX15m7D91ufTdvXB5pUiAyvsDIlbIRGwz2HUsaz3gLJMTxYJG1/Hzo92oywSNqqjK2xLU+lyHAnZX2zrscu1XljZaotoaZ2srVH1/bzSulhbtUqau+3+0q0smaUtejBg56c07A4B2sq5sjD98KDH0jKfe5F1Pfu6gMo2knpychu80+o6EYUTjwJJ24bW0X2soFAG2mEBt16UiztspQBOwtXnzpGoFfajA0al3m/otuv+0412/pa3uLaxVUqPYyrdvJardmjpsfuNTKdO2SOPr3Ktb4cW2+mEVYNLu+5gwh7bgm1rU+8DnU1WWLK92Tp2pM5DynY9JNpAjcPwWGeL45CtBxNxLX/l+A3Y9sMAUCwaNKQ6qpSrHlujasyMCrv3sBov5HGTandtg94uuoy2RJe61jboxcrSW1qWl6o+UijmjYAaI4eX2W1SIupH69ge75UdsrZH3qShQG5LkHS6RjLe+wJ69f0FjtZugW3dLrVtVF5Cos8XqLnLidl9flzA1dX39xhihXUZt76Xt9r99r01bUjHzkNKrOPxVa5edd1rF/L2qLCfVm1aJizbi4N2uwR62lXCbrmCXbaWERdl6bI3BUxSzVda25JE+vbyrL9EG4VU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9jj7qfq22kY5t8q8BoSDEKDmb5WOb3srB1i91LZcG/WYKc6p50vjWTe5Kes5yNKrdpdTayp5HrPqYyssVDfWDVMuWMGQ7dhbLhyzytcts8KiuxySev/OWrufl0fcdNd02u0zttLOa02322eMduyKu+kEuprtvLaqsV9oO6n6b0K2rc8cBth9zdNfv9BJvL90vXENEj3FfYZ5+pFA6sOzhs8FURcB5dDmqHR7Pl7kfk1pxQg74IIae50eX/lp+vNrV2qRH9+xAraLna9joHZ11a6E4lhfpyG91PR8qV2G4fNzKSnWvmbDahWoXRKF41+RcussE25ua+xbj5LFVdaxdOUtdlTJxG+FL/fYc+Ka+r2t43fWuFoqi9jttabd1fYuQ2wnvNqYPZYkQ0LbCbXe6XR/TAWV83WyaY193O3mJ9qh+kjS5zep/p0n2lOOn/01Zw84AgHvb4G+4uRJS0uvq2J1da8r4pIlS9DY2IgjjzwyFScSiWD69Ol49dVX+32T6cc//jHOOeccdHd3wxiDN954A48++iiuu+463H///Xmny00mQgghhBBCCCGEEIETDHr/pOojDgC0ttqbupFIBJFIpK+vAOh99tJFF12Egw46CJMnTwYANDb2/klQW2tvStfW1qac3/qT008/HfF4HJdccgk6OztxyimnYMSIEfjlL3+Jk046Ke90uclECCGEEEIIIYQQIgkEvVcW9hUHwKhRo6yPZ8+ejTlz5qT92rnnnot33nkHL7/8sifMUXfgGGM8n20u8Xgcv/vd73DcccfhjDPOwPr165FMJjFs2LDNTpubTIQQQgghhBBCCCECJxTyPl7BEycOAFixYgXKy93ba/2uYjrvvPPwpz/9CS+99BJGjhyZ+ryurveRC42Njaivr099vnbtWs/VTZtLKBTCD3/4Qyxe3Ps4npqamn5Lm+5yhBBCCCGEEEIIIRIn6F7NlO7l9F7JVF5ebr362mQyxuDcc8/FU089hX/+858YN26cFT5u3DjU1dXhueeeS30WjUbx4osv4oADDuj34u23335YtGhR5og5wiuZCCGEEEIIIYQQQiQ53C6XDeeccw5+//vf449//CPKyspSz2CqqKhAUVERHMfBrFmzcO2112LnnXfGzjvvjGuvvRbFxcU45ZRTNqckfXL22WfjRz/6EVauXIkpU6agpMR+UP2ee+6Z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ROlQJMq+0HWGvpqOTK5nclj1rcEMp5dziWe+Eu+DytlNr4Es1y89t0ons0xubnI+93OvzVSP0llMu2T7fU1VprWeTaTXmKPaWS9ZrXpW866Usl4jRBPp44aDtq7lKQvVulx3Qzk36HVxS49bzq4ef13LdEsK0q89dGvpOigMyrV4+nN4XHi1+164pM+wBFqxI5JIJu1BNF2cLcwll1yC4cOH49///rd97kQCxx57LIYOHYqXX34ZGzZswGmnnQZjDG6//faM6RYWFqK11du2H330EYYOHdrHN7KDVzIRQgghhBBCCCGECLaH2+X+9re/Ye7cubjppps8YXPnzsUHH3yA//mf/8E+++yDww8/HDfffDPuu+++PjePNF//+tdx1VVXIRaLAejd7F2+fDkuvfRSfOtb38o7z9xkIoQQQgghhBBCCBEkk+7Dv9O/euO2trZar56ens0+/5o1a3DGGWfgt7/9LYqLiz3h8+fPx+TJkzF8+PDUZ0cddRR6enqwYMGCjOnfdNNNWLduHYYNG4auri5Mnz4dO+20E8rKynDNNdfknW/eLkcIIYQQQgghhBAiMMZkfEzDpvBRo0ZZn8+ePRtz5szZrHPPnDkTZ511FqZOnYqlS5d64jQ2NqK2ttb6rKqqCuFwOKtnKpWXl+Pll1/GP//5TyxcuBDJZBL77rsvDj/88LzzDXCTiRBCCCGEEEIIIcQiETdAMMNz7754dtaKFStQXu4+IzIS6fsZhXPmzMGVV17pm+abb76JV199Fa2trbjssst84+rnmQG9G1R9fZ6Oww47DIcddljW8TPBTSZCCCGEEEIIIYQQQTbPXNoUXl5ebm0ypePcc8/FSSed5Btn7NixuPrqq/Haa695NqumTp2KU089FQ8//DDq6urw+uuvW+FNTU2IxWKeK5zS8cYbb+CFF17A2rVrkVQPMb/llluySkPDTSZCCCGEEEIIIYQQQS6bTNlSU1ODmpqajPF+9atf4eqrr04dr169GkcddRQef/xx7LfffgCAadOm4ZprrkFDQwPq6+sB9D4MPBKJYMqUKRnPce211+KnP/0pJk6ciNraWuvqp1yuhNJwkykNiVAhEspqNBPatlS6Hep7Of36orY19rsPNJfGl1agCeWzXBBwbToDyu40qbxbO2JuwTZ02dbU9aXue20Lqm2g40Zavtp5DYgy6yL2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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# PLOT ANALYSIS GRIDS (TRUTH)\n", + "an_as_celcius = analysis_pipeline[COMPARISON_ANALYSIS_TIME]['2m_temperature'] - 273\n", + "an_as_celcius.attrs[\"units\"] = \"deg C\"\n", + "an_as_celcius.plot(x='longitude', y='latitude', col='time', col_wrap=4)" + ] + }, + { + "cell_type": "markdown", + "id": "60a59f92-45cc-4091-a4ee-c1e1a8506f36", + "metadata": {}, + "source": [ + "# Things to try next\n", + "\n", + "1. The model may need to train for quite a while to converge. Experiment with additional training.\n", + "2. The data includes some additional analysis variables. Try training it on more data and chart out how that affects the training effectiveness as measured by the loss function\n", + "3. Try adding some context variables such as time-of-day and time-of-year\n", + "4. This notebook doesn't include any comparisons to benchmarks such as a persistence model or an alternative model. Charting skill score comparisons would be very useful.\n", + "5. A scorecard of results can also be informative, to see accuracy from multiple perspectives\n", + "6. Charting the skill of the model with respect to lead time would also be a useful exercise\n", + "7. Use PyTorch Lightning logging to graph training loss with respect to training step to understand how the network is converging" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "id": "7d297f04-07d4-4cf2-9669-9ff56cce0c25", + "metadata": {}, + "outputs": [], + "source": [ + "# Plot of Error (remember, this is a simplified model for the tutorial)\n", + "# Blue = model is too cold; RED = model is too hot\n", + "# Does the model capture the warming of the land as distinct from the ocean? Could a land-sea mask help?\n", + "# (prediction['2m_temperature'] - analysis_pipeline[COMPARISON_ANALYSIS_TIME]['2m_temperature']).plot(x='longitude', y='latitude', col='time', col_wrap=4)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5b4452b0-3deb-4e42-8ac7-d6c22e5a0a25", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.12" + }, + "nbsphinx": { + "orphan": true + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/packages/bundled_models/persistence/notebooks/LUCIE-Inference.ipynb b/packages/bundled_models/persistence/notebooks/LUCIE-Inference.ipynb new file mode 100644 index 00000000..f5a941d0 --- /dev/null +++ b/packages/bundled_models/persistence/notebooks/LUCIE-Inference.ipynb @@ -0,0 +1,165 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "351426c4-8312-4e46-a9aa-3e759ca97d8b", + "metadata": {}, + "source": [ + "# LUCIE Inference" + ] + }, + { + "cell_type": "markdown", + "id": "c9c0b785-8d4c-4b29-bac9-44ca72f9f440", + "metadata": {}, + "source": [ + "> **NOTE**\n", + "> Please see the [LUCIE Training](./LUCIE-Training.ipynb) tutorial to train the model weights required for this tutorial\n", + "> " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "175e2165-f568-48e2-bedb-1245603b1ab5", + "metadata": {}, + "outputs": [], + "source": [ + "import torch\n", + "import lucie\n", + "import lucie.inference\n", + "from pathlib import Path\n", + "import numpy as np\n", + "import xarray as xr" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "bc8460bd-8691-403f-8cbb-dbeb4e39875a", + "metadata": {}, + "outputs": [], + "source": [ + "device = torch.device(\"mps\" if torch.backends.mps.is_available() else \"cpu\")\n", + "device = torch.device(\"cuda:0\" if torch.cuda.is_available() else device)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "e5000929-4fec-4b6c-82c1-a5769287aa38", + "metadata": {}, + "outputs": [], + "source": [ + "regridded_path = Path.home() / 'dev/data/lucie' / 'era5_T30_regridded.npz'\n", + "regridded_data = lucie.train.load_data(regridded_path)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "3facc4a4-ec3f-4fc8-be29-c244ec4268e2", + "metadata": {}, + "outputs": [], + "source": [ + "preprocessed_path = Path.home() / 'dev/data/lucie' / 'era5_T30_preprocessed.npz'\n", + "preprocessed_data = np.load(preprocessed_path)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "fb5d5b70-681c-4cc4-b973-7b259bb6e81d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1min 49s, sys: 30.3 s, total: 2min 19s\n", + "Wall time: 1min 54s\n" + ] + } + ], + "source": [ + "%%time\n", + "%%capture\n", + "\n", + "# Note - these timings were obtained on a laptop, not on a high-performance GPU.\n", + "\n", + "predictions = lucie.inference.load_data_and_predict(device, regridded_data, preprocessed_data,model_weights_pth='model.pth')" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "9f9e517e-a6e6-4cff-bc82-fe0cff6c89ec", + "metadata": {}, + "outputs": [], + "source": [ + "da = xr.DataArray(predictions)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "e0f847a4-edd0-4dc4-9a2c-278c5df6127e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAh8AAAGxCAYAAADCo9TSAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjcsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvTLEjVAAAAAlwSFlzAAAPYQAAD2EBqD+naQAAcsxJREFUeJztnQl8HcWd56u736nbkizJxjY2R7CJIRBCwJAhLDA4hCVcuwmBcIWFhDWEY0OABBI2hDE5SciCmckSw04gzDATAnEYwhXMEMxlMMEcDhiDDbYs27Juvau791MtJOv/63Y/yc9+T8/v9/Xnfax6fVVXVbf+qqrfrwzXdV1FCCGEEFIkzGJdiBBCCCGEwQchhBBCig57PgghhBBSVBh8EEIIIaSoMPgghBBCSFFh8EEIIYSQosLggxBCCCFFhcEHIYQQQopKRO3mOI6j1q9fr2pra5VhGKXODiGEkAmM9t3s7e1VU6dOVaa56/4+T6VSKpPJFHyeWCymEomEKjd2++BDBx7Tp08vdTYIIYSUEevWrVPTpk3bZYHHrD1rVHuHXfC52tra1Jo1a8ouANntgw/d46H5zCe/qSJW3Ps50xAT+0RSsgEYOcd3nky9PCZbLSPiyKA8xrVkL4sdl+lcQqYN/yWVE4F9wAk/MijT2SqZJysjt1tpmU7Xy/3dgNaA1zCzcrvhyO2DTZCHNJwQ/pAws353fywbzIOCDiwnCl/A7k5UpqP9cgcbj/fqC/IJ7wjHgiy54dfI1MhrWAF/8OTg3eFCthLdbmg5uVC2DtRnHI7H9uWdA46JDGD9u+HPQQrbJNQltEGNkef9G+2zQ+siB8+WmZPXSGyWjXBgStKfB2jHuSQUJhSVYYevSoF1Y8Fz4wGnMOF5zVXD8w/3ncXt+JjkcH9/FiKDeeofnj18T1mwPZIKfw8GtYl4jxP6bCFYN7EeWTDRfnnjZsbfwAwbbmTUbeTstHr6tVtGfnfsCjKZjBd4rFm+p6qr3fHelZ5eR8065H3vfAw+JhjDQy068IhEht7uThSCjxwEH8ofCeAxbhReurnwh86AX3BuLH/wYeQLPuAl68TgFz+83SL4go3lDz4seMma8C4xYbuFeXDzBB/4xgwoG8yD7xdBnuDDgODDwoAnIPhQsrp9v/AMKCvfSxmuYeE9+a+o3Hh48GHF3NBywuDDd99wPLYv7xxwXxa0MWxT2OYiUFcO3nfAUlJmLvy+I1EIPvBdDfWHbSoC9xmJJvIGHy7cF7Y5DHDytuGAJoZtxoSy8b0jMADG7djuMRiF9uXly85T/1CfmGd8B0Xs/MGHC/UViY4v+FD47oX2EYlA8OEEBB/4jg9ol8UYpq+rNQsKPsqZ3b7ngxBCCJmI2K6j8nSi5T2+XGHwQQghhJQAR7nep5Djy5XK7O8hhBBCSMlgzwchhBBSAhzvX2HHlysMPgghhJASYLuu9ynk+HKlYoIPM2cr0x2a9Vz17la5EWY1O9X+aeHJfqmNjCeklCDTKI8ZmGSGzkTP1sJMdZjx7+UD1AqGI4/JgnwTZbAoQU01GqHSyzRI4Lx9usJnu6MKoGqTEyq9xePdgBnlsb5wKaxfrhkuQe7bA6R5vfLoRFeQugmk0dAkEiBBRSk1DsXWrIcZ+QP+a+ZAKo15QOl01UZ5jmyNlAlYaSdcat3glxWgZByPQWltvNsJlX/WrpcNO9Lr15zayUhoOdiooOqVZZkAaSVK5R1QR8R6/A/bQGs0tA1hWabrrdBnC6XaboCCw6dOslHGhQeEpwOEY4JEZ/5fVJF+mbaj4bJmbB8Dk+WNNrwNWl5d/l2ospPnTHTKY7KNUhpdvaZHnhB+ARtbuuX26ipfHlx4fztV2+Rtrq/gya6gYoIPQgghZCLhVPCEUwYfhBBCSAlwlKvsCg0+qHYhhBBCSFFhzwchhBBSAhwOuxBCCCGkmNhUuxBCCCGkmDgffQo5vlypmGGXbH1cuZEh/Zs9GVa1hMWPUE6mcWFFtRzIUnFxLDsWLr3DVoOy2qBz+q4RDZfuoVww2h++WFrtOv8CTCj3xDzg6p64QFfjm9lQWV2qyd8Eo31O+KJeIHvM1kZC89TwjryvWK+8iQwcHyStjAwaoXLNXJWUGBqw4BZuD1ppDCXELixOF9sq5d5O1AotW8zDYHMkv/zTkl/mktiGZLlkQDKObTBTI7/I1ASsboarFEPZ4Eq5qUaZx0gKpLiwsirOycvWWnnbHLb7vim4kqDvFPKScAksF02uOnxBPbta5snIwbOYBouAKCwCCftn6v2Zxuc5CipVBBenq+6Q16x9b0DuELSQ4ABKaaUUNtsg389mRpZDaqpcbdaCVcmtAKsEXx4G5XvJTm57mdqw0CjZNXDCKSGEEFIC7I/ULoV8xsOiRYvUgQceqOrq6rzPvHnz1H/8x3+MbE+lUmrBggWqqalJ1dTUqNNPP11t3LhRnGPt2rXqxBNPVFVVVaqlpUVdddVVKpcLMKrKA4MPQgghpATYbuGf8TBt2jR18803q+XLl6uXXnpJHXPMMerkk09Wr7/+urf9iiuuUH/4wx/U/fffr5YuXarWr1+vTjvttG35tW0v8MhkMurZZ59Vd999t7rrrrvUd7/73XHfe8UMuxBCCCGVzEknnSTSN910k9cb8txzz3mByZ133qnuvfdeLyjRLF68WM2ZM8fbfvjhh6tHH31UvfHGG+rxxx9Xra2t6qCDDlI33nijuvrqq9UNN9ygYrFtTrH5YM8HIYQQUsIJp04Bnx1F92Lcd999qr+/3xt+0b0h2WxWHXfccSP7zJ49W82YMUMtW7bMS+v/DzjgAC/wGGb+/Pmqp6dnpPdkrLDngxBCCCkBjjKUXcBaMvp4jf7lP5p4PO59gnjttde8YEPP79DzOh544AG1//77qxUrVng9Fw0NDWJ/HWi0t7d7P+v/Rwcew9uHt40H9nwQQgghZcz06dNVfX39yGfhwoXb3Xe//fbzAo3nn39eXXzxxercc8/1hlKKDXs+CCGEkBLguEOfQo7XrFu3zlOvDLO9Xg+N7t3YZ599vJ8POeQQ9eKLL6pf/OIX6ktf+pI3kbSrq0v0fmi1S1tbm/ez/v+FF14Q5xtWwwzvM1YqJvjYdEBcWR9VCGruUevuBMyZwWMwHeTTMRorNf4+KJ83CB6SCb+GBT4A/qXn5YihCb4SmhgsX+6Al4SLngz9dqj3hAPLoyc3+pdYN2FJ9FwSvCeqrdDl7KP94JcBxZBuQIMU/31XbZCFmauBJdf7wCcAlm3HPGIeUgHL2eOS6Fj2gy2yQcS34lLy6LkC5wMvCxP212RhOXv0qxhstkI9OVx4o6SgbqrbA7xkoI1EwacjB3mKDDihz2K0V9ZNFuqu5v3+QB+gMP8ZrBu8JpZ1vCt8f42dkGknAc9nnzwo3Srvy+q3xuf7gY3Qe7Zk2kyHvwvrV4f7+phvvS+3RwJ+xSRkWZu1siDMfnmfuVqZie5Zsj7j3fCsWfL8TkAWsN3mRlmN2LoMnlFFwS5w2GX42GHp7I7gOI5Kp9NeIBKNRtUTTzzhSWw1q1at8qS1ephGo//Xk1Q7Ojo8ma3mscce866th27GQ8UEH4QQQkglc+2116oTTjjBm0Ta29vrKVueeuop9ac//ckbrrngggvUlVdeqRobG72A4tJLL/UCDq100Rx//PFekHH22WerH/3oR948j+uuu87zBgnrbQmCwQchhBBSAuyd1PMxVnSPxTnnnKM2bNjgBRvacEwHHn//93/vbb/llluUaZpez4fuDdFKlttvv33keMuy1JIlS7y5Ijooqa6u9uaMfP/73x933hl8EEIIISXAcQ3vU8jx40H7eISRSCTUbbfd5n22x5577qkefvhhVSgMPgghhJAK6PmYSFBqSwghhJCiwp4PQgghpATYyvQ+O358+cLggxBCCCkBboFzPvTx5UrFBB/pJqXMYTm5CVp1W1agDXp7TbQ3vJJ93iHgwWGDnj5X5ebx5Ai4hhF+zcigTFtpeY3EVhkn5xJmqMeDxolY4R4a4AMR7QXTFPRQgXKwGyJ5NfgW+I+gz0NikzQncKIyUwb4eFiDMlPpSX6JmGuZoXmyEzLfTtwMrSsrI/McAT8FL595FmpAXw68LycKnhumTGdrsL4D2pwb7kVhQbvGxSVivfIENliqZKv9bQy9ImI94BUDZY+eGw7cZ2pyPPSadnKUqcNHpOvM0GcFyyU1Se5vwO42+GOM9pEYxoI2YMeM8DYEvh5GFnw94FHyeZNE/O81K4XtWoXe12Cz3D+5STaA7EF7i3R0i99TxY3LRmFk5TkGp8jCGpgMPj/4DoFnD/OcrfVlwXefo+vLyefJRHYKFRN8EEIIIRMJu4InnDL4IIQQQkqA7ZreZ8ePV2UL1S6EEEIIKSrs+SCEEEJKgKMM5RTQB+DgZKQygsEHIYQQUgLsCp7zwWEXQgghhBSViun5sJOOcpNDki7DkdGimQlfinxoH5mO9apQeRf2hqHszSer86+wrmxQgFogATOz4fKxTB0usW2FSi0DpZ4QWKOcM9ovD0o3ShmdNeiEXiNorhXKUlFqmW8589hmKe9zIyjFAwly1m/VYwzKwo1shMLOynQM5KJubbVI55qkfNDqR82qUna11Gc6cVlf8T5YUr1rQJ7AlUtq2wmQSffJ+0w1+x//nulmaJvK1mHDlkkzA89WNvw50lRvkOmtH4NygGxGB8IlyGYO2j08W7lG/8OGcu7uWeGvRpTB5qrDZe9YjoFy3BontCxRWotlj7LZ2vfk9myNPw+ZhvB8O9HwdM9MeROJTngYA/4wR6n7wJRkqCQ83RD+nsI82bX5yx7bVK5m27PhROwymnDqqnKlYoIPQgghZOLN+TAKOr5cYfBBCCGElACnQHv1cp5wyjkfhBBCCCkq7PkghBBCSoDNOR+EEEIIKfawi8NhF0IIIYSQXQ+HXQghhJASYLuG9ynk+HKlYoIPwzaUAfr/YXLVaD7h3y9TD7tE0CtEhXtmoPzdCdfbB54DPRVAjh4ZCPcWQb28zzfEzu81gt4EFngRpOMyHQE/Ews8VayU31wEfT1ySdD523DNRpmpqq1QEDEU9UufAbM/YH37nj6Zpz7pHWLEoCCS0kzAqZUFF+mR18jVxfP6H0Q3dMsdTJgfbssKMzPg49EkDRB6p0t/i1SzLwt5PVTcKPiZ+HxgYLsFHhxp/xz3XLU8SaJDbrey+ZanhzYH1Z9qUqHeI16+oZ0asE+8R6azkIdoX3ge0VdCY0GzM7rMUP8KN+KOy0uofxrsHiAvwHzlq3/0M4oMuqHP6mAzPCf6ea1PhL5bffcNeUo1QxuDNofvB6xLjZU2tt8uM8XTYdgFql1sql0IIYQQQsZGxfR8EEIIIRMJxzW9z44fX74+Hww+CCGEkBJgc9iFEEIIIaQ4sOeDEEIIKQFOgYqVoLVAywUGH4QQQkhZmoyZqlyZUDm/+eablWEY6vLLLx/5LpVKqQULFqimpiZVU1OjTj/9dLVx48aS5pMQQgjZWfbqdgGfcmXC9Hy8+OKL6h//8R/VgQceKL6/4oor1B//+Ed1//33q/r6enXJJZeo0047Tf3lL38Z1/mdSVmlkh/5HAxKvwMTvCqC6tMGLxC7Vm43QTeOYM8a7h/tN/Jq8B2ZbZWuz+PT4YT7CqD/Afp+aJJb5Gzq1CSZTyeKHhzy+Eyt3B7vlueLmv77TtfL72I9oOOHCd5mVn6R3qNOpO2ErNBIv8yklZL+Gt416pPyGgOysH1zzA2ZZzshK2+wTZ4vW+1vZJEUeKC0oVmE3D44ORLabrHs0Usm0+w3dsF26cTgTuPQqMA7xwAfD/RcCFoBHK+RbobnEY6Jgf1JBNptpi683Qe1cweaAHqF5PU/ge0+3x+/rYuyE/k8UvCc4XXje2+hLQiczztHUt5ItkqmrQF5Ujsh87C1xggtFzPAWwm9grBN5GqcUK8YF9qgkZV5dGtlZRo9Ab/mjIDfD8M/DwYYg5CdzoQIm/r6+tRZZ52lfvWrX6lJkyaNfN/d3a3uvPNO9bOf/Uwdc8wx6pBDDlGLFy9Wzz77rHruuedKmmdCCCGkEBxlFPwpVyZE8KGHVU488UR13HHHie+XL1+ustms+H727NlqxowZatmyZSXIKSGEELJzsDnsUjruu+8+9fLLL3vDLkh7e7uKxWKqoUH2F7e2tnrbgkin095nmJ4e8EUmhBBCSOX2fKxbt05ddtll6p577lGJBAyA7iALFy705oYMf6ZPn75TzksIIYTsCpMxu4BPuVLSnOthlY6ODvXJT35SRSIR77N06VJ16623ej/rHo5MJqO6urrEcVrt0tbWFnjOa6+91psrMvzRAQ4hhBAy0XBco+BPuVJStcuxxx6rXnvtNfHd+eef783ruPrqq71ei2g0qp544glPYqtZtWqVWrt2rZo3b17gOePxuPchhBBCyMSkpMFHbW2tmjt3rviuurra8/QY/v6CCy5QV155pWpsbFR1dXXq0ksv9QKPww8/vES5JoQQQgrHKXDopJxNxiaMz8f2uOWWW5Rpml7Ph55IOn/+fHX77beP+zxmxFZmdMjXwAEvAvTTUKC390C9OphNoA+IkQO/g6w8PjIQ7o/h5Rm9BCCfJsjRc1XhOv9sXb7t/vu2q1AQH+6hgMqvxBa4BpwvqNfQiRihOn/cbqXAq6BWGhrEO2VB2Qm53QgyQIDvjHRABYlMyDx17i99PTLgC2OBD4QmMoh5gO1p8MNoMEKvkauW6WwjmFkElL0NHguG6YYfg14TUWggA1ZoXXrfJcBDB5419K/Igg9MpF/uYKVwe2hVDV0TOkuzNSrca6R3fL4fQfWNfiO5avD+6Q1/3u0kvlPC84TtI+gc6NOiwKclO8kOfQ/iey+QuDyH0StfbE4VXMMxwtNwPtyOXiZDF0Vjlu38POFXtTVVuTLhgo+nnnpKpPVE1Ntuu837EEIIIaT8mXDBByGEEFIJ2MrwPoUcX64w+CCEEEJKgMNhF0IIIYQUE7vA3os8M9EmNOU7W4UQQgghZQmHXQghhJAS4HDYZfenvmFQWR8tFx1vlpLDqCU7rzI5f0zWn5brQPdulbpWAySFKgkdYlmQk0VVaDpIxhqTRq8qU59HepvMs8w3qB6j/UZeCSJKBjGPKBlGGXO6If+S24mtUhqXgWW7LZCcOjEzXHpbJzNhDcL5Ybsm1iPbSG5SIo9MORKax9wUeQ92gPov3RguxcTlzNPNIFkMKEuRZeznxAag/xqpkvftotIWLmH3yoZr1shG6GRQJxvQ2YrSyRgs6x6T9+lE5bOWq5HbbVhC3bXM0OcmqB2j+hrbcQ6etSjIXE1UpMJ2Tc0GeZ8mKKGRNEiMs7lwqS0SJOdHyb8dxzYmG4DVD9LpJllwkTr5grAD6tuxwYagGQp/IBIu/4V260IHvlUFbTBA/mvVyWtakW11YZvoH7DrF5bbUQo5ttSUb84JIYQQMq61zw499FDP4LOlpUWdcsopnmv4aFavXq1OPfVUNXnyZM/Y84tf/KK3pMloOjs71VlnneVt1wu/ajPQvr6+sWeEwQchhBBSGlxlKKeAjz5+POi10xYsWKCee+459dhjj6lsNquOP/541d8/5MSn/9dpwzDUk08+qf7yl79466uddNJJynG29Q7pwOP111/3zrFkyRL19NNPq4suumhceeGcD0IIIaQE2EUednnkkUdE+q677vJ6QPQir0cddZQXbLz33nvqlVde8Xo1NHfffbeaNGmSF4wcd9xx6s033/TO8+KLL6pPfepT3j6//OUv1ec//3n1k5/8RE2dOnVMeeGwCyGEEFLG9PT0iI9eimQs6JXfNXrtNI0+Tvd6jF6cVbuM6yVOnnnmGS+9bNkyb6hlOPDQ6KBE7/P888+POc8MPgghhJAS4LhGwR+NXgG+vr5+5KPnduS9tuOoyy+/XB155JEjC7nqBVv14q56VfmBgQFvGOab3/ymsm1bbdiwwdunvb3d6y0ZTSQS8QIYvW2scNiFEEIIKQF2gavaDh+7bt26kWESzeiei+2h536sXLlypEdDoyeZ3n///eriiy9Wt956q9eb8eUvf1l98pOf9H7emTD4IIQQQsqYuro6EXzk45JLLhmZKDpt2jSxTU841YqXzZs3ez0aeoilra1N7bXXXt52/XNHR4c4JpfLeQoYvW2sVEzwURcfVJGPlu6ui8nxsJ5M/iixvkqud57NwZLrsBy9DVr2LC4bHZX7m1n/rGX06bATeZbYllYkPt8AtHUw0FcgwHsisSX8GpjHCHhTxLrkRaOwvHm8x3/RKHhsVLXLfQZaZX0ZDvh+xM1Q7wKjWm6P9vvzkGqMhi5PHuuGwoOyjaRgqXHwssBy804RkcdkA5ZAlydVoUuHGxFIw/51jVAZnv8IlE1EmkP09slGaIGvQ748BliL+MrOxSXRM2C6YULZQtqtlXWTseTxVr//LzgX34RYX3XoNSLP4Vpy/+Sm/B4bg83yHMktsr7sGLYZI9QXBr1IfAQII1JNefbB+sIK7JIvhGy1vNG6Zr/8Et+d7kdDB8PkLFkOsaZcaDt2sL3A+cyARofv59b63m3Xj6TValUcnFFDJzt6/HhwXVddeuml6oEHHvBWkJ81a9Z2921ubvb+1xNNdbDxhS98wUvPmzdPdXV1eZNUDznkkJF99DDOYYcdNua8VEzwQQghhEwkHGV6n0KOHw96qOXee+9VDz74oOf1MTxHQ88TSSaH/iJavHixmjNnjjcEoyeXXnbZZeqKK65Q++23n7ddb/vc5z6nLrzwQnXHHXd4cl3dk3LGGWeMWemiYfBBCCGElADbNbxPIcePh0WLFnn/H3300eJ7HXCcd9553s/adOzaa6/1hlFmzpypvvOd73jBx2juueceL+A49thjvbkgp59+ujdHZDww+CCEEEIqABfXSwjg5ptv9j5haGWL7kEpBAYfhBBCSAlwijznYyLB4IMQQggpAW6Bq9rq48uV8s05IYQQQsoS9nwQQgghJcBWhvcp5PhypWKCj/p4WkXjQ5NttqSqxLaelPQuSEazfg/8AWnMkIhnQ3XjOdCyYxuxq6WW3ckFNCL4ygVvEPQrsFJGuJcIjg9CFm1Ie9+BH4Ul7U6UBb4eFi4pAPObklvBLyHgmoYtyyZbHQkvS/D1QD8EK+OGei70TfE/BujTEeuVeeqfCoYnQO8MqAu4hA2eHBo3BoWVgIwOgrdMDbTTXHhHZuPkbV4Gmn0nbfbt82G/NCrqS0tPlUjUDvVQSPXHwj086vzPlgv5NmPyGi48W77joV27Wbl/vEU22ky/9HAJOsYcQG8RmbTB9yPrQt1AnqrX+yf6ZaJyn/5W8A4xx+fjkalXoc9JrjrA76IG2hh4ZqCniq9NQt3EauULYDDlf06qq+Q+ff2yjdXXwksmjxcNtkGcB9GQ8J8vasn7yI06p4umPrsQxy1s3gZYHJUVHHYhhBBCSFGpmJ4PQgghZCLhFDjhtJBjSw2DD0IIIaQEOMrwPoUcX64w+CCEEEIqwOF0IlG+fTaEEEIIKUvY80EIIYSUAIdzPnZ/utNxFYkMSbqippRZNVXJpcW7Uv71zltqpUxx66CU6+KK2bm0FS4xHAiXjw4dA8u22yDfrM6GStAiG6TMzU66oUu4Ox9JkcU5ukBCaIcvRR4Bqe1Aq8xzpk4eYKUDJIi18pqZWnkOVMLhUuTJDlluuWp5vt7p4ZJGTbZGXnNgsjyHLdXZyomGS5B96r2g3lKUNaYhnzF5EhP3j8rt8aRsHzGQF3Zl4CaUUrUxWYG9KSmDzGWtUOmtC9staPdugIrRgPuKJzLyGlb4feMS7fFoLvS+O1357GpyGXkOB2XLIDE1emU7ztXL7ZE+uX1gsr/C8VlJNfp2kdeEskOZup1wQ6XbdpW/8C14h2B9xmOyLCdVD4j03nVSrp0BPXBn2l/WMXj/bk3I921Hd61IVydlQSUiMk8oVcX3eSbAQ2C0tFYTMUeVzeifizHnw63MOR8cdiGEEEJIUeGwCyGEEFIC3ALVLvr4coXBByGEEFICnApe1ZbDLoQQQggpKuz5IIQQQkqAQ7ULIYQQQoobfBgcdiGEEEIIKQYVM+ySsSPKsYdu98iWd8W2TZkakW5OSC27JvXRsYG6cKVUe29t6DLfCnw/jKwR7umhAS8B9IFAh4xEvTSXcPaWfgkOLJdtp+T5jW7/UuMRWI0aPTHMbLhXAR6P3gQq5p8wlQP7iVgf3DccMtgkM5Xa1wxdatyEos7W+XX90V55jnQLHATLeGOmzEEoKMizix4dmhzslJD5iiRlHiI+DwZZ2BHwt0hGZGXNqtniy8Lq3maRrolnQtu1AeWQjYOPS0TmYXrTVt818XlD/5F8k+oGc7LdZnIyDza0e58/ileWMp8ZrN8B8O2Jy7oxqmRdpPZGQxz/PVgJ8KvYlAhtI5FeK9yXBy4R2UOWa00MHlal1L5N0qdjUkwe87GqjSKdAkObLdlqkU6Dz0dLXPojaToz8pgtKekFUpuU77E01Cf+1qqOyTbaEIPnwGeyo1RPVpb1mq3bXlz2gPRH2pU4XNuFEEIIIcXEqeBhl4rp+SCEEEImEk4FBx+U2hJCCCGkqLDngxBCCCkBTgX3fDD4IIQQQkqAU8HBB4ddCCGEEFJU2PNBCCGElAD3I7ltIceXKxUTfEyp7lXR6rT38ytbp4ltDTHwxwhoDKgd78okRToZlZr9ZEu3SKdAq57NSc2+Bb4hGrNZNq0Y+DZ0bGiQ1+iJyxNY4I+RkR1dBvZ7BTwDNnhuRHtkOgZpByTyNR+AV0VaprNV/s631CT5XX+rzFj1Rnlfg5Pl8ZlJcN8RN9TvxEn6y97niACeC/H6tLzmJtke1CTpPRCJy7rL9vm9BCLVcEwEvCTAe6Kptl+kp1RJT4VZ1dLDIQ4GJxvTdb48VIEXiAnXxHbcOyAbyKRJ/aHtvDedyNt1PK1aPjtJS+ap35Zl15mWPhGD4OvQn5H7R8F7RJOIymvIHChlw33YW+BZg+qvqpPvlGTc77GRs2U774NyyA3Iss5V26E+IejbU1uVCn1/BHlg2JCH7py8sUlRWb8JMPrpyNaF+mlo6iIyX/vWbxLpzrT0AekYkD5MVVH5nMyq7RTpiGGHeo8Etbm9G7d53mTjGfWGKg4Oh10IIYQQQopDxfR8EEIIIRMJp4J7Phh8EEIIISXAqeDgg2oXQgghhBQV9nwQQgghJcCp4J4PBh+EEEJICXBdw78C+jiPL1cqJvjozcZUJBsPlBMiGRvXfVdqzaBcK742KqWWk6v6QiWKW9PJ0Ig1bsGS7Z5EMB66NHhds7xmz2YpSYsm5H1moboTIO1MR+Vy2d45psn7zIHkMP2elMXVvifzGB2UUj4HijZd7x/5y0nlpHJA1dj5cZm2Yel5F9IqCw8oLqmOy6cHSGUNR+YzC7JGBUusOxl5o9msFVp33jUgHxbks7VGSmk/0fChSDdH5XbHlXk2QVbZZ0PBKqUa4/KYjsFakZ5WK0Wo3THZPtI2yD1hew7yNJQvN3SfuqiUZmZhOz7POagrXHK9LpHKm4fGqoFQSeo7sWaRTsBy9TMbtop0X9YvrW4DafT7iUkincxzX80JmceujJS1ZuE91grvqKAl7zfDcvcoz260pNR2dapF5tmUZR2HctFsytSGXmNGlSy7KUnZ5gZBao1tTsE7Jgp1p6mB9/douXYu599/V+EooyCfj0KOLTWc80EIIYSQolIxPR+EEELIRMLhnA9CCCGEFBO3gud8cNiFEEIIIUWFwy6EEEJICXA47EIIIYSQYuJy2IUQQgghpDhUzLDLpPigisKy5sN0DEp/jIEATX4N+BV0gW9Hc1Lq37th6XD08UDNfsT0a8uTsHR0BvTsNXGZp/2a5NLUH/TVyzw0yjx09Mr7PnCm9I0Ioge8RxItctn21bOk/0FXP5RlCrwnUv77duLSc8G1ZNpIw1SlCPh0gB+KUSe9Blwo+6pav+9DY430ULDhmCaob/SmQF+IRCQXujS9ZkpVD5xDtte2uNxebw2KdGeuOnS587d69xDpxpi8B01TTN53HyyJ3hSXXhGpPL4eDTGZxyAiprzPyTF5jeZoX2i5YB6bE/77CnveNS1JeY0YeE+gZ8qUGbIuBm3pkZO0ZNnPqPL7XfTkZL73rtu2rLsmBedEr5BOeAfVQdnH4H2HdaeZU71epLfGZRuaFJFlaQV4ZoymK1sV6skSVN91Efn8bUxLHxAEPZKwHaNvSBTaS1C+GmLb8pDNyvfuru75cIo44XThwoXqd7/7nXrrrbdUMplURxxxhPrhD3+o9ttvv5F92tvb1VVXXaUee+wx1dvb6237zne+o04//fSRfTo7O9Wll16q/vCHPyjTNL1tv/jFL1RNjf/Z2h6ccEoIIYSUANcLIAr4jPN6S5cuVQsWLFDPPfecF1xks1l1/PHHq/7+bQHcOeeco1atWqUeeugh9dprr6nTTjtNffGLX1SvvPLKyD5nnXWWev31171zLFmyRD399NPqoosuGldeKqbngxBCCKlkHnnkEZG+6667VEtLi1q+fLk66qijvO+effZZtWjRIvXpT3/aS1933XXqlltu8fY5+OCD1Ztvvumd58UXX1Sf+tSnvH1++ctfqs9//vPqJz/5iZo6deqY8sKeD0IIIaQEOB/Zqxfy0fT09IhPOi2H4LZHd/eQdX1j47blQ/RQzL/8y794QyuO46j77rtPpVIpdfTRR3vbly1bphoaGkYCD81xxx3nDb88//zzY753Bh+EEEJICdUubgEfzfTp01V9ff3IR8/tyIcOLC6//HJ15JFHqrlz5458/6//+q/ecExTU5OKx+Pqa1/7mnrggQfUPvvsMzInRPeWjCYSiXgBjN42VjjsQgghhJQAxzWUUcCE0+HJquvWrVN1dXUj3+ugIR967sfKlSvVM888I76//vrrVVdXl3r88cdVc3Oz+v3vf+/N+fjP//xPdcABB6idBYMPQgghpIypq6sTwUc+LrnkkpGJotOmTRv5fvXq1er//J//4wUlH//40PLhn/jEJ7zA47bbblN33HGHamtrUx0dHeJ8uVzOG6bR28pi2EVPajnwwANHCm7evHnqP/7jP0a263EmHZ3p7h8t4dFyno0bN5Yyy4QQQshOwXUL/4zveq4XeOhhlCeffFLNmjVLbB8YGJLa6/kbo7Esyxum0ejf07pnRE9AHUafS28/7LDDyqPnQ0dcN998s9p33329Qrn77rvVySef7El6dNR1xRVXqD/+8Y/q/vvv98axdKFp2c9f/vKXcV+rKTqgYrEhrX2fLfXyWwaktt22/TFZOsCXYTQbHalNj1hSWx5VMp11LJGuivh9ABqSUv+egWPqonK7CRr8+VOlhr850ivSqwamiPSgI30FNNMSW0W6PS2j602gyd9jUpe8hzaZx2nJrtA8e9dISX+SzSnpHdDRK6/Z2y39DiJxqfO3IvIaUxuGJlkNs2+d9CoJyhfeZ11U+ldEYX+sm6QpvQOCtP014HeQMOR91FvSg6PbluVSa8nj7Y8mow1zQK30cenOJfPmIVkr870hLesGaU3KNtYYlR4Mfba/Oxh9GGosOVkuDn4l6F8yq3pzqNcE+j7EwGciiJ6sLJt9a+RfepaS9d0DZZm0MqGeHpoo5MOGNlEVSYf6eqA3UAN4tNRAHvap8v/hNuDEQttY1rVC0+jBMiUmn2874O9b9EzptWXZzKluD63/7hx6icC7FNpPe0CbbY3LdpobVXZpx/8u3l0cThcsWKDuvfde9eCDD6ra2tqRORr696v2/Zg9e7Y3t0PP89DKFf2Hvx52GZbUaubMmaM+97nPqQsvvNDrCdHzQ/Tv5jPOOGPMSpeSBx8nnXSSSN90001eb4jWIOvA5M477/QK6phjjvG2L1682Ltxvf3www8vUa4JIYSQ8mPRokXe/8PKlWH079bzzjtPRaNR9fDDD6trrrnG+/3c19fnBSO6Y0BLaYe55557vIDj2GOPHTEZu/XWW8tzzodt214PhzY70d06uktHR1RawjOMjspmzJjhSX0YfBBCCCln3CL3fOgRhnzokYh///d/D91HK1t0x0AhlDz40A5qOtjQ8zv0vA49FrX//vurFStWqFgs5umJR9Pa2hoq59H65tEaZ615JoQQQnZXtUs5UnKfD+0brwMNbU5y8cUXq3PPPVe98cYbO3w+rW8erXfW+mdCCCGETBxKHnzo3g09pnTIIYd4gYOW9egFarRkJ5PJeLNqR6PVLmFynmuvvdZzbRv+aP0zIYQQUulql4lEyYMPRMt19LCJDkb05JcnnnhiZJte7Gbt2rXeMM320OYqw9Ld8WqfCSGEkGLhegFEIQ6n5VtXJZ3zoXspTjjhBG8SqV66V09geeqpp9Sf/vQnb8jkggsuUFdeeaU3uUUHEXoJXx14cLIpIYQQUr6UNPjQLml6+d4NGzZ4wYY2HNOBx9///d972/VKesMyHt0bMn/+fHX77bcXfN21fZNEuiYudeHVUamP1zTGpf59ZYf0yEAGMtIzI1krtePTa+Rw0l5Vfq+JTRnpLdEN3gPItIQ8Z0tUTrbdmpN+JtMTnaF6e00NeEe0gU1D0pL3dUCd9L9ATFgEelpM5kGzR1zex+uW1I5v6qsR6cnNPaH1OQPKGr1LWmL+ScnoHTA13h3qqVFlymta4PsRN2Q5NYCfgiYGvh7okVBryGuuyzWJ9Mas9DNohmsML0K1vbrVVMN9fOA2yu3godCckD4eaVu+UqZUy3JLuf5XDvqZoPcE+oDUW+FtDH1CouDzUQ0+EUHXSDsyn63wLKGHyiTwM0lAfadifg+dfH4VXfC8z23YIK8ZkfXbnpG9vJNj0suiL+D5npv8QJ4D2hD6E3WBt4zvOYnKZ6vX8V+zM1cT+iyhxw62B/R5wecCn8Va8EsaOmd8u9eIwjttd1K7TCRKGnxoH48wEomEZ+mqP4QQQsjuhPvRp5Djy5WSS20JIYSQSsSt4J6PCTfhlBBCCCG7N+z5IIQQQkqBW7njLgw+CCGEkFLgFjbsoo8vVzjsQgghhJCiUjE9H02xXpX4SO62b52MuTampEQtY0v5mMY0ZP/W1HopIUznZFGmctHQ4/dIdocuRa2pj0hJ4dLN+4p0U7wvVC6I0rrWaHfo/vslpJQvSFqH8kCU+6H0Du8LJYgoSdU0R6REcDLc5wEtMp8zk1tCl/mOgpQT5YCbcn4jur1icgl1B+L0BlPed60p5Xz9PnlguJw0UJ7pRkPzsEdE3kdbpCu07posKQfdYkvptcaCdhqN26FtpgkkpmtTUpr7er+USc+u9rexxoisLxPyjbJky/K3GZnHcMlyVcwvpcf6aI7KNmgpec29QCL+XqY5NE9B0up89d8ajYRKULEu8PlGyWka2pOmzZLH9IMEFZ9nLEtcWwSPzwRIq1HijWWb709klIM3WrL97AHPwdqcbJOa9dlJ278cPKu7ErdAl1KajBFCCCFknMGDQbULIYQQQkgxqJhhF0IIIWRC4RqFTRot4wmnDD4IIYSQEuBW8JwPql0IIYQQUlTY80EIIYSUApcmY4QQQggpZuzhVq7apWJ6PiZF+lUyEglcYhs9OHBJbs3GtPSCOKr5HZH+MN0g0hsG5dLUNRF5zjbwv1iblsujB+n4cfnyFlgyG5ennxzxLxUfpnWvjfqXKk850hvgwKq1It1rJ0OXbZ8Mnh02PCzoK6HZlKsNLasm8GDAcorDktvTo1tCvQtwSe8gHw/06WgHbxBcYt125YhmQuVCfUE0m2y51HiDKeujw64N9YVA/wMs2wzcNy5NHrSEOg7Moh/GxxLtoZ4dWfB5SAX4PqA3BPo+4H2hfwn6W2CbrbUGQ5dTD2qnH2QaQ9sYtmP0hcE8BrVzPCcuNY9+NOj7MjO6WaRXZ1pEekZUvg/WZv1+F3/LtIp0DPKEZYftutdNhD7/WHdB94m+LPXo6+LK9lBlZkLbMb7PPxaVdaOZCe+ErlHvsf4cPAO7GldVJJzzQQghhJCiUjE9H4QQQshEwuWwCyGEEEKKG32oil3VlsMuhBBCCCkqHHYhhBBCSoLx0aeQ48sTBh+EEEJIKXA57EIIIYQQUhQqpufjgPgHqjoxNMVlwI2F6sa35qp9x+9dtUmkP121WqTfjrSJ9L7JjlAvgl5b6uM3G9LDYSgf0ivANJxQnwD0r7Bgf5/3BOjjN4F3habRkr4NneBFgedAj4aEIcu2y8GylRr/IJ0/5hv9D9CvpCrAWyAsz02G9E/RZJUsy/fSzSL9sdhGkd4C94V53mRb4X4aSqkeR3qmmDCbDH09kF5HtqkqI7wc0A/jo4uG+lnMq347NM8IejigD0jQs4DtGvNp5mlj1eCp0wPnrwX/lCAfjinQxrrBtwO9SdDPogHacFDdrcs2javd4jnfyzaHtusVqRkivW9cerJo2rPSn6gOPG/wHYKeOe25+tBynGz15PX5sKHRbYRzoicOvuf8zxr65chy07SBf0nK2far0IZ73qW4ldvzUTHBByGEEDKhcCt3VVuqXQghhBBSVNjzQQghhJQA1x36FHJ8ucLggxBCCCkFbuXO+Rj3sMurr76qfvCDH6jbb79dbd4sFzbq6elRX/3qV3dm/gghhJDde86HW8CnEoKPRx99VH36059W9913n/rhD3+oZs+erf785z+PbB8cHFR33333rsgnIYQQQnYTxjXscsMNN6hvfvOb6qabblKu66of//jH6gtf+IK6//771ec+9zk1kVmba1TJ7NDt/l3yfbEtAXJAlKgFyb1QJrc3SC8zsHS4A3FeW6QrVCapmRGXsrZ3Uy2hEmGU6qFEMQUS431jUnq3yfZLbTFfkyNSOrcFZG24JHd7riE0j0FLjffa8dAl0FHui3nCZdx98mCQPaYMv+QU89UC17DBWRDPibJHbC+YZ80eka2hEkJ/Hp3Qa+By6Ai2B03WlmW3KScl4HvGNodKULGusA2ijDo4X9FQKeZmkIRvhLrYNy6fxV6QA2MbHbqmGVrfWJ94X9jG8NnsdGsCrinP4cBfsXhNlPeuB5nsADw3KO9/ZWCmLw/7QFm9MrCnSH8sId8RA45sM3Fo9yitDSrraTH5XkMcqIt8EmSU3vqebyhnzdvZxHbf1wOOP8+7CsMd+hRyfEUEH6+//rr653/+Z+9nwzDUt771LTVt2jT13/7bf/N6Qw499NBdlU9CCCFk96KC53yMK/iIx+Oqq0v+xX7mmWcq0zTVl770JfXTn/50Z+ePEEIIIbsZ4wo+DjroIG+OxyGHHCK+P+OMM7xhmHPPPXdn548QQgjZPXEr12RsXMHHxRdfrJ5++unAbV/+8pe9AORXv/rVzsobIYQQsvviVu6wy7jULqeeeqq65ZZbtrtdD8GMVr/89re/Vf39/nUzCCGEEFK57FJ79a997Wtq40Y5m5oQQgghalvPRyGfMmWXBh96GIYQQgghpQ8+Fi5c6KlSa2trVUtLizrllFPUqlWrRra/9957npI16KMtNYZZu3atOvHEE1VVVZV3nquuukrlcv4VysOoGHv1aZGtqjo6pAd/Od0Wui8uj63ZI9oZ6vuBngzo84HLeqNmv9HyD0+hDwfq19ErwuexAD4OuJT4a6npIr1XrMOXB8eQE5qqYfnyqCW9BNrt+tBlwDOgycd00DG4xLoFS6ojWLa4rDv6Riioq6FjZBtozONP0QW+Dw1Q1jOj0h+jP8BjI8iPIKz+7Tx+CLg/lktDQJvbBB4a6DWBfhZIc6Q31Bfig2yj7xj02ZkKy9mj1wzWZxzMDtCrogaWia81/W0OQc8UBP0t8HlP5cLrMsgjZVqsM/SdsSE7KfS52Cche5lNeE7Q9yPIUwN9PfBdmIDnH5+lARc9efzlYII/DV6jy67O44eC71orvM3m/G0W2/7oa6Dny+7E0qVL1YIFC7wARAcL3/72t9Xxxx+v3njjDVVdXa2mT5+uNmzYII75p3/6J8/T64QTTvDStm17gUdbW5t69tlnvf3POeccFY1G1T/8wz+MOS8VE3wQQgghlax2eeSRR0T6rrvu8nouli9fro466ihlWZYXVIzmgQceUF/84hdVTU3NiNO5DlYef/xx1dra6qlgb7zxRnX11Vd7RqSxmP8PqyB23xCPEEIImcAYbuGfQujuHuoNa2z090hqdFCyYsUKdcEFF4x8t2zZMnXAAQd4gccw8+fP99Z200akY4U9H4QQQkgZS217enp8hqD6E4bjOOryyy9XRx55pJo7d27gPnfeeaeaM2eOOuKII0a+a29vF4GHZjitt42VXdrzseeee3rjQIQQQgjZNUyfPl3V19ePfPTE0nzouR8rV670lkYJQi8Ue++994pej51JwT0ffX19XgQ1mrq6oUlr+sYIIYQQsutYt27dyO9dTb5ej0suuUQtWbLEMw3V67MF8W//9m9qYGDAm0w6Gj0n5IUXXhDfDVtq4HyRnd7zsWbNGm+2q54dq6OsSZMmeZ+Ghgbvf0IIIYSEYxQ67+Oj8+jAY/Rne8GHtr/QgYeeRPrkk0+qWbNmbTdveshFr1o/efJk8f28efPUa6+9pjo6tqkjH3vsMe+6+++//67t+fjKV77i3cSvf/1rb6xHa4AJIYQQMnFZsGCBN5Ty4IMPel4fw3M0dCdCMpkc2e+dd97xekUefvhh3zm0NFcHGWeffbb60Y9+5J3juuuu886dr8el4ODj1Vdf9WbB7rfffqpc6HaqVNaxAvXxSJUpteyaLXZtqL4dPRXezzSLNF4TvSxqLekLEeyZ0R/qydAU6ZN5BBkW6uUxD12O9B3w9jHlPh1QDkiLJX0e1oKvA2rwgzw78vl0pMBTA8+JvgFBfhajmQp+KZpN4DWAfhU94D2RBa+BTUp6C5gwq2xmgOfCivQUkW6y+kLrp8nsD62rAfASiUE5ojfFUD5lfdRb6dCyxbpCsO6Cnj30p2nPyXbfkZHt/GPJ9tBz4n1iHlen5WS5IH8SbFPY5moj0jukI1sXmqcgj42UK6+xPtsQ6mfhyzM8v43QXhBsox7wd2NUhRtF4X1tgvueHZceER2u/30Rg3P02tt+6QWB79aYKfPYAe/BLLTrtoi/7PFZGv0eyucjVM5S20WLFnn/H3300eL7xYsXq/POO28krTsW9HCMDjQQLcfVQzZ6rTfdC6JHQPSist///vfHlZcdCj60QYkeYyqn4IMQQgip5IXl3DG6jmuzsDDDMC0mCeoV2eXBx//9v/9Xff3rX1cffvihJ9FBRcuBBx5YUKYIIYQQsvuyQ8HHpk2b1OrVq9X5558/8p2e96GjKv2/tl8lhBBCyMTp+Sj74OOrX/2qOvjgg9Vvf/tbTjglhBBCdgCjQJfSQh1Oyy74eP/999VDDz2k9tlnn52fI0IIIYTs1uyQz8cxxxzjKV4IIYQQUuCwi1vAp5J6Pk466SR1xRVXeEYjeoEZnHCqjUkmGhtzdSqZjQQuRW9B31UTyEU1Lw/OFOlOszpUSlcLy3ijVG89LI/dHCD3bIHvUBo5PboldDlrvK9eOz4uad7QOaTsbFiuvL0l1nvNZKiMsg7KBaWYmlqVCpXF4Tlwme9GkByjxBile+05vxwQ9+l0akJlzFjWtabM49sZ6fy3R0B97xHZGrrUfL8j51KZWL8g/62GckGCZLIopU2BXNcGeSjKJrE+sRxRDj50Tvk3UBcsNd+QlJLSqb5yAslqbtK433oor/dJhC2QmEKbQakukk9OqunLyfprjoJ0HnSx2M7xWZwdk7LXVnhuNKvgPYTvGF/9gWwd5eDYZoP4EOoH21wGJMF4X9he8B2F79q3slLCHiSlHi0hTrlFnLPocs7HuNBKF02QrpcTTgkhhJD8GJzzMT5wLRdCCCGEkKItLEcIIYSQie9wWpbBx6233qouuugilUgkvJ/D+MY3vrEz8kYIIYTsvric85GXW265RZ111lle8KF/3h56zgeDD0IIIYQU3POxZs2awJ8JIYQQMn4MTjjNz5VXXjm2wjQM9dOf/pTtkBBCCAnD5bBLXl555RWRfvnll1UulxtZ2fZvf/ubt9TuIYccMiEb21a7Sg3aQ/pvG7zVUKtuG/5JPOtScmn4z9S9LdIfZJpCl6/GdDzPMvBBXhGoof8wJ5fgrjLkORzwBZgd6w71iWgKyMN7sFw1LqmNecSynR7bErrcNWryvXMYZuhS8eg1sC7XGJqHPSx531vANySr/EuNxxTe56BI9zrSt6ENroHeIvvH14v0h1CuQfe1CfwN2sB/pgd8OmqNPP4oUFdBy9tjWaNnBt431l8D3AO2D/SiCTrHjGinSPeD1wjWF3rm4H2hh0eQvw16nuB9Yv1jO8bnG/PQHeDzgfedg3NshOXq0468ZiOUdactvYdMWBq+w5J1G+TjgaB3DDIDlqvPwLOXgnIK8lBpNPtCvWNWZ1vgeDvUcwefvW7wjdFk3fR2zzno5PcqIYUz5mGXP//5zyM//+xnP1O1tbXq7rvvVpMmDT34W7du9Raa+7u/+7udkC1CCCFkN8ctcH0Wt8Ls1fWwysKFC0cCD43++Qc/+AGHXAghhJCx4FauvfoOBR89PT1q06ZNvu/1d7294TbDhBBCCKlsdij4OPXUU70hlt/97nfqgw8+8D7//u//ri644AJ12mmn7fxcEkIIIbsbbuX2fOyQw+kdd9yhvvnNb6ozzzxTZbNDE5YikYgXfPz4xz/e2XkkhBBCdjsMSm3HR1VVlbr99tu9QGP16tXed3vvvbeqrpazrQkhhBBCdsqwyzA62DjwwAO9z44EHnrS6qGHHuopZ1paWtQpp5yiVq1aJfZJpVJqwYIFqqmpSdXU1KjTTz9dbdy4sZBsE0IIIaRSF5ZbunSpF1joAER7hnz7299Wxx9/vHrjjTdGgpkrrrhC/fGPf1T333+/qq+vV5dccok3r+Qvf/nLuK7luKb3CdLc99lSy14foIdvi/WI9OsDe4j0HvGtIj2Qi4V6DbRG0RdC7h/kJYDEQC+P+nbcnnHD/RHWg/dIkCbfMpzQ7SbEs9WGvO8M+GeklN/nAz1P8L62gJ8B+gTUwfEd4JexR0TWZT+cP4guO9zXwwZPlQbwhag1ZTkNOLG8ngrYJtCfJApl6UDZd4G/wQB4WWDdeXlwE6H72JCnyVCW6BOCeVqf83suoIcGttN8zwF6VaDPC97DpgCPFfTpwDaH9ZUGj44BOxbqoTMYUN/ow7EpLdvptIR8pzjgZ/FK354ivV/VBpFe1ruPSO+V9IsE9op1iPT0qLzme9mmUA+Ofqgb9B7amKv3XXMqXKPdlvsMv6eH+SAjfXxqLOkdY0E5Yjvvhfd7oFfIqDaTLuZibS5NxkrCI488ItJ33XWX1wOyfPlyddRRR6nu7m515513qnvvvVcdc8wx3j6LFy9Wc+bMUc8995w6/PDDS5NxQgghpECMCp7zUdCwy85GBxuaxsahSFcHIXpC63HHHTeyz+zZs9WMGTPUsmXLSpZPQgghhJTpsMtoHMdRl19+uTryyCPV3Llzve/a29tVLBZTDQ2yK6+1tdXbFkQ6nfY+oz1JCCGEkAmJqyqSCdPzoed+rFy5Ut13330FnUdPYtVzQ4Y/06dP32l5JIQQQnYabuX6fEyI4ENPIl2yZIm3fsy0adNGvm9ra1OZTEZ1dXWJ/bXaRW8L4tprr/WGb4Y/69at2+X5J4QQQkiZBB+u63qBxwMPPKCefPJJNWvWLLFdr5AbjUbVE088MfKdluKuXbtWzZs3L/Cc8Xhc1dXViQ8hhBAyUSecGgV8ypVIqYdatJLlwQcf9Lw+hudx6OGSZDLp/a9dU6+88kpvEqoOJC699FIv8KDShRBCSFnjUmpbEhYtWuT9f/TRR4vvtZz2vPPO836+5ZZblGmanrmYnkg6f/58z111vHi6/I+0+Qnws6gC3fhbg1MCjpdxWndW+j7UROQ5kMaI9KJIgU/A9OiWvP4FXY685ha7NtQfIwMafEyjl0GVksd718jVhF6jF/LUAB4pXeBNYYOGvtb0lxv6MDRY/aFeI+g9gH4YMyLSk6MLfADQkyHIO6DF6gs9ps2S6U5bplPgXdADnh5BnhgJ8EjZAn4l2D5aLLmo48diG0P9FNCTQ/NhblJoOWAawTaF3iWNUI5BYJvB+kXQx6ML2g96OmAeNVtz1aFeEujrgd4R+E5BBmz/NS0YsG+IDoR6i0RNWfamkve9OtUSet8dmbq8ecD7wDbWAe8c9CrBNozvnKB3Sh2UtWz12iMpHdoGu8HPBj1b6iP+dr45K/MQH+XDk8qF1yXZDXo+9LBLPhKJhLrtttu8DyGEELK7YFSwz8eEkdoSQgghFYXLYRdCCCGEMPioHKktIYQQQioHDrsQQgghJcDgnA9CCCGEFBWXcz4qijjIyYKWN0dmJqQUtsOSsrU1A5NFempCurKuz8r1aarMTF45IS7T3g8SUZScoTQTZW5tsPw5sj4rZZZB0tlOkHuiVLYrj+zNBGkfnk8TB3kfLi1vgiT4Q1uWbRtIa+thcNEGMd8WW8qFNYlR0ruhYySOI+smasn9m0F6u9kOlyRqXuqXJnuz4ptC5dgoOe6HcsKyrzLSoftrmkCui2X/XqZ5XHlCsH0ElUWtORgqz0Y5aJddF3rfuD8u2a5pjsj77gQ5qAPP4rRYp0hvzPqXjh9NleWXb0bgmcfneUsW5L8R2W5nJjaL9Jv9U0W6JyflwFMS8rkIKqu9I/K+TFhd/o1MS6gsGmXPM2OyDWt64XnD99qmXLiFgF86DWULTXDAludHaa2vjcA2smvgsAshhBBSClz2fBBCCCGkiBgVPOeDahdCCCGEFBUOuxBCCCGlwOWwCyGEEEKKiMFhF0IIIYSQ4sA5H4QQQkgph13cAj7jYOHCherQQw9VtbW1qqWlRZ1yyilq1apVvv2WLVumjjnmGFVdXa3q6urUUUcdpQYHt0ngOzs71VlnneVta2hoUBdccIHq68u/YnVFzvnos+MqG7CstabKkjrySWNYgvnk+uUi/WD3ISK9ZkD6IexdJfXubaB/x6WpgzT06MOBXiHob4F6+C7Q16N+Hj09gjT3eE3U6G+E5cxx/z5Yihy3e99F5XcpWM48Y4CPA/iCRMFrYsC3erI0L8gqeT5NkyG9Jj6E+9oDPFPqTOnJ8FpGHt/l1IT6ZWgaI/2hnhkrU9NFep94e+hS8egbge0jG7Dc+Ye5SaG+DfvCNdvBv8axZdna4KnRGPG/oNBTA+vThHJ4LzM5dH9sUz6/E2j3QWWNy7Bvzcn63ZCR910TkX43feCx0Zfze6rsldwc6j+EPh9r+ptEehK0l4aozHNXVj7vHw7KPGvScdkGZkZkvj+0B0PbA3p2NFqyfjeBB0vQOTKOFerDgu0D6c4lQ32bcnB+TVOsb7vvmFS4VU1Zz/lYunSpWrBggReA5HI59e1vf1sdf/zx6o033vACjeHA43Of+5y69tpr1S9/+UsViUTUq6++qkxzW73owGPDhg3qscceU9lsVp1//vnqoosuUvfee++Y81IxwQchhBAykTB8fwqN//jx8Mgjj4j0XXfd5fWALF++3Ovd0FxxxRXqG9/4hrrmmmtG9ttvv/1Gfn7zzTe987z44ovqU5/6lPedDlI+//nPq5/85Cdq6lRpdrc9OOxCCCGElDE9PT3ik077e/eC6O4e6g1tbGz0/u/o6FDPP/+8F5AcccQRqrW1VX32s59VzzzzzMgxumdED7UMBx6a4447zusZ0ceOFQYfhBBCSBnP+Zg+fbqqr68f+ei5HflwHEddfvnl6sgjj1Rz5871vnv33Xe9/2+44QZ14YUXej0cn/zkJ9Wxxx6r3n77bW9be3u7F5yMRg/N6ABGbxsrHHYhhBBCylhqu27dOm/y5zDxuH89G0TP/Vi5cqXo1dABieZrX/uaN49Dc/DBB6snnnhC/frXvx5TUDNWGHwQQgghZUxdXZ0IPvJxySWXqCVLlqinn35aTZs2beT7KVOmeP/vv//+Yv85c+aotWvXej+3tbV5wzOj0ZNXtQJGbxsrHHYhhBBCKkBq67quF3g88MAD6sknn1SzZsmVtGfOnOlNGEX57d/+9je15557ej/PmzdPdXV1eZNUh9Hn0r0mhx122Jjzwp4PQgghpFS4xbuUHmrRctgHH3zQ8/oYnqOh54kkk0llGIa66qqr1Pe+9z31iU98Qh100EHq7rvvVm+99Zb6t3/7t5FeEC3F1XNC7rjjDk9qqwOaM844Y8xKl4oNPtJOJNQHAP0WNM3RXpEecKWWfN+EnGizR0yOub07ODn0fKiPD/Jp2Csuu7qQtdmhGcvDxMDnw4aOLrzvXvDg0JgwIIk+HgnQ7E+OyPt6Py39LFqj0mui1/Ffcz14RyDoX1JrSo+FTZDHJvCWsF0pUGvP+a/3FqQPSbwn0lOt8E7DDHiHZKCssZw0neAlUWuCx0JU3jcyAJ4rM6ObQ8saPVqC2gR6rKDXDN6H31MDPTf8rxz0l1kH7RivEYM2h9fE8+F9d9vSP0djg2gR2zV6aqCXBN5n2pTl1hCVdRlY31Yq1O/ChN9Sawbls9UPXiIDkE5Y8p40U2Lyefw9eInsBX5E6NuC3jLvZ5vz+vhgG/owDf5FVibUxwPPiR4rAxl531nIs6YjI/2LakZ5PaXt3XdAYNGiRd7/Rx99tPh+8eLF6rzzzvN+1pNQU6mUJ7nVQyk6CNF+HnvvvffI/vfcc48XcOiJqFrlcvrpp6tbb711XHmpyOCDEEIIqbS1XVyf4WIw2uNjtM8HopUt4zEUC4LBByGEEFIK3Mpd1Xb37V8ihBBCyISEPR+EEEJIBQy7TCQYfBBCCCGlwK3cYRcGH4QQQkgJMNjzsfsTN3Mqbg7J6db0SznY9KqtoVLOIDnYock18hhYahyX/Z4c6w2V0Xba4ctGj2V5e5S9OSApRSkmygWbA+Sf3bBkNi4V7oAsbm26MTQP+WTP3jEwFSlth8fIKJPUdR1WnygfDqI/J8sqDZLTTHJoDYRhpkPZ1RkyT+tdKeetDljWPWtZofneBGXvk4iDXHuLLaWcvY6syyBwuXmULeNS8pjHzVloo6MkjNuTXvrvQ8pau0AaixJUbKOrU3LdiUnR/tDjgzBN+Xx2ZOpC5Z1Yn1ZEHr8Znl3NByn5TnGiss3URUF6C+08C0vF92Vlm52S7Ak9n+a13j1E+hMt74t0Cp5Py3BCpbetke7Q9hPUhlBai+8hlHu/NyjlwI1RKa2OmLJNRiHPmo60rI9+a1vZZdL+Nkp2Puz5IIQQQkqBy2EXQgghhDD4KAqU2hJCCCGkqHDYhRBCCCkBBiecEkIIIaSouJU754PDLoQQQggpKhx2IYQQQkqA4brep5Djy5WKCT7eH2hSUSMW6D3xSqfUuvfU+pd5n5aQXiAvDs4S6SbwWEBNPi6X/UFG+mFsNOp918RzoKa+z5a6frwv9DPApaXrwKtgRWqGLw+4FDguR49eIZgH9NQI8vVA+mAp8Hz3EaTjH00HeE9Ug69AkP+BCefckJH188/9R4r0QbVrRfrYqlUi3WAOhHpwBNXvXwdlfdRAG9qYlnlaoyaH+iNgG2yNSk8GzfuZ5ryeN6NZ0TsjtNywjfWAV4mmOyt9OvZIynyZ4ImzISXvuxb9MKANdmb9ZY3U4rLsNnjgRPtC/VD6wUOnz/bfJ1KNHijQLt/snSLSCSsr0jloL5jeM7kF8uz3r5gZ3yTSPeAFs8WtDX3ee8FjBbcHgfWDfiX4bsT6joGPT2e2KrQug9452E5LhsthF0IIIYSQolAxPR+EEELIRMKg2oUQQgghRcWt3GEX9nwQQgghJcCo4J4PSm0JIYQQUlTY80EIIYSUApfDLoQQQggpIkYFD7tUTM/H1nRCRSLxQO+JrpTUqv9n116+42e3dIh0U7xfpKfEpTeBBTryuJkN9U/oAb289102EapXR718xJTn7MpI/ftATvo+NMal90QMjtes6W8S6ZqI9CZIgjfBIPgjoPcAemwEafDjoOPHffCcHwzWyDxG5TUiUBd92Xiob4h3DfAeSNmy7GKWzOMfNx4g0h82TBLpY+tel+dz/X4Im3PSU6EjI9NbTelX8Xav9PXAdt0Qk/X7vi39E9ZFZR6DzoHlgGzJVId6MPSCr0dnWrZJTQLKclVPi0inbFn/WVvmyTKd0HvoTcs8VMdkG9b0pOQ+U2t7RHpOXXvo84tt1Pd85/zPdx88K+iB0pmWx6Rt2R5qo/I+co5sxyu6p4v0x+vW+/Kwd3xjqDfM64PTRHrdoGwzM8FLpD4ifYE6c36PFayfLeDD0p+Tz2cc2sdWeK8hH/ZJX5CmpHwONH3Z7fuR5FL+9kF2PhUTfBBCCCETCpfDLoQQQggpMkYZD50UAtUuhBBCCCkqHHYhhBBCSoHrDn0KOb5MYfBBCCGElACjgtUuHHYhhBBCSFGpmJ6PnlRSWdaQhKuzR0q1GuukFCub9csLt6TkMW9ubA29XlOtlOJOq5FS3HpYqn5TWspFg2WtUq77WueUUEnaQIicTNM5UBW6tLWmOiZlq7YjZWyJCEiILTtUFonXQMmqxgUJMUrlMnBOlBz75KIgpY2ANHNtn19yinLdnoyU/yHt3XUivWWwOnS59KCl6juy8hxZV97n2r7G0PvMJ5PNgBx0TZ+UUQeV5ZYBuI9YuIy5Pp4KlW/j+TWboKzwPrAN5WxZn201vSLdC3WF10yB5FxjQLZWfiifrQ9660Pb6Kdb3xfpTWkpi62D512zpqcp9HlN56Cdg5Q2FZP30TMgpboD8A6anJDlpNkKy9F3Qboa3kEmLCbytz75HpwE8u4gKT3WR3O8L/SYQZC5bxmUeUxG5TuoGp7dNV3yudEY+OyMui97MFxevlNxqXYhhBBCSBExnKFPIceXKxXT80EIIYRMKNzK7fngnA9CCCGEFBX2fBBCCCElwKhgtQuDD0IIIaQUuJXr88FhF0IIIYQUFfZ8EEIIISXA4LDL7k9vKqYsc0j/b4OPB/pG1FT7l1Tu6Ja6/VwuvNOoa0Auh22DRr86VhW6rLjm3QG/D8NoBjJS/542ZSzZNwBLx6fldsOUXXZOxq9v74J9TAu0XeCPEIvL+6hNSt+HnkHpRRBEMp4N9T+wwSegFpZIR9+IvnQ81OcD/RS8ayRlfXWlZH02VfWH+j5gm/rDBweI9NFT3vZd01QyX93ZZKjXyFbwO5hcLf0S2vtlmx3MyvYSA08W77tILtRToxvadX3VYKgfRiN4tKRt/987+GxkII1LxWP9rVw7VaSrasKXRI8E3PfAoCzb+lp5Xw2JwdCyR8+UPqirHLQnjQMPD/agW/DsDcLz29crn6VYUj43m3vlc/B4z36+PEyuk22mCjwyplV3iXRjTLb7npzMQ100lbe++2z5PPfnZFl1pmW+u9PyGj150tg+BtNBvi7udp9fO1XEoQyXahdCCCGEkKLAYRdCCCGkBBgcdiGEEEJIUXGpdiGEEEIIKQocdiGEEEJKgFHBwy70+SCEEEJKgbsTPuNg4cKF6tBDD1W1tbWqpaVFnXLKKWrVqlVin6OPPloZhiE+X//618U+a9euVSeeeKKqqqryznPVVVepXM6v2JywwcfTTz+tTjrpJDV16lTvBn//+9+L7a7rqu9+97tqypQpKplMquOOO069/bZfokgIIYSUa8+HUcBnPCxdulQtWLBAPffcc+qxxx5T2WxWHX/88aq/X0qoL7zwQrVhw4aRz49+9KORbbZte4FHJpNRzz77rLr77rvVXXfd5f2uLpthF33Dn/jEJ9RXv/pVddppp/m26xu+9dZbvZubNWuWuv7669X8+fPVG2+8oRKJ/H4Ro0n1JZRpf3SMLfX1W9ulH4IR869TbEbgO6h0B/wQ0PdhcEBq212nTl4TNP3eObPgb5CQkaUNvhxOWqYNuE/lhvsKGFnYX+8Tl/dtWzJPhiVPkoZrDnZKXwgjJ7dbk/yeDFvhmHiD9A5ABlKybLPg4xIH75HuQbl/PCG9DTQbuutCvUe2DEgvgoZq6QPRvqFBnhDK6RlrL981967bItIxU+a7BjwYNvXVyGv21ob6RGTBzwT9EIL8aSbX9IX6NgxkZFk62O5z0dDnIsh/BPOJ9ZkZlPtbUfB16Jd5isSkr4cNz6omB89S30eeQNvL0wC0IWyD6QGZx40xWTdBfkIm/CZBfwrXMULvC99B+E7Bewyq72wcyh78atBTB/2JOgZr83rJbAXPHPTc+HBrQ2i5YHvAconC847tJeiY0e9zZ7CMxzLy8Mgjj4i0Dhp0z8Xy5cvVUUcdNfK97tFoa2sLPMejjz7q/Q5+/PHHVWtrqzrooIPUjTfeqK6++mp1ww03qFhMPgsTsufjhBNOUD/4wQ/Uqaee6tumez1+/vOfq+uuu06dfPLJ6sADD1T/7//9P7V+/XpfDwkhhBBSdjhu4Z8C6O7u9v5vbGwU399zzz2qublZzZ07V1177bVqYGCbWeCyZcvUAQcc4AUew+hOgZ6eHvX666+X/4TTNWvWqPb2dm+oZZj6+np12GGHeTd/xhlnlDR/hBBCyERwOO3p6RFfx+Nx7xOG4zjq8ssvV0ceeaQXZAxz5plnqj333NObDvHXv/7V69HQ80J+97vfedv17+XRgYdmOK23lX3wMXwTQTcZdoPpdNr7DIOVQgghhOxOTJ8+XaS/973veUMgYei5HytXrlTPPPOM+P6iiy4a+Vn3cOg5l8cee6xavXq12nvvvXdanids8LGj6Nm8//t//+9SZ4MQQggJxShQLjs8c2XdunWqrm7bPLV8vR6XXHKJWrJkiSf6mDZtWui+erRB884773jBh54L8sILL4h9Nm7c6P2/vXkiZSW1Hb6J4ZsaRqfDblCPT+lxrOGPrhRCCCFkwjqcugV89IJ+dXXis73gQ8+l1IHHAw88oJ588klPyJGPFStWeP/rHhDNvHnz1GuvvaY6OjpG9tHKGX3d/fffv/yDD10oOsh44oknxBDK888/79389tCFjhVBCCGEVDoLFixQv/nNb9S9997reX3oKQz6Mzg4pNbTQytauaLVL++995566KGH1DnnnOMpYbToQ6OluTrIOPvss9Wrr76q/vSnP3nCEH3ufD0uE2bYpa+vz+vKGT3JVEdZeubtjBkzvMkwWg2z7777jkht9SQYbYxCCCGElDNGkR1OFy1aNGIkNprFixer8847z5PJagmtVppqKww9l+T000/3gothLMvyhmwuvvhiryOgurpanXvuuer73//+uPJS0uDjpZdeUv/lv/yXkfSVV17p/a9vROuPv/Wtb3kFoCfAdHV1qc985jOeTnm8Hh8aN2soN/LRCBl4DZiDoI/vCyiWllSopt7slNpmOwEeCmgTEnVD/S+8fSKg00+BTh8OMTLQkQXHqwAvEXE9fxaUypdP2O6i1wD4W5jV2VCvkqBjfDr9EI2+dzz4PvTD8Sb4Iwz0+ttTBLwCBpxw7fqAioWWNXoZdPZV+a9pOKGeCn1ZeY0cljUUS8ySviCDtiyHTVv83hN19dskdZoP8nguxKKynAbB7wLJgEeDJgWeGXa/zKcB9RWDNpSB/fG5yKbheYf25Z0zmQ31gUBfD3tQviNs8NiJJmW5ZPv85dKTAx+fuLzPDBxjRu1QzxT09UDfHwW+QZr+HvDIaZZ+NdXgw5Nx5Dk/7K0X6UlJ2X7WdYPfjVKqtzsZeh8W1HcG7gM9lxwox3QG/voOep9X+f1HRgjwgZnoapexooddwtDBhjYiy4dWwzz88MOqEEoafOjoK6wwtOupjqbGG1ERQgghZOKy26ldCCGEkHLAcF3vU8jx5QqDD0IIIaQUOP4h+XEfX6Yw+CCEEEJKgFHBPR8TVmpLCCGEkN0T9nwQQgghpcAtrtplIlExwYc5aClTDUm23JgcKDNAVuck/TIsawPIMRulNM+pz4bK/VyQuRlpmbZADqixk+Mc0ENpbR5cK7/cV8F3KMc1BkDOlwA5INwCShQNO0BijDLVgci47kNhniDpWlD/ARJklFLjfZhwDkwnajKhS6ynYbl0zRazWqS7YOnxfD2slumELnePS8n7lhXX1+yQ8ttIVS5UGp2C+8ZzZlORvGXtbpHSSAufx6xMZ6A9GLDdrZFt0KqWdWGBVFOTy7NMu4vtFO8DyjbbCe8LkN5qnHQkXDIM2PAOMeGcDt4DSFCDlPQulEUG8v1uV2vou9Kqk2W7Zb2U3hrwrg3ChXeM3S/LxQXpvE9CjHXjjuG9iPYKo85hDPrl4LsMd5tL6Q4fX6Zw2IUQQgghRaViej4IIYSQSnY4nUgw+CCEEEJKgcthF0IIIYSQosCeD0IIIaQEGM7Qp5DjyxUGH4QQQkgpcDnsQgghhBBSFCqz5wP08k7cCfXg0NhxWBq+W2rynYQTOg3ZwO3oTRBQE2YG8gnnQI2+L994G3BNC/wwHNDTD30JGnrcB68B5zRQMg/Lhvs0+p5vgxnqHWL0QmEl3PDlsoMMDkbhBviIRBtTch8oBxs8FGKxXKj8PprA7f5MZXKysOLQhjJZ8IUAn4hEMhO6VH2QrweCvivoy4L+Fk4KljuH+nfrwd9iq7+sXahuAy0xIN++rma8LchDLi29K+wa8OTxygY8NDrl8+3Cs2dmwIskBs87Pt9mgHcE7gPX8D0H+fwudkD54LsGgO8Ia1DmOZcyw589N7+vR9D7Fj2axP75fD2gjQY8aj6ViDPKC8TnG7QrcWkyRgghhJAiYlTw2i6V2fNBCCGElBqXcz4IIYQQQooCez4IIYSQUuDqCScFHl+mMPgghBBCSoBRwXM+uLAcIYQQQooKez4IIYSQUuAG6PLHe3yZUjHBh9Z6D+u90SfArZHadFfaAgx9B9ry0brwQO06+EAo1MOD9twJsAEwbfAOAG8BMxUJ1eBna8GbACT4eA/oK+Ltk8zvgSLOAb4BxgBs3xpwo3lwImaoJ4rPvgLK3gBvEbc7FurZoMmmquQ54CIueAn0W7Hw/etyoe3FOwZ8HNJOXKQjyWzoi2egOxm6XaEvxBhw0TJlEMrWDPfs8PllBHkoQFniw2HmwsvehXZsYNlC2uyQ5RqUbzynic8v5hiqxlf/Ac8W3rcB+/jvA/wuoNjcKHyBdRfgqeOCPwnWjwMTElwL7isG7wfwffH5AunbhncIlj2+x1zZhHz1j+9SvM8gC3IH8i3Okd8OZ+fhUu1CCCGEEFIUKqbngxBCCJlQOAX2tHBhOUIIIYSMB6OC1S7s+SCEEEJKgcs5H4QQQgghRYE9H4QQQkgpcCu356Nigg8zayjzI5mYARI0c72USQZhJ8Mlaaj+8i/jDPIvbDMBy53jNSL9UpOW2Cz3j/XIdC4h98/UwyVBwhYZ9GVBZepgiXRUa8KNJDfI+4j1yt2TnbLwzZz/4cGys+PmuKS4TkSeINUkm3m0D64XoEBFuZ6VlvmM9kP9Q5sabJLHD06WbSwHqlhNthalz7C8fVUkXLaaR3LoxmF2WoD807cPNmyQWqoE3Dgswe7E4fggxSnIWFEy6rtPfFZgO8ri8VmzayHPmgDpszhntTzG6LdCyy3gcfZhZLGhG+H3jVWBeYiMU4Ic8LvLwVdhnjyZUN/YBl1nDG0ML2niSUJ3zyvFDprQ6ZPjjj5HHjuBnYpbucEHHU4JIYQQUlQqpueDEEIImVA4lNoSQgghpIgYFSy15bALIYQQQooKh10IIYSQUuBW7oRTBh+EEEJIKXDcAOnjOI8vUzjsQgghhJCiUjE9H9EeQ1kf+SbEuuU2K6VCPR2ClpJGL4l0Q/j10UMj1guafNS2K6VqNkhvgRx6JgDomYF5Nt+T23unyeq3/SuNqxrwDokMynNUr8e1xMHnY6ssXHMgE7q/l+9YJLRr0QVfDzcivQbspEzXrbFDjw/668FOyHNYKTt0O5Z1vDu8vdSu9XsdZKtlvrI1kE3Id6oJ8lwFy5uD2YTVZYX6vHjfYTuEtAHeIyoTCfWSMHMqr9cEeoE44POBS7D7lkgftELLwefjEmTCgcu690EbA08U9JJAbxoDvSLQeyLgvtCPArc7sXD/E1/ZuvnLHj2PHPAG8f1Vjo8O1JXvPhP+du7zYcnl8W3Bohw0w8s+Txv09oE8jL4PX/valbgcdiGEEEJIUXELnLdRvsMuFdPzQQghhEwo3Mrt+eCcD0IIIYQUFfZ8EEIIIaXA0T0Xlal2YfBBCCGElALXGfoUcnyZwmEXQgghpAJYuHChOvTQQ1Vtba1qaWlRp5xyilq1alXgvq7rqhNOOEEZhqF+//vfi21r165VJ554oqqqqvLOc9VVV6lcLkBWFAKDD0IIIaSUE07dAj7jYOnSpWrBggXqueeeU4899pjKZrPq+OOPV/39/b59f/7zn3uBB2Lbthd4ZDIZ9eyzz6q7775b3XXXXeq73/3uuPJSMcMuDe84KhId6qJywJMhsUVGbNkaNAbQ2m/ZvRXvlsfgOdG/It0gz5nskP4YTjTA7yIqY8NEpzwmVx0J1cMb4PuB6Xg3+kL4suDzPDFgjDHaI/Nk5ORJrHUb5Qkj0OQsf/xrRKUBhZuMibS5bos8oCoptzfVyfNlZF0ZfWDsEg8wvID6s+sSMp2U92FloCxtWU6RfvAegO1eNrbKfbK1ss2kJpmhHg12Anwh4LZysphUpM+XBZUL8JsR18waoR4aLvhEGKn8f99Yg+BHMgAnzXcKKMp4J/q+4PUCzgG37UAzRa8I9OUxs2aoZ060fzsrmo5OBjRDsT2O3hRyuy0fE7+/ScAvKhPq0xqANgaHRAag/tH2ZRI8B13+XzFWCp6tJL5jwtsH+p3g+aw0HD/gy4Kq2rT991ou66r31O455+ORRx4RaR006J6L5cuXq6OOOmrk+xUrVqif/vSn6qWXXlJTpkwRxzz66KPqjTfeUI8//rhqbW1VBx10kLrxxhvV1VdfrW644QYVi0FD3A7s+SCEEELKmJ6eHvFJpyEC2w7d3UOOm42NjSPfDQwMqDPPPFPddtttqq2tzXfMsmXL1AEHHOAFHsPMnz/fu+7rr78+5jwz+CCEEELKeNhl+vTpqr6+fuSj53bkw3Ecdfnll6sjjzxSzZ07d+T7K664Qh1xxBHq5JNPDjyuvb1dBB6a4bTeNlYqZtiFEEIImVC4BRqFfXTounXrVF3dtuHmeDxgrQxAz/1YuXKleuaZZ0a+e+ihh9STTz6pXnnlFbWrYc8HIYQQUsbU1dWJT77g45JLLlFLlixRf/7zn9W0adNGvteBx+rVq1VDQ4OKRCLeR3P66aero48+2vtZD8Vs3Cjn8g2ng4ZptgeDD0IIIaQC1C6u63qBxwMPPOAFGrNmzRLbr7nmGvXXv/7Vm3A6/NHccsstavHixd7P8+bNU6+99prq6OgYOU4rZ3TQs//++485Lxx2IYQQQkqBo6U9BRiFgQpzLEMt9957r3rwwQc9r4/hORp6nkgymfR6LoJ6L2bMmDESqGhprg4yzj77bPWjH/3IO8d1113nnXsswz2VF3yMUjRZGYgWrfwyyMiAEypbxS4kw5b7R0FqibJIF5Zc947plfo+qw+WrwfsmBm6DLyZlXlKvi81h5m2Wt85UVrrwDUiqz+UB5i4NjksPd/TK8/f0uy7Zm71GnmKA+fIPEyXk50U1JfZI+/LTYB0t0bKZt1YJK/UNlct5WPRnkzo/vYkuX+kV9ZdutEvR0O5dbpOlmUGqsfKhr+Hon3h0stctb+dW/3hy7yjKtAnQYVywGtaoHIeyodMx4Ym4G87hxmejoB0FuWfic3heQ6SLUdS4cu6owQ9W22GHo/S66B9UEqN8v0slFOuBs4H9W3CsvEotQ6qn2gvWgaoUAkxymLjW8Ll3oEy5ky4VBbrBuXBCLaxaIDUNlsjzxHvckMtB3aXheUWLVrk/T88hDKM7tU477zzxnQOy7K8IZuLL77Y6wWprq5W5557rvr+978/rrxUTvBBCCGEVDDuDgQ6Qcfsueee6uGHHy4oLww+CCGEkFLgFrfnYyLB4IMQQggpBU7lrmpLtQshhBBCigp7PgghhJAS4LqO9ynk+HKFwQchhBBSCly3sKGTMp7zwWEXQgghhBSViun5cGKGsmND2u5Yj+yqytTIGCwKnh6aVKMUxFevt0N9PzAi9W0HDb81GNB9BhFxpjHcwCVXbYUuh21CHgb3rBfpqrc3B5xUmiI4jbWheXRbGkQ6PUUubx/f2BfqC+Gd8rOfFGljUBpa2InwZhvrkl4ibrX09XDAs8PIgJGAznerNEWI9MlyMOGYbIOsGzMj69NOWHm9ZKws1HetLJsYeDCgh0K8S6YHm8P9EuJb/GVvZvIsy55vf6ga9McwA6xqIuDDgPeFvg84P8+CPPjARzPAJgK/8vuX5PG36HZCn7Vcwn9R9PWId8lz5Kqw8MCPCF8ZTj4/DV8W8npaYF2gBwuSzwdEE+mX6Wh/eL79ZY0eKnD+QSj7qoB2ntt+OsgHZpfhFjjhtIx7Piom+CCEEEImFI5TmKtZGc/54LALIYQQQooKez4IIYSQUuBy2IUQQgghxYw9HEe5BQy7UGpLCCGEkPFGD6pSJ5xyzgchhBBCikpZBB+33XabmjlzpkokEuqwww5TL7zwQqmzRAghhBSG4xb+KVMm/ITTf/mXf1FXXnmluuOOO7zA4+c//7maP3++WrVqlWppaRnzeaJ9jopEnUANvpWWY25uxK8Lj/WBBj8Z7ttgDYI/xkceI9vT8FtR/zWr1w7Kc8STod4D8a3ZcH8D8BaJ9sn9c63S90OTqZdC/9hWaRZgz5ku0pHulEzDNZx4NNQfI8hDw+yXRg/Wh1vkOdsa5X1MlWknCmWdtvOG4LEt8ppmGrxGauKh92lk5TVyNdJbJLFB+p1ospNk/db3ynOaWWiDNdFQb5H6mLwxF3wijIAXlx2Xx5jgPRKFPOWq5SskC14ziBPwbKGXRLQfPFLgPmI98tmy4L6x3eOz6jP1CDgHXhPLJdof7vNjQHd4POB3RKYhGvo8J7bAfablfcS35kI9VdDnJ6js8V2IbSJXJa8Z7c2F1r8DRW2l/PMZsrWR0HaN+c5hm4Q8RwZkXbjwvEf7xvALenTRwLO7S3F13gqR2pZv8DHhez5+9rOfqQsvvFCdf/75av/99/eCkKqqKvXrX/+61FkjhBBCyO4WfGQyGbV8+XJ13HHHjXxnmqaXXrZsWUnzRgghhBSC67gFf8qVCT3ssnnzZmXbtmptbRXf6/Rbb70VeEw6nfY+w/T09OzyfBJCCCE75lDq7HjB0eF04rBw4UJVX18/8pk+Xc5JIIQQQkhpmdDDLs3NzcqyLLVx40bxvU63tbUFHnPttdeq7u7ukc+6deuKlFtCCCFk7LgVPOwyoYOPWCymDjnkEPXEE0+MfOc4jpeeN29e4DHxeFzV1dWJDyGEEDLhcJ3CP2XKhJ7zodEy23PPPVd96lOfUp/+9Kc9qW1/f7+nfhkL7kdSpFw2tV35oAEyWTdozW3AgHOYuLQ8LEWfQ5lkFmSQcD5vH1vKPXM5mS/MppsLlxz6pJUg0wpy+c2B7MzMSamti7pFyLONUj4bygG2D10DZIxwTsORaceW8l4nJ/V+jgFlDecPkqu5Jsj77FzoffnzjPfphN7T0D7h7c6Ec+TgmrjdMXdAaov3jfWXA6ltTr5Cctk8UtuAZwubnQHPig31Z8Kzla/d+/IUUMx4DrwmlouRG5/UNsjEEp8t3/Pse2fI+7By45TaBpQ9WgT4pLZwTcP3XgOpLVYF1k3AMXmltnnapIK6cPFvarjHQEYVTS6XEr87diU5lS3I4NQ7vkyZ8MHHl770JbVp0yb13e9+V7W3t6uDDjpIPfLII75JqNujt7fX+3/5Izft4pySkrGeZU8I2bno3x163uCu6tVva2tTz7Q/XPC59Hn0+coNwy1GeFdC9DDN+vXrvSh2xowZ3hwQDsUUhlYQ6Ym8LEuW40SBbZJlubPQvyt04DF16lTP2mFXkUqlPDuJQtGBh3b/LjcmfM9HoejGM23atBHJLeeB7DxYlizHiQbbJMtyZ7CrejxGk0gkyjJoqIgJp4QQQgjZ/WDwQQghhJCiUjHBh5bgfu973/P+JyzLiQDbJMtyIsJ2SYrBbj/hlBBCCCETi4rp+SCEEELIxIDBByGEEEKKCoMPQgghhBSVigk+brvtNjVz5kxPV33YYYepF154odRZmvCrAx966KGqtrZWtbS0qFNOOUWtWrXKZ5KzYMEC1dTUpGpqatTpp5/uWwSQSG6++WZlGIa6/PLLWY47wIcffqi+8pWveG0umUyqAw44QL300ksj2/UUNu2GPGXKFG/7cccdp95++202Q8C2bXX99derWbNmeeW09957qxtvvFFYirMsyS7FrQDuu+8+NxaLub/+9a/d119/3b3wwgvdhoYGd+PGjaXO2oRl/vz57uLFi92VK1e6K1ascD//+c+7M2bMcPv6+kb2+frXv+5Onz7dfeKJJ9yXXnrJPfzww90jjjiipPmeyLzwwgvuzJkz3QMPPNC97LLLRr5nOY6Nzs5Od88993TPO+889/nnn3ffffdd909/+pP7zjvvjOxz8803u/X19e7vf/9799VXX3W/8IUvuLNmzXIHBwd3QY2WLzfddJPb1NTkLlmyxF2zZo17//33uzU1Ne4vfvGLkX1YlmRXUhHBx6c//Wl3wYIFI2nbtt2pU6e6CxcuLGm+yomOjg79J5G7dOlSL93V1eVGo1HvpTXMm2++6e2zbNmyEuZ0YtLb2+vuu+++7mOPPeZ+9rOfHQk+WI5j5+qrr3Y/85nPbHe74zhuW1ub++Mf/3jkO12+8Xjc/e1vf1tQ/e1unHjiie5Xv/pV8d1pp53mnnXWWd7PLEuyq9nth120d/7y5cu97tfRlus6vWzZspLmrZzo7u72/m9sbPT+12WazWZFuc6ePdtbP4fl6kcPT5144omivFiO4+Ohhx7yVrf+7//9v3tDgQcffLD61a9+NbJ9zZo13uKTo8tY22TrYVa2SckRRxyhnnjiCfW3v/3NS7/66qvqmWeeUSeccALLkhSF3X5tl82bN3vjm7gKrk6/9dZbJctXuS3Op+coHHnkkWru3Lned/olrxc0amho8JWr3ka2cd9996mXX35Zvfjii75iYTmOnXfffVctWrRIXXnllerb3/62V57f+MY3vHZ47rnnjrS7oGedbVJyzTXXeOtd6T8YLMvy3pE33XSTOuuss0baJcuS7Ep2++CD7Jy/2leuXOn9ZUTGh17597LLLlOPPfZYRS8itbOCYN3z8Q//8A9eWvd86HZ5xx13eMEHGTv/+q//qu655x517733qo9//ONqxYoV3h8YeiVXliUpBrv9sEtzc7MX2aMKQ6fb2tpKlq9y4ZJLLlFLlixRf/7zn73VgYfRZaeHtLq6usT+LFeJHp7q6OhQn/zkJ1UkEvE+S5cuVbfeeqv3s/6rnOU4NrSCZf/99xffzZkzR61du3akTQ63QbbJcK666iqv9+OMM87wFENnn322uuKKKzyVG8uSFIPdPvjQXbKHHHKIN745+i8onZ43b15J8zaR0ZORdeDxwAMPqCeffNKT5I1Gl2k0GhXlqqW4+hcBy3Ubxx57rHrttde8vyyHP/qvd929Pfwzy3Fs6GE/lHvrOQt77rmn97NuozoAGd0m9dDC888/zzYJDAwMeHPfRqP/SNPvRpYlKQpuhUht9Yz3u+66y33jjTfciy66yJPatre3lzprE5aLL77Ykyw+9dRT7oYNG0Y+AwMDQiKq5bdPPvmkJ7WdN2+e9yHhjFa7sBzHJ1WORCKeTPTtt99277nnHreqqsr9zW9+I+Sh+tl+8MEH3b/+9a/uySefTKltAOeee667xx57jEhtf/e737nNzc3ut771LZYlKQoVEXxofvnLX3q/KLXfh5bePvfcc6XO0oRGx6VBH+39MYz2Tvif//N/upMmTfJ+CZx66qlegELGF3ywHMfOH/7wB3fu3LneHxOzZ892/+mf/kls1xLR66+/3m1tbfX2OfbYY91Vq1axSQI9PT1eG9TvxEQi4e61117ud77zHTedTrMsSVHgqraEEEIIKSq7/ZwPQgghhEwsGHwQQgghpKgw+CCEEEJIUWHwQQghhJCiwuCDEEIIIUWFwQchhBBCigqDD0IIIYQUFQYfhBBCCCkqDD4IKXOOPvpob0VSzcyZM9XPf/7zUmeJEEJCYfBByG7Eiy++qC666KJddv5nnnnGW+CtqalJJZNJNXv2bHXLLbfssusRQnZPIqXOACFk5zF58uRdWpzV1dXeascHHnig97MORr72ta95P+/KoIcQsnvBng9Cyoj+/n51zjnnqJqaGjVlyhT105/+VGzHYRfDMNQ//uM/qv/6X/+rqqqqUnPmzFHLli1T77zzjjdco4OGI444Qq1evXpM1z/44IPVl7/8ZfXxj3/cu9ZXvvIVNX/+fPWf//mfO/1eCSG7Lww+CCkjrrrqKrV06VL14IMPqkcffVQ99dRT6uWXXw495sYbb/QClhUrVnjDJGeeeabXW3Httdeql156Sa9s7fVm7AivvPKKevbZZ9VnP/vZHbwjQkglwmEXQsqEvr4+deedd6rf/OY36thjj/W+u/vuu9W0adNCjzv//PPVF7/4Re/nq6++Ws2bN09df/31Xo+F5rLLLvP2GQ/6mps2bVK5XE7dcMMN6n/8j/+xw/dFCKk8GHwQUibooZFMJqMOO+ywke8aGxvVfvvtF3qcnp8xTGtrq/f/AQccIL5LpVKqp6dH1dXVjSkvephFB0PPPfecuuaaa9Q+++zjDccQQshYYPBByG5ONBoVc0C2953jOGM+56xZs0aCmI0bN3q9Hww+CCFjhXM+CCkT9t57by9oeP7550e+27p1q/rb3/5W0nzpoCWdTpc0D4SQ8oI9H4SUCVrhcsEFF3iTTrXPRktLi/rOd76jTLN4f0PcdtttasaMGd7EVc3TTz+tfvKTn6hvfOMbRcsDIaT8YfBBSBnx4x//2JtrcdJJJ6na2lr1v/7X/1Ld3d1F7eXQKpk1a9aoSCTi9cb88Ic/9NQzhBAyVgxX6+wIIYQQQooE53wQQgghpKgw+CCEjKCdS/XckqDPPffcw5IihOwUOOxCCBnh/fffV9lsNrBEtB+InmdCCCGFwuCDEEIIIUWFwy6EEEIIKSoMPgghhBBSVBh8EEIIIaSoMPgghBBCSFFh8EEIIYSQosLggxBCCCFFhcEHIYQQQooKgw9CCCGEqGLy/wFUKCuQ/MePmwAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Please note - this image was generated from only a few samples of training and does not represent the final model\n", + "da[5][0].plot()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25598079-8d9b-4893-a807-cfe1c50d35b8", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/packages/bundled_models/persistence/notebooks/README.md b/packages/bundled_models/persistence/notebooks/README.md new file mode 100644 index 00000000..e6166bfc --- /dev/null +++ b/packages/bundled_models/persistence/notebooks/README.md @@ -0,0 +1,3 @@ +# TO BE MOVED + +This is a temporary space for the notebooks, they will eventually be moved to the root notebooks folder. diff --git a/packages/bundled_models/persistence/notebooks/pipeline_example.ipynb b/packages/bundled_models/persistence/notebooks/pipeline_example.ipynb new file mode 100644 index 00000000..a4c90af9 --- /dev/null +++ b/packages/bundled_models/persistence/notebooks/pipeline_example.ipynb @@ -0,0 +1,282 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "55054796-a6e7-45bf-b891-4298e89a6f4e", + "metadata": {}, + "source": [ + "## Description\n", + "\n", + "This is mostly derived from `fourcastnext`. It is used to illustrate how to create a similar \"inference\" pipeline using PET persistence model." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "b4ef53ab-bf5b-4486-a0d6-41dd8e4803fa", + "metadata": {}, + "outputs": [], + "source": [ + "# Most users should change this to the current directory.\n", + "# os.environ['ERA5LOWRESDEMO'] = os.path.abspath('/tmp/')\n", + "\n", + "# NOTE: /var/tmp/ is used for longer running tasks and persistence that's required across reboots\n", + "import os\n", + "os.environ['ERA5LOWRESDEMO'] = os.path.abspath('/var/tmp/era5demo_persistence')\n", + "os.makedirs(os.environ['ERA5LOWRESDEMO'], exist_ok=True)\n", + "EXPERIMENT_VERSION='v1'\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "33f8877a-5c8a-42a3-946c-d0f3de7199ee", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "import pathlib\n", + "import xarray as xr\n", + "from pathlib import Path\n", + "\n", + "# ---\n", + "# unsure what these are for:\n", + "# ---\n", + "# import hydra\n", + "# from omegaconf import OmegaConf\n", + "# ---\n", + "\n", + "# ---\n", + "# we are downloading from the internet. Does this register the archive?\n", + "# ---\n", + "# import pyearthtools.data\n", + "# import pyearthtools.data.archive\n", + "# ---\n", + "\n", + "# ---\n", + "# training not required\n", + "# ---\n", + "# import pyearthtools.training\n", + "# import fourcastnext\n", + "# ---\n", + "\n", + "import pyearthtools.tutorial\n", + "import pyearthtools.pipeline\n", + "\n", + "# ---\n", + "# no gpu required\n", + "# ---\n", + "# import torch; torch.set_default_device() # Uncomment and set this if you need to configure a non-default device.\n", + "# ---" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "1aaa420e-7928-43d1-b478-36b157a4268b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "This tutorial will download a copy of the input data to /var/tmp/era5demo_persistence. It will also create model checkpoint files and other data here.\n" + ] + } + ], + "source": [ + "workdir = os.environ['ERA5LOWRESDEMO']\n", + "print(f'This tutorial will download a copy of the input data to {workdir}. It will also create model checkpoint files and other data here.')\n", + "\n", + "# ---\n", + "# this does not work, but it may not be needed\n", + "# ---\n", + "# if pyearthtools.data.archive.ROOT_DIRECTORIES['era5lowresdemo'] != workdir:\n", + "# print(\"There is some misconfiguration of your working directory, please review the commented out cells at the start of the notebook\")\n", + "# ---\n", + "\n", + "file_location = workdir + '/mini.nc'\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "388c9c6a-3cc5-479d-b160-491107e6695c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "File already downloaded (/var/tmp/era5demo_persistence/mini.nc), skipping ...\n" + ] + } + ], + "source": [ + "if not os.path.exists(file_location):\n", + " print(\"Training data not found, downloading around 2.8GB of data\")\n", + " era5_lowres = xr.open_zarr('gs://weatherbench2/datasets/era5/1959-2022-6h-64x32_equiangular_conservative.zarr')\n", + " subset = era5_lowres[['10m_u_component_of_wind', \n", + " '10m_v_component_of_wind', \n", + " '2m_temperature', \n", + " 'mean_sea_level_pressure',\n", + " #'geopotential', # Uncomment this to fetch additional data\n", + " #'toa_incident_solar_radiation_6hr', # Uncomment this to fetch additional data\n", + " #'temperature' # Uncomment this to fetch additional data\n", + " ]]\n", + "\n", + " # bilevel = subset.sel({'level': [50, 500]}) Uncomment if fetching addtional data \n", + " # bilevel.to_netcdf(file_location)\n", + "\n", + " subset.to_netcdf(file_location) # Comment this out if using the bilevel data instead\n", + " print(f\"Wrote file to {file_location}\")\n", + " assert os.path.exists(file_location)\n", + "else:\n", + " print(f\"File already downloaded ({file_location}), skipping ...\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "4686b4bf-f0e4-44d8-a758-42863ba4c90c", + "metadata": {}, + "outputs": [], + "source": [ + "accessor = pyearthtools.tutorial.ERA5DataClass.ERA5LowResDemoIndex([\n", + " '10m_u_component_of_wind', \n", + " '10m_v_component_of_wind', \n", + " 'mean_sea_level_pressure',\n", + " '2m_temperature' \n", + "],\n", + "filename_override=file_location)" + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "3ad9c174-c714-42da-8fc8-02b13e27bbd0", + "metadata": {}, + "outputs": [], + "source": [ + "# --- scratch space/workings ---\n", + "# 1. data is in 6 hour intervals\n", + "# 2. we want the past 6 indices for median to work, so taking 2 days is sufficient without breaking the intervals\n", + "# 3. explicitly set the time update to 6 hours since the median will be performed on every index using past 3 indices (but padded to 8 for safety/imputation)\n", + "# ---\n", + "# SequentialRetrieval works like this:\n", + "# (a, b, c)\n", + "# a = start\n", + "# b = number of values to get\n", + "# c = interval or how many values to skip\n", + "# ---\n", + "# TemporalRetrieval does the same, but each index is a delta-unit mapping - so we need to reverse engineer a bit\n", + "# !!! IMPORTANT. The circumstance here is that time is already 6 hourly windows, \n", + "# What this means is this:\n", + "# - selecting 1 index =>\n", + "# ---\n", + "import datetime\n", + "import functools\n", + "data_pipeline = pyearthtools.pipeline.Pipeline(\n", + " accessor,\n", + " pyearthtools.data.transforms.coordinates.StandardLongitude(type=\"-180-180\"),\n", + " pyearthtools.pipeline.modifications.TemporalWindow(\n", + " prior_indexes=list(range(-6,0,1)),\n", + " posterior_indexes=[0],\n", + " timedelta=datetime.timedelta(hours=6),\n", + " merge_method=functools.partial(xr.concat, dim=\"time\"),\n", + " ),\n", + " iterator=pyearthtools.pipeline.iterators.DateRange(1980, 2016, interval='6h')\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "576b55de-5494-445c-9888-52bffdfb85f7", + "metadata": {}, + "outputs": [ + { + "ename": "DataNotFoundError", + "evalue": "Data with args: (Petdt('2000-05-03T12'),) could not be found.", + "output_type": "error", + "traceback": [ + "\u001b[31m---------------------------------------------------------------------------\u001b[39m", + "\u001b[31mKeyError\u001b[39m Traceback (most recent call last)", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/bundled_models/persistence/.pixi/envs/dev/lib/python3.13/site-packages/xarray/backends/file_manager.py:219\u001b[39m, in \u001b[36mCachingFileManager._acquire_with_cache_info\u001b[39m\u001b[34m(self, needs_lock)\u001b[39m\n\u001b[32m 218\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m219\u001b[39m file = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_cache\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_key\u001b[49m\u001b[43m]\u001b[49m\n\u001b[32m 220\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m:\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/bundled_models/persistence/.pixi/envs/dev/lib/python3.13/site-packages/xarray/backends/lru_cache.py:56\u001b[39m, in \u001b[36mLRUCache.__getitem__\u001b[39m\u001b[34m(self, key)\u001b[39m\n\u001b[32m 55\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28mself\u001b[39m._lock:\n\u001b[32m---> \u001b[39m\u001b[32m56\u001b[39m value = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_cache\u001b[49m\u001b[43m[\u001b[49m\u001b[43mkey\u001b[49m\u001b[43m]\u001b[49m\n\u001b[32m 57\u001b[39m \u001b[38;5;28mself\u001b[39m._cache.move_to_end(key)\n", + "\u001b[31mKeyError\u001b[39m: [, ('/var/tmp/era5demo_persistence/mini.nc',), 'r', (('decode_vlen_strings', True), ('driver', None), ('format', 'NETCDF4'), ('invalid_netcdf', None), ('phony_dims', 'access')), '08669add-4cf2-4250-a554-3fdbad8525cb']", + "\nDuring handling of the above exception, another exception occurred:\n", + "\u001b[31mImportError\u001b[39m Traceback (most recent call last)", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/data/src/pyearthtools/data/indexes/_indexes.py:203\u001b[39m, in \u001b[36mFileSystemIndex.get\u001b[39m\u001b[34m(self, *args, **kwargs)\u001b[39m\n\u001b[32m 202\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m203\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mload\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43msearch\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 204\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/tutorial/src/pyearthtools/tutorial/ERA5DataClass.py:309\u001b[39m, in \u001b[36mERA5LowResDemoIndex.load\u001b[39m\u001b[34m(self, *args, **kwargs)\u001b[39m\n\u001b[32m 307\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m.dataset\n\u001b[32m--> \u001b[39m\u001b[32m309\u001b[39m ds = \u001b[43mxr\u001b[49m\u001b[43m.\u001b[49m\u001b[43mopen_dataset\u001b[49m\u001b[43m(\u001b[49m\u001b[43margs\u001b[49m\u001b[43m[\u001b[49m\u001b[32;43m0\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m[\u001b[49m\u001b[32;43m0\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mengine\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mh5netcdf\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[32m 310\u001b[39m \u001b[38;5;28mself\u001b[39m.dataset = ds\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/bundled_models/persistence/.pixi/envs/dev/lib/python3.13/site-packages/xarray/backends/api.py:606\u001b[39m, in \u001b[36mopen_dataset\u001b[39m\u001b[34m(filename_or_obj, engine, chunks, cache, decode_cf, mask_and_scale, decode_times, decode_timedelta, use_cftime, concat_characters, decode_coords, drop_variables, create_default_indexes, inline_array, chunked_array_type, from_array_kwargs, backend_kwargs, **kwargs)\u001b[39m\n\u001b[32m 605\u001b[39m overwrite_encoded_chunks = kwargs.pop(\u001b[33m\"\u001b[39m\u001b[33moverwrite_encoded_chunks\u001b[39m\u001b[33m\"\u001b[39m, \u001b[38;5;28;01mNone\u001b[39;00m)\n\u001b[32m--> \u001b[39m\u001b[32m606\u001b[39m backend_ds = \u001b[43mbackend\u001b[49m\u001b[43m.\u001b[49m\u001b[43mopen_dataset\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 607\u001b[39m \u001b[43m \u001b[49m\u001b[43mfilename_or_obj\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 608\u001b[39m \u001b[43m \u001b[49m\u001b[43mdrop_variables\u001b[49m\u001b[43m=\u001b[49m\u001b[43mdrop_variables\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 609\u001b[39m \u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mdecoders\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 610\u001b[39m \u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 611\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 612\u001b[39m ds = _dataset_from_backend_dataset(\n\u001b[32m 613\u001b[39m backend_ds,\n\u001b[32m 614\u001b[39m filename_or_obj,\n\u001b[32m (...)\u001b[39m\u001b[32m 625\u001b[39m **kwargs,\n\u001b[32m 626\u001b[39m )\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/bundled_models/persistence/.pixi/envs/dev/lib/python3.13/site-packages/xarray/backends/h5netcdf_.py:540\u001b[39m, in \u001b[36mH5netcdfBackendEntrypoint.open_dataset\u001b[39m\u001b[34m(self, filename_or_obj, mask_and_scale, decode_times, concat_characters, decode_coords, drop_variables, use_cftime, decode_timedelta, format, group, lock, invalid_netcdf, phony_dims, decode_vlen_strings, driver, driver_kwds, storage_options)\u001b[39m\n\u001b[32m 539\u001b[39m filename_or_obj = _normalize_filename_or_obj(filename_or_obj)\n\u001b[32m--> \u001b[39m\u001b[32m540\u001b[39m store = \u001b[43mH5NetCDFStore\u001b[49m\u001b[43m.\u001b[49m\u001b[43mopen\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 541\u001b[39m \u001b[43m \u001b[49m\u001b[43mfilename_or_obj\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 542\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43mformat\u001b[39;49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mformat\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 543\u001b[39m \u001b[43m \u001b[49m\u001b[43mgroup\u001b[49m\u001b[43m=\u001b[49m\u001b[43mgroup\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 544\u001b[39m \u001b[43m \u001b[49m\u001b[43mlock\u001b[49m\u001b[43m=\u001b[49m\u001b[43mlock\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 545\u001b[39m \u001b[43m \u001b[49m\u001b[43minvalid_netcdf\u001b[49m\u001b[43m=\u001b[49m\u001b[43minvalid_netcdf\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 546\u001b[39m \u001b[43m \u001b[49m\u001b[43mphony_dims\u001b[49m\u001b[43m=\u001b[49m\u001b[43mphony_dims\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 547\u001b[39m \u001b[43m \u001b[49m\u001b[43mdecode_vlen_strings\u001b[49m\u001b[43m=\u001b[49m\u001b[43mdecode_vlen_strings\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 548\u001b[39m \u001b[43m \u001b[49m\u001b[43mdriver\u001b[49m\u001b[43m=\u001b[49m\u001b[43mdriver\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 549\u001b[39m \u001b[43m \u001b[49m\u001b[43mdriver_kwds\u001b[49m\u001b[43m=\u001b[49m\u001b[43mdriver_kwds\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 550\u001b[39m \u001b[43m \u001b[49m\u001b[43mstorage_options\u001b[49m\u001b[43m=\u001b[49m\u001b[43mstorage_options\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 551\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 553\u001b[39m store_entrypoint = StoreBackendEntrypoint()\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/bundled_models/persistence/.pixi/envs/dev/lib/python3.13/site-packages/xarray/backends/h5netcdf_.py:242\u001b[39m, in \u001b[36mH5NetCDFStore.open\u001b[39m\u001b[34m(cls, filename, mode, format, group, lock, autoclose, invalid_netcdf, phony_dims, decode_vlen_strings, driver, driver_kwds, storage_options)\u001b[39m\n\u001b[32m 240\u001b[39m manager = manager_cls(h5netcdf.File, filename, mode=mode, kwargs=kwargs)\n\u001b[32m--> \u001b[39m\u001b[32m242\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mcls\u001b[39;49m\u001b[43m(\u001b[49m\n\u001b[32m 243\u001b[39m \u001b[43m \u001b[49m\u001b[43mmanager\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 244\u001b[39m \u001b[43m \u001b[49m\u001b[43mgroup\u001b[49m\u001b[43m=\u001b[49m\u001b[43mgroup\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 245\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43mformat\u001b[39;49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mformat\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 246\u001b[39m \u001b[43m \u001b[49m\u001b[43mmode\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmode\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 247\u001b[39m \u001b[43m \u001b[49m\u001b[43mlock\u001b[49m\u001b[43m=\u001b[49m\u001b[43mlock\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 248\u001b[39m \u001b[43m \u001b[49m\u001b[43mautoclose\u001b[49m\u001b[43m=\u001b[49m\u001b[43mautoclose\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 249\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/bundled_models/persistence/.pixi/envs/dev/lib/python3.13/site-packages/xarray/backends/h5netcdf_.py:152\u001b[39m, in \u001b[36mH5NetCDFStore.__init__\u001b[39m\u001b[34m(self, manager, group, mode, format, lock, autoclose)\u001b[39m\n\u001b[32m 150\u001b[39m \u001b[38;5;66;03m# todo: utilizing find_root_and_group seems a bit clunky\u001b[39;00m\n\u001b[32m 151\u001b[39m \u001b[38;5;66;03m# making filename available on h5netcdf.Group seems better\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m152\u001b[39m \u001b[38;5;28mself\u001b[39m._filename = find_root_and_group(\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mds\u001b[49m)[\u001b[32m0\u001b[39m].filename\n\u001b[32m 153\u001b[39m \u001b[38;5;28mself\u001b[39m.is_remote = is_remote_uri(\u001b[38;5;28mself\u001b[39m._filename)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/bundled_models/persistence/.pixi/envs/dev/lib/python3.13/site-packages/xarray/backends/h5netcdf_.py:260\u001b[39m, in \u001b[36mH5NetCDFStore.ds\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 258\u001b[39m \u001b[38;5;129m@property\u001b[39m\n\u001b[32m 259\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mds\u001b[39m(\u001b[38;5;28mself\u001b[39m):\n\u001b[32m--> \u001b[39m\u001b[32m260\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_acquire\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/bundled_models/persistence/.pixi/envs/dev/lib/python3.13/site-packages/xarray/backends/h5netcdf_.py:252\u001b[39m, in \u001b[36mH5NetCDFStore._acquire\u001b[39m\u001b[34m(self, needs_lock)\u001b[39m\n\u001b[32m 251\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34m_acquire\u001b[39m(\u001b[38;5;28mself\u001b[39m, needs_lock=\u001b[38;5;28;01mTrue\u001b[39;00m):\n\u001b[32m--> \u001b[39m\u001b[32m252\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mwith\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_manager\u001b[49m\u001b[43m.\u001b[49m\u001b[43macquire_context\u001b[49m\u001b[43m(\u001b[49m\u001b[43mneeds_lock\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mas\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mroot\u001b[49m\u001b[43m:\u001b[49m\n\u001b[32m 253\u001b[39m \u001b[43m \u001b[49m\u001b[43mds\u001b[49m\u001b[43m \u001b[49m\u001b[43m=\u001b[49m\u001b[43m \u001b[49m\u001b[43m_nc4_require_group\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 254\u001b[39m \u001b[43m \u001b[49m\u001b[43mroot\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_group\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_mode\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcreate_group\u001b[49m\u001b[43m=\u001b[49m\u001b[43m_h5netcdf_create_group\u001b[49m\n\u001b[32m 255\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/bundled_models/persistence/.pixi/envs/dev/lib/python3.13/contextlib.py:141\u001b[39m, in \u001b[36m_GeneratorContextManager.__enter__\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 140\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m141\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mnext\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mgen\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 142\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mStopIteration\u001b[39;00m:\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/bundled_models/persistence/.pixi/envs/dev/lib/python3.13/site-packages/xarray/backends/file_manager.py:207\u001b[39m, in \u001b[36mCachingFileManager.acquire_context\u001b[39m\u001b[34m(self, needs_lock)\u001b[39m\n\u001b[32m 206\u001b[39m \u001b[38;5;250m\u001b[39m\u001b[33;03m\"\"\"Context manager for acquiring a file.\"\"\"\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m207\u001b[39m file, cached = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_acquire_with_cache_info\u001b[49m\u001b[43m(\u001b[49m\u001b[43mneeds_lock\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 208\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/bundled_models/persistence/.pixi/envs/dev/lib/python3.13/site-packages/xarray/backends/file_manager.py:225\u001b[39m, in \u001b[36mCachingFileManager._acquire_with_cache_info\u001b[39m\u001b[34m(self, needs_lock)\u001b[39m\n\u001b[32m 224\u001b[39m kwargs[\u001b[33m\"\u001b[39m\u001b[33mmode\u001b[39m\u001b[33m\"\u001b[39m] = \u001b[38;5;28mself\u001b[39m._mode\n\u001b[32m--> \u001b[39m\u001b[32m225\u001b[39m file = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_opener\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_args\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 226\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m._mode == \u001b[33m\"\u001b[39m\u001b[33mw\u001b[39m\u001b[33m\"\u001b[39m:\n\u001b[32m 227\u001b[39m \u001b[38;5;66;03m# ensure file doesn't get overridden when opened again\u001b[39;00m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/bundled_models/persistence/.pixi/envs/dev/lib/python3.13/site-packages/h5netcdf/core.py:1892\u001b[39m, in \u001b[36mFile.__init__\u001b[39m\u001b[34m(self, path, mode, format, invalid_netcdf, phony_dims, backend, **kwargs)\u001b[39m\n\u001b[32m 1891\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m-> \u001b[39m\u001b[32m1892\u001b[39m \u001b[38;5;28mself\u001b[39m._backend = \u001b[43m_parse_backend\u001b[49m\u001b[43m(\u001b[49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmode\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mbackend\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1893\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.backend == \u001b[33m\"\u001b[39m\u001b[33mpyfive\u001b[39m\u001b[33m\"\u001b[39m:\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/bundled_models/persistence/.pixi/envs/dev/lib/python3.13/site-packages/h5netcdf/core.py:174\u001b[39m, in \u001b[36m_parse_backend\u001b[39m\u001b[34m(path, mode, backend, **kwargs)\u001b[39m\n\u001b[32m 173\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m no_backend.get(backend, \u001b[38;5;28;01mFalse\u001b[39;00m):\n\u001b[32m--> \u001b[39m\u001b[32m174\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mImportError\u001b[39;00m(\n\u001b[32m 175\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mNo module named \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mbackend\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[33m, backend not available. \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 176\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mPlease install \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mbackend\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[33m into your Python environment.\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 177\u001b[39m )\n\u001b[32m 179\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m backend\n", + "\u001b[31mImportError\u001b[39m: No module named 'h5py', backend not available. Please install 'h5py' into your Python environment.", + "\nThe above exception was the direct cause of the following exception:\n", + "\u001b[31mDataNotFoundError\u001b[39m Traceback (most recent call last)", + "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[30]\u001b[39m\u001b[32m, line 4\u001b[39m\n\u001b[32m 2\u001b[39m data_pipeline[\u001b[33m\"\u001b[39m\u001b[33m2000-05-05\u001b[39m\u001b[33m\"\u001b[39m][\u001b[32m0\u001b[39m]\n\u001b[32m 3\u001b[39m \u001b[38;5;66;03m# does not work\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m4\u001b[39m \u001b[43mdata_pipeline\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43m2000-05-05T00\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m]\u001b[49m[\u001b[32m0\u001b[39m]\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/pipeline/src/pyearthtools/pipeline/controller.py:551\u001b[39m, in \u001b[36mPipeline.__getitem__\u001b[39m\u001b[34m(self, idx)\u001b[39m\n\u001b[32m 543\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mmap\u001b[39m(\u001b[38;5;28mself\u001b[39m.\u001b[34m__getitem__\u001b[39m, indexes)\n\u001b[32m 545\u001b[39m \u001b[38;5;66;03m# Start the pipeline with the raw/initial data\u001b[39;00m\n\u001b[32m 546\u001b[39m \u001b[38;5;66;03m# `idx` here is the index of the sample within the dataset, not the\u001b[39;00m\n\u001b[32m 547\u001b[39m \u001b[38;5;66;03m# position of the step within the list of steps\u001b[39;00m\n\u001b[32m 548\u001b[39m \u001b[38;5;66;03m# `sample` is actual data\u001b[39;00m\n\u001b[32m 549\u001b[39m \u001b[38;5;66;03m# `step_index` *is* the index of the sample provier within the list of steps\u001b[39;00m\n\u001b[32m 550\u001b[39m \u001b[38;5;66;03m# Initial just means untransformed by the pipeline\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m551\u001b[39m sample, step_index = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_get_initial_sample\u001b[49m\u001b[43m(\u001b[49m\u001b[43midx\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 552\u001b[39m LOG.debug(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mCall pipeline __getitem__ for \u001b[39m\u001b[38;5;132;01m{\u001b[39;00midx\u001b[38;5;250m \u001b[39m\u001b[38;5;132;01m= }\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m 554\u001b[39m \u001b[38;5;66;03m# Apply each pipeline step to the sample, starting from the latest source\u001b[39;00m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/pipeline/src/pyearthtools/pipeline/controller.py:521\u001b[39m, in \u001b[36mPipeline._get_initial_sample\u001b[39m\u001b[34m(self, idx)\u001b[39m\n\u001b[32m 519\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(step, PipelineIndex):\n\u001b[32m 520\u001b[39m LOG.debug(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mGetting initial sample from \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mstep\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m at \u001b[39m\u001b[38;5;132;01m{\u001b[39;00midx\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m--> \u001b[39m\u001b[32m521\u001b[39m sample = \u001b[43mstep\u001b[49m\u001b[43m[\u001b[49m\u001b[43midx\u001b[49m\u001b[43m]\u001b[49m\n\u001b[32m 522\u001b[39m whereinthesequence = \u001b[38;5;28mlen\u001b[39m(\u001b[38;5;28mself\u001b[39m.steps) - (index + \u001b[32m1\u001b[39m)\n\u001b[32m 523\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m (sample, whereinthesequence)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/pipeline/src/pyearthtools/pipeline/modifications/idx_modification.py:546\u001b[39m, in \u001b[36mTemporalWindow.__getitem__\u001b[39m\u001b[34m(self, date_of_interest)\u001b[39m\n\u001b[32m 543\u001b[39m prior_i = [i * \u001b[38;5;28mself\u001b[39m.timedelta \u001b[38;5;28;01mfor\u001b[39;00m i \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m.prior_indexes]\n\u001b[32m 544\u001b[39m posterior_i = [i * \u001b[38;5;28mself\u001b[39m.timedelta \u001b[38;5;28;01mfor\u001b[39;00m i \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m.posterior_indexes]\n\u001b[32m--> \u001b[39m\u001b[32m546\u001b[39m prior = [\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mparent_pipeline\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;28;43mstr\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mdate_of_interest\u001b[49m\u001b[43m \u001b[49m\u001b[43m+\u001b[49m\u001b[43m \u001b[49m\u001b[43mdelta\u001b[49m\u001b[43m)\u001b[49m\u001b[43m]\u001b[49m \u001b[38;5;28;01mfor\u001b[39;00m delta \u001b[38;5;129;01min\u001b[39;00m prior_i]\n\u001b[32m 547\u001b[39m posterior = [\u001b[38;5;28mself\u001b[39m.parent_pipeline()[\u001b[38;5;28mstr\u001b[39m(date_of_interest + delta)] \u001b[38;5;28;01mfor\u001b[39;00m delta \u001b[38;5;129;01min\u001b[39;00m posterior_i]\n\u001b[32m 549\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.merge_method:\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/pipeline/src/pyearthtools/pipeline/controller.py:551\u001b[39m, in \u001b[36mPipeline.__getitem__\u001b[39m\u001b[34m(self, idx)\u001b[39m\n\u001b[32m 543\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mmap\u001b[39m(\u001b[38;5;28mself\u001b[39m.\u001b[34m__getitem__\u001b[39m, indexes)\n\u001b[32m 545\u001b[39m \u001b[38;5;66;03m# Start the pipeline with the raw/initial data\u001b[39;00m\n\u001b[32m 546\u001b[39m \u001b[38;5;66;03m# `idx` here is the index of the sample within the dataset, not the\u001b[39;00m\n\u001b[32m 547\u001b[39m \u001b[38;5;66;03m# position of the step within the list of steps\u001b[39;00m\n\u001b[32m 548\u001b[39m \u001b[38;5;66;03m# `sample` is actual data\u001b[39;00m\n\u001b[32m 549\u001b[39m \u001b[38;5;66;03m# `step_index` *is* the index of the sample provier within the list of steps\u001b[39;00m\n\u001b[32m 550\u001b[39m \u001b[38;5;66;03m# Initial just means untransformed by the pipeline\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m551\u001b[39m sample, step_index = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_get_initial_sample\u001b[49m\u001b[43m(\u001b[49m\u001b[43midx\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 552\u001b[39m LOG.debug(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mCall pipeline __getitem__ for \u001b[39m\u001b[38;5;132;01m{\u001b[39;00midx\u001b[38;5;250m \u001b[39m\u001b[38;5;132;01m= }\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m 554\u001b[39m \u001b[38;5;66;03m# Apply each pipeline step to the sample, starting from the latest source\u001b[39;00m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/pipeline/src/pyearthtools/pipeline/controller.py:528\u001b[39m, in \u001b[36mPipeline._get_initial_sample\u001b[39m\u001b[34m(self, idx)\u001b[39m\n\u001b[32m 526\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(\u001b[38;5;28mself\u001b[39m.steps[\u001b[32m0\u001b[39m], (_Pipeline, Index)):\n\u001b[32m 527\u001b[39m LOG.debug(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mGetting initial sample from \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m.steps[\u001b[32m0\u001b[39m]\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m at \u001b[39m\u001b[38;5;132;01m{\u001b[39;00midx\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m--> \u001b[39m\u001b[32m528\u001b[39m sample = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43msteps\u001b[49m\u001b[43m[\u001b[49m\u001b[32;43m0\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m[\u001b[49m\u001b[43midx\u001b[49m\u001b[43m]\u001b[49m\n\u001b[32m 529\u001b[39m whereinthesequence = \u001b[32m0\u001b[39m\n\u001b[32m 530\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m sample, whereinthesequence\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/data/src/pyearthtools/data/indexes/utilities/mixins/call_redirect.py:42\u001b[39m, in \u001b[36mCallRedirectMixin.__getitem__\u001b[39m\u001b[34m(self, idx)\u001b[39m\n\u001b[32m 40\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34m__getitem__\u001b[39m(\u001b[38;5;28mself\u001b[39m, idx: Any):\n\u001b[32m 41\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"[] accessor\"\"\"\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m42\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[34;43m__call__\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43midx\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/data/src/pyearthtools/data/indexes/_indexes.py:893\u001b[39m, in \u001b[36mAdvancedTimeIndex.__call__\u001b[39m\u001b[34m(self, *args, **kwargs)\u001b[39m\n\u001b[32m 884\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m.series(\n\u001b[32m 885\u001b[39m start_time,\n\u001b[32m 886\u001b[39m end_time,\n\u001b[32m (...)\u001b[39m\u001b[32m 889\u001b[39m **kwargs,\n\u001b[32m 890\u001b[39m )\n\u001b[32m 892\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(args) == \u001b[32m1\u001b[39m:\n\u001b[32m--> \u001b[39m\u001b[32m893\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mretrieve\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 895\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m.series(*args, **kwargs)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/data/src/pyearthtools/data/indexes/_indexes.py:788\u001b[39m, in \u001b[36mAdvancedTimeIndex.retrieve\u001b[39m\u001b[34m(self, querytime, aggregation, select, use_simple, **kwargs)\u001b[39m\n\u001b[32m 785\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m data \u001b[38;5;66;03m# selectdata(querytime, data)\u001b[39;00m\n\u001b[32m 787\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m querytime.resolution == \u001b[38;5;28mself\u001b[39m.data_resolution: \u001b[38;5;66;03m# Equal Resolution\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m788\u001b[39m data = \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m.\u001b[49m\u001b[43mretrieve\u001b[49m\u001b[43m(\u001b[49m\u001b[43mquerytime\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mselect\u001b[49m\u001b[43m=\u001b[49m\u001b[43mselect\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 789\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m data\n\u001b[32m 791\u001b[39m start_time = querytime.at_resolution(\u001b[38;5;28mself\u001b[39m.data_resolution)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/data/src/pyearthtools/data/indexes/_indexes.py:671\u001b[39m, in \u001b[36mSingleTimeDataIndex.retrieve\u001b[39m\u001b[34m(self, transforms, *args, **kwargs)\u001b[39m\n\u001b[32m 667\u001b[39m \u001b[38;5;66;03m# kwargs.update(self._get_preprocess(kwargs.pop(\"preprocess\", None))) # type: ignore\u001b[39;00m\n\u001b[32m 668\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m ChangeValue(\u001b[38;5;28mself\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33m_skip_transforms\u001b[39m\u001b[33m\"\u001b[39m, \u001b[38;5;28;01mTrue\u001b[39;00m):\n\u001b[32m 669\u001b[39m \u001b[38;5;66;03m# Skip transforms, so that they are only applied once\u001b[39;00m\n\u001b[32m 670\u001b[39m \u001b[38;5;66;03m# By applying transforms after time retrieve, prevents more then necessary data going to transforms\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m671\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m transforms(\u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m.\u001b[49m\u001b[43mretrieve\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/data/src/pyearthtools/data/indexes/_indexes.py:462\u001b[39m, in \u001b[36mSingleTimeIndex.retrieve\u001b[39m\u001b[34m(self, querytime, select, round, *args, **kwargs)\u001b[39m\n\u001b[32m 459\u001b[39m querytime = querytime.at_resolution(\u001b[38;5;28mself\u001b[39m.data_resolution)\n\u001b[32m 461\u001b[39m retrieval_function = \u001b[38;5;28mgetattr\u001b[39m(\u001b[38;5;28msuper\u001b[39m(), \u001b[33m\"\u001b[39m\u001b[33mretrieve\u001b[39m\u001b[33m\"\u001b[39m, \u001b[38;5;28msuper\u001b[39m().get)\n\u001b[32m--> \u001b[39m\u001b[32m462\u001b[39m data = \u001b[43mretrieval_function\u001b[49m\u001b[43m(\u001b[49m\u001b[43mquerytime\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 464\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(data, (xr.Dataset, xr.DataArray)):\n\u001b[32m 465\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m data\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/data/src/pyearthtools/data/indexes/_indexes.py:285\u001b[39m, in \u001b[36mDataIndex.retrieve\u001b[39m\u001b[34m(self, transforms, *args, **kwargs)\u001b[39m\n\u001b[32m 282\u001b[39m kwargs.update(\u001b[38;5;28mself\u001b[39m._get_preprocess(kwargs.pop(\u001b[33m\"\u001b[39m\u001b[33mpreprocess\u001b[39m\u001b[33m\"\u001b[39m, \u001b[38;5;28;01mNone\u001b[39;00m))) \u001b[38;5;66;03m# type: ignore\u001b[39;00m\n\u001b[32m 284\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m._skip_transforms:\n\u001b[32m--> \u001b[39m\u001b[32m285\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 287\u001b[39m untransformed = \u001b[38;5;28mself\u001b[39m.get(*args, **kwargs)\n\u001b[32m 288\u001b[39m transformed = transforms(untransformed)\n", + "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/data/src/pyearthtools/data/indexes/_indexes.py:205\u001b[39m, in \u001b[36mFileSystemIndex.get\u001b[39m\u001b[34m(self, *args, **kwargs)\u001b[39m\n\u001b[32m 203\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m.load(\u001b[38;5;28mself\u001b[39m.search(*args), **kwargs)\n\u001b[32m 204\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[32m--> \u001b[39m\u001b[32m205\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m DataNotFoundError(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mData with args: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mstr\u001b[39m(args)\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m could not be found.\u001b[39m\u001b[33m\"\u001b[39m) \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01me\u001b[39;00m\n", + "\u001b[31mDataNotFoundError\u001b[39m: Data with args: (Petdt('2000-05-03T12'),) could not be found." + ] + } + ], + "source": [ + "# works\n", + "data_pipeline[\"2000-05-05\"][0]\n", + "# does not work\n", + "data_pipeline[\"2000-05-05T00\"][0]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e2f2843f-2afb-4570-8f7d-b88854d8d706", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 71ed8bf2bf288ca357fb4aecbc9d441b3ecbc08e Mon Sep 17 00:00:00 2001 From: Nikeeth Ramanathan Date: Tue, 17 Mar 2026 11:27:23 +1100 Subject: [PATCH 15/16] [skip ci][wip] update persistence pipeline examples and notebook --- .../persistence/examples/zigc.py | 4 +- .../notebooks/pipeline_example.ipynb | 1165 +++++++++++++++-- .../bundled_models/persistence/pyproject.toml | 2 + 3 files changed, 1082 insertions(+), 89 deletions(-) diff --git a/packages/bundled_models/persistence/examples/zigc.py b/packages/bundled_models/persistence/examples/zigc.py index 31ff8401..3a1a51d3 100644 --- a/packages/bundled_models/persistence/examples/zigc.py +++ b/packages/bundled_models/persistence/examples/zigc.py @@ -212,7 +212,7 @@ def run_example(ds_input, use_real=True, backend="zig", num_workers=1, num_chunk # CAUTION: windows/mac - see WHEN IN DOUBT above, except it applies ALMOST ALWAYS. # --- NUM_WORKERS = 1 - NUM_CHUNKS = 5 + NUM_CHUNKS = 1 try: multiprocessing.set_start_method("forkserver") @@ -225,7 +225,7 @@ def run_example(ds_input, use_real=True, backend="zig", num_workers=1, num_chunk ds_input, use_real=False, backend="zig", - num_workers=NUM_CHUNKS, + num_workers=NUM_WORKERS, num_chunks=NUM_CHUNKS, ) # NOTE: second run can be a bit faster as it likely does some caching, so actual times (not diff --git a/packages/bundled_models/persistence/notebooks/pipeline_example.ipynb b/packages/bundled_models/persistence/notebooks/pipeline_example.ipynb index a4c90af9..3a9db744 100644 --- a/packages/bundled_models/persistence/notebooks/pipeline_example.ipynb +++ b/packages/bundled_models/persistence/notebooks/pipeline_example.ipynb @@ -39,6 +39,10 @@ "import pathlib\n", "import xarray as xr\n", "from pathlib import Path\n", + "import pyearthtools.tutorial\n", + "import pyearthtools.pipeline\n", + "\n", + "# The following are derived from the mini demo fourcastnext tutorial but not used here.\n", "\n", "# ---\n", "# unsure what these are for:\n", @@ -61,9 +65,6 @@ "# import fourcastnext\n", "# ---\n", "\n", - "import pyearthtools.tutorial\n", - "import pyearthtools.pipeline\n", - "\n", "# ---\n", "# no gpu required\n", "# ---\n", @@ -81,23 +82,22 @@ "name": "stdout", "output_type": "stream", "text": [ - "This tutorial will download a copy of the input data to /var/tmp/era5demo_persistence. It will also create model checkpoint files and other data here.\n" + "This tutorial will download a copy of the input data to /var/tmp/era5demo_persistence. It will also create model checkpoint files and other data here.\n", + "This is a light weight example and will only use the following variables: ['10m_u_component_of_wind', '10m_v_component_of_wind', '2m_temperature', 'mean_sea_level_pressure']\n" ] } ], "source": [ "workdir = os.environ['ERA5LOWRESDEMO']\n", - "print(f'This tutorial will download a copy of the input data to {workdir}. It will also create model checkpoint files and other data here.')\n", - "\n", - "# ---\n", - "# this does not work, but it may not be needed\n", - "# ---\n", - "# if pyearthtools.data.archive.ROOT_DIRECTORIES['era5lowresdemo'] != workdir:\n", - "# print(\"There is some misconfiguration of your working directory, please review the commented out cells at the start of the notebook\")\n", - "# ---\n", - "\n", "file_location = workdir + '/mini.nc'\n", - "\n" + "name_vars = [\n", + " '10m_u_component_of_wind', \n", + " '10m_v_component_of_wind', \n", + " '2m_temperature', \n", + " 'mean_sea_level_pressure',\n", + "]\n", + "print(f'This tutorial will download a copy of the input data to {workdir}. It will also create model checkpoint files and other data here.')\n", + "print(f\"This is a light weight example and will only use the following variables: {name_vars}\")" ] }, { @@ -118,19 +118,8 @@ "if not os.path.exists(file_location):\n", " print(\"Training data not found, downloading around 2.8GB of data\")\n", " era5_lowres = xr.open_zarr('gs://weatherbench2/datasets/era5/1959-2022-6h-64x32_equiangular_conservative.zarr')\n", - " subset = era5_lowres[['10m_u_component_of_wind', \n", - " '10m_v_component_of_wind', \n", - " '2m_temperature', \n", - " 'mean_sea_level_pressure',\n", - " #'geopotential', # Uncomment this to fetch additional data\n", - " #'toa_incident_solar_radiation_6hr', # Uncomment this to fetch additional data\n", - " #'temperature' # Uncomment this to fetch additional data\n", - " ]]\n", - "\n", - " # bilevel = subset.sel({'level': [50, 500]}) Uncomment if fetching addtional data \n", - " # bilevel.to_netcdf(file_location)\n", - "\n", - " subset.to_netcdf(file_location) # Comment this out if using the bilevel data instead\n", + " subset = era5_lowres[name_vars]\n", + " subset.to_netcdf(file_location)\n", " print(f\"Wrote file to {file_location}\")\n", " assert os.path.exists(file_location)\n", "else:\n", @@ -144,18 +133,15 @@ "metadata": {}, "outputs": [], "source": [ - "accessor = pyearthtools.tutorial.ERA5DataClass.ERA5LowResDemoIndex([\n", - " '10m_u_component_of_wind', \n", - " '10m_v_component_of_wind', \n", - " 'mean_sea_level_pressure',\n", - " '2m_temperature' \n", - "],\n", - "filename_override=file_location)" + "accessor = pyearthtools.tutorial.ERA5DataClass.ERA5LowResDemoIndex(\n", + " name_vars,\n", + " filename_override=file_location\n", + ")" ] }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 6, "id": "3ad9c174-c714-42da-8fc8-02b13e27bbd0", "metadata": {}, "outputs": [], @@ -172,15 +158,17 @@ "# c = interval or how many values to skip\n", "# ---\n", "# TemporalRetrieval does the same, but each index is a delta-unit mapping - so we need to reverse engineer a bit\n", - "# !!! IMPORTANT. The circumstance here is that time is already 6 hourly windows, \n", - "# What this means is this:\n", - "# - selecting 1 index =>\n", + "# !!! IMPORTANT !!!\n", + "# > The circumstance here is that time is already 6 hourly windows and in chunks of 4.\n", + "# > Something about the behaviour has changed, causing the window to select 4 indices at a time,\n", + "# > so specifying `a = -6` actually fetches data from 4 * 6 * 6 = 6 days in the past, rather than 36 hours in the past.\n", + "# > Unfortunately, this means we can't propagate in timesteps of 6 hours without some manual tweaks.\n", "# ---\n", "import datetime\n", "import functools\n", "data_pipeline = pyearthtools.pipeline.Pipeline(\n", " accessor,\n", - " pyearthtools.data.transforms.coordinates.StandardLongitude(type=\"-180-180\"),\n", + " pyearthtools.data.transforms.coordinates.StandardLongitude(type=\"0-360\"),\n", " pyearthtools.pipeline.modifications.TemporalWindow(\n", " prior_indexes=list(range(-6,0,1)),\n", " posterior_indexes=[0],\n", @@ -193,69 +181,1072 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 7, "id": "576b55de-5494-445c-9888-52bffdfb85f7", "metadata": {}, "outputs": [ { - "ename": "DataNotFoundError", - "evalue": "Data with args: (Petdt('2000-05-03T12'),) could not be found.", - "output_type": "error", - "traceback": [ - "\u001b[31m---------------------------------------------------------------------------\u001b[39m", - "\u001b[31mKeyError\u001b[39m Traceback (most recent call last)", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/bundled_models/persistence/.pixi/envs/dev/lib/python3.13/site-packages/xarray/backends/file_manager.py:219\u001b[39m, in \u001b[36mCachingFileManager._acquire_with_cache_info\u001b[39m\u001b[34m(self, needs_lock)\u001b[39m\n\u001b[32m 218\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m219\u001b[39m file = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_cache\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_key\u001b[49m\u001b[43m]\u001b[49m\n\u001b[32m 220\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mKeyError\u001b[39;00m:\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/bundled_models/persistence/.pixi/envs/dev/lib/python3.13/site-packages/xarray/backends/lru_cache.py:56\u001b[39m, in \u001b[36mLRUCache.__getitem__\u001b[39m\u001b[34m(self, key)\u001b[39m\n\u001b[32m 55\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m \u001b[38;5;28mself\u001b[39m._lock:\n\u001b[32m---> \u001b[39m\u001b[32m56\u001b[39m value = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_cache\u001b[49m\u001b[43m[\u001b[49m\u001b[43mkey\u001b[49m\u001b[43m]\u001b[49m\n\u001b[32m 57\u001b[39m \u001b[38;5;28mself\u001b[39m._cache.move_to_end(key)\n", - "\u001b[31mKeyError\u001b[39m: [, ('/var/tmp/era5demo_persistence/mini.nc',), 'r', (('decode_vlen_strings', True), ('driver', None), ('format', 'NETCDF4'), ('invalid_netcdf', None), ('phony_dims', 'access')), '08669add-4cf2-4250-a554-3fdbad8525cb']", - "\nDuring handling of the above exception, another exception occurred:\n", - "\u001b[31mImportError\u001b[39m Traceback (most recent call last)", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/data/src/pyearthtools/data/indexes/_indexes.py:203\u001b[39m, in \u001b[36mFileSystemIndex.get\u001b[39m\u001b[34m(self, *args, **kwargs)\u001b[39m\n\u001b[32m 202\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m203\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mload\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43msearch\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 204\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/tutorial/src/pyearthtools/tutorial/ERA5DataClass.py:309\u001b[39m, in \u001b[36mERA5LowResDemoIndex.load\u001b[39m\u001b[34m(self, *args, **kwargs)\u001b[39m\n\u001b[32m 307\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m.dataset\n\u001b[32m--> \u001b[39m\u001b[32m309\u001b[39m ds = \u001b[43mxr\u001b[49m\u001b[43m.\u001b[49m\u001b[43mopen_dataset\u001b[49m\u001b[43m(\u001b[49m\u001b[43margs\u001b[49m\u001b[43m[\u001b[49m\u001b[32;43m0\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m[\u001b[49m\u001b[32;43m0\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mengine\u001b[49m\u001b[43m=\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43mh5netcdf\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[32m 310\u001b[39m \u001b[38;5;28mself\u001b[39m.dataset = ds\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/bundled_models/persistence/.pixi/envs/dev/lib/python3.13/site-packages/xarray/backends/api.py:606\u001b[39m, in \u001b[36mopen_dataset\u001b[39m\u001b[34m(filename_or_obj, engine, chunks, cache, decode_cf, mask_and_scale, decode_times, decode_timedelta, use_cftime, concat_characters, decode_coords, drop_variables, create_default_indexes, inline_array, chunked_array_type, from_array_kwargs, backend_kwargs, **kwargs)\u001b[39m\n\u001b[32m 605\u001b[39m overwrite_encoded_chunks = kwargs.pop(\u001b[33m\"\u001b[39m\u001b[33moverwrite_encoded_chunks\u001b[39m\u001b[33m\"\u001b[39m, \u001b[38;5;28;01mNone\u001b[39;00m)\n\u001b[32m--> \u001b[39m\u001b[32m606\u001b[39m backend_ds = \u001b[43mbackend\u001b[49m\u001b[43m.\u001b[49m\u001b[43mopen_dataset\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 607\u001b[39m \u001b[43m \u001b[49m\u001b[43mfilename_or_obj\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 608\u001b[39m \u001b[43m \u001b[49m\u001b[43mdrop_variables\u001b[49m\u001b[43m=\u001b[49m\u001b[43mdrop_variables\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 609\u001b[39m \u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mdecoders\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 610\u001b[39m \u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 611\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 612\u001b[39m ds = _dataset_from_backend_dataset(\n\u001b[32m 613\u001b[39m backend_ds,\n\u001b[32m 614\u001b[39m filename_or_obj,\n\u001b[32m (...)\u001b[39m\u001b[32m 625\u001b[39m **kwargs,\n\u001b[32m 626\u001b[39m )\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/bundled_models/persistence/.pixi/envs/dev/lib/python3.13/site-packages/xarray/backends/h5netcdf_.py:540\u001b[39m, in \u001b[36mH5netcdfBackendEntrypoint.open_dataset\u001b[39m\u001b[34m(self, filename_or_obj, mask_and_scale, decode_times, concat_characters, decode_coords, drop_variables, use_cftime, decode_timedelta, format, group, lock, invalid_netcdf, phony_dims, decode_vlen_strings, driver, driver_kwds, storage_options)\u001b[39m\n\u001b[32m 539\u001b[39m filename_or_obj = _normalize_filename_or_obj(filename_or_obj)\n\u001b[32m--> \u001b[39m\u001b[32m540\u001b[39m store = \u001b[43mH5NetCDFStore\u001b[49m\u001b[43m.\u001b[49m\u001b[43mopen\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 541\u001b[39m \u001b[43m \u001b[49m\u001b[43mfilename_or_obj\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 542\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43mformat\u001b[39;49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mformat\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 543\u001b[39m \u001b[43m \u001b[49m\u001b[43mgroup\u001b[49m\u001b[43m=\u001b[49m\u001b[43mgroup\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 544\u001b[39m \u001b[43m \u001b[49m\u001b[43mlock\u001b[49m\u001b[43m=\u001b[49m\u001b[43mlock\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 545\u001b[39m \u001b[43m \u001b[49m\u001b[43minvalid_netcdf\u001b[49m\u001b[43m=\u001b[49m\u001b[43minvalid_netcdf\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 546\u001b[39m \u001b[43m \u001b[49m\u001b[43mphony_dims\u001b[49m\u001b[43m=\u001b[49m\u001b[43mphony_dims\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 547\u001b[39m \u001b[43m \u001b[49m\u001b[43mdecode_vlen_strings\u001b[49m\u001b[43m=\u001b[49m\u001b[43mdecode_vlen_strings\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 548\u001b[39m \u001b[43m \u001b[49m\u001b[43mdriver\u001b[49m\u001b[43m=\u001b[49m\u001b[43mdriver\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 549\u001b[39m \u001b[43m \u001b[49m\u001b[43mdriver_kwds\u001b[49m\u001b[43m=\u001b[49m\u001b[43mdriver_kwds\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 550\u001b[39m \u001b[43m \u001b[49m\u001b[43mstorage_options\u001b[49m\u001b[43m=\u001b[49m\u001b[43mstorage_options\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 551\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 553\u001b[39m store_entrypoint = StoreBackendEntrypoint()\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/bundled_models/persistence/.pixi/envs/dev/lib/python3.13/site-packages/xarray/backends/h5netcdf_.py:242\u001b[39m, in \u001b[36mH5NetCDFStore.open\u001b[39m\u001b[34m(cls, filename, mode, format, group, lock, autoclose, invalid_netcdf, phony_dims, decode_vlen_strings, driver, driver_kwds, storage_options)\u001b[39m\n\u001b[32m 240\u001b[39m manager = manager_cls(h5netcdf.File, filename, mode=mode, kwargs=kwargs)\n\u001b[32m--> \u001b[39m\u001b[32m242\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mcls\u001b[39;49m\u001b[43m(\u001b[49m\n\u001b[32m 243\u001b[39m \u001b[43m \u001b[49m\u001b[43mmanager\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 244\u001b[39m \u001b[43m \u001b[49m\u001b[43mgroup\u001b[49m\u001b[43m=\u001b[49m\u001b[43mgroup\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 245\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43mformat\u001b[39;49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mformat\u001b[39;49m\u001b[43m,\u001b[49m\n\u001b[32m 246\u001b[39m \u001b[43m \u001b[49m\u001b[43mmode\u001b[49m\u001b[43m=\u001b[49m\u001b[43mmode\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 247\u001b[39m \u001b[43m \u001b[49m\u001b[43mlock\u001b[49m\u001b[43m=\u001b[49m\u001b[43mlock\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 248\u001b[39m \u001b[43m \u001b[49m\u001b[43mautoclose\u001b[49m\u001b[43m=\u001b[49m\u001b[43mautoclose\u001b[49m\u001b[43m,\u001b[49m\n\u001b[32m 249\u001b[39m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/bundled_models/persistence/.pixi/envs/dev/lib/python3.13/site-packages/xarray/backends/h5netcdf_.py:152\u001b[39m, in \u001b[36mH5NetCDFStore.__init__\u001b[39m\u001b[34m(self, manager, group, mode, format, lock, autoclose)\u001b[39m\n\u001b[32m 150\u001b[39m \u001b[38;5;66;03m# todo: utilizing find_root_and_group seems a bit clunky\u001b[39;00m\n\u001b[32m 151\u001b[39m \u001b[38;5;66;03m# making filename available on h5netcdf.Group seems better\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m152\u001b[39m \u001b[38;5;28mself\u001b[39m._filename = find_root_and_group(\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mds\u001b[49m)[\u001b[32m0\u001b[39m].filename\n\u001b[32m 153\u001b[39m \u001b[38;5;28mself\u001b[39m.is_remote = is_remote_uri(\u001b[38;5;28mself\u001b[39m._filename)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/bundled_models/persistence/.pixi/envs/dev/lib/python3.13/site-packages/xarray/backends/h5netcdf_.py:260\u001b[39m, in \u001b[36mH5NetCDFStore.ds\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 258\u001b[39m \u001b[38;5;129m@property\u001b[39m\n\u001b[32m 259\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mds\u001b[39m(\u001b[38;5;28mself\u001b[39m):\n\u001b[32m--> \u001b[39m\u001b[32m260\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_acquire\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/bundled_models/persistence/.pixi/envs/dev/lib/python3.13/site-packages/xarray/backends/h5netcdf_.py:252\u001b[39m, in \u001b[36mH5NetCDFStore._acquire\u001b[39m\u001b[34m(self, needs_lock)\u001b[39m\n\u001b[32m 251\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34m_acquire\u001b[39m(\u001b[38;5;28mself\u001b[39m, needs_lock=\u001b[38;5;28;01mTrue\u001b[39;00m):\n\u001b[32m--> \u001b[39m\u001b[32m252\u001b[39m \u001b[43m \u001b[49m\u001b[38;5;28;43;01mwith\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_manager\u001b[49m\u001b[43m.\u001b[49m\u001b[43macquire_context\u001b[49m\u001b[43m(\u001b[49m\u001b[43mneeds_lock\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43;01mas\u001b[39;49;00m\u001b[43m \u001b[49m\u001b[43mroot\u001b[49m\u001b[43m:\u001b[49m\n\u001b[32m 253\u001b[39m \u001b[43m \u001b[49m\u001b[43mds\u001b[49m\u001b[43m \u001b[49m\u001b[43m=\u001b[49m\u001b[43m \u001b[49m\u001b[43m_nc4_require_group\u001b[49m\u001b[43m(\u001b[49m\n\u001b[32m 254\u001b[39m \u001b[43m \u001b[49m\u001b[43mroot\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_group\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_mode\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcreate_group\u001b[49m\u001b[43m=\u001b[49m\u001b[43m_h5netcdf_create_group\u001b[49m\n\u001b[32m 255\u001b[39m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/bundled_models/persistence/.pixi/envs/dev/lib/python3.13/contextlib.py:141\u001b[39m, in \u001b[36m_GeneratorContextManager.__enter__\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 140\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m141\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mnext\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mgen\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 142\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mStopIteration\u001b[39;00m:\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/bundled_models/persistence/.pixi/envs/dev/lib/python3.13/site-packages/xarray/backends/file_manager.py:207\u001b[39m, in \u001b[36mCachingFileManager.acquire_context\u001b[39m\u001b[34m(self, needs_lock)\u001b[39m\n\u001b[32m 206\u001b[39m \u001b[38;5;250m\u001b[39m\u001b[33;03m\"\"\"Context manager for acquiring a file.\"\"\"\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m207\u001b[39m file, cached = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_acquire_with_cache_info\u001b[49m\u001b[43m(\u001b[49m\u001b[43mneeds_lock\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 208\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/bundled_models/persistence/.pixi/envs/dev/lib/python3.13/site-packages/xarray/backends/file_manager.py:225\u001b[39m, in \u001b[36mCachingFileManager._acquire_with_cache_info\u001b[39m\u001b[34m(self, needs_lock)\u001b[39m\n\u001b[32m 224\u001b[39m kwargs[\u001b[33m\"\u001b[39m\u001b[33mmode\u001b[39m\u001b[33m\"\u001b[39m] = \u001b[38;5;28mself\u001b[39m._mode\n\u001b[32m--> \u001b[39m\u001b[32m225\u001b[39m file = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_opener\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_args\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 226\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m._mode == \u001b[33m\"\u001b[39m\u001b[33mw\u001b[39m\u001b[33m\"\u001b[39m:\n\u001b[32m 227\u001b[39m \u001b[38;5;66;03m# ensure file doesn't get overridden when opened again\u001b[39;00m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/bundled_models/persistence/.pixi/envs/dev/lib/python3.13/site-packages/h5netcdf/core.py:1892\u001b[39m, in \u001b[36mFile.__init__\u001b[39m\u001b[34m(self, path, mode, format, invalid_netcdf, phony_dims, backend, **kwargs)\u001b[39m\n\u001b[32m 1891\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m-> \u001b[39m\u001b[32m1892\u001b[39m \u001b[38;5;28mself\u001b[39m._backend = \u001b[43m_parse_backend\u001b[49m\u001b[43m(\u001b[49m\u001b[43mpath\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mmode\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mbackend\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 1893\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.backend == \u001b[33m\"\u001b[39m\u001b[33mpyfive\u001b[39m\u001b[33m\"\u001b[39m:\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/bundled_models/persistence/.pixi/envs/dev/lib/python3.13/site-packages/h5netcdf/core.py:174\u001b[39m, in \u001b[36m_parse_backend\u001b[39m\u001b[34m(path, mode, backend, **kwargs)\u001b[39m\n\u001b[32m 173\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m no_backend.get(backend, \u001b[38;5;28;01mFalse\u001b[39;00m):\n\u001b[32m--> \u001b[39m\u001b[32m174\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mImportError\u001b[39;00m(\n\u001b[32m 175\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mNo module named \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mbackend\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[33m, backend not available. \u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 176\u001b[39m \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mPlease install \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mbackend\u001b[38;5;132;01m!r}\u001b[39;00m\u001b[33m into your Python environment.\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 177\u001b[39m )\n\u001b[32m 179\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m backend\n", - "\u001b[31mImportError\u001b[39m: No module named 'h5py', backend not available. Please install 'h5py' into your Python environment.", - "\nThe above exception was the direct cause of the following exception:\n", - "\u001b[31mDataNotFoundError\u001b[39m Traceback (most recent call last)", - "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[30]\u001b[39m\u001b[32m, line 4\u001b[39m\n\u001b[32m 2\u001b[39m data_pipeline[\u001b[33m\"\u001b[39m\u001b[33m2000-05-05\u001b[39m\u001b[33m\"\u001b[39m][\u001b[32m0\u001b[39m]\n\u001b[32m 3\u001b[39m \u001b[38;5;66;03m# does not work\u001b[39;00m\n\u001b[32m----> \u001b[39m\u001b[32m4\u001b[39m \u001b[43mdata_pipeline\u001b[49m\u001b[43m[\u001b[49m\u001b[33;43m\"\u001b[39;49m\u001b[33;43m2000-05-05T00\u001b[39;49m\u001b[33;43m\"\u001b[39;49m\u001b[43m]\u001b[49m[\u001b[32m0\u001b[39m]\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/pipeline/src/pyearthtools/pipeline/controller.py:551\u001b[39m, in \u001b[36mPipeline.__getitem__\u001b[39m\u001b[34m(self, idx)\u001b[39m\n\u001b[32m 543\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mmap\u001b[39m(\u001b[38;5;28mself\u001b[39m.\u001b[34m__getitem__\u001b[39m, indexes)\n\u001b[32m 545\u001b[39m \u001b[38;5;66;03m# Start the pipeline with the raw/initial data\u001b[39;00m\n\u001b[32m 546\u001b[39m \u001b[38;5;66;03m# `idx` here is the index of the sample within the dataset, not the\u001b[39;00m\n\u001b[32m 547\u001b[39m \u001b[38;5;66;03m# position of the step within the list of steps\u001b[39;00m\n\u001b[32m 548\u001b[39m \u001b[38;5;66;03m# `sample` is actual data\u001b[39;00m\n\u001b[32m 549\u001b[39m \u001b[38;5;66;03m# `step_index` *is* the index of the sample provier within the list of steps\u001b[39;00m\n\u001b[32m 550\u001b[39m \u001b[38;5;66;03m# Initial just means untransformed by the pipeline\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m551\u001b[39m sample, step_index = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_get_initial_sample\u001b[49m\u001b[43m(\u001b[49m\u001b[43midx\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 552\u001b[39m LOG.debug(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mCall pipeline __getitem__ for \u001b[39m\u001b[38;5;132;01m{\u001b[39;00midx\u001b[38;5;250m \u001b[39m\u001b[38;5;132;01m= }\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m 554\u001b[39m \u001b[38;5;66;03m# Apply each pipeline step to the sample, starting from the latest source\u001b[39;00m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/pipeline/src/pyearthtools/pipeline/controller.py:521\u001b[39m, in \u001b[36mPipeline._get_initial_sample\u001b[39m\u001b[34m(self, idx)\u001b[39m\n\u001b[32m 519\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(step, PipelineIndex):\n\u001b[32m 520\u001b[39m LOG.debug(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mGetting initial sample from \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mstep\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m at \u001b[39m\u001b[38;5;132;01m{\u001b[39;00midx\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m--> \u001b[39m\u001b[32m521\u001b[39m sample = \u001b[43mstep\u001b[49m\u001b[43m[\u001b[49m\u001b[43midx\u001b[49m\u001b[43m]\u001b[49m\n\u001b[32m 522\u001b[39m whereinthesequence = \u001b[38;5;28mlen\u001b[39m(\u001b[38;5;28mself\u001b[39m.steps) - (index + \u001b[32m1\u001b[39m)\n\u001b[32m 523\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m (sample, whereinthesequence)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/pipeline/src/pyearthtools/pipeline/modifications/idx_modification.py:546\u001b[39m, in \u001b[36mTemporalWindow.__getitem__\u001b[39m\u001b[34m(self, date_of_interest)\u001b[39m\n\u001b[32m 543\u001b[39m prior_i = [i * \u001b[38;5;28mself\u001b[39m.timedelta \u001b[38;5;28;01mfor\u001b[39;00m i \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m.prior_indexes]\n\u001b[32m 544\u001b[39m posterior_i = [i * \u001b[38;5;28mself\u001b[39m.timedelta \u001b[38;5;28;01mfor\u001b[39;00m i \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mself\u001b[39m.posterior_indexes]\n\u001b[32m--> \u001b[39m\u001b[32m546\u001b[39m prior = [\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mparent_pipeline\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;28;43mstr\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43mdate_of_interest\u001b[49m\u001b[43m \u001b[49m\u001b[43m+\u001b[49m\u001b[43m \u001b[49m\u001b[43mdelta\u001b[49m\u001b[43m)\u001b[49m\u001b[43m]\u001b[49m \u001b[38;5;28;01mfor\u001b[39;00m delta \u001b[38;5;129;01min\u001b[39;00m prior_i]\n\u001b[32m 547\u001b[39m posterior = [\u001b[38;5;28mself\u001b[39m.parent_pipeline()[\u001b[38;5;28mstr\u001b[39m(date_of_interest + delta)] \u001b[38;5;28;01mfor\u001b[39;00m delta \u001b[38;5;129;01min\u001b[39;00m posterior_i]\n\u001b[32m 549\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.merge_method:\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/pipeline/src/pyearthtools/pipeline/controller.py:551\u001b[39m, in \u001b[36mPipeline.__getitem__\u001b[39m\u001b[34m(self, idx)\u001b[39m\n\u001b[32m 543\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mmap\u001b[39m(\u001b[38;5;28mself\u001b[39m.\u001b[34m__getitem__\u001b[39m, indexes)\n\u001b[32m 545\u001b[39m \u001b[38;5;66;03m# Start the pipeline with the raw/initial data\u001b[39;00m\n\u001b[32m 546\u001b[39m \u001b[38;5;66;03m# `idx` here is the index of the sample within the dataset, not the\u001b[39;00m\n\u001b[32m 547\u001b[39m \u001b[38;5;66;03m# position of the step within the list of steps\u001b[39;00m\n\u001b[32m 548\u001b[39m \u001b[38;5;66;03m# `sample` is actual data\u001b[39;00m\n\u001b[32m 549\u001b[39m \u001b[38;5;66;03m# `step_index` *is* the index of the sample provier within the list of steps\u001b[39;00m\n\u001b[32m 550\u001b[39m \u001b[38;5;66;03m# Initial just means untransformed by the pipeline\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m551\u001b[39m sample, step_index = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m_get_initial_sample\u001b[49m\u001b[43m(\u001b[49m\u001b[43midx\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 552\u001b[39m LOG.debug(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mCall pipeline __getitem__ for \u001b[39m\u001b[38;5;132;01m{\u001b[39;00midx\u001b[38;5;250m \u001b[39m\u001b[38;5;132;01m= }\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m 554\u001b[39m \u001b[38;5;66;03m# Apply each pipeline step to the sample, starting from the latest source\u001b[39;00m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/pipeline/src/pyearthtools/pipeline/controller.py:528\u001b[39m, in \u001b[36mPipeline._get_initial_sample\u001b[39m\u001b[34m(self, idx)\u001b[39m\n\u001b[32m 526\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(\u001b[38;5;28mself\u001b[39m.steps[\u001b[32m0\u001b[39m], (_Pipeline, Index)):\n\u001b[32m 527\u001b[39m LOG.debug(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mGetting initial sample from \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mself\u001b[39m.steps[\u001b[32m0\u001b[39m]\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m at \u001b[39m\u001b[38;5;132;01m{\u001b[39;00midx\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m)\n\u001b[32m--> \u001b[39m\u001b[32m528\u001b[39m sample = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43msteps\u001b[49m\u001b[43m[\u001b[49m\u001b[32;43m0\u001b[39;49m\u001b[43m]\u001b[49m\u001b[43m[\u001b[49m\u001b[43midx\u001b[49m\u001b[43m]\u001b[49m\n\u001b[32m 529\u001b[39m whereinthesequence = \u001b[32m0\u001b[39m\n\u001b[32m 530\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m sample, whereinthesequence\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/data/src/pyearthtools/data/indexes/utilities/mixins/call_redirect.py:42\u001b[39m, in \u001b[36mCallRedirectMixin.__getitem__\u001b[39m\u001b[34m(self, idx)\u001b[39m\n\u001b[32m 40\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34m__getitem__\u001b[39m(\u001b[38;5;28mself\u001b[39m, idx: Any):\n\u001b[32m 41\u001b[39m \u001b[38;5;250m \u001b[39m\u001b[33;03m\"\"\"[] accessor\"\"\"\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m42\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[34;43m__call__\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43midx\u001b[49m\u001b[43m)\u001b[49m\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/data/src/pyearthtools/data/indexes/_indexes.py:893\u001b[39m, in \u001b[36mAdvancedTimeIndex.__call__\u001b[39m\u001b[34m(self, *args, **kwargs)\u001b[39m\n\u001b[32m 884\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m.series(\n\u001b[32m 885\u001b[39m start_time,\n\u001b[32m 886\u001b[39m end_time,\n\u001b[32m (...)\u001b[39m\u001b[32m 889\u001b[39m **kwargs,\n\u001b[32m 890\u001b[39m )\n\u001b[32m 892\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(args) == \u001b[32m1\u001b[39m:\n\u001b[32m--> \u001b[39m\u001b[32m893\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mretrieve\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 895\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m.series(*args, **kwargs)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/data/src/pyearthtools/data/indexes/_indexes.py:788\u001b[39m, in \u001b[36mAdvancedTimeIndex.retrieve\u001b[39m\u001b[34m(self, querytime, aggregation, select, use_simple, **kwargs)\u001b[39m\n\u001b[32m 785\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m data \u001b[38;5;66;03m# selectdata(querytime, data)\u001b[39;00m\n\u001b[32m 787\u001b[39m \u001b[38;5;28;01melif\u001b[39;00m querytime.resolution == \u001b[38;5;28mself\u001b[39m.data_resolution: \u001b[38;5;66;03m# Equal Resolution\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m788\u001b[39m data = \u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m.\u001b[49m\u001b[43mretrieve\u001b[49m\u001b[43m(\u001b[49m\u001b[43mquerytime\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mselect\u001b[49m\u001b[43m=\u001b[49m\u001b[43mselect\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 789\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m data\n\u001b[32m 791\u001b[39m start_time = querytime.at_resolution(\u001b[38;5;28mself\u001b[39m.data_resolution)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/data/src/pyearthtools/data/indexes/_indexes.py:671\u001b[39m, in \u001b[36mSingleTimeDataIndex.retrieve\u001b[39m\u001b[34m(self, transforms, *args, **kwargs)\u001b[39m\n\u001b[32m 667\u001b[39m \u001b[38;5;66;03m# kwargs.update(self._get_preprocess(kwargs.pop(\"preprocess\", None))) # type: ignore\u001b[39;00m\n\u001b[32m 668\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m ChangeValue(\u001b[38;5;28mself\u001b[39m, \u001b[33m\"\u001b[39m\u001b[33m_skip_transforms\u001b[39m\u001b[33m\"\u001b[39m, \u001b[38;5;28;01mTrue\u001b[39;00m):\n\u001b[32m 669\u001b[39m \u001b[38;5;66;03m# Skip transforms, so that they are only applied once\u001b[39;00m\n\u001b[32m 670\u001b[39m \u001b[38;5;66;03m# By applying transforms after time retrieve, prevents more then necessary data going to transforms\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m671\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m transforms(\u001b[38;5;28;43msuper\u001b[39;49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\u001b[43m.\u001b[49m\u001b[43mretrieve\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/data/src/pyearthtools/data/indexes/_indexes.py:462\u001b[39m, in \u001b[36mSingleTimeIndex.retrieve\u001b[39m\u001b[34m(self, querytime, select, round, *args, **kwargs)\u001b[39m\n\u001b[32m 459\u001b[39m querytime = querytime.at_resolution(\u001b[38;5;28mself\u001b[39m.data_resolution)\n\u001b[32m 461\u001b[39m retrieval_function = \u001b[38;5;28mgetattr\u001b[39m(\u001b[38;5;28msuper\u001b[39m(), \u001b[33m\"\u001b[39m\u001b[33mretrieve\u001b[39m\u001b[33m\"\u001b[39m, \u001b[38;5;28msuper\u001b[39m().get)\n\u001b[32m--> \u001b[39m\u001b[32m462\u001b[39m data = \u001b[43mretrieval_function\u001b[49m\u001b[43m(\u001b[49m\u001b[43mquerytime\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 464\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(data, (xr.Dataset, xr.DataArray)):\n\u001b[32m 465\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m data\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/data/src/pyearthtools/data/indexes/_indexes.py:285\u001b[39m, in \u001b[36mDataIndex.retrieve\u001b[39m\u001b[34m(self, transforms, *args, **kwargs)\u001b[39m\n\u001b[32m 282\u001b[39m kwargs.update(\u001b[38;5;28mself\u001b[39m._get_preprocess(kwargs.pop(\u001b[33m\"\u001b[39m\u001b[33mpreprocess\u001b[39m\u001b[33m\"\u001b[39m, \u001b[38;5;28;01mNone\u001b[39;00m))) \u001b[38;5;66;03m# type: ignore\u001b[39;00m\n\u001b[32m 284\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m._skip_transforms:\n\u001b[32m--> \u001b[39m\u001b[32m285\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43mget\u001b[49m\u001b[43m(\u001b[49m\u001b[43m*\u001b[49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43m*\u001b[49m\u001b[43m*\u001b[49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 287\u001b[39m untransformed = \u001b[38;5;28mself\u001b[39m.get(*args, **kwargs)\n\u001b[32m 288\u001b[39m transformed = transforms(untransformed)\n", - "\u001b[36mFile \u001b[39m\u001b[32m~/repos/projects/PyEarthTools/packages/data/src/pyearthtools/data/indexes/_indexes.py:205\u001b[39m, in \u001b[36mFileSystemIndex.get\u001b[39m\u001b[34m(self, *args, **kwargs)\u001b[39m\n\u001b[32m 203\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m.load(\u001b[38;5;28mself\u001b[39m.search(*args), **kwargs)\n\u001b[32m 204\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mException\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[32m--> \u001b[39m\u001b[32m205\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m DataNotFoundError(\u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mData with args: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;28mstr\u001b[39m(args)\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m could not be found.\u001b[39m\u001b[33m\"\u001b[39m) \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01me\u001b[39;00m\n", - "\u001b[31mDataNotFoundError\u001b[39m: Data with args: (Petdt('2000-05-03T12'),) could not be found." - ] + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.Dataset> Size: 787kB\n",
+       "Dimensions:                  (time: 24, longitude: 64, latitude: 32)\n",
+       "Coordinates:\n",
+       "  * time                     (time) datetime64[ns] 192B 2000-05-03 ... 2000-0...\n",
+       "  * longitude                (longitude) float64 512B 0.0 5.625 ... 348.8 354.4\n",
+       "  * latitude                 (latitude) float64 256B -87.19 -81.56 ... 87.19\n",
+       "Data variables:\n",
+       "    10m_v_component_of_wind  (time, longitude, latitude) float32 197kB dask.array<chunksize=(4, 36, 18), meta=np.ndarray>\n",
+       "    2m_temperature           (time, longitude, latitude) float32 197kB dask.array<chunksize=(4, 36, 18), meta=np.ndarray>\n",
+       "    10m_u_component_of_wind  (time, longitude, latitude) float32 197kB dask.array<chunksize=(4, 36, 18), meta=np.ndarray>\n",
+       "    mean_sea_level_pressure  (time, longitude, latitude) float32 197kB dask.array<chunksize=(4, 36, 18), meta=np.ndarray>
" + ], + "text/plain": [ + " Size: 787kB\n", + "Dimensions: (time: 24, longitude: 64, latitude: 32)\n", + "Coordinates:\n", + " * time (time) datetime64[ns] 192B 2000-05-03 ... 2000-0...\n", + " * longitude (longitude) float64 512B 0.0 5.625 ... 348.8 354.4\n", + " * latitude (latitude) float64 256B -87.19 -81.56 ... 87.19\n", + "Data variables:\n", + " 10m_v_component_of_wind (time, longitude, latitude) float32 197kB dask.array\n", + " 2m_temperature (time, longitude, latitude) float32 197kB dask.array\n", + " 10m_u_component_of_wind (time, longitude, latitude) float32 197kB dask.array\n", + " mean_sea_level_pressure (time, longitude, latitude) float32 197kB dask.array" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "# works\n", + "# Check that accessor works through the pipeline:\n", "data_pipeline[\"2000-05-05\"][0]\n", - "# does not work\n", - "data_pipeline[\"2000-05-05T00\"][0]" + "# ---\n", + "# DOES NOT WORK - needs investigation\n", + "# data_pipeline[\"2000-05-05T00\"][0]\n", + "# --" + ] + }, + { + "cell_type": "markdown", + "id": "ae31bd8a-df44-41b4-989c-25b0032b39d5", + "metadata": {}, + "source": [ + "## Run persistence model as \"inference\"\n", + "\n", + "This does the on-the-fly calculation based on historical data, the most recent index of the dataset is assumed to be the \"base\" forecast time." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "id": "e2f2843f-2afb-4570-8f7d-b88854d8d706", "metadata": {}, "outputs": [], - "source": [] + "source": [ + "from persistence import persistence_impl\n", + "\n", + "num_workers=1 # IMPORTANT: set this to 1 to force everything to run on the main thread (preferrable)\n", + "num_chunks=1 # tunable depending on data size\n", + "ds_input = data_pipeline[\"2010-01-01\"][0]\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "9ed67f8c-8237-4d0e-aa8c-ef137fab2452", + "metadata": {}, + "outputs": [], + "source": [ + "ds_output = persistence_impl.predict(\n", + " ds_input,\n", + " idx_time_dim=list(ds_input.dims).index(\"time\"),\n", + " num_workers=num_workers,\n", + " num_chunks=num_chunks,\n", + " method=\"median_of_three\",\n", + " simple_impute=False,\n", + " backend_type=\"zig\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "50c46911-9007-4b65-94fa-8c003ce0f567", + "metadata": {}, + "outputs": [], + "source": [ + "# TODO: the following should have been auto-handled by a Predictor wrapper class that does any _required_ \"undo-ing\" of the forward pipeline.\n", + "import copy\n", + "coords = copy.deepcopy(ds_input.coords)\n", + "del coords[\"time\"]\n", + "ds_output = ds_output.assign_coords(coords)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "d7c236e9-8a83-49d9-a002-a9d453b1c130", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAj4AAAGwCAYAAACpYG+ZAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAaKRJREFUeJzt3Xl8VNX9P/7XLJnJOgnZg4SwKYssIiAGFFEwgHxdad1AFv3g0sQKKIW0KgitQVyrRWh/KmALalUQRERBFkUDYgQRhFRSIAgJIJh9m+X+/kgzMuS8h8xkSGaY1/PxmMfDnLudu8x4OPfe89JpmqaBiIiIKAjoW7sCRERERC2FDR8iIiIKGmz4EBERUdBgw4eIiIiCBhs+REREFDTY8CEiIqKgwYYPERERBQ1ja1fAnzgcDhw7dgxRUVHQ6XStXR0iIvJjmqahvLwcbdu2hV5//voRampqUFdX1+z1mEwmhIaG+qBGgY0NnzMcO3YMqamprV0NIiIKIEeOHEG7du3Oy7pramrQMS0SxSfszV5XcnIyDh48GPSNHzZ8zhAVFQUAGDxgOoxGc5OX0/Tq3iGHyaAst4Wry60R8r8YNPUisJuEbYcI62mBjiydp2OBu5lfJ3zX9Xb1QjqHZ9vwuK7+ytP9c3fMHeqJeqt6fkOd+qAbatTl+jr5B1xvEyomDTDv6flzc/1rBs++HDrhGoTQW+wwulm/8BviKa0Ve6rF4+fumEuLSIdWuD70NuGL70UwgfQboil+nm22Wny541nn/zvOh7q6OhSfsONwXgdYorzvVSordyCt3yHU1dWx4dPaFfAnDbe3jEYzjMamXxhiw8cotFZC1OVaiHxRO6QzJTR8dGz4NGkbwdrwEY8T3DR8hPkNmtDwEf5npHe4afhIO+KPDR/x4LLh48KXDR/hhOvROg0f5zItcNwjo3SIjPJ+Ow53JyLIsOFDRETk5+yaA1InY1OXp3ps+BAREfk5BzQ4PO7idF2e6vF1diIiIgoa7PEhIiLycw44pCeZmrw81WPDh4iIyM/ZNQ12Lx7WPnN5qsdbXURERBQ02ONDRETk5/hws++w4UNEROTnHNBgZ8PHJ3iri4iIiIIGe3wUyjuEwWBqPHKzzexZPIRUbgvzbH7Au9FEVQy1npUDclSBXhpV2ar+l4WxRj2/sVp+20CKMJCGrrdGqEfFlo6ttG+Au7gHoU7S4MI+HDDVIBxbdyMxe0wYJU0cLVs6R8II0G5Hy5aWEUaB1jmEHfdmpDfpuySMyqsZ1QtoQliloUYesVqKv9BVqYMpdadL1Csyqn/StZhIcdvWBPU0W5j6u1QXrS63hwijyLu5NqXvt6FWKBdiUGrbqPfbm21L16dq5G2b8H08H3iry3fY8CEiIvJzfKvLd3iri4iIiIIGe3yIiIj8nON/n+YsT/XY8CEiIvJz9ma+1dWcZS80bPgQERH5Obvm3TP7Zy5P9fiMDxEREQUN9vgQERH5OT7j4zts+BAREfk5B3Sww/tBwRzNWPZCw1tdREREFDTY40NEROTnHJo4sHmTl6d6bPgolFysgz5UMTx5pPouqSNMGE6/zsOuRXf9b8INWr1N2IZdXW6sVJeHVMibDv1FvXFjjW/iE0IqbOI0Y6kwZL8wCmmYVR0LYG2jzgmpiTeJ25ZiLnTCMZdiB6RoDynSAZCH/7eb1BeJGA8h1alO3rbH0RsGD69zdyPIKmIBAAB6dUyC5hCOhxBx4Y5mEuJOhDp5GmkCh3r9AKCzC98BIRbD0TZeWW6NVV/nFRfJ13lpZ/X+WSM9+z+lsUq9nrAT8jLmUnW5FHujCefCHqo+TnUR8rUpxb/URquXqW2j2G6NAfhI3IRP2Zt5q6s5y15oeKuLiIiIggZ7fIiIiPwce3x8hw0fIiIiP+fQdHCI95ubtjzVC5hbXR06dIBOp2v0yczMBAAMHTq00bQHH3ywlWtNRERE/iRgenx27NgBu/3Xp0T37NmD66+/Hr/97W+dZZMnT8acOXOcf4eHh7doHYmIiM4H3urynYBp+CQkJLj8PW/ePHTu3BnXXHONsyw8PBzJycktXTUiIqLzyg497M24SSO8XBqUAuZW15nq6urwr3/9C/feey90ul9bscuWLUN8fDx69uyJ7OxsVFVVuV1PbW0tysrKXD5ERET+RvvfMz7efjQ+4+MUMD0+Z/rggw9QUlKCiRMnOsvuvvtupKWloW3btti9ezdmzJiB/Px8rFixQlxPTk4OnnrqqRaoMREREfmDgGz4vP766xg1ahTatm3rLLv//vud/92rVy+kpKRg2LBhKCgoQOfOnZXryc7OxrRp05x/l5WVITU19fxVnIiIyAt8xsd3Aq7hc/jwYWzYsMFtTw4ADBw4EABw4MABseFjNpthNpt9XkciIiJfsmt62LVmPOPDyAqngHvGZ/HixUhMTMTo0aPdzrdr1y4AQEpKSgvUioiIiAJBQPX4OBwOLF68GBMmTIDR+GvVCwoKsHz5ctxwww2Ii4vD7t27MXXqVAwZMgS9e/f2eDvWGDv0YYpn4FVlAHR6IQ+pUp2PY/pF6HJ00yLXpDMlLBNSri4PPa1eIPxn+Zl/Y5V6WsgvNeoFpHyoU0IwT7WwHgCIFIYksKnrpFki1duuVc8v7RsAGCvV+UmmwtPqBaxWdXlNrbpc76br2aS+dmztE5TlNYmh6vnDhKynKHnbmhApJT0b6RByxaT1mMrkHC2zME3ctpSjJWSX2c3yv/WkjDm9TciNEuoUUilcaxXC9QHAFiVkaQmBcbYodXlNjPqg203y+bYcVO9fTax6mcp2QsZVsnr/rBY5o8zyo/p8hP6i3oZ0vm3qyx9W9c8BAKA8Qb0uh0nKCWtc7qjxPBPOWw7o4GhGX4XD3f9ggkxANXw2bNiAwsJC3HvvvS7lJpMJGzZswEsvvYTKykqkpqZizJgxePzxx1uppkRERL7DZ3x8J6AaPhkZGdAUycepqanYsmVLK9SIiIiIAklANXyIiIiCUfMfbuatrgZs+BAREfm5+md8mhFSyltdTgH3VhcRERGRt9jjQ0RE5Occzczq4ltdv2LDh4iIyM/xGR/f4a0uIiIiP+eAvtkfTyxcuBC9e/eGxWKBxWJBeno6Pv74Y+f0mpoaZGZmIi4uDpGRkRgzZgyOHz/uso7CwkKMHj0a4eHhSExMxPTp02GzqcdHa0ls+BAREZGLdu3aYd68ecjLy8M333yD6667DjfffDP27t0LAJg6dSo+/PBDvPvuu9iyZQuOHTuG2267zbm83W7H6NGjUVdXh6+++gpLly7FkiVL8OSTT7bWLjnxVhcREZGfs2s62KXhwpu4vCduvPFGl7//8pe/YOHChdi2bRvatWuH119/HcuXL8d1110HoD5Oqnv37ti2bRuuvPJKfPrpp/jhhx+wYcMGJCUl4bLLLsPcuXMxY8YMzJ49GyZhdPqWwIaPSrgNCGt6d5z+Z/UJDD8mDOUvHHWdm03qhWQFY7VUrr6fG1IplJfKw+mbjpYoy7Vf1OWwqnfEIXRx6tvEiNuWohu0U8eV5SgtUxYbS6PU5SfchNTqhB+Kykp1uV6Ih6hQz68LE8bZB6ALUUcSGI+p4zLCHG2U5WWdI5TlNXHyj6BdSk+Qfi087DfWdPICIVWexUMYatWRAVL8hKlEiA8BYBBiTST6SmFdwrMUmkn+uZUiNhwmddyDTojRCKmSjod8vo016nWZS9Tz6xzqOpV3VJc7IuTjKkWIFA9UH6u6OGFdZuH3y+omoqRGPU0TIysaH1uH9MN8Htib+XCz/X8PN5eVuf5GNiWs2263491330VlZSXS09ORl5cHq9WK4cOHO+fp1q0b2rdvj9zcXFx55ZXIzc1Fr169kJSU5JxnxIgReOihh7B371707dvX631pLt7qIiIiChKpqamIjo52fnJycsR5v//+e0RGRsJsNuPBBx/EypUr0aNHDxQXF8NkMiEmJsZl/qSkJBQXFwMAiouLXRo9DdMbprUm9vgQERH5OYemh6MZb3U5/tcTeeTIEVgsFme5u96erl27YteuXSgtLcV7772HCRMmXBDxUGz4EBER+Tlf3epqeEurKUwmE7p06QIA6NevH3bs2IG//vWvuOOOO1BXV4eSkhKXXp/jx48jOTkZAJCcnIyvv/7aZX0Nb301zNNaeKuLiIiIzsnhcKC2thb9+vVDSEgIPvvsM+e0/Px8FBYWIj09HQCQnp6O77//HidOnHDOs379elgsFvTo0aPF634m9vgQERH5OQc8fzPr7OU9kZ2djVGjRqF9+/YoLy/H8uXLsXnzZnzyySeIjo7Gfffdh2nTpiE2NhYWiwUPP/ww0tPTceWVVwIAMjIy0KNHD9xzzz2YP38+iouL8fjjjyMzM/OcD1Ofb2z4EBER+TlvBiE8e3lPnDhxAuPHj0dRURGio6PRu3dvfPLJJ7j++usBAC+++CL0ej3GjBmD2tpajBgxAq+++qpzeYPBgDVr1uChhx5Ceno6IiIiMGHCBMyZM8frffAVNnyIiIjIxeuvv+52emhoKBYsWIAFCxaI86SlpWHt2rW+rlqzseFDRETk55qf1cVHehuw4UNEROTnHNDBgeY84+P9shcaNnyIiIj8HHt8fIdHgoiIiIIGe3wUdHoNOn3jvBZ9sfoVPHOJuguxLlrYgNDcNJXIdZLeYrSqo5hQ00a9QEilulwnBTQBMJ1QXyY6aRAsqzo3RydlGLVR52gBgCNSnWdlQJKyXMrqsh0rUtfJoM4XAgB9tHr/dEbhaxOqvj708bHK8tpuKeK2y9qrz4eUQWUPFa7BSPX8Vanyy60Os3CeQoQcKOHa1OzqCVUp8r+3SrsKeWdmob5C7pehXF0eXaDOQAOA+Lxy9Sbs6jwmzSysyybU1c37xJpefawcRvV+1MYIGV7SYXKzbWOlev9sEVJOmLAN4XzrhOsGACp61inLDWZ1nQw69bUp/kC6yVzUIoRlpFwzW+NzoTMI9TkPmj+AIfs5GrDhQ0RE5Occmg6O5ozj04xlLzRsAhIREVHQYI8PERGRn3M081ZXcwY/vNCw4UNEROTnmp/OzoZPAx4JIiIiChrs8SEiIvJzduhgb8YghM1Z9kLDhg8REZGf460u3+GRICIioqDBHh8iIiI/Z0fzbleph4QMTmz4EBER+Tne6vIdNnwUNE0HTTHKpT1OPfx5Vbj6gtIJQ58bqtTzW+XkBujVKRCwhavLHSb1UOo1CdJ65OgGQB25YAtT71/EMfUw9MayGvV6LOpYCgCobKuObtC3V++4qbSNstz8c6J6PT+dELetVVWry9slK8trktX5IbVt1F+zXy6Rf4jq4oRh/oXRV6MOqmevSVRfB45QNxkGJiGaQqquECOgk35dTPK/PaW0AL1BymIQituo5y9Nlf/F/EsP9fmL2afe8bBT6m2Yf5FjEiSOEHW9NKPwG1IrnFdhPRVuYkKsEerojej/1irL9Tb1tmvj1Ce8JkY+5mHR6t8EaZRhvXStCeWq3/FzTbMpoikAwKaavyUjKxhS6jM8EkRERBQ02ONDRETk5zTo4GjGMz4aX2d3YsOHiIjIz/FWl+/wSBAREVHQYI8PERGRn3NoOvGh76YuT/XY8CEiIvJz9mamszdn2QsNjwQREREFDfb4EBER+Tne6vIdNnyIiIj8nAN6OJpxk6Y5y15oAuZIzJ49GzqdzuXTrVs35/SamhpkZmYiLi4OkZGRGDNmDI4fP96KNSYiIiJ/EzANHwC49NJLUVRU5Pxs3brVOW3q1Kn48MMP8e6772LLli04duwYbrvttlasLRERkW/YNV2zP1QvoG51GY1GJCc3zkkqLS3F66+/juXLl+O6664DACxevBjdu3fHtm3bcOWVVyrXV1tbi9raX/NoysrKAADm8DoYFPlbVqs6z8puUJfrQtRZPnYhu8ZaI58OQ4Q6rEsnXMv2amFd1eq2bmU7+UshTjOqc2r0NWZluem0OpNLyiEDgLCf1eV6q3rb1nB17pAWot5vs16duwUAhgohR0g46CEV6oym2lj1uaiLkXN+xCwt4VSUXiqsSNpEiJuMIeGfQ3qjOmPLIFznEodd/veWJlRLyurSC1lJRoO6rqEmOUfLHqE+37/EqDO8yo+pr/Ow4+p8OVOFuGkx/8phUJ/wumj1eqqShSwr6cACcJjU29DZ1ftXIfweCLMD1XIOYE2I+lilJp9Wz29Vf7+l/6nbHZ5fa6HSfkQ0zi6zV9XiiLgF3+IzPr4TUD0+P/74I9q2bYtOnTph7NixKCwsBADk5eXBarVi+PDhznm7deuG9u3bIzc3V1xfTk4OoqOjnZ/U1NTzvg9ERESe0v6Xzu7tR+PIzU4BcyQGDhyIJUuWYN26dVi4cCEOHjyIq6++GuXl5SguLobJZEJMTIzLMklJSSguLhbXmZ2djdLSUufnyJGWarsTERFRawiYW12jRo1y/nfv3r0xcOBApKWl4d///jfCwsK8WqfZbIbZLPVrEhER+Qc7dLA3I2i0OcteaAKmx+dsMTExuOSSS3DgwAEkJyejrq4OJSUlLvMcP35c+UwQERFRIHFovz7n492ntffAfwRsw6eiogIFBQVISUlBv379EBISgs8++8w5PT8/H4WFhUhPT2/FWhIREZE/CZhbXY899hhuvPFGpKWl4dixY5g1axYMBgPuuusuREdH47777sO0adMQGxsLi8WChx9+GOnp6eIbXURERIGi4SHl5ixP9QKm4fPTTz/hrrvuwqlTp5CQkICrrroK27ZtQ0JCAgDgxRdfhF6vx5gxY1BbW4sRI0bg1VdfbeVaExERNZ8DOjia8ZxOc5a90ARMw+ftt992Oz00NBQLFizAggULWqhGREREFGjY90VEROTnWnrk5pycHAwYMABRUVFITEzELbfcgvz8fJd5CgoKcOuttyIhIQEWiwW33357o6io06dPY+zYsbBYLIiJicF9992Higo3o3m2ADZ8iIiI/FxzBi/05vmgLVu2IDMzE9u2bcP69ethtVqRkZGByspKAEBlZSUyMjKg0+mwceNGfPnll6irq8ONN94Ih+PXkdbHjh2LvXv3Yv369VizZg0+//xz3H///T49Np4KmFtdRERE1DLWrVvn8veSJUuQmJiIvLw8DBkyBF9++SUOHTqEnTt3wmKxAACWLl2KNm3aYOPGjRg+fDj27duHdevWYceOHejfvz8A4JVXXsENN9yA5557Dm3btm3x/QLY8FEKNdtgMDcOkEqOLlPOHyLkAhn16nyh4xVRyvJwU51Yp9MV4crymmp11g3qhGyq5CplubucMJ2QhxQepq5vRaFFWW4LV3e1GuTdRllHsVbKUodJXdfyMvX+mX9RnwsA0DmE83RSfV4dwiEsb6c+Fw6z+roBAISpp5nC1MFmBiHLSsqX09x0exuM6nVJ+VchUoaXXshTs8nZTTYhx0svrEvadoTwXYoxV4vblh7+rKhUZ8wZGkc3AQDs6tlRI3xVAUDTe/bdqE5SHw+7RbimrPK/9h1CvSq6uLk+FXQmzzLbACA0XL2DZoM6Uy0iRD1/pVW9E1a7fK1J3wG7Q11eWdt4G3bh+3U+ONDMrK7/Xd8NmZQNmjqQb2lpKQAgNjYWQH3WpU6nc1k2NDQUer0eW7duxfDhw5Gbm4uYmBhnowcAhg8fDr1ej+3bt+PWW2/1en+ag7e6iIiI/Jz2v7e6vP1o/2v4pKamumRU5uTknHPbDocDU6ZMweDBg9GzZ08AwJVXXomIiAjMmDEDVVVVqKysxGOPPQa73Y6ioiIAQHFxMRITE13WZTQaERsb6zZO6nxjjw8REZGf81U6+5EjR5y3pgA0qbcnMzMTe/bswdatW51lCQkJePfdd/HQQw/h5Zdfhl6vx1133YXLL78cer1/96mw4UNERBQkLBaLS8PnXLKyspwPJbdr185lWkZGBgoKCvDzzz/DaDQiJiYGycnJ6NSpEwAgOTkZJ06ccFnGZrPh9OnTrRon5d/NMiIiImrxt7o0TUNWVhZWrlyJjRs3omNH8YFLxMfHIyYmBhs3bsSJEydw0003AQDS09NRUlKCvLw857wbN26Ew+HAwIEDvTsQPsAeHyIiIj/nq1tdTZWZmYnly5dj1apViIqKcj6TEx0djbCwMADA4sWL0b17dyQkJCA3NxePPPIIpk6diq5duwIAunfvjpEjR2Ly5MlYtGgRrFYrsrKycOedd7baG10AGz5ERER0loULFwIAhg4d6lK+ePFiTJw4EUB9GHh2djZOnz6NDh064E9/+hOmTp3qMv+yZcuQlZWFYcOGOWOlXn755ZbYBREbPkRERH6upbO6NE09ZMKZ5s2bh3nz5rmdJzY2FsuXL/do2+cbGz5ERER+rqVvdV3I+HAzERERBQ32+BAREfk59vj4Dhs+CvHhFTBGNI4GCDeq4wJizZWerV+Yv6QuTFymolY9yJTNrh5WXoutUZYbhBgNc5R6fkCOBfj5WLSyPHafuiPRqk7dgLu3LM0l6nIpFqDOov5y1ySq97suTt629DtR2lVdrq8VhsCPEob+D1MPyw8Alhh1tMhF0aXKcunarLGpv+J1Ds+H2pd+OPW6cz8LcCabQz7hNbYQj7YRLkQYWEzqPIlQg/o4AUCdXX2sUuN/UZYfKlXHJOgr1cdWb5X/x2OTrhHh0OrbCFkWUjRFqBwn0f6ik+o6CV9M6Vx48z9WKZoixqT+PZLOX6iwnhrhnAKAXji40v6Vmxr/Btv0tfiPuAXfYsPHd3iri4iIiIIGe3yIiIj8HHt8fIcNHyIiIj+nwfNX0s9enuqx4UNEROTn2OPjO3zGh4iIiIIGe3yIiIj8HHt8fIcNHyIiIj/Hho/v8FYXERERBQ32+BAREfk59vj4Dhs+REREfk7TdNCa0XhpzrIXGt7qIiIioqDBHh+FxLAKmMIaZ+HEmyuU89uE3COjXp2/U25TB03FmtX5TABgjFFn7RRVWJTlbcLU6zpVFaEsd5e3ZDSo9yMiTr2NS8YVK8u//uZiZXnkQbn9bY1Ul2tC1JSU4aWLU2c36dw0/R116o2EhAl5QWZ1eYRZnasUHVotbjsxVH2tRYWoM4zC3WRQqZRZhQMFwCpkNDnEcvW/JL0ZbE3Ky5K+S1FG9XkNM6iPebVdna8FyNlNCWHqbL2IbkeU5ZVW9TaOno4Rtx1qUH+/TSHqDKqESPX1USscv55tisRtS+e10s2xUpGOn7vrwKRX71+kcP6k6yBMuP5rHW6yunRCfp+wjOo30moTMtPOAwd0zRrAsDnLXmjY8CEiIvJzfMbHd3iri4iIiIIGe3yIiIj8HB9u9h02fIiIiPwcb3X5Dhs+REREfo49Pr7DZ3yIiIgoaLDHh4iIyM9pzbzVxR6fX7HhQ0RE5Oc0AJo83FqTlqd6vNVFREREQYM9PkRERH7OAR10HLnZJ9jwUegVdRShkY0PzS9WddzDMWu0stzgUHcuSsPKS0OlA0BKWKmyPFQYql0ahr5L4s/KcrNBPXQ8ABh16mHiDYnqId9DhGHljVeoy6sul4fGrxGOlSVEHVXwU4X6XFTUqrdhEuI4AMAsxAXEhaqjOixCnERMiDqawmKUIyuihWlmvRCXofMssqLKZBanldrDlOV24ZrylFXKG3EjVNhvKSahyqE+39L3wh0pzkX67qWElynLpe89ANTZ1cdkUNIhZXlq6Gllud2H/3OrsquvEU+fM5F+DwAgRPhtkUjXoFmIvnBXV4dww8MsfJfaWBpHl9SYbfhY3IJv8a0u3+GtLiIiIgoa7PEhIiLycw5NB12QD2BYU1OD0FA5YLmpAqbHJycnBwMGDEBUVBQSExNxyy23ID8/32WeoUOHQqfTuXwefPDBVqoxERGRb2ha8z+ByOFwYO7cubjooosQGRmJ//73vwCAJ554Aq+//rpX6wyYhs+WLVuQmZmJbdu2Yf369bBarcjIyEBlpet918mTJ6OoqMj5mT9/fivVmIiIiJrjz3/+M5YsWYL58+fDZPr12b2ePXvitdde82qdAXOra926dS5/L1myBImJicjLy8OQIUOc5eHh4UhOTm7p6hEREZ03wfpw85tvvol//OMfGDZsmMsdnD59+mD//v1erTNgenzOVlpa/5ZTbGysS/myZcsQHx+Pnj17Ijs7G1VV6jdwAKC2thZlZWUuHyIiIn/T0PBpzicQHT16FF26dGlU7nA4YLV69jZrg4Dp8TmTw+HAlClTMHjwYPTs2dNZfvfddyMtLQ1t27bF7t27MWPGDOTn52PFihXK9eTk5OCpp55qqWoTERF5JVgfbu7Rowe++OILpKWluZS/99576Nu3r1frDMiGT2ZmJvbs2YOtW7e6lN9///3O/+7VqxdSUlIwbNgwFBQUoHPnzo3Wk52djWnTpjn/LisrQ2pq6vmrOBERETXZk08+iQkTJuDo0aNwOBxYsWIF8vPz8eabb2LNmjVerTPgbnVlZWVhzZo12LRpE9q1a+d23oEDBwIADhw4oJxuNpthsVhcPkRERP4mWN/quvnmm/Hhhx9iw4YNiIiIwJNPPol9+/bhww8/xPXXX+/VOgOm4aNpGrKysrBy5Ups3LgRHTt2POcyu3btAgCkpKSc59oRERGdP/WNl+Y84+PZ9poyhExxcTHuueceJCcnIyIiApdffjnef/99l3lOnz6NsWPHwmKxICYmBvfddx8qKiqaVAebzYY5c+agY8eOWL9+PU6cOIGqqips3boVGRkZnu3QGQKm4ZOZmYl//etfWL58OaKiolBcXIzi4mJUV9cP7V9QUIC5c+ciLy8Phw4dwurVqzF+/HgMGTIEvXv3buXaExERBY6mDCEzfvx45OfnY/Xq1fj+++9x22234fbbb8fOnTud84wdOxZ79+7F+vXrsWbNGnz++ecuj6W4YzQaMX/+fNhscqSSN3SaFhgdYDqd+sGsxYsXY+LEiThy5AjGjRuHPXv2oLKyEqmpqbj11lvx+OOPN/kWVllZGaKjo/H/fdsP4VGNs3OOWWOUyx2piVWWS07XqTO/3GV1tQ0rUZanmNQZXkdrY5TltcI20sJOiduONqhzo0w69cUo5SdJGU3lDs9H4pQyl2ocIcryX2zhyvJIgzrzCwDC3UxTCRWOhzf5WiHCukweZhvVCcdcyikC5PMk5STZhXVJD1NK2VfeLFMrnO8aTX2dW918x6TvhrR/Bqiz6qSctXC9fD3lljZ+BhEAUsN+UZYnhajfQI03qsurHHI2m7R/0vdbXI+H1wcgH0NpGenalMqlvDEAiDKos/W6mIvV61Icw6pyO+69fCdKS0vP26MSDf9f6vLPbBjCvR+12F5VgwP35Hhd15MnTyIxMRFbtmxxDiETGRmJhQsX4p577nHOFxcXh2eeeQb/93//h3379qFHjx7YsWMH+vfvD6B+aJobbrgBP/30E9q2bXvO7d5888247bbbMGHCBI/rLAmYh5vP1T5LTU3Fli1bWqg2RERELUf736c5ywNoNGyL2WyG2Sw3EBuohpAZNGgQ3nnnHYwePRoxMTH497//jZqaGgwdOhQAkJubi5iYGGejBwCGDx8OvV6P7du349Zbbz3ndkeNGoWZM2fi+++/R79+/RAR4dpxcNNNN51zHWcLmIYPERERNc/Zby7PmjULs2fPdruMNITMv//9b9xxxx2Ii4uD0WhEeHg4Vq5c6Rx3p7i4GImJiS7rMhqNiI2NRXGxumftbL/73e8AAC+88EKjaTqdDna7Z73gABs+REREfs9XIzcfOXLE5VZXU3p7pCFknnjiCZSUlGDDhg2Ij4/HBx98gNtvvx1ffPEFevXq5XVdz+RwqG+HNgcbPkRERP7OR/e6PB26pWEImc8//9xlCJmCggL87W9/w549e3DppZcCqI+R+OKLL7BgwQIsWrQIycnJOHHihMv6bDYbTp8+3arRUmz4EBER+bvmxk54uKymaXj44YexcuVKbN68udEQMg1xUHq964PoBoPB2UuTnp6OkpIS5OXloV+/fgCAjRs3wuFwOMfZO5c5c+a4nf7kk082aT1nYsOHiIiIXGRmZmL58uVYtWqVcwgZAIiOjkZYWBi6deuGLl264IEHHsBzzz2HuLg4fPDBB87X1gGge/fuGDlyJCZPnoxFixbBarUiKysLd955Z5Pe6AKAlStXuvxttVpx8OBBGI1GdO7cmQ0fIiKiC1FzR1/2dNmFCxcCgPMNrQYNQ8iEhIRg7dq1mDlzJm688UZUVFSgS5cuWLp0KW644Qbn/MuWLUNWVhaGDRsGvV6PMWPG4OWXX25yPc4cE6hBWVkZJk6c2KS3wlTY8CEiIvJzvnq4uenzn7uldPHFFzcaqflssbGxWL58uUfbPheLxYKnnnoKN954o8sYQk0VMCM3ExEREQH14wo1jC3kKfb4EBER+TtN5/EDyo2WD0Bn3xbTNA1FRUX45z//iVGjRnm1TjZ8FJKMJYgwNh4C3S5cOFazerh0iTT8fqVNHk9BilaQIhqSzeqWcLi+Tlke4WY4fSmCQlpGr1OPuyANTx/ukLdtFaIHQoUYiCi9Oi6g3BGmLJfOKSDHQ9Ro6mMeIszvTfyEu3op5xc6b0OFcyFFftQvoz620jY8ras77qI0VKRYhXApPsHgeXyCdF7F9QjHXLo2AeD2hB3K8hK7Omql3K6OLpCiKRKEKAsAqNFMynLpeEikeBRvSNeBp3Eq7mJh0kw/K8sHhZ5Wlv9ka3ydV5h8P8aMpKWf8fEXL774osvfer0eCQkJmDBhArKzs71aJxs+RERE5JcOHjzo83XyGR8iIiJ/p/ngE4DuvfdelJeXNyqvrKzEvffe69U62fAhIiLycw1vdTXnE4iWLl2K6urGt4mrq6vx5ptverVO3uoiIiIiv1JWVgZN06BpGsrLyxEa+utzbXa7HWvXrm0UgNpUbPgQEREFggC9XeWNmJgY6HQ66HQ6XHLJJY2m63Q6PPXUU16tmw0fIiIiP9fSAxi2tk2bNkHTNFx33XV4//33ERsb65xmMpmQlpbW5NiLs7HhQ0RE5O98lM4eKK655hoA9W91paamNgpDbQ42fIiIiMgvpaWlAahPgy8sLERdnetYdL179/Z4nWz4EBER+T3d/z7NWT7wnDx5EpMmTcLHH3+snG63ezbAKMDX2YmIiPxfkI7jM2XKFJSUlGD79u0ICwvDunXrsHTpUlx88cVYvXq1V+tkjw8RERH5pY0bN2LVqlXo378/9Ho90tLScP3118NisSAnJwejR4/2eJ1s+CiE6mzKjKP2IeoMFyk3SsrRahtSoiwPd5OXVSpk9hyoSVKWtzNWKMuluprdZNpImVLSuiRSppO7nDBAPc0g5J1JOVoSd3lZUuZYKNT7LWVySXV1x9NlQoQ6SZlHdg9zmOq3IfCwB13KXwMAvXAdStliemk/hDp5cy4kcraX+jqoFHK0ALleUk5YgrHxSLaAnJflbtsHa9VjoSSFqPP+pMy2OIP6N8cdaV0GSLl3nt/WkEi5Zt/VRSjLYxRZa/aWvH0UZA83N6isrHSO19OmTRucPHkSl1xyCXr16oVvv/3Wq3V6favriy++wLhx45Ceno6jR48CAP75z39i69at3q6SiIiIVBrS2ZvzCUBdu3ZFfn4+AKBPnz74+9//jqNHj2LRokVISUnxap1eNXzef/99jBgxAmFhYdi5cydqa+v/VV5aWoqnn37aq4oQERERnemRRx5BUVERAGDWrFn4+OOP0b59e7z88stetze8utX15z//GYsWLcL48ePx9ttvO8sHDx6MP//5z15VhIiIiNQ0rf7TnOUD0bhx45z/3a9fPxw+fBj79+9H+/btER8f79U6verxyc/Px5AhQxqVR0dHo6SkxKuKEBERkSAI3+qyWq3o3Lkz9u3b5ywLDw/H5Zdf7nWjB/Cy4ZOcnIwDBw40Kt+6dSs6derkdWWIiIiIACAkJAQ1NTU+X69XDZ/JkyfjkUcewfbt26HT6XDs2DEsW7YMjz32GB566CFf15GIiCi4BenDzZmZmXjmmWdgs6nflvSGV8/4zJw5Ew6HA8OGDUNVVRWGDBkCs9mMxx57DA8//LDPKkdERESATqv/NGf5QLRjxw589tln+PTTT9GrVy9ERLgON7BixQqP1+lVw0en0+FPf/oTpk+fjgMHDqCiogI9evRAZGSkN6sjIiIid4J0HJ+YmBiMGTPGp+ts1gCGJpMJPXr08FVdiIiIiJwWL17s83U2ueFz2223NXml3nQ9ERERkaC5z+kE6DM+AGCz2bB582YUFBTg7rvvRlRUFI4dOwaLxeLVnaYmN3yio6Od/61pGlauXIno6Gj0798fAJCXl4eSkhKPGkj+6rjNgnCbeuh3FSmaQopDqBO6HE/b5BMoDe3eN/ywslwaNt9dXIBEim4Q4wLE9ajLpVgFQB6C3+qQohg8e17fXcSFXhMiK4SoDjl2wHcP5UnsQqSDFBPSmtxFnXgaQREiXR/Cde7u+vD0WFmFEA/pmpLOEQBYz/NtiNN2dQwDAFQ5TMpyaT+kqI4SIVbHq/PtI+4iSqRr4aTNoi5H4/KqOjuAo17VzWNBeqvr8OHDGDlyJAoLC1FbW4vrr78eUVFReOaZZ1BbW4tFixZ5vM4m/1/wzO6mGTNm4Pbbb8eiRYtgMNT/8Njtdvzud7+DxaK+aIiIiIg88cgjj6B///747rvvEBcX5yy/9dZbMXnyZK/W6dUzPm+88Qa2bt3qbPQAgMFgwLRp0zBo0CA8++yzXlWGiIiIFIK0x+eLL77AV199BZPJtWeyQ4cOzpxQT3k1jo/NZsP+/fsble/fvx8Ox/ntuiQiIgo6QThyMwA4HA7Y7Y0fG/npp58QFRXl1Tq96vGZNGkS7rvvPhQUFOCKK64AAGzfvh3z5s3DpEmTvKoIERER0ZkyMjLw0ksv4R//+AeA+uF0KioqMGvWLNxwww1erdOrhs9zzz2H5ORkPP/8887U1JSUFEyfPh2PPvqoVxUhIiIiQZC+1fX8889jxIgR6NGjB2pqanD33Xfjxx9/RHx8PN566y2v1ulVw0ev1+MPf/gD/vCHP6CsrAwA+FAzERHReRKsIze3a9cO3333Hd5++23s3r0bFRUVuO+++zB27FiEhYV5tc5mDWAIsMFDRERE54/RaMS4ceN8tz5vFurYsSN0Ornb7L///a/XFfKFBQsW4Nlnn0VxcTH69OmDV155xfksEhERUcAJ0re6ACA/Px+vvPIK9u3bBwDo3r07srKy0K1bN6/W51XDZ8qUKS5/W61W7Ny5E+vWrcP06dO9qoivvPPOO5g2bRoWLVqEgQMH4qWXXsKIESOQn5+PxMTEVq0bERERNd3777+PO++8E/3790d6ejoAYNu2bejVqxfefvttr3K8vGr4PPLII8ryBQsW4JtvvvFmlT7zwgsvYPLkyc63yxYtWoSPPvoIb7zxBmbOnNmqdSMiIvKGDs18xsdnNWlZf/jDH5CdnY05c+a4lM+aNQt/+MMfvGr4eDWOj2TUqFF4//33fblKj9TV1SEvLw/Dhw93lun1egwfPhy5ubmN5q+trUVZWZnLh4iIKNjl5ORgwIABiIqKQmJiIm655Rbk5+c7px86dAg6nU75effdd53zFRYWYvTo0QgPD0diYiKmT58Om63pMT5FRUUYP358o/Jx48Y53yr3VLMfbj7Te++9h9jYWF+u0iM///wz7HY7kpKSXMqTkpKUAy7m5OTgqaeealReo5mgFzKAPFHpMCvLpWycUiHrBpBzfiL0tcpyKR/KXW6OvG31vxWqhP2TMnCkjB93pJwwaf9qHOqn/EN1Ur6WXCdp21IGm+SUkMHmLjctzlihLJdy4aTjESLU1V0+mqfEbUPatvxvT+nakY6VlMVkF641d9lQdiGbSjpWVuE3QsxN8+Exd3j4avIvVjmrq735lLJc+s5IvyHS74S761z6jkm/a9IxdHhxzKX62qE+r6rroLYl+1Fa+HX2LVu2IDMzEwMGDIDNZsMf//hHZGRk4IcffkBERARSU1MbNTz+8Y9/4Nlnn8WoUaMA1EdZjR49GsnJyfjqq6+cjZiQkBA8/fTTTarH0KFD8cUXX6BLly4u5Vu3bsXVV1/t0T418Krh07dvX5eHmzVNQ3FxMU6ePIlXX33Vq4q0huzsbEybNs35d1lZGVJTU1uxRkRERAot/HDzunXrXP5esmQJEhMTkZeXhyFDhsBgMCA5OdllnpUrV+L22293JqZ/+umn+OGHH7BhwwYkJSXhsssuw9y5czFjxgzMnj27UQyFyk033YQZM2YgLy8PV155JYD6Z3zeffddPPXUU1i9erXLvE3hVcPn5ptvdmn46PV6JCQkYOjQoV4/Ze0L8fHxMBgMOH78uEv58ePHG50gADCbzTCb1b0WREREF5qzH+lo6v8HS0tLAUC8q5OXl4ddu3ZhwYIFzrLc3Fz06tXL5S7MiBEj8NBDD2Hv3r3o27fvObf7u9/9DgDw6quvNupYaZgG1I/orIq2UPGq4TN79mxvFjvvTCYT+vXrh88++wy33HILgPqcj88++wxZWVmtWzkiIiJv+ajH5+y7GrNmzTrn/9MdDgemTJmCwYMHo2fPnsp5Xn/9dXTv3h2DBg1ylhUXFysfPWmY1hTnI//Tq4aPwWBAUVFRo9fDT506hcTExCa3us6HadOmYcKECejfvz+uuOIKvPTSS6isrGSGGBERBSxfjdx85MgRl4GHm9Lbk5mZiT179mDr1q3K6dXV1Vi+fDmeeOIJ7yvYgrxq+Gia+ujX1tY26Z7d+XTHHXfg5MmTePLJJ1FcXIzLLrsM69ata9TqJCIiCjYWi8WjxIWsrCysWbMGn3/+Odq1a6ec57333kNVVVWjt6+Sk5Px9ddfu5Q1PIqievxEsmPHDmzatAknTpxo1AP0wgsvNHk9DTxq+Lz88ssA6u+lvfbaa84HmID6p7c///zzVn3Gp0FWVhZvbRER0YWjhR9u1jQNDz/8MFauXInNmzejY8eO4ryvv/46brrpJiQkJLiUp6en4y9/+QtOnDjhvEO0fv16WCwW9OjRo0n1ePrpp/H444+ja9euSEpKcnm+2F2ChDseNXxefPFFAPUHZNGiRTAYfn3tz2QyoUOHDli0aJFXFSEiIiJBCzd8MjMzsXz5cqxatQpRUVHOZ3Kio6NdwkEPHDiAzz//HGvXrm20joyMDPTo0QP33HMP5s+fj+LiYjz++OPIzMxs8otFf/3rX/HGG29g4sSJnu2AGx41fA4ePAgAuPbaa7FixQq0adPGZxUhIiIi/7Bw4UIA9ePonGnx4sUujZA33ngD7dq1Q0ZGRqN1GAwGrFmzBg899BDS09MRERGBCRMmNBqF2R29Xo/Bgwd7tQ8Sr57x2bRpk08rQURERDJfPdzcVNKzvGd7+umn3Q5GmJaWpuwNaqqpU6diwYIFeOmll7xex9ma3PCZNm0a5s6di4iICJdB/1S8ediIiIiIBC08crO/eOyxxzB69Gh07twZPXr0QEiI6wjrK1as8HidTW747Ny5E1Zr/VDl3377rdcPFQWCai0EcDQ+NNLw/9LQ7oV18R5tVxrq3p0YQ5VH89d5EcUhDQcvDTcv3UuWho+XIg/cbVuKEZDOhbQNKfLA7TJC7IEUUZJoVGfAldjlGIGjVvVt5ARhXdKx9eaakvbP0/NXI0RAuIsJkWIupGVqNPVbpFKsiLvrX7oWHB7+S1mKsnAX3SDGX/goEuEXmxyHk2IqUZbXQH3+rHZp/6R98Pwa9DQWRrrW3PE0ckS5XUdLRlagRZ/x8Re///3vsWnTJlx77bWIi4vzSdujyQ2fM29vbd68udkbJiIiInJn6dKleP/99zF69GifrdOr1Lx7770X5eXljcorKytx7733NrtSRERE9KuGZ3ya8wlEsbGx6Ny5s0/X6VXDZ+nSpaiurm5UXl1djTfffLPZlSIiIqIzaD74BKDZs2dj1qxZqKry7LEOdzx6q6usrAyapkHTNJSXlyM0NNQ5zW63Y+3atY1iLIiIiIi88fLLL6OgoABJSUno0KFDo4ebv/32W4/X6VHDJyYmBjqdDjqdDpdcckmj6TqdDk899ZTHlSAiIiI3mnu7KkB7fBoCx33Jo4bPpk2boGkarrvuOrz//vsu8fQmkwlpaWlo27atzytJREQU1IL0ra5Zs2b5fJ0eNXyuueYaAPUjOKempkKv9+oRISIiIqImKSkpwXvvvYeCggJMnz4dsbGx+Pbbb5GUlISLLrrI4/V5NXJzWloaAKCqqgqFhYWoq6tzmd67d29vVktEREQqQdrjs3v3bgwfPhzR0dE4dOgQJk+ejNjYWKxYsQKFhYVevVDlVcPn5MmTmDRpEj7++GPldLvds8GniIiISNbSkRX+Ytq0aZg4cSLmz5+PqKgoZ/kNN9yAu+++26t1enWvasqUKSgpKcH27dsRFhaGdevWYenSpbj44ouxevVqrypCREREdKYdO3bggQceaFR+0UUXORPjPeVVj8/GjRuxatUq9O/fH3q9Hmlpabj++uthsViQk5Pj0xEWiYiIKDiZzWaUlTWO6vnPf/6DhIQEr9bpVcOnsrLSOV5PmzZtcPLkSVxyySXo1auXV+/U+5tfbJGotjX90Eh5OifqopTlcSGVHtcpylCj3rYQPBeqV2dWSVlFUj4TAEhxQVL2kJi9JXS1SnlcgOc5P273w0Oe5kbFGdTn9YRdfR24yyi7KOQXZble2D8pd0jiLjdKykGTzkWNw7Nj7rauwumW6isdD2kf3F1rckaZ+gug9/DadJe7VeNQZ01VOdRZZLWKLEF3Ig214rS3jgxQll+dVKAs7xJ6XFku/eZI+wYApXZ1hpiYAygRflvcXWtSvaRrTXX+ahzyd9jngvQZn5tuuglz5szBv//9bwD1w+YUFhZixowZGDNmjFfr9OpWV9euXZGfnw8A6NOnD/7+97/j6NGjWLRoEVJSUryqCBEREakFa2TF888/j4qKCiQmJqK6uhrXXHMNunTpgqioKPzlL3/xap1e9fg88sgjKCoqAlD/jv3IkSPxr3/9CyaTCUuXLvWqIkRERERnio6Oxvr16/Hll1/iu+++Q0VFBS6//HIMHz7c63V61fAZN26c87/79euHw4cPY//+/Wjfvj3i4+O9rgwREREJArTXpjnefPNN3HHHHRg8eDAGDx7sLK+rq8Pbb7+N8ePHe7zOJjd8pk2b1uSVvvDCCx5XhIiIiARB+ozPpEmTMHLkyEY5oOXl5Zg0adL5bfjs3LmzSfPpdPIDfERERERNpWmasl3x008/ITo62qt1Nrnhs2nTJq82QERERM0TbAMY9u3b1xmKPmzYMBiNvzZX7HY7Dh48iJEjR3q1bq+e8SEiIqIWFGS3uhpS2Xft2oURI0YgMjLSOc1kMqFDhw5ev87Ohg8RERH5lYZU9g4dOuCOO+5AaGio2/nfeust3HTTTYiIiDjnuhmvTkRE5OeCdRyfCRMmnLPRAwAPPPAAjh9XD655Nvb4EBER+bsgu9XlKU1r+g6yx4eIiIiCBnt8FGocRkCR4yLlvlQLeTptQqqU5fEh5cryUJ066wYA4owVwjJ1ynI5V0mdT2Nw0w8q5YFJeUhyBo76+LnL43II25aWkbKYpFysaIP6HAFAB/MpZXmSQZ3hFapT71++VX1ei+3yq5gnbep8L6m+nuYOOdzkRknswrGt0qmvf73wT0wp6wyQ6yvlQHmaUeaOlGNXq8lZUyp64btkcPMvUumYSPlh0ve11q4+ftVu9iE98ZCyfHPRxcrypPaNAyMBIMpQLW7DU55+j6XfTinLDQD0evUxrBHOk5Sb1mLY4+MzbPgQERH5uWB7nf18YsOHiIjI37HHx2f4jA8REREFtLS0NISENO22NHt8iIiI/B17fFBRUQGHw/W5LYvFAgDYs2dPk9fDHh8iIiI/F6zj+Bw8eBCjR49GREQEoqOj0aZNG7Rp0wYxMTFo06aNV+tkjw8RERH5pXHjxkHTNLzxxhtISkrySRA6Gz5ERET+LkhvdX333XfIy8tD165dfbZO3uoiIiLyc8F6q2vAgAE4cuSIT9fJHh8iIiLyS6+99hoefPBBHD16FD179mz05lbv3r09XicbPkRERP4uSG91nTx5EgUFBZg0aZKzTKfTQdM06HQ62O3qkfTdYcNHIURvQ4i+8QNUtTb14bI61HcMo03qIdxTQ9RRCMesnj+hXmKPUJZLQ7tLQ91LsRSAHCPgaQSFNOR7rRC34G4bUkSDFHERolN/Oewm+W5vL1OYsrzCUaMsL9XUw+Z3FXavzFErbvu0LUVZLsVGSJEHdcK5c3fM7UKchRQj4On80rkAgHC9+pi4W+Z88/Q6l/bbXbyGdF4lUhSD2aD+3kOIsgCAU3XhHm3b03NUYpfXnxRSqiyXjq1d811shHQ+pN8Qg6LloCo7b1q44ZOTk4MVK1Zg//79CAsLw6BBg/DMM880etYmNzcXf/rTn7B9+3YYDAZcdtll+OSTTxAWVv/7efr0aTz88MP48MMPodfrMWbMGPz1r39FZGRkk+px7733om/fvnjrrbf4cDMRERGdH1u2bEFmZiYGDBgAm82GP/7xj8jIyMAPP/yAiIj6f3Dn5uZi5MiRyM7OxiuvvAKj0YjvvvsOev2vDdexY8eiqKgI69evh9VqxaRJk3D//fdj+fLlTarH4cOHsXr1anTp0sVn+xYQDZ9Dhw5h7ty52LhxI4qLi9G2bVuMGzcOf/rTn2AymZzzdOzYsdGyubm5uPLKK1u6ykRERD6j+9+nOcsDQFmZa8is2WyG2WxuNP+6detc/l6yZAkSExORl5eHIUOGAACmTp2K3//+95g5c6ZzvjN7hPbt24d169Zhx44d6N+/PwDglVdewQ033IDnnnsObdu2PWe9r7vuOnz33XfB1/DZv38/HA4H/v73v6NLly7Ys2cPJk+ejMrKSjz33HMu827YsAGXXnqp8++4uLiWri4REZFv+ehWV2pqqkvxrFmzMHv27HMuXlpaf1syNjYWAHDixAls374dY8eOxaBBg1BQUIBu3brhL3/5C6666ioA9R0PMTExzkYPAAwfPhx6vR7bt2/Hrbfees7t3njjjZg6dSq+//579OrVq9HDzTfddNM513G2gGj4jBw5EiNHjnT+3alTJ+Tn52PhwoWNGj5xcXFITk5u0npra2tRW/vr/eqzW8JERET+wFfp7EeOHHHGPABQ9vaczeFwYMqUKRg8eDB69uwJAPjvf/8LAJg9ezaee+45XHbZZXjzzTcxbNgw7NmzBxdffDGKi4uRmJjosi6j0YjY2FgUFxc3qd4PPvggAGDOnDmN98nLh5sDdhyf0tJSZ8vzTDfddBMSExNx1VVXYfXq1W7XkZOTg+joaOfn7JYwERHRhcRisbh8mtLwyczMxJ49e/D22287yxoysx544AFMmjQJffv2xYsvvoiuXbvijTfe8Fl9HQ6H+PGm0QMEaMPnwIEDeOWVV/DAAw84yyIjI/H888/j3XffxUcffYSrrroKt9xyi9vGT3Z2NkpLS50fXw+SRERE5BOaDz5eyMrKwpo1a7Bp0ya0a9fOWZ6SUv/maY8ePVzm7969OwoLCwEAycnJOHHihMt0m82G06dPN/nOzPnQqg2fmTNnQqfTuf3s37/fZZmjR49i5MiR+O1vf4vJkyc7y+Pj4zFt2jQMHDgQAwYMwLx58zBu3Dg8++yz4vbNZnOj1i8REZFfasFGj6ZpyMrKwsqVK7Fx48ZGLw916NABbdu2RX5+vkv5f/7zH6SlpQEA0tPTUVJSgry8POf0jRs3wuFwYODAgeesQ3V1NbZu3Yoffvih0bSamhq8+eabnu8YWvkZn0cffRQTJ050O0+nTp2c/33s2DFce+21GDRoEP7xj3+cc/0DBw7E+vXrm1tNIiKioJKZmYnly5dj1apViIqKcj6TEx0djbCwMOh0OkyfPh2zZs1Cnz59cNlll2Hp0qXYv38/3nvvPQD1vT8jR47E5MmTsWjRIlitVmRlZeHOO+885xtd//nPf5CRkYHCwkLodDpcddVVePvtt509TaWlpZg0aRLGjx/v8b61asMnISEBCQkJTZr36NGjuPbaa9GvXz8sXrzYZZwAya5du5wHiYiIKFD56uHmplq4cCEAYOjQoS7lixcvdnZYTJkyBTU1NZg6dSpOnz6NPn36YP369ejcubNz/mXLliErKwvDhg1zDmD48ssvn3P7M2bMQM+ePfHNN9+gpKTE+XD15s2b0b59e8925iwB8VbX0aNHMXToUKSlpeG5557DyZMnndMa7hMuXboUJpMJffv2BQCsWLECb7zxBl577bVWqTMREZHPtPDIzZrWtAVmzpzpMo7P2WJjY5s8WOGZvvrqK2zYsAHx8fGIj4/Hhx9+iN/97ne4+uqrsWnTJucgit4IiIbP+vXrceDAARw4cMDl4SrA9eTMnTsXhw8fhtFoRLdu3fDOO+/gN7/5TUtXl4iIiJqhuroaRuOvTRSdToeFCxciKysL11xzjVeNqQYB0fCZOHHiOZ8FmjBhAiZMmOCT7ZVYI2C2Ns4ycniYzfOzVZ1F0smsnr/cESrWqaZOna1UalPn4Eg5NOEGdc6Ou8wZX+UkVdnVr03WOuTLUNqPars6s8cqnAubsJ6TdVHitv8/nTp7q1TIHjptU/8LpHe4+m3BCCHzCJCzyGoc0cryWGOFUCf1NfiLcN34kpQLZ3RzPVXpfZPFZHWoz7feTX9/qF59vsWcN2EcXSkHTaoTIF/nUrlNWJdUJ2l+AKixq+vbPVY9zop0/ZcKp9VdRtkxa4w4zRPSOXL3u1Yj5NhJv1PViqzB2jr1NXM+tPStrtbWrVs3fPPNN+jevbtL+d/+9jcA3g1c2CAgX2cnIiIKKq30OntrufXWW/HWW28pp/3tb3/DXXfd1eTbcWdjw4eIiIj8SnZ2NtauXStOf/XVV52DKHoqIG51ERERBbNgu9V1PrHhQ0RE5O9a+K2uCxkbPkRERP6ODR+f4TM+REREFDTY40NEROTn+IyP77DhQ0RE5O94q8tneKuLiIiIggZ7fIiIiPycTtOg83LAvoblqR4bPgpFtdEIMTYentyhqYeDN+rUgyhJ8Qlfll/scZ3k6Ab1cPOVNvWw6w5hSPs6u3wpGPXq4eAjjHXK8ihjjbguFXfD6ZfZ1DEeVTbPog2kfXC37bUnewnLeNZRerAyXlmeGv6LuExiSJmyXIqykKIppEiOEmuYuO06IUJEL1znUmSL9H0JNcjD/MeaKoVtq3+0pfMnffekutZvQ71/Zr1NWR6i92zwNKub66ZSiEkot8oxNp6Q9g0AyurU24iOqFaW/7dafT27i56ReBNrohIi7F+YQf0bBcgRRFXCb6rqt7auTl6/z/FWl8/wVhcREREFDfb4EBER+Tm+1eU7bPgQERH5O97q8hne6iIiIqKgwR4fIiIiP8dbXb7Dhg8REZG/460un2HDh4iIyM+xx8d3+IwPERERBQ32+BAREfk73uryGTZ8iIiIAgBvV/kGb3URERFR0GCPj8KpmnAYDY2zc6SMJilHSK9T5ydJGUZSubttSHWyChlGtVb1KTfo5X9KGIRMohAh/0qa32RQz6930wdbZRNyc6zqrC7pGErHzyjUyd0y0jbqbOpjWyJs43StnJeVFqmeFqZX51xV2NXHo0LIeiqzqrOhAKDOrr52jMJ5tQrzS7lw0nXjjpS1JmVv1Qh5S3Vustncff9UQg3qDC8pi0yqEyBf51K5XfjeS8fc3XdM+q2INVep6yRcayerI5TlZuE4AZ7n3oUa1esyCdeHVO6OnGfY+DhZa1syq0ur/zRneQLAhg8REZHf41tdvsNbXURERBQ02ONDRETk7/hWl8+w4UNEROTndI76T3OWp3q81UVERERBgz0+RERE/o63unyGDR8iIiI/x7e6fIcNHyIiIn/HcXx8hs/4EBERUdBgjw8REZGf460u32HDR6HwdBsYahoP9S9dN1K0gebhEPjuhszXHOppUu+lXoigcAjrcUfaD50U1SFsWyoPMcrDykvHxGoTYhLsnu2f0Si/4xlqVkcPSJEcNXXqeAHpHFWGqIf+B4Bau/qr6WncgxRtUCVEfgCA3cNjLpHOd1iI+rgCwLEqi7I83Cgvo2KToiyEWBEAqBOOuXQN2uzqbRgN6uvD3e+BXfheSnESnt61cBdJI+3HaXO4svxklTqKp1q4/t2R9luqrzS/Tji0oSbPrhsAqKpRfzdU3yV7VY3H6/caH272Gd7qIiIiIhc5OTkYMGAAoqKikJiYiFtuuQX5+fku8wwdOhQ6nc7l8+CDD7rMU1hYiNGjRyM8PByJiYmYPn06bDY5v60lsMeHiIjIz7X0ra4tW7YgMzMTAwYMgM1mwx//+EdkZGTghx9+QETEr4G0kydPxpw5c5x/h4f/2lNot9sxevRoJCcn46uvvkJRURHGjx+PkJAQPP30097vTDOx4UNEROTvfPRWV1lZmUux2WyG2WxuNPu6detc/l6yZAkSExORl5eHIUOGOMvDw8ORnJys3OSnn36KH374ARs2bEBSUhIuu+wyzJ07FzNmzMDs2bNhMsm33M8n3uoiIiIKEqmpqYiOjnZ+cnJymrRcaWkpACA2NtalfNmyZYiPj0fPnj2RnZ2Nqqoq57Tc3Fz06tULSUlJzrIRI0agrKwMe/fu9cHeeIc9PkRERH7OV7e6jhw5Aovl15cIVL09Z3M4HJgyZQoGDx6Mnj17OsvvvvtupKWloW3btti9ezdmzJiB/Px8rFixAgBQXFzs0ugB4Py7uLjY+51pJjZ8iIiI/J2P3uqyWCwuDZ+myMzMxJ49e7B161aX8vvvv9/537169UJKSgqGDRuGgoICdO7cuRmVPb8C5lZXhw4dGj09Pm/ePJd5du/ejauvvhqhoaFITU3F/PnzW6m2REREgS8rKwtr1qzBpk2b0K5dO7fzDhw4EABw4MABAEBycjKOHz/uMk/D39JzQS0hYBo+ADBnzhwUFRU5Pw8//LBzWllZGTIyMpCWloa8vDw8++yzmD17Nv7xj3+0Yo2JiIiar+FWV3M+ntA0DVlZWVi5ciU2btyIjh07nnOZXbt2AQBSUlIAAOnp6fj+++9x4sQJ5zzr16+HxWJBjx49PKuQDwXUra6oqCixlbhs2TLU1dXhjTfegMlkwqWXXopdu3bhhRdecOmOIyIiCjgOrf7TnOU9kJmZieXLl2PVqlWIiopyPpMTHR2NsLAwFBQUYPny5bjhhhsQFxeH3bt3Y+rUqRgyZAh69+4NAMjIyECPHj1wzz33YP78+SguLsbjjz+OzMzMJj1bdL4EVI/PvHnzEBcXh759++LZZ591GQQpNzcXQ4YMcXk9bsSIEcjPz8cvv/yiXF9tbS3KyspcPkRERH5H88HHAwsXLkRpaSmGDh2KlJQU5+edd94BAJhMJmzYsAEZGRno1q0bHn30UYwZMwYffvihcx0GgwFr1qyBwWBAeno6xo0bh/Hjx7uM+9MaAqbH5/e//z0uv/xyxMbG4quvvkJ2djaKiorwwgsvAKh/Qvzsrrgznx5v06ZNo3Xm5OTgqaeeOv+VJyIiCiDaOcYMSk1NxZYtW865nrS0NKxdu9ZX1fKJVm34zJw5E88884zbefbt24du3bph2rRpzrLevXvDZDLhgQceQE5OjtddZtnZ2S7rLSsrQ2pqKmrLQqG3Ns7qkkjXh5Qf4+n89ROlhaRKSetxs41WUuNNnaSILSkPSTh+djdZXdY69ddDJ2R1aUIulpQfVi1uGSg3qq8/g5v6esTNb5qnGXNSZptOyFtyl/lVbVXnPZUJGWXSd0bKG6uulfOkHML58/TmgjeX8/nehnSO3Dn0S6yyXMr2slmF/DyrfGNBE9YFKZtQyuIT9q/S4MWtIQ+uf0d1ywVg6dDM19l9VpPA16oNn0cffRQTJ050O0+nTp2U5QMHDoTNZsOhQ4fQtWtXr54el0asJCIi8is+GrmZWrnhk5CQgISEBK+W3bVrF/R6PRITEwHUPz3+pz/9CVarFSEh9f+qW79+Pbp27aq8zUVERETBJyAebs7NzcVLL72E7777Dv/973+xbNkyTJ06FePGjXM2au6++26YTCbcd9992Lt3L9555x389a9/dbmVRUREFIha+nX2C1lAPNxsNpvx9ttvY/bs2aitrUXHjh0xdepUl0ZNdHQ0Pv30U2RmZqJfv36Ij4/Hk08+yVfZiYgo8Plo5GYKkIbP5Zdfjm3btp1zvt69e+OLL75ogRoRERFRIAqIhg8REVEw02kadM14QLk5y15o2PAhIiLydw7Iw3g0dXkCECAPNxMRERH5Ant8iIiI/BxvdfkOGz5ERET+jm91+QwbPip1esDQ9LuAno6P4DaaQiIM/y/y5UXuaSyGMNy8OL83N1yldQlD1GvSNjQ5PsFuEzail1bm4X67OUf2OnW9pCp5SuduKH+PL2h1sd6gfqjA5uZ8SwkAmhRhIF1r7jYikb5jHsbFSFEd3sRG+CpnQDxOcBMDIUQ3SPPrbJ6txy0pBkX8bZFOhueb9miZuhZ8WoQjN/sMn/EhIiKioMEeHyIiIj/X3NGXOXLzr9jwISIi8ne81eUzvNVFREREQYM9PkRERH5O56j/NGd5qseGDxERkb/jrS6f4a0uIiIiChrs8SEiIvJ3HMDQZ9jwISIi8nOMrPAd3uoiIiKioMEeHyIiIn/Hh5t9hg0fFfv/Pk3laZaVlCvjNo/LRzlQUuyQuywfKXvI00wuaRM2cdPyNjx8NVMzev6l16Q8K+H8Sa+L6oRsI3d1krbt8eir0jH3JsNIykET1mWHkIPmzRCy0jG3SteHUO4uo8zTPDwPN+0VjzOrhPW4y8sSM/fUxXop20vi1fEQvjO+PLbipj3IZmvJV8S1Zm6P7R4nNnyIiIj8HJ/x8R0+40NERERBgz0+RERE/k5DM5/x8VlNAh4bPkRERP6ODzf7DG91ERERUdBgjw8REZG/c8DLN+TOWJ4AsOFDRETk9/hWl+/wVhcREREFDfb4EBER+Ts+3OwzbPgQERH5OzZ8fIa3uoiIiChosMdHQV+nh17fuE2ol3KBpIa0kP3jkI66Tn5k31Cjnqa3quc3lUrrUZcba+R/DVjD1e3jmgRhAWFV4cXq8shjclhXSKU6NE1fqy7X9J699mAPk78CUi6QwerZ6xGGanVdbeHytqsT1NPqotTnwhauXk9tG3W5LVw+3/ZwYf+EjCbxiEvZV5oX/96SsvM8zb1z949eH4VsiWtxs20pz83TQeekQ+suHk3KmDNUqlcmzW8P8/yYayFC/puP/knuzX6LG1fNX9uCfQfs8fEZ9vgQERH5O4cPPh7IycnBgAEDEBUVhcTERNxyyy3Iz89XzqtpGkaNGgWdTocPPvjAZVphYSFGjx6N8PBwJCYmYvr06bDZ3CRTtwA2fIiIiPxcw+vszfl4YsuWLcjMzMS2bduwfv16WK1WZGRkoLKystG8L730EnSKOxZ2ux2jR49GXV0dvvrqKyxduhRLlizBk08+6fVx8AXe6iIiIgoSZWVlLn+bzWaYzeZG861bt87l7yVLliAxMRF5eXkYMmSIs3zXrl14/vnn8c033yAlJcVlmU8//RQ//PADNmzYgKSkJFx22WWYO3cuZsyYgdmzZ8NkMvlwz5qOPT5ERET+ruEZn+Z8AKSmpiI6Otr5ycnJadLmS0vrHxyNjY11llVVVeHuu+/GggULkJyc3GiZ3Nxc9OrVC0lJSc6yESNGoKysDHv37m3O0WgW9vgQERH5O4fm/mntpiwP4MiRI7BYLM5iVW9Po0UdDkyZMgWDBw9Gz549neVTp07FoEGDcPPNNyuXKy4udmn0AHD+XVwsvO3SAtjwISIiChIWi8Wl4dMUmZmZ2LNnD7Zu3eosW716NTZu3IidO3f6uornHW91ERER+Tsf3eryVFZWFtasWYNNmzahXbt2zvKNGzeioKAAMTExMBqNMBrr+1HGjBmDoUOHAgCSk5Nx/Phxl/U1/K26NdZS2PAhIiLye81t9HjW8NE0DVlZWVi5ciU2btyIjh07ukyfOXMmdu/ejV27djk/APDiiy9i8eLFAID09HR8//33OHHihHO59evXw2KxoEePHs06Gs3BW11ERETkIjMzE8uXL8eqVasQFRXlfCYnOjoaYWFhSE5OVvbatG/f3tlIysjIQI8ePXDPPfdg/vz5KC4uxuOPP47MzMwmPVt0vrDHh4iIyN+18K2uhQsXorS0FEOHDkVKSorz88477zR5HQaDAWvWrIHBYEB6ejrGjRuH8ePHY86cOZ7uvU8FRI/P5s2bce211yqnff311xgwYAAOHTrUqCsOqH+d7sorr/Roe/E7NRgVQ6nrbeoLRyr3dLh5eax7md6q3oihVj1Mp75OXe4u6sEeblCW246plzHWqutkOl2rLDdU1onb1lWpl4FNyDCQ9kMRQVK/AS8OukMYAlVal0G9beNpedvmIvU0LVQ97kVtXKiy/GRf9fzWaPni1NmEenl4qMTYAXf/3BKqpRPiJKTYAX2F+po1CJcTABiky9DD5A2HMDSJuxgGvTCQrV6okxRVY1dfBtDUhwMAoBO+SlK8jRS5oxeuG4ebbTukZUzqgy5dgoZa9ZSQCnnbhmp1ubSMqaLxxWazajgsb8K3HJ7frmq8fNNpXjwTpFomLS0Na9eu9Xhd51NANHwGDRqEoqIil7InnngCn332Gfr37+9SvmHDBlx66aXOv+Pi4lqkjkREROT/AqLhYzKZXO4lWq1WrFq1Cg8//HCjYbLj4uJa9WlxIiIin9Mc9Z/mLE8AAvQZn9WrV+PUqVOYNGlSo2k33XQTEhMTcdVVV2H16tVu11NbW4uysjKXDxERkd9ppdfZL0QB2fB5/fXXMWLECJcxBSIjI/H888/j3XffxUcffYSrrroKt9xyi9vGT05OjsvQ3ampqS1RfSIiIs84tOZ/CEArN3xmzpwJnU7n9rN//36XZX766Sd88sknuO+++1zK4+PjMW3aNAwcOBADBgzAvHnzMG7cODz77LPi9rOzs1FaWur8HDly5LzsJxEREfmHVn3G59FHH8XEiRPdztOpUyeXvxcvXoy4uDjcdNNN51z/wIEDsX79enG6lEpLRETkV5p7u4q3upxateGTkJCAhISEJs+vaRoWL16M8ePHIyQk5Jzz79q1CykpKc2pIhERUevT0MyGj89qEvAC4q2uBhs3bsTBgwfxf//3f42mLV26FCaTCX379gUArFixAm+88QZee+21lq4mERER+amAavi8/vrrGDRoELp166acPnfuXBw+fBhGoxHdunXDO++8g9/85jctXEsiIiIf460unwmohs/y5cvFaRMmTMCECRNasDZEREQtxOEA0IyxeKQR54NQQL7OTkREROSNgOrxaSkhVeqsLp0wDoKUFwS7NL+Q+SXMD8gZWzqblMmlDuDRWYVgHjf/GpC2AbsUlCTkTEl5WW6a35pZuESlcilGy5vcGYOUvaUOY9KM6h3RQtRhRZqQ4QUAjhD1NGuUer+r44VAJGG3TafdbFv6VRAWETOopNwtH/a4SzlT4ral+d1M00k5WkK5lPkl5WsB8m+CxGFUX5s2KcPLzcur4u+XUCUxV0wod/dQrXTt6DQhm02K6PO0TgBCKtUVM1YLv8+qXEQhK/G84K0un2HDh4iIyN+x4eMzvNVFREREQYM9PkRERP7OoaFZg/EwssKJDR8iIiI/p2kOaM1IWG/OshcaNnyIiIj8ndbMoFE+4+PEZ3yIiIgoaLDHh4iIyN9pzXzGhz0+Tmz4EBER+TuHw82gS03AZ3yceKuLiIiIggZ7fIiIiPwdb3X5DBs+CqYyK4xGRQSAr64bIQnBHU2IgYAQk+DQCfOb1NEGbofMl74wnsZDeFruzTb06nJNmt9NTIiYB+gQ4kDsHsaESOcIgF6IyzBWqM936El1eXSB+nzbzULEhRtSTILDpC63m9V1qouS99sRImxD+KUSYyakaAM351u6iyBGUNR5Fj2jc7dtaZL0lRH66qVz5O5aEwnfGek4iZE+3vxuehp3Ih1zN9uWzof026Ka3yBlhJwHmsMBrRm3uvg6+694q4uIiIiCBnt8iIiI/B1vdfkMGz5ERET+zqF5ed/wf9jwceKtLiIiIgoa7PEhIiLyd5oG+Y2Lpi5PABs+REREfk9zaNCacatLfLM1CLHhQ0RE5O80B5rX48PX2RvwGR8iIiIKGuzxISIi8nO81eU7bPgQERH5O97q8hk2fM7Q0CK22WqFGXy0IS9GjxeHcJeGiZeGxxeHofciskIcVt6z+X0aWeFpubvICk+J+yfcUXYTI6BJF4mmLteEu9YOvRBZYfAiskLYthSPYter62SvcxNZIW1Dug48jKzQvIis0IRUAr3VDyMrhOMXtJEVbjbhi8iKhv9XtERvig3WZv0/yIaWi9fwd2z4nKG8vBwA8FXuM61cEyIiChTl5eWIjo4+L+s2mUxITk7G1uK1zV5XcnIyTCaTD2oV2HQab/w5ORwOHDt2DFFRUdDpdCgrK0NqaiqOHDkCi8XS2tXzKe5bYOK+BSbuW2A6175pmoby8nK0bdsWeqGH0xdqampQVyek5XrAZDIhNDTUBzUKbOzxOYNer0e7du0alVsslgvuC92A+xaYuG+BifsWmNzt2/nq6TlTaGgoGyw+xNfZiYiIKGiw4UNERERBgw0fN8xmM2bNmgWz2dzaVfE57ltg4r4FJu5bYLqQ9y2Y8eFmIiIiChrs8SEiIqKgwYYPERERBQ02fIiIiChosOFDREREQYMNH8GCBQvQoUMHhIaGYuDAgfj6669bu0oemz17NnQ6ncunW7duzuk1NTXIzMxEXFwcIiMjMWbMGBw/frwVayz7/PPPceONN6Jt27bQ6XT44IMPXKZrmoYnn3wSKSkpCAsLw/Dhw/Hjjz+6zHP69GmMHTsWFosFMTExuO+++1BRUdGCe6F2rn2bOHFio/M4cuRIl3n8dd9ycnIwYMAAREVFITExEbfccgvy8/Nd5mnKdVhYWIjRo0cjPDwciYmJmD59Omw2W0vuSiNN2behQ4c2OncPPvigyzz+uG8LFy5E7969nQP3paen4+OPP3ZOD9RzBpx73wL1nFHTseGj8M4772DatGmYNWsWvv32W/Tp0wcjRozAiRMnWrtqHrv00ktRVFTk/GzdutU5berUqfjwww/x7rvvYsuWLTh27Bhuu+22VqytrLKyEn369MGCBQuU0+fPn4+XX34ZixYtwvbt2xEREYERI0agpqbGOc/YsWOxd+9erF+/HmvWrMHnn3+O+++/v6V2QXSufQOAkSNHupzHt956y2W6v+7bli1bkJmZiW3btmH9+vWwWq3IyMhAZWWlc55zXYd2ux2jR49GXV0dvvrqKyxduhRLlizBk08+2Rq75NSUfQOAyZMnu5y7+fPnO6f56761a9cO8+bNQ15eHr755htcd911uPnmm7F3714AgXvOgHPvGxCY54w8oFEjV1xxhZaZmen82263a23bttVycnJasVaemzVrltanTx/ltJKSEi0kJER79913nWX79u3TAGi5ubktVEPvANBWrlzp/NvhcGjJycnas88+6ywrKSnRzGaz9tZbb2mapmk//PCDBkDbsWOHc56PP/5Y0+l02tGjR1us7udy9r5pmqZNmDBBu/nmm8VlAmXfNE3TTpw4oQHQtmzZomla067DtWvXanq9XisuLnbOs3DhQs1isWi1tbUtuwNunL1vmqZp11xzjfbII4+IywTKvmmaprVp00Z77bXXLqhz1qBh3zTtwjpnpMYen7PU1dUhLy8Pw4cPd5bp9XoMHz4cubm5rVgz7/z4449o27YtOnXqhLFjx6KwsBAAkJeXB6vV6rKf3bp1Q/v27QNuPw8ePIji4mKXfYmOjsbAgQOd+5Kbm4uYmBj079/fOc/w4cOh1+uxffv2Fq+zpzZv3ozExER07doVDz30EE6dOuWcFkj7VlpaCgCIjY0F0LTrMDc3F7169UJSUpJznhEjRqCsrMzlX+mt7ex9a7Bs2TLEx8ejZ8+eyM7ORlVVlXNaIOyb3W7H22+/jcrKSqSnp19Q5+zsfWsQ6OeM3GNI6Vl+/vln2O12l4saAJKSkrB///5WqpV3Bg4ciCVLlqBr164oKirCU089hauvvhp79uxBcXExTCYTYmJiXJZJSkpCcXFx61TYSw31VZ2zhmnFxcVITEx0mW40GhEbG+v3+zty5Ejcdttt6NixIwoKCvDHP/4Ro0aNQm5uLgwGQ8Dsm8PhwJQpUzB48GD07NkTAJp0HRYXFyvPbcM0f6DaNwC4++67kZaWhrZt22L37t2YMWMG8vPzsWLFCgD+vW/ff/890tPTUVNTg8jISKxcuRI9evTArl27Av6cSfsGBPY5o6Zhw+cCNmrUKOd/9+7dGwMHDkRaWhr+/e9/IywsrBVrRp648847nf/dq1cv9O7dG507d8bmzZsxbNiwVqyZZzIzM7Fnzx6X58wuFNK+nfmcVa9evZCSkoJhw4ahoKAAnTt3bulqeqRr167YtWsXSktL8d5772HChAnYsmVLa1fLJ6R969GjR0CfM2oa3uo6S3x8PAwGQ6M3FI4fP47k5ORWqpVvxMTE4JJLLsGBAweQnJyMuro6lJSUuMwTiPvZUF935yw5ObnRw+k2mw2nT58OuP3t1KkT4uPjceDAAQCBsW9ZWVlYs2YNNm3ahHbt2jnLm3IdJicnK89tw7TWJu2bysCBAwHA5dz5676ZTCZ06dIF/fr1Q05ODvr06YO//vWvF8Q5k/ZNJZDOGTUNGz5nMZlM6NevHz777DNnmcPhwGeffeZyDzgQVVRUoKCgACkpKejXrx9CQkJc9jM/Px+FhYUBt58dO3ZEcnKyy76UlZVh+/btzn1JT09HSUkJ8vLynPNs3LgRDofD+cMWKH766SecOnUKKSkpAPx73zRNQ1ZWFlauXImNGzeiY8eOLtObch2mp6fj+++/d2ncrV+/HhaLxXl7ojWca99Udu3aBQAu584f903F4XCgtrY2oM+ZpGHfVAL5nJGgtZ+u9kdvv/22ZjabtSVLlmg//PCDdv/992sxMTEuT/EHgkcffVTbvHmzdvDgQe3LL7/Uhg8frsXHx2snTpzQNE3THnzwQa19+/baxo0btW+++UZLT0/X0tPTW7nWauXl5drOnTu1nTt3agC0F154Qdu5c6d2+PBhTdM0bd68eVpMTIy2atUqbffu3drNN9+sdezYUauurnauY+TIkVrfvn217du3a1u3btUuvvhi7a677mqtXXJyt2/l5eXaY489puXm5moHDx7UNmzYoF1++eXaxRdfrNXU1DjX4a/79tBDD2nR0dHa5s2btaKiIuenqqrKOc+5rkObzab17NlTy8jI0Hbt2qWtW7dOS0hI0LKzs1tjl5zOtW8HDhzQ5syZo33zzTfawYMHtVWrVmmdOnXShgwZ4lyHv+7bzJkztS1btmgHDx7Udu/erc2cOVPT6XTap59+qmla4J4zTXO/b4F8zqjp2PARvPLKK1r79u01k8mkXXHFFdq2bdtau0oeu+OOO7SUlBTNZDJpF110kXbHHXdoBw4ccE6vrq7Wfve732lt2rTRwsPDtVtvvVUrKipqxRrLNm3apAFo9JkwYYKmafWvtD/xxBNaUlKSZjabtWHDhmn5+fku6zh16pR21113aZGRkZrFYtEmTZqklZeXt8LeuHK3b1VVVVpGRoaWkJCghYSEaGlpadrkyZMbNcL9dd9U+wVAW7x4sXOeplyHhw4d0kaNGqWFhYVp8fHx2qOPPqpZrdYW3htX59q3wsJCbciQIVpsbKxmNpu1Ll26aNOnT9dKS0td1uOP+3bvvfdqaWlpmslk0hISErRhw4Y5Gz2aFrjnTNPc71sgnzNqOp2maVrL9S8RERERtR4+40NERERBgw0fIiIiChps+BAREVHQYMOHiIiIggYbPkRERBQ02PAhIiKioMGGDxEREQUNNnyIiIgoaLDhQxQghg4diilTplww25w4cSJuueWW87JuIiKJsbUrQET+a8WKFQgJCXH+3aFDB0yZMqXFG2BERL7Chg8RiWJjY1u7CkREPsVbXUQB6JdffsH48ePRpk0bhIeHY9SoUfjxxx+d05csWYKYmBh88skn6N69OyIjIzFy5EgUFRU557HZbPj973+PmJgYxMXFYcaMGZgwYYLL7aczb3UNHToUhw8fxtSpU6HT6aDT6QAAs2fPxmWXXeZSv5deegkdOnRw/m232zFt2jTntv7whz/g7JhAh8OBnJwcdOzYEWFhYejTpw/ee+893xwwIqL/YcOHKABNnDgR33zzDVavXo3c3FxomoYbbrgBVqvVOU9VVRWee+45/POf/8Tnn3+OwsJCPPbYY87pzzzzDJYtW4bFixfjyy+/RFlZGT744ANxmytWrEC7du0wZ84cFBUVuTSizuX555/HkiVL8MYbb2Dr1q04ffo0Vq5c6TJPTk4O3nzzTSxatAh79+7F1KlTMW7cOGzZsqXpB4aI6Bx4q4sowPz4449YvXo1vvzySwwaNAgAsGzZMqSmpuKDDz7Ab3/7WwCA1WrFokWL0LlzZwBAVlYW5syZ41zPK6+8guzsbNx6660AgL/97W9Yu3atuN3Y2FgYDAZERUUhOTnZozq/9NJLyM7Oxm233QYAWLRoET755BPn9NraWjz99NPYsGED0tPTAQCdOnXC1q1b8fe//x3XXHONR9sjIpKw4UMUYPbt2wej0YiBAwc6y+Li4tC1a1fs27fPWRYeHu5s9ABASkoKTpw4AQAoLS3F8ePHccUVVzinGwwG9OvXDw6Hw6f1LS0tRVFRkUt9jUYj+vfv77zddeDAAVRVVeH66693Wbaurg59+/b1aX2IKLix4UN0gTrzbSwA0Ol0jZ6r8QW9Xt9ovWfecmuKiooKAMBHH32Eiy66yGWa2WxuXgWJiM7AZ3yIAkz37t1hs9mwfft2Z9mpU6eQn5+PHj16NGkd0dHRSEpKwo4dO5xldrsd3377rdvlTCYT7Ha7S1lCQgKKi4tdGj+7du1y2VZKSopLfW02G/Ly8px/9+jRA2azGYWFhejSpYvLJzU1tUn7RETUFOzxIQowF198MW6++WZMnjwZf//73xEVFYWZM2fioosuws0339zk9Tz88MPIyclBly5d0K1bN7zyyiv45ZdfnG9rqXTo0AGff/457rzzTpjNZsTHx2Po0KE4efIk5s+fj9/85jdYt24dPv74Y1gsFudyjzzyCObNm4eLL74Y3bp1wwsvvICSkhLn9KioKDz22GOYOnUqHA4HrrrqKpSWluLLL7+ExWLBhAkTvDpWRERnY48PUQBavHgx+vXrh//3//4f0tPToWka1q5d2+j2ljszZszAXXfdhfHjxyM9PR2RkZEYMWIEQkNDxWXmzJmDQ4cOoXPnzkhISABQ3wP16quvYsGCBejTpw++/vprl7fHAODRRx/FPffcgwkTJiA9PR1RUVHOh6obzJ07F0888QRycnLQvXt3jBw5Eh999BE6duzowZEhInJPp52Pm/5EFHAcDge6d++O22+/HXPnzm3t6hARnRe81UUUpA4fPoxPP/0U11xzDWpra/G3v/0NBw8exN13393aVSMiOm94q4soSOn1eixZsgQDBgzA4MGD8f3332PDhg3o3r17a1eNiOi84a0uIiIiChrs8SEiIqKgwYYPERERBQ02fIiIiChosOFDREREQYMNHyIiIgoabPgQERFR0GDDh4iIiIIGGz5EREQUNP5/WBsWZhSy8zUAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ds_output['2m_temperature'].plot(x='longitude', y='latitude')" + ] } ], "metadata": { diff --git a/packages/bundled_models/persistence/pyproject.toml b/packages/bundled_models/persistence/pyproject.toml index dbe3d989..0ed338d4 100644 --- a/packages/bundled_models/persistence/pyproject.toml +++ b/packages/bundled_models/persistence/pyproject.toml @@ -76,6 +76,8 @@ xarray = ">=2026.1.0,<2027" meson = ">=1.10.1,<2" cffi = ">=2.0.0,<3" setuptools = ">=82.0.1,<83" +pip = ">=26.0.1,<27" +jupyter = ">=1.1.1,<2" [tool.pixi.feature.testing.dependencies] pytest = ">=9.0.2,<10" From 980becfd747bcbbe7bad0477648fb14f24d7a50c Mon Sep 17 00:00:00 2001 From: Nikeeth Ramanathan Date: Tue, 17 Mar 2026 11:29:19 +1100 Subject: [PATCH 16/16] [skip ci] minor wording --- .../persistence/notebooks/pipeline_example.ipynb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/bundled_models/persistence/notebooks/pipeline_example.ipynb b/packages/bundled_models/persistence/notebooks/pipeline_example.ipynb index 3a9db744..2459c851 100644 --- a/packages/bundled_models/persistence/notebooks/pipeline_example.ipynb +++ b/packages/bundled_models/persistence/notebooks/pipeline_example.ipynb @@ -160,8 +160,9 @@ "# TemporalRetrieval does the same, but each index is a delta-unit mapping - so we need to reverse engineer a bit\n", "# !!! IMPORTANT !!!\n", "# > The circumstance here is that time is already 6 hourly windows and in chunks of 4.\n", - "# > Something about the behaviour has changed, causing the window to select 4 indices at a time,\n", - "# > so specifying `a = -6` actually fetches data from 4 * 6 * 6 = 6 days in the past, rather than 36 hours in the past.\n", + "# > Something about the behaviour has changed, causing the window to select 4 indices at a time.\n", + "# > Due to the additional bundling of 4 timesteps, specifying `a = -6` actually fetches data from-\n", + "# > 6 days (4 * 6 * 6) in the past, rather than 36 hours (6 * 6) in the past.\n", "# > Unfortunately, this means we can't propagate in timesteps of 6 hours without some manual tweaks.\n", "# ---\n", "import datetime\n",