Merge branch 'master' of github.com:nworbmot/PyPSA-Eur-Sec

This commit is contained in:
Fabian Neumann 2021-09-28 17:24:09 +02:00
commit 8cf8a49b48
7 changed files with 413 additions and 63 deletions

View File

@ -1,4 +1,7 @@
from snakemake.remote.HTTP import RemoteProvider as HTTPRemoteProvider
HTTP = HTTPRemoteProvider()
configfile: "config.yaml" configfile: "config.yaml"
@ -180,6 +183,21 @@ rule build_biomass_potentials:
script: 'scripts/build_biomass_potentials.py' 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: rule build_ammonia_production:
input: input:
usgs="data/myb1-2017-nitro.xls" usgs="data/myb1-2017-nitro.xls"
@ -321,8 +339,8 @@ rule prepare_sector_network:
energy_totals_name='resources/energy_totals.csv', energy_totals_name='resources/energy_totals.csv',
co2_totals_name='resources/co2_totals.csv', co2_totals_name='resources/co2_totals.csv',
transport_name='resources/transport_data.csv', transport_name='resources/transport_data.csv',
traffic_data_KFZ = "data/emobility/KFZ__count", traffic_data_KFZ="data/emobility/KFZ__count",
traffic_data_Pkw = "data/emobility/Pkw__count", traffic_data_Pkw="data/emobility/Pkw__count",
biomass_potentials='resources/biomass_potentials.csv', biomass_potentials='resources/biomass_potentials.csv',
heat_profile="data/heat_load_profile_BDEW.csv", heat_profile="data/heat_load_profile_BDEW.csv",
costs=CDIR + "costs_{planning_horizons}.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_total="resources/solar_thermal_total_elec_s{simpl}_{clusters}.nc",
solar_thermal_urban="resources/solar_thermal_urban_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", 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' output: RDIR + '/prenetworks/elec_s{simpl}_{clusters}_lv{lv}_{opts}_{sector_opts}_{planning_horizons}.nc'
threads: 1 threads: 1
resources: mem_mb=2000 resources: mem_mb=2000

View File

@ -223,7 +223,8 @@ sector:
co2_vent: true co2_vent: true
SMR: true SMR: true
co2_sequestration_potential: 200 #MtCO2/a sequestration potential for Europe 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 cc_fraction: 0.9 # default fraction of CO2 captured with post-combustion capture
hydrogen_underground_storage: true hydrogen_underground_storage: true
use_fischer_tropsch_waste_heat: true use_fischer_tropsch_waste_heat: true
@ -233,6 +234,7 @@ sector:
electricity_grid_connection: true # only applies to onshore wind and utility PV electricity_grid_connection: true # only applies to onshore wind and utility PV
gas_distribution_grid: true gas_distribution_grid: true
gas_distribution_grid_cost_factor: 1.0 #multiplies cost in data/costs.csv gas_distribution_grid_cost_factor: 1.0 #multiplies cost in data/costs.csv
biomass_transport: false # biomass transport between nodes
conventional_generation: # generator : carrier conventional_generation: # generator : carrier
OCGT: gas OCGT: gas
@ -480,6 +482,7 @@ plotting:
hot water storage: '#BBBBBB' hot water storage: '#BBBBBB'
hot water charging: '#BBBBBB' hot water charging: '#BBBBBB'
hot water discharging: '#999999' hot water discharging: '#999999'
CO2 pipeline: '#999999'
CHP: r CHP: r
CHP heat: r CHP heat: r
CHP electric: r CHP electric: r
@ -520,5 +523,6 @@ plotting:
shipping oil: "#6495ED" shipping oil: "#6495ED"
shipping oil emissions: "#6495ED" shipping oil emissions: "#6495ED"
electricity distribution grid: '#333333' electricity distribution grid: '#333333'
solid biomass transport: green
H2 for industry: "#222222" H2 for industry: "#222222"
H2 for shipping: "#6495ED" H2 for shipping: "#6495ED"

View File

@ -62,12 +62,26 @@ Future release
These are included in the environment specifications of PyPSA-Eur. These are included in the environment specifications of PyPSA-Eur.
* Consistent use of ``__main__`` block and further unspecific code cleaning. * Consistent use of ``__main__`` block and further unspecific code cleaning.
* Distinguish costs for home battery storage and inverter from utility-scale battery costs. * 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. * Added option for hydrogen liquefaction costs for hydrogen demand in shipping.
This introduces a new ``H2 liquid`` bus at each location. This introduces a new ``H2 liquid`` bus at each location.
It is activated via ``sector: shipping_hydrogen_liquefaction: true``. 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 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. * 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. * 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 <http://dx.doi.org/10.2790/01017>`_
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. * Compatibility with ``xarray`` version 0.19.
* Separate basic chemicals into HVC, chlorine, methanol and ammonia [`#166 <https://github.com/PyPSA/PyPSA-Eur-Sec/pull/166>`_]. * Separate basic chemicals into HVC, chlorine, methanol and ammonia [`#166 <https://github.com/PyPSA/PyPSA-Eur-Sec/pull/166>`_].
* Add option to specify reuse, primary production, and mechanical and chemical recycling fraction of platics [`#166 <https://github.com/PyPSA/PyPSA-Eur-Sec/pull/166>`_]. * Add option to specify reuse, primary production, and mechanical and chemical recycling fraction of platics [`#166 <https://github.com/PyPSA/PyPSA-Eur-Sec/pull/166>`_].

View File

@ -44,11 +44,13 @@ Hydrogen network: nodal.
Methane network: single node for Europe, since future demand is so Methane network: single node for Europe, since future demand is so
low and no bottlenecks are expected. low and no bottlenecks are expected.
Solid biomass: single node for Europe, until transport costs can be Solid biomass: choice between single node for Europe and nodal where biomass
incorporated. 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 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 Liquid hydrocarbons: single node for Europe, since transport costs for
liquids are low. liquids are low.

View File

@ -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()

View File

@ -19,6 +19,56 @@ from helper import override_component_attrs
import logging import logging
logger = logging.getLogger(__name__) 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): def emission_sectors_from_opts(opts):
@ -54,6 +104,40 @@ def get(item, investment_year=None):
return item 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): def co2_emissions_year(countries, opts, year):
""" """
Calculate CO2 emissions in one specific year (e.g. 1990 or 2018). 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'] 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 # TODO merge issue with PyPSA-Eur
def update_wind_solar_costs(n, costs): def update_wind_solar_costs(n, costs):
""" """
@ -299,26 +430,26 @@ def add_co2_tracking(n, options):
) )
# this tracks CO2 stored, e.g. underground # this tracks CO2 stored, e.g. underground
n.add("Bus", n.madd("Bus",
"co2 stored", spatial.co2.nodes,
location="EU", location=spatial.co2.locations,
carrier="co2 stored" carrier="co2 stored"
) )
n.add("Store", n.madd("Store",
"co2 stored", spatial.co2.nodes,
e_nom_extendable=True, e_nom_extendable=True,
e_nom_max=options['co2_sequestration_potential'] * 1e6, e_nom_max=np.inf,
capital_cost=options['co2_sequestration_cost'], capital_cost=options['co2_sequestration_cost'],
carrier="co2 stored", carrier="co2 stored",
bus="co2 stored" bus=spatial.co2.nodes
) )
if options['co2_vent']: if options['co2_vent']:
n.add("Link", n.madd("Link",
"co2 vent", spatial.co2.vents,
bus0="co2 stored", bus0=spatial.co2.nodes,
bus1="co2 atmosphere", bus1="co2 atmosphere",
carrier="co2 vent", carrier="co2 vent",
efficiency=1., 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): def add_dac(n, costs):
heat_carriers = ["urban central heat", "services urban decentral heat"] heat_carriers = ["urban central heat", "services urban decentral heat"]
@ -339,7 +492,7 @@ def add_dac(n, costs):
locations, locations,
suffix=" DAC", suffix=" DAC",
bus0="co2 atmosphere", bus0="co2 atmosphere",
bus1="co2 stored", bus1=spatial.co2.df.loc[locations, "nodes"].values,
bus2=locations.values, bus2=locations.values,
bus3=heat_buses, bus3=heat_buses,
carrier="DAC", carrier="DAC",
@ -990,10 +1143,11 @@ def add_storage(n, costs):
if options['methanation']: if options['methanation']:
n.madd("Link", n.madd("Link",
nodes + " Sabatier", spatial.nodes,
suffix=" Sabatier",
bus0=nodes + " H2", bus0=nodes + " H2",
bus1="EU gas", bus1="EU gas",
bus2="co2 stored", bus2=spatial.co2.nodes,
p_nom_extendable=True, p_nom_extendable=True,
carrier="Sabatier", carrier="Sabatier",
efficiency=costs.at["methanation", "efficiency"], efficiency=costs.at["methanation", "efficiency"],
@ -1005,10 +1159,11 @@ def add_storage(n, costs):
if options['helmeth']: if options['helmeth']:
n.madd("Link", n.madd("Link",
nodes + " helmeth", spatial.nodes,
suffix=" helmeth",
bus0=nodes, bus0=nodes,
bus1="EU gas", bus1="EU gas",
bus2="co2 stored", bus2=spatial.co2.nodes,
carrier="helmeth", carrier="helmeth",
p_nom_extendable=True, p_nom_extendable=True,
efficiency=costs.at["helmeth", "efficiency"], efficiency=costs.at["helmeth", "efficiency"],
@ -1021,11 +1176,12 @@ def add_storage(n, costs):
if options['SMR']: if options['SMR']:
n.madd("Link", n.madd("Link",
nodes + " SMR CC", spatial.nodes,
suffix=" SMR CC",
bus0="EU gas", bus0="EU gas",
bus1=nodes + " H2", bus1=nodes + " H2",
bus2="co2 atmosphere", bus2="co2 atmosphere",
bus3="co2 stored", bus3=spatial.co2.nodes,
p_nom_extendable=True, p_nom_extendable=True,
carrier="SMR CC", carrier="SMR CC",
efficiency=costs.at["SMR CC", "efficiency"], efficiency=costs.at["SMR CC", "efficiency"],
@ -1374,7 +1530,7 @@ def add_heat(n, costs):
bus1=nodes[name], bus1=nodes[name],
bus2=nodes[name] + " urban central heat", bus2=nodes[name] + " urban central heat",
bus3="co2 atmosphere", bus3="co2 atmosphere",
bus4="co2 stored", bus4=spatial.co2.df.loc[nodes[name], "nodes"].values,
carrier="urban central gas CHP CC", carrier="urban central gas CHP CC",
p_nom_extendable=True, 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'], 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) 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("Carrier", "solid biomass")
n.add("Bus", n.add("Bus",
@ -1543,9 +1707,9 @@ def add_biomass(n, costs):
carrier="biogas" carrier="biogas"
) )
n.add("Bus", n.madd("Bus",
"EU solid biomass", spatial.biomass.nodes,
location="EU", location=spatial.biomass.locations,
carrier="solid biomass" carrier="solid biomass"
) )
@ -1558,13 +1722,13 @@ def add_biomass(n, costs):
e_initial=biomass_potentials.loc[countries, "biogas"].sum() e_initial=biomass_potentials.loc[countries, "biogas"].sum()
) )
n.add("Store", n.madd("Store",
"EU solid biomass", spatial.biomass.nodes,
bus="EU solid biomass", bus=spatial.biomass.nodes,
carrier="solid biomass", 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'], 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", n.add("Link",
@ -1579,6 +1743,32 @@ def add_biomass(n, costs):
p_nom_extendable=True 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 #AC buses with district heating
urban_central = n.buses.index[n.buses.carrier == "urban central heat"] urban_central = n.buses.index[n.buses.carrier == "urban central heat"]
@ -1589,7 +1779,7 @@ def add_biomass(n, costs):
n.madd("Link", n.madd("Link",
urban_central + " urban central solid biomass CHP", urban_central + " urban central solid biomass CHP",
bus0="EU solid biomass", bus0=spatial.biomass.df.loc[urban_central, "nodes"].values,
bus1=urban_central, bus1=urban_central,
bus2=urban_central + " urban central heat", bus2=urban_central + " urban central heat",
carrier="urban central solid biomass CHP", carrier="urban central solid biomass CHP",
@ -1603,11 +1793,11 @@ def add_biomass(n, costs):
n.madd("Link", n.madd("Link",
urban_central + " urban central solid biomass CHP CC", urban_central + " urban central solid biomass CHP CC",
bus0="EU solid biomass", bus0=spatial.biomass.df.loc[urban_central, "nodes"].values,
bus1=urban_central, bus1=urban_central,
bus2=urban_central + " urban central heat", bus2=urban_central + " urban central heat",
bus3="co2 atmosphere", bus3="co2 atmosphere",
bus4="co2 stored", bus4=spatial.co2.df.loc[urban_central, "nodes"].values,
carrier="urban central solid biomass CHP CC", carrier="urban central solid biomass CHP CC",
p_nom_extendable=True, 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'], 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 # 1e6 to convert TWh to MWh
industrial_demand = pd.read_csv(snakemake.input.industrial_demand, index_col=0) * 1e6 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.madd("Bus",
spatial.biomass.industry,
n.add("Bus", location=spatial.biomass.locations,
"solid biomass for industry",
location="EU",
carrier="solid biomass for industry" carrier="solid biomass for industry"
) )
n.add("Load", if options["biomass_transport"]:
"solid biomass for industry", p_set = industrial_demand.loc[spatial.biomass.locations, "solid biomass"].rename(index=lambda x: x + " solid biomass for industry") / 8760
bus="solid biomass for industry", else:
p_set = industrial_demand["solid biomass"].sum() / 8760
n.madd("Load",
spatial.biomass.industry,
bus=spatial.biomass.industry,
carrier="solid biomass for industry", carrier="solid biomass for industry",
p_set=solid_biomass_by_country.sum() / 8760 p_set=p_set
) )
n.add("Link", n.madd("Link",
"solid biomass for industry", spatial.biomass.industry,
bus0="EU solid biomass", bus0=spatial.biomass.nodes,
bus1="solid biomass for industry", bus1=spatial.biomass.industry,
carrier="solid biomass for industry", carrier="solid biomass for industry",
p_nom_extendable=True, p_nom_extendable=True,
efficiency=1. efficiency=1.
) )
n.add("Link", n.madd("Link",
"solid biomass for industry CC", spatial.biomass.industry_cc,
bus0="EU solid biomass", bus0=spatial.biomass.nodes,
bus1="solid biomass for industry", bus1=spatial.biomass.industry,
bus2="co2 atmosphere", bus2="co2 atmosphere",
bus3="co2 stored", bus3=spatial.co2.nodes,
carrier="solid biomass for industry CC", carrier="solid biomass for industry CC",
p_nom_extendable=True, p_nom_extendable=True,
capital_cost=costs.at["cement capture", "fixed"] * costs.at['solid biomass', 'CO2 intensity'], 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'] efficiency2=costs.at['gas', 'CO2 intensity']
) )
n.add("Link", n.madd("Link",
"gas for industry CC", spatial.co2.locations,
suffix=" gas for industry CC",
bus0="EU gas", bus0="EU gas",
bus1="gas for industry", bus1="gas for industry",
bus2="co2 atmosphere", bus2="co2 atmosphere",
bus3="co2 stored", bus3=spatial.co2.nodes,
carrier="gas for industry CC", carrier="gas for industry CC",
p_nom_extendable=True, p_nom_extendable=True,
capital_cost=costs.at["cement capture", "fixed"] * costs.at['gas', 'CO2 intensity'], capital_cost=costs.at["cement capture", "fixed"] * costs.at['gas', 'CO2 intensity'],
@ -1827,7 +2021,7 @@ def add_industry(n, costs):
nodes + " Fischer-Tropsch", nodes + " Fischer-Tropsch",
bus0=nodes + " H2", bus0=nodes + " H2",
bus1="EU oil", bus1="EU oil",
bus2="co2 stored", bus2=spatial.co2.nodes,
carrier="Fischer-Tropsch", carrier="Fischer-Tropsch",
efficiency=costs.at["Fischer-Tropsch", 'efficiency'], efficiency=costs.at["Fischer-Tropsch", 'efficiency'],
capital_cost=costs.at["Fischer-Tropsch", 'fixed'], capital_cost=costs.at["Fischer-Tropsch", 'fixed'],
@ -1916,11 +2110,12 @@ def add_industry(n, costs):
) )
#assume enough local waste heat for CC #assume enough local waste heat for CC
n.add("Link", n.madd("Link",
"process emissions CC", spatial.co2.locations,
suffix=" process emissions CC",
bus0="process emissions", bus0="process emissions",
bus1="co2 atmosphere", bus1="co2 atmosphere",
bus2="co2 stored", bus2=spatial.co2.nodes,
carrier="process emissions CC", carrier="process emissions CC",
p_nom_extendable=True, p_nom_extendable=True,
capital_cost=costs.at["cement capture", "fixed"], capital_cost=costs.at["cement capture", "fixed"],
@ -2039,6 +2234,8 @@ if __name__ == "__main__":
patch_electricity_network(n) patch_electricity_network(n)
define_spatial(pop_layout.index)
if snakemake.config["foresight"] == 'myopic': if snakemake.config["foresight"] == 'myopic':
add_lifetime_wind_solar(n, costs) add_lifetime_wind_solar(n, costs)
@ -2061,6 +2258,8 @@ if __name__ == "__main__":
if o[:4] == "dist": if o[:4] == "dist":
options['electricity_distribution_grid'] = True options['electricity_distribution_grid'] = True
options['electricity_distribution_grid_cost_factor'] = float(o[4:].replace("p", ".").replace("m", "-")) 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) 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: if "noH2network" in opts:
remove_h2_network(n) remove_h2_network(n)
if options["co2_network"]:
add_co2_network(n, costs)
for o in opts: for o in opts:
m = re.match(r'^\d+h$', o, re.IGNORECASE) m = re.match(r'^\d+h$', o, re.IGNORECASE)
if m is not None: if m is not None:

View File

@ -3,6 +3,7 @@
import pypsa import pypsa
import numpy as np import numpy as np
import pandas as pd
from pypsa.linopt import get_var, linexpr, define_constraints 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') 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): def extra_functionality(n, snapshots):
add_battery_constraints(n) add_battery_constraints(n)
add_co2_sequestration_limit(n, snapshots)
def solve_network(n, config, opts='', **kwargs): def solve_network(n, config, opts='', **kwargs):