-
Notifications
You must be signed in to change notification settings - Fork 418
Expand file tree
/
Copy pathbuild_heat_source_utilisation_profiles.py
More file actions
293 lines (249 loc) · 11.4 KB
/
build_heat_source_utilisation_profiles.py
File metadata and controls
293 lines (249 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# SPDX-FileCopyrightText: Contributors to PyPSA-Eur <https://github.com/pypsa/pypsa-eur>
#
# SPDX-License-Identifier: MIT
"""
Build heat source utilisation profiles for district heating networks.
This script calculates when and how much heat from various sources (geothermal,
PTES, river water, etc.) can be used, based on the temperature relationship
between the heat source and the district heating network.
Two utilisation modes are calculated:
1. **Direct utilisation**: When the source temperature meets or exceeds the
forward temperature (T_source ≥ T_forward), the heat source can directly
supply the district heating network. Profile value is 1.0 (full utilisation)
or 0.0 (not possible).
2. **Preheater utilisation**: When the source temperature is between the return
and forward temperatures (T_return < T_source < T_forward), the heat source
can preheat the return flow before a heat pump lifts it to forward temperature.
The profile value represents that share of the heat above the return temperature which is utilised to increase the heat pump's sink inflow temperature. The return flow serves as the source inlet.
These profiles are used by ``prepare_sector_network.py`` to configure heat
utilisation links that model cascading temperature use: direct supply when
possible, preheating when beneficial, with heat pumps handling the final lift.
Relevant Settings
-----------------
.. code:: yaml
sector:
heat_sources:
urban central:
- air
- geothermal
district_heating:
heat_source_cooling: 6 # K
geothermal:
constant_temperature_celsius: 65
Inputs
------
- ``resources/<run_name>/central_heating_forward_temperature_profiles_base_s_{clusters}_{planning_horizons}.nc``
Forward temperature profiles for district heating networks (°C).
- ``resources/<run_name>/central_heating_return_temperature_profiles_base_s_{clusters}_{planning_horizons}.nc``
Return temperature profiles for district heating networks (°C).
- Heat source temperature profiles (for variable-temperature sources like PTES, air, ground).
Outputs
-------
- ``resources/<run_name>/heat_source_direct_utilisation_profiles_base_s_{clusters}_{planning_horizons}.nc``
Direct utilisation profiles indexed by (time, name, heat_source).
Values: 1.0 when T_source ≥ T_forward, 0.0 otherwise.
- ``resources/<run_name>/heat_source_preheater_utilisation_profiles_base_s_{clusters}_{planning_horizons}.nc``
Preheater utilisation profiles indexed by (time, name, heat_source).
Values: heat extraction efficiency when T_return < T_source < T_forward, 0.0 otherwise.
"""
import logging
import xarray as xr
from scripts._helpers import configure_logging, set_scenario_config
from scripts.definitions.heat_source import HeatSource, HeatSourceType
logger = logging.getLogger(__name__)
def get_source_temperature(
snakemake_params: dict, snakemake_input: dict, heat_source_name: str
) -> float | xr.DataArray:
"""
Get the temperature profile or constant value for a heat source.
Parameters
----------
snakemake_params : dict
Snakemake parameters containing heat_source_temperatures dict for
constant-temperature sources.
snakemake_input : dict
Snakemake input files containing temperature profiles for variable sources.
heat_source_name : str
Name of the heat source (e.g., 'geothermal', 'ptes', 'air', 'electrolysis_waste').
Returns
-------
float | xr.DataArray
Either a constant temperature (float) for sources like geothermal,
or a DataArray with time-varying temperatures for sources like PTES or air.
Notes
-----
Presence of constant-temperature entries in ``heat_source_temperatures``
is validated at config load time by ``SectorConfig``.
"""
heat_source = HeatSource(heat_source_name)
if heat_source.temperature_from_config:
return snakemake_params["heat_source_temperatures"][heat_source_name]
elif heat_source.source_type == HeatSourceType.STORAGE:
# PTES layer temperatures are constants from the ptes_operations dataset
if heat_source_name.startswith("ptes layer"):
layer_idx = int(heat_source_name.split()[-1])
return float(ptes_ds["layer_temperatures"].sel(layer=layer_idx).item())
else:
return float(ptes_ds.attrs["top_temperature"])
else:
if f"temp_{heat_source_name}" not in snakemake_input.keys():
raise ValueError(
f"Missing input temperature for heat source {heat_source_name}."
)
return xr.open_dataarray(snakemake_input[f"temp_{heat_source_name}"])
def get_direct_utilisation_profile(
source_temperature: float | xr.DataArray, forward_temperature: xr.DataArray
) -> xr.DataArray | float:
"""
Calculate when a heat source can directly supply district heating.
Direct utilisation is possible when the source temperature meets or exceeds
the required forward temperature of the district heating network.
Parameters
----------
source_temperature : float | xr.DataArray
Heat source temperature in °C. If float, applies uniformly.
If DataArray, indexed by (time, name).
forward_temperature : xr.DataArray
District heating forward temperature profiles in °C,
indexed by (time, name).
Returns
-------
xr.DataArray
Binary profile: 1.0 where T_source ≥ T_forward (direct use possible),
0.0 otherwise.
"""
return xr.where(source_temperature >= forward_temperature, 1.0, 0.0)
def get_preheater_utilisation_profile(
source_temperature: float | xr.DataArray,
forward_temperature: xr.DataArray,
return_temperature: xr.DataArray,
heat_source_cooling: float,
) -> xr.DataArray | float:
"""
Calculate preheater utilisation efficiency for intermediate-temperature sources.
When a heat source temperature is between the return and forward temperatures,
it can preheat the return flow before a heat pump provides the final temperature
lift. This improves overall efficiency by reducing the heat pump's lift.
The efficiency represents the fraction of heat extracted from the source that
goes into preheating (vs. the additional cooling through the heat pump):
efficiency = (T_source - T_return) / (T_source - T_return + heat_source_cooling)
Parameters
----------
source_temperature : float | xr.DataArray
Heat source temperature in °C. If float, applies uniformly.
If DataArray, indexed by (time, name).
forward_temperature : xr.DataArray
District heating forward temperature profiles in °C,
indexed by (time, name).
return_temperature : xr.DataArray
District heating return temperature profiles in °C,
indexed by (time, name).
heat_source_cooling : float | xr.DataArray
Additional temperature drop (K) when extracting heat from the source
through the heat pump, beyond the preheating contribution.
Returns
-------
xr.DataArray
Preheater efficiency profile: value in (0, 1) where T_return < T_source < T_forward,
0.0 otherwise (source too cold or hot enough for direct use).
"""
return xr.where(
(source_temperature < forward_temperature)
* (source_temperature > return_temperature),
(source_temperature - return_temperature)
/ (source_temperature - return_temperature + heat_source_cooling),
0.0,
)
def get_heat_pump_cooling(
heat_source_name: str,
default_heat_source_cooling: float,
return_temperature: xr.DataArray = None,
) -> float | xr.DataArray:
"""
Get the additional heat source cooling (temperature drop) through the heat pump for a heat source.
For PTES, this equals the temperature difference between
return flow and bottom layer (return_temperature - bottom_temperature).
For other sources, uses the default constant value.
Parameters
----------
heat_source_name : str
Name of the heat source (e.g., 'ptes', 'geothermal', 'air').
default_heat_source_cooling : float
Default heat source cooling in Kelvin, from config.
return_temperature : xr.DataArray, optional
District heating return temperature profiles in °C. Required for PTES.
Returns
-------
float | xr.DataArray
Heat source cooling in Kelvin. Returns a float for most sources,
or a DataArray for PTES.
"""
heat_source = HeatSource(heat_source_name)
if heat_source.source_type == HeatSourceType.STORAGE:
if return_temperature is None:
raise ValueError(
"PTES heat source requires return_temperature to calculate heat pump cooling."
)
bottom_temperature = float(ptes_ds.attrs["bottom_temperature"])
return return_temperature - bottom_temperature
return default_heat_source_cooling
if __name__ == "__main__":
if "snakemake" not in globals():
from scripts._helpers import mock_snakemake
snakemake = mock_snakemake(
"build_heat_source_utilisation_profiles",
clusters=48,
)
configure_logging(snakemake)
set_scenario_config(snakemake)
heat_sources: list[str] = snakemake.params.heat_sources
ptes_enable: bool = snakemake.params.ptes_enable
# Load PTES operations dataset if enabled
if ptes_enable:
ptes_ds = xr.open_dataset(snakemake.input.ptes_operations)
num_ptes_layers = int(ptes_ds.attrs["num_layers"])
else:
ptes_ds = None
num_ptes_layers = 0
central_heating_forward_temperature: xr.DataArray = xr.open_dataarray(
snakemake.input.central_heating_forward_temperature_profiles
)
central_heating_return_temperature: xr.DataArray = xr.open_dataarray(
snakemake.input.central_heating_return_temperature_profiles
)
xr.concat(
[
get_direct_utilisation_profile(
source_temperature=get_source_temperature(
snakemake_params=snakemake.params,
snakemake_input=snakemake.input,
heat_source_name=heat_source_key,
),
forward_temperature=central_heating_forward_temperature,
).assign_coords(heat_source=heat_source_key)
for heat_source_key in heat_sources
],
dim="heat_source",
).to_netcdf(snakemake.output.heat_source_direct_utilisation_profiles)
xr.concat(
[
get_preheater_utilisation_profile(
source_temperature=get_source_temperature(
heat_source_name=heat_source_key,
snakemake_params=snakemake.params,
snakemake_input=snakemake.input,
),
forward_temperature=central_heating_forward_temperature,
return_temperature=central_heating_return_temperature,
heat_source_cooling=get_heat_pump_cooling(
heat_source_name=heat_source_key,
default_heat_source_cooling=snakemake.params.heat_source_cooling,
return_temperature=central_heating_return_temperature,
),
).assign_coords(heat_source=heat_source_key)
for heat_source_key in heat_sources
],
dim="heat_source",
).to_netcdf(snakemake.output.heat_source_preheater_utilisation_profiles)
if ptes_ds is not None:
ptes_ds.close()