diff --git a/Snakefile b/Snakefile index de34f386..d747b8fe 100644 --- a/Snakefile +++ b/Snakefile @@ -1,4 +1,7 @@ +from snakemake.remote.HTTP import RemoteProvider as HTTPRemoteProvider +HTTP = HTTPRemoteProvider() + configfile: "config.yaml" @@ -180,6 +183,21 @@ rule build_biomass_potentials: script: 'scripts/build_biomass_potentials.py' +if config["sector"]["biomass_transport"]: + rule build_biomass_transport_costs: + input: + transport_cost_data=HTTP.remote("publications.jrc.ec.europa.eu/repository/bitstream/JRC98626/biomass potentials in europe_web rev.pdf", keep_local=True) + output: + biomass_transport_costs="resources/biomass_transport_costs.csv", + threads: 1 + resources: mem_mb=1000 + benchmark: "benchmarks/build_biomass_transport_costs" + script: 'scripts/build_biomass_transport_costs.py' + build_biomass_transport_costs_output = rules.build_biomass_transport_costs.output +else: + build_biomass_transport_costs_output = {} + + rule build_ammonia_production: input: usgs="data/myb1-2017-nitro.xls" @@ -321,8 +339,8 @@ rule prepare_sector_network: energy_totals_name='resources/energy_totals.csv', co2_totals_name='resources/co2_totals.csv', transport_name='resources/transport_data.csv', - traffic_data_KFZ = "data/emobility/KFZ__count", - traffic_data_Pkw = "data/emobility/Pkw__count", + traffic_data_KFZ="data/emobility/KFZ__count", + traffic_data_Pkw="data/emobility/Pkw__count", biomass_potentials='resources/biomass_potentials.csv', heat_profile="data/heat_load_profile_BDEW.csv", costs=CDIR + "costs_{planning_horizons}.csv", @@ -352,7 +370,8 @@ rule prepare_sector_network: solar_thermal_total="resources/solar_thermal_total_elec_s{simpl}_{clusters}.nc", solar_thermal_urban="resources/solar_thermal_urban_elec_s{simpl}_{clusters}.nc", solar_thermal_rural="resources/solar_thermal_rural_elec_s{simpl}_{clusters}.nc", - **build_retro_cost_output + **build_retro_cost_output, + **build_biomass_transport_costs_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 9c76a3f7..d928fb86 100644 --- a/config.default.yaml +++ b/config.default.yaml @@ -223,7 +223,8 @@ sector: co2_vent: true SMR: true co2_sequestration_potential: 200 #MtCO2/a sequestration potential for Europe - co2_sequestration_cost: 20 #EUR/tCO2 for transport and sequestration of CO2 + 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 use_fischer_tropsch_waste_heat: true @@ -233,6 +234,7 @@ sector: electricity_grid_connection: true # only applies to onshore wind and utility PV 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 @@ -480,6 +482,7 @@ plotting: hot water storage: '#BBBBBB' hot water charging: '#BBBBBB' hot water discharging: '#999999' + CO2 pipeline: '#999999' CHP: r CHP heat: r CHP electric: r @@ -520,5 +523,6 @@ plotting: shipping oil: "#6495ED" shipping oil emissions: "#6495ED" electricity distribution grid: '#333333' + solid biomass transport: green H2 for industry: "#222222" H2 for shipping: "#6495ED" diff --git a/doc/release_notes.rst b/doc/release_notes.rst index da1f5c1d..4d0b37bf 100644 --- a/doc/release_notes.rst +++ b/doc/release_notes.rst @@ -62,12 +62,26 @@ Future release These are included in the environment specifications of PyPSA-Eur. * Consistent use of ``__main__`` block and further unspecific code cleaning. * Distinguish costs for home battery storage and inverter from utility-scale battery costs. +* Add option to regionally resolve CO2 storage and add CO2 pipeline transport because geological storage potential, + CO2 utilisation sites and CO2 capture sites may be separated. + The CO2 network is built from zero based on the topology of the electricity grid (greenfield). + Pipelines are assumed to be bidirectional and lossless. + Furthermore, neither retrofitting of natural gas pipelines (required pressures are too high, 80-160 bar vs <80 bar) + nor other modes of CO2 transport (by ship, road or rail) are considered. + The regional representation of CO2 is activated with the config setting ``sector: co2_network: true`` but is deactivated by default. + The global limit for CO2 sequestration now applies to the sum of all CO2 stores via an ``extra_functionality`` constraint. * Added option for hydrogen liquefaction costs for hydrogen demand in shipping. This introduces a new ``H2 liquid`` bus at each location. It is activated via ``sector: shipping_hydrogen_liquefaction: true``. * The share of shipping transformed into hydrogen fuel cell can be now defined for different years in the ``config.yaml`` file. The carbon emission from the remaining share is treated as a negative load on the atmospheric carbon dioxide bus, just like aviation and land transport emissions. * The transformation of the Steel and Aluminium production can be now defined for different years in the ``config.yaml`` file. * Include the option to alter the maximum energy capacity of a store via the ``carrier+factor`` in the ``{sector_opts}`` wildcard. This can be useful for sensitivity analyses. Example: ``co2 stored+e2`` multiplies the ``e_nom_max`` by factor 2. In this example, ``e_nom_max`` represents the CO2 sequestration potential in Europe. +* Add option to regionally disaggregate biomass potential to individual nodes + (currently given per country, then distributed by population density within) + and allow the transport of solid biomass. + The transport costs are determined based on the `JRC-EU-Times Bioenergy report `_ + in the new optional rule ``build_biomass_transport_costs``. + Biomass transport can be activated with the setting ``sector: biomass_transport: true``. * Compatibility with ``xarray`` version 0.19. * Separate basic chemicals into HVC, chlorine, methanol and ammonia [`#166 `_]. * Add option to specify reuse, primary production, and mechanical and chemical recycling fraction of platics [`#166 `_]. diff --git a/doc/spatial_resolution.rst b/doc/spatial_resolution.rst index 1be9f3ad..83a33f73 100644 --- a/doc/spatial_resolution.rst +++ b/doc/spatial_resolution.rst @@ -44,11 +44,13 @@ Hydrogen network: nodal. Methane network: single node for Europe, since future demand is so low and no bottlenecks are expected. -Solid biomass: single node for Europe, until transport costs can be -incorporated. +Solid biomass: choice between single node for Europe and nodal where biomass +potential is regionally disaggregated (currently given per country, +then distributed by population density within) +and transport of solid biomass is possible. CO2: single node for Europe, but a transport and storage cost is added for -sequestered CO2. +sequestered CO2. Optionally: nodal, with CO2 transport via pipelines. Liquid hydrocarbons: single node for Europe, since transport costs for liquids are low. diff --git a/scripts/build_biomass_transport_costs.py b/scripts/build_biomass_transport_costs.py new file mode 100644 index 00000000..aaec215b --- /dev/null +++ b/scripts/build_biomass_transport_costs.py @@ -0,0 +1,90 @@ +""" +Reads biomass transport costs for different countries of the JRC report + + "The JRC-EU-TIMES model. + Bioenergy potentials + for EU and neighbouring countries." + (2015) + +converts them from units 'EUR per km/ton' -> 'EUR/ (km MWh)' + +assuming as an approximation energy content of wood pellets + +@author: bw0928 +""" + +import pandas as pd +import tabula as tbl + +ENERGY_CONTENT = 4.8 # unit MWh/t (wood pellets) + +def get_countries(): + + pandas_options = dict( + skiprows=range(6), + header=None, + index_col=0 + ) + + return tbl.read_pdf( + str(snakemake.input.transport_cost_data), + pages="145", + multiple_tables=False, + pandas_options=pandas_options + )[0].index + + +def get_cost_per_tkm(page, countries): + + pandas_options = dict( + skiprows=range(6), + header=0, + sep=' |,', + engine='python', + index_col=False, + ) + + sc = tbl.read_pdf( + str(snakemake.input.transport_cost_data), + pages=page, + multiple_tables=False, + pandas_options=pandas_options + )[0] + sc.index = countries + sc.columns = sc.columns.str.replace("€", "EUR") + + return sc + + +def build_biomass_transport_costs(): + + countries = get_countries() + + sc1 = get_cost_per_tkm(146, countries) + sc2 = get_cost_per_tkm(147, countries) + + # take mean of both supply chains + to_concat = [sc1["EUR/km/ton"], sc2["EUR/km/ton"]] + transport_costs = pd.concat(to_concat, axis=1).mean(axis=1) + + # convert tonnes to MWh + transport_costs /= ENERGY_CONTENT + transport_costs.name = "EUR/km/MWh" + + # rename country names + to_rename = { + "UK": "GB", + "XK": "KO", + "EL": "GR" + } + transport_costs.rename(to_rename, inplace=True) + + # add missing Norway with data from Sweden + transport_costs["NO"] = transport_costs["SE"] + + transport_costs.to_csv(snakemake.output[0]) + + +if __name__ == "__main__": + + build_biomass_transport_costs() diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py index ef6845fc..99afc5d6 100644 --- a/scripts/prepare_sector_network.py +++ b/scripts/prepare_sector_network.py @@ -19,10 +19,60 @@ from helper import override_component_attrs import logging logger = logging.getLogger(__name__) +from types import SimpleNamespace +spatial = SimpleNamespace() + + +def define_spatial(nodes): + """ + Namespace for spatial + + Parameters + ---------- + nodes : list-like + """ + + global spatial + global options + + spatial.nodes = nodes + + # biomass + + spatial.biomass = SimpleNamespace() + + if options["biomass_transport"]: + spatial.biomass.nodes = nodes + " solid biomass" + spatial.biomass.locations = nodes + spatial.biomass.industry = nodes + " solid biomass for industry" + spatial.biomass.industry_cc = nodes + " solid biomass for industry CC" + else: + spatial.biomass.nodes = ["EU solid biomass"] + spatial.biomass.locations = ["EU"] + spatial.biomass.industry = ["solid biomass for industry"] + spatial.biomass.industry_cc = ["solid biomass for industry CC"] + + spatial.biomass.df = pd.DataFrame(vars(spatial.biomass), index=nodes) + + # co2 + + spatial.co2 = SimpleNamespace() + + if options["co2_network"]: + spatial.co2.nodes = nodes + " co2 stored" + spatial.co2.locations = nodes + spatial.co2.vents = nodes + " co2 vent" + else: + spatial.co2.nodes = ["co2 stored"] + spatial.co2.locations = ["EU"] + spatial.co2.vents = ["co2 vent"] + + spatial.co2.df = pd.DataFrame(vars(spatial.co2), index=nodes) + def emission_sectors_from_opts(opts): - sectors = ["electricity"] + sectors = ["electricity"] if "T" in opts: sectors += [ "rail non-elec", @@ -54,6 +104,40 @@ def get(item, investment_year=None): return item +def create_network_topology(n, prefix, connector=" -> "): + """ + Create a network topology like the power transmission network. + + Parameters + ---------- + n : pypsa.Network + prefix : str + connector : str + + Returns + ------- + pd.DataFrame with columns bus0, bus1 and length + """ + + ln_attrs = ["bus0", "bus1", "length"] + lk_attrs = ["bus0", "bus1", "length", "underwater_fraction"] + + candidates = pd.concat([ + n.lines[ln_attrs], + n.links.loc[n.links.carrier == "DC", lk_attrs] + ]).fillna(0) + + positive_order = candidates.bus0 < candidates.bus1 + candidates_p = candidates[positive_order] + swap_buses = {"bus0": "bus1", "bus1": "bus0"} + candidates_n = candidates[~positive_order].rename(columns=swap_buses) + candidates = pd.concat([candidates_p, candidates_n]) + + topo = candidates.groupby(["bus0", "bus1"], as_index=False).mean() + topo.index = topo.apply(lambda c: prefix + c.bus0 + connector + c.bus1, axis=1) + return topo + + def co2_emissions_year(countries, opts, year): """ Calculate CO2 emissions in one specific year (e.g. 1990 or 2018). @@ -141,6 +225,53 @@ def add_lifetime_wind_solar(n, costs): n.generators.loc[gen_i, "lifetime"] = costs.at[carrier, 'lifetime'] +def create_network_topology(n, prefix, connector=" -> ", bidirectional=True): + """ + Create a network topology like the power transmission network. + + Parameters + ---------- + n : pypsa.Network + prefix : str + connector : str + bidirectional : bool, default True + True: one link for each connection + False: one link for each connection and direction (back and forth) + + Returns + ------- + pd.DataFrame with columns bus0, bus1 and length + """ + + ln_attrs = ["bus0", "bus1", "length"] + lk_attrs = ["bus0", "bus1", "length", "underwater_fraction"] + + candidates = pd.concat([ + n.lines[ln_attrs], + n.links.loc[n.links.carrier == "DC", lk_attrs] + ]).fillna(0) + + positive_order = candidates.bus0 < candidates.bus1 + candidates_p = candidates[positive_order] + swap_buses = {"bus0": "bus1", "bus1": "bus0"} + candidates_n = candidates[~positive_order].rename(columns=swap_buses) + candidates = pd.concat([candidates_p, candidates_n]) + + def make_index(c): + return prefix + c.bus0 + connector + c.bus1 + + topo = candidates.groupby(["bus0", "bus1"], as_index=False).mean() + topo.index = topo.apply(make_index, axis=1) + + if not bidirectional: + topo_reverse = topo.copy() + topo_reverse.rename(columns=swap_buses, inplace=True) + topo_reverse.index = topo_reverse.apply(make_index, axis=1) + topo = topo.append(topo_reverse) + + return topo + + # TODO merge issue with PyPSA-Eur def update_wind_solar_costs(n, costs): """ @@ -299,26 +430,26 @@ def add_co2_tracking(n, options): ) # this tracks CO2 stored, e.g. underground - n.add("Bus", - "co2 stored", - location="EU", + n.madd("Bus", + spatial.co2.nodes, + location=spatial.co2.locations, carrier="co2 stored" ) - n.add("Store", - "co2 stored", + n.madd("Store", + spatial.co2.nodes, e_nom_extendable=True, - e_nom_max=options['co2_sequestration_potential'] * 1e6, + e_nom_max=np.inf, capital_cost=options['co2_sequestration_cost'], carrier="co2 stored", - bus="co2 stored" + bus=spatial.co2.nodes ) if options['co2_vent']: - n.add("Link", - "co2 vent", - bus0="co2 stored", + n.madd("Link", + spatial.co2.vents, + bus0=spatial.co2.nodes, bus1="co2 atmosphere", carrier="co2 vent", efficiency=1., @@ -326,6 +457,28 @@ def add_co2_tracking(n, options): ) +def add_co2_network(n, costs): + + logger.info("Adding CO2 network.") + co2_links = create_network_topology(n, "CO2 pipeline ") + + cost_onshore = (1 - co2_links.underwater_fraction) * costs.at['CO2 pipeline', 'fixed'] * co2_links.length + cost_submarine = co2_links.underwater_fraction * costs.at['CO2 submarine pipeline', 'fixed'] * co2_links.length + capital_cost = cost_onshore + cost_submarine + + n.madd("Link", + co2_links.index, + bus0=co2_links.bus0.values + " co2 stored", + bus1=co2_links.bus1.values + " co2 stored", + p_min_pu=-1, + p_nom_extendable=True, + length=co2_links.length.values, + capital_cost=capital_cost.values, + carrier="CO2 pipeline", + lifetime=costs.at['CO2 pipeline', 'lifetime'] + ) + + def add_dac(n, costs): heat_carriers = ["urban central heat", "services urban decentral heat"] @@ -339,7 +492,7 @@ def add_dac(n, costs): locations, suffix=" DAC", bus0="co2 atmosphere", - bus1="co2 stored", + bus1=spatial.co2.df.loc[locations, "nodes"].values, bus2=locations.values, bus3=heat_buses, carrier="DAC", @@ -990,10 +1143,11 @@ def add_storage(n, costs): if options['methanation']: n.madd("Link", - nodes + " Sabatier", + spatial.nodes, + suffix=" Sabatier", bus0=nodes + " H2", bus1="EU gas", - bus2="co2 stored", + bus2=spatial.co2.nodes, p_nom_extendable=True, carrier="Sabatier", efficiency=costs.at["methanation", "efficiency"], @@ -1005,10 +1159,11 @@ def add_storage(n, costs): if options['helmeth']: n.madd("Link", - nodes + " helmeth", + spatial.nodes, + suffix=" helmeth", bus0=nodes, bus1="EU gas", - bus2="co2 stored", + bus2=spatial.co2.nodes, carrier="helmeth", p_nom_extendable=True, efficiency=costs.at["helmeth", "efficiency"], @@ -1021,11 +1176,12 @@ def add_storage(n, costs): if options['SMR']: n.madd("Link", - nodes + " SMR CC", + spatial.nodes, + suffix=" SMR CC", bus0="EU gas", bus1=nodes + " H2", bus2="co2 atmosphere", - bus3="co2 stored", + bus3=spatial.co2.nodes, p_nom_extendable=True, carrier="SMR CC", efficiency=costs.at["SMR CC", "efficiency"], @@ -1374,7 +1530,7 @@ def add_heat(n, costs): bus1=nodes[name], bus2=nodes[name] + " urban central heat", bus3="co2 atmosphere", - bus4="co2 stored", + bus4=spatial.co2.df.loc[nodes[name], "nodes"].values, carrier="urban central gas CHP CC", p_nom_extendable=True, capital_cost=costs.at['central gas CHP', 'fixed']*costs.at['central gas CHP', 'efficiency'] + costs.at['biomass CHP capture', 'fixed']*costs.at['gas', 'CO2 intensity'], @@ -1533,8 +1689,16 @@ def add_biomass(n, costs): biomass_potentials = pd.read_csv(snakemake.input.biomass_potentials, index_col=0) - n.add("Carrier", "biogas") + if options["biomass_transport"]: + # potential per node distributed within country by population + biomass_potentials_spatial = (biomass_potentials.loc[pop_layout.ct] + .set_index(pop_layout.index) + .mul(pop_layout.fraction, axis="index") + .rename(index=lambda x: x + " solid biomass")) + else: + biomass_potentials_spatial = biomass_potentials.sum() + n.add("Carrier", "biogas") n.add("Carrier", "solid biomass") n.add("Bus", @@ -1543,9 +1707,9 @@ def add_biomass(n, costs): carrier="biogas" ) - n.add("Bus", - "EU solid biomass", - location="EU", + n.madd("Bus", + spatial.biomass.nodes, + location=spatial.biomass.locations, carrier="solid biomass" ) @@ -1558,13 +1722,13 @@ def add_biomass(n, costs): e_initial=biomass_potentials.loc[countries, "biogas"].sum() ) - n.add("Store", - "EU solid biomass", - bus="EU solid biomass", + n.madd("Store", + spatial.biomass.nodes, + bus=spatial.biomass.nodes, carrier="solid biomass", - e_nom=biomass_potentials.loc[countries, "solid biomass"].sum(), + e_nom=biomass_potentials_spatial["solid biomass"], marginal_cost=costs.at['solid biomass', 'fuel'], - e_initial=biomass_potentials.loc[countries, "solid biomass"].sum() + e_initial=biomass_potentials_spatial["solid biomass"] ) n.add("Link", @@ -1579,6 +1743,32 @@ def add_biomass(n, costs): p_nom_extendable=True ) + if options["biomass_transport"]: + + transport_costs = pd.read_csv( + snakemake.input.biomass_transport_costs, + index_col=0, + squeeze=True + ) + + # add biomass transport + biomass_transport = create_network_topology(n, "biomass transport ", bidirectional=False) + + # costs + bus0_costs = biomass_transport.bus0.apply(lambda x: transport_costs[x[:2]]) + bus1_costs = biomass_transport.bus1.apply(lambda x: transport_costs[x[:2]]) + biomass_transport["costs"] = pd.concat([bus0_costs, bus1_costs], axis=1).mean(axis=1) + + n.madd("Link", + biomass_transport.index, + bus0=biomass_transport.bus0 + " solid biomass", + bus1=biomass_transport.bus1 + " solid biomass", + p_nom_extendable=True, + length=biomass_transport.length.values, + marginal_cost=biomass_transport.costs * biomass_transport.length.values, + capital_cost=1, + carrier="solid biomass transport" + ) #AC buses with district heating urban_central = n.buses.index[n.buses.carrier == "urban central heat"] @@ -1589,7 +1779,7 @@ def add_biomass(n, costs): n.madd("Link", urban_central + " urban central solid biomass CHP", - bus0="EU solid biomass", + bus0=spatial.biomass.df.loc[urban_central, "nodes"].values, bus1=urban_central, bus2=urban_central + " urban central heat", carrier="urban central solid biomass CHP", @@ -1603,11 +1793,11 @@ def add_biomass(n, costs): n.madd("Link", urban_central + " urban central solid biomass CHP CC", - bus0="EU solid biomass", + bus0=spatial.biomass.df.loc[urban_central, "nodes"].values, bus1=urban_central, bus2=urban_central + " urban central heat", bus3="co2 atmosphere", - bus4="co2 stored", + bus4=spatial.co2.df.loc[urban_central, "nodes"].values, carrier="urban central solid biomass CHP CC", p_nom_extendable=True, capital_cost=costs.at[key, 'fixed'] * costs.at[key, 'efficiency'] + costs.at['biomass CHP capture', 'fixed'] * costs.at['solid biomass', 'CO2 intensity'], @@ -1629,36 +1819,39 @@ def add_industry(n, costs): # 1e6 to convert TWh to MWh industrial_demand = pd.read_csv(snakemake.input.industrial_demand, index_col=0) * 1e6 - solid_biomass_by_country = industrial_demand["solid biomass"].groupby(pop_layout.ct).sum() - - n.add("Bus", - "solid biomass for industry", - location="EU", + n.madd("Bus", + spatial.biomass.industry, + location=spatial.biomass.locations, carrier="solid biomass for industry" ) - n.add("Load", - "solid biomass for industry", - bus="solid biomass for industry", + if 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 + + n.madd("Load", + spatial.biomass.industry, + bus=spatial.biomass.industry, carrier="solid biomass for industry", - p_set=solid_biomass_by_country.sum() / 8760 + p_set=p_set ) - n.add("Link", - "solid biomass for industry", - bus0="EU solid biomass", - bus1="solid biomass for industry", + n.madd("Link", + spatial.biomass.industry, + bus0=spatial.biomass.nodes, + bus1=spatial.biomass.industry, carrier="solid biomass for industry", p_nom_extendable=True, efficiency=1. ) - n.add("Link", - "solid biomass for industry CC", - bus0="EU solid biomass", - bus1="solid biomass for industry", + n.madd("Link", + spatial.biomass.industry_cc, + bus0=spatial.biomass.nodes, + bus1=spatial.biomass.industry, bus2="co2 atmosphere", - bus3="co2 stored", + bus3=spatial.co2.nodes, carrier="solid biomass for industry CC", p_nom_extendable=True, capital_cost=costs.at["cement capture", "fixed"] * costs.at['solid biomass', 'CO2 intensity'], @@ -1691,12 +1884,13 @@ def add_industry(n, costs): efficiency2=costs.at['gas', 'CO2 intensity'] ) - n.add("Link", - "gas for industry CC", + n.madd("Link", + spatial.co2.locations, + suffix=" gas for industry CC", bus0="EU gas", bus1="gas for industry", bus2="co2 atmosphere", - bus3="co2 stored", + bus3=spatial.co2.nodes, carrier="gas for industry CC", p_nom_extendable=True, capital_cost=costs.at["cement capture", "fixed"] * costs.at['gas', 'CO2 intensity'], @@ -1827,7 +2021,7 @@ def add_industry(n, costs): nodes + " Fischer-Tropsch", bus0=nodes + " H2", bus1="EU oil", - bus2="co2 stored", + bus2=spatial.co2.nodes, carrier="Fischer-Tropsch", efficiency=costs.at["Fischer-Tropsch", 'efficiency'], capital_cost=costs.at["Fischer-Tropsch", 'fixed'], @@ -1916,11 +2110,12 @@ def add_industry(n, costs): ) #assume enough local waste heat for CC - n.add("Link", - "process emissions CC", + n.madd("Link", + spatial.co2.locations, + suffix=" process emissions CC", bus0="process emissions", bus1="co2 atmosphere", - bus2="co2 stored", + bus2=spatial.co2.nodes, carrier="process emissions CC", p_nom_extendable=True, capital_cost=costs.at["cement capture", "fixed"], @@ -2039,6 +2234,8 @@ if __name__ == "__main__": patch_electricity_network(n) + define_spatial(pop_layout.index) + if snakemake.config["foresight"] == 'myopic': add_lifetime_wind_solar(n, costs) @@ -2061,6 +2258,8 @@ if __name__ == "__main__": if o[:4] == "dist": options['electricity_distribution_grid'] = True options['electricity_distribution_grid_cost_factor'] = float(o[4:].replace("p", ".").replace("m", "-")) + if o == "biomasstransport": + options["biomass_transport"] = True nodal_energy_totals, heat_demand, ashp_cop, gshp_cop, solar_thermal, transport, avail_profile, dsm_profile, nodal_transport_data = prepare_data(n) @@ -2091,6 +2290,9 @@ if __name__ == "__main__": if "noH2network" in opts: remove_h2_network(n) + if options["co2_network"]: + add_co2_network(n, costs) + for o in opts: m = re.match(r'^\d+h$', o, re.IGNORECASE) if m is not None: diff --git a/scripts/solve_network.py b/scripts/solve_network.py index a46acc30..9534a123 100644 --- a/scripts/solve_network.py +++ b/scripts/solve_network.py @@ -3,6 +3,7 @@ import pypsa import numpy as np +import pandas as pd from pypsa.linopt import get_var, linexpr, define_constraints @@ -150,8 +151,26 @@ def add_chp_constraints(n): define_constraints(n, lhs, "<=", 0, 'chplink', 'backpressure') +def add_co2_sequestration_limit(n, sns): + + co2_stores = n.stores.loc[n.stores.carrier=='co2 stored'].index + + if co2_stores.empty or ('Store', 'e') not in n.variables.index: + return + + vars_final_co2_stored = get_var(n, 'Store', 'e').loc[sns[-1], co2_stores] + + lhs = linexpr((1, vars_final_co2_stored)).sum() + rhs = n.config["sector"].get("co2_sequestration_potential", 200) * 1e6 + + name = 'co2_sequestration_limit' + define_constraints(n, lhs, "<=", rhs, 'GlobalConstraint', + 'mu', axes=pd.Index([name]), spec=name) + + def extra_functionality(n, snapshots): add_battery_constraints(n) + add_co2_sequestration_limit(n, snapshots) def solve_network(n, config, opts='', **kwargs):