From c84129de087e6c48398f62649654feed97e42cef Mon Sep 17 00:00:00 2001 From: Tom Bland Date: Thu, 26 Mar 2026 15:13:26 +0000 Subject: [PATCH 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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