Skip to content

replace build_cop_profiles and build_heat_source_utilisation_profiles…#2184

Draft
amos-schledorn wants to merge 1 commit into
refactor-ptes-boostingfrom
numeric-delta-t-computation
Draft

replace build_cop_profiles and build_heat_source_utilisation_profiles…#2184
amos-schledorn wants to merge 1 commit into
refactor-ptes-boostingfrom
numeric-delta-t-computation

Conversation

@amos-schledorn
Copy link
Copy Markdown
Contributor

@amos-schledorn amos-schledorn commented Jun 1, 2026

Consistent heat-pump cooling for COP and heat-source utilisation profiles

For preheating heat sources (PTES and geothermal) the temperature drop the heat pump imposes on its source is not a free parameter — the heat pump's energy balance fixes it — yet that same drop feeds back into the COP correlation that determines the balance. This PR replaces the previous constant-cooling assumption with an iterative solve that finds the cooling and the COP consistently, and merges the former build_cop_profiles and build_heat_source_utilisation_profiles rules into a single build_heat_source_profiles rule that produces both.

Background

District-heating COPs come from the Jensen et al. (2018) correlation in CentralHeatingCopApproximator. Among its inputs are the source inlet and source outlet temperatures — that is, how far the heat pump cools the stream it draws heat from. The gap between them, call it ΔT_cool, was set by a single config number, heat_source_cooling (6 K), for every source and every hour.

That is fine for an ambient source like air, where the evaporator ΔT is essentially a design choice. It is not fine for a source that is first used to preheat the return flow and then lifted to the network forward temperature. Under the plant layout we assume for those sources — a single storage stream, a preheater followed by the evaporator, equal mass flows on both sides — the heat pump's energy balance ties the source-side cooling directly to the lift and the COP:

ΔT_cool = (COP - 1) / COP * (T_forward - T_source)

because the heat pulled from the source is the delivered heat minus the electrical work, so Q_source / Q_sink = (COP - 1) / COP. The catch is that the COP on the right-hand side is itself a function of ΔT_cool: the cooling sets the source outlet temperature that goes into the correlation. So ΔT_cool and the COP are bound by one physical relation, and you cannot pin ΔT_cool to a constant and read the COP off the correlation at that constant and expect the two to agree.

The current implemementation does exactly that. The reported COP and the cooling implied by its own energy balance are therefore inconsistent — the cooling is over-specified, once by the config constant and once (implicitly) by the heat pump's energy balance.

The same ΔT_cool also appears in the preheater utilisation profile, in the denominator that splits source heat between direct preheating and the heat-pump cold side. Whatever value we settle on has to be used in both the COP and the utilisation profile, which is the second reason for merging the build_cop_profiles and build_heat_source_utilisation_profiles rules.

Code changes

build_heat_source_profiles produces the three profiles the sector network already consumes:

  • cop_profiles
  • heat_source_direct_utilisation_profiles
  • heat_source_preheater_utilisation_profiles

For each (heat system, source, node, snapshot) it derives the source and sink inlet temperatures from the existing logic and then decides the cooling:

  • Preheating central sources (PTES, geothermal): the new function compute_heat_pump_cooling starts from the previous heat_source_cooling, evaluates the COP resulting from that initial value, recomputes the cooling from (COP - 1) / COP * (T_forward - T_source), and repeats until the cooling stops changing.
  • All other heat sources (currently) - the toggle off, non-preheating central sources such as air, and all decentral systems - keeps the flat heat_source_cooling value, exactly as before.

The solved cooling is then reused for both the COP profile and the preheater-utilisation profile.

build_cop_profiles and build_heat_source_utilisation_profiles always run on the same inputs (forward/return temperatures and the source temperature profiles), and with this change both depend on the same solved cooling. Keeping them apart would mean either computing heat pump cooling and COP profiles twice or shuttling them between rules.

This feature merges them and keeps the existing output file names, so nothing downstream needs to change: prepare_sector_network, solve_myopic/solve_perfect and plot_cop_profiles still resolve cop_profiles and the two utilisation profiles by path. The only workflow change is that heat_sources is now passed as the full {system: [sources]} dict (as build_cop_profiles had it) instead of just the urban-central list that the old utilisation rule used.

New config settings

All live under sector: district_heating:.

  • heat_source_cooling (existing, default 6): unchanged for non-preheating sources. It now also serves as the starting guess for the iterative solve.
  • heat_pump_cooling_iterative (new, bool, default true): turns the iterative solve on. Set it to false to fall back to flat cooling for all sources. That path reproduces pre-this-feature results exactly.
  • log_heat_pump_cooling_iterations (new, bool, default false): a debugging aid. When enabled, the full per-node, per-timestep, per-iteration trace (forward and source temperature, cooling, COP) is written to heat_pump_cooling_iterations_<...>.csv alongside cop_profiles. Might be removed in final version.

The two new settings are registered in the config-validation model (_DistrictHeatingConfig in scripts/lib/validation/config/sector.py) and in config/config.default.yaml.

File changes

New

  • scripts/build_heat_source_profiles/run.py - the merged rule and compute_heat_pump_cooling.
  • scripts/build_heat_source_profiles/{base,central_heating,decentral_heating}_cop_approximator.py - the COP approximators moved into the package so it is self-contained. They are identical to the old ones.

Removed

  • scripts/build_cop_profiles/ and scripts/build_heat_source_utilisation_profiles.py.

Changed

  • rules/build_sector.smk - the two rules replaced by build_heat_source_profiles, which passes the full heat_sources dict and the two new params.
  • config/config.default.yaml and scripts/lib/validation/config/sector.py - the two new settings.
  • doc/sector.rst - the autodoc entry renamed from build_cop_profiles to build_heat_source_profiles.
  • scripts/definitions/heat_source.py - the See Also docstring now points at the new rule.
  • scripts/build_central_heating_temperature_profiles/run.py - fixed a stray mock_snakemake("build_cop_profiles") left over from a copy-paste; it now names its own rule (and would otherwise have referenced the deleted rule).

Checklist

Required:

  • Changes are tested locally and behave as expected.
  • Code and workflow changes are documented.
  • A release note entry is added to doc/release_notes.rst.

If applicable:

  • Changes in configuration options are reflected in scripts/lib/validation.
  • For new data sources or versions, these instructions have been followed.
  • New rules are documented in the appropriate doc/*.rst files.

… with a single rule and compute heat_pump_coolling for COP approximation iteratively when enabled
@amos-schledorn amos-schledorn marked this pull request as draft June 1, 2026 16:14
@amos-schledorn amos-schledorn requested a review from cpschau June 1, 2026 16:14
Copy link
Copy Markdown
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.

Looks great! New structure with all scripts consolidated in build_heat_source_profiles is way cleaner.

Comment on lines +489 to +496
# Write the full per-node, per-timestep Picard trace only when logging is on.
if cooling_iteration_log:
iterations_path = (
Path(snakemake.output.cop_profiles).parent
/ f"heat_pump_cooling_iterations_base_s_{snakemake.wildcards.clusters}_{snakemake.wildcards.planning_horizons}.csv"
)
pd.concat(cooling_iteration_log, ignore_index=True).to_csv(
iterations_path, index=False
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Would be the first log-type resource afaik. Maybe the normal log of the snakemake rule suffices.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just realized that the additional log resource is a config option, so nvm.

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.

2 participants