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

Distinguish sequestered and stored CO2 #844

Merged
merged 12 commits into from
Jan 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Snakefile
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ rule sync:
shell:
"""
rsync -uvarh --ignore-missing-args --files-from=.sync-send . {params.cluster}
rsync -uvarh --no-g {params.cluster}/resources . || echo "No resources directory, skipping rsync"
rsync -uvarh --no-g {params.cluster}/results . || echo "No results directory, skipping rsync"
rsync -uvarh --no-g {params.cluster}/logs . || echo "No logs directory, skipping rsync"
"""
2 changes: 2 additions & 0 deletions config/config.default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,7 @@ sector:
co2_sequestration_lifetime: 50
co2_spatial: false
co2network: false
co2_network_cost_factor: 1
cc_fraction: 0.9
hydrogen_underground_storage: true
hydrogen_underground_storage_locations:
Expand Down Expand Up @@ -985,6 +986,7 @@ plotting:
CO2 sequestration: '#f29dae'
DAC: '#ff5270'
co2 stored: '#f2385a'
co2 sequestered: '#f2682f'
co2: '#f29dae'
co2 vent: '#ffd4dc'
CO2 pipeline: '#f5627f'
Expand Down
1 change: 1 addition & 0 deletions doc/configtables/sector.csv
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ co2_sequestration_cost,currency/tCO2,float,The cost of sequestering a ton of CO2
co2_spatial,--,"{true, false}","Add option to spatially resolve carrier representing stored carbon dioxide. This allows for more detailed modelling of CCUTS, e.g. regarding the capturing of industrial process emissions, usage as feedstock for electrofuels, transport of carbon dioxide, and geological sequestration sites."
,,,
co2network,--,"{true, false}",Add option for planning a new carbon dioxide transmission network
co2_network_cost_factor,p.u.,float,The cost factor for the capital cost of the carbon dioxide transmission network
,,,
cc_fraction,--,float,The default fraction of CO2 captured with post-combustion capture
hydrogen_underground _storage,--,"{true, false}",Add options for storing hydrogen underground. Storage potential depends regionally.
Expand Down
7 changes: 7 additions & 0 deletions doc/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ Release Notes
Upcoming Release
================

* Distinguish between stored and sequestered CO2. Stored CO2 is stored
overground in tanks and can be used for CCU (e.g. methanolisation).
Sequestered CO2 is stored underground and can no longer be used for CCU. This
distinction is made because storage in tanks is more expensive than
underground storage. The link that connects stored and sequestered CO2 is
unidirectional.

* Increase deployment density of solar to 5.1 MW/sqkm by default.

* Default to full electrification of land transport by 2050.
Expand Down
2 changes: 1 addition & 1 deletion rules/common.smk
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import os, sys, glob

helper_source_path = [match for match in glob.glob('**/_helpers.py', recursive=True)]
helper_source_path = [match for match in glob.glob("**/_helpers.py", recursive=True)]

for path in helper_source_path:
path = os.path.dirname(os.path.abspath(path))
Expand Down
4 changes: 1 addition & 3 deletions scripts/add_brownfield.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,7 @@ def disable_grid_expansion_if_LV_limit_hit(n):

# allow small numerical differences
if lv_limit - total_expansion < 1:
logger.info(
f"LV is already reached, disabling expansion and LV limit"
)
logger.info(f"LV is already reached, disabling expansion and LV limit")
extendable_acs = n.lines.query("s_nom_extendable").index
n.lines.loc[extendable_acs, "s_nom_extendable"] = False
n.lines.loc[extendable_acs, "s_nom"] = n.lines.loc[extendable_acs, "s_nom_min"]
Expand Down
73 changes: 60 additions & 13 deletions scripts/prepare_sector_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,10 @@ def define_spatial(nodes, options):
spatial.gas.biogas = ["EU biogas"]
spatial.gas.industry = ["gas for industry"]
spatial.gas.biogas_to_gas = ["EU biogas to gas"]
spatial.gas.biogas_to_gas_cc = ["EU biogas to gas CC"]
if options.get("biomass_spatial", options["biomass_transport"]):
spatial.gas.biogas_to_gas_cc = nodes + " biogas to gas CC"
else:
spatial.gas.biogas_to_gas_cc = ["EU biogas to gas CC"]
if options.get("co2_spatial", options["co2network"]):
spatial.gas.industry_cc = nodes + " gas for industry CC"
else:
Expand Down Expand Up @@ -549,7 +552,7 @@ def patch_electricity_network(n):
n.loads_t.p_set.rename(lambda x: x.strip(), axis=1, inplace=True)


def add_co2_tracking(n, options):
def add_co2_tracking(n, costs, options):
# minus sign because opposite to how fossil fuels used:
# CH4 burning puts CH4 down, atmosphere up
n.add("Carrier", "co2", co2_emissions=-1.0)
Expand All @@ -567,7 +570,7 @@ def add_co2_tracking(n, options):
bus="co2 atmosphere",
)

# this tracks CO2 stored, e.g. underground
# add CO2 tanks
n.madd(
"Bus",
spatial.co2.nodes,
Expand All @@ -576,6 +579,39 @@ def add_co2_tracking(n, options):
unit="t_co2",
)

n.madd(
"Store",
spatial.co2.nodes,
e_nom_extendable=True,
capital_cost=costs.at["CO2 storage tank", "fixed"],
carrier="co2 stored",
e_cyclic=True,
bus=spatial.co2.nodes,
)
n.add("Carrier", "co2 stored")

# this tracks CO2 sequestered, e.g. underground
sequestration_buses = pd.Index(spatial.co2.nodes).str.replace(
" stored", " sequestered"
)
n.madd(
"Bus",
sequestration_buses,
location=spatial.co2.locations,
carrier="co2 sequestered",
unit="t_co2",
)

n.madd(
"Link",
sequestration_buses,
bus0=spatial.co2.nodes,
bus1=sequestration_buses,
carrier="co2 sequestered",
efficiency=1.0,
p_nom_extendable=True,
)

if options["regional_co2_sequestration_potential"]["enable"]:
upper_limit = (
options["regional_co2_sequestration_potential"]["max_size"] * 1e3
Expand All @@ -591,22 +627,22 @@ def add_co2_tracking(n, options):
.mul(1e6)
/ annualiser
) # t
e_nom_max = e_nom_max.rename(index=lambda x: x + " co2 stored")
e_nom_max = e_nom_max.rename(index=lambda x: x + " co2 sequestered")
else:
e_nom_max = np.inf

n.madd(
"Store",
spatial.co2.nodes,
sequestration_buses,
e_nom_extendable=True,
e_nom_max=e_nom_max,
capital_cost=options["co2_sequestration_cost"],
carrier="co2 stored",
bus=spatial.co2.nodes,
bus=sequestration_buses,
lifetime=options["co2_sequestration_lifetime"],
carrier="co2 sequestered",
)

n.add("Carrier", "co2 stored")
n.add("Carrier", "co2 sequestered")

if options["co2_vent"]:
n.madd(
Expand Down Expand Up @@ -635,6 +671,8 @@ def add_co2_network(n, costs):
* co2_links.length
)
capital_cost = cost_onshore + cost_submarine
cost_factor = snakemake.config["sector"]["co2_network_cost_factor"]
capital_cost *= cost_factor

n.madd(
"Link",
Expand Down Expand Up @@ -2224,13 +2262,12 @@ def add_biomass(n, costs):
# Assuming for costs that the CO2 from upgrading is pure, such as in amine scrubbing. I.e., with and without CC is
# equivalent. Adding biomass CHP capture because biogas is often small-scale and decentral so further
# from e.g. CO2 grid or buyers. This is a proxy for the added cost for e.g. a raw biogas pipeline to a central upgrading facility

n.madd(
"Link",
spatial.gas.biogas_to_gas_cc,
bus0=spatial.gas.biogas,
bus1=spatial.gas.nodes,
bus2="co2 stored",
bus2=spatial.co2.nodes,
bus3="co2 atmosphere",
carrier="biogas to gas CC",
capital_cost=costs.at["biogas CC", "fixed"]
Expand Down Expand Up @@ -2297,6 +2334,14 @@ def add_biomass(n, costs):
marginal_cost=costs.at["solid biomass", "fuel"]
+ bus_transport_costs * average_distance,
)
n.add(
"GlobalConstraint",
"biomass limit",
carrier_attribute="solid biomass",
sense="<=",
constant=biomass_potentials["solid biomass"].sum(),
type="operational_limit",
)

# AC buses with district heating
urban_central = n.buses.index[n.buses.carrier == "urban central heat"]
Expand Down Expand Up @@ -3000,8 +3045,9 @@ def add_industry(n, costs):

if options["co2_spatial"] or options["co2network"]:
p_set = (
-industrial_demand.loc[nodes, "process emission"]
.rename(index=lambda x: x + " process emissions")
-industrial_demand.loc[nodes, "process emission"].rename(
index=lambda x: x + " process emissions"
)
/ nhours
)
else:
Expand Down Expand Up @@ -3414,6 +3460,7 @@ def define_clustering(attributes, aggregate_dict):
pnl = c.pnl
agg = define_clustering(pd.Index(pnl.keys()), aggregate_dict)
for k in pnl.keys():

def renamer(s):
return s.replace("residential ", "").replace("services ", "")

Expand Down Expand Up @@ -3626,7 +3673,7 @@ def lossy_bidirectional_links(n, carrier, efficiencies={}):
for carrier in conventional:
add_carrier_buses(n, carrier)

add_co2_tracking(n, options)
add_co2_tracking(n, costs, options)

add_generation(n, costs)

Expand Down
15 changes: 6 additions & 9 deletions scripts/solve_network.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,6 @@ def add_co2_sequestration_limit(n, config, limit=200):
"""
Add a global constraint on the amount of Mt CO2 that can be sequestered.
"""
n.carriers.loc["co2 stored", "co2_absorptions"] = -1
n.carriers.co2_absorptions = n.carriers.co2_absorptions.fillna(0)

limit = limit * 1e6
for o in opts:
if "seq" not in o:
Expand All @@ -202,10 +199,10 @@ def add_co2_sequestration_limit(n, config, limit=200):
n.madd(
"GlobalConstraint",
names,
sense="<=",
constant=limit,
type="primary_energy",
carrier_attribute="co2_absorptions",
sense=">=",
constant=-limit,
type="operational_limit",
carrier_attribute="co2 sequestered",
investment_period=periods,
)

Expand Down Expand Up @@ -396,7 +393,7 @@ def prepare_network(
if snakemake.params["sector"]["limit_max_growth"]["enable"]:
n = add_max_growth(n, config)

if n.stores.carrier.eq("co2 stored").any():
if n.stores.carrier.eq("co2 sequestered").any():
limit = co2_sequestration_potential
add_co2_sequestration_limit(n, config, limit=limit)

Expand Down Expand Up @@ -856,7 +853,7 @@ def solve_network(n, config, solving, opts="", **kwargs):
kwargs["assign_all_duals"] = cf_solving.get("assign_all_duals", False)

if kwargs["solver_name"] == "gurobi":
logging.getLogger('gurobipy').setLevel(logging.CRITICAL)
logging.getLogger("gurobipy").setLevel(logging.CRITICAL)

rolling_horizon = cf_solving.pop("rolling_horizon", False)
skip_iterations = cf_solving.pop("skip_iterations", False)
Expand Down