diff --git a/REUSE.toml b/REUSE.toml index 27f8c4ff97..cd59159a9d 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -126,4 +126,11 @@ path = [ "data/ch_cantons.csv", ] SPDX-FileCopyrightText = "Wikipedia contributors" -SPDX-License-Identifier = "CC-BY-SA-4.0" \ No newline at end of file +SPDX-License-Identifier = "CC-BY-SA-4.0" + +[[annotations]] +path = [ + "data/ente202300981-sup-0001-suppdata-s1.xlsx", +] +SPDX-FileCopyrightText = "Fleiter et al." +SPDX-License-Identifier = "CC-BY-4.0" \ No newline at end of file diff --git a/config/config.default.yaml b/config/config.default.yaml index eee27b2fb3..874e1d264e 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -940,6 +940,40 @@ sector: conventional_generation: OCGT: gas CCGT: gas + industry_t: + endogen: false + must_run: 0.8 + endogenise_sectors: + "Aluminium - primary production": true + "Aluminium - secondary production": true + Alumina production: true + "Other non-ferrous metals": true + HVC: true + Cement: true + "Ceramics & other NMM": true + Glass production: true + Pulp production: true + Paper production: true + Printing and media reproduction: true + Food, beverages and tobacco: true + Pharmaceutical products etc.: true + Transport equipment: true + Machinery equipment: true + Wood and wood products: true + Textiles and leather: true + Other industrial sectors: true + "heat100-200": + biomass: true + methane: true + heat_pumps: true + electric_boiler: true + "heat200-500": + biomass: true + methane: true + hydrogen: true + "heat>500": + methane: true + hydrogen: true biomass_to_liquid: true biomass_to_liquid_cc: false electrobiofuels: true diff --git a/config/plotting.default.yaml b/config/plotting.default.yaml index a2d7405123..ee812f14af 100644 --- a/config/plotting.default.yaml +++ b/config/plotting.default.yaml @@ -449,6 +449,12 @@ plotting: gas for industry co2 to stored: '#8a3400' gas for industry: '#853403' gas for industry CC: '#692e0a' + heat100-200 industry gas: '#853403' + heat200-500 industry gas: '#853403' + heat>500 industry gas: '#853403' + heat100-200 industry gas CC: '#692e0a' + heat200-500 industry gas CC: '#692e0a' + heat>500 industry gas CC: '#692e0a' gas pipeline: '#ebbca0' gas pipeline new: '#a87c62' # oil @@ -505,6 +511,10 @@ plotting: BioSNG: '#123456' BioSNG CC: '#45233b' solid biomass to hydrogen: '#654321' + heat100-200 industry solid biomass: '#7a6d26' + heat200-500 industry solid biomass: '#7a6d26' + heat100-200 industry solid biomass CC: '#47411c' + heat200-500 industry solid biomass CC: '#47411c' # power transmission lines: '#6c9459' transmission lines: '#6c9459' @@ -517,6 +527,7 @@ plotting: industry electricity: '#2d2a66' industry new electricity: '#2d2a66' agriculture electricity: '#494778' + heat100-200 industry electric boiler steam: '#2d2a66' # battery + EVs battery: '#ace37f' battery storage: '#ace37f' @@ -596,7 +607,10 @@ plotting: residential urban decentral heat: '#a33c3c' residential urban decentral heat dsm: '#a33c3c' services urban decentral heat: '#cc1f1f' - low-temperature heat for industry: '#8f2727' + low-temperature heat for industry: '#8f2626' + heat100-200 industry: '#8f2828' + heat200-500 industry: '#c72020' + heat>500 industry: '#ff0000' process heat: '#ff0000' agriculture heat: '#d9a5a5' # heat supply @@ -634,6 +648,7 @@ plotting: services rural resistive heater: '#a5ed9d' services urban decentral resistive heater: '#98e991' urban central resistive heater: '#8cdf85' + heat100-200 industry industrial heat pump high temperature: '#2fb537' retrofitting: '#8487e8' building retrofitting: '#8487e8' # hydrogen @@ -654,6 +669,9 @@ plotting: H2 Fuel Cell: '#c251ae' H2 fuel cell: '#c251ae' H2 turbine: '#991f83' + heat100-200 industry hydrogen: '#991f83' + heat200-500 industry hydrogen: '#991f83' + heat>500 industry hydrogen: '#991f83' H2 Electrolysis: '#ff29d9' H2 electrolysis: '#ff29d9' # ammonia diff --git a/config/schema.default.json b/config/schema.default.json index e03c0b0571..0f2e10e02a 100644 --- a/config/schema.default.json +++ b/config/schema.default.json @@ -4971,6 +4971,88 @@ "description": "Add a more detailed description of conventional carriers. Any power generation requires the consumption of fuel from nodes representing that fuel.", "type": "object" }, + "industry_t": { + "description": "Configuration for `sector.industry_t` (endogenous industry process heat).", + "properties": { + "endogen": { + "default": false, + "description": "Endogenously optimise the industry process heat supply per temperature band (does not cover the steel sector).", + "type": "boolean" + }, + "must_run": { + "default": 0.8, + "description": "Minimum capacity factor for endogenous industry heat supply links, reflecting that industrial heat is in reality supplied on-site by discrete plants.", + "type": "number" + }, + "endogenise_sectors": { + "additionalProperties": { + "type": "boolean" + }, + "description": "Per-sector switch, only used when `endogen` is true. True splits the sector's process heat into temperature bands so that the supply technology is chosen endogenously; false keeps the sector exogenous (flat low-temperature heat plus gas and biomass loads). Sectors omitted here default to true. Steel, HVC recycling and DRI are handled separately and cannot be configured here.", + "type": "object" + }, + "heat100-200": { + "description": "Supply technologies allowed for industry process heat at 100-200 \u00b0C.", + "properties": { + "biomass": { + "default": true, + "description": "Allow solid biomass boilers (with and without carbon capture) to supply 100-200 \u00b0C process heat.", + "type": "boolean" + }, + "methane": { + "default": true, + "description": "Allow gas boilers (with and without carbon capture) to supply 100-200 \u00b0C process heat.", + "type": "boolean" + }, + "heat_pumps": { + "default": true, + "description": "Allow high-temperature industrial heat pumps to supply 100-200 \u00b0C process heat.", + "type": "boolean" + }, + "electric_boiler": { + "default": true, + "description": "Allow electric steam boilers to supply 100-200 \u00b0C process heat.", + "type": "boolean" + } + } + }, + "heat200-500": { + "description": "Supply technologies allowed for industry process heat at 200-500 \u00b0C.", + "properties": { + "biomass": { + "default": true, + "description": "Allow solid biomass boilers (with and without carbon capture) to supply 200-500 \u00b0C process heat.", + "type": "boolean" + }, + "methane": { + "default": true, + "description": "Allow gas boilers (with and without carbon capture) to supply 200-500 \u00b0C process heat.", + "type": "boolean" + }, + "hydrogen": { + "default": true, + "description": "Allow hydrogen boilers to supply 200-500 \u00b0C process heat.", + "type": "boolean" + } + } + }, + "heat>500": { + "description": "Supply technologies allowed for industry process heat above 500 \u00b0C.", + "properties": { + "methane": { + "default": true, + "description": "Allow gas boilers (with and without carbon capture) to supply >500 \u00b0C process heat.", + "type": "boolean" + }, + "hydrogen": { + "default": true, + "description": "Allow hydrogen boilers to supply >500 \u00b0C process heat.", + "type": "boolean" + } + } + } + } + }, "biomass_to_liquid": { "default": true, "description": "Add option for transforming solid biomass into liquid fuel with the same properties as oil.", @@ -6763,6 +6845,148 @@ } } }, + "_IndustryHeat100200Config": { + "description": "Supply technologies allowed for industry process heat at 100-200 \u00b0C.", + "properties": { + "biomass": { + "default": true, + "description": "Allow solid biomass boilers (with and without carbon capture) to supply 100-200 \u00b0C process heat.", + "type": "boolean" + }, + "methane": { + "default": true, + "description": "Allow gas boilers (with and without carbon capture) to supply 100-200 \u00b0C process heat.", + "type": "boolean" + }, + "heat_pumps": { + "default": true, + "description": "Allow high-temperature industrial heat pumps to supply 100-200 \u00b0C process heat.", + "type": "boolean" + }, + "electric_boiler": { + "default": true, + "description": "Allow electric steam boilers to supply 100-200 \u00b0C process heat.", + "type": "boolean" + } + } + }, + "_IndustryHeat200500Config": { + "description": "Supply technologies allowed for industry process heat at 200-500 \u00b0C.", + "properties": { + "biomass": { + "default": true, + "description": "Allow solid biomass boilers (with and without carbon capture) to supply 200-500 \u00b0C process heat.", + "type": "boolean" + }, + "methane": { + "default": true, + "description": "Allow gas boilers (with and without carbon capture) to supply 200-500 \u00b0C process heat.", + "type": "boolean" + }, + "hydrogen": { + "default": true, + "description": "Allow hydrogen boilers to supply 200-500 \u00b0C process heat.", + "type": "boolean" + } + } + }, + "_IndustryHeat500Config": { + "description": "Supply technologies allowed for industry process heat above 500 \u00b0C.", + "properties": { + "methane": { + "default": true, + "description": "Allow gas boilers (with and without carbon capture) to supply >500 \u00b0C process heat.", + "type": "boolean" + }, + "hydrogen": { + "default": true, + "description": "Allow hydrogen boilers to supply >500 \u00b0C process heat.", + "type": "boolean" + } + } + }, + "_IndustryTConfig": { + "description": "Configuration for `sector.industry_t` (endogenous industry process heat).", + "properties": { + "endogen": { + "default": false, + "description": "Endogenously optimise the industry process heat supply per temperature band (does not cover the steel sector).", + "type": "boolean" + }, + "must_run": { + "default": 0.8, + "description": "Minimum capacity factor for endogenous industry heat supply links, reflecting that industrial heat is in reality supplied on-site by discrete plants.", + "type": "number" + }, + "endogenise_sectors": { + "additionalProperties": { + "type": "boolean" + }, + "description": "Per-sector switch, only used when `endogen` is true. True splits the sector's process heat into temperature bands so that the supply technology is chosen endogenously; false keeps the sector exogenous (flat low-temperature heat plus gas and biomass loads). Sectors omitted here default to true. Steel, HVC recycling and DRI are handled separately and cannot be configured here.", + "type": "object" + }, + "heat100-200": { + "description": "Supply technologies allowed for industry process heat at 100-200 \u00b0C.", + "properties": { + "biomass": { + "default": true, + "description": "Allow solid biomass boilers (with and without carbon capture) to supply 100-200 \u00b0C process heat.", + "type": "boolean" + }, + "methane": { + "default": true, + "description": "Allow gas boilers (with and without carbon capture) to supply 100-200 \u00b0C process heat.", + "type": "boolean" + }, + "heat_pumps": { + "default": true, + "description": "Allow high-temperature industrial heat pumps to supply 100-200 \u00b0C process heat.", + "type": "boolean" + }, + "electric_boiler": { + "default": true, + "description": "Allow electric steam boilers to supply 100-200 \u00b0C process heat.", + "type": "boolean" + } + } + }, + "heat200-500": { + "description": "Supply technologies allowed for industry process heat at 200-500 \u00b0C.", + "properties": { + "biomass": { + "default": true, + "description": "Allow solid biomass boilers (with and without carbon capture) to supply 200-500 \u00b0C process heat.", + "type": "boolean" + }, + "methane": { + "default": true, + "description": "Allow gas boilers (with and without carbon capture) to supply 200-500 \u00b0C process heat.", + "type": "boolean" + }, + "hydrogen": { + "default": true, + "description": "Allow hydrogen boilers to supply 200-500 \u00b0C process heat.", + "type": "boolean" + } + } + }, + "heat>500": { + "description": "Supply technologies allowed for industry process heat above 500 \u00b0C.", + "properties": { + "methane": { + "default": true, + "description": "Allow gas boilers (with and without carbon capture) to supply >500 \u00b0C process heat.", + "type": "boolean" + }, + "hydrogen": { + "default": true, + "description": "Allow hydrogen boilers to supply >500 \u00b0C process heat.", + "type": "boolean" + } + } + } + } + }, "_LimitMaxGrowthConfig": { "description": "Configuration for `sector.limit_max_growth` settings.", "properties": { @@ -11439,6 +11663,88 @@ "description": "Add a more detailed description of conventional carriers. Any power generation requires the consumption of fuel from nodes representing that fuel.", "type": "object" }, + "industry_t": { + "description": "Configuration for `sector.industry_t` (endogenous industry process heat).", + "properties": { + "endogen": { + "default": false, + "description": "Endogenously optimise the industry process heat supply per temperature band (does not cover the steel sector).", + "type": "boolean" + }, + "must_run": { + "default": 0.8, + "description": "Minimum capacity factor for endogenous industry heat supply links, reflecting that industrial heat is in reality supplied on-site by discrete plants.", + "type": "number" + }, + "endogenise_sectors": { + "additionalProperties": { + "type": "boolean" + }, + "description": "Per-sector switch, only used when `endogen` is true. True splits the sector's process heat into temperature bands so that the supply technology is chosen endogenously; false keeps the sector exogenous (flat low-temperature heat plus gas and biomass loads). Sectors omitted here default to true. Steel, HVC recycling and DRI are handled separately and cannot be configured here.", + "type": "object" + }, + "heat100-200": { + "description": "Supply technologies allowed for industry process heat at 100-200 \u00b0C.", + "properties": { + "biomass": { + "default": true, + "description": "Allow solid biomass boilers (with and without carbon capture) to supply 100-200 \u00b0C process heat.", + "type": "boolean" + }, + "methane": { + "default": true, + "description": "Allow gas boilers (with and without carbon capture) to supply 100-200 \u00b0C process heat.", + "type": "boolean" + }, + "heat_pumps": { + "default": true, + "description": "Allow high-temperature industrial heat pumps to supply 100-200 \u00b0C process heat.", + "type": "boolean" + }, + "electric_boiler": { + "default": true, + "description": "Allow electric steam boilers to supply 100-200 \u00b0C process heat.", + "type": "boolean" + } + } + }, + "heat200-500": { + "description": "Supply technologies allowed for industry process heat at 200-500 \u00b0C.", + "properties": { + "biomass": { + "default": true, + "description": "Allow solid biomass boilers (with and without carbon capture) to supply 200-500 \u00b0C process heat.", + "type": "boolean" + }, + "methane": { + "default": true, + "description": "Allow gas boilers (with and without carbon capture) to supply 200-500 \u00b0C process heat.", + "type": "boolean" + }, + "hydrogen": { + "default": true, + "description": "Allow hydrogen boilers to supply 200-500 \u00b0C process heat.", + "type": "boolean" + } + } + }, + "heat>500": { + "description": "Supply technologies allowed for industry process heat above 500 \u00b0C.", + "properties": { + "methane": { + "default": true, + "description": "Allow gas boilers (with and without carbon capture) to supply >500 \u00b0C process heat.", + "type": "boolean" + }, + "hydrogen": { + "default": true, + "description": "Allow hydrogen boilers to supply >500 \u00b0C process heat.", + "type": "boolean" + } + } + } + } + }, "biomass_to_liquid": { "default": true, "description": "Add option for transforming solid biomass into liquid fuel with the same properties as oil.", diff --git a/data/ente202300981-sup-0001-suppdata-s1.xlsx b/data/ente202300981-sup-0001-suppdata-s1.xlsx new file mode 100644 index 0000000000..9c0750f62c Binary files /dev/null and b/data/ente202300981-sup-0001-suppdata-s1.xlsx differ diff --git a/doc/configuration.md b/doc/configuration.md index b3ddeda0b6..c65ca3aa01 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -539,193 +539,9 @@ Configuration for `existing_capacities` settings. Only used for sector-coupling studies. -??? note "Details" +Configuration for `sector` settings. - Configuration for `sector` settings. - - | Property | Type | Default | Description | - |----------|------|---------|-------------| - | `transport` | boolean | `true` | Flag to include transport sector. | - | `heating` | boolean | `true` | Flag to include heating sector. | - | `biomass` | boolean | `true` | Flag to include biomass sector. | - | `industry` | boolean | `true` | Flag to include industry sector. | - | `shipping` | boolean | `true` | Flag to include shipping sector. | - | `aviation` | boolean | `true` | Flag to include aviation sector. | - | `agriculture` | boolean | `true` | Flag to include agriculture sector. | - | `fossil_fuels` | boolean | `true` | Flag to include imports of fossil fuels. | - | `district_heating` | any | | Configuration for `sector.district_heating` settings. | - | `potential` | number \| dict (str -> number) | `0.6` | Maximum fraction of urban demand which can be supplied by district heating. If given as dictionary, specify one value per country modeled or provide a default value with key `default` to fill values for all unspecified countries. | - | `progress` | dict (str -> number) | | Increase of today's district heating demand to potential maximum district heating share. Progress = 0 means today's district heating share. Progress = 1 means maximum fraction of urban demand is supplied by district heating. | - | `district_heating_loss` | number | `0.15` | Share increase in district heat demand in urban central due to heat losses. | - | `supply_temperature_approximation` | object | | Supply temperature approximation settings. | - | `ptes` | object | | Pit thermal energy storage settings. | - | `ates` | object | | Aquifer thermal energy storage settings. | - | `heat_source_cooling` | number | `6` | Cooling of heat source for heat pumps. | - | `heat_pump_cop_approximation` | object | | Heat pump COP approximation settings. | - | `limited_heat_sources` | object | | Dictionary with names of limited heat sources (not air). Must be `river_water` / `geothermal` or another heat source in [Manz et al. 2024 ](https://www.sciencedirect.com/science/article/pii/S0960148124001769). | - | `direct_utilisation_heat_sources` | list of string | | List of heat sources for direct heat utilisation in district heating. Must be in the keys of `heat_utilisation_potentials` (e.g. `geothermal`). | - | `temperature_limited_stores` | list of string | | List of names for stores used as limited heat sources. | - | `dh_areas` | object | | District heating areas settings. | - | `heat_pump_sources` | dict (str -> list of string) | | Heat pump sources by area. | - | `residential_heat` | any | | Configuration for `sector.residential_heat` settings. | - | `dsm` | any | | Configuration for `sector.residential_heat.dsm` settings. | - | `enable` | boolean | `false` | Enable residential heat demand-side management that allows heating systems to provide flexibility by shifting demand within configurable time periods. Models building thermal mass as energy storage. | - | `direction` | list of string | | 'overheat-undercool' means both pre-heating and delayed heating are allowed. 'overheat' allows only pre-heating where buildings are heated up above target temperature and then allowed to cool down, while 'undercool' allows only delayed heating where buildings can cool below target temperature and then be heated up again. | - | `restriction_value` | dict (str -> number) | | Maximum state of charge (as fraction) for heat flexibility storage representing available thermal buffer capacity in buildings. Set to 0 for no flexibility or to 1.0 to assume that the entire heating demand can contribute to flexibility. | - | `restriction_time` | list of integer | | Checkpoint hours (0-23) at which heat flexibility storage must return to baseline state of charge, i.e. the residence surplus or missing heat be balanced. Time is the local time for each country and bus. Default: [10, 22] creates 12-hour periods with checkpoints at 10am and 10pm. | - | `cluster_heat_buses` | boolean | `true` | Cluster residential and service heat buses in [prepare_sector_network.py ](https://github.com/PyPSA/pypsa-eur-sec/blob/master/scripts/prepare_sector_network.py) to one to save memory. | - | `heat_demand_cutout` | string | `default` | Heat demand cutout. | - | `bev_dsm_restriction_value` | number | `0.8` | Adds a lower state of charge (SOC) limit for battery electric vehicles (BEV) to manage its own energy demand (DSM). Located in [build_transport_demand.py ](https://github.com/PyPSA/pypsa-eur-sec/blob/master/scripts/build_transport_demand.py). Set to 0 for no restriction on BEV DSM. | - | `bev_dsm_restriction_time` | number | `7` | Time at which SOC of BEV has to be dsm_restriction_value. | - | `transport_heating_deadband_upper` | number | `20.0` | The maximum temperature in the vehicle. At higher temperatures, the energy required for cooling in the vehicle increases. | - | `transport_heating_deadband_lower` | number | `15.0` | The minimum temperature in the vehicle. At lower temperatures, the energy required for heating in the vehicle increases. | - | `ICE_lower_degree_factor` | number | `0.375` | Share increase in energy demand in internal combustion engine (ICE) for each degree difference between the cold environment and the minimum temperature. | - | `ICE_upper_degree_factor` | number | `1.6` | Share increase in energy demand in internal combustion engine (ICE) for each degree difference between the hot environment and the maximum temperature. | - | `EV_lower_degree_factor` | number | `0.98` | Share increase in energy demand in electric vehicles (EV) for each degree difference between the cold environment and the minimum temperature. | - | `EV_upper_degree_factor` | number | `0.63` | Share increase in energy demand in electric vehicles (EV) for each degree difference between the hot environment and the maximum temperature. | - | `bev_dsm` | boolean | `true` | Add the option for battery electric vehicles (BEV) to participate in demand-side management (DSM). | - | `bev_dsm_availability` | number | `0.5` | The share for battery electric vehicles (BEV) that are able to do demand side management (DSM). | - | `bev_energy` | number | `0.05` | The average size of battery electric vehicles (BEV) in MWh. | - | `bev_charge_efficiency` | number | `0.9` | Battery electric vehicles (BEV) charge and discharge efficiency. | - | `bev_charge_rate` | number | `0.011` | The power consumption for one electric vehicle (EV) in MWh. Value derived from 3-phase charger with 11 kW. | - | `bev_avail_max` | number | `0.95` | The maximum share plugged-in availability for passenger electric vehicles. | - | `bev_avail_mean` | number | `0.8` | The average share plugged-in availability for passenger electric vehicles. | - | `v2g` | boolean | `true` | Allows feed-in to grid from EV battery. This is only enabled if BEV demand-side management is enabled, and the share of vehicles participating is V2G is given by `bev_dsm_availability`. | - | `land_transport_fuel_cell_share` | dict (str -> number) | | The share of vehicles that uses fuel cells in a given year. | - | `land_transport_electric_share` | dict (str -> number) | | The share of vehicles that uses electric vehicles (EV) in a given year. | - | `land_transport_ice_share` | dict (str -> number) | | The share of vehicles that uses internal combustion engines (ICE) in a given year. What is not EV or FCEV is oil-fuelled ICE. | - | `transport_electric_efficiency` | number | `53.19` | The conversion efficiencies of electric vehicles in transport. | - | `transport_fuel_cell_efficiency` | number | `30.003` | The H2 conversion efficiencies of fuel cells in transport. | - | `transport_ice_efficiency` | number | `16.0712` | The oil conversion efficiencies of internal combustion engine (ICE) in transport. | - | `agriculture_machinery_electric_share` | number | `0.5` | The share for agricultural machinery that uses electricity. | - | `agriculture_machinery_oil_share` | number | `0.5` | The share for agricultural machinery that uses oil. | - | `agriculture_machinery_fuel_efficiency` | number | `0.7` | The efficiency of electric-powered machinery in the conversion of electricity to meet agricultural needs. | - | `agriculture_machinery_electric_efficiency` | number | `0.3` | The efficiency of oil-powered machinery in the conversion of oil to meet agricultural needs. | - | `shipping_hydrogen_liquefaction` | boolean | `false` | Whether to include liquefaction costs for hydrogen demand in shipping. | - | `shipping_hydrogen_share` | dict (str -> number) | | The share of ships powered by hydrogen in a given year. | - | `shipping_methanol_share` | dict (str -> number) | | The share of ships powered by methanol in a given year. | - | `shipping_oil_share` | dict (str -> number) | | The share of ships powered by oil in a given year. | - | `shipping_methanol_efficiency` | number | `0.46` | The efficiency of methanol-powered ships in the conversion of methanol to meet shipping needs (propulsion). The efficiency increase from oil can be 10-15% higher according to the [IEA ](https://www.iea-amf.org/app/webroot/files/file/Annex%20Reports/AMF_Annex_56.pdf). | - | `shipping_oil_efficiency` | number | `0.4` | The efficiency of oil-powered ships in the conversion of oil to meet shipping needs (propulsion). Base value derived from 2011. | - | `aviation_demand_factor` | number | `1.0` | The proportion of demand for aviation compared to today's consumption. | - | `HVC_demand_factor` | number | `1.0` | The proportion of demand for high-value chemicals compared to today's consumption. | - | `time_dep_hp_cop` | boolean | `true` | Consider the time dependent coefficient of performance (COP) of the heat pump. | - | `heat_pump_sink_T_individual_heating` | number | `55.0` | The temperature heat sink used in heat pumps based on DTU / large area radiators. The value is conservatively high to cover hot water and space heating in poorly-insulated buildings. | - | `reduce_space_heat_exogenously` | boolean | `true` | Influence on space heating demand by a certain factor (applied before losses in district heating). | - | `reduce_space_heat_exogenously_factor` | dict (str -> number) | | A positive factor can mean renovation or demolition of a building. If the factor is negative, it can mean an increase in floor area, increased thermal comfort, population growth. The default factors are determined by the [Eurocalc Homes and buildings decarbonization scenario ](http://tool.european-calculator.eu/app/buildings/building-types-area/?levers=1ddd4444421213bdbbbddd44444ffffff11f411111221111211l212221). | - | `retrofitting` | any | | Configuration for `sector.retrofitting` settings. | - | `retro_endogen` | boolean | `false` | Add retrofitting as an endogenous system which co-optimise space heat savings. | - | `cost_factor` | number | `1.0` | Weight costs for building renovation. | - | `interest_rate` | number | `0.04` | The interest rate for investment in building components. | - | `annualise_cost` | boolean | `true` | Annualise the investment costs of retrofitting. | - | `tax_weighting` | boolean | `false` | Weight the costs of retrofitting depending on taxes in countries. | - | `construction_index` | boolean | `true` | Weight the costs of retrofitting depending on labour/material costs per country. | - | `tes` | boolean | `true` | Add option for storing thermal energy in large water pits associated with district heating systems and individual thermal energy storage (TES). | - | `boilers` | boolean | `true` | Add option for transforming gas into heat using gas boilers. | - | `resistive_heaters` | boolean | `true` | Add option for transforming electricity into heat using resistive heaters (independently from gas boilers). | - | `oil_boilers` | boolean | `false` | Add option for transforming oil into heat using boilers. | - | `biomass_boiler` | boolean | `true` | Add option for transforming biomass into heat using boilers. | - | `overdimension_heat_generators` | dict (str -> number) | | Add option for overdimensioning heating systems by a certain factor. This allows them to cover heat demand peaks e.g. 10% higher than those in the data with a setting of 1.1. | - | `chp` | any | | Configuration for `sector.chp` settings. | - | `enable` | boolean | `true` | Add option for using Combined Heat and Power (CHP). | - | `fuel` | list of string | | Possible options are all fuels which have an existing bus and their CO2 intensity is given in the technology data. Currently possible are "gas", "oil", "methanol", "lignite", "coal" as well as "solid biomass". For all fuels except solid biomass, the techno-economic data from gas CHP is used. For the special case of solid biomass fuel, both CHP plants with and without carbon capture are added. | - | `micro_chp` | boolean | `false` | Add option for using gas-fired Combined Heat and Power (CHP) for decentral areas. | - | `solar_thermal` | boolean | `true` | Add option for using solar thermal to generate heat. | - | `solar_cf_correction` | number | `0.788457` | The correction factor for the value provided by the solar thermal profile calculations. | - | `methanation` | boolean | `true` | Add option for transforming hydrogen and CO2 into methane using methanation. | - | `coal_cc` | boolean | `false` | Add option for coal CHPs with carbon capture. | - | `dac` | boolean | `true` | Add option for Direct Air Capture (DAC). | - | `co2_vent` | boolean | `false` | Add option for vent out CO2 from storages to the atmosphere. | - | `heat_vent` | dict (str -> boolean) | | Heat venting by area. | - | `marginal_cost_heat_vent` | number | `0.02` | The marginal cost of heat-venting in all heating systems. | - | `allam_cycle_gas` | boolean | `false` | Add option to include [Allam cycle gas power plants ](https://en.wikipedia.org/wiki/Allam_power_cycle). | - | `hydrogen_fuel_cell` | boolean | `true` | Add option to include hydrogen fuel cell for re-electrification. Assuming OCGT technology costs. | - | `hydrogen_turbine` | boolean | `true` | Add option to include hydrogen turbine for re-electrification. Assuming OCGT technology costs. | - | `SMR` | boolean | `true` | Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR). | - | `SMR_cc` | boolean | `true` | Add option for transforming natural gas into hydrogen and CO2 using Steam Methane Reforming (SMR) and Carbon Capture (CC). | - | `regional_oil_demand` | boolean | `true` | Spatially resolve oil demand. Set to true if regional CO2 constraints needed. | - | `regional_coal_demand` | boolean | `false` | Regional coal demand. | - | `regional_co2_sequestration_potential` | object | | Add option for regionally-resolved geological carbon dioxide sequestration potentials based on [CO2StoP ](https://setis.ec.europa.eu/european-co2-storage-database_en). | - | `co2_sequestration_potential` | dict (str -> number) | | The potential of sequestering CO2 in Europe per year and investment period. | - | `co2_sequestration_cost` | number | `30` | The cost of sequestering a ton of CO2 (currency/tCO2). | - | `co2_sequestration_lifetime` | integer | `50` | The lifetime of a CO2 sequestration site (years). | - | `co2_spatial` | boolean | `true` | Add option to spatially resolve carrier representing stored carbon dioxide. This allows for more detailed modelling of CCUTS, e.g. regarding the capturing of industrial process emissions, usage as feedstock for electrofuels, transport of carbon dioxide, and geological sequestration sites. | - | `co2_network` | boolean | `true` | Add option for planning a new carbon dioxide transmission network. | - | `co2_network_cost_factor` | number | `1` | The cost factor for the capital cost of the carbon dioxide transmission network. | - | `cc_fraction` | number | `0.9` | The default fraction of CO2 captured with post-combustion capture. | - | `hydrogen_underground_storage` | boolean | `true` | Add options for storing hydrogen underground. Storage potential depends regionally. | - | `hydrogen_underground_storage_locations` | list of string | | The location where hydrogen underground storage can be located. Onshore, nearshore, offshore means it must be located more than 50 km away from the sea, within 50 km of the sea, or within the sea itself respectively. | - | `methanol` | any | | Configuration for `sector.methanol` settings. | - | `regional_methanol_demand` | boolean | `false` | Spatially resolve methanol demand. Set to true if regional CO2 constraints needed. | - | `methanol_reforming` | boolean | `false` | Add methanol reforming. | - | `methanol_reforming_cc` | boolean | `false` | Add methanol reforming with carbon capture. | - | `methanol_to_kerosene` | boolean | `false` | Add methanol to kerosene. | - | `methanol_to_power` | dict (str -> boolean) | | Add different methanol to power technologies. | - | `biomass_to_methanol` | boolean | `true` | Add biomass to methanol. | - | `biomass_to_methanol_cc` | boolean | `false` | Add biomass to methanol with carbon capture. | - | `ammonia` | boolean \| string | `true` | Add ammonia as a carrier. It can be either true (copperplated NH3), false (no NH3 carrier) or "regional" (regionalised NH3 without network). | - | `min_part_load_electrolysis` | number | `0` | The minimum unit dispatch (`p_min_pu`) for electrolysis. | - | `min_part_load_fischer_tropsch` | number | `0.5` | The minimum unit dispatch (`p_min_pu`) for the Fischer-Tropsch process. | - | `min_part_load_methanolisation` | number | `0.3` | The minimum unit dispatch (`p_min_pu`) for the methanolisation process. | - | `min_part_load_methanation` | number | `0.3` | Minimum part load methanation. | - | `use_fischer_tropsch_waste_heat` | number | `0.25` | Add option for using waste heat of Fischer Tropsch in district heating networks. | - | `use_haber_bosch_waste_heat` | number | `0.25` | Use Haber-Bosch waste heat. | - | `use_methanolisation_waste_heat` | number | `0.25` | Use methanolisation waste heat. | - | `use_methanation_waste_heat` | number | `0.25` | Use methanation waste heat. | - | `use_fuel_cell_waste_heat` | number | `1` | Add option for using waste heat of fuel cells in district heating networks. | - | `use_electrolysis_waste_heat` | number | `0.25` | Add option for using waste heat of electrolysis in district heating networks. | - | `electricity_transmission_grid` | boolean | `true` | Switch for enabling/disabling the electricity transmission grid. | - | `electricity_distribution_grid` | boolean | `true` | Add a simplified representation of the exchange capacity between transmission and distribution grid level through a link. | - | `electricity_distribution_grid_cost_factor` | number | `1.0` | Multiplies the investment cost of the electricity distribution grid. | - | `electricity_grid_connection` | boolean | `true` | Add the cost of electricity grid connection for onshore wind and solar. | - | `transmission_efficiency` | any | | Configuration for `sector.transmission_efficiency` settings. | - | `enable` | list of string | | Switch to select the carriers for which transmission efficiency is to be added. Carriers not listed assume lossless transmission. | - | `DC` | dict (str -> number) | | DC transmission efficiency. | - | `H2 pipeline` | dict (str -> number) | | H2 pipeline transmission efficiency. | - | `gas pipeline` | dict (str -> number) | | Gas pipeline transmission efficiency. | - | `electricity distribution grid` | dict (str -> number) | | Electricity distribution grid efficiency. | - | `H2_network` | boolean | `true` | Add option for new hydrogen pipelines. | - | `gas_network` | boolean | `true` | Add existing natural gas infrastructure, incl. LNG terminals, production and entry-points. The existing gas network is added with a lossless transport model. A length-weighted [k-edge augmentation algorithm ](https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.connectivity.edge_augmentation.k_edge_augmentation.html#networkx.algorithms.connectivity.edge_augmentation.k_edge_augmentation) can be run to add new candidate gas pipelines such that all regions of the model can be connected to the gas network. When activated, all the gas demands are regionally disaggregated as well. | - | `H2_retrofit` | boolean | `false` | Add option for retrofiting existing pipelines to transport hydrogen. | - | `H2_retrofit_capacity_per_CH4` | number | `0.6` | The ratio for H2 capacity per original CH4 capacity of retrofitted pipelines. The [European Hydrogen Backbone (April, 2020) p.15 ](https://gasforclimate2050.eu/wp-content/uploads/2020/07/2020_European-Hydrogen-Backbone_Report.pdf) 60% of original natural gas capacity could be used in cost-optimal case as H2 capacity. | - | `gas_network_connectivity_upgrade` | number | `1` | The number of desired edge connectivity (k) in the length-weighted [k-edge augmentation algorithm ](https://networkx.org/documentation/stable/reference/algorithms/generated/networkx.algorithms.connectivity.edge_augmentation.k_edge_augmentation.html#networkx.algorithms.connectivity.edge_augmentation.k_edge_augmentation) used for the gas network. | - | `gas_distribution_grid` | boolean | `true` | Add a gas distribution grid. | - | `gas_distribution_grid_cost_factor` | number | `1.0` | Multiplier for the investment cost of the gas distribution grid. | - | `biomass_spatial` | boolean | `true` | Add option for resolving biomass demand regionally. | - | `biomass_transport` | boolean | `false` | Add option for transporting solid biomass between nodes. | - | `biogas_upgrading` | boolean | `true` | Biogas upgrading. | - | `biogas_upgrading_cc` | boolean | `false` | Add option to capture CO2 from biomass upgrading. | - | `conventional_generation` | dict (str -> string) | | Add a more detailed description of conventional carriers. Any power generation requires the consumption of fuel from nodes representing that fuel. | - | `biomass_to_liquid` | boolean | `true` | Add option for transforming solid biomass into liquid fuel with the same properties as oil. | - | `biomass_to_liquid_cc` | boolean | `false` | Add option for transforming solid biomass into liquid fuel with the same properties as oil with carbon capture. | - | `electrobiofuels` | boolean | `true` | Electrobiofuels. | - | `biosng` | boolean | `false` | Add option for transforming solid biomass into synthesis gas with the same properties as natural gas. | - | `biosng_cc` | boolean | `false` | Add option for transforming solid biomass into synthesis gas with the same properties as natural gas with carbon capture. | - | `bioH2` | boolean | `false` | Add option for transforming solid biomass into hydrogen with carbon capture. | - | `municipal_solid_waste` | boolean | `false` | Add option for municipal solid waste. | - | `limit_max_growth` | any | | Configuration for `sector.limit_max_growth` settings. | - | `enable` | boolean | `false` | Add option to limit the maximum growth of a carrier. | - | `factor` | number | `1.3` | The maximum growth factor of a carrier (e.g. 1.3 allows 30% larger than max historic growth). | - | `max_growth` | dict (str -> number) | | The historic maximum growth of a carrier. | - | `max_relative_growth` | dict (str -> number) | | The historic maximum relative growth of a carrier. | - | `enhanced_geothermal` | any | | Configuration for `sector.enhanced_geothermal` settings. | - | `enable` | boolean | `false` | Add option to include Enhanced Geothermal Systems. | - | `flexible` | boolean | `true` | Add option for flexible operation (see Ricks et al. 2024). | - | `max_hours` | integer | `240` | The maximum hours the reservoir can be charged under flexible operation. | - | `max_boost` | number | `0.25` | The maximum boost in power output under flexible operation. | - | `var_cf` | boolean | `true` | Add option for variable capacity factor (see Ricks et al. 2024). | - | `sustainability_factor` | number | `0.0025` | Share of sourced heat that is replenished by the earth's core (see details in [build_egs_potentials.py ](https://github.com/PyPSA/pypsa-eur-sec/blob/master/scripts/build_egs_potentials.py)). | - | `solid_biomass_import` | any | | Configuration for `sector.solid_biomass_import` settings. | - | `enable` | boolean | `false` | Add option to include solid biomass imports. | - | `price` | number | `54` | Price for importing solid biomass (currency/MWh). | - | `max_amount` | number | `1390` | Maximum solid biomass import potential (TWh). | - | `upstream_emissions_factor` | number | `0.1` | Upstream emissions of solid biomass imports. | - | `imports` | any | | Configuration for `sector.imports` settings. | - | `enable` | boolean | `false` | Add option to include renewable energy imports. | - | `limit` | number | | Maximum allowed renewable energy imports (TWh). | - | `limit_sense` | string | `<=` | Sense of the limit. | - | `price` | dict (str -> number) | | Price for importing renewable energy of carrier. | - +{{ schema_table("sector") }} **YAML Syntax** diff --git a/doc/release_notes.md b/doc/release_notes.md index 101ab4a150..114d00e69a 100644 --- a/doc/release_notes.md +++ b/doc/release_notes.md @@ -36,6 +36,8 @@ - Added solar rooftop ratio setting to `add_existing_baseyear` for heuristically splitting existing solar capacity between rooftop and utility-scale (defaults to a 50:50 split). +* feat: Endogenous industry process heat supply. Industrial process heat demand is split into temperature bands (`heat<100`, `heat100-200`, `heat200-500`, `heat>500`) in the new rule [build_industry_sector_ratios_endogenous][], based mostly on Fleiter et al. 2025. When enabled via `sector: industry_t: endogen`, the model chooses the supply technology per band (heat pumps, electric boilers, solid biomass, methane and hydrogen) endogenously instead of relying on exogenous shares. The steel sector is excluded as its heat supply is handled separately. + ## PyPSA-Eur v2026.02.0 (18th February 2026) **Features** diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 59b6540fea..5b80f33266 100755 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -1048,6 +1048,32 @@ rule build_industry_sector_ratios: scripts("build_industry_sector_ratios.py") +rule build_industry_sector_ratios_endogenous: + input: + industrial_energy_demand_per_country_today=resources( + "industrial_energy_demand_per_country_today.csv" + ), + industrial_production_per_country=resources( + "industrial_production_per_country.csv" + ), + process_temperature_bands="data/ente202300981-sup-0001-suppdata-s1.xlsx", + output: + industry_sector_ratios_endogenous=resources( + "industry_sector_ratios_endogenous.csv" + ), + log: + logs("build_industry_sector_ratios_endogenous.log"), + benchmark: + benchmarks("build_industry_sector_ratios_endogenous") + threads: 1 + resources: + mem_mb=1000, + params: + endogenise_sectors=config_provider("sector", "industry_t", "endogenise_sectors"), + script: + "../scripts/build_industry_sector_ratios_endogenous.py" + + rule build_industry_sector_ratios_intermediate: input: industry_sector_ratios=resources("industry_sector_ratios.csv"), @@ -1190,40 +1216,6 @@ rule build_industrial_production_per_node: scripts("build_industrial_production_per_node.py") -rule build_industrial_energy_demand_per_node: - input: - industry_sector_ratios=resources( - "industry_sector_ratios_{planning_horizons}.csv" - ), - industrial_production_per_node=resources( - "industrial_production_base_s_{clusters}_{planning_horizons}.csv" - ), - industrial_energy_demand_per_node_today=resources( - "industrial_energy_demand_today_base_s_{clusters}.csv" - ), - output: - industrial_energy_demand_per_node=resources( - "industrial_energy_demand_base_s_{clusters}_{planning_horizons}.csv" - ), - log: - logs( - "build_industrial_energy_demand_per_node_{clusters}_{planning_horizons}.log" - ), - benchmark: - ( - benchmarks( - "build_industrial_energy_demand_per_node/s_{clusters}_{planning_horizons}" - ) - ) - threads: 1 - resources: - mem_mb=1000, - message: - "Building industrial energy demand per network node for {wildcards.clusters} clusters and {wildcards.planning_horizons} planning horizon" - script: - scripts("build_industrial_energy_demand_per_node.py") - - rule build_industrial_energy_demand_per_country_today: input: transformation_output_coke=resources("transformation_output_coke.csv"), @@ -1277,6 +1269,41 @@ rule build_industrial_energy_demand_per_node_today: scripts("build_industrial_energy_demand_per_node_today.py") +rule build_industrial_energy_demand_per_node: + input: + industry_sector_ratios=resources( + "industry_sector_ratios_{planning_horizons}.csv" + ), + industry_sector_ratios_endogenous=resources( + "industry_sector_ratios_endogenous.csv" + ), + industrial_production_per_node=resources( + "industrial_production_base_s_{clusters}_{planning_horizons}.csv" + ), + industrial_energy_demand_per_node_today=resources( + "industrial_energy_demand_today_base_s_{clusters}.csv" + ), + output: + industrial_energy_demand_per_node=resources( + "industrial_energy_demand_base_s_{clusters}_{planning_horizons}.csv" + ), + log: + logs( + "build_industrial_energy_demand_per_node_{clusters}_{planning_horizons}.log" + ), + benchmark: + ( + benchmarks( + "build_industrial_energy_demand_per_node/s_{clusters}_{planning_horizons}" + ) + ) + threads: 1 + resources: + mem_mb=1000, + script: + "../scripts/build_industrial_energy_demand_per_node.py" + + rule build_retro_cost: input: building_stock="data/retro/data_building_stock.csv", diff --git a/scripts/build_industrial_energy_demand_per_country_today.py b/scripts/build_industrial_energy_demand_per_country_today.py index 9896a24d87..af1b49bc1e 100644 --- a/scripts/build_industrial_energy_demand_per_country_today.py +++ b/scripts/build_industrial_energy_demand_per_country_today.py @@ -99,7 +99,7 @@ "Solid fossil fuels": "solid", "Peat and peat products": "solid", "Oil shale and oil sands": "solid", - "Oil and petroleum products": "liquid", + "Oil and petroleum products": "naphtha", "Manufactured gases": "gas", "Natural gas": "gas", "Nuclear heat": "heat", diff --git a/scripts/build_industrial_energy_demand_per_node.py b/scripts/build_industrial_energy_demand_per_node.py index 5dc97c5469..5f9db63549 100644 --- a/scripts/build_industrial_energy_demand_per_node.py +++ b/scripts/build_industrial_energy_demand_per_node.py @@ -31,6 +31,14 @@ logger = logging.getLogger(__name__) +sectors_copied_from_exogenous = [ + # also mainly has a heat demand, but can be electrified by methods unavailable in other processes. + # Therefore, here exogenously electrified as long as individual sectors are not disaggregated. + "HVC (chemical recycling)", + "HVC (mechanical recycling)", + "DRI + Electric arc", +] + if __name__ == "__main__": if "snakemake" not in globals(): from scripts._helpers import mock_snakemake @@ -43,9 +51,22 @@ configure_logging(snakemake) set_scenario_config(snakemake) - # import ratios + # import exogenous ratios fn = snakemake.input.industry_sector_ratios - sector_ratios = pd.read_csv(fn, header=[0, 1], index_col=0) + sector_ratios_exogenous = pd.read_csv(fn, header=[0, 1], index_col=0) + + # import endogenous ratios + fn = snakemake.input.industry_sector_ratios_endogenous + sector_ratios_endogenous = pd.read_csv(fn, header=[0, 1], index_col=0) + + cols = sector_ratios_exogenous.columns[ + sector_ratios_exogenous.columns.get_level_values(1).isin( + sectors_copied_from_exogenous + ) + ] + sector_ratios_endogenous = pd.concat( + [sector_ratios_endogenous, sector_ratios_exogenous.loc[:, cols]], axis=1 + ) # material demand per node and industry (Mton/a) fn = snakemake.input.industrial_production_per_node @@ -55,16 +76,28 @@ fn = snakemake.input.industrial_energy_demand_per_node_today nodal_today = pd.read_csv(fn, index_col=0) - nodal_sector_ratios = pd.concat( - {node: sector_ratios[node[:2]] for node in nodal_production.index}, axis=1 + nodal_sector_ratios_exogenous = pd.concat( + {node: sector_ratios_exogenous[node[:2]] for node in nodal_production.index}, + axis=1, + ) + + nodal_sector_ratios_endogenous = pd.concat( + {node: sector_ratios_endogenous[node[:2]] for node in nodal_production.index}, + axis=1, ) nodal_production_stacked = nodal_production.stack() nodal_production_stacked.index.names = [None, None] # final energy consumption per node and industry (TWh/a) - nodal_df = ( - (nodal_sector_ratios.multiply(nodal_production_stacked)) + nodal_df_exogenous = ( + (nodal_sector_ratios_exogenous.multiply(nodal_production_stacked)) + .T.groupby(level=0) + .sum() + ) + + nodal_df_endogenous = ( + (nodal_sector_ratios_endogenous.multiply(nodal_production_stacked)) .T.groupby(level=0) .sum() ) @@ -72,11 +105,51 @@ rename_sectors = { "elec": "electricity", "biomass": "solid biomass", + } + + nodal_df_exogenous.rename(columns=rename_sectors, inplace=True) + nodal_df_endogenous.rename(columns=rename_sectors, inplace=True) + + nodal_df_exogenous.rename( + columns={ + "heat": "low-temperature heat", + }, + inplace=True, + ) + + grouper = { + "heat<100": "low-temperature heat", "heat": "low-temperature heat", } - nodal_df.rename(columns=rename_sectors, inplace=True) - nodal_df["current electricity"] = nodal_today["electricity"] + def partial_group(df, grouper): + return pd.concat([df.groupby(grouper).sum(), df.drop(grouper)]) + + nodal_df_endogenous = partial_group(nodal_df_endogenous.T, grouper).T + + # Process emissions (calcination / chemical-reaction CO2) and the naphtha and + # methanol feedstocks are non-energy quantities. The endogenous ratios are derived + # purely from fuel demand, so these carriers cannot be represented there and would + # vanish when `industry_t: endogen` is enabled. They are independent of how process + # heat is supplied, so carry them over from the exogenous block. Naphtha is copied + # together with the feedstock emissions so the feedstock-emission / naphtha ratio in + # prepare_sector_network stays self-consistent. + for carrier in [ + "process emission", + "process emission from feedstock", + "naphtha", + "methanol", + ]: + nodal_df_endogenous[carrier] = nodal_df_exogenous[carrier] + + nodal_df_exogenous["current electricity"] = nodal_today["electricity"] + nodal_df_endogenous["current electricity"] = nodal_today["electricity"] + + nodal_df = pd.concat( + [nodal_df_exogenous, nodal_df_endogenous], + axis=1, + keys=["exogenous", "endogenous"], + ) nodal_df.index.name = "TWh/a (MtCO2/a)" diff --git a/scripts/build_industry_sector_ratios_endogenous.py b/scripts/build_industry_sector_ratios_endogenous.py new file mode 100644 index 0000000000..7c163d7e4d --- /dev/null +++ b/scripts/build_industry_sector_ratios_endogenous.py @@ -0,0 +1,300 @@ +# SPDX-FileCopyrightText: Contributors to PyPSA-Eur +# +# SPDX-License-Identifier: MIT +""" +Build endogenous industry sector ratios. + +Description +------- + +This rule first builds energy needs in MWh/t for different industry sectors and production routes. + +These energy needs are expressed in primary energy carriers methane, biomass, low-grade heat etc. + +However, a large share of that primary energy is used to produce heat which, depending on the +temperature requirement, can be substituted by other heat carriers. + +To the end of endogenising this decision in the model, the heat-producing input energies are here +translated and split into the respective temperature bands, depending on industry sector and process. + +These splits are mostly taken from Fleiter et al. 2025. +"Hydrogen Infrastructure in the Future CO2-Neutral European Energy System - +How Does the Demand for Hydrogen Affect the Need for Infrastructure?" + +The script does not cover the steel sector. Its endogenisation is taken care of elsewhere. +""" + +import logging + +import numpy as np +import pandas as pd + +from scripts._helpers import configure_logging, set_scenario_config + +logger = logging.getLogger(__name__) + +idx = pd.IndexSlice + +# maps processes to temperature bands distributions from Fleiter et al. 2025 +process_temperature_band_mapper = { + "Aluminium - secondary production": idx[ + "Non-ferrous metals", "NE metals", ["Aluminium, secondary"] + ], + "Aluminium - primary production": idx[ + "Non-ferrous metals", "NE metals", ["Aluminium, primary"] + ], + "Other non-ferrous metals": idx[ + "Non-ferrous metals", + "NE metals", + [ + "Copper, primary", + "Copper, secondary", + "Copper further treatment", + "Zinc, primary", + "Zinc, secondary", + ], + ], + "HVC": idx[ + :, + :, + [ + "Adipic acid", + "Calcium carbide", + "Carbon black", + "Nitric acid", + "Soda ash", + "TDI", + "Titanium dioxide", + ], + ], + "Cement": idx["Non-metallic mineral products", "Clinker", :], + "Ceramics & other NMM": idx["Non-metallic mineral products", "Ceramics", :], + "Glass production": idx["Non-metallic mineral products", "Glass", :], + "Pulp production": idx[ + "Paper and printing", "Paper products", ["Chemical pulp", "Mechanical pulp"] + ], + "Paper production": idx[ + "Paper and printing", "Paper products", ["Paper", "Paper Electrical"] + ], + "Printing and media reproduction": idx[ + "Paper and printing", "Paper products", ["Chemical pulp", "Mechanical pulp"] + ], + "Food, beverages and tobacco": idx["Food, drink and tobacco", :, :], +} + +backup_temperature_band_shares = { + # from JRC IDEES EU26 Industry + "Alumina production": { + "heat<100": 0.0, + "heat100-200": 0.0, + "heat200-500": 0.0, + "heat>500": 1.0, + }, + "Pharmaceutical products etc.": { + "heat<100": 0.3, + "heat100-200": 0.6, + "heat200-500": 0.1, + "heat>500": 0.0, + }, + # from https://energyinnovation.org/data-explorer/overcoming-all-barriers-to-industrial-electrification + "Other industrial sectors": { + "heat<100": 0.1, + "heat100-200": 0.65, + "heat200-500": 0.25, + "heat>500": 0.0, + }, + "Transport equipment": { + "heat<100": 0.25, + "heat100-200": 0.6, + "heat200-500": 0.15, + "heat>500": 0.0, + }, + "Machinery equipment": { + "heat<100": 0.25, + "heat100-200": 0.6, + "heat200-500": 0.15, + "heat>500": 0.0, + }, + # from https://www.fpl.fs.usda.gov/documents/fplgtr/fplgtr118.pdf + "Wood and wood products": { + "heat<100": 0.65, + "heat100-200": 0.35, + "heat200-500": 0.0, + "heat>500": 0.0, + }, + # from https://www.ifc.org/content/dam/ifc/doc/2000/2007-textiles-manufacturing-ehs-guidelines-en.pdf + "Textiles and leather": { + "heat<100": 0.7, + "heat100-200": 0.2, + "heat200-500": 0.1, + "heat>500": 0.0, + }, +} + + +def build_industry_sector_ratios_endogenous(): + # in TWh/a + demand = pd.read_csv( + snakemake.input.industrial_energy_demand_per_country_today, + header=[0, 1], + index_col=0, + ) + + # in Mt/a + production = ( + pd.read_csv(snakemake.input.industrial_production_per_country, index_col=0) + / 1e3 + ).stack() + production.index.names = [None, None] + + today_sector_ratios = demand.div(production, axis=1).replace([np.inf, -np.inf], 0) + + today_sector_ratios.dropna(how="all", axis=1, inplace=True) + + rename = { + "waste": "biomass", + "electricity": "elec", + "solid": "coke", + "gas": "methane", + "other": "biomass", + "liquid": "naphtha", + } + today_sector_ratios = today_sector_ratios.rename(rename).groupby(level=0).sum() + + process_temperature_bands = pd.read_excel( + snakemake.input.process_temperature_bands, + sheet_name="Industry_ESC_FEC", + index_col=[0, 1, 2], + header=[0, 1], + ) + process_temperature_bands.columns = process_temperature_bands.columns.droplevel(0) + process_temperature_bands.rename( + columns={ + "<\xa0100\xa0°C": "heat<100", + "<\xa0100–200\xa0°C": "heat100-200", + "200–500\xa0°C": "heat200-500", + "500–1000\xa0°C": "heat500-1000", + ">\xa01000\xa0°C": "heat>1000", + }, + inplace=True, + ) + process_temperature_bands.loc[:, "heat>500"] = ( + process_temperature_bands.loc[:, "heat500-1000"] + + process_temperature_bands.loc[:, "heat>1000"] + ) + process_temperature_bands = process_temperature_bands[ + ["heat<100", "heat100-200", "heat200-500", "heat>500"] + ] + + # Override the Fleiter 2025 Mechanical pulp row, which places 100% of + # process heat below 100 °C. TMP/CTMP refiner steam and chip preheat + # operate at 130-170 °C (BREF "Production of Pulp, Paper and Board", + # Valmet TMP process documentation, ScienceDirect TMP overview), so the + # Rehfeldt et al. 2018 value of 100% in 100-200 °C is used instead. + process_temperature_bands.loc[ + idx["Paper and printing", "Paper products", "Mechanical pulp"], : + ] = [0.0, 1.0, 0.0, 0.0] + + heat_carriers = ["heat", "biomass", "methane"] + bands = ["heat<100", "heat100-200", "heat200-500", "heat>500"] + + endogenous_sector_ratios = today_sector_ratios.copy() + + endogenous_sector_ratios = endogenous_sector_ratios.reindex( + endogenous_sector_ratios.index.union(bands) + ).replace(np.nan, 0) + + # Per-sector selection: only split the processes flagged true (or omitted) into + # temperature bands. Excluded sectors keep their heat/methane/biomass as flat + # fuels, so their heat folds into heat<100 (low-temperature heat) and their + # gas/biomass feed the flat industry loads in prepare_sector_network -- i.e. the + # exogenous treatment, applied per sector. + selected = snakemake.params.endogenise_sectors + band_mapper = { + process: key + for process, key in process_temperature_band_mapper.items() + if selected.get(process, True) + } + backup_bands = { + process: shares + for process, shares in backup_temperature_band_shares.items() + if selected.get(process, True) + } + + # for each heat-endogenous process, split energy in heat carriers into the respective temperature bands + for process, key in band_mapper.items(): + as_fuels = endogenous_sector_ratios.loc[heat_carriers, idx[:, process]] + + as_heat = pd.DataFrame( + np.outer( + process_temperature_bands.loc[key].mean().values, as_fuels.sum().values + ), + index=bands, + columns=as_fuels.sum().index, + ) + + endogenous_sector_ratios.loc[bands, idx[:, process]] = as_heat + endogenous_sector_ratios.loc[heat_carriers, idx[:, process]] = 0.0 + + # for processes not covered by Fleiter et al. 2025 + for process, band_shares in backup_bands.items(): + band_shares = pd.Series(band_shares) + + as_fuels = endogenous_sector_ratios.loc[heat_carriers, idx[:, process]] + + as_heat = pd.DataFrame( + np.outer(band_shares.values, as_fuels.sum().values), + index=band_shares.index, + columns=as_fuels.sum().index, + ) + + endogenous_sector_ratios.loc[bands, idx[:, process]] = as_heat + endogenous_sector_ratios.loc[heat_carriers, idx[:, process]] = 0.0 + + # heat is only nonzero in processes that are not endogenous here, so this does not produce an error + endogenous_sector_ratios.loc["heat<100"] += endogenous_sector_ratios.loc["heat"] + endogenous_sector_ratios.drop("heat", inplace=True) + + endogenous_sector_ratios.index.name = "MWh/t" + + # Ensure all level 1 values are present for all level 0 entries + # Get all unique level 1 values across the entire dataframe + all_level1_values = endogenous_sector_ratios.columns.get_level_values(1).unique() + + # For each level 0 entry, ensure all level 1 values exist + new_columns = [] + for level0 in endogenous_sector_ratios.columns.get_level_values(0).unique(): + existing_level1 = endogenous_sector_ratios[level0].columns + missing_level1 = all_level1_values.difference(existing_level1) + + if len(missing_level1) > 0: + # Calculate average values for missing columns across level 0 entries where they exist + for level1 in missing_level1: + # Find all level 0 entries that have this level 1 value + mask = endogenous_sector_ratios.columns.get_level_values(1) == level1 + if mask.any(): + # Calculate mean across all columns with this level 1 value + avg_values = endogenous_sector_ratios.loc[:, mask].mean(axis=1) + new_columns.append(((level0, level1), avg_values)) + + # Add the new columns to the dataframe + for (level0, level1), values in new_columns: + endogenous_sector_ratios[(level0, level1)] = values + + # Sort columns for consistency + endogenous_sector_ratios = endogenous_sector_ratios.sort_index(axis=1) + + endogenous_sector_ratios.to_csv(snakemake.output.industry_sector_ratios_endogenous) + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from scripts._helpers import mock_snakemake + + snakemake = mock_snakemake( + "build_industry_sector_ratios_endogenous", + ) + configure_logging(snakemake) + set_scenario_config(snakemake) + + build_industry_sector_ratios_endogenous() diff --git a/scripts/lib/validation/config/sector.py b/scripts/lib/validation/config/sector.py index 0ff3f482d5..5db4b07ddc 100644 --- a/scripts/lib/validation/config/sector.py +++ b/scripts/lib/validation/config/sector.py @@ -372,6 +372,117 @@ class _ImportsConfig(BaseModel): ) +class _IndustryHeat100200Config(BaseModel): + """Supply technologies allowed for industry process heat at 100-200 °C.""" + + biomass: bool = Field( + True, + description="Allow solid biomass boilers (with and without carbon capture) to supply 100-200 °C process heat.", + ) + methane: bool = Field( + True, + description="Allow gas boilers (with and without carbon capture) to supply 100-200 °C process heat.", + ) + heat_pumps: bool = Field( + True, + description="Allow high-temperature industrial heat pumps to supply 100-200 °C process heat.", + ) + electric_boiler: bool = Field( + True, + description="Allow electric steam boilers to supply 100-200 °C process heat.", + ) + + +class _IndustryHeat200500Config(BaseModel): + """Supply technologies allowed for industry process heat at 200-500 °C.""" + + biomass: bool = Field( + True, + description="Allow solid biomass boilers (with and without carbon capture) to supply 200-500 °C process heat.", + ) + methane: bool = Field( + True, + description="Allow gas boilers (with and without carbon capture) to supply 200-500 °C process heat.", + ) + hydrogen: bool = Field( + True, + description="Allow hydrogen boilers to supply 200-500 °C process heat.", + ) + + +class _IndustryHeat500Config(BaseModel): + """Supply technologies allowed for industry process heat above 500 °C.""" + + methane: bool = Field( + True, + description="Allow gas boilers (with and without carbon capture) to supply >500 °C process heat.", + ) + hydrogen: bool = Field( + True, + description="Allow hydrogen boilers to supply >500 °C process heat.", + ) + + +def _default_endogenise_sectors() -> dict[str, bool]: + return { + sector: True + for sector in [ + "Aluminium - primary production", + "Aluminium - secondary production", + "Alumina production", + "Other non-ferrous metals", + "HVC", + "Cement", + "Ceramics & other NMM", + "Glass production", + "Pulp production", + "Paper production", + "Printing and media reproduction", + "Food, beverages and tobacco", + "Pharmaceutical products etc.", + "Transport equipment", + "Machinery equipment", + "Wood and wood products", + "Textiles and leather", + "Other industrial sectors", + ] + } + + +class _IndustryTConfig(BaseModel): + """Configuration for `sector.industry_t` (endogenous industry process heat).""" + + endogen: bool = Field( + False, + description="Endogenously optimise the industry process heat supply per temperature band (does not cover the steel sector).", + ) + must_run: float = Field( + 0.8, + description="Minimum capacity factor for endogenous industry heat supply links, reflecting that industrial heat is in reality supplied on-site by discrete plants.", + ) + endogenise_sectors: dict[str, bool] = Field( + default_factory=_default_endogenise_sectors, + description="Per-sector switch, only used when `endogen` is true. True splits the sector's process heat into temperature bands so that the supply technology is chosen endogenously; false keeps the sector exogenous (flat low-temperature heat plus gas and biomass loads). Sectors omitted here default to true. Steel, HVC recycling and DRI are handled separately and cannot be configured here.", + ) + heat100_200: _IndustryHeat100200Config = Field( + default_factory=_IndustryHeat100200Config, + alias="heat100-200", + description="Supply technologies allowed in the 100-200 °C temperature band.", + ) + heat200_500: _IndustryHeat200500Config = Field( + default_factory=_IndustryHeat200500Config, + alias="heat200-500", + description="Supply technologies allowed in the 200-500 °C temperature band.", + ) + heat_500: _IndustryHeat500Config = Field( + default_factory=_IndustryHeat500Config, + alias="heat>500", + description="Supply technologies allowed in the >500 °C temperature band.", + ) + + model_config = ConfigDict(populate_by_name=True) + + class SectorConfig(BaseModel): """Configuration for `sector` settings.""" @@ -879,6 +990,11 @@ class SectorConfig(BaseModel): description="Add a more detailed description of conventional carriers. Any power generation requires the consumption of fuel from nodes representing that fuel.", ) + industry_t: _IndustryTConfig = Field( + default_factory=_IndustryTConfig, + description="Endogenous industry process heat supply settings.", + ) + biomass_to_liquid: bool = Field( True, description="Add option for transforming solid biomass into liquid fuel with the same properties as oil.", diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 1f84f038b4..bb16cdea95 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -1597,9 +1597,15 @@ def insert_electricity_distribution_grid( v2gs = n.links.index[n.links.carrier == "V2G"] n.links.loc[v2gs, "bus1"] += " low voltage" - hps = n.links.index[n.links.carrier.str.contains("heat pump")] + hps = n.links.index[ + n.links.carrier.str.contains("heat pump") + & ~n.links.carrier.str.contains("industrial") + ] n.links.loc[hps, "bus1"] += " low voltage" + industrial_hps = n.links.index[n.links.carrier.str.contains("industrial heat pump")] + n.links.loc[industrial_hps, "bus0"] += " low voltage" + rh = n.links.index[n.links.carrier.str.contains("resistive heater")] n.links.loc[rh, "bus0"] += " low voltage" @@ -4485,6 +4491,395 @@ def add_biomass( ) +def add_t_industry100_200(n, nodes, industrial_demand, costs, must_run): + """ + Adds industry heat demands and supplies in the temperature band 100-200 °C. + """ + + logger.info("Add industry heat in the temperature band 100-200 °C.") + pass + + n.add( + "Bus", + nodes + " heat100-200 industry", + location=nodes, + carrier="heat100-200 industry", + unit="MWh_LHV", + ) + + n.add( + "Load", + nodes, + suffix=" heat100-200 industry", + bus=nodes + " heat100-200 industry", + carrier="heat100-200 industry", + p_set=industrial_demand.loc[nodes, "heat100-200"] / 8760.0, + ) + + if ( + options["industry_t"]["heat100-200"]["biomass"] + or not options["industry_t"]["endogen"] + ): + logger.info( + "Heat demand is based on final energy consumption in primary carriers, and therefore efficiency should be 1?" + ) + + n.add( + "Link", + nodes, + suffix=" solid biomass for heat100-200 industry", + bus0=spatial.biomass.nodes, + bus1=nodes + " heat100-200 industry", + carrier="heat100-200 industry solid biomass", + p_nom_extendable=True, + p_min_pu=must_run, + efficiency=costs.at["solid biomass boiler steam", "efficiency"], + capital_cost=costs.at["solid biomass boiler steam", "capital_cost"] + * costs.at["solid biomass boiler steam", "efficiency"], + marginal_cost=costs.at["solid biomass boiler steam", "marginal_cost"], + lifetime=costs.at["solid biomass boiler steam", "lifetime"], + ) + + n.add( + "Link", + nodes, + suffix=" solid biomass for heat100-200 industry CC", + bus0=spatial.biomass.nodes, + bus1=nodes + " heat100-200 industry", + bus2="co2 atmosphere", + bus3=spatial.co2.nodes, + carrier="heat100-200 industry solid biomass CC", + p_nom_extendable=True, + p_min_pu=must_run, + efficiency=costs.at["solid biomass boiler steam CC", "efficiency"], + capital_cost=costs.at["solid biomass boiler steam CC", "capital_cost"] + * costs.at["solid biomass boiler steam CC", "efficiency"] + + costs.at["biomass CHP capture", "capital_cost"] + * costs.at["solid biomass", "CO2 intensity"], + marginal_cost=costs.at["solid biomass boiler steam CC", "marginal_cost"], + efficiency2=-costs.at["solid biomass", "CO2 intensity"] + * costs.at["biomass CHP capture", "capture_rate"], + efficiency3=costs.at["solid biomass", "CO2 intensity"] + * costs.at["biomass CHP capture", "capture_rate"], + lifetime=costs.at["solid biomass boiler steam CC", "lifetime"], + ) + + if options["industry_t"]["heat100-200"]["methane"]: + n.add( + "Link", + nodes, + suffix=" heat100-200 industry gas", + bus0=spatial.gas.nodes, + bus1=nodes + " heat100-200 industry", + bus2="co2 atmosphere", + carrier="heat100-200 industry gas", + p_nom_extendable=True, + p_min_pu=must_run, + efficiency=costs.at["gas boiler steam", "efficiency"], + capital_cost=costs.at["gas boiler steam", "capital_cost"] + * costs.at["gas boiler steam", "efficiency"], + marginal_cost=costs.at["gas boiler steam", "marginal_cost"], + efficiency2=costs.at["gas", "CO2 intensity"], + lifetime=costs.at["gas boiler steam", "lifetime"], + ) + + eta = ( + costs.at["gas boiler steam", "efficiency"] + - costs.at["gas", "CO2 intensity"] + * costs.at["biomass CHP capture", "heat-input"] + ) + n.add( + "Link", + nodes, + suffix=" heat100-200 industry gas CC", + bus0=spatial.gas.nodes, + bus1=nodes + " heat100-200 industry", + bus2=spatial.co2.nodes, + bus3="co2 atmosphere", + carrier="heat100-200 industry gas CC", + p_nom_extendable=True, + p_min_pu=must_run, + efficiency=eta, + capital_cost=costs.at["gas boiler steam", "capital_cost"] + * costs.at["gas boiler steam", "efficiency"] + + costs.at["biomass CHP capture", "capital_cost"] + * costs.at["gas", "CO2 intensity"], + marginal_cost=costs.at["gas boiler steam", "marginal_cost"], + efficiency3=costs.at["gas", "CO2 intensity"] + * (1 - costs.at["biomass CHP capture", "capture_rate"]), + efficiency2=costs.at["gas", "CO2 intensity"] + * costs.at["biomass CHP capture", "capture_rate"], + lifetime=costs.at["gas boiler steam", "lifetime"], + ) + + if options["industry_t"]["heat100-200"]["heat_pumps"]: + # high temperature industrial heat pump can heat up to 150°C + eta = costs.at["industrial heat pump high temperature", "efficiency"] + n.add( + "Link", + nodes, + suffix=" heat100-200 industry industrial heat pump high temperature", + bus0=nodes, + bus1=nodes + " heat100-200 industry", + carrier="heat100-200 industry industrial heat pump high temperature", + p_nom_extendable=True, + p_min_pu=must_run, + efficiency=eta, + capital_cost=costs.at[ + "industrial heat pump high temperature", "capital_cost" + ] + * eta, + marginal_cost=costs.at[ + "industrial heat pump high temperature", "marginal_cost" + ], + lifetime=costs.at["industrial heat pump high temperature", "lifetime"], + ) + + if options["industry_t"]["heat100-200"]["electric_boiler"]: + n.add( + "Link", + nodes, + suffix=" heat100-200 industry electric boiler steam", + bus0=nodes, + bus1=nodes + " heat100-200 industry", + carrier="heat100-200 industry electric boiler steam", + p_nom_extendable=True, + p_min_pu=must_run, + efficiency=costs.at["electric boiler steam", "efficiency"], + capital_cost=costs.at["electric boiler steam", "capital_cost"] + * costs.at["electric boiler steam", "efficiency"], + marginal_cost=costs.at["electric boiler steam", "marginal_cost"], + lifetime=costs.at["electric boiler steam", "lifetime"], + ) + + +def add_t_industry200_500(n, nodes, industrial_demand, costs, must_run): + """ + Adds industry heat demands and supplies in the temperature band 200-500 °C. + """ + + logger.info("Add industry heat in the temperature band 200-500 °C.") + + n.add( + "Bus", + nodes + " heat200-500 industry", + location=nodes, + carrier="heat200-500 industry", + unit="MWh_LHV", + ) + + n.add( + "Load", + nodes, + suffix=" heat200-500 industry", + bus=nodes + " heat200-500 industry", + carrier="heat200-500 industry", + p_set=industrial_demand.loc[nodes, "heat200-500"] / 8760.0, + ) + + if options["industry_t"]["heat200-500"]["biomass"]: + n.add( + "Link", + nodes, + suffix=" heat200-500 industry solid biomass", + bus0=spatial.biomass.nodes, + bus1=nodes + " heat200-500 industry", + carrier="heat200-500 industry solid biomass", + p_nom_extendable=True, + p_min_pu=must_run, + efficiency=costs.at["direct firing solid fuels", "efficiency"], + capital_cost=costs.at["direct firing solid fuels", "capital_cost"] + * costs.at["direct firing solid fuels", "efficiency"], + marginal_cost=costs.at["direct firing solid fuels", "marginal_cost"] + + costs.at["biomass boiler", "pelletizing cost"], + lifetime=costs.at["direct firing solid fuels", "lifetime"], + ) + + n.add( + "Link", + nodes, + suffix=" heat200-500 industry solid biomass CC", + bus0=spatial.biomass.nodes, + bus1=nodes + " heat200-500 industry", + bus2=spatial.co2.nodes, + bus3="co2 atmosphere", + carrier="heat200-500 industry solid biomass CC", + p_nom_extendable=True, + p_min_pu=must_run, + efficiency=costs.at["direct firing solid fuels CC", "efficiency"], + capital_cost=costs.at["direct firing solid fuels CC", "capital_cost"] + * costs.at["direct firing solid fuels CC", "efficiency"] + + costs.at["biomass CHP capture", "capital_cost"] + * costs.at["solid biomass", "CO2 intensity"], + marginal_cost=costs.at["direct firing solid fuels CC", "marginal_cost"] + + costs.at["biomass boiler", "pelletizing cost"], + efficiency2=costs.at["solid biomass", "CO2 intensity"] + * costs.at["biomass CHP capture", "capture_rate"], + efficiency3=-costs.at["solid biomass", "CO2 intensity"] + * costs.at["biomass CHP capture", "capture_rate"], + lifetime=costs.at["direct firing solid fuels CC", "lifetime"], + ) + + if options["industry_t"]["heat200-500"]["methane"]: + # TODO: add electricity input from DEA and adapt VOM to exclude electricity cost! + n.add( + "Link", + nodes, + suffix=" heat200-500 industry gas", + bus0=spatial.gas.nodes, + bus1=nodes + " heat200-500 industry", + bus2="co2 atmosphere", + carrier="heat200-500 industry gas", + p_nom_extendable=True, + p_min_pu=must_run, + efficiency=costs.at["direct firing gas", "efficiency"], + efficiency2=costs.at["gas", "CO2 intensity"], + capital_cost=costs.at["direct firing gas", "capital_cost"] + * costs.at["direct firing gas", "efficiency"], + marginal_cost=costs.at["direct firing gas", "marginal_cost"], + lifetime=costs.at["direct firing gas", "lifetime"], + ) + + eta = ( + costs.at["direct firing gas", "efficiency"] + - costs.at["gas", "CO2 intensity"] + * costs.at["biomass CHP capture", "heat-input"] + ) + n.add( + "Link", + nodes, + suffix=" heat200-500 industry gas CC", + bus0=spatial.gas.nodes, + bus1=nodes + " heat200-500 industry", + bus2=spatial.co2.nodes, + bus3="co2 atmosphere", + carrier="heat200-500 industry gas CC", + p_nom_extendable=True, + p_min_pu=must_run, + efficiency=eta, + efficiency2=costs.at["gas", "CO2 intensity"] + * costs.at["biomass CHP capture", "capture_rate"], + efficiency3=costs.at["gas", "CO2 intensity"] + * (1 - costs.at["biomass CHP capture", "capture_rate"]), + capital_cost=costs.at["direct firing gas CC", "capital_cost"] + * costs.at["direct firing gas CC", "efficiency"] + + costs.at["biomass CHP capture", "capital_cost"] + * costs.at["gas", "CO2 intensity"], + marginal_cost=costs.at["direct firing gas CC", "marginal_cost"], + lifetime=costs.at["direct firing gas", "lifetime"], + ) + + if options["industry_t"]["heat200-500"]["hydrogen"]: + # TODO: research cost of industrial H2 combustion, here set to 10x methane combustion + n.add( + "Link", + nodes, + suffix=" heat200-500 industry hydrogen", + bus0=nodes + " H2", + bus1=nodes + " heat200-500 industry", + carrier="heat200-500 industry hydrogen", + capital_cost=10 + * costs.at["direct firing gas", "capital_cost"] + * costs.at["direct firing gas", "efficiency"], + marginal_cost=10 * costs.at["direct firing gas", "marginal_cost"], + p_nom_extendable=True, + p_min_pu=must_run, + efficiency=costs.at["direct firing gas", "efficiency"], + ) + + +def add_t_industry500(n, nodes, industrial_demand, costs, must_run): + """ + Adds industry heat demands and supplies in the temperature band >500 °C. + """ + + logger.info("Add industry heat in the temperature band >500 °C.") + + n.add( + "Bus", + nodes + " heat>500 industry", + location=nodes, + carrier="heat>500 industry", + unit="MWh_LHV", + ) + + n.add( + "Load", + nodes, + suffix=" heat>500 industry", + bus=nodes + " heat>500 industry", + carrier="heat>500 industry", + p_set=industrial_demand.loc[nodes, "heat>500"] / 8760.0, + ) + + if options["industry_t"]["heat>500"]["methane"]: + n.add( + "Link", + nodes, + suffix=" heat>500 industry gas", + bus0=spatial.gas.nodes, + bus1=nodes + " heat>500 industry", + bus2="co2 atmosphere", + carrier="heat>500 industry gas", + p_nom_extendable=True, + p_min_pu=must_run, + efficiency=costs.at["direct firing gas", "efficiency"], + efficiency2=costs.at["gas", "CO2 intensity"], + capital_cost=costs.at["direct firing gas", "capital_cost"] + * costs.at["direct firing gas", "efficiency"], + marginal_cost=costs.at["direct firing gas", "marginal_cost"], + lifetime=costs.at["direct firing gas", "lifetime"], + ) + + eta = ( + costs.at["direct firing gas", "efficiency"] + - costs.at["gas", "CO2 intensity"] + * costs.at["biomass CHP capture", "heat-input"] + ) + n.add( + "Link", + nodes, + suffix=" heat>500 industry gas CC", + bus0=spatial.gas.nodes, + bus1=nodes + " heat>500 industry", + bus2=spatial.co2.nodes, + bus3="co2 atmosphere", + carrier="heat>500 industry gas CC", + p_nom_extendable=True, + p_min_pu=must_run, + efficiency=eta, + efficiency2=costs.at["gas", "CO2 intensity"] + * costs.at["biomass CHP capture", "capture_rate"], + efficiency3=costs.at["gas", "CO2 intensity"] + * (1 - costs.at["biomass CHP capture", "capture_rate"]), + capital_cost=costs.at["direct firing gas CC", "capital_cost"] + * costs.at["direct firing gas CC", "efficiency"] + + costs.at["biomass CHP capture", "capital_cost"] + * costs.at["gas", "CO2 intensity"], + marginal_cost=costs.at["direct firing gas CC", "marginal_cost"], + lifetime=costs.at["direct firing gas", "lifetime"], + ) + + if options["industry_t"]["heat>500"]["hydrogen"]: + # TODO: research cost of industrial H2 combustion, here set to 10x methane combustion + n.add( + "Link", + nodes, + suffix=" heat>500 industry hydrogen", + bus0=nodes + " H2", + bus1=nodes + " heat>500 industry", + carrier="heat>500 industry hydrogen", + capital_cost=10 + * costs.at["direct firing gas", "capital_cost"] + * costs.at["direct firing gas", "efficiency"], + marginal_cost=10 * costs.at["direct firing gas", "marginal_cost"], + p_nom_extendable=True, + p_min_pu=must_run, + efficiency=costs.at["direct firing gas", "efficiency"], + lifetime=costs.at["direct firing gas", "lifetime"], + ) + + def add_industry( n: pypsa.Network, costs: pd.DataFrame, @@ -4577,8 +4972,19 @@ def add_industry( nhours = n.snapshot_weightings.generators.sum() nyears = nhours / 8760 + n.add( + "Bus", + spatial.biomass.industry, + location=spatial.biomass.locations, + carrier="solid biomass for industry", + unit="MWh_LHV", + ) + + industrial_demand = pd.read_csv(industrial_demand_file, index_col=0, header=[0, 1]) + # 1e6 to convert TWh to MWh - industrial_demand = pd.read_csv(industrial_demand_file, index_col=0) * 1e6 * nyears + industrial_demand = industrial_demand * 1e6 * nyears + industrial_demand.index.name = "MWh" if not options["biomass"]: raise ValueError( @@ -4586,13 +4992,17 @@ def add_industry( "Enable `sector: {biomass: true}` in config." ) - n.add( - "Bus", - spatial.biomass.industry, - location=spatial.biomass.locations, - carrier="solid biomass for industry", - unit="MWh_LHV", - ) + if options["industry_t"]["endogen"]: + industrial_demand = industrial_demand.loc[:, "endogenous"] + + 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) + add_t_industry500(n, nodes, industrial_demand, costs, must_run) + + else: + industrial_demand = industrial_demand.loc[:, "exogenous"] if options.get("biomass_spatial", options["biomass_transport"]): p_set = (