diff --git a/.gitignore b/.gitignore index ea7b1a68..9d1a417e 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,8 @@ data/costs_*.csv dask-worker-space +dask-worker-space/ +publications.jrc.ec.europa.eu/ *.org @@ -53,3 +55,7 @@ doc/_build *.xls *.geojson + +*.ipynb + +data/costs_* \ No newline at end of file diff --git a/Snakefile b/Snakefile index 7e53884b..ee5f42f6 100644 --- a/Snakefile +++ b/Snakefile @@ -146,10 +146,9 @@ if config["sector"]["gas_network"] or config["sector"]["H2_retrofit"]: rule build_gas_input_locations: input: - lng="data/gas_network/scigrid-gas/data/IGGIELGN_LNGs.geojson", + lng=HTTP.remote("https://globalenergymonitor.org/wp-content/uploads/2022/09/Europe-Gas-Tracker-August-2022.xlsx", keep_local=True), entry="data/gas_network/scigrid-gas/data/IGGIELGN_BorderPoints.geojson", production="data/gas_network/scigrid-gas/data/IGGIELGN_Productions.geojson", - planned_lng="data/gas_network/planned_LNGs.csv", regions_onshore=pypsaeur("resources/regions_onshore_elec_s{simpl}_{clusters}.geojson"), regions_offshore=pypsaeur('resources/regions_offshore_elec_s{simpl}_{clusters}.geojson') output: @@ -288,6 +287,23 @@ else: build_biomass_transport_costs_output = {} +if config["sector"]["regional_co2_sequestration_potential"]["enable"]: + rule build_sequestration_potentials: + input: + sequestration_potential=HTTP.remote("https://raw.githubusercontent.com/ericzhou571/Co2Storage/main/resources/complete_map_2020_unit_Mt.geojson", keep_local=True), + regions_onshore=pypsaeur("resources/regions_onshore_elec_s{simpl}_{clusters}.geojson"), + regions_offshore=pypsaeur("resources/regions_offshore_elec_s{simpl}_{clusters}.geojson"), + output: + sequestration_potential="resources/co2_sequestration_potential_elec_s{simpl}_{clusters}.csv" + threads: 1 + resources: mem_mb=4000 + benchmark: "benchmarks/build_sequestration_potentials_s{simpl}_{clusters}" + script: "scripts/build_sequestration_potentials.py" + build_sequestration_potentials_output = rules.build_sequestration_potentials.output +else: + build_sequestration_potentials_output = {} + + rule build_salt_cavern_potentials: input: salt_caverns="data/h2_salt_caverns_GWh_per_sqkm.geojson", @@ -520,7 +536,8 @@ rule prepare_sector_network: solar_thermal_rural="resources/solar_thermal_rural_elec_s{simpl}_{clusters}.nc" if config["sector"]["solar_thermal"] else [], **build_retro_cost_output, **build_biomass_transport_costs_output, - **gas_infrastructure + **gas_infrastructure, + **build_sequestration_potentials_output output: RDIR + '/prenetworks/elec_s{simpl}_{clusters}_lv{lv}_{opts}_{sector_opts}_{planning_horizons}.nc' threads: 1 resources: mem_mb=2000 diff --git a/config.default.yaml b/config.default.yaml index 98ed4277..ed1035df 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -66,7 +66,7 @@ snapshots: # arguments to pd.date_range start: "2013-01-01" end: "2014-01-01" - closed: left # end is not inclusive + inclusive: left # end is not inclusive atlite: cutout: ../pypsa-eur/cutouts/europe-2013-era5.nc @@ -250,10 +250,19 @@ sector: coal_cc: false dac: true co2_vent: false + allam_cycle: false SMR: true + regional_co2_sequestration_potential: + enable: false # enable regionally resolved geological co2 storage potential + attribute: 'conservative estimate Mt' + include_onshore: false # include onshore sequestration potentials + min_size: 3 # Gt, sites with lower potential will be excluded + max_size: 25 # Gt, max sequestration potential for any one site, TODO research suitable value + years_of_storage: 25 # years until potential exhausted at optimised annual rate co2_sequestration_potential: 200 #MtCO2/a sequestration potential for Europe co2_sequestration_cost: 10 #EUR/tCO2 for sequestration of CO2 - co2_network: false + co2_spatial: false + co2network: false cc_fraction: 0.9 # default fraction of CO2 captured with post-combustion capture hydrogen_underground_storage: true hydrogen_underground_storage_locations: @@ -261,8 +270,11 @@ sector: - nearshore # within 50 km of sea # - offshore ammonia: false # can be false (no NH3 carrier), true (copperplated NH3), "regional" (regionalised NH3 without network) + min_part_load_fischer_tropsch: 0.9 # p_min_pu + min_part_load_methanolisation: 0.5 # p_min_pu use_fischer_tropsch_waste_heat: true use_fuel_cell_waste_heat: true + use_electrolysis_waste_heat: false electricity_distribution_grid: true electricity_distribution_grid_cost_factor: 1.0 #multiplies cost in data/costs.csv electricity_grid_connection: true # only applies to onshore wind and utility PV @@ -276,7 +288,8 @@ sector: gas_network_connectivity_upgrade: 1 # 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 gas_distribution_grid: true gas_distribution_grid_cost_factor: 1.0 #multiplies cost in data/costs.csv - biomass_transport: false # biomass transport between nodes + biomass_spatial: false # regionally resolve biomass (e.g. potentials) + biomass_transport: false # allow transport of solid biomass between nodes conventional_generation: # generator : carrier OCGT: gas biomass_to_liquid: false diff --git a/data/gas_network/planned_LNGs.csv b/data/gas_network/planned_LNGs.csv deleted file mode 100644 index 65706216..00000000 --- a/data/gas_network/planned_LNGs.csv +++ /dev/null @@ -1,8 +0,0 @@ -name,geometry,max_cap_store2pipe_M_m3_per_d,source -Wilhelmshaven,"POINT(8.133 53.516)",27.4,https://www.gem.wiki/Wilhelmshaven_LNG_Terminal -Brunsbüttel,"POINT(8.976 53.914)",19.2,https://www.gem.wiki/Brunsb%C3%BCttel_LNG_Terminal -Stade,"POINT(9.510 53.652)",32.9,https://www.gem.wiki/Stade_LNG_Terminal -Alexandroupolis,"POINT(25.843 40.775)",16.7,https://www.gem.wiki/Alexandroupolis_LNG_Terminal -Shannon,"POINT(-9.442 52.581)",22.5,https://www.gem.wiki/Shannon_LNG_Terminal -Gothenburg,"POINT(11.948 57.702)",1.4,https://www.gem.wiki/Gothenburg_LNG_Terminal -Cork,"POINT(-8.323 51.831)",11.0,https://www.gem.wiki/Cork_LNG_Terminal \ No newline at end of file diff --git a/doc/release_notes.rst b/doc/release_notes.rst index fd9a3549..4700f27b 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -65,10 +65,14 @@ incorporates retrofitting options to hydrogen. * Add option for BtL (Biomass to liquid fuel/oil) with and without CC +* Add option for minimum part load for Fischer-Tropsch plants (default: 90%) and methanolisation plants (default: 50%). + * Units are assigned to the buses. These only provide a better understanding. The specifications of the units are not taken into account in the optimisation, which means that no automatic conversion of units takes place. * Option ``retrieve_sector_databundle`` to automatically retrieve and extract data bundle. +* Add option to use waste heat of electrolysis in district heating networks (``use_electrolysis_waste_heat``). + * Add regionalised hydrogen salt cavern storage potentials from `Technical Potential of Salt Caverns for Hydrogen Storage in Europe `_. * Add option to sweep the global CO2 sequestration potentials with keyword ``seq200`` in the ``{sector_opts}`` wildcard (for limit of 200 Mt CO2). @@ -93,6 +97,30 @@ incorporates retrofitting options to hydrogen. as explicit ICE shares for land transport (``land_transport_ice_share``) and agriculture machinery (``agriculture_machinery_oil_share``). +* Add option to spatially resolve carrier representing stored carbon dioxide + (``co2_spatial``). 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. + +* Add option for planning a new carbon dioxide network (``co2network``). + + +* Add option for regionally-resolved geological carbon dioxide sequestration + potentials through new rule ``build_sequestration_potentials`` based on + `CO2StoP `_. This + can be controlled in the section ``regional_co2_sequestration_potential`` of + the ``config.yaml``. It includes options to select the level of conservatism, + whether onshore potentials should be included, the respective upper and lower + limits per region, and an annualisation parameter for the cumulative + potential. The defaults are preliminary and will be validated the next + release. + +* Separate option to regionally resolve biomass (``biomass_spatial``) from + option to allow biomass transport (``biomass_transport``). + +* Add option to include `Allam cycle gas power plants + `_ (``allam_cycle``). + **Bugfixes** * The CO2 sequestration limit implemented as GlobalConstraint (introduced in the previous version) diff --git a/scripts/build_gas_input_locations.py b/scripts/build_gas_input_locations.py index c08d92de..2edfd81b 100644 --- a/scripts/build_gas_input_locations.py +++ b/scripts/build_gas_input_locations.py @@ -19,14 +19,29 @@ def read_scigrid_gas(fn): return df -def build_gas_input_locations(lng_fn, planned_lng_fn, entry_fn, prod_fn, countries): +def build_gem_lng_data(lng_fn): + df = pd.read_excel(lng_fn[0], sheet_name='LNG terminals - data') + df = df.set_index("ComboID") + + remove_status = ['Cancelled'] + remove_country = ['Cyprus','Turkey'] + remove_terminal = ['Puerto de la Luz LNG Terminal', 'Gran Canaria LNG Terminal'] + + df = df.query("Status != 'Cancelled' \ + & Country != @remove_country \ + & TerminalName != @remove_terminal \ + & CapacityInMtpa != '--'") + + df.CapacityInMtpa = df.CapacityInMtpa.astype(float) + + geometry = gpd.points_from_xy(df['Longitude'], df['Latitude']) + return gpd.GeoDataFrame(df, geometry=geometry, crs="EPSG:4326") + + +def build_gas_input_locations(lng_fn, entry_fn, prod_fn, countries): # LNG terminals - lng = read_scigrid_gas(lng_fn) - planned_lng = pd.read_csv(planned_lng_fn) - planned_lng.geometry = planned_lng.geometry.apply(wkt.loads) - planned_lng = gpd.GeoDataFrame(planned_lng, crs=4326) - lng = pd.concat([lng, planned_lng], ignore_index=True) + lng = build_gem_lng_data(lng_fn) # Entry points from outside the model scope entry = read_scigrid_gas(entry_fn) @@ -45,10 +60,11 @@ def build_gas_input_locations(lng_fn, planned_lng_fn, entry_fn, prod_fn, countri (prod.country_code != "DE") ] - conversion_factor = 437.5 # MCM/day to MWh/h - lng["p_nom"] = lng["max_cap_store2pipe_M_m3_per_d"] * conversion_factor - entry["p_nom"] = entry["max_cap_from_to_M_m3_per_d"] * conversion_factor - prod["p_nom"] = prod["max_supply_M_m3_per_d"] * conversion_factor + mcm_per_day_to_mw = 437.5 # MCM/day to MWh/h + mtpa_to_mw = 1649.224 # mtpa to MWh/h + lng["p_nom"] = lng["CapacityInMtpa"] * mtpa_to_mw + entry["p_nom"] = entry["max_cap_from_to_M_m3_per_d"] * mcm_per_day_to_mw + prod["p_nom"] = prod["max_supply_M_m3_per_d"] * mcm_per_day_to_mw lng["type"] = "lng" entry["type"] = "pipeline" @@ -64,7 +80,7 @@ if __name__ == "__main__": if 'snakemake' not in globals(): from helper import mock_snakemake snakemake = mock_snakemake( - 'build_gas_import_locations', + 'build_gas_input_locations', simpl='', clusters='37', ) @@ -87,7 +103,6 @@ if __name__ == "__main__": gas_input_locations = build_gas_input_locations( snakemake.input.lng, - snakemake.input.planned_lng, snakemake.input.entry, snakemake.input.production, countries diff --git a/scripts/build_sequestration_potentials.py b/scripts/build_sequestration_potentials.py new file mode 100644 index 00000000..4983640b --- /dev/null +++ b/scripts/build_sequestration_potentials.py @@ -0,0 +1,43 @@ +import pandas as pd +import geopandas as gpd + +def area(gdf): + """Returns area of GeoDataFrame geometries in square kilometers.""" + return gdf.to_crs(epsg=3035).area.div(1e6) + + +def allocate_sequestration_potential(gdf, regions, attr='conservative estimate Mt', threshold=3): + gdf = gdf.loc[gdf[attr] > threshold, [attr, "geometry"]] + gdf["area_sqkm"] = area(gdf) + overlay = gpd.overlay(regions, gdf, keep_geom_type=True) + overlay["share"] = area(overlay) / overlay["area_sqkm"] + adjust_cols = overlay.columns.difference({"name", "area_sqkm", "geometry", "share"}) + overlay[adjust_cols] = overlay[adjust_cols].multiply(overlay["share"], axis=0) + gdf_regions = overlay.groupby("name").sum() + gdf_regions.drop(["area_sqkm", "share"], axis=1, inplace=True) + return gdf_regions.squeeze() + + +if __name__ == "__main__": + if 'snakemake' not in globals(): + from helper import mock_snakemake + snakemake = mock_snakemake( + 'build_sequestration_potentials', + simpl='', + clusters="181" + ) + + cf = snakemake.config["sector"]["regional_co2_sequestration_potential"] + + gdf = gpd.read_file(snakemake.input.sequestration_potential[0]) + + regions = gpd.read_file(snakemake.input.regions_offshore) + if cf["include_onshore"]: + onregions = gpd.read_file(snakemake.input.regions_onshore) + regions = pd.concat([regions, onregions]).dissolve(by='name').reset_index() + + s = allocate_sequestration_potential(gdf, regions, attr=cf["attribute"], threshold=cf["min_size"]) + + s = s.where(s>cf["min_size"]).dropna() + + s.to_csv(snakemake.output.sequestration_potential) diff --git a/scripts/helper.py b/scripts/helper.py index e6ddfd4a..62ae33c0 100644 --- a/scripts/helper.py +++ b/scripts/helper.py @@ -138,6 +138,6 @@ def parse(l): def update_config_with_sector_opts(config, sector_opts): for o in sector_opts.split("-"): - if o.startswith("CF:"): + if o.startswith("CF+"): l = o.split("+")[1:] update_config(config, parse(l)) \ No newline at end of file diff --git a/scripts/make_summary.py b/scripts/make_summary.py index f9c74c89..06680cb4 100644 --- a/scripts/make_summary.py +++ b/scripts/make_summary.py @@ -273,7 +273,7 @@ def calculate_supply(n, label, supply): for end in [col[3:] for col in c.df.columns if col[:3] == "bus"]: - items = c.df.index[c.df["bus" + end].map(bus_map, na_action=None)] + items = c.df.index[c.df["bus" + end].map(bus_map).fillna(False)] if len(items) == 0: continue @@ -318,7 +318,7 @@ def calculate_supply_energy(n, label, supply_energy): for end in [col[3:] for col in c.df.columns if col[:3] == "bus"]: - items = c.df.index[c.df["bus" + str(end)].map(bus_map, na_action=None)] + items = c.df.index[c.df["bus" + str(end)].map(bus_map).fillna(False)] if len(items) == 0: continue diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 98e62c32..d369df16 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -48,7 +48,7 @@ def define_spatial(nodes, options): spatial.biomass = SimpleNamespace() - if options["biomass_transport"]: + if options.get("biomass_spatial", options["biomass_transport"]): spatial.biomass.nodes = nodes + " solid biomass" spatial.biomass.locations = nodes spatial.biomass.industry = nodes + " solid biomass for industry" @@ -65,14 +65,16 @@ def define_spatial(nodes, options): spatial.co2 = SimpleNamespace() - if options["co2_network"]: + if options["co2_spatial"]: spatial.co2.nodes = nodes + " co2 stored" spatial.co2.locations = nodes spatial.co2.vents = nodes + " co2 vent" + spatial.co2.process_emissions = nodes + " process emissions" else: spatial.co2.nodes = ["co2 stored"] spatial.co2.locations = ["EU"] spatial.co2.vents = ["co2 vent"] + spatial.co2.process_emissions = ["process emissions"] spatial.co2.df = pd.DataFrame(vars(spatial.co2), index=nodes) @@ -92,8 +94,11 @@ def define_spatial(nodes, options): spatial.gas.locations = ["EU"] spatial.gas.biogas = ["EU biogas"] spatial.gas.industry = ["gas for industry"] - spatial.gas.industry_cc = ["gas for industry CC"] spatial.gas.biogas_to_gas = ["EU biogas to gas"] + if options.get("co2_spatial", options["co2network"]): + spatial.gas.industry_cc = nodes + " gas for industry CC" + else: + spatial.gas.industry_cc = ["gas for industry CC"] spatial.gas.df = pd.DataFrame(vars(spatial.gas), index=nodes) @@ -432,6 +437,7 @@ def add_carrier_buses(n, carrier, nodes=None): e_nom_extendable=True, e_cyclic=True, carrier=carrier, + capital_cost=0.2 * costs.at[carrier, "discount rate"] # preliminary value to avoid zeros ) n.madd("Generator", @@ -515,10 +521,17 @@ def add_co2_tracking(n, options): unit="t_co2" ) + if options["regional_co2_sequestration_potential"]["enable"]: + upper_limit = options["regional_co2_sequestration_potential"]["max_size"] * 1e3 # Mt + annualiser = options["regional_co2_sequestration_potential"]["years_of_storage"] + e_nom_max = pd.read_csv(snakemake.input.sequestration_potential, index_col=0).squeeze() + e_nom_max = e_nom_max.reindex(spatial.co2.locations).fillna(0.).clip(upper=upper_limit).mul(1e6) / annualiser # t + e_nom_max = e_nom_max.rename(index=lambda x: x + " co2 stored") + n.madd("Store", spatial.co2.nodes, e_nom_extendable=True, - e_nom_max=np.inf, + e_nom_max=e_nom_max, capital_cost=options['co2_sequestration_cost'], carrier="co2 stored", bus=spatial.co2.nodes @@ -560,6 +573,29 @@ def add_co2_network(n, costs): ) +def add_allam(n, costs): + + logger.info("Adding Allam cycle gas power plants.") + + nodes = pop_layout.index + + n.madd("Link", + nodes, + suffix=" allam", + bus0=spatial.gas.df.loc[nodes, "nodes"].values, + bus1=nodes, + bus2=spatial.co2.df.loc[nodes, "nodes"].values, + carrier="allam", + p_nom_extendable=True, + # TODO: add costs to technology-data + capital_cost=0.6*1.5e6*0.1, # efficiency * EUR/MW * annuity + marginal_cost=2, + efficiency=0.6, + efficiency2=costs.at['gas', 'CO2 intensity'], + lifetime=30., + ) + + def add_dac(n, costs): heat_carriers = ["urban central heat", "services urban decentral heat"] @@ -1228,7 +1264,7 @@ def add_storage_and_grids(n, costs): bus0=spatial.coal.nodes, bus1=spatial.nodes, bus2="co2 atmosphere", - bus3="co2 stored", + bus3=spatial.co2.nodes, marginal_cost=costs.at['coal', 'efficiency'] * costs.at['coal', 'VOM'], #NB: VOM is per MWel capital_cost=costs.at['coal', 'efficiency'] * costs.at['coal', 'fixed'] + costs.at['biomass CHP capture', 'fixed'] * costs.at['coal', 'CO2 intensity'], #NB: fixed cost is per MWel p_nom_extendable=True, @@ -1837,7 +1873,7 @@ def add_biomass(n, costs): else: biogas_potentials_spatial = biomass_potentials["biogas"].sum() - if options["biomass_transport"]: + if options.get("biomass_spatial", options["biomass_transport"]): solid_biomass_potentials_spatial = biomass_potentials["solid biomass"].rename(index=lambda x: x + " solid biomass") else: solid_biomass_potentials_spatial = biomass_potentials["solid biomass"].sum() @@ -1909,10 +1945,10 @@ def add_biomass(n, costs): biomass_transport.index, bus0=biomass_transport.bus0 + " solid biomass", bus1=biomass_transport.bus1 + " solid biomass", - p_nom_extendable=True, + p_nom_extendable=False, + p_nom=5e4, length=biomass_transport.length.values, marginal_cost=biomass_transport.costs * biomass_transport.length.values, - capital_cost=1, carrier="solid biomass transport" ) @@ -2061,7 +2097,7 @@ def add_industry(n, costs): unit="MWh_LHV" ) - if options["biomass_transport"]: + if options.get("biomass_spatial", options["biomass_transport"]): p_set = industrial_demand.loc[spatial.biomass.locations, "solid biomass"].rename(index=lambda x: x + " solid biomass for industry") / 8760 else: p_set = industrial_demand["solid biomass"].sum() / 8760 @@ -2232,6 +2268,7 @@ def add_industry(n, costs): bus3=spatial.co2.nodes, carrier="methanolisation", p_nom_extendable=True, + p_min_pu=options.get("min_part_load_methanolisation", 0), capital_cost=costs.at["methanolisation", 'fixed'] * options["MWh_MeOH_per_MWh_H2"], # EUR/MW_H2/a lifetime=costs.at["methanolisation", 'lifetime'], efficiency=options["MWh_MeOH_per_MWh_H2"], @@ -2339,6 +2376,7 @@ def add_industry(n, costs): capital_cost=costs.at["Fischer-Tropsch", 'fixed'] * costs.at["Fischer-Tropsch", 'efficiency'], # EUR/MW_H2/a efficiency2=-costs.at["oil", 'CO2 intensity'] * costs.at["Fischer-Tropsch", 'efficiency'], p_nom_extendable=True, + p_min_pu=options.get("min_part_load_fischer_tropsch", 0), lifetime=costs.at['Fischer-Tropsch', 'lifetime'] ) @@ -2405,25 +2443,31 @@ def add_industry(n, costs): p_set=industrial_demand.loc[nodes, "electricity"] / 8760 ) - n.add("Bus", - "process emissions", - location="EU", + n.madd("Bus", + spatial.co2.process_emissions, + location=spatial.co2.locations, carrier="process emissions", unit="t_co2" ) + sel = ["process emission", "process emission from feedstock"] + if options["co2_spatial"] or options["co2network"]: + p_set = -industrial_demand.loc[nodes, sel].sum(axis=1).rename(index=lambda x: x + " process emissions") / 8760 + else: + p_set = -industrial_demand.loc[nodes, sel].sum(axis=1).sum() / 8760 + # this should be process emissions fossil+feedstock # then need load on atmosphere for feedstock emissions that are currently going to atmosphere via Link Fischer-Tropsch demand - n.add("Load", - "process emissions", - bus="process emissions", + n.madd("Load", + spatial.co2.process_emissions, + bus=spatial.co2.process_emissions, carrier="process emissions", - p_set=-industrial_demand.loc[nodes,["process emission", "process emission from feedstock"]].sum(axis=1).sum() / 8760 + p_set=p_set, ) - n.add("Link", - "process emissions", - bus0="process emissions", + n.madd("Link", + spatial.co2.process_emissions, + bus0=spatial.co2.process_emissions, bus1="co2 atmosphere", carrier="process emissions", p_nom_extendable=True, @@ -2434,7 +2478,7 @@ def add_industry(n, costs): n.madd("Link", spatial.co2.locations, suffix=" process emissions CC", - bus0="process emissions", + bus0=spatial.co2.process_emissions, bus1="co2 atmosphere", bus2=spatial.co2.nodes, carrier="process emissions CC", @@ -2475,6 +2519,11 @@ def add_waste_heat(n): n.links.loc[urban_central + " Fischer-Tropsch", "bus3"] = urban_central + " urban central heat" n.links.loc[urban_central + " Fischer-Tropsch", "efficiency3"] = 0.95 - n.links.loc[urban_central + " Fischer-Tropsch", "efficiency"] + # TODO integrate useable waste heat efficiency into technology-data from DEA + if options.get('use_electrolysis_waste_heat', False): + n.links.loc[urban_central + " H2 Electrolysis", "bus2"] = urban_central + " urban central heat" + n.links.loc[urban_central + " H2 Electrolysis", "efficiency2"] = 0.84 - n.links.loc[urban_central + " H2 Electrolysis", "efficiency"] + if options['use_fuel_cell_waste_heat']: n.links.loc[urban_central + " H2 Fuel Cell", "bus2"] = urban_central + " urban central heat" n.links.loc[urban_central + " H2 Fuel Cell", "efficiency2"] = 0.95 - n.links.loc[urban_central + " H2 Fuel Cell", "efficiency"] @@ -2877,9 +2926,12 @@ if __name__ == "__main__": if "noH2network" in opts: remove_h2_network(n) - if options["co2_network"]: + if options["co2network"]: add_co2_network(n, costs) + if options["allam_cycle"]: + add_allam(n, costs) + solver_name = snakemake.config["solving"]["solver"]["name"] n = set_temporal_aggregation(n, opts, solver_name) diff --git a/test/config.myopic.yaml b/test/config.myopic.yaml index cf730a0b..38d4328a 100644 --- a/test/config.myopic.yaml +++ b/test/config.myopic.yaml @@ -17,7 +17,7 @@ snapshots: # arguments to pd.date_range start: "2013-03-01" end: "2013-04-01" - closed: left # end is not inclusive + inclusive: left # end is not inclusive atlite: cutout: ../pypsa-eur/cutouts/be-03-2013-era5.nc @@ -27,6 +27,118 @@ existing_capacities: sector: co2_vent: true + SMR: true + regional_co2_sequestration_potential: + enable: false + co2_sequestration_potential: 200 #MtCO2/a sequestration potential for Europe + co2_sequestration_cost: 10 #EUR/tCO2 for sequestration of CO2 + co2_network: false + cc_fraction: 0.9 # default fraction of CO2 captured with post-combustion capture + hydrogen_underground_storage: true + hydrogen_underground_storage_locations: + # - onshore # more than 50 km from sea + - nearshore # within 50 km of sea + # - offshore + use_fischer_tropsch_waste_heat: true + use_fuel_cell_waste_heat: true + electricity_distribution_grid: true + electricity_distribution_grid_cost_factor: 1.0 #multiplies cost in data/costs.csv + electricity_grid_connection: true # only applies to onshore wind and utility PV + H2_network: true + gas_network: false + H2_retrofit: false # if set to True existing gas pipes can be retrofitted to H2 pipes + # according to hydrogen backbone strategy (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 + H2_retrofit_capacity_per_CH4: 0.6 # ratio for H2 capacity per original CH4 capacity of retrofitted pipelines + gas_network_connectivity_upgrade: 1 # 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 + gas_distribution_grid: true + gas_distribution_grid_cost_factor: 1.0 #multiplies cost in data/costs.csv + biomass_transport: false # biomass transport between nodes + conventional_generation: # generator : carrier + OCGT: gas + biomass_boiler: false + biomass_to_liquid: false + biosng: false + + +industry: + St_primary_fraction: # 0.3 # fraction of steel produced via primary route versus secondary route (scrap+EAF); today fraction is 0.6 + 2020: 0.6 + 2025: 0.55 + 2030: 0.5 + 2035: 0.45 + 2040: 0.4 + 2045: 0.35 + 2050: 0.3 + DRI_fraction: # 1 # fraction of the primary route converted to DRI + EAF + 2020: 0 + 2025: 0 + 2030: 0.05 + 2035: 0.2 + 2040: 0.4 + 2045: 0.7 + 2050: 1 + H2_DRI: 1.7 #H2 consumption in Direct Reduced Iron (DRI), MWh_H2,LHV/ton_Steel from 51kgH2/tSt in Vogl et al (2018) doi:10.1016/j.jclepro.2018.08.279 + elec_DRI: 0.322 #electricity consumption in Direct Reduced Iron (DRI) shaft, MWh/tSt HYBRIT brochure https://ssabwebsitecdn.azureedge.net/-/media/hybrit/files/hybrit_brochure.pdf + Al_primary_fraction: # 0.2 # fraction of aluminium produced via the primary route versus scrap; today fraction is 0.4 + 2020: 0.4 + 2025: 0.375 + 2030: 0.35 + 2035: 0.325 + 2040: 0.3 + 2045: 0.25 + 2050: 0.2 + MWh_CH4_per_tNH3_SMR: 10.8 # 2012's demand from https://ec.europa.eu/docsroom/documents/4165/attachments/1/translations/en/renditions/pdf + MWh_elec_per_tNH3_SMR: 0.7 # same source, assuming 94-6% split methane-elec of total energy demand 11.5 MWh/tNH3 + MWh_H2_per_tNH3_electrolysis: 6.5 # from https://doi.org/10.1016/j.joule.2018.04.017, around 0.197 tH2/tHN3 (>3/17 since some H2 lost and used for energy) + MWh_elec_per_tNH3_electrolysis: 1.17 # from https://doi.org/10.1016/j.joule.2018.04.017 Table 13 (air separation and HB) + NH3_process_emissions: 24.5 # in MtCO2/a from SMR for H2 production for NH3 from UNFCCC for 2015 for EU28 + petrochemical_process_emissions: 25.5 # in MtCO2/a for petrochemical and other from UNFCCC for 2015 for EU28 + HVC_primary_fraction: 1. # fraction of today's HVC produced via primary route + HVC_mechanical_recycling_fraction: 0. # fraction of today's HVC produced via mechanical recycling + HVC_chemical_recycling_fraction: 0. # fraction of today's HVC produced via chemical recycling + HVC_production_today: 52. # MtHVC/a from DECHEMA (2017), Figure 16, page 107; includes ethylene, propylene and BTX + MWh_elec_per_tHVC_mechanical_recycling: 0.547 # from SI of https://doi.org/10.1016/j.resconrec.2020.105010, Table S5, for HDPE, PP, PS, PET. LDPE would be 0.756. + MWh_elec_per_tHVC_chemical_recycling: 6.9 # Material Economics (2019), page 125; based on pyrolysis and electric steam cracking + chlorine_production_today: 9.58 # MtCl/a from DECHEMA (2017), Table 7, page 43 + MWh_elec_per_tCl: 3.6 # DECHEMA (2017), Table 6, page 43 + MWh_H2_per_tCl: -0.9372 # DECHEMA (2017), page 43; negative since hydrogen produced in chloralkali process + methanol_production_today: 1.5 # MtMeOH/a from DECHEMA (2017), page 62 + MWh_elec_per_tMeOH: 0.167 # DECHEMA (2017), Table 14, page 65 + MWh_CH4_per_tMeOH: 10.25 # DECHEMA (2017), Table 14, page 65 + hotmaps_locate_missing: false + reference_year: 2015 + # references: + # DECHEMA (2017): https://dechema.de/dechema_media/Downloads/Positionspapiere/Technology_study_Low_carbon_energy_and_feedstock_for_the_European_chemical_industry-p-20002750.pdf + # Material Economics (2019): https://materialeconomics.com/latest-updates/industrial-transformation-2050 + +costs: + year: 2030 + version: v0.5.0 + lifetime: 25 #default lifetime + # From a Lion Hirth paper, also reflects average of Noothout et al 2016 + discountrate: 0.07 + # [EUR/USD] ECB: https://www.ecb.europa.eu/stats/exchange/eurofxref/html/eurofxref-graph-usd.en.html # noqa: E501 + USD2013_to_EUR2013: 0.7532 + + # Marginal and capital costs can be overwritten + # capital_cost: + # onwind: 500 + marginal_cost: + solar: 0.01 + onwind: 0.015 + offwind: 0.015 + hydro: 0. + H2: 0. + battery: 0. + + emission_prices: # only used with the option Ep (emission prices) + co2: 0. + + lines: + length_factor: 1.25 #to estimate offwind connection costs + solving: solver: diff --git a/test/config.overnight.yaml b/test/config.overnight.yaml index a21de4e7..c0e6f57b 100644 --- a/test/config.overnight.yaml +++ b/test/config.overnight.yaml @@ -16,13 +16,31 @@ snapshots: # arguments to pd.date_range start: "2013-03-01" end: "2013-04-01" - closed: left # end is not inclusive + inclusive: left # end is not inclusive atlite: cutout: ../pypsa-eur/cutouts/be-03-2013-era5.nc sector: co2_vent: true + SMR: true + regional_co2_sequestration_potential: + enable: false + co2_sequestration_potential: 200 #MtCO2/a sequestration potential for Europe + co2_sequestration_cost: 10 #EUR/tCO2 for sequestration of CO2 + co2_network: false + cc_fraction: 0.9 # default fraction of CO2 captured with post-combustion capture + hydrogen_underground_storage: true + hydrogen_underground_storage_locations: + # - onshore # more than 50 km from sea + - nearshore # within 50 km of sea + # - offshore + use_fischer_tropsch_waste_heat: true + use_fuel_cell_waste_heat: true + electricity_distribution_grid: true + electricity_distribution_grid_cost_factor: 1.0 #multiplies cost in data/costs.csv + electricity_grid_connection: true # only applies to onshore wind and utility PV + H2_network: true gas_network: true H2_retrofit: true # if set to True existing gas pipes can be retrofitted to H2 pipes biomass_boiler: false