From 7babf713b6a366c2ae98136a0df4ba6c3236e75c Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Wed, 25 Jun 2025 13:12:14 +0200 Subject: [PATCH 01/59] Initial setup of rules: download data, cutout, process slope --- AUTHORS | 3 +- CITATION.cff | 2 + README.md | 12 ++ config/config.yaml | 7 +- workflow/Snakefile | 3 +- workflow/envs/default.yaml | 18 +++ workflow/internal/config.schema.yaml | 13 ++- workflow/internal/settings.yaml | 19 ++- workflow/rules/automatic.smk | 148 +++++++++++++++++++++++- workflow/rules/dummy.smk | 16 --- workflow/rules/prepare.smk | 78 +++++++++++++ workflow/rules/process.smk | 18 +++ workflow/scripts/dummy_script.py | 20 ---- workflow/scripts/get_slope_too_steep.py | 16 +++ workflow/scripts/subset_netcdf.py | 19 +++ 15 files changed, 342 insertions(+), 50 deletions(-) create mode 100644 workflow/envs/default.yaml delete mode 100644 workflow/rules/dummy.smk create mode 100644 workflow/rules/prepare.smk create mode 100644 workflow/rules/process.smk delete mode 100644 workflow/scripts/dummy_script.py create mode 100644 workflow/scripts/get_slope_too_steep.py create mode 100644 workflow/scripts/subset_netcdf.py diff --git a/AUTHORS b/AUTHORS index 90b16a6..7bbec25 100644 --- a/AUTHORS +++ b/AUTHORS @@ -4,4 +4,5 @@ This does not necessarily list everyone who has contributed to this software's code or documentation. For a full contributor list, see: -Stefan Pfenninger-Lee, +Linh Ho-Tran, +Stefan Pfenninger-Lee, \ No newline at end of file diff --git a/CITATION.cff b/CITATION.cff index eb6cfa7..eac3d5f 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -7,5 +7,7 @@ title: "clio - module_area_potentials: Area potentials" repository: "https://github.com/calliope-project/module_area_potentials" license: Apache-2.0 authors: + - given-names: Linh + family-names: Ho-Tran - given-names: Stefan family-names: Pfenninger-Lee diff --git a/README.md b/README.md index 7c98408..fada853 100644 --- a/README.md +++ b/README.md @@ -39,3 +39,15 @@ pixi shell # activate this project's environment cd tests/integration/ # navigate to the integration example snakemake --use-conda # run the workflow! ``` + +## Data sources and licenses + +* [GlobCover land cover data](https://due.esrin.esa.int/page_globcover.php) + * License: "You may use the GlobCover land cover map for educational and/or scientific purposes, without any fee on the condition that you credit ESA and the Université Catholique de Louvain as the source of the GlobCover products." +* [GEBCO (General Bathymetric Chart of the Oceans)](https://www.gebco.net/data-products/gridded-bathymetry-data) 15 arc-second data + * License: "The GEBCO Grid is placed in the public domain and may be used free of charge. [...] Users must: Acknowledge the source of The GEBCO Grid. A suitable form of attribution is given in the documentation that accompanies The GEBCO Grid." +* [GHSL (Global Human Settlement Layer)](https://human-settlement.emergency.copernicus.eu/download.php) built-up surface data (R2023, GHS-BUILT-S, 100m resolution) + * License: "The GHSL has been produced by the EC JRC as open and free data. Reuse is authorised, provided the source is acknowledged." +* [WDPA (World Database on Protected Areas)](https://www.protectedplanet.net/) + * License: Non-commercial allowed. Citation: "UNEP-WCMC and IUCN (2025), Protected Planet: The World Database on Protected Areas (WDPA) and World Database on Other Effective Area-based Conservation Measures (WD-OECM) [Online], June 2025, Cambridge, UK: UNEP-WCMC and IUCN. Available at: www.protectedplanet.net." +* Slope is derived from the GMTED2010 public-domain dataset and stored as unsigned 8-bit integers to save space, so we only have integer slope values. diff --git a/config/config.yaml b/config/config.yaml index 8a3244d..dbd56d9 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -1,2 +1,5 @@ -# A minimal example of how to configure this module -dummy_text: This is a user input. +techs: + pv-open-field: + max_slope: 3 + wind-onshore: + max_slope: 20 diff --git a/workflow/Snakefile b/workflow/Snakefile index 3fe07e2..9092c1a 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -19,7 +19,8 @@ with open(workflow.source_path("internal/settings.yaml"), "r") as f: # Add all your includes here. include: "rules/automatic.smk" -include: "rules/dummy.smk" +include: "rules/prepare.smk" +include: "rules/process.smk" rule all: diff --git a/workflow/envs/default.yaml b/workflow/envs/default.yaml new file mode 100644 index 0000000..2630893 --- /dev/null +++ b/workflow/envs/default.yaml @@ -0,0 +1,18 @@ +name: default +channels: + - conda-forge + - nodefaults +dependencies: + - python=3.13 + - click=8.2.1 + - geopandas=1.1.0 + - pandas=2.3.0 + - pyarrow=19.0.1 + - xarray=2025.6.1 + - netcdf4=1.7.2 + - fiona=1.10.1 + - rasterio=1.4.3 + - rioxarray=0.19.0 + - libgdal-arrow-parquet=3.10.3 + - libgdal-hdf5=3.10.3 + - matplotlib=3.10.3 diff --git a/workflow/internal/config.schema.yaml b/workflow/internal/config.schema.yaml index c0b8a46..389a9d6 100644 --- a/workflow/internal/config.schema.yaml +++ b/workflow/internal/config.schema.yaml @@ -3,6 +3,13 @@ description: "Schema for user-provided configuration files." type: object additionalProperties: false properties: - dummy_text: - description: A template example. Should be deleted in real applications. - type: string + techs: + type: object + additionalProperties: + type: object + additionalProperties: false + properties: + max_slope: + type: number + required: [max_slope] +required: [techs] diff --git a/workflow/internal/settings.yaml b/workflow/internal/settings.yaml index cbb8ff5..073a817 100644 --- a/workflow/internal/settings.yaml +++ b/workflow/internal/settings.yaml @@ -1,5 +1,22 @@ # Module settings that users cannot modify. resources: automatic: + ## # Links for automatically downloaded files - dummy_readme: "https://raw.githubusercontent.com/calliope-project/clio/refs/heads/main/README.md" + ## + # Slope + slope_gmted2010_opendap: "https://opendap.4tu.nl/thredds/dodsC/data2/test/slope-global_20101117_gmted_mea075.nc" + slope_gmted2010: "https://surfdrive.surf.nl/files/index.php/s/S4dFpFW9aJDPcVr/download" + # Land cover + globcover: "https://due.esrin.esa.int/files/Globcover2009_V2.3_Global_.zip" + globcover_landcover_tif: "GLOBCOVER_L4_200901_200912_V2.3.tif" + globcover_landseamask_tif: "GLOBCOVER_L4_200901_200912_V2.3_CLA_QL.tif" + # Bathymetry + gebco: "https://www.bodc.ac.uk/data/open_download/gebco/gebco_2024_sub_ice_topo/geotiff/" + gebco_tif: "gebco_2024_sub_ice_topo.tif" + # Built-up areas + ghsl: "https://jeodpp.jrc.ec.europa.eu/ftp/jrc-opendata/GHSL/GHS_BUILT_S_GLOBE_R2023A/GHS_BUILT_S_E2025_GLOBE_R2023A_54009_100/V1-0/GHS_BUILT_S_E2025_GLOBE_R2023A_54009_100_V1_0.zip" + ghsl_tif: "GHS_BUILT_S_E2025_GLOBE_R2023A_54009_100_V1_0.tif" + # Protected areas + wdpa: "https://d1gam3xoknrgr2.cloudfront.net/current/WDPA_Jun2025_Public.zip" + wdpa_gdb: "WDPA_Jun2025_Public.gdb" diff --git a/workflow/rules/automatic.smk b/workflow/rules/automatic.smk index 0ebb236..4e60133 100644 --- a/workflow/rules/automatic.smk +++ b/workflow/rules/automatic.smk @@ -1,16 +1,152 @@ """Rules to used to download automatic resource files.""" +rule download_slope_gmted2010: + message: + "Download slope derived from GMTED2010 data (~1 GB)." + params: + url=internal["resources"]["automatic"]["slope_gmted2010"], + output: + "resources/automatic/slope.nc", + conda: + "../envs/shell.yaml" + shell: + 'curl -sSLo {output} "{params.url}"' + +rule download_wdpa: + message: + "Download the WDPA (World Database on Protected Areas) data (~1.5 GB)." + params: + url=internal["resources"]["automatic"]["wdpa"], + output: + "resources/automatic/wdpa.zip", + conda: + "../envs/shell.yaml" + shell: + 'curl -sSLo {output} "{params.url}"' + +rule unzip_wdpa: + message: + "Unzip the WDPA (World Database on Protected Areas) data (~2.0 GB)." + params: + target=internal["resources"]["automatic"]["wdpa_gdb"], + input: + rules.download_wdpa.output, + output: + directory("resources/automatic/wdpa.gdb"), + conda: + "../envs/shell.yaml" + shell: + """ + temp_dir=$(mktemp -d) + unzip {input} -d $temp_dir + mv $temp_dir/{params.target} {output} + rm -R $temp_dir + """ -rule dummy_download: +rule download_globcover: message: - "Download the clio README file." + "Download the GlobCover land cover data (~380 MB)." params: - url=internal["resources"]["automatic"]["dummy_readme"], + url=internal["resources"]["automatic"]["globcover"], output: - readme="resources/automatic/dummy_readme.md", + "resources/automatic/globcover.zip", + conda: + "../envs/shell.yaml" + shell: + 'curl -sSLo {output} "{params.url}"' + +rule unzip_globcover: + message: + "Unzip the relevant TIF files from the GlobCover zip file." + params: + target_file_1=internal["resources"]["automatic"]["globcover_landcover_tif"], + target_file_2=internal["resources"]["automatic"]["globcover_landseamask_tif"], + input: + rules.download_globcover.output, + output: + landcover="resources/automatic/globcover-landcover.tif", + landseamask="resources/automatic/globcover-landseamask.tif", log: - "logs/dummy_download.log", + "logs/unzip_globcover.log", + conda: + "../envs/shell.yaml" + shell: + """ + temp_dir=$(mktemp -d) + unzip -j {input} {params.target_file_1} -d $temp_dir + unzip -j {input} {params.target_file_2} -d $temp_dir + mv $temp_dir/{params.target_file_1} {output.landcover} + mv $temp_dir/{params.target_file_2} {output.landseamask} + rm -R $temp_dir + """ + +rule download_ghsl: + message: + "Download the GHSL (Global Human Settlement Layer) built-up surface data (R2023, GHS-BUILT-S, 100m resolution, ~2 GB)." + params: + url=internal["resources"]["automatic"]["ghsl"], + output: + "resources/automatic/ghsl_built_s.zip", + conda: + "../envs/shell.yaml" + shell: + 'curl -sSLo {output} "{params.url}"' + +rule unzip_ghsl: + message: + "Unzip the relevant TIF file from the GHSL data." + params: + target_file=internal["resources"]["automatic"]["ghsl_tif"], + input: + rules.download_ghsl.output, + output: + "resources/automatic/ghsl_built_s.tif", + conda: + "../envs/shell.yaml" + shell: + """ + temp_dir=$(mktemp -d) + unzip -j {input} {params.target_file} -d $temp_dir + mv $temp_dir/{params.target_file} {output} + rm -R $temp_dir + """ + +rule download_gebco: + message: + "Download the GEBCO (General Bathymetric Chart of the Oceans) 15 arc-second data (4 GB zipped, 8 GB unzipped)." + params: + url=internal["resources"]["automatic"]["gebco"], + output: + "resources/automatic/gebco_2024_sub_ice_topo_geotiff.zip", + conda: + "../envs/shell.yaml" + shell: + 'curl -sSLo {output} "{params.url}"' + +rule unzip_gebco: + message: + "Unzip all (TIF) files from the GEBCO data." + input: + rules.download_gebco.output, + output: + directory("resources/automatic/gebco"), + conda: + "../envs/shell.yaml" + shell: + """ + unzip -j {input} -d {output} + """ + +rule merge_gebco: + message: + "Merge all GEBCO TIF files into a single TIF file." + input: + rules.unzip_gebco.output + output: + "resources/automatic/gebco.tif", conda: "../envs/shell.yaml" shell: - 'curl -sSLo {output.readme} "{params.url}"' + """ + rio merge {input}/gebco_2024_sub_ice_*.tif {output} --overwrite + """ diff --git a/workflow/rules/dummy.smk b/workflow/rules/dummy.smk deleted file mode 100644 index af09f22..0000000 --- a/workflow/rules/dummy.smk +++ /dev/null @@ -1,16 +0,0 @@ -rule dummy_add_text: - message: - "Dummy rule combining user inputs and automatic downloads." - params: - config_text=config["dummy_text"], - input: - user_file="resources/user/user_message.md", - readme="resources/automatic/dummy_readme.md", - output: - combined="results/combined_text.md", - log: - "logs/dummy_add_text.log", - conda: - "../envs/shell.yaml" - script: - "../scripts/dummy_script.py" diff --git a/workflow/rules/prepare.smk b/workflow/rules/prepare.smk new file mode 100644 index 0000000..5a05478 --- /dev/null +++ b/workflow/rules/prepare.smk @@ -0,0 +1,78 @@ +"""Cut out the datasets to bounds determined by the input shapefile.""" + +BASE_DIR = workflow.basedir + +rule cutout_landcover: + message: + "Cut land cover data to the bounds of the input shapefile." + input: + shapes="resources/user/shapes.parquet", + landcover=rules.unzip_globcover.output.landcover, + output: + "resources/cutout_landcover.tif", + conda: + "../envs/default.yaml" + shell: + """ + rio clip --overwrite "{input.landcover}" "{output}" --bounds "$(fio info '{input.shapes}' --bounds)" + """ + +rule cutout_landseamask: + message: + "Cut land seamask data to the bounds of the input shapefile." + input: + shapes="resources/user/shapes.parquet", + landseamask=rules.unzip_globcover.output.landseamask, + output: + "resources/cutout_landseamask.tif", + conda: + "../envs/default.yaml" + shell: + """ + rio clip --overwrite "{input.landseamask}" "{output}" --bounds "$(fio info '{input.shapes}' --bounds)" + """ + +rule cutout_slope: + message: + "Cut slope data to the bounds of the input shapefile." + input: + shapes="resources/user/shapes.parquet", + slope=rules.download_slope_gmted2010.output, + output: + "resources/cutout_slope.nc", + conda: + "../envs/default.yaml" + shell: + """ + python {BASE_DIR}/scripts/subset_netcdf.py "{input.shapes}" "{input.slope}" "{output}" + """ + +rule cutout_bathymetry: + message: + "Cut bathymetry data to the bounds of the input shapefile." + input: + shapes="resources/user/shapes.parquet", + bathymetry=rules.merge_gebco.output, + output: + "resources/cutout_bathymetry.tif", + conda: + "../envs/default.yaml" + shell: + """ + rio clip --overwrite "{input.bathymetry}" "{output}" --bounds "$(fio info '{input.shapes}' --bounds)" + """ + +rule cutout_settlement: + message: + "Cut settlement data to the bounds of the input shapefile." + input: + shapes="resources/user/shapes.parquet", + settlement=rules.unzip_ghsl.output, + output: + "resources/cutout_settlement.tif", + conda: + "../envs/default.yaml" + shell: + """ + rio clip --overwrite "{input.settlement}" "{output}" --bounds "$(fio info '{input.shapes}' --bounds)" + """ diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk new file mode 100644 index 0000000..3236980 --- /dev/null +++ b/workflow/rules/process.smk @@ -0,0 +1,18 @@ +BASE_DIR = workflow.basedir + +wildcard_constraints: + tech="|".join(config["techs"].keys()) + +rule slope_too_steep: + message: + "Get areas with slope values greater than max_slope, i.e. too steep/not suitable for the tech {wildcards.tech}.", + params: + max_slope=lambda wildcards: config["techs"][f"{wildcards.tech}"]["max_slope"] + input: + shapes=rules.cutout_slope.output, + output: + "resources/slope_too_steep_{tech}.nc", + conda: + "../envs/default.yaml" + shell: + "python {BASE_DIR}/scripts/get_slope_too_steep.py {input} {params.max_slope} {output}" diff --git a/workflow/scripts/dummy_script.py b/workflow/scripts/dummy_script.py deleted file mode 100644 index 2ae0049..0000000 --- a/workflow/scripts/dummy_script.py +++ /dev/null @@ -1,20 +0,0 @@ -"""A simple script to serve as an example. - -Should be deleted in real workflows. -""" - -import sys -from pathlib import Path -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - snakemake: Any -sys.stderr = open(snakemake.log[0], "w") - -config = snakemake.params.config_text -readme = Path(snakemake.input.readme).read_text() -user = Path(snakemake.input.user_file).read_text() - -output_text = "\n\n".join([readme, user, config]) - -Path(snakemake.output.combined).write_text(output_text) diff --git a/workflow/scripts/get_slope_too_steep.py b/workflow/scripts/get_slope_too_steep.py new file mode 100644 index 0000000..4fab0f7 --- /dev/null +++ b/workflow/scripts/get_slope_too_steep.py @@ -0,0 +1,16 @@ +import click +import rioxarray as rxr + + +@click.command() +@click.argument("slope_path", type=str) +@click.argument("max_slope", type=int) +@click.argument("output_path", type=str) +def get_slope_too_steep(slope_path, max_slope, output_path): + ds_slope = rxr.open_rasterio(slope_path) + is_too_steep_slope = ds_slope > max_slope + is_too_steep_slope.to_netcdf(output_path) + + +if __name__ == "__main__": + get_slope_too_steep() diff --git a/workflow/scripts/subset_netcdf.py b/workflow/scripts/subset_netcdf.py new file mode 100644 index 0000000..d6a36ef --- /dev/null +++ b/workflow/scripts/subset_netcdf.py @@ -0,0 +1,19 @@ +import click +import geopandas as gpd +import xarray as xr + + +@click.command() +@click.argument("shapes_path") +@click.argument("netcdf_path") +@click.argument("output_path") +def subset_netcdf(shapes_path, netcdf_path, output_path): + shapes = gpd.read_parquet(shapes_path) + minlon, minlat, maxlon, maxlat = shapes.total_bounds + opendap_ds = xr.open_dataset(netcdf_path) + subset = opendap_ds.sel(lat=slice(minlat, maxlat), lon=slice(minlon, maxlon)) + subset.to_netcdf(output_path) + + +if __name__ == "__main__": + subset_netcdf() From 49ad79c7b4eaf9b84ade2b4a501de075993951cb Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Thu, 26 Jun 2025 11:55:35 +0200 Subject: [PATCH 02/59] Use 60m GEDTM30 for slope via COG wrapper --- README.md | 2 ++ workflow/internal/settings.yaml | 3 +-- workflow/rules/automatic.smk | 16 ++++++++-------- workflow/rules/prepare.smk | 14 -------------- workflow/rules/process.smk | 2 +- 5 files changed, 12 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index fada853..767ba4a 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,8 @@ snakemake --use-conda # run the workflow! ## Data sources and licenses +* [GEDTM30](https://github.com/openlandmap/GEDTM30) for slope + * License: Creative Commons Attribution 4.0 International * [GlobCover land cover data](https://due.esrin.esa.int/page_globcover.php) * License: "You may use the GlobCover land cover map for educational and/or scientific purposes, without any fee on the condition that you credit ESA and the Université Catholique de Louvain as the source of the GlobCover products." * [GEBCO (General Bathymetric Chart of the Oceans)](https://www.gebco.net/data-products/gridded-bathymetry-data) 15 arc-second data diff --git a/workflow/internal/settings.yaml b/workflow/internal/settings.yaml index 073a817..38aad8d 100644 --- a/workflow/internal/settings.yaml +++ b/workflow/internal/settings.yaml @@ -5,8 +5,7 @@ resources: # Links for automatically downloaded files ## # Slope - slope_gmted2010_opendap: "https://opendap.4tu.nl/thredds/dodsC/data2/test/slope-global_20101117_gmted_mea075.nc" - slope_gmted2010: "https://surfdrive.surf.nl/files/index.php/s/S4dFpFW9aJDPcVr/download" + slope: "https://s3.opengeohub.org/global/dtm/v3/slope.in.degree_edtm_m_60m_s_20000101_20221231_go_epsg.4326_v20241230.tif" # Land cover globcover: "https://due.esrin.esa.int/files/Globcover2009_V2.3_Global_.zip" globcover_landcover_tif: "GLOBCOVER_L4_200901_200912_V2.3.tif" diff --git a/workflow/rules/automatic.smk b/workflow/rules/automatic.smk index 4e60133..d46f86c 100644 --- a/workflow/rules/automatic.smk +++ b/workflow/rules/automatic.smk @@ -1,16 +1,16 @@ """Rules to used to download automatic resource files.""" -rule download_slope_gmted2010: +rule download_cutout_slope: message: - "Download slope derived from GMTED2010 data (~1 GB)." + "Download slope data covering the bounds of the input shapefile." params: - url=internal["resources"]["automatic"]["slope_gmted2010"], + tiff_url=internal["resources"]["automatic"]["slope"], + input: + vector="resources/user/shapes.parquet", output: - "resources/automatic/slope.nc", - conda: - "../envs/shell.yaml" - shell: - 'curl -sSLo {output} "{params.url}"' + path="resources/automatic/slope_cutout.tif", + wrapper: + "https://github.com/irm-codebase/snakemake-wrappers/raw/rasterio-tiff-clipping/geo/rasterio/clip-cog" rule download_wdpa: message: diff --git a/workflow/rules/prepare.smk b/workflow/rules/prepare.smk index 5a05478..614c327 100644 --- a/workflow/rules/prepare.smk +++ b/workflow/rules/prepare.smk @@ -32,20 +32,6 @@ rule cutout_landseamask: rio clip --overwrite "{input.landseamask}" "{output}" --bounds "$(fio info '{input.shapes}' --bounds)" """ -rule cutout_slope: - message: - "Cut slope data to the bounds of the input shapefile." - input: - shapes="resources/user/shapes.parquet", - slope=rules.download_slope_gmted2010.output, - output: - "resources/cutout_slope.nc", - conda: - "../envs/default.yaml" - shell: - """ - python {BASE_DIR}/scripts/subset_netcdf.py "{input.shapes}" "{input.slope}" "{output}" - """ rule cutout_bathymetry: message: diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index 3236980..1927163 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -9,7 +9,7 @@ rule slope_too_steep: params: max_slope=lambda wildcards: config["techs"][f"{wildcards.tech}"]["max_slope"] input: - shapes=rules.cutout_slope.output, + shapes=rules.download_cutout_slope.output, output: "resources/slope_too_steep_{tech}.nc", conda: From 75d91b04a452d3cb3ef75e828318b522d4b18717 Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Thu, 26 Jun 2025 15:59:02 +0200 Subject: [PATCH 03/59] Move bathymetry download to COG; cleanup file naming --- workflow/internal/settings.yaml | 5 ++- workflow/rules/automatic.smk | 58 +++++++++------------------------ workflow/rules/prepare.smk | 22 ++----------- 3 files changed, 20 insertions(+), 65 deletions(-) diff --git a/workflow/internal/settings.yaml b/workflow/internal/settings.yaml index 38aad8d..0b103e8 100644 --- a/workflow/internal/settings.yaml +++ b/workflow/internal/settings.yaml @@ -6,13 +6,12 @@ resources: ## # Slope slope: "https://s3.opengeohub.org/global/dtm/v3/slope.in.degree_edtm_m_60m_s_20000101_20221231_go_epsg.4326_v20241230.tif" + # Bathymetry + bathymetry: "https://zenodo.org/records/15741950/files/gebco_2024_sub_ice_cog.tif" # Land cover globcover: "https://due.esrin.esa.int/files/Globcover2009_V2.3_Global_.zip" globcover_landcover_tif: "GLOBCOVER_L4_200901_200912_V2.3.tif" globcover_landseamask_tif: "GLOBCOVER_L4_200901_200912_V2.3_CLA_QL.tif" - # Bathymetry - gebco: "https://www.bodc.ac.uk/data/open_download/gebco/gebco_2024_sub_ice_topo/geotiff/" - gebco_tif: "gebco_2024_sub_ice_topo.tif" # Built-up areas ghsl: "https://jeodpp.jrc.ec.europa.eu/ftp/jrc-opendata/GHSL/GHS_BUILT_S_GLOBE_R2023A/GHS_BUILT_S_E2025_GLOBE_R2023A_54009_100/V1-0/GHS_BUILT_S_E2025_GLOBE_R2023A_54009_100_V1_0.zip" ghsl_tif: "GHS_BUILT_S_E2025_GLOBE_R2023A_54009_100_V1_0.tif" diff --git a/workflow/rules/automatic.smk b/workflow/rules/automatic.smk index d46f86c..cc3fb6a 100644 --- a/workflow/rules/automatic.smk +++ b/workflow/rules/automatic.smk @@ -4,13 +4,25 @@ rule download_cutout_slope: message: "Download slope data covering the bounds of the input shapefile." params: - tiff_url=internal["resources"]["automatic"]["slope"], + cog_url=internal["resources"]["automatic"]["slope"], input: vector="resources/user/shapes.parquet", output: - path="resources/automatic/slope_cutout.tif", + path="resources/cutout/slope.tif", wrapper: - "https://github.com/irm-codebase/snakemake-wrappers/raw/rasterio-tiff-clipping/geo/rasterio/clip-cog" + "https://github.com/irm-codebase/snakemake-wrappers/raw/rasterio-tiff-clipping/geo/rasterio/clip-geotiff" + +rule download_cutout_bathymetry: + message: + "Download bathymetry data covering the bounds of the input shapefile." + params: + cog_url=internal["resources"]["automatic"]["bathymetry"], + input: + vector="resources/user/shapes.parquet", + output: + path="resources/cutout/bathymetry.tif", + wrapper: + "https://github.com/irm-codebase/snakemake-wrappers/raw/rasterio-tiff-clipping/geo/rasterio/clip-geotiff" rule download_wdpa: message: @@ -110,43 +122,3 @@ rule unzip_ghsl: mv $temp_dir/{params.target_file} {output} rm -R $temp_dir """ - -rule download_gebco: - message: - "Download the GEBCO (General Bathymetric Chart of the Oceans) 15 arc-second data (4 GB zipped, 8 GB unzipped)." - params: - url=internal["resources"]["automatic"]["gebco"], - output: - "resources/automatic/gebco_2024_sub_ice_topo_geotiff.zip", - conda: - "../envs/shell.yaml" - shell: - 'curl -sSLo {output} "{params.url}"' - -rule unzip_gebco: - message: - "Unzip all (TIF) files from the GEBCO data." - input: - rules.download_gebco.output, - output: - directory("resources/automatic/gebco"), - conda: - "../envs/shell.yaml" - shell: - """ - unzip -j {input} -d {output} - """ - -rule merge_gebco: - message: - "Merge all GEBCO TIF files into a single TIF file." - input: - rules.unzip_gebco.output - output: - "resources/automatic/gebco.tif", - conda: - "../envs/shell.yaml" - shell: - """ - rio merge {input}/gebco_2024_sub_ice_*.tif {output} --overwrite - """ diff --git a/workflow/rules/prepare.smk b/workflow/rules/prepare.smk index 614c327..a13c29b 100644 --- a/workflow/rules/prepare.smk +++ b/workflow/rules/prepare.smk @@ -9,7 +9,7 @@ rule cutout_landcover: shapes="resources/user/shapes.parquet", landcover=rules.unzip_globcover.output.landcover, output: - "resources/cutout_landcover.tif", + "resources/cutout/landcover.tif", conda: "../envs/default.yaml" shell: @@ -24,7 +24,7 @@ rule cutout_landseamask: shapes="resources/user/shapes.parquet", landseamask=rules.unzip_globcover.output.landseamask, output: - "resources/cutout_landseamask.tif", + "resources/cutout/landseamask.tif", conda: "../envs/default.yaml" shell: @@ -32,22 +32,6 @@ rule cutout_landseamask: rio clip --overwrite "{input.landseamask}" "{output}" --bounds "$(fio info '{input.shapes}' --bounds)" """ - -rule cutout_bathymetry: - message: - "Cut bathymetry data to the bounds of the input shapefile." - input: - shapes="resources/user/shapes.parquet", - bathymetry=rules.merge_gebco.output, - output: - "resources/cutout_bathymetry.tif", - conda: - "../envs/default.yaml" - shell: - """ - rio clip --overwrite "{input.bathymetry}" "{output}" --bounds "$(fio info '{input.shapes}' --bounds)" - """ - rule cutout_settlement: message: "Cut settlement data to the bounds of the input shapefile." @@ -55,7 +39,7 @@ rule cutout_settlement: shapes="resources/user/shapes.parquet", settlement=rules.unzip_ghsl.output, output: - "resources/cutout_settlement.tif", + "resources/cutout/settlement.tif", conda: "../envs/default.yaml" shell: From b5a36c7d778aa661a2c7c204363f872252c0dc11 Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Thu, 26 Jun 2025 16:22:14 +0200 Subject: [PATCH 04/59] Use 30 arcsec GHSL --- workflow/internal/settings.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/workflow/internal/settings.yaml b/workflow/internal/settings.yaml index 0b103e8..536355a 100644 --- a/workflow/internal/settings.yaml +++ b/workflow/internal/settings.yaml @@ -13,8 +13,8 @@ resources: globcover_landcover_tif: "GLOBCOVER_L4_200901_200912_V2.3.tif" globcover_landseamask_tif: "GLOBCOVER_L4_200901_200912_V2.3_CLA_QL.tif" # Built-up areas - ghsl: "https://jeodpp.jrc.ec.europa.eu/ftp/jrc-opendata/GHSL/GHS_BUILT_S_GLOBE_R2023A/GHS_BUILT_S_E2025_GLOBE_R2023A_54009_100/V1-0/GHS_BUILT_S_E2025_GLOBE_R2023A_54009_100_V1_0.zip" - ghsl_tif: "GHS_BUILT_S_E2025_GLOBE_R2023A_54009_100_V1_0.tif" + ghsl: "https://jeodpp.jrc.ec.europa.eu/ftp/jrc-opendata/GHSL/GHS_BUILT_S_GLOBE_R2023A/GHS_BUILT_S_E2025_GLOBE_R2023A_4326_3ss/V1-0/GHS_BUILT_S_E2025_GLOBE_R2023A_4326_3ss_V1_0.zip" + ghsl_tif: "GHS_BUILT_S_E2025_GLOBE_R2023A_4326_30ss_V1_0.tif" # Protected areas wdpa: "https://d1gam3xoknrgr2.cloudfront.net/current/WDPA_Jun2025_Public.zip" wdpa_gdb: "WDPA_Jun2025_Public.gdb" From 4597abfc3b18a67e7febee001d06344d1e137f65 Mon Sep 17 00:00:00 2001 From: Linh Ho <45103089+LinhHo@users.noreply.github.com> Date: Mon, 30 Jun 2025 11:17:09 +0200 Subject: [PATCH 05/59] First attempt rules for onshore --- config/config.yaml | 40 +++- workflow/internal/config.schema.yaml | 43 +++- workflow/internal/settings.yaml | 2 +- workflow/rules/automatic.smk | 2 +- workflow/rules/process.smk | 71 +++++- workflow/scripts/apply_technical_mask.py | 36 ++++ workflow/scripts/get_area_potential.py | 55 +++++ .../scripts/get_suitable_land_cover_types.py | 90 ++++++++ workflow/scripts/resample.py | 202 ++++++++++++++++++ 9 files changed, 529 insertions(+), 12 deletions(-) create mode 100644 workflow/scripts/apply_technical_mask.py create mode 100644 workflow/scripts/get_area_potential.py create mode 100644 workflow/scripts/get_suitable_land_cover_types.py create mode 100644 workflow/scripts/resample.py diff --git a/config/config.yaml b/config/config.yaml index dbd56d9..82c07a6 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -1,5 +1,39 @@ -techs: - pv-open-field: +# Output specifications +specs: + projection: EPSG:4326 + resolution: 0.008333 # 30/3600 arcsec in degree, yaml does not handle "/" + +# Technical criteria +techs_onshore: + pv_open_field: max_slope: 3 - wind-onshore: + land_cover: + FARM: 0.1 + FOREST: 0 # no PV open field in forest + OTHER: 0.2 + URBAN: 0 # no PV open field in urban + settlement: + max_settlement: 0.1 + weight: -1 + wind_onshore: max_slope: 20 + land_cover: + FARM: 0.2 + FOREST: 0.05 + OTHER: 0.3 + URBAN: 0 # no wind onshore in urban + settlement: + max_settlement: 0.1 + weight: -1 + pv_rooftop: + max_slope: 90 + land_cover: + FARM: 0 + FOREST: 0 + OTHER: 0 + URBAN: 1 + settlement: + max_settlement: 1 + weight: 0.8 +# techs_offshore: +# wind_offshore: \ No newline at end of file diff --git a/workflow/internal/config.schema.yaml b/workflow/internal/config.schema.yaml index 389a9d6..9a7bd13 100644 --- a/workflow/internal/config.schema.yaml +++ b/workflow/internal/config.schema.yaml @@ -3,13 +3,48 @@ description: "Schema for user-provided configuration files." type: object additionalProperties: false properties: - techs: + specs: + type: object + properties: + projection: + type: string + resolution: + type: number + required: [projection, resolution] + additionalProperties: false + + techs_onshore: type: object additionalProperties: type: object - additionalProperties: false properties: max_slope: type: number - required: [max_slope] -required: [techs] + land_cover: + type: object + additionalProperties: + type: number + settlement: + type: object + properties: + max_settlement: + type: number + weight: + type: number + required: [max_settlement, weight] + additionalProperties: false + required: [max_slope, land_cover, settlement] + additionalProperties: false +required: [techs_onshore] + +# properties: +# techs: +# type: object +# additionalProperties: +# type: object +# additionalProperties: false +# properties: +# max_slope: +# type: number +# required: [max_slope] +# required: [techs] diff --git a/workflow/internal/settings.yaml b/workflow/internal/settings.yaml index 536355a..5118915 100644 --- a/workflow/internal/settings.yaml +++ b/workflow/internal/settings.yaml @@ -13,7 +13,7 @@ resources: globcover_landcover_tif: "GLOBCOVER_L4_200901_200912_V2.3.tif" globcover_landseamask_tif: "GLOBCOVER_L4_200901_200912_V2.3_CLA_QL.tif" # Built-up areas - ghsl: "https://jeodpp.jrc.ec.europa.eu/ftp/jrc-opendata/GHSL/GHS_BUILT_S_GLOBE_R2023A/GHS_BUILT_S_E2025_GLOBE_R2023A_4326_3ss/V1-0/GHS_BUILT_S_E2025_GLOBE_R2023A_4326_3ss_V1_0.zip" + ghsl: "https://jeodpp.jrc.ec.europa.eu/ftp/jrc-opendata/GHSL/GHS_BUILT_S_GLOBE_R2023A/GHS_BUILT_S_E2025_GLOBE_R2023A_4326_30ss/V1-0/GHS_BUILT_S_E2025_GLOBE_R2023A_4326_30ss_V1_0.zip" ghsl_tif: "GHS_BUILT_S_E2025_GLOBE_R2023A_4326_30ss_V1_0.tif" # Protected areas wdpa: "https://d1gam3xoknrgr2.cloudfront.net/current/WDPA_Jun2025_Public.zip" diff --git a/workflow/rules/automatic.smk b/workflow/rules/automatic.smk index cc3fb6a..66fa2f5 100644 --- a/workflow/rules/automatic.smk +++ b/workflow/rules/automatic.smk @@ -94,7 +94,7 @@ rule unzip_globcover: rule download_ghsl: message: - "Download the GHSL (Global Human Settlement Layer) built-up surface data (R2023, GHS-BUILT-S, 100m resolution, ~2 GB)." + "Download the GHSL (Global Human Settlement Layer) built-up surface data." params: url=internal["resources"]["automatic"]["ghsl"], output: diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index 1927163..efb40a1 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -1,18 +1,83 @@ BASE_DIR = workflow.basedir wildcard_constraints: - tech="|".join(config["techs"].keys()) + tech="|".join(config["techs_onshore"].keys()) rule slope_too_steep: message: "Get areas with slope values greater than max_slope, i.e. too steep/not suitable for the tech {wildcards.tech}.", params: - max_slope=lambda wildcards: config["techs"][f"{wildcards.tech}"]["max_slope"] + max_slope=lambda wildcards: config["techs_onshore"][f"{wildcards.tech}"]["max_slope"] input: shapes=rules.download_cutout_slope.output, output: - "resources/slope_too_steep_{tech}.nc", + "resources/tmp/slope_too_steep_{tech}.nc", conda: "../envs/default.yaml" shell: "python {BASE_DIR}/scripts/get_slope_too_steep.py {input} {params.max_slope} {output}" + +rule suitable_land_cover: + message: + "Get suitable land cover types for the tech {wildcards.tech}.", + params: + suitable_land_cover_types=lambda wildcards: config["techs_onshore"][f"{wildcards.tech}"]["land_cover"] + input: + shapes=rules.cutout_landcover.output, + output: + "resources/tmp/suitable_land_cover_{tech}.nc", + conda: + "../envs/default.yaml" + shell: + "python {BASE_DIR}/scripts/get_suitable_land_cover_types.py {input} {params.suitable_land_cover_types} {output}" + +rule resample_same_resolution: + message: + "Resample slope, land cover, and settlement to the same resolution for the tech {wildcards.tech}.", + params: + specs=config["specs"] + suitable_land_cover_types=lambda wildcards: config["techs_onshore"][f"{wildcards.tech}"]["land_cover"] + input: + shapes_path="resources/user/shapes.parquet", + slope_path=rules.slope_too_steep.output, + land_cover_path=rules.suitable_land_cover.output, + settlement_path=rules.cutout_settlement.output, + output: + pixel_area="resources/tmp/pixel_area.nc", + resampled="resources/tmp/resampled_input_{tech}.nc", + conda: + "../envs/default.yaml" + shell: + "python {BASE_DIR}/scripts/resample.py {input.shapes_path} {params.specs} {params.suitable_land_cover_types} {input.slope_path} {input.land_cover_path} {input.settlement_path} {output.pixel_area} {output.resampled}" + +rule technical_mask: + message: + "Get areas with slope values greater than max_slope, i.e. too steep/not suitable for the tech {wildcards.tech}.", + params: + technical_mask=lambda wildcards: config["techs_onshore"][f"{wildcards.tech}"] + input: + pixel_area_path=rules.resample_same_resolution.output.pixel_area, + resampled_path=rules.resample_same_resolution.output.resampled, + output: + "resources/tmp/technical_masked_{tech}.nc", + conda: + "../envs/default.yaml" + shell: + "python {BASE_DIR}/scripts/apply_technical_mask.py {input.resampled_path} {input.pixel_area_path} {params.technical_mask} {output}" + +rule area_potential: + message: + "Get areas with slope values greater than max_slope, i.e. too steep/not suitable for the tech {wildcards.tech}.", + params: + technical_mask=lambda wildcards: config["techs_onshore"][f"{wildcards.tech}"] + input: + masked_path=rules.technical_mask.output, + pixel_area_path=rules.resample_same_resolution.output.pixel_area, + protected_area_path=rules.unzip_wdpa.output, + shapes_path="resources/user/shapes.parquet", + output: + "results/area_potential_{tech}.nc", + conda: + "../envs/default.yaml" + shell: + "python {BASE_DIR}/scripts/get_area_potential.py {input.masked_path} {input.pixel_area_path} {params.technical_mask} {input.protected_area_path} {input.shapes_path} {output}" \ No newline at end of file diff --git a/workflow/scripts/apply_technical_mask.py b/workflow/scripts/apply_technical_mask.py new file mode 100644 index 0000000..8d82a1e --- /dev/null +++ b/workflow/scripts/apply_technical_mask.py @@ -0,0 +1,36 @@ +import click +import rioxarray as rxr + + +@click.command() +@click.argument("resampled_path", type=str) +@click.argument("pixel_area_path", type=str) +@click.argument("technical_mask", type=int) +@click.argument("output_path", type=str) +def apply_technical_mask(resampled_path, pixel_area_path, technical_mask, output_path): + ds_resampled = rxr.open_rasterio(resampled_path) + + # get fraction of settlement (built-up surface) compared to pixel area, both in m2 + pixel_area = rxr.open_rasterio(pixel_area_path) + ds_resampled["settlement"] = ds_resampled["settlement"] / pixel_area + + # only keep pixel with fraction sum of suitable land cover >= 0.5, + # too steep slope < 0.5 + # settlement <= max_settlement + suitable_land_cover_types = [ + type != 0 for type in technical_mask["land_cover"].items + ] + land_cover_mask = ( + ds_resampled[suitable_land_cover_types].to_array().sum(dim="variable") >= 0.5 + ) + combined_mask = ( + (ds_resampled["slope"] < 0.5) + & land_cover_mask + & (ds_resampled["settlement"] <= technical_mask["settlement"]["max_settlement"]) + ) + ds_resampled = ds_resampled.where(combined_mask) + ds_resampled.to_netcdf(output_path) + + +if __name__ == "__main__": + apply_technical_mask() diff --git a/workflow/scripts/get_area_potential.py b/workflow/scripts/get_area_potential.py new file mode 100644 index 0000000..e0fba37 --- /dev/null +++ b/workflow/scripts/get_area_potential.py @@ -0,0 +1,55 @@ +import click +import rioxarray as rxr +import geopandas as gpd + + +@click.command() +@click.argument("masked_path", type=str) +@click.argument("pixel_area_path", type=str) +@click.argument("technical_mask", type=int) +@click.argument("protected_area_path", type=str) +@click.argument("shapes_path", type=str) +@click.argument("output_path", type=str) +def get_area_potential( + masked_path, + pixel_area_path, + technical_mask, + protected_area_path, + shapes_path, + output_path, +): + ds_masked = rxr.open_rasterio(masked_path) + + # apply weights + suitable_land_cover_types = [] + for type, value in technical_mask["land_cover"].items(): + if value == 0: + continue + suitable_land_cover_types.appends() + ds_masked[type] = ds_masked[type] * value + + eligible_fraction = ( + ds_masked[suitable_land_cover_types].to_array().sum(dim="variable") + - ds_masked["slope"] + + ds_masked["settlement"] * technical_mask["settlement"]["weight"] + ) + + # mask out protected area + protected_areas = gpd.read_file(protected_area_path) + eligible_fraction = eligible_fraction.rio.clip( + protected_areas.geometry, protected_areas.crs, invert=True + ) + + # multiply pixel area to get area potential + # cut with given shape to return raster inside the shape + pixel_area = rxr.open_rasterio(pixel_area_path) + shapes = gpd.read_parquet(shapes_path) + ds_area_potential = eligible_fraction * pixel_area + ds_area_potential = ds_area_potential.rio.clip( + shapes.geometry, shapes.crs, invert=False + ) + ds_area_potential.to_netcdf(output_path) + + +if __name__ == "__main__": + get_area_potential() diff --git a/workflow/scripts/get_suitable_land_cover_types.py b/workflow/scripts/get_suitable_land_cover_types.py new file mode 100644 index 0000000..ed2e80f --- /dev/null +++ b/workflow/scripts/get_suitable_land_cover_types.py @@ -0,0 +1,90 @@ +import click +import rioxarray as rxr +import xarray as xr +import numpy as np + + +# LAND COVER +# Original classification categories taken from GlobCover 2009 land cover. +# From Troendle et al. (2019) https://github.com/timtroendle/possibility-for-electricity-autarky +# suitable land cover types are defined in config.yaml, as 1, other types are 0 + + +GlobCover = { + 11: "POST_FLOODING", + 14: "RAINFED_CROPLANDS", + 20: "MOSAIC_CROPLAND", + 30: "MOSAIC_VEGETATION", + 40: "CLOSED_TO_OPEN_BROADLEAVED_FOREST", + 50: "CLOSED_BROADLEAVED_FOREST", + 60: "OPEN_BROADLEAVED_FOREST", + 70: "CLOSED_NEEDLELEAVED_FOREST", + 90: "OPEN_NEEDLELEAVED_FOREST", + 100: "CLOSED_TO_OPEN_MIXED_FOREST", + 110: "MOSAIC_FOREST", + 120: "MOSAIC_GRASSLAND", + 130: "CLOSED_TO_OPEN_SHRUBLAND", + 140: "CLOSED_TO_OPEN_HERBS", + 150: "SPARSE_VEGETATION", + 160: "CLOSED_TO_OPEN_REGULARLY_FLOODED_FOREST", # doesn't exist in Europe + 170: "CLOSED_REGULARLY_FLOODED_FOREST", # doesn't exist in Europe + 180: "CLOSED_TO_OPEN_REGULARLY_FLOODED_GRASSLAND", # roughly 2.3% of land in Europe + 190: "ARTIFICAL_SURFACES_AND_URBAN_AREAS", + 200: "BARE_AREAS", + 210: "WATER_BODIES", + 220: "PERMANENT_SNOW", + 230: "NO_DATA", +} + +CoverType = { + "POST_FLOODING": "FARM", + "RAINFED_CROPLANDS": "FARM", + "MOSAIC_CROPLAND": "FARM", + "MOSAIC_VEGETATION": "FARM", + "CLOSED_TO_OPEN_BROADLEAVED_FOREST": "FOREST", + "CLOSED_BROADLEAVED_FOREST": "FOREST", + "OPEN_BROADLEAVED_FOREST": "FOREST", + "CLOSED_NEEDLELEAVED_FOREST": "FOREST", + "OPEN_NEEDLELEAVED_FOREST": "FOREST", + "CLOSED_TO_OPEN_MIXED_FOREST": "FOREST", + "MOSAIC_FOREST": "FOREST", + "CLOSED_TO_OPEN_REGULARLY_FLOODED_FOREST": "FOREST", + "CLOSED_REGULARLY_FLOODED_FOREST": "FOREST", + "MOSAIC_GRASSLAND": "OTHER", # vegetation + "CLOSED_TO_OPEN_SHRUBLAND": "OTHER", # vegetation + "CLOSED_TO_OPEN_HERBS": "OTHER", # vegetation + "SPARSE_VEGETATION": "OTHER", # vegetation + "CLOSED_TO_OPEN_REGULARLY_FLOODED_GRASSLAND": "OTHER", # vegetation + "BARE_AREAS": "OTHER", + "ARTIFICAL_SURFACES_AND_URBAN_AREAS": "URBAN", + "WATER_BODIES": "WATER", + "PERMANENT_SNOW": "NA", + "NO_DATA": "NA", +} + + +@click.command() +@click.argument("land_cover_path", type=str) +@click.argument("suitable_land_cover_types", type=int) +@click.argument("output_path", type=str) +def get_suitable_land_cover_type( + land_cover_path, suitable_land_cover_types, output_path +): + ds_land_cover = rxr.open_rasterio(land_cover_path) + suitable_land_cover = xr.Dataset(coords=ds_land_cover.coords) + + # convert the input value to land cover type of interest + for value in np.unique(ds_land_cover.data): + if value in GlobCover: + ds_land_cover = ds_land_cover.where( + ds_land_cover != value, other=CoverType[GlobCover[value]], drop=False + ) + + # check if each pixel is in the list of suitable land cover types + for type in suitable_land_cover_types: + suitable_land_cover[type] = (ds_land_cover == type).astype(float) + suitable_land_cover.to_netcdf(output_path) + + +if __name__ == "__main__": + get_suitable_land_cover_type() diff --git a/workflow/scripts/resample.py b/workflow/scripts/resample.py new file mode 100644 index 0000000..05be490 --- /dev/null +++ b/workflow/scripts/resample.py @@ -0,0 +1,202 @@ +import click +import math +import geopandas as gpd +import rioxarray as rxr +import xarray as xr +import numpy as np +from rasterio.transform import from_bounds +from rasterio.enums import Resampling + + +def create_empty_geospatial_array( + bounds, + resolution, + projection, +): + """ + Create an empty geospatial array with specified resolution, projection, and bounds. + + Args: + bounds (tuple): Bounds of the array (minx, miny, maxx, maxy). + resolution (float): Resolution of the array in degrees (default: 30 arc-seconds). + projection (str): CRS of the array (default: "EPSG:4326"). + + Returns: + xarray.DataArray: An empty geospatial array. + """ + # Calculate the number of pixels in x and y directions + minx, miny, maxx, maxy = bounds + width = int((maxx - minx) / resolution) # Number of pixels in x-direction + height = int((maxy - miny) / resolution) # Number of pixels in y-direction + + # Create an empty numpy array filled with NaN + data = np.full((height, width), np.nan, dtype=np.float32) + + # Define the transform (affine transformation) for the array + transform = from_bounds(*bounds, width=width, height=height) + + # Generate longitude and latitude coordinates + longitude = np.linspace(minx, maxx, width) + latitude = np.linspace(maxy, miny, height) + + # Create an xarray.DataArray with the empty data and geospatial metadata + geospatial_array = xr.DataArray( + data, + dims=("y", "x"), # Define dimensions as y (latitude) and x (longitude) + coords={ + "y": ("y", latitude, {"units": "degrees_north"}), # Latitude coordinates + "x": ("x", longitude, {"units": "degrees_east"}), # Longitude coordinates + }, + ) + + # Assign CRS and transform to the DataArray + geospatial_array.rio.write_crs(projection, inplace=True) # Set the CRS + geospatial_array.rio.write_transform( + transform, inplace=True + ) # Set the affine transform + + return geospatial_array + + +def _area_of_pixel(pixel_size, center_lat): + """Calculate km^2 area of a wgs84 square pixel. + + Adapted from: https://gis.stackexchange.com/a/127327/2397 + + Parameters: + pixel_size (float): length of side of pixel in degrees. + center_lat (float): latitude of the center of the pixel. Note this + value +/- half the `pixel-size` must not exceed 90/-90 degrees + latitude or an invalid area will be calculated. + + Returns: + Area of square pixel of side length `pixel_size` centered at + `center_lat` in km^2. + + """ + a = 6378137 # meters + b = 6356752.3142 # meters + e = math.sqrt(1 - (b / a) ** 2) + area_list = [] + for f in [center_lat + pixel_size / 2, center_lat - pixel_size / 2]: + zm = 1 - e * math.sin(math.radians(f)) + zp = 1 + e * math.sin(math.radians(f)) + area_list.append( + math.pi + * b**2 + * (math.log(zp / zm) / (2 * e) + math.sin(math.radians(f)) / (zp * zm)) + ) + return pixel_size / 360.0 * (area_list[0] - area_list[1]) / 1e6 + + +def determine_pixel_areas(raster_input, bounds, resolution, output_name): + """Returns a raster in which the value corresponds to the area in [m2] of the pixel. + based on T.Troendle determine_pixel_areas (utils.py and technically_eligible_area.py) + This assumes the data comprises square pixel in WGS84. + + Parameters: + crs: the coordinate reference system of the data (must be WGS84) + bounds: an object with attributes left/right/top/bottom given in degrees + resolution: the scalar resolution (remember: square pixels) given in degrees + """ + # the following is based on https://gis.stackexchange.com/a/288034/77760 + # and assumes the data to be in EPSG:4326 (WGS84 is similar but with lat,lon instead of lon,lat) + assert ( + raster_input.rio.crs.to_epsg() == 4326 + ), "masked_rasters does not have the projection EPSG:4326" + minx, miny, maxx, maxy = bounds + width = int((maxx - minx) / resolution) # Number of pixels in x-direction + height = int((maxy - miny) / resolution) # Number of pixels in y-direction + + latitudes = np.linspace( + start=maxy, stop=miny, num=height, endpoint=True, dtype=np.float64 + ) + varea_of_pixel = np.vectorize(lambda lat: _area_of_pixel(resolution, lat)) + pixel_area = varea_of_pixel(latitudes) # vector + pixel_area = pixel_area.repeat(width).reshape(height, width).astype(np.float64) + pixel_area = xr.DataArray( + pixel_area * 1000**2, # convert to m^2 + coords=raster_input.coords, + dims=raster_input.dims, + name="pixel_area", + ) + return pixel_area + + +@click.command() +@click.argument("shapes_path") +@click.argument("specs") +@click.argument("criteria") +@click.argument("slope_path", type=str) +@click.argument("land_cover_path", type=str) +@click.argument("settlement_path", type=str) +@click.argument("output_path_pixel_area", type=str) +@click.argument("output_path", type=str) +def get_same_shape_and_resolution( + shapes_path, + specs, + suitable_land_cover_types, + slope_path, + land_cover_path, + settlement_path, + output_path_pixel_area, + output_path, +): + """ + Resample and crop the raster_input + to have the same bounds, projection, and resolution as the reference_raster + reproject_match ensures all rasters have the same bounds (minlon, minlat, maxlon, maxlat) + """ + # create reference raster with the same bounds as given shapes + shapes = gpd.read_parquet(shapes_path) + reference_raster = create_empty_geospatial_array( + bounds=shapes.total_bounds, + projection=specs["projection"], + resolution=specs["resolution"], + ) + + pixel_area = determine_pixel_areas( + reference_raster, + bounds=shapes.total_bounds, + projection=specs["projection"], + ) + pixel_area.to_netcdf(output_path_pixel_area) + + # Resamples the raster to a specified resolution and projection as the given sample + resampled = xr.Dataset() + + # slope in fraction + ds_slope = rxr.open_dataset(slope_path) + resampled["slope_too_steep"] = ds_slope.astype(float).rio.reproject_match( + reference_raster, resampling=Resampling.average + ) + + # land cover in fraction + ds_land_cover = rxr.open_dataset(land_cover_path) + for land_type, value in suitable_land_cover_types.items(): + skip = [] + if value == 0: + skip.append(land_type) # skip this one + else: + resampled[land_type] = ds_land_cover[land_type].rio.reproject_match( + reference_raster, resampling=Resampling.average + ) + + # TEST if it's necessary to clip again with cutout + # # Crops the input raster to the specified geographic bounds + # # from the bounding box of the sample raster + # resampled[land_type] = tmp_var.rio.clip_box(*shapes.total_bounds) + + print(f"Skip the land cover types not used in this tech: {skip}") + + # settlement in sum of area of built-up surface (m2) + ds_settlement = rxr.open_rasterio(settlement_path) + resampled["settlement"] = ds_settlement.rio.reproject_match( + reference_raster, resampling=Resampling.sum + ) + + resampled.to_netcdf(output_path) + + +if __name__ == "__main__": + get_same_shape_and_resolution() From 335ba06e7f5493e87daed05067b2f012a46f558b Mon Sep 17 00:00:00 2001 From: Linh Ho <45103089+LinhHo@users.noreply.github.com> Date: Mon, 30 Jun 2025 11:25:27 +0200 Subject: [PATCH 06/59] Update calling script for snakemake --- workflow/rules/prepare.smk | 2 -- workflow/rules/process.smk | 19 +++++++++++-------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/workflow/rules/prepare.smk b/workflow/rules/prepare.smk index a13c29b..6525722 100644 --- a/workflow/rules/prepare.smk +++ b/workflow/rules/prepare.smk @@ -1,7 +1,5 @@ """Cut out the datasets to bounds determined by the input shapefile.""" -BASE_DIR = workflow.basedir - rule cutout_landcover: message: "Cut land cover data to the bounds of the input shapefile." diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index efb40a1..de6c016 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -1,5 +1,3 @@ -BASE_DIR = workflow.basedir - wildcard_constraints: tech="|".join(config["techs_onshore"].keys()) @@ -7,15 +5,16 @@ rule slope_too_steep: message: "Get areas with slope values greater than max_slope, i.e. too steep/not suitable for the tech {wildcards.tech}.", params: - max_slope=lambda wildcards: config["techs_onshore"][f"{wildcards.tech}"]["max_slope"] + max_slope=lambda wildcards: config["techs_onshore"][f"{wildcards.tech}"]["max_slope"], input: + script=workflow.source_path("../scripts/get_slope_too_steep.py"), shapes=rules.download_cutout_slope.output, output: "resources/tmp/slope_too_steep_{tech}.nc", conda: "../envs/default.yaml" shell: - "python {BASE_DIR}/scripts/get_slope_too_steep.py {input} {params.max_slope} {output}" + "python {input.script} {input.shapes} {params.max_slope} {output}" rule suitable_land_cover: message: @@ -23,13 +22,14 @@ rule suitable_land_cover: params: suitable_land_cover_types=lambda wildcards: config["techs_onshore"][f"{wildcards.tech}"]["land_cover"] input: + script=workflow.source_path("../scripts/get_suitable_land_cover_types.py"), shapes=rules.cutout_landcover.output, output: "resources/tmp/suitable_land_cover_{tech}.nc", conda: "../envs/default.yaml" shell: - "python {BASE_DIR}/scripts/get_suitable_land_cover_types.py {input} {params.suitable_land_cover_types} {output}" + "python {input.script} {input.shapes} {params.suitable_land_cover_types} {output}" rule resample_same_resolution: message: @@ -38,6 +38,7 @@ rule resample_same_resolution: specs=config["specs"] suitable_land_cover_types=lambda wildcards: config["techs_onshore"][f"{wildcards.tech}"]["land_cover"] input: + script=workflow.source_path("../scripts/resample.py"), shapes_path="resources/user/shapes.parquet", slope_path=rules.slope_too_steep.output, land_cover_path=rules.suitable_land_cover.output, @@ -48,7 +49,7 @@ rule resample_same_resolution: conda: "../envs/default.yaml" shell: - "python {BASE_DIR}/scripts/resample.py {input.shapes_path} {params.specs} {params.suitable_land_cover_types} {input.slope_path} {input.land_cover_path} {input.settlement_path} {output.pixel_area} {output.resampled}" + "python {input.script} {input.shapes_path} {params.specs} {params.suitable_land_cover_types} {input.slope_path} {input.land_cover_path} {input.settlement_path} {output.pixel_area} {output.resampled}" rule technical_mask: message: @@ -56,6 +57,7 @@ rule technical_mask: params: technical_mask=lambda wildcards: config["techs_onshore"][f"{wildcards.tech}"] input: + script=workflow.source_path("../scripts/apply_technical_mask.py"), pixel_area_path=rules.resample_same_resolution.output.pixel_area, resampled_path=rules.resample_same_resolution.output.resampled, output: @@ -63,7 +65,7 @@ rule technical_mask: conda: "../envs/default.yaml" shell: - "python {BASE_DIR}/scripts/apply_technical_mask.py {input.resampled_path} {input.pixel_area_path} {params.technical_mask} {output}" + "python {input.script} {input.resampled_path} {input.pixel_area_path} {params.technical_mask} {output}" rule area_potential: message: @@ -71,6 +73,7 @@ rule area_potential: params: technical_mask=lambda wildcards: config["techs_onshore"][f"{wildcards.tech}"] input: + script=workflow.source_path("../scripts/get_area_potential.py"), masked_path=rules.technical_mask.output, pixel_area_path=rules.resample_same_resolution.output.pixel_area, protected_area_path=rules.unzip_wdpa.output, @@ -80,4 +83,4 @@ rule area_potential: conda: "../envs/default.yaml" shell: - "python {BASE_DIR}/scripts/get_area_potential.py {input.masked_path} {input.pixel_area_path} {params.technical_mask} {input.protected_area_path} {input.shapes_path} {output}" \ No newline at end of file + "python {input.script} {input.masked_path} {input.pixel_area_path} {params.technical_mask} {input.protected_area_path} {input.shapes_path} {output}" \ No newline at end of file From 3d614cdea1f534cf7834d94beb2af783405bbaa7 Mon Sep 17 00:00:00 2001 From: Linh Ho <45103089+LinhHo@users.noreply.github.com> Date: Mon, 30 Jun 2025 14:12:27 +0200 Subject: [PATCH 07/59] Resample with dictionary from config --- config/config.yaml | 2 +- workflow/envs/default.yaml | 1 + workflow/rules/process.smk | 21 +++++++++++-------- workflow/scripts/get_area_potential.py | 3 +++ .../scripts/get_suitable_land_cover_types.py | 5 +++-- workflow/scripts/resample.py | 20 +++++++++++------- 6 files changed, 32 insertions(+), 20 deletions(-) diff --git a/config/config.yaml b/config/config.yaml index 82c07a6..424afd2 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -13,7 +13,7 @@ techs_onshore: OTHER: 0.2 URBAN: 0 # no PV open field in urban settlement: - max_settlement: 0.1 + max_settlement: 0.1 # no PV open field in settlement weight: -1 wind_onshore: max_slope: 20 diff --git a/workflow/envs/default.yaml b/workflow/envs/default.yaml index 2630893..b6c7826 100644 --- a/workflow/envs/default.yaml +++ b/workflow/envs/default.yaml @@ -16,3 +16,4 @@ dependencies: - libgdal-arrow-parquet=3.10.3 - libgdal-hdf5=3.10.3 - matplotlib=3.10.3 + - pyyaml diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index de6c016..11adc6b 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -10,7 +10,7 @@ rule slope_too_steep: script=workflow.source_path("../scripts/get_slope_too_steep.py"), shapes=rules.download_cutout_slope.output, output: - "resources/tmp/slope_too_steep_{tech}.nc", + "resources/automatic/slope_too_steep_{tech}.nc", conda: "../envs/default.yaml" shell: @@ -20,12 +20,12 @@ rule suitable_land_cover: message: "Get suitable land cover types for the tech {wildcards.tech}.", params: - suitable_land_cover_types=lambda wildcards: config["techs_onshore"][f"{wildcards.tech}"]["land_cover"] + suitable_land_cover_types=lambda wildcards: list(config["techs_onshore"][f"{wildcards.tech}"]["land_cover"].keys()), input: script=workflow.source_path("../scripts/get_suitable_land_cover_types.py"), shapes=rules.cutout_landcover.output, output: - "resources/tmp/suitable_land_cover_{tech}.nc", + "resources/automatic/suitable_land_cover_{tech}.nc", conda: "../envs/default.yaml" shell: @@ -35,8 +35,9 @@ rule resample_same_resolution: message: "Resample slope, land cover, and settlement to the same resolution for the tech {wildcards.tech}.", params: - specs=config["specs"] - suitable_land_cover_types=lambda wildcards: config["techs_onshore"][f"{wildcards.tech}"]["land_cover"] + projection=config["specs"]["projection"], + resolution=config["specs"]["resolution"], + suitable_land_cover_types=lambda wildcards: config["techs_onshore"][f"{wildcards.tech}"]["land_cover"], input: script=workflow.source_path("../scripts/resample.py"), shapes_path="resources/user/shapes.parquet", @@ -44,12 +45,14 @@ rule resample_same_resolution: land_cover_path=rules.suitable_land_cover.output, settlement_path=rules.cutout_settlement.output, output: - pixel_area="resources/tmp/pixel_area.nc", - resampled="resources/tmp/resampled_input_{tech}.nc", + pixel_area="resources/automatic/pixel_area_{tech}.nc", + resampled="resources/automatic/resampled_input_{tech}.nc", conda: "../envs/default.yaml" shell: - "python {input.script} {input.shapes_path} {params.specs} {params.suitable_land_cover_types} {input.slope_path} {input.land_cover_path} {input.settlement_path} {output.pixel_area} {output.resampled}" + """ + python "{input.script}" "{input.shapes_path}" "{params.projection}" "{params.resolution}" "{params.suitable_land_cover_types}" "{input.slope_path}" "{input.land_cover_path}" "{input.settlement_path}" "{output.pixel_area}" "{output.resampled}" + """ rule technical_mask: message: @@ -61,7 +64,7 @@ rule technical_mask: pixel_area_path=rules.resample_same_resolution.output.pixel_area, resampled_path=rules.resample_same_resolution.output.resampled, output: - "resources/tmp/technical_masked_{tech}.nc", + "resources/automatic/technical_masked_{tech}.nc", conda: "../envs/default.yaml" shell: diff --git a/workflow/scripts/get_area_potential.py b/workflow/scripts/get_area_potential.py index e0fba37..3de55be 100644 --- a/workflow/scripts/get_area_potential.py +++ b/workflow/scripts/get_area_potential.py @@ -34,6 +34,9 @@ def get_area_potential( + ds_masked["settlement"] * technical_mask["settlement"]["weight"] ) + # remove negative values and values greater than 1 + eligible_fraction = eligible_fraction.clip(0, 1) + # mask out protected area protected_areas = gpd.read_file(protected_area_path) eligible_fraction = eligible_fraction.rio.clip( diff --git a/workflow/scripts/get_suitable_land_cover_types.py b/workflow/scripts/get_suitable_land_cover_types.py index ed2e80f..9e12597 100644 --- a/workflow/scripts/get_suitable_land_cover_types.py +++ b/workflow/scripts/get_suitable_land_cover_types.py @@ -65,7 +65,7 @@ @click.command() @click.argument("land_cover_path", type=str) -@click.argument("suitable_land_cover_types", type=int) +@click.argument("suitable_land_cover_types", type=str, nargs=-1) @click.argument("output_path", type=str) def get_suitable_land_cover_type( land_cover_path, suitable_land_cover_types, output_path @@ -83,7 +83,8 @@ def get_suitable_land_cover_type( # check if each pixel is in the list of suitable land cover types for type in suitable_land_cover_types: suitable_land_cover[type] = (ds_land_cover == type).astype(float) - suitable_land_cover.to_netcdf(output_path) + + suitable_land_cover.to_netcdf(output_path) if __name__ == "__main__": diff --git a/workflow/scripts/resample.py b/workflow/scripts/resample.py index 05be490..c596f2f 100644 --- a/workflow/scripts/resample.py +++ b/workflow/scripts/resample.py @@ -6,6 +6,7 @@ import numpy as np from rasterio.transform import from_bounds from rasterio.enums import Resampling +import yaml def create_empty_geospatial_array( @@ -124,9 +125,10 @@ def determine_pixel_areas(raster_input, bounds, resolution, output_name): @click.command() -@click.argument("shapes_path") -@click.argument("specs") -@click.argument("criteria") +@click.argument("shapes_path", type=str) +@click.argument("projection", type=str) +@click.argument("resolution", type=str) +@click.argument("suitable_land_cover_types", type=str) @click.argument("slope_path", type=str) @click.argument("land_cover_path", type=str) @click.argument("settlement_path", type=str) @@ -134,7 +136,8 @@ def determine_pixel_areas(raster_input, bounds, resolution, output_name): @click.argument("output_path", type=str) def get_same_shape_and_resolution( shapes_path, - specs, + projection, + resolution, suitable_land_cover_types, slope_path, land_cover_path, @@ -151,14 +154,14 @@ def get_same_shape_and_resolution( shapes = gpd.read_parquet(shapes_path) reference_raster = create_empty_geospatial_array( bounds=shapes.total_bounds, - projection=specs["projection"], - resolution=specs["resolution"], + projection=projection, + resolution=resolution, ) pixel_area = determine_pixel_areas( reference_raster, bounds=shapes.total_bounds, - projection=specs["projection"], + projection=projection, ) pixel_area.to_netcdf(output_path_pixel_area) @@ -173,7 +176,8 @@ def get_same_shape_and_resolution( # land cover in fraction ds_land_cover = rxr.open_dataset(land_cover_path) - for land_type, value in suitable_land_cover_types.items(): + suitable_land_cover_types_dict = yaml.safe_load(suitable_land_cover_types) + for land_type, value in suitable_land_cover_types_dict.items(): skip = [] if value == 0: skip.append(land_type) # skip this one From 21eec6073f9b4914bf95c070a4d55a48fa745e2e Mon Sep 17 00:00:00 2001 From: Linh Ho <45103089+LinhHo@users.noreply.github.com> Date: Mon, 30 Jun 2025 16:11:57 +0200 Subject: [PATCH 08/59] Cannot save netcdf AtttributeError --- workflow/rules/process.smk | 4 +++- workflow/scripts/resample.py | 15 ++++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index 11adc6b..302ba3d 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -51,7 +51,9 @@ rule resample_same_resolution: "../envs/default.yaml" shell: """ - python "{input.script}" "{input.shapes_path}" "{params.projection}" "{params.resolution}" "{params.suitable_land_cover_types}" "{input.slope_path}" "{input.land_cover_path}" "{input.settlement_path}" "{output.pixel_area}" "{output.resampled}" + python "{input.script}" "{input.shapes_path}" "{params.projection}" "{params.resolution}" \ + "{params.suitable_land_cover_types}" "{input.slope_path}" "{input.land_cover_path}" "{input.settlement_path}" \ + "{output.pixel_area}" "{output.resampled}" """ rule technical_mask: diff --git a/workflow/scripts/resample.py b/workflow/scripts/resample.py index c596f2f..82aa22a 100644 --- a/workflow/scripts/resample.py +++ b/workflow/scripts/resample.py @@ -90,7 +90,7 @@ def _area_of_pixel(pixel_size, center_lat): return pixel_size / 360.0 * (area_list[0] - area_list[1]) / 1e6 -def determine_pixel_areas(raster_input, bounds, resolution, output_name): +def determine_pixel_areas(raster_input, bounds, resolution): """Returns a raster in which the value corresponds to the area in [m2] of the pixel. based on T.Troendle determine_pixel_areas (utils.py and technically_eligible_area.py) This assumes the data comprises square pixel in WGS84. @@ -127,7 +127,7 @@ def determine_pixel_areas(raster_input, bounds, resolution, output_name): @click.command() @click.argument("shapes_path", type=str) @click.argument("projection", type=str) -@click.argument("resolution", type=str) +@click.argument("resolution", type=float) @click.argument("suitable_land_cover_types", type=str) @click.argument("slope_path", type=str) @click.argument("land_cover_path", type=str) @@ -161,21 +161,24 @@ def get_same_shape_and_resolution( pixel_area = determine_pixel_areas( reference_raster, bounds=shapes.total_bounds, - projection=projection, + resolution=resolution, ) + print("pixel area", pixel_area.dims, pixel_area) pixel_area.to_netcdf(output_path_pixel_area) # Resamples the raster to a specified resolution and projection as the given sample resampled = xr.Dataset() # slope in fraction - ds_slope = rxr.open_dataset(slope_path) + ds_slope = rxr.open_rasterio(slope_path) + ds_slope.rio.write_crs("EPSG:4326", inplace=True) resampled["slope_too_steep"] = ds_slope.astype(float).rio.reproject_match( reference_raster, resampling=Resampling.average ) # land cover in fraction - ds_land_cover = rxr.open_dataset(land_cover_path) + ds_land_cover = rxr.open_rasterio(land_cover_path) + ds_land_cover.rio.write_crs("EPSG:4326", inplace=True) suitable_land_cover_types_dict = yaml.safe_load(suitable_land_cover_types) for land_type, value in suitable_land_cover_types_dict.items(): skip = [] @@ -195,9 +198,11 @@ def get_same_shape_and_resolution( # settlement in sum of area of built-up surface (m2) ds_settlement = rxr.open_rasterio(settlement_path) + ds_settlement.rio.write_crs("EPSG:4326", inplace=True) resampled["settlement"] = ds_settlement.rio.reproject_match( reference_raster, resampling=Resampling.sum ) + print("resampled settlement", resampled.dims, resampled.coords, resampled) resampled.to_netcdf(output_path) From b8671a9f8b532b0a3ffd7bd52e812b70c524bab1 Mon Sep 17 00:00:00 2001 From: Linh Ho <45103089+LinhHo@users.noreply.github.com> Date: Mon, 30 Jun 2025 17:31:11 +0200 Subject: [PATCH 09/59] Coordinates error? --- workflow/rules/process.smk | 9 ++++--- workflow/scripts/apply_technical_mask.py | 33 +++++++++++++++++++----- workflow/scripts/resample.py | 7 ++++- 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index 302ba3d..945c9f4 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -60,17 +60,20 @@ rule technical_mask: message: "Get areas with slope values greater than max_slope, i.e. too steep/not suitable for the tech {wildcards.tech}.", params: - technical_mask=lambda wildcards: config["techs_onshore"][f"{wildcards.tech}"] + suitable_land_cover_types=lambda wildcards: config["techs_onshore"][f"{wildcards.tech}"]["land_cover"], + max_settlement=lambda wildcards: config["techs_onshore"][f"{wildcards.tech}"]["settlement"]["max_settlement"], input: script=workflow.source_path("../scripts/apply_technical_mask.py"), - pixel_area_path=rules.resample_same_resolution.output.pixel_area, resampled_path=rules.resample_same_resolution.output.resampled, + pixel_area_path=rules.resample_same_resolution.output.pixel_area, output: "resources/automatic/technical_masked_{tech}.nc", conda: "../envs/default.yaml" shell: - "python {input.script} {input.resampled_path} {input.pixel_area_path} {params.technical_mask} {output}" + """ + python "{input.script}" "{input.resampled_path}" "{input.pixel_area_path}" "{params.suitable_land_cover_types}" "{params.max_settlement}" "{output}" + """ rule area_potential: message: diff --git a/workflow/scripts/apply_technical_mask.py b/workflow/scripts/apply_technical_mask.py index 8d82a1e..622a9ad 100644 --- a/workflow/scripts/apply_technical_mask.py +++ b/workflow/scripts/apply_technical_mask.py @@ -1,34 +1,53 @@ import click import rioxarray as rxr +import yaml @click.command() @click.argument("resampled_path", type=str) @click.argument("pixel_area_path", type=str) -@click.argument("technical_mask", type=int) +@click.argument("suitable_land_cover_types", type=str) +@click.argument("max_settlement", type=float) @click.argument("output_path", type=str) -def apply_technical_mask(resampled_path, pixel_area_path, technical_mask, output_path): +def apply_technical_mask( + resampled_path, + pixel_area_path, + suitable_land_cover_types, + max_settlement, + output_path, +): ds_resampled = rxr.open_rasterio(resampled_path) + ds_resampled.rio.write_crs("EPSG:4326", inplace=True) + print("ds_resampled before applying technical mask", ds_resampled) # get fraction of settlement (built-up surface) compared to pixel area, both in m2 pixel_area = rxr.open_rasterio(pixel_area_path) + pixel_area.rio.write_crs("EPSG:4326", inplace=True) ds_resampled["settlement"] = ds_resampled["settlement"] / pixel_area # only keep pixel with fraction sum of suitable land cover >= 0.5, # too steep slope < 0.5 # settlement <= max_settlement - suitable_land_cover_types = [ - type != 0 for type in technical_mask["land_cover"].items + suitable_land_cover_types = yaml.safe_load(suitable_land_cover_types) + + land_cover_types = [ + type for type, value in suitable_land_cover_types.items() if value != 0 ] land_cover_mask = ( - ds_resampled[suitable_land_cover_types].to_array().sum(dim="variable") >= 0.5 + ds_resampled[land_cover_types].to_array().sum(dim="variable") >= 0.5 ) combined_mask = ( - (ds_resampled["slope"] < 0.5) + (ds_resampled["slope_too_steep"] < 0.5) & land_cover_mask - & (ds_resampled["settlement"] <= technical_mask["settlement"]["max_settlement"]) + & (ds_resampled["settlement"] <= max_settlement) ) ds_resampled = ds_resampled.where(combined_mask) + print("ds_resampled before saving", ds_resampled) + # remove the attributes from the data_vars to avoid AttributeError: NetCDF: String match to name in use + for v in ds_resampled.data_vars: + print(f"{v}: {ds_resampled[v].attrs}") + ds_resampled[v].attrs = {} + ds_resampled.rio.write_crs("EPSG:4326", inplace=True) ds_resampled.to_netcdf(output_path) diff --git a/workflow/scripts/resample.py b/workflow/scripts/resample.py index 82aa22a..c106792 100644 --- a/workflow/scripts/resample.py +++ b/workflow/scripts/resample.py @@ -183,7 +183,7 @@ def get_same_shape_and_resolution( for land_type, value in suitable_land_cover_types_dict.items(): skip = [] if value == 0: - skip.append(land_type) # skip this one + skip.append(land_type) # skip land cover types with 0 weight else: resampled[land_type] = ds_land_cover[land_type].rio.reproject_match( reference_raster, resampling=Resampling.average @@ -204,6 +204,11 @@ def get_same_shape_and_resolution( ) print("resampled settlement", resampled.dims, resampled.coords, resampled) + # remove the attributes from the data_vars to avoid AttributeError: NetCDF: String match to name in use + for v in resampled.data_vars: + print(f"{v}: {resampled[v].attrs}") + resampled[v].attrs = {} + resampled.rio.write_crs("EPSG:4326", inplace=True) resampled.to_netcdf(output_path) From 84321a21932257a320531bbfcae9a639fd1b52c3 Mon Sep 17 00:00:00 2001 From: Linh Ho <45103089+LinhHo@users.noreply.github.com> Date: Mon, 30 Jun 2025 17:43:24 +0200 Subject: [PATCH 10/59] Update messages in rules --- workflow/rules/process.smk | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index 945c9f4..f9ec493 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -58,7 +58,7 @@ rule resample_same_resolution: rule technical_mask: message: - "Get areas with slope values greater than max_slope, i.e. too steep/not suitable for the tech {wildcards.tech}.", + "Get fraction satisfied all technical criteria: not too steep slope, suitable land cover, and not exceeding max_settlement for the tech {wildcards.tech}.", params: suitable_land_cover_types=lambda wildcards: config["techs_onshore"][f"{wildcards.tech}"]["land_cover"], max_settlement=lambda wildcards: config["techs_onshore"][f"{wildcards.tech}"]["settlement"]["max_settlement"], @@ -77,7 +77,7 @@ rule technical_mask: rule area_potential: message: - "Get areas with slope values greater than max_slope, i.e. too steep/not suitable for the tech {wildcards.tech}.", + "Apply weights, mask out protected area then calculate the potential area for the tech {wildcards.tech}.", params: technical_mask=lambda wildcards: config["techs_onshore"][f"{wildcards.tech}"] input: From 5dea4f2ebb1645e45d9ce9843f57945e0bc7c844 Mon Sep 17 00:00:00 2001 From: Linh Ho <45103089+LinhHo@users.noreply.github.com> Date: Mon, 30 Jun 2025 18:27:33 +0200 Subject: [PATCH 11/59] Fix mixing rasterio and xarray dataset --- workflow/scripts/apply_technical_mask.py | 20 ++++++++---------- workflow/scripts/get_slope_too_steep.py | 3 ++- workflow/scripts/resample.py | 27 ++++++++++-------------- 3 files changed, 22 insertions(+), 28 deletions(-) diff --git a/workflow/scripts/apply_technical_mask.py b/workflow/scripts/apply_technical_mask.py index 622a9ad..7cd5233 100644 --- a/workflow/scripts/apply_technical_mask.py +++ b/workflow/scripts/apply_technical_mask.py @@ -1,5 +1,5 @@ import click -import rioxarray as rxr +import xarray as xr import yaml @@ -16,14 +16,13 @@ def apply_technical_mask( max_settlement, output_path, ): - ds_resampled = rxr.open_rasterio(resampled_path) - ds_resampled.rio.write_crs("EPSG:4326", inplace=True) + ds_resampled = xr.open_dataset(resampled_path, engine="netcdf4") print("ds_resampled before applying technical mask", ds_resampled) # get fraction of settlement (built-up surface) compared to pixel area, both in m2 - pixel_area = rxr.open_rasterio(pixel_area_path) - pixel_area.rio.write_crs("EPSG:4326", inplace=True) - ds_resampled["settlement"] = ds_resampled["settlement"] / pixel_area + pixel_area = xr.open_dataset(pixel_area_path, engine="netcdf4") + ds_resampled["settlement"] = ds_resampled["settlement"] / pixel_area["pixel_area"] + print("ds_resampled after calculating settlement", ds_resampled) # only keep pixel with fraction sum of suitable land cover >= 0.5, # too steep slope < 0.5 @@ -43,11 +42,10 @@ def apply_technical_mask( ) ds_resampled = ds_resampled.where(combined_mask) print("ds_resampled before saving", ds_resampled) - # remove the attributes from the data_vars to avoid AttributeError: NetCDF: String match to name in use - for v in ds_resampled.data_vars: - print(f"{v}: {ds_resampled[v].attrs}") - ds_resampled[v].attrs = {} - ds_resampled.rio.write_crs("EPSG:4326", inplace=True) + # # remove the attributes from the data_vars to avoid AttributeError: NetCDF: String match to name in use + # for v in ds_resampled.data_vars: + # print(f"{v}: {ds_resampled[v].attrs}") + # ds_resampled[v].attrs = {} ds_resampled.to_netcdf(output_path) diff --git a/workflow/scripts/get_slope_too_steep.py b/workflow/scripts/get_slope_too_steep.py index 4fab0f7..eb5df7c 100644 --- a/workflow/scripts/get_slope_too_steep.py +++ b/workflow/scripts/get_slope_too_steep.py @@ -9,7 +9,8 @@ def get_slope_too_steep(slope_path, max_slope, output_path): ds_slope = rxr.open_rasterio(slope_path) is_too_steep_slope = ds_slope > max_slope - is_too_steep_slope.to_netcdf(output_path) + ds_out = is_too_steep_slope.to_dataset(name="slope_too_steep") + ds_out.to_netcdf(output_path) if __name__ == "__main__": diff --git a/workflow/scripts/resample.py b/workflow/scripts/resample.py index c106792..5ce95df 100644 --- a/workflow/scripts/resample.py +++ b/workflow/scripts/resample.py @@ -170,15 +170,15 @@ def get_same_shape_and_resolution( resampled = xr.Dataset() # slope in fraction - ds_slope = rxr.open_rasterio(slope_path) - ds_slope.rio.write_crs("EPSG:4326", inplace=True) - resampled["slope_too_steep"] = ds_slope.astype(float).rio.reproject_match( - reference_raster, resampling=Resampling.average + ds_slope = xr.open_dataset(slope_path, engine="netcdf4") + resampled["slope_too_steep"] = ( + ds_slope["slope_too_steep"] + .astype(float) + .rio.reproject_match(reference_raster, resampling=Resampling.average) ) # land cover in fraction - ds_land_cover = rxr.open_rasterio(land_cover_path) - ds_land_cover.rio.write_crs("EPSG:4326", inplace=True) + ds_land_cover = xr.open_dataset(land_cover_path, engine="netcdf4") suitable_land_cover_types_dict = yaml.safe_load(suitable_land_cover_types) for land_type, value in suitable_land_cover_types_dict.items(): skip = [] @@ -189,11 +189,6 @@ def get_same_shape_and_resolution( reference_raster, resampling=Resampling.average ) - # TEST if it's necessary to clip again with cutout - # # Crops the input raster to the specified geographic bounds - # # from the bounding box of the sample raster - # resampled[land_type] = tmp_var.rio.clip_box(*shapes.total_bounds) - print(f"Skip the land cover types not used in this tech: {skip}") # settlement in sum of area of built-up surface (m2) @@ -204,11 +199,11 @@ def get_same_shape_and_resolution( ) print("resampled settlement", resampled.dims, resampled.coords, resampled) - # remove the attributes from the data_vars to avoid AttributeError: NetCDF: String match to name in use - for v in resampled.data_vars: - print(f"{v}: {resampled[v].attrs}") - resampled[v].attrs = {} - resampled.rio.write_crs("EPSG:4326", inplace=True) + # # remove the attributes from the data_vars to avoid AttributeError: NetCDF: String match to name in use + # for v in resampled.data_vars: + # print(f"{v}: {resampled[v].attrs}") + # resampled[v].attrs = {} + # resampled.rio.write_crs("EPSG:4326", inplace=True) resampled.to_netcdf(output_path) From deace6782f0757066c5bcef851f97ea4adcbc3ac Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Mon, 30 Jun 2025 19:31:26 +0200 Subject: [PATCH 12/59] Get full workflow to run --- workflow/rules/process.smk | 4 +++- workflow/scripts/get_area_potential.py | 16 ++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index f9ec493..f293bf2 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -91,4 +91,6 @@ rule area_potential: conda: "../envs/default.yaml" shell: - "python {input.script} {input.masked_path} {input.pixel_area_path} {params.technical_mask} {input.protected_area_path} {input.shapes_path} {output}" \ No newline at end of file + """ + python "{input.script}" "{input.masked_path}" "{input.pixel_area_path}" "{params.technical_mask}" "{input.protected_area_path}" "{input.shapes_path}" "{output}" + """ diff --git a/workflow/scripts/get_area_potential.py b/workflow/scripts/get_area_potential.py index 3de55be..387b557 100644 --- a/workflow/scripts/get_area_potential.py +++ b/workflow/scripts/get_area_potential.py @@ -1,12 +1,13 @@ import click -import rioxarray as rxr import geopandas as gpd +import xarray as xr +import yaml @click.command() @click.argument("masked_path", type=str) @click.argument("pixel_area_path", type=str) -@click.argument("technical_mask", type=int) +@click.argument("technical_mask", type=str) @click.argument("protected_area_path", type=str) @click.argument("shapes_path", type=str) @click.argument("output_path", type=str) @@ -18,26 +19,29 @@ def get_area_potential( shapes_path, output_path, ): - ds_masked = rxr.open_rasterio(masked_path) + ds_masked = xr.open_dataset(masked_path) + technical_mask = yaml.safe_load(technical_mask) # apply weights suitable_land_cover_types = [] for type, value in technical_mask["land_cover"].items(): if value == 0: continue - suitable_land_cover_types.appends() + suitable_land_cover_types.append(type) ds_masked[type] = ds_masked[type] * value eligible_fraction = ( ds_masked[suitable_land_cover_types].to_array().sum(dim="variable") - - ds_masked["slope"] + - ds_masked["slope_too_steep"] + ds_masked["settlement"] * technical_mask["settlement"]["weight"] ) + eligible_fraction.rio.write_crs("EPSG:4326", inplace=True) # remove negative values and values greater than 1 eligible_fraction = eligible_fraction.clip(0, 1) # mask out protected area + # FIXME: read the right layer(s) and deal with both poly and point layers protected_areas = gpd.read_file(protected_area_path) eligible_fraction = eligible_fraction.rio.clip( protected_areas.geometry, protected_areas.crs, invert=True @@ -45,7 +49,7 @@ def get_area_potential( # multiply pixel area to get area potential # cut with given shape to return raster inside the shape - pixel_area = rxr.open_rasterio(pixel_area_path) + pixel_area = xr.open_dataset(pixel_area_path) shapes = gpd.read_parquet(shapes_path) ds_area_potential = eligible_fraction * pixel_area ds_area_potential = ds_area_potential.rio.clip( From cc930984cb0643d2f12dbcf91d290ec4742c2999 Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Mon, 30 Jun 2025 21:01:08 +0200 Subject: [PATCH 13/59] Cleanup and merge scripts --- config/config.yaml | 11 +- workflow/internal/config.schema.yaml | 16 +- workflow/rules/process.smk | 85 ++++------ workflow/scripts/apply_technical_mask.py | 12 +- workflow/scripts/get_area_potential.py | 5 +- workflow/scripts/get_slope_too_steep.py | 17 -- .../scripts/get_suitable_land_cover_types.py | 91 ----------- workflow/scripts/resample.py | 145 ++++++++++++++---- workflow/scripts/subset_netcdf.py | 19 --- 9 files changed, 155 insertions(+), 246 deletions(-) delete mode 100644 workflow/scripts/get_slope_too_steep.py delete mode 100644 workflow/scripts/get_suitable_land_cover_types.py delete mode 100644 workflow/scripts/subset_netcdf.py diff --git a/config/config.yaml b/config/config.yaml index 424afd2..5d133f8 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -1,7 +1,7 @@ -# Output specifications +# Output specifications specs: projection: EPSG:4326 - resolution: 0.008333 # 30/3600 arcsec in degree, yaml does not handle "/" + resolution: 0.008333 # 30/3600 arcsec in degrees # Technical criteria techs_onshore: @@ -15,7 +15,7 @@ techs_onshore: settlement: max_settlement: 0.1 # no PV open field in settlement weight: -1 - wind_onshore: + wind_onshore: max_slope: 20 land_cover: FARM: 0.2 @@ -35,5 +35,6 @@ techs_onshore: settlement: max_settlement: 1 weight: 0.8 -# techs_offshore: -# wind_offshore: \ No newline at end of file + +techs_offshore: + wind_offshore: diff --git a/workflow/internal/config.schema.yaml b/workflow/internal/config.schema.yaml index 9a7bd13..196d5ac 100644 --- a/workflow/internal/config.schema.yaml +++ b/workflow/internal/config.schema.yaml @@ -35,16 +35,8 @@ properties: additionalProperties: false required: [max_slope, land_cover, settlement] additionalProperties: false -required: [techs_onshore] -# properties: -# techs: -# type: object -# additionalProperties: -# type: object -# additionalProperties: false -# properties: -# max_slope: -# type: number -# required: [max_slope] -# required: [techs] + techs_offshore: + type: object + +required: [techs_onshore] diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index f293bf2..e44f46a 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -1,96 +1,65 @@ wildcard_constraints: - tech="|".join(config["techs_onshore"].keys()) + tech_onshore="|".join(config["techs_onshore"].keys()), + tech_offshore="|".join(config["techs_offshore"].keys()), -rule slope_too_steep: +rule resample_same_resolution_onshore: message: - "Get areas with slope values greater than max_slope, i.e. too steep/not suitable for the tech {wildcards.tech}.", - params: - max_slope=lambda wildcards: config["techs_onshore"][f"{wildcards.tech}"]["max_slope"], - input: - script=workflow.source_path("../scripts/get_slope_too_steep.py"), - shapes=rules.download_cutout_slope.output, - output: - "resources/automatic/slope_too_steep_{tech}.nc", - conda: - "../envs/default.yaml" - shell: - "python {input.script} {input.shapes} {params.max_slope} {output}" - -rule suitable_land_cover: - message: - "Get suitable land cover types for the tech {wildcards.tech}.", - params: - suitable_land_cover_types=lambda wildcards: list(config["techs_onshore"][f"{wildcards.tech}"]["land_cover"].keys()), - input: - script=workflow.source_path("../scripts/get_suitable_land_cover_types.py"), - shapes=rules.cutout_landcover.output, - output: - "resources/automatic/suitable_land_cover_{tech}.nc", - conda: - "../envs/default.yaml" - shell: - "python {input.script} {input.shapes} {params.suitable_land_cover_types} {output}" - -rule resample_same_resolution: - message: - "Resample slope, land cover, and settlement to the same resolution for the tech {wildcards.tech}.", + "Resample slope, land cover (subset to suitable types), and settlement to the same resolution for the tech {wildcards.tech_onshore}.", params: projection=config["specs"]["projection"], resolution=config["specs"]["resolution"], - suitable_land_cover_types=lambda wildcards: config["techs_onshore"][f"{wildcards.tech}"]["land_cover"], + suitable_land_cover_types=lambda wildcards: config["techs_onshore"][f"{wildcards.tech_onshore}"]["land_cover"], + max_slope=lambda wildcards: config["techs_onshore"][f"{wildcards.tech_onshore}"]["max_slope"], input: script=workflow.source_path("../scripts/resample.py"), - shapes_path="resources/user/shapes.parquet", - slope_path=rules.slope_too_steep.output, - land_cover_path=rules.suitable_land_cover.output, + shapes="resources/user/shapes.parquet", + slope_path=rules.download_cutout_slope.output, + land_cover_path=rules.cutout_landcover.output, settlement_path=rules.cutout_settlement.output, output: - pixel_area="resources/automatic/pixel_area_{tech}.nc", - resampled="resources/automatic/resampled_input_{tech}.nc", + "resources/resampled_input_{tech_onshore}.nc", conda: "../envs/default.yaml" shell: """ - python "{input.script}" "{input.shapes_path}" "{params.projection}" "{params.resolution}" \ - "{params.suitable_land_cover_types}" "{input.slope_path}" "{input.land_cover_path}" "{input.settlement_path}" \ - "{output.pixel_area}" "{output.resampled}" + python "{input.script}" "{input.shapes}" "{params.projection}" "{params.resolution}" \ + "{params.suitable_land_cover_types}" "{input.slope_path}" "{input.land_cover_path}" "{input.settlement_path}" "{params.max_slope}" \ + "{output}" """ -rule technical_mask: +rule technical_mask_onshore: message: - "Get fraction satisfied all technical criteria: not too steep slope, suitable land cover, and not exceeding max_settlement for the tech {wildcards.tech}.", + "Get fraction satisfied all technical criteria: not too steep slope, suitable land cover, and not exceeding max_settlement for the tech {wildcards.tech_onshore}.", params: - suitable_land_cover_types=lambda wildcards: config["techs_onshore"][f"{wildcards.tech}"]["land_cover"], - max_settlement=lambda wildcards: config["techs_onshore"][f"{wildcards.tech}"]["settlement"]["max_settlement"], + suitable_land_cover_types=lambda wildcards: config["techs_onshore"][f"{wildcards.tech_onshore}"]["land_cover"], + max_settlement=lambda wildcards: config["techs_onshore"][f"{wildcards.tech_onshore}"]["settlement"]["max_settlement"], input: script=workflow.source_path("../scripts/apply_technical_mask.py"), - resampled_path=rules.resample_same_resolution.output.resampled, - pixel_area_path=rules.resample_same_resolution.output.pixel_area, + resampled_path=rules.resample_same_resolution_onshore.output, output: - "resources/automatic/technical_masked_{tech}.nc", + "resources/technical_mask_{tech_onshore}.nc", conda: "../envs/default.yaml" shell: """ - python "{input.script}" "{input.resampled_path}" "{input.pixel_area_path}" "{params.suitable_land_cover_types}" "{params.max_settlement}" "{output}" + python "{input.script}" "{input.resampled_path}" "{params.suitable_land_cover_types}" "{params.max_settlement}" "{output}" """ -rule area_potential: +rule area_potential_onshore: message: - "Apply weights, mask out protected area then calculate the potential area for the tech {wildcards.tech}.", + "Apply weights, mask out protected area then calculate the potential area for the tech {wildcards.tech_onshore}.", params: - technical_mask=lambda wildcards: config["techs_onshore"][f"{wildcards.tech}"] + technical_mask=lambda wildcards: config["techs_onshore"][f"{wildcards.tech_onshore}"] input: script=workflow.source_path("../scripts/get_area_potential.py"), - masked_path=rules.technical_mask.output, - pixel_area_path=rules.resample_same_resolution.output.pixel_area, + shapes="resources/user/shapes.parquet", + masked_path=rules.technical_mask_onshore.output, protected_area_path=rules.unzip_wdpa.output, - shapes_path="resources/user/shapes.parquet", output: - "results/area_potential_{tech}.nc", + "results/area_potential_{tech_onshore}.nc", conda: "../envs/default.yaml" shell: """ - python "{input.script}" "{input.masked_path}" "{input.pixel_area_path}" "{params.technical_mask}" "{input.protected_area_path}" "{input.shapes_path}" "{output}" + python "{input.script}" "{input.masked_path}" "{params.technical_mask}" "{input.protected_area_path}" "{input.shapes}" "{output}" """ diff --git a/workflow/scripts/apply_technical_mask.py b/workflow/scripts/apply_technical_mask.py index 7cd5233..622364e 100644 --- a/workflow/scripts/apply_technical_mask.py +++ b/workflow/scripts/apply_technical_mask.py @@ -5,13 +5,11 @@ @click.command() @click.argument("resampled_path", type=str) -@click.argument("pixel_area_path", type=str) @click.argument("suitable_land_cover_types", type=str) @click.argument("max_settlement", type=float) @click.argument("output_path", type=str) def apply_technical_mask( resampled_path, - pixel_area_path, suitable_land_cover_types, max_settlement, output_path, @@ -20,8 +18,7 @@ def apply_technical_mask( print("ds_resampled before applying technical mask", ds_resampled) # get fraction of settlement (built-up surface) compared to pixel area, both in m2 - pixel_area = xr.open_dataset(pixel_area_path, engine="netcdf4") - ds_resampled["settlement"] = ds_resampled["settlement"] / pixel_area["pixel_area"] + ds_resampled["settlement"] = ds_resampled["settlement"] / ds_resampled["pixel_area"] print("ds_resampled after calculating settlement", ds_resampled) # only keep pixel with fraction sum of suitable land cover >= 0.5, @@ -35,17 +32,14 @@ def apply_technical_mask( land_cover_mask = ( ds_resampled[land_cover_types].to_array().sum(dim="variable") >= 0.5 ) + combined_mask = ( - (ds_resampled["slope_too_steep"] < 0.5) + (ds_resampled["slope_too_steep"] <= 0.5) & land_cover_mask & (ds_resampled["settlement"] <= max_settlement) ) ds_resampled = ds_resampled.where(combined_mask) print("ds_resampled before saving", ds_resampled) - # # remove the attributes from the data_vars to avoid AttributeError: NetCDF: String match to name in use - # for v in ds_resampled.data_vars: - # print(f"{v}: {ds_resampled[v].attrs}") - # ds_resampled[v].attrs = {} ds_resampled.to_netcdf(output_path) diff --git a/workflow/scripts/get_area_potential.py b/workflow/scripts/get_area_potential.py index 387b557..61c9407 100644 --- a/workflow/scripts/get_area_potential.py +++ b/workflow/scripts/get_area_potential.py @@ -6,14 +6,12 @@ @click.command() @click.argument("masked_path", type=str) -@click.argument("pixel_area_path", type=str) @click.argument("technical_mask", type=str) @click.argument("protected_area_path", type=str) @click.argument("shapes_path", type=str) @click.argument("output_path", type=str) def get_area_potential( masked_path, - pixel_area_path, technical_mask, protected_area_path, shapes_path, @@ -49,9 +47,8 @@ def get_area_potential( # multiply pixel area to get area potential # cut with given shape to return raster inside the shape - pixel_area = xr.open_dataset(pixel_area_path) shapes = gpd.read_parquet(shapes_path) - ds_area_potential = eligible_fraction * pixel_area + ds_area_potential = eligible_fraction * ds_masked["pixel_area"] ds_area_potential = ds_area_potential.rio.clip( shapes.geometry, shapes.crs, invert=False ) diff --git a/workflow/scripts/get_slope_too_steep.py b/workflow/scripts/get_slope_too_steep.py deleted file mode 100644 index eb5df7c..0000000 --- a/workflow/scripts/get_slope_too_steep.py +++ /dev/null @@ -1,17 +0,0 @@ -import click -import rioxarray as rxr - - -@click.command() -@click.argument("slope_path", type=str) -@click.argument("max_slope", type=int) -@click.argument("output_path", type=str) -def get_slope_too_steep(slope_path, max_slope, output_path): - ds_slope = rxr.open_rasterio(slope_path) - is_too_steep_slope = ds_slope > max_slope - ds_out = is_too_steep_slope.to_dataset(name="slope_too_steep") - ds_out.to_netcdf(output_path) - - -if __name__ == "__main__": - get_slope_too_steep() diff --git a/workflow/scripts/get_suitable_land_cover_types.py b/workflow/scripts/get_suitable_land_cover_types.py deleted file mode 100644 index 9e12597..0000000 --- a/workflow/scripts/get_suitable_land_cover_types.py +++ /dev/null @@ -1,91 +0,0 @@ -import click -import rioxarray as rxr -import xarray as xr -import numpy as np - - -# LAND COVER -# Original classification categories taken from GlobCover 2009 land cover. -# From Troendle et al. (2019) https://github.com/timtroendle/possibility-for-electricity-autarky -# suitable land cover types are defined in config.yaml, as 1, other types are 0 - - -GlobCover = { - 11: "POST_FLOODING", - 14: "RAINFED_CROPLANDS", - 20: "MOSAIC_CROPLAND", - 30: "MOSAIC_VEGETATION", - 40: "CLOSED_TO_OPEN_BROADLEAVED_FOREST", - 50: "CLOSED_BROADLEAVED_FOREST", - 60: "OPEN_BROADLEAVED_FOREST", - 70: "CLOSED_NEEDLELEAVED_FOREST", - 90: "OPEN_NEEDLELEAVED_FOREST", - 100: "CLOSED_TO_OPEN_MIXED_FOREST", - 110: "MOSAIC_FOREST", - 120: "MOSAIC_GRASSLAND", - 130: "CLOSED_TO_OPEN_SHRUBLAND", - 140: "CLOSED_TO_OPEN_HERBS", - 150: "SPARSE_VEGETATION", - 160: "CLOSED_TO_OPEN_REGULARLY_FLOODED_FOREST", # doesn't exist in Europe - 170: "CLOSED_REGULARLY_FLOODED_FOREST", # doesn't exist in Europe - 180: "CLOSED_TO_OPEN_REGULARLY_FLOODED_GRASSLAND", # roughly 2.3% of land in Europe - 190: "ARTIFICAL_SURFACES_AND_URBAN_AREAS", - 200: "BARE_AREAS", - 210: "WATER_BODIES", - 220: "PERMANENT_SNOW", - 230: "NO_DATA", -} - -CoverType = { - "POST_FLOODING": "FARM", - "RAINFED_CROPLANDS": "FARM", - "MOSAIC_CROPLAND": "FARM", - "MOSAIC_VEGETATION": "FARM", - "CLOSED_TO_OPEN_BROADLEAVED_FOREST": "FOREST", - "CLOSED_BROADLEAVED_FOREST": "FOREST", - "OPEN_BROADLEAVED_FOREST": "FOREST", - "CLOSED_NEEDLELEAVED_FOREST": "FOREST", - "OPEN_NEEDLELEAVED_FOREST": "FOREST", - "CLOSED_TO_OPEN_MIXED_FOREST": "FOREST", - "MOSAIC_FOREST": "FOREST", - "CLOSED_TO_OPEN_REGULARLY_FLOODED_FOREST": "FOREST", - "CLOSED_REGULARLY_FLOODED_FOREST": "FOREST", - "MOSAIC_GRASSLAND": "OTHER", # vegetation - "CLOSED_TO_OPEN_SHRUBLAND": "OTHER", # vegetation - "CLOSED_TO_OPEN_HERBS": "OTHER", # vegetation - "SPARSE_VEGETATION": "OTHER", # vegetation - "CLOSED_TO_OPEN_REGULARLY_FLOODED_GRASSLAND": "OTHER", # vegetation - "BARE_AREAS": "OTHER", - "ARTIFICAL_SURFACES_AND_URBAN_AREAS": "URBAN", - "WATER_BODIES": "WATER", - "PERMANENT_SNOW": "NA", - "NO_DATA": "NA", -} - - -@click.command() -@click.argument("land_cover_path", type=str) -@click.argument("suitable_land_cover_types", type=str, nargs=-1) -@click.argument("output_path", type=str) -def get_suitable_land_cover_type( - land_cover_path, suitable_land_cover_types, output_path -): - ds_land_cover = rxr.open_rasterio(land_cover_path) - suitable_land_cover = xr.Dataset(coords=ds_land_cover.coords) - - # convert the input value to land cover type of interest - for value in np.unique(ds_land_cover.data): - if value in GlobCover: - ds_land_cover = ds_land_cover.where( - ds_land_cover != value, other=CoverType[GlobCover[value]], drop=False - ) - - # check if each pixel is in the list of suitable land cover types - for type in suitable_land_cover_types: - suitable_land_cover[type] = (ds_land_cover == type).astype(float) - - suitable_land_cover.to_netcdf(output_path) - - -if __name__ == "__main__": - get_suitable_land_cover_type() diff --git a/workflow/scripts/resample.py b/workflow/scripts/resample.py index 5ce95df..57c0de8 100644 --- a/workflow/scripts/resample.py +++ b/workflow/scripts/resample.py @@ -1,12 +1,88 @@ -import click import math + +import click import geopandas as gpd +import numpy as np import rioxarray as rxr import xarray as xr -import numpy as np -from rasterio.transform import from_bounds -from rasterio.enums import Resampling import yaml +from rasterio.enums import Resampling +from rasterio.transform import from_bounds + +# LAND COVER +# Original classification categories taken from GlobCover 2009 land cover. +# From Troendle et al. (2019) https://github.com/timtroendle/possibility-for-electricity-autarky +# suitable land cover types are defined in config.yaml, as 1, other types are 0 + + +GlobCover = { + 11: "POST_FLOODING", + 14: "RAINFED_CROPLANDS", + 20: "MOSAIC_CROPLAND", + 30: "MOSAIC_VEGETATION", + 40: "CLOSED_TO_OPEN_BROADLEAVED_FOREST", + 50: "CLOSED_BROADLEAVED_FOREST", + 60: "OPEN_BROADLEAVED_FOREST", + 70: "CLOSED_NEEDLELEAVED_FOREST", + 90: "OPEN_NEEDLELEAVED_FOREST", + 100: "CLOSED_TO_OPEN_MIXED_FOREST", + 110: "MOSAIC_FOREST", + 120: "MOSAIC_GRASSLAND", + 130: "CLOSED_TO_OPEN_SHRUBLAND", + 140: "CLOSED_TO_OPEN_HERBS", + 150: "SPARSE_VEGETATION", + 160: "CLOSED_TO_OPEN_REGULARLY_FLOODED_FOREST", # doesn't exist in Europe + 170: "CLOSED_REGULARLY_FLOODED_FOREST", # doesn't exist in Europe + 180: "CLOSED_TO_OPEN_REGULARLY_FLOODED_GRASSLAND", # roughly 2.3% of land in Europe + 190: "ARTIFICAL_SURFACES_AND_URBAN_AREAS", + 200: "BARE_AREAS", + 210: "WATER_BODIES", + 220: "PERMANENT_SNOW", + 230: "NO_DATA", +} + +CoverType = { + "POST_FLOODING": "FARM", + "RAINFED_CROPLANDS": "FARM", + "MOSAIC_CROPLAND": "FARM", + "MOSAIC_VEGETATION": "FARM", + "CLOSED_TO_OPEN_BROADLEAVED_FOREST": "FOREST", + "CLOSED_BROADLEAVED_FOREST": "FOREST", + "OPEN_BROADLEAVED_FOREST": "FOREST", + "CLOSED_NEEDLELEAVED_FOREST": "FOREST", + "OPEN_NEEDLELEAVED_FOREST": "FOREST", + "CLOSED_TO_OPEN_MIXED_FOREST": "FOREST", + "MOSAIC_FOREST": "FOREST", + "CLOSED_TO_OPEN_REGULARLY_FLOODED_FOREST": "FOREST", + "CLOSED_REGULARLY_FLOODED_FOREST": "FOREST", + "MOSAIC_GRASSLAND": "OTHER", # vegetation + "CLOSED_TO_OPEN_SHRUBLAND": "OTHER", # vegetation + "CLOSED_TO_OPEN_HERBS": "OTHER", # vegetation + "SPARSE_VEGETATION": "OTHER", # vegetation + "CLOSED_TO_OPEN_REGULARLY_FLOODED_GRASSLAND": "OTHER", # vegetation + "BARE_AREAS": "OTHER", + "ARTIFICAL_SURFACES_AND_URBAN_AREAS": "URBAN", + "WATER_BODIES": "WATER", + "PERMANENT_SNOW": "NA", + "NO_DATA": "NA", +} + + +def get_suitable_land_cover_type(ds_land_cover, suitable_land_cover_types): + suitable_land_cover = xr.Dataset(coords=ds_land_cover.coords) + + # convert the input value to land cover type of interest + for value in np.unique(ds_land_cover.data): + if value in GlobCover: + ds_land_cover = ds_land_cover.where( + ds_land_cover != value, other=CoverType[GlobCover[value]], drop=False + ) + + # check if each pixel is in the list of suitable land cover types + for type in suitable_land_cover_types: + suitable_land_cover[type] = (ds_land_cover == type).astype(float) + + return suitable_land_cover def create_empty_geospatial_array( @@ -14,8 +90,7 @@ def create_empty_geospatial_array( resolution, projection, ): - """ - Create an empty geospatial array with specified resolution, projection, and bounds. + """Create an empty geospatial array with specified resolution, projection, and bounds. Args: bounds (tuple): Bounds of the array (minx, miny, maxx, maxy). @@ -91,7 +166,9 @@ def _area_of_pixel(pixel_size, center_lat): def determine_pixel_areas(raster_input, bounds, resolution): - """Returns a raster in which the value corresponds to the area in [m2] of the pixel. + """Determine area of each pixel. + + Returns a raster in which the value corresponds to the area in [m2] of the pixel. based on T.Troendle determine_pixel_areas (utils.py and technically_eligible_area.py) This assumes the data comprises square pixel in WGS84. @@ -132,7 +209,7 @@ def determine_pixel_areas(raster_input, bounds, resolution): @click.argument("slope_path", type=str) @click.argument("land_cover_path", type=str) @click.argument("settlement_path", type=str) -@click.argument("output_path_pixel_area", type=str) +@click.argument("max_slope", type=float) @click.argument("output_path", type=str) def get_same_shape_and_resolution( shapes_path, @@ -142,13 +219,14 @@ def get_same_shape_and_resolution( slope_path, land_cover_path, settlement_path, - output_path_pixel_area, + max_slope, output_path, ): - """ - Resample and crop the raster_input - to have the same bounds, projection, and resolution as the reference_raster - reproject_match ensures all rasters have the same bounds (minlon, minlat, maxlon, maxlat) + """Resample and crop the raster_input. + + Goal is to have the same bounds, projection, and resolution as the reference_raster: + reproject_match ensures all rasters have the same bounds + (minlon, minlat, maxlon, maxlat) """ # create reference raster with the same bounds as given shapes shapes = gpd.read_parquet(shapes_path) @@ -157,30 +235,40 @@ def get_same_shape_and_resolution( projection=projection, resolution=resolution, ) + resampled = xr.Dataset() pixel_area = determine_pixel_areas( reference_raster, bounds=shapes.total_bounds, resolution=resolution, ) - print("pixel area", pixel_area.dims, pixel_area) - pixel_area.to_netcdf(output_path_pixel_area) - - # Resamples the raster to a specified resolution and projection as the given sample - resampled = xr.Dataset() + print(f"Pixel area: {pixel_area.dims}, {pixel_area}") + resampled["pixel_area"] = pixel_area # slope in fraction - ds_slope = xr.open_dataset(slope_path, engine="netcdf4") - resampled["slope_too_steep"] = ( - ds_slope["slope_too_steep"] - .astype(float) - .rio.reproject_match(reference_raster, resampling=Resampling.average) + da_slope = rxr.open_rasterio(slope_path) + + slope_too_steep = da_slope > max_slope + + resampled["slope"] = da_slope.astype(float).rio.reproject_match( + reference_raster, resampling=Resampling.average + ) + + resampled["slope_too_steep"] = slope_too_steep.astype(float).rio.reproject_match( + reference_raster, resampling=Resampling.average + ) + + ## + # Land cover + ## + suitable_land_cover_types = yaml.safe_load(suitable_land_cover_types) + land_cover = ds_land_cover = rxr.open_rasterio(land_cover_path) + ds_land_cover = get_suitable_land_cover_type( + land_cover, suitable_land_cover_types.keys() ) # land cover in fraction - ds_land_cover = xr.open_dataset(land_cover_path, engine="netcdf4") - suitable_land_cover_types_dict = yaml.safe_load(suitable_land_cover_types) - for land_type, value in suitable_land_cover_types_dict.items(): + for land_type, value in suitable_land_cover_types.items(): skip = [] if value == 0: skip.append(land_type) # skip land cover types with 0 weight @@ -199,11 +287,6 @@ def get_same_shape_and_resolution( ) print("resampled settlement", resampled.dims, resampled.coords, resampled) - # # remove the attributes from the data_vars to avoid AttributeError: NetCDF: String match to name in use - # for v in resampled.data_vars: - # print(f"{v}: {resampled[v].attrs}") - # resampled[v].attrs = {} - # resampled.rio.write_crs("EPSG:4326", inplace=True) resampled.to_netcdf(output_path) diff --git a/workflow/scripts/subset_netcdf.py b/workflow/scripts/subset_netcdf.py deleted file mode 100644 index d6a36ef..0000000 --- a/workflow/scripts/subset_netcdf.py +++ /dev/null @@ -1,19 +0,0 @@ -import click -import geopandas as gpd -import xarray as xr - - -@click.command() -@click.argument("shapes_path") -@click.argument("netcdf_path") -@click.argument("output_path") -def subset_netcdf(shapes_path, netcdf_path, output_path): - shapes = gpd.read_parquet(shapes_path) - minlon, minlat, maxlon, maxlat = shapes.total_bounds - opendap_ds = xr.open_dataset(netcdf_path) - subset = opendap_ds.sel(lat=slice(minlat, maxlat), lon=slice(minlon, maxlon)) - subset.to_netcdf(output_path) - - -if __name__ == "__main__": - subset_netcdf() From a2fa551d7efeee2e1fa1843c128ce6106d49c346 Mon Sep 17 00:00:00 2001 From: Linh Ho <45103089+LinhHo@users.noreply.github.com> Date: Wed, 2 Jul 2025 14:25:03 +0200 Subject: [PATCH 14/59] Add wind offshore --- config/config.yaml | 4 + workflow/internal/config.schema.yaml | 13 ++- workflow/rules/process.smk | 25 ++++++ workflow/scripts/wind_offshore.py | 117 +++++++++++++++++++++++++++ 4 files changed, 158 insertions(+), 1 deletion(-) create mode 100644 workflow/scripts/wind_offshore.py diff --git a/config/config.yaml b/config/config.yaml index 5d133f8..a3b5a99 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -38,3 +38,7 @@ techs_onshore: techs_offshore: wind_offshore: + water_depth: + max: 0 + min: -50 + weight: 0.8 diff --git a/workflow/internal/config.schema.yaml b/workflow/internal/config.schema.yaml index 196d5ac..a0dae55 100644 --- a/workflow/internal/config.schema.yaml +++ b/workflow/internal/config.schema.yaml @@ -37,6 +37,17 @@ properties: additionalProperties: false techs_offshore: + type: object + additionalProperties: type: object + properties: + water_depth: + type: object + additionalProperties: + type: number + weight: + type: number + required: [water_depth, weight] + additionalProperties: false -required: [techs_onshore] +required: [techs_onshore, techs_offshore] diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index e44f46a..d6603ad 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -63,3 +63,28 @@ rule area_potential_onshore: """ python "{input.script}" "{input.masked_path}" "{params.technical_mask}" "{input.protected_area_path}" "{input.shapes}" "{output}" """ + +rule area_potential_offshore: + message: + "Get area potential for the tech {wildcards.tech_offshore}" + params: + projection=config["specs"]["projection"], + resolution=config["specs"]["resolution"], + water_depth=lambda wildcards: config["techs_offshore"][f"{wildcards.tech_offshore}"]["water_depth"], + weight=lambda wildcards: config["techs_offshore"][f"{wildcards.tech_offshore}"]["weight"], + input: + script=workflow.source_path("../scripts/wind_offshore.py"), + shapes="resources/user/shapes.parquet", + bathymetry_path=rules.download_cutout_bathymetry.output, + land_sea_mask_path=rules.cutout_landseamask.output, + protected_area_path=rules.unzip_wdpa.output, + output: + "results/area_potential_{tech_offshore}.nc", + conda: + "../envs/default.yaml" + shell: + """ + python "{input.script}" "{input.shapes}" "{params.projection}" "{params.resolution}" \ + "{input.bathymetry_path}" "{params.water_depth}" "{input.land_sea_mask_path}" \ + "{input.protected_area_path}" "{params.weight}" "{output}" + """ \ No newline at end of file diff --git a/workflow/scripts/wind_offshore.py b/workflow/scripts/wind_offshore.py new file mode 100644 index 0000000..8ac2cb3 --- /dev/null +++ b/workflow/scripts/wind_offshore.py @@ -0,0 +1,117 @@ +import click +import yaml +import geopandas as gpd +import xarray as xr +import rioxarray as rxr +from rasterio.enums import Resampling +from rasterio.features import rasterize + +from resample import create_empty_geospatial_array, determine_pixel_areas + + +@click.command() +@click.argument("shapes_path", type=str) +@click.argument("projection", type=str) +@click.argument("resolution", type=float) +@click.argument("bathymetry_path", type=str) +@click.argument("water_depth", type=str) +@click.argument("land_sea_mask_path", type=str) +@click.argument("protected_area_path", type=str) +@click.argument("weight", type=float) +@click.argument("output_path", type=str) +def area_potential_wind_offshore( + shapes_path, + projection, + resolution, + bathymetry_path, + water_depth, + land_sea_mask_path, + protected_area_path, + weight, + output_path, +): + """Get area potential for wind offshore technology. + + Steps: + - Resample bathymetry and land-sea mask to the same bounds and resolution as the reference raster. + - Mask out to get only bathymetry within water depth range + - Mask out land areas, buffer 10 km from country shape, inside geo-boundaries + - Convert to area potenital in m2 + + """ + # resample to the same bounds and resolution as the reference raster + # create reference raster with the same bounds as given shapes + shapes = gpd.read_parquet(shapes_path) + reference_raster = create_empty_geospatial_array( + bounds=shapes.total_bounds, + projection=projection, + resolution=resolution, + ) + resampled = xr.Dataset() + + pixel_area = determine_pixel_areas( + reference_raster, + bounds=shapes.total_bounds, + resolution=resolution, + ) + print(f"Pixel area: {pixel_area.dims}, {pixel_area}") + resampled["pixel_area"] = pixel_area + + # get bathymetry within the water depth range then resample + water_depth = yaml.safe_load(water_depth) + ds_bathymetry = rxr.open_rasterio(bathymetry_path) + masked_bathymetry = ( + (ds_bathymetry < water_depth["max"]) & (ds_bathymetry >= water_depth["min"]) + ).astype(float) + + resampled["bathymetry"] = masked_bathymetry.rio.reproject_match( + reference_raster, resampling=Resampling.average + ) + + # keep only bathymetry in sea area using land sea mask + ds_land_sea_mask = rxr.open_rasterio(land_sea_mask_path) + ds_land_sea_mask = ds_land_sea_mask.rio.reproject_match( + ds_bathymetry, resampling=Resampling.mode + ) + resampled["masked_bathymetry"] = masked_bathymetry.where( + ds_land_sea_mask == 1, other=0 + ) + + print(f"Resampled bathymetry and land sea mask: {resampled.dims}, {resampled}") + + # keep bathymetry inside geo boundaries, i.e. within the exclusive economic zone (EEZ) + eligible_fraction = resampled["bathymetry"].rio.clip( + shapes.geometry, shapes.crs, invert=False + ) + + # Buffer 10 km from country shape, no wind offshore too close to the coastline + # Project to a metric CRS equal area EPSG:6933 for buffering + # Note: a UTM zone specific would be more accurate (e.g., UTM or EPSG:32648) but it varies by country + shapes_land = shapes[shapes["shape_class"] == "land"] + sea_buffer = shapes_land.to_crs(epsg=6933).buffer(10_000) # 10 km buffer outward + buffer_geo = gpd.GeoDataFrame(geometry=sea_buffer).to_crs(pixel_area.rio.crs) + eligible_fraction = eligible_fraction.rio.clip( + buffer_geo.geometry, buffer_geo.crs, invert=True + ) + + # # mask out protected area + # # FIXME: read the right layer(s) and deal with both poly and point layers + # protected_areas = gpd.read_file(protected_area_path) + # # files = list(path_protected_area.glob("*_shp_?/*_shp-polygons.shp")) + # # gdfs = (gpd.read_file(file) for file in files) # generator + # # protected_areas = gpd.GeoDataFrame(pd.concat(gdfs, ignore_index=True)) + + # eligible_fraction = eligible_fraction.rio.clip( + # protected_areas.geometry, protected_areas.crs, invert=True + # ) + + # apply weight, then multiply pixel area to get area potential + ds_area_potential = xr.Dataset( + {"wind_offshore": eligible_fraction * weight * resampled["pixel_area"]}, + coords=resampled["pixel_area"].coords, + ) + ds_area_potential.to_netcdf(output_path) + + +if __name__ == "__main__": + area_potential_wind_offshore() From 08e39f16f3c739b09ec1b65063dd084bae579dd1 Mon Sep 17 00:00:00 2001 From: Linh Ho <45103089+LinhHo@users.noreply.github.com> Date: Wed, 2 Jul 2025 14:40:15 +0200 Subject: [PATCH 15/59] Add wind offshore --- workflow/scripts/wind_offshore.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/workflow/scripts/wind_offshore.py b/workflow/scripts/wind_offshore.py index 8ac2cb3..55f7eb8 100644 --- a/workflow/scripts/wind_offshore.py +++ b/workflow/scripts/wind_offshore.py @@ -69,11 +69,12 @@ def area_potential_wind_offshore( ) # keep only bathymetry in sea area using land sea mask + # NOTE: maybe remove this step since we already include EEZ outside buffer 10 km from coastline ds_land_sea_mask = rxr.open_rasterio(land_sea_mask_path) ds_land_sea_mask = ds_land_sea_mask.rio.reproject_match( - ds_bathymetry, resampling=Resampling.mode + pixel_area, resampling=Resampling.mode ) - resampled["masked_bathymetry"] = masked_bathymetry.where( + resampled["bathymetry"] = resampled["bathymetry"].where( ds_land_sea_mask == 1, other=0 ) From 2c7acc710ccd9ec9ba809ddaa0118679fed79e45 Mon Sep 17 00:00:00 2001 From: Linh Ho <45103089+LinhHo@users.noreply.github.com> Date: Wed, 2 Jul 2025 14:47:18 +0200 Subject: [PATCH 16/59] Remove land sea mask --- workflow/rules/prepare.smk | 14 -------------- workflow/rules/process.smk | 4 +--- workflow/scripts/wind_offshore.py | 12 ------------ 3 files changed, 1 insertion(+), 29 deletions(-) diff --git a/workflow/rules/prepare.smk b/workflow/rules/prepare.smk index 6525722..b011deb 100644 --- a/workflow/rules/prepare.smk +++ b/workflow/rules/prepare.smk @@ -15,20 +15,6 @@ rule cutout_landcover: rio clip --overwrite "{input.landcover}" "{output}" --bounds "$(fio info '{input.shapes}' --bounds)" """ -rule cutout_landseamask: - message: - "Cut land seamask data to the bounds of the input shapefile." - input: - shapes="resources/user/shapes.parquet", - landseamask=rules.unzip_globcover.output.landseamask, - output: - "resources/cutout/landseamask.tif", - conda: - "../envs/default.yaml" - shell: - """ - rio clip --overwrite "{input.landseamask}" "{output}" --bounds "$(fio info '{input.shapes}' --bounds)" - """ rule cutout_settlement: message: diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index d6603ad..b9f10b8 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -76,7 +76,6 @@ rule area_potential_offshore: script=workflow.source_path("../scripts/wind_offshore.py"), shapes="resources/user/shapes.parquet", bathymetry_path=rules.download_cutout_bathymetry.output, - land_sea_mask_path=rules.cutout_landseamask.output, protected_area_path=rules.unzip_wdpa.output, output: "results/area_potential_{tech_offshore}.nc", @@ -85,6 +84,5 @@ rule area_potential_offshore: shell: """ python "{input.script}" "{input.shapes}" "{params.projection}" "{params.resolution}" \ - "{input.bathymetry_path}" "{params.water_depth}" "{input.land_sea_mask_path}" \ - "{input.protected_area_path}" "{params.weight}" "{output}" + "{input.bathymetry_path}" "{params.water_depth}" "{input.protected_area_path}" "{params.weight}" "{output}" """ \ No newline at end of file diff --git a/workflow/scripts/wind_offshore.py b/workflow/scripts/wind_offshore.py index 55f7eb8..8dbdaba 100644 --- a/workflow/scripts/wind_offshore.py +++ b/workflow/scripts/wind_offshore.py @@ -15,7 +15,6 @@ @click.argument("resolution", type=float) @click.argument("bathymetry_path", type=str) @click.argument("water_depth", type=str) -@click.argument("land_sea_mask_path", type=str) @click.argument("protected_area_path", type=str) @click.argument("weight", type=float) @click.argument("output_path", type=str) @@ -25,7 +24,6 @@ def area_potential_wind_offshore( resolution, bathymetry_path, water_depth, - land_sea_mask_path, protected_area_path, weight, output_path, @@ -68,16 +66,6 @@ def area_potential_wind_offshore( reference_raster, resampling=Resampling.average ) - # keep only bathymetry in sea area using land sea mask - # NOTE: maybe remove this step since we already include EEZ outside buffer 10 km from coastline - ds_land_sea_mask = rxr.open_rasterio(land_sea_mask_path) - ds_land_sea_mask = ds_land_sea_mask.rio.reproject_match( - pixel_area, resampling=Resampling.mode - ) - resampled["bathymetry"] = resampled["bathymetry"].where( - ds_land_sea_mask == 1, other=0 - ) - print(f"Resampled bathymetry and land sea mask: {resampled.dims}, {resampled}") # keep bathymetry inside geo boundaries, i.e. within the exclusive economic zone (EEZ) From bee7ac5baf578300fc7628a060e9592144ef5dd0 Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Fri, 4 Jul 2025 14:04:18 +0200 Subject: [PATCH 17/59] Add reports --- workflow/Snakefile | 1 + workflow/internal/settings.yaml | 1 - workflow/rules/automatic.smk | 12 +++------ workflow/rules/prepare.smk | 2 +- workflow/rules/process.smk | 34 +++++++++++++++++++------- workflow/scripts/get_area_potential.py | 6 +++++ workflow/scripts/resample.py | 5 ++++ workflow/scripts/script_utils.py | 34 ++++++++++++++++++++++++++ workflow/scripts/wind_offshore.py | 34 ++++++++++++++------------ 9 files changed, 94 insertions(+), 35 deletions(-) create mode 100644 workflow/scripts/script_utils.py diff --git a/workflow/Snakefile b/workflow/Snakefile index 9092c1a..b73f5b4 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -16,6 +16,7 @@ validate(config, workflow.source_path("internal/config.schema.yaml")) with open(workflow.source_path("internal/settings.yaml"), "r") as f: internal = yaml.safe_load(f) +workflow.source_path("scripts/script_utils.py") # Add all your includes here. include: "rules/automatic.smk" diff --git a/workflow/internal/settings.yaml b/workflow/internal/settings.yaml index 5118915..b77090d 100644 --- a/workflow/internal/settings.yaml +++ b/workflow/internal/settings.yaml @@ -11,7 +11,6 @@ resources: # Land cover globcover: "https://due.esrin.esa.int/files/Globcover2009_V2.3_Global_.zip" globcover_landcover_tif: "GLOBCOVER_L4_200901_200912_V2.3.tif" - globcover_landseamask_tif: "GLOBCOVER_L4_200901_200912_V2.3_CLA_QL.tif" # Built-up areas ghsl: "https://jeodpp.jrc.ec.europa.eu/ftp/jrc-opendata/GHSL/GHS_BUILT_S_GLOBE_R2023A/GHS_BUILT_S_E2025_GLOBE_R2023A_4326_30ss/V1-0/GHS_BUILT_S_E2025_GLOBE_R2023A_4326_30ss_V1_0.zip" ghsl_tif: "GHS_BUILT_S_E2025_GLOBE_R2023A_4326_30ss_V1_0.tif" diff --git a/workflow/rules/automatic.smk b/workflow/rules/automatic.smk index 66fa2f5..e3ad22c 100644 --- a/workflow/rules/automatic.smk +++ b/workflow/rules/automatic.smk @@ -71,13 +71,11 @@ rule unzip_globcover: message: "Unzip the relevant TIF files from the GlobCover zip file." params: - target_file_1=internal["resources"]["automatic"]["globcover_landcover_tif"], - target_file_2=internal["resources"]["automatic"]["globcover_landseamask_tif"], + target_file=internal["resources"]["automatic"]["globcover_landcover_tif"], input: rules.download_globcover.output, output: - landcover="resources/automatic/globcover-landcover.tif", - landseamask="resources/automatic/globcover-landseamask.tif", + "resources/automatic/globcover-landcover.tif", log: "logs/unzip_globcover.log", conda: @@ -85,10 +83,8 @@ rule unzip_globcover: shell: """ temp_dir=$(mktemp -d) - unzip -j {input} {params.target_file_1} -d $temp_dir - unzip -j {input} {params.target_file_2} -d $temp_dir - mv $temp_dir/{params.target_file_1} {output.landcover} - mv $temp_dir/{params.target_file_2} {output.landseamask} + unzip -j {input} {params.target_file} -d $temp_dir + mv $temp_dir/{params.target_file} {output} rm -R $temp_dir """ diff --git a/workflow/rules/prepare.smk b/workflow/rules/prepare.smk index b011deb..e8eb0f5 100644 --- a/workflow/rules/prepare.smk +++ b/workflow/rules/prepare.smk @@ -5,7 +5,7 @@ rule cutout_landcover: "Cut land cover data to the bounds of the input shapefile." input: shapes="resources/user/shapes.parquet", - landcover=rules.unzip_globcover.output.landcover, + landcover=rules.unzip_globcover.output, output: "resources/cutout/landcover.tif", conda: diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index b9f10b8..7e4a111 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -17,14 +17,18 @@ rule resample_same_resolution_onshore: land_cover_path=rules.cutout_landcover.output, settlement_path=rules.cutout_settlement.output, output: - "resources/resampled_input_{tech_onshore}.nc", + resampled_input="resources/resampled_input_{tech_onshore}.nc", + plot=report( + "resources/resampled_input_{tech_onshore}.pdf", + category="resampled_input", + ), conda: "../envs/default.yaml" shell: """ python "{input.script}" "{input.shapes}" "{params.projection}" "{params.resolution}" \ "{params.suitable_land_cover_types}" "{input.slope_path}" "{input.land_cover_path}" "{input.settlement_path}" "{params.max_slope}" \ - "{output}" + "{output.resampled_input}" "{output.plot}" """ rule technical_mask_onshore: @@ -35,9 +39,13 @@ rule technical_mask_onshore: max_settlement=lambda wildcards: config["techs_onshore"][f"{wildcards.tech_onshore}"]["settlement"]["max_settlement"], input: script=workflow.source_path("../scripts/apply_technical_mask.py"), - resampled_path=rules.resample_same_resolution_onshore.output, + resampled_path=rules.resample_same_resolution_onshore.output.resampled_input, output: - "resources/technical_mask_{tech_onshore}.nc", + technical_mask="resources/technical_mask_{tech_onshore}.nc", + # plot=report( + # "resources/technical_mask_{tech_onshore}.pdf", + # category="technical_mask", + # ), conda: "../envs/default.yaml" shell: @@ -53,15 +61,19 @@ rule area_potential_onshore: input: script=workflow.source_path("../scripts/get_area_potential.py"), shapes="resources/user/shapes.parquet", - masked_path=rules.technical_mask_onshore.output, + masked_path=rules.technical_mask_onshore.output.technical_mask, protected_area_path=rules.unzip_wdpa.output, output: - "results/area_potential_{tech_onshore}.nc", + area_potential="results/area_potential_{tech_onshore}.nc", + plot=report( + "results/area_potential_{tech_onshore}.png", + category="area_potential", + ), conda: "../envs/default.yaml" shell: """ - python "{input.script}" "{input.masked_path}" "{params.technical_mask}" "{input.protected_area_path}" "{input.shapes}" "{output}" + python "{input.script}" "{input.masked_path}" "{params.technical_mask}" "{input.protected_area_path}" "{input.shapes}" "{output.area_potential}" "{output.plot}" """ rule area_potential_offshore: @@ -78,11 +90,15 @@ rule area_potential_offshore: bathymetry_path=rules.download_cutout_bathymetry.output, protected_area_path=rules.unzip_wdpa.output, output: - "results/area_potential_{tech_offshore}.nc", + area_potential="results/area_potential_{tech_offshore}.nc", + plot=report( + "results/area_potential_{tech_offshore}.png", + category="area_potential", + ), conda: "../envs/default.yaml" shell: """ python "{input.script}" "{input.shapes}" "{params.projection}" "{params.resolution}" \ - "{input.bathymetry_path}" "{params.water_depth}" "{input.protected_area_path}" "{params.weight}" "{output}" + "{input.bathymetry_path}" "{params.water_depth}" "{input.protected_area_path}" "{params.weight}" "{output.area_potential}" "{output.plot}" """ \ No newline at end of file diff --git a/workflow/scripts/get_area_potential.py b/workflow/scripts/get_area_potential.py index 61c9407..af4a6b0 100644 --- a/workflow/scripts/get_area_potential.py +++ b/workflow/scripts/get_area_potential.py @@ -1,5 +1,6 @@ import click import geopandas as gpd +import matplotlib.pyplot as plt import xarray as xr import yaml @@ -10,12 +11,14 @@ @click.argument("protected_area_path", type=str) @click.argument("shapes_path", type=str) @click.argument("output_path", type=str) +@click.argument("plot_path", type=str) def get_area_potential( masked_path, technical_mask, protected_area_path, shapes_path, output_path, + plot_path, ): ds_masked = xr.open_dataset(masked_path) technical_mask = yaml.safe_load(technical_mask) @@ -54,6 +57,9 @@ def get_area_potential( ) ds_area_potential.to_netcdf(output_path) + plot = ds_area_potential.plot() + plt.savefig(plot_path, bbox_inches="tight") + if __name__ == "__main__": get_area_potential() diff --git a/workflow/scripts/resample.py b/workflow/scripts/resample.py index 57c0de8..74dbe78 100644 --- a/workflow/scripts/resample.py +++ b/workflow/scripts/resample.py @@ -4,6 +4,7 @@ import geopandas as gpd import numpy as np import rioxarray as rxr +import script_utils import xarray as xr import yaml from rasterio.enums import Resampling @@ -211,6 +212,7 @@ def determine_pixel_areas(raster_input, bounds, resolution): @click.argument("settlement_path", type=str) @click.argument("max_slope", type=float) @click.argument("output_path", type=str) +@click.argument("plot_path", type=str) def get_same_shape_and_resolution( shapes_path, projection, @@ -221,6 +223,7 @@ def get_same_shape_and_resolution( settlement_path, max_slope, output_path, + plot_path, ): """Resample and crop the raster_input. @@ -289,6 +292,8 @@ def get_same_shape_and_resolution( resampled.to_netcdf(output_path) + script_utils.plot_all_dataset_variables(resampled, savefig=plot_path) + if __name__ == "__main__": get_same_shape_and_resolution() diff --git a/workflow/scripts/script_utils.py b/workflow/scripts/script_utils.py new file mode 100644 index 0000000..db26bae --- /dev/null +++ b/workflow/scripts/script_utils.py @@ -0,0 +1,34 @@ +import math + +import matplotlib.pyplot as plt + + +def plot_all_dataset_variables(ds, ncols=2, savefig=None): + + # Drop dimensionless variables + ds = ds.drop_vars(lambda x: [v for v, da in x.variables.items() if not da.ndim]) + + vars_to_plot = list(ds.data_vars) + + nrows = math.ceil(len(vars_to_plot) / ncols) + + fig, axes = plt.subplots(nrows=nrows, ncols=ncols, figsize=(12, 4 * nrows)) + axes = axes.flatten() + + for i, var in enumerate(vars_to_plot): + ds[var].plot(ax=axes[i]) + axes[i].set_title(var) + # We want to save rasterized images also for e.g. PDF output + # Any actor with a zorder below the value given here is rasterized + axes[i].set_rasterization_zorder(10000) + + for j in range(i + 1, len(axes)): + axes[j].set_visible(False) + + plt.tight_layout() + + if savefig: + + plt.savefig(savefig, bbox_inches="tight") + + return fig diff --git a/workflow/scripts/wind_offshore.py b/workflow/scripts/wind_offshore.py index 8dbdaba..771f511 100644 --- a/workflow/scripts/wind_offshore.py +++ b/workflow/scripts/wind_offshore.py @@ -1,10 +1,10 @@ import click -import yaml import geopandas as gpd -import xarray as xr +import matplotlib.pyplot as plt import rioxarray as rxr +import xarray as xr +import yaml from rasterio.enums import Resampling -from rasterio.features import rasterize from resample import create_empty_geospatial_array, determine_pixel_areas @@ -18,6 +18,7 @@ @click.argument("protected_area_path", type=str) @click.argument("weight", type=float) @click.argument("output_path", type=str) +@click.argument("plot_path", type=str) def area_potential_wind_offshore( shapes_path, projection, @@ -27,6 +28,7 @@ def area_potential_wind_offshore( protected_area_path, weight, output_path, + plot_path, ): """Get area potential for wind offshore technology. @@ -83,23 +85,23 @@ def area_potential_wind_offshore( buffer_geo.geometry, buffer_geo.crs, invert=True ) - # # mask out protected area - # # FIXME: read the right layer(s) and deal with both poly and point layers - # protected_areas = gpd.read_file(protected_area_path) - # # files = list(path_protected_area.glob("*_shp_?/*_shp-polygons.shp")) - # # gdfs = (gpd.read_file(file) for file in files) # generator - # # protected_areas = gpd.GeoDataFrame(pd.concat(gdfs, ignore_index=True)) + # mask out protected area + # FIXME: read the right layer(s) and deal with both poly and point layers + protected_areas = gpd.read_file(protected_area_path) - # eligible_fraction = eligible_fraction.rio.clip( - # protected_areas.geometry, protected_areas.crs, invert=True - # ) + eligible_fraction = eligible_fraction.rio.clip( + protected_areas.geometry, protected_areas.crs, invert=True + ) # apply weight, then multiply pixel area to get area potential - ds_area_potential = xr.Dataset( - {"wind_offshore": eligible_fraction * weight * resampled["pixel_area"]}, + da_area_potential = xr.Dataset( + {"data": eligible_fraction * weight * resampled["pixel_area"]}, coords=resampled["pixel_area"].coords, - ) - ds_area_potential.to_netcdf(output_path) + )["data"] + da_area_potential.to_netcdf(output_path) + + plot = da_area_potential.plot() + plt.savefig(plot_path, bbox_inches="tight") if __name__ == "__main__": From 25b86d2abf3e285aaac7a57ed2d1b583f5354ab1 Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Fri, 4 Jul 2025 14:05:06 +0200 Subject: [PATCH 18/59] Improve file naming consistency --- workflow/rules/process.smk | 4 ++-- .../scripts/{wind_offshore.py => potential_offshore.py} | 8 ++++++-- .../{get_area_potential.py => potential_onshore.py} | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) rename workflow/scripts/{wind_offshore.py => potential_offshore.py} (92%) rename workflow/scripts/{get_area_potential.py => potential_onshore.py} (96%) diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index 7e4a111..fdcebd9 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -59,7 +59,7 @@ rule area_potential_onshore: params: technical_mask=lambda wildcards: config["techs_onshore"][f"{wildcards.tech_onshore}"] input: - script=workflow.source_path("../scripts/get_area_potential.py"), + script=workflow.source_path("../scripts/potential_onshore.py"), shapes="resources/user/shapes.parquet", masked_path=rules.technical_mask_onshore.output.technical_mask, protected_area_path=rules.unzip_wdpa.output, @@ -85,7 +85,7 @@ rule area_potential_offshore: water_depth=lambda wildcards: config["techs_offshore"][f"{wildcards.tech_offshore}"]["water_depth"], weight=lambda wildcards: config["techs_offshore"][f"{wildcards.tech_offshore}"]["weight"], input: - script=workflow.source_path("../scripts/wind_offshore.py"), + script=workflow.source_path("../scripts/potential_offshore.py"), shapes="resources/user/shapes.parquet", bathymetry_path=rules.download_cutout_bathymetry.output, protected_area_path=rules.unzip_wdpa.output, diff --git a/workflow/scripts/wind_offshore.py b/workflow/scripts/potential_offshore.py similarity index 92% rename from workflow/scripts/wind_offshore.py rename to workflow/scripts/potential_offshore.py index 771f511..18a2edb 100644 --- a/workflow/scripts/wind_offshore.py +++ b/workflow/scripts/potential_offshore.py @@ -19,7 +19,7 @@ @click.argument("weight", type=float) @click.argument("output_path", type=str) @click.argument("plot_path", type=str) -def area_potential_wind_offshore( +def get_area_potential_offshore( shapes_path, projection, resolution, @@ -87,7 +87,11 @@ def area_potential_wind_offshore( # mask out protected area # FIXME: read the right layer(s) and deal with both poly and point layers + minx, maxx, miny, maxy = shapes.total_bounds protected_areas = gpd.read_file(protected_area_path) + print(f"Protected areas: {len(protected_areas)}") + protected_areas = protected_areas.cx[minx:maxx, miny:maxy] + print(f"Protected areas after applying total_bounds: {len(protected_areas)}") eligible_fraction = eligible_fraction.rio.clip( protected_areas.geometry, protected_areas.crs, invert=True @@ -105,4 +109,4 @@ def area_potential_wind_offshore( if __name__ == "__main__": - area_potential_wind_offshore() + get_area_potential_offshore() diff --git a/workflow/scripts/get_area_potential.py b/workflow/scripts/potential_onshore.py similarity index 96% rename from workflow/scripts/get_area_potential.py rename to workflow/scripts/potential_onshore.py index af4a6b0..ef15fe6 100644 --- a/workflow/scripts/get_area_potential.py +++ b/workflow/scripts/potential_onshore.py @@ -12,7 +12,7 @@ @click.argument("shapes_path", type=str) @click.argument("output_path", type=str) @click.argument("plot_path", type=str) -def get_area_potential( +def get_area_potential_onshore( masked_path, technical_mask, protected_area_path, @@ -62,4 +62,4 @@ def get_area_potential( if __name__ == "__main__": - get_area_potential() + get_area_potential_onshore() From 9e1e1317dfffa21055dbfc69b78b94e62ea85cdc Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Mon, 7 Jul 2025 10:47:10 +0200 Subject: [PATCH 19/59] Move more of the geo-processing into resample.py; Add summary report --- config/config.yaml | 4 +- workflow/Snakefile | 2 + workflow/envs/default.yaml | 2 + workflow/rules/process.smk | 59 +++++++++---- workflow/scripts/apply_technical_mask.py | 8 +- workflow/scripts/geo.py | 77 +++++++++++++++++ workflow/scripts/potential_offshore.py | 27 +++--- workflow/scripts/potential_onshore.py | 17 ++-- workflow/scripts/report.py | 20 +++++ workflow/scripts/resample.py | 100 +++++++++++++++-------- 10 files changed, 235 insertions(+), 81 deletions(-) create mode 100644 workflow/scripts/geo.py create mode 100644 workflow/scripts/report.py diff --git a/config/config.yaml b/config/config.yaml index a3b5a99..d724a84 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -13,7 +13,7 @@ techs_onshore: OTHER: 0.2 URBAN: 0 # no PV open field in urban settlement: - max_settlement: 0.1 # no PV open field in settlement + max_settlement: 0.01 # no PV open field in settlement weight: -1 wind_onshore: max_slope: 20 @@ -23,7 +23,7 @@ techs_onshore: OTHER: 0.3 URBAN: 0 # no wind onshore in urban settlement: - max_settlement: 0.1 + max_settlement: 0.01 weight: -1 pv_rooftop: max_slope: 90 diff --git a/workflow/Snakefile b/workflow/Snakefile index b73f5b4..8289fc3 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -16,7 +16,9 @@ validate(config, workflow.source_path("internal/config.schema.yaml")) with open(workflow.source_path("internal/settings.yaml"), "r") as f: internal = yaml.safe_load(f) +# Python files that are imported from other scripts and need to be included when accessing the module workflow.source_path("scripts/script_utils.py") +workflow.source_path("scripts/geo.py") # Add all your includes here. include: "rules/automatic.smk" diff --git a/workflow/envs/default.yaml b/workflow/envs/default.yaml index b6c7826..0f99c18 100644 --- a/workflow/envs/default.yaml +++ b/workflow/envs/default.yaml @@ -17,3 +17,5 @@ dependencies: - libgdal-hdf5=3.10.3 - matplotlib=3.10.3 - pyyaml + - pyproj=3.7.1 + - utm=0.7.0 diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index fdcebd9..887b08c 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -2,32 +2,32 @@ wildcard_constraints: tech_onshore="|".join(config["techs_onshore"].keys()), tech_offshore="|".join(config["techs_offshore"].keys()), -rule resample_same_resolution_onshore: +rule resample_same_resolution: message: - "Resample slope, land cover (subset to suitable types), and settlement to the same resolution for the tech {wildcards.tech_onshore}.", + "Resample inputs (with land cover subset to suitable types) to the requested projection and resolution.", params: projection=config["specs"]["projection"], resolution=config["specs"]["resolution"], - suitable_land_cover_types=lambda wildcards: config["techs_onshore"][f"{wildcards.tech_onshore}"]["land_cover"], - max_slope=lambda wildcards: config["techs_onshore"][f"{wildcards.tech_onshore}"]["max_slope"], input: script=workflow.source_path("../scripts/resample.py"), shapes="resources/user/shapes.parquet", - slope_path=rules.download_cutout_slope.output, land_cover_path=rules.cutout_landcover.output, + slope_path=rules.download_cutout_slope.output, settlement_path=rules.cutout_settlement.output, + protected_area_path=rules.unzip_wdpa.output, output: - resampled_input="resources/resampled_input_{tech_onshore}.nc", + resampled_input="resources/resampled_inputs.nc", plot=report( - "resources/resampled_input_{tech_onshore}.pdf", + "resources/resampled_inputs.pdf", category="resampled_input", ), conda: "../envs/default.yaml" shell: """ - python "{input.script}" "{input.shapes}" "{params.projection}" "{params.resolution}" \ - "{params.suitable_land_cover_types}" "{input.slope_path}" "{input.land_cover_path}" "{input.settlement_path}" "{params.max_slope}" \ + python "{input.script}" \ + "{params.projection}" "{params.resolution}" \ + "{input.shapes}" "{input.land_cover_path}" "{input.slope_path}" "{input.settlement_path}" "{input.protected_area_path}" \ "{output.resampled_input}" "{output.plot}" """ @@ -36,10 +36,11 @@ rule technical_mask_onshore: "Get fraction satisfied all technical criteria: not too steep slope, suitable land cover, and not exceeding max_settlement for the tech {wildcards.tech_onshore}.", params: suitable_land_cover_types=lambda wildcards: config["techs_onshore"][f"{wildcards.tech_onshore}"]["land_cover"], + max_slope=lambda wildcards: config["techs_onshore"][f"{wildcards.tech_onshore}"]["max_slope"], max_settlement=lambda wildcards: config["techs_onshore"][f"{wildcards.tech_onshore}"]["settlement"]["max_settlement"], input: script=workflow.source_path("../scripts/apply_technical_mask.py"), - resampled_path=rules.resample_same_resolution_onshore.output.resampled_input, + resampled_path=rules.resample_same_resolution.output.resampled_input, output: technical_mask="resources/technical_mask_{tech_onshore}.nc", # plot=report( @@ -50,19 +51,18 @@ rule technical_mask_onshore: "../envs/default.yaml" shell: """ - python "{input.script}" "{input.resampled_path}" "{params.suitable_land_cover_types}" "{params.max_settlement}" "{output}" + python "{input.script}" "{input.resampled_path}" "{params.suitable_land_cover_types}" "{params.max_slope}" "{params.max_settlement}" "{output}" """ rule area_potential_onshore: message: - "Apply weights, mask out protected area then calculate the potential area for the tech {wildcards.tech_onshore}.", + "Apply weights then calculate the potential area for the tech {wildcards.tech_onshore}.", params: technical_mask=lambda wildcards: config["techs_onshore"][f"{wildcards.tech_onshore}"] input: script=workflow.source_path("../scripts/potential_onshore.py"), shapes="resources/user/shapes.parquet", masked_path=rules.technical_mask_onshore.output.technical_mask, - protected_area_path=rules.unzip_wdpa.output, output: area_potential="results/area_potential_{tech_onshore}.nc", plot=report( @@ -73,7 +73,7 @@ rule area_potential_onshore: "../envs/default.yaml" shell: """ - python "{input.script}" "{input.masked_path}" "{params.technical_mask}" "{input.protected_area_path}" "{input.shapes}" "{output.area_potential}" "{output.plot}" + python "{input.script}" "{input.masked_path}" "{params.technical_mask}" "{input.shapes}" "{output.area_potential}" "{output.plot}" """ rule area_potential_offshore: @@ -88,17 +88,42 @@ rule area_potential_offshore: script=workflow.source_path("../scripts/potential_offshore.py"), shapes="resources/user/shapes.parquet", bathymetry_path=rules.download_cutout_bathymetry.output, - protected_area_path=rules.unzip_wdpa.output, + resampled_input_path=rules.resample_same_resolution.output.resampled_input, output: area_potential="results/area_potential_{tech_offshore}.nc", plot=report( "results/area_potential_{tech_offshore}.png", category="area_potential", ), + log: + "logs/area_potential_{tech_offshore}.log", conda: "../envs/default.yaml" shell: """ python "{input.script}" "{input.shapes}" "{params.projection}" "{params.resolution}" \ - "{input.bathymetry_path}" "{params.water_depth}" "{input.protected_area_path}" "{params.weight}" "{output.area_potential}" "{output.plot}" - """ \ No newline at end of file + "{input.bathymetry_path}" "{params.water_depth}" "{input.resampled_input_path}" "{params.weight}" "{output.area_potential}" "{output.plot}" 2> "{log}" + """ + + +rule area_potential_report: + message: + "Generate an overview report of the area potential for all techs.", + input: + area_potentials=expand( + "results/area_potential_{tech}.nc", + tech=config["techs_offshore"].keys(), + ) + expand( + "results/area_potential_{tech}.nc", + tech=config["techs_onshore"].keys(), + ), + output: + csv="results/area_potential_report.csv", + html=report( + "results/area_potential_report.html", + category="area_potential_report", + ), + conda: + "../envs/default.yaml" + script: + "../scripts/report.py" diff --git a/workflow/scripts/apply_technical_mask.py b/workflow/scripts/apply_technical_mask.py index 622364e..9760c64 100644 --- a/workflow/scripts/apply_technical_mask.py +++ b/workflow/scripts/apply_technical_mask.py @@ -6,11 +6,13 @@ @click.command() @click.argument("resampled_path", type=str) @click.argument("suitable_land_cover_types", type=str) +@click.argument("max_slope", type=float) @click.argument("max_settlement", type=float) @click.argument("output_path", type=str) def apply_technical_mask( resampled_path, suitable_land_cover_types, + max_slope, max_settlement, output_path, ): @@ -26,8 +28,12 @@ def apply_technical_mask( # settlement <= max_settlement suitable_land_cover_types = yaml.safe_load(suitable_land_cover_types) + ds_resampled["slope_too_steep"] = ds_resampled["slope"] > max_slope + land_cover_types = [ - type for type, value in suitable_land_cover_types.items() if value != 0 + f"landcover_{type}" + for type, value in suitable_land_cover_types.items() + if value != 0 ] land_cover_mask = ( ds_resampled[land_cover_types].to_array().sum(dim="variable") >= 0.5 diff --git a/workflow/scripts/geo.py b/workflow/scripts/geo.py new file mode 100644 index 0000000..01bd314 --- /dev/null +++ b/workflow/scripts/geo.py @@ -0,0 +1,77 @@ +import warnings + +import geopandas as gpd +import utm +from pyproj import CRS + + +def get_utm_crs_from_lonlat(lon, lat): + """Return the appropriate UTM CRS based for the given longitude and latitude. + + Args: + lon (float): Longitude of the point. + lat (float): Latitude of the point. + + Returns: + CRS: The UTM CRS corresponding to the given longitude and latitude. + + """ + easting, northing, zone_number, zone_letter = utm.from_latlon(lat, lon) + is_northern = lat >= 0 + epsg_code = 32600 + zone_number if is_northern else 32700 + zone_number + return CRS.from_epsg(epsg_code) + + +def utm_buffer(geom, buffer_distance_m=10000, source_crs="EPSG:4326"): + """Project a geom to UTM, buffer it, then re-project to its source CRS. + + Args: + geom (shapely.geometry): The geometry to buffer, in the given source_crs. + buffer_distance_m (int): The buffer distance in meters (default is 10,000 m). + source_crs (str): The source CRS of the geometry (default is "EPSG:4326"). + + Returns: + shapely.geometry: The buffered geometry in its original CRS. + + """ + try: + centroid = geom.centroid + lon, lat = centroid.x, centroid.y + local_crs = get_utm_crs_from_lonlat(lon, lat) + + # Project to local UTM CRS + gdf_single = gpd.GeoDataFrame(geometry=[geom], crs=source_crs) + gdf_utm = gdf_single.to_crs(local_crs) + + # Buffer in meters + gdf_utm["geometry"] = gdf_utm.buffer(buffer_distance_m) + + # Reproject back to WGS84 + gdf_buffered = gdf_utm.to_crs(source_crs) + return gdf_buffered.iloc[0].geometry + + except Exception as e: + warnings.warn(f"Failed to buffer geometry: {e}") + return None + + +def apply_utm_buffer(gdf, buffer_distance_m=10000): + """Apply a UTM-based buffer to a GeoDataFrame with an arbitrary CRS. + + The buffering will be performed row-by-row using the most appropriate UTM zone for + each geometry's centroid. + + Args: + gdf (geopandas.GeoDataFrame): The GeoDataFrame containing geometries to buffer. + buffer_distance_m (int): The buffer distance in meters (default is 10,000 m). + + Returns: + geopandas.GeoDataFrame: A new GeoDataFrame with buffered geometries. + + """ + source_crs = gdf.crs + gdf_buffered = gdf.copy() + gdf_buffered["geometry"] = gdf_buffered["geometry"].apply( + lambda geom: utm_buffer(geom, buffer_distance_m, source_crs) + ) + return gdf_buffered diff --git a/workflow/scripts/potential_offshore.py b/workflow/scripts/potential_offshore.py index 18a2edb..6f3130b 100644 --- a/workflow/scripts/potential_offshore.py +++ b/workflow/scripts/potential_offshore.py @@ -1,11 +1,11 @@ import click +import geo import geopandas as gpd import matplotlib.pyplot as plt import rioxarray as rxr import xarray as xr import yaml from rasterio.enums import Resampling - from resample import create_empty_geospatial_array, determine_pixel_areas @@ -15,7 +15,7 @@ @click.argument("resolution", type=float) @click.argument("bathymetry_path", type=str) @click.argument("water_depth", type=str) -@click.argument("protected_area_path", type=str) +@click.argument("resampled_input_path", type=str) @click.argument("weight", type=float) @click.argument("output_path", type=str) @click.argument("plot_path", type=str) @@ -25,7 +25,7 @@ def get_area_potential_offshore( resolution, bathymetry_path, water_depth, - protected_area_path, + resampled_input_path, weight, output_path, plot_path, @@ -39,6 +39,8 @@ def get_area_potential_offshore( - Convert to area potenital in m2 """ + ds_inputs = xr.open_dataset(resampled_input_path) + # resample to the same bounds and resolution as the reference raster # create reference raster with the same bounds as given shapes shapes = gpd.read_parquet(shapes_path) @@ -76,26 +78,17 @@ def get_area_potential_offshore( ) # Buffer 10 km from country shape, no wind offshore too close to the coastline - # Project to a metric CRS equal area EPSG:6933 for buffering - # Note: a UTM zone specific would be more accurate (e.g., UTM or EPSG:32648) but it varies by country shapes_land = shapes[shapes["shape_class"] == "land"] - sea_buffer = shapes_land.to_crs(epsg=6933).buffer(10_000) # 10 km buffer outward + sea_buffer = geo.apply_utm_buffer(shapes_land, buffer_distance_m=10000).to_crs( + pixel_area.rio.crs + )["geometry"] buffer_geo = gpd.GeoDataFrame(geometry=sea_buffer).to_crs(pixel_area.rio.crs) eligible_fraction = eligible_fraction.rio.clip( buffer_geo.geometry, buffer_geo.crs, invert=True ) - # mask out protected area - # FIXME: read the right layer(s) and deal with both poly and point layers - minx, maxx, miny, maxy = shapes.total_bounds - protected_areas = gpd.read_file(protected_area_path) - print(f"Protected areas: {len(protected_areas)}") - protected_areas = protected_areas.cx[minx:maxx, miny:maxy] - print(f"Protected areas after applying total_bounds: {len(protected_areas)}") - - eligible_fraction = eligible_fraction.rio.clip( - protected_areas.geometry, protected_areas.crs, invert=True - ) + # exclude protected areas + eligible_fraction = eligible_fraction.where(ds_inputs["protected"] != 1) # apply weight, then multiply pixel area to get area potential da_area_potential = xr.Dataset( diff --git a/workflow/scripts/potential_onshore.py b/workflow/scripts/potential_onshore.py index ef15fe6..4933344 100644 --- a/workflow/scripts/potential_onshore.py +++ b/workflow/scripts/potential_onshore.py @@ -8,14 +8,12 @@ @click.command() @click.argument("masked_path", type=str) @click.argument("technical_mask", type=str) -@click.argument("protected_area_path", type=str) @click.argument("shapes_path", type=str) @click.argument("output_path", type=str) @click.argument("plot_path", type=str) def get_area_potential_onshore( masked_path, technical_mask, - protected_area_path, shapes_path, output_path, plot_path, @@ -28,26 +26,23 @@ def get_area_potential_onshore( for type, value in technical_mask["land_cover"].items(): if value == 0: continue - suitable_land_cover_types.append(type) - ds_masked[type] = ds_masked[type] * value + key = f"landcover_{type}" + suitable_land_cover_types.append(key) + ds_masked[key] = ds_masked[key] * value eligible_fraction = ( ds_masked[suitable_land_cover_types].to_array().sum(dim="variable") - ds_masked["slope_too_steep"] + ds_masked["settlement"] * technical_mask["settlement"]["weight"] ) + + eligible_fraction = eligible_fraction.where(ds_masked["protected"] != 1) + eligible_fraction.rio.write_crs("EPSG:4326", inplace=True) # remove negative values and values greater than 1 eligible_fraction = eligible_fraction.clip(0, 1) - # mask out protected area - # FIXME: read the right layer(s) and deal with both poly and point layers - protected_areas = gpd.read_file(protected_area_path) - eligible_fraction = eligible_fraction.rio.clip( - protected_areas.geometry, protected_areas.crs, invert=True - ) - # multiply pixel area to get area potential # cut with given shape to return raster inside the shape shapes = gpd.read_parquet(shapes_path) diff --git a/workflow/scripts/report.py b/workflow/scripts/report.py new file mode 100644 index 0000000..3e7073b --- /dev/null +++ b/workflow/scripts/report.py @@ -0,0 +1,20 @@ +import pandas as pd +import xarray as xr + + +def report(area_potentials, csv_path, html_path): + report_values = [] + for area_potential in area_potentials: + ds = xr.open_dataset(area_potential) + val = ds.squeeze().sum().to_dataarray().values[0] / (1e6) # Convert to km² + report_values.append((area_potential, val)) + # report += f"Area potential for {area_potential}: {val:.2f} km²\n" + # with open(report_path, "w") as f: + # f.write(report) + df = pd.DataFrame(report_values, columns=["Area Potential", "Value (km²)"]) + df.to_csv(csv_path, index=False) + df.to_html(html_path, index=False) + + +if __name__ == "__main__": + report(snakemake.input.area_potentials, snakemake.output.csv, snakemake.output.html) diff --git a/workflow/scripts/resample.py b/workflow/scripts/resample.py index 74dbe78..ef64f21 100644 --- a/workflow/scripts/resample.py +++ b/workflow/scripts/resample.py @@ -6,8 +6,8 @@ import rioxarray as rxr import script_utils import xarray as xr -import yaml from rasterio.enums import Resampling +from rasterio.features import rasterize from rasterio.transform import from_bounds # LAND COVER @@ -203,25 +203,23 @@ def determine_pixel_areas(raster_input, bounds, resolution): @click.command() -@click.argument("shapes_path", type=str) @click.argument("projection", type=str) @click.argument("resolution", type=float) -@click.argument("suitable_land_cover_types", type=str) -@click.argument("slope_path", type=str) +@click.argument("shapes_path", type=str) @click.argument("land_cover_path", type=str) +@click.argument("slope_path", type=str) @click.argument("settlement_path", type=str) -@click.argument("max_slope", type=float) +@click.argument("protected_area_path", type=str) @click.argument("output_path", type=str) @click.argument("plot_path", type=str) def get_same_shape_and_resolution( shapes_path, projection, resolution, - suitable_land_cover_types, - slope_path, land_cover_path, + slope_path, settlement_path, - max_slope, + protected_area_path, output_path, plot_path, ): @@ -233,6 +231,8 @@ def get_same_shape_and_resolution( """ # create reference raster with the same bounds as given shapes shapes = gpd.read_parquet(shapes_path) + print(f"Number of shapes: {len(shapes)}") + print(f"Requested resolution: {resolution}") reference_raster = create_empty_geospatial_array( bounds=shapes.total_bounds, projection=projection, @@ -245,50 +245,84 @@ def get_same_shape_and_resolution( bounds=shapes.total_bounds, resolution=resolution, ) - print(f"Pixel area: {pixel_area.dims}, {pixel_area}") + # print(f"Pixel area: {pixel_area.dims}, {pixel_area}") resampled["pixel_area"] = pixel_area - # slope in fraction - da_slope = rxr.open_rasterio(slope_path) + ## + # Regions + ## - slope_too_steep = da_slope > max_slope + regions = ((geom, idx) for idx, geom in zip(shapes.index, shapes.geometry)) + rasterized = rasterize( + shapes=regions, + out_shape=reference_raster.rio.shape, + transform=reference_raster.rio.transform(), + all_touched=True, + fill=np.nan, + dtype=np.float32, + ) + resampled["regions"] = (("y", "x"), rasterized) + # Slope + da_slope = rxr.open_rasterio(slope_path, masked=True) / 100 + print(f"Slope resolution: {da_slope.rio.resolution()}") resampled["slope"] = da_slope.astype(float).rio.reproject_match( reference_raster, resampling=Resampling.average ) - resampled["slope_too_steep"] = slope_too_steep.astype(float).rio.reproject_match( - reference_raster, resampling=Resampling.average - ) + # ds_slope = xr.open_dataset(slope_path) + # for slope_class in ds_slope.data_vars: + # resampled[f"slope_{slope_class}"] = ds_slope.astype(float).rio.reproject_match( + # reference_raster, resampling=Resampling.average + # ) + + # resampled["slope_too_steep"] = slope_too_steep.astype(float).rio.reproject_match( + # reference_raster, resampling=Resampling.average + # ) ## # Land cover ## - suitable_land_cover_types = yaml.safe_load(suitable_land_cover_types) - land_cover = ds_land_cover = rxr.open_rasterio(land_cover_path) - ds_land_cover = get_suitable_land_cover_type( - land_cover, suitable_land_cover_types.keys() - ) - - # land cover in fraction - for land_type, value in suitable_land_cover_types.items(): - skip = [] - if value == 0: - skip.append(land_type) # skip land cover types with 0 weight - else: - resampled[land_type] = ds_land_cover[land_type].rio.reproject_match( - reference_raster, resampling=Resampling.average - ) + suitable_land_cover_types = sorted(list(set(CoverType.values()))) + ds_land_cover = rxr.open_rasterio(land_cover_path) + print(f"Land cover resolution: {ds_land_cover.rio.resolution()}") + land_cover = get_suitable_land_cover_type(ds_land_cover, suitable_land_cover_types) + + for land_type in suitable_land_cover_types: + # skip = [] + # if value == 0: + # skip.append(land_type) # skip land cover types with 0 weight + # else: + resampled[f"landcover_{land_type}"] = land_cover[land_type].rio.reproject_match( + reference_raster, resampling=Resampling.average + ) - print(f"Skip the land cover types not used in this tech: {skip}") + # print(f"Skip the land cover types not used in this tech: {skip}") - # settlement in sum of area of built-up surface (m2) + ## + # Settlement in sum of area of built-up surface (m2) + ## ds_settlement = rxr.open_rasterio(settlement_path) + print(f"Settlement resolution: {ds_settlement.rio.resolution()}") ds_settlement.rio.write_crs("EPSG:4326", inplace=True) resampled["settlement"] = ds_settlement.rio.reproject_match( reference_raster, resampling=Resampling.sum ) - print("resampled settlement", resampled.dims, resampled.coords, resampled) + # print("resampled settlement", resampled.dims, resampled.coords, resampled) + + ## + # Protected areas + ## + # FIXME: read the right layer(s) and deal with both poly and point layers + xmin, ymin, xmax, ymax = shapes.total_bounds + protected_areas = gpd.read_file(protected_area_path) + print(f"Protected areas: {len(protected_areas)}") + protected_areas = protected_areas.cx[xmin:xmax, ymin:ymax] + print(f"Protected areas after applying total_bounds: {len(protected_areas)}") + + resampled["protected"] = reference_raster.fillna(1).rio.clip( + protected_areas.geometry, protected_areas.crs + ) resampled.to_netcdf(output_path) From a95ae05e0d735d9a3df697bd0176b9115ff69c0e Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Mon, 7 Jul 2025 17:24:55 +0200 Subject: [PATCH 20/59] Further clean up processing steps; Report by shape --- config/config.yaml | 5 - workflow/rules/process.smk | 12 +- workflow/scripts/apply_technical_mask.py | 13 +- workflow/scripts/potential_offshore.py | 42 ++---- workflow/scripts/potential_onshore.py | 2 + workflow/scripts/report.py | 48 +++++-- workflow/scripts/resample.py | 163 ++++++----------------- 7 files changed, 98 insertions(+), 187 deletions(-) diff --git a/config/config.yaml b/config/config.yaml index d724a84..b1905e1 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -1,8 +1,3 @@ -# Output specifications -specs: - projection: EPSG:4326 - resolution: 0.008333 # 30/3600 arcsec in degrees - # Technical criteria techs_onshore: pv_open_field: diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index 887b08c..4b147c6 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -4,10 +4,7 @@ wildcard_constraints: rule resample_same_resolution: message: - "Resample inputs (with land cover subset to suitable types) to the requested projection and resolution.", - params: - projection=config["specs"]["projection"], - resolution=config["specs"]["resolution"], + "Resample inputs to the projection and resolution of the land cover data, while aggregating land cover types.", input: script=workflow.source_path("../scripts/resample.py"), shapes="resources/user/shapes.parquet", @@ -26,7 +23,6 @@ rule resample_same_resolution: shell: """ python "{input.script}" \ - "{params.projection}" "{params.resolution}" \ "{input.shapes}" "{input.land_cover_path}" "{input.slope_path}" "{input.settlement_path}" "{input.protected_area_path}" \ "{output.resampled_input}" "{output.plot}" """ @@ -80,8 +76,6 @@ rule area_potential_offshore: message: "Get area potential for the tech {wildcards.tech_offshore}" params: - projection=config["specs"]["projection"], - resolution=config["specs"]["resolution"], water_depth=lambda wildcards: config["techs_offshore"][f"{wildcards.tech_offshore}"]["water_depth"], weight=lambda wildcards: config["techs_offshore"][f"{wildcards.tech_offshore}"]["weight"], input: @@ -101,7 +95,7 @@ rule area_potential_offshore: "../envs/default.yaml" shell: """ - python "{input.script}" "{input.shapes}" "{params.projection}" "{params.resolution}" \ + python "{input.script}" "{input.shapes}" \ "{input.bathymetry_path}" "{params.water_depth}" "{input.resampled_input_path}" "{params.weight}" "{output.area_potential}" "{output.plot}" 2> "{log}" """ @@ -110,6 +104,8 @@ rule area_potential_report: message: "Generate an overview report of the area potential for all techs.", input: + shapes="resources/user/shapes.parquet", + resampled_path=rules.resample_same_resolution.output.resampled_input, area_potentials=expand( "results/area_potential_{tech}.nc", tech=config["techs_offshore"].keys(), diff --git a/workflow/scripts/apply_technical_mask.py b/workflow/scripts/apply_technical_mask.py index 9760c64..4a0b0dd 100644 --- a/workflow/scripts/apply_technical_mask.py +++ b/workflow/scripts/apply_technical_mask.py @@ -17,13 +17,8 @@ def apply_technical_mask( output_path, ): ds_resampled = xr.open_dataset(resampled_path, engine="netcdf4") - print("ds_resampled before applying technical mask", ds_resampled) - # get fraction of settlement (built-up surface) compared to pixel area, both in m2 - ds_resampled["settlement"] = ds_resampled["settlement"] / ds_resampled["pixel_area"] - print("ds_resampled after calculating settlement", ds_resampled) - - # only keep pixel with fraction sum of suitable land cover >= 0.5, + # only keep pixel with fraction sum of suitable land cover >= 0, # too steep slope < 0.5 # settlement <= max_settlement suitable_land_cover_types = yaml.safe_load(suitable_land_cover_types) @@ -35,9 +30,7 @@ def apply_technical_mask( for type, value in suitable_land_cover_types.items() if value != 0 ] - land_cover_mask = ( - ds_resampled[land_cover_types].to_array().sum(dim="variable") >= 0.5 - ) + land_cover_mask = ds_resampled[land_cover_types].to_array().sum(dim="variable") > 0 combined_mask = ( (ds_resampled["slope_too_steep"] <= 0.5) @@ -45,7 +38,7 @@ def apply_technical_mask( & (ds_resampled["settlement"] <= max_settlement) ) ds_resampled = ds_resampled.where(combined_mask) - print("ds_resampled before saving", ds_resampled) + # print("ds_resampled before saving", ds_resampled) ds_resampled.to_netcdf(output_path) diff --git a/workflow/scripts/potential_offshore.py b/workflow/scripts/potential_offshore.py index 6f3130b..b49f7e7 100644 --- a/workflow/scripts/potential_offshore.py +++ b/workflow/scripts/potential_offshore.py @@ -6,13 +6,10 @@ import xarray as xr import yaml from rasterio.enums import Resampling -from resample import create_empty_geospatial_array, determine_pixel_areas @click.command() @click.argument("shapes_path", type=str) -@click.argument("projection", type=str) -@click.argument("resolution", type=float) @click.argument("bathymetry_path", type=str) @click.argument("water_depth", type=str) @click.argument("resampled_input_path", type=str) @@ -21,8 +18,6 @@ @click.argument("plot_path", type=str) def get_area_potential_offshore( shapes_path, - projection, - resolution, bathymetry_path, water_depth, resampled_input_path, @@ -39,25 +34,8 @@ def get_area_potential_offshore( - Convert to area potenital in m2 """ - ds_inputs = xr.open_dataset(resampled_input_path) - - # resample to the same bounds and resolution as the reference raster - # create reference raster with the same bounds as given shapes + ds_inputs = xr.open_dataset(resampled_input_path, decode_coords="all") shapes = gpd.read_parquet(shapes_path) - reference_raster = create_empty_geospatial_array( - bounds=shapes.total_bounds, - projection=projection, - resolution=resolution, - ) - resampled = xr.Dataset() - - pixel_area = determine_pixel_areas( - reference_raster, - bounds=shapes.total_bounds, - resolution=resolution, - ) - print(f"Pixel area: {pixel_area.dims}, {pixel_area}") - resampled["pixel_area"] = pixel_area # get bathymetry within the water depth range then resample water_depth = yaml.safe_load(water_depth) @@ -66,23 +44,21 @@ def get_area_potential_offshore( (ds_bathymetry < water_depth["max"]) & (ds_bathymetry >= water_depth["min"]) ).astype(float) - resampled["bathymetry"] = masked_bathymetry.rio.reproject_match( - reference_raster, resampling=Resampling.average + ds_inputs["bathymetry"] = masked_bathymetry.rio.reproject_match( + ds_inputs["pixel_area"], resampling=Resampling.average ) - print(f"Resampled bathymetry and land sea mask: {resampled.dims}, {resampled}") - # keep bathymetry inside geo boundaries, i.e. within the exclusive economic zone (EEZ) - eligible_fraction = resampled["bathymetry"].rio.clip( + eligible_fraction = ds_inputs["bathymetry"].rio.clip( shapes.geometry, shapes.crs, invert=False ) # Buffer 10 km from country shape, no wind offshore too close to the coastline shapes_land = shapes[shapes["shape_class"] == "land"] sea_buffer = geo.apply_utm_buffer(shapes_land, buffer_distance_m=10000).to_crs( - pixel_area.rio.crs + ds_inputs.rio.crs )["geometry"] - buffer_geo = gpd.GeoDataFrame(geometry=sea_buffer).to_crs(pixel_area.rio.crs) + buffer_geo = gpd.GeoDataFrame(geometry=sea_buffer).to_crs(ds_inputs.rio.crs) eligible_fraction = eligible_fraction.rio.clip( buffer_geo.geometry, buffer_geo.crs, invert=True ) @@ -92,9 +68,9 @@ def get_area_potential_offshore( # apply weight, then multiply pixel area to get area potential da_area_potential = xr.Dataset( - {"data": eligible_fraction * weight * resampled["pixel_area"]}, - coords=resampled["pixel_area"].coords, - )["data"] + {"area_potential": eligible_fraction * weight * ds_inputs["pixel_area"]}, + coords=ds_inputs["pixel_area"].coords, + )["area_potential"] da_area_potential.to_netcdf(output_path) plot = da_area_potential.plot() diff --git a/workflow/scripts/potential_onshore.py b/workflow/scripts/potential_onshore.py index 4933344..4b37920 100644 --- a/workflow/scripts/potential_onshore.py +++ b/workflow/scripts/potential_onshore.py @@ -38,6 +38,7 @@ def get_area_potential_onshore( eligible_fraction = eligible_fraction.where(ds_masked["protected"] != 1) + # FIXME: is this necessary if the inputs are already in EPSG:4326? eligible_fraction.rio.write_crs("EPSG:4326", inplace=True) # remove negative values and values greater than 1 @@ -50,6 +51,7 @@ def get_area_potential_onshore( ds_area_potential = ds_area_potential.rio.clip( shapes.geometry, shapes.crs, invert=False ) + ds_area_potential.name = "area_potential" ds_area_potential.to_netcdf(output_path) plot = ds_area_potential.plot() diff --git a/workflow/scripts/report.py b/workflow/scripts/report.py index 3e7073b..41f6167 100644 --- a/workflow/scripts/report.py +++ b/workflow/scripts/report.py @@ -1,20 +1,44 @@ +import geopandas as gpd import pandas as pd import xarray as xr -def report(area_potentials, csv_path, html_path): - report_values = [] +def report(shapes, resampled_path, area_potentials, csv_path, html_path): + shapes = gpd.read_parquet(shapes) + ds_inputs = xr.open_dataset(resampled_path, decode_coords="all") + + for area_potential in area_potentials: + da = xr.open_dataset(area_potential) + ds_inputs[area_potential] = da["area_potential"] + + ds_inputs = ds_inputs.squeeze().drop_vars("band") + + dataframes = [] for area_potential in area_potentials: - ds = xr.open_dataset(area_potential) - val = ds.squeeze().sum().to_dataarray().values[0] / (1e6) # Convert to km² - report_values.append((area_potential, val)) - # report += f"Area potential for {area_potential}: {val:.2f} km²\n" - # with open(report_path, "w") as f: - # f.write(report) - df = pd.DataFrame(report_values, columns=["Area Potential", "Value (km²)"]) - df.to_csv(csv_path, index=False) - df.to_html(html_path, index=False) + dataframes.append( + ds_inputs[area_potential].groupby(ds_inputs["regions"]).sum().to_pandas() + ) + + df = pd.concat(dataframes, axis=1) + df.insert(0, "parent_name", shapes["parent_name"]) + df.insert(0, "shape_class", shapes["shape_class"]) + df.insert(0, "country_id", shapes["country_id"]) + df.insert(0, "shape_id", shapes["shape_id"]) + + df.to_csv(csv_path) + + sums = df.sum(numeric_only=True) + sums.name = "Total" + df = pd.concat([df, sums.to_frame().T]) + + df.to_html(html_path) if __name__ == "__main__": - report(snakemake.input.area_potentials, snakemake.output.csv, snakemake.output.html) + report( + snakemake.input.shapes, + snakemake.input.resampled_path, + snakemake.input.area_potentials, + snakemake.output.csv, + snakemake.output.html, + ) diff --git a/workflow/scripts/resample.py b/workflow/scripts/resample.py index ef64f21..d864ac0 100644 --- a/workflow/scripts/resample.py +++ b/workflow/scripts/resample.py @@ -8,7 +8,6 @@ import xarray as xr from rasterio.enums import Resampling from rasterio.features import rasterize -from rasterio.transform import from_bounds # LAND COVER # Original classification categories taken from GlobCover 2009 land cover. @@ -86,55 +85,6 @@ def get_suitable_land_cover_type(ds_land_cover, suitable_land_cover_types): return suitable_land_cover -def create_empty_geospatial_array( - bounds, - resolution, - projection, -): - """Create an empty geospatial array with specified resolution, projection, and bounds. - - Args: - bounds (tuple): Bounds of the array (minx, miny, maxx, maxy). - resolution (float): Resolution of the array in degrees (default: 30 arc-seconds). - projection (str): CRS of the array (default: "EPSG:4326"). - - Returns: - xarray.DataArray: An empty geospatial array. - """ - # Calculate the number of pixels in x and y directions - minx, miny, maxx, maxy = bounds - width = int((maxx - minx) / resolution) # Number of pixels in x-direction - height = int((maxy - miny) / resolution) # Number of pixels in y-direction - - # Create an empty numpy array filled with NaN - data = np.full((height, width), np.nan, dtype=np.float32) - - # Define the transform (affine transformation) for the array - transform = from_bounds(*bounds, width=width, height=height) - - # Generate longitude and latitude coordinates - longitude = np.linspace(minx, maxx, width) - latitude = np.linspace(maxy, miny, height) - - # Create an xarray.DataArray with the empty data and geospatial metadata - geospatial_array = xr.DataArray( - data, - dims=("y", "x"), # Define dimensions as y (latitude) and x (longitude) - coords={ - "y": ("y", latitude, {"units": "degrees_north"}), # Latitude coordinates - "x": ("x", longitude, {"units": "degrees_east"}), # Longitude coordinates - }, - ) - - # Assign CRS and transform to the DataArray - geospatial_array.rio.write_crs(projection, inplace=True) # Set the CRS - geospatial_array.rio.write_transform( - transform, inplace=True - ) # Set the affine transform - - return geospatial_array - - def _area_of_pixel(pixel_size, center_lat): """Calculate km^2 area of a wgs84 square pixel. @@ -166,7 +116,7 @@ def _area_of_pixel(pixel_size, center_lat): return pixel_size / 360.0 * (area_list[0] - area_list[1]) / 1e6 -def determine_pixel_areas(raster_input, bounds, resolution): +def determine_pixel_areas(raster_input): """Determine area of each pixel. Returns a raster in which the value corresponds to the area in [m2] of the pixel. @@ -175,36 +125,23 @@ def determine_pixel_areas(raster_input, bounds, resolution): Parameters: crs: the coordinate reference system of the data (must be WGS84) - bounds: an object with attributes left/right/top/bottom given in degrees - resolution: the scalar resolution (remember: square pixels) given in degrees """ # the following is based on https://gis.stackexchange.com/a/288034/77760 # and assumes the data to be in EPSG:4326 (WGS84 is similar but with lat,lon instead of lon,lat) assert ( raster_input.rio.crs.to_epsg() == 4326 - ), "masked_rasters does not have the projection EPSG:4326" - minx, miny, maxx, maxy = bounds - width = int((maxx - minx) / resolution) # Number of pixels in x-direction - height = int((maxy - miny) / resolution) # Number of pixels in y-direction - - latitudes = np.linspace( - start=maxy, stop=miny, num=height, endpoint=True, dtype=np.float64 - ) + ), "raster_input does not have the projection EPSG:4326" + resolution = raster_input.rio.resolution()[0] # resolution in degrees varea_of_pixel = np.vectorize(lambda lat: _area_of_pixel(resolution, lat)) - pixel_area = varea_of_pixel(latitudes) # vector - pixel_area = pixel_area.repeat(width).reshape(height, width).astype(np.float64) - pixel_area = xr.DataArray( - pixel_area * 1000**2, # convert to m^2 - coords=raster_input.coords, - dims=raster_input.dims, - name="pixel_area", - ) - return pixel_area + pixel_area = varea_of_pixel(raster_input.y) * 1000**2 # convert to m^2 + + pixel_area_da = xr.DataArray(pixel_area, coords={"y": raster_input.y}, dims="y") + return pixel_area_da @click.command() -@click.argument("projection", type=str) -@click.argument("resolution", type=float) +# @click.argument("projection", type=str) +# @click.argument("resolution", type=float) @click.argument("shapes_path", type=str) @click.argument("land_cover_path", type=str) @click.argument("slope_path", type=str) @@ -214,8 +151,8 @@ def determine_pixel_areas(raster_input, bounds, resolution): @click.argument("plot_path", type=str) def get_same_shape_and_resolution( shapes_path, - projection, - resolution, + # projection, + # resolution, land_cover_path, slope_path, settlement_path, @@ -229,24 +166,37 @@ def get_same_shape_and_resolution( reproject_match ensures all rasters have the same bounds (minlon, minlat, maxlon, maxlat) """ - # create reference raster with the same bounds as given shapes shapes = gpd.read_parquet(shapes_path) - print(f"Number of shapes: {len(shapes)}") - print(f"Requested resolution: {resolution}") - reference_raster = create_empty_geospatial_array( - bounds=shapes.total_bounds, - projection=projection, - resolution=resolution, - ) + print(f"Number of shapes in input data: {len(shapes)}") + + ## + # Land cover + ## + suitable_land_cover_types = sorted(list(set(CoverType.values()))) + ds_land_cover = rxr.open_rasterio(land_cover_path) + reference_raster = xr.ones_like(ds_land_cover) + reference_resolution = ds_land_cover.rio.resolution() + print(f"Land cover resolution: {reference_resolution}") + land_cover = get_suitable_land_cover_type(ds_land_cover, suitable_land_cover_types) + resampled = xr.Dataset() - pixel_area = determine_pixel_areas( - reference_raster, - bounds=shapes.total_bounds, - resolution=resolution, + for land_type in suitable_land_cover_types: + resampled[f"landcover_{land_type}"] = land_cover[land_type].rio.reproject_match( + reference_raster, resampling=Resampling.average + ) + + # Drop the "band" dimension + resampled = resampled.squeeze().drop_vars("band") + + ## + # Pixel area + ## + + pixel_area = determine_pixel_areas(resampled) + resampled["pixel_area"] = pixel_area.expand_dims({"x": resampled.x}).transpose( + "y", "x" ) - # print(f"Pixel area: {pixel_area.dims}, {pixel_area}") - resampled["pixel_area"] = pixel_area ## # Regions @@ -263,42 +213,15 @@ def get_same_shape_and_resolution( ) resampled["regions"] = (("y", "x"), rasterized) + ## # Slope + ## da_slope = rxr.open_rasterio(slope_path, masked=True) / 100 print(f"Slope resolution: {da_slope.rio.resolution()}") resampled["slope"] = da_slope.astype(float).rio.reproject_match( reference_raster, resampling=Resampling.average ) - # ds_slope = xr.open_dataset(slope_path) - # for slope_class in ds_slope.data_vars: - # resampled[f"slope_{slope_class}"] = ds_slope.astype(float).rio.reproject_match( - # reference_raster, resampling=Resampling.average - # ) - - # resampled["slope_too_steep"] = slope_too_steep.astype(float).rio.reproject_match( - # reference_raster, resampling=Resampling.average - # ) - - ## - # Land cover - ## - suitable_land_cover_types = sorted(list(set(CoverType.values()))) - ds_land_cover = rxr.open_rasterio(land_cover_path) - print(f"Land cover resolution: {ds_land_cover.rio.resolution()}") - land_cover = get_suitable_land_cover_type(ds_land_cover, suitable_land_cover_types) - - for land_type in suitable_land_cover_types: - # skip = [] - # if value == 0: - # skip.append(land_type) # skip land cover types with 0 weight - # else: - resampled[f"landcover_{land_type}"] = land_cover[land_type].rio.reproject_match( - reference_raster, resampling=Resampling.average - ) - - # print(f"Skip the land cover types not used in this tech: {skip}") - ## # Settlement in sum of area of built-up surface (m2) ## @@ -308,7 +231,8 @@ def get_same_shape_and_resolution( resampled["settlement"] = ds_settlement.rio.reproject_match( reference_raster, resampling=Resampling.sum ) - # print("resampled settlement", resampled.dims, resampled.coords, resampled) + # get fraction of settlement (built-up surface) compared to pixel area, both in m2 + resampled["settlement"] = resampled["settlement"] / resampled["pixel_area"] ## # Protected areas @@ -320,9 +244,10 @@ def get_same_shape_and_resolution( protected_areas = protected_areas.cx[xmin:xmax, ymin:ymax] print(f"Protected areas after applying total_bounds: {len(protected_areas)}") - resampled["protected"] = reference_raster.fillna(1).rio.clip( + resampled["protected"] = reference_raster.rio.clip( protected_areas.geometry, protected_areas.crs ) + resampled["protected"] = resampled["protected"].fillna(0) resampled.to_netcdf(output_path) From 55808e19fe3b6de7715c6c046692ece45f55d175 Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Tue, 8 Jul 2025 10:38:32 +0200 Subject: [PATCH 21/59] Process bathymetry at the start; minor cleanup --- workflow/internal/settings.yaml | 8 +++-- workflow/rules/automatic.smk | 4 +-- workflow/rules/process.smk | 16 ++++----- workflow/scripts/potential_offshore.py | 20 +++++------ workflow/scripts/potential_onshore.py | 3 +- workflow/scripts/report.py | 16 ++++++--- workflow/scripts/resample.py | 47 +++++++++++++++----------- 7 files changed, 64 insertions(+), 50 deletions(-) diff --git a/workflow/internal/settings.yaml b/workflow/internal/settings.yaml index b77090d..582759d 100644 --- a/workflow/internal/settings.yaml +++ b/workflow/internal/settings.yaml @@ -5,8 +5,12 @@ resources: # Links for automatically downloaded files ## # Slope - slope: "https://s3.opengeohub.org/global/dtm/v3/slope.in.degree_edtm_m_60m_s_20000101_20221231_go_epsg.4326_v20241230.tif" - # Bathymetry + # 60m version: + # slope: "https://s3.opengeohub.org/global/dtm/v3/slope.in.degree_edtm_m_60m_s_20000101_20221231_go_epsg.4326_v20241230.tif" + # 120m version: + # slope: "https://zenodo.org/records/14920379/files/slope.in.degree_edtm_m_120m_s_20000101_20221231_go_epsg.4326_v20241230.tif" + # 240m version: + slope: "https://zenodo.org/records/14920379/files/slope.in.degree_edtm_m_240m_s_20000101_20221231_go_epsg.4326_v20241230.tif" bathymetry: "https://zenodo.org/records/15741950/files/gebco_2024_sub_ice_cog.tif" # Land cover globcover: "https://due.esrin.esa.int/files/Globcover2009_V2.3_Global_.zip" diff --git a/workflow/rules/automatic.smk b/workflow/rules/automatic.smk index e3ad22c..ae992a6 100644 --- a/workflow/rules/automatic.smk +++ b/workflow/rules/automatic.smk @@ -10,7 +10,7 @@ rule download_cutout_slope: output: path="resources/cutout/slope.tif", wrapper: - "https://github.com/irm-codebase/snakemake-wrappers/raw/rasterio-tiff-clipping/geo/rasterio/clip-geotiff" + "v7.2.0/geo/rasterio/clip-geotiff" rule download_cutout_bathymetry: message: @@ -22,7 +22,7 @@ rule download_cutout_bathymetry: output: path="resources/cutout/bathymetry.tif", wrapper: - "https://github.com/irm-codebase/snakemake-wrappers/raw/rasterio-tiff-clipping/geo/rasterio/clip-geotiff" + "v7.2.0/geo/rasterio/clip-geotiff" rule download_wdpa: message: diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index 4b147c6..10d5465 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -11,11 +11,12 @@ rule resample_same_resolution: land_cover_path=rules.cutout_landcover.output, slope_path=rules.download_cutout_slope.output, settlement_path=rules.cutout_settlement.output, + bathymetry_path=rules.download_cutout_bathymetry.output, protected_area_path=rules.unzip_wdpa.output, output: resampled_input="resources/resampled_inputs.nc", plot=report( - "resources/resampled_inputs.pdf", + "resources/resampled_inputs.png", category="resampled_input", ), conda: @@ -23,7 +24,7 @@ rule resample_same_resolution: shell: """ python "{input.script}" \ - "{input.shapes}" "{input.land_cover_path}" "{input.slope_path}" "{input.settlement_path}" "{input.protected_area_path}" \ + "{input.shapes}" "{input.land_cover_path}" "{input.slope_path}" "{input.settlement_path}" "{input.bathymetry_path}" "{input.protected_area_path}" \ "{output.resampled_input}" "{output.plot}" """ @@ -60,7 +61,7 @@ rule area_potential_onshore: shapes="resources/user/shapes.parquet", masked_path=rules.technical_mask_onshore.output.technical_mask, output: - area_potential="results/area_potential_{tech_onshore}.nc", + area_potential="results/area_potential_{tech_onshore}.tif", plot=report( "results/area_potential_{tech_onshore}.png", category="area_potential", @@ -81,10 +82,9 @@ rule area_potential_offshore: input: script=workflow.source_path("../scripts/potential_offshore.py"), shapes="resources/user/shapes.parquet", - bathymetry_path=rules.download_cutout_bathymetry.output, resampled_input_path=rules.resample_same_resolution.output.resampled_input, output: - area_potential="results/area_potential_{tech_offshore}.nc", + area_potential="results/area_potential_{tech_offshore}.tif", plot=report( "results/area_potential_{tech_offshore}.png", category="area_potential", @@ -96,7 +96,7 @@ rule area_potential_offshore: shell: """ python "{input.script}" "{input.shapes}" \ - "{input.bathymetry_path}" "{params.water_depth}" "{input.resampled_input_path}" "{params.weight}" "{output.area_potential}" "{output.plot}" 2> "{log}" + "{params.water_depth}" "{input.resampled_input_path}" "{params.weight}" "{output.area_potential}" "{output.plot}" 2> "{log}" """ @@ -107,10 +107,10 @@ rule area_potential_report: shapes="resources/user/shapes.parquet", resampled_path=rules.resample_same_resolution.output.resampled_input, area_potentials=expand( - "results/area_potential_{tech}.nc", + "results/area_potential_{tech}.tif", tech=config["techs_offshore"].keys(), ) + expand( - "results/area_potential_{tech}.nc", + "results/area_potential_{tech}.tif", tech=config["techs_onshore"].keys(), ), output: diff --git a/workflow/scripts/potential_offshore.py b/workflow/scripts/potential_offshore.py index b49f7e7..0694a7b 100644 --- a/workflow/scripts/potential_offshore.py +++ b/workflow/scripts/potential_offshore.py @@ -2,15 +2,12 @@ import geo import geopandas as gpd import matplotlib.pyplot as plt -import rioxarray as rxr import xarray as xr import yaml -from rasterio.enums import Resampling @click.command() @click.argument("shapes_path", type=str) -@click.argument("bathymetry_path", type=str) @click.argument("water_depth", type=str) @click.argument("resampled_input_path", type=str) @click.argument("weight", type=float) @@ -18,7 +15,6 @@ @click.argument("plot_path", type=str) def get_area_potential_offshore( shapes_path, - bathymetry_path, water_depth, resampled_input_path, weight, @@ -39,17 +35,15 @@ def get_area_potential_offshore( # get bathymetry within the water depth range then resample water_depth = yaml.safe_load(water_depth) - ds_bathymetry = rxr.open_rasterio(bathymetry_path) + masked_bathymetry = ( - (ds_bathymetry < water_depth["max"]) & (ds_bathymetry >= water_depth["min"]) + (ds_inputs["bathymetry"] < water_depth["max"]) + & (ds_inputs["bathymetry"] >= water_depth["min"]) ).astype(float) - ds_inputs["bathymetry"] = masked_bathymetry.rio.reproject_match( - ds_inputs["pixel_area"], resampling=Resampling.average - ) - # keep bathymetry inside geo boundaries, i.e. within the exclusive economic zone (EEZ) - eligible_fraction = ds_inputs["bathymetry"].rio.clip( + # FIXME: cut this to the EEZ boundary regions specifically, excluding land areas + eligible_fraction = masked_bathymetry.rio.clip( shapes.geometry, shapes.crs, invert=False ) @@ -71,7 +65,9 @@ def get_area_potential_offshore( {"area_potential": eligible_fraction * weight * ds_inputs["pixel_area"]}, coords=ds_inputs["pixel_area"].coords, )["area_potential"] - da_area_potential.to_netcdf(output_path) + + da_area_potential = da_area_potential.transpose("band", "y", "x") + da_area_potential.rio.to_raster(output_path, driver="GTiff", compress="LZW") plot = da_area_potential.plot() plt.savefig(plot_path, bbox_inches="tight") diff --git a/workflow/scripts/potential_onshore.py b/workflow/scripts/potential_onshore.py index 4b37920..ed55208 100644 --- a/workflow/scripts/potential_onshore.py +++ b/workflow/scripts/potential_onshore.py @@ -52,7 +52,8 @@ def get_area_potential_onshore( shapes.geometry, shapes.crs, invert=False ) ds_area_potential.name = "area_potential" - ds_area_potential.to_netcdf(output_path) + ds_area_potential = ds_area_potential.transpose("band", "y", "x") + ds_area_potential.rio.to_raster(output_path, driver="GTiff", compress="LZW") plot = ds_area_potential.plot() plt.savefig(plot_path, bbox_inches="tight") diff --git a/workflow/scripts/report.py b/workflow/scripts/report.py index 41f6167..52bd080 100644 --- a/workflow/scripts/report.py +++ b/workflow/scripts/report.py @@ -1,5 +1,6 @@ import geopandas as gpd import pandas as pd +import rioxarray as rxr import xarray as xr @@ -7,19 +8,22 @@ def report(shapes, resampled_path, area_potentials, csv_path, html_path): shapes = gpd.read_parquet(shapes) ds_inputs = xr.open_dataset(resampled_path, decode_coords="all") + # Collect the area potentials from the input files for area_potential in area_potentials: - da = xr.open_dataset(area_potential) - ds_inputs[area_potential] = da["area_potential"] - + ds_inputs[area_potential] = rxr.open_rasterio(area_potential) ds_inputs = ds_inputs.squeeze().drop_vars("band") + # Group the area potentials by regions, sum them up, and collect the resulting Series + # into a DataFrame, where each column corresponds to a technology's area potential, + # and the index corresponds to the regions. dataframes = [] for area_potential in area_potentials: dataframes.append( ds_inputs[area_potential].groupby(ds_inputs["regions"]).sum().to_pandas() ) - df = pd.concat(dataframes, axis=1) + + # Add metadata columns from shapes in front of the data columns df.insert(0, "parent_name", shapes["parent_name"]) df.insert(0, "shape_class", shapes["shape_class"]) df.insert(0, "country_id", shapes["country_id"]) @@ -27,11 +31,13 @@ def report(shapes, resampled_path, area_potentials, csv_path, html_path): df.to_csv(csv_path) + # After saving the CSV and before saving a HTML file, + # we add a "total" row for the numeric columns sums = df.sum(numeric_only=True) sums.name = "Total" df = pd.concat([df, sums.to_frame().T]) - df.to_html(html_path) + df.to_html(html_path, float_format=lambda x: f"{x/1e6:.2f}") if __name__ == "__main__": diff --git a/workflow/scripts/resample.py b/workflow/scripts/resample.py index d864ac0..7a960c9 100644 --- a/workflow/scripts/resample.py +++ b/workflow/scripts/resample.py @@ -12,7 +12,6 @@ # LAND COVER # Original classification categories taken from GlobCover 2009 land cover. # From Troendle et al. (2019) https://github.com/timtroendle/possibility-for-electricity-autarky -# suitable land cover types are defined in config.yaml, as 1, other types are 0 GlobCover = { @@ -139,33 +138,36 @@ def determine_pixel_areas(raster_input): return pixel_area_da +def _rasterize_regions(shapes, reference_raster): + regions = [(geom, idx) for idx, geom in zip(shapes.index, shapes.geometry)] + return rasterize( + shapes=regions, + out_shape=reference_raster.rio.shape, + transform=reference_raster.rio.transform(), + fill=np.nan, + dtype=np.float32, + ) + + @click.command() -# @click.argument("projection", type=str) -# @click.argument("resolution", type=float) @click.argument("shapes_path", type=str) @click.argument("land_cover_path", type=str) @click.argument("slope_path", type=str) @click.argument("settlement_path", type=str) +@click.argument("bathymetry_path", type=str) @click.argument("protected_area_path", type=str) @click.argument("output_path", type=str) @click.argument("plot_path", type=str) def get_same_shape_and_resolution( shapes_path, - # projection, - # resolution, land_cover_path, slope_path, settlement_path, + bathymetry_path, protected_area_path, output_path, plot_path, ): - """Resample and crop the raster_input. - - Goal is to have the same bounds, projection, and resolution as the reference_raster: - reproject_match ensures all rasters have the same bounds - (minlon, minlat, maxlon, maxlat) - """ shapes = gpd.read_parquet(shapes_path) print(f"Number of shapes in input data: {len(shapes)}") @@ -202,16 +204,11 @@ def get_same_shape_and_resolution( # Regions ## - regions = ((geom, idx) for idx, geom in zip(shapes.index, shapes.geometry)) - rasterized = rasterize( - shapes=regions, - out_shape=reference_raster.rio.shape, - transform=reference_raster.rio.transform(), - all_touched=True, - fill=np.nan, - dtype=np.float32, + + resampled["regions"] = ( + ("y", "x"), + _rasterize_regions(shapes, reference_raster), ) - resampled["regions"] = (("y", "x"), rasterized) ## # Slope @@ -234,6 +231,16 @@ def get_same_shape_and_resolution( # get fraction of settlement (built-up surface) compared to pixel area, both in m2 resampled["settlement"] = resampled["settlement"] / resampled["pixel_area"] + ## + # Bathymetry + ## + + ds_bathymetry = rxr.open_rasterio(bathymetry_path) + print(f"Bathymetry resolution: {ds_bathymetry.rio.resolution()}") + resampled["bathymetry"] = ds_bathymetry.rio.reproject_match( + reference_raster, resampling=Resampling.average + ) + ## # Protected areas ## From 8440e6ea9a8c8fb5164e805ee6aef0041bec4940 Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Tue, 8 Jul 2025 10:59:15 +0200 Subject: [PATCH 22/59] Make shape a wildcard --- workflow/Snakefile | 5 ++++ workflow/rules/automatic.smk | 8 +++--- workflow/rules/prepare.smk | 8 +++--- workflow/rules/process.smk | 48 +++++++++++++++++------------------- 4 files changed, 35 insertions(+), 34 deletions(-) diff --git a/workflow/Snakefile b/workflow/Snakefile index 8289fc3..946747b 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -20,6 +20,11 @@ with open(workflow.source_path("internal/settings.yaml"), "r") as f: workflow.source_path("scripts/script_utils.py") workflow.source_path("scripts/geo.py") +wildcard_constraints: + shape="[a-zA-Z0-9_-]+", + tech_onshore="|".join(config["techs_onshore"].keys()), + tech_offshore="|".join(config["techs_offshore"].keys()), + # Add all your includes here. include: "rules/automatic.smk" include: "rules/prepare.smk" diff --git a/workflow/rules/automatic.smk b/workflow/rules/automatic.smk index ae992a6..ab566eb 100644 --- a/workflow/rules/automatic.smk +++ b/workflow/rules/automatic.smk @@ -6,9 +6,9 @@ rule download_cutout_slope: params: cog_url=internal["resources"]["automatic"]["slope"], input: - vector="resources/user/shapes.parquet", + vector="resources/user/shapes/{shape}.parquet", output: - path="resources/cutout/slope.tif", + path="resources/cutout/{shape}/slope.tif", wrapper: "v7.2.0/geo/rasterio/clip-geotiff" @@ -18,9 +18,9 @@ rule download_cutout_bathymetry: params: cog_url=internal["resources"]["automatic"]["bathymetry"], input: - vector="resources/user/shapes.parquet", + vector="resources/user/shapes/{shape}.parquet", output: - path="resources/cutout/bathymetry.tif", + path="resources/cutout/{shape}/bathymetry.tif", wrapper: "v7.2.0/geo/rasterio/clip-geotiff" diff --git a/workflow/rules/prepare.smk b/workflow/rules/prepare.smk index e8eb0f5..419ddbe 100644 --- a/workflow/rules/prepare.smk +++ b/workflow/rules/prepare.smk @@ -4,10 +4,10 @@ rule cutout_landcover: message: "Cut land cover data to the bounds of the input shapefile." input: - shapes="resources/user/shapes.parquet", + shapes="resources/user/shapes/{shape}.parquet", landcover=rules.unzip_globcover.output, output: - "resources/cutout/landcover.tif", + "resources/cutout/{shape}/landcover.tif", conda: "../envs/default.yaml" shell: @@ -20,10 +20,10 @@ rule cutout_settlement: message: "Cut settlement data to the bounds of the input shapefile." input: - shapes="resources/user/shapes.parquet", + shapes="resources/user/shapes/{shape}.parquet", settlement=rules.unzip_ghsl.output, output: - "resources/cutout/settlement.tif", + "resources/cutout/{shape}/settlement.tif", conda: "../envs/default.yaml" shell: diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index 10d5465..8eb8a88 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -1,22 +1,18 @@ -wildcard_constraints: - tech_onshore="|".join(config["techs_onshore"].keys()), - tech_offshore="|".join(config["techs_offshore"].keys()), - rule resample_same_resolution: message: - "Resample inputs to the projection and resolution of the land cover data, while aggregating land cover types.", + "Resample inputs for {wildcards.shape} to the projection and resolution of the land cover data, while aggregating land cover types.", input: script=workflow.source_path("../scripts/resample.py"), - shapes="resources/user/shapes.parquet", + shapes="resources/user/shapes/{shape}.parquet", land_cover_path=rules.cutout_landcover.output, slope_path=rules.download_cutout_slope.output, settlement_path=rules.cutout_settlement.output, bathymetry_path=rules.download_cutout_bathymetry.output, protected_area_path=rules.unzip_wdpa.output, output: - resampled_input="resources/resampled_inputs.nc", + resampled_input="resources/{shape}/resampled_inputs.nc", plot=report( - "resources/resampled_inputs.png", + "resources/{shape}/resampled_inputs.png", category="resampled_input", ), conda: @@ -30,7 +26,7 @@ rule resample_same_resolution: rule technical_mask_onshore: message: - "Get fraction satisfied all technical criteria: not too steep slope, suitable land cover, and not exceeding max_settlement for the tech {wildcards.tech_onshore}.", + "Get fraction satisfied all technical criteria: not too steep slope, suitable land cover, and not exceeding max_settlement for the tech {wildcards.tech_onshore} and shape {wildcards.shape}.", params: suitable_land_cover_types=lambda wildcards: config["techs_onshore"][f"{wildcards.tech_onshore}"]["land_cover"], max_slope=lambda wildcards: config["techs_onshore"][f"{wildcards.tech_onshore}"]["max_slope"], @@ -39,9 +35,9 @@ rule technical_mask_onshore: script=workflow.source_path("../scripts/apply_technical_mask.py"), resampled_path=rules.resample_same_resolution.output.resampled_input, output: - technical_mask="resources/technical_mask_{tech_onshore}.nc", + technical_mask="resources/{shape}/technical_mask_{tech_onshore}.nc", # plot=report( - # "resources/technical_mask_{tech_onshore}.pdf", + # "resources/{shape}/technical_mask_{tech_onshore}.pdf", # category="technical_mask", # ), conda: @@ -53,17 +49,17 @@ rule technical_mask_onshore: rule area_potential_onshore: message: - "Apply weights then calculate the potential area for the tech {wildcards.tech_onshore}.", + "Compute onshore area potential for the tech {wildcards.tech_onshore} and shape {wildcards.shape}." params: technical_mask=lambda wildcards: config["techs_onshore"][f"{wildcards.tech_onshore}"] input: script=workflow.source_path("../scripts/potential_onshore.py"), - shapes="resources/user/shapes.parquet", + shapes="resources/user/shapes/{shape}.parquet", masked_path=rules.technical_mask_onshore.output.technical_mask, output: - area_potential="results/area_potential_{tech_onshore}.tif", + area_potential="results/{shape}/area_potential_{tech_onshore}.tif", plot=report( - "results/area_potential_{tech_onshore}.png", + "results/{shape}/area_potential_{tech_onshore}.png", category="area_potential", ), conda: @@ -75,22 +71,22 @@ rule area_potential_onshore: rule area_potential_offshore: message: - "Get area potential for the tech {wildcards.tech_offshore}" + "Compute offshore area potential for the tech {wildcards.tech_offshore} and shape {wildcards.shape}." params: water_depth=lambda wildcards: config["techs_offshore"][f"{wildcards.tech_offshore}"]["water_depth"], weight=lambda wildcards: config["techs_offshore"][f"{wildcards.tech_offshore}"]["weight"], input: script=workflow.source_path("../scripts/potential_offshore.py"), - shapes="resources/user/shapes.parquet", + shapes="resources/user/shapes/{shape}.parquet", resampled_input_path=rules.resample_same_resolution.output.resampled_input, output: - area_potential="results/area_potential_{tech_offshore}.tif", + area_potential="results/{shape}/area_potential_{tech_offshore}.tif", plot=report( - "results/area_potential_{tech_offshore}.png", + "results/{shape}/area_potential_{tech_offshore}.png", category="area_potential", ), log: - "logs/area_potential_{tech_offshore}.log", + "logs/area_potential_{shape}_{tech_offshore}.log", conda: "../envs/default.yaml" shell: @@ -102,21 +98,21 @@ rule area_potential_offshore: rule area_potential_report: message: - "Generate an overview report of the area potential for all techs.", + "Generate an overview report of the area potential for all techs in shape {wildcards.shape}.", input: - shapes="resources/user/shapes.parquet", + shapes="resources/user/shapes/{shape}.parquet", resampled_path=rules.resample_same_resolution.output.resampled_input, area_potentials=expand( - "results/area_potential_{tech}.tif", + "results/{{shape}}/area_potential_{tech}.tif", tech=config["techs_offshore"].keys(), ) + expand( - "results/area_potential_{tech}.tif", + "results/{{shape}}/area_potential_{tech}.tif", tech=config["techs_onshore"].keys(), ), output: - csv="results/area_potential_report.csv", + csv="results/{shape}/area_potential_report.csv", html=report( - "results/area_potential_report.html", + "results/{shape}/area_potential_report.html", category="area_potential_report", ), conda: From 98e959ff391c68c39b213a4533225a60b35482ae Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Tue, 8 Jul 2025 11:28:06 +0200 Subject: [PATCH 23/59] Make UTM-based buffering configurable --- config/config.yaml | 6 ++++++ workflow/internal/config.schema.yaml | 3 +++ workflow/rules/process.smk | 3 ++- workflow/scripts/potential_offshore.py | 12 +++++++++--- 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/config/config.yaml b/config/config.yaml index b1905e1..0e3540f 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -1,3 +1,9 @@ +# Options for buffering: either a string of the form "epsg:xxxx" or "UTM"/"utm" +# - "UTM": project each shape to the UTM zone of its centroid for buffering +# - "epsg:xxxx": use the specified CRS for all buffering +# A good option is "epsg:8857" (WGS 84 / Equal Earth Greenwich) for global coverage +buffer_crs: "epsg:8857" + # Technical criteria techs_onshore: pv_open_field: diff --git a/workflow/internal/config.schema.yaml b/workflow/internal/config.schema.yaml index a0dae55..daf59d7 100644 --- a/workflow/internal/config.schema.yaml +++ b/workflow/internal/config.schema.yaml @@ -3,6 +3,9 @@ description: "Schema for user-provided configuration files." type: object additionalProperties: false properties: + buffer_crs: + type: string + description: "CRS for buffering shapes. Use 'UTM' for UTM zones or 'epsg:xxxx' for a specific CRS." specs: type: object properties: diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index 8eb8a88..b16223d 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -75,6 +75,7 @@ rule area_potential_offshore: params: water_depth=lambda wildcards: config["techs_offshore"][f"{wildcards.tech_offshore}"]["water_depth"], weight=lambda wildcards: config["techs_offshore"][f"{wildcards.tech_offshore}"]["weight"], + buffer_crs=lambda wildcards: config["buffer_crs"], input: script=workflow.source_path("../scripts/potential_offshore.py"), shapes="resources/user/shapes/{shape}.parquet", @@ -92,7 +93,7 @@ rule area_potential_offshore: shell: """ python "{input.script}" "{input.shapes}" \ - "{params.water_depth}" "{input.resampled_input_path}" "{params.weight}" "{output.area_potential}" "{output.plot}" 2> "{log}" + "{params.water_depth}" "{input.resampled_input_path}" "{params.weight}" "{params.buffer_crs}" "{output.area_potential}" "{output.plot}" 2> "{log}" """ diff --git a/workflow/scripts/potential_offshore.py b/workflow/scripts/potential_offshore.py index 0694a7b..77a9d24 100644 --- a/workflow/scripts/potential_offshore.py +++ b/workflow/scripts/potential_offshore.py @@ -11,6 +11,7 @@ @click.argument("water_depth", type=str) @click.argument("resampled_input_path", type=str) @click.argument("weight", type=float) +@click.argument("buffer_crs", type=str) @click.argument("output_path", type=str) @click.argument("plot_path", type=str) def get_area_potential_offshore( @@ -18,6 +19,7 @@ def get_area_potential_offshore( water_depth, resampled_input_path, weight, + buffer_crs, output_path, plot_path, ): @@ -49,9 +51,13 @@ def get_area_potential_offshore( # Buffer 10 km from country shape, no wind offshore too close to the coastline shapes_land = shapes[shapes["shape_class"] == "land"] - sea_buffer = geo.apply_utm_buffer(shapes_land, buffer_distance_m=10000).to_crs( - ds_inputs.rio.crs - )["geometry"] + if buffer_crs.lower() == "utm": + sea_buffer = geo.apply_utm_buffer(shapes_land, buffer_distance_m=10000).to_crs( + ds_inputs.rio.crs + )["geometry"] + else: + sea_buffer = shapes_land.to_crs(buffer_crs).buffer(10_000) + buffer_geo = gpd.GeoDataFrame(geometry=sea_buffer).to_crs(ds_inputs.rio.crs) eligible_fraction = eligible_fraction.rio.clip( buffer_geo.geometry, buffer_geo.crs, invert=True From 9b39da030cf9f5bd9657d2e700a9acdadb833d32 Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Tue, 8 Jul 2025 19:22:08 +0200 Subject: [PATCH 24/59] Simplify and generalise the area potential processing; Cleanup --- config/config.yaml | 103 +++++++++++++++-------- workflow/Snakefile | 3 +- workflow/internal/config.schema.yaml | 46 ++++------ workflow/rules/automatic.smk | 16 ++-- workflow/rules/prepare.smk | 4 +- workflow/rules/process.smk | 84 ++++-------------- workflow/scripts/apply_technical_mask.py | 46 ---------- workflow/scripts/area_potential.py | 94 +++++++++++++++++++++ workflow/scripts/potential_offshore.py | 83 ------------------ workflow/scripts/potential_onshore.py | 63 -------------- workflow/scripts/report.py | 4 +- workflow/scripts/resample.py | 60 ++++++++++--- workflow/scripts/script_utils.py | 4 +- 13 files changed, 255 insertions(+), 355 deletions(-) delete mode 100644 workflow/scripts/apply_technical_mask.py create mode 100644 workflow/scripts/area_potential.py delete mode 100644 workflow/scripts/potential_offshore.py delete mode 100644 workflow/scripts/potential_onshore.py diff --git a/config/config.yaml b/config/config.yaml index 0e3540f..14d0224 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -4,42 +4,73 @@ # A good option is "epsg:8857" (WGS 84 / Equal Earth Greenwich) for global coverage buffer_crs: "epsg:8857" -# Technical criteria -techs_onshore: +techs: + pv_rooftop: + initial_area: settlement_area + continuous_layers: + settlement_share: + min: 0.01 + max: 1 + share: 0.8 + binary_layers: + regions_maritime: 0 + regions_land: 1 + protected: 0 + # Include all land cover types; selection is done by settlement_share + landcover_FARM: 1 + landcover_FOREST: 1 + landcover_URBAN: 1 + landcover_OTHER: 1 + landcover_NOT_SUITABLE: 0 + landcover_WATER: 0 pv_open_field: - max_slope: 3 - land_cover: - FARM: 0.1 - FOREST: 0 # no PV open field in forest - OTHER: 0.2 - URBAN: 0 # no PV open field in urban - settlement: - max_settlement: 0.01 # no PV open field in settlement - weight: -1 + initial_area: pixel_area + continuous_layers: + slope: + min: 0 + max: 3 + settlement_share: + min: 0 + max: 0.01 + binary_layers: + regions_maritime: 0 + regions_land: 1 + protected: 0 + landcover_FARM: 0.1 + landcover_FOREST: 0 + landcover_URBAN: 0 + landcover_OTHER: 0.2 + landcover_NOT_SUITABLE: 0 + landcover_WATER: 0 wind_onshore: - max_slope: 20 - land_cover: - FARM: 0.2 - FOREST: 0.05 - OTHER: 0.3 - URBAN: 0 # no wind onshore in urban - settlement: - max_settlement: 0.01 - weight: -1 - pv_rooftop: - max_slope: 90 - land_cover: - FARM: 0 - FOREST: 0 - OTHER: 0 - URBAN: 1 - settlement: - max_settlement: 1 - weight: 0.8 - -techs_offshore: + initial_area: pixel_area + continuous_layers: + slope: + min: 0 + max: 20 + settlement_share: + min: 0 + max: 0.01 + binary_layers: + regions_maritime: 0 + regions_land: 1 + protected: 0 + landcover_FARM: 0.2 + landcover_FOREST: 0.05 + landcover_URBAN: 0 + landcover_OTHER: 0.3 + landcover_NOT_SUITABLE: 0 + landcover_WATER: 0 wind_offshore: - water_depth: - max: 0 - min: -50 - weight: 0.8 + initial_area: pixel_area + continuous_layers: + bathymetry: + min: -50 + max: 0 + share: 0.8 + binary_layers: + regions_land: 0 + regions_maritime: 1 + protected: 0 + shapes_buffer: + land: 10000 # meters diff --git a/workflow/Snakefile b/workflow/Snakefile index 946747b..dddc777 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -22,8 +22,7 @@ workflow.source_path("scripts/geo.py") wildcard_constraints: shape="[a-zA-Z0-9_-]+", - tech_onshore="|".join(config["techs_onshore"].keys()), - tech_offshore="|".join(config["techs_offshore"].keys()), + tech="|".join(config["techs"].keys()), # Add all your includes here. include: "rules/automatic.smk" diff --git a/workflow/internal/config.schema.yaml b/workflow/internal/config.schema.yaml index daf59d7..efa9cb0 100644 --- a/workflow/internal/config.schema.yaml +++ b/workflow/internal/config.schema.yaml @@ -16,41 +16,29 @@ properties: required: [projection, resolution] additionalProperties: false - techs_onshore: + techs: type: object additionalProperties: type: object properties: - max_slope: - type: number - land_cover: + initial_area: + type: string + enum: ["settlement_area", "pixel_area"] + continuous_layers: type: object additionalProperties: - type: number - settlement: - type: object - properties: - max_settlement: - type: number - weight: - type: number - required: [max_settlement, weight] - additionalProperties: false - required: [max_slope, land_cover, settlement] - additionalProperties: false - - techs_offshore: - type: object - additionalProperties: - type: object - properties: - water_depth: + type: object + properties: + min: + type: number + max: + type: number + required: ["min", "max"] + binary_layers: type: object additionalProperties: type: number - weight: - type: number - required: [water_depth, weight] - additionalProperties: false - -required: [techs_onshore, techs_offshore] + shapes_buffer: + type: object + required: ["initial_area", "continuous_layers", "binary_layers"] + additionalProperties: false \ No newline at end of file diff --git a/workflow/rules/automatic.smk b/workflow/rules/automatic.smk index ab566eb..83b4bab 100644 --- a/workflow/rules/automatic.smk +++ b/workflow/rules/automatic.smk @@ -8,7 +8,7 @@ rule download_cutout_slope: input: vector="resources/user/shapes/{shape}.parquet", output: - path="resources/cutout/{shape}/slope.tif", + path="resources/automatic/cutout/{shape}/slope.tif", wrapper: "v7.2.0/geo/rasterio/clip-geotiff" @@ -20,7 +20,7 @@ rule download_cutout_bathymetry: input: vector="resources/user/shapes/{shape}.parquet", output: - path="resources/cutout/{shape}/bathymetry.tif", + path="resources/automatic/cutout/{shape}/bathymetry.tif", wrapper: "v7.2.0/geo/rasterio/clip-geotiff" @@ -30,7 +30,7 @@ rule download_wdpa: params: url=internal["resources"]["automatic"]["wdpa"], output: - "resources/automatic/wdpa.zip", + "resources/automatic/global/wdpa.zip", conda: "../envs/shell.yaml" shell: @@ -44,7 +44,7 @@ rule unzip_wdpa: input: rules.download_wdpa.output, output: - directory("resources/automatic/wdpa.gdb"), + directory("resources/automatic/global/wdpa.gdb"), conda: "../envs/shell.yaml" shell: @@ -61,7 +61,7 @@ rule download_globcover: params: url=internal["resources"]["automatic"]["globcover"], output: - "resources/automatic/globcover.zip", + "resources/automatic/global/globcover.zip", conda: "../envs/shell.yaml" shell: @@ -75,7 +75,7 @@ rule unzip_globcover: input: rules.download_globcover.output, output: - "resources/automatic/globcover-landcover.tif", + "resources/automatic/global/globcover-landcover.tif", log: "logs/unzip_globcover.log", conda: @@ -94,7 +94,7 @@ rule download_ghsl: params: url=internal["resources"]["automatic"]["ghsl"], output: - "resources/automatic/ghsl_built_s.zip", + "resources/automatic/global/ghsl_built_s.zip", conda: "../envs/shell.yaml" shell: @@ -108,7 +108,7 @@ rule unzip_ghsl: input: rules.download_ghsl.output, output: - "resources/automatic/ghsl_built_s.tif", + "resources/automatic/global/ghsl_built_s.tif", conda: "../envs/shell.yaml" shell: diff --git a/workflow/rules/prepare.smk b/workflow/rules/prepare.smk index 419ddbe..3d06eda 100644 --- a/workflow/rules/prepare.smk +++ b/workflow/rules/prepare.smk @@ -7,7 +7,7 @@ rule cutout_landcover: shapes="resources/user/shapes/{shape}.parquet", landcover=rules.unzip_globcover.output, output: - "resources/cutout/{shape}/landcover.tif", + "resources/automatic/cutout/{shape}/landcover.tif", conda: "../envs/default.yaml" shell: @@ -23,7 +23,7 @@ rule cutout_settlement: shapes="resources/user/shapes/{shape}.parquet", settlement=rules.unzip_ghsl.output, output: - "resources/cutout/{shape}/settlement.tif", + "resources/automatic/cutout/{shape}/settlement.tif", conda: "../envs/default.yaml" shell: diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index b16223d..5bd5b1a 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -1,4 +1,4 @@ -rule resample_same_resolution: +rule prepare_resampled_inputs: message: "Resample inputs for {wildcards.shape} to the projection and resolution of the land cover data, while aggregating land cover types.", input: @@ -10,9 +10,9 @@ rule resample_same_resolution: bathymetry_path=rules.download_cutout_bathymetry.output, protected_area_path=rules.unzip_wdpa.output, output: - resampled_input="resources/{shape}/resampled_inputs.nc", + resampled_input="resources/automatic/{shape}.resampled_inputs.nc", plot=report( - "resources/{shape}/resampled_inputs.png", + "resources/automatic/{shape}.resampled_inputs.png", category="resampled_input", ), conda: @@ -24,92 +24,40 @@ rule resample_same_resolution: "{output.resampled_input}" "{output.plot}" """ -rule technical_mask_onshore: +rule area_potential: message: - "Get fraction satisfied all technical criteria: not too steep slope, suitable land cover, and not exceeding max_settlement for the tech {wildcards.tech_onshore} and shape {wildcards.shape}.", + "Compute area potential for the tech {wildcards.tech} and shapes {wildcards.shape}." params: - suitable_land_cover_types=lambda wildcards: config["techs_onshore"][f"{wildcards.tech_onshore}"]["land_cover"], - max_slope=lambda wildcards: config["techs_onshore"][f"{wildcards.tech_onshore}"]["max_slope"], - max_settlement=lambda wildcards: config["techs_onshore"][f"{wildcards.tech_onshore}"]["settlement"]["max_settlement"], - input: - script=workflow.source_path("../scripts/apply_technical_mask.py"), - resampled_path=rules.resample_same_resolution.output.resampled_input, - output: - technical_mask="resources/{shape}/technical_mask_{tech_onshore}.nc", - # plot=report( - # "resources/{shape}/technical_mask_{tech_onshore}.pdf", - # category="technical_mask", - # ), - conda: - "../envs/default.yaml" - shell: - """ - python "{input.script}" "{input.resampled_path}" "{params.suitable_land_cover_types}" "{params.max_slope}" "{params.max_settlement}" "{output}" - """ - -rule area_potential_onshore: - message: - "Compute onshore area potential for the tech {wildcards.tech_onshore} and shape {wildcards.shape}." - params: - technical_mask=lambda wildcards: config["techs_onshore"][f"{wildcards.tech_onshore}"] - input: - script=workflow.source_path("../scripts/potential_onshore.py"), - shapes="resources/user/shapes/{shape}.parquet", - masked_path=rules.technical_mask_onshore.output.technical_mask, - output: - area_potential="results/{shape}/area_potential_{tech_onshore}.tif", - plot=report( - "results/{shape}/area_potential_{tech_onshore}.png", - category="area_potential", - ), - conda: - "../envs/default.yaml" - shell: - """ - python "{input.script}" "{input.masked_path}" "{params.technical_mask}" "{input.shapes}" "{output.area_potential}" "{output.plot}" - """ - -rule area_potential_offshore: - message: - "Compute offshore area potential for the tech {wildcards.tech_offshore} and shape {wildcards.shape}." - params: - water_depth=lambda wildcards: config["techs_offshore"][f"{wildcards.tech_offshore}"]["water_depth"], - weight=lambda wildcards: config["techs_offshore"][f"{wildcards.tech_offshore}"]["weight"], + config=lambda wildcards: config["techs"][f"{wildcards.tech}"], buffer_crs=lambda wildcards: config["buffer_crs"], input: - script=workflow.source_path("../scripts/potential_offshore.py"), + script=workflow.source_path("../scripts/area_potential.py"), shapes="resources/user/shapes/{shape}.parquet", - resampled_input_path=rules.resample_same_resolution.output.resampled_input, + resampled_path=rules.prepare_resampled_inputs.output.resampled_input, output: - area_potential="results/{shape}/area_potential_{tech_offshore}.tif", + area_potential="results/{shape}/area_potential_{tech}.tif", plot=report( - "results/{shape}/area_potential_{tech_offshore}.png", + "results/{shape}/area_potential_{tech}.png", category="area_potential", ), - log: - "logs/area_potential_{shape}_{tech_offshore}.log", conda: "../envs/default.yaml" shell: """ - python "{input.script}" "{input.shapes}" \ - "{params.water_depth}" "{input.resampled_input_path}" "{params.weight}" "{params.buffer_crs}" "{output.area_potential}" "{output.plot}" 2> "{log}" + set -x + python "{input.script}" "{input.shapes}" "{input.resampled_path}" "{params.config}" "{params.buffer_crs}" "{output.area_potential}" "{output.plot}" """ - rule area_potential_report: message: - "Generate an overview report of the area potential for all techs in shape {wildcards.shape}.", + "Generate an overview report of the area potential for all techs in shapes {wildcards.shape}.", input: shapes="resources/user/shapes/{shape}.parquet", - resampled_path=rules.resample_same_resolution.output.resampled_input, + resampled_path=rules.prepare_resampled_inputs.output.resampled_input, area_potentials=expand( "results/{{shape}}/area_potential_{tech}.tif", - tech=config["techs_offshore"].keys(), - ) + expand( - "results/{{shape}}/area_potential_{tech}.tif", - tech=config["techs_onshore"].keys(), - ), + tech=config["techs"].keys(), + ) output: csv="results/{shape}/area_potential_report.csv", html=report( diff --git a/workflow/scripts/apply_technical_mask.py b/workflow/scripts/apply_technical_mask.py deleted file mode 100644 index 4a0b0dd..0000000 --- a/workflow/scripts/apply_technical_mask.py +++ /dev/null @@ -1,46 +0,0 @@ -import click -import xarray as xr -import yaml - - -@click.command() -@click.argument("resampled_path", type=str) -@click.argument("suitable_land_cover_types", type=str) -@click.argument("max_slope", type=float) -@click.argument("max_settlement", type=float) -@click.argument("output_path", type=str) -def apply_technical_mask( - resampled_path, - suitable_land_cover_types, - max_slope, - max_settlement, - output_path, -): - ds_resampled = xr.open_dataset(resampled_path, engine="netcdf4") - - # only keep pixel with fraction sum of suitable land cover >= 0, - # too steep slope < 0.5 - # settlement <= max_settlement - suitable_land_cover_types = yaml.safe_load(suitable_land_cover_types) - - ds_resampled["slope_too_steep"] = ds_resampled["slope"] > max_slope - - land_cover_types = [ - f"landcover_{type}" - for type, value in suitable_land_cover_types.items() - if value != 0 - ] - land_cover_mask = ds_resampled[land_cover_types].to_array().sum(dim="variable") > 0 - - combined_mask = ( - (ds_resampled["slope_too_steep"] <= 0.5) - & land_cover_mask - & (ds_resampled["settlement"] <= max_settlement) - ) - ds_resampled = ds_resampled.where(combined_mask) - # print("ds_resampled before saving", ds_resampled) - ds_resampled.to_netcdf(output_path) - - -if __name__ == "__main__": - apply_technical_mask() diff --git a/workflow/scripts/area_potential.py b/workflow/scripts/area_potential.py new file mode 100644 index 0000000..163e2ef --- /dev/null +++ b/workflow/scripts/area_potential.py @@ -0,0 +1,94 @@ +import click +import geo +import geopandas as gpd +import matplotlib.pyplot as plt +import xarray as xr +import yaml + + +@click.command() +@click.argument("shapes_path", type=str) +@click.argument("resampled_path", type=str) +@click.argument("config", type=str) +@click.argument("buffer_crs", type=str) +@click.argument("output_path", type=str) +@click.argument("plot_path", type=str) +def get_area_potential( + shapes_path, + resampled_path, + config, + buffer_crs, + output_path, + plot_path, +): + shapes = gpd.read_parquet(shapes_path) + ds = xr.open_dataset(resampled_path, decode_coords="all") + # FIXME: this is a workaround for the CRS not being set correctly; not sure why + ds.rio.write_crs(ds.spatial_ref.attrs["crs_wkt"], inplace=True) + config = yaml.safe_load(config) + + # Start with the configured pixel area as a base + potential_da = ds[config["initial_area"]].squeeze(drop=True) # Drop `band` + + # Drop pixels from binary layers with share 0 from potential_da + binary_layers = config.get("binary_layers", {}) + zero_binary_layers = [layer for layer, value in binary_layers.items() if value == 0] + for layer in zero_binary_layers: + if layer in ds: + potential_da = potential_da.where(~(ds[layer] > 0)) + else: + print(f"Warning: Layer '{layer}' not found in dataset. Skipping.") + + # Apply the continuous_layers criteria to drop additional pixels + continuous_layers = config.get("continuous_layers", {}) + for layer, layer_config in continuous_layers.items(): + if layer in ds: + # Apply the min-max criteria + potential_da = potential_da.where( + (ds[layer] <= layer_config["max"]) & (ds[layer] >= layer_config["min"]) + ) + # If a share is defined, multiply the pixel area by the share + if "share" in layer_config: + potential_da = potential_da * layer_config["share"] + else: + print(f"Warning: Layer '{layer}' not found in dataset. Skipping.") + + # Multiply pixels by their share from the binary layers + for layer, value in binary_layers.items(): + if layer in ds: + if value != 0: + potential_da = xr.where( + ds[layer] != 0, potential_da * ds[layer] * value, potential_da + ) + else: + print(f"Warning: Layer '{layer}' not found in dataset. Skipping.") + + # Apply shapes-based buffering + if "shapes_buffer" in config: + for shape_class in config["shapes_buffer"]: + buffer_distance = config["shapes_buffer"][shape_class] + shapes_subset = shapes[shapes["shape_class"] == shape_class] + if buffer_crs.lower() == "utm": + buffer = geo.apply_utm_buffer( + shapes_subset, buffer_distance_m=buffer_distance + ).to_crs(ds.rio.crs)["geometry"] + else: + buffer = shapes_subset.to_crs(buffer_crs).buffer(buffer_distance) + + # Clip the potential area with the buffered shapes + potential_da.rio.write_crs(ds.rio.crs, inplace=True) + buffer_geo = gpd.GeoDataFrame(geometry=buffer).to_crs(ds.rio.crs) + potential_da = potential_da.rio.clip( + buffer_geo.geometry, buffer_geo.crs, invert=True + ) + + potential_da.name = "area_potential" + potential_da = potential_da.transpose("band", "y", "x") + potential_da.rio.to_raster(output_path, driver="GTiff", compress="LZW") + + plot = potential_da.plot() + plt.savefig(plot_path, bbox_inches="tight") + + +if __name__ == "__main__": + get_area_potential() diff --git a/workflow/scripts/potential_offshore.py b/workflow/scripts/potential_offshore.py deleted file mode 100644 index 77a9d24..0000000 --- a/workflow/scripts/potential_offshore.py +++ /dev/null @@ -1,83 +0,0 @@ -import click -import geo -import geopandas as gpd -import matplotlib.pyplot as plt -import xarray as xr -import yaml - - -@click.command() -@click.argument("shapes_path", type=str) -@click.argument("water_depth", type=str) -@click.argument("resampled_input_path", type=str) -@click.argument("weight", type=float) -@click.argument("buffer_crs", type=str) -@click.argument("output_path", type=str) -@click.argument("plot_path", type=str) -def get_area_potential_offshore( - shapes_path, - water_depth, - resampled_input_path, - weight, - buffer_crs, - output_path, - plot_path, -): - """Get area potential for wind offshore technology. - - Steps: - - Resample bathymetry and land-sea mask to the same bounds and resolution as the reference raster. - - Mask out to get only bathymetry within water depth range - - Mask out land areas, buffer 10 km from country shape, inside geo-boundaries - - Convert to area potenital in m2 - - """ - ds_inputs = xr.open_dataset(resampled_input_path, decode_coords="all") - shapes = gpd.read_parquet(shapes_path) - - # get bathymetry within the water depth range then resample - water_depth = yaml.safe_load(water_depth) - - masked_bathymetry = ( - (ds_inputs["bathymetry"] < water_depth["max"]) - & (ds_inputs["bathymetry"] >= water_depth["min"]) - ).astype(float) - - # keep bathymetry inside geo boundaries, i.e. within the exclusive economic zone (EEZ) - # FIXME: cut this to the EEZ boundary regions specifically, excluding land areas - eligible_fraction = masked_bathymetry.rio.clip( - shapes.geometry, shapes.crs, invert=False - ) - - # Buffer 10 km from country shape, no wind offshore too close to the coastline - shapes_land = shapes[shapes["shape_class"] == "land"] - if buffer_crs.lower() == "utm": - sea_buffer = geo.apply_utm_buffer(shapes_land, buffer_distance_m=10000).to_crs( - ds_inputs.rio.crs - )["geometry"] - else: - sea_buffer = shapes_land.to_crs(buffer_crs).buffer(10_000) - - buffer_geo = gpd.GeoDataFrame(geometry=sea_buffer).to_crs(ds_inputs.rio.crs) - eligible_fraction = eligible_fraction.rio.clip( - buffer_geo.geometry, buffer_geo.crs, invert=True - ) - - # exclude protected areas - eligible_fraction = eligible_fraction.where(ds_inputs["protected"] != 1) - - # apply weight, then multiply pixel area to get area potential - da_area_potential = xr.Dataset( - {"area_potential": eligible_fraction * weight * ds_inputs["pixel_area"]}, - coords=ds_inputs["pixel_area"].coords, - )["area_potential"] - - da_area_potential = da_area_potential.transpose("band", "y", "x") - da_area_potential.rio.to_raster(output_path, driver="GTiff", compress="LZW") - - plot = da_area_potential.plot() - plt.savefig(plot_path, bbox_inches="tight") - - -if __name__ == "__main__": - get_area_potential_offshore() diff --git a/workflow/scripts/potential_onshore.py b/workflow/scripts/potential_onshore.py deleted file mode 100644 index ed55208..0000000 --- a/workflow/scripts/potential_onshore.py +++ /dev/null @@ -1,63 +0,0 @@ -import click -import geopandas as gpd -import matplotlib.pyplot as plt -import xarray as xr -import yaml - - -@click.command() -@click.argument("masked_path", type=str) -@click.argument("technical_mask", type=str) -@click.argument("shapes_path", type=str) -@click.argument("output_path", type=str) -@click.argument("plot_path", type=str) -def get_area_potential_onshore( - masked_path, - technical_mask, - shapes_path, - output_path, - plot_path, -): - ds_masked = xr.open_dataset(masked_path) - technical_mask = yaml.safe_load(technical_mask) - - # apply weights - suitable_land_cover_types = [] - for type, value in technical_mask["land_cover"].items(): - if value == 0: - continue - key = f"landcover_{type}" - suitable_land_cover_types.append(key) - ds_masked[key] = ds_masked[key] * value - - eligible_fraction = ( - ds_masked[suitable_land_cover_types].to_array().sum(dim="variable") - - ds_masked["slope_too_steep"] - + ds_masked["settlement"] * technical_mask["settlement"]["weight"] - ) - - eligible_fraction = eligible_fraction.where(ds_masked["protected"] != 1) - - # FIXME: is this necessary if the inputs are already in EPSG:4326? - eligible_fraction.rio.write_crs("EPSG:4326", inplace=True) - - # remove negative values and values greater than 1 - eligible_fraction = eligible_fraction.clip(0, 1) - - # multiply pixel area to get area potential - # cut with given shape to return raster inside the shape - shapes = gpd.read_parquet(shapes_path) - ds_area_potential = eligible_fraction * ds_masked["pixel_area"] - ds_area_potential = ds_area_potential.rio.clip( - shapes.geometry, shapes.crs, invert=False - ) - ds_area_potential.name = "area_potential" - ds_area_potential = ds_area_potential.transpose("band", "y", "x") - ds_area_potential.rio.to_raster(output_path, driver="GTiff", compress="LZW") - - plot = ds_area_potential.plot() - plt.savefig(plot_path, bbox_inches="tight") - - -if __name__ == "__main__": - get_area_potential_onshore() diff --git a/workflow/scripts/report.py b/workflow/scripts/report.py index 52bd080..5d345fc 100644 --- a/workflow/scripts/report.py +++ b/workflow/scripts/report.py @@ -7,11 +7,13 @@ def report(shapes, resampled_path, area_potentials, csv_path, html_path): shapes = gpd.read_parquet(shapes) ds_inputs = xr.open_dataset(resampled_path, decode_coords="all") + # FIXME: this is a workaround for the CRS not being set correctly; not sure why + ds_inputs.rio.write_crs(ds_inputs.spatial_ref.attrs["crs_wkt"], inplace=True) # Collect the area potentials from the input files for area_potential in area_potentials: ds_inputs[area_potential] = rxr.open_rasterio(area_potential) - ds_inputs = ds_inputs.squeeze().drop_vars("band") + ds_inputs = ds_inputs.squeeze().drop_vars(["band", "spatial_ref"]) # Group the area potentials by regions, sum them up, and collect the resulting Series # into a DataFrame, where each column corresponds to a technology's area potential, diff --git a/workflow/scripts/resample.py b/workflow/scripts/resample.py index 7a960c9..9535880 100644 --- a/workflow/scripts/resample.py +++ b/workflow/scripts/resample.py @@ -62,8 +62,8 @@ "BARE_AREAS": "OTHER", "ARTIFICAL_SURFACES_AND_URBAN_AREAS": "URBAN", "WATER_BODIES": "WATER", - "PERMANENT_SNOW": "NA", - "NO_DATA": "NA", + "PERMANENT_SNOW": "NOT_SUITABLE", + "NO_DATA": "NOT_SUITABLE", } @@ -126,7 +126,7 @@ def determine_pixel_areas(raster_input): crs: the coordinate reference system of the data (must be WGS84) """ # the following is based on https://gis.stackexchange.com/a/288034/77760 - # and assumes the data to be in EPSG:4326 (WGS84 is similar but with lat,lon instead of lon,lat) + # and assumes the data to be in EPSG:4326 assert ( raster_input.rio.crs.to_epsg() == 4326 ), "raster_input does not have the projection EPSG:4326" @@ -169,7 +169,6 @@ def get_same_shape_and_resolution( plot_path, ): shapes = gpd.read_parquet(shapes_path) - print(f"Number of shapes in input data: {len(shapes)}") ## # Land cover @@ -188,9 +187,6 @@ def get_same_shape_and_resolution( reference_raster, resampling=Resampling.average ) - # Drop the "band" dimension - resampled = resampled.squeeze().drop_vars("band") - ## # Pixel area ## @@ -204,12 +200,30 @@ def get_same_shape_and_resolution( # Regions ## + shapes_land = shapes[shapes["shape_class"] == "land"].index + shapes_maritime = shapes[shapes["shape_class"] == "maritime"].index + print(f"Number of regions in input data: {len(shapes)}") + print(f"Number of land regions: {len(shapes_land)}") + print(f"Number of maritime regions: {len(shapes_maritime)}") resampled["regions"] = ( ("y", "x"), _rasterize_regions(shapes, reference_raster), ) + mask_land = xr.DataArray( + np.isin(resampled["regions"], shapes_land), + dims=resampled["regions"].dims, + coords=resampled["regions"].coords, + ) + mask_maritime = xr.DataArray( + np.isin(resampled["regions"], shapes_maritime), + dims=resampled["regions"].dims, + coords=resampled["regions"].coords, + ) + resampled["regions_land"] = xr.where(mask_land, 1.0, np.nan) + resampled["regions_maritime"] = xr.where(mask_maritime, 1.0, np.nan) + ## # Slope ## @@ -224,18 +238,30 @@ def get_same_shape_and_resolution( ## ds_settlement = rxr.open_rasterio(settlement_path) print(f"Settlement resolution: {ds_settlement.rio.resolution()}") - ds_settlement.rio.write_crs("EPSG:4326", inplace=True) - resampled["settlement"] = ds_settlement.rio.reproject_match( - reference_raster, resampling=Resampling.sum + + ds_settlement_pixel_area = ( + determine_pixel_areas(ds_settlement) + .expand_dims({"x": ds_settlement.x}) + .transpose("y", "x") + ) + + # Divide built-up surface (m2) by pixel area (m2) to get built-up surface density, + # then reproject to match the reference raster + resampled["settlement_share"] = ( + ds_settlement / ds_settlement_pixel_area + ).rio.reproject_match(reference_raster, resampling=Resampling.average) + + resampled["settlement_area"] = ( + resampled["settlement_share"] * resampled["pixel_area"] ) - # get fraction of settlement (built-up surface) compared to pixel area, both in m2 - resampled["settlement"] = resampled["settlement"] / resampled["pixel_area"] ## # Bathymetry ## ds_bathymetry = rxr.open_rasterio(bathymetry_path) + # Only keep values <= 0, i.e., below sea level + ds_bathymetry = ds_bathymetry.where(ds_bathymetry <= 0, other=np.nan) print(f"Bathymetry resolution: {ds_bathymetry.rio.resolution()}") resampled["bathymetry"] = ds_bathymetry.rio.reproject_match( reference_raster, resampling=Resampling.average @@ -256,9 +282,15 @@ def get_same_shape_and_resolution( ) resampled["protected"] = resampled["protected"].fillna(0) - resampled.to_netcdf(output_path) + compression = { + var: {"zlib": True, "complevel": 1} + for var in resampled.data_vars + if var not in ["spatial_ref", "band"] + } + + resampled.to_netcdf(output_path, encoding=compression) - script_utils.plot_all_dataset_variables(resampled, savefig=plot_path) + script_utils.plot_all_dataset_variables(resampled, ncols=3, savefig=plot_path) if __name__ == "__main__": diff --git a/workflow/scripts/script_utils.py b/workflow/scripts/script_utils.py index db26bae..1b17934 100644 --- a/workflow/scripts/script_utils.py +++ b/workflow/scripts/script_utils.py @@ -9,10 +9,9 @@ def plot_all_dataset_variables(ds, ncols=2, savefig=None): ds = ds.drop_vars(lambda x: [v for v, da in x.variables.items() if not da.ndim]) vars_to_plot = list(ds.data_vars) - nrows = math.ceil(len(vars_to_plot) / ncols) - fig, axes = plt.subplots(nrows=nrows, ncols=ncols, figsize=(12, 4 * nrows)) + fig, axes = plt.subplots(nrows=nrows, ncols=ncols, figsize=(6 * ncols, 4 * nrows)) axes = axes.flatten() for i, var in enumerate(vars_to_plot): @@ -28,7 +27,6 @@ def plot_all_dataset_variables(ds, ncols=2, savefig=None): plt.tight_layout() if savefig: - plt.savefig(savefig, bbox_inches="tight") return fig From b88df0dff5065fb4c86aebadf862ce7c75ba96b2 Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Tue, 8 Jul 2025 20:24:57 +0200 Subject: [PATCH 25/59] User must manually download WDPA --- workflow/internal/settings.yaml | 11 ----------- workflow/rules/automatic.smk | 31 ------------------------------- workflow/rules/process.smk | 2 +- 3 files changed, 1 insertion(+), 43 deletions(-) diff --git a/workflow/internal/settings.yaml b/workflow/internal/settings.yaml index 582759d..93dae19 100644 --- a/workflow/internal/settings.yaml +++ b/workflow/internal/settings.yaml @@ -4,20 +4,9 @@ resources: ## # Links for automatically downloaded files ## - # Slope - # 60m version: - # slope: "https://s3.opengeohub.org/global/dtm/v3/slope.in.degree_edtm_m_60m_s_20000101_20221231_go_epsg.4326_v20241230.tif" - # 120m version: - # slope: "https://zenodo.org/records/14920379/files/slope.in.degree_edtm_m_120m_s_20000101_20221231_go_epsg.4326_v20241230.tif" - # 240m version: slope: "https://zenodo.org/records/14920379/files/slope.in.degree_edtm_m_240m_s_20000101_20221231_go_epsg.4326_v20241230.tif" bathymetry: "https://zenodo.org/records/15741950/files/gebco_2024_sub_ice_cog.tif" - # Land cover globcover: "https://due.esrin.esa.int/files/Globcover2009_V2.3_Global_.zip" globcover_landcover_tif: "GLOBCOVER_L4_200901_200912_V2.3.tif" - # Built-up areas ghsl: "https://jeodpp.jrc.ec.europa.eu/ftp/jrc-opendata/GHSL/GHS_BUILT_S_GLOBE_R2023A/GHS_BUILT_S_E2025_GLOBE_R2023A_4326_30ss/V1-0/GHS_BUILT_S_E2025_GLOBE_R2023A_4326_30ss_V1_0.zip" ghsl_tif: "GHS_BUILT_S_E2025_GLOBE_R2023A_4326_30ss_V1_0.tif" - # Protected areas - wdpa: "https://d1gam3xoknrgr2.cloudfront.net/current/WDPA_Jun2025_Public.zip" - wdpa_gdb: "WDPA_Jun2025_Public.gdb" diff --git a/workflow/rules/automatic.smk b/workflow/rules/automatic.smk index 83b4bab..35dee0c 100644 --- a/workflow/rules/automatic.smk +++ b/workflow/rules/automatic.smk @@ -24,37 +24,6 @@ rule download_cutout_bathymetry: wrapper: "v7.2.0/geo/rasterio/clip-geotiff" -rule download_wdpa: - message: - "Download the WDPA (World Database on Protected Areas) data (~1.5 GB)." - params: - url=internal["resources"]["automatic"]["wdpa"], - output: - "resources/automatic/global/wdpa.zip", - conda: - "../envs/shell.yaml" - shell: - 'curl -sSLo {output} "{params.url}"' - -rule unzip_wdpa: - message: - "Unzip the WDPA (World Database on Protected Areas) data (~2.0 GB)." - params: - target=internal["resources"]["automatic"]["wdpa_gdb"], - input: - rules.download_wdpa.output, - output: - directory("resources/automatic/global/wdpa.gdb"), - conda: - "../envs/shell.yaml" - shell: - """ - temp_dir=$(mktemp -d) - unzip {input} -d $temp_dir - mv $temp_dir/{params.target} {output} - rm -R $temp_dir - """ - rule download_globcover: message: "Download the GlobCover land cover data (~380 MB)." diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index 5bd5b1a..34caa04 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -8,7 +8,7 @@ rule prepare_resampled_inputs: slope_path=rules.download_cutout_slope.output, settlement_path=rules.cutout_settlement.output, bathymetry_path=rules.download_cutout_bathymetry.output, - protected_area_path=rules.unzip_wdpa.output, + protected_area_path="resources/user/wdpa.gdb", output: resampled_input="resources/automatic/{shape}.resampled_inputs.nc", plot=report( From c061cc5c0d3d0cfcdbd8aa593109865d3ed6186a Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Tue, 8 Jul 2025 20:25:16 +0200 Subject: [PATCH 26/59] Add integration test --- tests/integration/Snakefile | 38 ++++++++------ tests/integration/test_config.yaml | 79 ++++++++++++++++++++++++++++-- workflow/rules/prepare.smk | 1 - 3 files changed, 98 insertions(+), 20 deletions(-) diff --git a/tests/integration/Snakefile b/tests/integration/Snakefile index 080a9da..43183d7 100644 --- a/tests/integration/Snakefile +++ b/tests/integration/Snakefile @@ -1,21 +1,25 @@ # Emulate a user configuring the module. configfile: workflow.source_path("./test_config.yaml") -# Emulate how another workflow might create inputs for this module. -rule create_external_input: - message: "Example of a rule external to your module." +rule download_netherlands_shapes: + message: "Download and unzip the Netherlands shapes." output: - text_file = "results/module_area_potentials/resources/user/user_message.md" - run: - from pathlib import Path - from textwrap import dedent - text = dedent("""Modular workflows can be used by more than one project! - For example, this text comes from a file external to the module. - Try your best to make this workflow reusable so that others may benefit from your methods.""" - ) - file_path = Path(output.text_file) - with file_path.open("w") as f: - f.write(text) + "results/module_area_potentials/resources/user/shapes/NLD.parquet", + shell: + """ + curl -sSLo {output} https://surfdrive.surf.nl/files/index.php/s/ey3RmiCbajp69oQ/download + """ + +rule download_netherlands_protected_areas: + message: "Download and unzip a dummy drop-in dataset for Netherlands protected areas (not based on WDPA)." + output: + zipfile="results/module_area_potentials/resources/user/wdpa.gdb.zip", + wdpa=directory("results/module_area_potentials/resources/user/wdpa.gdb"), + shell: + """ + curl -sSLo {output.zipfile} https://surfdrive.surf.nl/files/index.php/s/msuXQETNbCWawDh/download + unzip {output.zipfile} -d results/module_area_potentials/resources/user/ + """ # Import the module and configure it. # `snakefile:` specifies the module. It can use file paths and special github(...) / gitlab(...) markers @@ -30,7 +34,9 @@ use rule * from module_area_potentials as module_area_potentials_* # Request something from the module rule all: - message: "A generic test case for your module." + message: "Run the module for the Netherlands shapes." default_target: True input: - "results/module_area_potentials/results/combined_text.md" + "results/module_area_potentials/resources/user/shapes/NLD.parquet", + "results/module_area_potentials/resources/user/wdpa.gdb", + "results/module_area_potentials/results/NLD/area_potential_report.html", diff --git a/tests/integration/test_config.yaml b/tests/integration/test_config.yaml index f09502f..be428b8 100644 --- a/tests/integration/test_config.yaml +++ b/tests/integration/test_config.yaml @@ -1,4 +1,77 @@ module_area_potentials: - dummy_text: >- - Configuration values (like this one) are also external to the module! - This gives users a lot of flexibility in how they apply your methodology to solve their particular needs. + # Options for buffering: either a string of the form "epsg:xxxx" or "UTM"/"utm" + # - "UTM": project each shape to the UTM zone of its centroid for buffering + # - "epsg:xxxx": use the specified CRS for all buffering + # A good option is "epsg:8857" (WGS 84 / Equal Earth Greenwich) for global coverage + buffer_crs: "epsg:8857" + + techs: + pv_rooftop: + initial_area: settlement_area + continuous_layers: + settlement_share: + min: 0.01 + max: 1 + share: 0.8 + binary_layers: + regions_maritime: 0 + regions_land: 1 + protected: 0 + # Include all land cover types; selection is done by settlement_share + landcover_FARM: 1 + landcover_FOREST: 1 + landcover_URBAN: 1 + landcover_OTHER: 1 + landcover_NOT_SUITABLE: 0 + landcover_WATER: 0 + pv_open_field: + initial_area: pixel_area + continuous_layers: + slope: + min: 0 + max: 3 + settlement_share: + min: 0 + max: 0.01 + binary_layers: + regions_maritime: 0 + regions_land: 1 + protected: 0 + landcover_FARM: 0.1 + landcover_FOREST: 0 + landcover_URBAN: 0 + landcover_OTHER: 0.2 + landcover_NOT_SUITABLE: 0 + landcover_WATER: 0 + wind_onshore: + initial_area: pixel_area + continuous_layers: + slope: + min: 0 + max: 20 + settlement_share: + min: 0 + max: 0.01 + binary_layers: + regions_maritime: 0 + regions_land: 1 + protected: 0 + landcover_FARM: 0.2 + landcover_FOREST: 0.05 + landcover_URBAN: 0 + landcover_OTHER: 0.3 + landcover_NOT_SUITABLE: 0 + landcover_WATER: 0 + wind_offshore: + initial_area: pixel_area + continuous_layers: + bathymetry: + min: -50 + max: 0 + share: 0.8 + binary_layers: + regions_land: 0 + regions_maritime: 1 + protected: 0 + shapes_buffer: + land: 10000 # meters diff --git a/workflow/rules/prepare.smk b/workflow/rules/prepare.smk index 3d06eda..a140aa2 100644 --- a/workflow/rules/prepare.smk +++ b/workflow/rules/prepare.smk @@ -15,7 +15,6 @@ rule cutout_landcover: rio clip --overwrite "{input.landcover}" "{output}" --bounds "$(fio info '{input.shapes}' --bounds)" """ - rule cutout_settlement: message: "Cut settlement data to the bounds of the input shapefile." From f061c688f43f70f327f21a40e97442c584e021a6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 8 Jul 2025 18:29:45 +0000 Subject: [PATCH 27/59] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- AUTHORS | 2 +- tests/integration/Snakefile | 23 +++++++++++++++++------ workflow/Snakefile | 2 ++ workflow/internal/config.schema.yaml | 2 +- workflow/rules/automatic.smk | 6 ++++++ workflow/rules/prepare.smk | 2 ++ workflow/rules/process.smk | 8 +++++--- workflow/scripts/area_potential.py | 7 +------ workflow/scripts/report.py | 2 +- workflow/scripts/resample.py | 11 ++++------- workflow/scripts/script_utils.py | 1 - 11 files changed, 40 insertions(+), 26 deletions(-) diff --git a/AUTHORS b/AUTHORS index 7bbec25..2f66b3b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -5,4 +5,4 @@ code or documentation. For a full contributor list, see: Linh Ho-Tran, -Stefan Pfenninger-Lee, \ No newline at end of file +Stefan Pfenninger-Lee, diff --git a/tests/integration/Snakefile b/tests/integration/Snakefile index 43183d7..4564819 100644 --- a/tests/integration/Snakefile +++ b/tests/integration/Snakefile @@ -1,8 +1,10 @@ # Emulate a user configuring the module. configfile: workflow.source_path("./test_config.yaml") + rule download_netherlands_shapes: - message: "Download and unzip the Netherlands shapes." + message: + "Download and unzip the Netherlands shapes." output: "results/module_area_potentials/resources/user/shapes/NLD.parquet", shell: @@ -10,8 +12,10 @@ rule download_netherlands_shapes: curl -sSLo {output} https://surfdrive.surf.nl/files/index.php/s/ey3RmiCbajp69oQ/download """ + rule download_netherlands_protected_areas: - message: "Download and unzip a dummy drop-in dataset for Netherlands protected areas (not based on WDPA)." + message: + "Download and unzip a dummy drop-in dataset for Netherlands protected areas (not based on WDPA)." output: zipfile="results/module_area_potentials/resources/user/wdpa.gdb.zip", wdpa=directory("results/module_area_potentials/resources/user/wdpa.gdb"), @@ -21,20 +25,27 @@ rule download_netherlands_protected_areas: unzip {output.zipfile} -d results/module_area_potentials/resources/user/ """ + # Import the module and configure it. # `snakefile:` specifies the module. It can use file paths and special github(...) / gitlab(...) markers # `prefix:` re-routes all input/output paths of the module, helping to avoid file conflicts. module module_area_potentials: - snakefile: "../../workflow/Snakefile" - config: config["module_area_potentials"] - prefix: "results/module_area_potentials/" + snakefile: + "../../workflow/Snakefile" + config: + config["module_area_potentials"] + prefix: + "results/module_area_potentials/" + # rename all module rules with a prefix, to avoid naming conflicts. use rule * from module_area_potentials as module_area_potentials_* + # Request something from the module rule all: - message: "Run the module for the Netherlands shapes." + message: + "Run the module for the Netherlands shapes." default_target: True input: "results/module_area_potentials/resources/user/shapes/NLD.parquet", diff --git a/workflow/Snakefile b/workflow/Snakefile index dddc777..791925e 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -20,10 +20,12 @@ with open(workflow.source_path("internal/settings.yaml"), "r") as f: workflow.source_path("scripts/script_utils.py") workflow.source_path("scripts/geo.py") + wildcard_constraints: shape="[a-zA-Z0-9_-]+", tech="|".join(config["techs"].keys()), + # Add all your includes here. include: "rules/automatic.smk" include: "rules/prepare.smk" diff --git a/workflow/internal/config.schema.yaml b/workflow/internal/config.schema.yaml index efa9cb0..fd0ef3b 100644 --- a/workflow/internal/config.schema.yaml +++ b/workflow/internal/config.schema.yaml @@ -41,4 +41,4 @@ properties: shapes_buffer: type: object required: ["initial_area", "continuous_layers", "binary_layers"] - additionalProperties: false \ No newline at end of file + additionalProperties: false diff --git a/workflow/rules/automatic.smk b/workflow/rules/automatic.smk index 35dee0c..669fcb1 100644 --- a/workflow/rules/automatic.smk +++ b/workflow/rules/automatic.smk @@ -1,5 +1,6 @@ """Rules to used to download automatic resource files.""" + rule download_cutout_slope: message: "Download slope data covering the bounds of the input shapefile." @@ -12,6 +13,7 @@ rule download_cutout_slope: wrapper: "v7.2.0/geo/rasterio/clip-geotiff" + rule download_cutout_bathymetry: message: "Download bathymetry data covering the bounds of the input shapefile." @@ -24,6 +26,7 @@ rule download_cutout_bathymetry: wrapper: "v7.2.0/geo/rasterio/clip-geotiff" + rule download_globcover: message: "Download the GlobCover land cover data (~380 MB)." @@ -36,6 +39,7 @@ rule download_globcover: shell: 'curl -sSLo {output} "{params.url}"' + rule unzip_globcover: message: "Unzip the relevant TIF files from the GlobCover zip file." @@ -57,6 +61,7 @@ rule unzip_globcover: rm -R $temp_dir """ + rule download_ghsl: message: "Download the GHSL (Global Human Settlement Layer) built-up surface data." @@ -69,6 +74,7 @@ rule download_ghsl: shell: 'curl -sSLo {output} "{params.url}"' + rule unzip_ghsl: message: "Unzip the relevant TIF file from the GHSL data." diff --git a/workflow/rules/prepare.smk b/workflow/rules/prepare.smk index a140aa2..17af1f6 100644 --- a/workflow/rules/prepare.smk +++ b/workflow/rules/prepare.smk @@ -1,5 +1,6 @@ """Cut out the datasets to bounds determined by the input shapefile.""" + rule cutout_landcover: message: "Cut land cover data to the bounds of the input shapefile." @@ -15,6 +16,7 @@ rule cutout_landcover: rio clip --overwrite "{input.landcover}" "{output}" --bounds "$(fio info '{input.shapes}' --bounds)" """ + rule cutout_settlement: message: "Cut settlement data to the bounds of the input shapefile." diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index 34caa04..b210f86 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -1,6 +1,6 @@ rule prepare_resampled_inputs: message: - "Resample inputs for {wildcards.shape} to the projection and resolution of the land cover data, while aggregating land cover types.", + "Resample inputs for {wildcards.shape} to the projection and resolution of the land cover data, while aggregating land cover types." input: script=workflow.source_path("../scripts/resample.py"), shapes="resources/user/shapes/{shape}.parquet", @@ -24,6 +24,7 @@ rule prepare_resampled_inputs: "{output.resampled_input}" "{output.plot}" """ + rule area_potential: message: "Compute area potential for the tech {wildcards.tech} and shapes {wildcards.shape}." @@ -48,16 +49,17 @@ rule area_potential: python "{input.script}" "{input.shapes}" "{input.resampled_path}" "{params.config}" "{params.buffer_crs}" "{output.area_potential}" "{output.plot}" """ + rule area_potential_report: message: - "Generate an overview report of the area potential for all techs in shapes {wildcards.shape}.", + "Generate an overview report of the area potential for all techs in shapes {wildcards.shape}." input: shapes="resources/user/shapes/{shape}.parquet", resampled_path=rules.prepare_resampled_inputs.output.resampled_input, area_potentials=expand( "results/{{shape}}/area_potential_{tech}.tif", tech=config["techs"].keys(), - ) + ), output: csv="results/{shape}/area_potential_report.csv", html=report( diff --git a/workflow/scripts/area_potential.py b/workflow/scripts/area_potential.py index 163e2ef..bf7975f 100644 --- a/workflow/scripts/area_potential.py +++ b/workflow/scripts/area_potential.py @@ -14,12 +14,7 @@ @click.argument("output_path", type=str) @click.argument("plot_path", type=str) def get_area_potential( - shapes_path, - resampled_path, - config, - buffer_crs, - output_path, - plot_path, + shapes_path, resampled_path, config, buffer_crs, output_path, plot_path ): shapes = gpd.read_parquet(shapes_path) ds = xr.open_dataset(resampled_path, decode_coords="all") diff --git a/workflow/scripts/report.py b/workflow/scripts/report.py index 5d345fc..7d276aa 100644 --- a/workflow/scripts/report.py +++ b/workflow/scripts/report.py @@ -39,7 +39,7 @@ def report(shapes, resampled_path, area_potentials, csv_path, html_path): sums.name = "Total" df = pd.concat([df, sums.to_frame().T]) - df.to_html(html_path, float_format=lambda x: f"{x/1e6:.2f}") + df.to_html(html_path, float_format=lambda x: f"{x / 1e6:.2f}") if __name__ == "__main__": diff --git a/workflow/scripts/resample.py b/workflow/scripts/resample.py index 9535880..96e24ea 100644 --- a/workflow/scripts/resample.py +++ b/workflow/scripts/resample.py @@ -127,9 +127,9 @@ def determine_pixel_areas(raster_input): """ # the following is based on https://gis.stackexchange.com/a/288034/77760 # and assumes the data to be in EPSG:4326 - assert ( - raster_input.rio.crs.to_epsg() == 4326 - ), "raster_input does not have the projection EPSG:4326" + assert raster_input.rio.crs.to_epsg() == 4326, ( + "raster_input does not have the projection EPSG:4326" + ) resolution = raster_input.rio.resolution()[0] # resolution in degrees varea_of_pixel = np.vectorize(lambda lat: _area_of_pixel(resolution, lat)) pixel_area = varea_of_pixel(raster_input.y) * 1000**2 # convert to m^2 @@ -206,10 +206,7 @@ def get_same_shape_and_resolution( print(f"Number of land regions: {len(shapes_land)}") print(f"Number of maritime regions: {len(shapes_maritime)}") - resampled["regions"] = ( - ("y", "x"), - _rasterize_regions(shapes, reference_raster), - ) + resampled["regions"] = (("y", "x"), _rasterize_regions(shapes, reference_raster)) mask_land = xr.DataArray( np.isin(resampled["regions"], shapes_land), diff --git a/workflow/scripts/script_utils.py b/workflow/scripts/script_utils.py index 1b17934..995a29c 100644 --- a/workflow/scripts/script_utils.py +++ b/workflow/scripts/script_utils.py @@ -4,7 +4,6 @@ def plot_all_dataset_variables(ds, ncols=2, savefig=None): - # Drop dimensionless variables ds = ds.drop_vars(lambda x: [v for v, da in x.variables.items() if not da.ndim]) From 684fa8bc28c22c3efcf64714f18c6fb34adbe1b0 Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Tue, 8 Jul 2025 21:01:12 +0200 Subject: [PATCH 28/59] Emulate `unzip` in Python --- tests/integration/Snakefile | 4 +++- workflow/envs/shell.yaml | 2 ++ workflow/rules/automatic.smk | 10 +++++---- workflow/scripts/unzip_like.py | 38 ++++++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 workflow/scripts/unzip_like.py diff --git a/tests/integration/Snakefile b/tests/integration/Snakefile index 4564819..ae00221 100644 --- a/tests/integration/Snakefile +++ b/tests/integration/Snakefile @@ -16,13 +16,15 @@ rule download_netherlands_shapes: rule download_netherlands_protected_areas: message: "Download and unzip a dummy drop-in dataset for Netherlands protected areas (not based on WDPA)." + input: + script=workflow.source_path("../../workflow/scripts/unzip_like.py"), output: zipfile="results/module_area_potentials/resources/user/wdpa.gdb.zip", wdpa=directory("results/module_area_potentials/resources/user/wdpa.gdb"), shell: """ curl -sSLo {output.zipfile} https://surfdrive.surf.nl/files/index.php/s/msuXQETNbCWawDh/download - unzip {output.zipfile} -d results/module_area_potentials/resources/user/ + python {input.script} {output.zipfile} -t results/module_area_potentials/resources/user/ """ diff --git a/workflow/envs/shell.yaml b/workflow/envs/shell.yaml index e4b351d..252a508 100644 --- a/workflow/envs/shell.yaml +++ b/workflow/envs/shell.yaml @@ -3,4 +3,6 @@ channels: - conda-forge - nodefaults dependencies: + - python=3.13 + - click=8.2.1 - curl=8.9.1 diff --git a/workflow/rules/automatic.smk b/workflow/rules/automatic.smk index 669fcb1..c9beeca 100644 --- a/workflow/rules/automatic.smk +++ b/workflow/rules/automatic.smk @@ -46,7 +46,8 @@ rule unzip_globcover: params: target_file=internal["resources"]["automatic"]["globcover_landcover_tif"], input: - rules.download_globcover.output, + script=workflow.source_path("../scripts/unzip_like.py"), + zipfile=rules.download_globcover.output, output: "resources/automatic/global/globcover-landcover.tif", log: @@ -56,7 +57,7 @@ rule unzip_globcover: shell: """ temp_dir=$(mktemp -d) - unzip -j {input} {params.target_file} -d $temp_dir + python {input.script} {input.zipfile} -f {params.target_file} -t $temp_dir mv $temp_dir/{params.target_file} {output} rm -R $temp_dir """ @@ -81,7 +82,8 @@ rule unzip_ghsl: params: target_file=internal["resources"]["automatic"]["ghsl_tif"], input: - rules.download_ghsl.output, + script=workflow.source_path("../scripts/unzip_like.py"), + zipfile=rules.download_ghsl.output, output: "resources/automatic/global/ghsl_built_s.tif", conda: @@ -89,7 +91,7 @@ rule unzip_ghsl: shell: """ temp_dir=$(mktemp -d) - unzip -j {input} {params.target_file} -d $temp_dir + python {input.script} {input.zipfile} -f {params.target_file} -t $temp_dir mv $temp_dir/{params.target_file} {output} rm -R $temp_dir """ diff --git a/workflow/scripts/unzip_like.py b/workflow/scripts/unzip_like.py new file mode 100644 index 0000000..25e23e0 --- /dev/null +++ b/workflow/scripts/unzip_like.py @@ -0,0 +1,38 @@ +import os +import zipfile + +import click + + +@click.command() +@click.argument("zip_path", type=click.Path(exists=True)) +@click.option( + "--target", + "-t", + type=click.Path(), + default=".", + help="Target directory to extract to.", +) +@click.option("--file", "-f", help="Specific file inside the zip to extract.") +def unzip(zip_path, target, file): + """Emulates the `unzip` command across platforms. + + ZIP_PATH: Path to the .zip file + """ + os.makedirs(target, exist_ok=True) + + with zipfile.ZipFile(zip_path, "r") as zip_ref: + if file: + # Check if file exists in zip + if file not in zip_ref.namelist(): + click.echo(f"Error: '{file}' not found in archive.") + return + zip_ref.extract(file, target) + click.echo(f"Extracted '{file}' to '{target}'.") + else: + zip_ref.extractall(target) + click.echo(f"Extracted all files to '{target}'.") + + +if __name__ == "__main__": + unzip() From aa58edf4b2ae1b548956c421cfaff35f124e1d03 Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Tue, 8 Jul 2025 21:11:54 +0200 Subject: [PATCH 29/59] Attempt to fix Windows tests --- tests/integration/Snakefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/Snakefile b/tests/integration/Snakefile index ae00221..d549545 100644 --- a/tests/integration/Snakefile +++ b/tests/integration/Snakefile @@ -9,7 +9,7 @@ rule download_netherlands_shapes: "results/module_area_potentials/resources/user/shapes/NLD.parquet", shell: """ - curl -sSLo {output} https://surfdrive.surf.nl/files/index.php/s/ey3RmiCbajp69oQ/download + curl -sSLo "{output}" "https://surfdrive.surf.nl/files/index.php/s/ey3RmiCbajp69oQ/download" """ @@ -23,8 +23,8 @@ rule download_netherlands_protected_areas: wdpa=directory("results/module_area_potentials/resources/user/wdpa.gdb"), shell: """ - curl -sSLo {output.zipfile} https://surfdrive.surf.nl/files/index.php/s/msuXQETNbCWawDh/download - python {input.script} {output.zipfile} -t results/module_area_potentials/resources/user/ + curl -sSLo "{output.zipfile}" "https://surfdrive.surf.nl/files/index.php/s/msuXQETNbCWawDh/download" + python "{input.script}" "{output.zipfile}" -t "results/module_area_potentials/resources/user/" """ From 966e4635d9065530830516354b8d41396d86a3c9 Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Wed, 9 Jul 2025 14:50:55 +0200 Subject: [PATCH 30/59] Add docstrings and logfiles --- workflow/rules/automatic.smk | 10 ++++++++++ workflow/rules/prepare.smk | 4 ++++ workflow/rules/process.smk | 11 +++++++++-- workflow/scripts/area_potential.py | 19 +++++++++++++++++-- workflow/scripts/geo.py | 2 ++ workflow/scripts/report.py | 3 +++ workflow/scripts/resample.py | 13 +++++++++++-- workflow/scripts/script_utils.py | 3 +++ workflow/scripts/unzip_like.py | 2 ++ 9 files changed, 61 insertions(+), 6 deletions(-) diff --git a/workflow/rules/automatic.smk b/workflow/rules/automatic.smk index c9beeca..490ef32 100644 --- a/workflow/rules/automatic.smk +++ b/workflow/rules/automatic.smk @@ -10,6 +10,8 @@ rule download_cutout_slope: vector="resources/user/shapes/{shape}.parquet", output: path="resources/automatic/cutout/{shape}/slope.tif", + log: + "logs/{shape}/download_cutout_slope.log", wrapper: "v7.2.0/geo/rasterio/clip-geotiff" @@ -23,6 +25,8 @@ rule download_cutout_bathymetry: vector="resources/user/shapes/{shape}.parquet", output: path="resources/automatic/cutout/{shape}/bathymetry.tif", + log: + "logs/{shape}/download_cutout_bathymetry.log", wrapper: "v7.2.0/geo/rasterio/clip-geotiff" @@ -34,6 +38,8 @@ rule download_globcover: url=internal["resources"]["automatic"]["globcover"], output: "resources/automatic/global/globcover.zip", + log: + "logs/download_globcover.log", conda: "../envs/shell.yaml" shell: @@ -70,6 +76,8 @@ rule download_ghsl: url=internal["resources"]["automatic"]["ghsl"], output: "resources/automatic/global/ghsl_built_s.zip", + log: + "logs/download_ghsl.log", conda: "../envs/shell.yaml" shell: @@ -86,6 +94,8 @@ rule unzip_ghsl: zipfile=rules.download_ghsl.output, output: "resources/automatic/global/ghsl_built_s.tif", + log: + "logs/unzip_ghsl.log", conda: "../envs/shell.yaml" shell: diff --git a/workflow/rules/prepare.smk b/workflow/rules/prepare.smk index 17af1f6..27c7d1a 100644 --- a/workflow/rules/prepare.smk +++ b/workflow/rules/prepare.smk @@ -9,6 +9,8 @@ rule cutout_landcover: landcover=rules.unzip_globcover.output, output: "resources/automatic/cutout/{shape}/landcover.tif", + log: + "logs/{shape}/cutout_landcover.log", conda: "../envs/default.yaml" shell: @@ -25,6 +27,8 @@ rule cutout_settlement: settlement=rules.unzip_ghsl.output, output: "resources/automatic/cutout/{shape}/settlement.tif", + log: + "logs/{shape}/cutout_settlement.log", conda: "../envs/default.yaml" shell: diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index b210f86..e86f5f7 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -15,13 +15,16 @@ rule prepare_resampled_inputs: "resources/automatic/{shape}.resampled_inputs.png", category="resampled_input", ), + log: + "logs/{shape}/prepare_resampled_inputs.log", conda: "../envs/default.yaml" shell: """ + set -x python "{input.script}" \ "{input.shapes}" "{input.land_cover_path}" "{input.slope_path}" "{input.settlement_path}" "{input.bathymetry_path}" "{input.protected_area_path}" \ - "{output.resampled_input}" "{output.plot}" + "{output.resampled_input}" "{output.plot}" 2> "{log}" """ @@ -41,12 +44,14 @@ rule area_potential: "results/{shape}/area_potential_{tech}.png", category="area_potential", ), + log: + "logs/{shape}/area_potential_{tech}.log", conda: "../envs/default.yaml" shell: """ set -x - python "{input.script}" "{input.shapes}" "{input.resampled_path}" "{params.config}" "{params.buffer_crs}" "{output.area_potential}" "{output.plot}" + python "{input.script}" "{input.shapes}" "{input.resampled_path}" "{params.config}" "{params.buffer_crs}" "{output.area_potential}" "{output.plot}" 2> "{log}" """ @@ -66,6 +71,8 @@ rule area_potential_report: "results/{shape}/area_potential_report.html", category="area_potential_report", ), + log: + "logs/{shape}/area_potential_report.log", conda: "../envs/default.yaml" script: diff --git a/workflow/scripts/area_potential.py b/workflow/scripts/area_potential.py index bf7975f..116d372 100644 --- a/workflow/scripts/area_potential.py +++ b/workflow/scripts/area_potential.py @@ -1,7 +1,8 @@ +"""This script calculates the area potential based on the provided configuration.""" + import click import geo import geopandas as gpd -import matplotlib.pyplot as plt import xarray as xr import yaml @@ -16,6 +17,20 @@ def get_area_potential( shapes_path, resampled_path, config, buffer_crs, output_path, plot_path ): + """Calculate the area potential based on the provided configuration. + + Args: + shapes_path (str): Path to the input shapes in the parquet format. + resampled_path (str): Path to the resampled input data in the NetCDF format. + config (str): Configuration YAML string. + buffer_crs (str): Coordinate Reference System for buffering shapes. + output_path (str): Path to save the resulting area potential raster. + plot_path (str): Path to save the plot of the area potential. + + Returns: + None + + """ shapes = gpd.read_parquet(shapes_path) ds = xr.open_dataset(resampled_path, decode_coords="all") # FIXME: this is a workaround for the CRS not being set correctly; not sure why @@ -82,7 +97,7 @@ def get_area_potential( potential_da.rio.to_raster(output_path, driver="GTiff", compress="LZW") plot = potential_da.plot() - plt.savefig(plot_path, bbox_inches="tight") + plot.savefig(plot_path, bbox_inches="tight") if __name__ == "__main__": diff --git a/workflow/scripts/geo.py b/workflow/scripts/geo.py index 01bd314..654d3a1 100644 --- a/workflow/scripts/geo.py +++ b/workflow/scripts/geo.py @@ -1,3 +1,5 @@ +"""This module provides functions to buffer geometries using UTM projections.""" + import warnings import geopandas as gpd diff --git a/workflow/scripts/report.py b/workflow/scripts/report.py index 7d276aa..4d799dc 100644 --- a/workflow/scripts/report.py +++ b/workflow/scripts/report.py @@ -1,3 +1,5 @@ +"""This script generates a report summarizing area potentials for different technologies.""" + import geopandas as gpd import pandas as pd import rioxarray as rxr @@ -5,6 +7,7 @@ def report(shapes, resampled_path, area_potentials, csv_path, html_path): + """Generate a report summarizing area potentials for different technologies.""" shapes = gpd.read_parquet(shapes) ds_inputs = xr.open_dataset(resampled_path, decode_coords="all") # FIXME: this is a workaround for the CRS not being set correctly; not sure why diff --git a/workflow/scripts/resample.py b/workflow/scripts/resample.py index 96e24ea..0295218 100644 --- a/workflow/scripts/resample.py +++ b/workflow/scripts/resample.py @@ -1,3 +1,5 @@ +"""This script resamples various geospatial datasets to a common shape and resolution.""" + import math import click @@ -68,6 +70,7 @@ def get_suitable_land_cover_type(ds_land_cover, suitable_land_cover_types): + """Convert raw GlobCover data to a dataset with suitable land cover types.""" suitable_land_cover = xr.Dataset(coords=ds_land_cover.coords) # convert the input value to land cover type of interest @@ -158,7 +161,7 @@ def _rasterize_regions(shapes, reference_raster): @click.argument("protected_area_path", type=str) @click.argument("output_path", type=str) @click.argument("plot_path", type=str) -def get_same_shape_and_resolution( +def resample_inputs( shapes_path, land_cover_path, slope_path, @@ -168,6 +171,12 @@ def get_same_shape_and_resolution( output_path, plot_path, ): + """Resample various geospatial datasets to a common shape and resolution. + + Results are saved to the specified output path in NetCDF format, + and a plot of the resampled data is saved to the specified plot path. + + """ shapes = gpd.read_parquet(shapes_path) ## @@ -291,4 +300,4 @@ def get_same_shape_and_resolution( if __name__ == "__main__": - get_same_shape_and_resolution() + resample_inputs() diff --git a/workflow/scripts/script_utils.py b/workflow/scripts/script_utils.py index 995a29c..bba8d51 100644 --- a/workflow/scripts/script_utils.py +++ b/workflow/scripts/script_utils.py @@ -1,9 +1,12 @@ +"""Utility functions.""" + import math import matplotlib.pyplot as plt def plot_all_dataset_variables(ds, ncols=2, savefig=None): + """Plot all variables in an xarray dataset on a grid of plots.""" # Drop dimensionless variables ds = ds.drop_vars(lambda x: [v for v, da in x.variables.items() if not da.ndim]) diff --git a/workflow/scripts/unzip_like.py b/workflow/scripts/unzip_like.py index 25e23e0..028c670 100644 --- a/workflow/scripts/unzip_like.py +++ b/workflow/scripts/unzip_like.py @@ -1,3 +1,5 @@ +"""Emulates the `unzip` command across platforms.""" + import os import zipfile From 42c4e119ac48bde185d5bf14cf2c38661ef20172 Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Wed, 9 Jul 2025 14:59:52 +0200 Subject: [PATCH 31/59] Fix savefig call --- workflow/scripts/area_potential.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/workflow/scripts/area_potential.py b/workflow/scripts/area_potential.py index 116d372..7aa8a72 100644 --- a/workflow/scripts/area_potential.py +++ b/workflow/scripts/area_potential.py @@ -3,6 +3,7 @@ import click import geo import geopandas as gpd +import matplotlib.pyplot as plt import xarray as xr import yaml @@ -96,8 +97,8 @@ def get_area_potential( potential_da = potential_da.transpose("band", "y", "x") potential_da.rio.to_raster(output_path, driver="GTiff", compress="LZW") - plot = potential_da.plot() - plot.savefig(plot_path, bbox_inches="tight") + potential_da.plot() + plt.savefig(plot_path, bbox_inches="tight") if __name__ == "__main__": From 8e81fd93e34919e0283a078a4963ec6db701db55 Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Thu, 10 Jul 2025 10:34:54 +0200 Subject: [PATCH 32/59] Add INTERFACE.yaml --- INTERFACE.yaml | 25 ++++++++++--------------- README.md | 1 - 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/INTERFACE.yaml b/INTERFACE.yaml index 72104d4..9f15cca 100644 --- a/INTERFACE.yaml +++ b/INTERFACE.yaml @@ -1,20 +1,15 @@ # Module Input-Output structure for automated doc. generation resources: user: - user_message.md: "Example file emulating external files requested from a user." + "shapes/{shape}.parquet": "Region geometries in parquet format. These should conform to the schema defined in https://github.com/calliope-project/module_geo_boundaries/blob/main/workflow/internal/shape.schema.yaml" + "wdpa.gdb": "WDPA protected areas database from https://www.protectedplanet.net/ in GeoDB format (choose 'File Geodatabase' when downloading)." automatic: - dummy_readme.md: "Example file emulating downloads determined by internal settings." + "{shape}.resampled_inputs.nc": "Resampled and rasterized input data layers for the regions, including land cover, slope, settlement, bathymetry, and protected areas." + "{shape}.resampled_inputs.png": "Visualization of the resampled input data for the regions." results: - combined_text.md: "Example file emulating module results." - -# Wildcard example: -# resources: -# user: -# shapes_{resolution}.geojson: region geometries. -# automatic: -# technology_data.parquet: dataset with technology characteristics. -# results: -# '{resolution}/tech_capacity.parquet': description of output data. -# '{resolution}/results_image.png': description of output image. -# wildcards: -# resolution: description of the wildcard’s purpose. + "results/{shape}/area_potential_{tech}.tif": "Area potential GeoTIFF raster for the specified technology and region geometries." + "results/{shape}/area_potential_report.csv": "CSV summary report of area potential for all techs, for the given region geometries." + "results/{shape}/area_potential_report.html": "HTML summary report of area potential for all techs, for the given region geometries." +wildcards: + shape: "Name of the shape to be processed, e.g., 'world', 'europe', 'MEX'." + tech: "Name of the technology, e.g., 'pv_rooftop' or 'wind_offshore'. Available technologies are defined in the module configuration." diff --git a/README.md b/README.md index 767ba4a..32baad6 100644 --- a/README.md +++ b/README.md @@ -52,4 +52,3 @@ snakemake --use-conda # run the workflow! * License: "The GHSL has been produced by the EC JRC as open and free data. Reuse is authorised, provided the source is acknowledged." * [WDPA (World Database on Protected Areas)](https://www.protectedplanet.net/) * License: Non-commercial allowed. Citation: "UNEP-WCMC and IUCN (2025), Protected Planet: The World Database on Protected Areas (WDPA) and World Database on Other Effective Area-based Conservation Measures (WD-OECM) [Online], June 2025, Cambridge, UK: UNEP-WCMC and IUCN. Available at: www.protectedplanet.net." -* Slope is derived from the GMTED2010 public-domain dataset and stored as unsigned 8-bit integers to save space, so we only have integer slope values. From 47136ee17fd1373bbf0fb4166e9869749eb54c2e Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Fri, 11 Jul 2025 11:17:03 +0200 Subject: [PATCH 33/59] Template updates, README cleanup, add pixi.lock --- README.md | 17 +- docs/index.md | 14 + pixi.lock | 8785 +++++++++++++++++++++++++++++++++++ tests/integration/Snakefile | 16 +- 4 files changed, 8809 insertions(+), 23 deletions(-) create mode 100644 pixi.lock diff --git a/README.md b/README.md index 32baad6..ab60938 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ pixi install --all For testing, simply run: ```shell -pixi run test +pixi run test-integration ``` To view the documentation locally, use: @@ -37,18 +37,5 @@ To test a minimal example of a workflow using this module: ```shell pixi shell # activate this project's environment cd tests/integration/ # navigate to the integration example -snakemake --use-conda # run the workflow! +snakemake --use-conda --cores 2 # run the workflow! ``` - -## Data sources and licenses - -* [GEDTM30](https://github.com/openlandmap/GEDTM30) for slope - * License: Creative Commons Attribution 4.0 International -* [GlobCover land cover data](https://due.esrin.esa.int/page_globcover.php) - * License: "You may use the GlobCover land cover map for educational and/or scientific purposes, without any fee on the condition that you credit ESA and the Université Catholique de Louvain as the source of the GlobCover products." -* [GEBCO (General Bathymetric Chart of the Oceans)](https://www.gebco.net/data-products/gridded-bathymetry-data) 15 arc-second data - * License: "The GEBCO Grid is placed in the public domain and may be used free of charge. [...] Users must: Acknowledge the source of The GEBCO Grid. A suitable form of attribution is given in the documentation that accompanies The GEBCO Grid." -* [GHSL (Global Human Settlement Layer)](https://human-settlement.emergency.copernicus.eu/download.php) built-up surface data (R2023, GHS-BUILT-S, 100m resolution) - * License: "The GHSL has been produced by the EC JRC as open and free data. Reuse is authorised, provided the source is acknowledged." -* [WDPA (World Database on Protected Areas)](https://www.protectedplanet.net/) - * License: Non-commercial allowed. Citation: "UNEP-WCMC and IUCN (2025), Protected Planet: The World Database on Protected Areas (WDPA) and World Database on Other Effective Area-based Conservation Measures (WD-OECM) [Online], June 2025, Cambridge, UK: UNEP-WCMC and IUCN. Available at: www.protectedplanet.net." diff --git a/docs/index.md b/docs/index.md index a983c8d..cfd7c77 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,3 +2,17 @@ Welcome to the documentation of the `module_area_potentials` data module! Please consult the [specification guidelines](./specification.md) and the [`clio` documentation](https://clio.readthedocs.io/) for more information. + + +## Data sources + +* [GEDTM30](https://github.com/openlandmap/GEDTM30) for slope + * License: Creative Commons Attribution 4.0 International +* [GlobCover land cover data](https://due.esrin.esa.int/page_globcover.php) + * License: "You may use the GlobCover land cover map for educational and/or scientific purposes, without any fee on the condition that you credit ESA and the Université Catholique de Louvain as the source of the GlobCover products." +* [GEBCO (General Bathymetric Chart of the Oceans)](https://www.gebco.net/data-products/gridded-bathymetry-data) 15 arc-second data + * License: "The GEBCO Grid is placed in the public domain and may be used free of charge. [...] Users must: Acknowledge the source of The GEBCO Grid. A suitable form of attribution is given in the documentation that accompanies The GEBCO Grid." +* [GHSL (Global Human Settlement Layer)](https://human-settlement.emergency.copernicus.eu/download.php) built-up surface data (R2023, GHS-BUILT-S, 100m resolution) + * License: "The GHSL has been produced by the EC JRC as open and free data. Reuse is authorised, provided the source is acknowledged." +* [WDPA (World Database on Protected Areas)](https://www.protectedplanet.net/) + * License: Non-commercial allowed. Citation: "UNEP-WCMC and IUCN (2025), Protected Planet: The World Database on Protected Areas (WDPA) and World Database on Other Effective Area-based Conservation Measures (WD-OECM) [Online], June 2025, Cambridge, UK: UNEP-WCMC and IUCN. Available at: www.protectedplanet.net." \ No newline at end of file diff --git a/pixi.lock b/pixi.lock new file mode 100644 index 0000000..019195f --- /dev/null +++ b/pixi.lock @@ -0,0 +1,8785 @@ +version: 6 +environments: + default: + channels: + - url: https://conda.anaconda.org/conda-forge/ + - url: https://conda.anaconda.org/bioconda/ + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-48.1-unix_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/amply-0.1.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/appdirs-1.4.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/archspec-0.2.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argparse-dataclass-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-atk-2.38.0-h0630a04_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-core-2.40.3-h0630a04_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/atk-1.0-2.38.0-h04ea711_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/black-24.10.0-py312h7900ff3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/boltons-25.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h2ec8cdc_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.5-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.7.9-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.7.9-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py312h06ac9bb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.2.1-pyh707e725_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/clio-tools-2025.03.03-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coin-or-cbc-2.10.12-h00e76a6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coin-or-cgl-0.60.9-h82e2f02_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coin-or-clp-1.17.10-h8a7a1e7_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coin-or-osi-0.108.11-h96cc833_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coin-or-utils-2.11.12-h3a12e53_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/conda-25.5.1-py312h7900ff3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/conda-inject-1.3.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/conda-libmamba-solver-25.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/conda-package-handling-2.4.0-pyh7900ff3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/conda-package-streaming-0.12.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/configargparse-1.7.1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/connection_pool-0.0.3-pyhd3deb0d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/cpp-expected-1.1.0-hff21bea_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h3c4dab8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.14-py312h2ec8cdc_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/distro-1.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dpath-2.2.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/epoxy-1.5.10-h166bdaf_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fmt-11.1.4-h07f6e7f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.15.0-h7e30c49_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.13.3-ha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.10-h36c2ea0_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/frozendict-2.4.6-py312h66e93f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.42.12-hb9ae30d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.44-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.84.2-h4833e2c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-h5888daf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/graphviz-13.1.0-hcae58fd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gtk3-3.24.43-h0c6a113_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gts-0.7.6-h977cf35_4.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-11.2.1-h3beb420_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/hicolor-icon-theme-0.17-ha770c72_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/humanfriendly-10.0-pyh707e725_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/immutables-0.21-py312h66e93f0_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipdb-0.13.13-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh3099207_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.4.0-pyhfa0c392_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpatch-1.33-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/jsonpointer-3.0.0-py312h7900ff3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.24.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.4.1-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.1-h166bdaf_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarchive-3.8.1-gpl_h98cc613_100.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-32_h59b9bed_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-32_he106b2a_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-hb8b1518_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.14.1-h332b0f4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.24-h86f0d12_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.13.3-ha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.13.3-h48d6fc4_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgd-2.3.3-h6f5c62b_11.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.84.2-h3618099_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h4ce23a2_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-32_h7ac8fdf_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapacke-3.9.0-32_he2f377e_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmamba-2.3.0-h44402ff_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmambapy-2.3.0-py312hd15d01f_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.64.0-h161d5f1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.50-h943b412_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/librsvg-2.58.4-he92a37e_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.20-h4ab18f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsolv-0.7.33-h7955e40_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.2-hee844dc_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.0-hf01ce69_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.10.0-h65c71a3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.8-h4bc477f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lzo-2.10-hd590300_1001.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py312h178313f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/menuinst-2.3.0-py312h7900ff3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.16.1-py312h66e93f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.5-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.11.3-he02047a_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.1-py312h6cf2f7f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.1-py312hf79963d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandera-0.25.0-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandera-base-0.25.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.4-hadf4263_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.45-hc749103_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.2-h29eaf8c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/plac-1.4.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.8-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.0.0-py312h66e93f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pulp-2.8.0-py312hd0750ca_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pybind11-abi-4-hd8ed1ab_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/pycosat-0.6.6-py312h66e93f0_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.11.7-pyh3cfb1c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.33.2-py312h680f630_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pydot-4.0.1-py312h7900ff3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.11-h9e4cc4f_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py312h178313f_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.0.0-py312hbf22597_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/reproc-14.2.5.post0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/reproc-cpp-14.2.5.post0-h5888daf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/reretry-0.11.8-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.26.0-py312h680f630_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruamel.yaml-0.18.14-py312h66e93f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruamel.yaml.clib-0.2.8-py312h66e93f0_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.12.2-hcc1af86_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/simdjson-3.13.0-h84d6215_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smart_open-7.3.0.post1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/bioconda/noarch/snakefmt-0.11.0-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-common-1.20.1-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-executor-plugins-9.3.7-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-logger-plugins-1.2.3-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-report-plugins-1.1.0-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-storage-plugins-4.2.1-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-minimal-9.8.0-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tabulate-0.9.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/throttler-1.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.1-py312h66e93f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/truststore-0.10.1-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typeguard-4.4.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.14.1-h4440ef1_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.14.1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_inspect-0.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.24.0-h3e06ad9_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-1.17.2-py312h66e93f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.45-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.12-h4f16b4b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcomposite-0.4.6-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcursor-1.2.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdamage-1.1.6-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.6-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.1-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxi-1.8.2-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxinerama-1.1.5-h5888daf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrandr-1.5.4-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxtst-1.2.5-hb9d3cd8_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-cpp-0.8.0-h3f2d84a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/yte-1.8.1-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h3b0a872_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py312h66e93f0_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-48.1-unix_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/amply-0.1.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/appdirs-1.4.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/archspec-0.2.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argparse-dataclass-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/atk-1.0-2.38.0-hd03087b_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/black-24.10.0-py312h81bd7bf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/boltons-25.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.1.0-py312hd8f9ff3_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.5-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.7.9-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cairo-1.18.4-h6a3b0d2_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.7.9-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-1.17.1-py312h0fad829_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.2.1-pyh707e725_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/clio-tools-2025.03.03-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coin-or-cbc-2.10.12-h8ec3750_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coin-or-cgl-0.60.9-h7ef17a8_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coin-or-clp-1.17.10-h73553b4_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coin-or-osi-0.108.11-h1c7c69d_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coin-or-utils-2.11.12-h38baedf_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/conda-25.5.1-py312h81bd7bf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/conda-inject-1.3.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/conda-libmamba-solver-25.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/conda-package-handling-2.4.0-pyh7900ff3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/conda-package-streaming-0.12.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/configargparse-1.7.1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/connection_pool-0.0.3-pyhd3deb0d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cpp-expected-1.1.0-h177bc72_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.14-py312hd8f9ff3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/distro-1.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dpath-2.2.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/epoxy-1.5.10-h1c322ee_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fmt-11.1.4-h440487c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fontconfig-2.15.0-h1383a14_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.13.3-hce30654_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fribidi-1.0.10-h27ca646_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/frozendict-2.4.6-py312h0bf5046_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdk-pixbuf-2.42.12-h7ddc832_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.44-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/glib-tools-2.84.2-h1dc7a0c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphite2-1.3.14-h286801f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphviz-13.1.0-haeab78c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gtk3-3.24.43-h07173f4_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gts-0.7.6-he42f4ea_4.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/harfbuzz-11.2.1-hab40de2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hicolor-icon-theme-0.17-hce30654_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/humanfriendly-10.0-pyh707e725_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/immutables-0.21-py312hea69d52_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipdb-0.13.13-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh57ce528_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.4.0-pyhfa0c392_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpatch-1.33-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/jsonpointer-3.0.0-py312h81bd7bf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.24.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.4.1-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.0.0-hd64df32_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarchive-3.8.1-gpl_h46e8061_100.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.9.0-32_h10e41b3_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.9.0-32_hb3479ef_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.14.1-h73640d1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-20.1.8-ha82da77_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.24-h5773f1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.0-h286801f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.6-h1da3d7d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.13.3-hce30654_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.13.3-h1d14073_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgd-2.3.3-hb2c3a21_11.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-5.0.0-14_2_0_h6c33f7e_103.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-14.2.0-h6c33f7e_103.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libglib-2.84.2-hbec27ea_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-hfe07756_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libintl-0.25.1-h493aca8_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.1.0-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.9.0-32_hc9a63f6_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapacke-3.9.0-32_hbb7bcf8_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmamba-2.3.0-h8ac2bdb_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmambapy-2.3.0-py312h3097733_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.64.0-h6d7220d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.30-openmp_hf332438_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.50-h3783ad8_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/librsvg-2.58.4-h266df6f_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.20-h99b78c6_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsolv-0.7.33-h13dfb9a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.50.2-hf8de324_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-h1590b86_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.0-h2f21f7c_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.13.8-h52572c6_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-20.1.7-hdb05f8b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-c-1.10.0-h286801f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lzo-2.10-h93a5062_1001.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.2-py312h998013c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/menuinst-2.3.0-py312h81bd7bf_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.16.1-py312hea69d52_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.5-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nlohmann_json-3.11.3-h00cdb27_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-2.3.1-py312h113b91d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.5.1-h81ee809_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pandas-2.3.1-py312h98f7732_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandera-0.25.0-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandera-base-0.25.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pango-1.56.4-h875632e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.45-ha881caa_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pixman-0.46.2-h2f9eb0b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/plac-1.4.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.8-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.0.0-py312hea69d52_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pulp-2.8.0-py312h38bd297_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pybind11-abi-4-hd8ed1ab_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pycosat-0.6.6-py312hea69d52_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.11.7-pyh3cfb1c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pydantic-core-2.33.2-py312hd3c0895_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pydot-4.0.1-py312h81bd7bf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.11-hc22306f_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.2-py312h998013c_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.0.0-py312hf4875e0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/reproc-14.2.5.post0-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/reproc-cpp-14.2.5.post0-h286801f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/reretry-0.11.8-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.26.0-py312hd3c0895_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruamel.yaml-0.18.14-py312hea69d52_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruamel.yaml.clib-0.2.8-py312h0bf5046_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.12.2-h412e174_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/simdjson-3.13.0-ha393de7_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smart_open-7.3.0.post1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/bioconda/noarch/snakefmt-0.11.0-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-common-1.20.1-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-executor-plugins-9.3.7-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-logger-plugins-1.2.3-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-report-plugins-1.1.0-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-storage-plugins-4.2.1-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-minimal-9.8.0-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tabulate-0.9.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/throttler-1.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.1-py312hea69d52_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/truststore-0.10.1-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typeguard-4.4.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.14.1-h4440ef1_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.14.1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_inspect-0.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/wrapt-1.17.2-py312hea69d52_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h3422bc3_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-cpp-0.8.0-ha1acc90_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/yte-1.8.1-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-hc1bb282_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.23.0-py312hea69d52_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda + win-64: + - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-2_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/amply-0.1.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/appdirs-1.4.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/archspec-0.2.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argparse-dataclass-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/black-24.10.0-py312h2e8e312_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/boltons-25.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py312h275cf98_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h2466b09_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.7.9-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cairo-1.18.4-h5782bbf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.7.9-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-1.17.1-py312h4389bb4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.2.1-pyh7428d3b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/clio-tools-2025.03.03-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/coin-or-cbc-2.10.12-h1c9cd67_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/coin-or-cgl-0.60.9-h8aa12e5_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/coin-or-clp-1.17.10-ha4dcb5c_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/coin-or-osi-0.108.11-h7d32387_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/coin-or-utils-2.11.12-hf90b928_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/conda-25.5.1-py312h2e8e312_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/conda-inject-1.3.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/conda-libmamba-solver-25.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/conda-package-handling-2.4.0-pyh7900ff3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/conda-package-streaming-0.12.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/configargparse-1.7.1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/connection_pool-0.0.3-pyhd3deb0d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/cpp-expected-1.1.0-hc790b64_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.12.11-py312hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.14-py312h275cf98_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/distro-1.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dpath-2.2.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fmt-11.1.4-h5f12afc_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fontconfig-2.15.0-h765892d_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.13.3-h57928b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fribidi-1.0.10-h8d14728_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/frozendict-2.4.6-py312h4389bb4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/getopt-win32-0.1-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.44-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/graphite2-1.3.14-he0c23c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/graphviz-13.1.0-ha5e8f4b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/gts-0.7.6-h6b5321d_4.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/harfbuzz-11.2.1-h8796e6f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/humanfriendly-10.0-pyh7428d3b_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/icu-75.1-he0c23c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/immutables-0.21-py312h4389bb4_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/intel-openmp-2024.2.1-h57928b3_1083.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipdb-0.13.13-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh4bbf305_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.4.0-pyh6be1c34_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpatch-1.33-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/jsonpointer-3.0.0-py312h2e8e312_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.24.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.4.1-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh5737063_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.0.0-h6470a55_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libarchive-3.8.1-gpl_h1ca5a36_100.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.9.0-32_h641d27c_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.9.0-32_h5e41251_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.14.1-h88aaa65_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.24-h76ddb4d_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.0-he0c23c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.6-h537db12_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype-2.13.3-h57928b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype6-2.13.3-h0b5ce68_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgcc-15.1.0-h1383e82_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgd-2.3.3-h7208af6_11.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.84.2-hbc94333_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgomp-15.1.0-h1383e82_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.11.2-default_ha69328c_1001.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-h135ad9c_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.0-h2466b09_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.9.0-32_h1aa476e_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libmamba-2.3.0-hd0d0357_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libmambapy-2.3.0-py312h1fc3bf7_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.50-h95bef1e_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.20-hc70643c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsolv-0.7.33-hbb528cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.50.2-hf5d6505_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libssh2-1.11.1-h9aa295b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libtiff-4.7.0-h05922d8_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libwebp-base-1.6.0-h4d5522a_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libwinpthread-12.0.0.r4.gg4f2fc60ca-h57928b3_9.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxcb-1.17.0-h0e4246c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.13.8-h442d1da_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.10.0-h2466b09_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lzo-2.10-hcfcfb64_1001.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.2-py312h31fea79_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/menuinst-2.3.0-py312hbb81ca0_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2024.2.2-h66d3029_15.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-include-2024.2.2-h66d3029_15.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-static-2024.2.2-h66d3029_15.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.16.1-py312h4389bb4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.5-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/nlohmann_json-3.11.3-he0c23c2_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.3.1-py312h12c3145_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.5.1-h725018a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pandas-2.3.1-py312hc128f0a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandera-0.25.0-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandera-base-0.25.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pango-1.56.4-h03d888a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pcre2-10.45-h99c9b8b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pixman-0.46.2-had0cd8c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/plac-1.4.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.8-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.0.0-py312h4389bb4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pthread-stubs-0.4-h0e40799_1002.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pulp-2.8.0-py312he39998a_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pybind11-abi-4-hd8ed1ab_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/pycosat-0.6.6-py312h4389bb4_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.11.7-pyh3cfb1c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pydantic-core-2.33.2-py312h8422cdd_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pydot-4.0.1-py312h2e8e312_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyreadline3-3.5.4-py312h2e8e312_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.11-h3f84c4b_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-307-py312h275cf98_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.2-py312h31fea79_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.0.0-py312hd7027bb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/reproc-14.2.5.post0-h2466b09_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/reproc-cpp-14.2.5.post0-he0c23c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/reretry-0.11.8-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.26.0-py312hdabe01f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ruamel.yaml-0.18.14-py312h4389bb4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ruamel.yaml.clib-0.2.8-py312h4389bb4_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.12.2-hd40eec1_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/simdjson-3.13.0-hc790b64_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smart_open-7.3.0.post1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/bioconda/noarch/snakefmt-0.11.0-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-common-1.20.1-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-executor-plugins-9.3.7-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-logger-plugins-1.2.3-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-report-plugins-1.1.0-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-storage-plugins-4.2.1-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-minimal-9.8.0-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tabulate-0.9.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2021.13.0-h62715c5_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/throttler-1.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.1-py312h4389bb4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/truststore-0.10.1-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typeguard-4.4.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.14.1-h4440ef1_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.14.1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_inspect-0.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_26.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_26.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.44.35208-h38c0c73_26.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/wrapt-1.17.2-py312h4389bb4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libice-1.1.2-h0e40799_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libsm-1.2.6-h0e40799_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libx11-1.8.12-hf48077a_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxau-1.0.12-h0e40799_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxdmcp-1.1.5-h0e40799_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxext-1.3.6-h0e40799_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxpm-3.5.17-h0e40799_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxt-1.3.1-h0e40799_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h8ffe710_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-cpp-0.8.0-he0c23c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/yte-1.8.1-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-ha9f60a1_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.23.0-py312h4389bb4_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-hbeecb71_2.conda + docs: + channels: + - url: https://conda.anaconda.org/conda-forge/ + - url: https://conda.anaconda.org/bioconda/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-48.1-unix_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/amply-0.1.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/appdirs-1.4.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/archspec-0.2.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argparse-dataclass-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-atk-2.38.0-h0630a04_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-core-2.40.3-h0630a04_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/atk-1.0-2.38.0-h04ea711_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backrefs-5.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/black-24.10.0-py312h7900ff3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/boltons-25.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h2ec8cdc_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.5-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.7.9-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.7.9-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py312h06ac9bb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.2.1-pyh707e725_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/clio-tools-2025.03.03-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coin-or-cbc-2.10.12-h00e76a6_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coin-or-cgl-0.60.9-h82e2f02_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coin-or-clp-1.17.10-h8a7a1e7_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coin-or-osi-0.108.11-h96cc833_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/coin-or-utils-2.11.12-h3a12e53_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/conda-25.5.1-py312h7900ff3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/conda-inject-1.3.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/conda-libmamba-solver-25.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/conda-package-handling-2.4.0-pyh7900ff3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/conda-package-streaming-0.12.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/configargparse-1.7.1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/connection_pool-0.0.3-pyhd3deb0d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/cpp-expected-1.1.0-hff21bea_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h3c4dab8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.14-py312h2ec8cdc_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/distro-1.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dpath-2.2.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/epoxy-1.5.10-h166bdaf_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fmt-11.1.4-h07f6e7f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.15.0-h7e30c49_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.13.3-ha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.10-h36c2ea0_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/frozendict-2.4.6-py312h66e93f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.42.12-hb9ae30d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ghp-import-2.1.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.44-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.84.2-h4833e2c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-h5888daf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/graphviz-13.1.0-hcae58fd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gtk3-3.24.43-h0c6a113_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/gts-0.7.6-h977cf35_4.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-11.2.1-h3beb420_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/hicolor-icon-theme-0.17-ha770c72_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/humanfriendly-10.0-pyh707e725_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/immutables-0.21-py312h66e93f0_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipdb-0.13.13-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh3099207_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.4.0-pyhfa0c392_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpatch-1.33-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/jsonpointer-3.0.0-py312h7900ff3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.24.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.4.1-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.1-h166bdaf_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarchive-3.8.1-gpl_h98cc613_100.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-32_h59b9bed_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-32_he106b2a_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-hb8b1518_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.14.1-h332b0f4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.24-h86f0d12_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.13.3-ha770c72_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.13.3-h48d6fc4_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgd-2.3.3-h6f5c62b_11.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.84.2-h3618099_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h4ce23a2_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-32_h7ac8fdf_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapacke-3.9.0-32_he2f377e_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmamba-2.3.0-h44402ff_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmambapy-2.3.0-py312hd15d01f_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.64.0-h161d5f1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.50-h943b412_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/librsvg-2.58.4-he92a37e_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.20-h4ab18f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsolv-0.7.33-h7955e40_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.2-hee844dc_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.0-hf01ce69_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.10.0-h65c71a3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.8-h4bc477f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lzo-2.10-hd590300_1001.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.8.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py312h178313f_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/menuinst-2.3.0-py312h7900ff3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mergedeep-1.3.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-1.6.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-get-deps-0.2.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-9.6.15-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-extensions-1.3.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.16.1-py312h66e93f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.5-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.11.3-he02047a_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.1-py312h6cf2f7f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/paginate-0.5.7-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.1-py312hf79963d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandera-0.25.0-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandera-base-0.25.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.4-hadf4263_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.45-hc749103_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.2-h29eaf8c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/plac-1.4.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.8-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.0.0-py312h66e93f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pulp-2.8.0-py312hd0750ca_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pybind11-abi-4-hd8ed1ab_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/pycosat-0.6.6-py312h66e93f0_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.11.7-pyh3cfb1c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.33.2-py312h680f630_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pydot-4.0.1-py312h7900ff3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pymdown-extensions-10.16-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.11-h9e4cc4f_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py312h178313f_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyyaml-env-tag-1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.0.0-py312hbf22597_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/reproc-14.2.5.post0-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/reproc-cpp-14.2.5.post0-h5888daf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/reretry-0.11.8-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.26.0-py312h680f630_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruamel.yaml-0.18.14-py312h66e93f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruamel.yaml.clib-0.2.8-py312h66e93f0_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.12.2-hcc1af86_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/simdjson-3.13.0-h84d6215_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smart_open-7.3.0.post1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/bioconda/noarch/snakefmt-0.11.0-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-common-1.20.1-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-executor-plugins-9.3.7-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-logger-plugins-1.2.3-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-report-plugins-1.1.0-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-storage-plugins-4.2.1-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-minimal-9.8.0-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tabulate-0.9.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/throttler-1.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.1-py312h66e93f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/truststore-0.10.1-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typeguard-4.4.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.14.1-h4440ef1_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.14.1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_inspect-0.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/watchdog-6.0.0-py312h7900ff3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.24.0-h3e06ad9_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-1.17.2-py312h66e93f0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.45-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.12-h4f16b4b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcomposite-0.4.6-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcursor-1.2.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdamage-1.1.6-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.6-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.1-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxi-1.8.2-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxinerama-1.1.5-h5888daf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrandr-1.5.4-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxtst-1.2.5-hb9d3cd8_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-cpp-0.8.0-h3f2d84a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/yte-1.8.1-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h3b0a872_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py312h66e93f0_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda + - pypi: https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/fd/a40c621ff207f3ce8e484aa0fc8ba4eb6e3ecf52e15b42ba764b457a9550/editorconfig-0.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/14/1c65fccf8413d5f5c6e8425f84675169654395098000d8bddc4e9d3390e1/jsbeautifier-1.15.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/24/ce/c8a41cb0f3044990c8afbdc20c853845a9e940995d4e0cffecafbb5e927b/mkdocs_mermaid2_plugin-1.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-48.1-unix_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/amply-0.1.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/appdirs-1.4.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/archspec-0.2.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argparse-dataclass-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/atk-1.0-2.38.0-hd03087b_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backrefs-5.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/black-24.10.0-py312h81bd7bf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/boltons-25.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.1.0-py312hd8f9ff3_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.5-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.7.9-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cairo-1.18.4-h6a3b0d2_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.7.9-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-1.17.1-py312h0fad829_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.2.1-pyh707e725_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/clio-tools-2025.03.03-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coin-or-cbc-2.10.12-h8ec3750_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coin-or-cgl-0.60.9-h7ef17a8_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coin-or-clp-1.17.10-h73553b4_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coin-or-osi-0.108.11-h1c7c69d_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/coin-or-utils-2.11.12-h38baedf_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/conda-25.5.1-py312h81bd7bf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/conda-inject-1.3.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/conda-libmamba-solver-25.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/conda-package-handling-2.4.0-pyh7900ff3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/conda-package-streaming-0.12.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/configargparse-1.7.1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/connection_pool-0.0.3-pyhd3deb0d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cpp-expected-1.1.0-h177bc72_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.14-py312hd8f9ff3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/distro-1.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dpath-2.2.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/epoxy-1.5.10-h1c322ee_1.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fmt-11.1.4-h440487c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fontconfig-2.15.0-h1383a14_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.13.3-hce30654_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fribidi-1.0.10-h27ca646_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/frozendict-2.4.6-py312h0bf5046_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdk-pixbuf-2.42.12-h7ddc832_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ghp-import-2.1.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.44-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/glib-tools-2.84.2-h1dc7a0c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphite2-1.3.14-h286801f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphviz-13.1.0-haeab78c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gtk3-3.24.43-h07173f4_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/gts-0.7.6-he42f4ea_4.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/harfbuzz-11.2.1-hab40de2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/hicolor-icon-theme-0.17-hce30654_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/humanfriendly-10.0-pyh707e725_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/immutables-0.21-py312hea69d52_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipdb-0.13.13-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh57ce528_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.4.0-pyhfa0c392_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpatch-1.33-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/jsonpointer-3.0.0-py312h81bd7bf_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.24.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.4.1-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.0.0-hd64df32_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarchive-3.8.1-gpl_h46e8061_100.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.9.0-32_h10e41b3_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.9.0-32_hb3479ef_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.14.1-h73640d1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-20.1.8-ha82da77_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.24-h5773f1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.0-h286801f_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.6-h1da3d7d_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.13.3-hce30654_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.13.3-h1d14073_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgd-2.3.3-hb2c3a21_11.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-5.0.0-14_2_0_h6c33f7e_103.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-14.2.0-h6c33f7e_103.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libglib-2.84.2-hbec27ea_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-hfe07756_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libintl-0.25.1-h493aca8_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.1.0-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.9.0-32_hc9a63f6_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapacke-3.9.0-32_hbb7bcf8_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmamba-2.3.0-h8ac2bdb_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmambapy-2.3.0-py312h3097733_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.64.0-h6d7220d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.30-openmp_hf332438_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.50-h3783ad8_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/librsvg-2.58.4-h266df6f_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.20-h99b78c6_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsolv-0.7.33-h13dfb9a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.50.2-hf8de324_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-h1590b86_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.0-h2f21f7c_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.13.8-h52572c6_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-20.1.7-hdb05f8b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-c-1.10.0-h286801f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lzo-2.10-h93a5062_1001.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.8.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.2-py312h998013c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/menuinst-2.3.0-py312h81bd7bf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mergedeep-1.3.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-1.6.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-get-deps-0.2.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-9.6.15-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-extensions-1.3.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.16.1-py312hea69d52_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.5-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/nlohmann_json-3.11.3-h00cdb27_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-2.3.1-py312h113b91d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.5.1-h81ee809_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/paginate-0.5.7-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pandas-2.3.1-py312h98f7732_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandera-0.25.0-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandera-base-0.25.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pango-1.56.4-h875632e_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.45-ha881caa_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pixman-0.46.2-h2f9eb0b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/plac-1.4.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.8-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.0.0-py312hea69d52_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pulp-2.8.0-py312h38bd297_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pybind11-abi-4-hd8ed1ab_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pycosat-0.6.6-py312hea69d52_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.11.7-pyh3cfb1c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pydantic-core-2.33.2-py312hd3c0895_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pydot-4.0.1-py312h81bd7bf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pymdown-extensions-10.16-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.11-hc22306f_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.2-py312h998013c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyyaml-env-tag-1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.0.0-py312hf4875e0_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/reproc-14.2.5.post0-h5505292_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/reproc-cpp-14.2.5.post0-h286801f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/reretry-0.11.8-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.26.0-py312hd3c0895_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruamel.yaml-0.18.14-py312hea69d52_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruamel.yaml.clib-0.2.8-py312h0bf5046_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.12.2-h412e174_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/simdjson-3.13.0-ha393de7_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smart_open-7.3.0.post1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/bioconda/noarch/snakefmt-0.11.0-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-common-1.20.1-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-executor-plugins-9.3.7-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-logger-plugins-1.2.3-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-report-plugins-1.1.0-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-storage-plugins-4.2.1-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-minimal-9.8.0-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tabulate-0.9.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/throttler-1.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.1-py312hea69d52_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/truststore-0.10.1-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typeguard-4.4.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.14.1-h4440ef1_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.14.1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_inspect-0.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/watchdog-6.0.0-py312hea69d52_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/wrapt-1.17.2-py312hea69d52_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h3422bc3_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-cpp-0.8.0-ha1acc90_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/yte-1.8.1-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-hc1bb282_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.23.0-py312hea69d52_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda + - pypi: https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/fd/a40c621ff207f3ce8e484aa0fc8ba4eb6e3ecf52e15b42ba764b457a9550/editorconfig-0.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/14/1c65fccf8413d5f5c6e8425f84675169654395098000d8bddc4e9d3390e1/jsbeautifier-1.15.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/24/ce/c8a41cb0f3044990c8afbdc20c853845a9e940995d4e0cffecafbb5e927b/mkdocs_mermaid2_plugin-1.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl + win-64: + - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-2_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/amply-0.1.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/appdirs-1.4.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/archspec-0.2.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argparse-dataclass-2.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/backrefs-5.8-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/black-24.10.0-py312h2e8e312_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/boltons-25.0.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py312h275cf98_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h2466b09_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.7.9-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cairo-1.18.4-h5782bbf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.7.9-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cffi-1.17.1-py312h4389bb4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.2.1-pyh7428d3b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/clio-tools-2025.03.03-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/coin-or-cbc-2.10.12-h1c9cd67_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/coin-or-cgl-0.60.9-h8aa12e5_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/coin-or-clp-1.17.10-ha4dcb5c_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/coin-or-osi-0.108.11-h7d32387_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/coin-or-utils-2.11.12-hf90b928_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/conda-25.5.1-py312h2e8e312_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/conda-inject-1.3.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/conda-libmamba-solver-25.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/conda-package-handling-2.4.0-pyh7900ff3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/conda-package-streaming-0.12.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/configargparse-1.7.1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/connection_pool-0.0.3-pyhd3deb0d_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/cpp-expected-1.1.0-hc790b64_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.12.11-py312hd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.14-py312h275cf98_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/distro-1.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/dpath-2.2.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fmt-11.1.4-h5f12afc_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fontconfig-2.15.0-h765892d_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.13.3-h57928b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fribidi-1.0.10-h8d14728_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/frozendict-2.4.6-py312h4389bb4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/getopt-win32-0.1-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ghp-import-2.1.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.44-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/graphite2-1.3.14-he0c23c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/graphviz-13.1.0-ha5e8f4b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/gts-0.7.6-h6b5321d_4.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/harfbuzz-11.2.1-h8796e6f_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/humanfriendly-10.0-pyh7428d3b_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/icu-75.1-he0c23c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/immutables-0.21-py312h4389bb4_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/intel-openmp-2024.2.1-h57928b3_1083.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipdb-0.13.13-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh4bbf305_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.4.0-pyh6be1c34_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonpatch-1.33-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/jsonpointer-3.0.0-py312h2e8e312_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.24.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.4.1-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh5737063_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.0.0-h6470a55_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libarchive-3.8.1-gpl_h1ca5a36_100.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.9.0-32_h641d27c_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.9.0-32_h5e41251_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.14.1-h88aaa65_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.24-h76ddb4d_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.0-he0c23c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.6-h537db12_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype-2.13.3-h57928b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype6-2.13.3-h0b5ce68_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgcc-15.1.0-h1383e82_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgd-2.3.3-h7208af6_11.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.84.2-hbc94333_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgomp-15.1.0-h1383e82_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.11.2-default_ha69328c_1001.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-h135ad9c_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.0-h2466b09_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.9.0-32_h1aa476e_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libmamba-2.3.0-hd0d0357_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libmambapy-2.3.0-py312h1fc3bf7_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.50-h95bef1e_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.20-hc70643c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsolv-0.7.33-hbb528cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.50.2-hf5d6505_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libssh2-1.11.1-h9aa295b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libtiff-4.7.0-h05922d8_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libwebp-base-1.6.0-h4d5522a_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libwinpthread-12.0.0.r4.gg4f2fc60ca-h57928b3_9.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxcb-1.17.0-h0e4246c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.13.8-h442d1da_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.10.0-h2466b09_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lzo-2.10-hcfcfb64_1001.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.8.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.2-py312h31fea79_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/menuinst-2.3.0-py312hbb81ca0_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mergedeep-1.3.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-1.6.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-get-deps-0.2.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-9.6.15-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-extensions-1.3.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2024.2.2-h66d3029_15.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-include-2024.2.2-h66d3029_15.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-static-2024.2.2-h66d3029_15.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.16.1-py312h4389bb4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.5-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/nlohmann_json-3.11.3-he0c23c2_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.3.1-py312h12c3145_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.5.1-h725018a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/paginate-0.5.7-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pandas-2.3.1-py312hc128f0a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandera-0.25.0-hd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pandera-base-0.25.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pango-1.56.4-h03d888a_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pcre2-10.45-h99c9b8b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pixman-0.46.2-had0cd8c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/plac-1.4.5-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.8-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.0.0-py312h4389bb4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pthread-stubs-0.4-h0e40799_1002.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pulp-2.8.0-py312he39998a_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pybind11-abi-4-hd8ed1ab_3.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/pycosat-0.6.6-py312h4389bb4_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.11.7-pyh3cfb1c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pydantic-core-2.33.2-py312h8422cdd_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pydot-4.0.1-py312h2e8e312_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pymdown-extensions-10.16-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyreadline3-3.5.4-py312h2e8e312_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.11-h3f84c4b_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-307-py312h275cf98_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.2-py312h31fea79_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyyaml-env-tag-1.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.0.0-py312hd7027bb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/reproc-14.2.5.post0-h2466b09_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/reproc-cpp-14.2.5.post0-he0c23c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/reretry-0.11.8-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.26.0-py312hdabe01f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ruamel.yaml-0.18.14-py312h4389bb4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ruamel.yaml.clib-0.2.8-py312h4389bb4_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.12.2-hd40eec1_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/simdjson-3.13.0-hc790b64_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smart_open-7.3.0.post1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/bioconda/noarch/snakefmt-0.11.0-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-common-1.20.1-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-executor-plugins-9.3.7-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-logger-plugins-1.2.3-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-report-plugins-1.1.0-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-storage-plugins-4.2.1-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-minimal-9.8.0-pyhdfd78af_0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tabulate-0.9.0-pyhd8ed1ab_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2021.13.0-h62715c5_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/throttler-1.2.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.1-py312h4389bb4_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/truststore-0.10.1-pyh29332c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typeguard-4.4.4-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.14.1-h4440ef1_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.14.1-pyhe01879c_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_inspect-0.9.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_26.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_26.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.44.35208-h38c0c73_26.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/watchdog-6.0.0-py312h2e8e312_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/wrapt-1.17.2-py312h4389bb4_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libice-1.1.2-h0e40799_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libsm-1.2.6-h0e40799_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libx11-1.8.12-hf48077a_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxau-1.0.12-h0e40799_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxdmcp-1.1.5-h0e40799_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxext-1.3.6-h0e40799_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxpm-3.5.17-h0e40799_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxt-1.3.1-h0e40799_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h8ffe710_2.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-cpp-0.8.0-he0c23c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/yte-1.8.1-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-ha9f60a1_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.23.0-py312h4389bb4_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-hbeecb71_2.conda + - pypi: https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/96/fd/a40c621ff207f3ce8e484aa0fc8ba4eb6e3ecf52e15b42ba764b457a9550/editorconfig-0.17.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/2d/14/1c65fccf8413d5f5c6e8425f84675169654395098000d8bddc4e9d3390e1/jsbeautifier-1.15.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/24/ce/c8a41cb0f3044990c8afbdc20c853845a9e940995d4e0cffecafbb5e927b/mkdocs_mermaid2_plugin-1.2.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl +packages: +- conda: https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2 + sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 + md5: d7c89558ba9fa0495403155b64376d81 + license: None + purls: [] + size: 2562 + timestamp: 1578324546067 +- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_gnu.tar.bz2 + build_number: 16 + sha256: fbe2c5e56a653bebb982eda4876a9178aedfc2b545f25d0ce9c4c0b508253d22 + md5: 73aaf86a425cc6e73fcf236a5a46396d + depends: + - _libgcc_mutex 0.1 conda_forge + - libgomp >=7.5.0 + constrains: + - openmp_impl 9999 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 23621 + timestamp: 1650670423406 +- conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-2_gnu.conda + build_number: 8 + sha256: 1a62cd1f215fe0902e7004089693a78347a30ad687781dfda2289cab000e652d + md5: 37e16618af5c4851a3f3d66dd0e11141 + depends: + - libgomp >=7.5.0 + - libwinpthread >=12.0.0.r2.ggc561118da + constrains: + - openmp_impl 9999 + - msys2-conda-epoch <0.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 49468 + timestamp: 1718213032772 +- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-48.1-unix_0.conda + sha256: 824a7349bbb2ef8014077ddcfd418065a0a4de873ada1bd1b8826e20bed18c15 + md5: eeb18017386c92765ad8ffa986c3f4ce + depends: + - __unix + - hicolor-icon-theme + - librsvg + license: LGPL-3.0-or-later OR CC-BY-SA-3.0 + license_family: LGPL + purls: [] + size: 619606 + timestamp: 1750236493212 +- conda: https://conda.anaconda.org/conda-forge/noarch/amply-0.1.6-pyhd8ed1ab_1.conda + sha256: e8d87cb66bcc62bc8d8168037b776de962ebf659e45acb1a813debde558f7339 + md5: 5a81866192811f3a0827f5f93e589f02 + depends: + - docutils >=0.3 + - pyparsing + - python >=3.9 + license: EPL-2.0 + purls: + - pkg:pypi/amply?source=hash-mapping + size: 21899 + timestamp: 1734603085333 +- conda: https://conda.anaconda.org/conda-forge/noarch/annotated-types-0.7.0-pyhd8ed1ab_1.conda + sha256: e0ea1ba78fbb64f17062601edda82097fcf815012cf52bb704150a2668110d48 + md5: 2934f256a8acfe48f6ebb4fce6cde29c + depends: + - python >=3.9 + - typing-extensions >=4.0.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/annotated-types?source=hash-mapping + size: 18074 + timestamp: 1733247158254 +- conda: https://conda.anaconda.org/conda-forge/noarch/appdirs-1.4.4-pyhd8ed1ab_1.conda + sha256: 5b9ef6d338525b332e17c3ed089ca2f53a5d74b7a7b432747d29c6466e39346d + md5: f4e90937bbfc3a4a92539545a37bb448 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/appdirs?source=hash-mapping + size: 14835 + timestamp: 1733754069532 +- conda: https://conda.anaconda.org/conda-forge/noarch/appnope-0.1.4-pyhd8ed1ab_1.conda + sha256: 8f032b140ea4159806e4969a68b4a3c0a7cab1ad936eb958a2b5ffe5335e19bf + md5: 54898d0f524c9dee622d44bbb081a8ab + depends: + - python >=3.9 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/appnope?source=hash-mapping + size: 10076 + timestamp: 1733332433806 +- conda: https://conda.anaconda.org/conda-forge/noarch/archspec-0.2.5-pyhd8ed1ab_0.conda + sha256: eb68e1ce9e9a148168a4b1e257a8feebffdb0664b557bb526a1e4853f2d2fc00 + md5: 845b38297fca2f2d18a29748e2ece7fa + depends: + - python >=3.9 + license: MIT OR Apache-2.0 + purls: + - pkg:pypi/archspec?source=hash-mapping + size: 50894 + timestamp: 1737352715041 +- conda: https://conda.anaconda.org/conda-forge/noarch/argparse-dataclass-2.0.0-pyhd8ed1ab_0.conda + sha256: 67e8c1fde7cd025bc7b3190b83bfe967099672a2bcff8e6864f52abfcc25769b + md5: be47a0ee841e940a9a8eec03c2f776a3 + depends: + - python >=3.8 + license: MIT + license_family: MIT + purls: + - pkg:pypi/argparse-dataclass?source=hash-mapping + size: 12203 + timestamp: 1691002812997 +- conda: https://conda.anaconda.org/conda-forge/noarch/asttokens-3.0.0-pyhd8ed1ab_1.conda + sha256: 93b14414b3b3ed91e286e1cbe4e7a60c4e1b1c730b0814d1e452a8ac4b9af593 + md5: 8f587de4bcf981e26228f268df374a9b + depends: + - python >=3.9 + constrains: + - astroid >=2,<4 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/asttokens?source=hash-mapping + size: 28206 + timestamp: 1733250564754 +- conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-atk-2.38.0-h0630a04_3.tar.bz2 + sha256: 26ab9386e80bf196e51ebe005da77d57decf6d989b4f34d96130560bc133479c + md5: 6b889f174df1e0f816276ae69281af4d + depends: + - at-spi2-core >=2.40.0,<2.41.0a0 + - atk-1.0 >=2.36.0 + - dbus >=1.13.6,<2.0a0 + - libgcc-ng >=9.3.0 + - libglib >=2.68.1,<3.0a0 + license: LGPL-2.1-or-later + license_family: LGPL + purls: [] + size: 339899 + timestamp: 1619122953439 +- conda: https://conda.anaconda.org/conda-forge/linux-64/at-spi2-core-2.40.3-h0630a04_0.tar.bz2 + sha256: c4f9b66bd94c40d8f1ce1fad2d8b46534bdefda0c86e3337b28f6c25779f258d + md5: 8cb2fc4cd6cc63f1369cfa318f581cc3 + depends: + - dbus >=1.13.6,<2.0a0 + - libgcc-ng >=9.3.0 + - libglib >=2.68.3,<3.0a0 + - xorg-libx11 + - xorg-libxi + - xorg-libxtst + license: LGPL-2.1-or-later + license_family: LGPL + purls: [] + size: 658390 + timestamp: 1625848454791 +- conda: https://conda.anaconda.org/conda-forge/linux-64/atk-1.0-2.38.0-h04ea711_2.conda + sha256: df682395d05050cd1222740a42a551281210726a67447e5258968dd55854302e + md5: f730d54ba9cd543666d7220c9f7ed563 + depends: + - libgcc-ng >=12 + - libglib >=2.80.0,<3.0a0 + - libstdcxx-ng >=12 + constrains: + - atk-1.0 2.38.0 + license: LGPL-2.0-or-later + license_family: LGPL + purls: [] + size: 355900 + timestamp: 1713896169874 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/atk-1.0-2.38.0-hd03087b_2.conda + sha256: b0747f9b1bc03d1932b4d8c586f39a35ac97e7e72fe6e63f2b2a2472d466f3c1 + md5: 57301986d02d30d6805fdce6c99074ee + depends: + - __osx >=11.0 + - libcxx >=16 + - libglib >=2.80.0,<3.0a0 + - libintl >=0.22.5,<1.0a0 + constrains: + - atk-1.0 2.38.0 + license: LGPL-2.0-or-later + license_family: LGPL + purls: [] + size: 347530 + timestamp: 1713896411580 +- conda: https://conda.anaconda.org/conda-forge/noarch/attrs-25.3.0-pyh71513ae_0.conda + sha256: 99c53ffbcb5dc58084faf18587b215f9ac8ced36bbfb55fa807c00967e419019 + md5: a10d11958cadc13fdb43df75f8b1903f + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/attrs?source=hash-mapping + size: 57181 + timestamp: 1741918625732 +- conda: https://conda.anaconda.org/conda-forge/noarch/babel-2.17.0-pyhd8ed1ab_0.conda + sha256: 1c656a35800b7f57f7371605bc6507c8d3ad60fbaaec65876fce7f73df1fc8ac + md5: 0a01c169f0ab0f91b26e77a3301fbfe4 + depends: + - python >=3.9 + - pytz >=2015.7 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/babel?source=compressed-mapping + size: 6938256 + timestamp: 1738490268466 +- conda: https://conda.anaconda.org/conda-forge/noarch/backrefs-5.8-pyhd8ed1ab_0.conda + sha256: 3a0af23d357a07154645c41d035a4efbd15b7a642db397fa9ea0193fd58ae282 + md5: b16e2595d3a9042aa9d570375978835f + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/backrefs?source=hash-mapping + size: 143810 + timestamp: 1740887689966 +- pypi: https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl + name: beautifulsoup4 + version: 4.13.4 + sha256: 9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b + requires_dist: + - soupsieve>1.2 + - typing-extensions>=4.0.0 + - cchardet ; extra == 'cchardet' + - chardet ; extra == 'chardet' + - charset-normalizer ; extra == 'charset-normalizer' + - html5lib ; extra == 'html5lib' + - lxml ; extra == 'lxml' + requires_python: '>=3.7.0' +- conda: https://conda.anaconda.org/conda-forge/linux-64/black-24.10.0-py312h7900ff3_0.conda + sha256: 2b4344d18328b3e8fd9b5356f4ee15556779766db8cb21ecf2ff818809773df6 + md5: 2daba153b913b1b901cf61440ad5e019 + depends: + - click >=8.0.0 + - mypy_extensions >=0.4.3 + - packaging >=22.0 + - pathspec >=0.9 + - platformdirs >=2 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/black?source=hash-mapping + size: 390571 + timestamp: 1728503839694 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/black-24.10.0-py312h81bd7bf_0.conda + sha256: 7e0cd77935e68717506469463546365637abf3f73aa597a890cb5f5ef3c75caf + md5: 702d7bf6d22135d3e30811ed9c62bb07 + depends: + - click >=8.0.0 + - mypy_extensions >=0.4.3 + - packaging >=22.0 + - pathspec >=0.9 + - platformdirs >=2 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/black?source=hash-mapping + size: 392801 + timestamp: 1728503954904 +- conda: https://conda.anaconda.org/conda-forge/win-64/black-24.10.0-py312h2e8e312_0.conda + sha256: 64df9c7e1454386b5ec763e82a40062b47e80700b1bc556878b7aa7b659c3ae1 + md5: 6e943a224409da3599a8ec52944e3c15 + depends: + - click >=8.0.0 + - mypy_extensions >=0.4.3 + - packaging >=22.0 + - pathspec >=0.9 + - platformdirs >=2 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/black?source=hash-mapping + size: 417913 + timestamp: 1728504045145 +- conda: https://conda.anaconda.org/conda-forge/noarch/boltons-25.0.0-pyhd8ed1ab_0.conda + sha256: ea5f4c876eff2ed469551b57f1cc889a3c01128bf3e2e10b1fea11c3ef39eac2 + md5: c7eb87af73750d6fd97eff8bbee8cb9c + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/boltons?source=hash-mapping + size: 302296 + timestamp: 1749686302834 +- conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py312h2ec8cdc_3.conda + sha256: dc27c58dc717b456eee2d57d8bc71df3f562ee49368a2351103bc8f1b67da251 + md5: a32e0c069f6c3dcac635f7b0b0dac67e + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + constrains: + - libbrotlicommon 1.1.0 hb9d3cd8_3 + license: MIT + license_family: MIT + purls: + - pkg:pypi/brotli?source=hash-mapping + size: 351721 + timestamp: 1749230265727 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.1.0-py312hd8f9ff3_3.conda + sha256: 35df7079768b4c51764149c42b14ccc25c4415e4365ecc06c38f74562d9e4d16 + md5: c7c728df70dc05a443f1e337c28de22d + depends: + - __osx >=11.0 + - libcxx >=18 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + constrains: + - libbrotlicommon 1.1.0 h5505292_3 + license: MIT + license_family: MIT + purls: + - pkg:pypi/brotli?source=compressed-mapping + size: 339365 + timestamp: 1749230606596 +- conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.1.0-py312h275cf98_3.conda + sha256: d5c18a90220853c86f7cc23db62b32b22c6c5fe5d632bc111fc1e467c9fd776f + md5: a87a39f9eb9fd5f171b13d8c79f7a99a + depends: + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - libbrotlicommon 1.1.0 h2466b09_3 + license: MIT + license_family: MIT + purls: + - pkg:pypi/brotli?source=hash-mapping + size: 321941 + timestamp: 1749231054102 +- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda + sha256: 5ced96500d945fb286c9c838e54fa759aa04a7129c59800f0846b4335cee770d + md5: 62ee74e96c5ebb0af99386de58cf9553 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc-ng >=12 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 252783 + timestamp: 1720974456583 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-h99b78c6_7.conda + sha256: adfa71f158cbd872a36394c56c3568e6034aa55c623634b37a4836bd036e6b91 + md5: fc6948412dbbbe9a4c9ddbbcfe0a79ab + depends: + - __osx >=11.0 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 122909 + timestamp: 1720974522888 +- conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h2466b09_7.conda + sha256: 35a5dad92e88fdd7fc405e864ec239486f4f31eec229e31686e61a140a8e573b + md5: 276e7ffe9ffe39688abc665ef0f45596 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 54927 + timestamp: 1720974860185 +- conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.5-hb9d3cd8_0.conda + sha256: f8003bef369f57396593ccd03d08a8e21966157269426f71e943f96e4b579aeb + md5: f7f0d6cc2dc986d42ac2689ec88192be + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: MIT + license_family: MIT + purls: [] + size: 206884 + timestamp: 1744127994291 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.5-h5505292_0.conda + sha256: b4bb55d0806e41ffef94d0e3f3c97531f322b3cb0ca1f7cdf8e47f62538b7a2b + md5: f8cd1beb98240c7edb1a95883360ccfa + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 179696 + timestamp: 1744128058734 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.7.9-h4c7d964_0.conda + sha256: 35c83fc1cab4b9aedba317ba617e37fee20e5ed1cf7135d8eba6f4d8cdf9c4b3 + md5: c7a9b2d28779665c251e6a4db1f8cd23 + depends: + - __win + license: ISC + purls: [] + size: 152706 + timestamp: 1752037404993 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2025.7.9-hbd8a1cb_0.conda + sha256: d2d7327b09d990d0f51e7aec859a5879743675e377fcf9b4ec4db2dbeb75e15d + md5: 54521bf3b59c86e2f55b7294b40a04dc + depends: + - __unix + license: ISC + purls: [] + size: 152448 + timestamp: 1752037382564 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.4-h3394656_0.conda + sha256: 3bd6a391ad60e471de76c0e9db34986c4b5058587fbf2efa5a7f54645e28c2c7 + md5: 09262e66b19567aff4f592fb53b28760 + depends: + - __glibc >=2.17,<3.0.a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - freetype >=2.12.1,<3.0a0 + - icu >=75.1,<76.0a0 + - libexpat >=2.6.4,<3.0a0 + - libgcc >=13 + - libglib >=2.82.2,<3.0a0 + - libpng >=1.6.47,<1.7.0a0 + - libstdcxx >=13 + - libxcb >=1.17.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - pixman >=0.44.2,<1.0a0 + - xorg-libice >=1.1.2,<2.0a0 + - xorg-libsm >=1.2.5,<2.0a0 + - xorg-libx11 >=1.8.11,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxrender >=0.9.12,<0.10.0a0 + license: LGPL-2.1-only or MPL-1.1 + purls: [] + size: 978114 + timestamp: 1741554591855 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cairo-1.18.4-h6a3b0d2_0.conda + sha256: 00439d69bdd94eaf51656fdf479e0c853278439d22ae151cabf40eb17399d95f + md5: 38f6df8bc8c668417b904369a01ba2e2 + depends: + - __osx >=11.0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - freetype >=2.12.1,<3.0a0 + - icu >=75.1,<76.0a0 + - libcxx >=18 + - libexpat >=2.6.4,<3.0a0 + - libglib >=2.82.2,<3.0a0 + - libpng >=1.6.47,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + - pixman >=0.44.2,<1.0a0 + license: LGPL-2.1-only or MPL-1.1 + purls: [] + size: 896173 + timestamp: 1741554795915 +- conda: https://conda.anaconda.org/conda-forge/win-64/cairo-1.18.4-h5782bbf_0.conda + sha256: b9f577bddb033dba4533e851853924bfe7b7c1623d0697df382eef177308a917 + md5: 20e32ced54300292aff690a69c5e7b97 + depends: + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - freetype >=2.12.1,<3.0a0 + - icu >=75.1,<76.0a0 + - libexpat >=2.6.4,<3.0a0 + - libglib >=2.82.2,<3.0a0 + - libpng >=1.6.47,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + - pixman >=0.44.2,<1.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: LGPL-2.1-only or MPL-1.1 + purls: [] + size: 1524254 + timestamp: 1741555212198 +- conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2025.7.9-pyhd8ed1ab_0.conda + sha256: d5bcebb3748005b50479055b69bd6a19753219effcf921b9158ef3ff588c752b + md5: fac657ab965a05f69ba777a7b934255a + depends: + - python >=3.9 + license: ISC + purls: + - pkg:pypi/certifi?source=compressed-mapping + size: 156733 + timestamp: 1752115379962 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.17.1-py312h06ac9bb_0.conda + sha256: cba6ea83c4b0b4f5b5dc59cb19830519b28f95d7ebef7c9c5cf1c14843621457 + md5: a861504bbea4161a9170b85d4d2be840 + depends: + - __glibc >=2.17,<3.0.a0 + - libffi >=3.4,<4.0a0 + - libgcc >=13 + - pycparser + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/cffi?source=hash-mapping + size: 294403 + timestamp: 1725560714366 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cffi-1.17.1-py312h0fad829_0.conda + sha256: 8d91a0d01358b5c3f20297c6c536c5d24ccd3e0c2ddd37f9d0593d0f0070226f + md5: 19a5456f72f505881ba493979777b24e + depends: + - __osx >=11.0 + - libffi >=3.4,<4.0a0 + - pycparser + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/cffi?source=hash-mapping + size: 281206 + timestamp: 1725560813378 +- conda: https://conda.anaconda.org/conda-forge/win-64/cffi-1.17.1-py312h4389bb4_0.conda + sha256: ac007bf5fd56d13e16d95eea036433012f2e079dc015505c8a79efebbad1fcbc + md5: 08310c1a22ef957d537e547f8d484f92 + depends: + - pycparser + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + purls: + - pkg:pypi/cffi?source=hash-mapping + size: 288142 + timestamp: 1725560896359 +- conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.2-pyhd8ed1ab_0.conda + sha256: 535ae5dcda8022e31c6dc063eb344c80804c537a5a04afba43a845fa6fa130f5 + md5: 40fe4284b8b5835a9073a645139f35af + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/charset-normalizer?source=hash-mapping + size: 50481 + timestamp: 1746214981991 +- conda: https://conda.anaconda.org/conda-forge/noarch/click-8.2.1-pyh707e725_0.conda + sha256: 8aee789c82d8fdd997840c952a586db63c6890b00e88c4fb6e80a38edd5f51c0 + md5: 94b550b8d3a614dbd326af798c7dfb40 + depends: + - __unix + - python >=3.10 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/click?source=hash-mapping + size: 87749 + timestamp: 1747811451319 +- conda: https://conda.anaconda.org/conda-forge/noarch/click-8.2.1-pyh7428d3b_0.conda + sha256: 20c2d8ea3d800485245b586a28985cba281dd6761113a49d7576f6db92a0a891 + md5: 3a59475037bc09da916e4062c5cad771 + depends: + - __win + - colorama + - python >=3.10 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/click?source=compressed-mapping + size: 88117 + timestamp: 1747811467132 +- conda: https://conda.anaconda.org/conda-forge/noarch/clio-tools-2025.03.03-pyhd8ed1ab_0.conda + sha256: aeefe88574384f212db396a6b16e2980daa298239bce51a096d22317c839d856 + md5: 1eb44fb84e5eef6f73c491890ca0dd4b + depends: + - networkx >=3.4.2 + - numpy >=2.2.3 + - pandas >=2.2.3 + - pandera >=0.22.1 + - pydantic >=2.10.6 + - pydot >=3.0.4 + - python >=3.12 + - pyyaml >=6.0.2 + license: MIT + license_family: MIT + purls: + - pkg:pypi/clio-tools?source=hash-mapping + size: 14536 + timestamp: 1741282465165 +- conda: https://conda.anaconda.org/conda-forge/linux-64/coin-or-cbc-2.10.12-h00e76a6_2.conda + sha256: c9c125fc26459d760dd75859e4f84b78804088649fd231fd3d0c55c50f50d4a2 + md5: e96d087e020082fc811457dba4ad4715 + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - coin-or-cgl >=0.60,<0.61.0a0 + - coin-or-clp >=1.17,<1.18.0a0 + - coin-or-osi >=0.108,<0.109.0a0 + - coin-or-utils >=2.11,<2.12.0a0 + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - libgcc >=13 + - libgfortran + - libgfortran5 >=13.3.0 + - liblapack >=3.9.0,<4.0a0 + - liblapacke >=3.9.0,<4.0a0 + - libstdcxx >=13 + - libzlib >=1.3.1,<2.0a0 + constrains: + - coincbc * *_metapackage + license: EPL-2.0 + license_family: OTHER + purls: [] + size: 901705 + timestamp: 1741144192046 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/coin-or-cbc-2.10.12-h8ec3750_2.conda + sha256: 99bd2ecfd4cb36a6f8099ea3e85e1d3ac9f6599dd80c6bbb40809e9045f39543 + md5: d78ad1606f3c0962c2a837667024e937 + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - coin-or-cgl >=0.60,<0.61.0a0 + - coin-or-clp >=1.17,<1.18.0a0 + - coin-or-osi >=0.108,<0.109.0a0 + - coin-or-utils >=2.11,<2.12.0a0 + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - libcxx >=18 + - libgfortran >=5 + - libgfortran5 >=13.2.0 + - liblapack >=3.9.0,<4.0a0 + - liblapacke >=3.9.0,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + constrains: + - coincbc * *_metapackage + license: EPL-2.0 + license_family: OTHER + purls: [] + size: 793637 + timestamp: 1741144402683 +- conda: https://conda.anaconda.org/conda-forge/win-64/coin-or-cbc-2.10.12-h1c9cd67_2.conda + sha256: bfedaafceaf37b81868c7f257f3ad2642f1e08756335f75a37bd99e9a0c618f7 + md5: 508c3ea6a7abc08904cb6e209145eaf5 + depends: + - bzip2 >=1.0.8,<2.0a0 + - coin-or-cgl >=0.60,<0.61.0a0 + - coin-or-clp >=1.17,<1.18.0a0 + - coin-or-osi >=0.108,<0.109.0a0 + - coin-or-utils >=2.11,<2.12.0a0 + - libcblas >=3.9.0,<4.0a0 + - liblapack >=3.9.0,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - mkl-static + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - coincbc * *_metapackage + license: EPL-2.0 + license_family: OTHER + purls: [] + size: 4026507 + timestamp: 1741144791376 +- conda: https://conda.anaconda.org/conda-forge/linux-64/coin-or-cgl-0.60.9-h82e2f02_4.conda + sha256: b7315746fe3e5d2b562a1e7049e7ef6dc6cc4545d19f0b69ae20f5bd1460c35e + md5: 51bb0c16c15e099e4ebb813ce91291e3 + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - coin-or-clp >=1.17,<1.18.0a0 + - coin-or-osi >=0.108,<0.109.0a0 + - coin-or-utils >=2.11,<2.12.0a0 + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - libgcc >=13 + - libgfortran + - libgfortran5 >=13.3.0 + - liblapack >=3.9.0,<4.0a0 + - liblapacke >=3.9.0,<4.0a0 + - libstdcxx >=13 + - libzlib >=1.3.1,<2.0a0 + constrains: + - coincbc * *_metapackage + license: EPL-2.0 + license_family: OTHER + purls: [] + size: 528474 + timestamp: 1741117399688 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/coin-or-cgl-0.60.9-h7ef17a8_4.conda + sha256: 7c9ee330207912364e581f7011422c05ce40f88117697fe16bd7324401499d69 + md5: 20853d831a8702be3dc5a2f2235a3213 + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - coin-or-clp >=1.17,<1.18.0a0 + - coin-or-osi >=0.108,<0.109.0a0 + - coin-or-utils >=2.11,<2.12.0a0 + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - libcxx >=18 + - libgfortran >=5 + - libgfortran5 >=13.2.0 + - liblapack >=3.9.0,<4.0a0 + - liblapacke >=3.9.0,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + constrains: + - coincbc * *_metapackage + license: EPL-2.0 + license_family: OTHER + purls: [] + size: 441347 + timestamp: 1741117634415 +- conda: https://conda.anaconda.org/conda-forge/win-64/coin-or-cgl-0.60.9-h8aa12e5_4.conda + sha256: 306c08ba347138824badcf894d3475af471e6ec44488899caa34b91b3354bfab + md5: 0c1dd0a7dc684fcbdc58440d937a023a + depends: + - bzip2 >=1.0.8,<2.0a0 + - coin-or-clp >=1.17,<1.18.0a0 + - coin-or-osi >=0.108,<0.109.0a0 + - coin-or-utils >=2.11,<2.12.0a0 + - libcblas >=3.9.0,<4.0a0 + - liblapack >=3.9.0,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - mkl-static + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - coincbc * *_metapackage + license: EPL-2.0 + license_family: OTHER + purls: [] + size: 1267571 + timestamp: 1741117739342 +- conda: https://conda.anaconda.org/conda-forge/linux-64/coin-or-clp-1.17.10-h8a7a1e7_1.conda + sha256: dcacf05ac49c3f3a9f03b0bcd95497f35c27797dd6c60f1a1518f440cbe7454a + md5: c66c0187a5ed784bb1a9a360f6d2ce0c + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - coin-or-osi >=0.108,<0.109.0a0 + - coin-or-utils >=2.11,<2.12.0a0 + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - libgcc >=13 + - libgfortran + - libgfortran5 >=13.3.0 + - liblapack >=3.9.0,<4.0a0 + - liblapacke >=3.9.0,<4.0a0 + - libstdcxx >=13 + - libzlib >=1.3.1,<2.0a0 + constrains: + - coincbc * *_metapackage + license: EPL-2.0 + license_family: OTHER + purls: [] + size: 1137171 + timestamp: 1740621657782 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/coin-or-clp-1.17.10-h73553b4_1.conda + sha256: 60f00e36b67e4bff09475170ecc310175cb4d0ca502c53de4eb5705632e7b6e1 + md5: 55726cb7edffbf59d72d4c32cf80d4f9 + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - coin-or-osi >=0.108,<0.109.0a0 + - coin-or-utils >=2.11,<2.12.0a0 + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - libcxx >=18 + - libgfortran >=5 + - libgfortran5 >=13.2.0 + - liblapack >=3.9.0,<4.0a0 + - liblapacke >=3.9.0,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + constrains: + - coincbc * *_metapackage + license: EPL-2.0 + license_family: OTHER + purls: [] + size: 908476 + timestamp: 1740621849900 +- conda: https://conda.anaconda.org/conda-forge/win-64/coin-or-clp-1.17.10-ha4dcb5c_1.conda + sha256: d6dd5edbf2a7d32f249d6781f0fd03f6f02079073d3e121850f6815156e30a4e + md5: 8d5189590033742873f82e89d8f6b067 + depends: + - bzip2 >=1.0.8,<2.0a0 + - coin-or-osi >=0.108,<0.109.0a0 + - coin-or-utils >=2.11,<2.12.0a0 + - libcblas >=3.9.0,<4.0a0 + - liblapack >=3.9.0,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - mkl-static + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - coincbc * *_metapackage + license: EPL-2.0 + license_family: OTHER + purls: [] + size: 3596420 + timestamp: 1740622203527 +- conda: https://conda.anaconda.org/conda-forge/linux-64/coin-or-osi-0.108.11-h96cc833_4.conda + sha256: e6e219c6f55ab22f792ff5379edce0d81a415cca2cb8e4ab2cb54f57186744c1 + md5: d188e67fb44ce078616c78b14dafb314 + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - coin-or-utils >=2.11,<2.12.0a0 + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - libgcc >=13 + - libgfortran + - libgfortran5 >=13.3.0 + - liblapack >=3.9.0,<4.0a0 + - liblapacke >=3.9.0,<4.0a0 + - libstdcxx >=13 + - libzlib >=1.3.1,<2.0a0 + constrains: + - coincbc * *_metapackage + license: EPL-2.0 + license_family: OTHER + purls: [] + size: 365406 + timestamp: 1740595201453 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/coin-or-osi-0.108.11-h1c7c69d_4.conda + sha256: 43f11afebc479dfb2d5c2b9e03aa67d18b74380ed7c11daac33f3eca7049af9b + md5: 395dc6617235b169449c93120b8042ad + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - coin-or-utils >=2.11,<2.12.0a0 + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - libcxx >=18 + - libgfortran >=5 + - libgfortran5 >=13.2.0 + - liblapack >=3.9.0,<4.0a0 + - liblapacke >=3.9.0,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + constrains: + - coincbc * *_metapackage + license: EPL-2.0 + license_family: OTHER + purls: [] + size: 323719 + timestamp: 1740595512714 +- conda: https://conda.anaconda.org/conda-forge/win-64/coin-or-osi-0.108.11-h7d32387_4.conda + sha256: cf34ce113131ed6b9492f9d1d1500f1831c950646c591ea4695b28fb46046222 + md5: 6c97f9c7175c8f73cc22f22bd381532e + depends: + - bzip2 >=1.0.8,<2.0a0 + - coin-or-utils >=2.11,<2.12.0a0 + - libzlib >=1.3.1,<2.0a0 + - mkl-static + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - coincbc * *_metapackage + license: EPL-2.0 + license_family: OTHER + purls: [] + size: 942273 + timestamp: 1740595519414 +- conda: https://conda.anaconda.org/conda-forge/linux-64/coin-or-utils-2.11.12-h3a12e53_2.conda + sha256: 516b784b7a5ffd04dc095e6d43366266a90f575c2379065992b480afb335710d + md5: 450607d9c6f578ca6c26061973c2a548 + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - libgcc >=13 + - libgfortran + - libgfortran5 >=13.3.0 + - liblapack >=3.9.0,<4.0a0 + - liblapacke >=3.9.0,<4.0a0 + - libstdcxx >=13 + - libzlib >=1.3.1,<2.0a0 + constrains: + - coincbc * *_metapackage + license: EPL-2.0 + license_family: OTHER + purls: [] + size: 661594 + timestamp: 1740585063174 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/coin-or-utils-2.11.12-h38baedf_2.conda + sha256: 9f5e83cf3463783a89b1597855ac57ba9b32ac4590b2464fc72c03ad07e20faf + md5: 77bfbe12fe693786d1f489a7122aa7e5 + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - libcxx >=18 + - libgfortran >=5 + - libgfortran5 >=13.2.0 + - liblapack >=3.9.0,<4.0a0 + - liblapacke >=3.9.0,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + constrains: + - coincbc * *_metapackage + license: EPL-2.0 + license_family: OTHER + purls: [] + size: 553791 + timestamp: 1740585377245 +- conda: https://conda.anaconda.org/conda-forge/win-64/coin-or-utils-2.11.12-hf90b928_2.conda + sha256: ee8d88744831db87028a00bbcb6bfd0ff4a044dc970833811dc8b5615a68389b + md5: 77850165d0d41ee3a7ce4c877ba2d547 + depends: + - bzip2 >=1.0.8,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - coincbc * *_metapackage + license: EPL-2.0 + license_family: OTHER + purls: [] + size: 1400990 + timestamp: 1740585260332 +- conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + sha256: ab29d57dc70786c1269633ba3dff20288b81664d3ff8d21af995742e2bb03287 + md5: 962b9857ee8e7018c22f2776ffa0b2d7 + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/colorama?source=hash-mapping + size: 27011 + timestamp: 1733218222191 +- conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.2-pyhd8ed1ab_1.conda + sha256: 7e87ef7c91574d9fac19faedaaee328a70f718c9b4ddadfdc0ba9ac021bd64af + md5: 74673132601ec2b7fc592755605f4c1b + depends: + - python >=3.9 + - traitlets >=5.3 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/comm?source=hash-mapping + size: 12103 + timestamp: 1733503053903 +- conda: https://conda.anaconda.org/conda-forge/linux-64/conda-25.5.1-py312h7900ff3_0.conda + sha256: 36faa091f85ac0ff7f51447299d08e8b342376a78f4b8177ebb2288512e231ba + md5: 72a0d0e86336e7c734389d4995e50265 + depends: + - archspec >=0.2.3 + - boltons >=23.0.0 + - charset-normalizer + - conda-libmamba-solver >=24.11.0 + - conda-package-handling >=2.2.0 + - distro >=1.5.0 + - frozendict >=2.4.2 + - jsonpatch >=1.32 + - menuinst >=2 + - packaging >=23.0 + - platformdirs >=3.10.0 + - pluggy >=1.0.0 + - pycosat >=0.6.3 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - requests >=2.28.0,<3 + - ruamel.yaml >=0.11.14,<0.19 + - setuptools >=60.0.0 + - tqdm >=4 + - truststore >=0.8.0 + - zstandard >=0.19.0 + constrains: + - conda-env >=2.6 + - conda-content-trust >=0.1.1 + - conda-build >=24.3 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/conda?source=hash-mapping + size: 1188079 + timestamp: 1749201905646 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/conda-25.5.1-py312h81bd7bf_0.conda + sha256: 624d4d6a55c2a1026aed5cfebf9290ac8d28c67134644e0e6d5a9a6157eb912f + md5: 98c19ba2db58ad961d3293189553b5d2 + depends: + - archspec >=0.2.3 + - boltons >=23.0.0 + - charset-normalizer + - conda-libmamba-solver >=24.11.0 + - conda-package-handling >=2.2.0 + - distro >=1.5.0 + - frozendict >=2.4.2 + - jsonpatch >=1.32 + - menuinst >=2 + - packaging >=23.0 + - platformdirs >=3.10.0 + - pluggy >=1.0.0 + - pycosat >=0.6.3 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + - requests >=2.28.0,<3 + - ruamel.yaml >=0.11.14,<0.19 + - setuptools >=60.0.0 + - tqdm >=4 + - truststore >=0.8.0 + - zstandard >=0.19.0 + constrains: + - conda-build >=24.3 + - conda-content-trust >=0.1.1 + - conda-env >=2.6 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/conda?source=hash-mapping + size: 1189481 + timestamp: 1749202050025 +- conda: https://conda.anaconda.org/conda-forge/win-64/conda-25.5.1-py312h2e8e312_0.conda + sha256: 984904f1f024512f78ea4d5ff0179a86f687a4d9feccfaf5423bbd17f35a565b + md5: 22a2e15ecf2963f9c35ecfd56c0d2d6d + depends: + - archspec >=0.2.3 + - boltons >=23.0.0 + - charset-normalizer + - conda-libmamba-solver >=24.11.0 + - conda-package-handling >=2.2.0 + - distro >=1.5.0 + - frozendict >=2.4.2 + - jsonpatch >=1.32 + - menuinst >=2 + - packaging >=23.0 + - platformdirs >=3.10.0 + - pluggy >=1.0.0 + - pycosat >=0.6.3 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - requests >=2.28.0,<3 + - ruamel.yaml >=0.11.14,<0.19 + - setuptools >=60.0.0 + - tqdm >=4 + - truststore >=0.8.0 + - zstandard >=0.19.0 + constrains: + - conda-content-trust >=0.1.1 + - conda-build >=24.3 + - conda-env >=2.6 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/conda?source=hash-mapping + size: 1190563 + timestamp: 1749202149054 +- conda: https://conda.anaconda.org/conda-forge/noarch/conda-inject-1.3.2-pyhd8ed1ab_0.conda + sha256: c1b355af599e548c4b69129f4d723ddcdb9f6defb939985731499cee2e26a578 + md5: e52c2a160d6bd0649c9fafdf0c813357 + depends: + - python >=3.9.0,<4.0.0 + - pyyaml >=6.0.0,<7.0.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/conda-inject?source=hash-mapping + size: 10327 + timestamp: 1717043667069 +- conda: https://conda.anaconda.org/conda-forge/noarch/conda-libmamba-solver-25.4.0-pyhd8ed1ab_0.conda + sha256: 48999a7a6e300075e4ef1c85130614d75429379eea8fe78f18a38a8aab8da384 + md5: d62b8f745ff471d5594ad73605cb9b59 + depends: + - boltons >=23.0.0 + - conda >=24.11 + - libmambapy >=2.0.0 + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/conda-libmamba-solver?source=hash-mapping + size: 41985 + timestamp: 1745834587643 +- conda: https://conda.anaconda.org/conda-forge/noarch/conda-package-handling-2.4.0-pyh7900ff3_2.conda + sha256: 8b2b1c235b7cbfa8488ad88ff934bdad25bac6a4c035714681fbff85b602f3f0 + md5: 32c158f481b4fd7630c565030f7bc482 + depends: + - conda-package-streaming >=0.9.0 + - python >=3.9 + - requests + - zstandard >=0.15 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/conda-package-handling?source=hash-mapping + size: 257995 + timestamp: 1736345601691 +- conda: https://conda.anaconda.org/conda-forge/noarch/conda-package-streaming-0.12.0-pyhd8ed1ab_0.conda + sha256: 11b76b0be2f629e8035be1d723ccb6e583eb0d2af93bde56113da7fa6e2f2649 + md5: ff75d06af779966a5aeae1be1d409b96 + depends: + - python >=3.9 + - zstandard >=0.15 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/conda-package-streaming?source=compressed-mapping + size: 21933 + timestamp: 1751548225624 +- conda: https://conda.anaconda.org/conda-forge/noarch/configargparse-1.7.1-pyhe01879c_0.conda + sha256: 61d31e5181e29b5bcd47e0a5ef590caf0aec3ec1a6c8f19f50b42ed5bdc065d2 + md5: 18dfeef40f049992f4b46b06e6f3b497 + depends: + - python >=3.9 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/configargparse?source=hash-mapping + size: 40511 + timestamp: 1748302135421 +- conda: https://conda.anaconda.org/conda-forge/noarch/connection_pool-0.0.3-pyhd3deb0d_0.tar.bz2 + sha256: 799a515e9e73e447f46f60fb3f9162f437ae1a2a00defddde84282e9e225cb36 + md5: e270fff08907db8691c02a0eda8d38ae + depends: + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/connection-pool?source=hash-mapping + size: 8331 + timestamp: 1608581999360 +- conda: https://conda.anaconda.org/conda-forge/linux-64/cpp-expected-1.1.0-hff21bea_1.conda + sha256: 234e423531e0d5f31e8e8b2979c4dfa05bdb4c502cb3eb0a5db865bd831d333e + md5: 54e8e1a8144fd678c5d43905e3ba684d + depends: + - libstdcxx >=13 + - libgcc >=13 + - __glibc >=2.17,<3.0.a0 + license: CC0-1.0 + purls: [] + size: 24113 + timestamp: 1745308833071 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/cpp-expected-1.1.0-h177bc72_1.conda + sha256: a41d97157e628947d13bf5920bf0d533f81b8a3ed68dbe4171149f522e99eae6 + md5: 05692bdc7830e860bd32652fa7857705 + depends: + - __osx >=11.0 + - libcxx >=18 + license: CC0-1.0 + purls: [] + size: 24791 + timestamp: 1745308950557 +- conda: https://conda.anaconda.org/conda-forge/win-64/cpp-expected-1.1.0-hc790b64_1.conda + sha256: 926f42a29321981c8cca0736bb419d562e1f40c5269723252e4c4848eba22d09 + md5: 90a81b6b7b4e903362329b8b740047fe + depends: + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - ucrt >=10.0.20348.0 + license: CC0-1.0 + purls: [] + size: 21428 + timestamp: 1745308845974 +- conda: https://conda.anaconda.org/conda-forge/noarch/cpython-3.12.11-py312hd8ed1ab_0.conda + noarch: generic + sha256: 7e7bc8e73a2f3736444a8564cbece7216464c00f0bc38e604b0c792ff60d621a + md5: e5279009e7a7f7edd3cd2880c502b3cc + depends: + - python >=3.12,<3.13.0a0 + - python_abi * *_cp312 + license: Python-2.0 + purls: [] + size: 45852 + timestamp: 1749047748072 +- conda: https://conda.anaconda.org/conda-forge/linux-64/dbus-1.16.2-h3c4dab8_0.conda + sha256: 3b988146a50e165f0fa4e839545c679af88e4782ec284cc7b6d07dd226d6a068 + md5: 679616eb5ad4e521c83da4650860aba7 + depends: + - libstdcxx >=13 + - libgcc >=13 + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libexpat >=2.7.0,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + - libglib >=2.84.2,<3.0a0 + license: GPL-2.0-or-later + license_family: GPL + purls: [] + size: 437860 + timestamp: 1747855126005 +- conda: https://conda.anaconda.org/conda-forge/linux-64/debugpy-1.8.14-py312h2ec8cdc_0.conda + sha256: 8f0b338687f79ea87324f067bedddd2168f07b8eec234f0fe63b522344c6a919 + md5: 089cf3a3becf0e2f403feaf16e921678 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/debugpy?source=hash-mapping + size: 2630748 + timestamp: 1744321406939 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/debugpy-1.8.14-py312hd8f9ff3_0.conda + sha256: c833d92953a4c747f2606cefaebdbeaeec7c8d374bb7652dd0cc241cb120fdbc + md5: f1be818f2cee62e6edc12d5aaae13f57 + depends: + - __osx >=11.0 + - libcxx >=18 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/debugpy?source=hash-mapping + size: 2581221 + timestamp: 1744321582400 +- conda: https://conda.anaconda.org/conda-forge/win-64/debugpy-1.8.14-py312h275cf98_0.conda + sha256: 02ceea9c12eaaf29c7c40142e4789b77c5c98aa477bdfca1db3ae97440b9e2fe + md5: 331737db69ae5431acb6ef3e198ec623 + depends: + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + purls: + - pkg:pypi/debugpy?source=hash-mapping + size: 3561750 + timestamp: 1744321803729 +- conda: https://conda.anaconda.org/conda-forge/noarch/decorator-5.2.1-pyhd8ed1ab_0.conda + sha256: c17c6b9937c08ad63cb20a26f403a3234088e57d4455600974a0ce865cb14017 + md5: 9ce473d1d1be1cc3810856a48b3fab32 + depends: + - python >=3.9 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/decorator?source=compressed-mapping + size: 14129 + timestamp: 1740385067843 +- conda: https://conda.anaconda.org/conda-forge/noarch/distro-1.9.0-pyhd8ed1ab_1.conda + sha256: 5603c7d0321963bb9b4030eadabc3fd7ca6103a38475b4e0ed13ed6d97c86f4e + md5: 0a2014fd9860f8b1eaa0b1f3d3771a08 + depends: + - python >=3.9 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/distro?source=hash-mapping + size: 41773 + timestamp: 1734729953882 +- conda: https://conda.anaconda.org/conda-forge/noarch/docutils-0.21.2-pyhd8ed1ab_1.conda + sha256: fa5966bb1718bbf6967a85075e30e4547901410cc7cb7b16daf68942e9a94823 + md5: 24c1ca34138ee57de72a943237cde4cc + depends: + - python >=3.9 + license: CC-PDDC AND BSD-3-Clause AND BSD-2-Clause AND ZPL-2.1 + purls: + - pkg:pypi/docutils?source=hash-mapping + size: 402700 + timestamp: 1733217860944 +- conda: https://conda.anaconda.org/conda-forge/noarch/dpath-2.2.0-pyha770c72_0.conda + sha256: ab88f587a9b7dc3cbb636823423c2ecfd868d4719b491af37c09b0384214bacf + md5: b2681af65644be41a18d4b00b67938f1 + depends: + - python >3.6 + license: MIT + license_family: MIT + purls: + - pkg:pypi/dpath?source=hash-mapping + size: 21344 + timestamp: 1718243548474 +- pypi: https://files.pythonhosted.org/packages/96/fd/a40c621ff207f3ce8e484aa0fc8ba4eb6e3ecf52e15b42ba764b457a9550/editorconfig-0.17.1-py3-none-any.whl + name: editorconfig + version: 0.17.1 + sha256: 1eda9c2c0db8c16dbd50111b710572a5e6de934e39772de1959d41f64fc17c82 + requires_dist: + - mypy>=1.15 ; extra == 'dev' + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/linux-64/epoxy-1.5.10-h166bdaf_1.tar.bz2 + sha256: 1e58ee2ed0f4699be202f23d49b9644b499836230da7dd5b2f63e6766acff89e + md5: a089d06164afd2d511347d3f87214e0b + depends: + - libgcc-ng >=10.3.0 + license: MIT + license_family: MIT + purls: [] + size: 1440699 + timestamp: 1648505042260 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/epoxy-1.5.10-h1c322ee_1.tar.bz2 + sha256: 8b93dbebab0fe12ece4767e6a2dc53a6600319ece0b8ba5121715f28c7b0f8d1 + md5: 20dd7359a6052120d52e1e13b4c818b9 + license: MIT + license_family: MIT + purls: [] + size: 355201 + timestamp: 1648505273975 +- conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.0-pyhd8ed1ab_0.conda + sha256: ce61f4f99401a4bd455b89909153b40b9c823276aefcbb06f2044618696009ca + md5: 72e42d28960d875c7654614f8b50939a + depends: + - python >=3.9 + - typing_extensions >=4.6.0 + license: MIT and PSF-2.0 + purls: + - pkg:pypi/exceptiongroup?source=compressed-mapping + size: 21284 + timestamp: 1746947398083 +- conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.0-pyhd8ed1ab_0.conda + sha256: 7510dd93b9848c6257c43fdf9ad22adf62e7aa6da5f12a6a757aed83bcfedf05 + md5: 81d30c08f9a3e556e8ca9e124b044d14 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/executing?source=hash-mapping + size: 29652 + timestamp: 1745502200340 +- conda: https://conda.anaconda.org/conda-forge/linux-64/fmt-11.1.4-h07f6e7f_1.conda + sha256: 2db2a6a1629bc2ac649b31fd990712446394ce35930025e960e1765a9249af5d + md5: 288a90e722fd7377448b00b2cddcb90d + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + license: MIT + license_family: MIT + purls: [] + size: 191161 + timestamp: 1742833273257 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/fmt-11.1.4-h440487c_1.conda + sha256: 39249dc4021742f1a126ad0efc39904fe058c89fdf43240f39316d34f948f3f1 + md5: f957ef7cf1dda0c27acdfbeff72ddb84 + depends: + - __osx >=11.0 + - libcxx >=18 + license: MIT + license_family: MIT + purls: [] + size: 178005 + timestamp: 1742833557859 +- conda: https://conda.anaconda.org/conda-forge/win-64/fmt-11.1.4-h5f12afc_1.conda + sha256: fd88cd4796572d649da4d249ffb91bd387558c75ab14ad344b7ac45f714079b6 + md5: 65be2289e596e757fc03a5383072a2e7 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + purls: [] + size: 184746 + timestamp: 1742833874774 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2 + sha256: 58d7f40d2940dd0a8aa28651239adbf5613254df0f75789919c4e6762054403b + md5: 0c96522c6bdaed4b1566d11387caaf45 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 397370 + timestamp: 1566932522327 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2 + sha256: c52a29fdac682c20d252facc50f01e7c2e7ceac52aa9817aaf0bb83f7559ec5c + md5: 34893075a5c9e55cdafac56607368fc6 + license: OFL-1.1 + license_family: Other + purls: [] + size: 96530 + timestamp: 1620479909603 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2 + sha256: 00925c8c055a2275614b4d983e1df637245e19058d79fc7dd1a93b8d9fb4b139 + md5: 4d59c254e01d9cde7957100457e2d5fb + license: OFL-1.1 + license_family: Other + purls: [] + size: 700814 + timestamp: 1620479612257 +- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda + sha256: 2821ec1dc454bd8b9a31d0ed22a7ce22422c0aef163c59f49dfdf915d0f0ca14 + md5: 49023d73832ef61042f6a237cb2687e7 + license: LicenseRef-Ubuntu-Font-Licence-Version-1.0 + license_family: Other + purls: [] + size: 1620504 + timestamp: 1727511233259 +- conda: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.15.0-h7e30c49_1.conda + sha256: 7093aa19d6df5ccb6ca50329ef8510c6acb6b0d8001191909397368b65b02113 + md5: 8f5b0b297b59e1ac160ad4beec99dbee + depends: + - __glibc >=2.17,<3.0.a0 + - freetype >=2.12.1,<3.0a0 + - libexpat >=2.6.3,<3.0a0 + - libgcc >=13 + - libuuid >=2.38.1,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 265599 + timestamp: 1730283881107 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/fontconfig-2.15.0-h1383a14_1.conda + sha256: f79d3d816fafbd6a2b0f75ebc3251a30d3294b08af9bb747194121f5efa364bc + md5: 7b29f48742cea5d1ccb5edd839cb5621 + depends: + - __osx >=11.0 + - freetype >=2.12.1,<3.0a0 + - libexpat >=2.6.3,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 234227 + timestamp: 1730284037572 +- conda: https://conda.anaconda.org/conda-forge/win-64/fontconfig-2.15.0-h765892d_1.conda + sha256: ed122fc858fb95768ca9ca77e73c8d9ddc21d4b2e13aaab5281e27593e840691 + md5: 9bb0026a2131b09404c59c4290c697cd + depends: + - freetype >=2.12.1,<3.0a0 + - libexpat >=2.6.3,<3.0a0 + - libiconv >=1.17,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + purls: [] + size: 192355 + timestamp: 1730284147944 +- conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + sha256: a997f2f1921bb9c9d76e6fa2f6b408b7fa549edd349a77639c9fe7a23ea93e61 + md5: fee5683a3f04bd15cbd8318b096a27ab + depends: + - fonts-conda-forge + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 3667 + timestamp: 1566974674465 +- conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2 + sha256: 53f23a3319466053818540bcdf2091f253cbdbab1e0e9ae7b9e509dcaa2a5e38 + md5: f766549260d6815b0c52253f1fb1bb29 + depends: + - font-ttf-dejavu-sans-mono + - font-ttf-inconsolata + - font-ttf-source-code-pro + - font-ttf-ubuntu + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 4102 + timestamp: 1566932280397 +- conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.13.3-ha770c72_1.conda + sha256: 7ef7d477c43c12a5b4cddcf048a83277414512d1116aba62ebadfa7056a7d84f + md5: 9ccd736d31e0c6e41f54e704e5312811 + depends: + - libfreetype 2.13.3 ha770c72_1 + - libfreetype6 2.13.3 h48d6fc4_1 + license: GPL-2.0-only OR FTL + purls: [] + size: 172450 + timestamp: 1745369996765 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.13.3-hce30654_1.conda + sha256: 6b63c72ea51a41d41964841404564c0729fdddd3e952e2715839fd759b7cfdfc + md5: e684de4644067f1956a580097502bf03 + depends: + - libfreetype 2.13.3 hce30654_1 + - libfreetype6 2.13.3 h1d14073_1 + license: GPL-2.0-only OR FTL + purls: [] + size: 172220 + timestamp: 1745370149658 +- conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.13.3-h57928b3_1.conda + sha256: 0bcc9c868d769247c12324f957c97c4dbee7e4095485db90d9c295bcb3b1bb43 + md5: 633504fe3f96031192e40e3e6c18ef06 + depends: + - libfreetype 2.13.3 h57928b3_1 + - libfreetype6 2.13.3 h0b5ce68_1 + license: GPL-2.0-only OR FTL + purls: [] + size: 184162 + timestamp: 1745370242683 +- conda: https://conda.anaconda.org/conda-forge/linux-64/fribidi-1.0.10-h36c2ea0_0.tar.bz2 + sha256: 5d7b6c0ee7743ba41399e9e05a58ccc1cfc903942e49ff6f677f6e423ea7a627 + md5: ac7bc6a654f8f41b352b38f4051135f8 + depends: + - libgcc-ng >=7.5.0 + license: LGPL-2.1 + purls: [] + size: 114383 + timestamp: 1604416621168 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/fribidi-1.0.10-h27ca646_0.tar.bz2 + sha256: 4b37ea851a2cf85edf0a63d2a63266847ec3dcbba4a31156d430cdd6aa811303 + md5: c64443234ff91d70cb9c7dc926c58834 + license: LGPL-2.1 + purls: [] + size: 60255 + timestamp: 1604417405528 +- conda: https://conda.anaconda.org/conda-forge/win-64/fribidi-1.0.10-h8d14728_0.tar.bz2 + sha256: e0323e6d7b6047042970812ee810c6b1e1a11a3af4025db26d0965ae5d206104 + md5: 807e81d915f2bb2e49951648615241f6 + depends: + - vc >=14.1,<15.0a0 + - vs2015_runtime >=14.16.27012 + license: LGPL-2.1 + purls: [] + size: 64567 + timestamp: 1604417122064 +- conda: https://conda.anaconda.org/conda-forge/linux-64/frozendict-2.4.6-py312h66e93f0_0.conda + sha256: a251569d25e9658f87406efda6640e2816659c5d4dd244d1008bb789793cf32e + md5: 9fa8408745a0621314b7751d11fecc18 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: LGPL-3.0-only + license_family: LGPL + purls: + - pkg:pypi/frozendict?source=hash-mapping + size: 30486 + timestamp: 1728841445822 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/frozendict-2.4.6-py312h0bf5046_0.conda + sha256: 357cef10885bd2fb5d5d3197a8565d0c0b86fffd0dbaff58acee29f7d897a935 + md5: 22df6d6ec0345fc46182ce47e7ee8e24 + depends: + - __osx >=11.0 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + license: LGPL-3.0-only + license_family: LGPL + purls: + - pkg:pypi/frozendict?source=hash-mapping + size: 30959 + timestamp: 1728841539128 +- conda: https://conda.anaconda.org/conda-forge/win-64/frozendict-2.4.6-py312h4389bb4_0.conda + sha256: 7148c848521bfb2a5d3a0bac9fafc006999bade8a1f872312429b5193e6aff39 + md5: 1d16a74859f027c8654e30400780a033 + depends: + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: LGPL-3.0-only + license_family: LGPL + purls: + - pkg:pypi/frozendict?source=hash-mapping + size: 31147 + timestamp: 1728841600933 +- conda: https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.42.12-hb9ae30d_0.conda + sha256: d5283b95a8d49dcd88d29b360d8b38694aaa905d968d156d72ab71d32b38facb + md5: 201db6c2d9a3c5e46573ac4cb2e92f4f + depends: + - libgcc-ng >=12 + - libglib >=2.80.2,<3.0a0 + - libjpeg-turbo >=3.0.0,<4.0a0 + - libpng >=1.6.43,<1.7.0a0 + - libtiff >=4.6.0,<4.8.0a0 + license: LGPL-2.1-or-later + license_family: LGPL + purls: [] + size: 528149 + timestamp: 1715782983957 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gdk-pixbuf-2.42.12-h7ddc832_0.conda + sha256: 72bcf0a4d3f9aa6d99d7d1d224d19f76ccdb3a4fa85e60f77d17e17985c81bd2 + md5: 151309a7e1eb57a3c2ab8088a1d74f3e + depends: + - __osx >=11.0 + - libglib >=2.80.2,<3.0a0 + - libintl >=0.22.5,<1.0a0 + - libjpeg-turbo >=3.0.0,<4.0a0 + - libpng >=1.6.43,<1.7.0a0 + - libtiff >=4.6.0,<4.8.0a0 + license: LGPL-2.1-or-later + license_family: LGPL + purls: [] + size: 509570 + timestamp: 1715783199780 +- conda: https://conda.anaconda.org/conda-forge/win-64/getopt-win32-0.1-h6a83c73_3.conda + sha256: d04c4a6c11daa72c4a0242602e1d00c03291ef66ca2d7cd0e171088411d57710 + md5: 49c36fcad2e9af6b91e91f2ce5be8ebd + depends: + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + license: LGPL-3.0-only + license_family: LGPL + purls: [] + size: 26238 + timestamp: 1750744808182 +- conda: https://conda.anaconda.org/conda-forge/noarch/ghp-import-2.1.0-pyhd8ed1ab_2.conda + sha256: 40fdf5a9d5cc7a3503cd0c33e1b90b1e6eab251aaaa74e6b965417d089809a15 + md5: 93f742fe078a7b34c29a182958d4d765 + depends: + - python >=3.9 + - python-dateutil >=2.8.1 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/ghp-import?source=hash-mapping + size: 16538 + timestamp: 1734344477841 +- conda: https://conda.anaconda.org/conda-forge/noarch/gitdb-4.0.12-pyhd8ed1ab_0.conda + sha256: dbbec21a369872c8ebe23cb9a3b9d63638479ee30face165aa0fccc96e93eec3 + md5: 7c14f3706e099f8fcd47af2d494616cc + depends: + - python >=3.9 + - smmap >=3.0.1,<6 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/gitdb?source=hash-mapping + size: 53136 + timestamp: 1735887290843 +- conda: https://conda.anaconda.org/conda-forge/noarch/gitpython-3.1.44-pyhff2d567_0.conda + sha256: b996e717ca693e4e831d3d3143aca3abb47536561306195002b226fe4dde53c3 + md5: 140a4e944f7488467872e562a2a52789 + depends: + - gitdb >=4.0.1,<5 + - python >=3.9 + - typing_extensions >=3.7.4.3 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/gitpython?source=hash-mapping + size: 157200 + timestamp: 1735929768433 +- conda: https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.84.2-h4833e2c_0.conda + sha256: eee7655422577df78386513322ea2aa691e7638947584faa715a20488ef6cc4e + md5: f2ec1facec64147850b7674633978050 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libglib 2.84.2 h3618099_0 + license: LGPL-2.1-or-later + purls: [] + size: 116819 + timestamp: 1747836718327 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/glib-tools-2.84.2-h1dc7a0c_0.conda + sha256: 809cb62fe75ca0bcf0eecd223d100b4b4aa4555eee4c3e335ab7f453506bbb78 + md5: c6dd3b852d7287ee3bf1d392f107f1ac + depends: + - __osx >=11.0 + - libglib 2.84.2 hbec27ea_0 + - libintl >=0.24.1,<1.0a0 + license: LGPL-2.1-or-later + purls: [] + size: 101786 + timestamp: 1747837093760 +- conda: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.14-h5888daf_0.conda + sha256: cac69f3ff7756912bbed4c28363de94f545856b35033c0b86193366b95f5317d + md5: 951ff8d9e5536896408e89d63230b8d5 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + license: LGPL-2.0-or-later + license_family: LGPL + purls: [] + size: 98419 + timestamp: 1750079957535 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphite2-1.3.14-h286801f_0.conda + sha256: e1c431b66b0a632e8fcc2b886cccde4eb5ec5eb8a3d84e89b7639d603c174646 + md5: 64d15e1dfe86fa13cf0d519d1074dcd9 + depends: + - __osx >=11.0 + - libcxx >=18 + license: LGPL-2.0-or-later + license_family: LGPL + purls: [] + size: 81566 + timestamp: 1750080158744 +- conda: https://conda.anaconda.org/conda-forge/win-64/graphite2-1.3.14-he0c23c2_0.conda + sha256: bcbcece7719f2a14ede6bfead8f5fdbb65ed102d47769c817b375e4e9d43be39 + md5: 692bc31c646f7e221af07ccc924e1ae4 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: LGPL-2.0-or-later + license_family: LGPL + purls: [] + size: 95862 + timestamp: 1750080330012 +- conda: https://conda.anaconda.org/conda-forge/linux-64/graphviz-13.1.0-hcae58fd_0.conda + sha256: 692f544be3868c590b4db177d39c552e3eeb1631f66a10f5b27982a0e1b0c984 + md5: aa7e2fbfb1f5878d6cee930c43af2200 + depends: + - __glibc >=2.17,<3.0.a0 + - adwaita-icon-theme + - cairo >=1.18.4,<2.0a0 + - fonts-conda-ecosystem + - gdk-pixbuf >=2.42.12,<3.0a0 + - gtk3 >=3.24.43,<4.0a0 + - gts >=0.7.6,<0.8.0a0 + - libexpat >=2.7.0,<3.0a0 + - libgcc >=13 + - libgd >=2.3.3,<2.4.0a0 + - libglib >=2.84.2,<3.0a0 + - librsvg >=2.58.4,<3.0a0 + - libstdcxx >=13 + - libwebp-base >=1.5.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - pango >=1.56.4,<2.0a0 + license: EPL-1.0 + license_family: Other + purls: [] + size: 2426873 + timestamp: 1751389810326 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphviz-13.1.0-haeab78c_0.conda + sha256: 33a78b7c8b016004977d6f7bc57fd34ffe59e09d707f6e32ea431200e5c5da42 + md5: 9c42d3852d69fd546f87674e46a96b16 + depends: + - __osx >=11.0 + - adwaita-icon-theme + - cairo >=1.18.4,<2.0a0 + - fonts-conda-ecosystem + - gdk-pixbuf >=2.42.12,<3.0a0 + - gtk3 >=3.24.43,<4.0a0 + - gts >=0.7.6,<0.8.0a0 + - libcxx >=18 + - libexpat >=2.7.0,<3.0a0 + - libgd >=2.3.3,<2.4.0a0 + - libglib >=2.84.2,<3.0a0 + - librsvg >=2.58.4,<3.0a0 + - libwebp-base >=1.5.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - pango >=1.56.4,<2.0a0 + license: EPL-1.0 + license_family: Other + purls: [] + size: 2203405 + timestamp: 1751390045031 +- conda: https://conda.anaconda.org/conda-forge/win-64/graphviz-13.1.0-ha5e8f4b_0.conda + sha256: 7c1406cfe21964a48ca0f82c3a06af993ecb105371d58a2936436df1ea97572c + md5: c08489e81c3ac3920b0a7a7f849d13b8 + depends: + - cairo >=1.18.4,<2.0a0 + - getopt-win32 >=0.1,<0.1.1.0a0 + - gts >=0.7.6,<0.8.0a0 + - libexpat >=2.7.0,<3.0a0 + - libgd >=2.3.3,<2.4.0a0 + - libglib >=2.84.2,<3.0a0 + - libwebp-base >=1.5.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - pango >=1.56.4,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: EPL-1.0 + license_family: Other + purls: [] + size: 1205420 + timestamp: 1751389900374 +- conda: https://conda.anaconda.org/conda-forge/linux-64/gtk3-3.24.43-h0c6a113_5.conda + sha256: d36263cbcbce34ec463ce92bd72efa198b55d987959eab6210cc256a0e79573b + md5: 67d00e9cfe751cfe581726c5eff7c184 + depends: + - __glibc >=2.17,<3.0.a0 + - at-spi2-atk >=2.38.0,<3.0a0 + - atk-1.0 >=2.38.0 + - cairo >=1.18.4,<2.0a0 + - epoxy >=1.5.10,<1.6.0a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - fribidi >=1.0.10,<2.0a0 + - gdk-pixbuf >=2.42.12,<3.0a0 + - glib-tools + - harfbuzz >=11.0.0,<12.0a0 + - hicolor-icon-theme + - libcups >=2.3.3,<2.4.0a0 + - libcups >=2.3.3,<3.0a0 + - libexpat >=2.6.4,<3.0a0 + - libgcc >=13 + - libglib >=2.84.0,<3.0a0 + - liblzma >=5.6.4,<6.0a0 + - libxkbcommon >=1.8.1,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - pango >=1.56.3,<2.0a0 + - wayland >=1.23.1,<2.0a0 + - xorg-libx11 >=1.8.12,<2.0a0 + - xorg-libxcomposite >=0.4.6,<1.0a0 + - xorg-libxcursor >=1.2.3,<2.0a0 + - xorg-libxdamage >=1.1.6,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxfixes >=6.0.1,<7.0a0 + - xorg-libxi >=1.8.2,<2.0a0 + - xorg-libxinerama >=1.1.5,<1.2.0a0 + - xorg-libxrandr >=1.5.4,<2.0a0 + - xorg-libxrender >=0.9.12,<0.10.0a0 + license: LGPL-2.0-or-later + license_family: LGPL + purls: [] + size: 5585389 + timestamp: 1743405684985 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gtk3-3.24.43-h07173f4_5.conda + sha256: 9650ac1a02975ae0a3917443dc3c35ddc4d8e87a1cb04fda115af5f98e5d457c + md5: 8353369d4c2ecc5afd888405d3226fd9 + depends: + - __osx >=11.0 + - atk-1.0 >=2.38.0 + - cairo >=1.18.4,<2.0a0 + - epoxy >=1.5.10,<1.6.0a0 + - fribidi >=1.0.10,<2.0a0 + - gdk-pixbuf >=2.42.12,<3.0a0 + - glib-tools + - harfbuzz >=11.0.0,<12.0a0 + - hicolor-icon-theme + - libexpat >=2.6.4,<3.0a0 + - libglib >=2.84.0,<3.0a0 + - libintl >=0.23.1,<1.0a0 + - liblzma >=5.6.4,<6.0a0 + - libzlib >=1.3.1,<2.0a0 + - pango >=1.56.3,<2.0a0 + license: LGPL-2.0-or-later + license_family: LGPL + purls: [] + size: 4792338 + timestamp: 1743406461562 +- conda: https://conda.anaconda.org/conda-forge/linux-64/gts-0.7.6-h977cf35_4.conda + sha256: b5cd16262fefb836f69dc26d879b6508d29f8a5c5948a966c47fe99e2e19c99b + md5: 4d8df0b0db060d33c9a702ada998a8fe + depends: + - libgcc-ng >=12 + - libglib >=2.76.3,<3.0a0 + - libstdcxx-ng >=12 + license: LGPL-2.0-or-later + license_family: LGPL + purls: [] + size: 318312 + timestamp: 1686545244763 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/gts-0.7.6-he42f4ea_4.conda + sha256: e0f8c7bc1b9ea62ded78ffa848e37771eeaaaf55b3146580513c7266862043ba + md5: 21b4dd3098f63a74cf2aa9159cbef57d + depends: + - libcxx >=15.0.7 + - libglib >=2.76.3,<3.0a0 + license: LGPL-2.0-or-later + license_family: LGPL + purls: [] + size: 304331 + timestamp: 1686545503242 +- conda: https://conda.anaconda.org/conda-forge/win-64/gts-0.7.6-h6b5321d_4.conda + sha256: b79755d2f9fc2113b6949bfc170c067902bc776e2c20da26e746e780f4f5a2d4 + md5: a41f14768d5e377426ad60c613f2923b + depends: + - libglib >=2.76.3,<3.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: LGPL-2.0-or-later + license_family: LGPL + purls: [] + size: 188688 + timestamp: 1686545648050 +- conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.2.0-pyhd8ed1ab_0.conda + sha256: 0aa1cdc67a9fe75ea95b5644b734a756200d6ec9d0dff66530aec3d1c1e9df75 + md5: b4754fb1bdcb70c8fd54f918301582c6 + depends: + - hpack >=4.1,<5 + - hyperframe >=6.1,<7 + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/h2?source=hash-mapping + size: 53888 + timestamp: 1738578623567 +- conda: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-11.2.1-h3beb420_0.conda + sha256: 5bd0f3674808862838d6e2efc0b3075e561c34309c5c2f4c976f7f1f57c91112 + md5: 0e6e192d4b3d95708ad192d957cf3163 + depends: + - __glibc >=2.17,<3.0.a0 + - cairo >=1.18.4,<2.0a0 + - freetype + - graphite2 + - icu >=75.1,<76.0a0 + - libexpat >=2.7.0,<3.0a0 + - libfreetype >=2.13.3 + - libfreetype6 >=2.13.3 + - libgcc >=13 + - libglib >=2.84.1,<3.0a0 + - libstdcxx >=13 + - libzlib >=1.3.1,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 1730226 + timestamp: 1747091044218 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/harfbuzz-11.2.1-hab40de2_0.conda + sha256: 244e4071229aa3b824dd2a9814c0e8b4c2b40dfb28914ec2247bf27c5c681584 + md5: 12f4520f618ff6e398a2c8e0bed1e580 + depends: + - __osx >=11.0 + - cairo >=1.18.4,<2.0a0 + - freetype + - graphite2 + - icu >=75.1,<76.0a0 + - libcxx >=18 + - libexpat >=2.7.0,<3.0a0 + - libfreetype >=2.13.3 + - libfreetype6 >=2.13.3 + - libglib >=2.84.1,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 1395282 + timestamp: 1747091793921 +- conda: https://conda.anaconda.org/conda-forge/win-64/harfbuzz-11.2.1-h8796e6f_0.conda + sha256: 26e09e2b43d498523c08c58ea485c883478b74e2fb664c0321089e5c10318d32 + md5: bccea58fbf7910ce868b084f27ffe8bd + depends: + - cairo >=1.18.4,<2.0a0 + - freetype + - graphite2 + - icu >=75.1,<76.0a0 + - libexpat >=2.7.0,<3.0a0 + - libfreetype >=2.13.3 + - libfreetype6 >=2.13.3 + - libglib >=2.84.1,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + purls: [] + size: 1126103 + timestamp: 1747093237683 +- conda: https://conda.anaconda.org/conda-forge/linux-64/hicolor-icon-theme-0.17-ha770c72_2.tar.bz2 + sha256: 336f29ceea9594f15cc8ec4c45fdc29e10796573c697ee0d57ebb7edd7e92043 + md5: bbf6f174dcd3254e19a2f5d2295ce808 + license: GPL-2.0-or-later + license_family: GPL + purls: [] + size: 13841 + timestamp: 1605162808667 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/hicolor-icon-theme-0.17-hce30654_2.tar.bz2 + sha256: 286e33fb452f61133a3a61d002890235d1d1378554218ab063d6870416440281 + md5: 237b05b7eb284d7eebc3c5d93f5e4bca + license: GPL-2.0-or-later + license_family: GPL + purls: [] + size: 13800 + timestamp: 1611053664863 +- conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + sha256: 6ad78a180576c706aabeb5b4c8ceb97c0cb25f1e112d76495bff23e3779948ba + md5: 0a802cb9888dd14eeefc611f05c40b6e + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/hpack?source=hash-mapping + size: 30731 + timestamp: 1737618390337 +- conda: https://conda.anaconda.org/conda-forge/noarch/humanfriendly-10.0-pyh707e725_8.conda + sha256: fa2071da7fab758c669e78227e6094f6b3608228740808a6de5d6bce83d9e52d + md5: 7fe569c10905402ed47024fc481bb371 + depends: + - __unix + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/humanfriendly?source=hash-mapping + size: 73563 + timestamp: 1733928021866 +- conda: https://conda.anaconda.org/conda-forge/noarch/humanfriendly-10.0-pyh7428d3b_8.conda + sha256: acdf32d1f9600091f0efc1a4293ad217074c86a96889509d3d04c13ffbc92e5a + md5: d243aef76c0a30e4c89cd39e496ea1be + depends: + - __win + - pyreadline3 + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/humanfriendly?source=hash-mapping + size: 74084 + timestamp: 1733928364561 +- conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + sha256: 77af6f5fe8b62ca07d09ac60127a30d9069fdc3c68d6b256754d0ffb1f7779f8 + md5: 8e6923fc12f1fe8f8c4e5c9f343256ac + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/hyperframe?source=hash-mapping + size: 17397 + timestamp: 1737618427549 +- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda + sha256: 71e750d509f5fa3421087ba88ef9a7b9be11c53174af3aa4d06aff4c18b38e8e + md5: 8b189310083baabfb622af68fd9d3ae3 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc-ng >=12 + - libstdcxx-ng >=12 + license: MIT + license_family: MIT + purls: [] + size: 12129203 + timestamp: 1720853576813 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-75.1-hfee45f7_0.conda + sha256: 9ba12c93406f3df5ab0a43db8a4b4ef67a5871dfd401010fbe29b218b2cbe620 + md5: 5eb22c1d7b3fc4abb50d92d621583137 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 11857802 + timestamp: 1720853997952 +- conda: https://conda.anaconda.org/conda-forge/win-64/icu-75.1-he0c23c2_0.conda + sha256: 1d04369a1860a1e9e371b9fc82dd0092b616adcf057d6c88371856669280e920 + md5: 8579b6bb8d18be7c0b27fb08adeeeb40 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + purls: [] + size: 14544252 + timestamp: 1720853966338 +- conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.10-pyhd8ed1ab_1.conda + sha256: d7a472c9fd479e2e8dcb83fb8d433fce971ea369d704ece380e876f9c3494e87 + md5: 39a4f67be3286c86d696df570b1201b7 + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/idna?source=hash-mapping + size: 49765 + timestamp: 1733211921194 +- conda: https://conda.anaconda.org/conda-forge/linux-64/immutables-0.21-py312h66e93f0_1.conda + sha256: 5405a85a45eedc3079ec719188ece89983d490b636025ef94590f55525f0509e + md5: 1ae66d7a2792aa9f3beaeb4c67c71bbd + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/immutables?source=hash-mapping + size: 54657 + timestamp: 1747742470005 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/immutables-0.21-py312hea69d52_1.conda + sha256: 93b2ebc748faa4ab2e690b326e28fbbb8c3e31a105989607239c517af6ab5558 + md5: 57a7dd38b4c5c1f134cce4a74859ddec + depends: + - __osx >=11.0 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/immutables?source=hash-mapping + size: 51492 + timestamp: 1747742634443 +- conda: https://conda.anaconda.org/conda-forge/win-64/immutables-0.21-py312h4389bb4_1.conda + sha256: f12d00be41820b5b957f32e4fa8ab8876cc62bef216119a19a0e491a0d9ba62f + md5: b305af37433b3bb6370959eb53cd033b + depends: + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/immutables?source=hash-mapping + size: 54837 + timestamp: 1747742818245 +- conda: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-8.7.0-pyhe01879c_1.conda + sha256: c18ab120a0613ada4391b15981d86ff777b5690ca461ea7e9e49531e8f374745 + md5: 63ccfdc3a3ce25b027b8767eb722fca8 + depends: + - python >=3.9 + - zipp >=3.20 + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/importlib-metadata?source=hash-mapping + size: 34641 + timestamp: 1747934053147 +- conda: https://conda.anaconda.org/conda-forge/noarch/importlib_resources-6.5.2-pyhd8ed1ab_0.conda + sha256: acc1d991837c0afb67c75b77fdc72b4bf022aac71fedd8b9ea45918ac9b08a80 + md5: c85c76dc67d75619a92f51dfbce06992 + depends: + - python >=3.9 + - zipp >=3.1.0 + constrains: + - importlib-resources >=6.5.2,<6.5.3.0a0 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/importlib-resources?source=hash-mapping + size: 33781 + timestamp: 1736252433366 +- conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.0.0-pyhd8ed1ab_1.conda + sha256: 0ec8f4d02053cd03b0f3e63168316530949484f80e16f5e2fb199a1d117a89ca + md5: 6837f3eff7dcea42ecd714ce1ac2b108 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/iniconfig?source=hash-mapping + size: 11474 + timestamp: 1733223232820 +- conda: https://conda.anaconda.org/conda-forge/win-64/intel-openmp-2024.2.1-h57928b3_1083.conda + sha256: 0fd2b0b84c854029041b0ede8f4c2369242ee92acc0092f8407b1fe9238a8209 + md5: 2d89243bfb53652c182a7c73182cce4f + license: LicenseRef-IntelSimplifiedSoftwareOct2022 + license_family: Proprietary + purls: [] + size: 1852356 + timestamp: 1723739573141 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipdb-0.13.13-pyhd8ed1ab_1.conda + sha256: 33275d537122e67df200203d541170db8b55886667d30cc7262cc1e463b04406 + md5: 044c5249ad8ea18a414d07baa1f369ea + depends: + - decorator + - ipython + - python >=3.9 + - toml >=0.10.2 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipdb?source=hash-mapping + size: 18713 + timestamp: 1734884952029 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh3099207_0.conda + sha256: 33cfd339bb4efac56edf93474b37ddc049e08b1b4930cf036c893cc1f5a1f32a + md5: b40131ab6a36ac2c09b7c57d4d3fbf99 + depends: + - __linux + - comm >=0.1.1 + - debugpy >=1.6.5 + - ipython >=7.23.1 + - jupyter_client >=6.1.12 + - jupyter_core >=4.12,!=5.0.* + - matplotlib-inline >=0.1 + - nest-asyncio + - packaging + - psutil + - python >=3.8 + - pyzmq >=24 + - tornado >=6.1 + - traitlets >=5.4.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipykernel?source=hash-mapping + size: 119084 + timestamp: 1719845605084 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh4bbf305_0.conda + sha256: dc569094125127c0078aa536f78733f383dd7e09507277ef8bcd1789786e7086 + md5: 18df5fc4944a679e085e0e8f31775fc8 + depends: + - __win + - comm >=0.1.1 + - debugpy >=1.6.5 + - ipython >=7.23.1 + - jupyter_client >=6.1.12 + - jupyter_core >=4.12,!=5.0.* + - matplotlib-inline >=0.1 + - nest-asyncio + - packaging + - psutil + - python >=3.8 + - pyzmq >=24 + - tornado >=6.1 + - traitlets >=5.4.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipykernel?source=hash-mapping + size: 119853 + timestamp: 1719845858082 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipykernel-6.29.5-pyh57ce528_0.conda + sha256: 072534d4d379225b2c3a4e38bc7730b65ae171ac7f0c2d401141043336e97980 + md5: 9eb15d654daa0ef5a98802f586bb4ffc + depends: + - __osx + - appnope + - comm >=0.1.1 + - debugpy >=1.6.5 + - ipython >=7.23.1 + - jupyter_client >=6.1.12 + - jupyter_core >=4.12,!=5.0.* + - matplotlib-inline >=0.1 + - nest-asyncio + - packaging + - psutil + - python >=3.8 + - pyzmq >=24 + - tornado >=6.1 + - traitlets >=5.4.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipykernel?source=hash-mapping + size: 119568 + timestamp: 1719845667420 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.4.0-pyh6be1c34_0.conda + sha256: 8fb441c9f4b50e38b6059e8984e49208a4e2a4ec4e41b543ebaa894f8261d4c9 + md5: b551e25e4fb27ccb51aff2c5dcf178f4 + depends: + - __win + - colorama + - decorator + - exceptiongroup + - ipython_pygments_lexers + - jedi >=0.16 + - matplotlib-inline + - pickleshare + - prompt-toolkit >=3.0.41,<3.1.0 + - pygments >=2.4.0 + - python >=3.11 + - stack_data + - traitlets >=5.13.0 + - typing_extensions >=4.6 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipython?source=hash-mapping + size: 627419 + timestamp: 1751470649672 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipython-9.4.0-pyhfa0c392_0.conda + sha256: ff5138bf6071ca01d84e1329f6baa96f0723df6fe183cfa1ab3ebc96240e6d8f + md5: cb7706b10f35e7507917cefa0978a66d + depends: + - __unix + - pexpect >4.3 + - decorator + - exceptiongroup + - ipython_pygments_lexers + - jedi >=0.16 + - matplotlib-inline + - pickleshare + - prompt-toolkit >=3.0.41,<3.1.0 + - pygments >=2.4.0 + - python >=3.11 + - stack_data + - traitlets >=5.13.0 + - typing_extensions >=4.6 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipython?source=compressed-mapping + size: 628259 + timestamp: 1751465044469 +- conda: https://conda.anaconda.org/conda-forge/noarch/ipython_pygments_lexers-1.1.1-pyhd8ed1ab_0.conda + sha256: 894682a42a7d659ae12878dbcb274516a7031bbea9104e92f8e88c1f2765a104 + md5: bd80ba060603cc228d9d81c257093119 + depends: + - pygments + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/ipython-pygments-lexers?source=hash-mapping + size: 13993 + timestamp: 1737123723464 +- conda: https://conda.anaconda.org/conda-forge/noarch/jedi-0.19.2-pyhd8ed1ab_1.conda + sha256: 92c4d217e2dc68983f724aa983cca5464dcb929c566627b26a2511159667dba8 + md5: a4f4c5dc9b80bc50e0d3dc4e6e8f1bd9 + depends: + - parso >=0.8.3,<0.9.0 + - python >=3.9 + license: Apache-2.0 AND MIT + purls: + - pkg:pypi/jedi?source=hash-mapping + size: 843646 + timestamp: 1733300981994 +- conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhd8ed1ab_0.conda + sha256: f1ac18b11637ddadc05642e8185a851c7fab5998c6f5470d716812fae943b2af + md5: 446bd6c8cb26050d528881df495ce646 + depends: + - markupsafe >=2.0 + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jinja2?source=compressed-mapping + size: 112714 + timestamp: 1741263433881 +- pypi: https://files.pythonhosted.org/packages/2d/14/1c65fccf8413d5f5c6e8425f84675169654395098000d8bddc4e9d3390e1/jsbeautifier-1.15.4-py3-none-any.whl + name: jsbeautifier + version: 1.15.4 + sha256: 72f65de312a3f10900d7685557f84cb61a9733c50dcc27271a39f5b0051bf528 + requires_dist: + - six>=1.13.0 + - editorconfig>=0.12.2 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonpatch-1.33-pyhd8ed1ab_1.conda + sha256: 304955757d1fedbe344af43b12b5467cca072f83cce6109361ba942e186b3993 + md5: cb60ae9cf02b9fcb8004dec4089e5691 + depends: + - jsonpointer >=1.9 + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jsonpatch?source=hash-mapping + size: 17311 + timestamp: 1733814664790 +- conda: https://conda.anaconda.org/conda-forge/linux-64/jsonpointer-3.0.0-py312h7900ff3_1.conda + sha256: 76ccb7bffc7761d1d3133ffbe1f7f1710a0f0d9aaa9f7ea522652e799f3601f4 + md5: 6b51f7459ea4073eeb5057207e2e1e3d + depends: + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jsonpointer?source=hash-mapping + size: 17277 + timestamp: 1725303032027 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/jsonpointer-3.0.0-py312h81bd7bf_1.conda + sha256: f6fb3734e967d1cd0cde32844ee952809f6c0a49895da7ec1c8cfdf97739b947 + md5: 80f403c03290e1662be03e026fb5f8ab + depends: + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jsonpointer?source=hash-mapping + size: 17865 + timestamp: 1725303130815 +- conda: https://conda.anaconda.org/conda-forge/win-64/jsonpointer-3.0.0-py312h2e8e312_1.conda + sha256: 6865b97780e795337f65592582aee6f25e5b96214c64ffd3f8cdf580fd64ba22 + md5: e3ceda014d8461a11ca8552830a978f9 + depends: + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jsonpointer?source=hash-mapping + size: 42235 + timestamp: 1725303419414 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-4.24.0-pyhd8ed1ab_0.conda + sha256: 812134fabb49493a50f7f443dc0ffafd0f63766f403a0bd8e71119763e57456a + md5: 59220749abcd119d645e6879983497a1 + depends: + - attrs >=22.2.0 + - importlib_resources >=1.4.0 + - jsonschema-specifications >=2023.03.6 + - pkgutil-resolve-name >=1.3.10 + - python >=3.9 + - referencing >=0.28.4 + - rpds-py >=0.7.1 + license: MIT + license_family: MIT + purls: + - pkg:pypi/jsonschema?source=hash-mapping + size: 75124 + timestamp: 1748294389597 +- conda: https://conda.anaconda.org/conda-forge/noarch/jsonschema-specifications-2025.4.1-pyh29332c3_0.conda + sha256: 66fbad7480f163509deec8bd028cd3ea68e58022982c838683586829f63f3efa + md5: 41ff526b1083fde51fbdc93f29282e0e + depends: + - python >=3.9 + - referencing >=0.31.0 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/jsonschema-specifications?source=hash-mapping + size: 19168 + timestamp: 1745424244298 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_client-8.6.3-pyhd8ed1ab_1.conda + sha256: 19d8bd5bb2fde910ec59e081eeb59529491995ce0d653a5209366611023a0b3a + md5: 4ebae00eae9705b0c3d6d1018a81d047 + depends: + - importlib-metadata >=4.8.3 + - jupyter_core >=4.12,!=5.0.* + - python >=3.9 + - python-dateutil >=2.8.2 + - pyzmq >=23.0 + - tornado >=6.2 + - traitlets >=5.3 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-client?source=hash-mapping + size: 106342 + timestamp: 1733441040958 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh31011fe_0.conda + sha256: 56a7a7e907f15cca8c4f9b0c99488276d4cb10821d2d15df9245662184872e81 + md5: b7d89d860ebcda28a5303526cdee68ab + depends: + - __unix + - platformdirs >=2.5 + - python >=3.8 + - traitlets >=5.3 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-core?source=hash-mapping + size: 59562 + timestamp: 1748333186063 +- conda: https://conda.anaconda.org/conda-forge/noarch/jupyter_core-5.8.1-pyh5737063_0.conda + sha256: 928c2514c2974fda78447903217f01ca89a77eefedd46bf6a2fe97072df57e8d + md5: 324e60a0d3f39f268e899709575ea3cd + depends: + - __win + - cpython + - platformdirs >=2.5 + - python >=3.8 + - pywin32 >=300 + - traitlets >=5.3 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/jupyter-core?source=compressed-mapping + size: 59972 + timestamp: 1748333368923 +- conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.1-h166bdaf_0.tar.bz2 + sha256: 150c05a6e538610ca7c43beb3a40d65c90537497a4f6a5f4d15ec0451b6f5ebb + md5: 30186d27e2c9fa62b45fb1476b7200e3 + depends: + - libgcc-ng >=10.3.0 + license: LGPL-2.1-or-later + purls: [] + size: 117831 + timestamp: 1646151697040 +- conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda + sha256: 99df692f7a8a5c27cd14b5fb1374ee55e756631b9c3d659ed3ee60830249b238 + md5: 3f43953b7d3fb3aaa1d0d0723d91e368 + depends: + - keyutils >=1.6.1,<2.0a0 + - libedit >=3.1.20191231,<3.2.0a0 + - libedit >=3.1.20191231,<4.0a0 + - libgcc-ng >=12 + - libstdcxx-ng >=12 + - openssl >=3.3.1,<4.0a0 + license: MIT + license_family: MIT + purls: [] + size: 1370023 + timestamp: 1719463201255 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.21.3-h237132a_0.conda + sha256: 4442f957c3c77d69d9da3521268cad5d54c9033f1a73f99cde0a3658937b159b + md5: c6dc8a0fdec13a0565936655c33069a1 + depends: + - __osx >=11.0 + - libcxx >=16 + - libedit >=3.1.20191231,<3.2.0a0 + - libedit >=3.1.20191231,<4.0a0 + - openssl >=3.3.1,<4.0a0 + license: MIT + license_family: MIT + purls: [] + size: 1155530 + timestamp: 1719463474401 +- conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.21.3-hdf4eb48_0.conda + sha256: 18e8b3430d7d232dad132f574268f56b3eb1a19431d6d5de8c53c29e6c18fa81 + md5: 31aec030344e962fbd7dbbbbd68e60a9 + depends: + - openssl >=3.3.1,<4.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + purls: [] + size: 712034 + timestamp: 1719463874284 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.44-h1423503_1.conda + sha256: 1a620f27d79217c1295049ba214c2f80372062fd251b569e9873d4a953d27554 + md5: 0be7c6e070c19105f966d3758448d018 + depends: + - __glibc >=2.17,<3.0.a0 + constrains: + - binutils_impl_linux-64 2.44 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 676044 + timestamp: 1752032747103 +- conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.0.0-h0aef613_1.conda + sha256: 412381a43d5ff9bbed82cd52a0bbca5b90623f62e41007c9c42d3870c60945ff + md5: 9344155d33912347b37f0ae6c410a835 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 264243 + timestamp: 1745264221534 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.0.0-hd64df32_1.conda + sha256: 12361697f8ffc9968907d1a7b5830e34c670e4a59b638117a2cdfed8f63a38f8 + md5: a74332d9b60b62905e3d30709df08bf1 + depends: + - __osx >=11.0 + - libcxx >=18 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 188306 + timestamp: 1745264362794 +- conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.0.0-h6470a55_1.conda + sha256: 868a3dff758cc676fa1286d3f36c3e0101cca56730f7be531ab84dc91ec58e9d + md5: c1b81da6d29a14b542da14a36c9fbf3f + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 164701 + timestamp: 1745264384716 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libarchive-3.8.1-gpl_h98cc613_100.conda + sha256: 6f35e429909b0fa6a938f8ff79e1d7000e8f15fbb37f67be6f789348fea4c602 + md5: 9de6247361e1ee216b09cfb8b856e2ee + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - libgcc >=13 + - liblzma >=5.8.1,<6.0a0 + - libxml2 >=2.13.8,<2.14.0a0 + - libzlib >=1.3.1,<2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - lzo >=2.10,<3.0a0 + - openssl >=3.5.0,<4.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 883383 + timestamp: 1749385818314 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarchive-3.8.1-gpl_h46e8061_100.conda + sha256: 7728d08880637622caaf03e6f8e92ee383715e145637a779d668e1ac677717f0 + md5: b8d09de5df5352f9e0eb7a27cc79a675 + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libxml2 >=2.13.8,<2.14.0a0 + - libzlib >=1.3.1,<2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - lzo >=2.10,<3.0a0 + - openssl >=3.5.0,<4.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 788465 + timestamp: 1749385999215 +- conda: https://conda.anaconda.org/conda-forge/win-64/libarchive-3.8.1-gpl_h1ca5a36_100.conda + sha256: 7efe65c7ab7056f1a84d5f234584e60ba3cc55b487ba4065a29d23aacb4c5ef6 + md5: d8f4c086758bbf52b30750550cd38b1a + depends: + - bzip2 >=1.0.8,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libxml2 >=2.13.8,<2.14.0a0 + - libzlib >=1.3.1,<2.0a0 + - lz4-c >=1.10.0,<1.11.0a0 + - lzo >=2.10,<3.0a0 + - openssl >=3.5.0,<4.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - zstd >=1.5.7,<1.6.0a0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 1098688 + timestamp: 1749386269743 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-32_h59b9bed_openblas.conda + build_number: 32 + sha256: 1540bf739feb446ff71163923e7f044e867d163c50b605c8b421c55ff39aa338 + md5: 2af9f3d5c2e39f417ce040f5a35c40c6 + depends: + - libopenblas >=0.3.30,<0.3.31.0a0 + - libopenblas >=0.3.30,<1.0a0 + constrains: + - libcblas 3.9.0 32*_openblas + - mkl <2025 + - liblapacke 3.9.0 32*_openblas + - blas 2.132 openblas + - liblapack 3.9.0 32*_openblas + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 17330 + timestamp: 1750388798074 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.9.0-32_h10e41b3_openblas.conda + build_number: 32 + sha256: 2775472dd81d43dc20804b484028560bfecd5ab4779e39f1fb95684da3ff2029 + md5: d4a1732d2b330c9d5d4be16438a0ac78 + depends: + - libopenblas >=0.3.30,<0.3.31.0a0 + - libopenblas >=0.3.30,<1.0a0 + constrains: + - blas 2.132 openblas + - liblapack 3.9.0 32*_openblas + - mkl <2025 + - libcblas 3.9.0 32*_openblas + - liblapacke 3.9.0 32*_openblas + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 17520 + timestamp: 1750388963178 +- conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.9.0-32_h641d27c_mkl.conda + build_number: 32 + sha256: 809d78b096e70fed7ebb17c867dd5dde2f9f4ed8564967a6e10c65b3513b0c31 + md5: 49b36a01450e96c516bbc5486d4a0ea0 + depends: + - mkl 2024.2.2 h66d3029_15 + constrains: + - libcblas 3.9.0 32*_mkl + - liblapack 3.9.0 32*_mkl + - liblapacke 3.9.0 32*_mkl + - blas 2.132 mkl + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 3735390 + timestamp: 1750389080409 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-32_he106b2a_openblas.conda + build_number: 32 + sha256: 92a001fc181e6abe4f4a672b81d9413ca2f22609f8a95327dfcc6eee593ffeb9 + md5: 3d3f9355e52f269cd8bc2c440d8a5263 + depends: + - libblas 3.9.0 32_h59b9bed_openblas + constrains: + - blas 2.132 openblas + - liblapack 3.9.0 32*_openblas + - liblapacke 3.9.0 32*_openblas + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 17308 + timestamp: 1750388809353 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.9.0-32_hb3479ef_openblas.conda + build_number: 32 + sha256: 25d46ace14c3ac45d4aa18b5f7a0d3d30cec422297e900f8b97a66334232061c + md5: d8e8ba717ae863b13a7495221f2b5a71 + depends: + - libblas 3.9.0 32_h10e41b3_openblas + constrains: + - blas 2.132 openblas + - liblapack 3.9.0 32*_openblas + - liblapacke 3.9.0 32*_openblas + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 17485 + timestamp: 1750388970626 +- conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.9.0-32_h5e41251_mkl.conda + build_number: 32 + sha256: d0f81145ae795592f3f3b5d7ff641c1019a99d6b308bfaf2a4cc5ba24b067bb0 + md5: 054b9b4b48296e4413cf93e6ece7b27d + depends: + - libblas 3.9.0 32_h641d27c_mkl + constrains: + - liblapack 3.9.0 32*_mkl + - liblapacke 3.9.0 32*_mkl + - blas 2.132 mkl + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 3735392 + timestamp: 1750389122586 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcups-2.3.3-hb8b1518_5.conda + sha256: cb83980c57e311783ee831832eb2c20ecb41e7dee6e86e8b70b8cef0e43eab55 + md5: d4a250da4737ee127fb1fa6452a9002e + depends: + - __glibc >=2.17,<3.0.a0 + - krb5 >=1.21.3,<1.22.0a0 + - libgcc >=13 + - libstdcxx >=13 + - libzlib >=1.3.1,<2.0a0 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 4523621 + timestamp: 1749905341688 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.14.1-h332b0f4_0.conda + sha256: b6c5cf340a4f80d70d64b3a29a7d9885a5918d16a5cb952022820e6d3e79dc8b + md5: 45f6713cb00f124af300342512219182 + depends: + - __glibc >=2.17,<3.0.a0 + - krb5 >=1.21.3,<1.22.0a0 + - libgcc >=13 + - libnghttp2 >=1.64.0,<2.0a0 + - libssh2 >=1.11.1,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.0,<4.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: curl + license_family: MIT + purls: [] + size: 449910 + timestamp: 1749033146806 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.14.1-h73640d1_0.conda + sha256: 0055b68137309db41ec34c938d95aec71d1f81bd9d998d5be18f32320c3ccba0 + md5: 1af57c823803941dfc97305248a56d57 + depends: + - __osx >=11.0 + - krb5 >=1.21.3,<1.22.0a0 + - libnghttp2 >=1.64.0,<2.0a0 + - libssh2 >=1.11.1,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.0,<4.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: curl + license_family: MIT + purls: [] + size: 403456 + timestamp: 1749033320430 +- conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.14.1-h88aaa65_0.conda + sha256: b2cface2cf35d8522289df7fffc14370596db6f6dc481cc1b6ca313faeac19d8 + md5: 836b9c08f34d2017dbcaec907c6a1138 + depends: + - krb5 >=1.21.3,<1.22.0a0 + - libssh2 >=1.11.1,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: curl + license_family: MIT + purls: [] + size: 368346 + timestamp: 1749033492826 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-20.1.8-ha82da77_0.conda + sha256: 3d7fd77e37794c28e99812da03de645b8e1ddefa876d9400c4d552b9eb8dd880 + md5: 149bb93ede144e7c86bf5f88378ae5f6 + depends: + - __osx >=11.0 + license: Apache-2.0 WITH LLVM-exception + license_family: Apache + purls: [] + size: 567309 + timestamp: 1752050056857 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.24-h86f0d12_0.conda + sha256: 8420748ea1cc5f18ecc5068b4f24c7a023cc9b20971c99c824ba10641fb95ddf + md5: 64f0c503da58ec25ebd359e4d990afa8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: MIT + license_family: MIT + purls: [] + size: 72573 + timestamp: 1747040452262 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.24-h5773f1b_0.conda + sha256: 417d52b19c679e1881cce3f01cad3a2d542098fa2d6df5485aac40f01aede4d1 + md5: 3baf58a5a87e7c2f4d243ce2f8f2fe5c + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 54790 + timestamp: 1747040549847 +- conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.24-h76ddb4d_0.conda + sha256: 65347475c0009078887ede77efe60db679ea06f2b56f7853b9310787fe5ad035 + md5: 08d988e266c6ae77e03d164b83786dc4 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + purls: [] + size: 156292 + timestamp: 1747040812624 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda + sha256: d789471216e7aba3c184cd054ed61ce3f6dac6f87a50ec69291b9297f8c18724 + md5: c277e0a4d549b03ac1e9d6cbbe3d017b + depends: + - ncurses + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - ncurses >=6.5,<7.0a0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 134676 + timestamp: 1738479519902 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda + sha256: 66aa216a403de0bb0c1340a88d1a06adaff66bae2cfd196731aa24db9859d631 + md5: 44083d2d2c2025afca315c7a172eab2b + depends: + - ncurses + - __osx >=11.0 + - ncurses >=6.5,<7.0a0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 107691 + timestamp: 1738479560845 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda + sha256: 1cd6048169fa0395af74ed5d8f1716e22c19a81a8a36f934c110ca3ad4dd27b4 + md5: 172bf1cd1ff8629f2b1179945ed45055 + depends: + - libgcc-ng >=12 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 112766 + timestamp: 1702146165126 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda + sha256: 95cecb3902fbe0399c3a7e67a5bed1db813e5ab0e22f4023a5e0f722f2cc214f + md5: 36d33e440c31857372a72137f78bacf5 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 107458 + timestamp: 1702146414478 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.0-h5888daf_0.conda + sha256: 33ab03438aee65d6aa667cf7d90c91e5e7d734c19a67aa4c7040742c0a13d505 + md5: db0bfbe7dd197b68ad5f30333bae6ce0 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - expat 2.7.0.* + license: MIT + license_family: MIT + purls: [] + size: 74427 + timestamp: 1743431794976 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.0-h286801f_0.conda + sha256: ee550e44765a7bbcb2a0216c063dcd53ac914a7be5386dd0554bd06e6be61840 + md5: 6934bbb74380e045741eb8637641a65b + depends: + - __osx >=11.0 + constrains: + - expat 2.7.0.* + license: MIT + license_family: MIT + purls: [] + size: 65714 + timestamp: 1743431789879 +- conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.0-he0c23c2_0.conda + sha256: 1a227c094a4e06bd54e8c2f3ec40c17ff99dcf3037d812294f842210aa66dbeb + md5: b6f5352fdb525662f4169a0431d2dd7a + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - expat 2.7.0.* + license: MIT + license_family: MIT + purls: [] + size: 140896 + timestamp: 1743432122520 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.6-h2dba641_1.conda + sha256: 764432d32db45466e87f10621db5b74363a9f847d2b8b1f9743746cd160f06ab + md5: ede4673863426c0883c0063d853bbd85 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: MIT + license_family: MIT + purls: [] + size: 57433 + timestamp: 1743434498161 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.4.6-h1da3d7d_1.conda + sha256: c6a530924a9b14e193ea9adfe92843de2a806d1b7dbfd341546ece9653129e60 + md5: c215a60c2935b517dcda8cad4705734d + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 39839 + timestamp: 1743434670405 +- conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.4.6-h537db12_1.conda + sha256: d3b0b8812eab553d3464bbd68204f007f1ebadf96ce30eb0cbc5159f72e353f5 + md5: 85d8fa5e55ed8f93f874b3b23ed54ec6 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + purls: [] + size: 44978 + timestamp: 1743435053850 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.13.3-ha770c72_1.conda + sha256: 7be9b3dac469fe3c6146ff24398b685804dfc7a1de37607b84abd076f57cc115 + md5: 51f5be229d83ecd401fb369ab96ae669 + depends: + - libfreetype6 >=2.13.3 + license: GPL-2.0-only OR FTL + purls: [] + size: 7693 + timestamp: 1745369988361 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.13.3-hce30654_1.conda + sha256: 1f8c16703fe333cdc2639f7cdaf677ac2120843453222944a7c6c85ec342903c + md5: d06282e08e55b752627a707d58779b8f + depends: + - libfreetype6 >=2.13.3 + license: GPL-2.0-only OR FTL + purls: [] + size: 7813 + timestamp: 1745370144506 +- conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype-2.13.3-h57928b3_1.conda + sha256: e5bc7d0a8d11b7b234da4fcd9d78f297f7dec3fec8bd06108fd3ac7b2722e32e + md5: 410ba2c8e7bdb278dfbb5d40220e39d2 + depends: + - libfreetype6 >=2.13.3 + license: GPL-2.0-only OR FTL + purls: [] + size: 8159 + timestamp: 1745370227235 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.13.3-h48d6fc4_1.conda + sha256: 7759bd5c31efe5fbc36a7a1f8ca5244c2eabdbeb8fc1bee4b99cf989f35c7d81 + md5: 3c255be50a506c50765a93a6644f32fe + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libpng >=1.6.47,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + constrains: + - freetype >=2.13.3 + license: GPL-2.0-only OR FTL + purls: [] + size: 380134 + timestamp: 1745369987697 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.13.3-h1d14073_1.conda + sha256: c278df049b1a071841aa0aca140a338d087ea594e07dcf8a871d2cfe0e330e75 + md5: b163d446c55872ef60530231879908b9 + depends: + - __osx >=11.0 + - libpng >=1.6.47,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + constrains: + - freetype >=2.13.3 + license: GPL-2.0-only OR FTL + purls: [] + size: 333529 + timestamp: 1745370142848 +- conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype6-2.13.3-h0b5ce68_1.conda + sha256: 61308653e7758ff36f80a60d598054168a1389ddfbac46d7864c415fafe18e69 + md5: a84b7d1a13060a9372bea961a8131dbc + depends: + - libpng >=1.6.47,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - freetype >=2.13.3 + license: GPL-2.0-only OR FTL + purls: [] + size: 337007 + timestamp: 1745370226578 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.1.0-h767d61c_3.conda + sha256: 59a87161212abe8acc57d318b0cc8636eb834cdfdfddcf1f588b5493644b39a3 + md5: 9e60c55e725c20d23125a5f0dd69af5d + depends: + - __glibc >=2.17,<3.0.a0 + - _openmp_mutex >=4.5 + constrains: + - libgcc-ng ==15.1.0=*_3 + - libgomp 15.1.0 h767d61c_3 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 824921 + timestamp: 1750808216066 +- conda: https://conda.anaconda.org/conda-forge/win-64/libgcc-15.1.0-h1383e82_3.conda + sha256: 05978c4e8c826dd3b727884e009a19ceee75b0a530c18fc14f0ba56b090f2ea3 + md5: d8314be93c803e2e2b430f6389d6ce6a + depends: + - _openmp_mutex >=4.5 + - libwinpthread >=12.0.0.r4.gg4f2fc60ca + constrains: + - libgomp 15.1.0 h1383e82_3 + - msys2-conda-epoch <0.0a0 + - libgcc-ng ==15.1.0=*_3 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 669602 + timestamp: 1750808309041 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.1.0-h69a702a_3.conda + sha256: b0b0a5ee6ce645a09578fc1cb70c180723346f8a45fdb6d23b3520591c6d6996 + md5: e66f2b8ad787e7beb0f846e4bd7e8493 + depends: + - libgcc 15.1.0 h767d61c_3 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 29033 + timestamp: 1750808224854 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgd-2.3.3-h6f5c62b_11.conda + sha256: 19e5be91445db119152217e8e8eec4fd0499d854acc7d8062044fb55a70971cd + md5: 68fc66282364981589ef36868b1a7c78 + depends: + - __glibc >=2.17,<3.0.a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - freetype >=2.12.1,<3.0a0 + - icu >=75.1,<76.0a0 + - libexpat >=2.6.4,<3.0a0 + - libgcc >=13 + - libjpeg-turbo >=3.0.0,<4.0a0 + - libpng >=1.6.45,<1.7.0a0 + - libtiff >=4.7.0,<4.8.0a0 + - libwebp-base >=1.5.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + license: GD + license_family: BSD + purls: [] + size: 177082 + timestamp: 1737548051015 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgd-2.3.3-hb2c3a21_11.conda + sha256: be038eb8dfe296509aee2df21184c72cb76285b0340448525664bc396aa6146d + md5: 4581aa3cfcd1a90967ed02d4a9f3db4b + depends: + - __osx >=11.0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - freetype >=2.12.1,<3.0a0 + - icu >=75.1,<76.0a0 + - libexpat >=2.6.4,<3.0a0 + - libiconv >=1.17,<2.0a0 + - libjpeg-turbo >=3.0.0,<4.0a0 + - libpng >=1.6.45,<1.7.0a0 + - libtiff >=4.7.0,<4.8.0a0 + - libwebp-base >=1.5.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + license: GD + license_family: BSD + purls: [] + size: 156868 + timestamp: 1737548290283 +- conda: https://conda.anaconda.org/conda-forge/win-64/libgd-2.3.3-h7208af6_11.conda + sha256: 485a30af9e710feeda8d5b537b2db1e32e41f29ef24683bbe7deb6f7fd915825 + md5: 2070a706123b2d5e060b226a00e96488 + depends: + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - freetype >=2.12.1,<3.0a0 + - icu >=75.1,<76.0a0 + - libexpat >=2.6.4,<3.0a0 + - libjpeg-turbo >=3.0.0,<4.0a0 + - libpng >=1.6.45,<1.7.0a0 + - libtiff >=4.7.0,<4.8.0a0 + - libwebp-base >=1.5.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - xorg-libxpm >=3.5.17,<4.0a0 + license: GD + license_family: BSD + purls: [] + size: 165838 + timestamp: 1737548342665 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.1.0-h69a702a_3.conda + sha256: 77dd1f1efd327e6991e87f09c7c97c4ae1cfbe59d9485c41d339d6391ac9c183 + md5: bfbca721fd33188ef923dfe9ba172f29 + depends: + - libgfortran5 15.1.0 hcea5267_3 + constrains: + - libgfortran-ng ==15.1.0=*_3 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 29057 + timestamp: 1750808257258 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-5.0.0-14_2_0_h6c33f7e_103.conda + sha256: 8628746a8ecd311f1c0d14bb4f527c18686251538f7164982ccbe3b772de58b5 + md5: 044a210bc1d5b8367857755665157413 + depends: + - libgfortran5 14.2.0 h6c33f7e_103 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 156291 + timestamp: 1743863532821 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.1.0-hcea5267_3.conda + sha256: eea6c3cf22ad739c279b4d665e6cf20f8081f483b26a96ddd67d4df3c88dfa0a + md5: 530566b68c3b8ce7eec4cd047eae19fe + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=15.1.0 + constrains: + - libgfortran 15.1.0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 1565627 + timestamp: 1750808236464 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-14.2.0-h6c33f7e_103.conda + sha256: 8599453990bd3a449013f5fa3d72302f1c68f0680622d419c3f751ff49f01f17 + md5: 69806c1e957069f1d515830dcc9f6cbb + depends: + - llvm-openmp >=8.0.0 + constrains: + - libgfortran 5.0.0 14_2_0_*_103 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 806566 + timestamp: 1743863491726 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.84.2-h3618099_0.conda + sha256: a6b5cf4d443044bc9a0293dd12ca2015f0ebe5edfdc9c4abdde0b9947f9eb7bd + md5: 072ab14a02164b7c0c089055368ff776 + depends: + - __glibc >=2.17,<3.0.a0 + - libffi >=3.4.6,<3.5.0a0 + - libgcc >=13 + - libiconv >=1.18,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - pcre2 >=10.45,<10.46.0a0 + constrains: + - glib 2.84.2 *_0 + license: LGPL-2.1-or-later + purls: [] + size: 3955066 + timestamp: 1747836671118 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libglib-2.84.2-hbec27ea_0.conda + sha256: 5fcc5e948706cc64e45e2454267f664ed5a1e84f15345aae04a41d852a879c0e + md5: 7bbb8961dca1b4b9f2b01b6e722111a7 + depends: + - __osx >=11.0 + - libffi >=3.4.6,<3.5.0a0 + - libiconv >=1.18,<2.0a0 + - libintl >=0.24.1,<1.0a0 + - libzlib >=1.3.1,<2.0a0 + - pcre2 >=10.45,<10.46.0a0 + constrains: + - glib 2.84.2 *_0 + license: LGPL-2.1-or-later + purls: [] + size: 3666180 + timestamp: 1747837044507 +- conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.84.2-hbc94333_0.conda + sha256: 457e297389609ff6886fef88ae7f1f6ea4f4f3febea7dd690662a50983967d6d + md5: fee05801cc5db97bec20a5e78fb3905b + depends: + - libffi >=3.4.6,<3.5.0a0 + - libiconv >=1.18,<2.0a0 + - libintl >=0.22.5,<1.0a0 + - libzlib >=1.3.1,<2.0a0 + - pcre2 >=10.45,<10.46.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - glib 2.84.2 *_0 + license: LGPL-2.1-or-later + purls: [] + size: 3771466 + timestamp: 1747837394297 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.1.0-h767d61c_3.conda + sha256: 43710ab4de0cd7ff8467abff8d11e7bb0e36569df04ce1c099d48601818f11d1 + md5: 3cd1a7238a0dd3d0860fdefc496cc854 + depends: + - __glibc >=2.17,<3.0.a0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 447068 + timestamp: 1750808138400 +- conda: https://conda.anaconda.org/conda-forge/win-64/libgomp-15.1.0-h1383e82_3.conda + sha256: 2e6e286c817d2274b109c448f63d804dcc85610c5abf97e183440aa2d84b8c72 + md5: 94545e52b3d21a7ab89961f7bda3da0d + depends: + - libwinpthread >=12.0.0.r4.gg4f2fc60ca + constrains: + - msys2-conda-epoch <0.0a0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 535456 + timestamp: 1750808243424 +- conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.11.2-default_ha69328c_1001.conda + sha256: 850e255997f538d5fb6ed651321141155a33bb781d43d326fc4ff62114dd2842 + md5: b87a0ac5ab6495d8225db5dc72dd21cd + depends: + - libwinpthread >=12.0.0.r4.gg4f2fc60ca + - libxml2 >=2.13.4,<2.14.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 2390021 + timestamp: 1731375651179 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h4ce23a2_1.conda + sha256: 18a4afe14f731bfb9cf388659994263904d20111e42f841e9eea1bb6f91f4ab4 + md5: e796ff8ddc598affdf7c173d6145f087 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: LGPL-2.1-only + purls: [] + size: 713084 + timestamp: 1740128065462 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-hfe07756_1.conda + sha256: d30780d24bf3a30b4f116fca74dedb4199b34d500fe6c52cced5f8cc1e926f03 + md5: 450e6bdc0c7d986acf7b8443dce87111 + depends: + - __osx >=11.0 + license: LGPL-2.1-only + purls: [] + size: 681804 + timestamp: 1740128227484 +- conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-h135ad9c_1.conda + sha256: ea5ed2b362b6dbc4ba7188eb4eaf576146e3dfc6f4395e9f0db76ad77465f786 + md5: 21fc5dba2cbcd8e5e26ff976a312122c + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: LGPL-2.1-only + purls: [] + size: 638142 + timestamp: 1740128665984 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libintl-0.25.1-h493aca8_0.conda + sha256: 99d2cebcd8f84961b86784451b010f5f0a795ed1c08f1e7c76fbb3c22abf021a + md5: 5103f6a6b210a3912faf8d7db516918c + depends: + - __osx >=11.0 + - libiconv >=1.18,<2.0a0 + license: LGPL-2.1-or-later + purls: [] + size: 90957 + timestamp: 1751558394144 +- conda: https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda + sha256: c7e4600f28bcada8ea81456a6530c2329312519efcf0c886030ada38976b0511 + md5: 2cf0cf76cc15d360dfa2f17fd6cf9772 + depends: + - libiconv >=1.17,<2.0a0 + license: LGPL-2.1-or-later + purls: [] + size: 95568 + timestamp: 1723629479451 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.0-hb9d3cd8_0.conda + sha256: 98b399287e27768bf79d48faba8a99a2289748c65cd342ca21033fab1860d4a4 + md5: 9fa334557db9f63da6c9285fd2a48638 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - jpeg <0.0.0a + license: IJG AND BSD-3-Clause AND Zlib + purls: [] + size: 628947 + timestamp: 1745268527144 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.1.0-h5505292_0.conda + sha256: 78df2574fa6aa5b6f5fc367c03192f8ddf8e27dc23641468d54e031ff560b9d4 + md5: 01caa4fbcaf0e6b08b3aef1151e91745 + depends: + - __osx >=11.0 + constrains: + - jpeg <0.0.0a + license: IJG AND BSD-3-Clause AND Zlib + purls: [] + size: 553624 + timestamp: 1745268405713 +- conda: https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.1.0-h2466b09_0.conda + sha256: e61b0adef3028b51251124e43eb6edf724c67c0f6736f1628b02511480ac354e + md5: 7c51d27540389de84852daa1cdb9c63c + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - jpeg <0.0.0a + license: IJG AND BSD-3-Clause AND Zlib + purls: [] + size: 838154 + timestamp: 1745268437136 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-32_h7ac8fdf_openblas.conda + build_number: 32 + sha256: 5b55a30ed1b3f8195dad9020fe1c6d0f514829bfaaf0cf5e393e93682af009f2 + md5: 6c3f04ccb6c578138e9f9899da0bd714 + depends: + - libblas 3.9.0 32_h59b9bed_openblas + constrains: + - libcblas 3.9.0 32*_openblas + - blas 2.132 openblas + - liblapacke 3.9.0 32*_openblas + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 17316 + timestamp: 1750388820745 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.9.0-32_hc9a63f6_openblas.conda + build_number: 32 + sha256: 5e1cfa3581d1dec6b07a75084ff6cfa4b4465c646c6884a71c78a28543f83b61 + md5: bf9ead3fa92fd75ad473c6a1d255ffcb + depends: + - libblas 3.9.0 32_h10e41b3_openblas + constrains: + - blas 2.132 openblas + - libcblas 3.9.0 32*_openblas + - liblapacke 3.9.0 32*_openblas + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 17507 + timestamp: 1750388977861 +- conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.9.0-32_h1aa476e_mkl.conda + build_number: 32 + sha256: 5629e592137114b24bfdea71e1c4b6bee11379631409ed91dfe2f83b32a8b298 + md5: 1652285573db93afc3ba9b3b9356e3d3 + depends: + - libblas 3.9.0 32_h641d27c_mkl + constrains: + - libcblas 3.9.0 32*_mkl + - liblapacke 3.9.0 32*_mkl + - blas 2.132 mkl + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 3735534 + timestamp: 1750389164366 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblapacke-3.9.0-32_he2f377e_openblas.conda + build_number: 32 + sha256: 48e1da503af1b8cfc48c1403c1ea09a5570ce194077adad3d46f15ea95ef4253 + md5: 54e7f7896d0dbf56665bcb0078bfa9d2 + depends: + - libblas 3.9.0 32_h59b9bed_openblas + - libcblas 3.9.0 32_he106b2a_openblas + - liblapack 3.9.0 32_h7ac8fdf_openblas + constrains: + - blas 2.132 openblas + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 17316 + timestamp: 1750388832284 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapacke-3.9.0-32_hbb7bcf8_openblas.conda + build_number: 32 + sha256: 72579b41e83c546f775543364b7a69dcd9922af6aa38b3f0ab06b9deab2db55c + md5: 2cf62381fc88b745e4f942677db6bc74 + depends: + - libblas 3.9.0 32_h10e41b3_openblas + - libcblas 3.9.0 32_hb3479ef_openblas + - liblapack 3.9.0 32_hc9a63f6_openblas + constrains: + - blas 2.132 openblas + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 17549 + timestamp: 1750388985274 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.1-hb9d3cd8_2.conda + sha256: f2591c0069447bbe28d4d696b7fcb0c5bd0b4ac582769b89addbcf26fb3430d8 + md5: 1a580f7796c7bf6393fddb8bbbde58dc + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - xz 5.8.1.* + license: 0BSD + purls: [] + size: 112894 + timestamp: 1749230047870 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.1-h39f12f2_2.conda + sha256: 0cb92a9e026e7bd4842f410a5c5c665c89b2eb97794ffddba519a626b8ce7285 + md5: d6df911d4564d77c4374b02552cb17d1 + depends: + - __osx >=11.0 + constrains: + - xz 5.8.1.* + license: 0BSD + purls: [] + size: 92286 + timestamp: 1749230283517 +- conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.1-h2466b09_2.conda + sha256: 55764956eb9179b98de7cc0e55696f2eff8f7b83fc3ebff5e696ca358bca28cc + md5: c15148b2e18da456f5108ccb5e411446 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - xz 5.8.1.* + license: 0BSD + purls: [] + size: 104935 + timestamp: 1749230611612 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libmamba-2.3.0-h44402ff_1.conda + sha256: 817bee7c075ee99d73af95756f11a3ace3b84dc9994dadf09140b99f84bf19a7 + md5: 6826be8fbb2b2105ef738dfb5d8db373 + depends: + - nlohmann_json >=3.11.3,<3.11.4.0a0 + - cpp-expected >=1.1.0,<1.1.1.0a0 + - libstdcxx >=13 + - libgcc >=13 + - __glibc >=2.17,<3.0.a0 + - reproc-cpp >=14.2,<15.0a0 + - simdjson >=3.13.0,<3.14.0a0 + - libsolv >=0.7.33,<0.8.0a0 + - zstd >=1.5.7,<1.6.0a0 + - fmt >=11.1.4,<11.2.0a0 + - yaml-cpp >=0.8.0,<0.9.0a0 + - libcurl >=8.14.1,<9.0a0 + - openssl >=3.5.0,<4.0a0 + - libarchive >=3.8.1,<3.9.0a0 + - reproc >=14.2,<15.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 2383213 + timestamp: 1750078835684 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmamba-2.3.0-h8ac2bdb_1.conda + sha256: 6f191e961b9928f282eb3cade1c775cb9ccfb1623094f772f6864cd00f51c262 + md5: 053a5adc5dd7fba8f3843c6f73139ffb + depends: + - nlohmann_json >=3.11.3,<3.11.4.0a0 + - cpp-expected >=1.1.0,<1.1.1.0a0 + - __osx >=11.0 + - libcxx >=18 + - libsolv >=0.7.33,<0.8.0a0 + - yaml-cpp >=0.8.0,<0.9.0a0 + - zstd >=1.5.7,<1.6.0a0 + - libarchive >=3.8.1,<3.9.0a0 + - openssl >=3.5.0,<4.0a0 + - reproc >=14.2,<15.0a0 + - fmt >=11.1.4,<11.2.0a0 + - libcurl >=8.14.1,<9.0a0 + - simdjson >=3.13.0,<3.14.0a0 + - reproc-cpp >=14.2,<15.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 1630904 + timestamp: 1750078769908 +- conda: https://conda.anaconda.org/conda-forge/win-64/libmamba-2.3.0-hd0d0357_1.conda + sha256: 0bfbb8a141a0d8ecd218ba4b0da47e67c78ec681a524800885df276b9022eb7e + md5: 98568aa9b4b0dfca446fcac1beac7c1d + depends: + - nlohmann_json >=3.11.3,<3.11.4.0a0 + - cpp-expected >=1.1.0,<1.1.1.0a0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - ucrt >=10.0.20348.0 + - openssl >=3.5.0,<4.0a0 + - libsolv >=0.7.33,<0.8.0a0 + - simdjson >=3.13.0,<3.14.0a0 + - libcurl >=8.14.1,<9.0a0 + - libarchive >=3.8.1,<3.9.0a0 + - reproc-cpp >=14.2,<15.0a0 + - yaml-cpp >=0.8.0,<0.9.0a0 + - zstd >=1.5.7,<1.6.0a0 + - reproc >=14.2,<15.0a0 + - fmt >=11.1.4,<11.2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 5075129 + timestamp: 1750078824665 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libmambapy-2.3.0-py312hd15d01f_1.conda + sha256: be675a337f465c8dc9ce8592eac55b331d5d14f66ed79e65dc8490f2f3a01247 + md5: 4622266e83387a3cdd0459cdd3e92771 + depends: + - python + - libmamba ==2.3.0 h44402ff_1 + - libstdcxx >=13 + - libgcc >=13 + - __glibc >=2.17,<3.0.a0 + - zstd >=1.5.7,<1.6.0a0 + - pybind11-abi ==4 + - yaml-cpp >=0.8.0,<0.9.0a0 + - libmamba >=2.3.0,<2.4.0a0 + - openssl >=3.5.0,<4.0a0 + - python_abi 3.12.* *_cp312 + - fmt >=11.1.4,<11.2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/libmambapy?source=hash-mapping + size: 732348 + timestamp: 1750078835684 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmambapy-2.3.0-py312h3097733_1.conda + sha256: 62ac6825d49d575f4709920b1510091e942afcb3c861f3cfdd094494859d14b9 + md5: 64b888437cfe18b9696badf34c94a431 + depends: + - python + - libmamba ==2.3.0 h8ac2bdb_1 + - python 3.12.* *_cpython + - libcxx >=18 + - __osx >=11.0 + - yaml-cpp >=0.8.0,<0.9.0a0 + - pybind11-abi ==4 + - zstd >=1.5.7,<1.6.0a0 + - openssl >=3.5.0,<4.0a0 + - fmt >=11.1.4,<11.2.0a0 + - libmamba >=2.3.0,<2.4.0a0 + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/libmambapy?source=hash-mapping + size: 639951 + timestamp: 1750078769909 +- conda: https://conda.anaconda.org/conda-forge/win-64/libmambapy-2.3.0-py312h1fc3bf7_1.conda + sha256: d1ea684f1e8998fc167240204f5b0fcdc2cf019131229b816ff9984b6e068a07 + md5: fab0925a05aafa16d490f146279875e0 + depends: + - python + - libmamba ==2.3.0 hd0d0357_1 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - ucrt >=10.0.20348.0 + - pybind11-abi ==4 + - zstd >=1.5.7,<1.6.0a0 + - yaml-cpp >=0.8.0,<0.9.0a0 + - python_abi 3.12.* *_cp312 + - libmamba >=2.3.0,<2.4.0a0 + - fmt >=11.1.4,<11.2.0a0 + - openssl >=3.5.0,<4.0a0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/libmambapy?source=hash-mapping + size: 493013 + timestamp: 1750078824667 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.64.0-h161d5f1_0.conda + sha256: b0f2b3695b13a989f75d8fd7f4778e1c7aabe3b36db83f0fe80b2cd812c0e975 + md5: 19e57602824042dfd0446292ef90488b + depends: + - __glibc >=2.17,<3.0.a0 + - c-ares >=1.32.3,<2.0a0 + - libev >=4.33,<4.34.0a0 + - libev >=4.33,<5.0a0 + - libgcc >=13 + - libstdcxx >=13 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.3.2,<4.0a0 + license: MIT + license_family: MIT + purls: [] + size: 647599 + timestamp: 1729571887612 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.64.0-h6d7220d_0.conda + sha256: 00cc685824f39f51be5233b54e19f45abd60de5d8847f1a56906f8936648b72f + md5: 3408c02539cee5f1141f9f11450b6a51 + depends: + - __osx >=11.0 + - c-ares >=1.34.2,<2.0a0 + - libcxx >=17 + - libev >=4.33,<4.34.0a0 + - libev >=4.33,<5.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.3.2,<4.0a0 + license: MIT + license_family: MIT + purls: [] + size: 566719 + timestamp: 1729572385640 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda + sha256: 927fe72b054277cde6cb82597d0fcf6baf127dcbce2e0a9d8925a68f1265eef5 + md5: d864d34357c3b65a4b731f78c0801dc4 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: LGPL-2.1-only + license_family: GPL + purls: [] + size: 33731 + timestamp: 1750274110928 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.30-pthreads_h94d23a6_0.conda + sha256: 225f4cfdb06b3b73f870ad86f00f49a9ca0a8a2d2afe59440521fafe2b6c23d9 + md5: 323dc8f259224d13078aaf7ce96c3efe + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libgfortran + - libgfortran5 >=14.3.0 + constrains: + - openblas >=0.3.30,<0.3.31.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 5916819 + timestamp: 1750379877844 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.30-openmp_hf332438_0.conda + sha256: 501c8c64f1a6e6b671e49835e6c483bc25f0e7147f3eb4bbb19a4c3673dcaf28 + md5: 5d7dbaa423b4c253c476c24784286e4b + depends: + - __osx >=11.0 + - libgfortran 5.* + - libgfortran5 >=13.3.0 + - llvm-openmp >=18.1.8 + constrains: + - openblas >=0.3.30,<0.3.31.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 4163399 + timestamp: 1750378829050 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.50-h943b412_0.conda + sha256: c7b212bdd3f9d5450c4bae565ccb9385222bf9bb92458c2a23be36ff1b981389 + md5: 51de14db340a848869e69c632b43cca7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libzlib >=1.3.1,<2.0a0 + license: zlib-acknowledgement + purls: [] + size: 289215 + timestamp: 1751559366724 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.50-h3783ad8_0.conda + sha256: 38d89e4ceae81f24a11129d2f5e8d10acfc12f057b7b4fd5af9043604a689941 + md5: f39e4bd5424259d8dfcbdbf0e068558e + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: zlib-acknowledgement + purls: [] + size: 260895 + timestamp: 1751559636317 +- conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.50-h95bef1e_0.conda + sha256: 17f3bfb6d852eec200f68a4cfb4ef1d8950b73dfa48931408e3dbdfc89a4848a + md5: 2e63db2e13cd6a5e2c08f771253fb8a0 + depends: + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: zlib-acknowledgement + purls: [] + size: 352422 + timestamp: 1751559786122 +- conda: https://conda.anaconda.org/conda-forge/linux-64/librsvg-2.58.4-he92a37e_3.conda + sha256: a45ef03e6e700cc6ac6c375e27904531cf8ade27eb3857e080537ff283fb0507 + md5: d27665b20bc4d074b86e628b3ba5ab8b + depends: + - __glibc >=2.17,<3.0.a0 + - cairo >=1.18.4,<2.0a0 + - freetype >=2.13.3,<3.0a0 + - gdk-pixbuf >=2.42.12,<3.0a0 + - harfbuzz >=11.0.0,<12.0a0 + - libgcc >=13 + - libglib >=2.84.0,<3.0a0 + - libpng >=1.6.47,<1.7.0a0 + - libxml2 >=2.13.7,<2.14.0a0 + - pango >=1.56.3,<2.0a0 + constrains: + - __glibc >=2.17 + license: LGPL-2.1-or-later + purls: [] + size: 6543651 + timestamp: 1743368725313 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/librsvg-2.58.4-h266df6f_3.conda + sha256: 0ec066d7f22bcd9acb6ca48b2e6a15e9be4f94e67cb55b0a2c05a37ac13f9315 + md5: 95d6ad8fb7a2542679c08ce52fafbb6c + depends: + - __osx >=11.0 + - cairo >=1.18.4,<2.0a0 + - gdk-pixbuf >=2.42.12,<3.0a0 + - libglib >=2.84.0,<3.0a0 + - libxml2 >=2.13.7,<2.14.0a0 + - pango >=1.56.3,<2.0a0 + constrains: + - __osx >=11.0 + license: LGPL-2.1-or-later + purls: [] + size: 4607782 + timestamp: 1743369546790 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsodium-1.0.20-h4ab18f5_0.conda + sha256: 0105bd108f19ea8e6a78d2d994a6d4a8db16d19a41212070d2d1d48a63c34161 + md5: a587892d3c13b6621a6091be690dbca2 + depends: + - libgcc-ng >=12 + license: ISC + purls: [] + size: 205978 + timestamp: 1716828628198 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsodium-1.0.20-h99b78c6_0.conda + sha256: fade8223e1e1004367d7101dd17261003b60aa576df6d7802191f8972f7470b1 + md5: a7ce36e284c5faaf93c220dfc39e3abd + depends: + - __osx >=11.0 + license: ISC + purls: [] + size: 164972 + timestamp: 1716828607917 +- conda: https://conda.anaconda.org/conda-forge/win-64/libsodium-1.0.20-hc70643c_0.conda + sha256: 7bcb3edccea30f711b6be9601e083ecf4f435b9407d70fc48fbcf9e5d69a0fc6 + md5: 198bb594f202b205c7d18b936fa4524f + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: ISC + purls: [] + size: 202344 + timestamp: 1716828757533 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsolv-0.7.33-h7955e40_0.conda + sha256: 12b7b97f5fa7f325683cb8b34a6c4069612a7e3ce270dcd6b449e4e75e079b55 + md5: 9400594fb2639595bb20a7e723d347f0 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 477523 + timestamp: 1749043837490 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsolv-0.7.33-h13dfb9a_0.conda + sha256: 91e1366d6cb35ac342336666f71270b8002dbeb8be804d0334e16d7eaa123e68 + md5: fa67b3f6f6e6488922629a1be1a740e8 + depends: + - __osx >=11.0 + - libcxx >=18 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 389542 + timestamp: 1749043912078 +- conda: https://conda.anaconda.org/conda-forge/win-64/libsolv-0.7.33-hbb528cf_0.conda + sha256: a32dfa34e224d9b36ec004ab70d23d46fc8e73cd2f52b07fd31befce50241975 + md5: 38e0ea7d7be743ca8a1a48607414cd59 + depends: + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 432339 + timestamp: 1749044229927 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.50.2-hee844dc_2.conda + sha256: 62040da9b55f409cd43697eb7391381ffede90b2ea53634a94876c6c867dcd73 + md5: be96b9fdd7b579159df77ece9bb80e48 + depends: + - __glibc >=2.17,<3.0.a0 + - icu >=75.1,<76.0a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + license: Unlicense + purls: [] + size: 935828 + timestamp: 1752072043 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.50.2-hf8de324_2.conda + sha256: 02c292e5fb95f8ce408a3c98a846787095639217bd199a264b149dfe08a2ccb3 + md5: e0fe6df79600e1db7405ccf29c61d784 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: Unlicense + purls: [] + size: 899248 + timestamp: 1752072259470 +- conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.50.2-hf5d6505_2.conda + sha256: f12cdfe29c248d6a1c7d11b6fe1a3e0d0563206deb422ddb1b84b909818168d4 + md5: 58f810279ac6caec2d996a56236c3254 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Unlicense + purls: [] + size: 1288312 + timestamp: 1752072137328 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda + sha256: fa39bfd69228a13e553bd24601332b7cfeb30ca11a3ca50bb028108fe90a7661 + md5: eecce068c7e4eddeb169591baac20ac4 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.0,<4.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 304790 + timestamp: 1745608545575 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-h1590b86_0.conda + sha256: 8bfe837221390ffc6f111ecca24fa12d4a6325da0c8d131333d63d6c37f27e0a + md5: b68e8f66b94b44aaa8de4583d3d4cc40 + depends: + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.0,<4.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 279193 + timestamp: 1745608793272 +- conda: https://conda.anaconda.org/conda-forge/win-64/libssh2-1.11.1-h9aa295b_0.conda + sha256: cbdf93898f2e27cefca5f3fe46519335d1fab25c4ea2a11b11502ff63e602c09 + md5: 9dce2f112bfd3400f4f432b3d0ac07b2 + depends: + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.0,<4.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 292785 + timestamp: 1745608759342 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.1.0-h8f9b012_3.conda + sha256: 7650837344b7850b62fdba02155da0b159cf472b9ab59eb7b472f7bd01dff241 + md5: 6d11a5edae89fe413c0569f16d308f5a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc 15.1.0 h767d61c_3 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 3896407 + timestamp: 1750808251302 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.1.0-h4852527_3.conda + sha256: bbaea1ecf973a7836f92b8ebecc94d3c758414f4de39d2cc6818a3d10cb3216b + md5: 57541755b5a51691955012b8e197c06c + depends: + - libstdcxx 15.1.0 h8f9b012_3 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 29093 + timestamp: 1750808292700 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.0-hf01ce69_5.conda + sha256: 7fa6ddac72e0d803bb08e55090a8f2e71769f1eb7adbd5711bdd7789561601b1 + md5: e79a094918988bb1807462cd42c83962 + depends: + - __glibc >=2.17,<3.0.a0 + - lerc >=4.0.0,<5.0a0 + - libdeflate >=1.24,<1.25.0a0 + - libgcc >=13 + - libjpeg-turbo >=3.1.0,<4.0a0 + - liblzma >=5.8.1,<6.0a0 + - libstdcxx >=13 + - libwebp-base >=1.5.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: HPND + purls: [] + size: 429575 + timestamp: 1747067001268 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.0-h2f21f7c_5.conda + sha256: cc5ee1cffb8a8afb25a4bfd08fce97c5447f97aa7064a055cb4a617df45bc848 + md5: 4eb183bbf7f734f69875702fdbe17ea0 + depends: + - __osx >=11.0 + - lerc >=4.0.0,<5.0a0 + - libcxx >=18 + - libdeflate >=1.24,<1.25.0a0 + - libjpeg-turbo >=3.1.0,<4.0a0 + - liblzma >=5.8.1,<6.0a0 + - libwebp-base >=1.5.0,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - zstd >=1.5.7,<1.6.0a0 + license: HPND + purls: [] + size: 370943 + timestamp: 1747067160710 +- conda: https://conda.anaconda.org/conda-forge/win-64/libtiff-4.7.0-h05922d8_5.conda + sha256: 1bb0b2e7d076fecc2f8147336bc22e7e6f9a4e0505e0e4ab2be1f56023a4a458 + md5: 75370aba951b47ec3b5bfe689f1bcf7f + depends: + - lerc >=4.0.0,<5.0a0 + - libdeflate >=1.24,<1.25.0a0 + - libjpeg-turbo >=3.1.0,<4.0a0 + - liblzma >=5.8.1,<6.0a0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - zstd >=1.5.7,<1.6.0a0 + license: HPND + purls: [] + size: 979074 + timestamp: 1747067408877 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.38.1-h0b41bf4_0.conda + sha256: 787eb542f055a2b3de553614b25f09eefb0a0931b0c87dbcce6efdfd92f04f18 + md5: 40b61aab5c7ba9ff276c41cfffe6b80b + depends: + - libgcc-ng >=12 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 33601 + timestamp: 1680112270483 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda + sha256: 3aed21ab28eddffdaf7f804f49be7a7d701e8f0e46c856d801270b470820a37b + md5: aea31d2e5b1091feca96fcfe945c3cf9 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - libwebp 1.6.0 + license: BSD-3-Clause + purls: [] + size: 429011 + timestamp: 1752159441324 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda + sha256: a4de3f371bb7ada325e1f27a4ef7bcc81b2b6a330e46fac9c2f78ac0755ea3dd + md5: e5e7d467f80da752be17796b87fe6385 + depends: + - __osx >=11.0 + constrains: + - libwebp 1.6.0 + license: BSD-3-Clause + purls: [] + size: 294974 + timestamp: 1752159906788 +- conda: https://conda.anaconda.org/conda-forge/win-64/libwebp-base-1.6.0-h4d5522a_0.conda + sha256: 7b6316abfea1007e100922760e9b8c820d6fc19df3f42fb5aca684cfacb31843 + md5: f9bbae5e2537e3b06e0f7310ba76c893 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - libwebp 1.6.0 + license: BSD-3-Clause + purls: [] + size: 279176 + timestamp: 1752159543911 +- conda: https://conda.anaconda.org/conda-forge/win-64/libwinpthread-12.0.0.r4.gg4f2fc60ca-h57928b3_9.conda + sha256: 373f2973b8a358528b22be5e8d84322c165b4c5577d24d94fd67ad1bb0a0f261 + md5: 08bfa5da6e242025304b206d152479ef + depends: + - ucrt + constrains: + - pthreads-win32 <0.0a0 + - msys2-conda-epoch <0.0a0 + license: MIT AND BSD-3-Clause-Clear + purls: [] + size: 35794 + timestamp: 1737099561703 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda + sha256: 666c0c431b23c6cec6e492840b176dde533d48b7e6fb8883f5071223433776aa + md5: 92ed62436b625154323d40d5f2f11dd7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - pthread-stubs + - xorg-libxau >=1.0.11,<2.0a0 + - xorg-libxdmcp + license: MIT + license_family: MIT + purls: [] + size: 395888 + timestamp: 1727278577118 +- conda: https://conda.anaconda.org/conda-forge/win-64/libxcb-1.17.0-h0e4246c_0.conda + sha256: 08dec73df0e161c96765468847298a420933a36bc4f09b50e062df8793290737 + md5: a69bbf778a462da324489976c84cfc8c + depends: + - libgcc >=13 + - libwinpthread >=12.0.0.r4.gg4f2fc60ca + - pthread-stubs + - ucrt >=10.0.20348.0 + - xorg-libxau >=1.0.11,<2.0a0 + - xorg-libxdmcp + license: MIT + license_family: MIT + purls: [] + size: 1208687 + timestamp: 1727279378819 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + sha256: 6ae68e0b86423ef188196fff6207ed0c8195dd84273cb5623b85aa08033a410c + md5: 5aa797f8787fe7a17d1b0821485b5adc + depends: + - libgcc-ng >=12 + license: LGPL-2.1-or-later + purls: [] + size: 100393 + timestamp: 1702724383534 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.10.0-h65c71a3_0.conda + sha256: a8043a46157511b3ceb6573a99952b5c0232313283f2d6a066cec7c8dcaed7d0 + md5: fedf6bfe5d21d21d2b1785ec00a8889a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + - libxcb >=1.17.0,<2.0a0 + - libxml2 >=2.13.8,<2.14.0a0 + - xkeyboard-config + - xorg-libxau >=1.0.12,<2.0a0 + license: MIT/X11 Derivative + license_family: MIT + purls: [] + size: 707156 + timestamp: 1747911059945 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.8-h4bc477f_0.conda + sha256: b0b3a96791fa8bb4ec030295e8c8bf2d3278f33c0f9ad540e73b5e538e6268e7 + md5: 14dbe05b929e329dbaa6f2d0aa19466d + depends: + - __glibc >=2.17,<3.0.a0 + - icu >=75.1,<76.0a0 + - libgcc >=13 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libzlib >=1.3.1,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 690864 + timestamp: 1746634244154 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.13.8-h52572c6_0.conda + sha256: 13eb825eddce93761d965da3edaf3a42d868c61ece7d9cf21f7e2a13087c2abe + md5: d7884c7af8af5a729353374c189aede8 + depends: + - __osx >=11.0 + - icu >=75.1,<76.0a0 + - libiconv >=1.18,<2.0a0 + - liblzma >=5.8.1,<6.0a0 + - libzlib >=1.3.1,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 583068 + timestamp: 1746634531197 +- conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.13.8-h442d1da_0.conda + sha256: 473b8a53c8df714d676ab41711551c8d250f8d799f2db5cb7cb2b177a0ce13f6 + md5: 833c2dbc1a5020007b520b044c713ed3 + depends: + - libiconv >=1.18,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + purls: [] + size: 1513627 + timestamp: 1746634633560 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + sha256: d4bfe88d7cb447768e31650f06257995601f89076080e76df55e3112d4e47dc4 + md5: edb0dca6bc32e4f4789199455a1dbeb8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 60963 + timestamp: 1727963148474 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + sha256: ce34669eadaba351cd54910743e6a2261b67009624dbc7daeeafdef93616711b + md5: 369964e85dc26bfe78f41399b366c435 + depends: + - __osx >=11.0 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 46438 + timestamp: 1727963202283 +- conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda + sha256: ba945c6493449bed0e6e29883c4943817f7c79cbff52b83360f7b341277c6402 + md5: 41fbfac52c601159df6c01f875de31b9 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 55476 + timestamp: 1727963768015 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-20.1.7-hdb05f8b_0.conda + sha256: e7d95b50a90cdc9e0fc38bc37f493a61b9d08164114b562bbd9ff0034f45eca2 + md5: 741e1da0a0798d32e13e3724f2ca2dcf + depends: + - __osx >=11.0 + constrains: + - openmp 20.1.7|20.1.7.* + license: Apache-2.0 WITH LLVM-exception + license_family: APACHE + purls: [] + size: 281996 + timestamp: 1749892286735 +- conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda + sha256: 47326f811392a5fd3055f0f773036c392d26fdb32e4d8e7a8197eed951489346 + md5: 9de5350a85c4a20c685259b889aa6393 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 167055 + timestamp: 1733741040117 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-c-1.10.0-h286801f_1.conda + sha256: 94d3e2a485dab8bdfdd4837880bde3dd0d701e2b97d6134b8806b7c8e69c8652 + md5: 01511afc6cc1909c5303cf31be17b44f + depends: + - __osx >=11.0 + - libcxx >=18 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 148824 + timestamp: 1733741047892 +- conda: https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.10.0-h2466b09_1.conda + sha256: 632cf3bdaf7a7aeb846de310b6044d90917728c73c77f138f08aa9438fc4d6b5 + md5: 0b69331897a92fac3d8923549d48d092 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 139891 + timestamp: 1733741168264 +- conda: https://conda.anaconda.org/conda-forge/linux-64/lzo-2.10-hd590300_1001.conda + sha256: 88433b98a9dd9da315400e7fb9cd5f70804cb17dca8b1c85163a64f90f584126 + md5: ec7398d21e2651e0dcb0044d03b9a339 + depends: + - libgcc-ng >=12 + license: GPL-2.0-or-later + license_family: GPL2 + purls: [] + size: 171416 + timestamp: 1713515738503 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/lzo-2.10-h93a5062_1001.conda + sha256: b68160b0a8ec374cea12de7afb954ca47419cdc300358232e19cec666d60b929 + md5: 915996063a7380c652f83609e970c2a7 + license: GPL-2.0-or-later + license_family: GPL2 + purls: [] + size: 131447 + timestamp: 1713516009610 +- conda: https://conda.anaconda.org/conda-forge/win-64/lzo-2.10-hcfcfb64_1001.conda + sha256: 39e176b8cc8fe878d87594fae0504c649d1c2c6d5476dd7238237d19eb825751 + md5: 629f4f4e874cf096eb93a23240910cee + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: GPL-2.0-or-later + license_family: GPL2 + purls: [] + size: 142771 + timestamp: 1713516312465 +- conda: https://conda.anaconda.org/conda-forge/noarch/markdown-3.8.2-pyhd8ed1ab_0.conda + sha256: d495279d947e01300bfbc124859151be4eec3a088c1afe173323fd3aa89423b2 + md5: b0404922d0459f188768d1e613ed8a87 + depends: + - importlib-metadata >=4.4 + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/markdown?source=hash-mapping + size: 80353 + timestamp: 1750360406187 +- conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py312h178313f_1.conda + sha256: 4a6bf68d2a2b669fecc9a4a009abd1cf8e72c2289522ff00d81b5a6e51ae78f5 + md5: eb227c3e0bf58f5bd69c0532b157975b + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + constrains: + - jinja2 >=3.0.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/markupsafe?source=hash-mapping + size: 24604 + timestamp: 1733219911494 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.2-py312h998013c_1.conda + sha256: 4aa997b244014d3707eeef54ab0ee497d12c0d0d184018960cce096169758283 + md5: 46e547061080fddf9cf95a0327e8aba6 + depends: + - __osx >=11.0 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + constrains: + - jinja2 >=3.0.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/markupsafe?source=hash-mapping + size: 24048 + timestamp: 1733219945697 +- conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.2-py312h31fea79_1.conda + sha256: bbb9595fe72231a8fbc8909cfa479af93741ecd2d28dfe37f8f205fef5df2217 + md5: 944fdd848abfbd6929e57c790b8174dd + depends: + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - jinja2 >=3.0.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/markupsafe?source=hash-mapping + size: 27582 + timestamp: 1733220007802 +- conda: https://conda.anaconda.org/conda-forge/noarch/matplotlib-inline-0.1.7-pyhd8ed1ab_1.conda + sha256: 69b7dc7131703d3d60da9b0faa6dd8acbf6f6c396224cf6aef3e855b8c0c41c6 + md5: af6ab708897df59bd6e7283ceab1b56b + depends: + - python >=3.9 + - traitlets + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/matplotlib-inline?source=hash-mapping + size: 14467 + timestamp: 1733417051523 +- conda: https://conda.anaconda.org/conda-forge/linux-64/menuinst-2.3.0-py312h7900ff3_0.conda + sha256: 45753c04e947020cacda4fb92fbc927a86a3c5cd403760f8329be508e3c0411b + md5: 3de345c0744dae205b36d53b1671d210 + depends: + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause AND MIT + purls: + - pkg:pypi/menuinst?source=hash-mapping + size: 172740 + timestamp: 1750792417618 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/menuinst-2.3.0-py312h81bd7bf_0.conda + sha256: 80edc0456ead340ca16707752e9dd143c534d9cf2daef914fcc44f3a94b484dc + md5: 80cb651e023d36daccc33c3ead359340 + depends: + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause AND MIT + purls: + - pkg:pypi/menuinst?source=hash-mapping + size: 174339 + timestamp: 1750792742701 +- conda: https://conda.anaconda.org/conda-forge/win-64/menuinst-2.3.0-py312hbb81ca0_0.conda + sha256: 21d9d7bb45c061400120f2ccb5677c028d253ada106a997c709f954042c32a27 + md5: 51f0b4068d6189d90a94156b546a5a7e + depends: + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause AND MIT + purls: + - pkg:pypi/menuinst?source=hash-mapping + size: 140384 + timestamp: 1750792754303 +- conda: https://conda.anaconda.org/conda-forge/noarch/mergedeep-1.3.4-pyhd8ed1ab_1.conda + sha256: e5b555fd638334a253d83df14e3c913ef8ce10100090e17fd6fb8e752d36f95d + md5: d9a8fc1f01deae61735c88ec242e855c + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/mergedeep?source=hash-mapping + size: 11676 + timestamp: 1734157119152 +- conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-1.6.1-pyhd8ed1ab_1.conda + sha256: 902d2e251f9a7ffa7d86a3e62be5b2395e28614bd4dbe5f50acf921fd64a8c35 + md5: 14661160be39d78f2b210f2cc2766059 + depends: + - click >=7.0 + - colorama >=0.4 + - ghp-import >=1.0 + - importlib-metadata >=4.4 + - jinja2 >=2.11.1 + - markdown >=3.3.6 + - markupsafe >=2.0.1 + - mergedeep >=1.3.4 + - mkdocs-get-deps >=0.2.0 + - packaging >=20.5 + - pathspec >=0.11.1 + - python >=3.9 + - pyyaml >=5.1 + - pyyaml-env-tag >=0.1 + - watchdog >=2.0 + constrains: + - babel >=2.9.0 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/mkdocs?source=hash-mapping + size: 3524754 + timestamp: 1734344673481 +- conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-get-deps-0.2.0-pyhd8ed1ab_1.conda + sha256: e0b501b96f7e393757fb2a61d042015966f6c5e9ac825925e43f9a6eafa907b6 + md5: 84382acddb26c27c70f2de8d4c830830 + depends: + - importlib-metadata >=4.3 + - mergedeep >=1.3.4 + - platformdirs >=2.2.0 + - python >=3.9 + - pyyaml >=5.1 + license: MIT + license_family: MIT + purls: + - pkg:pypi/mkdocs-get-deps?source=hash-mapping + size: 14757 + timestamp: 1734353035244 +- conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-9.6.15-pyhd8ed1ab_0.conda + sha256: 0959b4a959e7b23970889a03c6ebd7daee10a3839f80250e958c61c86fcd53eb + md5: de72813ba0ea94ad6f5ab27c89cbc271 + depends: + - babel >=2.10,<3.dev0 + - backrefs >=5.7.post1,<6.dev0 + - colorama >=0.4,<1.dev0 + - jinja2 >=3.0,<4.dev0 + - markdown >=3.2,<4.dev0 + - mkdocs >=1.6,<2.dev0 + - mkdocs-material-extensions >=1.3,<2.dev0 + - paginate >=0.5,<1.dev0 + - pygments >=2.16,<3.dev0 + - pymdown-extensions >=10.2,<11.dev0 + - python >=3.9 + - requests >=2.26,<3.dev0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/mkdocs-material?source=compressed-mapping + size: 4917784 + timestamp: 1751382857197 +- conda: https://conda.anaconda.org/conda-forge/noarch/mkdocs-material-extensions-1.3.1-pyhd8ed1ab_1.conda + sha256: f62955d40926770ab65cc54f7db5fde6c073a3ba36a0787a7a5767017da50aa3 + md5: de8af4000a4872e16fb784c649679c8e + depends: + - python >=3.9 + constrains: + - mkdocs-material >=5.0.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/mkdocs-material-extensions?source=hash-mapping + size: 16122 + timestamp: 1734641109286 +- pypi: https://files.pythonhosted.org/packages/24/ce/c8a41cb0f3044990c8afbdc20c853845a9e940995d4e0cffecafbb5e927b/mkdocs_mermaid2_plugin-1.2.1-py3-none-any.whl + name: mkdocs-mermaid2-plugin + version: 1.2.1 + sha256: 22d2cf2c6867d4959a5e0903da2dde78d74581fc0b107b791bc4c7ceb9ce9741 + requires_dist: + - beautifulsoup4>=4.6.3 + - jsbeautifier + - mkdocs>=1.0.4 + - pymdown-extensions>=8.0 + - requests + - setuptools>=18.5 + - mkdocs-macros-test ; extra == 'test' + - mkdocs-material ; extra == 'test' + - packaging ; extra == 'test' + - requests-html ; extra == 'test' + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2024.2.2-h66d3029_15.conda + sha256: 20e52b0389586d0b914a49cd286c5ccc9c47949bed60ca6df004d1d295f2edbd + md5: 302dff2807f2927b3e9e0d19d60121de + depends: + - intel-openmp 2024.* + - tbb 2021.* + license: LicenseRef-IntelSimplifiedSoftwareOct2022 + license_family: Proprietary + purls: [] + size: 103106385 + timestamp: 1730232843711 +- conda: https://conda.anaconda.org/conda-forge/win-64/mkl-include-2024.2.2-h66d3029_15.conda + sha256: 87b53fd205282de67ff0627ea43d1d4293b7f5d6c5d5a62902c07b71bee7eb58 + md5: e2f516189b44b6e042199d13e7015361 + license: LicenseRef-IntelSimplifiedSoftwareOct2022 + license_family: Proprietary + purls: [] + size: 718823 + timestamp: 1730232277136 +- conda: https://conda.anaconda.org/conda-forge/win-64/mkl-static-2024.2.2-h66d3029_15.conda + sha256: adefe0f7bf99a46eb32908c90fe57845e2b2eb2ae498eda33080efbb1c0cf603 + md5: 2ca557fc0b33bc5c9e3a371a4eed8538 + depends: + - intel-openmp 2024.* + - mkl-include 2024.2.2 h66d3029_15 + - tbb 2021.* + license: LicenseRef-IntelSimplifiedSoftwareOct2022 + license_family: Proprietary + purls: [] + size: 108659788 + timestamp: 1730233129079 +- conda: https://conda.anaconda.org/conda-forge/linux-64/mypy-1.16.1-py312h66e93f0_0.conda + sha256: 2d1dca2a580374470e8a108565356e13aec8598c83eec17d888a4cc0b014cddd + md5: d52e9cc0c93e47a87e1024158ed2bcd3 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - mypy_extensions >=1.0.0 + - pathspec >=0.9.0 + - psutil >=4.0 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - typing_extensions >=4.6.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/mypy?source=hash-mapping + size: 18860143 + timestamp: 1750118219318 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/mypy-1.16.1-py312hea69d52_0.conda + sha256: 32920a7074b450a15f0f7b1ba48e5246484c44c167d38311db85ca9d7b100eac + md5: ab4b5c37bfdd0002491a92239a580af7 + depends: + - __osx >=11.0 + - mypy_extensions >=1.0.0 + - pathspec >=0.9.0 + - psutil >=4.0 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + - typing_extensions >=4.6.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/mypy?source=hash-mapping + size: 10320816 + timestamp: 1750118464722 +- conda: https://conda.anaconda.org/conda-forge/win-64/mypy-1.16.1-py312h4389bb4_0.conda + sha256: 5bb00863b7a736e5bfdabc576af1ff93bb95ab78b3f35e5d800feda57148ba1d + md5: 749b32d06a926c492bc48de440fc6919 + depends: + - mypy_extensions >=1.0.0 + - pathspec >=0.9.0 + - psutil >=4.0 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - typing_extensions >=4.6.0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + purls: + - pkg:pypi/mypy?source=hash-mapping + size: 10100093 + timestamp: 1750118435536 +- conda: https://conda.anaconda.org/conda-forge/noarch/mypy_extensions-1.1.0-pyha770c72_0.conda + sha256: 6ed158e4e5dd8f6a10ad9e525631e35cee8557718f83de7a4e3966b1f772c4b1 + md5: e9c622e0d00fa24a6292279af3ab6d06 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/mypy-extensions?source=hash-mapping + size: 11766 + timestamp: 1745776666688 +- conda: https://conda.anaconda.org/conda-forge/noarch/nbformat-5.10.4-pyhd8ed1ab_1.conda + sha256: 7a5bd30a2e7ddd7b85031a5e2e14f290898098dc85bea5b3a5bf147c25122838 + md5: bbe1963f1e47f594070ffe87cdf612ea + depends: + - jsonschema >=2.6 + - jupyter_core >=4.12,!=5.0.* + - python >=3.9 + - python-fastjsonschema >=2.15 + - traitlets >=5.1 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/nbformat?source=hash-mapping + size: 100945 + timestamp: 1733402844974 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586 + md5: 47e340acb35de30501a76c7c799c41d7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: X11 AND BSD-3-Clause + purls: [] + size: 891641 + timestamp: 1738195959188 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + sha256: 2827ada40e8d9ca69a153a45f7fd14f32b2ead7045d3bbb5d10964898fe65733 + md5: 068d497125e4bf8a66bf707254fff5ae + depends: + - __osx >=11.0 + license: X11 AND BSD-3-Clause + purls: [] + size: 797030 + timestamp: 1738196177597 +- conda: https://conda.anaconda.org/conda-forge/noarch/nest-asyncio-1.6.0-pyhd8ed1ab_1.conda + sha256: bb7b21d7fd0445ddc0631f64e66d91a179de4ba920b8381f29b9d006a42788c0 + md5: 598fd7d4d0de2455fb74f56063969a97 + depends: + - python >=3.9 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/nest-asyncio?source=hash-mapping + size: 11543 + timestamp: 1733325673691 +- conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.5-pyhe01879c_0.conda + sha256: 02019191a2597865940394ff42418b37bc585a03a1c643d7cea9981774de2128 + md5: 16bff3d37a4f99e3aa089c36c2b8d650 + depends: + - python >=3.11 + - python + constrains: + - numpy >=1.25 + - scipy >=1.11.2 + - matplotlib >=3.8 + - pandas >=2.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/networkx?source=hash-mapping + size: 1564462 + timestamp: 1749078300258 +- conda: https://conda.anaconda.org/conda-forge/linux-64/nlohmann_json-3.11.3-he02047a_1.conda + sha256: ce4bcced4f8eea71b7cac8bc3daac097abf7a5792f278cd811dedada199500c1 + md5: e46f7ac4917215b49df2ea09a694a3fa + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc-ng >=12 + - libstdcxx-ng >=12 + license: MIT + license_family: MIT + purls: [] + size: 122743 + timestamp: 1723652407663 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/nlohmann_json-3.11.3-h00cdb27_1.conda + sha256: 3f4e6a4fa074bb297855f8111ab974dab6d9f98b7d4317d4dd46f8687ee2363b + md5: d2dee849c806430eee64d3acc98ce090 + depends: + - __osx >=11.0 + - libcxx >=16 + license: MIT + license_family: MIT + purls: [] + size: 123250 + timestamp: 1723652704997 +- conda: https://conda.anaconda.org/conda-forge/win-64/nlohmann_json-3.11.3-he0c23c2_1.conda + sha256: 106af14431772a6bc659e8d5a3bb1930cf1010b85e0e7eca99ecd3e556e91470 + md5: 340cbb4ab78c90cd9d08f826ad22aed2 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + purls: [] + size: 124255 + timestamp: 1723652081336 +- conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.3.1-py312h6cf2f7f_0.conda + sha256: 731325aea31b3825c8c1b371f4314c096f7981de1c2cc276a7931f889b5bb6d8 + md5: 7e086a30150af2536a1059885368dcf0 + depends: + - __glibc >=2.17,<3.0.a0 + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - libgcc >=13 + - liblapack >=3.9.0,<4.0a0 + - libstdcxx >=13 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + constrains: + - numpy-base <0a0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/numpy?source=hash-mapping + size: 8364184 + timestamp: 1751342617648 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-2.3.1-py312h113b91d_0.conda + sha256: b9c79d31c214964614e6c70606ff99c7993a1eb1b8743cba484b268a386a5c46 + md5: 84048f61fe33762c214055fb2df161f0 + depends: + - __osx >=11.0 + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - libcxx >=18 + - liblapack >=3.9.0,<4.0a0 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + constrains: + - numpy-base <0a0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/numpy?source=hash-mapping + size: 6504304 + timestamp: 1751342649896 +- conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.3.1-py312h12c3145_0.conda + sha256: 2b0081eedda950026080f5e93fba915f73e75cf0c1c8e483a08cde82b3cf0a27 + md5: f11fc3fd4a1d4615cc84e50046c661ed + depends: + - libblas >=3.9.0,<4.0a0 + - libcblas >=3.9.0,<4.0a0 + - liblapack >=3.9.0,<4.0a0 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - numpy-base <0a0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/numpy?source=hash-mapping + size: 7070261 + timestamp: 1751342914306 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.5.1-h7b32b05_0.conda + sha256: 942347492164190559e995930adcdf84e2fea05307ec8012c02a505f5be87462 + md5: c87df2ab1448ba69169652ab9547082d + depends: + - __glibc >=2.17,<3.0.a0 + - ca-certificates + - libgcc >=13 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3131002 + timestamp: 1751390382076 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.5.1-h81ee809_0.conda + sha256: f94fde0f096fa79794c8aa0a2665630bbf9026cc6438e8253f6555fc7281e5a8 + md5: a8ac77e7c7e58d43fa34d60bd4361062 + depends: + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3071649 + timestamp: 1751390309393 +- conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.5.1-h725018a_0.conda + sha256: 2b2eb73b0661ff1aed55576a3d38614852b5d857c2fa9205ac115820c523306c + md5: d124fc2fd7070177b5e2450627f8fc1a + depends: + - ca-certificates + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 9327033 + timestamp: 1751392489008 +- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + sha256: 289861ed0c13a15d7bbb408796af4de72c2fe67e2bcb0de98f4c3fce259d7991 + md5: 58335b26c38bf4a20f399384c33cbcf9 + depends: + - python >=3.8 + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/packaging?source=compressed-mapping + size: 62477 + timestamp: 1745345660407 +- conda: https://conda.anaconda.org/conda-forge/noarch/paginate-0.5.7-pyhd8ed1ab_1.conda + sha256: f6fef1b43b0d3d92476e1870c08d7b9c229aebab9a0556b073a5e1641cf453bd + md5: c3f35453097faf911fd3f6023fc2ab24 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/paginate?source=hash-mapping + size: 18865 + timestamp: 1734618649164 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.1-py312hf79963d_0.conda + sha256: 6ec86b1da8432059707114270b9a45d767dac97c4910ba82b1f4fa6f74e077c8 + md5: 7c73e62e62e5864b8418440e2a2cc246 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + - numpy >=1.22.4 + - numpy >=1.23,<3 + - python >=3.12,<3.13.0a0 + - python-dateutil >=2.8.2 + - python-tzdata >=2022.7 + - python_abi 3.12.* *_cp312 + - pytz >=2020.1 + constrains: + - html5lib >=1.1 + - fastparquet >=2022.12.0 + - xarray >=2022.12.0 + - pyqt5 >=5.15.9 + - pyxlsb >=1.0.10 + - matplotlib >=3.6.3 + - numba >=0.56.4 + - odfpy >=1.4.1 + - bottleneck >=1.3.6 + - tabulate >=0.9.0 + - scipy >=1.10.0 + - pyreadstat >=1.2.0 + - pandas-gbq >=0.19.0 + - openpyxl >=3.1.0 + - xlrd >=2.0.1 + - pyarrow >=10.0.1 + - xlsxwriter >=3.0.5 + - python-calamine >=0.1.7 + - gcsfs >=2022.11.0 + - zstandard >=0.19.0 + - fsspec >=2022.11.0 + - lxml >=4.9.2 + - s3fs >=2022.11.0 + - numexpr >=2.8.4 + - psycopg2 >=2.9.6 + - qtpy >=2.3.0 + - pytables >=3.8.0 + - tzdata >=2022.7 + - sqlalchemy >=2.0.0 + - beautifulsoup4 >=4.11.2 + - blosc >=1.21.3 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pandas?source=hash-mapping + size: 15092371 + timestamp: 1752082221274 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pandas-2.3.1-py312h98f7732_0.conda + sha256: f4f98436dde01309935102de2ded045bb5500b42fb30a3bf8751b15affee4242 + md5: d3775e9b27579a0e96150ce28a2542bd + depends: + - __osx >=11.0 + - libcxx >=19 + - numpy >=1.22.4 + - numpy >=1.23,<3 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python-dateutil >=2.8.2 + - python-tzdata >=2022.7 + - python_abi 3.12.* *_cp312 + - pytz >=2020.1 + constrains: + - openpyxl >=3.1.0 + - pyarrow >=10.0.1 + - s3fs >=2022.11.0 + - zstandard >=0.19.0 + - psycopg2 >=2.9.6 + - fastparquet >=2022.12.0 + - fsspec >=2022.11.0 + - qtpy >=2.3.0 + - blosc >=1.21.3 + - xlsxwriter >=3.0.5 + - xarray >=2022.12.0 + - python-calamine >=0.1.7 + - tabulate >=0.9.0 + - odfpy >=1.4.1 + - numexpr >=2.8.4 + - tzdata >=2022.7 + - scipy >=1.10.0 + - pyreadstat >=1.2.0 + - beautifulsoup4 >=4.11.2 + - numba >=0.56.4 + - pyqt5 >=5.15.9 + - pytables >=3.8.0 + - lxml >=4.9.2 + - xlrd >=2.0.1 + - matplotlib >=3.6.3 + - bottleneck >=1.3.6 + - pandas-gbq >=0.19.0 + - html5lib >=1.1 + - pyxlsb >=1.0.10 + - sqlalchemy >=2.0.0 + - gcsfs >=2022.11.0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pandas?source=hash-mapping + size: 13991815 + timestamp: 1752082557265 +- conda: https://conda.anaconda.org/conda-forge/win-64/pandas-2.3.1-py312hc128f0a_0.conda + sha256: 711cf7b3aee4a92614744364ea996500b65fd5a11bceddb1fc03a5fd818b11d3 + md5: 77e4ad6ddb37a0b489746352f8d2275d + depends: + - numpy >=1.22.4 + - numpy >=1.23,<3 + - python >=3.12,<3.13.0a0 + - python-dateutil >=2.8.2 + - python-tzdata >=2022.7 + - python_abi 3.12.* *_cp312 + - pytz >=2020.1 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - qtpy >=2.3.0 + - pandas-gbq >=0.19.0 + - scipy >=1.10.0 + - beautifulsoup4 >=4.11.2 + - pytables >=3.8.0 + - sqlalchemy >=2.0.0 + - zstandard >=0.19.0 + - odfpy >=1.4.1 + - xarray >=2022.12.0 + - lxml >=4.9.2 + - pyreadstat >=1.2.0 + - matplotlib >=3.6.3 + - bottleneck >=1.3.6 + - s3fs >=2022.11.0 + - xlsxwriter >=3.0.5 + - pyqt5 >=5.15.9 + - blosc >=1.21.3 + - tabulate >=0.9.0 + - xlrd >=2.0.1 + - psycopg2 >=2.9.6 + - fsspec >=2022.11.0 + - numba >=0.56.4 + - pyxlsb >=1.0.10 + - fastparquet >=2022.12.0 + - tzdata >=2022.7 + - pyarrow >=10.0.1 + - openpyxl >=3.1.0 + - html5lib >=1.1 + - gcsfs >=2022.11.0 + - numexpr >=2.8.4 + - python-calamine >=0.1.7 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pandas?source=hash-mapping + size: 13875687 + timestamp: 1752082441874 +- conda: https://conda.anaconda.org/conda-forge/noarch/pandera-0.25.0-hd8ed1ab_1.conda + sha256: b4eb7857d927b9001a2fdc11ee70add246c2de80e1e08bff3a8b67ce0cdc7912 + md5: c9dca5dbec0de5c56e248087ba18ac02 + depends: + - numpy >=1.24.4 + - pandas >=2.1.1 + - pandera-base 0.25.0 pyhd8ed1ab_1 + license: MIT + license_family: MIT + purls: [] + size: 7458 + timestamp: 1752079800481 +- conda: https://conda.anaconda.org/conda-forge/noarch/pandera-base-0.25.0-pyhd8ed1ab_1.conda + sha256: 98c3b93e690426dbdd5ef788db9b183bc75202ebbc563ed1859df39da2f86e8f + md5: 8f88cb3ba3aac2992171892cd5f6d48d + depends: + - packaging >=20.0 + - pydantic + - python >=3.9 + - typeguard + - typing_inspect >=0.6.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pandera?source=hash-mapping + size: 164068 + timestamp: 1752079799520 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pango-1.56.4-hadf4263_0.conda + sha256: 3613774ad27e48503a3a6a9d72017087ea70f1426f6e5541dbdb59a3b626eaaf + md5: 79f71230c069a287efe3a8614069ddf1 + depends: + - __glibc >=2.17,<3.0.a0 + - cairo >=1.18.4,<2.0a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - fribidi >=1.0.10,<2.0a0 + - harfbuzz >=11.0.1 + - libexpat >=2.7.0,<3.0a0 + - libfreetype >=2.13.3 + - libfreetype6 >=2.13.3 + - libgcc >=13 + - libglib >=2.84.2,<3.0a0 + - libpng >=1.6.49,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + license: LGPL-2.1-or-later + purls: [] + size: 455420 + timestamp: 1751292466873 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pango-1.56.4-h875632e_0.conda + sha256: 705484ad60adee86cab1aad3d2d8def03a699ece438c864e8ac995f6f66401a6 + md5: 7d57f8b4b7acfc75c777bc231f0d31be + depends: + - __osx >=11.0 + - cairo >=1.18.4,<2.0a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - fribidi >=1.0.10,<2.0a0 + - harfbuzz >=11.0.1 + - libexpat >=2.7.0,<3.0a0 + - libfreetype >=2.13.3 + - libfreetype6 >=2.13.3 + - libglib >=2.84.2,<3.0a0 + - libpng >=1.6.49,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + license: LGPL-2.1-or-later + purls: [] + size: 426931 + timestamp: 1751292636271 +- conda: https://conda.anaconda.org/conda-forge/win-64/pango-1.56.4-h03d888a_0.conda + sha256: dcda7e9bedc1c87f51ceef7632a5901e26081a1f74a89799a3e50dbdc801c0bd + md5: 452d6d3b409edead3bd90fc6317cd6d4 + depends: + - cairo >=1.18.4,<2.0a0 + - fontconfig >=2.15.0,<3.0a0 + - fonts-conda-ecosystem + - fribidi >=1.0.10,<2.0a0 + - harfbuzz >=11.0.1 + - libexpat >=2.7.0,<3.0a0 + - libfreetype >=2.13.3 + - libfreetype6 >=2.13.3 + - libglib >=2.84.2,<3.0a0 + - libpng >=1.6.49,<1.7.0a0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: LGPL-2.1-or-later + purls: [] + size: 454854 + timestamp: 1751292618315 +- conda: https://conda.anaconda.org/conda-forge/noarch/parso-0.8.4-pyhd8ed1ab_1.conda + sha256: 17131120c10401a99205fc6fe436e7903c0fa092f1b3e80452927ab377239bcc + md5: 5c092057b6badd30f75b06244ecd01c9 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/parso?source=hash-mapping + size: 75295 + timestamp: 1733271352153 +- conda: https://conda.anaconda.org/conda-forge/noarch/pathspec-0.12.1-pyhd8ed1ab_1.conda + sha256: 9f64009cdf5b8e529995f18e03665b03f5d07c0b17445b8badef45bde76249ee + md5: 617f15191456cc6a13db418a275435e5 + depends: + - python >=3.9 + license: MPL-2.0 + license_family: MOZILLA + purls: + - pkg:pypi/pathspec?source=hash-mapping + size: 41075 + timestamp: 1733233471940 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.45-hc749103_0.conda + sha256: 27c4014f616326240dcce17b5f3baca3953b6bc5f245ceb49c3fa1e6320571eb + md5: b90bece58b4c2bf25969b70f3be42d25 + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - libgcc >=13 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 1197308 + timestamp: 1745955064657 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.45-ha881caa_0.conda + sha256: e9ecb706b58b5a2047c077b3a1470e8554f3aad02e9c3c00cfa35d537420fea3 + md5: a52385b93558d8e6bbaeec5d61a21cd7 + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 837826 + timestamp: 1745955207242 +- conda: https://conda.anaconda.org/conda-forge/win-64/pcre2-10.45-h99c9b8b_0.conda + sha256: 165d6f76e7849615cfa5fe5f0209b90103102db17a7b4632f933fa9c0e8d8bfe + md5: f4c483274001678e129f5cbaf3a8d765 + depends: + - bzip2 >=1.0.8,<2.0a0 + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 1040584 + timestamp: 1745955875845 +- conda: https://conda.anaconda.org/conda-forge/noarch/pexpect-4.9.0-pyhd8ed1ab_1.conda + sha256: 202af1de83b585d36445dc1fda94266697341994d1a3328fabde4989e1b3d07a + md5: d0d408b1f18883a944376da5cf8101ea + depends: + - ptyprocess >=0.5 + - python >=3.9 + license: ISC + purls: + - pkg:pypi/pexpect?source=compressed-mapping + size: 53561 + timestamp: 1733302019362 +- conda: https://conda.anaconda.org/conda-forge/noarch/pickleshare-0.7.5-pyhd8ed1ab_1004.conda + sha256: e2ac3d66c367dada209fc6da43e645672364b9fd5f9d28b9f016e24b81af475b + md5: 11a9d1d09a3615fc07c3faf79bc0b943 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pickleshare?source=hash-mapping + size: 11748 + timestamp: 1733327448200 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.46.2-h29eaf8c_0.conda + sha256: 6cb261595b5f0ae7306599f2bb55ef6863534b6d4d1bc0dcfdfa5825b0e4e53d + md5: 39b4228a867772d610c02e06f939a5b8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + license: MIT + license_family: MIT + purls: [] + size: 402222 + timestamp: 1749552884791 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pixman-0.46.2-h2f9eb0b_0.conda + sha256: 68d1eef12946d779ce4b4b9de88bc295d07adce5dd825a0baf0e1d7cf69bc5a6 + md5: 0587a57e200568a71982173c07684423 + depends: + - __osx >=11.0 + - libcxx >=18 + license: MIT + license_family: MIT + purls: [] + size: 214660 + timestamp: 1749553221709 +- conda: https://conda.anaconda.org/conda-forge/win-64/pixman-0.46.2-had0cd8c_0.conda + sha256: d7d1f1052f15601406883f17ec149abf5e99262782ef536a415a41add060596e + md5: 2566a45fb15e2f540eff14261f1242af + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + purls: [] + size: 476515 + timestamp: 1749553103224 +- conda: https://conda.anaconda.org/conda-forge/noarch/pkgutil-resolve-name-1.3.10-pyhd8ed1ab_2.conda + sha256: adb2dde5b4f7da70ae81309cce6188ed3286ff280355cf1931b45d91164d2ad8 + md5: 5a5870a74432aa332f7d32180633ad05 + depends: + - python >=3.9 + license: MIT AND PSF-2.0 + purls: + - pkg:pypi/pkgutil-resolve-name?source=hash-mapping + size: 10693 + timestamp: 1733344619659 +- conda: https://conda.anaconda.org/conda-forge/noarch/plac-1.4.5-pyhd8ed1ab_0.conda + sha256: bc4885f1ebd818b01832f5a26cdc5703248e26e12de33117985e9e4d96b0e3da + md5: 3f30dc72be42bb4619502fa496f8d86a + depends: + - python >=3.9 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/plac?source=hash-mapping + size: 26484 + timestamp: 1743816198 +- conda: https://conda.anaconda.org/conda-forge/noarch/platformdirs-4.3.8-pyhe01879c_0.conda + sha256: 0f48999a28019c329cd3f6fd2f01f09fc32cc832f7d6bbe38087ddac858feaa3 + md5: 424844562f5d337077b445ec6b1398a7 + depends: + - python >=3.9 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/platformdirs?source=hash-mapping + size: 23531 + timestamp: 1746710438805 +- conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhd8ed1ab_0.conda + sha256: a8eb555eef5063bbb7ba06a379fa7ea714f57d9741fe0efdb9442dbbc2cccbcc + md5: 7da7ccd349dbf6487a7778579d2bb971 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pluggy?source=hash-mapping + size: 24246 + timestamp: 1747339794916 +- conda: https://conda.anaconda.org/conda-forge/noarch/prompt-toolkit-3.0.51-pyha770c72_0.conda + sha256: ebc1bb62ac612af6d40667da266ff723662394c0ca78935340a5b5c14831227b + md5: d17ae9db4dc594267181bd199bf9a551 + depends: + - python >=3.9 + - wcwidth + constrains: + - prompt_toolkit 3.0.51 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/prompt-toolkit?source=hash-mapping + size: 271841 + timestamp: 1744724188108 +- conda: https://conda.anaconda.org/conda-forge/linux-64/psutil-7.0.0-py312h66e93f0_0.conda + sha256: 158047d7a80e588c846437566d0df64cec5b0284c7184ceb4f3c540271406888 + md5: 8e30db4239508a538e4a3b3cdf5b9616 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/psutil?source=hash-mapping + size: 466219 + timestamp: 1740663246825 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/psutil-7.0.0-py312hea69d52_0.conda + sha256: cb11dcb39b2035ef42c3df89b5a288744b5dcb5a98fb47385760843b1d4df046 + md5: 0f461bd37cb428dc20213a08766bb25d + depends: + - __osx >=11.0 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/psutil?source=hash-mapping + size: 476376 + timestamp: 1740663381256 +- conda: https://conda.anaconda.org/conda-forge/win-64/psutil-7.0.0-py312h4389bb4_0.conda + sha256: 088451ee2c9a349e1168f70afe275e58f86350faffb09c032cff76f97d4fb7bb + md5: f5b86d6e2e645ee276febe79a310b640 + depends: + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/psutil?source=hash-mapping + size: 484682 + timestamp: 1740663813103 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda + sha256: 9c88f8c64590e9567c6c80823f0328e58d3b1efb0e1c539c0315ceca764e0973 + md5: b3c17d95b5a10c6e64a21fa17573e70e + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: MIT + license_family: MIT + purls: [] + size: 8252 + timestamp: 1726802366959 +- conda: https://conda.anaconda.org/conda-forge/win-64/pthread-stubs-0.4-h0e40799_1002.conda + sha256: 7e446bafb4d692792310ed022fe284e848c6a868c861655a92435af7368bae7b + md5: 3c8f2573569bb816483e5cf57efbbe29 + depends: + - libgcc >=13 + - libwinpthread >=12.0.0.r4.gg4f2fc60ca + - ucrt >=10.0.20348.0 + license: MIT + license_family: MIT + purls: [] + size: 9389 + timestamp: 1726802555076 +- conda: https://conda.anaconda.org/conda-forge/noarch/ptyprocess-0.7.0-pyhd8ed1ab_1.conda + sha256: a7713dfe30faf17508ec359e0bc7e0983f5d94682492469bd462cdaae9c64d83 + md5: 7d9daffbb8d8e0af0f769dbbcd173a54 + depends: + - python >=3.9 + license: ISC + purls: + - pkg:pypi/ptyprocess?source=hash-mapping + size: 19457 + timestamp: 1733302371990 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pulp-2.8.0-py312hd0750ca_2.conda + sha256: aebb79738fbd303c46379a4da8dbab9dfcd1f06a38856ffc128515a09ff7de1f + md5: 9d0f74674964adfaa467e9877d0d0060 + depends: + - amply >=0.1.2 + - coin-or-cbc + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pulp?source=hash-mapping + size: 224707 + timestamp: 1748870015953 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pulp-2.8.0-py312h38bd297_2.conda + sha256: c9936daa97c64560509af5fd4a71c320aa6799e4eee83028c33f0a8a4ba4b287 + md5: e82c2b91689ee04f2280d512efe78007 + depends: + - amply >=0.1.2 + - coin-or-cbc + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pulp?source=hash-mapping + size: 225979 + timestamp: 1748870136987 +- conda: https://conda.anaconda.org/conda-forge/win-64/pulp-2.8.0-py312he39998a_2.conda + sha256: d45f44202977c6f637ab5cc33ba27b5544cd82a39bd9be7c9532e07100bbcacc + md5: b43a3a4dfe7e5876ac02d7834ffe701b + depends: + - amply >=0.1.2 + - coin-or-cbc + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pulp?source=hash-mapping + size: 14262455 + timestamp: 1748870491982 +- conda: https://conda.anaconda.org/conda-forge/noarch/pure_eval-0.2.3-pyhd8ed1ab_1.conda + sha256: 71bd24600d14bb171a6321d523486f6a06f855e75e547fa0cb2a0953b02047f0 + md5: 3bfdfb8dbcdc4af1ae3f9a8eb3948f04 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pure-eval?source=hash-mapping + size: 16668 + timestamp: 1733569518868 +- conda: https://conda.anaconda.org/conda-forge/noarch/pybind11-abi-4-hd8ed1ab_3.tar.bz2 + sha256: d4fb485b79b11042a16dc6abfb0c44c4f557707c2653ac47c81e5d32b24a3bb0 + md5: 878f923dd6acc8aeb47a75da6c4098be + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 9906 + timestamp: 1610372835205 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pycosat-0.6.6-py312h66e93f0_2.conda + sha256: dad83b55d1511a853ecf1d5bff3027055337262aa63084986ee2e329ee26d71b + md5: 08223e6a73e0bca5ade16ec4cebebf23 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pycosat?source=hash-mapping + size: 87749 + timestamp: 1732588516003 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pycosat-0.6.6-py312hea69d52_2.conda + sha256: ad64eadac6b0a9534cbba1088df9de84c95f7f69c700a5a9cb8b20dfc175e6aa + md5: b62d16d1aabb9349c8e81d842dfb2268 + depends: + - __osx >=11.0 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pycosat?source=hash-mapping + size: 84234 + timestamp: 1732588806999 +- conda: https://conda.anaconda.org/conda-forge/win-64/pycosat-0.6.6-py312h4389bb4_2.conda + sha256: e8375488806c16a067f62e81f1d84aa05e149c35c72ed443b645b9292fc3b35f + md5: b05ea9cb9eb430aa417b84ea34414551 + depends: + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pycosat?source=hash-mapping + size: 77781 + timestamp: 1732588951422 +- conda: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyh29332c3_1.conda + sha256: 79db7928d13fab2d892592223d7570f5061c192f27b9febd1a418427b719acc6 + md5: 12c566707c80111f9799308d9e265aef + depends: + - python >=3.9 + - python + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pycparser?source=hash-mapping + size: 110100 + timestamp: 1733195786147 +- conda: https://conda.anaconda.org/conda-forge/noarch/pydantic-2.11.7-pyh3cfb1c2_0.conda + sha256: ee7823e8bc227f804307169870905ce062531d36c1dcf3d431acd65c6e0bd674 + md5: 1b337e3d378cde62889bb735c024b7a2 + depends: + - annotated-types >=0.6.0 + - pydantic-core 2.33.2 + - python >=3.9 + - typing-extensions >=4.6.1 + - typing-inspection >=0.4.0 + - typing_extensions >=4.12.2 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pydantic?source=compressed-mapping + size: 307333 + timestamp: 1749927245525 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pydantic-core-2.33.2-py312h680f630_0.conda + sha256: 4d14d7634c8f351ff1e63d733f6bb15cba9a0ec77e468b0de9102014a4ddc103 + md5: cfbd96e5a0182dfb4110fc42dda63e57 + depends: + - python + - typing-extensions >=4.6.0,!=4.7.0 + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - python_abi 3.12.* *_cp312 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pydantic-core?source=hash-mapping + size: 1890081 + timestamp: 1746625309715 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pydantic-core-2.33.2-py312hd3c0895_0.conda + sha256: 4e583aab0854a3a9c88e3e5c55348f568a1fddce43952a74892e490537327522 + md5: affb6b478c21735be55304d47bfe1c63 + depends: + - python + - typing-extensions >=4.6.0,!=4.7.0 + - python 3.12.* *_cpython + - __osx >=11.0 + - python_abi 3.12.* *_cp312 + constrains: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pydantic-core?source=hash-mapping + size: 1715338 + timestamp: 1746625327204 +- conda: https://conda.anaconda.org/conda-forge/win-64/pydantic-core-2.33.2-py312h8422cdd_0.conda + sha256: f377214abd06f1870011a6068b10c9e23dc62065d4c2de13b2f0a6014636e0ae + md5: c61e3f191da309117e0b0478b49f6e91 + depends: + - python + - typing-extensions >=4.6.0,!=4.7.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - ucrt >=10.0.20348.0 + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pydantic-core?source=hash-mapping + size: 1900306 + timestamp: 1746625389678 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pydot-4.0.1-py312h7900ff3_0.conda + sha256: be87ec5ee93853c3deb8115099c74f002e0af711410e23b1ae14cc30c12c0041 + md5: 6ff35cf1336b0aa49338b8171aa74c94 + depends: + - graphviz >=2.38.0 + - pyparsing >=3.0.9 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pydot?source=hash-mapping + size: 83083 + timestamp: 1750503648393 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pydot-4.0.1-py312h81bd7bf_0.conda + sha256: 1d9fc2c0f982ff9b1ee9d7bd27bdbed6cbcb53bedd8200b6249f12710c48e359 + md5: 2e6a2e3c32f6a48b6cacfec0e1f3da90 + depends: + - graphviz >=2.38.0 + - pyparsing >=3.0.9 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pydot?source=hash-mapping + size: 83084 + timestamp: 1750503807587 +- conda: https://conda.anaconda.org/conda-forge/win-64/pydot-4.0.1-py312h2e8e312_0.conda + sha256: 2aed920a88a9bc003e56da4b462c181b91776817ba4ebc12949a400f8d1d3d97 + md5: c81f64a8ee9e03f87155db2c7876d965 + depends: + - graphviz >=2.38.0 + - pyparsing >=3.0.9 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pydot?source=hash-mapping + size: 84675 + timestamp: 1750503721244 +- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + sha256: 5577623b9f6685ece2697c6eb7511b4c9ac5fb607c9babc2646c811b428fd46a + md5: 6b6ece66ebcae2d5f326c77ef2c5a066 + depends: + - python >=3.9 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/pygments?source=compressed-mapping + size: 889287 + timestamp: 1750615908735 +- conda: https://conda.anaconda.org/conda-forge/noarch/pymdown-extensions-10.16-pyhd8ed1ab_0.conda + sha256: 7465d67daa980999606138d74631563f5c233624cf5d65fc3f1f7210fce91b64 + md5: 79dbb1bfe734d8e8b36ca328a63fb4de + depends: + - markdown >=3.6 + - python >=3.9 + - pyyaml + license: MIT + license_family: MIT + purls: + - pkg:pypi/pymdown-extensions?source=hash-mapping + size: 171431 + timestamp: 1750571864207 +- conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.2.3-pyhd8ed1ab_1.conda + sha256: b92afb79b52fcf395fd220b29e0dd3297610f2059afac45298d44e00fcbf23b6 + md5: 513d3c262ee49b54a8fec85c5bc99764 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyparsing?source=hash-mapping + size: 95988 + timestamp: 1743089832359 +- conda: https://conda.anaconda.org/conda-forge/win-64/pyreadline3-3.5.4-py312h2e8e312_1.conda + sha256: 0d9055f133bf90eb4eeb7e1f1526ab875acda34ab39b71a0a719c670ee163913 + md5: 9987d07be9812b80d129b08c1dfbe30e + depends: + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pyreadline3?source=hash-mapping + size: 170545 + timestamp: 1749148459863 +- conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda + sha256: d016e04b0e12063fbee4a2d5fbb9b39a8d191b5a0042f0b8459188aedeabb0ca + md5: e2fd202833c4a981ce8a65974fe4abd1 + depends: + - __win + - python >=3.9 + - win_inet_pton + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pysocks?source=hash-mapping + size: 21784 + timestamp: 1733217448189 +- conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + sha256: ba3b032fa52709ce0d9fd388f63d330a026754587a2f461117cac9ab73d8d0d8 + md5: 461219d1a5bd61342293efa2c0c90eac + depends: + - __unix + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pysocks?source=hash-mapping + size: 21085 + timestamp: 1733217331982 +- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-8.4.1-pyhd8ed1ab_0.conda + sha256: 93e267e4ec35353e81df707938a6527d5eb55c97bf54c3b87229b69523afb59d + md5: a49c2283f24696a7b30367b7346a0144 + depends: + - colorama >=0.4 + - exceptiongroup >=1 + - iniconfig >=1 + - packaging >=20 + - pluggy >=1.5,<2 + - pygments >=2.7.2 + - python >=3.9 + - tomli >=1 + constrains: + - pytest-faulthandler >=2 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pytest?source=hash-mapping + size: 276562 + timestamp: 1750239526127 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.11-h9e4cc4f_0_cpython.conda + sha256: 6cca004806ceceea9585d4d655059e951152fc774a471593d4f5138e6a54c81d + md5: 94206474a5608243a10c92cefbe0908f + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.7.0,<3.0a0 + - libffi >=3.4.6,<3.5.0a0 + - libgcc >=13 + - liblzma >=5.8.1,<6.0a0 + - libnsl >=2.0.1,<2.1.0a0 + - libsqlite >=3.50.0,<4.0a0 + - libuuid >=2.38.1,<3.0a0 + - libxcrypt >=4.4.36 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.0,<4.0a0 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + purls: [] + size: 31445023 + timestamp: 1749050216615 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.11-hc22306f_0_cpython.conda + sha256: cde8b944c2dc378a5afbc48028d0843583fd215493d5885a80f1b41de085552f + md5: 9207ebad7cfbe2a4af0702c92fd031c4 + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.0,<3.0a0 + - libffi >=3.4.6,<3.5.0a0 + - liblzma >=5.8.1,<6.0a0 + - libsqlite >=3.50.0,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.0,<4.0a0 + - readline >=8.2,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + purls: [] + size: 13009234 + timestamp: 1749048134449 +- conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.11-h3f84c4b_0_cpython.conda + sha256: b69412e64971b5da3ced0fc36f05d0eacc9393f2084c6f92b8f28ee068d83e2e + md5: 6aa5e62df29efa6319542ae5025f4376 + depends: + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.0,<3.0a0 + - libffi >=3.4.6,<3.5.0a0 + - liblzma >=5.8.1,<6.0a0 + - libsqlite >=3.50.0,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.0,<4.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - python_abi 3.12.* *_cp312 + license: Python-2.0 + purls: [] + size: 15829289 + timestamp: 1749047682640 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + sha256: d6a17ece93bbd5139e02d2bd7dbfa80bee1a4261dced63f65f679121686bf664 + md5: 5b8d21249ff20967101ffa321cab24e8 + depends: + - python >=3.9 + - six >=1.5 + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/python-dateutil?source=hash-mapping + size: 233310 + timestamp: 1751104122689 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-fastjsonschema-2.21.1-pyhd8ed1ab_0.conda + sha256: 1b09a28093071c1874862422696429d0d35bd0b8420698003ac004746c5e82a2 + md5: 38e34d2d1d9dca4fb2b9a0a04f604e2c + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/fastjsonschema?source=hash-mapping + size: 226259 + timestamp: 1733236073335 +- conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2025.2-pyhd8ed1ab_0.conda + sha256: e8392a8044d56ad017c08fec2b0eb10ae3d1235ac967d0aab8bd7b41c4a5eaf0 + md5: 88476ae6ebd24f39261e0854ac244f33 + depends: + - python >=3.9 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/tzdata?source=compressed-mapping + size: 144160 + timestamp: 1742745254292 +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-7_cp312.conda + build_number: 7 + sha256: a1bbced35e0df66cc713105344263570e835625c28d1bdee8f748f482b2d7793 + md5: 0dfcdc155cf23812a0c9deada86fb723 + constrains: + - python 3.12.* *_cpython + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 6971 + timestamp: 1745258861359 +- conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2025.2-pyhd8ed1ab_0.conda + sha256: 8d2a8bf110cc1fc3df6904091dead158ba3e614d8402a83e51ed3a8aa93cdeb0 + md5: bc8e3267d44011051f2eb14d22fb0960 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pytz?source=compressed-mapping + size: 189015 + timestamp: 1742920947249 +- conda: https://conda.anaconda.org/conda-forge/win-64/pywin32-307-py312h275cf98_3.conda + sha256: 68f8781b83942b91dbc0df883f9edfd1a54a1e645ae2a97c48203ff6c2919de3 + md5: 1747fbbdece8ab4358b584698b19c44d + depends: + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: PSF-2.0 + license_family: PSF + purls: + - pkg:pypi/pywin32?source=hash-mapping + size: 6032183 + timestamp: 1728636767192 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.2-py312h178313f_2.conda + sha256: 159cba13a93b3fe084a1eb9bda0a07afc9148147647f0d437c3c3da60980503b + md5: cf2485f39740de96e2a7f2bb18ed2fee + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - yaml >=0.2.5,<0.3.0a0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyyaml?source=hash-mapping + size: 206903 + timestamp: 1737454910324 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.2-py312h998013c_2.conda + sha256: ad225ad24bfd60f7719709791345042c3cb32da1692e62bd463b084cf140e00d + md5: 68149ed4d4e9e1c42d2ba1f27f08ca96 + depends: + - __osx >=11.0 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + - yaml >=0.2.5,<0.3.0a0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyyaml?source=hash-mapping + size: 192148 + timestamp: 1737454886351 +- conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.2-py312h31fea79_2.conda + sha256: 76fec03ef7e67e37724873e1f805131fb88efb57f19e9a77b4da616068ef5c28 + md5: ba00a2e5059c1fde96459858537cc8f5 + depends: + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - yaml >=0.2.5,<0.3.0a0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyyaml?source=hash-mapping + size: 181734 + timestamp: 1737455207230 +- conda: https://conda.anaconda.org/conda-forge/noarch/pyyaml-env-tag-1.1-pyhd8ed1ab_0.conda + sha256: 69ab63bd45587406ae911811fc4d4c1bf972d643fa57a009de7c01ac978c4edd + md5: e8e53c4150a1bba3b160eacf9d53a51b + depends: + - python >=3.9 + - pyyaml + license: MIT + license_family: MIT + purls: + - pkg:pypi/pyyaml-env-tag?source=hash-mapping + size: 11137 + timestamp: 1747237061448 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyzmq-27.0.0-py312hbf22597_0.conda + sha256: 8564a7beb906476813a59a81a814d00e8f9697c155488dbc59a5c6e950d5f276 + md5: 4b9a9cda3292668831cf47257ade22a6 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libsodium >=1.0.20,<1.0.21.0a0 + - libstdcxx >=13 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - zeromq >=4.3.5,<4.4.0a0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pyzmq?source=hash-mapping + size: 378610 + timestamp: 1749898590652 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyzmq-27.0.0-py312hf4875e0_0.conda + sha256: 709c673d5b45774ce003648427103732c834a300447452a3c8369469e2aa6bfd + md5: 0ff6afa66b15299c051f57e5ec257e88 + depends: + - __osx >=11.0 + - libcxx >=18 + - libsodium >=1.0.20,<1.0.21.0a0 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + - zeromq >=4.3.5,<4.4.0a0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pyzmq?source=hash-mapping + size: 359326 + timestamp: 1749898793266 +- conda: https://conda.anaconda.org/conda-forge/win-64/pyzmq-27.0.0-py312hd7027bb_0.conda + sha256: e66267a7a61bfba5cdb50089c04a6f140edb9133c5ce34331ee2f95370460b8c + md5: 37d6508caaa4c3a91e3434192d192685 + depends: + - libsodium >=1.0.20,<1.0.21.0a0 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - zeromq >=4.3.5,<4.3.6.0a0 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/pyzmq?source=hash-mapping + size: 364291 + timestamp: 1749899188003 +- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8c095d6_2.conda + sha256: 2d6d0c026902561ed77cd646b5021aef2d4db22e57a5b0178dfc669231e06d2c + md5: 283b96675859b20a825f8fa30f311446 + depends: + - libgcc >=13 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 282480 + timestamp: 1740379431762 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.2-h1d1bf99_2.conda + sha256: 7db04684d3904f6151eff8673270922d31da1eea7fa73254d01c437f49702e34 + md5: 63ef3f6e6d6d5c589e64f11263dc5676 + depends: + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 252359 + timestamp: 1740379663071 +- conda: https://conda.anaconda.org/conda-forge/noarch/referencing-0.36.2-pyh29332c3_0.conda + sha256: e20909f474a6cece176dfc0dc1addac265deb5fa92ea90e975fbca48085b20c3 + md5: 9140f1c09dd5489549c6a33931b943c7 + depends: + - attrs >=22.2.0 + - python >=3.9 + - rpds-py >=0.7.0 + - typing_extensions >=4.4.0 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/referencing?source=hash-mapping + size: 51668 + timestamp: 1737836872415 +- conda: https://conda.anaconda.org/conda-forge/linux-64/reproc-14.2.5.post0-hb9d3cd8_0.conda + sha256: a1973f41a6b956f1305f9aaefdf14b2f35a8c9615cfe5f143f1784ed9aa6bf47 + md5: 69fbc0a9e42eb5fe6733d2d60d818822 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: MIT + license_family: MIT + purls: [] + size: 34194 + timestamp: 1731925834928 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/reproc-14.2.5.post0-h5505292_0.conda + sha256: a5f0dbfa8099a3d3c281ea21932b6359775fd8ce89acc53877a6ee06f50642bc + md5: f1d129089830365d9dac932c4dd8c675 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 32023 + timestamp: 1731926255834 +- conda: https://conda.anaconda.org/conda-forge/win-64/reproc-14.2.5.post0-h2466b09_0.conda + sha256: 112dee79da4f55de91f029dd9808f4284bc5e0cf0c4d308d4cec3381bf5bc836 + md5: c3ca4c18c99a3b9832e11b11af227713 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + purls: [] + size: 37058 + timestamp: 1731926140985 +- conda: https://conda.anaconda.org/conda-forge/linux-64/reproc-cpp-14.2.5.post0-h5888daf_0.conda + sha256: 568485837b905b1ea7bdb6e6496d914b83db57feda57f6050d5a694977478691 + md5: 828302fca535f9cfeb598d5f7c204323 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + - reproc 14.2.5.post0 hb9d3cd8_0 + license: MIT + license_family: MIT + purls: [] + size: 25665 + timestamp: 1731925852714 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/reproc-cpp-14.2.5.post0-h286801f_0.conda + sha256: f1b6aa9d9131ea159a5883bc5990b91b4b8f56eb52e0dc2b01aa9622e14edc81 + md5: 11a3d09937d250fc4423bf28837d9363 + depends: + - __osx >=11.0 + - libcxx >=18 + - reproc 14.2.5.post0 h5505292_0 + license: MIT + license_family: MIT + purls: [] + size: 24834 + timestamp: 1731926355120 +- conda: https://conda.anaconda.org/conda-forge/win-64/reproc-cpp-14.2.5.post0-he0c23c2_0.conda + sha256: ccf49fb5149298015ab410aae88e43600954206608089f0dfb7aea8b771bbe8e + md5: d2ce31fa746dddeb37f24f32da0969e9 + depends: + - reproc 14.2.5.post0 h2466b09_0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + purls: [] + size: 30096 + timestamp: 1731926177599 +- conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.4-pyhd8ed1ab_0.conda + sha256: 9866aaf7a13c6cfbe665ec7b330647a0fb10a81e6f9b8fee33642232a1920e18 + md5: f6082eae112814f1447b56a5e1f6ed05 + depends: + - certifi >=2017.4.17 + - charset-normalizer >=2,<4 + - idna >=2.5,<4 + - python >=3.9 + - urllib3 >=1.21.1,<3 + constrains: + - chardet >=3.0.2,<6 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/requests?source=hash-mapping + size: 59407 + timestamp: 1749498221996 +- conda: https://conda.anaconda.org/conda-forge/noarch/reretry-0.11.8-pyhd8ed1ab_1.conda + sha256: f010d25e0ab452c0339a42807c84316bf30c5b8602b9d74d566abf1956d23269 + md5: b965b0dfdb3c89966a6a25060f73aa67 + depends: + - python >=3.9 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/reretry?source=hash-mapping + size: 12563 + timestamp: 1735477549872 +- conda: https://conda.anaconda.org/conda-forge/linux-64/rpds-py-0.26.0-py312h680f630_0.conda + sha256: bb051358e7550fd8ef9129def61907ad03853604f5e641108b1dbe2ce93247cc + md5: 5b251d4dd547d8b5970152bae2cc1600 + depends: + - python + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - python_abi 3.12.* *_cp312 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + purls: + - pkg:pypi/rpds-py?source=compressed-mapping + size: 389020 + timestamp: 1751467350968 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/rpds-py-0.26.0-py312hd3c0895_0.conda + sha256: b22152ead8e06a489cc6ed03828b884bfccfa085d972a0420179757809d721fd + md5: 19681f34a4071b4380a986fc524fe1c4 + depends: + - python + - __osx >=11.0 + - python 3.12.* *_cpython + - python_abi 3.12.* *_cp312 + constrains: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/rpds-py?source=hash-mapping + size: 357102 + timestamp: 1751467161700 +- conda: https://conda.anaconda.org/conda-forge/win-64/rpds-py-0.26.0-py312hdabe01f_0.conda + sha256: 665d771c3d4a028dc49c45e47634ef3adac80500ed6206ba6837885f02b0947f + md5: 353d4c6bd46906805189af9a7394b0d1 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/rpds-py?source=hash-mapping + size: 250960 + timestamp: 1751467083088 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ruamel.yaml-0.18.14-py312h66e93f0_0.conda + sha256: ba0216708dd5f3f419f58d337d0498d8d28ae508784b8111d79cecb6a547b2d6 + md5: ebef257605116235f5feac68640b44ca + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ruamel.yaml.clib >=0.1.2 + license: MIT + license_family: MIT + purls: + - pkg:pypi/ruamel-yaml?source=hash-mapping + size: 268479 + timestamp: 1749480091070 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruamel.yaml-0.18.14-py312hea69d52_0.conda + sha256: 701239de5094f567f2f3d54f2fdef87238de039c8405826011eadee2bb761d88 + md5: c82d1ddf44663c982945f42f36f96f3d + depends: + - __osx >=11.0 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + - ruamel.yaml.clib >=0.1.2 + license: MIT + license_family: MIT + purls: + - pkg:pypi/ruamel-yaml?source=hash-mapping + size: 268872 + timestamp: 1749480207447 +- conda: https://conda.anaconda.org/conda-forge/win-64/ruamel.yaml-0.18.14-py312h4389bb4_0.conda + sha256: efe81379882195da402b0386e5c94950591a6963fc99d982d36ba8b3dc447d66 + md5: 53e81cf55c2bcea269771678e0d53ed8 + depends: + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ruamel.yaml.clib >=0.1.2 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + purls: + - pkg:pypi/ruamel-yaml?source=hash-mapping + size: 268437 + timestamp: 1749480149128 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ruamel.yaml.clib-0.2.8-py312h66e93f0_1.conda + sha256: ac987b1c186d79e4e1ce4354a84724fc68db452b2bd61de3a3e1b6fc7c26138d + md5: 532c3e5d0280be4fea52396ec1fa7d5d + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/ruamel-yaml-clib?source=hash-mapping + size: 145481 + timestamp: 1728724626666 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruamel.yaml.clib-0.2.8-py312h0bf5046_1.conda + sha256: ce979a9bcb4b987e30c4aadfbd4151006cd6ac480bdbee1d059e6f0186b48bca + md5: 2ed5f254c9ea57b6d0fd4e12baa4b87f + depends: + - __osx >=11.0 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/ruamel-yaml-clib?source=hash-mapping + size: 117121 + timestamp: 1728724705098 +- conda: https://conda.anaconda.org/conda-forge/win-64/ruamel.yaml.clib-0.2.8-py312h4389bb4_1.conda + sha256: d5583406ea6d17391294da0a6dadf9a22aad732d1f658f2d6d12fc50b968c0fa + md5: 5758e70a80936d7527f70196685c6695 + depends: + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MIT + license_family: MIT + purls: + - pkg:pypi/ruamel-yaml-clib?source=hash-mapping + size: 108926 + timestamp: 1728725024979 +- conda: https://conda.anaconda.org/conda-forge/linux-64/ruff-0.12.2-hcc1af86_0.conda + noarch: python + sha256: fc1cf93cca78a31943429f11743c5145c5781d4346b9f8ea1de74cf0f0707d6b + md5: 9160006765c4c01ec0bb48d40c1c6b6e + depends: + - python + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - __glibc >=2.17 + license: MIT + license_family: MIT + purls: + - pkg:pypi/ruff?source=hash-mapping + size: 9377215 + timestamp: 1751584630794 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ruff-0.12.2-h412e174_0.conda + noarch: python + sha256: 216cc46672f28cf25fe631eaf6b3c83e7486bdd3a13be8659d3ae154dd6db5df + md5: 4c0640914d19cd144bef69196d8e850f + depends: + - python + - __osx >=11.0 + constrains: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/ruff?source=hash-mapping + size: 8668814 + timestamp: 1751584689374 +- conda: https://conda.anaconda.org/conda-forge/win-64/ruff-0.12.2-hd40eec1_0.conda + noarch: python + sha256: 5bd96d72e8e038847fcb562e781fff4ce8927aacf3241fa11a20061bcc7e057f + md5: 6357ee6be70d6889f402cd6c8ae1b3e3 + depends: + - python + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + - ucrt >=10.0.20348.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/ruff?source=hash-mapping + size: 9648327 + timestamp: 1751584640933 +- conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-80.9.0-pyhff2d567_0.conda + sha256: 972560fcf9657058e3e1f97186cc94389144b46dbdf58c807ce62e83f977e863 + md5: 4de79c071274a53dcaf2a8c749d1499e + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/setuptools?source=hash-mapping + size: 748788 + timestamp: 1748804951958 +- conda: https://conda.anaconda.org/conda-forge/linux-64/simdjson-3.13.0-h84d6215_0.conda + sha256: c256cc95f50a5b9f68603c0849b82a3be9ba29527d05486f3e1465e8fed76c4a + md5: f2d511bfca0cc4acca4bb40cd1905dff + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 248262 + timestamp: 1749080745183 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/simdjson-3.13.0-ha393de7_0.conda + sha256: 9a34757a186b6931cb123d7b1e56164ac1f55a4083b7d0f942dfed0f06b53d16 + md5: 4ca40a1a4049e3dbd7847200763ac6f5 + depends: + - __osx >=11.0 + - libcxx >=18 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 208556 + timestamp: 1749080957534 +- conda: https://conda.anaconda.org/conda-forge/win-64/simdjson-3.13.0-hc790b64_0.conda + sha256: b0f7bf715bd0ae0eaa0585844bf6ae03f269cb1963c90c7fbab74a4c56b58539 + md5: bb927044f1999ff62cb2c99d385ad597 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 255973 + timestamp: 1749080928478 +- conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhd8ed1ab_0.conda + sha256: 41db0180680cc67c3fa76544ffd48d6a5679d96f4b71d7498a759e94edc9a2db + md5: a451d576819089b0d672f18768be0f65 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/six?source=hash-mapping + size: 16385 + timestamp: 1733381032766 +- conda: https://conda.anaconda.org/conda-forge/noarch/smart_open-7.3.0.post1-pyhe01879c_0.conda + sha256: b91438f9d3fda19ac9690dc4d1207a2d01240c47f35f13787d3e7b88396b1ae5 + md5: 40579e9a7e1f6ba0d249770ec26a5345 + depends: + - python >=3.9 + - wrapt + - python + license: MIT + purls: + - pkg:pypi/smart-open?source=hash-mapping + size: 54781 + timestamp: 1752113562407 +- conda: https://conda.anaconda.org/conda-forge/noarch/smmap-5.0.2-pyhd8ed1ab_0.conda + sha256: eb92d0ad94b65af16c73071cc00cc0e10f2532be807beb52758aab2b06eb21e2 + md5: 87f47a78808baf2fa1ea9c315a1e48f1 + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/smmap?source=hash-mapping + size: 26051 + timestamp: 1739781801801 +- conda: https://conda.anaconda.org/bioconda/noarch/snakefmt-0.11.0-pyhdfd78af_0.tar.bz2 + sha256: 3607605dc8efc796e28d69f201ff9dcc1a532a542d5af05609c2eef74125cbcf + md5: dbc8bc755a58632c456845df597453d9 + depends: + - black >=24.3,<25.0 + - click >=8.0.0,<9.0.0 + - python >=3.8 + - toml >=0.10.2,<0.11.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/snakefmt?source=hash-mapping + size: 31992 + timestamp: 1742569930064 +- conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-common-1.20.1-pyhdfd78af_0.tar.bz2 + sha256: c1694d7fdad7aa33ef4311ae74c769373cf8acf1e63a03cf9c63c3b30afe2998 + md5: cc586d7251b88847e03f44ef0dca5f40 + depends: + - argparse-dataclass >=2.0.0,<3.0.0 + - configargparse >=1.7,<2.0 + - packaging >=24.0,<26.0 + - python >=3.8.0,<4.0.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/snakemake-interface-common?source=hash-mapping + size: 20213 + timestamp: 1751645195686 +- conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-executor-plugins-9.3.7-pyhdfd78af_0.tar.bz2 + sha256: 70f7dd3ab1685bf27d86e9ff7900656751056503734472616c5d840c637df266 + md5: f54fd47c15198486efe5ceb35d556216 + depends: + - argparse-dataclass >=2.0.0,<3.0.0 + - python >=3.11.0,<4.0.0 + - snakemake-interface-common >=1.19.0 + - throttler >=1.2.2,<2.0.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/snakemake-interface-executor-plugins?source=hash-mapping + size: 22834 + timestamp: 1750715329449 +- conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-logger-plugins-1.2.3-pyhdfd78af_0.tar.bz2 + sha256: 156579df920ecc5c275dc4e94b595e0b8ce97f9f037631515e1b19c0652e1f88 + md5: de9701be8653cc2442f32fa76c08da61 + depends: + - python >=3.11.0,<4.0.0 + - snakemake-interface-common >=1.17.4,<2.0.0 + license: MIT + purls: + - pkg:pypi/snakemake-interface-logger-plugins?source=hash-mapping + size: 12539 + timestamp: 1742472457005 +- conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-report-plugins-1.1.0-pyhdfd78af_0.tar.bz2 + sha256: 1d110a5c54b9f46824bb03f80c09cdc5045d6c27c0166662d5f4e8a7c07d3535 + md5: 3a7dd19cd530b27b59aed6cb606a7987 + depends: + - python >=3.11.0,<4.0.0 + - snakemake-interface-common >=1.16.0,<2.0.0 + license: MIT + purls: + - pkg:pypi/snakemake-interface-report-plugins?source=hash-mapping + size: 13269 + timestamp: 1728055589409 +- conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-storage-plugins-4.2.1-pyhdfd78af_0.tar.bz2 + sha256: 759127ee57236f6cfff795c497d56fd758a0aaae9cf54b6e6a100bb600816695 + md5: de5d573c67176a5b1756987f9a8a595e + depends: + - python >=3.11.0,<4.0.0 + - reretry >=0.11.8,<0.12.0 + - snakemake-interface-common >=1.12.0,<2.0.0 + - throttler >=1.2.2,<2.0.0 + - wrapt >=1.15.0,<2.0.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/snakemake-interface-storage-plugins?source=hash-mapping + size: 19926 + timestamp: 1742480267140 +- conda: https://conda.anaconda.org/bioconda/noarch/snakemake-minimal-9.8.0-pyhdfd78af_0.tar.bz2 + sha256: a64f6810a1ffba386835ba53d49c78ae557839a604b00159e4cf6274d8cea6fc + md5: 588f915c84e7b3ed7bc7534254099493 + depends: + - appdirs + - conda-inject >=1.3.1,<2.0 + - configargparse + - connection_pool >=0.0.3 + - docutils + - dpath >=2.1.6,<3.0.0 + - gitpython + - humanfriendly + - immutables + - jinja2 >=3.0,<4.0 + - jsonschema + - nbformat + - packaging >=24.0,<26.0 + - psutil + - pulp >=2.3.1,<3.1 + - python >=3.11,<3.13 + - pyyaml + - requests >=2.8.1,<3.0 + - reretry + - smart_open >=4.0,<8.0 + - snakemake-interface-common >=1.20.1,<2.0 + - snakemake-interface-executor-plugins >=9.3.2,<10.0 + - snakemake-interface-logger-plugins >=1.1.0,<2.0.0 + - snakemake-interface-report-plugins >=1.1.0,<2.0.0 + - snakemake-interface-storage-plugins >=4.1.0,<5.0 + - tabulate + - throttler + - wrapt + - yte >=1.5.5,<2.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/snakemake?source=hash-mapping + size: 877774 + timestamp: 1752146131114 +- pypi: https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl + name: soupsieve + version: '2.7' + sha256: 6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4 + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/noarch/stack_data-0.6.3-pyhd8ed1ab_1.conda + sha256: 570da295d421661af487f1595045760526964f41471021056e993e73089e9c41 + md5: b1b505328da7a6b246787df4b5a49fbc + depends: + - asttokens + - executing + - pure_eval + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/stack-data?source=hash-mapping + size: 26988 + timestamp: 1733569565672 +- conda: https://conda.anaconda.org/conda-forge/noarch/tabulate-0.9.0-pyhd8ed1ab_2.conda + sha256: 090023bddd40d83468ef86573976af8c514f64119b2bd814ee63a838a542720a + md5: 959484a66b4b76befcddc4fa97c95567 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/tabulate?source=hash-mapping + size: 37554 + timestamp: 1733589854804 +- conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2021.13.0-h62715c5_1.conda + sha256: 03cc5442046485b03dd1120d0f49d35a7e522930a2ab82f275e938e17b07b302 + md5: 9190dd0a23d925f7602f9628b3aed511 + depends: + - libhwloc >=2.11.2,<2.11.3.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: Apache-2.0 + license_family: APACHE + purls: [] + size: 151460 + timestamp: 1732982860332 +- conda: https://conda.anaconda.org/conda-forge/noarch/throttler-1.2.2-pyhd8ed1ab_0.conda + sha256: cdd2067b03db7ed7a958de74edc1a4f8c4ae6d0aa1a61b5b70b89de5013f0f78 + md5: 6fc48bef3b400c82abaee323a9d4e290 + depends: + - python >=3.6 + license: MIT + license_family: MIT + purls: + - pkg:pypi/throttler?source=hash-mapping + size: 12341 + timestamp: 1691135604942 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd72426e_102.conda + sha256: a84ff687119e6d8752346d1d408d5cf360dee0badd487a472aa8ddedfdc219e1 + md5: a0116df4f4ed05c303811a837d5b39d8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libzlib >=1.3.1,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3285204 + timestamp: 1748387766691 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h892fb3f_2.conda + sha256: cb86c522576fa95c6db4c878849af0bccfd3264daf0cc40dd18e7f4a7bfced0e + md5: 7362396c170252e7b7b0c8fb37fe9c78 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3125538 + timestamp: 1748388189063 +- conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h2c6b04d_2.conda + sha256: e3614b0eb4abcc70d98eae159db59d9b4059ed743ef402081151a948dce95896 + md5: ebd0e761de9aa879a51d22cc721bd095 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: TCL + license_family: BSD + purls: [] + size: 3466348 + timestamp: 1748388121356 +- conda: https://conda.anaconda.org/conda-forge/noarch/toml-0.10.2-pyhd8ed1ab_1.conda + sha256: 34f3a83384ac3ac30aefd1309e69498d8a4aa0bf2d1f21c645f79b180e378938 + md5: b0dd904de08b7db706167240bf37b164 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/toml?source=hash-mapping + size: 22132 + timestamp: 1734091907682 +- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.2.1-pyhd8ed1ab_1.conda + sha256: 18636339a79656962723077df9a56c0ac7b8a864329eb8f847ee3d38495b863e + md5: ac944244f1fed2eb49bae07193ae8215 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/tomli?source=hash-mapping + size: 19167 + timestamp: 1733256819729 +- conda: https://conda.anaconda.org/conda-forge/linux-64/tornado-6.5.1-py312h66e93f0_0.conda + sha256: c96be4c8bca2431d7ad7379bad94ed6d4d25cd725ae345540a531d9e26e148c9 + md5: c532a6ee766bed75c4fa0c39e959d132 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/tornado?source=hash-mapping + size: 850902 + timestamp: 1748003427956 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tornado-6.5.1-py312hea69d52_0.conda + sha256: 02835bf9f49a7c6f73622614be67dc20f9b5c2ce9f663f427150dc0579007daa + md5: 375a5a90946ff09cd98b9cf5b833023c + depends: + - __osx >=11.0 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/tornado?source=hash-mapping + size: 851614 + timestamp: 1748003575892 +- conda: https://conda.anaconda.org/conda-forge/win-64/tornado-6.5.1-py312h4389bb4_0.conda + sha256: cec4ab331788122f7f01dd02f57f8e21d9ae14553dedd6389d7dfeceb3592399 + md5: 06b156bbbe1597eb5ea30b931cadaa32 + depends: + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: Apache-2.0 + license_family: Apache + purls: + - pkg:pypi/tornado?source=hash-mapping + size: 853357 + timestamp: 1748003925528 +- conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.1-pyhd8ed1ab_1.conda + sha256: 11e2c85468ae9902d24a27137b6b39b4a78099806e551d390e394a8c34b48e40 + md5: 9efbfdc37242619130ea42b1cc4ed861 + depends: + - colorama + - python >=3.9 + license: MPL-2.0 or MIT + purls: + - pkg:pypi/tqdm?source=hash-mapping + size: 89498 + timestamp: 1735661472632 +- conda: https://conda.anaconda.org/conda-forge/noarch/traitlets-5.14.3-pyhd8ed1ab_1.conda + sha256: f39a5620c6e8e9e98357507262a7869de2ae8cc07da8b7f84e517c9fd6c2b959 + md5: 019a7385be9af33791c989871317e1ed + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/traitlets?source=hash-mapping + size: 110051 + timestamp: 1733367480074 +- conda: https://conda.anaconda.org/conda-forge/noarch/truststore-0.10.1-pyh29332c3_0.conda + sha256: 12ac41c281dc2cb6e15b7d9a758913550fc452debfe985634c9f8d347429b0af + md5: 373a72aeffd8a5d93652ef1235062252 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/truststore?source=hash-mapping + size: 23354 + timestamp: 1739009763560 +- conda: https://conda.anaconda.org/conda-forge/noarch/typeguard-4.4.4-pyhd8ed1ab_0.conda + sha256: 591e03a61b4966a61b15a99f91d462840b6e77bf707ecb48690b24649fee921a + md5: 8b2613dbfd4e2bc9080b2779b53fc210 + depends: + - importlib-metadata >=3.6 + - python >=3.9 + - typing-extensions >=4.10.0 + - typing_extensions >=4.14.0 + constrains: + - pytest >=7 + license: MIT + license_family: MIT + purls: + - pkg:pypi/typeguard?source=hash-mapping + size: 35158 + timestamp: 1750249264892 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.14.1-h4440ef1_0.conda + sha256: 349951278fa8d0860ec6b61fcdc1e6f604e6fce74fabf73af2e39a37979d0223 + md5: 75be1a943e0a7f99fcf118309092c635 + depends: + - typing_extensions ==4.14.1 pyhe01879c_0 + license: PSF-2.0 + license_family: PSF + purls: [] + size: 90486 + timestamp: 1751643513473 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing-inspection-0.4.1-pyhd8ed1ab_0.conda + sha256: 4259a7502aea516c762ca8f3b8291b0d4114e094bdb3baae3171ccc0900e722f + md5: e0c3cd765dc15751ee2f0b03cd015712 + depends: + - python >=3.9 + - typing_extensions >=4.12.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/typing-inspection?source=compressed-mapping + size: 18809 + timestamp: 1747870776989 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.14.1-pyhe01879c_0.conda + sha256: 4f52390e331ea8b9019b87effaebc4f80c6466d09f68453f52d5cdc2a3e1194f + md5: e523f4f1e980ed7a4240d7e27e9ec81f + depends: + - python >=3.9 + - python + license: PSF-2.0 + license_family: PSF + purls: + - pkg:pypi/typing-extensions?source=hash-mapping + size: 51065 + timestamp: 1751643513473 +- conda: https://conda.anaconda.org/conda-forge/noarch/typing_inspect-0.9.0-pyhd8ed1ab_1.conda + sha256: a3fbdd31b509ff16c7314e8d01c41d9146504df632a360ab30dbc1d3ca79b7c0 + md5: fa31df4d4193aabccaf09ce78a187faf + depends: + - mypy_extensions >=0.3.0 + - python >=3.9 + - typing_extensions >=3.7.4 + license: MIT + license_family: MIT + purls: + - pkg:pypi/typing-inspect?source=hash-mapping + size: 14919 + timestamp: 1733845966415 +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025b-h78e105d_0.conda + sha256: 5aaa366385d716557e365f0a4e9c3fca43ba196872abbbe3d56bb610d131e192 + md5: 4222072737ccff51314b5ece9c7d6f5a + license: LicenseRef-Public-Domain + purls: [] + size: 122968 + timestamp: 1742727099393 +- conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_1.conda + sha256: db8dead3dd30fb1a032737554ce91e2819b43496a0db09927edf01c32b577450 + md5: 6797b005cd0f439c4c5c9ac565783700 + constrains: + - vs2015_runtime >=14.29.30037 + license: LicenseRef-MicrosoftWindowsSDK10 + purls: [] + size: 559710 + timestamp: 1728377334097 +- conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.5.0-pyhd8ed1ab_0.conda + sha256: 4fb9789154bd666ca74e428d973df81087a697dbb987775bc3198d2215f240f8 + md5: 436c165519e140cb08d246a4472a9d6a + depends: + - brotli-python >=1.0.9 + - h2 >=4,<5 + - pysocks >=1.5.6,<2.0,!=1.5.7 + - python >=3.9 + - zstandard >=0.18.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/urllib3?source=hash-mapping + size: 101735 + timestamp: 1750271478254 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_26.conda + sha256: b388d88e04aa0257df4c1d28f8d85d985ad07c1e5645aa62335673c98704c4c6 + md5: 18b6bf6f878501547786f7bf8052a34d + depends: + - vc14_runtime >=14.44.35208 + track_features: + - vc14 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 17914 + timestamp: 1750371462857 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_26.conda + sha256: 7bad6e25a7c836d99011aee59dcf600b7f849a6fa5caa05a406255527e80a703 + md5: 14d65350d3f5c8ff163dc4f76d6e2830 + depends: + - ucrt >=10.0.20348.0 + constrains: + - vs2015_runtime 14.44.35208.* *_26 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime + license_family: Proprietary + purls: [] + size: 756109 + timestamp: 1750371459116 +- conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.44.35208-h38c0c73_26.conda + sha256: d18d77c8edfbad37fa0e0bb0f543ad80feb85e8fe5ced0f686b8be463742ec0b + md5: 312f3a0a6b3c5908e79ce24002411e32 + depends: + - vc14_runtime >=14.44.35208 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 17888 + timestamp: 1750371463202 +- conda: https://conda.anaconda.org/conda-forge/linux-64/watchdog-6.0.0-py312h7900ff3_0.conda + sha256: 2436c4736b8135801f6bfcd09c7283f2d700a66a90ebd14b666b996e33ef8c9a + md5: 687b37d1325f228429409465e811c0bc + depends: + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - pyyaml >=3.10 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/watchdog?source=hash-mapping + size: 140940 + timestamp: 1730493008472 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/watchdog-6.0.0-py312hea69d52_0.conda + sha256: f6c2eb941ffc25fc4fc637c71a5465678ed20e57b53698020a50dca86c584f04 + md5: ce2a02fd5a911d4eb963af9a84c00d2c + depends: + - __osx >=11.0 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + - pyyaml >=3.10 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/watchdog?source=hash-mapping + size: 149164 + timestamp: 1730493202256 +- conda: https://conda.anaconda.org/conda-forge/win-64/watchdog-6.0.0-py312h2e8e312_0.conda + sha256: d273308e2e936ab1963d958ecd342c77b0aa5a39d334aa4126c886e8dfd9e802 + md5: 3b401a2d5ecf5da721aa89ffa003cd76 + depends: + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - pyyaml >=3.10 + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/watchdog?source=hash-mapping + size: 165888 + timestamp: 1730493286260 +- conda: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.24.0-h3e06ad9_0.conda + sha256: ba673427dcd480cfa9bbc262fd04a9b1ad2ed59a159bd8f7e750d4c52282f34c + md5: 0f2ca7906bf166247d1d760c3422cb8a + depends: + - __glibc >=2.17,<3.0.a0 + - libexpat >=2.7.0,<3.0a0 + - libffi >=3.4.6,<3.5.0a0 + - libgcc >=13 + - libstdcxx >=13 + license: MIT + license_family: MIT + purls: [] + size: 330474 + timestamp: 1751817998141 +- conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_1.conda + sha256: f21e63e8f7346f9074fd00ca3b079bd3d2fa4d71f1f89d5b6934bf31446dc2a5 + md5: b68980f2495d096e71c7fd9d7ccf63e6 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/wcwidth?source=hash-mapping + size: 32581 + timestamp: 1733231433877 +- conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + sha256: 93807369ab91f230cf9e6e2a237eaa812492fe00face5b38068735858fba954f + md5: 46e441ba871f524e2b067929da3051c2 + depends: + - __win + - python >=3.9 + license: LicenseRef-Public-Domain + purls: + - pkg:pypi/win-inet-pton?source=hash-mapping + size: 9555 + timestamp: 1733130678956 +- conda: https://conda.anaconda.org/conda-forge/linux-64/wrapt-1.17.2-py312h66e93f0_0.conda + sha256: ed3a1700ecc5d38c7e7dc7d2802df1bc1da6ba3d6f6017448b8ded0affb4ae00 + md5: 669e63af87710f8d52fdec9d4d63b404 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/wrapt?source=hash-mapping + size: 63590 + timestamp: 1736869574299 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/wrapt-1.17.2-py312hea69d52_0.conda + sha256: 6a3e68b57de29802e8703d1791dcacb7613bfdc17bbb087c6b2ea2796e6893ef + md5: e49608c832fcf438f70cbcae09c3adc5 + depends: + - __osx >=11.0 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/wrapt?source=hash-mapping + size: 61198 + timestamp: 1736869673767 +- conda: https://conda.anaconda.org/conda-forge/win-64/wrapt-1.17.2-py312h4389bb4_0.conda + sha256: a1b86d727cc5f9d016a6fc9d8ac8b3e17c8e137764e018555ecadef05979ce93 + md5: b9a81b36e0d35c9a172587ead532273b + depends: + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/wrapt?source=hash-mapping + size: 62232 + timestamp: 1736869967220 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xkeyboard-config-2.45-hb9d3cd8_0.conda + sha256: a5d4af601f71805ec67403406e147c48d6bad7aaeae92b0622b7e2396842d3fe + md5: 397a013c2dc5145a70737871aaa87e98 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.12,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 392406 + timestamp: 1749375847832 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libice-1.1.2-hb9d3cd8_0.conda + sha256: c12396aabb21244c212e488bbdc4abcdef0b7404b15761d9329f5a4a39113c4b + md5: fb901ff28063514abb6046c9ec2c4a45 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: MIT + license_family: MIT + purls: [] + size: 58628 + timestamp: 1734227592886 +- conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libice-1.1.2-h0e40799_0.conda + sha256: bf1d34142b1bf9b5a4eed96bcc77bc4364c0e191405fd30d2f9b48a04d783fd3 + md5: 105cb93a47df9c548e88048dc9cbdbc9 + depends: + - libgcc >=13 + - libwinpthread >=12.0.0.r4.gg4f2fc60ca + - ucrt >=10.0.20348.0 + - xorg-libx11 >=1.8.10,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 236306 + timestamp: 1734228116846 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.6-he73a12e_0.conda + sha256: 277841c43a39f738927145930ff963c5ce4c4dacf66637a3d95d802a64173250 + md5: 1c74ff8c35dcadf952a16f752ca5aa49 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libuuid >=2.38.1,<3.0a0 + - xorg-libice >=1.1.2,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 27590 + timestamp: 1741896361728 +- conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libsm-1.2.6-h0e40799_0.conda + sha256: 065d49b0d1e6873ed1238e962f56cb8204c585cdc5c9bd4ae2bf385cadb5bd65 + md5: 570c9a6d9b4909e45d49e9a5daa528de + depends: + - libgcc >=13 + - libwinpthread >=12.0.0.r4.gg4f2fc60ca + - ucrt >=10.0.20348.0 + - xorg-libice >=1.1.2,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 97096 + timestamp: 1741896840170 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.12-h4f16b4b_0.conda + sha256: 51909270b1a6c5474ed3978628b341b4d4472cd22610e5f22b506855a5e20f67 + md5: db038ce880f100acc74dba10302b5630 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libxcb >=1.17.0,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 835896 + timestamp: 1741901112627 +- conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libx11-1.8.12-hf48077a_0.conda + sha256: 3f0854bc592d31a5742c6c4550914a976c89d73b74d052545b418521d21b3043 + md5: c4f435ac09fd41606bba9f0deb12e412 + depends: + - libgcc >=13 + - libwinpthread >=12.0.0.r4.gg4f2fc60ca + - libxcb >=1.17.0,<2.0a0 + - ucrt >=10.0.20348.0 + license: MIT + license_family: MIT + purls: [] + size: 951392 + timestamp: 1741902072732 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb9d3cd8_0.conda + sha256: ed10c9283974d311855ae08a16dfd7e56241fac632aec3b92e3cfe73cff31038 + md5: f6ebe2cb3f82ba6c057dde5d9debe4f7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: MIT + license_family: MIT + purls: [] + size: 14780 + timestamp: 1734229004433 +- conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxau-1.0.12-h0e40799_0.conda + sha256: 047836241b2712aab1e29474a6f728647bff3ab57de2806b0bb0a6cf9a2d2634 + md5: 2ffbfae4548098297c033228256eb96e + depends: + - libgcc >=13 + - libwinpthread >=12.0.0.r4.gg4f2fc60ca + - ucrt >=10.0.20348.0 + license: MIT + license_family: MIT + purls: [] + size: 108013 + timestamp: 1734229474049 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcomposite-0.4.6-hb9d3cd8_2.conda + sha256: 753f73e990c33366a91fd42cc17a3d19bb9444b9ca5ff983605fa9e953baf57f + md5: d3c295b50f092ab525ffe3c2aa4b7413 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + - xorg-libxfixes >=6.0.1,<7.0a0 + license: MIT + license_family: MIT + purls: [] + size: 13603 + timestamp: 1727884600744 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxcursor-1.2.3-hb9d3cd8_0.conda + sha256: 832f538ade441b1eee863c8c91af9e69b356cd3e9e1350fff4fe36cc573fc91a + md5: 2ccd714aa2242315acaf0a67faea780b + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + - xorg-libxfixes >=6.0.1,<7.0a0 + - xorg-libxrender >=0.9.11,<0.10.0a0 + license: MIT + license_family: MIT + purls: [] + size: 32533 + timestamp: 1730908305254 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdamage-1.1.6-hb9d3cd8_0.conda + sha256: 43b9772fd6582bf401846642c4635c47a9b0e36ca08116b3ec3df36ab96e0ec0 + md5: b5fcc7172d22516e1f965490e65e33a4 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxfixes >=6.0.1,<7.0a0 + license: MIT + license_family: MIT + purls: [] + size: 13217 + timestamp: 1727891438799 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb9d3cd8_0.conda + sha256: 6b250f3e59db07c2514057944a3ea2044d6a8cdde8a47b6497c254520fade1ee + md5: 8035c64cb77ed555e3f150b7b3972480 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: MIT + license_family: MIT + purls: [] + size: 19901 + timestamp: 1727794976192 +- conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxdmcp-1.1.5-h0e40799_0.conda + sha256: 9075f98dcaa8e9957e4a3d9d30db05c7578a536950a31c200854c5c34e1edb2c + md5: 8393c0f7e7870b4eb45553326f81f0ff + depends: + - libgcc >=13 + - libwinpthread >=12.0.0.r4.gg4f2fc60ca + - ucrt >=10.0.20348.0 + license: MIT + license_family: MIT + purls: [] + size: 69920 + timestamp: 1727795651979 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.6-hb9d3cd8_0.conda + sha256: da5dc921c017c05f38a38bd75245017463104457b63a1ce633ed41f214159c14 + md5: febbab7d15033c913d53c7a2c102309d + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 50060 + timestamp: 1727752228921 +- conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxext-1.3.6-h0e40799_0.conda + sha256: 7fdc3135a340893aa544921115c3994ef4071a385d47cc11232d818f006c63e4 + md5: 4cd74e74f063fb6900d6eed2e9288112 + depends: + - libgcc >=13 + - libwinpthread >=12.0.0.r4.gg4f2fc60ca + - ucrt >=10.0.20348.0 + - xorg-libx11 >=1.8.10,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 284715 + timestamp: 1727752838922 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-6.0.1-hb9d3cd8_0.conda + sha256: 2fef37e660985794617716eb915865ce157004a4d567ed35ec16514960ae9271 + md5: 4bdb303603e9821baf5fe5fdff1dc8f8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 19575 + timestamp: 1727794961233 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxi-1.8.2-hb9d3cd8_0.conda + sha256: 1a724b47d98d7880f26da40e45f01728e7638e6ec69f35a3e11f92acd05f9e7a + md5: 17dcc85db3c7886650b8908b183d6876 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxfixes >=6.0.1,<7.0a0 + license: MIT + license_family: MIT + purls: [] + size: 47179 + timestamp: 1727799254088 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxinerama-1.1.5-h5888daf_1.conda + sha256: 1b9141c027f9d84a9ee5eb642a0c19457c788182a5a73c5a9083860ac5c20a8c + md5: 5e2eb9bf77394fc2e5918beefec9f9ab + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 13891 + timestamp: 1727908521531 +- conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxpm-3.5.17-h0e40799_1.conda + sha256: a605b43b2622a4cae8df6edc148c02b527da4ea165ec67cabb5c9bc4f3f8ef13 + md5: e8b816fb37bc61aa3f1c08034331ef53 + depends: + - libgcc >=13 + - libwinpthread >=12.0.0.r4.gg4f2fc60ca + - ucrt >=10.0.20348.0 + - xorg-libx11 >=1.8.10,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxt >=1.3.0,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 236112 + timestamp: 1727801849623 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrandr-1.5.4-hb9d3cd8_0.conda + sha256: ac0f037e0791a620a69980914a77cb6bb40308e26db11698029d6708f5aa8e0d + md5: 2de7f99d6581a4a7adbff607b5c278ca + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxrender >=0.9.11,<0.10.0a0 + license: MIT + license_family: MIT + purls: [] + size: 29599 + timestamp: 1727794874300 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.12-hb9d3cd8_0.conda + sha256: 044c7b3153c224c6cedd4484dd91b389d2d7fd9c776ad0f4a34f099b3389f4a1 + md5: 96d57aba173e878a2089d5638016dc5e + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 33005 + timestamp: 1734229037766 +- conda: https://conda.anaconda.org/conda-forge/win-64/xorg-libxt-1.3.1-h0e40799_0.conda + sha256: c940a6b71a1e59450b01ebfb3e21f3bbf0a8e611e5fbfc7982145736b0f20133 + md5: 31baf0ce8ef19f5617be73aee0527618 + depends: + - libgcc >=13 + - libwinpthread >=12.0.0.r4.gg4f2fc60ca + - ucrt >=10.0.20348.0 + - xorg-libice >=1.1.1,<2.0a0 + - xorg-libsm >=1.2.4,<2.0a0 + - xorg-libx11 >=1.8.10,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 918674 + timestamp: 1731861024233 +- conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxtst-1.2.5-hb9d3cd8_3.conda + sha256: 752fdaac5d58ed863bbf685bb6f98092fe1a488ea8ebb7ed7b606ccfce08637a + md5: 7bbe9a0cc0df0ac5f5a8ad6d6a11af2f + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - xorg-libx11 >=1.8.10,<2.0a0 + - xorg-libxext >=1.3.6,<2.0a0 + - xorg-libxi >=1.7.10,<2.0a0 + license: MIT + license_family: MIT + purls: [] + size: 32808 + timestamp: 1727964811275 +- conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h7f98852_2.tar.bz2 + sha256: a4e34c710eeb26945bdbdaba82d3d74f60a78f54a874ec10d373811a5d217535 + md5: 4cb3ad778ec2d5a7acbdf254eb1c42ae + depends: + - libgcc-ng >=9.4.0 + license: MIT + license_family: MIT + purls: [] + size: 89141 + timestamp: 1641346969816 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h3422bc3_2.tar.bz2 + sha256: 93181a04ba8cfecfdfb162fc958436d868cc37db504c58078eab4c1a3e57fbb7 + md5: 4bb3f014845110883a3c5ee811fd84b4 + license: MIT + license_family: MIT + purls: [] + size: 88016 + timestamp: 1641347076660 +- conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h8ffe710_2.tar.bz2 + sha256: 4e2246383003acbad9682c7c63178e2e715ad0eb84f03a8df1fbfba455dfedc5 + md5: adbfb9f45d1004a26763652246a33764 + depends: + - vc >=14.1,<15.0a0 + - vs2015_runtime >=14.16.27012 + license: MIT + license_family: MIT + purls: [] + size: 63274 + timestamp: 1641347623319 +- conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-cpp-0.8.0-h3f2d84a_0.conda + sha256: 4b0b713a4308864a59d5f0b66ac61b7960151c8022511cdc914c0c0458375eca + md5: 92b90f5f7a322e74468bb4909c7354b5 + depends: + - libstdcxx >=13 + - libgcc >=13 + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: MIT + license_family: MIT + purls: [] + size: 223526 + timestamp: 1745307989800 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-cpp-0.8.0-ha1acc90_0.conda + sha256: 66ba31cfb8014fdd3456f2b3b394df123bbd05d95b75328b7c4131639e299749 + md5: 30475b3d0406587cf90386a283bb3cd0 + depends: + - libcxx >=18 + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 136222 + timestamp: 1745308075886 +- conda: https://conda.anaconda.org/conda-forge/win-64/yaml-cpp-0.8.0-he0c23c2_0.conda + sha256: 031642d753e0ebd666a76cea399497cc7048ff363edf7d76a630ee0a19e341da + md5: 9bb5064a9fca5ca8e7d7f1ae677354b6 + depends: + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + - ucrt >=10.0.20348.0 + license: MIT + license_family: MIT + purls: [] + size: 148572 + timestamp: 1745308037198 +- conda: https://conda.anaconda.org/conda-forge/noarch/yte-1.8.1-pyha770c72_0.conda + sha256: 439ebef131ef2e4711f286375240f8d779fce2fe54b4ec786fb58c6c9141b17b + md5: 55a52c71e7919a4951cfc6cccf4fa16f + depends: + - dpath + - plac + - python >=3.7 + - pyyaml + license: MIT + license_family: MIT + purls: + - pkg:pypi/yte?source=hash-mapping + size: 15805 + timestamp: 1749657286268 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zeromq-4.3.5-h3b0a872_7.conda + sha256: a4dc72c96848f764bb5a5176aa93dd1e9b9e52804137b99daeebba277b31ea10 + md5: 3947a35e916fcc6b9825449affbf4214 + depends: + - __glibc >=2.17,<3.0.a0 + - krb5 >=1.21.3,<1.22.0a0 + - libgcc >=13 + - libsodium >=1.0.20,<1.0.21.0a0 + - libstdcxx >=13 + license: MPL-2.0 + license_family: MOZILLA + purls: [] + size: 335400 + timestamp: 1731585026517 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zeromq-4.3.5-hc1bb282_7.conda + sha256: 9e585569fe2e7d3bea71972cd4b9f06b1a7ab8fa7c5139f92a31cbceecf25a8a + md5: f7e6b65943cb73bce0143737fded08f1 + depends: + - __osx >=11.0 + - krb5 >=1.21.3,<1.22.0a0 + - libcxx >=18 + - libsodium >=1.0.20,<1.0.21.0a0 + license: MPL-2.0 + license_family: MOZILLA + purls: [] + size: 281565 + timestamp: 1731585108039 +- conda: https://conda.anaconda.org/conda-forge/win-64/zeromq-4.3.5-ha9f60a1_7.conda + sha256: 15cc8e2162d0a33ffeb3f7b7c7883fd830c54a4b1be6a4b8c7ee1f4fef0088fb + md5: e03f2c245a5ee6055752465519363b1c + depends: + - krb5 >=1.21.3,<1.22.0a0 + - libsodium >=1.0.20,<1.0.21.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: MPL-2.0 + license_family: MOZILLA + purls: [] + size: 2527503 + timestamp: 1731585151036 +- conda: https://conda.anaconda.org/conda-forge/noarch/zipp-3.23.0-pyhd8ed1ab_0.conda + sha256: 7560d21e1b021fd40b65bfb72f67945a3fcb83d78ad7ccf37b8b3165ec3b68ad + md5: df5e78d904988eb55042c0c97446079f + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/zipp?source=hash-mapping + size: 22963 + timestamp: 1749421737203 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py312h66e93f0_2.conda + sha256: ff62d2e1ed98a3ec18de7e5cf26c0634fd338cb87304cf03ad8cbafe6fe674ba + md5: 630db208bc7bbb96725ce9832c7423bb + depends: + - __glibc >=2.17,<3.0.a0 + - cffi >=1.11 + - libgcc >=13 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/zstandard?source=hash-mapping + size: 732224 + timestamp: 1745869780524 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstandard-0.23.0-py312hea69d52_2.conda + sha256: c499a2639c2981ac2fd33bae2d86c15d896bc7524f1c5651a7d3b088263f7810 + md5: ba0eb639914e4033e090b46f53bec31c + depends: + - __osx >=11.0 + - cffi >=1.11 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/zstandard?source=hash-mapping + size: 532173 + timestamp: 1745870087418 +- conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.23.0-py312h4389bb4_2.conda + sha256: 10f25f85f856dbc776b4a2cf801d31edd07cbfaa45b9cca14dd776a9f2887cb5 + md5: 24554d76d0efcca11faa0a013c16ed5a + depends: + - cffi >=1.11 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/zstandard?source=hash-mapping + size: 444685 + timestamp: 1745870132644 +- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb8e6e7a_2.conda + sha256: a4166e3d8ff4e35932510aaff7aa90772f84b4d07e9f6f83c614cba7ceefe0eb + md5: 6432cb5d4ac0046c3ac0a8a0f95842f9 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + - libstdcxx >=13 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 567578 + timestamp: 1742433379869 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-h6491c7d_2.conda + sha256: 0d02046f57f7a1a3feae3e9d1aa2113788311f3cf37a3244c71e61a93177ba67 + md5: e6f69c7bcccdefa417f056fa593b40f0 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 399979 + timestamp: 1742433432699 +- conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-hbeecb71_2.conda + sha256: bc64864377d809b904e877a98d0584f43836c9f2ef27d3d2a1421fa6eae7ca04 + md5: 21f56217d6125fb30c3c3f10c786d751 + depends: + - libzlib >=1.3.1,<2.0a0 + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 354697 + timestamp: 1742433568506 diff --git a/tests/integration/Snakefile b/tests/integration/Snakefile index d549545..81c0421 100644 --- a/tests/integration/Snakefile +++ b/tests/integration/Snakefile @@ -6,7 +6,7 @@ rule download_netherlands_shapes: message: "Download and unzip the Netherlands shapes." output: - "results/module_area_potentials/resources/user/shapes/NLD.parquet", + "results/integration_test/resources/user/shapes/NLD.parquet", shell: """ curl -sSLo "{output}" "https://surfdrive.surf.nl/files/index.php/s/ey3RmiCbajp69oQ/download" @@ -19,12 +19,12 @@ rule download_netherlands_protected_areas: input: script=workflow.source_path("../../workflow/scripts/unzip_like.py"), output: - zipfile="results/module_area_potentials/resources/user/wdpa.gdb.zip", - wdpa=directory("results/module_area_potentials/resources/user/wdpa.gdb"), + zipfile="results/integration_test/resources/user/wdpa.gdb.zip", + wdpa=directory("results/integration_test/resources/user/wdpa.gdb"), shell: """ curl -sSLo "{output.zipfile}" "https://surfdrive.surf.nl/files/index.php/s/msuXQETNbCWawDh/download" - python "{input.script}" "{output.zipfile}" -t "results/module_area_potentials/resources/user/" + python "{input.script}" "{output.zipfile}" -t "results/integration_test/resources/user/" """ @@ -37,7 +37,7 @@ module module_area_potentials: config: config["module_area_potentials"] prefix: - "results/module_area_potentials/" + "results/integration_test/" # rename all module rules with a prefix, to avoid naming conflicts. @@ -50,6 +50,6 @@ rule all: "Run the module for the Netherlands shapes." default_target: True input: - "results/module_area_potentials/resources/user/shapes/NLD.parquet", - "results/module_area_potentials/resources/user/wdpa.gdb", - "results/module_area_potentials/results/NLD/area_potential_report.html", + "results/integration_test/resources/user/shapes/NLD.parquet", + "results/integration_test/resources/user/wdpa.gdb", + "results/integration_test/results/NLD/area_potential_report.html", From 287004f2992892b303717e965ebe608f4987e881 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 11 Jul 2025 09:18:03 +0000 Subject: [PATCH 34/59] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index cfd7c77..1fa5206 100644 --- a/docs/index.md +++ b/docs/index.md @@ -15,4 +15,4 @@ Please consult the [specification guidelines](./specification.md) and the [`clio * [GHSL (Global Human Settlement Layer)](https://human-settlement.emergency.copernicus.eu/download.php) built-up surface data (R2023, GHS-BUILT-S, 100m resolution) * License: "The GHSL has been produced by the EC JRC as open and free data. Reuse is authorised, provided the source is acknowledged." * [WDPA (World Database on Protected Areas)](https://www.protectedplanet.net/) - * License: Non-commercial allowed. Citation: "UNEP-WCMC and IUCN (2025), Protected Planet: The World Database on Protected Areas (WDPA) and World Database on Other Effective Area-based Conservation Measures (WD-OECM) [Online], June 2025, Cambridge, UK: UNEP-WCMC and IUCN. Available at: www.protectedplanet.net." \ No newline at end of file + * License: Non-commercial allowed. Citation: "UNEP-WCMC and IUCN (2025), Protected Planet: The World Database on Protected Areas (WDPA) and World Database on Other Effective Area-based Conservation Measures (WD-OECM) [Online], June 2025, Cambridge, UK: UNEP-WCMC and IUCN. Available at: www.protectedplanet.net." From 6e91b28a71cef1a196ef6c462573343fe7b3de0a Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Fri, 11 Jul 2025 11:23:49 +0200 Subject: [PATCH 35/59] Add missing pixi task update --- pixi.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pixi.toml b/pixi.toml index 5521eb1..bcfb004 100644 --- a/pixi.toml +++ b/pixi.toml @@ -21,7 +21,7 @@ snakefmt = ">=0.10.2" snakemake-minimal = ">=8.29.0" [tasks] -test = {cmd = "pytest tests/"} +test-integration = {cmd = "pytest tests/clio_test.py"} [feature.docs.dependencies] mkdocs-material = ">=9.6.7" From ea72cebaee5c5e865b1976fa8ed88b4705b839cc Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Fri, 11 Jul 2025 11:59:13 +0200 Subject: [PATCH 36/59] Update integration test Snakefile --- tests/integration/Snakefile | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/integration/Snakefile b/tests/integration/Snakefile index 81c0421..a0dfd21 100644 --- a/tests/integration/Snakefile +++ b/tests/integration/Snakefile @@ -7,6 +7,10 @@ rule download_netherlands_shapes: "Download and unzip the Netherlands shapes." output: "results/integration_test/resources/user/shapes/NLD.parquet", + log: + "results/integration_test/logs/download_netherlands_shapes.log", + conda: + "../../workflow/envs/shell.yaml" shell: """ curl -sSLo "{output}" "https://surfdrive.surf.nl/files/index.php/s/ey3RmiCbajp69oQ/download" @@ -21,10 +25,14 @@ rule download_netherlands_protected_areas: output: zipfile="results/integration_test/resources/user/wdpa.gdb.zip", wdpa=directory("results/integration_test/resources/user/wdpa.gdb"), + log: + "results/integration_test/logs/download_netherlands_protected_areas.log", + conda: + "../../workflow/envs/shell.yaml" shell: """ curl -sSLo "{output.zipfile}" "https://surfdrive.surf.nl/files/index.php/s/msuXQETNbCWawDh/download" - python "{input.script}" "{output.zipfile}" -t "results/integration_test/resources/user/" + python "{input.script}" "{output.zipfile}" -t "results/integration_test/resources/user/" 2> "{log}" """ From 50b6dce186a51903a40362bb2c8e090a4c9f3261 Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Fri, 11 Jul 2025 13:54:45 +0200 Subject: [PATCH 37/59] Separate download from unzip in integration test, scale down to 1 core --- tests/clio_test.py | 2 +- tests/integration/Snakefile | 23 ++++++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/tests/clio_test.py b/tests/clio_test.py index fcd4971..412b722 100644 --- a/tests/clio_test.py +++ b/tests/clio_test.py @@ -40,7 +40,7 @@ def test_standard_file_existance(module_path, file): def test_snakemake_all_failure(module_path): """The snakemake 'all' rule should return an error by default.""" process = subprocess.run( - "snakemake --cores 4", shell=True, cwd=module_path, capture_output=True + "snakemake --cores 1", shell=True, cwd=module_path, capture_output=True ) assert "INVALID (missing locally)" in str(process.stderr) diff --git a/tests/integration/Snakefile b/tests/integration/Snakefile index a0dfd21..62ad698 100644 --- a/tests/integration/Snakefile +++ b/tests/integration/Snakefile @@ -18,21 +18,34 @@ rule download_netherlands_shapes: rule download_netherlands_protected_areas: + message: + "Download and unzip a dummy drop-in dataset for Netherlands protected areas (not based on WDPA)." + output: + "results/integration_test/resources/user/wdpa.gdb.zip" + log: + "results/integration_test/logs/download_netherlands_protected_areas.log", + conda: + "../../workflow/envs/shell.yaml" + shell: + """ + curl -sSLo "{output}" "https://surfdrive.surf.nl/files/index.php/s/msuXQETNbCWawDh/download" + """ + +rule unzip_netherlands_protected_areas: message: "Download and unzip a dummy drop-in dataset for Netherlands protected areas (not based on WDPA)." input: script=workflow.source_path("../../workflow/scripts/unzip_like.py"), + zipfile=rules.download_netherlands_protected_areas.output, output: - zipfile="results/integration_test/resources/user/wdpa.gdb.zip", - wdpa=directory("results/integration_test/resources/user/wdpa.gdb"), + directory("results/integration_test/resources/user/wdpa.gdb"), log: - "results/integration_test/logs/download_netherlands_protected_areas.log", + "results/integration_test/logs/unzip_netherlands_protected_areas.log", conda: "../../workflow/envs/shell.yaml" shell: """ - curl -sSLo "{output.zipfile}" "https://surfdrive.surf.nl/files/index.php/s/msuXQETNbCWawDh/download" - python "{input.script}" "{output.zipfile}" -t "results/integration_test/resources/user/" 2> "{log}" + python "{input.script}" "{input.zipfile}" -t "results/integration_test/resources/user/" 2> "{log}" """ From 834ff15660fcee4f2c6577df12ae3deb3860d454 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 11 Jul 2025 11:54:55 +0000 Subject: [PATCH 38/59] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/integration/Snakefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration/Snakefile b/tests/integration/Snakefile index 62ad698..9bec01c 100644 --- a/tests/integration/Snakefile +++ b/tests/integration/Snakefile @@ -21,7 +21,7 @@ rule download_netherlands_protected_areas: message: "Download and unzip a dummy drop-in dataset for Netherlands protected areas (not based on WDPA)." output: - "results/integration_test/resources/user/wdpa.gdb.zip" + "results/integration_test/resources/user/wdpa.gdb.zip", log: "results/integration_test/logs/download_netherlands_protected_areas.log", conda: @@ -31,6 +31,7 @@ rule download_netherlands_protected_areas: curl -sSLo "{output}" "https://surfdrive.surf.nl/files/index.php/s/msuXQETNbCWawDh/download" """ + rule unzip_netherlands_protected_areas: message: "Download and unzip a dummy drop-in dataset for Netherlands protected areas (not based on WDPA)." From 4cb8bf681026b3d7b8163c5d32b1ad46c76e8380 Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Fri, 11 Jul 2025 14:13:12 +0200 Subject: [PATCH 39/59] One core only to avoid parallelism on Windows --- tests/clio_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/clio_test.py b/tests/clio_test.py index 412b722..d810e92 100644 --- a/tests/clio_test.py +++ b/tests/clio_test.py @@ -48,7 +48,7 @@ def test_snakemake_all_failure(module_path): def test_snakemake_integration_testing(module_path): """Run a light-weight test simulating someone using this module.""" assert subprocess.run( - "snakemake --use-conda --cores 4", + "snakemake --use-conda --cores 1", shell=True, check=True, cwd=module_path / "tests/integration", From 48c520f900b6aecafc250fee78a2fe781625beb1 Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Fri, 11 Jul 2025 15:06:45 +0200 Subject: [PATCH 40/59] Improve unzip_like and remove bash-based tempdirs --- tests/integration/Snakefile | 4 ++-- workflow/rules/automatic.smk | 18 ++++++++---------- workflow/scripts/unzip_like.py | 28 +++++++++++++++++++++++----- 3 files changed, 33 insertions(+), 17 deletions(-) diff --git a/tests/integration/Snakefile b/tests/integration/Snakefile index 9bec01c..e747415 100644 --- a/tests/integration/Snakefile +++ b/tests/integration/Snakefile @@ -19,7 +19,7 @@ rule download_netherlands_shapes: rule download_netherlands_protected_areas: message: - "Download and unzip a dummy drop-in dataset for Netherlands protected areas (not based on WDPA)." + "Download a dummy drop-in dataset for Netherlands protected areas (not based on WDPA)." output: "results/integration_test/resources/user/wdpa.gdb.zip", log: @@ -34,7 +34,7 @@ rule download_netherlands_protected_areas: rule unzip_netherlands_protected_areas: message: - "Download and unzip a dummy drop-in dataset for Netherlands protected areas (not based on WDPA)." + "Download protected areas data." input: script=workflow.source_path("../../workflow/scripts/unzip_like.py"), zipfile=rules.download_netherlands_protected_areas.output, diff --git a/workflow/rules/automatic.smk b/workflow/rules/automatic.smk index 490ef32..af8ff80 100644 --- a/workflow/rules/automatic.smk +++ b/workflow/rules/automatic.smk @@ -43,7 +43,9 @@ rule download_globcover: conda: "../envs/shell.yaml" shell: - 'curl -sSLo {output} "{params.url}"' + """ + curl -sSLo "{output}" "{params.url}" + """ rule unzip_globcover: @@ -62,10 +64,7 @@ rule unzip_globcover: "../envs/shell.yaml" shell: """ - temp_dir=$(mktemp -d) - python {input.script} {input.zipfile} -f {params.target_file} -t $temp_dir - mv $temp_dir/{params.target_file} {output} - rm -R $temp_dir + python "{input.script}" "{input.zipfile}" -f "{params.target_file}" -o "{output}" 2> "{log}" """ @@ -81,7 +80,9 @@ rule download_ghsl: conda: "../envs/shell.yaml" shell: - 'curl -sSLo {output} "{params.url}"' + """ + curl -sSLo "{output}" "{params.url}" + """ rule unzip_ghsl: @@ -100,8 +101,5 @@ rule unzip_ghsl: "../envs/shell.yaml" shell: """ - temp_dir=$(mktemp -d) - python {input.script} {input.zipfile} -f {params.target_file} -t $temp_dir - mv $temp_dir/{params.target_file} {output} - rm -R $temp_dir + python "{input.script}" "{input.zipfile}" -f "{params.target_file}" -o "{output}" 2> "{log}" """ diff --git a/workflow/scripts/unzip_like.py b/workflow/scripts/unzip_like.py index 028c670..e1bbd0f 100644 --- a/workflow/scripts/unzip_like.py +++ b/workflow/scripts/unzip_like.py @@ -1,6 +1,7 @@ """Emulates the `unzip` command across platforms.""" import os +import shutil import zipfile import click @@ -15,8 +16,18 @@ default=".", help="Target directory to extract to.", ) -@click.option("--file", "-f", help="Specific file inside the zip to extract.") -def unzip(zip_path, target, file): +@click.option( + "--file", + "-f", + help="Specific file inside the zip to extract.", +) +@click.option( + "--output", + "-o", + type=click.Path(), + help="Output filename to save the extracted file as (used with -f).", +) +def unzip(zip_path, target, file, output): """Emulates the `unzip` command across platforms. ZIP_PATH: Path to the .zip file @@ -25,12 +36,19 @@ def unzip(zip_path, target, file): with zipfile.ZipFile(zip_path, "r") as zip_ref: if file: - # Check if file exists in zip if file not in zip_ref.namelist(): click.echo(f"Error: '{file}' not found in archive.") return - zip_ref.extract(file, target) - click.echo(f"Extracted '{file}' to '{target}'.") + + extracted_path = zip_ref.extract(file, target) + + if output: + output_path = os.path.join(target, output) + os.makedirs(os.path.dirname(output_path), exist_ok=True) + shutil.move(extracted_path, output_path) + click.echo(f"Extracted '{file}' to '{output_path}'.") + else: + click.echo(f"Extracted '{file}' to '{extracted_path}'.") else: zip_ref.extractall(target) click.echo(f"Extracted all files to '{target}'.") From 4ca80ae7ae8887a871e6eba8bdfc52e2cbc431a4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 11 Jul 2025 13:06:55 +0000 Subject: [PATCH 41/59] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- workflow/scripts/unzip_like.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/workflow/scripts/unzip_like.py b/workflow/scripts/unzip_like.py index e1bbd0f..29c57f0 100644 --- a/workflow/scripts/unzip_like.py +++ b/workflow/scripts/unzip_like.py @@ -16,11 +16,7 @@ default=".", help="Target directory to extract to.", ) -@click.option( - "--file", - "-f", - help="Specific file inside the zip to extract.", -) +@click.option("--file", "-f", help="Specific file inside the zip to extract.") @click.option( "--output", "-o", From b031d796e324e291de2ebb3e1c164c3d7e9283cb Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Fri, 11 Jul 2025 16:11:57 +0200 Subject: [PATCH 42/59] Move shell-based raster clipping to Python script --- workflow/rules/prepare.smk | 6 +++-- workflow/scripts/clip_raster.py | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 workflow/scripts/clip_raster.py diff --git a/workflow/rules/prepare.smk b/workflow/rules/prepare.smk index 27c7d1a..08f8f19 100644 --- a/workflow/rules/prepare.smk +++ b/workflow/rules/prepare.smk @@ -5,6 +5,7 @@ rule cutout_landcover: message: "Cut land cover data to the bounds of the input shapefile." input: + script=workflow.source_path("../scripts/clip_raster.py"), shapes="resources/user/shapes/{shape}.parquet", landcover=rules.unzip_globcover.output, output: @@ -15,7 +16,7 @@ rule cutout_landcover: "../envs/default.yaml" shell: """ - rio clip --overwrite "{input.landcover}" "{output}" --bounds "$(fio info '{input.shapes}' --bounds)" + python "{input.script}" "{input.landcover}" "{input.shapes}" "{output}" 2> "{log}" """ @@ -23,6 +24,7 @@ rule cutout_settlement: message: "Cut settlement data to the bounds of the input shapefile." input: + script=workflow.source_path("../scripts/clip_raster.py"), shapes="resources/user/shapes/{shape}.parquet", settlement=rules.unzip_ghsl.output, output: @@ -33,5 +35,5 @@ rule cutout_settlement: "../envs/default.yaml" shell: """ - rio clip --overwrite "{input.settlement}" "{output}" --bounds "$(fio info '{input.shapes}' --bounds)" + python "{input.script}" "{input.settlement}" "{input.shapes}" "{output}" 2> "{log}" """ diff --git a/workflow/scripts/clip_raster.py b/workflow/scripts/clip_raster.py new file mode 100644 index 0000000..73b424c --- /dev/null +++ b/workflow/scripts/clip_raster.py @@ -0,0 +1,42 @@ +"""Clip raster files based on the bounding box from a parquet shapefile.""" + +import subprocess + +import click + + +@click.command() +@click.argument("input_tif", type=click.Path(exists=True)) +@click.argument("input_parquet", type=click.Path(exists=True)) +@click.argument("output_tif", type=click.Path()) +def clip_raster(input_tif, input_parquet, output_tif): + """Clip INPUT_TIF using the bounding box from INPUT_PARQUET and save as OUTPUT_TIF. + + This script calls 'fio' and 'rio' directly, assuming they are installed. + + """ + try: + # Step 1: Get bounds from the input_parquet using fio + fio_cmd = ["fio", "info", input_parquet, "--bounds"] + result = subprocess.run(fio_cmd, capture_output=True, text=True, check=True) + bounds = result.stdout.strip() + + # Step 2: Run rio clip with the bounds obtained + rio_cmd = [ + "rio", + "clip", + "--overwrite", + input_tif, + output_tif, + "--bounds", + bounds, + ] + subprocess.run(rio_cmd, check=True) + + except subprocess.CalledProcessError as e: + click.echo(f"Error running command: {e.cmd}", err=True) + click.echo(e.stderr, err=True) + + +if __name__ == "__main__": + clip_raster() From 862611c15123f9afef154ae7e1e6b7be6abdf0d3 Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Fri, 11 Jul 2025 16:20:05 +0200 Subject: [PATCH 43/59] Remove 'set -x' --- workflow/rules/process.smk | 2 -- 1 file changed, 2 deletions(-) diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index e86f5f7..fb4fca6 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -21,7 +21,6 @@ rule prepare_resampled_inputs: "../envs/default.yaml" shell: """ - set -x python "{input.script}" \ "{input.shapes}" "{input.land_cover_path}" "{input.slope_path}" "{input.settlement_path}" "{input.bathymetry_path}" "{input.protected_area_path}" \ "{output.resampled_input}" "{output.plot}" 2> "{log}" @@ -50,7 +49,6 @@ rule area_potential: "../envs/default.yaml" shell: """ - set -x python "{input.script}" "{input.shapes}" "{input.resampled_path}" "{params.config}" "{params.buffer_crs}" "{output.area_potential}" "{output.plot}" 2> "{log}" """ From d36bbd06478c216722b966bf6859f50403d80bde Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Tue, 15 Jul 2025 12:51:26 +0200 Subject: [PATCH 44/59] Land cover types configurable, memory improvements --- config/config.yaml | 25 ++++++ workflow/internal/config.schema.yaml | 6 ++ workflow/rules/process.smk | 3 + workflow/scripts/resample.py | 114 +++++++++++++++------------ 4 files changed, 97 insertions(+), 51 deletions(-) diff --git a/config/config.yaml b/config/config.yaml index 14d0224..a0d880c 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -4,6 +4,31 @@ # A good option is "epsg:8857" (WGS 84 / Equal Earth Greenwich) for global coverage buffer_crs: "epsg:8857" +land_cover_types: + POST_FLOODING: FARM + RAINFED_CROPLANDS: FARM + MOSAIC_CROPLAND: FARM + MOSAIC_VEGETATION: FARM + CLOSED_TO_OPEN_BROADLEAVED_FOREST: FOREST + CLOSED_BROADLEAVED_FOREST: FOREST + OPEN_BROADLEAVED_FOREST: FOREST + CLOSED_NEEDLELEAVED_FOREST: FOREST + OPEN_NEEDLELEAVED_FOREST: FOREST + CLOSED_TO_OPEN_MIXED_FOREST: FOREST + MOSAIC_FOREST: FOREST + CLOSED_TO_OPEN_REGULARLY_FLOODED_FOREST: FOREST + CLOSED_REGULARLY_FLOODED_FOREST: FOREST + MOSAIC_GRASSLAND: OTHER + CLOSED_TO_OPEN_SHRUBLAND: OTHER + CLOSED_TO_OPEN_HERBS: OTHER + SPARSE_VEGETATION: OTHER + CLOSED_TO_OPEN_REGULARLY_FLOODED_GRASSLAND: OTHER + BARE_AREAS: OTHER + ARTIFICIAL_SURFACES_AND_URBAN_AREAS: URBAN + WATER_BODIES: WATER + PERMANENT_SNOW: NOT_SUITABLE + NO_DATA: NOT_SUITABLE + techs: pv_rooftop: initial_area: settlement_area diff --git a/workflow/internal/config.schema.yaml b/workflow/internal/config.schema.yaml index fd0ef3b..ea7edc4 100644 --- a/workflow/internal/config.schema.yaml +++ b/workflow/internal/config.schema.yaml @@ -16,6 +16,12 @@ properties: required: [projection, resolution] additionalProperties: false + land_cover_types: + type: object + additionalProperties: + type: string + description: "Mapping of land cover types to their categories." + techs: type: object additionalProperties: diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index fb4fca6..9480981 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -1,6 +1,8 @@ rule prepare_resampled_inputs: message: "Resample inputs for {wildcards.shape} to the projection and resolution of the land cover data, while aggregating land cover types." + params: + land_cover_types_yaml_string=config["land_cover_types"], input: script=workflow.source_path("../scripts/resample.py"), shapes="resources/user/shapes/{shape}.parquet", @@ -23,6 +25,7 @@ rule prepare_resampled_inputs: """ python "{input.script}" \ "{input.shapes}" "{input.land_cover_path}" "{input.slope_path}" "{input.settlement_path}" "{input.bathymetry_path}" "{input.protected_area_path}" \ + "{params.land_cover_types_yaml_string}" \ "{output.resampled_input}" "{output.plot}" 2> "{log}" """ diff --git a/workflow/scripts/resample.py b/workflow/scripts/resample.py index 0295218..2d51f24 100644 --- a/workflow/scripts/resample.py +++ b/workflow/scripts/resample.py @@ -8,6 +8,7 @@ import rioxarray as rxr import script_utils import xarray as xr +import yaml from rasterio.enums import Resampling from rasterio.features import rasterize @@ -16,7 +17,7 @@ # From Troendle et al. (2019) https://github.com/timtroendle/possibility-for-electricity-autarky -GlobCover = { +GLOBCOVER_TYPES = { 11: "POST_FLOODING", 14: "RAINFED_CROPLANDS", 20: "MOSAIC_CROPLAND", @@ -32,57 +33,33 @@ 130: "CLOSED_TO_OPEN_SHRUBLAND", 140: "CLOSED_TO_OPEN_HERBS", 150: "SPARSE_VEGETATION", - 160: "CLOSED_TO_OPEN_REGULARLY_FLOODED_FOREST", # doesn't exist in Europe - 170: "CLOSED_REGULARLY_FLOODED_FOREST", # doesn't exist in Europe - 180: "CLOSED_TO_OPEN_REGULARLY_FLOODED_GRASSLAND", # roughly 2.3% of land in Europe - 190: "ARTIFICAL_SURFACES_AND_URBAN_AREAS", + 160: "CLOSED_TO_OPEN_REGULARLY_FLOODED_FOREST", + 170: "CLOSED_REGULARLY_FLOODED_FOREST", + 180: "CLOSED_TO_OPEN_REGULARLY_FLOODED_GRASSLAND", + 190: "ARTIFICIAL_SURFACES_AND_URBAN_AREAS", 200: "BARE_AREAS", 210: "WATER_BODIES", 220: "PERMANENT_SNOW", 230: "NO_DATA", } -CoverType = { - "POST_FLOODING": "FARM", - "RAINFED_CROPLANDS": "FARM", - "MOSAIC_CROPLAND": "FARM", - "MOSAIC_VEGETATION": "FARM", - "CLOSED_TO_OPEN_BROADLEAVED_FOREST": "FOREST", - "CLOSED_BROADLEAVED_FOREST": "FOREST", - "OPEN_BROADLEAVED_FOREST": "FOREST", - "CLOSED_NEEDLELEAVED_FOREST": "FOREST", - "OPEN_NEEDLELEAVED_FOREST": "FOREST", - "CLOSED_TO_OPEN_MIXED_FOREST": "FOREST", - "MOSAIC_FOREST": "FOREST", - "CLOSED_TO_OPEN_REGULARLY_FLOODED_FOREST": "FOREST", - "CLOSED_REGULARLY_FLOODED_FOREST": "FOREST", - "MOSAIC_GRASSLAND": "OTHER", # vegetation - "CLOSED_TO_OPEN_SHRUBLAND": "OTHER", # vegetation - "CLOSED_TO_OPEN_HERBS": "OTHER", # vegetation - "SPARSE_VEGETATION": "OTHER", # vegetation - "CLOSED_TO_OPEN_REGULARLY_FLOODED_GRASSLAND": "OTHER", # vegetation - "BARE_AREAS": "OTHER", - "ARTIFICAL_SURFACES_AND_URBAN_AREAS": "URBAN", - "WATER_BODIES": "WATER", - "PERMANENT_SNOW": "NOT_SUITABLE", - "NO_DATA": "NOT_SUITABLE", -} - -def get_suitable_land_cover_type(ds_land_cover, suitable_land_cover_types): +def aggregate_land_cover_types(ds_land_cover, land_cover_types): """Convert raw GlobCover data to a dataset with suitable land cover types.""" suitable_land_cover = xr.Dataset(coords=ds_land_cover.coords) # convert the input value to land cover type of interest for value in np.unique(ds_land_cover.data): - if value in GlobCover: + if value in GLOBCOVER_TYPES: ds_land_cover = ds_land_cover.where( - ds_land_cover != value, other=CoverType[GlobCover[value]], drop=False + ds_land_cover != value, + other=land_cover_types[GLOBCOVER_TYPES[value]], + drop=False, ) # check if each pixel is in the list of suitable land cover types - for type in suitable_land_cover_types: - suitable_land_cover[type] = (ds_land_cover == type).astype(float) + for type_ in sorted(list(set(land_cover_types.values()))): + suitable_land_cover[type_] = (ds_land_cover == type_).astype(np.byte) return suitable_land_cover @@ -159,6 +136,7 @@ def _rasterize_regions(shapes, reference_raster): @click.argument("settlement_path", type=str) @click.argument("bathymetry_path", type=str) @click.argument("protected_area_path", type=str) +@click.argument("land_cover_configuration_yaml_string", type=str) @click.argument("output_path", type=str) @click.argument("plot_path", type=str) def resample_inputs( @@ -168,6 +146,7 @@ def resample_inputs( settlement_path, bathymetry_path, protected_area_path, + land_cover_configuration_yaml_string, output_path, plot_path, ): @@ -178,23 +157,21 @@ def resample_inputs( """ shapes = gpd.read_parquet(shapes_path) + resampled = xr.Dataset() ## # Land cover ## - suitable_land_cover_types = sorted(list(set(CoverType.values()))) ds_land_cover = rxr.open_rasterio(land_cover_path) - reference_raster = xr.ones_like(ds_land_cover) + land_cover_types = yaml.safe_load(land_cover_configuration_yaml_string) + reference_raster = xr.ones_like(ds_land_cover, dtype=np.byte) reference_resolution = ds_land_cover.rio.resolution() - print(f"Land cover resolution: {reference_resolution}") - land_cover = get_suitable_land_cover_type(ds_land_cover, suitable_land_cover_types) - - resampled = xr.Dataset() + print(f"Land cover resolution used as reference resolution: {reference_resolution}") + land_cover = aggregate_land_cover_types(ds_land_cover, land_cover_types) - for land_type in suitable_land_cover_types: - resampled[f"landcover_{land_type}"] = land_cover[land_type].rio.reproject_match( - reference_raster, resampling=Resampling.average - ) + for land_type in sorted(list(set(land_cover_types.values()))): + resampled[f"landcover_{land_type}"] = land_cover[land_type] + del ds_land_cover, land_cover ## # Pixel area @@ -204,6 +181,7 @@ def resample_inputs( resampled["pixel_area"] = pixel_area.expand_dims({"x": resampled.x}).transpose( "y", "x" ) + del pixel_area ## # Regions @@ -227,17 +205,21 @@ def resample_inputs( dims=resampled["regions"].dims, coords=resampled["regions"].coords, ) - resampled["regions_land"] = xr.where(mask_land, 1.0, np.nan) - resampled["regions_maritime"] = xr.where(mask_maritime, 1.0, np.nan) + resampled["regions_land"] = xr.where(mask_land, np.half(1.0), np.half(np.nan)) + resampled["regions_maritime"] = xr.where( + mask_maritime, np.half(1.0), np.half(np.nan) + ) + del mask_land, mask_maritime ## # Slope ## da_slope = rxr.open_rasterio(slope_path, masked=True) / 100 print(f"Slope resolution: {da_slope.rio.resolution()}") - resampled["slope"] = da_slope.astype(float).rio.reproject_match( + resampled["slope"] = da_slope.rio.reproject_match( reference_raster, resampling=Resampling.average ) + del da_slope ## # Settlement in sum of area of built-up surface (m2) @@ -260,6 +242,7 @@ def resample_inputs( resampled["settlement_area"] = ( resampled["settlement_share"] * resampled["pixel_area"] ) + del ds_settlement, ds_settlement_pixel_area ## # Bathymetry @@ -272,6 +255,7 @@ def resample_inputs( resampled["bathymetry"] = ds_bathymetry.rio.reproject_match( reference_raster, resampling=Resampling.average ) + del ds_bathymetry ## # Protected areas @@ -287,14 +271,42 @@ def resample_inputs( protected_areas.geometry, protected_areas.crs ) resampled["protected"] = resampled["protected"].fillna(0) + del protected_areas - compression = { + netcdf4_encoding = { var: {"zlib": True, "complevel": 1} for var in resampled.data_vars if var not in ["spatial_ref", "band"] } + for v in ["regions_land", "regions_maritime"]: + netcdf4_encoding[v]["dtype"] = "int8" + netcdf4_encoding[v]["scale_factor"] = 1 + netcdf4_encoding[v]["add_offset"] = 0 + netcdf4_encoding[v]["_FillValue"] = -128 + + print("Saving result to output path:", output_path) + resampled.to_netcdf(output_path, encoding=netcdf4_encoding) + + print("Saving image to plot path:", plot_path) + # If needed, resample `resampled` to fit within a maximum of `max_pixels` pixels + max_pixels = 1000000 + total_pixels = resampled.sizes["y"] * resampled.sizes["x"] + if total_pixels > max_pixels: + # Calculate the new resolution to fit within the max_pixels limit + resolution_multiplier = 1 / math.sqrt(total_pixels / max_pixels) + new_y_size = int(resampled.sizes["y"] * resolution_multiplier) + new_x_size = int(resampled.sizes["x"] * resolution_multiplier) + print( + f"Resampling old size {resampled.sizes['y']} x {resampled.sizes['x']} " + f"to new size: {new_y_size} x {new_x_size} " + f"to fit within {max_pixels} pixels." + ) - resampled.to_netcdf(output_path, encoding=compression) + resampled = resampled.coarsen( + x=round(resampled.sizes["x"] / new_x_size), + y=round(resampled.sizes["y"] / new_y_size), + boundary="trim", + ).mean() script_utils.plot_all_dataset_variables(resampled, ncols=3, savefig=plot_path) From 8879741fb2f68aa929d0ff8b281798098c136e78 Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Tue, 15 Jul 2025 14:14:36 +0200 Subject: [PATCH 45/59] Update test config; increase figure resolution --- tests/integration/test_config.yaml | 29 +++++++++++++++++++++++++---- workflow/scripts/resample.py | 8 ++++---- workflow/scripts/script_utils.py | 2 +- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/tests/integration/test_config.yaml b/tests/integration/test_config.yaml index be428b8..e7ad63a 100644 --- a/tests/integration/test_config.yaml +++ b/tests/integration/test_config.yaml @@ -1,10 +1,31 @@ module_area_potentials: - # Options for buffering: either a string of the form "epsg:xxxx" or "UTM"/"utm" - # - "UTM": project each shape to the UTM zone of its centroid for buffering - # - "epsg:xxxx": use the specified CRS for all buffering - # A good option is "epsg:8857" (WGS 84 / Equal Earth Greenwich) for global coverage buffer_crs: "epsg:8857" + land_cover_types: + POST_FLOODING: FARM + RAINFED_CROPLANDS: FARM + MOSAIC_CROPLAND: FARM + MOSAIC_VEGETATION: FARM + CLOSED_TO_OPEN_BROADLEAVED_FOREST: FOREST + CLOSED_BROADLEAVED_FOREST: FOREST + OPEN_BROADLEAVED_FOREST: FOREST + CLOSED_NEEDLELEAVED_FOREST: FOREST + OPEN_NEEDLELEAVED_FOREST: FOREST + CLOSED_TO_OPEN_MIXED_FOREST: FOREST + MOSAIC_FOREST: FOREST + CLOSED_TO_OPEN_REGULARLY_FLOODED_FOREST: FOREST + CLOSED_REGULARLY_FLOODED_FOREST: FOREST + MOSAIC_GRASSLAND: OTHER + CLOSED_TO_OPEN_SHRUBLAND: OTHER + CLOSED_TO_OPEN_HERBS: OTHER + SPARSE_VEGETATION: OTHER + CLOSED_TO_OPEN_REGULARLY_FLOODED_GRASSLAND: OTHER + BARE_AREAS: OTHER + ARTIFICIAL_SURFACES_AND_URBAN_AREAS: URBAN + WATER_BODIES: WATER + PERMANENT_SNOW: NOT_SUITABLE + NO_DATA: NOT_SUITABLE + techs: pv_rooftop: initial_area: settlement_area diff --git a/workflow/scripts/resample.py b/workflow/scripts/resample.py index 2d51f24..0e2f3b3 100644 --- a/workflow/scripts/resample.py +++ b/workflow/scripts/resample.py @@ -107,9 +107,9 @@ def determine_pixel_areas(raster_input): """ # the following is based on https://gis.stackexchange.com/a/288034/77760 # and assumes the data to be in EPSG:4326 - assert raster_input.rio.crs.to_epsg() == 4326, ( - "raster_input does not have the projection EPSG:4326" - ) + assert ( + raster_input.rio.crs.to_epsg() == 4326 + ), "raster_input does not have the projection EPSG:4326" resolution = raster_input.rio.resolution()[0] # resolution in degrees varea_of_pixel = np.vectorize(lambda lat: _area_of_pixel(resolution, lat)) pixel_area = varea_of_pixel(raster_input.y) * 1000**2 # convert to m^2 @@ -289,7 +289,7 @@ def resample_inputs( print("Saving image to plot path:", plot_path) # If needed, resample `resampled` to fit within a maximum of `max_pixels` pixels - max_pixels = 1000000 + max_pixels = 5000000 total_pixels = resampled.sizes["y"] * resampled.sizes["x"] if total_pixels > max_pixels: # Calculate the new resolution to fit within the max_pixels limit diff --git a/workflow/scripts/script_utils.py b/workflow/scripts/script_utils.py index bba8d51..42d60b0 100644 --- a/workflow/scripts/script_utils.py +++ b/workflow/scripts/script_utils.py @@ -29,6 +29,6 @@ def plot_all_dataset_variables(ds, ncols=2, savefig=None): plt.tight_layout() if savefig: - plt.savefig(savefig, bbox_inches="tight") + plt.savefig(savefig, dpi=300, bbox_inches="tight") return fig From 5d04201df60364a699bb059d06a2e75b47e87ee0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 15 Jul 2025 12:16:54 +0000 Subject: [PATCH 46/59] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- workflow/scripts/resample.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/workflow/scripts/resample.py b/workflow/scripts/resample.py index 0e2f3b3..9756a56 100644 --- a/workflow/scripts/resample.py +++ b/workflow/scripts/resample.py @@ -107,9 +107,9 @@ def determine_pixel_areas(raster_input): """ # the following is based on https://gis.stackexchange.com/a/288034/77760 # and assumes the data to be in EPSG:4326 - assert ( - raster_input.rio.crs.to_epsg() == 4326 - ), "raster_input does not have the projection EPSG:4326" + assert raster_input.rio.crs.to_epsg() == 4326, ( + "raster_input does not have the projection EPSG:4326" + ) resolution = raster_input.rio.resolution()[0] # resolution in degrees varea_of_pixel = np.vectorize(lambda lat: _area_of_pixel(resolution, lat)) pixel_area = varea_of_pixel(raster_input.y) * 1000**2 # convert to m^2 From d18037d240e39cd1e2962cfddc9ae82f55ab1dbd Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Tue, 15 Jul 2025 14:48:02 +0200 Subject: [PATCH 47/59] Write CRS to TIFFs before saving Closes #2 --- workflow/scripts/area_potential.py | 1 + 1 file changed, 1 insertion(+) diff --git a/workflow/scripts/area_potential.py b/workflow/scripts/area_potential.py index 7aa8a72..f3215cf 100644 --- a/workflow/scripts/area_potential.py +++ b/workflow/scripts/area_potential.py @@ -95,6 +95,7 @@ def get_area_potential( potential_da.name = "area_potential" potential_da = potential_da.transpose("band", "y", "x") + potential_da.rio.write_crs(ds.rio.crs, inplace=True) potential_da.rio.to_raster(output_path, driver="GTiff", compress="LZW") potential_da.plot() From 5718c8ab2629ac3dbc1331ca09dbd3a4dab02e04 Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Tue, 22 Jul 2025 17:07:20 +0200 Subject: [PATCH 48/59] Allow workflow to break larger shapes into subunits; other improvements * Allow per-subunit tech configuration override * Clean up download rules, download full TIF files by default * Improve plotting --- config/config.yaml | 18 ++ tests/integration/test_config.yaml | 4 + workflow/Snakefile | 3 +- workflow/envs/default.yaml | 2 + workflow/internal/config.schema.yaml | 22 +- workflow/rules/automatic.smk | 203 ++++++++++++++++--- workflow/rules/functions.smk | 8 + workflow/rules/prepare.smk | 39 ---- workflow/rules/process.smk | 83 ++++++-- workflow/scripts/area_potential.py | 24 ++- workflow/scripts/breakup_shape.py | 55 +++++ workflow/scripts/clip_and_rasterise_polys.py | 34 ++++ workflow/scripts/report.py | 25 ++- workflow/scripts/resample.py | 21 +- workflow/scripts/script_utils.py | 30 ++- 15 files changed, 457 insertions(+), 114 deletions(-) create mode 100644 workflow/rules/functions.smk delete mode 100644 workflow/rules/prepare.smk create mode 100644 workflow/scripts/breakup_shape.py create mode 100644 workflow/scripts/clip_and_rasterise_polys.py diff --git a/config/config.yaml b/config/config.yaml index a0d880c..8d2235c 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -1,9 +1,13 @@ +tiny_files: False + # Options for buffering: either a string of the form "epsg:xxxx" or "UTM"/"utm" # - "UTM": project each shape to the UTM zone of its centroid for buffering # - "epsg:xxxx": use the specified CRS for all buffering # A good option is "epsg:8857" (WGS 84 / Equal Earth Greenwich) for global coverage buffer_crs: "epsg:8857" +split_by: country_id + land_cover_types: POST_FLOODING: FARM RAINFED_CROPLANDS: FARM @@ -99,3 +103,17 @@ techs: protected: 0 shapes_buffer: land: 10000 # meters + +# Optional: override settings for specific subunits (countries, regions, etc.) +# This allows you to set specific parameters that differ from the defaults, +# or apply settings that are not defined in the defaults (defaults = `techs` section). +# The subunit keys should match the subunit IDs used in column selected in `split_by`. +overrides: + PRT: # Inside a subunit, any setting from `techs` can be overridden + wind_offshore: + shapes_buffer: + land: 2000 # meters + NLD: + wind_offshore: + shapes_buffer: + land: 22000 # 22 km = 12 nautical miles diff --git a/tests/integration/test_config.yaml b/tests/integration/test_config.yaml index e7ad63a..ecd6bf1 100644 --- a/tests/integration/test_config.yaml +++ b/tests/integration/test_config.yaml @@ -1,5 +1,9 @@ + + module_area_potentials: + tiny_files: True buffer_crs: "epsg:8857" + split_by: country_id land_cover_types: POST_FLOODING: FARM diff --git a/workflow/Snakefile b/workflow/Snakefile index 791925e..ea55963 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -23,12 +23,13 @@ workflow.source_path("scripts/geo.py") wildcard_constraints: shape="[a-zA-Z0-9_-]+", + subunit="[a-zA-Z0-9_]+", tech="|".join(config["techs"].keys()), # Add all your includes here. +include: "rules/functions.smk" include: "rules/automatic.smk" -include: "rules/prepare.smk" include: "rules/process.smk" diff --git a/workflow/envs/default.yaml b/workflow/envs/default.yaml index 0f99c18..a9a15e3 100644 --- a/workflow/envs/default.yaml +++ b/workflow/envs/default.yaml @@ -19,3 +19,5 @@ dependencies: - pyyaml - pyproj=3.7.1 - utm=0.7.0 + - glom=24.11.0 + - dask=2025.7.0 diff --git a/workflow/internal/config.schema.yaml b/workflow/internal/config.schema.yaml index ea7edc4..0ff7423 100644 --- a/workflow/internal/config.schema.yaml +++ b/workflow/internal/config.schema.yaml @@ -3,18 +3,17 @@ description: "Schema for user-provided configuration files." type: object additionalProperties: false properties: + tiny_files: + type: boolean + description: "If True, use smaller, clipped files for processing. If False, use full global datasets." + buffer_crs: type: string description: "CRS for buffering shapes. Use 'UTM' for UTM zones or 'epsg:xxxx' for a specific CRS." - specs: - type: object - properties: - projection: - type: string - resolution: - type: number - required: [projection, resolution] - additionalProperties: false + + split_by: + type: string + description: "Field to split the input shapes by, e.g., 'country_id'." land_cover_types: type: object @@ -48,3 +47,8 @@ properties: type: object required: ["initial_area", "continuous_layers", "binary_layers"] additionalProperties: false + + overrides: + type: object + additionalProperties: + type: object diff --git a/workflow/rules/automatic.smk b/workflow/rules/automatic.smk index af8ff80..7b8ca5e 100644 --- a/workflow/rules/automatic.smk +++ b/workflow/rules/automatic.smk @@ -1,34 +1,117 @@ """Rules to used to download automatic resource files.""" +if config["tiny_files"]: -rule download_cutout_slope: - message: - "Download slope data covering the bounds of the input shapefile." - params: - cog_url=internal["resources"]["automatic"]["slope"], - input: - vector="resources/user/shapes/{shape}.parquet", - output: - path="resources/automatic/cutout/{shape}/slope.tif", - log: - "logs/{shape}/download_cutout_slope.log", - wrapper: - "v7.2.0/geo/rasterio/clip-geotiff" + ## + # Directly download clipped slope and bathymetry data + ## + rule clip_slope: + message: + "Download slope data covering the bounds of the input shapefile." + params: + cog_url=internal["resources"]["automatic"]["slope"], + input: + vector="resources/user/shapes/{shape}.parquet", + output: + path="resources/automatic/cutout/{shape}/slope.tif", + log: + "logs/{shape}/clip_slope.log", + wrapper: + "v7.2.0/geo/rasterio/clip-geotiff" -rule download_cutout_bathymetry: - message: - "Download bathymetry data covering the bounds of the input shapefile." - params: - cog_url=internal["resources"]["automatic"]["bathymetry"], - input: - vector="resources/user/shapes/{shape}.parquet", - output: - path="resources/automatic/cutout/{shape}/bathymetry.tif", - log: - "logs/{shape}/download_cutout_bathymetry.log", - wrapper: - "v7.2.0/geo/rasterio/clip-geotiff" + rule clip_bathymetry: + message: + "Download bathymetry data covering the bounds of the input shapefile." + params: + cog_url=internal["resources"]["automatic"]["bathymetry"], + input: + vector="resources/user/shapes/{shape}.parquet", + output: + path="resources/automatic/cutout/{shape}/bathymetry.tif", + log: + "logs/{shape}/clip_bathymetry.log", + wrapper: + "v7.2.0/geo/rasterio/clip-geotiff" + +else: + + ## + # Download global slope and bathymetry data, then clip the files locally + ## + + rule download_slope: + message: + "Download global slope data." + params: + url=internal["resources"]["automatic"]["slope"], + output: + path="resources/automatic/global/slope.tif", + log: + "logs/download_slope.log", + conda: + "../envs/shell.yaml" + shell: + """ + curl -sSLo "{output}" "{params.url}" + """ + + rule download_bathymetry: + message: + "Download global bathymetry data." + params: + url=internal["resources"]["automatic"]["bathymetry"], + output: + path="resources/automatic/global/bathymetry.tif", + log: + "logs/download_bathymetry.log", + conda: + "../envs/shell.yaml" + shell: + """ + curl -sSLo "{output}" "{params.url}" + """ + + rule clip_slope: + message: + "Cut slope data to the bounds of the input shapefile." + input: + script=workflow.source_path("../scripts/clip_raster.py"), + shapes="resources/user/shapes/{shape}.parquet", + slope=rules.download_slope.output, + output: + "resources/automatic/cutout/{shape}/slope.tif", + log: + "logs/{shape}/clip_slope.log", + conda: + "../envs/default.yaml" + shell: + """ + python "{input.script}" "{input.slope}" "{input.shapes}" "{output}" 2> "{log}" + """ + + rule clip_bathymetry: + message: + "Cut bathymetry data to the bounds of the input shapefile." + input: + script=workflow.source_path("../scripts/clip_raster.py"), + shapes="resources/user/shapes/{shape}.parquet", + bathymetry=rules.download_bathymetry.output, + output: + "resources/automatic/cutout/{shape}/bathymetry.tif", + log: + "logs/{shape}/clip_bathymetry.log", + conda: + "../envs/default.yaml" + shell: + """ + python "{input.script}" "{input.bathymetry}" "{input.shapes}" "{output}" 2> "{log}" + """ + + +## +# Globcover +## rule download_globcover: @@ -68,6 +151,30 @@ rule unzip_globcover: """ +rule clip_landcover: + message: + "Cut land cover data to the bounds of the input shapefile." + input: + script=workflow.source_path("../scripts/clip_raster.py"), + shapes="resources/user/shapes/{shape}.parquet", + landcover=rules.unzip_globcover.output, + output: + "resources/automatic/cutout/{shape}/landcover.tif", + log: + "logs/{shape}/clip_landcover.log", + conda: + "../envs/default.yaml" + shell: + """ + python "{input.script}" "{input.landcover}" "{input.shapes}" "{output}" 2> "{log}" + """ + + +## +# Global Human Settlement Layer (GHSL) +## + + rule download_ghsl: message: "Download the GHSL (Global Human Settlement Layer) built-up surface data." @@ -103,3 +210,47 @@ rule unzip_ghsl: """ python "{input.script}" "{input.zipfile}" -f "{params.target_file}" -o "{output}" 2> "{log}" """ + + +rule clip_settlement: + message: + "Cut settlement data to the bounds of the input shapefile." + input: + script=workflow.source_path("../scripts/clip_raster.py"), + shapes="resources/user/shapes/{shape}.parquet", + settlement=rules.unzip_ghsl.output, + output: + "resources/automatic/cutout/{shape}/settlement.tif", + log: + "logs/{shape}/clip_settlement.log", + conda: + "../envs/default.yaml" + shell: + """ + python "{input.script}" "{input.settlement}" "{input.shapes}" "{output}" 2> "{log}" + """ + + +## +# Protected Areas (WDPA) +## + + +rule rasterise_clip_wdpa: + message: + "Rasterise and cut WDPA data to the bounds of the input shapefile, using the landcover raster as reference for the rasterisation." + input: + script=workflow.source_path("../scripts/clip_and_rasterise_polys.py"), + shapes="resources/user/shapes/{shape}.parquet", + reference_raster=rules.clip_landcover.output, + protected_areas="resources/user/wdpa.gdb", + output: + "resources/automatic/cutout/{shape}/wdpa.tif", + log: + "logs/{shape}/clip_wdpa.log", + conda: + "../envs/default.yaml" + shell: + """ + python "{input.script}" "{input.shapes}" "{input.reference_raster}" "{input.protected_areas}" "{output}" 2> "{log}" + """ diff --git a/workflow/rules/functions.smk b/workflow/rules/functions.smk new file mode 100644 index 0000000..238da9e --- /dev/null +++ b/workflow/rules/functions.smk @@ -0,0 +1,8 @@ +def get_subunits(wildcards): + checkpoint_output = checkpoints.breakup_shape.get(**wildcards).output[0] + return expand( + "results/{{shape}}/{subunit}/area_potential_{{tech}}.tif", + subunit=glob_wildcards( + os.path.join(checkpoint_output, "{subunit}.parquet") + ).subunit, + ) diff --git a/workflow/rules/prepare.smk b/workflow/rules/prepare.smk deleted file mode 100644 index 08f8f19..0000000 --- a/workflow/rules/prepare.smk +++ /dev/null @@ -1,39 +0,0 @@ -"""Cut out the datasets to bounds determined by the input shapefile.""" - - -rule cutout_landcover: - message: - "Cut land cover data to the bounds of the input shapefile." - input: - script=workflow.source_path("../scripts/clip_raster.py"), - shapes="resources/user/shapes/{shape}.parquet", - landcover=rules.unzip_globcover.output, - output: - "resources/automatic/cutout/{shape}/landcover.tif", - log: - "logs/{shape}/cutout_landcover.log", - conda: - "../envs/default.yaml" - shell: - """ - python "{input.script}" "{input.landcover}" "{input.shapes}" "{output}" 2> "{log}" - """ - - -rule cutout_settlement: - message: - "Cut settlement data to the bounds of the input shapefile." - input: - script=workflow.source_path("../scripts/clip_raster.py"), - shapes="resources/user/shapes/{shape}.parquet", - settlement=rules.unzip_ghsl.output, - output: - "resources/automatic/cutout/{shape}/settlement.tif", - log: - "logs/{shape}/cutout_settlement.log", - conda: - "../envs/default.yaml" - shell: - """ - python "{input.script}" "{input.settlement}" "{input.shapes}" "{output}" 2> "{log}" - """ diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index 9480981..9e966b5 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -1,30 +1,51 @@ +checkpoint breakup_shape: + message: + "Break up {wildcards.shape} into the configured subunits." + params: + split_by=config["split_by"], + input: + script=workflow.source_path("../scripts/breakup_shape.py"), + shapes="resources/user/shapes/{shape}.parquet", + output: + directory("resources/automatic/shapes/{shape}"), + log: + "logs/{shape}/breakup_shape.log", + conda: + "../envs/default.yaml" + shell: + """ + python "{input.script}" "{input.shapes}" "{params.split_by}" "{output}" 2> "{log}" + """ + + rule prepare_resampled_inputs: message: - "Resample inputs for {wildcards.shape} to the projection and resolution of the land cover data, while aggregating land cover types." + "Resample inputs for {wildcards.subunit} in {wildcards.shape} to the projection and resolution of the land cover data, while aggregating land cover types." params: land_cover_types_yaml_string=config["land_cover_types"], input: script=workflow.source_path("../scripts/resample.py"), - shapes="resources/user/shapes/{shape}.parquet", - land_cover_path=rules.cutout_landcover.output, - slope_path=rules.download_cutout_slope.output, - settlement_path=rules.cutout_settlement.output, - bathymetry_path=rules.download_cutout_bathymetry.output, - protected_area_path="resources/user/wdpa.gdb", + # shapes="resources/automatic/shapes/{shape}/{subunit}.parquet", + shapes=rules.breakup_shape.output, + land_cover_path=rules.clip_landcover.output, + slope_path=rules.clip_slope.output, + settlement_path=rules.clip_settlement.output, + bathymetry_path=rules.clip_bathymetry.output, + protected_area_path=rules.rasterise_clip_wdpa.output, output: - resampled_input="resources/automatic/{shape}.resampled_inputs.nc", + resampled_input="resources/automatic/resampled_inputs/{shape}/{subunit}.nc", plot=report( - "resources/automatic/{shape}.resampled_inputs.png", + "resources/automatic/resampled_inputs/{shape}/{subunit}.png", category="resampled_input", ), log: - "logs/{shape}/prepare_resampled_inputs.log", + "logs/{shape}/{subunit}/prepare_resampled_inputs.log", conda: "../envs/default.yaml" shell: """ python "{input.script}" \ - "{input.shapes}" "{input.land_cover_path}" "{input.slope_path}" "{input.settlement_path}" "{input.bathymetry_path}" "{input.protected_area_path}" \ + "{input.shapes}/{wildcards.subunit}.parquet" "{input.land_cover_path}" "{input.slope_path}" "{input.settlement_path}" "{input.bathymetry_path}" "{input.protected_area_path}" \ "{params.land_cover_types_yaml_string}" \ "{output.resampled_input}" "{output.plot}" 2> "{log}" """ @@ -32,27 +53,48 @@ rule prepare_resampled_inputs: rule area_potential: message: - "Compute area potential for the tech {wildcards.tech} and shapes {wildcards.shape}." + "Compute area potential for the tech {wildcards.tech} and {wildcards.subunit} in {wildcards.shape}." params: config=lambda wildcards: config["techs"][f"{wildcards.tech}"], + subunit_override_config=lambda wildcards: config["overrides"] + .get(wildcards.subunit, {}) + .get(wildcards.tech, {}), buffer_crs=lambda wildcards: config["buffer_crs"], input: script=workflow.source_path("../scripts/area_potential.py"), - shapes="resources/user/shapes/{shape}.parquet", + # shapes="resources/automatic/shapes/{shape}/{subunit}.parquet", + shapes=rules.breakup_shape.output, resampled_path=rules.prepare_resampled_inputs.output.resampled_input, output: - area_potential="results/{shape}/area_potential_{tech}.tif", + area_potential="results/{shape}/{subunit}/area_potential_{tech}.tif", plot=report( - "results/{shape}/area_potential_{tech}.png", + "results/{shape}/{subunit}/area_potential_{tech}.png", category="area_potential", ), log: - "logs/{shape}/area_potential_{tech}.log", + "logs/{shape}/{subunit}/area_potential_{tech}.log", + conda: + "../envs/default.yaml" + shell: + """ + python "{input.script}" "{input.shapes}/{wildcards.subunit}.parquet" "{input.resampled_path}" "{params.config}" "{params.buffer_crs}" "{output.area_potential}" "{output.plot}" --override_config="{params.subunit_override_config}" 2> "{log}" + """ + + +rule aggregate_area_potential: + message: + "Aggregate area potential for the tech {wildcards.tech} in {wildcards.shape}." + input: + get_subunits, + output: + aggregated_area_potential="results/{shape}/area_potential_{tech}.tif", + log: + "logs/{shape}/aggregate_area_potential_{tech}.log", conda: "../envs/default.yaml" shell: """ - python "{input.script}" "{input.shapes}" "{input.resampled_path}" "{params.config}" "{params.buffer_crs}" "{output.area_potential}" "{output.plot}" 2> "{log}" + gdal_merge -o "{output.aggregated_area_potential}" -a_nodata -1 {input} """ @@ -61,7 +103,6 @@ rule area_potential_report: "Generate an overview report of the area potential for all techs in shapes {wildcards.shape}." input: shapes="resources/user/shapes/{shape}.parquet", - resampled_path=rules.prepare_resampled_inputs.output.resampled_input, area_potentials=expand( "results/{{shape}}/area_potential_{tech}.tif", tech=config["techs"].keys(), @@ -70,7 +111,11 @@ rule area_potential_report: csv="results/{shape}/area_potential_report.csv", html=report( "results/{shape}/area_potential_report.html", - category="area_potential_report", + category="area_potential_report_table", + ), + png=report( + "results/{shape}/area_potential_report.png", + category="area_potential_report_image", ), log: "logs/{shape}/area_potential_report.log", diff --git a/workflow/scripts/area_potential.py b/workflow/scripts/area_potential.py index f3215cf..0b75aa3 100644 --- a/workflow/scripts/area_potential.py +++ b/workflow/scripts/area_potential.py @@ -3,6 +3,7 @@ import click import geo import geopandas as gpd +import glom import matplotlib.pyplot as plt import xarray as xr import yaml @@ -15,8 +16,15 @@ @click.argument("buffer_crs", type=str) @click.argument("output_path", type=str) @click.argument("plot_path", type=str) +@click.option("--override_config", type=str) def get_area_potential( - shapes_path, resampled_path, config, buffer_crs, output_path, plot_path + shapes_path, + resampled_path, + config, + buffer_crs, + output_path, + plot_path, + override_config, ): """Calculate the area potential based on the provided configuration. @@ -27,6 +35,7 @@ def get_area_potential( buffer_crs (str): Coordinate Reference System for buffering shapes. output_path (str): Path to save the resulting area potential raster. plot_path (str): Path to save the plot of the area potential. + override_config (str): Configuration override YAML string. Returns: None @@ -37,6 +46,10 @@ def get_area_potential( # FIXME: this is a workaround for the CRS not being set correctly; not sure why ds.rio.write_crs(ds.spatial_ref.attrs["crs_wkt"], inplace=True) config = yaml.safe_load(config) + if override_config: + override_config = yaml.safe_load(override_config) + config = glom.merge([config, override_config]) + print(f"\nConfig after override: {config}\n") # Start with the configured pixel area as a base potential_da = ds[config["initial_area"]].squeeze(drop=True) # Drop `band` @@ -96,11 +109,18 @@ def get_area_potential( potential_da.name = "area_potential" potential_da = potential_da.transpose("band", "y", "x") potential_da.rio.write_crs(ds.rio.crs, inplace=True) - potential_da.rio.to_raster(output_path, driver="GTiff", compress="LZW") potential_da.plot() plt.savefig(plot_path, bbox_inches="tight") + # Fill NaN with a nodata value only after plotting + nodata_value = -1 + potential_da = potential_da.fillna(nodata_value) + potential_da.rio.write_nodata(nodata_value, inplace=True) + potential_da.rio.to_raster( + output_path, driver="GTiff", compress="LZW", write_nodata=True + ) + if __name__ == "__main__": get_area_potential() diff --git a/workflow/scripts/breakup_shape.py b/workflow/scripts/breakup_shape.py new file mode 100644 index 0000000..106f3a4 --- /dev/null +++ b/workflow/scripts/breakup_shape.py @@ -0,0 +1,55 @@ +"""Break up shape.""" + +from pathlib import Path + +import click +import geopandas as gpd + + +@click.command() +@click.argument("shapes_path", type=str) +@click.argument("split_by", type=str) +@click.argument("output_path", type=str) +# @click.argument("output_list_of_subunits", type=str) +def breakup_shape(shapes_path, split_by, output_path): # , output_list_of_subunits): + """Break subunits out of shape and save the resulting shapefiles. + + Args: + shapes_path (str): Path to the input shapes in the parquet format. + split_by (str): Column on which to split the shapes. + output_path (str): Path to save the resulting broken-up shapes. + output_list_of_subunits (str): Path to save the list of subunits. + + Returns: + None + """ + output_path = Path(output_path) + output_path.mkdir(parents=True, exist_ok=True) + shapes = gpd.read_parquet(shapes_path) + + # Print rows where geometry is empty + if shapes.geometry.is_empty.any(): + print("Warning: The following rows have empty geometries and will be removed:") + print(shapes[shapes.geometry.is_empty]) + shapes = shapes[~shapes.geometry.is_empty] + + if split_by == "none": + subunits = ["all"] + shapes.to_parquet(Path(output_path / "all.parquet")) + + else: + subunits = sorted(shapes[split_by].unique()) + for subunit in subunits: + sub_shapes = shapes[shapes[split_by] == subunit] + if sub_shapes.empty: + raise ValueError(f"No shapes found for {split_by}: {subunit}") + + sub_shapes.to_parquet(output_path / f"{subunit}.parquet") + + # with open(output_list_of_subunits, "w") as f: + # for subunit in subunits: + # f.write(f"{subunit}\n") + + +if __name__ == "__main__": + breakup_shape() diff --git a/workflow/scripts/clip_and_rasterise_polys.py b/workflow/scripts/clip_and_rasterise_polys.py new file mode 100644 index 0000000..0da3bf4 --- /dev/null +++ b/workflow/scripts/clip_and_rasterise_polys.py @@ -0,0 +1,34 @@ +"""Subset to a bounding box and rasterise polygons.""" + +import click +import geopandas as gpd +import rioxarray as rxr + + +@click.command() +@click.argument("shapes_path", type=str) +@click.argument("reference_raster_path", type=str) +@click.argument("protected_area_path", type=str) +@click.argument("output_path", type=str) +def clip_and_rasterise_polys( + shapes_path, reference_raster_path, protected_area_path, output_path +): + """Clip the polygons in SHAPES_PATH to the bounding box of the reference raster, and save the clipped polygons as a raster to OUTPUT_PATH.""" + shapes = gpd.read_parquet(shapes_path) + reference_raster = rxr.open_rasterio(reference_raster_path) + + # FIXME: read the right layer(s) and deal with both poly and point layers + xmin, ymin, xmax, ymax = shapes.total_bounds + protected_areas = gpd.read_file(protected_area_path) + print(f"Protected areas: {len(protected_areas)}") + protected_areas = protected_areas.cx[xmin:xmax, ymin:ymax] + print(f"Protected areas after applying total_bounds: {len(protected_areas)}") + + protected_raster = reference_raster.rio.clip( + protected_areas.geometry, protected_areas.crs + ) + protected_raster.rio.to_raster(output_path, driver="GTiff", compress="LZW") + + +if __name__ == "__main__": + clip_and_rasterise_polys() diff --git a/workflow/scripts/report.py b/workflow/scripts/report.py index 4d799dc..3457bcb 100644 --- a/workflow/scripts/report.py +++ b/workflow/scripts/report.py @@ -3,19 +3,32 @@ import geopandas as gpd import pandas as pd import rioxarray as rxr +import script_utils import xarray as xr +from resample import _rasterize_regions -def report(shapes, resampled_path, area_potentials, csv_path, html_path): +def report(shapes, area_potentials, csv_path, html_path, png_path): """Generate a report summarizing area potentials for different technologies.""" shapes = gpd.read_parquet(shapes) - ds_inputs = xr.open_dataset(resampled_path, decode_coords="all") - # FIXME: this is a workaround for the CRS not being set correctly; not sure why - ds_inputs.rio.write_crs(ds_inputs.spatial_ref.attrs["crs_wkt"], inplace=True) + + ds_inputs = xr.Dataset() # Collect the area potentials from the input files for area_potential in area_potentials: - ds_inputs[area_potential] = rxr.open_rasterio(area_potential) + ds_inputs[area_potential] = rxr.open_rasterio( + area_potential, mask_and_scale=True + ) + + ds_inputs["regions"] = ( + ("y", "x"), + _rasterize_regions(shapes, ds_inputs[area_potential]), + ) + + script_utils.plot_all_dataset_variables( + ds_inputs, ncols=2, savefig=png_path, categorical_vars=["regions"] + ) + ds_inputs = ds_inputs.squeeze().drop_vars(["band", "spatial_ref"]) # Group the area potentials by regions, sum them up, and collect the resulting Series @@ -48,8 +61,8 @@ def report(shapes, resampled_path, area_potentials, csv_path, html_path): if __name__ == "__main__": report( snakemake.input.shapes, - snakemake.input.resampled_path, snakemake.input.area_potentials, snakemake.output.csv, snakemake.output.html, + snakemake.output.png, ) diff --git a/workflow/scripts/resample.py b/workflow/scripts/resample.py index 9756a56..6444a77 100644 --- a/workflow/scripts/resample.py +++ b/workflow/scripts/resample.py @@ -157,12 +157,20 @@ def resample_inputs( """ shapes = gpd.read_parquet(shapes_path) + xmin, ymin, xmax, ymax = shapes.total_bounds resampled = xr.Dataset() ## # Land cover ## ds_land_cover = rxr.open_rasterio(land_cover_path) + + # By subsetting the land cover dataset to the bounding box of the shapes, + # we ensure that we only work with the relevant area as this is the reference + # raster used elsewhere + ds_land_cover = ds_land_cover.rio.clip_box( + minx=xmin, miny=ymin, maxx=xmax, maxy=ymax + ) land_cover_types = yaml.safe_load(land_cover_configuration_yaml_string) reference_raster = xr.ones_like(ds_land_cover, dtype=np.byte) reference_resolution = ds_land_cover.rio.resolution() @@ -260,17 +268,10 @@ def resample_inputs( ## # Protected areas ## - # FIXME: read the right layer(s) and deal with both poly and point layers - xmin, ymin, xmax, ymax = shapes.total_bounds - protected_areas = gpd.read_file(protected_area_path) - print(f"Protected areas: {len(protected_areas)}") - protected_areas = protected_areas.cx[xmin:xmax, ymin:ymax] - print(f"Protected areas after applying total_bounds: {len(protected_areas)}") - - resampled["protected"] = reference_raster.rio.clip( - protected_areas.geometry, protected_areas.crs + protected_areas = rxr.open_rasterio(protected_area_path) + resampled["protected"] = protected_areas.rio.reproject_match( + reference_raster, resampling=Resampling.average ) - resampled["protected"] = resampled["protected"].fillna(0) del protected_areas netcdf4_encoding = { diff --git a/workflow/scripts/script_utils.py b/workflow/scripts/script_utils.py index 42d60b0..49aae9f 100644 --- a/workflow/scripts/script_utils.py +++ b/workflow/scripts/script_utils.py @@ -2,10 +2,32 @@ import math +import matplotlib.colors as mcolors import matplotlib.pyplot as plt +import numpy as np -def plot_all_dataset_variables(ds, ncols=2, savefig=None): +def random_categorical_cmap(n, base_cmap="tab20", seed=42): + """Generate a random categorical colormap from a continuous base colormap. + + Parameters: + n (int): Number of unique categories. + base_cmap (str or Colormap): Name of the base matplotlib colormap or a Colormap object. + seed (int, optional): Random seed for reproducibility. + + Returns: + matplotlib.colors.ListedColormap: Colormap with `n` random colors. + """ + if seed is not None: + np.random.seed(seed) + + cmap = plt.get_cmap(base_cmap) + # Sample `n` random values from [0, 1] and get corresponding colors + colors = cmap(np.random.rand(n)) + return mcolors.ListedColormap(colors) + + +def plot_all_dataset_variables(ds, ncols=2, savefig=None, categorical_vars=[]): """Plot all variables in an xarray dataset on a grid of plots.""" # Drop dimensionless variables ds = ds.drop_vars(lambda x: [v for v, da in x.variables.items() if not da.ndim]) @@ -17,7 +39,11 @@ def plot_all_dataset_variables(ds, ncols=2, savefig=None): axes = axes.flatten() for i, var in enumerate(vars_to_plot): - ds[var].plot(ax=axes[i]) + if var in categorical_vars: + cmap = random_categorical_cmap(len(ds[var].values)) + else: + cmap = "viridis" + ds[var].plot(ax=axes[i], cmap=cmap) axes[i].set_title(var) # We want to save rasterized images also for e.g. PDF output # Any actor with a zorder below the value given here is rasterized From 42c3285e1e25c13d0a6c0eec0c04231dbcd17cba Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Wed, 23 Jul 2025 08:43:00 +0200 Subject: [PATCH 49/59] Improve memory use in area_potential_report rule --- config/config.yaml | 2 +- tests/integration/test_config.yaml | 2 ++ workflow/rules/process.smk | 25 +++++++++++++++--- workflow/scripts/report.py | 42 ++++++++++++++---------------- workflow/scripts/resample.py | 20 -------------- workflow/scripts/script_utils.py | 20 ++++++++++++++ workflow/scripts/tif_to_png.py | 18 +++++++++++++ 7 files changed, 81 insertions(+), 48 deletions(-) create mode 100644 workflow/scripts/tif_to_png.py diff --git a/config/config.yaml b/config/config.yaml index 8d2235c..3f15dd2 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -6,7 +6,7 @@ tiny_files: False # A good option is "epsg:8857" (WGS 84 / Equal Earth Greenwich) for global coverage buffer_crs: "epsg:8857" -split_by: country_id +split_by: country_id # likely country_id or shape_id land_cover_types: POST_FLOODING: FARM diff --git a/tests/integration/test_config.yaml b/tests/integration/test_config.yaml index ecd6bf1..3a9e5c8 100644 --- a/tests/integration/test_config.yaml +++ b/tests/integration/test_config.yaml @@ -100,3 +100,5 @@ module_area_potentials: protected: 0 shapes_buffer: land: 10000 # meters + + overrides: {} diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index 9e966b5..df867ef 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -98,6 +98,23 @@ rule aggregate_area_potential: """ +rule plot_aggregated_area_potential: + message: + "Plot aggregated area potential for the tech {wildcards.tech} in {wildcards.shape}." + input: + rules.aggregate_area_potential.output.aggregated_area_potential, + output: + report( + "results/{shape}/area_potential_{tech}.png", category="area_potential_plot" + ), + log: + "logs/{shape}/plot_aggregated_area_potential_{tech}.log", + conda: + "../envs/default.yaml" + script: + "../scripts/tif_to_png.py" + + rule area_potential_report: message: "Generate an overview report of the area potential for all techs in shapes {wildcards.shape}." @@ -107,16 +124,16 @@ rule area_potential_report: "results/{{shape}}/area_potential_{tech}.tif", tech=config["techs"].keys(), ), + area_potential_plots=expand( + "results/{{shape}}/area_potential_{tech}.png", + tech=config["techs"].keys(), + ), output: csv="results/{shape}/area_potential_report.csv", html=report( "results/{shape}/area_potential_report.html", category="area_potential_report_table", ), - png=report( - "results/{shape}/area_potential_report.png", - category="area_potential_report_image", - ), log: "logs/{shape}/area_potential_report.log", conda: diff --git a/workflow/scripts/report.py b/workflow/scripts/report.py index 3457bcb..e6bc0b4 100644 --- a/workflow/scripts/report.py +++ b/workflow/scripts/report.py @@ -3,42 +3,39 @@ import geopandas as gpd import pandas as pd import rioxarray as rxr -import script_utils import xarray as xr from resample import _rasterize_regions -def report(shapes, area_potentials, csv_path, html_path, png_path): +def report(shapes, area_potentials, csv_path, html_path): """Generate a report summarizing area potentials for different technologies.""" shapes = gpd.read_parquet(shapes) - ds_inputs = xr.Dataset() - - # Collect the area potentials from the input files - for area_potential in area_potentials: - ds_inputs[area_potential] = rxr.open_rasterio( - area_potential, mask_and_scale=True - ) - - ds_inputs["regions"] = ( - ("y", "x"), - _rasterize_regions(shapes, ds_inputs[area_potential]), + print("Generating reference raster and rasterizing regions...") + reference_raster = rxr.open_rasterio(area_potentials[0]) + regions = xr.DataArray( + _rasterize_regions(shapes, reference_raster), + dims=("y", "x"), + coords={"y": reference_raster.y, "x": reference_raster.x}, ) + # regions = xr.DataArray(("y", "x"), _rasterize_regions(shapes, reference_raster)) + del reference_raster - script_utils.plot_all_dataset_variables( - ds_inputs, ncols=2, savefig=png_path, categorical_vars=["regions"] - ) - - ds_inputs = ds_inputs.squeeze().drop_vars(["band", "spatial_ref"]) - + # Collect the area potentials from the input files # Group the area potentials by regions, sum them up, and collect the resulting Series # into a DataFrame, where each column corresponds to a technology's area potential, # and the index corresponds to the regions. dataframes = [] - for area_potential in area_potentials: - dataframes.append( - ds_inputs[area_potential].groupby(ds_inputs["regions"]).sum().to_pandas() + for area_potential_file in area_potentials: + print(f"Processing area potential file: {area_potential_file}") + da_area_potential = ( + rxr.open_rasterio(area_potential_file, mask_and_scale=True) + .squeeze() + .drop_vars(["band", "spatial_ref"]) ) + dataframes.append(da_area_potential.groupby(regions).sum().to_pandas()) + del da_area_potential + df = pd.concat(dataframes, axis=1) # Add metadata columns from shapes in front of the data columns @@ -64,5 +61,4 @@ def report(shapes, area_potentials, csv_path, html_path, png_path): snakemake.input.area_potentials, snakemake.output.csv, snakemake.output.html, - snakemake.output.png, ) diff --git a/workflow/scripts/resample.py b/workflow/scripts/resample.py index 6444a77..6050765 100644 --- a/workflow/scripts/resample.py +++ b/workflow/scripts/resample.py @@ -289,26 +289,6 @@ def resample_inputs( resampled.to_netcdf(output_path, encoding=netcdf4_encoding) print("Saving image to plot path:", plot_path) - # If needed, resample `resampled` to fit within a maximum of `max_pixels` pixels - max_pixels = 5000000 - total_pixels = resampled.sizes["y"] * resampled.sizes["x"] - if total_pixels > max_pixels: - # Calculate the new resolution to fit within the max_pixels limit - resolution_multiplier = 1 / math.sqrt(total_pixels / max_pixels) - new_y_size = int(resampled.sizes["y"] * resolution_multiplier) - new_x_size = int(resampled.sizes["x"] * resolution_multiplier) - print( - f"Resampling old size {resampled.sizes['y']} x {resampled.sizes['x']} " - f"to new size: {new_y_size} x {new_x_size} " - f"to fit within {max_pixels} pixels." - ) - - resampled = resampled.coarsen( - x=round(resampled.sizes["x"] / new_x_size), - y=round(resampled.sizes["y"] / new_y_size), - boundary="trim", - ).mean() - script_utils.plot_all_dataset_variables(resampled, ncols=3, savefig=plot_path) diff --git a/workflow/scripts/script_utils.py b/workflow/scripts/script_utils.py index 49aae9f..2a1a0ad 100644 --- a/workflow/scripts/script_utils.py +++ b/workflow/scripts/script_utils.py @@ -29,6 +29,26 @@ def random_categorical_cmap(n, base_cmap="tab20", seed=42): def plot_all_dataset_variables(ds, ncols=2, savefig=None, categorical_vars=[]): """Plot all variables in an xarray dataset on a grid of plots.""" + # If needed, resample `ds` to fit within a maximum of `max_pixels` pixels + max_pixels = 5000000 + total_pixels = ds.sizes["y"] * ds.sizes["x"] + if total_pixels > max_pixels: + # Calculate the new resolution to fit within the max_pixels limit + resolution_multiplier = 1 / math.sqrt(total_pixels / max_pixels) + new_y_size = int(ds.sizes["y"] * resolution_multiplier) + new_x_size = int(ds.sizes["x"] * resolution_multiplier) + print( + f"Resampling old size {ds.sizes['y']} x {ds.sizes['x']} " + f"to new size: {new_y_size} x {new_x_size} " + f"to fit within {max_pixels} pixels." + ) + + ds = ds.coarsen( + x=round(ds.sizes["x"] / new_x_size), + y=round(ds.sizes["y"] / new_y_size), + boundary="trim", + ).mean() + # Drop dimensionless variables ds = ds.drop_vars(lambda x: [v for v, da in x.variables.items() if not da.ndim]) diff --git a/workflow/scripts/tif_to_png.py b/workflow/scripts/tif_to_png.py new file mode 100644 index 0000000..96337e2 --- /dev/null +++ b/workflow/scripts/tif_to_png.py @@ -0,0 +1,18 @@ +"""This script plots a TIF file to PNG format.""" + +import rioxarray as rxr +from script_utils import plot_all_dataset_variables + + +def tif_to_png(tif_file_in, png_file_out): + """Convert a TIF file to PNG format.""" + ds = rxr.open_rasterio(tif_file_in, mask_and_scale=True).to_dataset( + name=tif_file_in + ) + plot_all_dataset_variables( + ds, ncols=2, savefig=png_file_out, categorical_vars=["regions"] + ) + + +if __name__ == "__main__": + tif_to_png(snakemake.input[0], snakemake.output[0]) From 36b6091a9f3aa5e488723a844f66e4cb12507a11 Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Fri, 25 Jul 2025 13:55:05 +0200 Subject: [PATCH 50/59] Replace gdal_merge with gdalwarp, clean up report --- workflow/envs/default.yaml | 1 + workflow/rules/process.smk | 2 +- workflow/scripts/report.py | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/workflow/envs/default.yaml b/workflow/envs/default.yaml index a9a15e3..f9bd7db 100644 --- a/workflow/envs/default.yaml +++ b/workflow/envs/default.yaml @@ -13,6 +13,7 @@ dependencies: - fiona=1.10.1 - rasterio=1.4.3 - rioxarray=0.19.0 + - gdal=3.10.3 - libgdal-arrow-parquet=3.10.3 - libgdal-hdf5=3.10.3 - matplotlib=3.10.3 diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index df867ef..870ff6c 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -94,7 +94,7 @@ rule aggregate_area_potential: "../envs/default.yaml" shell: """ - gdal_merge -o "{output.aggregated_area_potential}" -a_nodata -1 {input} + gdalwarp --config GDAL_CACHEMAX 3000 -wm 3000 -of GTiff -co COMPRESS=LZW {input} "{output.aggregated_area_potential}" """ diff --git a/workflow/scripts/report.py b/workflow/scripts/report.py index e6bc0b4..5deb5ff 100644 --- a/workflow/scripts/report.py +++ b/workflow/scripts/report.py @@ -33,7 +33,9 @@ def report(shapes, area_potentials, csv_path, html_path): .squeeze() .drop_vars(["band", "spatial_ref"]) ) - dataframes.append(da_area_potential.groupby(regions).sum().to_pandas()) + df_ = da_area_potential.groupby(regions).sum().to_pandas() + df_.name = area_potential_file + dataframes.append(df_) del da_area_potential df = pd.concat(dataframes, axis=1) From f7fca2aafb0911a8f5713bddeacd5d49e17670be Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Fri, 1 Aug 2025 12:15:05 +0200 Subject: [PATCH 51/59] Run `copier recopy` --- .gitignore | 3 +++ mypy.ini | 1 + pixi.toml | 14 +------------- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/.gitignore b/.gitignore index 21787f0..f03928b 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ __pycache__ *.pyc +### Environments +.pixi/ + ### Snakemake .snakemake/ gurobi.log diff --git a/mypy.ini b/mypy.ini index d787271..c7b2d53 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,2 +1,3 @@ [mypy] disable_error_code = import-untyped +exclude = (^|/)\.(snakemake|pixi)(/|$) diff --git a/pixi.toml b/pixi.toml index bcfb004..8639a0f 100644 --- a/pixi.toml +++ b/pixi.toml @@ -13,6 +13,7 @@ clio-tools = ">=2025.03.03" conda = ">=25.0.0" ipdb = ">=0.13.13" ipykernel = ">=6.29.5" +jsonschema = ">=4.0.0" mypy = ">=1.15.0" pytest = ">=8.3.5" python = ">=3.12" @@ -22,16 +23,3 @@ snakemake-minimal = ">=8.29.0" [tasks] test-integration = {cmd = "pytest tests/clio_test.py"} - -[feature.docs.dependencies] -mkdocs-material = ">=9.6.7" - -[feature.docs.pypi-dependencies] -mkdocs-mermaid2-plugin = ">=1.2.1" - -[feature.docs.tasks] -serve-docs = {cmd = "mkdocs serve"} -build-docs = {cmd = "mkdocs build"} - -[environments] -docs = ["docs"] From 899642e938bd27920b2e927c0619475107b98359 Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Fri, 31 Oct 2025 11:26:25 +0100 Subject: [PATCH 52/59] Changes in response to review --- INTERFACE.yaml | 13 ++++---- config/config.yaml | 27 +---------------- tests/integration/Snakefile | 6 ++-- tests/integration/test_config.yaml | 30 ------------------- workflow/Snakefile | 4 +-- workflow/internal/settings.yaml | 25 ++++++++++++++++ workflow/rules/automatic.smk | 22 +++++++------- workflow/rules/process.smk | 24 +++++++-------- workflow/scripts/{geo.py => _geo.py} | 0 .../{script_utils.py => _script_utils.py} | 0 workflow/scripts/area_potential.py | 7 +++-- workflow/scripts/breakup_shape.py | 7 +---- workflow/scripts/resample.py | 10 +++---- workflow/scripts/tif_to_png.py | 2 +- 14 files changed, 71 insertions(+), 106 deletions(-) rename workflow/scripts/{geo.py => _geo.py} (100%) rename workflow/scripts/{script_utils.py => _script_utils.py} (100%) diff --git a/INTERFACE.yaml b/INTERFACE.yaml index 9f15cca..cdae1e1 100644 --- a/INTERFACE.yaml +++ b/INTERFACE.yaml @@ -3,13 +3,14 @@ resources: user: "shapes/{shape}.parquet": "Region geometries in parquet format. These should conform to the schema defined in https://github.com/calliope-project/module_geo_boundaries/blob/main/workflow/internal/shape.schema.yaml" "wdpa.gdb": "WDPA protected areas database from https://www.protectedplanet.net/ in GeoDB format (choose 'File Geodatabase' when downloading)." - automatic: - "{shape}.resampled_inputs.nc": "Resampled and rasterized input data layers for the regions, including land cover, slope, settlement, bathymetry, and protected areas." - "{shape}.resampled_inputs.png": "Visualization of the resampled input data for the regions." results: - "results/{shape}/area_potential_{tech}.tif": "Area potential GeoTIFF raster for the specified technology and region geometries." - "results/{shape}/area_potential_report.csv": "CSV summary report of area potential for all techs, for the given region geometries." - "results/{shape}/area_potential_report.html": "HTML summary report of area potential for all techs, for the given region geometries." + "{shape}/{subunit}/area_potential_{tech}.tif": "Area potential GeoTIFF raster for the specified technology and subunit." + "{shape}/{subunit}/area_potential_{tech}.png": "Area potential GeoTIFF raster for the specified technology and subunit." + "{shape}/area_potential_{tech}.tif": "Area potential GeoTIFF raster for the specified technology across all subunits." + "{shape}/area_potential_{tech}.png": "Area potential PNG for the specified technology across all subunits." + "{shape}/area_potential_report.csv": "CSV summary report of area potential for all techs, across all subunits." + "{shape}/area_potential_report.html": "HTML summary report of area potential for all techs, across all subunits." wildcards: shape: "Name of the shape to be processed, e.g., 'world', 'europe', 'MEX'." + subunit: "Name of a subunit from within the shape, e.g 'IRL' for Ireland within 'europe'." tech: "Name of the technology, e.g., 'pv_rooftop' or 'wind_offshore'. Available technologies are defined in the module configuration." diff --git a/config/config.yaml b/config/config.yaml index 3f15dd2..947d727 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -1,5 +1,3 @@ -tiny_files: False - # Options for buffering: either a string of the form "epsg:xxxx" or "UTM"/"utm" # - "UTM": project each shape to the UTM zone of its centroid for buffering # - "epsg:xxxx": use the specified CRS for all buffering @@ -8,30 +6,7 @@ buffer_crs: "epsg:8857" split_by: country_id # likely country_id or shape_id -land_cover_types: - POST_FLOODING: FARM - RAINFED_CROPLANDS: FARM - MOSAIC_CROPLAND: FARM - MOSAIC_VEGETATION: FARM - CLOSED_TO_OPEN_BROADLEAVED_FOREST: FOREST - CLOSED_BROADLEAVED_FOREST: FOREST - OPEN_BROADLEAVED_FOREST: FOREST - CLOSED_NEEDLELEAVED_FOREST: FOREST - OPEN_NEEDLELEAVED_FOREST: FOREST - CLOSED_TO_OPEN_MIXED_FOREST: FOREST - MOSAIC_FOREST: FOREST - CLOSED_TO_OPEN_REGULARLY_FLOODED_FOREST: FOREST - CLOSED_REGULARLY_FLOODED_FOREST: FOREST - MOSAIC_GRASSLAND: OTHER - CLOSED_TO_OPEN_SHRUBLAND: OTHER - CLOSED_TO_OPEN_HERBS: OTHER - SPARSE_VEGETATION: OTHER - CLOSED_TO_OPEN_REGULARLY_FLOODED_GRASSLAND: OTHER - BARE_AREAS: OTHER - ARTIFICIAL_SURFACES_AND_URBAN_AREAS: URBAN - WATER_BODIES: WATER - PERMANENT_SNOW: NOT_SUITABLE - NO_DATA: NOT_SUITABLE +# Optional - specify land_cover_types here to override the internal defaults. techs: pv_rooftop: diff --git a/tests/integration/Snakefile b/tests/integration/Snakefile index e747415..61e6a94 100644 --- a/tests/integration/Snakefile +++ b/tests/integration/Snakefile @@ -13,7 +13,7 @@ rule download_netherlands_shapes: "../../workflow/envs/shell.yaml" shell: """ - curl -sSLo "{output}" "https://surfdrive.surf.nl/files/index.php/s/ey3RmiCbajp69oQ/download" + curl -sSLo {output:q} "https://zenodo.org/records/16684683/files/NLD.parquet" """ @@ -28,7 +28,7 @@ rule download_netherlands_protected_areas: "../../workflow/envs/shell.yaml" shell: """ - curl -sSLo "{output}" "https://surfdrive.surf.nl/files/index.php/s/msuXQETNbCWawDh/download" + curl -sSLo {output:q} "https://zenodo.org/records/16684513/files/wdpa-nl-dropin.gdb.zip" """ @@ -46,7 +46,7 @@ rule unzip_netherlands_protected_areas: "../../workflow/envs/shell.yaml" shell: """ - python "{input.script}" "{input.zipfile}" -t "results/integration_test/resources/user/" 2> "{log}" + python {input.script:q} {input.zipfile:q} -t "results/integration_test/resources/user/" 2> {log:q} """ diff --git a/tests/integration/test_config.yaml b/tests/integration/test_config.yaml index 3a9e5c8..3a579f4 100644 --- a/tests/integration/test_config.yaml +++ b/tests/integration/test_config.yaml @@ -1,35 +1,7 @@ - - module_area_potentials: tiny_files: True buffer_crs: "epsg:8857" split_by: country_id - - land_cover_types: - POST_FLOODING: FARM - RAINFED_CROPLANDS: FARM - MOSAIC_CROPLAND: FARM - MOSAIC_VEGETATION: FARM - CLOSED_TO_OPEN_BROADLEAVED_FOREST: FOREST - CLOSED_BROADLEAVED_FOREST: FOREST - OPEN_BROADLEAVED_FOREST: FOREST - CLOSED_NEEDLELEAVED_FOREST: FOREST - OPEN_NEEDLELEAVED_FOREST: FOREST - CLOSED_TO_OPEN_MIXED_FOREST: FOREST - MOSAIC_FOREST: FOREST - CLOSED_TO_OPEN_REGULARLY_FLOODED_FOREST: FOREST - CLOSED_REGULARLY_FLOODED_FOREST: FOREST - MOSAIC_GRASSLAND: OTHER - CLOSED_TO_OPEN_SHRUBLAND: OTHER - CLOSED_TO_OPEN_HERBS: OTHER - SPARSE_VEGETATION: OTHER - CLOSED_TO_OPEN_REGULARLY_FLOODED_GRASSLAND: OTHER - BARE_AREAS: OTHER - ARTIFICIAL_SURFACES_AND_URBAN_AREAS: URBAN - WATER_BODIES: WATER - PERMANENT_SNOW: NOT_SUITABLE - NO_DATA: NOT_SUITABLE - techs: pv_rooftop: initial_area: settlement_area @@ -100,5 +72,3 @@ module_area_potentials: protected: 0 shapes_buffer: land: 10000 # meters - - overrides: {} diff --git a/workflow/Snakefile b/workflow/Snakefile index ea55963..ce5e049 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -17,8 +17,8 @@ with open(workflow.source_path("internal/settings.yaml"), "r") as f: internal = yaml.safe_load(f) # Python files that are imported from other scripts and need to be included when accessing the module -workflow.source_path("scripts/script_utils.py") -workflow.source_path("scripts/geo.py") +workflow.source_path("scripts/_script_utils.py") +workflow.source_path("scripts/_geo.py") wildcard_constraints: diff --git a/workflow/internal/settings.yaml b/workflow/internal/settings.yaml index 93dae19..531ffd5 100644 --- a/workflow/internal/settings.yaml +++ b/workflow/internal/settings.yaml @@ -10,3 +10,28 @@ resources: globcover_landcover_tif: "GLOBCOVER_L4_200901_200912_V2.3.tif" ghsl: "https://jeodpp.jrc.ec.europa.eu/ftp/jrc-opendata/GHSL/GHS_BUILT_S_GLOBE_R2023A/GHS_BUILT_S_E2025_GLOBE_R2023A_4326_30ss/V1-0/GHS_BUILT_S_E2025_GLOBE_R2023A_4326_30ss_V1_0.zip" ghsl_tif: "GHS_BUILT_S_E2025_GLOBE_R2023A_4326_30ss_V1_0.tif" + +land_cover_types: + POST_FLOODING: FARM + RAINFED_CROPLANDS: FARM + MOSAIC_CROPLAND: FARM + MOSAIC_VEGETATION: FARM + CLOSED_TO_OPEN_BROADLEAVED_FOREST: FOREST + CLOSED_BROADLEAVED_FOREST: FOREST + OPEN_BROADLEAVED_FOREST: FOREST + CLOSED_NEEDLELEAVED_FOREST: FOREST + OPEN_NEEDLELEAVED_FOREST: FOREST + CLOSED_TO_OPEN_MIXED_FOREST: FOREST + MOSAIC_FOREST: FOREST + CLOSED_TO_OPEN_REGULARLY_FLOODED_FOREST: FOREST + CLOSED_REGULARLY_FLOODED_FOREST: FOREST + MOSAIC_GRASSLAND: OTHER + CLOSED_TO_OPEN_SHRUBLAND: OTHER + CLOSED_TO_OPEN_HERBS: OTHER + SPARSE_VEGETATION: OTHER + CLOSED_TO_OPEN_REGULARLY_FLOODED_GRASSLAND: OTHER + BARE_AREAS: OTHER + ARTIFICIAL_SURFACES_AND_URBAN_AREAS: URBAN + WATER_BODIES: WATER + PERMANENT_SNOW: NOT_SUITABLE + NO_DATA: NOT_SUITABLE diff --git a/workflow/rules/automatic.smk b/workflow/rules/automatic.smk index 7b8ca5e..d2ad968 100644 --- a/workflow/rules/automatic.smk +++ b/workflow/rules/automatic.smk @@ -53,7 +53,7 @@ else: "../envs/shell.yaml" shell: """ - curl -sSLo "{output}" "{params.url}" + curl -sSLo {output:q} {params.url:q} """ rule download_bathymetry: @@ -69,7 +69,7 @@ else: "../envs/shell.yaml" shell: """ - curl -sSLo "{output}" "{params.url}" + curl -sSLo {output:q} {params.url:q} """ rule clip_slope: @@ -87,7 +87,7 @@ else: "../envs/default.yaml" shell: """ - python "{input.script}" "{input.slope}" "{input.shapes}" "{output}" 2> "{log}" + python {input.script:q} {input.slope:q} {input.shapes:q} {output:q} 2> {log:q} """ rule clip_bathymetry: @@ -105,7 +105,7 @@ else: "../envs/default.yaml" shell: """ - python "{input.script}" "{input.bathymetry}" "{input.shapes}" "{output}" 2> "{log}" + python {input.script:q} {input.bathymetry:q} {input.shapes:q} {output:q} 2> {log:q} """ @@ -127,7 +127,7 @@ rule download_globcover: "../envs/shell.yaml" shell: """ - curl -sSLo "{output}" "{params.url}" + curl -sSLo {output:q} {params.url:q} """ @@ -147,7 +147,7 @@ rule unzip_globcover: "../envs/shell.yaml" shell: """ - python "{input.script}" "{input.zipfile}" -f "{params.target_file}" -o "{output}" 2> "{log}" + python {input.script:q} {input.zipfile:q} -f {params.target_file:q} -o {output:q} 2> {log:q} """ @@ -166,7 +166,7 @@ rule clip_landcover: "../envs/default.yaml" shell: """ - python "{input.script}" "{input.landcover}" "{input.shapes}" "{output}" 2> "{log}" + python {input.script:q} {input.landcover:q} {input.shapes:q} {output:q} 2> {log:q} """ @@ -188,7 +188,7 @@ rule download_ghsl: "../envs/shell.yaml" shell: """ - curl -sSLo "{output}" "{params.url}" + curl -sSLo {output:q} {params.url:q} """ @@ -208,7 +208,7 @@ rule unzip_ghsl: "../envs/shell.yaml" shell: """ - python "{input.script}" "{input.zipfile}" -f "{params.target_file}" -o "{output}" 2> "{log}" + python {input.script:q} {input.zipfile:q} -f {params.target_file:q} -o {output:q} 2> {log:q} """ @@ -227,7 +227,7 @@ rule clip_settlement: "../envs/default.yaml" shell: """ - python "{input.script}" "{input.settlement}" "{input.shapes}" "{output}" 2> "{log}" + python {input.script:q} {input.settlement:q} {input.shapes:q} {output:q} 2> {log:q} """ @@ -252,5 +252,5 @@ rule rasterise_clip_wdpa: "../envs/default.yaml" shell: """ - python "{input.script}" "{input.shapes}" "{input.reference_raster}" "{input.protected_areas}" "{output}" 2> "{log}" + python {input.script:q} {input.shapes:q} {input.reference_raster:q} {input.protected_areas:q} {output:q} 2> {log:q} """ diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index 870ff6c..0c594fe 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -14,7 +14,7 @@ checkpoint breakup_shape: "../envs/default.yaml" shell: """ - python "{input.script}" "{input.shapes}" "{params.split_by}" "{output}" 2> "{log}" + python {input.script:q} {input.shapes:q} {params.split_by:q} {output:q} 2> {log:q} """ @@ -22,10 +22,10 @@ rule prepare_resampled_inputs: message: "Resample inputs for {wildcards.subunit} in {wildcards.shape} to the projection and resolution of the land cover data, while aggregating land cover types." params: - land_cover_types_yaml_string=config["land_cover_types"], + # Use internal defaults if not overridden + land_cover_types_yaml_string=internal["land_cover_types"] | config.get("land_cover_types", {}) input: script=workflow.source_path("../scripts/resample.py"), - # shapes="resources/automatic/shapes/{shape}/{subunit}.parquet", shapes=rules.breakup_shape.output, land_cover_path=rules.clip_landcover.output, slope_path=rules.clip_slope.output, @@ -44,10 +44,11 @@ rule prepare_resampled_inputs: "../envs/default.yaml" shell: """ - python "{input.script}" \ - "{input.shapes}/{wildcards.subunit}.parquet" "{input.land_cover_path}" "{input.slope_path}" "{input.settlement_path}" "{input.bathymetry_path}" "{input.protected_area_path}" \ - "{params.land_cover_types_yaml_string}" \ - "{output.resampled_input}" "{output.plot}" 2> "{log}" + python {input.script:q} \ + "{input.shapes}/{wildcards.subunit}.parquet" \ + {input.land_cover_path:q} {input.slope_path:q} {input.settlement_path:q} {input.bathymetry_path:q} {input.protected_area_path:q} \ + {params.land_cover_types_yaml_string:q} \ + {output.resampled_input:q} {output.plot:q} 2> {log:q} """ @@ -56,13 +57,10 @@ rule area_potential: "Compute area potential for the tech {wildcards.tech} and {wildcards.subunit} in {wildcards.shape}." params: config=lambda wildcards: config["techs"][f"{wildcards.tech}"], - subunit_override_config=lambda wildcards: config["overrides"] - .get(wildcards.subunit, {}) - .get(wildcards.tech, {}), + subunit_override_config=lambda wildcards: config.get("overrides", {}).get(wildcards.subunit, {}).get(wildcards.tech, {}), buffer_crs=lambda wildcards: config["buffer_crs"], input: script=workflow.source_path("../scripts/area_potential.py"), - # shapes="resources/automatic/shapes/{shape}/{subunit}.parquet", shapes=rules.breakup_shape.output, resampled_path=rules.prepare_resampled_inputs.output.resampled_input, output: @@ -77,7 +75,7 @@ rule area_potential: "../envs/default.yaml" shell: """ - python "{input.script}" "{input.shapes}/{wildcards.subunit}.parquet" "{input.resampled_path}" "{params.config}" "{params.buffer_crs}" "{output.area_potential}" "{output.plot}" --override_config="{params.subunit_override_config}" 2> "{log}" + python {input.script:q} "{input.shapes}/{wildcards.subunit}.parquet" {input.resampled_path:q} {params.config:q} {params.buffer_crs:q} {output.area_potential:q} {output.plot:q} --override_config={params.subunit_override_config:q} 2> {log:q} """ @@ -94,7 +92,7 @@ rule aggregate_area_potential: "../envs/default.yaml" shell: """ - gdalwarp --config GDAL_CACHEMAX 3000 -wm 3000 -of GTiff -co COMPRESS=LZW {input} "{output.aggregated_area_potential}" + gdalwarp --config GDAL_CACHEMAX 3000 -wm 3000 -of GTiff -co COMPRESS=LZW {input} {output.aggregated_area_potential:q} """ diff --git a/workflow/scripts/geo.py b/workflow/scripts/_geo.py similarity index 100% rename from workflow/scripts/geo.py rename to workflow/scripts/_geo.py diff --git a/workflow/scripts/script_utils.py b/workflow/scripts/_script_utils.py similarity index 100% rename from workflow/scripts/script_utils.py rename to workflow/scripts/_script_utils.py diff --git a/workflow/scripts/area_potential.py b/workflow/scripts/area_potential.py index 0b75aa3..976e1b0 100644 --- a/workflow/scripts/area_potential.py +++ b/workflow/scripts/area_potential.py @@ -1,7 +1,7 @@ """This script calculates the area potential based on the provided configuration.""" +import _geo import click -import geo import geopandas as gpd import glom import matplotlib.pyplot as plt @@ -43,7 +43,8 @@ def get_area_potential( """ shapes = gpd.read_parquet(shapes_path) ds = xr.open_dataset(resampled_path, decode_coords="all") - # FIXME: this is a workaround for the CRS not being set correctly; not sure why + # NOTE: this is a workaround for the CRS not being set correctly, ideally this + # should not be necessary ds.rio.write_crs(ds.spatial_ref.attrs["crs_wkt"], inplace=True) config = yaml.safe_load(config) if override_config: @@ -93,7 +94,7 @@ def get_area_potential( buffer_distance = config["shapes_buffer"][shape_class] shapes_subset = shapes[shapes["shape_class"] == shape_class] if buffer_crs.lower() == "utm": - buffer = geo.apply_utm_buffer( + buffer = _geo.apply_utm_buffer( shapes_subset, buffer_distance_m=buffer_distance ).to_crs(ds.rio.crs)["geometry"] else: diff --git a/workflow/scripts/breakup_shape.py b/workflow/scripts/breakup_shape.py index 106f3a4..6092cb2 100644 --- a/workflow/scripts/breakup_shape.py +++ b/workflow/scripts/breakup_shape.py @@ -10,8 +10,7 @@ @click.argument("shapes_path", type=str) @click.argument("split_by", type=str) @click.argument("output_path", type=str) -# @click.argument("output_list_of_subunits", type=str) -def breakup_shape(shapes_path, split_by, output_path): # , output_list_of_subunits): +def breakup_shape(shapes_path, split_by, output_path): """Break subunits out of shape and save the resulting shapefiles. Args: @@ -46,10 +45,6 @@ def breakup_shape(shapes_path, split_by, output_path): # , output_list_of_subun sub_shapes.to_parquet(output_path / f"{subunit}.parquet") - # with open(output_list_of_subunits, "w") as f: - # for subunit in subunits: - # f.write(f"{subunit}\n") - if __name__ == "__main__": breakup_shape() diff --git a/workflow/scripts/resample.py b/workflow/scripts/resample.py index 6050765..2a91e06 100644 --- a/workflow/scripts/resample.py +++ b/workflow/scripts/resample.py @@ -2,11 +2,11 @@ import math +import _script_utils import click import geopandas as gpd import numpy as np import rioxarray as rxr -import script_utils import xarray as xr import yaml from rasterio.enums import Resampling @@ -107,9 +107,9 @@ def determine_pixel_areas(raster_input): """ # the following is based on https://gis.stackexchange.com/a/288034/77760 # and assumes the data to be in EPSG:4326 - assert raster_input.rio.crs.to_epsg() == 4326, ( - "raster_input does not have the projection EPSG:4326" - ) + assert ( + raster_input.rio.crs.to_epsg() == 4326 + ), "raster_input does not have the projection EPSG:4326" resolution = raster_input.rio.resolution()[0] # resolution in degrees varea_of_pixel = np.vectorize(lambda lat: _area_of_pixel(resolution, lat)) pixel_area = varea_of_pixel(raster_input.y) * 1000**2 # convert to m^2 @@ -289,7 +289,7 @@ def resample_inputs( resampled.to_netcdf(output_path, encoding=netcdf4_encoding) print("Saving image to plot path:", plot_path) - script_utils.plot_all_dataset_variables(resampled, ncols=3, savefig=plot_path) + _script_utils.plot_all_dataset_variables(resampled, ncols=3, savefig=plot_path) if __name__ == "__main__": diff --git a/workflow/scripts/tif_to_png.py b/workflow/scripts/tif_to_png.py index 96337e2..993be40 100644 --- a/workflow/scripts/tif_to_png.py +++ b/workflow/scripts/tif_to_png.py @@ -1,7 +1,7 @@ """This script plots a TIF file to PNG format.""" import rioxarray as rxr -from script_utils import plot_all_dataset_variables +from _script_utils import plot_all_dataset_variables def tif_to_png(tif_file_in, png_file_out): From ebcca3845b7301ac0b34868f1b91bbdb597cdfaa Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 10:27:04 +0000 Subject: [PATCH 53/59] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- workflow/rules/process.smk | 7 +++++-- workflow/scripts/resample.py | 6 +++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/workflow/rules/process.smk b/workflow/rules/process.smk index 0c594fe..f3b7ce2 100644 --- a/workflow/rules/process.smk +++ b/workflow/rules/process.smk @@ -23,7 +23,8 @@ rule prepare_resampled_inputs: "Resample inputs for {wildcards.subunit} in {wildcards.shape} to the projection and resolution of the land cover data, while aggregating land cover types." params: # Use internal defaults if not overridden - land_cover_types_yaml_string=internal["land_cover_types"] | config.get("land_cover_types", {}) + land_cover_types_yaml_string=internal["land_cover_types"] + | config.get("land_cover_types", {}), input: script=workflow.source_path("../scripts/resample.py"), shapes=rules.breakup_shape.output, @@ -57,7 +58,9 @@ rule area_potential: "Compute area potential for the tech {wildcards.tech} and {wildcards.subunit} in {wildcards.shape}." params: config=lambda wildcards: config["techs"][f"{wildcards.tech}"], - subunit_override_config=lambda wildcards: config.get("overrides", {}).get(wildcards.subunit, {}).get(wildcards.tech, {}), + subunit_override_config=lambda wildcards: config.get("overrides", {}) + .get(wildcards.subunit, {}) + .get(wildcards.tech, {}), buffer_crs=lambda wildcards: config["buffer_crs"], input: script=workflow.source_path("../scripts/area_potential.py"), diff --git a/workflow/scripts/resample.py b/workflow/scripts/resample.py index 2a91e06..986778a 100644 --- a/workflow/scripts/resample.py +++ b/workflow/scripts/resample.py @@ -107,9 +107,9 @@ def determine_pixel_areas(raster_input): """ # the following is based on https://gis.stackexchange.com/a/288034/77760 # and assumes the data to be in EPSG:4326 - assert ( - raster_input.rio.crs.to_epsg() == 4326 - ), "raster_input does not have the projection EPSG:4326" + assert raster_input.rio.crs.to_epsg() == 4326, ( + "raster_input does not have the projection EPSG:4326" + ) resolution = raster_input.rio.resolution()[0] # resolution in degrees varea_of_pixel = np.vectorize(lambda lat: _area_of_pixel(resolution, lat)) pixel_area = varea_of_pixel(raster_input.y) * 1000**2 # convert to m^2 From 51010fa3e7af0cd583e35491d4c7ebc9b494c5f6 Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Fri, 31 Oct 2025 11:41:25 +0100 Subject: [PATCH 54/59] Make tiny_files optional --- workflow/rules/automatic.smk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflow/rules/automatic.smk b/workflow/rules/automatic.smk index d2ad968..76b2337 100644 --- a/workflow/rules/automatic.smk +++ b/workflow/rules/automatic.smk @@ -1,6 +1,6 @@ """Rules to used to download automatic resource files.""" -if config["tiny_files"]: +if config.get("tiny_files", False): ## # Directly download clipped slope and bathymetry data From 5009d125224d90839973923e66b467e52870f2fb Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Fri, 31 Oct 2025 16:35:16 +0100 Subject: [PATCH 55/59] Zero out rather than NaN areas with no potential --- workflow/scripts/_script_utils.py | 12 +++++++++++- workflow/scripts/area_potential.py | 13 ++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/workflow/scripts/_script_utils.py b/workflow/scripts/_script_utils.py index 2a1a0ad..b05ed67 100644 --- a/workflow/scripts/_script_utils.py +++ b/workflow/scripts/_script_utils.py @@ -27,6 +27,15 @@ def random_categorical_cmap(n, base_cmap="tab20", seed=42): return mcolors.ListedColormap(colors) +def plot_with_zero_separate(ax, da, cmap="viridis", zero_color="#e0e0e0"): + """Plot data array with zero values in a separate color.""" + da.where(da != 0).plot(ax=ax, cmap=cmap) + da.where(da == 0).plot( + ax=ax, cmap=mcolors.ListedColormap([zero_color]), add_colorbar=False + ) + return ax + + def plot_all_dataset_variables(ds, ncols=2, savefig=None, categorical_vars=[]): """Plot all variables in an xarray dataset on a grid of plots.""" # If needed, resample `ds` to fit within a maximum of `max_pixels` pixels @@ -63,7 +72,8 @@ def plot_all_dataset_variables(ds, ncols=2, savefig=None, categorical_vars=[]): cmap = random_categorical_cmap(len(ds[var].values)) else: cmap = "viridis" - ds[var].plot(ax=axes[i], cmap=cmap) + + plot_with_zero_separate(ax=axes[i], da=ds[var], cmap=cmap) axes[i].set_title(var) # We want to save rasterized images also for e.g. PDF output # Any actor with a zorder below the value given here is rasterized diff --git a/workflow/scripts/area_potential.py b/workflow/scripts/area_potential.py index 976e1b0..31da213 100644 --- a/workflow/scripts/area_potential.py +++ b/workflow/scripts/area_potential.py @@ -7,6 +7,7 @@ import matplotlib.pyplot as plt import xarray as xr import yaml +from _script_utils import plot_with_zero_separate @click.command() @@ -55,22 +56,23 @@ def get_area_potential( # Start with the configured pixel area as a base potential_da = ds[config["initial_area"]].squeeze(drop=True) # Drop `band` - # Drop pixels from binary layers with share 0 from potential_da + # Zero out pixels from binary layers with share 0 from potential_da binary_layers = config.get("binary_layers", {}) zero_binary_layers = [layer for layer, value in binary_layers.items() if value == 0] for layer in zero_binary_layers: if layer in ds: - potential_da = potential_da.where(~(ds[layer] > 0)) + potential_da = potential_da.where(~(ds[layer] > 0), other=0) else: print(f"Warning: Layer '{layer}' not found in dataset. Skipping.") - # Apply the continuous_layers criteria to drop additional pixels + # Apply the continuous_layers criteria to zero out additional pixels continuous_layers = config.get("continuous_layers", {}) for layer, layer_config in continuous_layers.items(): if layer in ds: # Apply the min-max criteria potential_da = potential_da.where( - (ds[layer] <= layer_config["max"]) & (ds[layer] >= layer_config["min"]) + (ds[layer] <= layer_config["max"]) & (ds[layer] >= layer_config["min"]), + other=0, ) # If a share is defined, multiply the pixel area by the share if "share" in layer_config: @@ -111,7 +113,8 @@ def get_area_potential( potential_da = potential_da.transpose("band", "y", "x") potential_da.rio.write_crs(ds.rio.crs, inplace=True) - potential_da.plot() + fig, ax = plt.subplots(1, 1) + ax = plot_with_zero_separate(ax=ax, da=potential_da) plt.savefig(plot_path, bbox_inches="tight") # Fill NaN with a nodata value only after plotting From d7e054a88c2a4a0f65738653bcdc83cfd63fb686 Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Tue, 4 Nov 2025 09:25:34 +0100 Subject: [PATCH 56/59] Add basic documentation --- docs/index.md | 70 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 3 deletions(-) diff --git a/docs/index.md b/docs/index.md index 1fa5206..37a580f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,8 +1,72 @@ -# Home +# Area potentials -Welcome to the documentation of the `module_area_potentials` data module! -Please consult the [specification guidelines](./specification.md) and the [`clio` documentation](https://clio.readthedocs.io/) for more information. +This module performs geospatial analyses to determine the available land area for specific technologies, on a by-pixel basis and aggregated to given geographic boundaries (e.g. generated by [module_geo_boundaries](https://github.com/calliope-project/module_geo_boundaries). +## Overview + +The analysis in this module is structured as follows: + +* Geospatial input data (vector and raster) are acquired. This is automatic for most data, but the following data need to be manually supplied: + * Geographic boundaries in the parquet format + * WDPA protected areas database from https://www.protectedplanet.net/ in GeoDB format (choose 'File Geodatabase' when downloading). +* For the extent of the provided boundaries, the input data are reprojected and rasterised to the resolution of the land cover data (GlobCover), and merged into a single dataset for further processing. +* Based on the supplied configuration, the land-use analysis is done for each defined technology. +* Results can be reported on a per-geography and per-technology basis, as pixel surface area values (TIFF files), images for reporting purposes (PNG files), and also as a summary report with per-region capacities (CSV and HTML files) + +See below for the [data sources](#data-sources). + +## Configuration + +Configure the analysis in the `config.yaml` file. Examples are visible in `config/config.yaml` and `tests/integration/test_config.yaml`. + +In the configuration, you can define any number of `techs`, and for each of them, specify the `initial_area`, `continuous_layers`, and `binary_layers`. + +By example, here is a `pv_rooftop` tech. We use the `settlement_area`, which is the settlement area in m² in each pixel, as the initial area from which the further analysis proceeds. In `continuous_layers`, we use the `settlement_share`, which is the share (0-1) of area covered by settlement, and exclude pixels with less than 0.01 settlement share while assuming that of those pixels not excluded by that, 0.8 (80%) of the settled area can be used for rooftop PV. Finally, in the `binary_layers`, we include all land use types except `NOT_SUITABLE` (since the main selection is done via the settlement_share). This means that, for example, `FOREST` pixels with a `settlement_area` > 0 can be included. + +```yaml +pv_rooftop: + initial_area: settlement_area + continuous_layers: + settlement_share: + min: 0.01 + max: 1 + share: 0.8 + binary_layers: + regions_maritime: 0 + regions_land: 1 + protected: 0 + landcover_FARM: 1 + landcover_FOREST: 1 + landcover_URBAN: 1 + landcover_OTHER: 1 + landcover_NOT_SUITABLE: 0 + landcover_WATER: 0 +``` + +Here is a `wind_offshore` example. We start with the `pixel_area`, the total surface area in m² for each pixel. We include pixels with a slope up to and including 20 degrees, and exclude pixels with a settlement share above 0.01. Furthermore, we include only land areas (`regions_land: 1` and `regions_maritime: 0`) and completely exclude some areas like protected areas or urban areas (`protected: 0`, `landcover_URBAN: 0`), while including only a fraction of other areas (e.g. if a pixel is considered farmland, only 20% of its surface is available: `landcover_FARM: 0.2`). + +```yaml +wind_onshore: + initial_area: pixel_area + continuous_layers: + slope: + min: 0 + max: 20 + settlement_share: + min: 0 + max: 0.01 + binary_layers: + regions_maritime: 0 + regions_land: 1 + protected: 0 + landcover_FARM: 0.2 + landcover_FOREST: 0.05 + landcover_URBAN: 0 + landcover_OTHER: 0.3 + landcover_NOT_SUITABLE: 0 + landcover_WATER: 0 + +``` ## Data sources From 8fa413f9c91f7db04a346de32b641d5e1b21e87e Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Fri, 14 Nov 2025 15:38:15 +0100 Subject: [PATCH 57/59] Add schema validation for shapes --- workflow/Snakefile | 3 ++- workflow/envs/default.yaml | 3 ++- workflow/scripts/_schemas.py | 25 +++++++++++++++++++++++++ workflow/scripts/breakup_shape.py | 2 ++ 4 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 workflow/scripts/_schemas.py diff --git a/workflow/Snakefile b/workflow/Snakefile index ce5e049..e69459b 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -17,8 +17,9 @@ with open(workflow.source_path("internal/settings.yaml"), "r") as f: internal = yaml.safe_load(f) # Python files that are imported from other scripts and need to be included when accessing the module -workflow.source_path("scripts/_script_utils.py") workflow.source_path("scripts/_geo.py") +workflow.source_path("scripts/_schemas.py") +workflow.source_path("scripts/_script_utils.py") wildcard_constraints: diff --git a/workflow/envs/default.yaml b/workflow/envs/default.yaml index f9bd7db..6594015 100644 --- a/workflow/envs/default.yaml +++ b/workflow/envs/default.yaml @@ -7,6 +7,7 @@ dependencies: - click=8.2.1 - geopandas=1.1.0 - pandas=2.3.0 + - pandera-geopandas=0.24.0 - pyarrow=19.0.1 - xarray=2025.6.1 - netcdf4=1.7.2 @@ -21,4 +22,4 @@ dependencies: - pyproj=3.7.1 - utm=0.7.0 - glom=24.11.0 - - dask=2025.7.0 + - dask=2025.7.0 \ No newline at end of file diff --git a/workflow/scripts/_schemas.py b/workflow/scripts/_schemas.py new file mode 100644 index 0000000..e2775cb --- /dev/null +++ b/workflow/scripts/_schemas.py @@ -0,0 +1,25 @@ +"""Schemas for tabular data used in the workflow.""" + +from pandera.pandas import DataFrameModel, Field, check +from pandera.typing.geopandas import GeoSeries +from pandera.typing.pandas import Series +from shapely.geometry import Point + + +class Shapes(DataFrameModel): + class Config: + coerce = True + strict = False + + shape_id: Series[str] = Field(unique=True) + "Unique ID for this shape." + country_id: Series[str] + "ISO alpha-3 code." + shape_class: Series[str] = Field(isin=["land", "maritime"]) + "Shape classifier" + geometry: GeoSeries[Point] = Field() + "Shape polygon." + + @check("geometry", element_wise=True) + def geom_not_empty(cls, geom): + return (geom is not None) and (not geom.is_empty) and geom.is_valid diff --git a/workflow/scripts/breakup_shape.py b/workflow/scripts/breakup_shape.py index 6092cb2..fba186d 100644 --- a/workflow/scripts/breakup_shape.py +++ b/workflow/scripts/breakup_shape.py @@ -4,6 +4,7 @@ import click import geopandas as gpd +from _schemas import Shapes @click.command() @@ -25,6 +26,7 @@ def breakup_shape(shapes_path, split_by, output_path): output_path = Path(output_path) output_path.mkdir(parents=True, exist_ok=True) shapes = gpd.read_parquet(shapes_path) + shapes = Shapes.validate(shapes) # Print rows where geometry is empty if shapes.geometry.is_empty.any(): From 5fc043ce8cfad493519024fc6ed30df04aae2de5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 14 Nov 2025 14:42:02 +0000 Subject: [PATCH 58/59] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- workflow/envs/default.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflow/envs/default.yaml b/workflow/envs/default.yaml index 6594015..d1cdc61 100644 --- a/workflow/envs/default.yaml +++ b/workflow/envs/default.yaml @@ -22,4 +22,4 @@ dependencies: - pyproj=3.7.1 - utm=0.7.0 - glom=24.11.0 - - dask=2025.7.0 \ No newline at end of file + - dask=2025.7.0 From d4c59bea2ba00a360d33b5500120d1c0aa65a264 Mon Sep 17 00:00:00 2001 From: Stefan Pfenninger Date: Mon, 17 Nov 2025 09:42:37 +0100 Subject: [PATCH 59/59] Improve schema --- workflow/scripts/_schemas.py | 33 +++++++++++++++++-------------- workflow/scripts/breakup_shape.py | 4 ++-- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/workflow/scripts/_schemas.py b/workflow/scripts/_schemas.py index e2775cb..ff17826 100644 --- a/workflow/scripts/_schemas.py +++ b/workflow/scripts/_schemas.py @@ -1,25 +1,28 @@ """Schemas for tabular data used in the workflow.""" -from pandera.pandas import DataFrameModel, Field, check +import pandera.pandas as pa from pandera.typing.geopandas import GeoSeries from pandera.typing.pandas import Series -from shapely.geometry import Point -class Shapes(DataFrameModel): - class Config: - coerce = True - strict = False +class ShapesSchema(pa.DataFrameModel): + """Schema for geographic shapes.""" - shape_id: Series[str] = Field(unique=True) - "Unique ID for this shape." + shape_id: Series[str] = pa.Field(unique=True) + "A unique identifier for this shape." country_id: Series[str] - "ISO alpha-3 code." - shape_class: Series[str] = Field(isin=["land", "maritime"]) - "Shape classifier" - geometry: GeoSeries[Point] = Field() - "Shape polygon." + "Country ISO alpha-3 code." + shape_class: Series[str] = pa.Field(isin=["land", "maritime"]) + "Identifier of the shape's context." + geometry: GeoSeries + "Shape (multi)polygon." + parent_name: Series[str] | None + "Human-readable name in the parent dataset." - @check("geometry", element_wise=True) - def geom_not_empty(cls, geom): + @pa.check("geometry", element_wise=True) + def check_geometries(cls, geom): return (geom is not None) and (not geom.is_empty) and geom.is_valid + + class Config: + coerce = True + strict = False diff --git a/workflow/scripts/breakup_shape.py b/workflow/scripts/breakup_shape.py index fba186d..615eb4a 100644 --- a/workflow/scripts/breakup_shape.py +++ b/workflow/scripts/breakup_shape.py @@ -4,7 +4,7 @@ import click import geopandas as gpd -from _schemas import Shapes +from _schemas import ShapesSchema @click.command() @@ -26,7 +26,7 @@ def breakup_shape(shapes_path, split_by, output_path): output_path = Path(output_path) output_path.mkdir(parents=True, exist_ok=True) shapes = gpd.read_parquet(shapes_path) - shapes = Shapes.validate(shapes) + shapes = ShapesSchema.validate(shapes) # Print rows where geometry is empty if shapes.geometry.is_empty.any():