diff --git a/.gitignore b/.gitignore index 44bf0dc..1da24dd 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ _book/ .venv/ eopf_101.egg-info/ index.tex +__pycache__ \ No newline at end of file diff --git a/41_rio_tiler_s2_fundamentals.ipynb b/41_rio_tiler_s2_fundamentals.ipynb new file mode 100644 index 0000000..4bf4255 --- /dev/null +++ b/41_rio_tiler_s2_fundamentals.ipynb @@ -0,0 +1,653 @@ +{ + "cells": [ + { + "cell_type": "raw", + "id": "0", + "metadata": {}, + "source": [ + "---\n", + "title: \"EOPF Zarr + Rio-tiler Fundamentals with Sentinel-2\"\n", + "execute:\n", + " enabled: true\n", + " keep-ipynb: false\n", + " freeze: auto\n", + "format: html\n", + "---" + ] + }, + { + "cell_type": "markdown", + "id": "1", + "metadata": {}, + "source": [ + "\n", + " \n", + "" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## Introduction\n", + "\n", + "This notebook demonstrates efficient tiling workflows with EOPF Zarr data using **rio-tiler** and **rio-xarray**. We'll showcase how direct Zarr access with proper chunking delivers superior performance for web mapping and visualization tasks.\n", + "\n", + "**Rio-tiler** is a powerful Python library designed for creating map tiles from raster data sources. Combined with EOPF Zarr's cloud-optimized format, it enables efficient tile generation for web mapping applications without downloading entire datasets." + ] + }, + { + "cell_type": "markdown", + "id": "3", + "metadata": {}, + "source": [ + "## What we will learn\n", + "\n", + "- πŸ—ΊοΈ How to integrate rio-tiler with EOPF Zarr datasets\n", + "- 🎨 Generate map tiles (RGB and false color composites) from Sentinel-2 data\n", + "- πŸ“Š Understand the relationship between Zarr chunks and tile performance\n", + "- ⚑ Observe memory usage patterns for large optical datasets\n", + "- 🌍 Create interactive web map visualizations" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "This tutorial builds on concepts from previous sections:\n", + "- [Understanding Zarr Structure](24_zarr_struct_S2L2A.ipynb) - Sentinel-2 data organization\n", + "- [STAC and xarray Tutorial](44_eopf_stac_xarray_tutorial.ipynb) - Accessing EOPF data\n", + "- [Zarr Chunking Strategies](sections/2x_about_eopf_zarr/253_zarr_chunking_practical.ipynb) - Chunking fundamentals\n", + "\n", + "As rio-tiler is extensively used in this notebook, familiarity with its core concepts is beneficial. Refer to the [rio-tiler documentation](https://docs.rio-tiler.io/en/latest/) for more details.\n", + "\n", + "![rio-tiler with EOPF Zarr](img/rio-tiler.png)\n", + "\n", + "**Required packages**: `rio-tiler`, `rio-xarray`, `xarray`, `zarr`, `pystac-client`" + ] + }, + { + "cell_type": "markdown", + "id": "5", + "metadata": {}, + "source": [ + "
" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "# Section 1: Direct Zarr Access Setup\n", + "\n", + "We'll start by connecting to the EOPF STAC catalog and loading a Sentinel-2 L2A dataset with its native Zarr chunking configuration." + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "### Import libraries" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "import xarray as xr\n", + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "from pystac_client import Client\n", + "from pystac import MediaType\n", + "from rio_tiler.io import XarrayReader\n", + "from rio_tiler.models import ImageData\n", + "import rioxarray # used through `.rio` accessor\n", + "from datetime import datetime\n", + "import warnings\n", + "warnings.filterwarnings('ignore')\n", + "\n", + "print(\"βœ… Libraries imported successfully\")" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "### Connect to EOPF STAC Catalog\n", + "\n", + "We'll search for a cloud-free Sentinel-2 L2A scene over a test region." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "# Connect to EOPF STAC API\n", + "eopf_stac_api_root = \"https://stac.core.eopf.eodc.eu/\"\n", + "catalog = Client.open(url=eopf_stac_api_root)\n", + "\n", + "# Search for Sentinel-2 L2A over Napoli during summer 2025\n", + "search_results = catalog.search(\n", + " collections='sentinel-2-l2a',\n", + " bbox=(14.268124, 40.835933, 14.433823, 40.898202), # Napoli AOI\n", + " datetime='2025-06-01T00:00:00Z/2025-09-30T23:59:59Z', # Summer 2025\n", + " max_items=1,\n", + " filter={\n", + " \"op\": \"and\",\n", + " \"args\": [\n", + " {\n", + " \"op\": \"lte\",\n", + " \"args\": [\n", + " {\"property\": \"eo:cloud_cover\"},\n", + " 5 # Cloud cover less than or equal to 10%\n", + " ]\n", + " }\n", + " ]\n", + " },\n", + " filter_lang='cql2-json'\n", + ")\n", + "\n", + "# Get first item\n", + "items = list(search_results.items())\n", + "if not items:\n", + " raise ValueError(\"No items found. Try adjusting the search parameters.\")\n", + "\n", + "item = items[0]\n", + "print(f\"πŸ“¦ Found item: {item.id}\")\n", + "print(f\"πŸ“… Acquisition date: {item.properties.get('datetime', 'N/A')}\")" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "### Open Zarr Dataset with xarray\n", + "\n", + "We'll use xarray's `open_datatree()` to access the hierarchical EOPF Zarr structure directly from cloud storage." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "# Get Zarr URL from STAC item\n", + "item_assets = item.get_assets(media_type=MediaType.ZARR)\n", + "zarr_url = item_assets['product'].href\n", + "print(f\"🌐 Zarr URL: {zarr_url}\")\n", + "\n", + "# Open with xarray DataTree\n", + "dt = xr.open_datatree(\n", + " zarr_url,\n", + " engine=\"zarr\",\n", + " chunks=\"auto\" # Use existing Zarr chunks\n", + ")\n", + "\n", + "print(\"\\nπŸ“‚ Available groups in DataTree:\")\n", + "for group in sorted(dt.groups):\n", + " if dt[group].ds.data_vars:\n", + " print(f\" {group}: {list(dt[group].ds.data_vars.keys())}\")" + ] + }, + { + "cell_type": "markdown", + "id": "13", + "metadata": {}, + "source": [ + "### Explore Sentinel-2 Band Structure\n", + "\n", + "Sentinel-2 L2A provides bands at three spatial resolutions:\n", + "- **10m**: B02 (Blue), B03 (Green), B04 (Red), B08 (NIR)\n", + "- **20m**: B05, B06, B07, B8A, B11, B12\n", + "- **60m**: B01, B09, B10\n", + "\n", + "Let's examine the 10m resolution group, which we'll use for RGB visualization." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "# Access 10m resolution bands\n", + "ds_10m = dt['/measurements/reflectance/r10m'].to_dataset()\n", + "\n", + "print(\"\\nπŸ” 10m Resolution Dataset:\")\n", + "print(f\"Dimensions: {dict(ds_10m.dims)}\")\n", + "print(f\"Bands: {list(ds_10m.data_vars.keys())}\")\n", + "print(f\"Coordinates: {list(ds_10m.coords.keys())}\")\n", + "\n", + "# Check chunking configuration\n", + "if 'b04' in ds_10m:\n", + " chunks = ds_10m['b04'].chunks\n", + " print(f\"\\nπŸ“¦ Current chunk configuration: {chunks}\")\n", + " print(f\" Y-axis chunks: {chunks[0] if len(chunks) > 0 else 'N/A'}\")\n", + " print(f\" X-axis chunks: {chunks[1] if len(chunks) > 1 else 'N/A'}\")" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "### Extract and Set CRS Information\n", + "\n", + "EOPF Zarr stores CRS information in the root DataTree attributes under `other_metadata.horizontal_CRS_code`. We need to extract this and set it using rioxarray for rio-tiler compatibility." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "# Extract CRS from EOPF metadata (following geozarr.py approach)\n", + "epsg_code_full = dt.attrs.get(\"other_metadata\", {}).get(\"horizontal_CRS_code\", \"EPSG:4326\")\n", + "epsg_code = epsg_code_full.split(\":\")[-1] # Extract numeric part (e.g., \"32632\" from \"EPSG:32632\")\n", + "\n", + "print(f\"πŸ“ Extracted CRS from EOPF metadata: EPSG:{epsg_code}\")\n", + "print(f\" Full code: {epsg_code_full}\")\n", + "\n", + "# Set CRS on the dataset using rioxarray\n", + "ds_10m.rio.write_crs(f\"epsg:{epsg_code}\", inplace=True)\n", + "\n", + "print(f\"\\nβœ… CRS set successfully on dataset\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": {}, + "outputs": [], + "source": [ + "# Verify CRS and geospatial metadata\n", + "crs = ds_10m.rio.crs\n", + "bounds = ds_10m.rio.bounds()\n", + "transform = ds_10m.rio.transform()\n", + "\n", + "print(f\"\\n🌍 Geospatial Metadata:\")\n", + "print(f\" CRS: {crs}\")\n", + "print(f\" EPSG Code: {crs.to_epsg()}\")\n", + "print(f\" Bounds (left, bottom, right, top): {bounds}\")\n", + "print(f\" Transform: {transform}\")\n", + "print(f\"\\n Width: {ds_10m.dims['x']} pixels\")\n", + "print(f\" Height: {ds_10m.dims['y']} pixels\")" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "### Verify Geospatial Metadata\n", + "\n", + "Now we can access CRS, bounds, and transform information through rioxarray. This is essential for rio-tiler integration." + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "# Section 2: Rio-tiler Integration Basics\n", + "\n", + "Now we'll integrate rio-tiler to generate map tiles from our Zarr dataset." + ] + }, + { + "cell_type": "markdown", + "id": "20", + "metadata": {}, + "source": [ + "### Prepare DataArray for Rio-tiler\n", + "\n", + "Rio-tiler's `XarrayReader` works with **DataArrays**, not Datasets. We need to stack our RGB bands into a single DataArray with a 'band' dimension." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "# Verify dataset is ready\n", + "if ds_10m.rio.crs is None:\n", + " raise ValueError(\"CRS not set! Check previous steps.\")\n", + "\n", + "# Stack RGB bands into a single DataArray for rio-tiler\n", + "# XarrayReader requires a DataArray, not a Dataset\n", + "rgb_bands = xr.concat(\n", + " [ds_10m['b04'], ds_10m['b03'], ds_10m['b02']], # Red, Green, Blue\n", + " dim='band'\n", + ").assign_coords(band=['red', 'green', 'blue'])\n", + "\n", + "# Preserve CRS information\n", + "rgb_bands = rgb_bands.rio.write_crs(ds_10m.rio.crs)\n", + "\n", + "print(\"βœ… DataArray prepared for rio-tiler\")\n", + "print(f\" CRS: {rgb_bands.rio.crs}\")\n", + "print(f\" Shape: {rgb_bands.shape} (band, y, x)\")\n", + "print(f\" Bands: {list(rgb_bands.coords['band'].values)}\")" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "### Generate True Color RGB Tile\n", + "\n", + "We'll create a Web Mercator tile (zoom 12) showing true color composite (B04-Red, B03-Green, B02-Blue)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "# Create RGB composite using XarrayReader with our stacked DataArray\n", + "with XarrayReader(rgb_bands) as src:\n", + " # Get dataset info\n", + " print(f\"\\nπŸ“Š Dataset Info:\")\n", + " print(f\" CRS: {src.crs}\")\n", + " print(f\" Bounds: {src.bounds}\")\n", + " print(f\" Available bands: {src.band_names}\")\n", + " \n", + " # Get a tile at zoom level 12 for Napoli area\n", + " # Data is rescaled for visualization\n", + " tile = src.tms.tile(14.23, 40.83, 12)\n", + " rgb_data = src.tile(tile.x, tile.y, 12, tilesize=512).rescale(((0, 0.4),))\n", + "\n", + "print(f\"\\nβœ… RGB tile generated:\")\n", + "print(f\" CRS: {rgb_data.crs}\")\n", + "print(f\" Bounds: {rgb_data.bounds}\")\n", + "print(f\" Shape: {rgb_data.data.shape}\")\n", + "print(f\" Data type: {rgb_data.data.dtype}\")" + ] + }, + { + "cell_type": "markdown", + "id": "24", + "metadata": {}, + "source": [ + "### Visualize True Color Composite\n", + "\n", + "Let's visualize the RGB composite with histogram stretching for better contrast." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": {}, + "outputs": [], + "source": [ + "plt.figure(figsize=(10, 10))\n", + "plt.imshow(rgb_data.array.transpose(1, 2, 0))\n", + "plt.title('Sentinel-2 L2A True Color Composite (B04-B03-B02)', fontsize=14, fontweight='bold')\n", + "plt.xlabel('X-coordinate')\n", + "plt.ylabel('Y-coordinate')\n", + "plt.grid(False)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "26", + "metadata": {}, + "source": [ + "# Section 3: Understanding the Data Flow\n", + "\n", + "Let's examine how Zarr chunks relate to tile generation and performance." + ] + }, + { + "cell_type": "markdown", + "id": "27", + "metadata": {}, + "source": [ + "### Visualize Chunk Grid and Tile Requests\n", + "\n", + "The key to understanding chunk performance is seeing **where** tiles land relative to chunk boundaries. Let's create a visualization showing the chunk grid overlaid on actual data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28", + "metadata": {}, + "outputs": [], + "source": [ + "# Create a smaller test dataset for visualization\n", + "# First create a subset, then rechunk it to show visible chunk grid\n", + "\n", + "subset_size = 2048\n", + "ds_subset_original = ds_10m.isel(x=slice(0, subset_size), y=slice(0, subset_size))\n", + "\n", + "print(f\"πŸ“¦ Original Subset:\")\n", + "print(f\" Size: {subset_size}Γ—{subset_size} pixels\")\n", + "print(f\" Original chunks: {ds_subset_original['b04'].chunks}\")\n", + "\n", + "# Rechunk to a size that will create a visible grid (512Γ—512)\n", + "ds_subset = ds_subset_original.chunk({'y': 512, 'x': 512})\n", + "\n", + "print(f\"\\nπŸ“¦ Rechunked Test Dataset:\")\n", + "print(f\" Size: {subset_size}Γ—{subset_size} pixels\")\n", + "print(f\" New chunks: {ds_subset['b04'].chunks}\")\n", + "print(f\" Bounds: {ds_subset.rio.bounds()}\")\n", + "\n", + "# Get chunk information\n", + "if ds_subset['b04'].chunks:\n", + " chunk_y = ds_subset['b04'].chunks[0][0]\n", + " chunk_x = ds_subset['b04'].chunks[1][0]\n", + " n_chunks_y = len(ds_subset['b04'].chunks[0])\n", + " n_chunks_x = len(ds_subset['b04'].chunks[1])\n", + " \n", + " print(f\"\\n🧩 Chunk Structure:\")\n", + " print(f\" Chunk size: {chunk_y}Γ—{chunk_x} pixels\")\n", + " print(f\" Number of chunks: {n_chunks_y}Γ—{n_chunks_x} = {n_chunks_y * n_chunks_x} total\")\n", + " print(f\" Chunk size per band: ~{(chunk_y * chunk_x * 2) / (1024**2):.2f} MB\")\n", + " \n", + "print(\"\\nNote: We rechunked to 512Γ—512 to create a visible grid for demonstration.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": {}, + "outputs": [], + "source": [ + "from zarr_tiling_utils import visualize_chunks_and_tiles\n", + "\n", + "# Visualize with test dataset (now properly rechunked to 512Γ—512)\n", + "tile_info = visualize_chunks_and_tiles(ds_subset, tile_size=256, num_sample_tiles=4)" + ] + }, + { + "cell_type": "markdown", + "id": "30", + "metadata": {}, + "source": [ + "### Compare Chunking Strategies\n", + "\n", + "Now let's create datasets with different chunking strategies and see how they affect tile access patterns. We assume the requested tiles CRS are aligned with the dataset CRS.\n", + "\n", + "We'll test the following scenarios:\n", + "1. **Aligned**: Chunks match tile size (256x256)\n", + "2. **Larger**: Chunks much larger than tiles (1024x1024)\n", + "3. **Misaligned**: Chunks don't align with tiles (300x300)\n", + "4. **Misaligned Large**: Large chunks that don't align with tiles (700x700)\n", + "5. **Smaller**: Chunks smaller than tiles (128x128)\n", + "6. **Misaligned Small**: Very small chunks that don't align with tiles (100x100)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31", + "metadata": {}, + "outputs": [], + "source": [ + "# Create three versions of the subset with different chunking strategies\n", + "strategies = {\n", + " 'Aligned (256x256)': {'chunks': {'y': 256, 'x': 256}},\n", + " 'Larger (1024x1024)': {'chunks': {'y': 1024, 'x': 1024}},\n", + " 'Misaligned (300x300)': {'chunks': {'y': 300, 'x': 300}},\n", + " 'Misaligned (700x700)': {'chunks': {'y': 700, 'x': 700}},\n", + " 'Smaller (128x128)': {'chunks': {'y': 128, 'x': 128}},\n", + " 'Misaligned Small (100x100)': {'chunks': {'y': 100, 'x': 100}}\n", + "}\n", + "\n", + "# Rechunk datasets in memory\n", + "ds_variants = {}\n", + "for name, config in strategies.items():\n", + " ds_rechunked = ds_subset.chunk(config['chunks'])\n", + " ds_variants[name] = ds_rechunked\n", + " \n", + " chunk_info = ds_rechunked['b04'].chunks\n", + " print(f\"{name}:\")\n", + " print(f\" Chunks: {chunk_info[0][0]}x{chunk_info[1][0]}\")\n", + " print(f\" Number of chunks: {len(chunk_info[0])}Γ—{len(chunk_info[1])} = {len(chunk_info[0]) * len(chunk_info[1])}\")\n", + " print()" + ] + }, + { + "cell_type": "markdown", + "id": "32", + "metadata": {}, + "source": [ + "### Visualize Chunking Strategy Comparison\n", + "\n", + "Let's visualize how a single tile request maps to chunks in each strategy:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33", + "metadata": {}, + "outputs": [], + "source": [ + "from zarr_tiling_utils import compare_chunking_strategies\n", + "\n", + "results = compare_chunking_strategies(ds_variants, tile_size=256, tile_x=512, tile_y=512)" + ] + }, + { + "cell_type": "markdown", + "id": "34", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "In this notebook, we've learned:\n", + "\n", + "1. βœ… How to **extract CRS from EOPF metadata** and set it for rioxarray compatibility\n", + "2. βœ… How to **prepare DataArrays for rio-tiler** by stacking bands with `xr.concat()`\n", + "3. βœ… Generated RGB color map tiles from Sentinel-2 data\n", + "4. βœ… **Visualized spatial relationships** between Zarr chunks and tile requests\n", + "5. βœ… **Compared chunking strategies** and their impact on tile access patterns\n", + "6. βœ… Understood when to rechunk for optimal tiling performance\n", + "\n", + "### Key Takeaways\n", + "\n", + "- **Spatial alignment is everything**: Tile positions relative to chunk boundaries determine efficiency\n", + "- **1 chunk per tile is optimal**: Match chunk size to tile size when possible (e.g., 256Γ—256)\n", + "- **2-4 chunks is acceptable**: Provides good balance for multi-zoom support\n", + "- **EOPF's default chunking**: Optimized for bulk processing (~4096px), not web tiling\n", + "- **Rechunking trade-offs**: Smaller chunks = more chunks to manage but better tile efficiency\n", + "- **CRS matters**: Data CRS (UTM) vs tile CRS (Web Mercator) affects spatial queries\n", + "\n", + "### Important Patterns for EOPF Data\n", + "\n", + "```python\n", + "# 1. Extract CRS from EOPF metadata\n", + "epsg_code = dt.attrs[\"other_metadata\"][\"horizontal_CRS_code\"].split(\":\")[-1]\n", + "ds = ds.rio.write_crs(f\"epsg:{epsg_code}\")\n", + "\n", + "# 2. Stack bands for rio-tiler\n", + "stacked_bands = xr.concat([ds['b04'], ds['b03'], ds['b02']], dim='band')\n", + "stacked_bands = stacked_bands.rio.write_crs(ds.rio.crs)\n", + "\n", + "# 3. Rechunk for tiling (if needed)\n", + "ds_tiling = ds.chunk({'y': 256, 'x': 256})\n", + "\n", + "# 4. Use with XarrayReader\n", + "with XarrayReader(stacked_bands) as src:\n", + " tile = src.tile(x, y, z, tilesize=256)\n", + "```\n", + "\n", + "**Completed in approximately 15-20 minutes ⏱️**" + ] + }, + { + "cell_type": "markdown", + "id": "35", + "metadata": {}, + "source": [ + "## What's Next?\n", + "\n", + "In the next notebooks, we'll dive deeper into:\n", + "\n", + "- **Notebook 2**: [Chunking Strategy Optimization with Sentinel-1 SAR](42_rio_tiler_s1_chunking.ipynb) - Systematic benchmarking of different chunk sizes\n", + "- **Notebook 3**: [Projections and TMS with Sentinel-3 OLCI](43_rio_tiler_s3_projections.ipynb) - Optimizing spatial reference systems for global datasets\n", + "\n", + "### πŸ’ͺ Try It Yourself\n", + "\n", + "**Challenge 1**: Create a custom band combination (e.g., SWIR-NIR-Red for B12-B08-B04)\n", + "\n", + "**Challenge 2**: Compare tile generation performance for different zoom levels\n", + "\n", + "**Challenge 3**: Experiment with different chunk sizes by rechunking the dataset" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "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.11.0rc1" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/img/rio-tiler.png b/img/rio-tiler.png new file mode 100644 index 0000000..4555ab0 Binary files /dev/null and b/img/rio-tiler.png differ diff --git a/pyproject.toml b/pyproject.toml index f67283e..616502e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,9 +37,10 @@ dependencies = [ "numpy>=2.3.1", "pandas>=2.0.2", "matplotlib>=3.10.3", - "rioxarray>=0.14.1", + "rioxarray>=0.19.0", + "rio-tiler>=7.9.2", "stackstac>=0.4.4", - "xarray>=2024.5.0", + "xarray>=2025.8.0", "distributed>=2024.5.0", "dask>=2024.5.0", "ipykernel>=6.23.1", diff --git a/uv.lock b/uv.lock index f0885c2..941f635 100644 --- a/uv.lock +++ b/uv.lock @@ -132,6 +132,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490 }, ] +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + [[package]] name = "anyio" version = "4.10.0" @@ -341,6 +350,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1c/2c/8a0b02d60a1dbbae7faa5af30484b016aa3023f9833dfc0d19b0b770dd6a/botocore-1.39.11-py3-none-any.whl", hash = "sha256:1545352931a8a186f3e977b1e1a4542d7d434796e274c3c62efd0210b5ea76dc", size = 13876276 }, ] +[[package]] +name = "cachetools" +version = "6.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/7e/b975b5814bd36faf009faebe22c1072a1fa1168db34d285ef0ba071ad78c/cachetools-6.2.1.tar.gz", hash = "sha256:3f391e4bd8f8bf0931169baf7456cc822705f4e2a31f840d218f445b9a854201", size = 31325 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/c5/1e741d26306c42e2bf6ab740b2202872727e0f606033c9dd713f8b93f5a8/cachetools-6.2.1-py3-none-any.whl", hash = "sha256:09868944b6dde876dfd44e1d47e18484541eaf12f26f29b7af91b26cc892d701", size = 11280 }, +] + [[package]] name = "cartopy" version = "0.25.0" @@ -521,6 +539,35 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/e8/64c37fadfc2816a7701fa8a6ed8d87327c7d54eacfbfb6edab14a2f2be75/cloudpickle-3.1.1-py3-none-any.whl", hash = "sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e", size = 20992 }, ] +[[package]] +name = "color-operations" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/d5/8daa1179809f0d8eab39bd83ce8131e84691eb6ba55f19b7b365a822fea3/color_operations-0.2.0.tar.gz", hash = "sha256:f1bff5cff5992ec7d240f1979320a981f2e9f77d983e9298291e02f3ffaac9bf", size = 18042 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/18/ed195e388f55ef46b89cee994a5dc7c36a6c76fd3c40ed1960b86dcba4ba/color_operations-0.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6fb9b74b9dc33832d08afc8f71ec4161531f48e8bf105d0412e9a718904c5369", size = 86416 }, + { url = "https://files.pythonhosted.org/packages/32/60/a9955ab7077309241d47f0b88d43b993abd49b753ed69449b8f2ced7c30c/color_operations-0.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:05838eee03df5304e014de76bd3ff19974964fc57dad8ce52cf56a9a62f5d572", size = 50869 }, + { url = "https://files.pythonhosted.org/packages/b5/79/3fb8aee10ceb0278bd256eee35dd9fd9607370e4bb75a75a56173fb04394/color_operations-0.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5222cf35ca089637d3424eb42a0c9bfa25aa91dbf771759f6c8003b09b5134cc", size = 49243 }, + { url = "https://files.pythonhosted.org/packages/ec/b5/1783a6834d10960a13c18b6a34f27597052510142ce89125662276184f99/color_operations-0.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b807de37a40ee1d6fa91d122b0afe1df5f17ee60b9ef1bd38e8c134ffb3070d", size = 189098 }, + { url = "https://files.pythonhosted.org/packages/91/d8/c33469d020f135414a5cd936bd31f4d3d2e3db557df94847ed59cf99a422/color_operations-0.2.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:780685c51e103f378c7bdffbafdd3e24f89b6dcd64079b7d6b3fbab7a23a06bf", size = 195175 }, + { url = "https://files.pythonhosted.org/packages/0b/b6/1dbff90606551f6e802fbabb4e422d3261552a7d8b7a5f4e00f5727e7525/color_operations-0.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:676812cd90ef37e8ca214c376d0a43f223f2717bf37d0b513a4a57c2e1fcfc62", size = 133689 }, + { url = "https://files.pythonhosted.org/packages/50/69/e8c09a930c45cbdf6d3cf84e32a6e53a44a200c447bed8ddf94a75a3f372/color_operations-0.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:98a3348d1dab6c5fdd79a9eeb90cd81bf6f5bf6ca65a24414460d90be76c2c37", size = 86112 }, + { url = "https://files.pythonhosted.org/packages/72/eb/d66611577d721318d5a70dcfcd8d26194cfa14e958bd14a02631c1e712f2/color_operations-0.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:55a10f40ca59505a260e0f8b1ee392a2c049314177d3858ae477e8cc5daff07d", size = 50735 }, + { url = "https://files.pythonhosted.org/packages/b1/57/13dbcc9913967489851f0b7d1c8d27840abe86e02d6e2e133d16db16d0d5/color_operations-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3d791eee208f208da9428f38ec9cad80bae4fa55bcde2af1b6d7e939d4f298d7", size = 49066 }, + { url = "https://files.pythonhosted.org/packages/ab/47/b143e2f0ef04cb3e7e4b7236f8a572449ea7340860baf88374ed5ac4f358/color_operations-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4061484091fab17f9cd71620ee10ae5902ae643fddd18dc01f1ba85636d9a0e1", size = 198667 }, + { url = "https://files.pythonhosted.org/packages/e4/f4/e754800604d6449d895d7118b346bc2f3c5176cb759934b245aae530138d/color_operations-0.2.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1dfba9c174f3bbd425da388fab22a9670500711d0982e6f82e9999792542d3bf", size = 204259 }, + { url = "https://files.pythonhosted.org/packages/56/45/fbd35c3ebb1a2d85339d70262739502f99447aab76220a7126adcb3722f9/color_operations-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7c7fea5d7d0e7dd8d469e93e1bdd29c03afb63cebfcb02747104e482be85ea97", size = 133391 }, + { url = "https://files.pythonhosted.org/packages/b4/93/fdd2e32eb1dd8929d36aabf8703adb9f438cfc79eb563b23140d0dd42475/color_operations-0.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fda310b57befc0aa3a02bf3863ff62adfedf7781ea8aab071887c5e82e5ab6c8", size = 84609 }, + { url = "https://files.pythonhosted.org/packages/e2/c4/abdcc64288c8249f5f312dd7b8ff0ccddc31ddf2d776e13796e3464dcc21/color_operations-0.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bf3218834d19e4d195885cb0fbf7b1f98db2f4fc6dd43ca5d035655d7ad3b6f7", size = 50084 }, + { url = "https://files.pythonhosted.org/packages/bb/75/5ce7c78e44f0660713e0b398620baa87d4ef1d98ec8ae42da2153afaf8d0/color_operations-0.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cee7d7da762f04110f15615ebd894820db38ab2aa262a940178a3d41350d2a0d", size = 48205 }, + { url = "https://files.pythonhosted.org/packages/28/54/eeffafffc815a8bb550d4ac1ae5a7bc86df0891e505b78c37a575ca35cb3/color_operations-0.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d2eb9dd747c081a801fc3b831bdf28f5115857934b00c4950c9ceecfb90d91f", size = 193013 }, + { url = "https://files.pythonhosted.org/packages/7f/87/835e83190dd00e2737162f9d66e5a1afcff2b6fb580b6545760749a2e0ec/color_operations-0.2.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fcca6e5593f05cf164d1a302c91c012acab2edf5a4d38c6cc0d4bc7b62388e7", size = 198605 }, + { url = "https://files.pythonhosted.org/packages/60/a4/b1a27ad6490fd316a2e6ba4d05c8dd9f5d867414707d1cd18dcfdb3dbe1f/color_operations-0.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:317d11b425ab802e1c343d8d1356f538e102d6ca57e435b7386593c69f630ac5", size = 132514 }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -853,6 +900,7 @@ dependencies = [ { name = "pystac-client" }, { name = "quarto" }, { name = "requests" }, + { name = "rio-tiler" }, { name = "rioxarray" }, { name = "s3fs" }, { name = "sarsen" }, @@ -888,13 +936,14 @@ requires-dist = [ { name = "pystac-client" }, { name = "quarto", specifier = ">=0.1.0" }, { name = "requests" }, - { name = "rioxarray", specifier = ">=0.14.1" }, + { name = "rio-tiler", specifier = ">=7.9.2" }, + { name = "rioxarray", specifier = ">=0.19.0" }, { name = "s3fs" }, { name = "sarsen" }, { name = "scikit-image", specifier = ">=0.21.0" }, { name = "shapely", specifier = ">=2.0.1" }, { name = "stackstac", specifier = ">=0.4.4" }, - { name = "xarray", specifier = ">=2024.5.0" }, + { name = "xarray", specifier = ">=2025.8.0" }, { name = "xarray-sentinel" }, { name = "zarr", specifier = ">=3.1.1" }, ] @@ -1918,6 +1967,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/01/4d/23c4e4f09da849e127e9f123241946c23c1e30f45a88366879e064211815/mistune-3.1.3-py3-none-any.whl", hash = "sha256:1a32314113cff28aa6432e99e522677c8587fd83e3d51c29b82a52409c842bd9", size = 53410 }, ] +[[package]] +name = "morecantile" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "pydantic" }, + { name = "pyproj" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/72/2d0e1f1e936538004581f792f8a2377831761fd12e4ed0a665abf768fc60/morecantile-6.2.0.tar.gz", hash = "sha256:65c7150ea68bbe16ee6f75f3f171ac1ae51ab26e7a77c92a768048f40f916412", size = 46317 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/6c/6ca6ed6b93c9879e6a804515169faefcd99e02114ef113598de9b71d27be/morecantile-6.2.0-py3-none-any.whl", hash = "sha256:a3cc8f85c6afcddb6c2ec933ad692557f96e89689730dbbd4350bdcf6ac52be0", size = 49473 }, +] + [[package]] name = "msgpack" version = "1.1.1" @@ -2224,6 +2287,65 @@ crc32c = [ { name = "crc32c" }, ] +[[package]] +name = "numexpr" +version = "2.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/2f/fdba158c9dbe5caca9c3eca3eaffffb251f2fb8674bf8e2d0aed5f38d319/numexpr-2.14.1.tar.gz", hash = "sha256:4be00b1086c7b7a5c32e31558122b7b80243fe098579b170967da83f3152b48b", size = 119400 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/67999bdd1ed1f938d38f3fedd4969632f2f197b090e50505f7cc1fa82510/numexpr-2.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2d03fcb4644a12f70a14d74006f72662824da5b6128bf1bcd10cc3ed80e64c34", size = 163195 }, + { url = "https://files.pythonhosted.org/packages/25/95/d64f680ea1fc56d165457287e0851d6708800f9fcea346fc1b9957942ee6/numexpr-2.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2773ee1133f77009a1fc2f34fe236f3d9823779f5f75450e183137d49f00499f", size = 152088 }, + { url = "https://files.pythonhosted.org/packages/0e/7f/3bae417cb13ae08afd86d08bb0301c32440fe0cae4e6262b530e0819aeda/numexpr-2.14.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebe4980f9494b9f94d10d2e526edc29e72516698d3bf95670ba79415492212a4", size = 451126 }, + { url = "https://files.pythonhosted.org/packages/4c/1a/edbe839109518364ac0bd9e918cf874c755bb2c128040e920f198c494263/numexpr-2.14.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a381e5e919a745c9503bcefffc1c7f98c972c04ec58fc8e999ed1a929e01ba6", size = 442012 }, + { url = "https://files.pythonhosted.org/packages/66/b1/be4ce99bff769a5003baddac103f34681997b31d4640d5a75c0e8ed59c78/numexpr-2.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d08856cfc1b440eb1caaa60515235369654321995dd68eb9377577392020f6cb", size = 1415975 }, + { url = "https://files.pythonhosted.org/packages/e7/33/b33b8fdc032a05d9ebb44a51bfcd4b92c178a2572cd3e6c1b03d8a4b45b2/numexpr-2.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03130afa04edf83a7b590d207444f05a00363c9b9ea5d81c0f53b1ea13fad55a", size = 1464683 }, + { url = "https://files.pythonhosted.org/packages/d0/b2/ddcf0ac6cf0a1d605e5aecd4281507fd79a9628a67896795ab2e975de5df/numexpr-2.14.1-cp311-cp311-win32.whl", hash = "sha256:db78fa0c9fcbaded3ae7453faf060bd7a18b0dc10299d7fcd02d9362be1213ed", size = 166838 }, + { url = "https://files.pythonhosted.org/packages/64/72/4ca9bd97b2eb6dce9f5e70a3b6acec1a93e1fb9b079cb4cba2cdfbbf295d/numexpr-2.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:e9b2f957798c67a2428be96b04bce85439bed05efe78eb78e4c2ca43737578e7", size = 160069 }, + { url = "https://files.pythonhosted.org/packages/9d/20/c473fc04a371f5e2f8c5749e04505c13e7a8ede27c09e9f099b2ad6f43d6/numexpr-2.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:91ebae0ab18c799b0e6b8c5a8d11e1fa3848eb4011271d99848b297468a39430", size = 162790 }, + { url = "https://files.pythonhosted.org/packages/45/93/b6760dd1904c2a498e5f43d1bb436f59383c3ddea3815f1461dfaa259373/numexpr-2.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47041f2f7b9e69498fb311af672ba914a60e6e6d804011caacb17d66f639e659", size = 152196 }, + { url = "https://files.pythonhosted.org/packages/72/94/cc921e35593b820521e464cbbeaf8212bbdb07f16dc79fe283168df38195/numexpr-2.14.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d686dfb2c1382d9e6e0ee0b7647f943c1886dba3adbf606c625479f35f1956c1", size = 452468 }, + { url = "https://files.pythonhosted.org/packages/d9/43/560e9ba23c02c904b5934496486d061bcb14cd3ebba2e3cf0e2dccb6c22b/numexpr-2.14.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee6d4fbbbc368e6cdd0772734d6249128d957b3b8ad47a100789009f4de7083", size = 443631 }, + { url = "https://files.pythonhosted.org/packages/7b/6c/78f83b6219f61c2c22d71ab6e6c2d4e5d7381334c6c29b77204e59edb039/numexpr-2.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3a2839efa25f3c8d4133252ea7342d8f81226c7c4dda81f97a57e090b9d87a48", size = 1417670 }, + { url = "https://files.pythonhosted.org/packages/0e/bb/1ccc9dcaf46281568ce769888bf16294c40e98a5158e4b16c241de31d0d3/numexpr-2.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9f9137f1351b310436662b5dc6f4082a245efa8950c3b0d9008028df92fefb9b", size = 1466212 }, + { url = "https://files.pythonhosted.org/packages/31/9f/203d82b9e39dadd91d64bca55b3c8ca432e981b822468dcef41a4418626b/numexpr-2.14.1-cp312-cp312-win32.whl", hash = "sha256:36f8d5c1bd1355df93b43d766790f9046cccfc1e32b7c6163f75bcde682cda07", size = 166996 }, + { url = "https://files.pythonhosted.org/packages/1f/67/ffe750b5452eb66de788c34e7d21ec6d886abb4d7c43ad1dc88ceb3d998f/numexpr-2.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:fdd886f4b7dbaf167633ee396478f0d0aa58ea2f9e7ccc3c6431019623e8d68f", size = 160187 }, + { url = "https://files.pythonhosted.org/packages/73/b4/9f6d637fd79df42be1be29ee7ba1f050fab63b7182cb922a0e08adc12320/numexpr-2.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:09078ba73cffe94745abfbcc2d81ab8b4b4e9d7bfbbde6cac2ee5dbf38eee222", size = 162794 }, + { url = "https://files.pythonhosted.org/packages/35/ae/d58558d8043de0c49f385ea2fa789e3cfe4d436c96be80200c5292f45f15/numexpr-2.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dce0b5a0447baa7b44bc218ec2d7dcd175b8eee6083605293349c0c1d9b82fb6", size = 152203 }, + { url = "https://files.pythonhosted.org/packages/13/65/72b065f9c75baf8f474fd5d2b768350935989d4917db1c6c75b866d4067c/numexpr-2.14.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:06855053de7a3a8425429bd996e8ae3c50b57637ad3e757e0fa0602a7874be30", size = 455860 }, + { url = "https://files.pythonhosted.org/packages/fc/f9/c9457652dfe28e2eb898372da2fe786c6db81af9540c0f853ee04a0699cc/numexpr-2.14.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f9366d23a2e991fd5a8b5e61a17558f028ba86158a4552f8f239b005cdf83c", size = 446574 }, + { url = "https://files.pythonhosted.org/packages/b6/99/8d3879c4d67d3db5560cf2de65ce1778b80b75f6fa415eb5c3e7bd37ba27/numexpr-2.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c5f1b1605695778896534dfc6e130d54a65cd52be7ed2cd0cfee3981fd676bf5", size = 1417306 }, + { url = "https://files.pythonhosted.org/packages/ea/05/6bddac9f18598ba94281e27a6943093f7d0976544b0cb5d92272c64719bd/numexpr-2.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a4ba71db47ea99c659d88ee6233fa77b6dc83392f1d324e0c90ddf617ae3f421", size = 1466145 }, + { url = "https://files.pythonhosted.org/packages/24/5d/cbeb67aca0c5a76ead13df7e8bd8dd5e0d49145f90da697ba1d9f07005b0/numexpr-2.14.1-cp313-cp313-win32.whl", hash = "sha256:638dce8320f4a1483d5ca4fda69f60a70ed7e66be6e68bc23fb9f1a6b78a9e3b", size = 166996 }, + { url = "https://files.pythonhosted.org/packages/cc/23/9281bceaeb282cead95f0aa5f7f222ffc895670ea689cc1398355f6e3001/numexpr-2.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:9fdcd4735121658a313f878fd31136d1bfc6a5b913219e7274e9fca9f8dac3bb", size = 160189 }, + { url = "https://files.pythonhosted.org/packages/f3/76/7aac965fd93a56803cbe502aee2adcad667253ae34b0badf6c5af7908b6c/numexpr-2.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:557887ad7f5d3c2a40fd7310e50597045a68e66b20a77b3f44d7bc7608523b4b", size = 163524 }, + { url = "https://files.pythonhosted.org/packages/58/65/79d592d5e63fbfab3b59a60c386853d9186a44a3fa3c87ba26bdc25b6195/numexpr-2.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:af111c8fe6fc55d15e4c7cab11920fc50740d913636d486545b080192cd0ad73", size = 152919 }, + { url = "https://files.pythonhosted.org/packages/84/78/3c8335f713d4aeb99fa758d7c62f0be1482d4947ce5b508e2052bb7aeee9/numexpr-2.14.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33265294376e7e2ae4d264d75b798a915d2acf37b9dd2b9405e8b04f84d05cfc", size = 465972 }, + { url = "https://files.pythonhosted.org/packages/35/81/9ee5f69b811e8f18746c12d6f71848617684edd3161927f95eee7a305631/numexpr-2.14.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83647d846d3eeeb9a9255311236135286728b398d0d41d35dedb532dca807fe9", size = 456953 }, + { url = "https://files.pythonhosted.org/packages/6d/39/9b8bc6e294d85cbb54a634e47b833e9f3276a8bdf7ce92aa808718a0212d/numexpr-2.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6e575fd3ad41ddf3355d0c7ef6bd0168619dc1779a98fe46693cad5e95d25e6e", size = 1426199 }, + { url = "https://files.pythonhosted.org/packages/1e/ce/0d4fcd31ab49319740d934fba1734d7dad13aa485532ca754e555ca16c8b/numexpr-2.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:67ea4771029ce818573b1998f5ca416bd255156feea017841b86176a938f7d19", size = 1474214 }, + { url = "https://files.pythonhosted.org/packages/b7/47/b2a93cbdb3ba4e009728ad1b9ef1550e2655ea2c86958ebaf03b9615f275/numexpr-2.14.1-cp313-cp313t-win32.whl", hash = "sha256:15015d47d3d1487072d58c0e7682ef2eb608321e14099c39d52e2dd689483611", size = 167676 }, + { url = "https://files.pythonhosted.org/packages/86/99/ee3accc589ed032eea68e12172515ed96a5568534c213ad109e1f4411df1/numexpr-2.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:94c711f6d8f17dfb4606842b403699603aa591ab9f6bf23038b488ea9cfb0f09", size = 161096 }, + { url = "https://files.pythonhosted.org/packages/ac/36/9db78dfbfdfa1f8bf0872993f1a334cdd8fca5a5b6567e47dcb128bcb7c2/numexpr-2.14.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ede79f7ff06629f599081de644546ce7324f1581c09b0ac174da88a470d39c21", size = 162848 }, + { url = "https://files.pythonhosted.org/packages/13/c1/a5c78ae637402c5550e2e0ba175275d2515d432ec28af0cdc23c9b476e65/numexpr-2.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2eac7a5a2f70b3768c67056445d1ceb4ecd9b853c8eda9563823b551aeaa5082", size = 152270 }, + { url = "https://files.pythonhosted.org/packages/9a/ed/aabd8678077848dd9a751c5558c2057839f5a09e2a176d8dfcd0850ee00e/numexpr-2.14.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aedf38d4c0c19d3cecfe0334c3f4099fb496f54c146223d30fa930084bc8574", size = 455918 }, + { url = "https://files.pythonhosted.org/packages/88/e1/3db65117f02cdefb0e5e4c440daf1c30beb45051b7f47aded25b7f4f2f34/numexpr-2.14.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439ec4d57b853792ebe5456e3160312281c3a7071ecac5532ded3278ede614de", size = 446512 }, + { url = "https://files.pythonhosted.org/packages/9a/fb/7ceb9ee55b5f67e4a3e4d73d5af4c7e37e3c9f37f54bee90361b64b17e3f/numexpr-2.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e23b87f744e04e302d82ac5e2189ae20a533566aec76a46885376e20b0645bf8", size = 1417845 }, + { url = "https://files.pythonhosted.org/packages/45/2d/9b5764d0eafbbb2889288f80de773791358acf6fad1a55767538d8b79599/numexpr-2.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:44f84e0e5af219dbb62a081606156420815890e041b87252fbcea5df55214c4c", size = 1466211 }, + { url = "https://files.pythonhosted.org/packages/5d/21/204db708eccd71aa8bc55bcad55bc0fc6c5a4e01ad78e14ee5714a749386/numexpr-2.14.1-cp314-cp314-win32.whl", hash = "sha256:1f1a5e817c534539351aa75d26088e9e1e0ef1b3a6ab484047618a652ccc4fc3", size = 168835 }, + { url = "https://files.pythonhosted.org/packages/4f/3e/d83e9401a1c3449a124f7d4b3fb44084798e0d30f7c11e60712d9b94cf11/numexpr-2.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:587c41509bc373dfb1fe6086ba55a73147297247bedb6d588cda69169fc412f2", size = 162608 }, + { url = "https://files.pythonhosted.org/packages/7f/d6/ec947806bb57836d6379a8c8a253c2aeaa602b12fef2336bfd2462bb4ed5/numexpr-2.14.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec368819502b64f190c3f71be14a304780b5935c42aae5bf22c27cc2cbba70b5", size = 163525 }, + { url = "https://files.pythonhosted.org/packages/0d/77/048f30dcf661a3d52963a88c29b52b6d5ce996d38e9313a56a922451c1e0/numexpr-2.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7e87f6d203ac57239de32261c941e9748f9309cbc0da6295eabd0c438b920d3a", size = 152917 }, + { url = "https://files.pythonhosted.org/packages/9e/d3/956a13e628d722d649fbf2fded615134a308c082e122a48bad0e90a99ce9/numexpr-2.14.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd72d8c2a165fe45ea7650b16eb8cc1792a94a722022006bb97c86fe51fd2091", size = 466242 }, + { url = "https://files.pythonhosted.org/packages/d6/dd/abe848678d82486940892f2cacf39e82eec790e8930d4d713d3f9191063b/numexpr-2.14.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70d80fcb418a54ca208e9a38e58ddc425c07f66485176b261d9a67c7f2864f73", size = 457149 }, + { url = "https://files.pythonhosted.org/packages/fd/bb/797b583b5fb9da5700a5708ca6eb4f889c94d81abb28de4d642c0f4b3258/numexpr-2.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:edea2f20c2040df8b54ee8ca8ebda63de9545b2112872466118e9df4d0ae99f3", size = 1426493 }, + { url = "https://files.pythonhosted.org/packages/77/c4/0519ab028fdc35e3e7ee700def7f2b4631b175cd9e1202bd7966c1695c33/numexpr-2.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:790447be6879a6c51b9545f79612d24c9ea0a41d537a84e15e6a8ddef0b6268e", size = 1474413 }, + { url = "https://files.pythonhosted.org/packages/d4/4a/33044878c8f4a75213cfe9c11d4c02058bb710a7a063fe14f362e8de1077/numexpr-2.14.1-cp314-cp314t-win32.whl", hash = "sha256:538961096c2300ea44240209181e31fae82759d26b51713b589332b9f2a4117e", size = 169502 }, + { url = "https://files.pythonhosted.org/packages/41/a2/5a1a2c72528b429337f49911b18c302ecd36eeab00f409147e1aa4ae4519/numexpr-2.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:a40b350cd45b4446076fa11843fa32bbe07024747aeddf6d467290bf9011b392", size = 163589 }, +] + [[package]] name = "numpy" version = "2.3.2" @@ -2648,6 +2770,118 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, ] +[[package]] +name = "pydantic" +version = "2.12.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400 }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873 }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826 }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869 }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890 }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740 }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021 }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378 }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761 }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303 }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355 }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875 }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549 }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305 }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902 }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990 }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003 }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200 }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578 }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504 }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816 }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366 }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698 }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603 }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591 }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068 }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908 }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145 }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179 }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403 }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206 }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307 }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258 }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917 }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186 }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164 }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146 }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788 }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133 }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852 }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679 }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766 }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005 }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622 }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725 }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040 }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691 }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897 }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302 }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877 }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680 }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960 }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102 }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039 }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126 }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489 }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288 }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255 }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760 }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092 }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385 }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832 }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585 }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078 }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914 }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560 }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244 }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955 }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906 }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607 }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769 }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441 }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291 }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632 }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905 }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495 }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388 }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879 }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017 }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980 }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865 }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256 }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762 }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141 }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317 }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992 }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302 }, +] + [[package]] name = "pygments" version = "2.19.2" @@ -3077,6 +3311,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368 }, ] +[[package]] +name = "rio-tiler" +version = "7.9.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "cachetools" }, + { name = "color-operations" }, + { name = "httpx" }, + { name = "morecantile" }, + { name = "numexpr" }, + { name = "numpy" }, + { name = "pydantic" }, + { name = "pystac" }, + { name = "rasterio" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/1b/684dd2478fdbf69befa7518936639c37c9fa1694fd75cca5c0430a2ab542/rio_tiler-7.9.2.tar.gz", hash = "sha256:55f96adcffcf67825c83a9906085b4d5b740139ec66432949a0e4c0b4ea6916b", size = 175772 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/8c/cbb6feed404ab0b2883b81349d8642d96047878394e7195d1bacfe36a277/rio_tiler-7.9.2-py3-none-any.whl", hash = "sha256:aeb078e63b59ef1041c99bdd4f776341ee8e940fa57ca2e37bab498738b49b56", size = 269983 }, +] + [[package]] name = "rioxarray" version = "0.19.0" @@ -3606,6 +3862,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, ] +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611 }, +] + [[package]] name = "tzdata" version = "2025.2" diff --git a/zarr_tiling_utils.py b/zarr_tiling_utils.py new file mode 100644 index 0000000..89a2c5a --- /dev/null +++ b/zarr_tiling_utils.py @@ -0,0 +1,703 @@ +""" +Zarr Tiling Utilities for EOPF-101 Notebooks + +This module provides reusable functions for zarr rechunking, tiling performance analysis, +and optimization experiments. Extracted and adapted from eopf-explorer geozarr.py. + +Key capabilities: +- Rechunking Zarr datasets with different strategies +- Calculating optimal chunk sizes for tiling workloads +- Performance benchmarking utilities +- Overview/pyramid level calculations +""" + +import time +import matplotlib.pyplot as plt +import numpy as np +import xarray as xr +from typing import Any, Dict, List, Tuple, Optional +from dataclasses import dataclass +import psutil +import os + + +@dataclass +class ChunkingStrategy: + """Configuration for a chunking strategy.""" + name: str + chunk_size: int + description: str + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary for serialization.""" + return { + 'name': self.name, + 'chunk_size': self.chunk_size, + 'description': self.description + } + + +@dataclass +class PerformanceMetrics: + """Container for tiling performance measurements.""" + chunk_size: int + tile_generation_time: float + memory_usage_mb: float + http_requests: int + data_transferred_mb: float + zoom_level: int + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary for analysis.""" + return { + 'chunk_size': self.chunk_size, + 'tile_generation_time_s': self.tile_generation_time, + 'memory_usage_mb': self.memory_usage_mb, + 'http_requests': self.http_requests, + 'data_transferred_mb': self.data_transferred_mb, + 'zoom_level': self.zoom_level + } + + +def calculate_aligned_chunk_size(dimension: int, target_chunk: int) -> int: + """ + Calculate chunk size that evenly divides the dimension. + + This ensures chunks align properly with data boundaries, avoiding + partial chunks which can degrade performance. + + Parameters + ---------- + dimension : int + Size of the data dimension + target_chunk : int + Desired chunk size + + Returns + ------- + int + Aligned chunk size that evenly divides dimension + + Examples + -------- + >>> calculate_aligned_chunk_size(10980, 1024) + 915 # 10980 / 12 = 915 + >>> calculate_aligned_chunk_size(5490, 512) + 915 # 5490 / 6 = 915 + """ + if target_chunk >= dimension: + return dimension + + # Find divisor of dimension closest to target_chunk + best_chunk = dimension + min_diff = abs(dimension - target_chunk) + + for divisor in range(1, int(np.sqrt(dimension)) + 1): + if dimension % divisor == 0: + # Check both divisor and its complement + for candidate in [divisor, dimension // divisor]: + diff = abs(candidate - target_chunk) + if diff < min_diff and candidate <= target_chunk * 1.5: + best_chunk = candidate + min_diff = diff + + return best_chunk + + +def calculate_optimal_chunks_for_tiling( + width: int, + height: int, + tile_size: int = 256, + target_zoom_levels: Optional[List[int]] = None +) -> Dict[int, Tuple[int, int]]: + """ + Calculate optimal chunk sizes for different zoom levels. + + For web mapping applications, chunk size should align with tile access + patterns at different zoom levels to minimize HTTP requests. + + Parameters + ---------- + width : int + Dataset width in pixels + height : int + Dataset height in pixels + tile_size : int, default 256 + Web tile size (typically 256 or 512) + target_zoom_levels : List[int], optional + Specific zoom levels to optimize for + + Returns + ------- + Dict[int, Tuple[int, int]] + Mapping of zoom level to (y_chunk, x_chunk) + + Examples + -------- + >>> calculate_optimal_chunks_for_tiling(10980, 10980, tile_size=256) + {8: (1830, 1830), 12: (915, 915), 16: (456, 456)} + """ + if target_zoom_levels is None: + target_zoom_levels = [8, 12, 16] + + optimal_chunks = {} + + for zoom in target_zoom_levels: + # At each zoom level, calculate how many tiles cover the dataset + tiles_at_zoom = 2 ** zoom + pixels_per_tile = width / tiles_at_zoom + + # Target chunk size should be a multiple of tile_size + target_chunk = max(tile_size, int(pixels_per_tile)) + + # Align chunk size to evenly divide dimensions + chunk_y = calculate_aligned_chunk_size(height, target_chunk) + chunk_x = calculate_aligned_chunk_size(width, target_chunk) + + optimal_chunks[zoom] = (chunk_y, chunk_x) + + return optimal_chunks + + +def rechunk_dataset( + ds: xr.Dataset, + chunk_size: int, + output_path: str, + variables: Optional[List[str]] = None, + spatial_dims: Tuple[str, str] = ('y', 'x') +) -> xr.Dataset: + """ + Rechunk an xarray Dataset with a new spatial chunk size. + + This creates a new Zarr store with the specified chunking. Useful for + experimenting with different chunk strategies. + + Parameters + ---------- + ds : xr.Dataset + Source dataset to rechunk + chunk_size : int + New spatial chunk size (applied to both y and x) + output_path : str + Path for output Zarr store + variables : List[str], optional + Specific variables to rechunk (default: all data variables) + spatial_dims : Tuple[str, str], default ('y', 'x') + Names of spatial dimensions + + Returns + ------- + xr.Dataset + Rechunked dataset + + Examples + -------- + >>> ds_rechunked = rechunk_dataset(ds, chunk_size=512, output_path='./test.zarr') + """ + if variables is None: + variables = list(ds.data_vars.keys()) + + # Create chunking dict + chunks = {spatial_dims[0]: chunk_size, spatial_dims[1]: chunk_size} + + # Apply rechunking + ds_rechunked = ds[variables].chunk(chunks) + + # Write to Zarr + print(f"Writing rechunked dataset to {output_path}") + print(f" Chunk size: {chunk_size}x{chunk_size}") + print(f" Variables: {variables}") + + ds_rechunked.to_zarr( + output_path, + mode='w', + consolidated=True, + compute=True + ) + + # Reload to verify + ds_reloaded = xr.open_zarr(output_path, chunks='auto') + print(f"βœ… Rechunking complete. New chunks: {ds_reloaded[variables[0]].chunks}") + + return ds_reloaded + + +def benchmark_tile_generation( + ds: xr.Dataset, + tile_size: int, + zoom_level: int, + num_tiles: int = 10, + bands: Optional[List[str]] = None +) -> PerformanceMetrics: + """ + Benchmark tile generation performance for a given chunk configuration. + + Measures: + - Tile generation time + - Memory usage + - Estimated HTTP requests + - Data transferred + + Parameters + ---------- + ds : xr.Dataset + Dataset to generate tiles from + tile_size : int + Size of tiles to generate + zoom_level : int + Zoom level for tile generation + num_tiles : int, default 10 + Number of tiles to generate for averaging + bands : List[str], optional + Specific bands to use + + Returns + ------- + PerformanceMetrics + Performance measurements + + Examples + -------- + >>> metrics = benchmark_tile_generation(ds, tile_size=256, zoom_level=12) + >>> print(f"Avg time per tile: {metrics.tile_generation_time:.3f}s") + """ + if bands is None: + bands = list(ds.data_vars.keys())[:3] # Use first 3 bands + + # Get memory before + process = psutil.Process(os.getpid()) + mem_before = process.memory_info().rss / (1024 * 1024) + + # Generate tiles and measure time + times = [] + for i in range(num_tiles): + # Random tile coordinates + y_offset = np.random.randint(0, max(1, ds.dims['y'] - tile_size)) + x_offset = np.random.randint(0, max(1, ds.dims['x'] - tile_size)) + + start = time.time() + tile_data = ds[bands].isel( + y=slice(y_offset, y_offset + tile_size), + x=slice(x_offset, x_offset + tile_size) + ).compute() + times.append(time.time() - start) + + # Get memory after + mem_after = process.memory_info().rss / (1024 * 1024) + memory_delta = mem_after - mem_before + + # Estimate HTTP requests (chunks accessed) + chunk_y, chunk_x = ds[bands[0]].chunks[0][0], ds[bands[0]].chunks[1][0] + chunks_per_tile = np.ceil(tile_size / chunk_y) * np.ceil(tile_size / chunk_x) + http_requests = int(chunks_per_tile * len(bands)) + + # Estimate data transferred + bytes_per_chunk = chunk_y * chunk_x * ds[bands[0]].dtype.itemsize + data_transferred_mb = (bytes_per_chunk * http_requests) / (1024 * 1024) + + avg_time = np.mean(times) + + return PerformanceMetrics( + chunk_size=chunk_y, + tile_generation_time=avg_time, + memory_usage_mb=memory_delta, + http_requests=http_requests, + data_transferred_mb=data_transferred_mb, + zoom_level=zoom_level + ) + + +def calculate_overview_levels( + native_width: int, + native_height: int, + min_dimension: int = 256, + tile_width: int = 256 +) -> List[Dict[str, int]]: + """ + Calculate overview/pyramid levels following COG /2 downsampling logic. + + This is used for creating multi-scale datasets optimized for different + zoom levels. + + Parameters + ---------- + native_width : int + Width of native resolution data + native_height : int + Height of native resolution data + min_dimension : int, default 256 + Stop creating overviews when dimension is smaller than this + tile_width : int, default 256 + Tile width for TMS compatibility + + Returns + ------- + List[Dict[str, int]] + List of overview level dictionaries + + Examples + -------- + >>> levels = calculate_overview_levels(10980, 10980) + >>> for level in levels: + ... print(f"Level {level['level']}: {level['width']}x{level['height']}") + Level 0: 10980x10980 + Level 1: 5490x5490 + Level 2: 2745x2745 + Level 3: 1372x1372 + Level 4: 686x686 + Level 5: 343x343 + """ + overview_levels = [] + level = 0 + current_width = native_width + current_height = native_height + + while min(current_width, current_height) >= min_dimension: + # Calculate zoom level for TMS compatibility + zoom_for_width = max(0, int(np.ceil(np.log2(current_width / tile_width)))) + zoom_for_height = max(0, int(np.ceil(np.log2(current_height / tile_width)))) + zoom = max(zoom_for_width, zoom_for_height) + + overview_levels.append({ + 'level': level, + 'zoom': zoom, + 'width': current_width, + 'height': current_height, + 'scale_factor': 2**level + }) + + level += 1 + current_width = native_width // (2**level) + current_height = native_height // (2**level) + + return overview_levels + + +def downsample_2d_array( + data: np.ndarray, + target_height: int, + target_width: int, + method: str = 'mean' +) -> np.ndarray: + """ + Downsample a 2D array to target dimensions. + + Parameters + ---------- + data : np.ndarray + Source 2D array + target_height : int + Target height + target_width : int + Target width + method : str, default 'mean' + Downsampling method ('mean', 'nearest', 'max') + + Returns + ------- + np.ndarray + Downsampled array + + Examples + -------- + >>> data = np.random.rand(1000, 1000) + >>> downsampled = downsample_2d_array(data, 500, 500) + >>> downsampled.shape + (500, 500) + """ + from skimage.transform import resize + + if method == 'mean': + return resize(data, (target_height, target_width), anti_aliasing=True, preserve_range=True) + elif method == 'nearest': + return resize(data, (target_height, target_width), order=0, preserve_range=True) + elif method == 'max': + # Block max pooling + block_y = data.shape[0] // target_height + block_x = data.shape[1] // target_width + result = np.zeros((target_height, target_width), dtype=data.dtype) + for i in range(target_height): + for j in range(target_width): + result[i, j] = np.max(data[i*block_y:(i+1)*block_y, j*block_x:(j+1)*block_x]) + return result + else: + raise ValueError(f"Unknown method: {method}") + + +def print_performance_summary(results: Dict[str, List[PerformanceMetrics]]) -> None: + """ + Print a formatted summary of benchmarking results. + + Parameters + ---------- + results : Dict[str, List[PerformanceMetrics]] + Results from compare_chunking_strategies() + """ + print("\\n" + "="*80) + print("PERFORMANCE SUMMARY") + print("="*80) + + for strategy_name, metrics_list in results.items(): + print(f"\\n{strategy_name.upper()}:") + print(f"{'Zoom':<8} {'Time (s)':<12} {'Memory (MB)':<15} {'HTTP Reqs':<12} {'Data (MB)':<12}") + print("-" * 80) + + for m in metrics_list: + print(f"{m.zoom_level:<8} {m.tile_generation_time:<12.3f} " + f"{m.memory_usage_mb:<15.2f} {m.http_requests:<12} " + f"{m.data_transferred_mb:<12.2f}") + + print("="*80) + + +def visualize_chunks_and_tiles(ds, tile_size=256, num_sample_tiles=4): + """ + Visualize spatial relationship between Zarr chunks and tile requests. + + Shows chunk boundaries on actual data with example tile requests overlaid. + """ + import matplotlib.patches as mpatches + + # Get chunk and dimension info from the ACTUAL dataset chunks + chunk_y = ds['b04'].chunks[0][0] + chunk_x = ds['b04'].chunks[1][0] + height, width = ds.dims['y'], ds.dims['x'] + + print(f"πŸ“ Using ACTUAL chunk dimensions from dataset:") + print(f" Chunk size: {chunk_y}Γ—{chunk_x} pixels (from ds['b04'].chunks)") + print(f" Dataset size: {height}Γ—{width} pixels") + print(f" Tile size for demo: {tile_size}Γ—{tile_size} pixels\n") + + # Create figure + fig, ax = plt.subplots(1, 1, figsize=(14, 14)) + + # Display band data as background + band_data = ds['b04'].values + + # Simple contrast stretch + p2, p98 = np.percentile(band_data[band_data > 0], [2, 98]) + band_stretched = np.clip((band_data - p2) / (p98 - p2), 0, 1) + + ax.imshow(band_stretched, cmap='gray', extent=[0, width, height, 0], alpha=0.7) + + # Draw chunk grid using ACTUAL chunk dimensions (NOT tile_size) + for i in range(0, height + 1, chunk_y): + ax.axhline(y=i, color='cyan', linewidth=2, alpha=0.8, linestyle='-') + for j in range(0, width + 1, chunk_x): + ax.axvline(x=j, color='cyan', linewidth=2, alpha=0.8, linestyle='-') + + # Add chunk labels + for i_chunk in range(int(np.ceil(height / chunk_y))): + for j_chunk in range(int(np.ceil(width / chunk_x))): + y_pos = i_chunk * chunk_y + chunk_y / 2 + x_pos = j_chunk * chunk_x + chunk_x / 2 + if y_pos < height and x_pos < width: + ax.text(x_pos, y_pos, f'C{i_chunk},{j_chunk}', + ha='center', va='center', fontsize=8, color='cyan', + bbox=dict(boxstyle='round', facecolor='black', alpha=0.5)) + + # Draw example tile requests + np.random.seed(112) + tile_colors = ['red', 'yellow', 'lime', 'magenta'] + tile_info = [] + + for i in range(num_sample_tiles): + # Random tile position + margin = tile_size + x_start = np.random.randint(margin, width - tile_size - margin) + y_start = np.random.randint(margin, height - tile_size - margin) + + # Calculate which chunks this tile intersects + chunk_y_start = int(y_start / chunk_y) + chunk_y_end = int((y_start + tile_size - 1) / chunk_y) + chunk_x_start = int(x_start / chunk_x) + chunk_x_end = int((x_start + tile_size - 1) / chunk_x) + + chunks_accessed = (chunk_y_end - chunk_y_start + 1) * (chunk_x_end - chunk_x_start + 1) + tile_info.append({ + 'tile': i+1, + 'chunks': chunks_accessed, + 'chunk_range': f'Y:{chunk_y_start}-{chunk_y_end}, X:{chunk_x_start}-{chunk_x_end}' + }) + + # Draw tile boundary + rect = mpatches.Rectangle( + (x_start, y_start), tile_size, tile_size, + linewidth=4, edgecolor=tile_colors[i], facecolor='none', + linestyle='--', label=f'Tile {i+1} ({chunks_accessed} chunks)' + ) + ax.add_patch(rect) + + # Add tile label + ax.text(x_start + tile_size/2, y_start + tile_size/2, f'T{i+1}', + color=tile_colors[i], fontsize=16, fontweight='bold', + ha='center', va='center', + bbox=dict(boxstyle='round', facecolor='black', alpha=0.8)) + + ax.set_title(f'Chunk Grid (cyan, {chunk_y}Γ—{chunk_x}px) with {tile_size}Γ—{tile_size}px Tile Requests', + fontsize=14, fontweight='bold') + ax.set_xlabel('X (pixels)', fontsize=12) + ax.set_ylabel('Y (pixels)', fontsize=12) + ax.legend(loc='upper right', fontsize=10) + ax.set_xlim(0, width) + ax.set_ylim(height, 0) + + plt.tight_layout() + plt.show() + + # Print tile information + print("\nπŸ“Š Tile-to-Chunk Mapping:") + print(f"{'Tile':<8} {'Chunks Accessed':<18} {'Chunk Range'}") + print("=" * 60) + for info in tile_info: + print(f"T{info['tile']:<7} {info['chunks']:<18} {info['chunk_range']}") + + return tile_info + +def compare_chunking_strategies(ds_variants, tile_size=256, tile_x=512, tile_y=512): + """ + Compare how a single tile request maps to chunks in different strategies. + Calculates chunks accessed and data transfer volume for each strategy. + """ + fig, axes = plt.subplots(2, 3, figsize=(18, 12)) + axes = axes.flatten() # Flatten 2D array to 1D for easy indexing + + results = {} + + for idx, (name, ds) in enumerate(ds_variants.items()): + ax = axes[idx] + + # Get chunk info + chunk_y = ds['b04'].chunks[0][0] + chunk_x = ds['b04'].chunks[1][0] + height, width = ds.dims['y'], ds.dims['x'] + + # Get dtype size + dtype_size = ds['b04'].dtype.itemsize + num_bands = len([v for v in ds.data_vars if v.startswith('b')]) + + # Display data + band_data = ds['b04'].values + p2, p98 = np.percentile(band_data[band_data > 0], [2, 98]) + band_stretched = np.clip((band_data - p2) / (p98 - p2), 0, 1) + ax.imshow(band_stretched, cmap='gray', extent=[0, width, height, 0], alpha=0.5) + + # Draw chunk grid + for i in range(0, height + 1, chunk_y): + ax.axhline(y=i, color='cyan', linewidth=1.5, alpha=0.7) + for j in range(0, width + 1, chunk_x): + ax.axvline(x=j, color='cyan', linewidth=1.5, alpha=0.7) + + # Draw the test tile + import matplotlib.patches as mpatches + rect = mpatches.Rectangle( + (tile_x, tile_y), tile_size, tile_size, + linewidth=4, edgecolor='red', facecolor='none', linestyle='--' + ) + ax.add_patch(rect) + + # Calculate chunks accessed + chunk_y_start = int(tile_y / chunk_y) + chunk_y_end = int((tile_y + tile_size - 1) / chunk_y) + chunk_x_start = int(tile_x / chunk_x) + chunk_x_end = int((tile_x + tile_size - 1) / chunk_x) + + chunks_accessed = (chunk_y_end - chunk_y_start + 1) * (chunk_x_end - chunk_x_start + 1) + + # Calculate data transfer volumes + # Actual tile data needed + tile_data_mb = (tile_size * tile_size * dtype_size * num_bands) / (1024 * 1024) + + # Data that must be transferred (full chunks) + chunk_size_bytes = chunk_y * chunk_x * dtype_size * num_bands + transferred_mb = (chunks_accessed * chunk_size_bytes) / (1024 * 1024) + + # Overhead ratio + overhead_ratio = transferred_mb / tile_data_mb if tile_data_mb > 0 else 0 + + # Highlight accessed chunks + for cy in range(chunk_y_start, chunk_y_end + 1): + for cx in range(chunk_x_start, chunk_x_end + 1): + rect_chunk = mpatches.Rectangle( + (cx * chunk_x, cy * chunk_y), chunk_x, chunk_y, + facecolor='red', alpha=0.2, edgecolor='red', linewidth=2 + ) + ax.add_patch(rect_chunk) + + ax.set_title(f'{name}\n{chunks_accessed} chunk{"s" if chunks_accessed > 1 else ""} accessed | {transferred_mb:.2f} MBits transferred', + fontsize=12, fontweight='bold') + ax.set_xlabel('X (pixels)') + ax.set_ylabel('Y (pixels)') + ax.set_xlim(0, width) + ax.set_ylim(height, 0) + + results[name] = { + 'chunks_accessed': chunks_accessed, + 'tile_data_mb': tile_data_mb, + 'transferred_mb': transferred_mb, + 'overhead_ratio': overhead_ratio, + 'chunk_size': f'{chunk_y}Γ—{chunk_x}' + } + + plt.tight_layout() + plt.show() + + # Summary + print(f"\n🎯 Chunk Access and Data Transfer Comparison for {tile_size}Γ—{tile_size}px Tile:") + print("=" * 100) + print(f"{'Strategy':<30} {'Chunk Size':<12} {'Chunks':<8} {'Tile Data':<12} {'Transferred':<14} {'Overhead':<10} {'Efficiency'}") + print("-" * 100) + + for name, metrics in results.items(): + chunks = metrics['chunks_accessed'] + overhead = metrics['overhead_ratio'] + + # Efficiency considers both chunk count (HTTP requests) and data overhead + if chunks == 1 and overhead <= 2.0: + efficiency_label = "βœ… Optimal" + elif chunks <= 4 and overhead <= 4.0: + efficiency_label = "βœ… Good" + elif chunks <= 4 and overhead <= 8.0: + efficiency_label = "⚠️ Acceptable" + elif chunks > 4: + efficiency_label = "⚠️ Many requests" + elif overhead > 8.0: + efficiency_label = "❌ High overhead" + else: + efficiency_label = "❌ Inefficient" + + print(f"{name:<20} {metrics['chunk_size']:<12} {chunks:<8} " + f"{metrics['tile_data_mb']:>8.2f} MB {metrics['transferred_mb']:>10.2f} MB " + f"{metrics['overhead_ratio']:>8.2f}x {efficiency_label}") + + print("=" * 100) + print(f"\nπŸ’‘ Interpretation:") + print(f" β€’ Tile Data: Actual data needed for the {tile_size}Γ—{tile_size}px tile") + print(" β€’ Transferred: Total data that must be read from storage (full chunks)") + print(" β€’ Overhead: Ratio of transferred/needed (lower is better, 1.0x is perfect)") + print("\n Efficiency considers BOTH chunk count (HTTP requests) AND data overhead:") + print(" β€’ βœ… Optimal: 1 chunk + low overhead (≀2x)") + print(" β€’ βœ… Good: 2-4 chunks + low overhead (≀2x)") + print(" β€’ ⚠️ Acceptable: Trade-offs between chunk count and overhead") + print(" β€’ ⚠️ Many requests: Many small chunks but minimal wasted data") + print(" β€’ ❌ High overhead: Reading >10x more data than needed") + print(" β€’ ❌ Inefficient: Poor performance on both metrics") + + return results + + +if __name__ == "__main__": + # Example usage + print("Zarr Tiling Utilities for EOPF-101") + print("="*50) + print("\\nExample: Calculate optimal chunks for 10980x10980 S2 scene") + + optimal = calculate_optimal_chunks_for_tiling(10980, 10980, tile_size=256) + for zoom, chunks in optimal.items(): + print(f" Zoom {zoom}: {chunks[0]}x{chunks[1]} chunks") + + print("\\nExample: Calculate overview levels") + levels = calculate_overview_levels(10980, 10980) + for level in levels[:5]: + print(f" Level {level['level']}: {level['width']}x{level['height']} " + f"(scale 1:{level['scale_factor']})")