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..9dfa217e --- /dev/null +++ b/packages/bundled_models/persistence/.gitignore @@ -0,0 +1,6 @@ +# pixi environments +.zig-cache +.pixi/* +!.pixi/config.toml +report.xml +worker 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/experiments/patterns/push-pull/.gitattributes b/packages/bundled_models/persistence/experiments/patterns/push-pull/.gitattributes new file mode 100644 index 00000000..997504b4 --- /dev/null +++ b/packages/bundled_models/persistence/experiments/patterns/push-pull/.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/experiments/patterns/push-pull/.gitignore b/packages/bundled_models/persistence/experiments/patterns/push-pull/.gitignore new file mode 100644 index 00000000..ae849e65 --- /dev/null +++ b/packages/bundled_models/persistence/experiments/patterns/push-pull/.gitignore @@ -0,0 +1,3 @@ +# pixi environments +.pixi/* +!.pixi/config.toml diff --git a/packages/bundled_models/persistence/experiments/patterns/push-pull/NOTES b/packages/bundled_models/persistence/experiments/patterns/push-pull/NOTES new file mode 100644 index 00000000..0407f778 --- /dev/null +++ b/packages/bundled_models/persistence/experiments/patterns/push-pull/NOTES @@ -0,0 +1,2 @@ +worker = server = performs an action => needs to be active before client requests get accepted. +manager = client = requests an action => needs to wait for server to be active before sending requests. diff --git a/packages/bundled_models/persistence/experiments/patterns/push-pull/manager.py b/packages/bundled_models/persistence/experiments/patterns/push-pull/manager.py new file mode 100644 index 00000000..2e33b9d4 --- /dev/null +++ b/packages/bundled_models/persistence/experiments/patterns/push-pull/manager.py @@ -0,0 +1,59 @@ +if __name__ == "__main__": + import zmq + import time + + PUSH_URL = "tcp://127.0.0.1:5558" + PULL_URL = "tcp://127.0.0.1:5559" + # best to keep with strict context management in python impl + with zmq.Context() as ctx: + with ctx.socket(zmq.PUSH) as s_push, ctx.socket(zmq.PULL) as s_pull: + print(">>> TEST get state (first time) with sleep 100ms") + # first control message + with s_push.connect(PUSH_URL): + print("client: A_CONTROLLER_STATE") + s_push.send_string("A_CONTROLLER_STATE") + # ---> sleep <--- + time.sleep(0.1) + # wait before receiving for initialization + with s_pull.bind(PULL_URL): + reply = s_pull.recv_string() + print("worker: " + reply) + assert reply == "C_OK" + + print(">>> TEST get state no sleep") + # second control message wait + with s_push.connect(PUSH_URL): + print("client: A_CONTROLLER_STATE") + s_push.send_string("A_CONTROLLER_STATE") + # ---> no sleep <--- + # should receive the reply instantly + with s_pull.bind(PULL_URL): + reply = s_pull.recv_string() + print("worker: " + reply) + assert reply == "C_OK" + + print(">>> TEST kill worker") + # stop worker (normally this is irrecoverable, unless the worker + # auto boots up after a timeout - or there is some slower polling + # going on to wake up the controller.) + with s_push.connect(PUSH_URL): + print("client: A_CONTROLLER_END") + s_push.send_string("A_CONTROLLER_END") + with s_pull.bind(PULL_URL): + reply = s_pull.recv_string() + print("worker: " + reply) + assert reply == "C_STOPPED" + + print(">>> TEST control message after worker died") + # !!! SHOULD ENDLESSLY BLOCK !!! + # send control message AFTER worker died + with s_push.connect(PUSH_URL): + print("client: A_CONTROLLER_STATE") + s_push.send_string("A_CONTROLLER_STATE") + # ---> sleep <--- + time.sleep(0.1) + # wait before receiving for initialization + with s_pull.bind(PULL_URL): + reply = s_pull.recv_string() + print("worker: " + reply) + assert reply == "C_OK" diff --git a/packages/bundled_models/persistence/experiments/patterns/push-pull/pixi.lock b/packages/bundled_models/persistence/experiments/patterns/push-pull/pixi.lock new file mode 100644 index 00000000..6a14885e --- /dev/null +++ b/packages/bundled_models/persistence/experiments/patterns/push-pull/pixi.lock @@ -0,0 +1,420 @@ +version: 6 +environments: + default: + channels: + - url: https://conda.anaconda.org/conda-forge/ + options: + pypi-prerelease-mode: if-necessary-or-explicit + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.3-py314hd8ed1ab_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.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_102.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/libexpat-2.7.4-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_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.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/libsodium-1.0.21-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.52.0-hf4e2dac_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.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.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.3-h32b2ec7_101_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.3-h4df99d1_101.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hda471dd_2.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.7-h7805a7d_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/linux-64/zeromq-4.3.5-h41580af_10.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda +packages: +- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + build_number: 20 + sha256: 1dd3fffd892081df9726d7eb7e0dea6198962ba775bd88842135a4ddb4deb3c9 + md5: a9f577daf3de00bca7c3c76c0ecbd1de + depends: + - __glibc >=2.17,<3.0.a0 + - libgomp >=7.5.0 + constrains: + - openmp_impl <0.0a0 + license: BSD-3-Clause + license_family: BSD + size: 28948 + timestamp: 1770939786096 +- conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + sha256: a3967b937b9abf0f2a99f3173fa4630293979bd1644709d89580e7c62a544661 + md5: aaa2a381ccc56eac91d63b6c1240312f + depends: + - cpython + - python-gil + license: MIT + license_family: MIT + size: 8191 + timestamp: 1744137672556 +- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + sha256: 0b75d45f0bba3e95dc693336fa51f40ea28c980131fec438afb7ce6118ed05f6 + md5: d2ffd7602c02f2b316fd921d39876885 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: bzip2-1.0.6 + license_family: BSD + size: 260182 + timestamp: 1771350215188 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda + sha256: 67cc7101b36421c5913a1687ef1b99f85b5d6868da3abbf6ec1a4181e79782fc + md5: 4492fd26db29495f0ba23f146cd5638d + depends: + - __unix + license: ISC + size: 147413 + timestamp: 1772006283803 +- conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.14.3-py314hd8ed1ab_101.conda + noarch: generic + sha256: 91b06300879df746214f7363d6c27c2489c80732e46a369eb2afc234bcafb44c + md5: 3bb89e4f795e5414addaa531d6b1500a + depends: + - python >=3.14,<3.15.0a0 + - python_abi * *_cp314 + license: Python-2.0 + size: 50078 + timestamp: 1770674447292 +- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda + sha256: fbf86c4a59c2ed05bbffb2ba25c7ed94f6185ec30ecb691615d42342baa1a16a + md5: c80d8a3b84358cb967fa81e7075fbc8a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT + size: 12723451 + timestamp: 1773822285671 +- 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 + 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 + size: 1386730 + timestamp: 1769769569681 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda + sha256: 3d584956604909ff5df353767f3a2a2f60e07d070b328d109f30ac40cd62df6c + md5: 18335a698559cdbcd86150a48bf54ba6 + 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 + size: 728002 + timestamp: 1774197446916 +- 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 + size: 134676 + timestamp: 1738479519902 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.4-hecca717_0.conda + sha256: d78f1d3bea8c031d2f032b760f36676d87929b18146351c4464c66b0869df3f5 + md5: e7f7ce06ec24cfcfb9e36d28cf82ba57 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - expat 2.7.4.* + license: MIT + license_family: MIT + size: 76798 + timestamp: 1771259418166 +- 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 + size: 58592 + timestamp: 1769456073053 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda + sha256: faf7d2017b4d718951e3a59d081eb09759152f93038479b768e3d612688f83f5 + md5: 0aa00f03f9e39fb9876085dee11a85d4 + depends: + - __glibc >=2.17,<3.0.a0 + - _openmp_mutex >=4.5 + constrains: + - libgcc-ng ==15.2.0=*_18 + - libgomp 15.2.0 he0feb66_18 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 1041788 + timestamp: 1771378212382 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda + sha256: 21337ab58e5e0649d869ab168d4e609b033509de22521de1bfed0c031bfc5110 + md5: 239c5e9546c38a1e884d69effcf4c882 + depends: + - __glibc >=2.17,<3.0.a0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 603262 + timestamp: 1771378117851 +- 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 + 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 + size: 92400 + timestamp: 1769482286018 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.21-h280c20c_3.conda + sha256: 64e5c80cbce4680a2d25179949739a6def695d72c40ca28f010711764e372d97 + md5: 7af961ef4aa2c1136e11dd43ded245ab + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + license: ISC + size: 277661 + timestamp: 1772479381288 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.52.0-hf4e2dac_0.conda + sha256: d716847b7deca293d2e49ed1c8ab9e4b9e04b9d780aea49a97c26925b28a7993 + md5: fd893f6a3002a635b5e50ceb9dd2c0f4 + depends: + - __glibc >=2.17,<3.0.a0 + - icu >=78.2,<79.0a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + license: blessing + size: 951405 + timestamp: 1772818874251 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda + sha256: 78668020064fdaa27e9ab65cd2997e2c837b564ab26ce3bf0e58a2ce1a525c6e + md5: 1b08cd684f34175e4514474793d44bcb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc 15.2.0 he0feb66_18 + constrains: + - libstdcxx-ng ==15.2.0=*_18 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + size: 5852330 + timestamp: 1771378262446 +- 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 + size: 40311 + timestamp: 1766271528534 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + sha256: 55044c403570f0dc26e6364de4dc5368e5f3fc7ff103e867c487e2b5ab2bcda9 + md5: d87ff7921124eccd67248aa483c23fec + depends: + - __glibc >=2.17,<3.0.a0 + constrains: + - zlib 1.3.2 *_2 + license: Zlib + license_family: Other + size: 63629 + timestamp: 1774072609062 +- 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 + size: 891641 + timestamp: 1738195959188 +- 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 + size: 3164551 + timestamp: 1769555830639 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.3-h32b2ec7_101_cp314.conda + build_number: 101 + sha256: cb0628c5f1732f889f53a877484da98f5a0e0f47326622671396fb4f2b0cd6bd + md5: c014ad06e60441661737121d3eae8a60 + 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.14.* *_cp314 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - zstd >=1.5.7,<1.6.0a0 + license: Python-2.0 + size: 36702440 + timestamp: 1770675584356 + python_site_packages_path: lib/python3.14/site-packages +- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.14.3-h4df99d1_101.conda + sha256: 233aebd94c704ac112afefbb29cf4170b7bc606e22958906f2672081bc50638a + md5: 235765e4ea0d0301c75965985163b5a1 + depends: + - cpython 3.14.3.* + - python_abi * *_cp314 + license: Python-2.0 + size: 50062 + timestamp: 1770674497152 +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda + build_number: 8 + sha256: ad6d2e9ac39751cc0529dd1566a26751a0bf2542adb0c232533d32e176e21db5 + md5: 0539938c55b6b1a59b560e843ad864a4 + constrains: + - python 3.14.* *_cp314 + license: BSD-3-Clause + license_family: BSD + size: 6989 + timestamp: 1752805904792 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.1.0-py312hda471dd_2.conda + noarch: python + sha256: be66c1f85c3b48137200d62c12d918f4f8ad329423daef04fed292818efd3c28 + md5: 082985717303dab433c976986c674b35 + depends: + - python + - libgcc >=14 + - libstdcxx >=14 + - __glibc >=2.17,<3.0.a0 + - zeromq >=4.3.5,<4.4.0a0 + - _python_abi3_support 1.* + - cpython >=3.12 + license: BSD-3-Clause + license_family: BSD + size: 211567 + timestamp: 1771716961404 +- 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 + size: 345073 + timestamp: 1765813471974 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.15.7-h7805a7d_1.conda + noarch: python + sha256: 2985cfff61368323db477c2a0d7f100a57f6cb34aafec51ae96b6fc409d9090f + md5: f5678c1a929d9efe3c2397675ae90a3c + depends: + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + size: 9220190 + timestamp: 1774012576023 +- 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 + size: 3301196 + timestamp: 1769460227866 +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + sha256: 1d30098909076af33a35017eed6f2953af1c769e273a0626a04722ac4acaba3c + md5: ad659d0a2b3e47e38d829aa8cad2d610 + license: LicenseRef-Public-Domain + size: 119135 + timestamp: 1767016325805 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h41580af_10.conda + sha256: 325d370b28e2b9cc1f765c5b4cdb394c91a5d958fbd15da1a14607a28fee09f6 + md5: 755b096086851e1193f3b10347415d7c + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + - krb5 >=1.22.2,<1.23.0a0 + - libsodium >=1.0.21,<1.0.22.0a0 + license: MPL-2.0 + license_family: MOZILLA + size: 311150 + timestamp: 1772476812121 +- 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 + size: 601375 + timestamp: 1764777111296 diff --git a/packages/bundled_models/persistence/experiments/patterns/push-pull/pixi.toml b/packages/bundled_models/persistence/experiments/patterns/push-pull/pixi.toml new file mode 100644 index 00000000..a6adff5d --- /dev/null +++ b/packages/bundled_models/persistence/experiments/patterns/push-pull/pixi.toml @@ -0,0 +1,13 @@ +[workspace] +authors = ["Nikeeth Ramanathan "] +channels = ["conda-forge"] +name = "push-pull" +platforms = ["linux-64"] +version = "0.1.0" + +[tasks] + +[dependencies] +python = ">=3.14.3,<3.15" +pyzmq = ">=27.1.0,<28" +ruff = ">=0.15.7,<0.16" diff --git a/packages/bundled_models/persistence/experiments/patterns/push-pull/run_test.sh b/packages/bundled_models/persistence/experiments/patterns/push-pull/run_test.sh new file mode 100755 index 00000000..c559b0e9 --- /dev/null +++ b/packages/bundled_models/persistence/experiments/patterns/push-pull/run_test.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash + +# --- zig-test (worker-only) --- +# description: +# zig unit tests to check the state transition flow for the zig +# server/worker +# requirements: +# - entr for file watch +# - pthread (usually included in system libs if on linux) +# - czmq (high level apis) +# - zmq and any dependencies of czmq +# - TODO: link netcdf lib for file loading +# --- +ls worker.zig | entr bash -c 'clear && zig test -lc -lpthread -lczmq -lzmq worker.zig' +# --- + +# --- python-test (manager-only) --- +# description: +# python unit tests to check the state transition flow for the python +# client/manager +# requirements: +# - whatever is on PET +# - pyzmq (or similar bindings) +# --- +echo "TODO: python client tests not implemented" +# --- + +# --- end-to-end ---- +# The following is a implementation goal, NOT current state. +# +# description: +# This test will perform an end to end processing of netcdf file using zmq. +# +# The default pattern used is intentionally contrived - for learning purposes. +# There are two planes of communication, control and data. +# +# The control plane is for metadata flows, and is BIDIRECTIONAL between the +# worker and client. It's tcp based (though IPC can work) +# +# The data plane has two UNIDIRECTIONAL connections, one receiving +# information from the controller (inproc - fast). The other sending +# information to the client directly (ipc - not as fast, but better than tcp) +# +# Here the controller in the worker run in the zig process is essentially a +# proxy that listens to client (python commands or function calls) and +# offloads them to a task. The task(s) then crunch the numbers and spit it +# back to a "SINK" socket that the python process can listen to in order to +# consolidate the results. +# +# From the perspective of someone running e.g. +# `call "median_of_three" with chunks x y z from file F.nc` +# (note this is a made up message protocol) +# +# is effectively the same as running median_of_three(file, [x, y, z]) as a +# python function. In fact, the interface will still likely be the same, +# except for python's ability to dynamically translate symbols to strings +# making it simple to serialize this information into a zeromq socket. +# +# Process: +# 1. Python initializes the zig executable (worker) and connects to it. +# 2. Zig worker responds with information. +# +# Simplifications can definitely be made down the track including the usage of: +# - zpoller | we use our own loops/sleep +# - zactors | we have actions through dedicated socket) +# - zproxy | we are essentially having two state machines in zig, 1 acting +# | as the controller and the other for work performed by each task +# - zloop (event based) +# - ... and many more convenience features +# +# NOTE: The equivilent function is median_of_three in persistence models. +# +# (Linux-only, zig can have num_workers > 1, in conjunction with pthreads to +# allow for lean work to be done, to bypass IO bound limitations). +# requirements: +# - ditto above +echo "TODO: end-to-end tests not implemented" + +# --- diff --git a/packages/bundled_models/persistence/experiments/patterns/push-pull/worker.zig b/packages/bundled_models/persistence/experiments/patterns/push-pull/worker.zig new file mode 100644 index 00000000..c4dc5247 --- /dev/null +++ b/packages/bundled_models/persistence/experiments/patterns/push-pull/worker.zig @@ -0,0 +1,549 @@ +// ============================================================================ +// TODO: [ ] ClientAction - these needs to be caught by the controller +// TODO: [ ] WorkerTask pthreads(inproc) +// ---------------------------------------------------------------------------- +// NOTE: --------------------- +// NOTE: @ = bind; > = connect +// NOTE: --------------------- +// NOTE: we typically want to: +// NOTE: - bind(@): if we want to LISTEN for information from an +// NOTE: external party. +// NOTE: - connect(>): if we want to REQUEST for information from an +// NOTE: external party. +// NOTE: therefore for our ventilator: +// NOTE: a) > => PULL data from external party (party=client) +// NOTE: b) @ => PUSH response to external party (party=server) +// ============================================================================ + +const std = @import("std"); +const czmq = @cImport({ + // See https://github.com/ziglang/zig/issues/515 + @cDefine("_NO_CRT_STDIO_INLINE", "1"); + @cInclude("stdio.h"); + @cInclude("czmq.h"); +}); + +const WorkerError = error{ + TaskAlreadyExists, + TaskDoesNotExist, + ClientActionInvalid, + NotImplemented, +}; + +// === PUSH === +// Task header when returning task id, used for new task. +const C_TASK_ID_KEY: *const u8 = "C_TASK_ID"; +// Task with id not found in controller hashmap. +const C_TASK_NOT_FOUND: *const u8 = "C_TASK_NOT_FOUND"; +// Check whether client is ready. +const C_CLIENT_READY: *const u8 = "C_CLIENT_READY"; + +// PUSH: single "ready state" - controller +const ControllerState = enum { + controller_ok, + controller_busy, + controller_error, + controller_stopped, + + fn to_str(self: ControllerState) [*c]const u8 { + switch (self) { + .controller_ok => return "C_OK", + .controller_busy => return "C_BUSY", + .controller_error => return "C_ERROR", + .controller_stopped => return "C_STOPPED", + } + } +}; + +// --------------------------------------------------------------------------- +// PUSH: task state to send back to client +// +// NOTE: +// - There is a hidden state here where if the task no longer exists it +// may have been cleaned up +// - The catch-22 is when a task is cleaned up, it can't be queried, in +// such a circumstance, the controller will just return something akin to +// "TASK NOT FOUND". Depending on the client state, this can mean: +// - "ERROR", OR +// - more likely, "TASK HAS BEEN SUCCESSFULLY CLEANED UP" +// --------------------------------------------------------------------------- +const TaskState = enum { + task_ready, + task_working, + task_finished, + task_error, + + fn to_str(self: TaskState) [*c]const u8 { + switch (self) { + .task_ready => return "T_READY", + .task_working => return "T_WORKING", + .task_finished => return "T_FINISHED", + .task_error => return "T_ERROR", + } + } +}; + +// === PULL === +const ClientAction = enum { + controller_state, + controller_end, + task_state, + task_create, + task_end, + + fn init_from_str(s: [*]u8) !ClientAction { + if (czmq.strcmp(s, ClientAction.controller_state.to_str()) == 0) + return ClientAction.controller_state; + if (czmq.strcmp(s, ClientAction.controller_end.to_str()) == 0) + return ClientAction.controller_end; + if (czmq.strcmp(s, ClientAction.task_state.to_str()) == 0) + return ClientAction.task_state; + if (czmq.strcmp(s, ClientAction.task_end.to_str()) == 0) + return ClientAction.task_end; + if (czmq.strcmp(s, ClientAction.task_create.to_str()) == 0) + return ClientAction.task_end; + return WorkerError.ClientActionInvalid; + } + + fn to_str(self: ClientAction) [*c]const u8 { + switch (self) { + .controller_state => return "A_CONTROLLER_STATE", + .controller_end => return "A_CONTROLLER_END", + .task_state => return "A_TASK_STATE", + .task_create => return "A_TASK_CREATE", + .task_end => return "A_TASK_DISCONNECT", + } + } +}; + +const ControlSocket = struct { + // --- members --- + // recv action from client (includes "GET_STATE") + ptr_sock_pull: ?*czmq.zsock_t, + // send state to client + ptr_sock_push: ?*czmq.zsock_t, + + // --- const --- + // socket to pull actions + const PULL_PORT: u32 = 5558; + // socket to push replies + const PUSH_PORT: u32 = 5559; + + // ----------------------------------------------------------------------- + // Initialize the Socket with the pull socket (bind). + // The push (connect) will be established after initial comms from the + // client. + // ----------------------------------------------------------------------- + fn init() ControlSocket { + std.debug.print("Setting up pull socket connection tcp://:{d}\n", .{PULL_PORT}); + const uri_pull = std.fmt.comptimePrint("@tcp://127.0.0.1:{d}", .{PULL_PORT}); + const ptr_sock_pull = czmq.zsock_new_pull(uri_pull); + czmq.zclock_sleep(10); + std.debug.assert(ptr_sock_pull != null); + + // --- builder --- + return ControlSocket{ + .ptr_sock_push = null, + .ptr_sock_pull = ptr_sock_pull, + }; + } + + // ----------------------------------------------------------------------- + // Initializing the push connection is driven by the controller (unlike + // the pull connection which should always exist if the controller + // exists). + // ----------------------------------------------------------------------- + fn connect_push_sock(self: *ControlSocket) void { + if (self.ptr_sock_push) |_| return; + + // --- push --- + std.debug.print("Setting up push socket connection tcp://:{d}\n", .{PUSH_PORT}); + const uri_push = std.fmt.comptimePrint(">tcp://127.0.0.1:{d}", .{PUSH_PORT}); + const ptr_sock_push: ?*czmq.zsock_t = czmq.zsock_new_push(uri_push); + czmq.zclock_sleep(10); + + // update the push socket + self.ptr_sock_push = ptr_sock_push; + } + + fn destroy_push(self: *ControlSocket) void { + czmq.zsock_destroy(&self.ptr_sock_push); + } + + fn destroy_pull(self: *ControlSocket) void { + czmq.zsock_destroy(&self.ptr_sock_pull); + } + + fn deinit(self: *ControlSocket) void { + self.destroy_push(); + self.destroy_pull(); + } +}; + +// TODO: PLACEHOLDER +const DataSocket = opaque {}; + +// -------------------------------------------------------------------------- +// This implementation is to better understand the control flow of zmq. +// +// The controller is _actually_ a proxy for the upstream client which maybe +// implemented in a different language. +// +// It's purpose is to parse the messages and map an action to a "task" as well +// as return any feedback control messages upstream. +// Briefly the controller: +// 1. handles the control socket state [LIMIT=1, push-pull pair] +// 2. handles the task management [LIMIT=2 tasks] +// above mentioned LIMITs are to be lifted when this framework is +// evaluated, tested and is no longer experimental. +// +// run_stm() Runs the main control state machine. This: +// - checks the control socket for incoming requests +// - updates the controller or managed task states accordingly +// - currently attempts to receive in a loop with a hard-coded sleep +// (lazy pirate) +// - recv's messages in bulk up to a limit of 100 +// +// it is HIGHLY likely that a proxy or router/dealer combo does exactly the +// same thing and should be explored. +// +// --- +// controller-transition: +// C_OK -> task_count == MAX_COUNT +// -> C_BUSY +// +// C_BUSY -> task_count < MAX_COUNT +// -> C_OK +// +// ANY -> error encountered +// -> clean and destroy +// +// --- +// pull-push: +// pull A_CONTROLLER_STATE.c_id +// => push C_STATE.c_id= +// +// pull A_TASK_STATE.c_id.t_id +// => push T_STATE.c_id.t_id= +// +// pull A_TASK_CREATE.c_id.t_id +// => push T_STATE.c_id.t_id= (state=READY) +// +// pull A_TASK_DISCONNECT.c_id.t_id +// => delete task +// +// (ids above are for sanity checks only) +// +// --- +// client-transition (recommendation only): +// +// [1] check if controller is ready : +// loop(push A_CONTROLLER_STATE) +// C_OK => request tasks until max burst count +// (agreed upon limit) +// C_BUSY => wait => goto [1] +// timeout => exit +// C_ERROR => exit +// +// [2] task request: +// task_create, loop(push A_TASK_STATE): +// C_OK && T_READY => goto [3] +// C_BUSY => wait => goto [2] +// timeout => exit +// *_ERROR => exit +// +// [3] send multiframe based on "ACTION spec" (see: Task) +// loop(push A_TASK_STATE) +// T_WORKING => goto [4] +// timeout => exit +// *_ERROR => exit +// +// [4] clean up or kill task +// loop(push A_TASK_STATE): +// T_FINISHED => push A_TASK_DISCONNECT +// timeout => exit +// *_ERROR => exit +// +// FUTUREWORK: use zpoller() instead. +// --------------------------------------------------------------------------- +const Controller = struct { + // allocation + a: *std.mem.Allocator, + // controller id + id_c: usize, + // running task id - auto incremented + id_t_next: u32, + // task states + tasks: std.AutoHashMap(usize, Task), + // controller state + state_c: ControllerState, + // used with TaskState to set the socket state for each task + sock_c: ControlSocket, + + const MAX_TASKS = 2; + const MAX_BURST_CONTROL_MSGS = 100; + + fn init(alloc: *std.mem.Allocator, id_c: usize) Controller { + return Controller{ + .a = alloc, + .id_c = id_c, + .id_t_next = 0, + .tasks = std.AutoHashMap(usize, Task).init(alloc.*), + .state_c = .controller_ok, + .sock_c = ControlSocket.init(), + }; + } + + fn deinit(self: *Controller) void { + // de-initialize tasks + var iter = self.tasks.valueIterator(); + while (iter.next()) |t| t.deinit(); + self.tasks.deinit(); + + // remove connections and bindings socket + self.sock_c.deinit(); + } + + fn run_stm(self: *Controller) !void { + const cycles_ms = 10; // wait before reconnecting when forced to stop + var cycles: u32 = 0; // unused because blocking, only useful when we are doing proper polling + while (true) : (cycles += 1) { + // blocking + var maybe_msg = czmq.zstr_recv(self.sock_c.ptr_sock_pull); + // received message => process + if (maybe_msg) |msg| { + const action = try ClientAction.init_from_str(msg); + switch (action) { + // controller state was requested + .controller_state => { + // container state is being checked => wake up + cycles = 0; + std.debug.print("CLIENT(recv): check controller state.\n", .{}); + // reconnect to socket if not still connected + self.sock_c.connect_push_sock(); + if (self.sock_c.ptr_sock_push) |_| { + // non-blocking + _ = czmq.zstr_send(self.sock_c.ptr_sock_push, self.state_c.to_str()); + } + czmq.zstr_free(&maybe_msg); + }, + // client wants to stop the controller + .controller_end => { + std.debug.print("CLIENT(recv): stop controller.\n", .{}); + self.state_c = .controller_stopped; + if (self.sock_c.ptr_sock_push) |_| { + // non-blocking + _ = czmq.zstr_send(self.sock_c.ptr_sock_push, self.state_c.to_str()); + } + czmq.zstr_free(&maybe_msg); + }, + else => { + return WorkerError.NotImplemented; + }, + } + } + czmq.zclock_sleep(cycles_ms); + } + } + + fn new_task(self: *Controller) !*Task { + if (self.tasks.get(self.id_t_next)) |_| { + return WorkerError.TaskAlreadyExists; + } else { + try self.tasks.put(self.id_t_next, Task.init(self.id_t_next)); + const ptr_t = self.tasks.getPtr(self.id_t_next).?; + self.id_t_next += 1; + return ptr_t; + } + } + + fn remove_task(self: *Controller, id_t: usize) !void { + var t_ptr = self.tasks.getPtr(id_t); + t_ptr.deinit(); + if (!self.tasks.remove(id_t)) { + return WorkerError.TaskDoesNotExist; + } + } +}; + +// -------------------------------------------------------------------------- +// Tasks currently flow from start to finish +// - retries and interrupts are NOT supported. Instead the client can trigger +// a command to end the task and create a new one to retry. IF the task has +// stalled, It's a DUAL responsiblity model: +// - worker to raise an error (or log it). +// - client to raise an error, timeout. +// - but NO imposition is made on fault tolerance (unlike elixir/erlang). +// - If a task is multithreaded, it MAY be killed. Obviously this won't work +// with a single process, since it'll also kill the controller. +// +// For now in-scope: single-threaded processing ONLY. +// +// --- +// task-transition: +// T_READY -> receive action +// -> call function +// -> T_WORKING +// +// T_WORKING -> finished processing +// -> T_FINISHED +// +// ANY -> error encountered +// -> T_ERROR +// +// ANY -> end received +// -> clean and destroy +// +// --- +// push-pull: +// pull: +// A_TASK_CREATE= -- frame 0 +// A_TASK_CREATE== -- frame 1 +// ... -- ... +// A_TASK_CREATE== -- frame N +// +// push: +// R_TASK= +// R_TASK= -- frame 0 +// R_TASK= -- frame 1 +// ... -- ... +// R_TASK= -- frame N +// +// --- +// client-transition (recommendation only): +// (assuming controller has polled finished task) +// task state = T_FINISHED => pull multiframe result +// => parse +// if success, tell controller to end task +// if failure, +// log error +// ask controlelr to end task (inproc) +// OR send retry (NOT SUPPORTED CURRENTLY) +// -------------------------------------------------------------------------- +const Task = struct { + // task id + id_t: usize, + socket: ?*DataSocket, + state: TaskState, + + fn init(id_t: usize) Task { + // create socket + return Task{ + .id_t = id_t, + .socket = null, + .state = TaskState.task_ready, + }; + } + + fn set_state(self: *Task, new_state: TaskState) void { + self.state = new_state; + } + + fn deinit(self: *Task) void { + // TODO: remove task level socket connection and bindings + _ = self; + } +}; + +// This should run on a separate thread +fn __test_worker_controller_stm(a: *std.mem.Allocator) void { + var c = Controller.init(a, 1); + defer c.deinit(); + c.run_stm() catch {}; +} + +pub fn main() !void { + // TODO: replace with actual workflow + var arena: std.heap.ArenaAllocator = .init(std.heap.page_allocator); + defer arena.deinit(); + var a = arena.allocator(); + // --- start worker --- + // sorker will have its pull socket (@), but not push - until the first + // status message is received. + const t_worker = try std.Thread.spawn( + .{ .allocator = a }, + __test_worker_controller_stm, + .{&a}, + ); + t_worker.join(); +} + +test "task state" { + var t = Task.init(10); + std.debug.print("t.state = {s}\n", .{t.state.to_str()}); + t.set_state(.task_working); + std.debug.print("t.state = {s}\n", .{t.state.to_str()}); +} + +test "controller tasks" { + var a = std.testing.allocator; + var c = Controller.init(&a, 0); + defer c.deinit(); + var t1 = try c.new_task(); + t1.set_state(TaskState.task_finished); + var t2 = try c.new_task(); + t2.set_state(TaskState.task_working); + std.debug.print( + "{d}\nt1: {any}\nt2: {any}\n", + .{ c.tasks.count(), t1.*, t2.* }, + ); +} + +test "controller status check" { + + // --- start worker --- + // sorker will have its pull socket (@), but not push - until the first + // status message is received. + const t_worker = try std.Thread.spawn( + .{ .allocator = std.testing.allocator }, + __test_worker_controller_stm, + .{}, + ); + + // --- create sockets to mimic client --- + // 1. push (worker pull) should already be available so connect to it (>) + var sock_client_push: ?*czmq.zsock_t = czmq.zsock_new_push(">tcp://127.0.0.1:5558"); + // 2. bind pull socket (@), this is independent of the worker + var sock_client_pull: ?*czmq.zsock_t = czmq.zsock_new_pull("@tcp://127.0.0.1:5559"); + // imposing wait times so test outputs are clearly seen. + czmq.zclock_sleep(100); + + // --- client checks controller status --- + // (this should prompt the worker to connect a push socket to reply) + _ = czmq.zstr_send(sock_client_push, ClientAction.controller_state.to_str()); + std.debug.print("Client-sent: {s}\n", .{ClientAction.controller_state.to_str()}); + czmq.zclock_sleep(100); + + // --- worker reply --- + // EXPECT: C_OK + { + const maybe_msg = czmq.zstr_recv(sock_client_pull); + if (maybe_msg) |msg| std.debug.print("Client-received: {s}\n", .{msg}); + _ = czmq.free(maybe_msg); + } + czmq.zclock_sleep(100); + + // --- kill worker remotely --- + // EXPECT: C_STOPPED + _ = czmq.zstr_send(sock_client_push, ClientAction.controller_end.to_str()); + std.debug.print("Client-sent: {s}\n", .{ClientAction.controller_end.to_str()}); + czmq.zclock_sleep(100); + + // --- worker will reply first then stop --- + // the worker is essentially in a held state until the state machine + // restarts through some other mechanism, or is cleaned up. + { + const maybe_msg = czmq.zstr_recv(sock_client_pull); + if (maybe_msg) |msg| std.debug.print("Client received: {s}\n", .{msg}); + _ = czmq.free(maybe_msg); + } + czmq.zclock_sleep(100); + + // --- cleanup client --- + // print received status message and reply. + // this will cleanup any sockets. + t_worker.join(); + // destroy sockets + _ = czmq.zsock_destroy(&sock_client_push); + _ = czmq.zsock_destroy(&sock_client_pull); +} 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..b24d301c --- /dev/null +++ b/packages/bundled_models/persistence/notebooks/pipeline_example.ipynb @@ -0,0 +1,1341 @@ +{ + "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": null, + "id": "28f676ce-2134-4eb5-83e6-fb75d93abc57", + "metadata": {}, + "outputs": [], + "source": [ + "import pathlib\n", + "import xarray as xr\n", + "from pathlib import Path\n", + "import pyearthtools.tutorial\n", + "import pyearthtools.pipeline" + ] + }, + { + "cell_type": "markdown", + "id": "c46a2512-9084-4443-a4d7-b3111739667b", + "metadata": {}, + "source": [ + "**Unused libraries**: the following are not required for persistence computations, they were derived from the fourcastnext mini demo." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "33f8877a-5c8a-42a3-946c-d0f3de7199ee", + "metadata": {}, + "outputs": [], + "source": [ + "# 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", + "# > The workaround is to use `TemporalWindow` instead and forcing it to work on 6hour window\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": "markdown", + "id": "3830a81f-f514-47ee-8a3b-99fd7e75efc8", + "metadata": {}, + "source": [ + "**NOTES** _(nikeethr)_:\n", + "- The input data seems to be retrieved by the pipeline in chunks of 4, each chunk is 6 hours\n", + "- Selecting 6 \"indices\" in the reference frame of temporal window, gives me back 24 indices as far as the raw data is concerned (24 * 6 hours that is)\n", + "- I actually _only want 6 indices_, each 6 hours in length, totalling 36 hours.\n", + "- Something may have either changed in the underlying libraries, the upstream data or regressed somehwere in the code.\n", + "- For now this doesn't affect me as the persistence algorithm chops up only what it needs anyway so it is not harmful to select extra data in this particular scenario." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a4c98c28-5d4d-4386-87d3-862aea738e2b", + "metadata": {}, + "outputs": [], + "source": [ + "# Check that accessor works through the pipeline:\n", + "data_pipeline[\"2000-05-05\"][0]" + ] + }, + { + "cell_type": "markdown", + "id": "e7a930ac-b093-47ba-bdc4-0b1f1bfa0fac", + "metadata": {}, + "source": [ + "**NOTES** _(nikeethr)_:\n", + "- The following selector \"2000-05-05T00\" does not work. \n", + "- I had to force it to \"daily\" mode above, by not including the hourly query. Even though the underlying data is 6 hourly.\n", + "- In case you're wondering it _used_ to work in the fourcastnext notebook, so its not that 6 hourly data _cannot_ be morphed to hourly - its likely that something must have regressed.\n", + "- Either that or something on my end is misconfigured, something regressed, or it was not meant to work in the first place." + ] + }, + { + "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",
+       "    2m_temperature           (time, longitude, latitude) float32 197kB dask.array<chunksize=(4, 36, 18), meta=np.ndarray>\n",
+       "    10m_v_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>\n",
+       "    10m_u_component_of_wind  (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", + " 2m_temperature (time, longitude, latitude) float32 197kB dask.array\n", + " 10m_v_component_of_wind (time, longitude, latitude) float32 197kB dask.array\n", + " mean_sea_level_pressure (time, longitude, latitude) float32 197kB dask.array\n", + " 10m_u_component_of_wind (time, longitude, latitude) float32 197kB dask.array" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# ---\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.\n", + "\n", + "\n", + "**NOTE**:\n", + "- The data pipeline may not come pre-concatted, in which case this function _may_ concatenate the incoming stream.\n", + "- This again may be peculiar and specific to the era5 demo dataset above, but is worth mentioning again.\n", + "- Care needs to be taken here that only the necessary data is selected in the pipeline for each base time.\n", + "- The models merely check for input conformance and try to morph if possible (with warning) or error out\n", + "- Preparing the input _ALWAYS_ the responsibility of the pipeline design _NOT_ the models, otherwise the concept of a \"pipeline\" is moot." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e2f2843f-2afb-4570-8f7d-b88854d8d706", + "metadata": {}, + "outputs": [], + "source": [ + "from persistence import registered_model\n", + "\n", + "num_threads=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", + "dt_str = \"2020-01-01\"\n", + "pm = registered_model.PersistenceRM(\n", + " pipeline=data_pipeline,\n", + " dimname_time=\"time\",\n", + " method=\"median_of_three\",\n", + " num_threads=num_threads,\n", + " num_chunks=num_chunks,\n", + " simple_impute=False,\n", + " backend_type=\"zig\",\n", + ")\n", + "ds_output = pm.predict(dt_str, [\"2m_temperature\", \"mean_sea_level_pressure\"])" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "d7c236e9-8a83-49d9-a002-a9d453b1c130", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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jwpCREREghba8Hn55Zfx1ltvYfz48T7L062Gz+HDhwEA119/PVasWIFWrVr5rCBEREREZ9Pr9ejfv79v8/Rkp40bN7LRQ0RE1ESaenCzv5gyZYpTGAxfaHSPz9SpU/H0008jLCzMadI/FU8GGxEREZHARzM3B5pHH30UI0aMQMeOHZGWloagIOcZwlesWOF2no1u+OzcuRMWiwUA8PXXX3s8qCgQVGtBgL3hqTHrLMrtQ/W1yvQf69wL32ETpogHAIMQeiBYry6TnI+62W9z8ddAnTDNv7uhG6Sp/6WwCq5IeYmhLKQwAi5+DMRQHVI9hHTp/pDOHwCU2kKV6THG08p06T7QexA2wt1QJK7uHRXpGgGAOuiBfP2kKBBSvaXvEeDi+yfVT+fm9i5IoTSke0QKDxGkV5/bE3XyTL2tgtRhI6SwGBU2dfiLGuEaSeFX6vOSw1moSN8l6XqL9w08C19yrmq7fC/7XAsd4/OnP/0JGzduxPXXX4/Y2FiftD0a3fDZuHGj4783bdrk9YGJiIiIXFm8eDHef/99jBgxwmd5ejTG595770VFRUWD9MrKStx7771eF4qIiIh+01LH+MTExKBjx44+zdOjhs/ixYtRXV3dIL26uhpvv/2214UiIiKis2g+WALQrFmzMHPmTFRVVfksT7deZy8vL4emadA0DRUVFQgO/u35rM1mw9q1ax2ByIiIiIi88corr+DQoUNITExEu3btGgxu/vrrr93O062GT3R0NHQ6HXQ6HS677LIG63U6HZ566im3C0FEREQuePu4KkB7fM4EHPcltxo+GzduhKZpuOGGG/D+++8jJua3t5ZMJhNSU1PRunVrnxeSiIioRWuhb3XNnDnT53m61fC57rrrANTP4JySkgK93qMhQkRERESNUlpaivfeew+HDh3CtGnTEBMTg6+//hqJiYm45JJL3M7Po+jsqampAICqqioUFRWhrq7OaX2PHj08yZaIiIhUWmiPz+7duzF48GBERUXhhx9+wKRJkxATE4MVK1agqKjIoxeqPGr4nDx5EhMmTMDHH3+sXG+zqSe+IiIiIvd5+0p6oL7OPnXqVIwfPx5z585FRESEI3348OG4++67PcrTo2dVkydPRmlpKbZv346QkBCsW7cOixcvxqWXXorVq1d7VBAiIiKis+3YsQP3339/g/RLLrkExcXFHuXp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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ds_output['2m_temperature'].plot(x='longitude', y='latitude')" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "abc5c111-c71f-4eb2-9f71-3362f1d02b44", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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2WzGvn37bH936tQJer0eeXl5aNeund0rNja2TutERORJOIJF5GXat2+PYcOGYcKECfjXv/6FoKAgzJkzB9dccw2GDRtW53omT56M1NRUtGvXDh07dsTrr7+Oixcv2u4OFGndujV27tyJ+++/H3q9HuHh4ejbty/Onz+PtLQ03H333cjIyMCXX36J4OBg2/umTJmC559/Hu3bt0fHjh2xZMkSFBcX26YHBQVhxowZmDZtGqxWK3r16oWSkhJ88803CA4OxpgxY1zaVkRETYUjWEReaOXKlejRowfuuOMOJCYmQlEUfPHFF7VOCzoye/ZsjBo1Cg8//DASExMRGBiIpKQk+Pr6St+zcOFCnDhxAm3btkVERASASyNq//znP7Fs2TJ069YN3333nd3digAwffp0PPTQQxgzZgwSExMRFBRku7i+xqJFizB//nykpqbiuuuuw6BBg7Bx40bEx8c7sWWIiDyDSmmIizKIyOtYrVZcd911uPfee7Fo0aKmbg4RkVfjKUKiq9TJkyexefNm9OnTB9XV1XjjjTeQm5uL0aNHN3XTiIi8Hk8REl2l1Go1Vq1ahZtvvhm33norDh06hK+++grXXXddUzeNiMjr8RQhERERkZtxBIuIiIjIzdjBIiIiInIzdrCIiIiI3IwdLCIiIiI3YweLiIiIyM3YwSIiIiJyM3awiIiIiNyMHSwiIiIiN/t/vHQ9CemmHZkAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ds_output['mean_sea_level_pressure'].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..98cf957d --- /dev/null +++ b/packages/bundled_models/persistence/pixi.lock @@ -0,0 +1,4662 @@ +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/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py313h07c4f96_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.4.0-pyhcf101f3_1.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/noarch/babel-2.18.0-pyhcf101f3_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/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py313hf159716_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.2.25-pyhd8ed1ab_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/charset-normalizer-3.4.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.1-pyh8f84b5b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.12-py313hd8ed1ab_100.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/linux-64/debugpy-1.8.20-py313h5d5ffb9_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/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + - 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/executing-2.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fqdn-1.5.1-pyhd8ed1ab_1.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/h11-0.16.0-pyhcf101f3_1.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/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-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/idna-3.11-pyhd8ed1ab_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/importlib_resources-6.5.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyha191276_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.11.0-pyhecfbec7_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/ipywidgets-8.1.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.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/noarch/json5-0.13.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.0.0-pyhcf101f3_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-1.1.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_console-6.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.0-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.16-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/noarch/lark-1.3.1-pyhd8ed1ab_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/libsodium-1.0.21-h280c20c_3.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/noarch/mistune-3.2.0-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/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.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/noarch/notebook-7.5.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_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/overrides-7.7.0-pyhd8ed1ab_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/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + - 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/pip-26.0.1-pyh145f28c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.4-pyhcf101f3_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/noarch/prometheus_client-0.24.1-pyhd8ed1ab_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/noarch/prompt_toolkit-3.0.52-hd8ed1ab_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/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-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.12-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.3-pyhd8ed1ab_0.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/pyzmq-27.1.0-py312hda471dd_2.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/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py313h843e2db_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/send2trash-2.1.0-pyha191276_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/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.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/noarch/terminado-0.18.1-pyhc90fa1f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_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-h396c80c_0.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/typing_utils-0.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_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/webcolors-25.10.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.15-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/linux-64/zeromq-4.3.5-h41580af_10.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/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/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/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: ./ + 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/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py313h07c4f96_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.4.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.18.0-pyhcf101f3_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/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.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/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.2.25-pyhd8ed1ab_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/charset-normalizer-3.4.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.12-py313hd8ed1ab_100.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.20-py313h5d5ffb9_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/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-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/fqdn-1.5.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h11-0.16.0-pyhcf101f3_1.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/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-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/idna-3.11-pyhd8ed1ab_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/importlib_resources-6.5.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyha191276_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.11.0-pyhecfbec7_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/ipywidgets-8.1.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.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/noarch/json5-0.13.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.0.0-pyhcf101f3_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-1.1.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_console-6.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.0-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.16-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/noarch/lark-1.3.1-pyhd8ed1ab_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/libedit-3.1.20250104-pl5321h7949ede_0.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/libsodium-1.0.21-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.51.2-hf4e2dac_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_17.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.3-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py313h3dea7bd_1.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/noarch/mistune-3.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ninja-1.13.2-h171cf75_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-7.5.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_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/noarch/overrides-7.7.0-pyhd8ed1ab_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/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.6-pyhcf101f3_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/pip-26.0.1-pyh145f28c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.24.1-pyhd8ed1ab_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/noarch/prompt_toolkit-3.0.52-hd8ed1ab_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/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/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-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.12-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.3-pyhd8ed1ab_0.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/pyzmq-27.1.0-py312hda471dd_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py313h843e2db_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/send2trash-2.1.0-pyha191276_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/noarch/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.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/terminado-0.18.1-pyhc90fa1f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_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/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-h396c80c_0.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/typing_utils-0.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_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/webcolors-25.10.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.15-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/linux-64/zeromq-4.3.5-h41580af_10.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/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/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.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/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/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/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: ./ + 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/_python_abi3_support-1.0-hd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.12.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py313h07c4f96_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/async-lru-2.2.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.4.0-pyhcf101f3_1.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/noarch/babel-2.18.0-pyhcf101f3_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/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py313hf159716_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_8.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.1.4-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.2.25-pyhd8ed1ab_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/charset-normalizer-3.4.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.1-pyh8f84b5b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cloudpickle-3.1.2-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coverage-7.13.4-py313h3dea7bd_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.13.12-py313hd8ed1ab_100.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/linux-64/debugpy-1.8.20-py313h5d5ffb9_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/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + - 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/fqdn-1.5.1-pyhd8ed1ab_1.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/h11-0.16.0-pyhcf101f3_1.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/httpcore-1.0.9-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-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/idna-3.11-pyhd8ed1ab_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/importlib_resources-6.5.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyha191276_1.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/ipywidgets-8.1.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.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/noarch/json5-0.13.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.0.0-pyhcf101f3_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-1.1.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_console-6.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.0-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.16-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/noarch/lark-1.3.1-pyhd8ed1ab_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/libsodium-1.0.21-h280c20c_3.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/noarch/mistune-3.2.0-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/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.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/noarch/notebook-7.5.5-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_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/overrides-7.7.0-pyhd8ed1ab_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/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + - 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/pip-26.0.1-pyh145f28c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.4-pyhcf101f3_0.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/prometheus_client-0.24.1-pyhd8ed1ab_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/noarch/prompt_toolkit-3.0.52-hd8ed1ab_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-fastjsonschema-2.21.2-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.12-h4df99d1_100.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.3-pyhd8ed1ab_0.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/pyzmq-27.1.0-py312hda471dd_2.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/noarch/referencing-0.37.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py313h843e2db_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/send2trash-2.1.0-pyha191276_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/sniffio-1.3.1-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/sortedcontainers-2.4.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.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/noarch/terminado-0.18.1-pyhc90fa1f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tinycss2-1.4.0-pyhd8ed1ab_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-h396c80c_0.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/typing_utils-0.1.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_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/webcolors-25.10.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.15-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/linux-64/zeromq-4.3.5-h41580af_10.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/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/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/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 +- conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda + sha256: a3967b937b9abf0f2a99f3173fa4630293979bd1644709d89580e7c62a544661 + md5: aaa2a381ccc56eac91d63b6c1240312f + depends: + - cpython + - python-gil + license: MIT + license_family: MIT + purls: [] + size: 8191 + timestamp: 1744137672556 +- 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' +- conda: https://conda.anaconda.org/conda-forge/noarch/anyio-4.12.1-pyhcf101f3_0.conda + sha256: eb0c4e2b24f1fbefaf96ce6c992c6bd64340bc3c06add4d7415ab69222b201da + md5: 11a2b8c732d215d977998ccd69a9d5e8 + depends: + - exceptiongroup >=1.0.2 + - idna >=2.8 + - python >=3.10 + - typing_extensions >=4.5 + - python + constrains: + - trio >=0.32.0 + - uvloop >=0.21 + license: MIT + license_family: MIT + purls: + - pkg:pypi/anyio?source=compressed-mapping + size: 145175 + timestamp: 1767719033569 +- conda: https://conda.anaconda.org/conda-forge/noarch/argon2-cffi-25.1.0-pyhd8ed1ab_0.conda + sha256: bea62005badcb98b1ae1796ec5d70ea0fc9539e7d59708ac4e7d41e2f4bb0bad + md5: 8ac12aff0860280ee0cff7fa2cf63f3b + depends: + - argon2-cffi-bindings + - python >=3.9 + - typing-extensions + constrains: + - argon2_cffi ==999 + license: MIT + license_family: MIT + purls: + - pkg:pypi/argon2-cffi?source=hash-mapping + size: 18715 + timestamp: 1749017288144 +- conda: https://conda.anaconda.org/conda-forge/linux-64/argon2-cffi-bindings-25.1.0-py313h07c4f96_2.conda + sha256: ad188ccc06a06c633dc124b09e9e06fb9df4c32ffc38acc96ecc86e506062090 + md5: 27bbec9f2f3a15d32b60ec5734f5b41c + depends: + - __glibc >=2.17,<3.0.a0 + - cffi >=1.0.1 + - libgcc >=14 + - python >=3.13,<3.14.0a0 + - python_abi 3.13.* *_cp313 + license: MIT + license_family: MIT + purls: + - pkg:pypi/argon2-cffi-bindings?source=hash-mapping + size: 35943 + timestamp: 1762509452935 +- conda: https://conda.anaconda.org/conda-forge/noarch/arrow-1.4.0-pyhcf101f3_0.conda + sha256: 792da8131b1b53ff667bd6fc617ea9087b570305ccb9913deb36b8e12b3b5141 + md5: 85c4f19f377424eafc4ed7911b291642 + depends: + - python >=3.10 + - python-dateutil >=2.7.0 + - python-tzdata + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/arrow?source=hash-mapping + size: 113854 + timestamp: 1760831179410 +- 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/noarch/async-lru-2.2.0-pyhcf101f3_0.conda + sha256: d078b0d3fdc13b0ff08485af20928a095c80dff03f7021ee18e8426a773ae948 + md5: 2cdaf7f8bda7eb9ce49c3e08f2f65803 + depends: + - python >=3.10 + - typing_extensions >=4.0.0 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/async-lru?source=hash-mapping + size: 21470 + timestamp: 1771623881915 +- conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.4.0-pyhcf101f3_1.conda + sha256: c13d5e42d187b1d0255f591b7ce91201d4ed8a5370f0d986707a802c20c9d32f + md5: 537296d57ea995666c68c821b00e360b + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/attrs?source=compressed-mapping + size: 64759 + timestamp: 1764875182184 +- 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/noarch/babel-2.18.0-pyhcf101f3_1.conda + sha256: a14a9ad02101aab25570543a59c5193043b73dc311a25650134ed9e6cb691770 + md5: f1976ce927373500cc19d3c0b2c85177 + depends: + - python >=3.10 + - python + constrains: + - pytz >=2015.7 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/babel?source=compressed-mapping + size: 7684321 + timestamp: 1772555330347 +- 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/noarch/beautifulsoup4-4.14.3-pyha770c72_0.conda + sha256: bf1e71c3c0a5b024e44ff928225a0874fc3c3356ec1a0b6fe719108e6d1288f6 + md5: 5267bef8efea4127aacd1f4e1f149b6e + depends: + - python >=3.10 + - soupsieve >=1.2 + - typing-extensions + license: MIT + license_family: MIT + purls: + - pkg:pypi/beautifulsoup4?source=hash-mapping + size: 90399 + timestamp: 1764520638652 +- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-6.3.0-pyhcf101f3_1.conda + sha256: f8ff1f98423674278964a46c93a1766f9e91960d44efd91c6c3ed56a33813f46 + md5: 7c5ebdc286220e8021bf55e6384acd67 + depends: + - python >=3.10 + - webencodings + - python + constrains: + - tinycss2 >=1.1.0,<1.5 + license: Apache-2.0 AND MIT + purls: + - pkg:pypi/bleach?source=compressed-mapping + size: 142008 + timestamp: 1770719370680 +- conda: https://conda.anaconda.org/conda-forge/noarch/bleach-with-css-6.3.0-hbca2aae_1.conda + sha256: 7c07a865e5e4cca233cc4e0eb3f0f5ff6c90776461687b4fb0b1764133e1fd61 + md5: f11a319b9700b203aa14c295858782b6 + depends: + - bleach ==6.3.0 pyhcf101f3_1 + - tinycss2 + license: Apache-2.0 AND MIT + purls: [] + size: 4409 + timestamp: 1770719370682 +- 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 +- conda: https://conda.anaconda.org/conda-forge/noarch/cached-property-1.5.2-hd8ed1ab_1.tar.bz2 + noarch: python + sha256: 561e6660f26c35d137ee150187d89767c988413c978e1b712d53f27ddf70ea17 + md5: 9b347a7ec10940d3f7941ff6c460b551 + depends: + - cached_property >=1.5.2,<1.5.3.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 4134 + timestamp: 1615209571450 +- conda: https://conda.anaconda.org/conda-forge/noarch/cached_property-1.5.2-pyha770c72_1.tar.bz2 + sha256: 6dbf7a5070cc43d90a1e4c2ec0c541c69d8e30a0e25f50ce9f6e4a432e42c5d7 + md5: 576d629e47797577ab0f1b351297ef4a + depends: + - python >=3.6 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/cached-property?source=hash-mapping + size: 11065 + timestamp: 1615209567874 +- conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.2.25-pyhd8ed1ab_0.conda + sha256: a6b118fd1ed6099dc4fc03f9c492b88882a780fadaef4ed4f93dc70757713656 + md5: 765c4d97e877cdbbb88ff33152b86125 + depends: + - python >=3.10 + license: ISC + purls: + - pkg:pypi/certifi?source=compressed-mapping + size: 151445 + timestamp: 1772001170301 +- 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 +- conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.5-pyhd8ed1ab_0.conda + sha256: 05ea76a016c77839b64f9f8ec581775f6c8a259044bd5b45a177e46ab4e7feac + md5: beb628209b2b354b98203066f90b3287 + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/charset-normalizer?source=compressed-mapping + size: 53210 + timestamp: 1772816516728 +- 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/noarch/comm-0.2.3-pyhe01879c_0.conda + sha256: 576a44729314ad9e4e5ebe055fbf48beb8116b60e58f9070278985b2b634f212 + md5: 2da13f2b299d8e1995bafbbe9689a2f7 + depends: + - python >=3.9 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/comm?source=hash-mapping + size: 14690 + timestamp: 1753453984907 +- 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/noarch/cpython-3.13.12-py313hd8ed1ab_100.conda + noarch: generic + sha256: 7636809bda35add7af66cda1fee156136fcba0a1e24bbef1d591ee859df755a8 + md5: 9a4b8a37303b933b847c14a310f0557b + depends: + - python >=3.13,<3.14.0a0 + - python_abi * *_cp313 + license: Python-2.0 + purls: [] + size: 48648 + timestamp: 1770270374831 +- 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 +- conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.20-py313h5d5ffb9_0.conda + sha256: 8d76d4eeb5a8e3c5666880b465593fdf4a44f47fbbe89ff5b8f9abbe43026326 + md5: e94dbbec2589f3b1d821f43a4bf2ab45 + depends: + - python + - libstdcxx >=14 + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.13.* *_cp313 + license: MIT + license_family: MIT + purls: + - pkg:pypi/debugpy?source=hash-mapping + size: 2872698 + timestamp: 1769744980407 +- 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/defusedxml-0.7.1-pyhd8ed1ab_0.tar.bz2 + sha256: 9717a059677553562a8f38ff07f3b9f61727bd614f505658b0a5ecbcf8df89be + md5: 961b3a227b437d82ad7054484cfa71b2 + depends: + - python >=3.6 + license: PSF-2.0 + license_family: PSF + purls: + - pkg:pypi/defusedxml?source=hash-mapping + size: 24062 + timestamp: 1615232388757 +- 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 +- 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/fqdn-1.5.1-pyhd8ed1ab_1.conda + sha256: 2509992ec2fd38ab27c7cdb42cf6cadc566a1cc0d1021a2673475d9fa87c6276 + md5: d3549fd50d450b6d9e7dddff25dd2110 + depends: + - cached-property >=1.3.0 + - python >=3.9,<4 + license: MPL-2.0 + license_family: MOZILLA + purls: + - pkg:pypi/fqdn?source=hash-mapping + size: 16705 + timestamp: 1733327494780 +- 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/h11-0.16.0-pyhcf101f3_1.conda + sha256: 96cac6573fd35ae151f4d6979bab6fbc90cb6b1fb99054ba19eb075da9822fcb + md5: b8993c19b0c32a2f7b66cbb58ca27069 + depends: + - python >=3.10 + - typing_extensions + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/h11?source=compressed-mapping + size: 39069 + timestamp: 1767729720872 +- 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 +- conda: https://conda.anaconda.org/conda-forge/noarch/httpcore-1.0.9-pyh29332c3_0.conda + sha256: 04d49cb3c42714ce533a8553986e1642d0549a05dc5cc48e0d43ff5be6679a5b + md5: 4f14640d58e2cc0aa0819d9d8ba125bb + depends: + - python >=3.9 + - h11 >=0.16 + - h2 >=3,<5 + - sniffio 1.* + - anyio >=4.0,<5.0 + - certifi + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/httpcore?source=hash-mapping + size: 49483 + timestamp: 1745602916758 +- conda: https://conda.anaconda.org/conda-forge/noarch/httpx-0.28.1-pyhd8ed1ab_0.conda + sha256: cd0f1de3697b252df95f98383e9edb1d00386bfdd03fdf607fa42fe5fcb09950 + md5: d6989ead454181f4f9bc987d3dc4e285 + depends: + - anyio + - certifi + - httpcore 1.* + - idna + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/httpx?source=hash-mapping + size: 63082 + timestamp: 1733663449209 +- 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 +- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.11-pyhd8ed1ab_0.conda + sha256: ae89d0299ada2a3162c2614a9d26557a92aa6a77120ce142f8e0109bbf0342b0 + md5: 53abe63df7e10a6ba605dc5f9f961d36 + depends: + - python >=3.10 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/idna?source=hash-mapping + size: 50721 + timestamp: 1760286526795 +- 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/importlib_resources-6.5.2-pyhd8ed1ab_0.conda + sha256: acc1d991837c0afb67c75b77fdc72b4bf022aac71fedd8b9ea45918ac9b08a80 + md5: c85c76dc67d75619a92f51dfbce06992 + depends: + - python >=3.9 + - zipp >=3.1.0 + constrains: + - importlib-resources >=6.5.2,<6.5.3.0a0 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/importlib-resources?source=hash-mapping + size: 33781 + timestamp: 1736252433366 +- 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 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-7.2.0-pyha191276_1.conda + sha256: b77ed58eb235e5ad80e742b03caeed4bbc2a2ef064cb9a2deee3b75dfae91b2a + md5: 8b267f517b81c13594ed68d646fd5dcb + depends: + - __linux + - comm >=0.1.1 + - debugpy >=1.6.5 + - ipython >=7.23.1 + - jupyter_client >=8.8.0 + - jupyter_core >=5.1,!=6.0.* + - matplotlib-inline >=0.1 + - nest-asyncio >=1.4 + - packaging >=22 + - psutil >=5.7 + - python >=3.10 + - pyzmq >=25 + - tornado >=6.4.1 + - traitlets >=5.4.0 + - python + constrains: + - appnope >=0.1.2 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipykernel?source=compressed-mapping + size: 133644 + timestamp: 1770566133040 +- 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 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.11.0-pyhecfbec7_0.conda + sha256: 1f90e346baab7926bc52d7b60c0625087e96b4fab1bdb9a7fe83ac842312c930 + md5: 326c46b8ec2a1b4964927c7ea55ebf49 + depends: + - __unix + - decorator >=5.1.0 + - ipython_pygments_lexers >=1.0.0 + - jedi >=0.18.2 + - matplotlib-inline >=0.1.6 + - prompt-toolkit >=3.0.41,<3.1.0 + - pygments >=2.14.0 + - python >=3.12 + - stack_data >=0.6.0 + - traitlets >=5.13.0 + - pexpect >4.6 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipython?source=compressed-mapping + size: 648197 + timestamp: 1772790149194 +- 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 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipywidgets-8.1.8-pyhd8ed1ab_0.conda + sha256: 6bb58afb7eabc8b4ac0c7e92707fb498313cc0164cf04e7ba1090dbf49af514b + md5: d68e3f70d1f068f1b66d94822fdc644e + depends: + - comm >=0.1.3 + - ipython >=6.1.0 + - jupyterlab_widgets >=3.0.15,<3.1.0 + - python >=3.10 + - traitlets >=4.3.1 + - widgetsnbextension >=4.0.14,<4.1.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipywidgets?source=hash-mapping + size: 114376 + timestamp: 1762040524661 +- conda: https://conda.anaconda.org/conda-forge/noarch/isoduration-20.11.0-pyhd8ed1ab_1.conda + sha256: 08e838d29c134a7684bca0468401d26840f41c92267c4126d7b43a6b533b0aed + md5: 0b0154421989637d424ccf0f104be51a + depends: + - arrow >=0.15.0 + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/isoduration?source=hash-mapping + size: 19832 + timestamp: 1733493720346 +- 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/noarch/json5-0.13.0-pyhd8ed1ab_0.conda + sha256: ba03ca5a6db38d9f48bd30172e8c512dea7a686a5c7701c6fcdb7b3023dae2ad + md5: 8d5f66ebf832c4ce28d5c37a0e76605c + depends: + - python >=3.10 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/json5?source=hash-mapping + size: 34017 + timestamp: 1767325114901 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonpointer-3.0.0-pyhcf101f3_3.conda + sha256: 1a1328476d14dfa8b84dbacb7f7cd7051c175498406dc513ca6c679dc44f3981 + md5: cd2214824e36b0180141d422aba01938 + depends: + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jsonpointer?source=hash-mapping + size: 13967 + timestamp: 1765026384757 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.26.0-pyhcf101f3_0.conda + sha256: db973a37d75db8e19b5f44bbbdaead0c68dde745407f281e2a7fe4db74ec51d7 + md5: ada41c863af263cc4c5fcbaff7c3e4dc + depends: + - attrs >=22.2.0 + - jsonschema-specifications >=2023.3.6 + - python >=3.10 + - referencing >=0.28.4 + - rpds-py >=0.25.0 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/jsonschema?source=compressed-mapping + size: 82356 + timestamp: 1767839954256 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.9.1-pyhcf101f3_0.conda + sha256: 0a4f3b132f0faca10c89fdf3b60e15abb62ded6fa80aebfc007d05965192aa04 + md5: 439cd0f567d697b20a8f45cb70a1005a + depends: + - python >=3.10 + - referencing >=0.31.0 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/jsonschema-specifications?source=hash-mapping + size: 19236 + timestamp: 1757335715225 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-with-format-nongpl-4.26.0-hcf101f3_0.conda + sha256: 6886fc61e4e4edd38fd38729976b134e8bd2143f7fce56cc80d7ac7bac99bce1 + md5: 8368d58342d0825f0843dc6acdd0c483 + depends: + - jsonschema >=4.26.0,<4.26.1.0a0 + - fqdn + - idna + - isoduration + - jsonpointer >1.13 + - rfc3339-validator + - rfc3986-validator >0.1.0 + - rfc3987-syntax >=1.1.0 + - uri-template + - webcolors >=24.6.0 + license: MIT + license_family: MIT + purls: [] + size: 4740 + timestamp: 1767839954258 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-1.1.1-pyhd8ed1ab_1.conda + sha256: b538e15067d05768d1c0532a6d9b0625922a1cce751dd6a2af04f7233a1a70e9 + md5: 9453512288d20847de4356327d0e1282 + depends: + - ipykernel + - ipywidgets + - jupyter_console + - jupyterlab + - nbconvert-core + - notebook + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter?source=hash-mapping + size: 8891 + timestamp: 1733818677113 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter-lsp-2.3.0-pyhcf101f3_0.conda + sha256: 897ad2e2c2335ef3c2826d7805e16002a1fd0d509b4ae0bc66617f0e0ff07bc2 + md5: 62b7c96c6cd77f8173cc5cada6a9acaa + depends: + - importlib-metadata >=4.8.3 + - jupyter_server >=1.1.2 + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-lsp?source=hash-mapping + size: 60377 + timestamp: 1756388269267 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.8.0-pyhcf101f3_0.conda + sha256: e402bd119720862a33229624ec23645916a7d47f30e1711a4af9e005162b84f3 + md5: 8a3d6d0523f66cf004e563a50d9392b3 + depends: + - jupyter_core >=5.1 + - python >=3.10 + - python-dateutil >=2.8.2 + - pyzmq >=25.0 + - tornado >=6.4.1 + - traitlets >=5.3 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-client?source=compressed-mapping + size: 112785 + timestamp: 1767954655912 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_console-6.6.3-pyhd8ed1ab_1.conda + sha256: aee0cdd0cb2b9321d28450aec4e0fd43566efcd79e862d70ce49a68bf0539bcd + md5: 801dbf535ec26508fac6d4b24adfb76e + depends: + - ipykernel >=6.14 + - ipython + - jupyter_client >=7.0.0 + - jupyter_core >=4.12,!=5.0.* + - prompt_toolkit >=3.0.30 + - pygments + - python >=3.9 + - pyzmq >=17 + - traitlets >=5.4 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-console?source=hash-mapping + size: 26874 + timestamp: 1733818130068 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.9.1-pyhc90fa1f_0.conda + sha256: 1d34b80e5bfcd5323f104dbf99a2aafc0e5d823019d626d0dce5d3d356a2a52a + md5: b38fe4e78ee75def7e599843ef4c1ab0 + depends: + - __unix + - python + - platformdirs >=2.5 + - python >=3.10 + - traitlets >=5.3 + - python + constrains: + - pywin32 >=300 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-core?source=hash-mapping + size: 65503 + timestamp: 1760643864586 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_events-0.12.0-pyhe01879c_0.conda + sha256: e9964aaaf6d24a685cd5ce9d75731b643ed7f010fb979574a6580cd2f974c6cd + md5: 31e11c30bbee1682a55627f953c6725a + depends: + - jsonschema-with-format-nongpl >=4.18.0 + - packaging + - python >=3.9 + - python-json-logger >=2.0.4 + - pyyaml >=5.3 + - referencing + - rfc3339-validator + - rfc3986-validator >=0.1.1 + - traitlets >=5.3 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-events?source=hash-mapping + size: 24306 + timestamp: 1770937604863 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server-2.17.0-pyhcf101f3_0.conda + sha256: 74c4e642be97c538dae1895f7052599dfd740d8bd251f727bce6453ce8d6cd9a + md5: d79a87dcfa726bcea8e61275feed6f83 + depends: + - anyio >=3.1.0 + - argon2-cffi >=21.1 + - jinja2 >=3.0.3 + - jupyter_client >=7.4.4 + - jupyter_core >=4.12,!=5.0.* + - jupyter_events >=0.11.0 + - jupyter_server_terminals >=0.4.4 + - nbconvert-core >=6.4.4 + - nbformat >=5.3.0 + - overrides >=5.0 + - packaging >=22.0 + - prometheus_client >=0.9 + - python >=3.10 + - pyzmq >=24 + - send2trash >=1.8.2 + - terminado >=0.8.3 + - tornado >=6.2.0 + - traitlets >=5.6.0 + - websocket-client >=1.7 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-server?source=hash-mapping + size: 347094 + timestamp: 1755870522134 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_server_terminals-0.5.4-pyhcf101f3_0.conda + sha256: 5eda79ed9f53f590031d29346abd183051263227dd9ee667b5ca1133ce297654 + md5: 7b8bace4943e0dc345fc45938826f2b8 + depends: + - python >=3.10 + - terminado >=0.8.3 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-server-terminals?source=hash-mapping + size: 22052 + timestamp: 1768574057200 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab-4.5.6-pyhd8ed1ab_0.conda + sha256: 436a70259a9b4e13ce8b15faa8b37342835954d77a0a74d21dd24547e0871088 + md5: bcbb401d6fa84e0cee34d4926b0e9e93 + depends: + - async-lru >=1.0.0 + - httpx >=0.25.0,<1 + - ipykernel >=6.5.0,!=6.30.0 + - jinja2 >=3.0.3 + - jupyter-lsp >=2.0.0 + - jupyter_core + - jupyter_server >=2.4.0,<3 + - jupyterlab_server >=2.28.0,<3 + - notebook-shim >=0.2 + - packaging + - python >=3.10 + - setuptools >=41.1.0 + - tomli >=1.2.2 + - tornado >=6.2.0 + - traitlets + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyterlab?source=compressed-mapping + size: 8245973 + timestamp: 1773240966438 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_pygments-0.3.0-pyhd8ed1ab_2.conda + sha256: dc24b900742fdaf1e077d9a3458fd865711de80bca95fe3c6d46610c532c6ef0 + md5: fd312693df06da3578383232528c468d + depends: + - pygments >=2.4.1,<3 + - python >=3.9 + constrains: + - jupyterlab >=4.0.8,<5.0.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyterlab-pygments?source=hash-mapping + size: 18711 + timestamp: 1733328194037 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_server-2.28.0-pyhcf101f3_0.conda + sha256: 381d2d6a259a3be5f38a69463e0f6c5dcf1844ae113058007b51c3bef13a7cee + md5: a63877cb23de826b1620d3adfccc4014 + depends: + - babel >=2.10 + - jinja2 >=3.0.3 + - json5 >=0.9.0 + - jsonschema >=4.18 + - jupyter_server >=1.21,<3 + - packaging >=21.3 + - python >=3.10 + - requests >=2.31 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyterlab-server?source=hash-mapping + size: 51621 + timestamp: 1761145478692 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyterlab_widgets-3.0.16-pyhcf101f3_1.conda + sha256: 5c03de243d7ae6247f39a402f4785d95e61c3be79ef18738e8f17155585d31a8 + md5: dbf8b81974504fa51d34e436ca7ef389 + depends: + - python >=3.10 + - python + constrains: + - jupyterlab >=3,<5 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyterlab-widgets?source=hash-mapping + size: 216779 + timestamp: 1762267481404 +- 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/noarch/lark-1.3.1-pyhd8ed1ab_0.conda + sha256: 49570840fb15f5df5d4b4464db8ee43a6d643031a2bc70ef52120a52e3809699 + md5: 9b965c999135d43a3d0f7bd7d024e26a + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/lark?source=compressed-mapping + size: 94312 + timestamp: 1761596921009 +- 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/libsodium-1.0.21-h280c20c_3.conda + sha256: 64e5c80cbce4680a2d25179949739a6def695d72c40ca28f010711764e372d97 + md5: 7af961ef4aa2c1136e11dd43ded245ab + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + license: ISC + purls: [] + size: 277661 + timestamp: 1772479381288 +- 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 +- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py313h3dea7bd_1.conda + sha256: 72ed7c0216541d65a17b171bf2eec4a3b81e9158d8ed48e59e1ecd3ae302d263 + md5: aeb9b9da79fd0258b3db091d1fefcd71 + 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=compressed-mapping + size: 26100 + timestamp: 1772445154165 +- 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/noarch/mistune-3.2.0-pyhcf101f3_0.conda + sha256: d3fb4beb5e0a52b6cc33852c558e077e1bfe44df1159eb98332d69a264b14bae + md5: b11e360fc4de2b0035fc8aaa74f17fd6 + depends: + - python >=3.10 + - typing_extensions + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/mistune?source=hash-mapping + size: 74250 + timestamp: 1766504456031 +- 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/noarch/nbclient-0.10.4-pyhd8ed1ab_0.conda + sha256: 1b66960ee06874ddceeebe375d5f17fb5f393d025a09e15b830ad0c4fffb585b + md5: 00f5b8dafa842e0c27c1cd7296aa4875 + depends: + - jupyter_client >=6.1.12 + - jupyter_core >=4.12,!=5.0.* + - nbformat >=5.1 + - python >=3.8 + - traitlets >=5.4 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/nbclient?source=compressed-mapping + size: 28473 + timestamp: 1766485646962 +- conda: https://conda.anaconda.org/conda-forge/noarch/nbconvert-core-7.17.0-pyhcf101f3_0.conda + sha256: 628fea99108df8e33396bb0b88658ec3d58edf245df224f57c0dce09615cbed2 + md5: b14079a39ae60ac7ad2ec3d9eab075ca + depends: + - beautifulsoup4 + - bleach-with-css !=5.0.0 + - defusedxml + - importlib-metadata >=3.6 + - jinja2 >=3.0 + - jupyter_core >=4.7 + - jupyterlab_pygments + - markupsafe >=2.0 + - mistune >=2.0.3,<4 + - nbclient >=0.5.0 + - nbformat >=5.7 + - packaging + - pandocfilters >=1.4.1 + - pygments >=2.4.1 + - python >=3.10 + - traitlets >=5.1 + - python + constrains: + - pandoc >=2.9.2,<4.0.0 + - nbconvert ==7.17.0 *_0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/nbconvert?source=compressed-mapping + size: 202284 + timestamp: 1769709543555 +- conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + sha256: 7a5bd30a2e7ddd7b85031a5e2e14f290898098dc85bea5b3a5bf147c25122838 + md5: bbe1963f1e47f594070ffe87cdf612ea + depends: + - jsonschema >=2.6 + - jupyter_core >=4.12,!=5.0.* + - python >=3.9 + - python-fastjsonschema >=2.15 + - traitlets >=5.1 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/nbformat?source=hash-mapping + size: 100945 + timestamp: 1733402844974 +- 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/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + sha256: bb7b21d7fd0445ddc0631f64e66d91a179de4ba920b8381f29b9d006a42788c0 + md5: 598fd7d4d0de2455fb74f56063969a97 + depends: + - python >=3.9 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/nest-asyncio?source=hash-mapping + size: 11543 + timestamp: 1733325673691 +- 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/noarch/notebook-7.5.5-pyhcf101f3_0.conda + sha256: 11cfeabc41ed73bb088315d96cfdfeaaa10470c06ce6332bae368590e3047ef6 + md5: 471096452091ae8c460928ad5ff143cc + depends: + - importlib_resources >=5.0 + - jupyter_server >=2.4.0,<3 + - jupyterlab >=4.5.6,<4.6 + - jupyterlab_server >=2.28.0,<3 + - notebook-shim >=0.2,<0.3 + - python >=3.10 + - tornado >=6.2.0 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/notebook?source=compressed-mapping + size: 10113914 + timestamp: 1773250273088 +- conda: https://conda.anaconda.org/conda-forge/noarch/notebook-shim-0.2.4-pyhd8ed1ab_1.conda + sha256: 7b920e46b9f7a2d2aa6434222e5c8d739021dbc5cc75f32d124a8191d86f9056 + md5: e7f89ea5f7ea9401642758ff50a2d9c1 + depends: + - jupyter_server >=1.8,<3 + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/notebook-shim?source=hash-mapping + size: 16817 + timestamp: 1733408419340 +- 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/overrides-7.7.0-pyhd8ed1ab_1.conda + sha256: 1840bd90d25d4930d60f57b4f38d4e0ae3f5b8db2819638709c36098c6ba770c + md5: e51f1e4089cad105b6cac64bd8166587 + depends: + - python >=3.9 + - typing_utils + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/overrides?source=hash-mapping + size: 30139 + timestamp: 1734587755455 +- 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 +- conda: https://conda.anaconda.org/conda-forge/noarch/pandocfilters-1.5.0-pyhd8ed1ab_0.tar.bz2 + sha256: 2bb9ba9857f4774b85900c2562f7e711d08dd48e2add9bee4e1612fbee27e16f + md5: 457c2c8c08e54905d6954e79cb5b5db9 + depends: + - python !=3.0,!=3.1,!=3.2,!=3.3 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pandocfilters?source=hash-mapping + size: 11627 + timestamp: 1631603397334 +- 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=hash-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 +- 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/pip-26.0.1-pyh145f28c_0.conda + sha256: 5f66ea31d62188c266c5a8752119b0cc90a5bf05963f665cf48a33e0ec58d39c + md5: 09a970fbf75e8ed1aa633827ded6aa4f + depends: + - python >=3.13.0a0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pip?source=compressed-mapping + size: 1180743 + timestamp: 1770270312477 +- conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.9.4-pyhcf101f3_0.conda + sha256: 0289f0a38337ee201d984f8f31f11f6ef076cfbbfd0ab9181d12d9d1d099bf46 + md5: 82c1787f2a65c0155ef9652466ee98d6 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/platformdirs?source=compressed-mapping + size: 25646 + timestamp: 1773199142345 +- 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 +- conda: https://conda.anaconda.org/conda-forge/noarch/prometheus_client-0.24.1-pyhd8ed1ab_0.conda + sha256: 75b2589159d04b3fb92db16d9970b396b9124652c784ab05b66f584edc97f283 + md5: 7526d20621b53440b0aae45d4797847e + depends: + - python >=3.10 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/prometheus-client?source=compressed-mapping + size: 56634 + timestamp: 1768476602855 +- 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/noarch/prompt_toolkit-3.0.52-hd8ed1ab_0.conda + sha256: e79922a360d7e620df978417dd033e66226e809961c3e659a193f978a75a9b0b + md5: 6d034d3a6093adbba7b24cb69c8c621e + depends: + - prompt-toolkit >=3.0.52,<3.0.53.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 7212 + timestamp: 1756321849562 +- 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 +- 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 +- 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: 3598f230d1a364c119a74046108b80de45117bf97df32ef631111a22b36324c2 + 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' +- 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-fastjsonschema-2.21.2-pyhe01879c_0.conda + sha256: df9aa74e9e28e8d1309274648aac08ec447a92512c33f61a8de0afa9ce32ebe8 + md5: 23029aae904a2ba587daba708208012f + depends: + - python >=3.9 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/fastjsonschema?source=hash-mapping + size: 244628 + timestamp: 1755304154927 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-gil-3.13.12-h4df99d1_100.conda + sha256: f306304235197434494355351ac56020a65b7c5c56ff10ca1ed53356d575557a + md5: 3d92938d5b83c49162ade038aab58a59 + depends: + - cpython 3.13.12.* + - python_abi * *_cp313 + license: Python-2.0 + purls: [] + size: 48618 + timestamp: 1770270436560 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-json-logger-2.0.7-pyhd8ed1ab_0.conda + sha256: 4790787fe1f4e8da616edca4acf6a4f8ed4e7c6967aa31b920208fc8f95efcca + md5: a61bf9ec79426938ff785eb69dbb1960 + depends: + - python >=3.6 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/python-json-logger?source=hash-mapping + size: 13383 + timestamp: 1677079727691 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.3-pyhd8ed1ab_0.conda + sha256: 467134ef39f0af2dbb57d78cb3e4821f01003488d331a8dd7119334f4f47bfbd + md5: 7ead57407430ba33f681738905278d03 + depends: + - python >=3.10 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/tzdata?source=hash-mapping + size: 143542 + timestamp: 1765719982349 +- 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 +- 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/pyzmq-27.1.0-py312hda471dd_2.conda + noarch: python + sha256: be66c1f85c3b48137200d62c12d918f4f8ad329423daef04fed292818efd3c28 + md5: 082985717303dab433c976986c674b35 + depends: + - python + - libgcc >=14 + - libstdcxx >=14 + - __glibc >=2.17,<3.0.a0 + - zeromq >=4.3.5,<4.4.0a0 + - _python_abi3_support 1.* + - cpython >=3.12 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pyzmq?source=compressed-mapping + size: 211567 + timestamp: 1771716961404 +- 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 +- conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.37.0-pyhcf101f3_0.conda + sha256: 0577eedfb347ff94d0f2fa6c052c502989b028216996b45c7f21236f25864414 + md5: 870293df500ca7e18bedefa5838a22ab + depends: + - attrs >=22.2.0 + - python >=3.10 + - rpds-py >=0.7.0 + - typing_extensions >=4.4.0 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/referencing?source=hash-mapping + size: 51788 + timestamp: 1760379115194 +- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.5-pyhcf101f3_1.conda + sha256: 7813c38b79ae549504b2c57b3f33394cea4f2ad083f0994d2045c2e24cb538c5 + md5: c65df89a0b2e321045a9e01d1337b182 + depends: + - python >=3.10 + - certifi >=2017.4.17 + - charset-normalizer >=2,<4 + - idna >=2.5,<4 + - urllib3 >=1.21.1,<3 + - python + constrains: + - chardet >=3.0.2,<6 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/requests?source=compressed-mapping + size: 63602 + timestamp: 1766926974520 +- conda: https://conda.anaconda.org/conda-forge/noarch/rfc3339-validator-0.1.4-pyhd8ed1ab_1.conda + sha256: 2e4372f600490a6e0b3bac60717278448e323cab1c0fecd5f43f7c56535a99c5 + md5: 36de09a8d3e5d5e6f4ee63af49e59706 + depends: + - python >=3.9 + - six + license: MIT + license_family: MIT + purls: + - pkg:pypi/rfc3339-validator?source=hash-mapping + size: 10209 + timestamp: 1733600040800 +- conda: https://conda.anaconda.org/conda-forge/noarch/rfc3986-validator-0.1.1-pyh9f0ad1d_0.tar.bz2 + sha256: 2a5b495a1de0f60f24d8a74578ebc23b24aa53279b1ad583755f223097c41c37 + md5: 912a71cc01012ee38e6b90ddd561e36f + depends: + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/rfc3986-validator?source=hash-mapping + size: 7818 + timestamp: 1598024297745 +- conda: https://conda.anaconda.org/conda-forge/noarch/rfc3987-syntax-1.1.0-pyhe01879c_1.conda + sha256: 70001ac24ee62058557783d9c5a7bbcfd97bd4911ef5440e3f7a576f9e43bc92 + md5: 7234f99325263a5af6d4cd195035e8f2 + depends: + - python >=3.9 + - lark >=1.2.2 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/rfc3987-syntax?source=hash-mapping + size: 22913 + timestamp: 1752876729969 +- conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.30.0-py313h843e2db_0.conda + sha256: 076d26e51c62c8ecfca6eb19e3c1febdd7632df1990a7aa53da5df5e54482b1c + md5: 779e3307a0299518713765b83a36f4b1 + depends: + - python + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - python_abi 3.13.* *_cp313 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + purls: + - pkg:pypi/rpds-py?source=hash-mapping + size: 383230 + timestamp: 1764543223529 +- 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/send2trash-2.1.0-pyha191276_1.conda + sha256: 59656f6b2db07229351dfb3a859c35e57cc8e8bcbc86d4e501bff881a6f771f1 + md5: 28eb91468df04f655a57bcfbb35fc5c5 + depends: + - __linux + - python >=3.10 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/send2trash?source=hash-mapping + size: 24108 + timestamp: 1770937597662 +- 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/sniffio-1.3.1-pyhd8ed1ab_2.conda + sha256: dce518f45e24cd03f401cb0616917773159a210c19d601c5f2d4e0e5879d30ad + md5: 03fe290994c5e4ec17293cfb6bdce520 + depends: + - python >=3.10 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/sniffio?source=hash-mapping + size: 15698 + timestamp: 1762941572482 +- 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 +- conda: https://conda.anaconda.org/conda-forge/noarch/soupsieve-2.8.3-pyhd8ed1ab_0.conda + sha256: 23b71ecf089967d2900126920e7f9ff18cdcef82dbff3e2f54ffa360243a17ac + md5: 18de09b20462742fe093ba39185d9bac + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/soupsieve?source=hash-mapping + size: 38187 + timestamp: 1769034509657 +- 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 +- conda: https://conda.anaconda.org/conda-forge/noarch/terminado-0.18.1-pyhc90fa1f_1.conda + sha256: 6b6727a13d1ca6a23de5e6686500d0669081a117736a87c8abf444d60c1e40eb + md5: 17b43cee5cc84969529d5d0b0309b2cb + depends: + - __unix + - ptyprocess + - python >=3.10 + - tornado >=6.1.0 + - python + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/terminado?source=hash-mapping + size: 24749 + timestamp: 1766513766867 +- 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/noarch/tinycss2-1.4.0-pyhd8ed1ab_0.conda + sha256: cad582d6f978276522f84bd209a5ddac824742fe2d452af6acf900f8650a73a2 + md5: f1acf5fdefa8300de697982bcb1761c9 + depends: + - python >=3.5 + - webencodings >=0.4 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/tinycss2?source=hash-mapping + size: 28285 + timestamp: 1729802975370 +- 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' +- 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-h396c80c_0.conda + sha256: 7c2df5721c742c2a47b2c8f960e718c930031663ac1174da67c1ed5999f7938c + md5: edd329d7d3a4ab45dcf905899a7a6115 + depends: + - typing_extensions ==4.15.0 pyhcf101f3_0 + license: PSF-2.0 + license_family: PSF + purls: [] + size: 91383 + timestamp: 1756220668932 +- 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/typing_utils-0.1.0-pyhd8ed1ab_1.conda + sha256: 3088d5d873411a56bf988eee774559335749aed6f6c28e07bf933256afb9eb6c + md5: f6d7aa696c67756a650e91e15e88223c + depends: + - python >=3.9 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/typing-utils?source=hash-mapping + size: 15183 + timestamp: 1733331395943 +- 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 +- conda: https://conda.anaconda.org/conda-forge/noarch/uri-template-1.3.0-pyhd8ed1ab_1.conda + sha256: e0eb6c8daf892b3056f08416a96d68b0a358b7c46b99c8a50481b22631a4dfc0 + md5: e7cb0f5745e4c5035a460248334af7eb + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/uri-template?source=hash-mapping + size: 23990 + timestamp: 1733323714454 +- 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 +- 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=hash-mapping + size: 71550 + timestamp: 1770634638503 +- conda: https://conda.anaconda.org/conda-forge/noarch/webcolors-25.10.0-pyhd8ed1ab_0.conda + sha256: 21f6c8a20fe050d09bfda3fb0a9c3493936ce7d6e1b3b5f8b01319ee46d6c6f6 + md5: 6639b6b0d8b5a284f027a2003669aa65 + depends: + - python >=3.10 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/webcolors?source=hash-mapping + size: 18987 + timestamp: 1761899393153 +- conda: https://conda.anaconda.org/conda-forge/noarch/webencodings-0.5.1-pyhd8ed1ab_3.conda + sha256: 19ff205e138bb056a46f9e3839935a2e60bd1cf01c8241a5e172a422fed4f9c6 + md5: 2841eb5bfc75ce15e9a0054b98dcd64d + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/webencodings?source=hash-mapping + size: 15496 + timestamp: 1733236131358 +- conda: https://conda.anaconda.org/conda-forge/noarch/websocket-client-1.9.0-pyhd8ed1ab_0.conda + sha256: 42a2b61e393e61cdf75ced1f5f324a64af25f347d16c60b14117393a98656397 + md5: 2f1ed718fcd829c184a6d4f0f2e07409 + depends: + - python >=3.10 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/websocket-client?source=hash-mapping + size: 61391 + timestamp: 1759928175142 +- conda: https://conda.anaconda.org/conda-forge/noarch/widgetsnbextension-4.0.15-pyhd8ed1ab_0.conda + sha256: 826af5e2c09e5e45361fa19168f46ff524e7a766022615678c3a670c45895d9a + md5: dc257b7e7cad9b79c1dfba194e92297b + depends: + - python >=3.10 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/widgetsnbextension?source=hash-mapping + size: 889195 + timestamp: 1762040404362 +- 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/linux-64/zeromq-4.3.5-h41580af_10.conda + sha256: 325d370b28e2b9cc1f765c5b4cdb394c91a5d958fbd15da1a14607a28fee09f6 + md5: 755b096086851e1193f3b10347415d7c + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - libstdcxx >=14 + - krb5 >=1.22.2,<1.23.0a0 + - libsodium >=1.0.21,<1.0.22.0a0 + license: MPL-2.0 + license_family: MOZILLA + purls: [] + size: 311150 + timestamp: 1772476812121 +- 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..292eab85 --- /dev/null +++ b/packages/bundled_models/persistence/src/persistence/interface/_compute.py @@ -0,0 +1,335 @@ +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 + with concurrent.futures.ThreadPoolExecutor( + max_workers=num_workers, + ) as th_exec: + results = th_exec.map( + PersistenceComputePool._job_wrapper, iter(self.chunk_generator) + ) + for res_chunk in iter(results): + arr_res[*res_chunk.slice_dims] = res_chunk.array + + # --- process pool implementation --- + # This is commented out for now due to issues with portability. + # - may not work on windows/mac + # - requires main guard + # - python threading is far more compatible with arbitrary devices + # - only really required for something like HPC, where python threading may not be + # optimized for compute cores. + # --- + # 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..2566181f --- /dev/null +++ b/packages/bundled_models/persistence/src/persistence/persistence_impl.py @@ -0,0 +1,229 @@ +""" +Runs persistence model on the data loaded from the pipeline. Chunks the input data from the pipeline. + +TODO: use threads instead of processes. + +ANTIPATTERNS (for developers): + + - do not chunk over time, on-the-fly (except for specific exceptions). Data is expected to + arrive already pre-bounded to a specific dimensional extent. Any chunking here is done on top + of that + + - do not assume the use of threads will "just work (TM)" if you have to, force threads to 1 if + its an issue. + + - do not share data between threads when it is not required. If you have to share data you MUST + include barriers appropriately to prevent race conditions and deadlocks. + + - do not assume this will be called as a library (but can be if the OS allows it and its been + tested sufficiently). + +FUTUREWORK: + + - Add the ability to bypass python completely for data loading. (see examples for a zmq example) + + - 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..0523e174 --- /dev/null +++ b/packages/bundled_models/persistence/src/persistence/registered_model.py @@ -0,0 +1,346 @@ +""" +Register persistence model in zoo + +Here a PhantomPredictor is used as a proxy for Predictor. A persistence model is a _light_ +predictor. A persistence model is a _light_ model. + +_Light_ here implies that the model only needs the reference to the data pipeline and some +"Metadata" about the persistence model. The predictor only needs to run the "predict" function under +the assumption that the datapipeline ingested as part of the model MUST be invertible or an +equivilent consistent inversion be provided to all predictors running aside the prediction pipeline +(if any). +""" + +import copy +import dataclasses +import datetime +import functools +import typing +import warnings + +import xarray as xr + +import pyearthtools.training as pet_train +import pyearthtools.pipeline as pet_pipe +import pyearthtools.zoo + +from persistence import persistence_impl + + +Prediction = typing.Any + +WARN_PIPELINE_NO_MULTIPLE_DATAINPUT = ( + "PersistencePredictor.predict: " + "Retrieved data has more than one element, make sure to concatenate it if this is " + "unintentional. Concatenation is going to performed automatically over the specified time " + "dimension, this may slow things down." +) +ERROR_PIPELINE_INVALID_TYPE_DATAINPUT = ( + "PersistencePredictor.predict: " + "Pipeline did NOT retrieve a xr.Dataset. This is a critical failure." +) +ERROR_PREDICT_INVALID_TYPE_VARNAMES = ( + "PersistencePredictor.predict: " + "in `predict` Variables (`varnames`) MUST be a list of strings." +) + + +class PhantomPredictor: + def predict(*args, **kwargs) -> Prediction: + """ + INTERNAL ONLY - this is aimed at developers not users + + Read the comments on `PhantomPredictor.register(...)` below first. + + This is NOT a abstract method, and it SHOULD NOT _ever_ be a abstract method. A phantom + predictor is a imaginary concept. It is simply being used as a gateway/proxy for + initializing a predictor without arbitrary impositions. To put it precisely: + + A Predictor is a category of THINGS that uses THINGS to _predict_ THINGS + + The THINGS it _predicts_ is called a Prediction. + + With no bounds in what they can actually encompass. + + ``` + >>> p = PersistencePredictor(PhantomPredictor) + >>> assert isinstance(p, Predictor) + ... # Note that if we get past this initialization was successful + >>> arr_in = np.array(...) + >>> arr_out: Prediction = p.predict(arr_in) + ``` + """ + + # --- + # NOTES: + # Don't let the word THINGS throw you away, it is an abstraction. Symbolically this is what is + # happening. + # + # * :: (* -> *) + # + # Contrived as it may seem every part of this docstring above is REQUIRED information for + # consideration because this is the impersonated ROOT of any Predictor. IT can't know ahead + # just like archetype Car can't know ahead if it will always be required to have an engine. + # But it can know about what it conceptually SHOULD represent at all times and that is + # + # 1. A Predictor is a named collection of things called `Predictor`, + # 2. containing maps that `predict` + # 3. and produce a namespaced output called `Prediction`. + # --- + raise NotImplementedError("A predictor MUST know how to predict things.") + + +class PhantomModel: + def get_model(*args, **kwargs) -> "PhantomModel": + """ + INTERNAL ONLY - this is aimed at developers not users + + Similar to predictor this is not a abstract method. But it is mandatory. A model can exist, + but until it returns a tangible version of itself it is a ghost. Without a concrete + definition of get_model that is ingestable by some other entity - its existence (or promised + existence) is meaningless. + """ + # --- + # NOTES: + # does not need to be shared => there is a guardrail that is YET to be determined + # usually by e.g. a predictor + # + # it must be shareable => some entity needs to refer to the tangential model (data, e.g. + # wiehgts) not the conceptual model (the contract or the class Model), for it to properly + # exist. + # + # these are again just _contracts_ + # --- + raise NotImplementedError( + "A model is a RESOURCE, a resource MUST be shareable, but it DOES NOT NEED to be shared." + ) + + +# --- +# Phantom classes: Do not contain data. They exist as an abstraction layer to remove class +# "impositions" such as abstractmethods. This is a flaw of object oriented design as opposed to type +# oriented design. +# +# Object-Oriented: A Car has a engine (pre EV) <--- defines somethings by its functionality +# Implement Car for ElectricCar <--- entire framework falls apart, because functionality has changed +# +# Type-Oriented: A Car is a Car <-- defines something as literally what it is, universal, never fails +# +# ``` +# >>> class MyPredictor(PhantomPredictor): pass +# >>> x = MyPredictor() # <--- works +# >>> isinstance(x, Predictor) +# ... True +# >>> class MyPredictorOrig(Predictor): pass +# >>> y = MyPredictorOrig() # <--- does not work +# ... Exception... +# ``` +# The above IS a hack, but a necessary evil barring a big refactor. +# --- +pet_train.Predictor.register(PhantomPredictor) +pet_train.ModelWrapper.register(PhantomModel) + + +@dataclasses.dataclass +class PersistencePredictor(PhantomPredictor): + """ + The persistence predictor is a temporal predictor. It uses a persistence model with a method + that the user provides to perform a prediction. Since the model computes this at runtime, it too + is a predictor. To see this consider that the persistence model will (most of the time) NOT fit + the mould of: train -> store -> retrieve weights. + + The concept of "time" is a bound criteria. In particular, Persistence is a causal predictor + and therefore can only predict use historical data strictly before a reference time and only + produce future reuslts strictly greater than or equal to a reference time. With the affordance + that the reference time is usually the first future timestep. + + Core assumption: + - All variables that will be interrogated MUST have a concept of "time" + - That concept of "time" must be CONSISTENT (e.g. shape, format etc.) + - There are no guarentees or checks done for this, because it is impossible to do so without + considering every edge case. this is a user requirement in sanitizing the data to conform. + - and its slow to do so... + + The input requirements or THINGS required to perform a prediction + 1. the pipeline - which is user defined, common to both the persistence model and dictates how + the loading and preprocessing happens, which usually is, but not always expected to be the + same as the pipeline of whatever model is running in parallel. + 2. the reference time. + 3. the model: is actually a true phantom data. A persistence method is not "modelled" in anyway + it has no "brains" or thought process behind it past heuristic and a null hypothesis + formation. Therefore the model IS the predictor, and the resource associated to the model IS + the cached set of metadata required to do the prediction + + Since this is usually the entry point, the class initalizer should ingest standard arguments + instead of internal structures. + + Args: + + pipeline: The data pipeline + + method: The persistence method to be computed; defaults to MEDIAN_OF_THREE (as enum + or "median_of_three" as string). For speedier compute, you should use + MOST_RECENT. There are fancier algorithms planned that may be slower but + worth it for the accuracy gain (within reason). + + dimname_time: The name of the time dimension. + + num_threads: The number of threads to allocate (usually set this to 1, but in some + systems it may help increasing this to make I/O faster. + + num_chunks: Chunking is useful to have even without multiprocessing or multithreading + because it can provide guarentees on how much data is being accessed if its + done consistently. + + simple_impute: Whether to impute the data. Usually simple and fast enough that its trivial + to do, but sometimes the backend algorithm may already be doing it in which + case its worth it to set it to false. + + backend_type: which backend to use to do the actual work (usually "numpy" but "zig" is + also supported as an experimental option). + """ + + _: dataclasses.KW_ONLY + + pipeline: pet_pipe.Pipeline + method: str + dimname_time: str + num_threads: int = 1 + num_chunks: int = 1 + simple_impute: bool = True + backend_type: str = "numpy" + + # NOTE: not caching this yet, since there's no guarentee that this class is frozen. + def indexofdim_time(self, ds_input) -> int: + return list(ds_input.dims).index(self.dimname_time) + + def predict( + self, + dt_base: datetime.datetime | str, + var_names: list[str], + ) -> Prediction: + """ + A predictor can only be used if it has a predict() method - this is part of the contract + + Args + dt_base: the base time to use for prediction + var_names: the variable names/keys to predict + """ + # ---------------------------------- + # --- FULL external-backend flow --- + # ---------------------------------- + # TODO: implement branching here to use the listener method with zmq+zig to do out of + # process computation of the methods using custom loaders + # --- + # ...ZMQ magic here if requested + # --- + + # ------------------------------------ + # --- python-native flow (default) --- + # ------------------------------------ + # (with optional backend flow) + # get data instance from pipeline (assume that entries are pre-concatted) + ds = self.pipeline[dt_base] + + # --- perform checks --- + # ds should ALWAYS be a dataset if we are using the pipeline + if not isinstance(ds, xr.Dataset): + if isinstance(ds, list) or isinstance(ds, tuple): + if len(ds) == 1: + ds = ds[0] + else: + warnings.warn(WARN_PIPELINE_NO_MULTIPLE_DATAINPUT) + ds = xr.concat(ds, dim=self.dimname_time) + else: + raise TypeError(ERROR_PIPELINE_INVALID_TYPE_DATAINPUT) + + # make allowance for a single string and lift to list + if isinstance(var_names, str): + var_names = [var_names] + + if not isinstance(var_names, list) or any( + map(lambda v: not isinstance(v, str), var_names) + ): + raise TypeError(ERROR_PREDICT_INVALID_TYPE_VARNAMES) + + # --- extract variables --- + # select variables of interest + # TODO: not sure if this the best way to select a view + ds_input = ds[var_names] + + # --- cache coordinates to reintroduce post compute --- + coords_out = copy.deepcopy(ds_input.coords) + # persistence model only returns a single time index + del coords_out[self.dimname_time] + + # --- run prediction --- + # TODO num_workers -> num_threads + ds_prediction = persistence_impl.predict( + ds_input, + idx_time_dim=self.indexofdim_time(ds_input), + num_workers=self.num_threads, + num_chunks=self.num_chunks, + method=self.method, + simple_impute=self.simple_impute, + backend_type=self.backend_type, + ) + + # --- re-assign stripped coordinates --- + ds_prediction = ds_prediction.assign_coords(coords_out) + + return ds_prediction + + +# --- +# IMPORTANT: models DO NOT need to be defined like this. But where they fit the mould exactly, they +# SHUOLD be defined like this, i.e. a static model with prediction capability ingrained. +# +# Usually this dependency would be reversed, because a model is pre-defined, in persistence the +# model does not exist until the user provides data and the method to compute it. +# +# Therefore, as far as persistence models are concerned you can just directly use the +# PersistencePredictor if you are using this as a library. +# +# TLDR; +# 1. this is just yet another namespacing issue from legacy code; +# 2. Persistence is a special case where models are predictors. +# +# RM => RegisteredModel as per usual +# --- +@pyearthtools.zoo.register("Development/Persistence", exists="ignore") +@dataclasses.dataclass +class PersistenceRM(PhantomModel, PersistencePredictor): + """ + This is the main entry point to the registered model that computes persistence. There is a + caveat here. Even though this is the main entry point, this is mainly for consistency. It is + completely interchangeable with using the PersistencePredictor directly which has a richer + documentation. + + See: `PersistencePredictor` for a more detailed information on the arguments. + + This is just a compatiblity layer to adhere to registered models. + """ + + _name = "Development/Persistence" + + # TODO: if there are docstrings issue, the user should just be referred to PersistencePredictor + def get_model(self) -> "PhantomModel": + """ + NOTE: usually this returns "data" that a external predictor can use, but a persistence + model's representation of data is itself. So contrived as it looks, this is an accurate + definition. But as specified in PhantomModel this part of the contract while, MUST be + defined, _DOES NOT NEED_ to be used. + + This part of the code was written _after_ the PhantomModel, so if you are wondering why that + part of the contract exists, this is why. + """ + # TODO: ensure this is a "view" not a copy, it most likely is a view. + return self + + +if __name__ == "__main__": + predictor = PhantomPredictor() + model = PhantomModel() + # Quick test of impersonation - this should never fail + assert isinstance(predictor, pet_train.PhantomPredictor) + assert isinstance(model, pet_train.PhantomModel) 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)