Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce PTES and TES constraints #1546

Open
wants to merge 54 commits into
base: master
Choose a base branch
from

Conversation

TomKae00
Copy link
Contributor

Changes proposed in this Pull Request

This pull request introduces the following adjustments to the prepare_sector_network and solve_network scripts, replacing PR #1444 (which did not work with the HiGHS solver):

  • Added PTES.
  • Introduced a constraint to enforce the energy-to-power ratio (etpr).
  • Implemented a constraint to ensure that the charger and discharger of the thermal energy stores are sized equally.

The following config data has been used:

scenario:
  clusters:
    - 6

planning_horizons:
  - 2030

foresight: overnight
countries: ['DE', 'DK']

clustering:
  temporal:
    resolution_sector: 12h

Constraints

The etpr ratio constraint and the constraint enforcing equal sizing of the charger and discharger are working as intended, as shown in the figure below. In particular, the size of the store divided by 150 equals the size of both the charger and the discharger.

charger_discharger_profile_comparison

The annual charging and discharging process for an exemplary PTES store is illustrated in the figure below:

charger_discharger_size_comparison

System Costs

System costs have increased slightly with the newly introduced constraints. This is logical given the larger capacities required when the charger and discharger can no longer be chosen independently.

total_cost_comparison

Checklist

  • [ x] I tested my contribution locally and it works as intended.
  • [ x] Code and workflow changes are sufficiently documented.
  • [ x] Changed dependencies are added to envs/environment.yaml.
  • [ x] Changes in configuration options are added in config/config.default.yaml.
  • [ x] Changes in configuration options are documented in doc/configtables/*.csv.
  • [ x] Sources of newly added data are documented in doc/data_sources.rst.
  • [ x] A release note doc/release_notes.rst is added.

Copy link
Contributor

@amos-schledorn amos-schledorn left a comment

Choose a reason for hiding this comment

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

Looks great! Just a few small comments. When the CI runs through, I'll test this locally.

@TomKae00
Copy link
Contributor Author

@amos-schledorn thanks for the review! I've implemented your suggestions. Moreover, i added error handling to ensure that the TES constraints are only applied when intended.

@TomKae00
Copy link
Contributor Author

Hi @fneum and @amos-schledorn,

Currently, the CI is failing when running:

snakemake make_summary_perfect --configfile config/test/config.perfect.yaml

Reason for CI Failure:
In the master, the water tank charger and discharger are included in costs.csv with an efficiency of 0.9. Because of this, they can be used as heat vents, allowing energy to be released from the system.

With this PR, the efficiency of both the central/decentral chargers and dischargers has been set to 1.0 according to the DEA catalogue. As a result, they can no longer act as heat vents, which prevents energy from being released. Additionally, chargers are now directly tied to the store size via the energy-to-power ratio, meaning they cannot be used independently.

Possible Solution:
Since energy can no longer be released through the chargers/dischargers, the system costs are now higher. A potential solution could be to allow heat vents to be built for all heat systems, as it is done for urban central heating systems in prepare_sector_network.py line 2437.

Is there a specific reason why heat vents are currently only allowed for urban central heating? Or are there any concerns against extending this option to all systems?

@fneum fneum requested a review from Copilot March 3, 2025 09:26
Copy link

@Copilot 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.

PR Overview

This PR implements new PTES and TES constraints to ensure a fixed energy-to-power ratio and equal sizing for TES charger/discharger pairs in the network, replacing the previous implementation incompatible with the HiGHS solver. Key changes include:

  • Updating the prepare_sector_network.py to dynamically reference cost parameters for water tank and water pit components.
  • Adding two new TES constraint functions (energy-to-power ratio and charger ratio) in solve_network.py.
  • Updating the configuration file to include new color mappings for water pits in visualizations.

Reviewed Changes

File Description
scripts/prepare_sector_network.py Updates to water tanks/pits component cost/efficiency and related linking.
scripts/solve_network.py New TES constraint functions added and integrated into network setup.
config/config.default.yaml New plotting color options added for water pits.

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

scripts/solve_network.py:886

  • The TES charger ratio constraint assumes a one-to-one ordering between charger and discharger indices. Ensure that these indices are explicitly paired to enforce that each TES unit’s charger and discharger are correctly matched.
lhs = ( n.model["Link-p_nom"].loc[indices_charger_p_nom_extendable] - n.model["Link-p_nom"].loc[indices_discharger_p_nom_extendable] * eff_discharger )

Copy link

@Copilot 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.

PR Overview

This pull request introduces PTES and TES constraints to the network optimization scripts by enforcing an energy‐to‐power ratio for thermal energy stores and equal sizing between the charger and discharger linked to TES units. Key changes include:

  • Adding new constraints and variables in prepare_sector_network.py to support water tanks and water pits.
  • Implementing TES energy-to-power ratio and TES charger ratio constraints in solve_network.py with linopy for expression merging.
  • Updating configuration settings in config.default.yaml to include cost and plotting parameters for water pits.

Reviewed Changes

File Description
scripts/prepare_sector_network.py Adds PTES-related constraints and extends the modeling of water tanks and water pits.
scripts/solve_network.py Introduces TES energy-to-power ratio and charger ratio constraints and a new linopy import.
config/config.default.yaml Updates cost and plotting settings to support new water pits parameters and associated components.

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

scripts/prepare_sector_network.py:2732

  • [nitpick] There is an inconsistency in how storage keys are referenced between water tanks and water pits. Consider constructing the key for water pit storage using heat_system.central_or_decentral to maintain consistency, if applicable.
lifetime=costs.at["central water pit storage", "lifetime"],

Comment on lines +868 to +869
if not tes.replace("-", " ").split(" ")[:5] == charger.split(" ")[:5]:
# e.g. "DE0 0 urban central water tanks charger" -> ["DE0", "0", "urban", "central", "water"]
Copy link
Preview

Copilot AI Mar 7, 2025

Choose a reason for hiding this comment

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

[nitpick] The string-based matching of charger and TES names using split and slice may be brittle. Consider refactoring this logic to use a more robust pairing mechanism (e.g. a helper function that parses and compares structured components) and explicitly sort the indices to ensure correct pairing.

Suggested change
if not tes.replace("-", " ").split(" ")[:5] == charger.split(" ")[:5]:
# e.g. "DE0 0 urban central water tanks charger" -> ["DE0", "0", "urban", "central", "water"]
if parse_name_components(tes) != parse_name_components(charger):
# e.g. "DE0 0 urban central water tanks charger" -> ("DE0", "0", "urban", "central", "water")

Copilot is powered by AI, so mistakes are possible. Review output carefully before use.

Positive Feedback
Negative Feedback

Provide additional feedback

Please help us improve GitHub Copilot by sharing more details about this comment.

Please select one or more of the options
Copy link
Contributor

Choose a reason for hiding this comment

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

Valid comment, but possible issues are addressed by throwing an error when respective string matching fails.

Comment on lines +922 to +924
for charger, discharger in zip(
indices_charger_p_nom_extendable, indices_discharger_p_nom_extendable
):
Copy link
Preview

Copilot AI Mar 7, 2025

Choose a reason for hiding this comment

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

[nitpick] The reliance on splitting strings to match charger and discharger links may lead to unexpected mismatches. It is advisable to standardize and explicitly sort both indices or use a dedicated matching function to ensure consistent pairing.

Suggested change
for charger, discharger in zip(
indices_charger_p_nom_extendable, indices_discharger_p_nom_extendable
):
def standardize_and_sort_indices(indices):
return sorted(indices, key=lambda x: " ".join(x.split(" ")[:5]))
sorted_charger_indices = standardize_and_sort_indices(indices_charger_p_nom_extendable)
sorted_discharger_indices = standardize_and_sort_indices(indices_discharger_p_nom_extendable)
for charger, discharger in zip(sorted_charger_indices, sorted_discharger_indices):

Copilot is powered by AI, so mistakes are possible. Review output carefully before use.

Positive Feedback
Negative Feedback

Provide additional feedback

Please help us improve GitHub Copilot by sharing more details about this comment.

Please select one or more of the options
Copy link
Contributor

Choose a reason for hiding this comment

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

Valid comment, but possible issues are addressed by throwing an error when respective string matching fails.

Copy link
Contributor

@cpschau cpschau left a comment

Choose a reason for hiding this comment

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

Hey @TomKae00,
only had a couple of questions, where I am not sure if they need to be addressed with new changes in the code. Did you discuss the addition of the "energy to power" column with @fneum?

)

n.links.loc[
Copy link
Contributor

Choose a reason for hiding this comment

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

Was not aware of the practice to store custom attributes in network files. Is this OK @fneum? Alternatively, I would suggest to read out energy-to-power ratio from costs file in solve_network.

lifetime=costs.at["central water pit storage", "lifetime"],
)

n.links.loc[
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here.

"No valid extendable charger links or stores found for TES energy to power constraints."
)

energy_to_power_ratio_values = n.links.loc[
Copy link
Contributor

Choose a reason for hiding this comment

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

Here the parameter would have to be accessed using the costs file as an additional data input to the rule.

)

eff_discharger = n.links.efficiency[indices_discharger_p_nom_extendable].values
lhs = (
Copy link
Contributor

Choose a reason for hiding this comment

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

Why only the discharger efficiency is considered here?

Comment on lines +1158 to +1159
n.links.index.str.lower()
.str.contains("pits charger|tanks charger", case=False, na=False)
Copy link
Contributor

Choose a reason for hiding this comment

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

Why lower() and contains(case=False) have to be used?

Comment on lines +1147 to +1150
n.buses.index.str.lower()
.str.contains(
r"urban central heat|urban decentral heat|rural heat",
case=False,
Copy link
Contributor

Choose a reason for hiding this comment

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

Why lower() and contains(case=False) have to be used?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants