From c84129de087e6c48398f62649654feed97e42cef Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 26 Mar 2026 15:13:26 +0000 Subject: [PATCH 01/12] Add support for python3.14, drop support for 3.9 and 3.10 --- .github/workflows/ci.yml | 6 ++--- .github/workflows/documentation.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/regression_tests.yml | 2 +- .readthedocs.yaml | 2 +- docs/installation/pipx-based.rst | 30 ++++++++++++------------- docs/installation/virtual-env-based.rst | 4 ++-- pyproject.toml | 5 ++--- tests/conftest.py | 12 +--------- 9 files changed, 27 insertions(+), 38 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32d58207a..1f0da0d47 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] # Test with the earliest and the latest python versions supported - python-version: ["3.9", "3.13"] + python-version: ["3.11", "3.14"] steps: - uses: actions/checkout@v6 @@ -40,7 +40,7 @@ jobs: --cov-report=xml - name: Upload coverage to Codecov - if: success() && (github.event_name == 'push' && runner.os == 'Linux' && matrix.python-version == 3.9) + if: success() && (github.event_name == 'push' && runner.os == 'Linux' && matrix.python-version == 3.11) uses: codecov/codecov-action@v5 with: fail_ci_if_error: true @@ -59,7 +59,7 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] # Test with the earliest and the latest python versions supported - python-version: ["3.9", "3.13"] + python-version: ["3.11", "3.14"] steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 04749bcfa..83911271b 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -13,7 +13,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: 3.13 + python-version: 3.14 - name: Install dependencies run: | sudo apt update -y diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 022f692a1..82694fe07 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -85,7 +85,7 @@ jobs: fail-fast: false matrix: os: [windows-latest] - python-version: ["3.9"] + python-version: ["3.11"] steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/regression_tests.yml b/.github/workflows/regression_tests.yml index cc7e4c497..bf1f5e068 100644 --- a/.github/workflows/regression_tests.yml +++ b/.github/workflows/regression_tests.yml @@ -11,7 +11,7 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] # Test with the earliest and the latest python versions supported - python-version: ["3.9", "3.13"] + python-version: ["3.11", "3.14"] steps: - uses: actions/checkout@v6 diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 36c1bfa5d..39b11a689 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -9,7 +9,7 @@ version: 2 build: os: ubuntu-20.04 tools: - python: "3.9" + python: "3.11" apt_packages: - graphviz - pandoc diff --git a/docs/installation/pipx-based.rst b/docs/installation/pipx-based.rst index 5d44792ca..6f18c901d 100644 --- a/docs/installation/pipx-based.rst +++ b/docs/installation/pipx-based.rst @@ -6,7 +6,7 @@ pipx-based installation To help you installing MUSE in your system we will follow these steps: - `Launching a terminal`_: Needed to both install and run MUSE. -- `Installing a compatible Python version`_: MUSE works with Python 3.9 to 3.13. +- `Installing a compatible Python version`_: MUSE works with Python 3.11 to 3.14. - `Installing pipx`_: A Python application manager that facilitates installing, keeping applications updated and run them in their own isolated environments. - `Installing MUSE itself`_ @@ -22,8 +22,8 @@ In the following sections, we will guide you step by step in configuring your sy .. code-block:: - pyenv install 3.9.13 - pyenv shell 3.9.13 + pyenv install 3.11.0 + pyenv shell 3.11.0 python -m pip install pipx python -m pipx ensurepath python -m pipx install muse-os @@ -67,7 +67,7 @@ Once you have launched the Terminal, the window that opens will show the command Installing a compatible Python version ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -MUSE needs Python to run and it works with versions 3.9 to 3.13, so the next step is to install a suitable version of Python. +MUSE needs Python to run and it works with versions 3.11 to 3.14, so the next step is to install a suitable version of Python. .. note:: @@ -92,7 +92,7 @@ The first thing will be to check if you already have a suitable python version i python --version -If the output is ``Python 3.Y.X`` or ``Python 3.Y.X``, where ``X`` is any number and ``Y`` is 9, 10, 11 or 12, then **you have a version of Python compatible with MUSE and you can skip this section altogether**. Move to `Installing pipx`_. In any other case, keep reading. +If the output is ``Python 3.Y.X`` or ``Python 3.Y.X``, where ``X`` is any number and ``Y`` is 11, 12, 13 or 14, then **you have a version of Python compatible with MUSE and you can skip this section altogether**. Move to `Installing pipx`_. In any other case, keep reading. There are multiple ways of installing Python, as well as multiple distributions. Here we have opted for the one that we believe is simplest, requires the smallest downloads and gives the maximum flexibility: using ``pyenv``. @@ -189,22 +189,22 @@ With ``pyenv`` installed and correctly configured, it is now easy to install any pyenv install -l -You should see a long list of versions to choose from. Let's install one of the later versions of the 3.9 family: +You should see a long list of versions to choose from. Let's install 3.11.0 as an example: .. code-block:: bash - pyenv install 3.9.13 + pyenv install 3.11.0 The command will take a minute or two to complete, depending on your internet connection, and show an output similar to the following (this is an example from Windows): .. code-block:: output :: [Info] :: Mirror: https://www.python.org/ftp/python - :: [Downloading] :: 3.9.13 ... - :: [Downloading] :: From https://www.python.org/ftp/python/3.9.13/python-3.9.13-amd64.exe - :: [Downloading] :: To C:\Users\your_username\.pyenv\pyenv-win\install_cache\python-3.9.13-amd64.exe - :: [Installing] :: 3.9.13 ... - :: [Info] :: completed! 3.9.13 + :: [Downloading] :: 3.11.0 ... + :: [Downloading] :: From https://www.python.org/ftp/python/3.11.0/python-3.11.0-amd64.exe + :: [Downloading] :: To C:\Users\your_username\.pyenv\pyenv-win\install_cache\python-3.11.0-amd64.exe + :: [Installing] :: 3.11.0 ... + :: [Info] :: completed! 3.11.0§ Now, we have a new Python version in our system, but it is still not available (if you run ``python --version`` you will get the same result as before). There are two options moving forward: @@ -212,15 +212,15 @@ Now, we have a new Python version in our system, but it is still not available ( .. code-block:: bash - pyenv global 3.9.13 + pyenv global 3.11.0 - If you just want it momentarily to install MUSE run instead the following command: .. code-block:: bash - pyenv shell 3.9.13 + pyenv shell 3.11.0 -In both cases, if you run ``python --version`` afterwards, you should get ``Python 3.9.13``. +In both cases, if you run ``python --version`` afterwards, you should get ``Python 3.11.0``. Installing ``pipx`` ~~~~~~~~~~~~~~~~~~~ diff --git a/docs/installation/virtual-env-based.rst b/docs/installation/virtual-env-based.rst index f487e23b7..0df6c630d 100644 --- a/docs/installation/virtual-env-based.rst +++ b/docs/installation/virtual-env-based.rst @@ -34,7 +34,7 @@ To create an environment called ``muse_env`` run: .. code-block:: bash - conda create -n muse_env python=3.9 + conda create -n muse_env python=3.11 Now, you can activate the environment with: @@ -95,7 +95,7 @@ Creating a virtual environment with ``pyenv + venv`` Alternatively to creating virtual environments in ``conda``, you can also make use of two well-tested and maintained libraries. We met the first one, ``pyenv``, already in the :ref:`pipx-based ` under the section :ref:`Installing pyenv ` and the installation procedure is exactly the same. -If you go down that route, please follow the steps outlined there and chose a recent version ``Python``, say 3.9. +If you go down that route, please follow the steps outlined there and chose a recent version ``Python``, say 3.13. The second package we need to create virtual environments for any specific ``Python`` version is called ``venv``, and it ships with ``Python`` by default. To create such an environment, we first need to ensure that the diff --git a/pyproject.toml b/pyproject.toml index 2bd166d1a..bf954c08a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,15 +11,14 @@ name = "MUSE_OS" description = "Energy System Model" readme = "README.md" license = {file = "LICENSE"} -requires-python = ">= 3.9, <3.14" +requires-python = ">= 3.11, <3.15" keywords = ["energy", "modelling"] classifiers = [ "Development Status :: 4 - Beta", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Intended Audience :: Science/Research", "Intended Audience :: Other Audience", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)" diff --git a/tests/conftest.py b/tests/conftest.py index d5d728e82..68c5ae763 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -524,7 +524,7 @@ def drop_optionals(settings): @fixture(autouse=True) def warnings_as_errors(request): - from warnings import filterwarnings, simplefilter + from warnings import simplefilter # disable fixture for some tests if ( @@ -538,16 +538,6 @@ def warnings_as_errors(request): simplefilter("error", DeprecationWarning) simplefilter("error", PendingDeprecationWarning) - # The following warning is safe to ignore (raised by adhoc solver with Python 3.9) - # TODO: may be able to remove this once support for Python 3.9 is dropped - if request.module.__name__ == "test_fullsim_regression": - filterwarnings( - "ignore", - message="__array__ implementation doesn't accept a copy keyword", - category=DeprecationWarning, - module="xarray.core.variable", - ) - @fixture def save_registries(): From 5cd08812b50beaa179cd796ea3a0a44f41fabdec Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 26 Mar 2026 16:26:14 +0000 Subject: [PATCH 02/12] Bump pandas slightly --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bf954c08a..eeb312334 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ classifiers = [ dependencies = [ "numpy>=2.0", "scipy>=1.13", - "pandas>=2.2,<3.0", + "pandas>=2.3,<3.0", "xarray>=2024.6,<=2024.11", "bottleneck>=1.4", "coloredlogs", From 5a668f8b1a3e338a85d32e0ebb13785bc70aa776 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Fri, 27 Mar 2026 11:56:45 +0000 Subject: [PATCH 03/12] Bump main dependencies --- pyproject.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index eeb312334..5054f949e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,10 +24,10 @@ classifiers = [ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)" ] dependencies = [ - "numpy>=2.0", - "scipy>=1.13", - "pandas>=2.3,<3.0", - "xarray>=2024.6,<=2024.11", + "numpy>=2.4,<3.0", + "scipy>=1.17,<2.0", + "pandas>=3.0,<4.0", + "xarray>=2026.2", "bottleneck>=1.4", "coloredlogs", "toml", From abaf39b22891229f922a62342e0ec08422a5b715 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Fri, 27 Mar 2026 12:03:16 +0000 Subject: [PATCH 04/12] Fix typo --- docs/installation/pipx-based.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/pipx-based.rst b/docs/installation/pipx-based.rst index 6f18c901d..ec97df892 100644 --- a/docs/installation/pipx-based.rst +++ b/docs/installation/pipx-based.rst @@ -204,7 +204,7 @@ The command will take a minute or two to complete, depending on your internet co :: [Downloading] :: From https://www.python.org/ftp/python/3.11.0/python-3.11.0-amd64.exe :: [Downloading] :: To C:\Users\your_username\.pyenv\pyenv-win\install_cache\python-3.11.0-amd64.exe :: [Installing] :: 3.11.0 ... - :: [Info] :: completed! 3.11.0§ + :: [Info] :: completed! 3.11.0 Now, we have a new Python version in our system, but it is still not available (if you run ``python --version`` you will get the same result as before). There are two options moving forward: From 056df4a914498bc31c5c89d7f1a832b07c64bccd Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Fri, 27 Mar 2026 12:09:15 +0000 Subject: [PATCH 05/12] Fix future warnings --- src/muse/utilities.py | 8 +++++--- tests/test_demand_share.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/muse/utilities.py b/src/muse/utilities.py index 66843382e..3bd0cf6e9 100644 --- a/src/muse/utilities.py +++ b/src/muse/utilities.py @@ -150,7 +150,7 @@ def operation(x): # Concatenate assets if a sequence is given if not isinstance(assets, (xr.Dataset, xr.DataArray)): - assets = xr.concat(assets, dim=dim) + assets = xr.concat(assets, dim=dim, join="outer") assert isinstance(assets, (xr.Dataset, xr.DataArray)) # If there are no assets, nothing needs to be done @@ -370,7 +370,7 @@ def merge_assets( capa_b_interp = interpolate_capacity(capa_b, year=years) # Concatenate the two capacity arrays - result = xr.concat((capa_a_interp, capa_b_interp), dim=dimension) + result = xr.concat((capa_a_interp, capa_b_interp), dim=dimension, join="outer") # forgroup = result.pipe(coords_to_multiindex, dimension=dimension) @@ -583,7 +583,9 @@ def agent_concatenation( ) else: datum[name] = key - result = xr.concat(data.values(), dim=dim) + result = xr.concat( + data.values(), dim=dim, join="outer", coords="different", compat="equals" + ) if isinstance(result, xr.Dataset): result = result.set_coords("agent") if "year" in result.dims: diff --git a/tests/test_demand_share.py b/tests/test_demand_share.py index 428446072..49657ddae 100644 --- a/tests/test_demand_share.py +++ b/tests/test_demand_share.py @@ -98,7 +98,7 @@ def create_regional_market(technologies, stock): usa_market = _matching_market( broadcast_over_assets(technologies, usa_stock), usa_stock.capacity ) - market = xr.concat((asia_market, usa_market), dim="region") + market = xr.concat((asia_market, usa_market), dim="region", join="outer") return market, asia_stock, usa_stock From 6949d896cc36ce8e8e1a21844903ec1a5d14e7d1 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Fri, 27 Mar 2026 12:17:22 +0000 Subject: [PATCH 06/12] Fix more future warnings --- src/muse/filters.py | 8 +++----- src/muse/utilities.py | 11 +++++++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/muse/filters.py b/src/muse/filters.py index 941525473..fc690e29d 100644 --- a/src/muse/filters.py +++ b/src/muse/filters.py @@ -489,9 +489,7 @@ def initialize_from_assets( if "asset" not in agent.assets.dims or len(agent.assets.asset) == 0: return replacement - assets = ( - xr.ones_like(reduce_assets(agent.assets.asset, coords=coords), dtype=bool) - .rename(technology="asset") - .set_index() - ) + assets = xr.ones_like( + reduce_assets(agent.assets.asset, coords=coords), dtype=bool + ).set_index(asset="technology") return (assets * replacement).transpose("asset", "replacement") diff --git a/src/muse/utilities.py b/src/muse/utilities.py index 3bd0cf6e9..6b015944a 100644 --- a/src/muse/utilities.py +++ b/src/muse/utilities.py @@ -150,7 +150,9 @@ def operation(x): # Concatenate assets if a sequence is given if not isinstance(assets, (xr.Dataset, xr.DataArray)): - assets = xr.concat(assets, dim=dim, join="outer") + assets = xr.concat( + assets, dim=dim, join="outer", coords="different", compat="equals" + ) assert isinstance(assets, (xr.Dataset, xr.DataArray)) # If there are no assets, nothing needs to be done @@ -584,7 +586,12 @@ def agent_concatenation( else: datum[name] = key result = xr.concat( - data.values(), dim=dim, join="outer", coords="different", compat="equals" + data.values(), + dim=dim, + join="outer", + coords="different", + compat="equals", + data_vars="all", ) if isinstance(result, xr.Dataset): result = result.set_coords("agent") From d0dda7ecb754d83a4ad9b2cd8edf594d157b7036 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Fri, 27 Mar 2026 12:29:06 +0000 Subject: [PATCH 07/12] Fix more future warnings --- src/muse/readers/csv.py | 6 ++++-- src/muse/readers/toml.py | 2 +- src/muse/utilities.py | 16 ++++++++++++++-- tests/test_demand_share.py | 9 ++++++++- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/muse/readers/csv.py b/src/muse/readers/csv.py index ee442e40a..5ddb652ae 100644 --- a/src/muse/readers/csv.py +++ b/src/muse/readers/csv.py @@ -679,7 +679,7 @@ def process_technologies( ) # Merge inputs/outputs with technodata - technodata = technodata.merge(outs).merge(ins) + technodata = technodata.merge(outs, join="outer").merge(ins, join="outer") # Merge technodata_timeslices if provided. This will prioritise values defined in # technodata_timeslices, and fallback to the non-timesliced technodata for any @@ -700,7 +700,9 @@ def process_technologies( technodata = check_commodities(technodata, fill_missing=False) # Add info about commodities - technodata = technodata.merge(COMMODITIES.sel(commodity=technodata.commodity)) + technodata = technodata.merge( + COMMODITIES.sel(commodity=technodata.commodity), join="outer" + ) # Add commodity usage flags technodata["comm_usage"] = ( diff --git a/src/muse/readers/toml.py b/src/muse/readers/toml.py index 5c49320d5..b90ae67b6 100644 --- a/src/muse/readers/toml.py +++ b/src/muse/readers/toml.py @@ -624,7 +624,7 @@ def read_technodata( # Drop duplicate data vars before merging common_vars = set(technologies.data_vars) & set(trade_data.data_vars) technologies = technologies.drop_vars(common_vars) - technologies = technologies.merge(trade_data) + technologies = technologies.merge(trade_data, join="outer") technologies = technologies.set_index(commodity="commodity") # See PR #638 return technologies diff --git a/src/muse/utilities.py b/src/muse/utilities.py index 6b015944a..f13e108ba 100644 --- a/src/muse/utilities.py +++ b/src/muse/utilities.py @@ -151,7 +151,12 @@ def operation(x): # Concatenate assets if a sequence is given if not isinstance(assets, (xr.Dataset, xr.DataArray)): assets = xr.concat( - assets, dim=dim, join="outer", coords="different", compat="equals" + assets, + dim=dim, + join="outer", + coords="different", + compat="equals", + data_vars="all", ) assert isinstance(assets, (xr.Dataset, xr.DataArray)) @@ -372,7 +377,14 @@ def merge_assets( capa_b_interp = interpolate_capacity(capa_b, year=years) # Concatenate the two capacity arrays - result = xr.concat((capa_a_interp, capa_b_interp), dim=dimension, join="outer") + result = xr.concat( + (capa_a_interp, capa_b_interp), + dim=dimension, + join="outer", + coords="different", + compat="equals", + data_vars="all", + ) # forgroup = result.pipe(coords_to_multiindex, dimension=dimension) diff --git a/tests/test_demand_share.py b/tests/test_demand_share.py index 49657ddae..84595e8e8 100644 --- a/tests/test_demand_share.py +++ b/tests/test_demand_share.py @@ -98,7 +98,14 @@ def create_regional_market(technologies, stock): usa_market = _matching_market( broadcast_over_assets(technologies, usa_stock), usa_stock.capacity ) - market = xr.concat((asia_market, usa_market), dim="region", join="outer") + market = xr.concat( + (asia_market, usa_market), + dim="region", + join="outer", + coords="different", + compat="equals", + data_vars="all", + ) return market, asia_stock, usa_stock From b530d7109af0add8d15b0f3da5bdd03386139064 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Fri, 27 Mar 2026 14:10:56 +0000 Subject: [PATCH 08/12] Fix a syntax error --- tests/test_timeslice_output.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_timeslice_output.py b/tests/test_timeslice_output.py index bcfad0357..42c8bc4ac 100644 --- a/tests/test_timeslice_output.py +++ b/tests/test_timeslice_output.py @@ -93,7 +93,7 @@ def test_zero_utilization_factor_supply_timeslice( zero_output = power_supply[ power_supply.timeslice.isin(zero_utilization_indices) - & (power_supply.technology == process_names) + & power_supply.technology.isin(process_names) ] assert len(zero_output) == 0 From f507c293d18b5e56613fa878a21288829b6c56c3 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Fri, 8 May 2026 11:00:31 +0100 Subject: [PATCH 09/12] Copilot suggestions --- .github/workflows/ci.yml | 2 +- .github/workflows/documentation.yml | 2 +- docs/installation/pipx-based.rst | 2 +- docs/installation/virtual-env-based.rst | 2 +- pyproject.toml | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c28c6394..19ab3f6f7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: --cov-report=xml - name: Upload coverage to Codecov - if: success() && (github.event_name == 'push' && runner.os == 'Linux' && matrix.python-version == 3.11) + if: success() && (github.event_name == 'push' && runner.os == 'Linux' && matrix.python-version == '3.11') uses: codecov/codecov-action@v6 with: fail_ci_if_error: true diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 83911271b..4e4e3dcfc 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -13,7 +13,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v6 with: - python-version: 3.14 + python-version: "3.14" - name: Install dependencies run: | sudo apt update -y diff --git a/docs/installation/pipx-based.rst b/docs/installation/pipx-based.rst index ec97df892..96fdf6aa3 100644 --- a/docs/installation/pipx-based.rst +++ b/docs/installation/pipx-based.rst @@ -92,7 +92,7 @@ The first thing will be to check if you already have a suitable python version i python --version -If the output is ``Python 3.Y.X`` or ``Python 3.Y.X``, where ``X`` is any number and ``Y`` is 11, 12, 13 or 14, then **you have a version of Python compatible with MUSE and you can skip this section altogether**. Move to `Installing pipx`_. In any other case, keep reading. +If the output is ``Python 3.Y.X`, where ``X`` is any number and ``Y`` is 11, 12, 13 or 14, then **you have a version of Python compatible with MUSE and you can skip this section altogether**. Move to `Installing pipx`_. In any other case, keep reading. There are multiple ways of installing Python, as well as multiple distributions. Here we have opted for the one that we believe is simplest, requires the smallest downloads and gives the maximum flexibility: using ``pyenv``. diff --git a/docs/installation/virtual-env-based.rst b/docs/installation/virtual-env-based.rst index 0df6c630d..c42f04897 100644 --- a/docs/installation/virtual-env-based.rst +++ b/docs/installation/virtual-env-based.rst @@ -95,7 +95,7 @@ Creating a virtual environment with ``pyenv + venv`` Alternatively to creating virtual environments in ``conda``, you can also make use of two well-tested and maintained libraries. We met the first one, ``pyenv``, already in the :ref:`pipx-based ` under the section :ref:`Installing pyenv ` and the installation procedure is exactly the same. -If you go down that route, please follow the steps outlined there and chose a recent version ``Python``, say 3.13. +If you go down that route, please follow the steps outlined there and choose a recent version ``Python``, say 3.13. The second package we need to create virtual environments for any specific ``Python`` version is called ``venv``, and it ships with ``Python`` by default. To create such an environment, we first need to ensure that the diff --git a/pyproject.toml b/pyproject.toml index 5054f949e..9dae37fc3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,8 +27,8 @@ dependencies = [ "numpy>=2.4,<3.0", "scipy>=1.17,<2.0", "pandas>=3.0,<4.0", - "xarray>=2026.2", - "bottleneck>=1.4", + "xarray>=2026.2,<2027.0", + "bottleneck>=1.4,<2.0", "coloredlogs", "toml", "xlrd", From 916f5a8cca014d74976197d2132371c88ebd80e6 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Fri, 8 May 2026 13:06:03 +0100 Subject: [PATCH 10/12] Update type annotation style --- src/muse/__init__.py | 2 -- src/muse/__main__.py | 2 -- src/muse/agents/agent.py | 2 -- src/muse/agents/factories.py | 2 -- src/muse/carbon_budget.py | 2 -- src/muse/commodities.py | 2 -- src/muse/constraints.py | 2 -- src/muse/costs.py | 2 -- src/muse/decisions.py | 2 -- src/muse/demand_matching.py | 2 -- src/muse/demand_share.py | 2 -- src/muse/dispatch.py | 2 -- src/muse/examples.py | 2 -- src/muse/filters.py | 2 -- src/muse/hooks.py | 2 -- src/muse/interactions.py | 2 -- src/muse/investments.py | 2 -- src/muse/lp_adapter.py | 5 ++--- src/muse/mca.py | 7 +++---- src/muse/objectives.py | 2 -- src/muse/outputs/cache.py | 2 -- src/muse/outputs/mca.py | 2 -- src/muse/outputs/sector.py | 2 -- src/muse/outputs/sinks.py | 2 -- src/muse/quantities.py | 2 -- src/muse/readers/csv.py | 2 -- src/muse/readers/toml.py | 2 -- src/muse/registration.py | 2 -- src/muse/regressions.py | 8 +++----- src/muse/sectors/abstract.py | 6 ++---- src/muse/sectors/preset_sector.py | 6 ++---- src/muse/sectors/register.py | 2 -- src/muse/sectors/sector.py | 5 ++--- src/muse/sectors/subsector.py | 5 ++--- src/muse/timeslices.py | 2 -- src/muse/utilities.py | 2 -- src/muse/wizard.py | 2 -- tests/test_csv_readers.py | 7 +++---- 38 files changed, 19 insertions(+), 90 deletions(-) diff --git a/src/muse/__init__.py b/src/muse/__init__.py index cac496a06..aa1e955c1 100644 --- a/src/muse/__init__.py +++ b/src/muse/__init__.py @@ -1,7 +1,5 @@ """MUSE model.""" -from __future__ import annotations - import os from contextlib import suppress from importlib.metadata import PackageNotFoundError, version diff --git a/src/muse/__main__.py b/src/muse/__main__.py index 8c033059d..391b30226 100644 --- a/src/muse/__main__.py +++ b/src/muse/__main__.py @@ -1,7 +1,5 @@ """Makes MUSE executable.""" -from __future__ import annotations - import pathlib from argparse import ArgumentParser diff --git a/src/muse/agents/agent.py b/src/muse/agents/agent.py index 9b3ac775f..92d57757d 100644 --- a/src/muse/agents/agent.py +++ b/src/muse/agents/agent.py @@ -1,7 +1,5 @@ """Holds all building agents.""" -from __future__ import annotations - from abc import ABC, abstractmethod from typing import Callable diff --git a/src/muse/agents/factories.py b/src/muse/agents/factories.py index c87949ea8..e4c8cd07b 100644 --- a/src/muse/agents/factories.py +++ b/src/muse/agents/factories.py @@ -1,7 +1,5 @@ """Holds all building agents.""" -from __future__ import annotations - from collections.abc import Mapping, Sequence from pathlib import Path from typing import Any, Callable diff --git a/src/muse/carbon_budget.py b/src/muse/carbon_budget.py index 37105dbe6..d9bb09f76 100644 --- a/src/muse/carbon_budget.py +++ b/src/muse/carbon_budget.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from collections.abc import MutableMapping, Sequence from logging import getLogger from typing import Callable diff --git a/src/muse/commodities.py b/src/muse/commodities.py index 517fd81e6..e2664a9b8 100644 --- a/src/muse/commodities.py +++ b/src/muse/commodities.py @@ -1,7 +1,5 @@ """Methods and types around commodities.""" -from __future__ import annotations - from collections.abc import Sequence from enum import IntFlag, auto from pathlib import Path diff --git a/src/muse/constraints.py b/src/muse/constraints.py index ff12a1150..f49136735 100644 --- a/src/muse/constraints.py +++ b/src/muse/constraints.py @@ -91,8 +91,6 @@ def constraints( Any other parameter. """ -from __future__ import annotations - from collections.abc import Mapping, MutableMapping, Sequence from enum import Enum, auto from typing import ( diff --git a/src/muse/costs.py b/src/muse/costs.py index 75e500d3f..fbeac9b1e 100644 --- a/src/muse/costs.py +++ b/src/muse/costs.py @@ -45,8 +45,6 @@ """ -from __future__ import annotations - from functools import wraps import numpy as np diff --git a/src/muse/decisions.py b/src/muse/decisions.py index 58923863f..f3aae64c4 100644 --- a/src/muse/decisions.py +++ b/src/muse/decisions.py @@ -26,8 +26,6 @@ def weighted_sum(objectives: Dataset, parameters: Any, **kwargs) -> DataArray: A data array with ranked replacement technologies. """ -from __future__ import annotations - __all__ = [ "epsilon_constraints", "factory", diff --git a/src/muse/demand_matching.py b/src/muse/demand_matching.py index 80c35fb77..ca520ee76 100644 --- a/src/muse/demand_matching.py +++ b/src/muse/demand_matching.py @@ -43,8 +43,6 @@ solve these constrained problems one way or another. """ -from __future__ import annotations - __all__ = ["demand_matching"] diff --git a/src/muse/demand_share.py b/src/muse/demand_share.py index 845f5a775..8c82396e4 100644 --- a/src/muse/demand_share.py +++ b/src/muse/demand_share.py @@ -35,8 +35,6 @@ def demand_share( A DataArray of demand shares. """ -from __future__ import annotations - from collections.abc import Hashable, MutableMapping, Sequence from functools import wraps from logging import getLogger diff --git a/src/muse/dispatch.py b/src/muse/dispatch.py index e2bcbe821..d01571985 100644 --- a/src/muse/dispatch.py +++ b/src/muse/dispatch.py @@ -30,8 +30,6 @@ def production( A `xr.DataArray` with the amount produced for each good from each asset. """ -from __future__ import annotations - __all__ = [ "PRODUCTION_SIGNATURE", "dispatch_by_merit_order", diff --git a/src/muse/examples.py b/src/muse/examples.py index 89b0a787a..055cc190e 100644 --- a/src/muse/examples.py +++ b/src/muse/examples.py @@ -25,8 +25,6 @@ model.run() """ -from __future__ import annotations - from logging import getLogger from os import mkdir from pathlib import Path diff --git a/src/muse/filters.py b/src/muse/filters.py index fc690e29d..993130d88 100644 --- a/src/muse/filters.py +++ b/src/muse/filters.py @@ -68,8 +68,6 @@ def search_space_initializer( An initial search space """ -from __future__ import annotations - __all__ = [ "compress", "currently_existing_tech", diff --git a/src/muse/hooks.py b/src/muse/hooks.py index 64575da4d..74d7a8269 100644 --- a/src/muse/hooks.py +++ b/src/muse/hooks.py @@ -1,7 +1,5 @@ """Pre and post hooks on agents.""" -from __future__ import annotations - __all__ = [ "asset_merge_factory", "clean", diff --git a/src/muse/interactions.py b/src/muse/interactions.py index 1f37ecf52..50a191248 100644 --- a/src/muse/interactions.py +++ b/src/muse/interactions.py @@ -14,8 +14,6 @@ arguments and returns nothing. It is expected to modify the agents in-place. """ -from __future__ import annotations - __all__ = [ "factory", "new_to_retro_net", diff --git a/src/muse/investments.py b/src/muse/investments.py index c5e5a361b..4d29d10fb 100644 --- a/src/muse/investments.py +++ b/src/muse/investments.py @@ -39,8 +39,6 @@ def investment( of newly invested capacity. """ -from __future__ import annotations - __all__ = [ "INVESTMENT_SIGNATURE", "adhoc_match_demand", diff --git a/src/muse/lp_adapter.py b/src/muse/lp_adapter.py index 5430f3689..ec2292c92 100644 --- a/src/muse/lp_adapter.py +++ b/src/muse/lp_adapter.py @@ -4,9 +4,8 @@ from the format required by scipy's linear programming solver. """ -from __future__ import annotations - from dataclasses import dataclass +from typing import Self import numpy as np import pandas as pd @@ -160,7 +159,7 @@ def from_muse_data( constraints: list, commodities: list[str], timeslice_level: str | None = None, - ) -> ScipyAdapter: + ) -> Self: """Creates a ScipyAdapter from MUSE data structures.""" # Calculate costs for the linear problem lpcosts = lp_costs(capacity_costs, commodities, timeslice_level) diff --git a/src/muse/mca.py b/src/muse/mca.py index 7627d23d4..3d2a29f94 100644 --- a/src/muse/mca.py +++ b/src/muse/mca.py @@ -1,11 +1,10 @@ -from __future__ import annotations - from collections.abc import Mapping, Sequence from pathlib import Path from typing import ( Any, Callable, NamedTuple, + Self, cast, ) @@ -27,7 +26,7 @@ class MCA: """ @classmethod - def factory(cls, settings: str | Path | Mapping | Any) -> MCA: + def factory(cls, settings: str | Path | Mapping | Any) -> Self: """Loads MCA from input settings and input files. Arguments: @@ -195,7 +194,7 @@ def __init__( def find_equilibrium( self, market: Dataset, - ) -> FindEquilibriumResults: + ) -> "FindEquilibriumResults": """Specialised version of the find_equilibrium function. Arguments: diff --git a/src/muse/objectives.py b/src/muse/objectives.py index 94a8c9db8..4040fe8f2 100644 --- a/src/muse/objectives.py +++ b/src/muse/objectives.py @@ -46,8 +46,6 @@ def comfort( A `timeslice` dimension may also be present. """ -from __future__ import annotations - __all__ = [ "capacity_to_service_demand", "capital_costs", diff --git a/src/muse/outputs/cache.py b/src/muse/outputs/cache.py index eb0b8efe1..47f2b1d2e 100644 --- a/src/muse/outputs/cache.py +++ b/src/muse/outputs/cache.py @@ -25,8 +25,6 @@ :py:func:`muse.outputs.cache.register_cached_quantity`. """ -from __future__ import annotations - from collections import ChainMap from collections.abc import Mapping, MutableMapping, Sequence from functools import reduce diff --git a/src/muse/outputs/mca.py b/src/muse/outputs/mca.py index a3703c432..1231785cb 100644 --- a/src/muse/outputs/mca.py +++ b/src/muse/outputs/mca.py @@ -18,8 +18,6 @@ def quantity( or an xarray xr.DataArray. """ -from __future__ import annotations - from collections.abc import Mapping, MutableMapping from operator import attrgetter from pathlib import Path diff --git a/src/muse/outputs/sector.py b/src/muse/outputs/sector.py index 9e5a824d9..e6c1a1288 100644 --- a/src/muse/outputs/sector.py +++ b/src/muse/outputs/sector.py @@ -19,8 +19,6 @@ def quantity( The function should never modify it's arguments. """ -from __future__ import annotations - from collections.abc import Mapping, MutableMapping from typing import Any, Callable, Union diff --git a/src/muse/outputs/sinks.py b/src/muse/outputs/sinks.py index 046c428e9..4be833801 100644 --- a/src/muse/outputs/sinks.py +++ b/src/muse/outputs/sinks.py @@ -17,8 +17,6 @@ def to_netcfd(quantity: DataArray, config: Mapping) -> Optional[Text]: pass """ -from __future__ import annotations - from collections.abc import Mapping, MutableMapping, Sequence from typing import ( Any, diff --git a/src/muse/quantities.py b/src/muse/quantities.py index 4b8ee3230..670ebccf9 100644 --- a/src/muse/quantities.py +++ b/src/muse/quantities.py @@ -7,8 +7,6 @@ Functions for calculating costs (e.g. LCOE, EAC) are in the `costs` module. """ -from __future__ import annotations - import numpy as np import xarray as xr diff --git a/src/muse/readers/csv.py b/src/muse/readers/csv.py index 6a3b3f26f..a98d92702 100644 --- a/src/muse/readers/csv.py +++ b/src/muse/readers/csv.py @@ -30,8 +30,6 @@ """ -from __future__ import annotations - __all__ = [ "read_agent_parameters", "read_attribute_table", diff --git a/src/muse/readers/toml.py b/src/muse/readers/toml.py index 73951a621..f5ef21bc9 100644 --- a/src/muse/readers/toml.py +++ b/src/muse/readers/toml.py @@ -1,7 +1,5 @@ """Ensemble of functions to read MUSE data.""" -from __future__ import annotations - __all__ = ["read_settings"] import importlib.util as implib diff --git a/src/muse/registration.py b/src/muse/registration.py index 15f5f6d34..2ed5e7bbe 100644 --- a/src/muse/registration.py +++ b/src/muse/registration.py @@ -1,7 +1,5 @@ """Registrators that allow pluggable data to logic transforms.""" -from __future__ import annotations - __all__ = ["registrator"] from collections.abc import MutableMapping, Sequence diff --git a/src/muse/regressions.py b/src/muse/regressions.py index ee531d569..cad1800fd 100644 --- a/src/muse/regressions.py +++ b/src/muse/regressions.py @@ -1,11 +1,9 @@ """Functions and functors to compute macro-drivers.""" -from __future__ import annotations - from abc import abstractmethod from collections.abc import Mapping, Sequence from pathlib import Path -from typing import Callable, ClassVar +from typing import Callable, ClassVar, Self from xarray import DataArray, Dataset @@ -93,7 +91,7 @@ def __call__( ) -> DataArray: pass - def sel(self, **filters) -> Regression: + def sel(self, **filters) -> Self: """Regression over part of the data only.""" return self.__class__( interpolation=self.interpolation, @@ -123,7 +121,7 @@ def factory( interpolation: str = "linear", base_year: int = 2010, **filters, - ) -> Regression: + ) -> Self: """Creates a regression function from standard muse input.""" from muse.readers import read_regression_parameters diff --git a/src/muse/sectors/abstract.py b/src/muse/sectors/abstract.py index 4fe560531..1c577b873 100644 --- a/src/muse/sectors/abstract.py +++ b/src/muse/sectors/abstract.py @@ -1,7 +1,5 @@ -from __future__ import annotations - from abc import ABC, abstractmethod -from typing import Any +from typing import Any, Self from xarray import Dataset @@ -22,7 +20,7 @@ class AbstractSector(ABC): @classmethod @abstractmethod - def factory(cls, name: str, settings: Any) -> AbstractSector: + def factory(cls, name: str, settings: Any) -> Self: """Creates class from settings named-tuple.""" pass diff --git a/src/muse/sectors/preset_sector.py b/src/muse/sectors/preset_sector.py index 844ee46ef..c5ff1c12b 100644 --- a/src/muse/sectors/preset_sector.py +++ b/src/muse/sectors/preset_sector.py @@ -1,8 +1,6 @@ """Sector with preset behaviours.""" -from __future__ import annotations - -from typing import Any +from typing import Any, Self from xarray import DataArray, Dataset @@ -14,7 +12,7 @@ class PresetSector(AbstractSector): # type: ignore """Sector with outcomes fixed from the start.""" @classmethod - def factory(cls, name: str, settings: Any) -> PresetSector: + def factory(cls, name: str, settings: Any) -> Self: """Constructs a PresetSectors from input data.""" from muse.readers.toml import read_presets_sector diff --git a/src/muse/sectors/register.py b/src/muse/sectors/register.py index ae48c62d9..212b89d6e 100644 --- a/src/muse/sectors/register.py +++ b/src/muse/sectors/register.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from collections.abc import Mapping, Sequence from typing import Callable diff --git a/src/muse/sectors/sector.py b/src/muse/sectors/sector.py index bf5b4b226..292be78ee 100644 --- a/src/muse/sectors/sector.py +++ b/src/muse/sectors/sector.py @@ -1,9 +1,8 @@ -from __future__ import annotations - from collections.abc import Iterator, Sequence from typing import ( Any, Callable, + Self, cast, ) @@ -23,7 +22,7 @@ class Sector(AbstractSector): # type: ignore """Base class for all sectors.""" @classmethod - def factory(cls, name: str, settings: Any) -> Sector: + def factory(cls, name: str, settings: Any) -> Self: from muse.dispatch import factory as pfactory from muse.interactions import factory as interaction_factory from muse.outputs.sector import factory as ofactory diff --git a/src/muse/sectors/subsector.py b/src/muse/sectors/subsector.py index 2b157d17d..ee668b8df 100644 --- a/src/muse/sectors/subsector.py +++ b/src/muse/sectors/subsector.py @@ -1,9 +1,8 @@ -from __future__ import annotations - from collections.abc import Sequence from typing import ( Any, Callable, + Self, ) import numpy as np @@ -120,7 +119,7 @@ def factory( current_year: int | None = None, name: str = "subsector", timeslice_level: str | None = None, - ) -> Subsector: + ) -> Self: from muse import constraints as cs from muse import demand_share as ds from muse import investments as iv diff --git a/src/muse/timeslices.py b/src/muse/timeslices.py index 9215340ae..3e3d6af72 100644 --- a/src/muse/timeslices.py +++ b/src/muse/timeslices.py @@ -1,7 +1,5 @@ """Timeslice utility functions.""" -from __future__ import annotations - __all__ = [ "broadcast_timeslice", "compress_timeslice", diff --git a/src/muse/utilities.py b/src/muse/utilities.py index 998d1e332..49bc41d0a 100644 --- a/src/muse/utilities.py +++ b/src/muse/utilities.py @@ -1,7 +1,5 @@ """Collection of functions and stand-alone algorithms.""" -from __future__ import annotations - from collections.abc import Hashable, Iterable, Iterator, Mapping, Sequence from typing import ( Any, diff --git a/src/muse/wizard.py b/src/muse/wizard.py index 70d8186cd..271f66003 100644 --- a/src/muse/wizard.py +++ b/src/muse/wizard.py @@ -7,8 +7,6 @@ not necessarily work with models that deviate from this structure. """ -from __future__ import annotations - from itertools import chain from pathlib import Path from typing import Callable diff --git a/tests/test_csv_readers.py b/tests/test_csv_readers.py index 40bf3187e..2ac7ef823 100644 --- a/tests/test_csv_readers.py +++ b/tests/test_csv_readers.py @@ -1,6 +1,5 @@ -from __future__ import annotations - from dataclasses import dataclass +from typing import Self import numpy as np import xarray as xr @@ -51,7 +50,7 @@ class DataArraySchema: name: str @classmethod - def from_da(cls, data: xr.DataArray) -> DataArraySchema: + def from_da(cls, data: xr.DataArray) -> Self: """Generate a DataArraySchema from an existing DataArray.""" return cls( dims=set(data.dims), @@ -79,7 +78,7 @@ class DatasetSchema: data_vars: dict[str, str] # var_name -> dtype @classmethod - def from_ds(cls, data: xr.Dataset) -> DatasetSchema: + def from_ds(cls, data: xr.Dataset) -> Self: """Generate a DatasetSchema from an existing Dataset.""" return cls( dims=set(data.dims), From 33559005a48b2070e13103d64c7d8bcb3c8bccc1 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Fri, 8 May 2026 13:22:49 +0100 Subject: [PATCH 11/12] Fix type annotation --- src/muse/utilities.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/muse/utilities.py b/src/muse/utilities.py index 49bc41d0a..8056e3342 100644 --- a/src/muse/utilities.py +++ b/src/muse/utilities.py @@ -1,12 +1,7 @@ """Collection of functions and stand-alone algorithms.""" from collections.abc import Hashable, Iterable, Iterator, Mapping, Sequence -from typing import ( - Any, - Callable, - NamedTuple, - cast, -) +from typing import Any, Callable, cast import numpy as np import xarray as xr @@ -432,7 +427,7 @@ def interpolate_capacity( ) -def nametuple_to_dict(nametup: Mapping | NamedTuple) -> Mapping: +def nametuple_to_dict(nametup: Mapping | tuple) -> Mapping: """Transforms a nametuple of type GenericDict into an OrderDict.""" from collections import OrderedDict from dataclasses import asdict, is_dataclass From 8a4141a5b7b1a1150d026aa85431539844a19241 Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Fri, 8 May 2026 13:46:49 +0100 Subject: [PATCH 12/12] Add backtick --- docs/installation/pipx-based.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation/pipx-based.rst b/docs/installation/pipx-based.rst index 96fdf6aa3..f60dc245b 100644 --- a/docs/installation/pipx-based.rst +++ b/docs/installation/pipx-based.rst @@ -92,7 +92,7 @@ The first thing will be to check if you already have a suitable python version i python --version -If the output is ``Python 3.Y.X`, where ``X`` is any number and ``Y`` is 11, 12, 13 or 14, then **you have a version of Python compatible with MUSE and you can skip this section altogether**. Move to `Installing pipx`_. In any other case, keep reading. +If the output is ``Python 3.Y.X``, where ``X`` is any number and ``Y`` is 11, 12, 13 or 14, then **you have a version of Python compatible with MUSE and you can skip this section altogether**. Move to `Installing pipx`_. In any other case, keep reading. There are multiple ways of installing Python, as well as multiple distributions. Here we have opted for the one that we believe is simplest, requires the smallest downloads and gives the maximum flexibility: using ``pyenv``.