From b6f5f15da306459e58fabf16915e7bd9b0d6354c Mon Sep 17 00:00:00 2001 From: bobbyxng Date: Tue, 6 Oct 2026 16:37:35 +0200 Subject: [PATCH 1/5] fix: support remote Snakemake module imports --- config/README.md | 6 + config/regions/index.yaml | 7 + mypy.ini | 2 +- pytest.ini | 2 +- tests/test_module_import.py | 201 ++++++++++++++++++++++ workflow/Snakefile | 36 +++- workflow/rules/network.smk | 11 +- workflow/rules/plot.smk | 4 +- workflow/rules/retrieve.smk | 5 +- workflow/scripts/_helpers.py | 25 +-- workflow/scripts/_schema.py | 30 +++- workflow/scripts/build_interactive_map.py | 4 +- workflow/scripts/build_network.py | 4 +- workflow/scripts/clean.py | 4 +- workflow/scripts/retrieve_osm_overpass.py | 4 +- workflow/scripts/retrieve_osm_pbf.py | 4 +- 16 files changed, 295 insertions(+), 54 deletions(-) create mode 100644 config/regions/index.yaml create mode 100644 tests/test_module_import.py diff --git a/config/README.md b/config/README.md index e5166c9..fa86a60 100644 --- a/config/README.md +++ b/config/README.md @@ -47,3 +47,9 @@ also lets you point at your own or a faster mirror instance instead of the shared public endpoint, without touching the checked-in default. The generated [schema](./config.schema.json) describes every option. + +After adding or removing a `config/regions/config..yaml` file, run +`pixi run generate-config`. It discovers the regional files and generates the +sorted `config/regions/index.yaml` manifest automatically. Commit the generated +index with the regional files so remote imports can discover them without a +local checkout. Do not edit the index manually; a test checks that it is current. diff --git a/config/regions/index.yaml b/config/regions/index.yaml new file mode 100644 index 0000000..4ec9cc1 --- /dev/null +++ b/config/regions/index.yaml @@ -0,0 +1,7 @@ +# Generated by pixi run generate-config; do not edit manually. +- BE +- BR +- MX +- NL +- PH +- US diff --git a/mypy.ini b/mypy.ini index 89978f2..00bb13d 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,7 +1,7 @@ [mypy] plugins = pydantic.mypy explicit_package_bases = True -mypy_path = workflow +mypy_path = workflow/scripts disable_error_code = import-untyped exclude = (^|/)\.(snakemake|pixi)(/|$) exclude_gitignore = True diff --git a/pytest.ini b/pytest.ini index 61b096f..6654657 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,3 @@ [pytest] testpaths = tests -pythonpath = . workflow +pythonpath = . workflow/scripts diff --git a/tests/test_module_import.py b/tests/test_module_import.py new file mode 100644 index 0000000..f2c9e36 --- /dev/null +++ b/tests/test_module_import.py @@ -0,0 +1,201 @@ +"""Exercise remote source caching without relying on GitHub or live OSM data.""" + +import functools +import json +import os +import shutil +import subprocess +import sys +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from threading import Thread + +import pytest + +from workflow.scripts._schema import generate_region_index + + +@pytest.fixture +def remote_module(tmp_path): + """Serve only module sources, with a fresh URL and cache for each test.""" + repository = Path(__file__).resolve().parents[1] + source = tmp_path / "source" + for directory in ("workflow", "config"): + shutil.copytree(repository / directory, source / directory) + payloads = {} + + class Handler(SimpleHTTPRequestHandler): + """Serve sources and a minimal deterministic Overpass API.""" + + def do_POST(self): + """Return fixture elements matching the requested feature.""" + query = self.rfile.read(int(self.headers["Content-Length"])).decode() + feature = None + if 'way["power"="line"]' in query: + feature = "lines_way" + elif 'way["power"="substation"]' in query: + feature = "substations_way" + body = json.dumps(payloads.get(feature, {"elements": []})).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + handler = functools.partial(Handler, directory=str(source)) + server = ThreadingHTTPServer(("127.0.0.1", 0), handler) + thread = Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + yield f"http://127.0.0.1:{server.server_port}/workflow/Snakefile", payloads + finally: + server.shutdown() + server.server_close() + thread.join() + + +@pytest.mark.parametrize("remote", [False, True]) +@pytest.mark.parametrize("backend", ["overpass", "geofabrik"]) +def test_module_execution(tmp_path, remote_module, remote, backend): + """Import beside a conflicting scripts package, then clean, build and plot.""" + remote_url, payloads = remote_module + api_url = remote_url.rsplit("/workflow/", 1)[0] + "/overpass" + workdir = tmp_path / "consumer" + workdir.mkdir() + scripts = workdir / "scripts" + scripts.mkdir() + (scripts / "__init__.py").touch() + (scripts / "_schema.py").write_text("parent_marker = True\n") + (scripts / "_helpers.py").write_text("parent_marker = 'parent helper'\n") + (scripts / "probe.py").write_text( + "from pathlib import Path\n" + "from _helpers import parent_marker\n" + "Path(snakemake.output[0]).write_text(parent_marker)\n" + ) + source = ( + remote_url + if remote + else str(Path(__file__).resolve().parents[1] / "workflow/Snakefile") + ) + (workdir / "Snakefile").write_text( + f"""import sys +sys.path.insert(0, {str(workdir)!r}) +import scripts._schema +import scripts._helpers + +module grid: + snakefile: {source!r} + config: {{"countries": ["US"], "retrieve": {{"source": {backend!r}, "overpass_api": {{"url": {api_url!r}}}}}}} + pathvars: + resources="custom/resources", + results="custom/results", + logs="custom/logs", + +use rule * from grid as grid_* + +assert scripts._schema.parent_marker +assert scripts._helpers.parent_marker == "parent helper" +assert "custom/resources/retrieve/US_lines_way.json" in rules.grid_retrieve_osm_all.input +if {backend!r} == "geofabrik": + assert rules.grid_retrieve_osm_pbf.params.data_dir == "custom/resources/automatic/earth-osm" +assert grid.config["regions"]["US"]["frequency_hz"]["AC"] == 60 + +rule parent_probe: + output: "parent.txt" + script: "scripts/probe.py" + +rule all: + default_target: True + input: rules.grid_build_interactive_map.output, rules.parent_probe.output +""" + ) + raw_dir = workdir / "custom/resources/retrieve" + raw_dir.mkdir(parents=True) + for feature in ( + "lines_way", + "cables_way", + "substations_way", + "substations_node", + "substations_relation", + "routes_relation", + ): + elements = [] + if feature == "lines_way": + elements = [ + { + "type": "way", + "id": 1, + "tags": { + "power": "line", + "voltage": "345000", + "frequency": "60", + "cables": "3", + }, + "geometry": [ + {"lon": -100.0, "lat": 40.0}, + {"lon": -99.9, "lat": 40.0}, + ], + } + ] + if feature == "substations_way": + elements = [ + { + "type": "way", + "id": i + 10, + "tags": { + "power": "substation", + "voltage": "345000", + "frequency": "60", + "substation": "transmission", + }, + "geometry": [ + {"lon": x, "lat": y} + for x, y in ( + (lon - 0.001, 39.999), + (lon + 0.001, 39.999), + (lon + 0.001, 40.001), + (lon - 0.001, 40.001), + (lon - 0.001, 39.999), + ) + ], + } + for i, lon in enumerate((-100.0, -99.9)) + ] + raw_path = raw_dir / f"US_{feature}.json" + payloads[feature] = {"elements": elements} + if backend == "geofabrik": + raw_path.parent.mkdir(parents=True, exist_ok=True) + raw_path.write_text(json.dumps(payloads[feature])) + env = {**os.environ, "XDG_CACHE_HOME": str(tmp_path / "cache")} + env.pop("PYTHONPATH", None) + result = subprocess.run( + [sys.executable, "-m", "snakemake", "--cores", "1", "--show-failed-logs"], + cwd=workdir, + env=env, + capture_output=True, + text=True, + timeout=120, + ) + assert result.returncode == 0, result.stdout + result.stderr + assert (workdir / "parent.txt").read_text() == "parent helper" + assert (workdir / "custom/resources/map.html").stat().st_size > 0 + lines = json.loads( + (workdir / "custom/resources/build/geojson/lines.geojson").read_text() + ) + assert len(lines["features"]) == 1 + assert (workdir / "custom/resources/build/csv/buses.csv").stat().st_size > 0 + if backend == "overpass": + assert ( + "Querying Overpass" + in (workdir / "custom/logs/retrieve_osm_overpass/US.log").read_text() + ) + + +def test_region_manifest(tmp_path): + """Every shipped regional default must be available to remote consumers.""" + regions = Path(__file__).resolve().parents[1] / "config/regions" + generated = tmp_path / "index.yaml" + generate_region_index(regions, generated) + assert generated.read_text() == (regions / "index.yaml").read_text(), ( + "Run pixi run generate-config after adding or removing regional files." + ) diff --git a/workflow/Snakefile b/workflow/Snakefile index b8ed000..c9107d4 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -1,14 +1,36 @@ +import hashlib +import importlib.util import sys -from pathlib import Path import yaml from snakemake.utils import min_version -sys.path.insert(0, workflow.basedir) -from scripts._schema import load_region_configs, validate_config - min_version("9.19") +# Resolve auxiliary files through Snakemake so remote modules are cached too. +# A path-specific name keeps separate grid-builder versions isolated in one host. +_schema_path = workflow.source_path("scripts/_schema.py") +_schema_name = ( + f"_grid_builder_schema_{hashlib.sha256(str(_schema_path).encode()).hexdigest()}" +) +_schema_spec = importlib.util.spec_from_file_location(_schema_name, str(_schema_path)) +_schema = importlib.util.module_from_spec(_schema_spec) +sys.modules[_schema_name] = _schema +_schema_spec.loader.exec_module(_schema) + +# Snakemake adds the cached script directory to each script's import path. +workflow.source_path("scripts/_helpers.py") +for filename in ("colors.yaml", "tag_corrections.yaml"): + workflow.source_path(f"internal/{filename}") + +# Remote directories cannot be enumerated: the manifest lists shipped overrides. +with open(workflow.source_path("../config/regions/index.yaml")) as f: + _region_codes = yaml.safe_load(f) +_region_paths = { + code: workflow.source_path(f"../config/regions/config.{code}.yaml") + for code in _region_codes +} + # Define pathvars to expose OSM retrieval outputs for downstream use. pathvars: @@ -20,9 +42,9 @@ pathvars: configfile: workflow.source_path("../config/config.yaml") -config = validate_config( - load_region_configs(config, Path(workflow.basedir).parent / "config" / "regions") -) +config = _schema.validate_config( + _schema.load_region_configs(config, _region_paths) +).model_dump(mode="json") # Load internal settings separately so users cannot modify them. with open(workflow.source_path("internal/settings.yaml"), "r") as f: diff --git a/workflow/rules/network.smk b/workflow/rules/network.smk index 3d5476b..34a1dac 100644 --- a/workflow/rules/network.smk +++ b/workflow/rules/network.smk @@ -43,12 +43,9 @@ rule clean: "../envs/network.yaml" threads: 1 params: - network=config["network"].model_dump(mode="json"), - regions={ - code: value.model_dump(mode="json") - for code, value in config["regions"].items() - }, - crs=config["crs"].model_dump(mode="json"), + network=config["network"], + regions=config["regions"], + crs=config["crs"], message: "Cleaning retrieved OSM power features." script: @@ -78,7 +75,7 @@ rule build_network: station_merge_radius_m=config["network"]["station_merge_radius_m"], remove_under_construction=config["network"]["remove_under_construction"], remove_after=config["network"]["remove_after"], - crs=config["crs"].model_dump(mode="json"), + crs=config["crs"], message: "Building a connected generic OSM network." script: diff --git a/workflow/rules/plot.smk b/workflow/rules/plot.smk index 3c06d71..163fbd5 100644 --- a/workflow/rules/plot.smk +++ b/workflow/rules/plot.smk @@ -18,8 +18,8 @@ rule build_interactive_map: "../envs/network.yaml" threads: 1 params: - crs=config["crs"].model_dump(mode="json"), - interactive_map=config["interactive_map"].model_dump(mode="json"), + crs=config["crs"], + interactive_map=config["interactive_map"], message: "Building an interactive OSM network map." script: diff --git a/workflow/rules/retrieve.smk b/workflow/rules/retrieve.smk index cc6144f..873367d 100644 --- a/workflow/rules/retrieve.smk +++ b/workflow/rules/retrieve.smk @@ -2,7 +2,6 @@ # # SPDX-License-Identifier: MIT -from pathlib import Path # Both retrieve_osm_pbf and retrieve_osm_overpass produce this same fixed # set of six files per country (routes_relation included even when @@ -40,7 +39,7 @@ if config["retrieve"]["source"] == "geofabrik": params: include_relations=config["network"]["include_relations"], force_redownload=config["retrieve"]["force_redownload"], - data_dir=str(Path(workflow.basedir).parent / "data" / "earth-osm"), + data_dir=workflow.pathvars.apply("/automatic/earth-osm"), message: "Retrieve OSM power features for one country from a local PBF file." script: @@ -58,7 +57,7 @@ elif config["retrieve"]["source"] == "overpass": threads: 1 params: include_relations=config["network"]["include_relations"], - overpass_api=config["retrieve"]["overpass_api"].model_dump(mode="json"), + overpass_api=config["retrieve"]["overpass_api"], message: "Retrieve OSM power features for one country from the Overpass API." script: diff --git a/workflow/scripts/_helpers.py b/workflow/scripts/_helpers.py index 0ca446b..8864824 100644 --- a/workflow/scripts/_helpers.py +++ b/workflow/scripts/_helpers.py @@ -7,15 +7,9 @@ Logging setup, internal data loading, shared geometry constants, and mock_snakemake for testing. -Other scripts import this module as ``from scripts._helpers import ...``, -not ``workflow.scripts._helpers``. That plain form resolves in both places -these scripts run: under a real -Snakemake ``script:`` execution, Snakemake puts ``workflow/`` on ``sys.path`` -(via the Snakefile's own ``sys.path.insert(0, workflow.basedir)``, captured -and propagated into every script's execution preamble); under pytest, -``pytest.ini``'s ``pythonpath`` setting puts ``workflow/`` on ``sys.path`` -too, alongside the project root that lets test files import -``workflow.scripts.X``. +Scripts import this module directly from their own script directory. The Snakefile caches +it and its internal YAML dependencies using workflow.source_path(); Snakemake +adds the cached script directory to sys.path for both local and remote jobs. """ import logging @@ -42,18 +36,7 @@ def configure_logging(log_path: str) -> None: def load_internal_yaml(filename: str) -> Any: - """Load a YAML file from workflow/internal/, resolved relative to this checkout. - - Uses ``__file__`` rather than ``workflow.source_path()``: this is called - from plain executed scripts (via Snakemake's ``script:`` directive), - which only get a ``snakemake`` object in scope, not the ``workflow`` - object that ``source_path()`` needs — that API is only reachable from - Snakefile-level rule code. Resolving via ``__file__`` still works - correctly when this project is used as a Snakemake submodule, because - Snakemake's own ``script:`` resolution already points each script at its - real location inside this checkout (the same reasoning ``mock_snakemake`` - below relies on for ``script_dir``). - """ + """Load an internal YAML file explicitly cached by the Snakefile.""" path = Path(__file__).resolve().parent.parent / "internal" / filename with open(path) as handle: return yaml.safe_load(handle) diff --git a/workflow/scripts/_schema.py b/workflow/scripts/_schema.py index 4877b15..dc66f92 100644 --- a/workflow/scripts/_schema.py +++ b/workflow/scripts/_schema.py @@ -303,7 +303,7 @@ def _deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any def load_region_configs( - config: dict[str, Any], regions_dir: str | Path + config: dict[str, Any], regions_dir: str | Path | dict[str, str] ) -> dict[str, Any]: """Load country settings selected by ``countries`` before validation. @@ -320,7 +320,12 @@ def load_region_configs( for country in countries: region = get_region_tuple(country).short - path = Path(regions_dir) / f"config.{region}.yaml" + if isinstance(regions_dir, dict): + if region not in regions_dir: + continue + path = Path(regions_dir[region]) + else: + path = Path(regions_dir) / f"config.{region}.yaml" if path.exists(): loaded = yaml_reader.load(path) or {} if not isinstance(loaded, dict): @@ -448,12 +453,32 @@ def remove_object_type(obj, is_root=True): return schema +def generate_region_index( + regions_dir: str | Path = "config/regions", path: str | Path | None = None +) -> list[str]: + """Generate the remote-import manifest from the available regional files.""" + regions_dir = Path(regions_dir) + codes = sorted( + file.stem.removeprefix("config.") + for file in regions_dir.glob("config.*.yaml") + if file.is_file() + ) + output = Path(path) if path is not None else regions_dir / "index.yaml" + writer = YAML() + writer.default_flow_style = False + with output.open("w") as handle: + handle.write("# Generated by pixi run generate-config; do not edit manually.\n") + writer.dump(codes, handle) + return codes + + __all__ = [ "ConfigSchema", "load_region_configs", "validate_config", "generate_config_defaults", "generate_config_schema", + "generate_region_index", "ValidationError", ] @@ -461,3 +486,4 @@ def remove_object_type(obj, is_root=True): if __name__ == "__main__": generate_config_defaults() generate_config_schema() + generate_region_index() diff --git a/workflow/scripts/build_interactive_map.py b/workflow/scripts/build_interactive_map.py index 4fb5e97..b2e1859 100644 --- a/workflow/scripts/build_interactive_map.py +++ b/workflow/scripts/build_interactive_map.py @@ -23,7 +23,7 @@ import geopandas as gpd import pandas as pd import pydeck as pdk -from scripts._helpers import configure_logging, load_internal_yaml +from _helpers import configure_logging, load_internal_yaml if TYPE_CHECKING: snakemake: Any @@ -1080,7 +1080,7 @@ def compress_json_in_script(match: re.Match[str]) -> str: if __name__ == "__main__": if "snakemake" not in globals(): - from scripts._helpers import mock_snakemake + from _helpers import mock_snakemake snakemake = mock_snakemake("build_interactive_map") configure_logging(snakemake.log[0]) diff --git a/workflow/scripts/build_network.py b/workflow/scripts/build_network.py index e4803af..c3e08eb 100644 --- a/workflow/scripts/build_network.py +++ b/workflow/scripts/build_network.py @@ -22,8 +22,8 @@ import networkx as nx import numpy as np import pandas as pd +from _helpers import BUS_TOL, configure_logging from pyproj import Transformer -from scripts._helpers import BUS_TOL, configure_logging from shapely import get_point from shapely.algorithms.polylabel import polylabel from shapely.geometry import LineString, MultiLineString, Point @@ -940,7 +940,7 @@ def _write_components( if __name__ == "__main__": if "snakemake" not in globals(): - from scripts._helpers import mock_snakemake + from _helpers import mock_snakemake snakemake = mock_snakemake("build_network") diff --git a/workflow/scripts/clean.py b/workflow/scripts/clean.py index 32f5fcd..6ac11f7 100644 --- a/workflow/scripts/clean.py +++ b/workflow/scripts/clean.py @@ -27,7 +27,7 @@ import geopandas as gpd import numpy as np import pandas as pd -from scripts._helpers import BUS_TOL, configure_logging, load_internal_yaml +from _helpers import BUS_TOL, configure_logging, load_internal_yaml from shapely.algorithms.polylabel import polylabel from shapely.geometry import LineString, MultiLineString, Point, Polygon from shapely.ops import linemerge, unary_union @@ -1197,7 +1197,7 @@ def clean( if __name__ == "__main__": if "snakemake" not in globals(): - from scripts._helpers import mock_snakemake + from _helpers import mock_snakemake snakemake = mock_snakemake("clean") diff --git a/workflow/scripts/retrieve_osm_overpass.py b/workflow/scripts/retrieve_osm_overpass.py index 6495dde..2a3da4b 100644 --- a/workflow/scripts/retrieve_osm_overpass.py +++ b/workflow/scripts/retrieve_osm_overpass.py @@ -27,9 +27,9 @@ from typing import TYPE_CHECKING, Any import requests +from _helpers import configure_logging from earth_osm.regions import get_region_tuple from requests.adapters import HTTPAdapter -from scripts._helpers import configure_logging from urllib3.util.retry import Retry if TYPE_CHECKING: @@ -172,7 +172,7 @@ def retrieve_from_overpass( if __name__ == "__main__": if "snakemake" not in globals(): - from scripts._helpers import mock_snakemake + from _helpers import mock_snakemake snakemake = mock_snakemake("retrieve_osm_overpass", country="BE") diff --git a/workflow/scripts/retrieve_osm_pbf.py b/workflow/scripts/retrieve_osm_pbf.py index ee1cadb..ba52fc0 100644 --- a/workflow/scripts/retrieve_osm_pbf.py +++ b/workflow/scripts/retrieve_osm_pbf.py @@ -41,8 +41,8 @@ from typing import TYPE_CHECKING, Any import osmium +from _helpers import configure_logging from earth_osm.regions import download_region_pbf, get_region_tuple -from scripts._helpers import configure_logging if TYPE_CHECKING: snakemake: Any @@ -334,7 +334,7 @@ def _extract_from_filtered_pbf( if __name__ == "__main__": if "snakemake" not in globals(): - from scripts._helpers import mock_snakemake + from _helpers import mock_snakemake snakemake = mock_snakemake("retrieve_osm_pbf", country="BE") From 51778f329bfb4c4a77b7dd4194f8302946b1f5ec Mon Sep 17 00:00:00 2001 From: bobbyxng Date: Tue, 6 Oct 2026 17:07:59 +0200 Subject: [PATCH 2/5] build: export locked execution environments for ModelBlocks --- INTERFACE.yaml | 2 + pixi.lock | 468 ++++++++++++++++++++++++- pixi.toml | 20 +- scripts/export_snakemake_env.py | 53 +++ tests/test_environments.py | 52 +++ workflow/envs/module.linux-64.pin.txt | 146 ++++++++ workflow/envs/module.osx-arm64.pin.txt | 157 +++++++++ workflow/envs/module.win-64.pin.txt | 149 ++++++++ workflow/envs/module.yaml | 20 ++ workflow/envs/network.yaml | 15 - workflow/envs/retrieve.yaml | 11 - workflow/rules/network.smk | 4 +- workflow/rules/plot.smk | 2 +- workflow/rules/retrieve.smk | 4 +- 14 files changed, 1061 insertions(+), 42 deletions(-) create mode 100644 scripts/export_snakemake_env.py create mode 100644 tests/test_environments.py create mode 100644 workflow/envs/module.linux-64.pin.txt create mode 100644 workflow/envs/module.osx-arm64.pin.txt create mode 100644 workflow/envs/module.win-64.pin.txt create mode 100644 workflow/envs/module.yaml delete mode 100644 workflow/envs/network.yaml delete mode 100644 workflow/envs/retrieve.yaml diff --git a/INTERFACE.yaml b/INTERFACE.yaml index 03794ca..ca1e6b9 100644 --- a/INTERFACE.yaml +++ b/INTERFACE.yaml @@ -1,4 +1,6 @@ # Module Input-Output structure for automated docs generation +convention_version: v1.0.0 + pathvars: snakemake_defaults: logs: diff --git a/pixi.lock b/pixi.lock index 9a11ef8..5f85502 100644 --- a/pixi.lock +++ b/pixi.lock @@ -249,7 +249,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/cfgv-3.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.3-pyhc90fa1f_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/clio-tools-2026.03.30-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/clio-tools-2026.06.23-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/conda-inject-1.3.2-pyhd8ed1ab_0.conda @@ -396,7 +396,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/cfgv-3.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.3-pyhc90fa1f_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/clio-tools-2026.03.30-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/clio-tools-2026.06.23-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/conda-index-0.13.0-pyh5ded981_0.conda @@ -726,7 +726,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/cfgv-3.5.0-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/click-8.3.3-pyh6dadd2b_0.conda - - conda: https://conda.anaconda.org/conda-forge/noarch/clio-tools-2026.03.30-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/clio-tools-2026.06.23-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/comm-0.2.3-pyhe01879c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/conda-index-0.13.0-pyh5ded981_0.conda @@ -1026,6 +1026,457 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/zlib-ng-2.3.3-h0261ad2_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstandard-0.25.0-py312he5662c2_4.conda - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda + module: + 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/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.4.0-py312h90b7ffd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/blosc-1.21.6-he440d0b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-1.2.0-hed03a55_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py312hdb49522_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.3.3-py312h0a2e395_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.62.1-py312h8a5da7c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.3-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/freexl-2.0.0-h9dce30a_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/geos-3.14.1-h480dda7_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/giflib-5.2.2-hd590300_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/json-c-0.18-h6688a6e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.5.0-py312h0a2e395_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.19-h0c24ade_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lerc-4.1.0-hdb68285_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260107.1-cxx17_h7b12aa8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libarchive-3.8.7-gpl_hc2c16d8_100.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-6_h4a7cf45_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libboost-1.90.0-h1101c1b_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-6_h0358290_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.20.0-hcf29cc6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_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.8.4-hd2095e1_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.3-h73754d4_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgdal-core-3.12.4-h08c5dba_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libhwy-1.4.0-h10be129_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.4.1-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libjxl-0.11.2-h174a0a3_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libkml-1.3.0-haa4a5bd_1023.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-6_h47877c9_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_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.32-pthreads_h94d23a6_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.58-h421ea60_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/librttopo-1.1.0-h46dd2a8_20.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libspatialite-5.1.0-gpl_hab3fe16_120.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.0-hf4e2dac_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.1-h9d88235_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_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/libxml2-16-2.15.3-hca6bf5a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.3-h49c6c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-devel-2.15.3-h49c6c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/lzo-2.10-h280c20c_1002.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py312h8a5da7c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.10.9-py312he3d6523_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/minizip-4.0.10-h05a5f5f_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/muparser-2.3.5-h5888daf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/numpy-2.4.3-py312h33ff503_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.4-h55fea9a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/osmium-tool-1.19.1-h7ac1ec8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.3-py312hf79963d_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-haa7fec5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pillow-12.2.0-py312h50c33e8_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/proj-9.8.1-he0df7b0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/protobuf-6.33.5-py312ha7b3241_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyogrio-0.12.1-py312h053e1f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyosmium-4.3.1-py312hc7fdb42_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyproj-3.7.2-py312hbc8341d_4.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.13-hd63d673_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py312h8a5da7c_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/qhull-2020.2-h434a139_5.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/scikit-learn-1.8.0-np2py312h3226591_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/scipy-1.17.1-py312h54fa4ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/shapely-2.1.2-py312h383787d_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.53.0-h04a0ce9_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-17.0.1-py312h4c3975b_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/uriparser-0.9.8-hac33072_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xerces-c-3.3.0-hd9031aa_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.2-h25fd6f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-ng-2.3.3-hceb46e0_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/branca-0.8.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/earth-osm-3.0.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/folium-0.20.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/geopandas-1.1.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/geopandas-base-1.1.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/joblib-1.5.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mapclassify-2.10.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydeck-0.9.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2026.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.3-pyh8f84b5b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2026.3.0-pyhd8ed1ab_0.conda + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/noarch/branca-0.8.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/earth-osm-3.0.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/folium-0.20.0-pyhd8ed1ab_0.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/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/geopandas-1.1.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/geopandas-base-1.1.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/joblib-1.5.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mapclassify-2.10.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/narwhals-2.26.0-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydeck-0.9.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-9_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2026.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.3-pyh8f84b5b_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2026.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.7.0-py312h1a36842_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/blosc-1.21.6-h7dd00d9_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.2.0-hf00d406_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-bin-1.2.0-he4a93e4_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py312ha52686f_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/cairo-1.18.4-he0f2337_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/contourpy-1.4.0-py312h67ba313_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fontconfig-2.17.1-h2b252f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fonttools-4.65.0-py312hf4454c7_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.14.3-hce30654_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/freexl-2.0.0-h3ab3353_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/fribidi-1.0.16-hc919400_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/geos-3.14.1-h5afe852_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/giflib-6.1.3-h74c22ad_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/graphite2-1.3.15-h784d473_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-hef89b57_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/json-c-0.18-he4178ee_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/kiwisolver-1.5.1-py312h67ba313_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-h385eeb1_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lcms2-2.19.1-hce41798_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.2.0-h1eee2c3_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20260526.0-cxx17_h4c27e2a_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libarchive-3.8.9-gpl_h76147b0_101.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-6_h51639a9_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libboost-1.90.0-h30c325d_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-h1dcdb26_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-h5295a6a_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-h2ddc9cb_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.11.0-6_hb0561ab_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.22.0-h6651222_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.4-h55c6f16_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.25-hc11a715_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.8.4-h6111c0a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.7.0-h47dc5ef_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.3-hce30654_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.14.3-hdfa99f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-15.2.0-hcbb3090_18.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgdal-core-3.13.3-hc1e97f0_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-h07b0088_18.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_18.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libglib-2.90.0-hccd281b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libharfbuzz-14.4.0-hcda0f7c_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libhwy-1.4.0-ha332bbd_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.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.2.0-h84a0fba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libjxl-0.12.0-hb71b141_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libkml-1.3.0-hb833057_1023.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.11.0-6_hd9741b5_openblas.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.32-openmp_he657e61_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.58-h132b30e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpsl-0.23.1-hdb0c161_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libpython-3.12.14-h4e5ec87_3_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libraqm-0.11.0-h45af499_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/librttopo-1.1.0-ha909e78_20.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libspatialite-5.1.0-gpl_h16ba230_122.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.4-hca69786_1.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.2-hf67920b_1.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/libxcb-1.17.0-hdb1d25a_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.4-heb56d2d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.15.4-h550178d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-devel-2.15.4-h550178d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-22.1.4-hc7d1edf_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-h925e9cb_1002.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py312h04c11ed_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/matplotlib-base-3.11.2-py312h11997a8_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/minizip-4.0.10-hff1a8ea_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/muparser-2.3.5-h11e0b38_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/numpy-2.5.3-py312hff34920_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openjpeg-2.5.4-hd9e9057_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/osmium-tool-1.19.1-h693c552_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pandas-2.3.3-py312h5978115_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.47-h30297fc_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pillow-12.3.0-py312hf9dc30a_4.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pixman-0.46.4-h81086ad_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/proj-9.9.0-h51dee2d_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/protobuf-7.35.1-py312h5674827_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pthread-stubs-0.4-hd74edd7_1002.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyogrio-0.13.0-py312hd1c4548_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyosmium-4.3.1-py312h892b0b2_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyproj-3.8.0-py312h16d7083_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.14-hd05a0c4_3_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py312h04c11ed_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/qhull-2020.2-h420ef59_5.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/scikit-learn-1.9.1-np2py312hda66c2e_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/scipy-1.18.0-py312h4519d97_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/shapely-2.1.2-py312h0fc4a54_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/snappy-1.2.2-hada39a4_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/sqlite-3.53.4-ha9f7ffe_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/unicodedata2-18.0.0-py312ha053593_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/uriparser-0.9.8-h00cdb27_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xerces-c-3.3.0-h25f632f_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxau-1.0.12-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxdmcp-1.1.5-hc919400_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.3.2-h8088a28_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zlib-ng-2.3.3-hed4e4f5_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda + win-64: + - conda: https://conda.anaconda.org/conda-forge/noarch/branca-0.8.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhcf101f3_2.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/earth-osm-3.0.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/folium-0.20.0-pyhd8ed1ab_0.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/noarch/fonts-conda-ecosystem-1-0.tar.bz2 + - conda: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/geopandas-1.1.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/geopandas-base-1.1.3-pyha770c72_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/joblib-1.5.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/mapclassify-2.10.0-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/narwhals-2.26.0-pyh5ded981_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pydeck-0.9.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.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-tzdata-2026.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-9_cp312.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytz-2026.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.3-pyha7b4d00_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xyzservices-2026.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/backports.zstd-1.7.0-py312h06d0912_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/blosc-1.21.6-hfd34d9b_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-1.2.0-hc8c2fe1_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-bin-1.2.0-hd477307_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py312ha763cb9_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/cairo-1.18.4-h477c42c_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/contourpy-1.4.0-py312h78d62e6_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fontconfig-2.17.1-hd47e2ca_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fonttools-4.65.0-py312h05f76fc_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/freetype-2.14.3-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/freexl-2.0.0-hf297d47_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/fribidi-1.0.16-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/geos-3.14.1-hdade9fe_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/graphite2-1.3.15-h5112557_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/icu-78.3-h637d24d_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/kiwisolver-1.5.1-py312h78d62e6_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h0ea6238_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lcms2-2.19.1-hf2c6c5f_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/lerc-4.2.0-hd936e49_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libarchive-3.8.9-gpl_h8fdc8af_101.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-6_hf2e6a31_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libboost-1.90.0-h9dfe17d_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlicommon-1.2.0-hf02afa3_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlidec-1.2.0-h84f9c24_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libbrotlienc-1.2.0-he2a975b_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-6_h2a3cdd5_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libcurl-8.22.0-hdb0ef4a_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.25-h51727cc_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.4-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.7.0-h3d046cb_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype-2.14.3-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libfreetype6-2.14.3-hdbac1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libgdal-core-3.13.3-h6864167_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libglib-2.90.0-ha564072_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libharfbuzz-14.4.0-h03b5201_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.12.2-default_h4379cf1_1000.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libhwy-1.4.0-h172a326_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.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.2.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libjxl-0.12.0-h932607e_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libkml-1.3.0-h68a222c_1023.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-6_hf9ab0e9_mkl.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.58-h7351971_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libpsl-0.23.1-h9b16d47_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libpython-3.12.14-hcc31f7b_3_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libraqm-0.11.0-h50d6d30_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/librttopo-1.1.0-haa95264_20.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libspatialite-5.1.0-gpl_hfd1cb16_122.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.4-hf5d6505_1.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.2-h8f73337_1.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_10.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.4-h3cfd58e_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.4-he095d88_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libxml2-devel-2.15.4-he095d88_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/llvm-openmp-22.1.4-h4fa8253_0.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-h6a83c73_1002.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py312h05f76fc_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/matplotlib-base-3.11.2-py312hf9659c1_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/minizip-4.0.10-h9fa1bad_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/mkl-2025.3.1-hac47afa_12.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/muparser-2.3.5-he0c23c2_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.5.3-py312ha3f287d_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2025.3.1-h57928b3_12.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.4-h0e57b4f_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.2-hf411b9b_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/osmium-tool-1.19.1-h60971b7_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pandas-2.3.3-py312hc128f0a_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pcre2-10.47-hd2b5f0e_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pillow-12.3.0-py312h7bb5964_4.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pixman-0.46.4-h5112557_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/proj-9.9.0-hd30e2cd_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/protobuf-7.35.1-py312h87014b1_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyogrio-0.13.0-py312h0e78342_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyosmium-4.3.1-py312h3e3ada1_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyproj-3.8.0-py312h5a521b9_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.12.14-hb12b558_3_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py312h05f76fc_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/qhull-2020.2-hc790b64_5.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/scikit-learn-1.9.1-np2py312hea30aaf_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/scipy-1.18.1-py312h8847828_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/shapely-2.1.2-py312h37f46ab_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/snappy-1.2.2-h7fa0ca8_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/sqlite-3.53.4-hdb435a2_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tbb-2023.0.0-ha3553a1_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/unicodedata2-18.0.0-py312he06e257_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/uriparser-0.9.8-h5a68840_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/xerces-c-3.3.0-hac47afa_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zlib-1.3.2-hfd05255_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zlib-ng-2.3.3-h0261ad2_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda packages: - conda: https://conda.anaconda.org/bioconda/noarch/snakefmt-1.1.0-pyhdfd78af_0.conda sha256: 11c6ea3fbc6ece521909b776e8697170cab3db5bfb1b7462149ddd582a795cc2 @@ -4189,9 +4640,9 @@ packages: license_family: BSD size: 100048 timestamp: 1777219902525 -- conda: https://conda.anaconda.org/conda-forge/noarch/clio-tools-2026.03.30-pyhd8ed1ab_0.conda - sha256: a8b34e4bb8854ad899e461430fbabad81fa694805e45421bfe3e56da031d44ad - md5: 169bb144044321bb1cec1bfa4ea82a5b +- conda: https://conda.anaconda.org/conda-forge/noarch/clio-tools-2026.06.23-pyhd8ed1ab_0.conda + sha256: 68f97f005c987796455f51bb1a84b5f8879d09a78959e369af3a00945795b568 + md5: de13c74a1b7580a7e4a2323b252f2da2 depends: - networkx >=3.4.2 - numpy >=2.2.3 @@ -4203,8 +4654,9 @@ packages: - pyyaml >=6.0.2 license: MIT license_family: MIT - size: 15754 - timestamp: 1774882664468 + run_exports: {} + size: 23713 + timestamp: 1782229072527 - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda sha256: ab29d57dc70786c1269633ba3dff20288b81664d3ff8d21af995742e2bb03287 md5: 962b9857ee8e7018c22f2776ffa0b2d7 diff --git a/pixi.toml b/pixi.toml index f095350..4a8910a 100644 --- a/pixi.toml +++ b/pixi.toml @@ -9,7 +9,7 @@ platforms = ["win-64", "linux-64", "osx-arm64"] homepage = "https://www.modelblocks.org/" [dependencies] -clio-tools = ">=2026.03.30" +clio-tools = ">=2026.06.23" conda = ">=25.0.0" ipdb = ">=0.13.13" ipykernel = ">=6.29.5" @@ -24,6 +24,12 @@ ruff = ">=0.9.9" snakefmt = ">=0.10.2" snakemake-minimal = ">=9.19.0" pytz = ">=2026.1.post1" +pre-commit = ">=4.6.2,<5" + +[feature.module.dependencies] +python = ">=3.12,<3.13" +pyproj = ">=3.7,<4" +pyyaml = ">=6,<7" earth-osm = ">=3.0.2" geopandas = ">=1.0,<2" networkx = ">=3,<4" @@ -35,12 +41,20 @@ pyosmium = ">=4,<5" pydeck = ">=0.9,<1" requests = ">=2,<3" shapely = ">=2,<3" -pre-commit = ">=4.6.2,<5" + +[environments] +default = { features = ["module"], solve-group = "default" } +module = { features = ["module"], no-default-feature = true, solve-group = "default" } [tasks] test = {cmd = "pytest tests"} test-integration = {cmd = "pytest tests/integration_test.py"} generate-config = {cmd = "python workflow/scripts/_schema.py"} -check-python = {cmd = "ruff check workflow/scripts tests && ruff format --check workflow/scripts tests && mypy workflow/scripts"} +check-python = {cmd = "ruff check workflow/scripts tests scripts && ruff format --check workflow/scripts tests scripts && mypy workflow/scripts"} check-workflow = {cmd = "snakefmt --check workflow tests/integration/Snakefile && snakemake --lint --snakefile workflow/Snakefile && snakemake --lint --snakefile tests/integration/Snakefile"} lint = {depends-on = ["check-python", "check-workflow"]} + +[tasks.export-snakemake-env] +description = "Export locked execution packages for Snakemake on all supported platforms" +args = [{ arg = "env", default = "module" }, { arg = "outdir", default = "workflow/envs" }] +cmd = "python scripts/export_snakemake_env.py '{{ env }}' '{{ outdir }}'" diff --git a/scripts/export_snakemake_env.py b/scripts/export_snakemake_env.py new file mode 100644 index 0000000..27c37b5 --- /dev/null +++ b/scripts/export_snakemake_env.py @@ -0,0 +1,53 @@ +"""Export Pixi's environment and platform pins using the ModelBlocks convention.""" + +import subprocess +import sys +import tempfile +import tomllib +from pathlib import Path + + +def export(environment: str = "module", output_dir: str = "workflow/envs") -> None: + """Write the environment YAML and explicit platform package specifications.""" + root = Path(__file__).resolve().parents[1] + manifest = tomllib.loads((root / "pixi.toml").read_text()) + if environment not in manifest["environments"]: + raise ValueError(f"Unknown environment: {environment}") + outdir = root / output_dir + outdir.mkdir(parents=True, exist_ok=True) + subprocess.run( + [ + "pixi", + "workspace", + "export", + "conda-environment", + "--environment", + environment, + str(outdir / f"{environment}.yaml"), + ], + cwd=root, + check=True, + ) + with tempfile.TemporaryDirectory() as temporary: + for platform in manifest["workspace"]["platforms"]: + subprocess.run( + [ + "pixi", + "workspace", + "export", + "conda-explicit-spec", + "--environment", + environment, + "--platform", + platform, + temporary, + ], + cwd=root, + check=True, + ) + pin = Path(temporary) / f"{environment}_{platform}_conda_spec.txt" + (outdir / f"{environment}.{platform}.pin.txt").write_bytes(pin.read_bytes()) + + +if __name__ == "__main__": + export(*sys.argv[1:]) diff --git a/tests/test_environments.py b/tests/test_environments.py new file mode 100644 index 0000000..7272d01 --- /dev/null +++ b/tests/test_environments.py @@ -0,0 +1,52 @@ +"""Check that Snakemake executes the packages resolved by Pixi.""" + +import subprocess +import tomllib +from pathlib import Path + +import yaml + + +def test_exported_environments_match_lock(): + """Prevent stale platform pins or manifest dependencies reaching consumers.""" + root = Path(__file__).resolve().parents[1] + manifest = tomllib.loads((root / "pixi.toml").read_text()) + lock = yaml.safe_load((root / "pixi.lock").read_text()) + environment = lock["environments"]["module"] + exported = yaml.safe_load((root / "workflow/envs/module.yaml").read_text()) + dependencies = manifest["feature"]["module"]["dependencies"] + assert set(exported["dependencies"]) == { + f"{name} {version}" for name, version in dependencies.items() + } + assert exported["channels"] == manifest["workspace"]["channels"] + ["nodefaults"] + packages = {package["conda"]: package for package in lock["packages"]} + for platform in manifest["workspace"]["platforms"]: + pin = root / f"workflow/envs/module.{platform}.pin.txt" + entries = { + line + for line in pin.read_text().splitlines() + if line and not line.startswith(("#", "@")) + } + expected = { + f"{item['conda']}#{packages[item['conda']]['md5']}" + for item in environment["packages"][platform] + } + assert entries == expected, ( + f"Re-export the {platform} environment from pixi.lock" + ) + + +def test_export_to_custom_directory(tmp_path): + """Support the same export command used by ModelBlocks integration tests.""" + root = Path(__file__).resolve().parents[1] + outdir = tmp_path / "exported environments" + subprocess.run( + ["pixi", "run", "--locked", "export-snakemake-env", "module", str(outdir)], + cwd=root, + check=True, + capture_output=True, + text=True, + timeout=60, + ) + for source in (root / "workflow/envs").glob("module.*"): + assert (outdir / source.name).read_bytes() == source.read_bytes() diff --git a/workflow/envs/module.linux-64.pin.txt b/workflow/envs/module.linux-64.pin.txt new file mode 100644 index 0000000..ea25fca --- /dev/null +++ b/workflow/envs/module.linux-64.pin.txt @@ -0,0 +1,146 @@ +# Generated by `pixi workspace export` +# platform: linux-64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda#239c5e9546c38a1e884d69effcf4c882 +https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda#a9f577daf3de00bca7c3c76c0ecbd1de +https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda#0aa00f03f9e39fb9876085dee11a85d4 +https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda#a77f85f77be52ff59391544bfe73390a +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda#c3efd25ac4d74b1584d2f7a57195ddf1 +https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda#ad659d0a2b3e47e38d829aa8cad2d610 +https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.2-h25fd6f3_2.conda#d87ff7921124eccd67248aa483c23fec +https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda#cffd3bdd58090148f4cfcd831f4b26ab +https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda#fc21868a1a5aacc937e7a18747acb8a5 +https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda#d7d95fc8287ea7bf33e0e7116d2b95ec +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda#e18ad67cf881dcadee8b8d9e2f8e5f73 +https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.2-h35e630c_0.conda#da1b85b6a87e141f5140bb9924cecab0 +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-15.2.0-h69a702a_18.conda#d5e96b1ed75ca01906b3d2469b4ce493 +https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda#5aa797f8787fe7a17d1b0821485b5adc +https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.42-h5347b49_0.conda#38ffe67b78c9d4de527be8315e5ada2c +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda#1b08cd684f34175e4514474793d44bcb +https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda#c80d8a3b84358cb967fa81e7075fbc8a +https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.53.0-hf4e2dac_0.conda#810d83373448da85c3f673fbcb7ad3a3 +https://conda.anaconda.org/conda-forge/linux-64/libnsl-2.0.1-hb9d3cd8_1.conda#d864d34357c3b65a4b731f78c0801dc4 +https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.3-hb03c661_0.conda#b88d90cad08e6bc8ad540cb310a761fb +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda#a360c33a5abe61c07959e449fa1453eb +https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.8.4-hd2095e1_0.conda#a77b44d6cbaad97f3ad050ab35fad55a +https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda#4a13eeac0b5c8e5b8ab496e6c4ddd829 +https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_102.conda#18335a698559cdbcd86150a48bf54ba6 +https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda#d2ffd7602c02f2b316fd921d39876885 +https://conda.anaconda.org/conda-forge/linux-64/python-3.12.13-hd63d673_0_cpython.conda#7eccb41177e15cc672e1babe9056018e +https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py312h8a5da7c_1.conda#15878599a87992e44c059731771591cb +https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda#461219d1a5bd61342293efa2c0c90eac +https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda#8e6923fc12f1fe8f8c4e5c9f343256ac +https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda#0a802cb9888dd14eeefc611f05c40b6e +https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda#164fc43f0b53b6e3a7bc7dce5e4f1dc9 +https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.2.0-py312hdb49522_1.conda#64088dffd7413a2dd557ce837b4cbbdb +https://conda.anaconda.org/conda-forge/linux-64/backports.zstd-1.4.0-py312h90b7ffd_0.conda#542da724e75cdeef19e29cca23935c25 +https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda#9272daa869e03efe68833e3dc7a02130 +https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda#fb7130c190f9b4ec91219840a05ba3ac +https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda#a9167b9571f3baa9d448faa2139d1089 +https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda#929471569c93acefb30282a22060dcd5 +https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda#9659f587a8ceacc21864260acd02fc67 +https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda#9de5350a85c4a20c685259b889aa6393 +https://conda.anaconda.org/conda-forge/linux-64/pyosmium-4.3.1-py312hc7fdb42_1.conda#44bba8b7d3d64b1af5dedfcbc906067a +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-15.2.0-h68bc16d_18.conda#646855f357199a12f02a87382d429b75 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-15.2.0-h69a702a_18.conda#9063115da5bc35fdc3e1002e69b9ef6e +https://conda.anaconda.org/conda-forge/linux-64/libopenblas-0.3.32-pthreads_h94d23a6_0.conda#89d61bc91d3f39fda0ca10fcd3c68594 +https://conda.anaconda.org/conda-forge/linux-64/libblas-3.11.0-6_h4a7cf45_openblas.conda#6d6d225559bfa6e2f3c90ee9c03d4e2e +https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.11.0-6_h47877c9_openblas.conda#881d801569b201c2e753f03c84b85e15 +https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.11.0-6_h0358290_openblas.conda#36ae340a916635b97ac8a0655ace2a35 +https://conda.anaconda.org/conda-forge/linux-64/numpy-2.4.3-py312h33ff503_0.conda#5930ee8a175a242b4f001b1e9e72024f +https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py312h8a5da7c_1.conda#93a4752d42b12943a355b682ee43285b +https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda#04558c96691bed63104678757beb4f8d +https://conda.anaconda.org/conda-forge/noarch/pydeck-0.9.3-pyhd8ed1ab_0.conda#7c94c948b32d681345f134ae11892fee +https://conda.anaconda.org/conda-forge/linux-64/libboost-1.90.0-h1101c1b_3.conda#bcabb17507a7c82f12e9a0cc67fd57c7 +https://conda.anaconda.org/conda-forge/linux-64/osmium-tool-1.19.1-h7ac1ec8_0.conda#14de95da725f036b4b55d67cdc0b0256 +https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.3-pyh8f84b5b_0.conda#e5ce43272193b38c2e9037446c1d9206 +https://conda.anaconda.org/conda-forge/linux-64/libabseil-20260107.1-cxx17_h7b12aa8_0.conda#6f7b4302263347698fd24565fbf11310 +https://conda.anaconda.org/conda-forge/linux-64/protobuf-6.33.5-py312ha7b3241_2.conda#0aee4f9ff95fc4bc78264a93e2a74adf +https://conda.anaconda.org/conda-forge/noarch/pytz-2026.2-pyhcf101f3_0.conda#03cb60f505ad3ada0a95277af5faeb1a +https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.3-pyhd8ed1ab_0.conda#eefc8d916bd2e708d76d40398ef9a1ee +https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda#3339e3b65d58accf4ca4fb8748ab16b3 +https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 +https://conda.anaconda.org/conda-forge/linux-64/pandas-2.3.3-py312hf79963d_1.conda#e597b3e812d9613f659b7d87ad252d18 +https://conda.anaconda.org/conda-forge/noarch/xyzservices-2026.3.0-pyhd8ed1ab_0.conda#4487b9c371d0161d54b5c7bbd890c0fc +https://conda.anaconda.org/conda-forge/linux-64/sqlite-3.53.0-h04a0ce9_0.conda#dc540e5bd5616d83a1ec46af8315ff98 +https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.6.0-hd42ef1d_0.conda#aea31d2e5b1091feca96fcfe945c3cf9 +https://conda.anaconda.org/conda-forge/linux-64/libjpeg-turbo-3.1.4.1-hb03c661_0.conda#6178c6f2fb254558238ef4e6c56fb782 +https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.25-h17f619e_0.conda#6c77a605a7a689d17d4819c0f8ac9a00 +https://conda.anaconda.org/conda-forge/linux-64/lerc-4.1.0-hdb68285_0.conda#a752488c68f2e7c456bcbd8f16eec275 +https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.7.1-h9d88235_1.conda#cd5a90476766d53e901500df9215e927 +https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.1-hcf80075_0.conda#eecce068c7e4eddeb169591baac20ac4 +https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda#172bf1cd1ff8629f2b1179945ed45055 +https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.34.6-hb03c661_0.conda#920bb03579f15389b9e512095ad995b7 +https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.68.1-h877daf1_0.conda#2a45e7f8af083626f009645a6481f12d +https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20250104-pl5321h7949ede_0.conda#c277e0a4d549b03ac1e9d6cbbe3d017b +https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda#b38117a3c920364aff79f870c984b4a3 +https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-ha1258a1_0.conda#fb53fb07ce46a575c5d004bbc96032c2 +https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.20.0-hcf29cc6_0.conda#c3cc2864f82a944bc90a7beb4d3b0e88 +https://conda.anaconda.org/conda-forge/linux-64/proj-9.8.1-he0df7b0_0.conda#b23619e5e9009eaa070ead0342034027 +https://conda.anaconda.org/conda-forge/linux-64/pyproj-3.7.2-py312hbc8341d_4.conda#9d5ea11aefbdf548a2d8c965c1b2ebad +https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda#58335b26c38bf4a20f399384c33cbcf9 +https://conda.anaconda.org/conda-forge/linux-64/xerces-c-3.3.0-hd9031aa_1.conda#66a1db55ecdb7377d2b91f54cd56eafa +https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.47-haa7fec5_0.conda#7a3bff861a6583f1889021facefc08b1 +https://conda.anaconda.org/conda-forge/linux-64/muparser-2.3.5-h5888daf_0.conda#ab3e3db511033340e75e7002e80ce8c0 +https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.18-h3b78370_2.conda#915f5995e94f60e9a4826e0b0920ee88 +https://conda.anaconda.org/conda-forge/linux-64/libxml2-16-2.15.3-hca6bf5a_0.conda#e79d2c2f24b027aa8d5ab1b1ba3061e7 +https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.15.3-h49c6c72_0.conda#995d8c8bad2a3cc8db14675a153dec2b +https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.2-h25fd6f3_2.conda#c2a01a08fc991620a74b32420e97868a +https://conda.anaconda.org/conda-forge/linux-64/libxml2-devel-2.15.3-h49c6c72_0.conda#be70cb50dd0ecc1531c58034d38f72e0 +https://conda.anaconda.org/conda-forge/linux-64/geos-3.14.1-h480dda7_0.conda#4d4efd0645cd556fab54617c4ad477ef +https://conda.anaconda.org/conda-forge/linux-64/librttopo-1.1.0-h46dd2a8_20.conda#df81fd57eacf341588d728c97920e86d +https://conda.anaconda.org/conda-forge/linux-64/minizip-4.0.10-h05a5f5f_0.conda#da01bb40572e689bd1535a5cee6b1d68 +https://conda.anaconda.org/conda-forge/linux-64/freexl-2.0.0-h9dce30a_2.conda#ecb5d11305b8ba1801543002e69d2f2f +https://conda.anaconda.org/conda-forge/linux-64/libspatialite-5.1.0-gpl_hab3fe16_120.conda#5947bdda89ced07ab0d8a5d6503082c3 +https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.58-h421ea60_0.conda#eba48a68a1a2b9d3c0d9511548db85db +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-15.2.0-hdf11a46_18.conda#6235adb93d064ecdf3d44faee6f468de +https://conda.anaconda.org/conda-forge/linux-64/uriparser-0.9.8-hac33072_0.conda#d71d3a66528853c0a1ac2c02d79a0284 +https://conda.anaconda.org/conda-forge/linux-64/libkml-1.3.0-haa4a5bd_1023.conda#953b7cca897e21215302dbfe2af5cd0c +https://conda.anaconda.org/conda-forge/linux-64/libhwy-1.4.0-h10be129_0.conda#3a9428b74c403c71048104d38437b48c +https://conda.anaconda.org/conda-forge/linux-64/libbrotlicommon-1.2.0-hb03c661_1.conda#72c8fd1af66bd67bf580645b426513ed +https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.2.0-hb03c661_1.conda#4ffbb341c8b616aa2494b6afb26a0c5f +https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.2.0-hb03c661_1.conda#366b40a69f0ad6072561c1d09301c886 +https://conda.anaconda.org/conda-forge/linux-64/libjxl-0.11.2-h174a0a3_1.conda#850f48943d6b4589800a303f0de6a816 +https://conda.anaconda.org/conda-forge/linux-64/lzo-2.10-h280c20c_1002.conda#45161d96307e3a447cc3eb5896cf6f8c +https://conda.anaconda.org/conda-forge/linux-64/libarchive-3.8.7-gpl_hc2c16d8_100.conda#dbeb5c8321cb2408d406a3da16a0ff0d +https://conda.anaconda.org/conda-forge/linux-64/json-c-0.18-h6688a6e_0.conda#38f5dbc9ac808e31c00650f7be1db93f +https://conda.anaconda.org/conda-forge/linux-64/giflib-5.2.2-hd590300_0.conda#3bf7b9fd5a7136126e0234db4b87c8b6 +https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.2-h03e3b7b_1.conda#98b6c9dc80eb87b2519b97bcf7e578dd +https://conda.anaconda.org/conda-forge/linux-64/blosc-1.21.6-he440d0b_1.conda#2c2fae981fd2afd00812c92ac47d023d +https://conda.anaconda.org/conda-forge/linux-64/libgdal-core-3.12.4-h08c5dba_2.conda#f5af6a9a075af46eff6a6a55a995e44d +https://conda.anaconda.org/conda-forge/linux-64/pyogrio-0.12.1-py312h053e1f3_0.conda#f8e7e5ddfbdca16b65335b0b6615eb4c +https://conda.anaconda.org/conda-forge/linux-64/qhull-2020.2-h434a139_5.conda#353823361b1d27eb3960efb076dfcaf6 +https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.2-pyhcf101f3_0.conda#3687cc0b82a8b4c17e1f0eb7e47163d5 +https://conda.anaconda.org/conda-forge/linux-64/zlib-ng-2.3.3-hceb46e0_1.conda#2aadb0d17215603a82a2a6b0afd9a4cb +https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.4-h55fea9a_0.conda#11b3379b191f63139e29c0d19dee24cd +https://conda.anaconda.org/conda-forge/linux-64/xorg-libxdmcp-1.1.5-hb03c661_1.conda#1dafce8548e38671bea82e3f5c6ce22f +https://conda.anaconda.org/conda-forge/linux-64/xorg-libxau-1.0.12-hb03c661_1.conda#b2895afaf55bf96a8c8282a2e47a5de0 +https://conda.anaconda.org/conda-forge/linux-64/pthread-stubs-0.4-hb9d3cd8_1002.conda#b3c17d95b5a10c6e64a21fa17573e70e +https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.17.0-h8a09558_0.conda#92ed62436b625154323d40d5f2f11dd7 +https://conda.anaconda.org/conda-forge/linux-64/libfreetype6-2.14.3-h73754d4_0.conda#fb16b4b69e3f1dcfe79d80db8fd0c55d +https://conda.anaconda.org/conda-forge/linux-64/libfreetype-2.14.3-ha770c72_0.conda#e289f3d17880e44b633ba911d57a321b +https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.19-h0c24ade_0.conda#f302dbf397ac82eaf9618575d0b5fe33 +https://conda.anaconda.org/conda-forge/linux-64/pillow-12.2.0-py312h50c33e8_0.conda#9e5609720e31213d4f39afe377f6217e +https://conda.anaconda.org/conda-forge/linux-64/kiwisolver-1.5.0-py312h0a2e395_0.conda#cd74a9525dc74bbbf93cf8aa2fa9eb5b +https://conda.anaconda.org/conda-forge/linux-64/freetype-2.14.3-ha770c72_0.conda#8462b5322567212beeb025f3519fb3e2 +https://conda.anaconda.org/conda-forge/linux-64/unicodedata2-17.0.1-py312h4c3975b_0.conda#0b6c506ec1f272b685240e70a29261b8 +https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda#37293a85a0f4f77bbd9cf7aaefc62609 +https://conda.anaconda.org/conda-forge/linux-64/brotli-bin-1.2.0-hb03c661_1.conda#af39b9a8711d4a8d437b52c1d78eb6a1 +https://conda.anaconda.org/conda-forge/linux-64/brotli-1.2.0-hed03a55_1.conda#8ccf913aaba749a5496c17629d859ed1 +https://conda.anaconda.org/conda-forge/linux-64/fonttools-4.62.1-py312h8a5da7c_0.conda#2396fee22e84f69dffc6e23135905ce8 +https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhcf101f3_2.conda#4c2a8fef270f6c69591889b93f9f55c1 +https://conda.anaconda.org/conda-forge/linux-64/contourpy-1.3.3-py312h0a2e395_4.conda#43c2bc96af3ae5ed9e8a10ded942aa50 +https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.10.9-py312he3d6523_0.conda#7d499b5b6d150f133800dc3a582771c7 +https://conda.anaconda.org/conda-forge/linux-64/scipy-1.17.1-py312h54fa4ab_0.conda#3e38daeb1fb05a95656ff5af089d2e4c +https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda#9d64911b31d57ca443e9f1e36b04385f +https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda#8e194e7b992f99a5015edbd4ebd38efd +https://conda.anaconda.org/conda-forge/noarch/joblib-1.5.3-pyhd8ed1ab_0.conda#615de2a4d97af50c350e5cf160149e77 +https://conda.anaconda.org/conda-forge/linux-64/scikit-learn-1.8.0-np2py312h3226591_1.conda#38decbeae260892040709cafc0514162 +https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda#a2c1eeadae7a309daed9d62c96012a2b +https://conda.anaconda.org/conda-forge/noarch/mapclassify-2.10.0-pyhd8ed1ab_1.conda#cc293b4cad9909bf66ca117ea90d4631 +https://conda.anaconda.org/conda-forge/linux-64/shapely-2.1.2-py312h383787d_2.conda#69e400d3deca12ee7afd4b73a5596905 +https://conda.anaconda.org/conda-forge/noarch/geopandas-base-1.1.3-pyha770c72_0.conda#18789a85c307970ae1786dfc6dfd234f +https://conda.anaconda.org/conda-forge/noarch/branca-0.8.2-pyhd8ed1ab_0.conda#1fcdf88e7a8c296d3df8409bf0690db4 +https://conda.anaconda.org/conda-forge/noarch/folium-0.20.0-pyhd8ed1ab_0.conda#a6997a7dcd6673c0692c61dfeaea14ab +https://conda.anaconda.org/conda-forge/noarch/geopandas-1.1.3-pyhd8ed1ab_0.conda#4eb8b870142ca06d2a1d2c74662eac7d +https://conda.anaconda.org/conda-forge/noarch/earth-osm-3.0.2-pyhd8ed1ab_0.conda#eea693a0e40b4aa17590a63d055a068a diff --git a/workflow/envs/module.osx-arm64.pin.txt b/workflow/envs/module.osx-arm64.pin.txt new file mode 100644 index 0000000..a02f1f0 --- /dev/null +++ b/workflow/envs/module.osx-arm64.pin.txt @@ -0,0 +1,157 @@ +# Generated by `pixi workspace export` +# platform: osx-arm64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/osx-arm64/yaml-0.2.5-h925e9cb_3.conda#78a0fe9e9c50d2c381e8ee47e3ea437d +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-9_cp312.conda#4c32076993e6270825441d059ab5c18b +https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda#ad659d0a2b3e47e38d829aa8cad2d610 +https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.2-h8088a28_2.conda#bc5a5721b6439f2f62a84f2548136082 +https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda#a9d86bc62f39b94c4661716624eb21b0 +https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.6-h1d4f5a5_0.conda#343d10ed5b44030a2f67193905aea159 +https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda#f8381319127120ce51e081dce4865cf4 +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-hbd8a1cb_0.conda#e18ad67cf881dcadee8b8d9e2f8e5f73 +https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.2-hd24854e_0.conda#25dcccd4f80f1638428613e0d7c9b4e1 +https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-hef89b57_0.conda#f1182c91c0de31a7abd40cedf6a5ebef +https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.53.4-hca69786_1.conda#fde96d40ebe9a9cb34e4122380189ddb +https://conda.anaconda.org/conda-forge/osx-arm64/libcxx-22.1.4-h55c6f16_0.conda#448a1af83a9205655ee1cf48d3875ca3 +https://conda.anaconda.org/conda-forge/osx-arm64/libpython-3.12.14-h4e5ec87_3_cpython.conda#7fd5ad6d9d1ae0af3ae9f039a992a148 +https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.3-h8088a28_0.conda#b1fd823b5ae54fbec272cea0811bd8a9 +https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.7.0-h47dc5ef_1.conda#216bbcc23c11e9695b3db4a8771d76eb +https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.8.4-h6111c0a_0.conda#f161bcc1e6de30d76093b4534b8f1a77 +https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda#620b85a3f45526a8bc4d23fd78fc22f0 +https://conda.anaconda.org/conda-forge/osx-arm64/python-3.12.14-hd05a0c4_3_cpython.conda#287a1be640df74bd3e2b9d76b2a7b300 +https://conda.anaconda.org/conda-forge/osx-arm64/pyyaml-6.0.3-py312h04c11ed_1.conda#95a5f0831b5e0b1075bbd80fcffc52ac +https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha55dd90_7.conda#461219d1a5bd61342293efa2c0c90eac +https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda#8e6923fc12f1fe8f8c4e5c9f343256ac +https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda#0a802cb9888dd14eeefc611f05c40b6e +https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda#164fc43f0b53b6e3a7bc7dce5e4f1dc9 +https://conda.anaconda.org/conda-forge/osx-arm64/brotli-python-1.2.0-py312ha52686f_4.conda#a99e0c53fc3932defa0c89757c14b8ce +https://conda.anaconda.org/conda-forge/osx-arm64/zstd-1.5.7-hbf9d68e_6.conda#ab136e4c34e97f34fb621d2592a393d8 +https://conda.anaconda.org/conda-forge/osx-arm64/backports.zstd-1.7.0-py312h1a36842_1.conda#4f0bd292b5aeab40ebcb3aae5afdf1a8 +https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda#9272daa869e03efe68833e3dc7a02130 +https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda#fb7130c190f9b4ec91219840a05ba3ac +https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda#a9167b9571f3baa9d448faa2139d1089 +https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda#929471569c93acefb30282a22060dcd5 +https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda#9659f587a8ceacc21864260acd02fc67 +https://conda.anaconda.org/conda-forge/osx-arm64/lz4-c-1.10.0-h286801f_1.conda#01511afc6cc1909c5303cf31be17b44f +https://conda.anaconda.org/conda-forge/osx-arm64/pyosmium-4.3.1-py312h892b0b2_1.conda#9608fc822b765f7a72282a2c2ef1de06 +https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-22.1.4-hc7d1edf_0.conda#46d04a647df7a4525e487d88068d19ef +https://conda.anaconda.org/conda-forge/osx-arm64/_openmp_mutex-4.5-7_kmp_llvm.conda#a44032f282e7d2acdeb1c240308052dd +https://conda.anaconda.org/conda-forge/osx-arm64/libgcc-15.2.0-hcbb3090_18.conda#92df6107310b1fff92c4cc84f0de247b +https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran5-15.2.0-hdae7583_18.conda#c4a6f7989cffb0544bfd9207b6789971 +https://conda.anaconda.org/conda-forge/osx-arm64/libgfortran-15.2.0-h07b0088_18.conda#26981599908ed2205366e8fc91b37fc6 +https://conda.anaconda.org/conda-forge/osx-arm64/libopenblas-0.3.32-openmp_he657e61_0.conda#3a1111a4b6626abebe8b978bb5a323bf +https://conda.anaconda.org/conda-forge/osx-arm64/libblas-3.11.0-6_h51639a9_openblas.conda#e551103471911260488a02155cef9c94 +https://conda.anaconda.org/conda-forge/osx-arm64/liblapack-3.11.0-6_hd9741b5_openblas.conda#ee33d2d05a7c5ea1f67653b37eb74db1 +https://conda.anaconda.org/conda-forge/osx-arm64/libcblas-3.11.0-6_hb0561ab_openblas.conda#805c6d31c5621fd75e53dfcf21fb243a +https://conda.anaconda.org/conda-forge/osx-arm64/numpy-2.5.3-py312hff34920_0.conda#6c950b1c9f19db9df914704508f3be20 +https://conda.anaconda.org/conda-forge/osx-arm64/markupsafe-3.0.3-py312h04c11ed_1.conda#11d95ab83ef0a82cc2de12c1e0b47fe4 +https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda#04558c96691bed63104678757beb4f8d +https://conda.anaconda.org/conda-forge/noarch/pydeck-0.9.3-pyhd8ed1ab_0.conda#7c94c948b32d681345f134ae11892fee +https://conda.anaconda.org/conda-forge/osx-arm64/libboost-1.90.0-h30c325d_3.conda#71cd9902ca7347767f4ea7170c0acdbb +https://conda.anaconda.org/conda-forge/osx-arm64/osmium-tool-1.19.1-h693c552_0.conda#8d50fef448dfb3621b347453e57e584c +https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.3-pyh8f84b5b_0.conda#e5ce43272193b38c2e9037446c1d9206 +https://conda.anaconda.org/conda-forge/osx-arm64/libabseil-20260526.0-cxx17_h4c27e2a_2.conda#2aa5e7dc7b5d218908effd2a8c70a8a8 +https://conda.anaconda.org/conda-forge/osx-arm64/protobuf-7.35.1-py312h5674827_3.conda#b3a91d2e886988232fb2eedc016aa22c +https://conda.anaconda.org/conda-forge/noarch/pytz-2026.2-pyhcf101f3_0.conda#03cb60f505ad3ada0a95277af5faeb1a +https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.3-pyhd8ed1ab_0.conda#eefc8d916bd2e708d76d40398ef9a1ee +https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda#3339e3b65d58accf4ca4fb8748ab16b3 +https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 +https://conda.anaconda.org/conda-forge/osx-arm64/pandas-2.3.3-py312h5978115_2.conda#fcbba82205afa4956c39136c68929385 +https://conda.anaconda.org/conda-forge/noarch/xyzservices-2026.3.0-pyhd8ed1ab_0.conda#4487b9c371d0161d54b5c7bbd890c0fc +https://conda.anaconda.org/conda-forge/osx-arm64/sqlite-3.53.4-ha9f7ffe_1.conda#c144cc02c82671616e0fb4caf731bc88 +https://conda.anaconda.org/conda-forge/osx-arm64/libwebp-base-1.6.0-h07db88b_0.conda#e5e7d467f80da752be17796b87fe6385 +https://conda.anaconda.org/conda-forge/osx-arm64/libjpeg-turbo-3.2.0-h84a0fba_1.conda#b2f8c8e5a7651a1d7c05404c3a159517 +https://conda.anaconda.org/conda-forge/osx-arm64/libdeflate-1.25-hc11a715_0.conda#a6130c709305cd9828b4e1bd9ba0000c +https://conda.anaconda.org/conda-forge/osx-arm64/lerc-4.2.0-h1eee2c3_0.conda#e429aec4037d5cb8fd34ded9f5dadd39 +https://conda.anaconda.org/conda-forge/osx-arm64/libtiff-4.7.2-hf67920b_1.conda#b6b191267455bf202af79f973690ed5d +https://conda.anaconda.org/conda-forge/osx-arm64/libssh2-1.11.1-h1590b86_0.conda#b68e8f66b94b44aaa8de4583d3d4cc40 +https://conda.anaconda.org/conda-forge/osx-arm64/libpsl-0.23.1-hdb0c161_1.conda#5d270f716a2b4bc13df9af8612071b17 +https://conda.anaconda.org/conda-forge/osx-arm64/libev-4.33-h93a5062_2.conda#36d33e440c31857372a72137f78bacf5 +https://conda.anaconda.org/conda-forge/osx-arm64/c-ares-1.34.6-hc919400_0.conda#bcb3cba70cf1eec964a03b4ba7775f01 +https://conda.anaconda.org/conda-forge/osx-arm64/libnghttp2-1.68.1-h8f3e76b_0.conda#6ea18834adbc3b33df9bd9fb45eaf95b +https://conda.anaconda.org/conda-forge/osx-arm64/libedit-3.1.20250104-pl5321hafb1f1b_0.conda#44083d2d2c2025afca315c7a172eab2b +https://conda.anaconda.org/conda-forge/osx-arm64/krb5-1.22.2-h385eeb1_0.conda#e446e1822f4da8e5080a9de93474184d +https://conda.anaconda.org/conda-forge/osx-arm64/libcurl-8.22.0-h6651222_0.conda#a7234b8d8e19a089dcb1eaaa18de1045 +https://conda.anaconda.org/conda-forge/osx-arm64/proj-9.9.0-h51dee2d_0.conda#3e1d545c468056c085841e97773978fd +https://conda.anaconda.org/conda-forge/osx-arm64/pyproj-3.8.0-py312h16d7083_2.conda#46be4aa3f1d295deba4ee929795d4d97 +https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda#58335b26c38bf4a20f399384c33cbcf9 +https://conda.anaconda.org/conda-forge/osx-arm64/xerces-c-3.3.0-h25f632f_1.conda#0b886d06130b774f086d3b2ce0b7277a +https://conda.anaconda.org/conda-forge/osx-arm64/pcre2-10.47-h30297fc_0.conda#9b4190c4055435ca3502070186eba53a +https://conda.anaconda.org/conda-forge/osx-arm64/muparser-2.3.5-h11e0b38_0.conda#1cdbe54881794ee356d3cba7e3ed6668 +https://conda.anaconda.org/conda-forge/osx-arm64/libiconv-1.18-h23cfdf5_2.conda#4d5a7445f0b25b6a3ddbb56e790f5251 +https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-16-2.15.4-heb56d2d_0.conda#b40633f711a5eb8617917e0e944b9965 +https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.15.4-h550178d_0.conda#4b97a34d06740b578c32deba195daf5a +https://conda.anaconda.org/conda-forge/osx-arm64/zlib-1.3.2-h8088a28_2.conda#f1c0bce276210bed45a04949cfe8dc20 +https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-devel-2.15.4-h550178d_0.conda#df2f9521a1b80a859d6d329dba7df2a0 +https://conda.anaconda.org/conda-forge/osx-arm64/geos-3.14.1-h5afe852_0.conda#4238412c29eff0bb2bb5c60a720c035a +https://conda.anaconda.org/conda-forge/osx-arm64/librttopo-1.1.0-ha909e78_20.conda#d07359797436cfc891b38e203cf0caac +https://conda.anaconda.org/conda-forge/osx-arm64/minizip-4.0.10-hff1a8ea_0.conda#93def148863d840e500490d6d78722f9 +https://conda.anaconda.org/conda-forge/osx-arm64/freexl-2.0.0-h3ab3353_2.conda#dd655a29b40fe0d1bf95c64cf3cb348d +https://conda.anaconda.org/conda-forge/osx-arm64/libspatialite-5.1.0-gpl_h16ba230_122.conda#59c144f60bb80b88854f96f62d0936ad +https://conda.anaconda.org/conda-forge/osx-arm64/libpng-1.6.58-h132b30e_0.conda#2259ae0949dbe20c0665850365109b27 +https://conda.anaconda.org/conda-forge/osx-arm64/uriparser-0.9.8-h00cdb27_0.conda#e8ff9e11babbc8cd77af5a4258dc2802 +https://conda.anaconda.org/conda-forge/osx-arm64/libkml-1.3.0-hb833057_1023.conda#3d8eeedb8e541d1cb593e8204fcc397d +https://conda.anaconda.org/conda-forge/osx-arm64/libhwy-1.4.0-ha332bbd_0.conda#7394850583ca88325244b68b532c7a39 +https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlicommon-1.2.0-h1dcdb26_4.conda#8f3981871d167a6cd323e636e1929dd4 +https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlienc-1.2.0-h2ddc9cb_4.conda#39a25d500b191c16996239c4afa104f1 +https://conda.anaconda.org/conda-forge/osx-arm64/libbrotlidec-1.2.0-h5295a6a_4.conda#da537a1643ef3e13bdccf16cca206f2c +https://conda.anaconda.org/conda-forge/osx-arm64/libjxl-0.12.0-hb71b141_2.conda#f62de5ae42f3c1f54f87ae2b303f6e90 +https://conda.anaconda.org/conda-forge/osx-arm64/lzo-2.10-h925e9cb_1002.conda#e56eaa1beab0e7fed559ae9c0264dd88 +https://conda.anaconda.org/conda-forge/osx-arm64/libarchive-3.8.9-gpl_h76147b0_101.conda#bdcec8546f15ded1b3718309e027e603 +https://conda.anaconda.org/conda-forge/osx-arm64/json-c-0.18-he4178ee_0.conda#94f14ef6157687c30feb44e1abecd577 +https://conda.anaconda.org/conda-forge/osx-arm64/giflib-6.1.3-h74c22ad_2.conda#edc562dcbe5872664817e64575aa325f +https://conda.anaconda.org/conda-forge/osx-arm64/snappy-1.2.2-hada39a4_1.conda#fca4a2222994acd7f691e57f94b750c5 +https://conda.anaconda.org/conda-forge/osx-arm64/blosc-1.21.6-h7dd00d9_1.conda#925acfb50a750aa178f7a0aced77f351 +https://conda.anaconda.org/conda-forge/osx-arm64/libgdal-core-3.13.3-hc1e97f0_2.conda#298c053b25279574b587cfbc3e24c1ee +https://conda.anaconda.org/conda-forge/osx-arm64/pyogrio-0.13.0-py312hd1c4548_0.conda#8a4e8760220acc83b445a0feb56d7b7a +https://conda.anaconda.org/conda-forge/osx-arm64/qhull-2020.2-h420ef59_5.conda#6483b1f59526e05d7d894e466b5b6924 +https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.2-pyhcf101f3_0.conda#3687cc0b82a8b4c17e1f0eb7e47163d5 +https://conda.anaconda.org/conda-forge/osx-arm64/zlib-ng-2.3.3-hed4e4f5_1.conda#d99c2a23a31b0172e90f456f580b695e +https://conda.anaconda.org/conda-forge/osx-arm64/openjpeg-2.5.4-hd9e9057_0.conda#4b5d3a91320976eec71678fad1e3569b +https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxdmcp-1.1.5-hc919400_1.conda#9d1299ace1924aa8f4e0bc8e71dd0cf7 +https://conda.anaconda.org/conda-forge/osx-arm64/xorg-libxau-1.0.12-hc919400_1.conda#78b548eed8227a689f93775d5d23ae09 +https://conda.anaconda.org/conda-forge/osx-arm64/pthread-stubs-0.4-hd74edd7_1002.conda#415816daf82e0b23a736a069a75e9da7 +https://conda.anaconda.org/conda-forge/osx-arm64/libxcb-1.17.0-hdb1d25a_0.conda#af523aae2eca6dfa1c8eec693f5b9a79 +https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype6-2.14.3-hdfa99f5_0.conda#e98ba7b5f09a5f450eca083d5a1c4649 +https://conda.anaconda.org/conda-forge/osx-arm64/libfreetype-2.14.3-hce30654_0.conda#f73b109d49568d5d1dda43bb147ae37f +https://conda.anaconda.org/conda-forge/osx-arm64/lcms2-2.19.1-hce41798_3.conda#4eed1ee02ef1992c046f2e81d5606633 +https://conda.anaconda.org/conda-forge/osx-arm64/pillow-12.3.0-py312hf9dc30a_4.conda#1c7b0a12a4657e8a296306b32712f0ed +https://conda.anaconda.org/conda-forge/osx-arm64/libintl-0.25.1-h493aca8_0.conda#5103f6a6b210a3912faf8d7db516918c +https://conda.anaconda.org/conda-forge/osx-arm64/libglib-2.90.0-hccd281b_0.conda#8f63a5f35547d0c6d819830ebe0d3245 +https://conda.anaconda.org/conda-forge/osx-arm64/graphite2-1.3.15-h784d473_1.conda#0c7b78d9ffff4f5a6ca28d346f734d8f +https://conda.anaconda.org/conda-forge/osx-arm64/pixman-0.46.4-h81086ad_1.conda#17c3d745db6ea72ae2fce17e7338547f +https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda#49023d73832ef61042f6a237cb2687e7 +https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2#4d59c254e01d9cde7957100457e2d5fb +https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2#34893075a5c9e55cdafac56607368fc6 +https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2#0c96522c6bdaed4b1566d11387caaf45 +https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda#a7970cd949a077b7cb9696379d338681 +https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab +https://conda.anaconda.org/conda-forge/osx-arm64/fontconfig-2.17.1-h2b252f5_0.conda#d06ae1a11b46cc4c74177ecd28de7c7a +https://conda.anaconda.org/conda-forge/osx-arm64/cairo-1.18.4-he0f2337_1.conda#36200ecfbbfbcb82063c87725434161f +https://conda.anaconda.org/conda-forge/osx-arm64/libharfbuzz-14.4.0-hcda0f7c_1.conda#4afba4fae30b467d6c699bb685fa35ce +https://conda.anaconda.org/conda-forge/osx-arm64/fribidi-1.0.16-hc919400_0.conda#04bdce8d93a4ed181d1d726163c2d447 +https://conda.anaconda.org/conda-forge/osx-arm64/libraqm-0.11.0-h45af499_0.conda#46ea0eb4e71bffa35ab7070678bcb053 +https://conda.anaconda.org/conda-forge/osx-arm64/kiwisolver-1.5.1-py312h67ba313_3.conda#0f9c3cfcf6106a7f6e99acf39095d3a6 +https://conda.anaconda.org/conda-forge/osx-arm64/freetype-2.14.3-hce30654_0.conda#6dcc75ba2e04c555e881b72793d3282f +https://conda.anaconda.org/conda-forge/osx-arm64/unicodedata2-18.0.0-py312ha053593_0.conda#639c032b3599544afe156821a816895b +https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda#37293a85a0f4f77bbd9cf7aaefc62609 +https://conda.anaconda.org/conda-forge/osx-arm64/brotli-bin-1.2.0-he4a93e4_4.conda#d065d0732d02ace747059c3d58338fcd +https://conda.anaconda.org/conda-forge/osx-arm64/brotli-1.2.0-hf00d406_4.conda#e32ad50b0f977e3fa616a578a2643e46 +https://conda.anaconda.org/conda-forge/osx-arm64/fonttools-4.65.0-py312hf4454c7_0.conda#4e3e046489bab9885148f2b228108d5d +https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhcf101f3_2.conda#4c2a8fef270f6c69591889b93f9f55c1 +https://conda.anaconda.org/conda-forge/osx-arm64/contourpy-1.4.0-py312h67ba313_1.conda#e2ed1d619ef9e3e0fb5f4703d029d8e2 +https://conda.anaconda.org/conda-forge/osx-arm64/matplotlib-base-3.11.2-py312h11997a8_1.conda#5b7f99b5b32911c24f0e40e3cf38861e +https://conda.anaconda.org/conda-forge/osx-arm64/scipy-1.18.0-py312h4519d97_0.conda#5a352ca7e4f49ca6585dd30a4812bd20 +https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda#9d64911b31d57ca443e9f1e36b04385f +https://conda.anaconda.org/conda-forge/noarch/narwhals-2.26.0-pyh5ded981_0.conda#afb7146667be7c8121f5f8337be60680 +https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda#8e194e7b992f99a5015edbd4ebd38efd +https://conda.anaconda.org/conda-forge/noarch/joblib-1.5.3-pyhd8ed1ab_0.conda#615de2a4d97af50c350e5cf160149e77 +https://conda.anaconda.org/conda-forge/osx-arm64/scikit-learn-1.9.1-np2py312hda66c2e_1.conda#981a4a060c154c440ebb298ad6f96b24 +https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda#a2c1eeadae7a309daed9d62c96012a2b +https://conda.anaconda.org/conda-forge/noarch/mapclassify-2.10.0-pyhd8ed1ab_1.conda#cc293b4cad9909bf66ca117ea90d4631 +https://conda.anaconda.org/conda-forge/osx-arm64/shapely-2.1.2-py312h0fc4a54_3.conda#24f93f977d15383977247b1b6e356626 +https://conda.anaconda.org/conda-forge/noarch/geopandas-base-1.1.3-pyha770c72_0.conda#18789a85c307970ae1786dfc6dfd234f +https://conda.anaconda.org/conda-forge/noarch/branca-0.8.2-pyhd8ed1ab_0.conda#1fcdf88e7a8c296d3df8409bf0690db4 +https://conda.anaconda.org/conda-forge/noarch/folium-0.20.0-pyhd8ed1ab_0.conda#a6997a7dcd6673c0692c61dfeaea14ab +https://conda.anaconda.org/conda-forge/noarch/geopandas-1.1.3-pyhd8ed1ab_0.conda#4eb8b870142ca06d2a1d2c74662eac7d +https://conda.anaconda.org/conda-forge/noarch/earth-osm-3.0.2-pyhd8ed1ab_0.conda#eea693a0e40b4aa17590a63d055a068a diff --git a/workflow/envs/module.win-64.pin.txt b/workflow/envs/module.win-64.pin.txt new file mode 100644 index 0000000..8c160b6 --- /dev/null +++ b/workflow/envs/module.win-64.pin.txt @@ -0,0 +1,149 @@ +# Generated by `pixi workspace export` +# platform: win-64 +@EXPLICIT +https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda#71b24316859acd00bdb8b38f5e2ce328 +https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda#242d9f25d2ae60c76b38a5e42858e51d +https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda#37eb311485d2d8b2c419449582046a42 +https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda#1e610f2416b6acdd231c5f573d754a0f +https://conda.anaconda.org/conda-forge/win-64/yaml-0.2.5-h6a83c73_3.conda#433699cba6602098ae8957a323da2664 +https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-9_cp312.conda#4c32076993e6270825441d059ab5c18b +https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda#ad659d0a2b3e47e38d829aa8cad2d610 +https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda#0481bfd9814bf525bd4b3ee4b51494c4 +https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.4.22-h4c7d964_0.conda#56fb2c6c73efc627b40c77d14caecfba +https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.2-hf411b9b_0.conda#05c7d624cff49dbd8db1ad5ba537a8a3 +https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.2-hfd05255_2.conda#dbabbd6234dea34040e631f87676292f +https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.53.4-hf5d6505_1.conda#a72d495965b144bb7da033642d389047 +https://conda.anaconda.org/conda-forge/win-64/libpython-3.12.14-hcc31f7b_3_cpython.conda#2a7b3fc2441dc4df29a5804d677fb720 +https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.3-hfd05255_0.conda#8f83619ab1588b98dd99c90b0bfc5c6d +https://conda.anaconda.org/conda-forge/win-64/libffi-3.7.0-h3d046cb_1.conda#ceb38b0ba900847d8da4289c3c8e5708 +https://conda.anaconda.org/conda-forge/win-64/libexpat-2.8.4-hac47afa_0.conda#7e9023b0034e87d324ad64809859184e +https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda#4cb8e6b48f67de0b018719cdf1136306 +https://conda.anaconda.org/conda-forge/win-64/python-3.12.14-hb12b558_3_cpython.conda#ca6b5db05602d2a98f8694c2ad6aa6fb +https://conda.anaconda.org/conda-forge/win-64/pyyaml-6.0.3-py312h05f76fc_1.conda#9f6ebef672522cb9d9a6257215ca5743 +https://conda.anaconda.org/conda-forge/noarch/win_inet_pton-1.1.0-pyh7428d3b_8.conda#46e441ba871f524e2b067929da3051c2 +https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyh09c184e_7.conda#e2fd202833c4a981ce8a65974fe4abd1 +https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.1.0-pyhd8ed1ab_0.conda#8e6923fc12f1fe8f8c4e5c9f343256ac +https://conda.anaconda.org/conda-forge/noarch/hpack-4.1.0-pyhd8ed1ab_0.conda#0a802cb9888dd14eeefc611f05c40b6e +https://conda.anaconda.org/conda-forge/noarch/h2-4.3.0-pyhcf101f3_0.conda#164fc43f0b53b6e3a7bc7dce5e4f1dc9 +https://conda.anaconda.org/conda-forge/win-64/brotli-python-1.2.0-py312ha763cb9_4.conda#fd844f78ba9802979d0e6d856d7bcac3 +https://conda.anaconda.org/conda-forge/win-64/zstd-1.5.7-h534d264_6.conda#053b84beec00b71ea8ff7a4f84b55207 +https://conda.anaconda.org/conda-forge/win-64/backports.zstd-1.7.0-py312h06d0912_1.conda#fd4bf31a33cda994eea805305cf1ed97 +https://conda.anaconda.org/conda-forge/noarch/urllib3-2.6.3-pyhd8ed1ab_0.conda#9272daa869e03efe68833e3dc7a02130 +https://conda.anaconda.org/conda-forge/noarch/idna-3.13-pyhcf101f3_0.conda#fb7130c190f9b4ec91219840a05ba3ac +https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.4.7-pyhd8ed1ab_0.conda#a9167b9571f3baa9d448faa2139d1089 +https://conda.anaconda.org/conda-forge/noarch/certifi-2026.4.22-pyhd8ed1ab_0.conda#929471569c93acefb30282a22060dcd5 +https://conda.anaconda.org/conda-forge/noarch/requests-2.33.1-pyhcf101f3_1.conda#9659f587a8ceacc21864260acd02fc67 +https://conda.anaconda.org/conda-forge/win-64/lz4-c-1.10.0-h2466b09_1.conda#0b69331897a92fac3d8923549d48d092 +https://conda.anaconda.org/conda-forge/win-64/pyosmium-4.3.1-py312h3e3ada1_1.conda#11fe8c8b785065defc642792be2a591a +https://conda.anaconda.org/conda-forge/win-64/libiconv-1.18-hc1393d2_2.conda#64571d1dd6cdcfa25d0664a5950fdaa2 +https://conda.anaconda.org/conda-forge/win-64/icu-78.3-h637d24d_0.conda#0097b24800cb696915c3dbd1f5335d3f +https://conda.anaconda.org/conda-forge/win-64/libxml2-16-2.15.4-h3cfd58e_0.conda#bab5489f4bd38494ffaa83d6ab924c1f +https://conda.anaconda.org/conda-forge/win-64/libxml2-2.15.4-he095d88_0.conda#3ce54d0f8f24dfab99d82414411640aa +https://conda.anaconda.org/conda-forge/win-64/libwinpthread-12.0.0.r4.gg4f2fc60ca-h57928b3_10.conda#8a86073cf3b343b87d03f41790d8b4e5 +https://conda.anaconda.org/conda-forge/win-64/libhwloc-2.12.2-default_h4379cf1_1000.conda#3b576f6860f838f950c570f4433b086e +https://conda.anaconda.org/conda-forge/win-64/tbb-2023.0.0-ha3553a1_1.conda#34aa94d586fe95fa121966c0d4e73cf4 +https://conda.anaconda.org/conda-forge/win-64/onemkl-license-2025.3.1-h57928b3_12.conda#29ad104e60faa7ed1dc549ec029764bb +https://conda.anaconda.org/conda-forge/win-64/llvm-openmp-22.1.4-h4fa8253_0.conda#761757ab617e8bfef18cc422dd02bbad +https://conda.anaconda.org/conda-forge/win-64/mkl-2025.3.1-hac47afa_12.conda#a3b1ee571898432da7e13ecb7bfd76ae +https://conda.anaconda.org/conda-forge/win-64/libblas-3.11.0-6_hf2e6a31_mkl.conda#95543eec964b4a4a7ca3c4c9be481aa1 +https://conda.anaconda.org/conda-forge/win-64/liblapack-3.11.0-6_hf9ab0e9_mkl.conda#7e9cdaf6f302142bc363bbab3b5e7074 +https://conda.anaconda.org/conda-forge/win-64/libcblas-3.11.0-6_h2a3cdd5_mkl.conda#9e4bf521c07f4d423cba9296b7927e3c +https://conda.anaconda.org/conda-forge/win-64/numpy-2.5.3-py312ha3f287d_0.conda#78895c70424556e2bf7bf2f628fc8778 +https://conda.anaconda.org/conda-forge/win-64/markupsafe-3.0.3-py312h05f76fc_1.conda#a73298d225c7852f97403ca105d10a13 +https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.6-pyhcf101f3_1.conda#04558c96691bed63104678757beb4f8d +https://conda.anaconda.org/conda-forge/noarch/pydeck-0.9.3-pyhd8ed1ab_0.conda#7c94c948b32d681345f134ae11892fee +https://conda.anaconda.org/conda-forge/win-64/libboost-1.90.0-h9dfe17d_3.conda#15620ea8e1fa6d5eb275ee8665f88b32 +https://conda.anaconda.org/conda-forge/win-64/osmium-tool-1.19.1-h60971b7_0.conda#c5ea6678eb9b3a4c7de28dd592e68662 +https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda#962b9857ee8e7018c22f2776ffa0b2d7 +https://conda.anaconda.org/conda-forge/noarch/tqdm-4.67.3-pyha7b4d00_0.conda#af77160f8428924c17db94e04aa69409 +https://conda.anaconda.org/conda-forge/win-64/protobuf-7.35.1-py312h87014b1_3.conda#92f82e85bdf7b9e69f7406c7c71c8e7d +https://conda.anaconda.org/conda-forge/noarch/pytz-2026.2-pyhcf101f3_0.conda#03cb60f505ad3ada0a95277af5faeb1a +https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2026.2-pyhd8ed1ab_0.conda#f6ad7450fc21e00ecc23812baed6d2e4 +https://conda.anaconda.org/conda-forge/noarch/six-1.17.0-pyhe01879c_1.conda#3339e3b65d58accf4ca4fb8748ab16b3 +https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0.post0-pyhe01879c_2.conda#5b8d21249ff20967101ffa321cab24e8 +https://conda.anaconda.org/conda-forge/win-64/pandas-2.3.3-py312hc128f0a_2.conda#57d80e87a8b3161bcf26472deceaa556 +https://conda.anaconda.org/conda-forge/noarch/xyzservices-2026.3.0-pyhd8ed1ab_0.conda#4487b9c371d0161d54b5c7bbd890c0fc +https://conda.anaconda.org/conda-forge/win-64/sqlite-3.53.4-hdb435a2_1.conda#df9d7c5d34602e0f2655a524d31fce87 +https://conda.anaconda.org/conda-forge/win-64/libjpeg-turbo-3.2.0-hfd05255_1.conda#fc2c23bacefd1e733f1e1d6cd3b2aaeb +https://conda.anaconda.org/conda-forge/win-64/libdeflate-1.25-h51727cc_0.conda#e77030e67343e28b084fabd7db0ce43e +https://conda.anaconda.org/conda-forge/win-64/lerc-4.2.0-hd936e49_0.conda#add59e2b60ac9d4299d17c938185c75a +https://conda.anaconda.org/conda-forge/win-64/libtiff-4.7.2-h8f73337_1.conda#970dbe48f843e7fbd179a9b6c22f865a +https://conda.anaconda.org/conda-forge/win-64/libssh2-1.11.1-h9aa295b_0.conda#9dce2f112bfd3400f4f432b3d0ac07b2 +https://conda.anaconda.org/conda-forge/win-64/libpsl-0.23.1-h9b16d47_1.conda#39cdf8c4f59e4d253cfa8091c8b3d7de +https://conda.anaconda.org/conda-forge/win-64/krb5-1.22.2-h0ea6238_0.conda#4432f52dc0c8eb6a7a6abc00a037d93c +https://conda.anaconda.org/conda-forge/win-64/libcurl-8.22.0-hdb0ef4a_0.conda#e8405e754eaac42d66f957222fcfd876 +https://conda.anaconda.org/conda-forge/win-64/proj-9.9.0-hd30e2cd_0.conda#0b883556cc76611b4231a33577a26335 +https://conda.anaconda.org/conda-forge/win-64/pyproj-3.8.0-py312h5a521b9_2.conda#2e48fc674b51a8a1bcfc887fc18f2abb +https://conda.anaconda.org/conda-forge/noarch/packaging-25.0-pyh29332c3_1.conda#58335b26c38bf4a20f399384c33cbcf9 +https://conda.anaconda.org/conda-forge/win-64/xerces-c-3.3.0-hac47afa_1.conda#d1097e01041cfed41c81f1e3d1f52572 +https://conda.anaconda.org/conda-forge/win-64/pcre2-10.47-hd2b5f0e_0.conda#77eaf2336f3ae749e712f63e36b0f0a1 +https://conda.anaconda.org/conda-forge/win-64/muparser-2.3.5-he0c23c2_0.conda#013aabb169d59009bdf7d70319360e9b +https://conda.anaconda.org/conda-forge/win-64/libwebp-base-1.6.0-h4d5522a_0.conda#f9bbae5e2537e3b06e0f7310ba76c893 +https://conda.anaconda.org/conda-forge/win-64/zlib-1.3.2-hfd05255_2.conda#5187ecf958be3c39110fe691cbd6873e +https://conda.anaconda.org/conda-forge/win-64/libxml2-devel-2.15.4-he095d88_0.conda#d50e00d25a326eb487f9f5540e09b77b +https://conda.anaconda.org/conda-forge/win-64/geos-3.14.1-hdade9fe_0.conda#8c75d7e401a4d799ce8d4bb922320967 +https://conda.anaconda.org/conda-forge/win-64/librttopo-1.1.0-haa95264_20.conda#7eeb5aed49853f8b3e1ca0463ef55a8e +https://conda.anaconda.org/conda-forge/win-64/minizip-4.0.10-h9fa1bad_0.conda#2ffdc180adc65f509e996d63513c04b7 +https://conda.anaconda.org/conda-forge/win-64/freexl-2.0.0-hf297d47_2.conda#d6a8059de245e53478b581742b53f71d +https://conda.anaconda.org/conda-forge/win-64/libspatialite-5.1.0-gpl_hfd1cb16_122.conda#6bb7178f7f280a88c1a79c0c0a79213a +https://conda.anaconda.org/conda-forge/win-64/libpng-1.6.58-h7351971_0.conda#52f1280563f3b48b5f75414cd2d15dd1 +https://conda.anaconda.org/conda-forge/win-64/uriparser-0.9.8-h5a68840_0.conda#28b4cf9065681f43cc567410edf8243d +https://conda.anaconda.org/conda-forge/win-64/libkml-1.3.0-h68a222c_1023.conda#b44e0d9eb84c6c086d809787aab0a1da +https://conda.anaconda.org/conda-forge/win-64/libhwy-1.4.0-h172a326_0.conda#aeca1cb6665f19e560c1fbd20b5bcf34 +https://conda.anaconda.org/conda-forge/win-64/libbrotlicommon-1.2.0-hf02afa3_4.conda#35d7e4a581c8364e8c675f6b95d183d0 +https://conda.anaconda.org/conda-forge/win-64/libbrotlienc-1.2.0-he2a975b_4.conda#562b5e39b7797423e7fd04bae1c6ad70 +https://conda.anaconda.org/conda-forge/win-64/libbrotlidec-1.2.0-h84f9c24_4.conda#1b4637ca71b21c0d31ab28c3c0b34c8e +https://conda.anaconda.org/conda-forge/win-64/libjxl-0.12.0-h932607e_2.conda#cf415a2296a1d862e93559645c91fd30 +https://conda.anaconda.org/conda-forge/win-64/lzo-2.10-h6a83c73_1002.conda#c5cb4159f0eea65663b31dd1e49bbb71 +https://conda.anaconda.org/conda-forge/win-64/libarchive-3.8.9-gpl_h8fdc8af_101.conda#74c32a74e1b188a4e14fc359c721e4b9 +https://conda.anaconda.org/conda-forge/win-64/snappy-1.2.2-h7fa0ca8_1.conda#3075846de68f942150069d4289aaad63 +https://conda.anaconda.org/conda-forge/win-64/blosc-1.21.6-hfd34d9b_1.conda#357d7be4146d5fec543bfaa96a8a40de +https://conda.anaconda.org/conda-forge/win-64/libgdal-core-3.13.3-h6864167_2.conda#39b2b64a9e7b6e5320659d5167c843a6 +https://conda.anaconda.org/conda-forge/win-64/pyogrio-0.13.0-py312h0e78342_0.conda#d915f85f4ddce1a8827055b6dcd8d217 +https://conda.anaconda.org/conda-forge/win-64/qhull-2020.2-hc790b64_5.conda#854fbdff64b572b5c0b470f334d34c11 +https://conda.anaconda.org/conda-forge/noarch/pyparsing-3.3.2-pyhcf101f3_0.conda#3687cc0b82a8b4c17e1f0eb7e47163d5 +https://conda.anaconda.org/conda-forge/win-64/zlib-ng-2.3.3-h0261ad2_1.conda#46a21c0a4e65f1a135251fc7c8663f83 +https://conda.anaconda.org/conda-forge/win-64/openjpeg-2.5.4-h0e57b4f_0.conda#e723ab7cc2794c954e1b22fde51c16e4 +https://conda.anaconda.org/conda-forge/win-64/libfreetype6-2.14.3-hdbac1cb_0.conda#f9975a0177ee6cdda10c86d1db1186b0 +https://conda.anaconda.org/conda-forge/win-64/libfreetype-2.14.3-h57928b3_0.conda#d9f70dd06674e26b6d5a657ddd22b568 +https://conda.anaconda.org/conda-forge/win-64/lcms2-2.19.1-hf2c6c5f_3.conda#a817b8a8f09864652c8700bd1893fe81 +https://conda.anaconda.org/conda-forge/win-64/pillow-12.3.0-py312h7bb5964_4.conda#7088244066ff5fbc9b786805a4d918cf +https://conda.anaconda.org/conda-forge/win-64/libintl-0.22.5-h5728263_3.conda#2cf0cf76cc15d360dfa2f17fd6cf9772 +https://conda.anaconda.org/conda-forge/win-64/libglib-2.90.0-ha564072_0.conda#29c689012c8a53a0e36a70dfd43227e4 +https://conda.anaconda.org/conda-forge/win-64/graphite2-1.3.15-h5112557_1.conda#79538e7a7bc024084eda5989f73fde35 +https://conda.anaconda.org/conda-forge/win-64/pixman-0.46.4-h5112557_1.conda#08c8fa3b419df480d985e304f7884d35 +https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_3.conda#49023d73832ef61042f6a237cb2687e7 +https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2#4d59c254e01d9cde7957100457e2d5fb +https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed37_0.tar.bz2#34893075a5c9e55cdafac56607368fc6 +https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2#0c96522c6bdaed4b1566d11387caaf45 +https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-hc364b38_1.conda#a7970cd949a077b7cb9696379d338681 +https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab +https://conda.anaconda.org/conda-forge/win-64/fontconfig-2.17.1-hd47e2ca_0.conda#a0b1b87e871011ca3b783bbf410bc39f +https://conda.anaconda.org/conda-forge/win-64/cairo-1.18.4-h477c42c_1.conda#52ea1beba35b69852d210242dd20f97d +https://conda.anaconda.org/conda-forge/win-64/libharfbuzz-14.4.0-h03b5201_1.conda#d7b92075b7461e40fd8e90f08669a742 +https://conda.anaconda.org/conda-forge/win-64/fribidi-1.0.16-hfd05255_0.conda#c27bd87e70f970010c1c6db104b88b18 +https://conda.anaconda.org/conda-forge/win-64/libraqm-0.11.0-h50d6d30_0.conda#df92be5685f712989830bdb7ee39383a +https://conda.anaconda.org/conda-forge/win-64/kiwisolver-1.5.1-py312h78d62e6_3.conda#b400442c59cd935bf39778129e13f549 +https://conda.anaconda.org/conda-forge/win-64/freetype-2.14.3-h57928b3_0.conda#507b36518b5a595edda64066c820a6ef +https://conda.anaconda.org/conda-forge/win-64/unicodedata2-18.0.0-py312he06e257_0.conda#99623d0df152a82f2ef345e8add291e1 +https://conda.anaconda.org/conda-forge/noarch/munkres-1.1.4-pyhd8ed1ab_1.conda#37293a85a0f4f77bbd9cf7aaefc62609 +https://conda.anaconda.org/conda-forge/win-64/brotli-bin-1.2.0-hd477307_4.conda#5353bd43aaade891fc9b614c5d537ce3 +https://conda.anaconda.org/conda-forge/win-64/brotli-1.2.0-hc8c2fe1_4.conda#bdbb2939efe8af10e7c5885367395710 +https://conda.anaconda.org/conda-forge/win-64/fonttools-4.65.0-py312h05f76fc_0.conda#2f922eaa948c1ae762c088582ad34b18 +https://conda.anaconda.org/conda-forge/noarch/cycler-0.12.1-pyhcf101f3_2.conda#4c2a8fef270f6c69591889b93f9f55c1 +https://conda.anaconda.org/conda-forge/win-64/contourpy-1.4.0-py312h78d62e6_1.conda#fa5b76e5fabf5cf5ac3660bd33c659d0 +https://conda.anaconda.org/conda-forge/win-64/matplotlib-base-3.11.2-py312hf9659c1_1.conda#f199e184d70b2de6bc1ec1129da2690a +https://conda.anaconda.org/conda-forge/win-64/scipy-1.18.1-py312h8847828_1.conda#b617dec223af024a143fe34569ec7b1d +https://conda.anaconda.org/conda-forge/noarch/threadpoolctl-3.6.0-pyhecae5ae_0.conda#9d64911b31d57ca443e9f1e36b04385f +https://conda.anaconda.org/conda-forge/noarch/narwhals-2.26.0-pyh5ded981_0.conda#afb7146667be7c8121f5f8337be60680 +https://conda.anaconda.org/conda-forge/noarch/setuptools-82.0.1-pyh332efcf_0.conda#8e194e7b992f99a5015edbd4ebd38efd +https://conda.anaconda.org/conda-forge/noarch/joblib-1.5.3-pyhd8ed1ab_0.conda#615de2a4d97af50c350e5cf160149e77 +https://conda.anaconda.org/conda-forge/win-64/scikit-learn-1.9.1-np2py312hea30aaf_1.conda#eb91fb49540ffb040c0cb445bb22988f +https://conda.anaconda.org/conda-forge/noarch/networkx-3.6.1-pyhcf101f3_0.conda#a2c1eeadae7a309daed9d62c96012a2b +https://conda.anaconda.org/conda-forge/noarch/mapclassify-2.10.0-pyhd8ed1ab_1.conda#cc293b4cad9909bf66ca117ea90d4631 +https://conda.anaconda.org/conda-forge/win-64/shapely-2.1.2-py312h37f46ab_3.conda#a74fffec4ad2a2893de16bf2acdcddf5 +https://conda.anaconda.org/conda-forge/noarch/geopandas-base-1.1.3-pyha770c72_0.conda#18789a85c307970ae1786dfc6dfd234f +https://conda.anaconda.org/conda-forge/noarch/branca-0.8.2-pyhd8ed1ab_0.conda#1fcdf88e7a8c296d3df8409bf0690db4 +https://conda.anaconda.org/conda-forge/noarch/folium-0.20.0-pyhd8ed1ab_0.conda#a6997a7dcd6673c0692c61dfeaea14ab +https://conda.anaconda.org/conda-forge/noarch/geopandas-1.1.3-pyhd8ed1ab_0.conda#4eb8b870142ca06d2a1d2c74662eac7d +https://conda.anaconda.org/conda-forge/noarch/earth-osm-3.0.2-pyhd8ed1ab_0.conda#eea693a0e40b4aa17590a63d055a068a diff --git a/workflow/envs/module.yaml b/workflow/envs/module.yaml new file mode 100644 index 0000000..bf9150e --- /dev/null +++ b/workflow/envs/module.yaml @@ -0,0 +1,20 @@ +name: module +channels: +- conda-forge +- bioconda +- nodefaults +dependencies: +- python >=3.12,<3.13 +- pyproj >=3.7,<4 +- pyyaml >=6,<7 +- earth-osm >=3.0.2 +- geopandas >=1.0,<2 +- networkx >=3,<4 +- numpy >=2,<3 +- osmium-tool >=1.19,<2 +- pandas >=2,<3 +- pyogrio >=0.10,<1 +- pyosmium >=4,<5 +- pydeck >=0.9,<1 +- requests >=2,<3 +- shapely >=2,<3 diff --git a/workflow/envs/network.yaml b/workflow/envs/network.yaml deleted file mode 100644 index 451eca8..0000000 --- a/workflow/envs/network.yaml +++ /dev/null @@ -1,15 +0,0 @@ -name: network -channels: - - conda-forge - - nodefaults -dependencies: - - python >=3.12,<3.13 - - geopandas >=1.0,<2 - - networkx >=3,<4 - - numpy >=2,<3 - - pandas >=2,<3 - - pyogrio >=0.10,<1 - - pyproj >=3.7,<4 - - pydeck >=0.9,<1 - - pyyaml >=6,<7 - - shapely >=2,<3 diff --git a/workflow/envs/retrieve.yaml b/workflow/envs/retrieve.yaml deleted file mode 100644 index c201444..0000000 --- a/workflow/envs/retrieve.yaml +++ /dev/null @@ -1,11 +0,0 @@ -name: retrieve -channels: - - conda-forge - - nodefaults -dependencies: - - python >=3.12,<3.13 - - earth-osm =3.0.2 - - osmium-tool >=1.19,<2 - - pyosmium >=4,<5 - - pyproj >=3.7,<4 - - requests >=2,<3 diff --git a/workflow/rules/network.smk b/workflow/rules/network.smk index 34a1dac..aee32ff 100644 --- a/workflow/rules/network.smk +++ b/workflow/rules/network.smk @@ -40,7 +40,7 @@ rule clean: log: "/clean.log", conda: - "../envs/network.yaml" + "../envs/module.yaml" threads: 1 params: network=config["network"], @@ -69,7 +69,7 @@ rule build_network: log: "/build_network.log", conda: - "../envs/network.yaml" + "../envs/module.yaml" threads: 1 params: station_merge_radius_m=config["network"]["station_merge_radius_m"], diff --git a/workflow/rules/plot.smk b/workflow/rules/plot.smk index 163fbd5..55ef57b 100644 --- a/workflow/rules/plot.smk +++ b/workflow/rules/plot.smk @@ -15,7 +15,7 @@ rule build_interactive_map: log: "/build_interactive_map.log", conda: - "../envs/network.yaml" + "../envs/module.yaml" threads: 1 params: crs=config["crs"], diff --git a/workflow/rules/retrieve.smk b/workflow/rules/retrieve.smk index 873367d..94b5624 100644 --- a/workflow/rules/retrieve.smk +++ b/workflow/rules/retrieve.smk @@ -34,7 +34,7 @@ if config["retrieve"]["source"] == "geofabrik": log: "/retrieve_osm_pbf/{country}.log", conda: - "../envs/retrieve.yaml" + "../envs/module.yaml" threads: 1 params: include_relations=config["network"]["include_relations"], @@ -53,7 +53,7 @@ elif config["retrieve"]["source"] == "overpass": log: "/retrieve_osm_overpass/{country}.log", conda: - "../envs/retrieve.yaml" + "../envs/module.yaml" threads: 1 params: include_relations=config["network"]["include_relations"], From 6484c46eecb82f467567e5b9494e73c37204a905 Mon Sep 17 00:00:00 2001 From: bobbyxng Date: Tue, 6 Oct 2026 17:12:47 +0200 Subject: [PATCH 3/5] refactor: align workflow paths with ModelBlocks conventions --- INTERFACE.yaml | 49 +++++-- config/config.yaml | 2 +- tests/integration/Snakefile | 1 + tests/integration_test.py | 6 +- tests/test_config.py | 3 + tests/test_module_import.py | 19 ++- workflow/Snakefile | 20 ++- workflow/internal/config.schema.yaml | 188 +++++++++++++++++++++++++++ workflow/rules/network.smk | 35 ++--- workflow/rules/plot.smk | 2 +- workflow/rules/retrieve.smk | 8 +- workflow/scripts/_schema.py | 16 ++- workflow/scripts/clean.py | 58 ++++++--- 13 files changed, 337 insertions(+), 70 deletions(-) create mode 100644 workflow/internal/config.schema.yaml diff --git a/INTERFACE.yaml b/INTERFACE.yaml index ca1e6b9..8ffeeba 100644 --- a/INTERFACE.yaml +++ b/INTERFACE.yaml @@ -1,21 +1,42 @@ -# Module Input-Output structure for automated docs generation convention_version: v1.0.0 - pathvars: snakemake_defaults: logs: - default: "" - description: location of snakemake log files. + default: + description: Location of module logs. resources: - default: "" - description: >- - Raw OSM files are written to retrieve/{country}_{feature}.json. - Clean features are written below clean; generic network outputs - below build (buses/lines/transformers as CSV under build/csv - and GeoJSON, including substation polygons, under build/geojson). - An interactive PyDeck map of the network, and the default target - of this workflow, is written to map.html. + default: + description: Location of downloaded PBFs, retrieved OSM features, and cleaning intermediates. results: - default: "" - description: location of module result files. + default: + description: Location of module results. user_resources: {} + results: + buses: + default: /network/csv/buses.csv + description: Network buses. + lines: + default: /network/csv/lines.csv + description: Network lines. + transformers: + default: /network/csv/transformers.csv + description: Network transformers. + buses_geojson: + default: /network/geojson/buses.geojson + description: Network buses geojson. + lines_geojson: + default: /network/geojson/lines.geojson + description: Network lines geojson. + transformers_geojson: + default: /network/geojson/transformers.geojson + description: Network transformers geojson. + stations_polygon: + default: /network/geojson/stations_polygon.geojson + description: Network stations polygon. + buses_polygon: + default: /network/geojson/buses_polygon.geojson + description: Network buses polygon. + map: + default: /map.html + description: Network map. +wildcards: {} diff --git a/config/config.yaml b/config/config.yaml index d508946..18c40db 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -1,6 +1,6 @@ %YAML 1.1 --- -# yaml-language-server: $schema=./config.schema.json +# yaml-language-server: $schema=../workflow/internal/config.schema.yaml countries: - BE diff --git a/tests/integration/Snakefile b/tests/integration/Snakefile index e1f7e10..6a8c383 100644 --- a/tests/integration/Snakefile +++ b/tests/integration/Snakefile @@ -6,6 +6,7 @@ module grid_builder: pathvars: resources="resources/grid-builder", logs="logs/grid-builder", + results="results/grid-builder", snakefile: "../../workflow/Snakefile" config: diff --git a/tests/integration_test.py b/tests/integration_test.py index 3c9af85..76ea39c 100644 --- a/tests/integration_test.py +++ b/tests/integration_test.py @@ -37,7 +37,7 @@ def test_interface_file(module_path): "LICENSE", "README.md", "config/config.yaml", - "config/config.schema.json", + "workflow/internal/config.schema.yaml", "config/examples/config.BE-NL.yaml", "tests/integration/Snakefile", ], @@ -94,7 +94,7 @@ def test_snakemake_integration_testing(module_path): shutil.copytree(workdir / "logs", log_dir, dirs_exist_ok=True) assert result.returncode == 0, run_log.read_text(encoding="utf-8") - output_dir = workdir / "resources/grid-builder/retrieve" + output_dir = workdir / "resources/grid-builder/automatic/retrieve" for feature in ("substations_way", "lines_way"): payload = json.loads( (output_dir / f"benin_{feature}.json").read_text(encoding="utf-8") @@ -103,7 +103,7 @@ def test_snakemake_integration_testing(module_path): assert elements, f"No {feature} records retrieved" assert all(item["geometry"] for item in elements) - build_dir = workdir / "resources/grid-builder/build" + build_dir = workdir / "results/grid-builder/network" for component in ("buses", "lines", "transformers"): with (build_dir / "csv" / f"{component}.csv").open( encoding="utf-8" diff --git a/tests/test_config.py b/tests/test_config.py index 05be5e2..1aa429b 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -46,6 +46,9 @@ def test_generated_config_matches_repository(tmp_path): defaults = generate_config_defaults(str(tmp_path / "config.yaml")) schema = generate_config_schema(str(tmp_path / "config.schema.json")) assert defaults == yaml.safe_load((config_dir / "config.yaml").read_text()) + assert schema == yaml.safe_load( + (config_dir.parent / "workflow/internal/config.schema.yaml").read_text() + ) assert schema == json.loads((config_dir / "config.schema.json").read_text()) validated = validate_config(defaults) assert validated.retrieve.overpass_api.max_tries == 5 diff --git a/tests/test_module_import.py b/tests/test_module_import.py index f2c9e36..c6b5c79 100644 --- a/tests/test_module_import.py +++ b/tests/test_module_import.py @@ -90,12 +90,15 @@ def test_module_execution(tmp_path, remote_module, remote, backend): resources="custom/resources", results="custom/results", logs="custom/logs", + osm_lines_way="rewired/{{country}}/lines.json", + map="rewired/map.html", + buses="rewired/buses.csv", use rule * from grid as grid_* assert scripts._schema.parent_marker assert scripts._helpers.parent_marker == "parent helper" -assert "custom/resources/retrieve/US_lines_way.json" in rules.grid_retrieve_osm_all.input +assert "rewired/US/lines.json" in rules.grid_retrieve_osm_all.input if {backend!r} == "geofabrik": assert rules.grid_retrieve_osm_pbf.params.data_dir == "custom/resources/automatic/earth-osm" assert grid.config["regions"]["US"]["frequency_hz"]["AC"] == 60 @@ -109,7 +112,7 @@ def test_module_execution(tmp_path, remote_module, remote, backend): input: rules.grid_build_interactive_map.output, rules.parent_probe.output """ ) - raw_dir = workdir / "custom/resources/retrieve" + raw_dir = workdir / "custom/resources/automatic/retrieve" raw_dir.mkdir(parents=True) for feature in ( "lines_way", @@ -161,7 +164,11 @@ def test_module_execution(tmp_path, remote_module, remote, backend): } for i, lon in enumerate((-100.0, -99.9)) ] - raw_path = raw_dir / f"US_{feature}.json" + raw_path = ( + workdir / "rewired/US/lines.json" + if feature == "lines_way" + else raw_dir / f"US_{feature}.json" + ) payloads[feature] = {"elements": elements} if backend == "geofabrik": raw_path.parent.mkdir(parents=True, exist_ok=True) @@ -178,12 +185,12 @@ def test_module_execution(tmp_path, remote_module, remote, backend): ) assert result.returncode == 0, result.stdout + result.stderr assert (workdir / "parent.txt").read_text() == "parent helper" - assert (workdir / "custom/resources/map.html").stat().st_size > 0 + assert (workdir / "rewired/map.html").stat().st_size > 0 lines = json.loads( - (workdir / "custom/resources/build/geojson/lines.geojson").read_text() + (workdir / "custom/results/network/geojson/lines.geojson").read_text() ) assert len(lines["features"]) == 1 - assert (workdir / "custom/resources/build/csv/buses.csv").stat().st_size > 0 + assert (workdir / "rewired/buses.csv").stat().st_size > 0 if backend == "overpass": assert ( "Querying Overpass" diff --git a/workflow/Snakefile b/workflow/Snakefile index c9107d4..b5cb242 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -32,10 +32,24 @@ _region_paths = { } -# Define pathvars to expose OSM retrieval outputs for downstream use. +# Retrieval paths and public outputs can be rewired by a consuming workflow. pathvars: - # OSM retrieval outputs by country and feature - osm_retrieve="/retrieve", + osm_retrieve="/automatic/retrieve", + osm_lines_way="/{country}_lines_way.json", + osm_cables_way="/{country}_cables_way.json", + osm_substations_way="/{country}_substations_way.json", + osm_substations_node="/{country}_substations_node.json", + osm_substations_relation="/{country}_substations_relation.json", + osm_routes_relation="/{country}_routes_relation.json", + buses="/network/csv/buses.csv", + lines="/network/csv/lines.csv", + transformers="/network/csv/transformers.csv", + buses_geojson="/network/geojson/buses.geojson", + lines_geojson="/network/geojson/lines.geojson", + transformers_geojson="/network/geojson/transformers.geojson", + stations_polygon="/network/geojson/stations_polygon.geojson", + buses_polygon="/network/geojson/buses_polygon.geojson", + map="/map.html", # Default configuration file generated from pydantic schema. diff --git a/workflow/internal/config.schema.yaml b/workflow/internal/config.schema.yaml new file mode 100644 index 0000000..ac257fc --- /dev/null +++ b/workflow/internal/config.schema.yaml @@ -0,0 +1,188 @@ +additionalProperties: false +description: Top-level grid-builder config. +properties: + countries: + default: + - BE + description: List of countries to retrieve OSM data for + items: + type: string + minItems: 1 + type: array + retrieve: + additionalProperties: false + description: Settings for retrieve_osm_pbf.py/retrieve_osm_overpass.py. + properties: + source: + default: geofabrik + description: Retrieval backend for OSM data + enum: + - geofabrik + - overpass + type: string + force_redownload: + default: false + description: Force refresh of cached data + type: boolean + target_date: + anyOf: + - format: date-time + type: string + - type: 'null' + default: + description: 'Optional historical date for data retrieval in ISO 8601 datetime format. Only honoured for retrieve.source: geofabrik.' + overpass_api: + additionalProperties: false + description: Settings for retrieve_osm_overpass.py's own Overpass API client. + properties: + url: + default: https://overpass-api.de/api/interpreter + description: Overpass API endpoint to query + type: string + max_tries: + default: 5 + description: Maximum number of attempts per query before giving up + minimum: 1 + type: integer + timeout: + default: 600 + description: Per-request timeout in seconds + exclusiveMinimum: 0 + type: integer + user_agent: + additionalProperties: false + description: Identifies this tool to the Overpass API, per its fair-use policy. + properties: + project_name: + default: grid-builder + description: Project name sent in the User-Agent header + type: string + email: + default: '' + description: Contact email sent in the User-Agent header + type: string + website: + default: https://github.com/pypsa/grid-builder + description: Project URL sent in the User-Agent header + type: string + network: + additionalProperties: false + description: Global assumptions for cleaning and connecting OSM grid features. + properties: + include_relations: + default: true + description: Whether the network should consider OSM route=power/power=circuit relations, grouping their member ways into a single line matching the relation's real-world circuit; retrieval respects this too, so relations aren't fetched at all when it's off + type: boolean + minimum_voltage_kv: + default: 220.0 + description: Minimum nominal AC voltage retained from OSM, in kV + exclusiveMinimum: 0 + type: number + frequency_hz: + additionalProperties: false + description: AC/DC frequency in Hz, used to classify and normalise OSM ``frequency`` tags. + properties: + AC: + default: 50.0 + description: AC frequency in Hz (50 for most of the world; 60 for the Americas and parts of Asia) + exclusiveMinimum: 0 + type: number + DC: + default: 0.0 + description: Frequency tag value OSM uses to mark a DC line/converter + minimum: 0 + type: number + station_merge_radius_m: + default: 500.0 + description: Buffer radius used to merge nearby substations and line endpoints, in metres + exclusiveMinimum: 0 + type: number + remove_under_construction: + default: true + description: Whether assets tagged as under construction are dropped + type: boolean + remove_after: + anyOf: + - format: date + type: string + - type: 'null' + default: '2026-12-31' + description: Exclude assets with a later planned start date; null disables this filter + regions: + additionalProperties: + additionalProperties: false + description: Optional country-specific overrides loaded from config/regions. + properties: + minimum_voltage_kv: + anyOf: + - exclusiveMinimum: 0 + type: number + - type: 'null' + default: + frequency_hz: + anyOf: + - additionalProperties: false + description: Optional per-country AC/DC override; unset fields fall back to network.frequency_hz. + properties: + AC: + anyOf: + - exclusiveMinimum: 0 + type: number + - type: 'null' + default: + DC: + anyOf: + - minimum: 0 + type: number + - type: 'null' + default: + - type: 'null' + default: + description: Country-specific network overrides loaded from config/regions + type: object + interactive_map: + additionalProperties: false + description: Geometry simplification and coordinate rounding for build_interactive_map.py. + properties: + coordinate_decimals: + default: 5 + description: Decimal places kept for coordinates embedded in the interactive map; 5 is about 1.1m of precision at the equator, comfortably below every simplification tolerance below + minimum: 0 + type: integer + simplify_geometries: + additionalProperties: false + description: Douglas-Peucker simplification tolerances for the interactive map. + properties: + enable: + default: true + description: Whether to simplify station/bus-polygon/line geometries before embedding them in the interactive map + type: boolean + stations_m: + default: 100.0 + description: Simplification tolerance for station polygon outlines, in metres + minimum: 0 + type: number + buses_polygon_m: + default: 5.0 + description: Simplification tolerance for individual bus/substation footprint polygons, in metres + minimum: 0 + type: number + lines_m: + default: 30.0 + description: Simplification tolerance for line geometries, in metres + minimum: 0 + type: number + crs: + additionalProperties: false + description: Coordinate reference systems used throughout clean/build_network. + properties: + geo: + default: EPSG:4326 + description: Geographic CRS used to store and exchange coordinates + type: string + distance: + default: EPSG:3035 + description: Equal-area/equal-distance CRS used for buffering and length calculations; must suit the geographic extent in use (the default, ETRS89-LAEA, covers Europe) + type: string +title: ConfigSchema +type: object diff --git a/workflow/rules/network.smk b/workflow/rules/network.smk index aee32ff..66faa3c 100644 --- a/workflow/rules/network.smk +++ b/workflow/rules/network.smk @@ -6,43 +6,44 @@ rule clean: input: lines_way=expand( - "/retrieve/{country}_lines_way.json", + workflow.pathvars.apply(""), country=config["countries"], ), cables_way=expand( - "/retrieve/{country}_cables_way.json", + workflow.pathvars.apply(""), country=config["countries"], ), substations_way=expand( - "/retrieve/{country}_substations_way.json", + workflow.pathvars.apply(""), country=config["countries"], ), substations_node=expand( - "/retrieve/{country}_substations_node.json", + workflow.pathvars.apply(""), country=config["countries"], ), substations_relation=expand( - "/retrieve/{country}_substations_relation.json", + workflow.pathvars.apply(""), country=config["countries"], ), routes_relation=( expand( - "/retrieve/{country}_routes_relation.json", + workflow.pathvars.apply(""), country=config["countries"], ) if config["network"]["include_relations"] else [] ), output: - substations="/clean/substations.geojson", - substations_polygon="/clean/substations_polygon.geojson", - lines="/clean/lines.geojson", + substations="/automatic/clean/substations.geojson", + substations_polygon="/automatic/clean/substations_polygon.geojson", + lines="/automatic/clean/lines.geojson", log: "/clean.log", conda: "../envs/module.yaml" threads: 1 params: + countries=[_schema.get_region_tuple(c).short for c in config["countries"]], network=config["network"], regions=config["regions"], crs=config["crs"], @@ -58,14 +59,14 @@ rule build_network: substations_polygon=rules.clean.output.substations_polygon, lines=rules.clean.output.lines, output: - buses="/build/csv/buses.csv", - lines="/build/csv/lines.csv", - transformers="/build/csv/transformers.csv", - buses_geojson="/build/geojson/buses.geojson", - lines_geojson="/build/geojson/lines.geojson", - transformers_geojson="/build/geojson/transformers.geojson", - stations_polygon="/build/geojson/stations_polygon.geojson", - buses_polygon="/build/geojson/buses_polygon.geojson", + buses="", + lines="", + transformers="", + buses_geojson="", + lines_geojson="", + transformers_geojson="", + stations_polygon="", + buses_polygon="", log: "/build_network.log", conda: diff --git a/workflow/rules/plot.smk b/workflow/rules/plot.smk index 55ef57b..9eef7fe 100644 --- a/workflow/rules/plot.smk +++ b/workflow/rules/plot.smk @@ -11,7 +11,7 @@ rule build_interactive_map: stations_polygon=rules.build_network.output.stations_polygon, buses_polygon=rules.build_network.output.buses_polygon, output: - map="/map.html", + map="", log: "/build_interactive_map.log", conda: diff --git a/workflow/rules/retrieve.smk b/workflow/rules/retrieve.smk index 94b5624..ca18f24 100644 --- a/workflow/rules/retrieve.smk +++ b/workflow/rules/retrieve.smk @@ -20,10 +20,7 @@ _OSM_FEATURES = [ "substations_relation", "routes_relation", ] -_OSM_OUTPUTS = { - feature: f"/retrieve/{{country}}_{feature}.json" - for feature in _OSM_FEATURES -} +_OSM_OUTPUTS = {feature: f"" for feature in _OSM_FEATURES} if config["retrieve"]["source"] == "geofabrik": @@ -67,7 +64,6 @@ elif config["retrieve"]["source"] == "overpass": rule retrieve_osm_all: input: expand( - "/retrieve/{country}_{feature}.json", + [workflow.pathvars.apply(path) for path in _OSM_OUTPUTS.values()], country=config["countries"], - feature=_OSM_FEATURES, ), diff --git a/workflow/scripts/_schema.py b/workflow/scripts/_schema.py index dc66f92..f5e22c9 100644 --- a/workflow/scripts/_schema.py +++ b/workflow/scripts/_schema.py @@ -364,7 +364,9 @@ def str_representer(dumper, data): yaml_writer.representer.add_representer(str, str_representer) data = CommentedMap() - data.yaml_set_start_comment("yaml-language-server: $schema=./config.schema.json") + data.yaml_set_start_comment( + "yaml-language-server: $schema=../workflow/internal/config.schema.yaml" + ) for key, value in defaults.items(): data[key] = value @@ -379,7 +381,7 @@ def str_representer(dumper, data): return defaults -def generate_config_schema(path: str = "config/config.schema.json") -> dict: +def generate_config_schema(path: str = "workflow/internal/config.schema.yaml") -> dict: """Generate JSON schema file and return the schema dict.""" def resolve_refs(obj, defs): @@ -447,8 +449,13 @@ def remove_object_type(obj, is_root=True): schema = remove_object_type(schema) with open(path, "w") as f: - json.dump(schema, f, indent=2) - f.write("\n") + if Path(path).suffix == ".json": + json.dump(schema, f, indent=2) + f.write("\n") + else: + yaml_writer = YAML() + yaml_writer.width = 4096 + yaml_writer.dump(schema, f) return schema @@ -486,4 +493,5 @@ def generate_region_index( if __name__ == "__main__": generate_config_defaults() generate_config_schema() + generate_config_schema("config/config.schema.json") generate_region_index() diff --git a/workflow/scripts/clean.py b/workflow/scripts/clean.py index 6ac11f7..819bb95 100644 --- a/workflow/scripts/clean.py +++ b/workflow/scripts/clean.py @@ -748,7 +748,9 @@ def _create_line(row: pd.Series) -> tuple[Any, list[str]]: ] -def _load_elements(paths: list[str], element_type: str) -> pd.DataFrame: +def _load_elements( + paths: list[str], element_type: str, countries: dict[str, str] | None = None +) -> pd.DataFrame: """Load our own raw Overpass-JSON-shaped retrieval output for one element type. Both retrieve_osm_pbf.py (pyosmium reading a local PBF) and @@ -763,7 +765,11 @@ def _load_elements(paths: list[str], element_type: str) -> pd.DataFrame: continue with open(path) as handle: payload = json.load(handle) - country = Path(path).stem.split("_", maxsplit=1)[0] + country = ( + countries[path] + if countries is not None + else Path(path).stem.split("_", maxsplit=1)[0] + ) elements = [ e for e in payload.get("elements", []) if e.get("type") == element_type ] @@ -796,11 +802,16 @@ def _flatten_tags(frame: pd.DataFrame, columns: list[str]) -> pd.DataFrame: def _import_lines_and_cables( - line_paths: list[str], cable_paths: list[str] + line_paths: list[str], + cable_paths: list[str], + countries: dict[str, str] | None = None, ) -> pd.DataFrame: """Read lines_way/cables_way JSON into the shape ``_clean_lines`` expects.""" frame = pd.concat( - [_load_elements(line_paths, "way"), _load_elements(cable_paths, "way")], + [ + _load_elements(line_paths, "way", countries), + _load_elements(cable_paths, "way", countries), + ], ignore_index=True, ) frame = _flatten_tags(frame, _LINE_TAG_COLUMNS) @@ -812,14 +823,16 @@ def _import_lines_and_cables( return frame.reset_index(drop=True) -def _import_substation_relations(paths: list[str]) -> pd.DataFrame: +def _import_substation_relations( + paths: list[str], countries: dict[str, str] | None = None +) -> pd.DataFrame: """Build one polygon per substation relation from its non-node members. Excludes members with an empty/"incoming_line"/"substation"/"inner" role (the actual boundary is only the "outer" ring), linemerges the rest, and takes the convex hull as the substation's polygon. """ - df_relations = _load_elements(paths, "relation") + df_relations = _load_elements(paths, "relation", countries) if df_relations.empty: return df_relations @@ -861,19 +874,24 @@ def _import_substation_relations(paths: list[str]) -> pd.DataFrame: return df_relations.dropna(subset=["geometry"]).reset_index(drop=True) -def _import_routes_relation(paths: list[str]) -> pd.DataFrame: +def _import_routes_relation( + paths: list[str], countries: dict[str, str] | None = None +) -> pd.DataFrame: """Read routes_relation JSON into the shape ``_create_line`` expects. Unlike substation relations, a route relation's ``members`` column is kept as-is (not converted to a polygon) — ``_create_line`` merges the member ways' own geometry into the relation's line. """ - frame = _load_elements(paths, "relation") + frame = _load_elements(paths, "relation", countries) return _flatten_tags(frame, _RELATION_TAG_COLUMNS) def _import_substations( - way_paths: list[str], node_paths: list[str], relation_paths: list[str] + way_paths: list[str], + node_paths: list[str], + relation_paths: list[str], + countries: dict[str, str] | None = None, ) -> pd.DataFrame: """Read substations_way/node/relation JSON into ``_clean_substations`` shape. @@ -889,7 +907,7 @@ def _import_substations( ``substations_node`` query, added for parity between the two retrieve.source options. """ - df_way = _load_elements(way_paths, "way") + df_way = _load_elements(way_paths, "way", countries) df_way = df_way[ df_way["geometry"].apply(lambda g: isinstance(g, list) and len(g) >= 1) ] @@ -899,7 +917,7 @@ def _import_substations( lambda coords: _create_polygon({"geometry": coords}) ) - df_node = _load_elements(node_paths, "node") + df_node = _load_elements(node_paths, "node", countries) df_node = df_node[ df_node["geometry"].apply(lambda g: isinstance(g, list) and len(g) >= 1) ] @@ -909,7 +927,7 @@ def _import_substations( lambda coords: Point(coords[0]["lon"], coords[0]["lat"]) ) - df_relation = _import_substation_relations(relation_paths) + df_relation = _import_substation_relations(relation_paths, countries) df_relation = _flatten_tags(df_relation, _SUBSTATION_TAG_COLUMNS) df_way["is_node"] = False @@ -957,6 +975,7 @@ def clean( network: dict[str, Any], regions: dict[str, Any], geo_crs: str, + countries: dict[str, str] | None = None, ) -> tuple[gpd.GeoDataFrame, gpd.GeoDataFrame, gpd.GeoDataFrame]: """Return clean substation buses, substation polygons, and AC lines/cables. @@ -964,7 +983,9 @@ def clean( ``substations_way``, ``substations_node``, ``substations_relation``, ``routes_relation``) to the list of per-country JSON files retrieved for it. A feature with no paths (e.g. ``routes_relation`` when - ``include_relations`` is off) is simply skipped. + ``include_relations`` is off) is simply skipped. ``countries`` maps input + paths to ISO codes so rewiring filenames preserves regional settings. + Without it, standalone callers retain the legacy filename convention. """ crs = geo_crs min_voltage_ac = network["minimum_voltage_kv"] * 1000 # V @@ -976,6 +997,7 @@ def clean( inputs.get("substations_way", []), inputs.get("substations_node", []), inputs.get("substations_relation", []), + countries, ) if df_substations.empty: empty_buses = gpd.GeoDataFrame(geometry=gpd.GeoSeries([], crs=crs), crs=crs) @@ -1046,7 +1068,7 @@ def clean( route_paths = inputs.get("routes_relation", []) if route_paths: logger.info("Importing power route relations.") - df_relation = _import_routes_relation(route_paths) + df_relation = _import_routes_relation(route_paths, countries) if not df_relation.empty: df_relation = _drop_duplicate_lines(df_relation) df_relation["under_construction"] = ( @@ -1125,7 +1147,7 @@ def clean( # --- AC lines/cables via individual ways --------------------------- logger.info("Importing lines and cables.") df_lines = _import_lines_and_cables( - inputs.get("lines_way", []), inputs.get("cables_way", []) + inputs.get("lines_way", []), inputs.get("cables_way", []), countries ) if not df_lines.empty: df_lines = _drop_duplicate_lines(df_lines) @@ -1208,6 +1230,12 @@ def clean( snakemake.params.network, snakemake.params.regions, snakemake.params.crs["geo"], + { + path: country + for paths in inputs.values() + if paths + for path, country in zip(paths, snakemake.params.countries, strict=True) + }, ) logger.info( "Retained %d buses, %d substation polygons, and %d AC lines/cables.", From f9c9702783efd74cc979933b4ceef6a27d8c3229 Mon Sep 17 00:00:00 2001 From: bobbyxng Date: Tue, 6 Oct 2026 17:19:38 +0200 Subject: [PATCH 4/5] docs: describe module usage and add workflow rule graph --- README.md | 237 ++++++++++++++++++++++++++++++++---------- config/README.md | 5 +- figures/rulegraph.png | Bin 0 -> 24984 bytes 3 files changed, 185 insertions(+), 57 deletions(-) create mode 100644 figures/rulegraph.png diff --git a/README.md b/README.md index 821a651..c0b667f 100644 --- a/README.md +++ b/README.md @@ -12,93 +12,212 @@ A modular Snakemake workflow for retrieving OpenStreetMap power infrastructure. ## About -`grid-builder` is a modular `snakemake` workflow that retrieves OpenStreetMap power infrastructure and builds a generic high-voltage network. It can be imported into another `snakemake` workflow. +`grid-builder` is a modular Snakemake workflow for retrieving OpenStreetMap power +infrastructure and building a generic high-voltage AC network. It follows the +[Modelblocks conventions](https://www.modelblocks.org/convention/) and can be +imported into another Snakemake workflow or run on its own. -The workflow retains AC substations, overhead lines, and cables at configured voltage levels, then creates generic buses, connected line segments, and voltage-pair transformers. The outputs preserve OSM provenance and geometry but contain no PyPSA-specific line types, capacities, or electrical-component assumptions. - -This module follows the Modelblocks conventions (https://www.modelblocks.org). For more information, consult the [integration example](./tests/integration/Snakefile) and the `snakemake` [modularisation documentation](https://snakemake.readthedocs.io/en/stable/snakefiles/modularization.html). +For more information, consult the [Modelblocks documentation](https://modelblocks.readthedocs.io/en/latest/), +the [integration example](./tests/integration/Snakefile), and the +[Snakemake modularisation documentation](https://snakemake.readthedocs.io/en/stable/snakefiles/modularization.html). ## Overview -Currently implemented: +

+ Rule graph: retrieve_osm_pbf → clean → build_network → build_interactive_map → all +

-1. Retrieve OSM substations, lines, cables, and (optionally) circuit relations by country, either from a cached local Geofabrik PBF extract or the live Overpass API. -2. Clean the raw retrieval output, filtering voltage, frequency, construction status, and future assets, and grouping relation member ways into one line per real-world circuit. -3. Merge nearby stations and line endpoints into generic buses, AC lines, and transformers. -4. Build a self-contained interactive map of the resulting network (`map.html`), with layer toggles, voltage/text filtering, and click-through OSM links — this is the workflow's default target. +The rule graph shows the default Geofabrik backend. Selecting Overpass replaces +`retrieve_osm_pbf` with `retrieve_osm_overpass`; the downstream rules are the same. + +Data processing steps: + +1. Retrieve OSM substations, overhead lines, cables, and optional circuit + relations by country, using Geofabrik PBF extracts or the Overpass API. + Downloaded PBF extracts are cached for reuse. +2. Clean tags and geometries, filter by voltage and frequency, and combine + circuit relation member ways when enabled. +3. Apply the configured construction-status and date filters, merge nearby + stations and line endpoints into buses, and construct AC lines and + transformers between voltage levels at the same station. +4. Generate an interactive HTML map with layer controls, voltage and text + filters, and links to the source OSM objects. This is the default target + when running the workflow on its own. + +### Important assumptions + +- The workflow builds AC topology for now, DC lines and converters will be + added at a later stage. +- Connections are inferred from geometry and OSM tags. Transformers are inferred + between every pair of voltage-level buses at the same real station, rather + than taken from an inventory of individual transformers. +- Outputs include geometry and OSM references for buses and lines. +- The map embeds the network data in an HTML file that opens without a local + server. Its JavaScript libraries and basemap require internet access. ## Configuration -Configuration lives in [`config/config.yaml`](./config/config.yaml), validated against a generated JSON schema. See the configuration [README](./config/README.md) for the available controls, including retrieval backends, regional overrides, and personal/local settings. +Consult the [configuration README](./config/README.md) and +[default configuration](./config/config.yaml) for retrieval options, network +settings, regional overrides, and local configuration. + +Configuration is validated with Pydantic. The same models generate the +[JSON Schema in YAML format](./workflow/internal/config.schema.yaml) describing +the available options. ## Input / output structure -Please consult the [interface file](./INTERFACE.yaml) for more information. - -Raw retrieval outputs use `/retrieve/{country}_{feature}.json`, one -file per country and feature (`lines_way`, `cables_way`, `substations_way`, -`substations_node`, `substations_relation`, `routes_relation`). Both retrieval -backends write the same raw-Overpass-JSON shape, so downstream cleaning doesn't -need to know which one ran. Clean features use `/clean/*.geojson`; -generic network components use `/build/csv/{buses,lines,transformers}.csv` -and matching GeoJSON files under `/build/geojson/`, which also -includes `stations_polygon.geojson` (clustered station shapes) and -`buses_polygon.geojson` (substation polygons scoped to the buses in the output). -An interactive map of the network is written to `/map.html`; it is -a standalone HTML file (no server required) and the workflow's default target. -Country logs use `/retrieve_osm_pbf/{country}.log` or -`/retrieve_osm_overpass/{country}.log`, depending on `retrieve.source`. The -integration example sets these roots to `resources/grid-builder` and -`logs/grid-builder`. Downloaded PBF files (used for `retrieve.source: geofabrik`) -are cached in `data/earth-osm` in this checkout. - -DC assets (links, converters, switching stations) are out of scope: this workflow -builds a generic AC topology only, with no PyPSA-specific line types or capacities. +No user-supplied data files are required; the workflow retrieves OSM data for +the configured countries. Each public output has a pathvar documented in the +[interface file](./INTERFACE.yaml). + +Main outputs: + +| Output | Default location | +| --- | --- | +| Network CSVs | `/network/csv/{buses,lines,transformers}.csv` | +| Network GeoJSONs | `/network/geojson/*.geojson` | +| Interactive map (default target) | `/map.html` | + +Network GeoJSONs include the three network components plus +`stations_polygon.geojson` and `buses_polygon.geojson`. + +Both retrieval backends write raw OSM JSON files to +`/automatic/retrieve/{country}_{feature}.json`, where `country` is an +identifier from the configured `countries`. The six feature names are: `lines_way`, +`cables_way`, `substations_way`, `substations_node`, `substations_relation`, and +`routes_relation`. + +Cleaning intermediates use `/automatic/clean/`. Downloaded PBFs are +cached in `/automatic/earth-osm/` inside the consuming workflow's +working directory. Logs use `/`, with a separate retrieval log per country. + +### Importing into another workflow + +The host environment needs Python 3.12, Snakemake >=9.19, `pydantic >=2`, +`ruamel.yaml >=0.18`, `pyyaml >=6,<7`, and `earth-osm >=3.0.2` to load and validate +the module. Rule dependencies are installed separately by `--use-conda`. + +Create `config/modules/grid_builder.yaml` with the module configuration: + +```yaml +grid_builder: + countries: [BE] + retrieve: + source: geofabrik +``` + +Then import the module in your Snakefile: + +```python +configfile: "config/modules/grid_builder.yaml" + +module grid_builder: + snakefile: + github( + "pypsa/grid-builder", + path="workflow/Snakefile", + tag="", + ) + config: + config["grid_builder"] + pathvars: + logs="logs/grid-builder", + resources="resources/grid-builder", + results="results/grid-builder", + # Optional: rewire an individual result for a downstream module. + buses="results/shared/buses.csv", + +use rule * from grid_builder as grid_builder_* + +rule all_grid_builder: + default_target: True + input: + rules.grid_builder_build_interactive_map.output, +``` + +Run `snakemake --use-conda --cores 2 all_grid_builder`. Replace the tag +placeholder with the published release or commit you want to use. For a local +checkout, replace `github(...)` with the path to its `workflow/Snakefile`. + +Use the output pathvars in [INTERFACE.yaml](./INTERFACE.yaml) to rewire network +files and the map. Retrieval intermediates can also be redirected: set +`osm_retrieve` to change their directory, or set an individual pathvar such as +`osm_lines_way="raw/{country}/lines.json"`. The six retrieval pathvars are +`osm_lines_way`, `osm_cables_way`, `osm_substations_way`, `osm_substations_node`, +`osm_substations_relation`, and `osm_routes_relation`. + +Retrieval-only consumers can request `rules.grid_builder_retrieve_osm_all.input` +regardless of where those files are stored. Referencing imported rule outputs +avoids hard-coding their locations. ## Development -We use [`pixi`](https://pixi.sh/) as our package manager for development. -Once installed, run the following to clone this repository and install all dependencies. +We use [Pixi](https://pixi.sh/) to manage dependencies. Clone the repository and +install the development environment: ```shell -git clone git@github.com:PyPSA/grid-builder.git +git clone https://github.com/pypsa/grid-builder.git cd grid-builder pixi install --locked ``` -For testing, simply run: +This is a multi-environment project; see [pixi.toml](./pixi.toml): + +- `default`: development, validation, and testing tools, including the execution + dependencies needed by unit tests. +- `module`: execution dependencies used by Snakemake rules. + +After changing dependencies, update the lock file and export the execution +environment for module users: ```shell -pixi run --locked lint -pixi run --locked test +pixi lock +pixi run --locked export-snakemake-env module ``` -To test a minimal example of a workflow using this module: +This writes `workflow/envs/module.yaml` and package pins from `pixi.lock` for +Linux (`linux-64`), macOS (`osx-arm64`), and Windows (`win-64`). The export command +also accepts an optional output directory after `module`. + +To regenerate the default configuration, schemas, and regional-file index after +changing the Pydantic models or adding or removing regional config files: ```shell -pixi shell # activate this project's environment -cd tests/integration/ # navigate to the integration example -snakemake --use-conda --cores 2 # run the workflow! +pixi run --locked generate-config ``` -The Pixi environment supplies Snakemake and the configuration-validation -dependencies. Snakemake installs the retrieval script's dependencies from -`workflow/envs/retrieve.yaml` when `--use-conda` is enabled. A consuming workflow -must also provide the host dependencies from `pixi.toml`; importing the module -does not activate its Pixi environment automatically. +## Testing + +Run the checks and test suite: + +```shell +pixi run --locked lint +pixi run --locked test +``` -The integration test uses a fresh temporary output directory, runs the retrieval, -cleaning, and generic network Conda environments, and checks the resulting -components and country logging. It needs internet access on the first run to -install dependencies and download Benin's OSM extract; subsequent runs can reuse -those caches. Test logs are retained in `tests/integration/logs`. +The suite covers configuration validation, network processing, local and HTTP +module imports, output rewiring, and environment consistency. Remote-import +tests use synthetic OSM data and a local mock Overpass server. -Each retrieval job runs with one worker (`threads: 1`), so CPU allocation stays -entirely under Snakemake's control. Snakemake can still run multiple country jobs -in parallel using `--cores`. +The live integration test retrieves Benin's OSM extract and runs the workflow +in its exported Conda environment. It requires internet access for the data +and, on the first run, environment installation. Conda environments are cached +between runs. Test logs are retained in `tests/integration/logs`. -If this checkout is moved and commands fail with a `bad interpreter` error, -rebuild the installed environment with `pixi reinstall --locked`. +To run only the integration checks: + +```shell +pixi run --locked test-integration +``` + +To run the example consuming workflow manually: + +```shell +pixi shell +cd tests/integration/ +snakemake --use-conda --cores 2 +``` ## License @@ -109,3 +228,9 @@ rebuild the installed environment with `pixi reinstall --locked`. * Jonas Hörsch et al. 2018. PyPSA-Eur: An open optimisation model of the European transmission system, *Energy Strategy Reviews*, Volume 22. https://doi.org/10.1016/j.esr.2018.08.012 * Maximilian Parzen et al. 2023. PyPSA-Earth: A new global open energy system optimization model demonstrated in Africa, *Applied Energy*, Volume 341. https://doi.org/10.1016/j.apenergy.2023.121096 * Bobby Xiong et al. 2025. Modelling the high-voltage grid using open data for Europe and beyond. *Sci Data* 12, 277. https://doi.org/10.1038/s41597-025-04550-7 + +## Contributors + +See [AUTHORS](./AUTHORS) for copyright attribution and the +[contributor list](https://github.com/pypsa/grid-builder/graphs/contributors) +for contributions to the project. diff --git a/config/README.md b/config/README.md index fa86a60..776743e 100644 --- a/config/README.md +++ b/config/README.md @@ -46,7 +46,10 @@ missing user agent risks being rate-limited or blocked. `retrieve.overpass_api.u also lets you point at your own or a faster mirror instance instead of the shared public endpoint, without touching the checked-in default. -The generated [schema](./config.schema.json) describes every option. +The generated [schema](../workflow/internal/config.schema.yaml) describes every +option using JSON Schema in YAML format. Pydantic models remain the validation +source of truth; `pixi run generate-config` updates the schema, default config, +and the JSON copy at `config/config.schema.json` together. After adding or removing a `config/regions/config..yaml` file, run `pixi run generate-config`. It discovers the regional files and generates the diff --git a/figures/rulegraph.png b/figures/rulegraph.png new file mode 100644 index 0000000000000000000000000000000000000000..ee78925618db9e474975acb29c3df09187499955 GIT binary patch literal 24984 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

hVm&PtEfBCIdRa7hfkxE- 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 OKq`uAh++j(|Nj9CQe~$A literal 0 HcmV?d00001 From 2d9d6acb6076bbbc038ffb2a1714b87b0b6018bf Mon Sep 17 00:00:00 2001 From: bobbyxng Date: Tue, 6 Oct 2026 17:37:41 +0200 Subject: [PATCH 5/5] fix: resolve Windows module import test failures. Upgrade to snakemake 9.27.0 --- README.md | 2 +- pixi.lock | 27 +++++++++++++++------------ pixi.toml | 2 +- tests/test_environments.py | 3 ++- workflow/Snakefile | 2 +- 5 files changed, 20 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index c0b667f..d1470d6 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ working directory. Logs use `/`, with a separate retrieval log per country ### Importing into another workflow -The host environment needs Python 3.12, Snakemake >=9.19, `pydantic >=2`, +The host environment needs Python 3.12, Snakemake >=9.27, `pydantic >=2`, `ruamel.yaml >=0.18`, `pyyaml >=6,<7`, and `earth-osm >=3.0.2` to load and validate the module. Rule dependencies are installed separately by `--use-conda`. diff --git a/pixi.lock b/pixi.lock index 5f85502..771e329 100644 --- a/pixi.lock +++ b/pixi.lock @@ -29,7 +29,7 @@ environments: - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-report-plugins-1.3.0-pyhd4c3c12_0.conda - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-scheduler-plugins-2.0.2-pyhd4c3c12_0.conda - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-storage-plugins-4.4.1-pyh84498cf_0.conda - - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-minimal-9.20.0-pyhdfd78af_0.conda + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-minimal-9.27.0-pyhdfd78af_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.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 @@ -375,7 +375,7 @@ environments: - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-report-plugins-1.3.0-pyhd4c3c12_0.conda - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-scheduler-plugins-2.0.2-pyhd4c3c12_0.conda - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-storage-plugins-4.4.1-pyh84498cf_0.conda - - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-minimal-9.20.0-pyhdfd78af_0.conda + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-minimal-9.27.0-pyhdfd78af_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda - conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/amply-0.1.6-pyhd8ed1ab_1.conda @@ -707,7 +707,7 @@ environments: - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-report-plugins-1.3.0-pyhd4c3c12_0.conda - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-scheduler-plugins-2.0.2-pyhd4c3c12_0.conda - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-interface-storage-plugins-4.4.1-pyh84498cf_0.conda - - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-minimal-9.20.0-pyhdfd78af_0.conda + - conda: https://conda.anaconda.org/bioconda/noarch/snakemake-minimal-9.27.0-pyhdfd78af_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.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 @@ -1555,14 +1555,14 @@ packages: license_family: MIT size: 22783 timestamp: 1773699846635 -- conda: https://conda.anaconda.org/bioconda/noarch/snakemake-minimal-9.20.0-pyhdfd78af_0.conda - sha256: 17bff0693974075d19ee57235aa7c87ac425c4987bafd84aa7561e23cec0529f - md5: 8fec66fdc146656a4f2f830732856b51 +- conda: https://conda.anaconda.org/bioconda/noarch/snakemake-minimal-9.27.0-pyhdfd78af_0.conda + sha256: 2c80dd4dce15bf6eb1806ccab28f12a47d81e623d50e8543f58f6eac9090498b + md5: ef7be2b45f827b930836cd91db64e769 depends: - conda-inject >=1.3.1,<2.0 - configargparse - connection_pool >=0.0.3 - - docutils + - docutils >=0.20,<0.23 - dpath >=2.1.6,<3.0.0 - gitpython - humanfriendly @@ -1570,11 +1570,11 @@ packages: - jinja2 >=3.0,<4.0 - jsonschema - nbformat - - packaging >=24.0 + - packaging >=24.0,<26 - platformdirs - psutil - pulp >=2.3.1,<3.4 - - python >=3.11,<3.14 + - python >=3.11 - pyyaml - referencing - requests >=2.8.1,<3.0 @@ -1584,7 +1584,7 @@ packages: - snakemake-interface-logger-plugins >=1.1.0,<3.0.0 - snakemake-interface-report-plugins >=1.2.0,<2.0.0 - snakemake-interface-scheduler-plugins >=2.0.0,<3.0.0 - - snakemake-interface-storage-plugins >=4.3.2,<5.0 + - snakemake-interface-storage-plugins >=4.4.1,<5.0 - sqlmodel >=0.0.37,<0.0.38 - tabulate - tenacity >=9.1.4,<10.0 @@ -1593,8 +1593,11 @@ packages: - yte >=1.5.5,<2.0 license: MIT license_family: MIT - size: 883505 - timestamp: 1777722551210 + run_exports: + weak: + - snakemake-minimal >=9.27.0,<10.0a0 + size: 891186 + timestamp: 1789337861924 - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda build_number: 20 sha256: 1dd3fffd892081df9726d7eb7e0dea6198962ba775bd88842135a4ddb4deb3c9 diff --git a/pixi.toml b/pixi.toml index 4a8910a..b6c0a3a 100644 --- a/pixi.toml +++ b/pixi.toml @@ -22,7 +22,7 @@ pyproj = ">=3.7,<4" pyyaml = ">=6,<7" ruff = ">=0.9.9" snakefmt = ">=0.10.2" -snakemake-minimal = ">=9.19.0" +snakemake-minimal = ">=9.27.0" pytz = ">=2026.1.post1" pre-commit = ">=4.6.2,<5" diff --git a/tests/test_environments.py b/tests/test_environments.py index 7272d01..1043a14 100644 --- a/tests/test_environments.py +++ b/tests/test_environments.py @@ -49,4 +49,5 @@ def test_export_to_custom_directory(tmp_path): timeout=60, ) for source in (root / "workflow/envs").glob("module.*"): - assert (outdir / source.name).read_bytes() == source.read_bytes() + # Git checkouts may use CRLF on Windows; compare normalized text. + assert (outdir / source.name).read_text() == source.read_text() diff --git a/workflow/Snakefile b/workflow/Snakefile index b5cb242..0858ff9 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -5,7 +5,7 @@ import sys import yaml from snakemake.utils import min_version -min_version("9.19") +min_version("9.27") # Resolve auxiliary files through Snakemake so remote modules are cached too. # A path-specific name keeps separate grid-builder versions isolated in one host.