Endogenous Industry Heat#1943
Conversation
Endogenous industry sector ratios: - Swap the Pulp/Paper PRODCOM mappings, which were crossed (pulp -> paper products, paper -> pulp products). - Fix the pharmaceuticals backup share: heat>500 was 1.0, making the row sum to 2.0; set to 0.0 so the bands sum to 1.0. - Override the Fleiter 2025 Mechanical pulp row (100% heat<100) with 100% heat100-200, reflecting TMP/CTMP refiner steam at 130-170 C (Rehfeldt et al. 2018). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
@fneum @Irieo @toniseibold I think this is now in a quite good state, and could maybe even be merged, some notes:
|
There was a problem hiding this comment.
Thanks for working on this and pushing it further!
Upon testing the PR code for an own project I ran in to the problem that options and spatial in the new functions in prepare_sector_network.py are global. My suggestion is to pass these as arguments to the functions (as is done for options in the other functions in prepare_sector_network.py and as I suggest in PR2232 for spatial).
Some further comments on a stray pass and a minor unnecessarily complex if condition.
bus2 and bus3 are not consistent throught out the newly added links:
Sometimes it is:
bus2="co2 atmosphere",
bus3=spatial.co2.nodes,
(e.g. ["industry_t"]["heat100-200"]["biomass"])
other times:
bus2=spatial.co2.nodes,
bus3="co2 atmosphere",
(e.g., "heat200-500" biomass and methane links)
| """ | ||
|
|
||
| logger.info("Add industry heat in the temperature band 100-200 °C.") | ||
| pass |
| ) | ||
|
|
||
|
|
||
| def add_t_industry100_200(n, nodes, industrial_demand, costs, must_run): |
There was a problem hiding this comment.
Add options, spatial as parameters:
def add_t_industry100_200(n, nodes, industrial_demand, costs, must_run, options, spatial):
|
|
||
| if ( | ||
| options["industry_t"]["heat100-200"]["biomass"] | ||
| or not options["industry_t"]["endogen"] |
There was a problem hiding this comment.
Cleanup, or not is not required, as "endogen" is required for "heat100-200".
if options["industry_t"]["heat100-200"]["biomass"]:
| options["industry_t"]["heat100-200"]["biomass"] | ||
| or not options["industry_t"]["endogen"] | ||
| ): | ||
| logger.info( |
| ) | ||
|
|
||
|
|
||
| def add_t_industry200_500(n, nodes, industrial_demand, costs, must_run): |
There was a problem hiding this comment.
Add options, spatial as parameters:
def add_t_industry200_500(n, nodes, industrial_demand, costs, must_run, options, spatial):
| p_nom_extendable=True, | ||
| p_min_pu=must_run, | ||
| efficiency=costs.at["direct firing gas", "efficiency"], | ||
| ) |
There was a problem hiding this comment.
It seems like the lifetime is missing. (lifetime=costs.at["direct firing gas", "lifetime"],)
|
|
||
| must_run = options["industry_t"]["must_run"] | ||
|
|
||
| add_t_industry100_200(n, nodes, industrial_demand, costs, must_run) |
There was a problem hiding this comment.
Pass arguments options and spatial add_t_industry100_200(n, nodes, industrial_demand, costs, must_run, options, spatial)
| must_run = options["industry_t"]["must_run"] | ||
|
|
||
| add_t_industry100_200(n, nodes, industrial_demand, costs, must_run) | ||
| add_t_industry200_500(n, nodes, industrial_demand, costs, must_run) |
There was a problem hiding this comment.
Pass arguments options and spatial add_t_industry200_500(n, nodes, industrial_demand, costs, must_run, options, spatial)
|
|
||
| add_t_industry100_200(n, nodes, industrial_demand, costs, must_run) | ||
| add_t_industry200_500(n, nodes, industrial_demand, costs, must_run) | ||
| add_t_industry500(n, nodes, industrial_demand, costs, must_run) |
There was a problem hiding this comment.
Pass arguments options and spatial add_t_industry500(n, nodes, industrial_demand, costs, must_run, options, spatial)

pypsa/pypsa-eur#611
Adds industry process heat demands
heat100-200,heat200-500andheat>500by subtracting them from industrybiomassandmethanedemandsif
config["sector"]["industry_t"]["endogen"] == True.Temperature splits are process-aware, based on Fraunhofer ISI research Fleiter et al. (2025), Hydrogen Infrastructure in the Future CO2-Neu....
The PR does not cover steelworks. Its endogenisation is in a separate PR.
Changes to the Model
With industry heat endogenous, all existing industry-related components remain, heat demands are purely subtracted from biomass and methane industry demands, and are added as separate loads with the respective supply opportunities.
The changes to
prepare_sector_networkare largely identical to the previous PR by @lisazeyen, and the heat generation technologies added are the same. The temperature bands are chosen to match potentially interesting alternative renewable heat supply such as solar thermal and geothermal, which could be added to the model if desired.The logic for exogenous heating remains untouched. There was a very sensible proposition by @koen-vg to set the exogenous heat supply mix in the config, as is done with industry production pathways. However, the volume of content this approach would add to the config was, in my estimation, too large.
Therefore, this PR introduces a new rule that builds (planning-horizons–unaware) heat-endogenous ratios and then simply infers endogenous industry demands (including the temperature bands) in
build_industry_energy_demand_per_node, following the same logic as exogenous demand usingratios_intermediate.As a result,
industry_energy_demands_per_nodeconsists essentially two concatenated dataframes. Level 0 in columns== "exogenous"contains the old dataframe and== "endogenous"is the new one.Based on the endogenisation-switch in config,
prepare_sector_networkpulls the respective columns.Towards Validation
Comparing exogenous and endogenous 2030 industry demands shows that the PR removes the vast majority of (industry)
solid biomassand most of (industry)methanedemand. Exogenous heat presumes electrification, likely explaining why the total energy demand is lower in the exogenous case.The PR uses Fleiter et al. (2025) to estimate temperature band distributions for each industry process, and uses alternative process-specific sources to fill data gaps in between. Again, no steelworks in this PR.
These are very distinct between industries; however, they still yield largely similar temperature distributions between countries:
Spatial heterogeneity might only manifest in higher spatial resolutions.
Here, and in the previous iterations, we are aiming to align with Agora's estimate of industry heating demand at a total of around 1850 TWh/a:
Considering we are not including steelworks, we are doing quite well:
ToDos and Food for Thought
process emissions, HVC disaggregation,current electricity, etc. are synergising correctly with current methods.heat100-200andlow-temperature heat, but in some sectors, electrification of higher-temperature demands is feasible.Happy about any feedback, and all the best,
Lukas
@lisazeyen @millingermarkus @Irieo @koen-vg @toniseibold @fneum @bobbyxng @p-glaum
PS: Are the pictures readable, or do they throw an error when clicked on?
Checklist
pixi.toml(usingpixi add <dependency-name>).config/config.default.yaml.doc/configtables/*.csv.these instructions <https://pypsa-eur.readthedocs.io/en/latest/data_sources.html>_ have been followed.doc/release_notes.rstis added.