Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

carbon management #296

Merged
merged 12 commits into from
Feb 16, 2023
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ gurobi.log
/data/Industrial_Database.csv
/data/retro/tabula-calculator-calcsetbuilding.csv
/data/nuts*
data/gas_network/scigrid-gas/
dask-worker-space/
publications.jrc.ec.europa.eu/

*.org

Expand Down
20 changes: 19 additions & 1 deletion Snakefile
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,23 @@ else:
build_biomass_transport_costs_output = {}


if config["sector"].get("sequestration_potential", False):
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",
Expand Down Expand Up @@ -512,7 +529,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
Expand Down
5 changes: 4 additions & 1 deletion config.default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -250,9 +250,11 @@ sector:
dac: true
co2_vent: false
SMR: true
sequestration_potential: true # geological co2 storage potential
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
fneum marked this conversation as resolved.
Show resolved Hide resolved
cc_fraction: 0.9 # default fraction of CO2 captured with post-combustion capture
hydrogen_underground_storage: true
hydrogen_underground_storage_locations:
Expand All @@ -275,6 +277,7 @@ 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_spatial: false # biomass transport between nodes
biomass_transport: false # biomass transport between nodes
conventional_generation: # generator : carrier
OCGT: gas
Expand Down
48 changes: 48 additions & 0 deletions scripts/build_sequestration_potentials.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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"
)

# TODO move to config.yaml
threshold = 3
include_onshore = False

gdf = gpd.read_file(snakemake.input.sequestration_potential[0])

regions = gpd.read_file(snakemake.input.regions_offshore)
if include_onshore:
onregions = gpd.read_file(snakemake.input.regions_onshore)
regions = pd.concat([regions, onregions]).dissolve(by='name').reset_index()

attr = snakemake.config['sector']["sequestration_potential"]
kwargs = dict(attr=attr, threshold=threshold) if isinstance(attr, str) else {}

s = allocate_sequestration_potential(gdf, regions, **kwargs)

s = s.where(s>threshold).dropna()

s.to_csv(snakemake.output.sequestration_potential)
2 changes: 1 addition & 1 deletion scripts/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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+"):
fneum marked this conversation as resolved.
Show resolved Hide resolved
l = o.split("+")[1:]
update_config(config, parse(l))
4 changes: 2 additions & 2 deletions scripts/make_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion scripts/plot_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ def plot_h2_map(network, regions):

else:

h2_total = h2_new
h2_total = h2_new.p_nom_opt

link_widths_total = h2_total / linewidth_factor

Expand Down
85 changes: 64 additions & 21 deletions scripts/prepare_sector_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,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"
Expand All @@ -61,14 +61,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)

Expand All @@ -88,8 +90,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)

Expand Down Expand Up @@ -428,6 +433,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",
Expand Down Expand Up @@ -507,10 +513,18 @@ def add_co2_tracking(n, options):
unit="t_co2"
)

if options["sequestration_potential"]:
# TODO make configurable options
upper_limit = 25000 # Mt
annualiser = 25 # TODO research suitable value
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
Expand Down Expand Up @@ -550,6 +564,28 @@ def add_co2_network(n, costs):
)


def add_allam(n, costs):

logger.info("Adding Allam cycle generators")

nodes = pop_layout.index

n.madd("Link",
nodes + " 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"]
Expand Down Expand Up @@ -1218,7 +1254,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,
Expand Down Expand Up @@ -1828,7 +1864,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()
Expand Down Expand Up @@ -1900,10 +1936,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"
)

Expand Down Expand Up @@ -2052,7 +2088,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
Expand Down Expand Up @@ -2396,25 +2432,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,
Expand All @@ -2425,7 +2467,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",
Expand Down Expand Up @@ -2775,8 +2817,9 @@ def set_temporal_aggregation(n, opts, solver_name):
if "noH2network" in opts:
remove_h2_network(n)

if options["co2_network"]:
if options["co2network"]:
add_co2_network(n, costs)
add_allam(n, costs)

solver_name = snakemake.config["solving"]["solver"]["name"]
n = set_temporal_aggregation(n, opts, solver_name)
Expand Down