Merge pull request #242 from PyPSA/nh3

add ammonia as carrier: with Haber-Bosch, crackers, store, load
This commit is contained in:
Fabian Neumann 2022-10-01 16:01:45 +02:00 committed by GitHub
commit 5c6ff44e13
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 119 additions and 6 deletions

View File

@ -252,6 +252,7 @@ sector:
# - onshore # more than 50 km from sea
- nearshore # within 50 km of sea
# - offshore
ammonia: false # can be false (no NH3 carrier), true (copperplated NH3), "regional" (regionalised NH3 without network)
use_fischer_tropsch_waste_heat: true
use_fuel_cell_waste_heat: true
electricity_distribution_grid: true
@ -302,10 +303,12 @@ industry:
2040: 0.3
2045: 0.25
2050: 0.2
MWh_NH3_per_tNH3: 5.166 # LHV
MWh_CH4_per_tNH3_SMR: 10.8 # 2012's demand from https://ec.europa.eu/docsroom/documents/4165/attachments/1/translations/en/renditions/pdf
MWh_elec_per_tNH3_SMR: 0.7 # same source, assuming 94-6% split methane-elec of total energy demand 11.5 MWh/tNH3
MWh_H2_per_tNH3_electrolysis: 6.5 # from https://doi.org/10.1016/j.joule.2018.04.017, around 0.197 tH2/tHN3 (>3/17 since some H2 lost and used for energy)
MWh_elec_per_tNH3_electrolysis: 1.17 # from https://doi.org/10.1016/j.joule.2018.04.017 Table 13 (air separation and HB)
MWh_NH3_per_MWh_H2_cracker: 1.46 # https://github.com/euronion/trace/blob/44a5ff8401762edbef80eff9cfe5a47c8d3c8be4/data/efficiencies.csv
NH3_process_emissions: 24.5 # in MtCO2/a from SMR for H2 production for NH3 from UNFCCC for 2015 for EU28
petrochemical_process_emissions: 25.5 # in MtCO2/a for petrochemical and other from UNFCCC for 2015 for EU28
HVC_primary_fraction: 1. # fraction of today's HVC produced via primary route
@ -586,6 +589,12 @@ plotting:
H2 pipeline retrofitted: '#ba99b5'
H2 Fuel Cell: '#c251ae'
H2 Electrolysis: '#ff29d9'
# ammonia
NH3: '#46caf0'
ammonia: '#46caf0'
ammonia store: '#00ace0'
ammonia cracker: '#87d0e6'
Haber-Bosch: '#076987'
# syngas
Sabatier: '#9850ad'
methanation: '#c44ce6'

View File

@ -73,6 +73,11 @@ incorporates retrofitting options to hydrogen.
* Add option to sweep the global CO2 sequestration potentials with keyword ``seq200`` in the ``{sector_opts}`` wildcard (for limit of 200 Mt CO2).
* Add option to resolve ammonia as separate energy carrier with Haber-Bosch
synthesis, ammonia cracking, storage and industrial demand. The ammonia
carrier can be nodally resolved or copperplated across Europe. This feature is
controlled by ``sector: ammonia:``.
* Updated `data bundle <https://zenodo.org/record/5824485/files/pypsa-eur-sec-data-bundle.tar.gz>`_ that includes the hydrogan salt cavern storage potentials.
**Bugfixes**

View File

@ -65,6 +65,8 @@ def industrial_energy_demand_per_country(country):
df = df_dict[sheet][year].groupby(fuels).sum()
df["ammonia"] = 0.
df['other'] = df['all'] - df.loc[df.index != 'all'].sum()
return df
@ -89,18 +91,21 @@ def add_ammonia_energy_demand(demand):
fn = snakemake.input.ammonia_production
ammonia = pd.read_csv(fn, index_col=0)[str(year)] / 1e3
def ammonia_by_fuel(x):
def get_ammonia_by_fuel(x):
fuels = {'gas': config['MWh_CH4_per_tNH3_SMR'],
'electricity': config['MWh_elec_per_tNH3_SMR']}
return pd.Series({k: x*v for k,v in fuels.items()})
ammonia = ammonia.apply(ammonia_by_fuel).T
ammonia_by_fuel = ammonia.apply(get_ammonia_by_fuel).T
ammonia_by_fuel = ammonia_by_fuel.unstack().reindex(index=demand.index, fill_value=0.)
ammonia = pd.DataFrame({"ammonia": ammonia * config['MWh_NH3_per_tNH3']}).T
demand['Ammonia'] = ammonia.unstack().reindex(index=demand.index, fill_value=0.)
demand['Basic chemicals (without ammonia)'] = demand["Basic chemicals"] - demand["Ammonia"]
demand['Basic chemicals (without ammonia)'] = demand["Basic chemicals"] - ammonia_by_fuel
demand['Basic chemicals (without ammonia)'].clip(lower=0, inplace=True)

View File

@ -9,6 +9,7 @@ if __name__ == '__main__':
'build_industrial_energy_demand_per_node',
simpl='',
clusters=48,
planning_horizons=2030,
)
# import EU ratios df as csv

View File

@ -60,6 +60,7 @@ index = [
"hydrogen",
"heat",
"naphtha",
"ammonia",
"process emission",
"process emission from feedstock",
]
@ -432,8 +433,11 @@ def chemicals_industry():
sector = "Ammonia"
df[sector] = 0.0
df.loc["hydrogen", sector] = config["MWh_H2_per_tNH3_electrolysis"]
df.loc["elec", sector] = config["MWh_elec_per_tNH3_electrolysis"]
if snakemake.config["sector"].get("ammonia", False):
df.loc["ammonia", sector] = config["MWh_NH3_per_tNH3"]
else:
df.loc["hydrogen", sector] = config["MWh_H2_per_tNH3_electrolysis"]
df.loc["elec", sector] = config["MWh_elec_per_tNH3_electrolysis"]
# Chlorine

View File

@ -23,6 +23,8 @@ def rename_techs_tyndp(tech):
return "power-to-gas"
elif tech == "H2":
return "H2 storage"
elif tech in ["NH3", "Haber-Bosch", "ammonia cracker", "ammonia store"]:
return "ammonia"
elif tech in ["OCGT", "CHP", "gas boiler", "H2 Fuel Cell"]:
return "gas-to-power/heat"
elif "solar" in tech:

View File

@ -52,6 +52,7 @@ def rename_techs(label):
"ror": "hydroelectricity",
"hydro": "hydroelectricity",
"PHS": "hydroelectricity",
"NH3": "ammonia",
"co2 Store": "DAC",
"co2 stored": "CO2 sequestration",
"AC": "transmission lines",
@ -107,6 +108,7 @@ preferred_order = pd.Index([
"natural gas",
"helmeth",
"methanation",
"ammonia",
"hydrogen storage",
"power-to-gas",
"power-to-liquid",
@ -255,7 +257,7 @@ def plot_balances():
df = df / 1e6
#remove trailing link ports
df.index = [i[:-1] if ((i != "co2") and (i[-1:] in ["0","1","2","3"])) else i for i in df.index]
df.index = [i[:-1] if ((i not in ["co2", "NH3"]) and (i[-1:] in ["0","1","2","3"])) else i for i in df.index]
df = df.groupby(df.index.map(rename_techs)).sum()

View File

@ -93,6 +93,19 @@ def define_spatial(nodes, options):
spatial.gas.df = pd.DataFrame(vars(spatial.gas), index=nodes)
# ammonia
if options.get('ammonia'):
spatial.ammonia = SimpleNamespace()
if options.get("ammonia") == "regional":
spatial.ammonia.nodes = nodes + " NH3"
spatial.ammonia.locations = nodes
else:
spatial.ammonia.nodes = ["EU NH3"]
spatial.ammonia.locations = ["EU"]
spatial.ammonia.df = pd.DataFrame(vars(spatial.ammonia), index=nodes)
# oil
spatial.oil = SimpleNamespace()
spatial.oil.nodes = ["EU oil"]
@ -664,6 +677,61 @@ def add_generation(n, costs):
)
def add_ammonia(n, costs):
logger.info("adding ammonia carrier with synthesis, cracking and storage")
nodes = pop_layout.index
cf_industry = snakemake.config["industry"]
n.add("Carrier", "NH3")
n.madd("Bus",
spatial.ammonia.nodes,
location=spatial.ammonia.locations,
carrier="NH3"
)
n.madd("Link",
nodes,
suffix=" Haber-Bosch",
bus0=nodes,
bus1=spatial.ammonia.nodes,
bus2=nodes + " H2",
p_nom_extendable=True,
carrier="Haber-Bosch",
efficiency=1 / (cf_industry["MWh_elec_per_tNH3_electrolysis"] / cf_industry["MWh_NH3_per_tNH3"]), # output: MW_NH3 per MW_elec
efficiency2=-cf_industry["MWh_H2_per_tNH3_electrolysis"] / cf_industry["MWh_elec_per_tNH3_electrolysis"], # input: MW_H2 per MW_elec
capital_cost=costs.at["Haber-Bosch synthesis", "fixed"],
lifetime=costs.at["Haber-Bosch synthesis", 'lifetime']
)
n.madd("Link",
nodes,
suffix=" ammonia cracker",
bus0=spatial.ammonia.nodes,
bus1=nodes + " H2",
p_nom_extendable=True,
carrier="ammonia cracker",
efficiency=1 / cf_industry["MWh_NH3_per_MWh_H2_cracker"],
capital_cost=costs.at["Ammonia cracker", "fixed"] / cf_industry["MWh_NH3_per_MWh_H2_cracker"], # given per MW_H2
lifetime=costs.at['Ammonia cracker', 'lifetime']
)
# Ammonia Storage
n.madd("Store",
spatial.ammonia.nodes,
suffix=" ammonia store",
bus=spatial.ammonia.nodes,
e_nom_extendable=True,
e_cyclic=True,
carrier="ammonia store",
capital_cost=costs.at["NH3 (l) storage tank incl. liquefaction", "fixed"],
lifetime=costs.at['NH3 (l) storage tank incl. liquefaction', 'lifetime']
)
def add_wave(n, wave_cost_factor):
# TODO: handle in Snakefile
@ -2278,6 +2346,20 @@ def add_industry(n, costs):
lifetime=costs.at['cement capture', 'lifetime']
)
if options.get("ammonia"):
if options["ammonia"] == 'regional':
p_set = industrial_demand.loc[spatial.ammonia.locations, "ammonia"].rename(index=lambda x: x + " NH3") / 8760
else:
p_set = industrial_demand["ammonia"].sum() / 8760
n.madd("Load",
spatial.ammonia.nodes,
bus=spatial.ammonia.nodes,
carrier="NH3",
p_set=p_set
)
def add_waste_heat(n):
# TODO options?
@ -2591,6 +2673,9 @@ if __name__ == "__main__":
if options['dac']:
add_dac(n, costs)
if options['ammonia']:
add_ammonia(n, costs)
if "decentral" in opts:
decentral(n)