diff --git a/config/config.default.yaml b/config/config.default.yaml index c74c15dd63..7ac5d3487c 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -610,6 +610,14 @@ existing_capacities: - 2010 - 2015 - 2019 + grouping_years_industry: + - 1995 + - 2000 + - 2005 + - 2010 + - 2015 + - 2020 + - 2025 threshold_capacity: 10 default_heating_lifetime: 20 conventional_carriers: @@ -889,6 +897,7 @@ sector: allam: false biomass_to_methanol: true biomass_to_methanol_cc: false + meoh_to_oa: false ammonia: true min_part_load_electrolysis: 0 min_part_load_fischer_tropsch: 0.5 @@ -977,6 +986,15 @@ sector: methanol: 121 gas: 122 oil: 125 + endogenous_sectors: + enable: false + subsectors: + - steel + - cement + hbi_relocation: false + steel_bof: + pledge: true + pledge_delay: 0 # docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#industry industry: diff --git a/config/config.pr1719.yaml b/config/config.pr1719.yaml new file mode 100644 index 0000000000..1922fa591d --- /dev/null +++ b/config/config.pr1719.yaml @@ -0,0 +1,31 @@ +# yaml-language-server: $schema=../schema.json +# SPDX-FileCopyrightText: Contributors to PyPSA-Eur +# +# SPDX-License-Identifier: CC0-1.0 + +run: + name: pr1719 + +foresight: myopic + +scenario: + clusters: + - 39 + planning_horizons: + - 2030 + - 2040 + +sector: + methanol: + regional_methanol_demand: true + ammonia: regional + +clustering: + temporal: + resolution_sector: 365H + +# TODO: Create new TD release including grey methanol +data: + costs: + source: primary + version: dev diff --git a/config/plotting.default.yaml b/config/plotting.default.yaml index a2d7405123..b7456e691e 100644 --- a/config/plotting.default.yaml +++ b/config/plotting.default.yaml @@ -737,3 +737,24 @@ plotting: import NH3: '#e2ed74' import oil: '#93eda2' import methanol: '#87d0e6' + "steel": "#22333B" + "steel emission": "#5E503F" + "steel emission CC": "#A9927D" + "hbi": "#CCDBDC" + "DRI": "#8E7C93" + "EAF": "#586357" + "H2 DRI": "#618AB0" + "gas DRI": "#BAAF68" + "BOF": "#0A0908" + "grey methanol": "#7C6C77" + "cement": "#C98349" + "cement emission": "#119DA4" + "cement emission CC": "#0C7489" + "cement kiln": "#D90368" + "cement finishing": "#820263" + "clinker": "#F5FDC6" + "DRI reduction agent": "#3A86A8" + "cement heat": "#F4A261" + "cement heat biomass": "#6A994E" + "cement heat gas": "#E9C46A" + "cement heat waste": "#9B5DE5" diff --git a/config/schema.default.json b/config/schema.default.json index bf9786194a..2f94dbed09 100644 --- a/config/schema.default.json +++ b/config/schema.default.json @@ -2393,6 +2393,13 @@ }, "type": "array" }, + "grouping_years_industry": { + "description": "Intervals to group existing capacities for industry.", + "items": { + "type": "integer" + }, + "type": "array" + }, "threshold_capacity": { "default": 10, "description": "Capacities (MW) of generators and links below threshold are removed during add_existing_capacities.", @@ -4764,6 +4771,11 @@ "default": false, "description": "Add biomass to methanol with carbon capture.", "type": "boolean" + }, + "meoh_to_oa": { + "default": false, + "description": "Add methanol-to-olefins/aromatics as a way to meet naphtha demand in industry sector for high value chemicals.", + "type": "boolean" } } }, @@ -5105,6 +5117,41 @@ "type": "object" } } + }, + "endogenous_sectors": { + "description": "Configuration for `sector.endogenous_sectors` settings.", + "properties": { + "enable": { + "default": false, + "description": "Enabling to specify industry subsectors to be modelled endogenously.", + "type": "boolean" + }, + "subsectors": { + "description": "Energy-intensive industry subsectors that are modelled endogenously.", + "items": {}, + "type": "array" + } + } + }, + "hbi_relocation": { + "default": false, + "description": "Allow the relocation of iron ore reduction step.", + "type": "boolean" + }, + "steel_bof": { + "description": "Configuration for `sector.steel_bof` settings.", + "properties": { + "pledge": { + "default": true, + "description": "Decommissions coal blast furnaces at their pledged phase out year.", + "type": "boolean" + }, + "pledge_delay": { + "default": 0, + "description": "Delays the decommission year by number of years.", + "type": "integer" + } + } } } }, @@ -6415,6 +6462,21 @@ } } }, + "_EndogenousSectorsConfig": { + "description": "Configuration for `sector.endogenous_sectors` settings.", + "properties": { + "enable": { + "default": false, + "description": "Enabling to specify industry subsectors to be modelled endogenously.", + "type": "boolean" + }, + "subsectors": { + "description": "Energy-intensive industry subsectors that are modelled endogenously.", + "items": {}, + "type": "array" + } + } + }, "_EnhancedGeothermalConfig": { "description": "Configuration for `sector.enhanced_geothermal` settings.", "properties": { @@ -6922,6 +6984,11 @@ "default": false, "description": "Add biomass to methanol with carbon capture.", "type": "boolean" + }, + "meoh_to_oa": { + "default": false, + "description": "Add methanol-to-olefins/aromatics as a way to meet naphtha demand in industry sector for high value chemicals.", + "type": "boolean" } } }, @@ -8017,6 +8084,21 @@ } } }, + "_SteelBOFConfig": { + "description": "Configuration for `sector.steel_bof` settings.", + "properties": { + "pledge": { + "default": true, + "description": "Decommissions coal blast furnaces at their pledged phase out year.", + "type": "boolean" + }, + "pledge_delay": { + "default": 0, + "description": "Delays the decommission year by number of years.", + "type": "integer" + } + } + }, "_TechnologyMappingConfig": { "description": "Configuration for `electricity.estimate_renewable_capacities.technology_mapping` settings.", "properties": { @@ -10539,6 +10621,13 @@ }, "type": "array" }, + "grouping_years_industry": { + "description": "Intervals to group existing capacities for industry.", + "items": { + "type": "integer" + }, + "type": "array" + }, "threshold_capacity": { "default": 10, "description": "Capacities (MW) of generators and links below threshold are removed during add_existing_capacities.", @@ -11208,6 +11297,11 @@ "default": false, "description": "Add biomass to methanol with carbon capture.", "type": "boolean" + }, + "meoh_to_oa": { + "default": false, + "description": "Add methanol-to-olefins/aromatics as a way to meet naphtha demand in industry sector for high value chemicals.", + "type": "boolean" } } }, @@ -11549,6 +11643,41 @@ "type": "object" } } + }, + "endogenous_sectors": { + "description": "Configuration for `sector.endogenous_sectors` settings.", + "properties": { + "enable": { + "default": false, + "description": "Enabling to specify industry subsectors to be modelled endogenously.", + "type": "boolean" + }, + "subsectors": { + "description": "Energy-intensive industry subsectors that are modelled endogenously.", + "items": {}, + "type": "array" + } + } + }, + "hbi_relocation": { + "default": false, + "description": "Allow the relocation of iron ore reduction step.", + "type": "boolean" + }, + "steel_bof": { + "description": "Configuration for `sector.steel_bof` settings.", + "properties": { + "pledge": { + "default": true, + "description": "Decommissions coal blast furnaces at their pledged phase out year.", + "type": "boolean" + }, + "pledge_delay": { + "default": 0, + "description": "Delays the decommission year by number of years.", + "type": "integer" + } + } } } }, diff --git a/data/1-s2.0-S0196890424010586-mmc2.xlsx b/data/1-s2.0-S0196890424010586-mmc2.xlsx new file mode 100644 index 0000000000..0b613ceea3 Binary files /dev/null and b/data/1-s2.0-S0196890424010586-mmc2.xlsx differ diff --git a/data/gem/Global-Cement-and-Concrete-Tracker_July-2025.xlsx b/data/gem/Global-Cement-and-Concrete-Tracker_July-2025.xlsx new file mode 100644 index 0000000000..c2a64bb46c Binary files /dev/null and b/data/gem/Global-Cement-and-Concrete-Tracker_July-2025.xlsx differ diff --git a/rules/build_sector.smk b/rules/build_sector.smk index 45c35281ac..fe8f0461d3 100755 --- a/rules/build_sector.smk +++ b/rules/build_sector.smk @@ -1562,6 +1562,27 @@ def input_heat_source_power(w): } +rule build_industry_plants: + params: + countries=config_provider("countries"), + input: + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), + ammonia="data/ammonia_plants.csv", + isi_database="data/1-s2.0-S0196890424010586-mmc2.xlsx", + gem_gcct=rules.retrieve_gem_cement_concrete_tracker.output["xlsx"], + output: + industry_plants=resources("industry_plants_{clusters}.csv"), + threads: 1 + resources: + mem_mb=2000, + log: + logs("build_industry_plants_{clusters}.log"), + benchmark: + benchmarks("build_industry_plants_{clusters}") + script: + "../scripts/build_industry_plants.py" + + rule prepare_sector_network: message: "Preparing integrated sector-coupled energy network for {wildcards.clusters} clusters, {wildcards.planning_horizons} planning horizon, {wildcards.opts} electric options and {wildcards.sector_opts} sector options" @@ -1724,6 +1745,11 @@ rule prepare_sector_network: if config_provider("sector", "district_heating", "ates", "enable")(w) else [] ), + industry_sector_ratios=lambda w: ( + resources("industry_sector_ratios_{planning_horizons}.csv") + if config_provider("sector", "endogenous_sectors", "enable")(w) + else [] + ), output: resources( "networks/base_s_{clusters}_{opts}_{sector_opts}_{planning_horizons}.nc" diff --git a/rules/solve_myopic.smk b/rules/solve_myopic.smk index fb50153252..6a7576169c 100644 --- a/rules/solve_myopic.smk +++ b/rules/solve_myopic.smk @@ -14,6 +14,8 @@ rule add_existing_baseyear: costs=config_provider("costs"), heat_pump_sources=config_provider("sector", "heat_pump_sources"), energy_totals_year=config_provider("energy", "energy_totals_year"), + MWh_NH3_per_tNH3=config_provider("industry", "MWh_NH3_per_tNH3"), + MWh_MeOH_per_tMeOH=config_provider("industry", "MWh_MeOH_per_tMeOH"), input: network=resources( "networks/base_s_{clusters}_{opts}_{sector_opts}_{planning_horizons}.nc" @@ -27,6 +29,13 @@ rule add_existing_baseyear: "existing_heating_distribution_base_s_{clusters}_{planning_horizons}.csv" ), heating_efficiencies=resources("heating_efficiencies.csv"), + industry_plants=lambda w: ( + ( + resources("industry_plants_{clusters}.csv") + if config_provider("sector", "industry")(w) + else [] + ), + ), output: resources( "networks/base_s_{clusters}_{opts}_{sector_opts}_{planning_horizons}_brownfield.nc" diff --git a/rules/solve_perfect.smk b/rules/solve_perfect.smk index f4e5a73c08..620d053522 100644 --- a/rules/solve_perfect.smk +++ b/rules/solve_perfect.smk @@ -12,6 +12,8 @@ rule add_existing_baseyear: costs=config_provider("costs"), heat_pump_sources=config_provider("sector", "heat_pump_sources"), energy_totals_year=config_provider("energy", "energy_totals_year"), + countries=config_provider("countries"), + MWh_NH3_per_tNH3=config_provider("industry", "MWh_NH3_per_tNH3"), input: network=resources( "networks/base_s_{clusters}_{opts}_{sector_opts}_{planning_horizons}.nc" @@ -29,6 +31,9 @@ rule add_existing_baseyear: ), existing_heating="data/existing_infrastructure/existing_heating_raw.csv", heating_efficiencies=resources("heating_efficiencies.csv"), + regions_onshore=resources("regions_onshore_base_s_{clusters}.geojson"), + ammonia="data/ammonia_plants.csv", + isi_database="data/1-s2.0-S0196890424010586-mmc2.xlsx", output: resources( "networks/base_s_{clusters}_{opts}_{sector_opts}_{planning_horizons}_brownfield.nc" diff --git a/scripts/add_existing_baseyear.py b/scripts/add_existing_baseyear.py index 7e03eccb9d..d901ab5730 100644 --- a/scripts/add_existing_baseyear.py +++ b/scripts/add_existing_baseyear.py @@ -760,17 +760,309 @@ def add_heating_capacities_installed_before_baseyear( ) +def add_existing_industry( + n: pypsa.Network, + options: dict, + plant_fn: str, + grouping_years: list, + baseyear: int, + costs: pd.DataFrame, +) -> None: + """ + Add existing industry plants depending on the industry options. + + Parameters + ---------- + n : pypsa.Network + Network to modify + options: dict + Sector config + plant_fn: str + Path to file for existing industry plants + grouping_years_industry: list + Intervals to group capacities + baseyear : int + Base year for analysis + costs : pd.DataFrame + Technology costs + """ + logger.info("Add existing industry plants.") + + plant_data = pd.read_csv(plant_fn) + + # fill missing build_year with average + mean_nonzero = plant_data.groupby("carrier")["build_year"].transform("mean").round() + plant_data["build_year"] = plant_data["build_year"].fillna(mean_nonzero) + plant_data["grouping_year"] = 0 + plant_data["Out"] = plant_data["Out"].fillna(0) + valid_mask = plant_data["build_year"] < baseyear + valid_mask = valid_mask & plant_data["build_year"].notna() + valid_years = plant_data.loc[valid_mask, "build_year"] + indices = np.searchsorted(grouping_years, valid_years, side="right") + plant_data.loc[valid_years.index, "grouping_year"] = np.array(grouping_years)[ + indices + ] + + plant_data = plant_data.groupby( + ["bus", "country", "carrier", "grouping_year", "Out"], as_index=False + )["p_set"].sum() + + if ( + options["endogenous_sectors"]["enable"] + and "cement" in options["endogenous_sectors"]["subsectors"] + ): + # add cement + cement = plant_data[plant_data.carrier == "cement"] + cement.index = ( + cement["bus"] + " cement kiln-" + cement["grouping_year"].astype(str) + ) + cement["clinker"] = ( + cement["bus"] + " cement production-" + cement["grouping_year"].astype(str) + ) + # get rid of capacities without demand + cement_buses = n.loads[n.loads.carrier == "cement"].bus.to_list() + cement_buses = [x.replace(" cement", "") for x in cement_buses] + cement = cement[cement.bus.isin(cement_buses)] + + logger.info(f"Adding {len(cement)} existing cement links.") + + # clinker production + gas_input = costs.at["cement dry clinker", "gas-input"] + heat_input = costs.at["cement dry clinker", "heat-input"] + electricity_input = costs.at["cement dry clinker", "electricity-input"] + # process emission from calcination + gas emissions + # https://www.ipcc-nggip.iges.or.jp/efdb/ef_detail.php + co2_emission = ( + 0.5071 / heat_input + + costs.at["gas", "CO2 intensity"] * gas_input / heat_input + ) + + n.add( + "Link", + cement.index, + bus0=[bus + " cement heat" for bus in cement.bus], + bus1=[bus + " clinker" for bus in cement.bus], + bus2=[bus + " gas" for bus in cement.bus] + if snakemake.params.sector["gas_network"] + else "EU gas", + bus3=cement.bus, + bus4=[bus + " cement emission" for bus in cement.bus], + carrier="cement kiln", + p_nom_extendable=False, + p_nom=cement["p_set"] + .mul(heat_input) + .mul(costs.at["cement finishing", "clinker-input"]) + .div(8760) + .values, + capital_cost=costs.at["cement dry clinker", "capital_cost"] / heat_input, + efficiency=1 / heat_input, + efficiency2=-gas_input / heat_input, + efficiency3=-electricity_input / heat_input, + efficiency4=co2_emission, + build_year=cement.grouping_year, + lifetime=costs.at["cement dry clinker", "lifetime"], + ) + + # cement finishing + electricity_input = ( + costs.at["cement finishing", "electricity-input"] + / costs.at["cement finishing", "clinker-input"] + ) + clinker_input = costs.at["cement finishing", "clinker-input"] + n.add( + "Link", + cement.clinker.to_list(), + bus0=[bus + " clinker" for bus in cement.bus], + bus1=[bus + " cement" for bus in cement.bus], + bus2=cement.bus.to_list(), + carrier="cement finishing", + p_nom_extendable=False, + p_nom=cement["p_set"].mul(clinker_input).div(8760).values, + capital_cost=costs.at["cement finishing", "capital_cost"] / clinker_input, + efficiency=1 / clinker_input, + efficiency2=-electricity_input, + build_year=cement.grouping_year.to_list(), + lifetime=costs.at["cement finishing", "lifetime"], + ) + + if options["ammonia"]: + ammonia = plant_data[plant_data.carrier == "Haber-Bosch"] + index = [ + f"{bus} Haber-Bosch-{year}" + for bus, year in zip(ammonia["bus"], ammonia["grouping_year"]) + ] + ammonia.index = index + logger.info(f"Adding {len(ammonia)} existing Haber-Bosch links.") + # implementing via H2 + n.add( + "Link", + ammonia.index, + bus0=[bus for bus in ammonia.bus], + bus1=[bus + " NH3" for bus in ammonia.bus] + if snakemake.params.sector["ammonia"] == "regional" + else "EU NH3", + bus2=[bus + " H2" for bus in ammonia.bus], + p_nom=ammonia["p_set"] + .mul(snakemake.params.MWh_NH3_per_tNH3) + .div(costs.at["Haber-Bosch", "electricity-input"]) + .div(8760) + .values, + p_nom_extendable=False, + carrier="Haber-Bosch", + efficiency=1 / costs.at["Haber-Bosch", "electricity-input"], + efficiency2=-costs.at["Haber-Bosch", "hydrogen-input"] + / costs.at["Haber-Bosch", "electricity-input"], + capital_cost=costs.at["Haber-Bosch", "capital_cost"] + / costs.at["Haber-Bosch", "electricity-input"], + marginal_cost=costs.at["Haber-Bosch", "VOM"] + / costs.at["Haber-Bosch", "electricity-input"], + build_year=ammonia["grouping_year"], + lifetime=costs.at["Haber-Bosch", "lifetime"], + ) + + # methanol + meoh = plant_data[plant_data.carrier == "grey methanol"] + index = [ + f"{bus} grey methanol-{year}" + for bus, year in zip(meoh["bus"], meoh["grouping_year"]) + ] + meoh.index = index + logger.info(f"Adding {len(meoh)} existing methanolisation links.") + # grey methanol + capital_cost = ( + costs.at["SMR", "capital_cost"] + + costs.at["methanolisation", "capital_cost"] + * costs.at["grey methanol synthesis", "efficiency"] + ) + co2_emissions = ( + costs.at["gas", "CO2 intensity"] + - costs.at["grey methanol synthesis", "efficiency"] + * costs.at["methanol", "CO2 intensity"] + ) + n.add( + "Link", + meoh.index, + bus0=[bus + " gas" for bus in meoh.bus] + if snakemake.params.sector["gas_network"] + else "EU gas", + bus1="EU methanol", + bus2="co2 atmosphere", + p_nom_extendable=False, + p_nom=meoh["p_set"] + .mul(snakemake.params.MWh_MeOH_per_tMeOH) + .div(costs.at["grey methanol synthesis", "efficiency"]) + .div(8760), + carrier="grey methanol", + efficiency=costs.at["grey methanol synthesis", "efficiency"], + efficiency2=co2_emissions, + capital_cost=capital_cost, + build_year=meoh.grouping_year, + lifetime=costs.at["SMR", "lifetime"], + ) + + if ( + options["endogenous_sectors"]["enable"] + and "steel" in options["endogenous_sectors"]["subsectors"] + ): + # natural gas DRI + ng_dri = plant_data[plant_data.carrier == "gas DRI"] + logger.info(f"Adding {len(ng_dri)} existing gas DRI links.") + index = [ + f"{bus} gas DRI-{year}" + for bus, year in zip(ng_dri["bus"], ng_dri["grouping_year"]) + ] + ng_dri.index = index + electricity_input = costs.at[ + "hydrogen direct iron reduction furnace", "electricity-input" + ] + marginal_cost = ( + costs.at["iron ore DRI-ready", "commodity"] + * costs.at["hydrogen direct iron reduction furnace", "ore-input"] + / electricity_input + ) + + n.add( + "Link", + ng_dri.index, + bus0=[bus for bus in ng_dri.bus], + bus1=[bus + " hbi" for bus in ng_dri.bus] + if not snakemake.params.sector["hbi_relocation"] + else "EU hbi", + bus2=[bus + " DRI reduction" for bus in ng_dri.bus], + p_nom=ng_dri["p_set"].mul(electricity_input).div(8760).values, + p_nom_extendable=False, + carrier="DRI", + efficiency=1 / electricity_input, + efficiency2=-1 / electricity_input, + capital_cost=costs.at[ + "hydrogen direct iron reduction furnace", "capital_cost" + ] + / electricity_input, + marginal_cost=marginal_cost, + build_year=ng_dri["grouping_year"], + lifetime=costs.at["hydrogen direct iron reduction furnace", "lifetime"], + ) + # BF-BOF + bof = plant_data[plant_data.carrier == "BOF"] + + if options["steel_bof"]["pledge"]: + lifetime = ( + bof["Out"] - bof["grouping_year"] + options["steel_bof"]["pledge_delay"] + ) + lifetime[lifetime < 0] = costs.at[ + "blast furnace-basic oxygen furnace", "lifetime" + ] + else: + lifetime = costs.at["blast furnace-basic oxygen furnace", "lifetime"] + # regroup + bof = bof.groupby( + ["bus", "country", "carrier", "grouping_year"], as_index=False + )["p_set"].sum() + + index = [ + f"{bus} BOF-{year}-{out}" + for bus, year, out in zip(bof["bus"], bof["grouping_year"], bof["Out"]) + ] + bof.index = index + logger.info(f"Adding {len(bof)} existing BOF links.") + + coal_input = costs.at["blast furnace-basic oxygen furnace", "coal-input"] + marginal_cost = ( + costs.at["iron ore DRI-ready", "commodity"] + * costs.at["blast furnace-basic oxygen furnace", "ore-input"] + / coal_input + ) + + n.add( + "Link", + bof.index, + bus0="EU coal", + bus1=[bus + " steel" for bus in bof.bus], + bus2=[bus + " BOF emission" for bus in bof.bus], + p_nom=bof["p_set"].mul(coal_input).div(8760).values, + p_nom_extendable=False, + carrier="BOF", + efficiency=1 / coal_input, + efficiency2=costs.at["coal", "CO2 intensity"], + marginal_cost=marginal_cost, + capital_cost=costs.at["blast furnace-basic oxygen furnace", "capital_cost"] + / coal_input, + build_year=bof["grouping_year"], + lifetime=lifetime.to_list(), + ) + + if __name__ == "__main__": if "snakemake" not in globals(): from scripts._helpers import mock_snakemake snakemake = mock_snakemake( "add_existing_baseyear", - configfiles="config/test/config.myopic.yaml", - clusters="5", + configfiles="config/config.default.yaml", + clusters="50", opts="", sector_opts="", - planning_horizons=2030, + planning_horizons=2025, ) configure_logging(snakemake) # pylint: disable=E0606 @@ -841,6 +1133,19 @@ def add_heating_capacities_installed_before_baseyear( if options.get("cluster_heat_buses", False): cluster_heat_buses(n) + # add existing industry plants + if options["industry"]: + add_existing_industry( + n=n, + options=options, + plant_fn=snakemake.input.industry_plants[0], + grouping_years=snakemake.params.existing_capacities[ + "grouping_years_industry" + ], + baseyear=snakemake.params.baseyear, + costs=costs, + ) + n.meta = dict(snakemake.config, **dict(wildcards=dict(snakemake.wildcards))) sanitize_custom_columns(n) diff --git a/scripts/build_industry_plants.py b/scripts/build_industry_plants.py new file mode 100644 index 0000000000..f7ade1be63 --- /dev/null +++ b/scripts/build_industry_plants.py @@ -0,0 +1,209 @@ +# SPDX-FileCopyrightText: Contributors to PyPSA-Eur +# +# SPDX-License-Identifier: MIT +""" +Adds existing power and heat generation capacities for initial planning +horizon. +""" + +import logging +from types import SimpleNamespace + +import country_converter as coco +import geopandas as gpd +import numpy as np +import pandas as pd +import pycountry + +from scripts._helpers import ( + configure_logging, + set_scenario_config, +) + +logger = logging.getLogger(__name__) +cc = coco.CountryConverter() +idx = pd.IndexSlice +spatial = SimpleNamespace() + + +def country_to_code(name): + try: + return pycountry.countries.lookup(name).alpha_2 + except LookupError: + return None + + +def prepare_gem_database(regions): + """ + Load GEM database of cement plants and map onto bus regions. + """ + df = pd.read_excel( + snakemake.input.gem_gcct, + sheet_name="Plant Data", + na_values=["N/A", "unknown", ">0"], + ) + + df["country_code"] = df["Country/Area"].apply(country_to_code) + # XK not in pycountry + df.loc[df["Country/Area"] == "Kosovo", "country_code"] = "XK" + + df = df[df.country_code.isin(snakemake.params.countries)] + + df["Start date"] = pd.to_numeric( + df["Start date"].str.split("-").str[0], errors="coerce" + ) + + latlon = ( + df["Coordinates"] + .str.split(", ", expand=True) + .rename(columns={0: "lat", 1: "lon"}) + ) + geometry = gpd.points_from_xy(latlon["lon"], latlon["lat"]) + gdf = gpd.GeoDataFrame(df, geometry=geometry, crs="EPSG:4326") + + gdf = gpd.sjoin_nearest(gdf, regions.to_crs("EPSG:4326"), how="left") + + gdf.rename(columns={"name": "bus"}, inplace=True) + gdf["country"] = gdf.bus.str[:2] + + # just get bus, country, grouping_year, plant_size, date_in, date_out, + # get plants that are still operating + gdf = gdf[gdf["Operating status"] == "operating"] + + gdf.rename( + columns={ + "Cement Capacity (millions metric tonnes per annum)": "p_set", + "Start date": "build_year", + }, + inplace=True, + ) + gdf["p_set"] *= 1e6 + gdf["carrier"] = "cement" + gdf["Out"] = 0 + + cement = gdf[["bus", "country", "carrier", "p_set", "build_year", "Out"]] + + return cement + + +def prepare_plant_data( + regions: gpd, + isi_database: str, +) -> tuple[pd.DataFrame, gpd.GeoDataFrame]: + """ + Reads in the Fraunhofer ISI database with high resolution plant data and maps them to the bus regions. + Returns the database as df. + + Parameters + ---------- + regions : gpd + onshore regions on which the industry sites are mapped to + isi_database: str + path to the fraunhofer isi database + """ + + isi_data = pd.read_excel(isi_database, sheet_name="Database", index_col=1) + # assign bus region to each plant + geometry = gpd.points_from_xy(isi_data["Longitude"], isi_data["Latitude"]) + plant_data = gpd.GeoDataFrame(isi_data, geometry=geometry, crs="EPSG:4326") + plant_data = gpd.sjoin_nearest(plant_data, regions.to_crs("EPSG:4326"), how="left") + plant_data.rename(columns={"name": "bus"}, inplace=True) + # filter for countries in model scope + plant_data = plant_data[plant_data.Country.isin(snakemake.params.countries)] + # replace UK with GB in Country column + plant_data["country"] = plant_data["Country"].replace("UK", "GB") + # add carrier column + carrier_dict = { + "Blast furnace": "BOF", + "Direct reduction NG": "gas DRI", + "Ammonia SMR": "Haber-Bosch", + "Methanol SMR": "grey methanol", + } + plant_data["carrier"] = plant_data["Process status qup"].replace(carrier_dict) + # get build_year + plant_data["build_year"] = ( + plant_data["Year of last modernisation"] + .replace("x", np.nan) + .fillna(plant_data["Last Relining"]) + ) + plant_data.rename( + columns={"Production in tons (calibrated)": "p_set"}, inplace=True + ) + plant_data = plant_data[["bus", "country", "carrier", "p_set", "build_year", "Out"]] + + return plant_data + + +def prepare_ammonia_data( + regions: gpd.GeoDataFrame, + plant_data: pd.DataFrame, +) -> pd.DataFrame: + """ + Adds ammonia production sites from the data folder and adds them for all countries that are not included in the Fraunhofer ISI database. + + Parameters + ---------- + regions : gpd + onshore regions on which the industry sites are mapped to + plant_data: pd.DataFrame + DataFrame with plant data from the Fraunhofer ISI database + """ + df = pd.read_csv(snakemake.input.ammonia, index_col=0) + + geometry = gpd.points_from_xy(df.Longitude, df.Latitude) + gdf = gpd.GeoDataFrame(df, geometry=geometry, crs="EPSG:4326") + + gdf = gpd.sjoin(gdf, regions, how="inner", predicate="within") + + gdf.rename(columns={"name": "bus"}, inplace=True) + gdf["country"] = gdf.bus.str[:2] + # filter for countries that are missing + gdf = gdf[ + (~gdf.country.isin(plant_data.country.unique())) + & (gdf.country.isin(snakemake.params.countries)) + ] + # following approach from build_industrial_distribution_key.py + min_prod_per_country = gdf.groupby("country")["Ammonia [kt/a]"].transform("min") + gdf["Ammonia [kt/a]"] = gdf["Ammonia [kt/a]"].fillna(0.5 * min_prod_per_country) + + # missing data + gdf.drop(gdf[gdf["Ammonia [kt/a]"].isna()].index, inplace=True) + + # get average plant age: + avg_age = plant_data[plant_data.carrier == "Haber-Bosch"]["build_year"].mean() + # restructure to fit other data sources + gdf["build_year"] = avg_age + gdf["Out"] = 0 + gdf["carrier"] = "Haber-Bosch" + gdf["Ammonia [kt/a]"] *= 1e3 + gdf.rename(columns={"Ammonia [kt/a]": "p_set"}, inplace=True) + gdf = gdf[["bus", "country", "carrier", "p_set", "build_year", "Out"]] + + return gdf + + +if __name__ == "__main__": + if "snakemake" not in globals(): + from scripts._helpers import mock_snakemake + + snakemake = mock_snakemake( + "build_industry_plants", + configfiles="config/config.default.yaml", + clusters="39", + ) + + configure_logging(snakemake) + set_scenario_config(snakemake) + + regions = gpd.read_file(snakemake.input.regions_onshore).set_index("name") + + # get cement plant data + cement = prepare_gem_database(regions) + # get data from fraunhofer database + fh_data = prepare_plant_data(regions, snakemake.input.isi_database) + # ammonia plants non-EU27 + ammonia = prepare_ammonia_data(regions, fh_data) + + industry_plants = pd.concat([cement, fh_data, ammonia], ignore_index=True) + + industry_plants.to_csv(snakemake.output.industry_plants) diff --git a/scripts/lib/validation/config/existing_capacities.py b/scripts/lib/validation/config/existing_capacities.py index cc39f84103..8716e40a8d 100644 --- a/scripts/lib/validation/config/existing_capacities.py +++ b/scripts/lib/validation/config/existing_capacities.py @@ -53,6 +53,18 @@ class ExistingCapacitiesConfig(ConfigModel): ], description="Intervals to group existing capacities for heat.", ) + grouping_years_industry: list[int] = Field( + default_factory=lambda: [ + 1995, + 2000, + 2005, + 2010, + 2015, + 2020, + 2025, + ], + description="Intervals to group existing capacities for industry.", + ) threshold_capacity: float = Field( 10, description="Capacities (MW) of generators and links below threshold are removed during add_existing_capacities.", diff --git a/scripts/lib/validation/config/sector.py b/scripts/lib/validation/config/sector.py index 5801f7fcbe..c47fe0825d 100644 --- a/scripts/lib/validation/config/sector.py +++ b/scripts/lib/validation/config/sector.py @@ -8,7 +8,7 @@ See docs in https://pypsa-eur.readthedocs.io/en/latest/configuration.html#sector """ -from typing import Any +from typing import Any, Literal from pydantic import BaseModel, ConfigDict, Field @@ -222,6 +222,10 @@ class _MethanolConfig(BaseModel): biomass_to_methanol_cc: bool = Field( False, description="Add biomass to methanol with carbon capture." ) + meoh_to_oa: bool = Field( + False, + description="Add methanol-to-olefins/aromatics as a way to meet naphtha demand in industry sector for high value chemicals.", + ) class _TransmissionEfficiencyConfig(BaseModel): @@ -363,6 +367,34 @@ class _ImportsConfig(BaseModel): ) +class _EndogenousSectorsConfig(BaseModel): + """Configuration for `sector.endogenous_sectors` settings.""" + + enable: bool = Field( + False, + description="Enabling to specify industry subsectors to be modelled endogenously.", + ) + subsectors: list = Field( + default_factory=lambda: ["steel", "cement"], + description="Energy-intensive industry subsectors that are modelled endogenously.", + ) + + +class _SteelBOFConfig(BaseModel): + """Configuration for `sector.steel_bof` settings.""" + + pledge: bool = Field( + True, + description="Decommissions coal blast furnaces at their pledged phase out year.", + ) + pledge_delay: int = Field( + 0, description="Delays the decommission year by number of years." + ) + + +AllowedEndogenousSectors = Literal["steel", "cement"] + + class SectorConfig(BaseModel): """Configuration for `sector` settings.""" @@ -910,3 +942,16 @@ class SectorConfig(BaseModel): imports: _ImportsConfig = Field( default_factory=_ImportsConfig, description="Imports configuration." ) + + # Endogenous sectors + endogenous_sectors: _EndogenousSectorsConfig = Field( + default_factory=_EndogenousSectorsConfig, + description="Sectors modelled endogenously.", + ) + hbi_relocation: bool = Field( + False, + description="Allow the relocation of iron ore reduction step.", + ) + steel_bof: _SteelBOFConfig = Field( + default_factory=_SteelBOFConfig, description="Steel BOF configuration." + ) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index d9434ef76e..3fc8fdbb43 100755 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -221,6 +221,27 @@ def define_spatial(nodes, options): spatial.geothermal_heat.nodes = ["EU enhanced geothermal systems"] spatial.geothermal_heat.locations = ["EU"] + # industry + spatial.hbi = SimpleNamespace() + if options["hbi_relocation"]: + spatial.hbi.nodes = ["EU hbi"] + spatial.hbi.location = ["EU"] + else: + spatial.hbi.nodes = nodes + " hbi" + spatial.hbi.location = nodes + + spatial.steel = SimpleNamespace() + spatial.steel.nodes = nodes + " steel" + spatial.steel.location = nodes + + spatial.cement = SimpleNamespace() + spatial.cement.nodes = nodes + " cement" + spatial.cement.heat = nodes + " cement heat" + spatial.cement.location = nodes + spatial.clinker = SimpleNamespace() + spatial.clinker.nodes = nodes + " clinker" + spatial.clinker.location = nodes + return spatial @@ -1215,6 +1236,36 @@ def add_methanol_reforming_cc(n, costs): ) +def add_meoh_to_oa(n, costs): + logger.info("Adding MeOH-to-Olefins/Aromatics for High Value Chemicals.") + tech = "methanol-to-olefins/aromatics" + + naphtha_per_t_hvc = 12.622 # MWh per tonne of HVC (taken from sector ratios) + + process_emissions = ( + costs.at[tech, "carbondioxide-output"] / costs.at[tech, "methanol-input"] + ) + n.add("Carrier", "methanol-to-olefins/aromatics") + + n.add( + "Link", + spatial.nodes, + suffix=f" {tech}", + bus0=spatial.methanol.nodes, + bus1=spatial.oil.naphtha, + bus2=spatial.nodes, + bus3=spatial.co2.process_emissions, + efficiency=1 + / costs.at[tech, "methanol-input"] + * naphtha_per_t_hvc, # because MtO produces t HVC not MWh no MWh HVC + efficiency2=-costs.at[tech, "electricity-input"] + / costs.at[tech, "methanol-input"], + efficiency3=process_emissions, + carrier=tech, + p_nom_extendable=True, + ) + + def add_dac(n, costs): heat_carriers = ["urban central heat", "services urban decentral heat"] heat_buses = n.buses.index[n.buses.carrier.isin(heat_carriers)] @@ -3741,6 +3792,9 @@ def add_methanol( if methanol_options["methanol_reforming_cc"]: add_methanol_reforming_cc(n=n, costs=costs) + if methanol_options["meoh_to_oa"]: + add_meoh_to_oa(n=n, costs=costs) + def add_biomass( n, @@ -4468,6 +4522,64 @@ def add_biomass( ) +def adjust_industry_demand(nodes): + # import ratios [MWh/t_Material] + fn = snakemake.input.industry_sector_ratios + sector_ratios = pd.read_csv(fn, header=[0, 1], index_col=0) + + # material demand per node and industry [kt/a] + fn = snakemake.input.industrial_production + nodal_production = pd.read_csv(fn, index_col=0) / 1e3 # kt/a -> Mt/a + + nodal_sector_ratios = pd.concat( + {node: sector_ratios[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)).T + # rename the columns to correct unit + nodal_df.columns.name = "TWh/a" + + # add up all industry sectors except the ones endogenously modelled + endogenous_sector = snakemake.params.sector["endogenous_sectors"]["subsectors"] + + sector_dict = { + "steel": ["Integrated steelworks", "DRI + Electric arc"], + "cement": "Cement", + } + keys = [sector_dict[k] for k in endogenous_sector if k in sector_dict] + keys = [x for item in keys for x in (item if isinstance(item, list) else [item])] + + remaining_sectors = ~nodal_df.index.get_level_values(1).isin(keys) + + remaining_demand = ( + nodal_df.loc[(nodes, remaining_sectors), :].groupby(level=0).sum() + ).mul(1e6) + remaining_demand.columns.name = "MWh/a" + remaining_demand.rename( + columns={ + "elec": "electricity", + "biomass": "solid biomass", + "heat": "low-temperature heat", + }, + inplace=True, + ) + # adjust initial demand + + # get the demand of steel and cement + steel = nodal_production[["Integrated steelworks", "DRI + Electric arc"]].sum( + axis=1 + ) + steel.name = "Mt/a" + cement = nodal_production["Cement"] + cement.name = "Mt/a" + + return remaining_demand, steel, cement + + def add_industry( n: pypsa.Network, costs: pd.DataFrame, @@ -4563,7 +4675,18 @@ def add_industry( nyears = nhours / 8760 # 1e6 to convert TWh to MWh - industrial_demand = pd.read_csv(industrial_demand_file, index_col=0) * 1e6 * nyears + industrial_demand_raw = ( + pd.read_csv(industrial_demand_file, index_col=0) * 1e6 * nyears + ) + if snakemake.params.sector["endogenous_sectors"]["enable"]: + # adjust the industrial energy demand excluding endogenously modelled carriers + adjusted_demand, steel, cement = adjust_industry_demand(nodes) + adjusted_demand["current electricity"] = industrial_demand_raw[ + "current electricity" + ] + industrial_demand = adjusted_demand.copy() + else: + industrial_demand = industrial_demand_raw.copy() if not options["biomass"]: raise ValueError( @@ -5128,6 +5251,426 @@ def add_industry( efficiency2=costs.at["coal", "CO2 intensity"], ) + if ( + snakemake.params.sector["endogenous_sectors"]["enable"] + and "steel" in snakemake.params.sector["endogenous_sectors"]["subsectors"] + ): + # add steel load + logger.info("Adding steel production technologies.") + n.add("Carrier", "steel") + n.add("Carrier", "hbi") + + steel.index = steel.index + " steel" + + n.add( + "Bus", + spatial.steel.nodes, + location=spatial.steel.location, + carrier="steel", + unit="t", + ) + + n.add( + "Bus", + spatial.hbi.nodes, + location=spatial.hbi.location, + carrier="hbi", + unit="t", + ) + + p_set = steel * 1e6 / nhours + n.add( + "Load", + spatial.steel.nodes, + bus=spatial.steel.nodes, + carrier="steel", + p_set=p_set, + ) + # structure allows switching from gas to H2 + n.add("Carrier", "DRI reduction agent") + n.add( + "Bus", + spatial.nodes, + suffix=" DRI reduction", + location=spatial.nodes, + carrier="DRI reduction agent", + unit="MWh", + ) + # H2 reduction agent + n.add("Carrier", "H2 DRI") + hydrogen_input = costs.at[ + "hydrogen direct iron reduction furnace", "hydrogen-input" + ] + n.add( + "Link", + spatial.nodes, + suffix=" H2 DRI reduction", + carrier="H2 DRI", + p_nom=0, + p_nom_extendable=True, + bus0=spatial.h2.nodes, + bus1=spatial.nodes + " DRI reduction", + efficiency=1 / hydrogen_input, + ) + # gas reduction agent + # bus to decide whether to capture emissions or not + n.add("Carrier", "steel emission") + n.add( + "Bus", + spatial.nodes, + suffix=" gas DRI emission", + location=spatial.nodes, + carrier="steel emission", + unit="t", + ) + n.add("Carrier", "gas DRI") + gas_input = costs.at["natural gas direct iron reduction furnace", "gas-input"] + n.add( + "Link", + spatial.nodes, + suffix=" gas DRI reduction", + carrier="gas DRI", + p_nom=0, + p_nom_extendable=True, + bus0=spatial.gas.nodes, + bus1=spatial.nodes + " DRI reduction", + bus2=spatial.nodes + " gas DRI emission", + efficiency=1 / gas_input, + efficiency2=costs.at["gas", "CO2 intensity"], + ) + + # DRI + electricity_input = costs.at[ + "hydrogen direct iron reduction furnace", "electricity-input" + ] + marginal_cost = ( + costs.at["iron ore DRI-ready", "commodity"] + * costs.at["hydrogen direct iron reduction furnace", "ore-input"] + / electricity_input + ) + n.add("Carrier", "DRI") + n.add( + "Link", + spatial.nodes, + suffix=" DRI", + carrier="DRI", + capital_cost=costs.at[ + "hydrogen direct iron reduction furnace", "capital_cost" + ] + / electricity_input, + marginal_cost=marginal_cost, + p_nom=0, + p_nom_extendable=True, + bus0=spatial.nodes, + bus1=spatial.hbi.nodes, + bus2=spatial.nodes + " DRI reduction", + efficiency=1 / electricity_input, + efficiency2=-1 / electricity_input, + lifetime=costs.at["hydrogen direct iron reduction furnace", "lifetime"], + ) + + # HBI to steel via electric arc furnace + electricity_input = costs.at["electric arc furnace", "electricity-input"] + n.add("Carrier", "EAF") + + n.add( + "Link", + spatial.nodes, + suffix=" EAF", + carrier="EAF", + capital_cost=costs.at["electric arc furnace", "capital_cost"] + / electricity_input, + p_nom=0, + p_nom_extendable=True, + bus0=spatial.nodes, + bus1=spatial.steel.nodes, + bus2=spatial.hbi.nodes, + efficiency=1 / electricity_input, + efficiency2=-costs.at["electric arc furnace", "hbi-input"] + / electricity_input, + lifetime=costs.at["electric arc furnace", "lifetime"], + ) + + # BOF + coal_input = costs.at["blast furnace-basic oxygen furnace", "coal-input"] + marginal_cost = ( + costs.at["iron ore DRI-ready", "commodity"] + * costs.at["blast furnace-basic oxygen furnace", "ore-input"] + / coal_input + ) + n.add("Carrier", "BOF") + n.add( + "Bus", + spatial.nodes, + suffix=" BOF emission", + location=spatial.nodes, + carrier="steel emission", + unit="t", + ) + n.add( + "Link", + spatial.nodes, + suffix=" BOF", + carrier="BOF", + capital_cost=costs.at["blast furnace-basic oxygen furnace", "capital_cost"] + / coal_input, + p_nom=0, + p_nom_extendable=True, + marginal_cost=marginal_cost, + bus0="EU coal", + bus1=spatial.steel.nodes, + bus2=spatial.nodes + " BOF emission", + efficiency=1 / coal_input, + efficiency2=costs.at["coal", "CO2 intensity"], + lifetime=costs.at["blast furnace-basic oxygen furnace", "lifetime"], + ) + # either capture or emitt co2 + n.add( + "Link", + spatial.nodes, + suffix=" gas DRI emission", + carrier="steel emission", + bus0=spatial.nodes + " gas DRI emission", + bus1="co2 atmosphere", + efficiency=1, + p_nom=0, + p_nom_extendable=True, + ) + n.add( + "Link", + spatial.nodes, + suffix=" BOF emission", + carrier="steel emission", + bus0=spatial.nodes + " BOF emission", + bus1="co2 atmosphere", + efficiency=1, + p_nom=0, + p_nom_extendable=True, + ) + + # Assumption: enough waste heat to recover sorbent + capture_rate = costs.at["steel carbon capture retrofit", "capture_rate"] + electricity_input = costs.at[ + "steel carbon capture retrofit", "electricity-input" + ] # MWh/t_CO2 + n.add("Carrier", "steel emission CC") + n.add( + "Link", + spatial.nodes, + suffix=" gas DRI emission CC", + carrier="steel emission CC", + capital_cost=costs.at["steel carbon capture retrofit", "capital_cost"], + bus0=spatial.nodes + " gas DRI emission", + bus1=spatial.co2.nodes, + bus2=spatial.nodes, + bus3="co2 atmosphere", + efficiency=capture_rate, + efficiency2=-electricity_input, + efficiency3=1 - capture_rate, + p_nom=0, + p_nom_extendable=True, + lifetime=costs.at["steel carbon capture retrofit", "lifetime"], + ) + n.add( + "Link", + spatial.nodes, + suffix=" BOF emission CC", + carrier="steel emission CC", + capital_cost=costs.at["steel carbon capture retrofit", "capital_cost"], + bus0=spatial.nodes + " BOF emission", + bus1=spatial.co2.nodes, + bus2=spatial.nodes, + bus3="co2 atmosphere", + efficiency=capture_rate, + efficiency2=-electricity_input, + efficiency3=1 - capture_rate, + p_nom=0, + p_nom_extendable=True, + lifetime=costs.at["steel carbon capture retrofit", "lifetime"], + ) + + if ( + snakemake.params.sector["endogenous_sectors"]["enable"] + and "cement" in snakemake.params.sector["endogenous_sectors"]["subsectors"] + ): + # add cement processes + logger.info("Adding cement production capacities.") + n.add("Carrier", "cement") + n.add("Carrier", "clinker") + n.add("Carrier", "cement emission") + n.add("Carrier", "cement heat") + + cement.index = cement.index + " cement" + + n.add( + "Bus", + spatial.cement.heat, + location=spatial.cement.location, + carrier="cement heat", + unit="MWh", + ) + + n.add( + "Bus", + spatial.cement.nodes, + location=spatial.cement.location, + carrier="cement", + unit="t", + ) + n.add( + "Bus", + spatial.clinker.nodes, + location=spatial.clinker.location, + carrier="clinker", + unit="t", + ) + + n.add( + "Bus", + spatial.cement.nodes, + suffix=" emission", + location=spatial.cement.location, + carrier="cement emission", + unit="t", + ) + + p_set = cement * 1e6 / nhours + n.add( + "Load", + cement.index, + bus=spatial.cement.nodes, + carrier="cement", + p_set=p_set, + ) + # add heat links + n.add("Carrier", "cement heat biomass") + n.add("Carrier", "cement heat waste") + n.add("Carrier", "cement heat gas") + + n.add( + "Link", + spatial.cement.heat, + suffix=" biomass", + bus0=spatial.biomass.nodes, + bus1=spatial.cement.heat, + bus2=spatial.cement.nodes + " emission", + bus3="co2 atmosphere", + carrier="cement heat biomass", + p_nom=1e6, + efficiency=0.9, + efficiency2=costs.at["solid biomass", "CO2 intensity"], + efficiency3=-costs.at["solid biomass", "CO2 intensity"], + ) + n.add( + "Link", + spatial.cement.heat, + suffix=" waste", + bus0=spatial.oil.non_sequestered_hvc, + bus1=spatial.cement.heat, + bus2=spatial.cement.nodes + " emission", + carrier="cement heat waste", + p_nom=1e6, + efficiency=0.9, + efficiency2=costs.at["oil", "CO2 intensity"], + ) + n.add( + "Link", + spatial.cement.nodes, + suffix=" heat gas", + bus0=spatial.gas.nodes, + bus1=spatial.cement.heat, + bus2=spatial.cement.nodes + " emission", + carrier="cement heat gas", + p_nom=1e6, + efficiency=1, + efficiency2=costs.at["gas", "CO2 intensity"], + ) + + # clinker production + gas_input = costs.at["cement dry clinker", "gas-input"] + heat_input = costs.at["cement dry clinker", "heat-input"] + electricity_input = costs.at["cement dry clinker", "electricity-input"] + # process emission from calcination + gas emissions + # https://www.ipcc-nggip.iges.or.jp/efdb/ef_detail.php + co2_emission = 0.5071 + costs.at["gas", "CO2 intensity"] * gas_input + n.add("Carrier", "cement kiln") + n.add( + "Link", + spatial.clinker.nodes, + suffix=" kiln", + bus0=spatial.cement.heat, + bus1=spatial.clinker.nodes, + bus2=spatial.gas.nodes, + bus3=spatial.nodes, + bus4=spatial.cement.nodes + " emission", + carrier="cement kiln", + p_nom_extendable=True, + capital_cost=costs.at["cement dry clinker", "capital_cost"] / heat_input, + efficiency=1 / heat_input, + efficiency2=-gas_input / heat_input, + efficiency3=-electricity_input / heat_input, + efficiency4=co2_emission / heat_input, + lifetime=costs.at["cement dry clinker", "lifetime"], + ) + + # cement finishing + electricity_input = ( + costs.at["cement finishing", "electricity-input"] + / costs.at["cement finishing", "clinker-input"] + ) + clinker_input = costs.at["cement finishing", "clinker-input"] + + n.add("Carrier", "cement finishing") + n.add( + "Link", + spatial.cement.nodes, + suffix=" production", + bus0=spatial.clinker.nodes, + bus1=spatial.cement.nodes, + bus2=spatial.nodes, + carrier="cement finishing", + p_nom_extendable=True, + capital_cost=costs.at["cement finishing", "capital_cost"] / clinker_input, + efficiency=1 / clinker_input, + efficiency2=-electricity_input, + lifetime=costs.at["cement finishing", "lifetime"], + ) + + capture_rate = costs.at["cement carbon capture retrofit", "capture_rate"] + electricity_input = costs.at[ + "cement carbon capture retrofit", "electricity-input" + ] + + # add post combustion retrofit + n.add("Carrier", "cement emission CC") + n.add( + "Link", + spatial.cement.nodes, + suffix=" CC", + bus0=spatial.cement.nodes + " emission", + bus1=spatial.co2.nodes, + bus2=spatial.nodes, + bus3="co2 atmosphere", + carrier="cement emission CC", + p_nom_extendable=True, + capital_cost=costs.at["cement carbon capture retrofit", "capital_cost"], + efficiency=capture_rate, + efficiency2=-electricity_input, + efficiency3=1 - capture_rate, + lifetime=costs.at["cement carbon capture retrofit", "lifetime"], + ) + n.add( + "Link", + spatial.cement.nodes, + suffix=" emission", + bus0=spatial.cement.nodes + " emission", + bus1="co2 atmosphere", + carrier="cement emission", + p_nom_extendable=True, + capital_cost=0, + efficiency=1, + ) + def add_aviation( n: pypsa.Network, @@ -6241,9 +6784,10 @@ def add_import_options( snakemake = mock_snakemake( "prepare_sector_network", opts="", - clusters="10", + clusters="50", sector_opts="", - planning_horizons="2050", + configfiles="config/config.default.yaml", + planning_horizons="2035", ) configure_logging(snakemake) # pylint: disable=E0606 diff --git a/scripts/process_cost_data.py b/scripts/process_cost_data.py index 6bcd9f368c..bd8a90e4ff 100644 --- a/scripts/process_cost_data.py +++ b/scripts/process_cost_data.py @@ -252,7 +252,7 @@ def costs_for_storage(store=None, link1=None, link2=None, max_hours=1.0): if __name__ == "__main__": if "snakemake" not in globals(): - from _helpers import mock_snakemake + from scripts._helpers import mock_snakemake snakemake = mock_snakemake("process_cost_data", planning_horizons=2030)