diff --git a/config/config.default.yaml b/config/config.default.yaml index 32c073b48..8518dc63e 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -597,6 +597,28 @@ sector: biogas_upgrading_cc: false conventional_generation: OCGT: gas + industry_t: # heat supply industry + endogen: false # if heat supply for industry should be enodgenously optimised + must_run: 0.8 # NB: industrial heat demand is in reality supplied at site by discrete plants + # which follow the local demand and therefore cannot be dispatched too much. + # Thus, p_min_pu is set to must run to avoid too much dispatch behaviour. Improvements welcome! + # share of medium and high temperature for process heat based on EU27 Fig. 5 https://www.agora-industry.org/publications/direct-electrification-of-industrial-process-heat + # TODO: should be improved and split up by country and sector + share_medium: 0.42 + share_high: 0.57 + # Options in case of endogenous industry heat supply + low_T: + biomass: true + heat_pumps: true + electric_boiler: true + methane: true + medium_T: + biomass: true + methane: true + hydrogen: true + high_T: + hydrogen: true + methane: true biomass_to_liquid: false electrobiofuels: false biosng: false diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 081ada586..942fc357a 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2718,17 +2718,397 @@ def add_biomass(n, costs): ) -def add_industry(n, costs): - logger.info("Add industrial demand") +def add_low_t_industry(n, nodes, industrial_demand, costs, must_run): + """ + Add low temperature heat for industry. + """ - nodes = pop_layout.index - nhours = n.snapshot_weightings.generators.sum() - nyears = nhours / 8760 + logger.info("Add low temperature industry.") - # 1e6 to convert TWh to MWh - industrial_demand = ( - pd.read_csv(snakemake.input.industrial_demand, index_col=0) * 1e6 - ) * nyears + n.madd( + "Bus", + nodes + " lowT industry", + location=nodes, + carrier="lowT industry", + unit="MWh_LHV", + ) + + n.madd( + "Load", + nodes, + suffix=" lowT industry", + bus=nodes + " lowT industry", + carrier="lowT industry", + p_set=industrial_demand.loc[nodes, "solid biomass"] / 8760.0, + ) + + if ( + options["industry_t"]["low_T"]["biomass"] + or not options["industry_t"]["endogen"] + ): + n.madd( + "Link", + nodes, + suffix=" solid biomass for lowT industry", + bus0=spatial.biomass.nodes, + bus1=nodes + " lowT industry", + carrier="lowT 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", "fixed"] + * costs.at["solid biomass boiler steam", "efficiency"], + marginal_cost=costs.at["solid biomass boiler steam", "VOM"], + lifetime=costs.at["solid biomass boiler steam", "lifetime"], + ) + + n.madd( + "Link", + nodes, + suffix=" solid biomass for lowT industry CC", + bus0=spatial.biomass.nodes, + bus1=nodes + " lowT industry", + bus2="co2 atmosphere", + bus3=spatial.co2, + carrier="lowT 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", "fixed"] + * costs.at["solid biomass boiler steam CC", "efficiency"] + + costs.at["biomass CHP capture", "fixed"] + * costs.at["solid biomass", "CO2 intensity"], + marginal_cost=costs.at["solid biomass boiler steam CC", "VOM"], + efficiency2=-costs.at["solid biomass", "CO2 intensity"] + * costs.at["biomass CHP capture", "capture_rate"], + efficiency3=costs.at["solid biomass", "CO2 intensity"] + * (1 - costs.at["biomass CHP capture", "capture_rate"]) + - costs.at["solid biomass", "CO2 intensity"], + lifetime=costs.at["solid biomass boiler steam CC", "lifetime"], + ) + + if options["industry_t"]["low_T"]["methane"]: + n.madd( + "Link", + nodes, + suffix=" gas for lowT industry", + bus0=spatial.gas.nodes, + bus1=nodes + " lowT industry", + bus2="co2 atmosphere", + carrier="lowT industry methane", + p_nom_extendable=True, + p_min_pu=must_run, + efficiency=costs.at["gas boiler steam", "efficiency"], + capital_cost=costs.at["gas boiler steam", "fixed"] + * costs.at["gas boiler steam", "efficiency"], + marginal_cost=costs.at["gas boiler steam", "VOM"], + 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.madd( + "Link", + nodes, + suffix=" gas for lowT industry CC", + bus0=spatial.gas.nodes, + bus1=nodes + " lowT industry", + bus2=spatial.co2.nodes, + bus3="co2 atmosphere", + carrier="lowT industry methane CC", + p_nom_extendable=True, + p_min_pu=must_run, + efficiency=eta, + capital_cost=costs.at["gas boiler steam", "fixed"] + * costs.at["gas boiler steam", "efficiency"] + + costs.at["biomass CHP capture", "fixed"] + * costs.at["gas", "CO2 intensity"], + marginal_cost=costs.at["gas boiler steam", "VOM"], + 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"]["low_T"]["heat_pumps"]: + # high temperature industrial heat pump can heat up to 150°C + eta = costs.at["industrial heat pump high temperature", "efficiency"] + n.madd( + "Link", + nodes, + suffix=" industrial heat pump steam for lowT industry", + bus0=nodes, + bus1=nodes + " lowT industry", + carrier="lowT industry heat pump", + p_nom_extendable=True, + p_min_pu=must_run, + efficiency=eta, + capital_cost=costs.at["industrial heat pump high temperature", "fixed"] + * eta, + marginal_cost=costs.at["industrial heat pump high temperature", "VOM"], + lifetime=costs.at["industrial heat pump high temperature", "lifetime"], + ) + + if options["industry_t"]["low_T"]["electric_boiler"]: + n.madd( + "Link", + nodes, + suffix=" electricity for lowT industry", + bus0=nodes, + bus1=nodes + " lowT industry", + carrier="lowT industry electricity", + p_nom_extendable=True, + p_min_pu=must_run, + efficiency=costs.at["electric boiler steam", "efficiency"], + capital_cost=costs.at["electric boiler steam", "fixed"] + * costs.at["electric boiler steam", "efficiency"], + marginal_cost=costs.at["electric boiler steam", "VOM"], + lifetime=costs.at["electric boiler steam", "lifetime"], + ) + + +def add_medium_t_industry(n, nodes, industrial_demand, costs, must_run): + """ + Add medium temperature heat for industry. + + Medium and high temperature heat demands are taken from today's + industry methane demand and split according to config setting. + """ + + logger.info("Add medium temperature industry.") + + n.madd( + "Bus", + nodes + " mediumT industry", + location=nodes, + carrier="mediumT industry", + unit="MWh_LHV", + ) + + share_m = options["industry_t"]["share_medium"] + n.madd( + "Load", + nodes, + suffix=" mediumT industry", + bus=nodes + " mediumT industry", + carrier="mediumT industry", + p_set=share_m * industrial_demand.loc[nodes, "methane"] / 8760.0, + ) + + if options["industry_t"]["medium_T"]["biomass"]: + n.madd( + "Link", + nodes, + suffix=" solid biomass for mediumT industry", + bus0=spatial.biomass.nodes, + bus1=nodes + " mediumT industry", + carrier="solid biomass for mediumT industry", + 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", "fixed"] + * costs.at["direct firing solid fuels", "efficiency"], + marginal_cost=costs.at["direct firing solid fuels", "VOM"] + + costs.at["biomass boiler", "pelletizing cost"], + lifetime=costs.at["direct firing solid fuels", "lifetime"], + ) + + n.madd( + "Link", + nodes, + suffix=" solid biomass for mediumT industry CC", + bus0=spatial.biomass.nodes, + bus1=nodes + " mediumT industry", + bus2=spatial.co2.nodes, + bus3="co2 atmosphere", + carrier="solid biomass for mediumT industry 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", "fixed"] + * costs.at["direct firing solid fuels CC", "efficiency"] + + costs.at["biomass CHP capture", "fixed"] + * costs.at["solid biomass", "CO2 intensity"], + marginal_cost=costs.at["direct firing solid fuels CC", "VOM"] + + 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"]["medium_T"]["methane"]: + # TODO: add electricity input from DEA and adapt VOM to exclude electricity cost! + n.madd( + "Link", + nodes, + suffix=" gas for mediumT industry", + bus0=spatial.gas.nodes, + bus1=nodes + " mediumT industry", + bus2="co2 atmosphere", + carrier="gas for mediumT industry", + 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", "fixed"] + * costs.at["direct firing gas", "efficiency"], + marginal_cost=costs.at["direct firing gas", "VOM"], + 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.madd( + "Link", + nodes, + suffix=" gas for mediumT industry CC", + bus0=spatial.gas.nodes, + bus1=nodes + " mediumT industry", + bus2=spatial.co2.nodes, + bus3="co2 atmosphere", + carrier="gas for mediumT industry 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", "fixed"] + * costs.at["direct firing gas CC", "efficiency"] + + costs.at["biomass CHP capture", "fixed"] + * costs.at["gas", "CO2 intensity"], + marginal_cost=costs.at["direct firing gas CC", "VOM"], + lifetime=costs.at["direct firing gas", "lifetime"], + ) + + if options["industry_t"]["medium_T"]["hydrogen"]: + # TODO: research cost of industrial H2 combustion, here set to 10x methane combustion + n.madd( + "Link", + nodes, + suffix=" hydrogen for mediumT industry", + bus0=nodes + " H2", + bus1=nodes + " mediumT industry", + carrier="hydrogen for mediumT industry", + capital_cost=10 + * costs.at["direct firing gas", "fixed"] + * costs.at["direct firing gas", "efficiency"], + marginal_cost=10 * costs.at["direct firing gas", "VOM"], + p_nom_extendable=True, + p_min_pu=must_run, + efficiency=costs.at["direct firing gas", "efficiency"], + ) + + +def add_high_t_industry(n, nodes, industrial_demand, costs, must_run): + """ + Add high temperature heat for industry. + + Medium and high temperature heat demands are taken from today's + industry methane demand and split according to config setting. + """ + + logger.info("Add high temperature industry.") + + n.madd("Bus", nodes + " highT industry", location=nodes, carrier="highT industry") + + share_h = options["industry_t"]["share_high"] + + n.madd( + "Load", + nodes, + suffix=" highT industry", + bus=nodes + " highT industry", + carrier="highT industry", + p_set=share_h * industrial_demand.loc[nodes, "methane"] / 8760.0, + ) + + if options["industry_t"]["high_T"]["methane"]: + n.madd( + "Link", + nodes, + suffix=" gas for highT industry", + bus0=spatial.gas.nodes, + bus1=nodes + " highT industry", + bus2="co2 atmosphere", + carrier="gas for highT industry", + 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", "fixed"] + * costs.at["direct firing gas", "efficiency"], + marginal_cost=costs.at["direct firing gas", "VOM"], + 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.madd( + "Link", + nodes, + suffix=" gas for highT industry CC", + bus0=spatial.gas.nodes, + bus1=nodes + " highT industry", + bus2=spatial.co2.nodes, + bus3="co2 atmosphere", + carrier="gas for highT industry 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", "fixed"] + * costs.at["direct firing gas CC", "efficiency"] + + costs.at["biomass CHP capture", "fixed"] + * costs.at["gas", "CO2 intensity"], + marginal_cost=costs.at["direct firing gas CC", "VOM"], + lifetime=costs.at["direct firing gas", "lifetime"], + ) + + if options["industry_t"]["high_T"]["hydrogen"]: + # TODO: research cost of industrial H2 combustion, here set to 10x methane combustion + n.madd( + "Link", + nodes, + suffix=" hydrogen for highT industry", + bus0=nodes + " H2", + bus1=nodes + " highT industry", + carrier="hydrogen for highT industry", + capital_cost=10 + * costs.at["direct firing gas", "fixed"] + * costs.at["direct firing gas", "efficiency"], + marginal_cost=10 * costs.at["direct firing gas", "VOM"], + 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_exogen_t_industry(n, nodes, industrial_demand, costs): + """ + Add heat demand for industry with exogenous supply. + + low temperature is supplied by biomass medium and high temperature + is supplied by gas + """ n.madd( "Bus", @@ -2844,6 +3224,34 @@ def add_industry(n, costs): lifetime=costs.at["cement capture", "lifetime"], ) + +def add_industry(n, costs): + logger.info("Add industrial demand") + + nodes = pop_layout.index + nhours = n.snapshot_weightings.generators.sum() + nyears = nhours / 8760 + + # 1e6 to convert TWh to MWh + industrial_demand = ( + pd.read_csv(snakemake.input.industrial_demand, index_col=0) * 1e6 + ) * nyears + + # endogenous heat supply for industry + if options["industry_t"]["endogen"]: + must_run = options["industry_t"]["must_run"] + logger.info( + f"Endogenise heat supply of industry with must run condition {must_run}" + ) + + add_low_t_industry(n, nodes, industrial_demand, costs, must_run) + + add_medium_t_industry(n, nodes, industrial_demand, costs, must_run) + + add_high_t_industry(n, nodes, industrial_demand, costs, must_run) + else: + add_exogen_t_industry(n, nodes, industrial_demand, costs) + n.madd( "Load", nodes,