From 254d50b1b4dc35d7db3b8cdc56ba99b798df3546 Mon Sep 17 00:00:00 2001 From: Fabian Date: Mon, 18 Sep 2023 12:25:04 +0200 Subject: [PATCH 01/11] prepare sectors: allow for updating co2 network costs --- Snakefile | 1 + config/config.default.yaml | 2 ++ scripts/prepare_sector_network.py | 47 ++++++++++++++++++++++++++----- scripts/solve_network.py | 8 +++--- 4 files changed, 47 insertions(+), 11 deletions(-) diff --git a/Snakefile b/Snakefile index 14ce0e40..f6e581a4 100644 --- a/Snakefile +++ b/Snakefile @@ -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" """ diff --git a/config/config.default.yaml b/config/config.default.yaml index ba41fe5a..331f7382 100644 --- a/config/config.default.yaml +++ b/config/config.default.yaml @@ -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: @@ -985,6 +986,7 @@ plotting: CO2 sequestration: '#f29dae' DAC: '#ff5270' co2 stored: '#f2385a' + co2 sequestered: '#f2682f' co2: '#f29dae' co2 vent: '#ffd4dc' CO2 pipeline: '#f5627f' diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 79bc67e9..b0d0a4e6 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -549,7 +549,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) @@ -576,6 +576,37 @@ def add_co2_tracking(n, options): unit="t_co2", ) + # add CO2 tanks + n.madd( + "Store", + spatial.co2.nodes, + e_nom_extendable=True, + capital_cost=costs.loc["CO2 storage tank"], + carrier="co2 stored", + bus=spatial.co2.nodes, + ) + n.add("Carrier", "co2 stored") + + # this tracks CO2 stored, e.g. underground + sequestration_buses = 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 @@ -591,22 +622,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( @@ -635,6 +666,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", @@ -3626,7 +3659,7 @@ if __name__ == "__main__": for carrier in conventional: add_carrier_buses(n, carrier) - add_co2_tracking(n, options) + add_co2_tracking(n, costs, options) add_generation(n, costs) diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 203d8b0f..2bbd0164 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -202,10 +202,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, ) From 2d027e80c3561f60ac293edffce3ab3c69981842 Mon Sep 17 00:00:00 2001 From: Fabian Date: Tue, 19 Sep 2023 12:22:16 +0200 Subject: [PATCH 02/11] fix capital costs of co2 tanks --- Snakefile | 2 +- scripts/prepare_sector_network.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Snakefile b/Snakefile index f6e581a4..7c16ff9f 100644 --- a/Snakefile +++ b/Snakefile @@ -125,7 +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}/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" """ diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index b0d0a4e6..4cfaa95f 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -567,7 +567,7 @@ def add_co2_tracking(n, costs, options): bus="co2 atmosphere", ) - # this tracks CO2 stored, e.g. underground + # add CO2 tanks n.madd( "Bus", spatial.co2.nodes, @@ -576,13 +576,13 @@ def add_co2_tracking(n, costs, options): unit="t_co2", ) - # add CO2 tanks n.madd( "Store", spatial.co2.nodes, e_nom_extendable=True, - capital_cost=costs.loc["CO2 storage tank"], + capital_cost=costs.at["CO2 storage tank", "fixed"], carrier="co2 stored", + e_cyclic=True, bus=spatial.co2.nodes, ) n.add("Carrier", "co2 stored") From c71c4e75675638e55a56336d13ac3dbb3e0cbf6c Mon Sep 17 00:00:00 2001 From: Fabian Date: Wed, 20 Sep 2023 15:27:09 +0200 Subject: [PATCH 03/11] add biomass constraint for biomass spatial enabled --- scripts/prepare_sector_network.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 4cfaa95f..feb3faef 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2330,6 +2330,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"] From be5331c89c8aeabd64108d1c15827da47519c43a Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 22 Sep 2023 10:51:36 +0200 Subject: [PATCH 04/11] formulate sequestration limit constraint as operational_limit constraint --- scripts/prepare_sector_network.py | 2 +- scripts/solve_network.py | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index feb3faef..0caf45dd 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -587,7 +587,7 @@ def add_co2_tracking(n, costs, options): ) n.add("Carrier", "co2 stored") - # this tracks CO2 stored, e.g. underground + # this tracks CO2 sequestered, e.g. underground sequestration_buses = spatial.co2.nodes.str.replace(" stored", " sequestered") n.madd( "Bus", diff --git a/scripts/solve_network.py b/scripts/solve_network.py index 2bbd0164..e76d4004 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -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: @@ -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) From 4e03e5a7ecb8165306087bad6413d4e9eb2f8ce3 Mon Sep 17 00:00:00 2001 From: Fabian Date: Thu, 5 Oct 2023 16:07:04 +0200 Subject: [PATCH 05/11] prepare_sector: add VOM for FT and methanolization, always use `.at` accessor for costs --- scripts/prepare_sector_network.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 0caf45dd..3a3c9992 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -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: @@ -2257,13 +2260,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"] @@ -2734,6 +2736,7 @@ def add_industry(n, costs): carrier="methanolisation", p_nom_extendable=True, p_min_pu=options.get("min_part_load_methanolisation", 0), + marginal_cost=options["MWh_MeOH_per_MWh_H2"] * costs.at["fuel cell", "VOM"], capital_cost=costs.at["methanolisation", "fixed"] * options["MWh_MeOH_per_MWh_H2"], # EUR/MW_H2/a marginal_cost=options["MWh_MeOH_per_MWh_H2"] From 6078b4626239ef94984a08bc16ac19b5b2e9244b Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 4 Jan 2024 20:04:56 +0100 Subject: [PATCH 06/11] remove duplicate marginal_cost for methanolisation from merge --- scripts/prepare_sector_network.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 3a3c9992..0cb9759c 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -2736,7 +2736,6 @@ def add_industry(n, costs): carrier="methanolisation", p_nom_extendable=True, p_min_pu=options.get("min_part_load_methanolisation", 0), - marginal_cost=options["MWh_MeOH_per_MWh_H2"] * costs.at["fuel cell", "VOM"], capital_cost=costs.at["methanolisation", "fixed"] * options["MWh_MeOH_per_MWh_H2"], # EUR/MW_H2/a marginal_cost=options["MWh_MeOH_per_MWh_H2"] From af7c1b15e6b9268a2948cfd9bd8f7c8752920e26 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 4 Jan 2024 20:10:05 +0100 Subject: [PATCH 07/11] add documentation --- doc/configtables/sector.csv | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/configtables/sector.csv b/doc/configtables/sector.csv index 5e2514e4..90979180 100644 --- a/doc/configtables/sector.csv +++ b/doc/configtables/sector.csv @@ -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. From 48832874171601e2eec0bf2b7512ae5a4ce718e1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 4 Jan 2024 19:10:04 +0000 Subject: [PATCH 08/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- rules/common.smk | 2 +- scripts/add_brownfield.py | 4 +--- scripts/prepare_sector_network.py | 6 ++++-- scripts/solve_network.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/rules/common.smk b/rules/common.smk index 0e85b620..2298ff91 100644 --- a/rules/common.smk +++ b/rules/common.smk @@ -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)) diff --git a/scripts/add_brownfield.py b/scripts/add_brownfield.py index e151c441..cb1f51c8 100644 --- a/scripts/add_brownfield.py +++ b/scripts/add_brownfield.py @@ -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"] diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 3a3c9992..ec76399d 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -3044,8 +3044,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: @@ -3458,6 +3459,7 @@ def cluster_heat_buses(n): 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 ", "") diff --git a/scripts/solve_network.py b/scripts/solve_network.py index e76d4004..36b53086 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -853,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) From becba42a88669c5e59fbb127336a67cf5498da49 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 4 Jan 2024 20:15:00 +0100 Subject: [PATCH 09/11] add release notes --- doc/release_notes.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/release_notes.rst b/doc/release_notes.rst index 33857780..88f05854 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -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. From e594f34e0029ce73a8083e6b28b60315b23b91c0 Mon Sep 17 00:00:00 2001 From: Fabian Neumann Date: Thu, 4 Jan 2024 20:30:00 +0100 Subject: [PATCH 10/11] fix sequestration buses string replacement with pd.Index --- scripts/prepare_sector_network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index 9fe4c95a..e35d3a64 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -591,7 +591,7 @@ def add_co2_tracking(n, costs, options): n.add("Carrier", "co2 stored") # this tracks CO2 sequestered, e.g. underground - sequestration_buses = spatial.co2.nodes.str.replace(" stored", " sequestered") + sequestration_buses = pd.Index(spatial.co2.nodes).str.replace(" stored", " sequestered") n.madd( "Bus", sequestration_buses, From 29cda7042b2b506001d2955059ca0d6ce75f1d0c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 4 Jan 2024 19:30:29 +0000 Subject: [PATCH 11/11] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- scripts/prepare_sector_network.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index e35d3a64..51ab52d9 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -591,7 +591,9 @@ def add_co2_tracking(n, costs, options): n.add("Carrier", "co2 stored") # this tracks CO2 sequestered, e.g. underground - sequestration_buses = pd.Index(spatial.co2.nodes).str.replace(" stored", " sequestered") + sequestration_buses = pd.Index(spatial.co2.nodes).str.replace( + " stored", " sequestered" + ) n.madd( "Bus", sequestration_buses,