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..8eb575f6 --- /dev/null +++ b/packages/bundled_models/persistence/.gitignore @@ -0,0 +1,5 @@ +# pixi environments +.zig-cache +.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/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/README.md b/packages/bundled_models/persistence/examples/README.md new file mode 100644 index 00000000..6ec804bb --- /dev/null +++ b/packages/bundled_models/persistence/examples/README.md @@ -0,0 +1,109 @@ +## Overview & Philosophy + +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. + +* **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 (statistical methods like mean, median, etc.) differ significantly from inference models (pre-trained weights) in computational requirements. + +### Comparison: Persistence vs. Inference + +| 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 | + +**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. + +## Execution Modes + +The examples are organized around specific execution paradigms. Understanding these modes is critical to selecting the correct example for your environment. + +### Core Concepts + +* **`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. + +### Execution Matrix + +```mermaid +flowchart TD + A[Start] --> B{Data Loading Strategy}; + + B -- Standard --> C[pet-pipeline
Retrieves Indexing]; + B -- Custom --> D[standalone
User loads Data]; + + C --> E{Computation Strategy}; + D --> E; + + 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]; + + 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]; +``` + +### 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 + +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. | + +> **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) + 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..d9b499bc --- /dev/null +++ b/packages/bundled_models/persistence/examples/nci_py_mp.py @@ -0,0 +1,156 @@ +""" +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, num_workers=1, num_chunks=1): + # 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=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="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 + + # 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, num_workers=1, num_chunks=1) diff --git a/packages/bundled_models/persistence/examples/zigc.py b/packages/bundled_models/persistence/examples/zigc.py new file mode 100644 index 00000000..3a1a51d3 --- /dev/null +++ b/packages/bundled_models/persistence/examples/zigc.py @@ -0,0 +1,251 @@ +""" +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__": + import multiprocessing + + # --- + # 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. + # + # CAUTION: windows/mac - see WHEN IN DOUBT above, except it applies ALMOST ALWAYS. + # --- + NUM_WORKERS = 1 + NUM_CHUNKS = 1 + + 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() + ds_output1 = run_example( + ds_input, + use_real=False, + backend="zig", + 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 + # 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=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) 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": "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", + "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..2459c851 --- /dev/null +++ b/packages/bundled_models/persistence/notebooks/pipeline_example.ipynb @@ -0,0 +1,1274 @@ +{ + "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", + "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", + "# ---\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", + "# ---\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", + "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", + "file_location = workdir + '/mini.nc'\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}\")" + ] + }, + { + "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[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", + " 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", + " name_vars,\n", + " filename_override=file_location\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "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 !!!\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", + "# > 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", + "import functools\n", + "data_pipeline = pyearthtools.pipeline.Pipeline(\n", + " accessor,\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", + " 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": 7, + "id": "576b55de-5494-445c-9888-52bffdfb85f7", + "metadata": {}, + "outputs": [ + { + "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": [ + "# Check that accessor works through the pipeline:\n", + "data_pipeline[\"2000-05-05\"][0]\n", + "# ---\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": 8, + "id": "e2f2843f-2afb-4570-8f7d-b88854d8d706", + "metadata": {}, + "outputs": [], + "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": "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Wjz99NPYsGED0tPTAQCdOnXC1q1b8fe//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": { + "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/pixi.lock b/packages/bundled_models/persistence/pixi.lock new file mode 100644 index 00000000..1b8cd7b6 --- /dev/null +++ b/packages/bundled_models/persistence/pixi.lock @@ -0,0 +1,3448 @@ +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/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 + - 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/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 + - 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/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 + - 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/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 + - 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/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 + - 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/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 + - 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/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 + - 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/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 + - 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/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 + - 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/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 + - 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' +- 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 + 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/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 + 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/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 + 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 +- 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 + 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: 5bfc864f36b9852afbf5135847de28dcfd7087cfc682b360f2639cd84d1aa4dd + 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' +- 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 + 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..0ed338d4 --- /dev/null +++ b/packages/bundled_models/persistence/pyproject.toml @@ -0,0 +1,96 @@ +[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" +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" +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/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/lib/zig/lib.zig b/packages/bundled_models/persistence/src/lib/zig/lib.zig new file mode 100644 index 00000000..ad7ac38a --- /dev/null +++ b/packages/bundled_models/persistence/src/lib/zig/lib.zig @@ -0,0 +1,26 @@ +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); +} + +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 new file mode 100644 index 00000000..50dd8644 --- /dev/null +++ b/packages/bundled_models/persistence/src/lib/zig/median.zig @@ -0,0 +1,266 @@ +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 +// ---------------------------------------------------------------------------- +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 { + // --- 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); + std.debug.assert(@as(usize, @intCast(len_in)) == shape[@as(usize, @intCast(idx_time))] * len_out); + + 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], + ); + } + } +} + +// ---------------------------------------------------------------------------- +// 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 +// ---------------------------------------------------------------------------- +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; + for (xs) |x| { + if (!std.math.isNan(x)) { + valid[num_valid] = x; + num_valid += 1; + } + } + + 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(@min(@max(x0, x1), x2), @min(x0, x1)); + 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); +} + +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/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/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/__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..73e3b42f --- /dev/null +++ b/packages/bundled_models/persistence/src/persistence/include/_cffi.py @@ -0,0 +1,36 @@ +""" +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); +void median_of_three_nd(int, int[], int, float[], int, float[], int); +""" +_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/interface/__init__.py b/packages/bundled_models/persistence/src/persistence/interface/__init__.py new file mode 100644 index 00000000..02584428 --- /dev/null +++ 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/_backend.py b/packages/bundled_models/persistence/src/persistence/interface/_backend.py new file mode 100644 index 00000000..a4377cc7 --- /dev/null +++ b/packages/bundled_models/persistence/src/persistence/interface/_backend.py @@ -0,0 +1,67 @@ +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" + NUMBA = "numba" + NUMPY = "numpy" + RUST = "rust" + ZIG = "zig" + 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 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 new file mode 100644 index 00000000..1319b707 --- /dev/null +++ b/packages/bundled_models/persistence/src/persistence/interface/_chunker.py @@ -0,0 +1,438 @@ +from dataclasses import dataclass +import copy +import math +import numpy as np +import xarray as xr +import functools + +from typing import Generator + +from persistence.interface._metadata import PersistenceMetadata +from persistence.interface.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] + + # reduced dimensions expected from the output (reduced) + slice_dims_reduced: 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. + """ + # 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 + 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 + 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, + slice_dims, + slice_dims_reduced, + ) + + # 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..518ebcba --- /dev/null +++ b/packages/bundled_models/persistence/src/persistence/interface/_compute.py @@ -0,0 +1,318 @@ +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 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, _median_of_three_zig +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. + + 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. + """ + # 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 + 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) + + # 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) + 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 + with concurrent.futures.ProcessPoolExecutor( + 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 _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.ZIG: + return self._method_impl_zig(arr) + case _: + self._raise_unimplemented_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 _: + self._raise_unimplemented_method() + + 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: + """ + 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..8e674538 --- /dev/null +++ b/packages/bundled_models/persistence/src/persistence/interface/types.py @@ -0,0 +1,199 @@ +""" +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: + _dummyvarname = "dummy_varname" + + def __init__( + self, + arraylike: PetDataArrayLike, + dummy_varname="dummy_varname", # 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._dummyvarname = dummy_varname + 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 = {} + + # 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) + + 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: + # if returning a raw result compare original type and strip as necessary + 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 == 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 == PetInputDataType.XR_DATASET: + # nothing to do + return ds + elif self.raw_type == PetInputDataType.XR_DATAARRAY: + # extract the dataarray + return ds[self._dummyvarname] + 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/__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..3c79ec83 --- /dev/null +++ b/packages/bundled_models/persistence/src/persistence/methods/_median.py @@ -0,0 +1,135 @@ +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 + # computed at a higher level. + # + # 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, axis=idx_time, keepdims=True) + assert list(shape_out) == list(arr_median.shape) + return arr_median + + +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) 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..fe66dfc0 --- /dev/null +++ b/packages/bundled_models/persistence/src/persistence/persistence_impl.py @@ -0,0 +1,274 @@ +""" +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. +""" + +import xarray as xr + +from persistence.interface import ( + PetDataArrayLike, + PersistenceComputePool, + PersistenceBackendType, + PersistenceMethod, + PersistenceMetadata, + PersistenceChunker, + PersistenceChunkInfo, + PetDataset, +) + +from persistence.config.dask import _set_synchronous_dask + + +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, +) -> 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. + """ + # force it to EnumStr - auto raises error if not compatible. + if isinstance(method, str): + 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 + + +def _predict_single_var( + da: xr.DataArray, + 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 + """ + # --- 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__": + 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)