diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e3ceb1b..4a8f2ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,24 +1,20 @@ -name: Python 🐍 CI tests +name: Python 🐍 CI install check on: pull_request: - types: [closed] branches: [main] paths-ignore: - "**.md" - "**.bib" - - "**.ya?ml" - "LICENSE" - ".gitignore" workflow_dispatch: jobs: - test: - if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true - env: - SHIFT_SOLVER: highs - SHIFT_SOLVER_OPTIONS: highs-default - SHIFT_COMPUTE_IIS: "false" + install-check: + # No test suite exists yet (see tests/ removal history) - this job only + # confirms the pixi environment resolves and installs cleanly on both + # platforms. Replace with a real test job once tests/ exists again. runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -29,13 +25,10 @@ jobs: - uses: actions/checkout@v4 - name: Install Pixi - run: python -m pip install --upgrade pip pixi + uses: prefix-dev/setup-pixi@v0.9.0 - - name: Install dependencies via Pixi (test env) - run: pixi install --environment test + - name: Install dependencies via Pixi (default env) + run: pixi install - name: Validate environment - run: pixi info --environment test - - - name: Run tests - run: pixi run -e test unit-tests + run: pixi info diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2088d4f..43f2f13 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,16 +28,4 @@ repos: rev: v1.0.0 hooks: - id: snakefmt - - # Run some default pre-commit hooks - - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 - hooks: - - id: check-added-large-files - args: ["--maxkb=2000"] - - id: check-merge-conflict - - id: check-yaml - exclude: "pixi.lock" - - id: end-of-file-fixer - - id: trailing-whitespace diff --git a/environment.yaml b/environment.yaml index fe20f3b..cbc9754 100644 --- a/environment.yaml +++ b/environment.yaml @@ -12,18 +12,14 @@ dependencies: - pypsa>=0.32.1 - atlite>=0.3 - linopy>=0.4.4 -- powerplantmatching>=0.5.15 # Dependencies of the workflow itself -- dask -- xlrd - openpyxl - seaborn - snakemake-minimal>=9 - snakemake-storage-plugin-http>=0.3 - snakemake-executor-plugin-slurm - snakemake-executor-plugin-cluster-generic -- memory_profiler - yaml - pytables - lxml @@ -31,32 +27,21 @@ dependencies: - pandas>=2.1 - geopandas>=1 - xarray>=2024.03.0 -- rioxarray - netcdf4 - libgdal-netcdf - networkx - scipy -- glpk - shapely>=2.0 - matplotlib -- proj -- fiona -- country_converter - geopy - tqdm -- pytz -- jpype1 -- pyxlsb -- graphviz - geojson -- pyscipopt +- graphviz - wbdata - searoute # GIS dependencies: - cartopy -- descartes -- rasterio # Development dependencies - jupyter @@ -68,6 +53,5 @@ dependencies: - pip: - gurobipy - highspy - - tsam>=2.3.1 - - entsoe-py + - wbgapi - pypsatopo diff --git a/pixi.lock b/pixi.lock index 0ce911f..b1115ee 100644 --- a/pixi.lock +++ b/pixi.lock @@ -109,6 +109,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/eccodes-2.46.0-h83bc92c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ecmwf-datastores-client-0.4.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/entrypoints-0.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/et_xmlfile-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/findlibs-0.1.2-pyhd8ed1ab_0.conda @@ -331,6 +332,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/oauthlib-3.3.1-pyhd8ed1ab_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/openldap-2.6.10-hbde042b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openpyxl-3.1.5-py312h7f6eeab_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.3.0-h21090e2_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda @@ -512,6 +514,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/09/b6/621bc73492accf30a584291c45a4ceb7524b371d5e2df64213e8430d56b6/shelved_cache-0.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/96/8538b80d6425e53ed68553688ca1a615e0b1f789015f55c370b337aaada5/wbdata-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8f/9e/053111e07789d0dfcbb4ff334b06fdbe6ad8c567be0c6278d0a1781e0793/wbgapi-1.0.14-py3-none-any.whl win-64: - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-20_gnu.conda - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda @@ -607,6 +610,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/eccodes-2.46.0-h2bffdaa_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ecmwf-datastores-client-0.4.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/entrypoints-0.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/et_xmlfile-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/findlibs-0.1.2-pyhd8ed1ab_0.conda @@ -797,6 +801,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.4.6-py312ha3f287d_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/oauthlib-3.3.1-pyhd8ed1ab_0.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/openpyxl-3.1.5-py312h83acffa_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.1-hf411b9b_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/orc-2.3.0-h8fc0eb6_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda @@ -963,6 +968,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/09/b6/621bc73492accf30a584291c45a4ceb7524b371d5e2df64213e8430d56b6/shelved_cache-0.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/96/8538b80d6425e53ed68553688ca1a615e0b1f789015f55c370b337aaada5/wbdata-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8f/9e/053111e07789d0dfcbb4ff334b06fdbe6ad8c567be0c6278d0a1781e0793/wbgapi-1.0.14-py3-none-any.whl dev: channels: - url: https://conda.anaconda.org/conda-forge/ @@ -1078,6 +1084,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/eccodes-2.46.0-h83bc92c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ecmwf-datastores-client-0.4.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/entrypoints-0.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/et_xmlfile-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.25.2-pyhd8ed1ab_0.conda @@ -1306,6 +1313,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/oauthlib-3.3.1-pyhd8ed1ab_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/openldap-2.6.10-hbde042b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openpyxl-3.1.5-py312h7f6eeab_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.3.0-h21090e2_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda @@ -1497,6 +1505,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/09/b6/621bc73492accf30a584291c45a4ceb7524b371d5e2df64213e8430d56b6/shelved_cache-0.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/96/8538b80d6425e53ed68553688ca1a615e0b1f789015f55c370b337aaada5/wbdata-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8f/9e/053111e07789d0dfcbb4ff334b06fdbe6ad8c567be0c6278d0a1781e0793/wbgapi-1.0.14-py3-none-any.whl win-64: - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-20_gnu.conda - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda @@ -1598,6 +1607,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/eccodes-2.46.0-h2bffdaa_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ecmwf-datastores-client-0.4.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/entrypoints-0.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/et_xmlfile-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/filelock-3.25.2-pyhd8ed1ab_0.conda @@ -1794,6 +1804,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.4.6-py312ha3f287d_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/oauthlib-3.3.1-pyhd8ed1ab_0.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/openpyxl-3.1.5-py312h83acffa_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.1-hf411b9b_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/orc-2.3.0-h8fc0eb6_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda @@ -1970,6 +1981,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/09/b6/621bc73492accf30a584291c45a4ceb7524b371d5e2df64213e8430d56b6/shelved_cache-0.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/96/8538b80d6425e53ed68553688ca1a615e0b1f789015f55c370b337aaada5/wbdata-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8f/9e/053111e07789d0dfcbb4ff334b06fdbe6ad8c567be0c6278d0a1781e0793/wbgapi-1.0.14-py3-none-any.whl test: channels: - url: https://conda.anaconda.org/conda-forge/ @@ -2080,6 +2092,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/eccodes-2.46.0-h83bc92c_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ecmwf-datastores-client-0.4.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/entrypoints-0.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/et_xmlfile-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/findlibs-0.1.2-pyhd8ed1ab_0.conda @@ -2302,6 +2315,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/oauthlib-3.3.1-pyhd8ed1ab_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/openldap-2.6.10-hbde042b_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openpyxl-3.1.5-py312h7f6eeab_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/orc-2.3.0-h21090e2_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda @@ -2484,6 +2498,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/09/b6/621bc73492accf30a584291c45a4ceb7524b371d5e2df64213e8430d56b6/shelved_cache-0.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/96/8538b80d6425e53ed68553688ca1a615e0b1f789015f55c370b337aaada5/wbdata-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8f/9e/053111e07789d0dfcbb4ff334b06fdbe6ad8c567be0c6278d0a1781e0793/wbgapi-1.0.14-py3-none-any.whl win-64: - conda: https://conda.anaconda.org/conda-forge/win-64/_openmp_mutex-4.5-20_gnu.conda - conda: https://conda.anaconda.org/conda-forge/noarch/_python_abi3_support-1.0-hd8ed1ab_2.conda @@ -2580,6 +2595,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/eccodes-2.46.0-h2bffdaa_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ecmwf-datastores-client-0.4.2-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/entrypoints-0.4-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/et_xmlfile-2.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/executing-2.2.1-pyhd8ed1ab_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/findlibs-0.1.2-pyhd8ed1ab_0.conda @@ -2770,6 +2786,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/numpy-2.4.6-py312ha3f287d_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/oauthlib-3.3.1-pyhd8ed1ab_0.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/openpyxl-3.1.5-py312h83acffa_3.conda - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.1-hf411b9b_1.conda - conda: https://conda.anaconda.org/conda-forge/win-64/orc-2.3.0-h8fc0eb6_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/overrides-7.7.0-pyhd8ed1ab_1.conda @@ -2937,6 +2954,7 @@ environments: - pypi: https://files.pythonhosted.org/packages/09/b6/621bc73492accf30a584291c45a4ceb7524b371d5e2df64213e8430d56b6/shelved_cache-0.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/76/96/8538b80d6425e53ed68553688ca1a615e0b1f789015f55c370b337aaada5/wbdata-1.1.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/8f/9e/053111e07789d0dfcbb4ff334b06fdbe6ad8c567be0c6278d0a1781e0793/wbgapi-1.0.14-py3-none-any.whl packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda build_number: 20 @@ -5254,6 +5272,17 @@ packages: - pkg:pypi/entrypoints?source=hash-mapping size: 11259 timestamp: 1733327239578 +- conda: https://conda.anaconda.org/conda-forge/noarch/et_xmlfile-2.0.0-pyhd8ed1ab_1.conda + sha256: 2209534fbf2f70c20661ff31f57ab6a97b82ee98812e8a2dcb2b36a0d345727c + md5: 71bf9646cbfabf3022c8da4b6b4da737 + depends: + - python >=3.9 + license: MIT + license_family: MIT + purls: + - pkg:pypi/et-xmlfile?source=hash-mapping + size: 21908 + timestamp: 1733749746332 - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda sha256: ee6cf346d017d954255bbcbdb424cddea4d14e4ed7e9813e429db1d795d01144 md5: 8e662bd460bda79b1ea39194e3c4c9ab @@ -10269,6 +10298,36 @@ packages: purls: [] size: 785570 timestamp: 1771970256722 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openpyxl-3.1.5-py312h7f6eeab_3.conda + sha256: 04839d313708a6b8c185bc9fcc56ccef985ed91520420c665b5e67b55fd8b5fb + md5: 04ab345ef65b88bcbb8ac3d083427bfc + depends: + - et_xmlfile + - libgcc >=14 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + license: MIT + license_family: MIT + purls: + - pkg:pypi/openpyxl?source=hash-mapping + size: 476128 + timestamp: 1769122092500 +- conda: https://conda.anaconda.org/conda-forge/win-64/openpyxl-3.1.5-py312h83acffa_3.conda + sha256: 824eeb546a08c990eb461706280d3064c0437f67eb320018986fa439f468e43a + md5: ff342a314798173eaaf2753a22f044fa + depends: + - et_xmlfile + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: + - pkg:pypi/openpyxl?source=hash-mapping + size: 475979 + timestamp: 1769122144585 - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda sha256: 44c877f8af015332a5d12f5ff0fb20ca32f896526a7d0cdb30c769df1144fb5c md5: f61eb8cd60ff9057122a3d338b99c00f @@ -13359,6 +13418,15 @@ packages: - mkdocstrings[python]>=0.30,<1 ; extra == 'docs' - pandas>=1,<3 ; extra == 'pandas' requires_python: '>=3.10,<4' +- pypi: https://files.pythonhosted.org/packages/8f/9e/053111e07789d0dfcbb4ff334b06fdbe6ad8c567be0c6278d0a1781e0793/wbgapi-1.0.14-py3-none-any.whl + name: wbgapi + version: 1.0.14 + sha256: abf43fcbbdc85a84c0774755e725df76ad5ae076b3ecc38a940fa718e14c514b + requires_dist: + - requests + - pyyaml + - tabulate + requires_python: '>=3.0' - conda: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.6.0-pyhd8ed1ab_0.conda sha256: e298b508b2473c4227206800dfb14c39e4b14fd79d4636132e9e1e4244cdf4aa md5: c3197f8c0d5b955c904616b716aca093 diff --git a/pixi.toml b/pixi.toml index 58f7c00..f05e14c 100644 --- a/pixi.toml +++ b/pixi.toml @@ -39,11 +39,14 @@ snakemake-minimal = "==9.6.2" snakemake-storage-plugin-http = ">=0.3" tqdm = "*" xarray = ">=2026.0.0" +openpyxl = ">=3.1.5,<4" [pypi-dependencies] # pip-installable packages (always included) gurobipy = "*" wbdata = ">=1.1.0" +wbgapi = ">=1.0.14, <2" +highspy = "*" [feature.test.dependencies] pytest = "*" diff --git a/workflow/scripts/_helpers.py b/workflow/scripts/_helpers.py index 6a8d51d..b580392 100644 --- a/workflow/scripts/_helpers.py +++ b/workflow/scripts/_helpers.py @@ -1,4 +1,5 @@ import logging +import sys from pathlib import Path import requests @@ -41,7 +42,30 @@ def setup_logging( formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") if not any(getattr(h, "_shift_console_handler", False) for h in log.handlers): - stream_handler = logging.StreamHandler() + # Windows consoles default to a legacy codepage (e.g. cp1252) that + # can't encode characters like the arrows some log messages use + # (e.g. "3462 rows → gni_per_capita.csv"), so the bare stream write + # inside logging.StreamHandler.emit() raises UnicodeEncodeError. + # logging swallows that (prints a "--- Logging error ---" banner + # instead of the intended message and moves on), so this never + # crashes a run, but it does silently eat log output. Wrap our own + # handle to sys.stderr's underlying fd with errors="backslashreplace" + # instead of mutating sys.stderr itself, since reconfiguring the + # shared sys.stderr object doesn't reliably propagate to handlers/ + # codecs that already hold a reference to it. + console_stream = sys.stderr + if hasattr(sys.stderr, "fileno"): + try: + console_stream = open( + sys.stderr.fileno(), + mode="w", + encoding=sys.stderr.encoding or "utf-8", + errors="backslashreplace", + closefd=False, + ) + except (OSError, ValueError): + pass + stream_handler = logging.StreamHandler(console_stream) stream_handler.setLevel(level) stream_handler.setFormatter(formatter) stream_handler._shift_console_handler = True # type: ignore[attr-defined] @@ -57,7 +81,12 @@ def setup_logging( getattr(h, "baseFilename", None) == str(log_path) for h in log.handlers ): log_path.parent.mkdir(parents=True, exist_ok=True) - file_handler = logging.FileHandler(log_path) + # Explicit encoding: without it, FileHandler defaults to the + # platform locale's preferred encoding (cp1252 on Windows), which + # can't represent characters like the arrows some log messages use + # and raises UnicodeEncodeError on emit. A log file has no display + # constraint the way a console does, so just use UTF-8. + file_handler = logging.FileHandler(log_path, encoding="utf-8") file_handler.setLevel(logging.DEBUG) file_handler.setFormatter(formatter) file_handler._shift_file_handler = True # type: ignore[attr-defined] diff --git a/workflow/scripts/model_trade.py b/workflow/scripts/model_trade.py index de90570..705c682 100644 --- a/workflow/scripts/model_trade.py +++ b/workflow/scripts/model_trade.py @@ -182,7 +182,6 @@ def building_model( # --- Stage 1 supply: ore → interone (material) or direct supply (energy) --- for s in range(len(region_data_interone)): - if s == 0: p_nom_interone = float(region_data_interone[f"demand [{unit}]"][s]) else: @@ -230,7 +229,6 @@ def building_model( # --- Stage 2 supply: interone → final (two-stage material chain only) --- if two_stage: for s in range(len(region_data_intertwo)): - if s == 0: p_nom_intertwo = float(region_data_intertwo[f"demand [{unit}]"][s]) else: @@ -245,9 +243,7 @@ def building_model( ) p_nom_intertwo = ( grid_potential.loc[region_name, "potential_mt_steel"] * 1e6 - ) / len( - region_data_intertwo - ) # split evenly across supply steps + ) / len(region_data_intertwo) # split evenly across supply steps m_cost = float( region_data_intertwo[f"{cost_descriptor} [EUR/{unit}]"][s] @@ -345,26 +341,16 @@ def create_links(trade_options): # checking if the row connects with pipeline if (trade_options["pipeline"][r] == 1) & (final == "hydrogen"): - r_from = trade_options["region_from"][r] - r_to = trade_options["region_to"][r] - p_cost = int(float(trade_options["pipeline_distance [km]"][r]) * pipe_mc) - filling_demand = 1.5 / 100 # DEA, energy transport datasheet, 2050, % - losses = 1.7 / 100 # DEA, energy transport datasheet 2022, 2050, %/1000km - eff = (1 - filling_demand) * (1 - losses) ** ( - float(trade_options["pipeline_distance [km]"][r]) / 1000 - ) - - n.add( - "Link", - "pipeline {}-{}".format(r_from, r_to), - bus0=r_from, - bus1=r_to, - efficiency=eff, # calculated above - marginal_cost=p_cost, # EUR/MWh - p_nom_extendable=True, - capital_cost=1 / 1000, # to prevent optimisation shenenigans + raise NotImplementedError( + "Pipeline-based trade links (trade_options rows with " + "pipeline=1, final_product='hydrogen') are not currently " + "supported: the pipeline marginal cost used to come from a " + "'transport_costs' input that no longer exists in this " + "codebase - shipping costs were migrated to " + "config['trade']['shipping'], but the pipeline cost path " + "was never updated to match. Configure a pipeline cost " + "source before enabling this trade_options combination." ) - logger.info("pipeline link made from %s to %s - eff %s", r_from, r_to, eff) return @@ -471,6 +457,19 @@ def save_network_collection(nc, output_path, optimal_network=None): logger.info(f"Saved {total_saved} networks to {output_dir}") +def _carrier_values(stats, carrier): + """Select a carrier's per-bus values from an n.statistics(...) result. + + Returns 0 (matching plot_trade_network's existing "not applicable" + convention, which n.plot.map already handles) when the carrier has no + entries in the network at all, instead of letting pandas raise KeyError + on a missing MultiIndex level. + """ + if carrier not in stats.index.get_level_values(-1): + return 0 + return stats.loc[:, :, carrier].droplevel(0) + + def plot_trade_network( n, product="steel", @@ -508,15 +507,13 @@ def plot_trade_network( }.get(product) if product in [interone, intertwo, final, "iron_ore"]: - supply = ( - n.statistics.supply(comps=[supply_comp], groupby=["bus", "carrier"]) - .loc[:, :, supply_carrier] - .droplevel(0) + supply = _carrier_values( + n.statistics.supply(comps=[supply_comp], groupby=["bus", "carrier"]), + supply_carrier, ) - demand = ( - n.statistics.withdrawal(comps=[demand_comp], groupby=["bus", "carrier"]) - .loc[:, :, demand_carrier] - .droplevel(0) + demand = _carrier_values( + n.statistics.withdrawal(comps=[demand_comp], groupby=["bus", "carrier"]), + demand_carrier, ) trade = n.links[n.links.carrier == trade_carrier].p_nom_opt.astype(int) else: