commit
ec04d7909a
@ -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
|
||||||
@ -467,6 +468,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
|
||||||
|
@ -62,6 +62,14 @@ 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``.
|
||||||
|
@ -48,7 +48,7 @@ Solid biomass: single node for Europe, until transport costs can be
|
|||||||
incorporated.
|
incorporated.
|
||||||
|
|
||||||
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.
|
||||||
|
@ -19,6 +19,37 @@ 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
|
||||||
|
|
||||||
|
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 +85,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).
|
||||||
@ -299,26 +364,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 +391,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 +426,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 +1077,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 +1093,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 +1110,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 +1464,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'],
|
||||||
@ -1607,7 +1697,7 @@ def add_biomass(n, costs):
|
|||||||
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'],
|
||||||
@ -1653,12 +1743,13 @@ def add_industry(n, costs):
|
|||||||
efficiency=1.
|
efficiency=1.
|
||||||
)
|
)
|
||||||
|
|
||||||
n.add("Link",
|
n.madd("Link",
|
||||||
"solid biomass for industry CC",
|
spatial.co2.locations,
|
||||||
|
suffix=" solid biomass for industry CC",
|
||||||
bus0="EU solid biomass",
|
bus0="EU solid biomass",
|
||||||
bus1="solid biomass for industry",
|
bus1="solid biomass for 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 +1782,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 +1919,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 +2008,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 +2132,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)
|
||||||
@ -2091,6 +2186,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:
|
||||||
|
@ -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):
|
||||||
|
Loading…
Reference in New Issue
Block a user