Skip to content

Split demand between subsectors#685

Merged
tsmbland merged 35 commits intomainfrom
subsector_demand
Apr 3, 2025
Merged

Split demand between subsectors#685
tsmbland merged 35 commits intomainfrom
subsector_demand

Conversation

@tsmbland
Copy link
Copy Markdown
Collaborator

@tsmbland tsmbland commented Mar 24, 2025

Description

MUSE has a subsectors feature, which has been largely neglected up until now (most sectors have a single "all" subsector), but come into focus recently as researchers want to split up large sectors into multiple subsectors serving distinct commodity demands (e.g. a residential sector that serves both heating and lighting demand using different technologies). There are two reasons why this is useful:

  • Since agents are defined at the subsector level, this allows agents to have different objectives for servicing different commodity demands (e.g. minimizing LCOE for servicing heat, but minimize running costs for servicing lighting)
  • Very large sectors cannot be solved by the scipy solver since the constraints matrix cannot fit into memory. Since the scipy solver performs optimization at the subsector level, if we can split up a sector into multiple subsectors we'll end up with multiple smaller problems, each of which can fit into memory on their own. In fact, the size of the constraints matrix is proportional to n_technologies x n_commodities squared, so splitting a sector into multiple subsectors, each with a reduced set of technologies and commodities can shrink the problem hugely.

However, things are currently not working. If you try to split up a sector into multiple subsectors you will end up with overinvestment by a factor equal to the number of subsectors, and it will take longer to run (again, by approximately a factor equal to the number of subsectors). Hmm, suspicious.

The reason is that each subsector is getting passed the full set of commodity demands and technologies for the whole sector, and performs a full optimization problem aiming to meet the full commodity demands of the sector with the full set of technologies. Repeating this for each subsector, then adding subsector investments together, you get overinvestment (and a long running time).

We need to split the commodity demands and technologies between the subsectors, so that then each subsector solves a reduced problem.

Main changes to the code:

  • Step 1: pass subsectors only the demands for the commodities they are designed to service (see subsectors.py). This is based on self.commodities, which was already in place but not being used. There are already checks in place to ensure that subsectors have non-overlapping commodities.
  • Step 2: filter the technologies dataset according to the search space (see agent.py). As of Limit search space to relevant technologies #687, the search space will only contain relevant technologies that can actually service the demands selected for above, so we can shrink the technologies dataset based on this. (I don't actually think this is necessary as the search space will already filter irrelevant technologies out of the optimization problem, but still worth doing for safety)
  • Step 3: prepare the decision variables. I needed to modify lp_costs to take a list of commodities as an argument so it creates decision variables only for these commodities. This function was over-complicated anyway so I've simplified it

And voila. Each subsector now solves a reduced problem to service a subset of commodities in the sector. Since subsector commodities are non-overlapping (and this is enforced), we shouldn't get any doubling up of investment.

If any of this is unclear (probably), I've created a tutorial in #690 which is based off this branch. This shows that things are working nicely as expected.

Other changes:

  • Added a lot of inline comments and docstrings to the constraints module as I went through trying to understand what each function was doing. There are a lot of diffs, but the only function with major changes to the code is lp_costs (and a couple of the constraints, see next point)
  • Some of the constraints were filtering demand and technologies, but since this filtering is now done outside of the constraints I've removed this
  • Controversial, but I've removed some doctests in the constraints module (ScipyAdapter and lp_constraint_matrix). I banged my head against a wall for ages trying to get these to work and eventually gave up and decided I have better things to do with my time. I didn't find them particularly helpful or well written anyway, and, quite frankly, if they can start failing despite me doing nothing major to the functions in question, then they're not good tests anyway. Hopefully the addition of inline comments to the code makes up for this
  • test_run_retro_agent started failing. I don't know why, but I'm not going to invest time into fixing it as retrofit agents are on their way out (planning to add a deprecation warning in the next release). The default_retro model still works though, so don't think it's a major breakage
  • Deleted a failing test in test_trade. Similar to above, the trade feature is a massive headache and I'm not going to waste time trying to fix it, especially since the trade results are currently nonsense anyway. The trade model still runs, however.
  • Some changes to test_constraints, mostly to tidy up and get everything passing again (like is often the case, fixing the failing tests took 10x longer that fixing the bug itself!)
  • _unified_dataset took technologies as an argument, but this wasn't used so I've removed it

Fixes #684
Provides a solution to #389, since the sector in question can be split up into multiple subsectors. However, I'll leave that issue open for now as a reminder that MUSE is still incredibly wasteful with memory.

Type of change

  • New feature (non-breaking change which adds functionality)
  • Optimization (non-breaking, back-end change that speeds up the code)
  • Bug fix (non-breaking change which fixes an issue)
  • Breaking change (whatever its nature)

Key checklist

  • All tests pass: $ python -m pytest
  • The documentation builds and looks OK: $ python -m sphinx -b html docs docs/build

Further checks

  • Code is commented, particularly in hard-to-understand areas
  • Tests added that prove fix is effective or that feature works

@tsmbland tsmbland changed the base branch from main to refactor_filters March 25, 2025 16:33
Base automatically changed from refactor_filters to main March 26, 2025 10:33
@tsmbland tsmbland force-pushed the subsector_demand branch 2 times, most recently from 96f5c9d to db42c16 Compare March 26, 2025 10:43
@tsmbland tsmbland changed the base branch from main to search_space March 26, 2025 19:31
Base automatically changed from search_space to main March 27, 2025 13:48
@tsmbland tsmbland requested a review from Copilot March 28, 2025 13:59
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR splits demand across subsectors and updates the associated tests, constraints, and investments functions to handle commodities explicitly. Key changes include:

  • Removal of the obsolete lp_costs test and update of test fixtures to include a 'commodities' parameter.
  • Adjustments in constraint and cost formulations to separate capacity and production decision variables.
  • Modifications in the subsector and agent modules to filter and pass commodity‐specific data.

Reviewed Changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
tests/test_trade.py Removed the obsolete lp_costs test to streamline trade model testing.
tests/test_constraints.py Updated fixtures and assertions to include commodity-based constraints.
tests/test_agents.py Marked retrofit agent test as xfail and imported additional markers.
src/muse/sectors/subsector.py Modified demand splitting to select commodity-specific consumption data.
src/muse/investments.py Updated function calls by passing parameters as keywords for clarity.
src/muse/constraints.py Refactored LP cost calculations and constraint transformations with new renaming rules.
src/muse/agents/agent.py Filtered technologies based on the search space and passed commodities to constraints.
Comments suppressed due to low confidence (3)

src/muse/constraints.py:194

  • Directly asserting that 'constraint.b' is not scalar may cause unexpected failures if scalar constraints are valid. Consider explicitly checking the number of dimensions and handling the scalar case appropriately.
assert not constraint.b.dims == ()

src/muse/constraints.py:795

  • Ensure that 'pandas' is imported (e.g., 'import pandas as pd') since 'pd.Index' is used, to prevent a runtime error.
production_costs["timeslice"] = pd.Index(production_costs.get_index("timeslice"), tupleize_cols=False)

src/muse/constraints.py:1341

  • [nitpick] Using string slicing for dimension renaming (str(k)[2:-1]) may be fragile if dimension names change unexpectedly. Consider using a more explicit and robust renaming strategy.
return result.rename({k: str(k)[2:-1] for k in result.dims})

@tsmbland tsmbland marked this pull request as ready for review March 28, 2025 15:27
@tsmbland tsmbland requested a review from dalonsoa March 28, 2025 15:28
Copy link
Copy Markdown
Collaborator

@dalonsoa dalonsoa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a quite meaty PRs, so it is going to taka a while. I've managed to review just the constrains, for now, with some comments and suggestions.

Comment thread src/muse/constraints.py Outdated
Comment thread src/muse/constraints.py Outdated
Comment thread src/muse/constraints.py Outdated
Comment thread src/muse/constraints.py Outdated
Comment thread src/muse/constraints.py Outdated
Comment thread src/muse/investments.py
Comment thread src/muse/constraints.py
Comment on lines 987 to +992
def to_muse(x: np.ndarray) -> xr.Dataset:
return ScipyAdapter._back_to_muse(x, capacities.costs, productions.costs)
return ScipyAdapter._back_to_muse(
x,
capacity_template=capacities.costs,
production_template=productions.costs,
)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I always thought this was a very odd design pattern. Why not simply storing the templates in attributes of ScipyAdapter and making the relevant methods like back_to_muse not static? It will be way simpler to follow.

All in all, and considering the size of this file, I think it will make sense to extract this as in its own module and make most of these static methods just plain functions in there. They way they are used here feels like the wrong way of using static methods, specially when you are calling them within the class itself.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, it's odd. I think I'll leave it as it is for now and have a play around in another PR, otherwise this PR will become very difficult to review

Copy link
Copy Markdown
Collaborator

@dalonsoa dalonsoa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice. I think now things look very good and are easier to understand. Together with the tutorial, I think this is a great step forward.

@tsmbland tsmbland merged commit 2bb5c84 into main Apr 3, 2025
14 checks passed
@tsmbland tsmbland deleted the subsector_demand branch April 3, 2025 12:30
@github-project-automation github-project-automation Bot moved this to ✅ Done in MUSE Apr 3, 2025
@tsmbland tsmbland restored the subsector_demand branch April 3, 2025 12:31
@tsmbland tsmbland deleted the subsector_demand branch April 3, 2025 12:34
@tsmbland tsmbland mentioned this pull request Jun 10, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

Properly allocate demand to subsectors

3 participants