From d1427175638cd926dfbc6bbb6fc9357670b94663 Mon Sep 17 00:00:00 2001
From: Philipp Glaum
Date: Mon, 29 Jul 2024 14:49:57 +0200
Subject: [PATCH] add methanol techs for master to branch
---
config/config.default.yaml | 31 +-
rules/build_sector.smk | 3 +
scripts/prepare_sector_network.py | 459 ++++++++++++++++++++++++++++--
3 files changed, 461 insertions(+), 32 deletions(-)
diff --git a/config/config.default.yaml b/config/config.default.yaml
index 0af34734..a31abd3d 100644
--- a/config/config.default.yaml
+++ b/config/config.default.yaml
@@ -541,7 +541,6 @@ sector:
hydrogen_turbine: false
SMR: true
SMR_cc: true
- regional_methanol_demand: false
regional_oil_demand: false
regional_coal_demand: false
regional_co2_sequestration_potential:
@@ -567,6 +566,21 @@ sector:
# - onshore # more than 50 km from sea
- nearshore # within 50 km of sea
# - offshore
+ methanol: false # if industry is modelled, methanol is still added even if false
+ methanol_spatial: false # if true demand is also regional even if regional demand is set to false, since methanol is regionally resolved
+ regional_methanol_demand: false
+ methanol_transport: false
+ methanol_reforming: false
+ methanol_reforming_cc: false
+ methanol_to_kerosene: false
+ methanol_to_olefins: false
+ methanol_to_power:
+ ccgt: false
+ ccgt_cc: false
+ ocgt: false
+ allam: false
+ biomass_to_methanol: false
+ biomass_to_methanol_cc: false
ammonia: false
min_part_load_fischer_tropsch: 0.5
min_part_load_methanolisation: 0.3
@@ -1157,8 +1171,19 @@ plotting:
liquid: '#25c49a'
kerosene for aviation: '#a1ffe6'
naphtha for industry: '#57ebc4'
- methanolisation: '#83d6d5'
- methanol: '#468c8b'
+ methanol-to-kerosene: '#C98468'
+ methanol-to-olefins/aromatics: '#FFA07A'
+ Methanol steam reforming: '#FFBF00'
+ Methanol steam reforming CC: '#A2EA8A'
+ methanolisation: '#00FFBF'
+ biomass-to-methanol: #EAD28A
+ biomass-to-methanol CC: #EADBAD
+ allam methanol: '#B98F76'
+ CCGT methanol: '#B98F76'
+ CCGT methanol CC: '#B98F76'
+ OCGT methanol: '#B98F76'
+ methanol: '#FF7B00'
+ methanol transport: '#FF7B00'
shipping methanol: '#468c8b'
industry methanol: '#468c8b'
# co2
diff --git a/rules/build_sector.smk b/rules/build_sector.smk
index 6614b163..276f61e5 100644
--- a/rules/build_sector.smk
+++ b/rules/build_sector.smk
@@ -1020,6 +1020,9 @@ rule prepare_sector_network:
hourly_heat_demand_total=resources(
"hourly_heat_demand_total_elec_s{simpl}_{clusters}.nc"
),
+ industrial_production=resources(
+ "industrial_production_elec_s{simpl}_{clusters}_{planning_horizons}.csv"
+ ),
district_heat_share=resources(
"district_heat_share_elec_s{simpl}_{clusters}_{planning_horizons}.csv"
),
diff --git a/scripts/prepare_sector_network.py b/scripts/prepare_sector_network.py
index 8cfa62a4..2288354d 100644
--- a/scripts/prepare_sector_network.py
+++ b/scripts/prepare_sector_network.py
@@ -141,17 +141,24 @@ def define_spatial(nodes, options):
spatial.methanol = SimpleNamespace()
- spatial.methanol.nodes = ["EU methanol"]
- spatial.methanol.locations = ["EU"]
-
- if options["regional_methanol_demand"]:
+ if options.get("methanol_spatial", False):
+ spatial.methanol.nodes = nodes + " methanol"
+ spatial.methanol.locations = nodes
spatial.methanol.demand_locations = nodes
spatial.methanol.industry = nodes + " industry methanol"
spatial.methanol.shipping = nodes + " shipping methanol"
else:
- spatial.methanol.demand_locations = ["EU"]
- spatial.methanol.shipping = ["EU shipping methanol"]
- spatial.methanol.industry = ["EU industry methanol"]
+ spatial.methanol.nodes = ["EU methanol"]
+ spatial.methanol.locations = ["EU"]
+
+ if options["regional_methanol_demand"]:
+ spatial.methanol.demand_locations = nodes
+ spatial.methanol.industry = nodes + " industry methanol"
+ spatial.methanol.shipping = nodes + " shipping methanol"
+ else:
+ spatial.methanol.demand_locations = ["EU"]
+ spatial.methanol.shipping = ["EU shipping methanol"]
+ spatial.methanol.industry = ["EU industry methanol"]
# oil
spatial.oil = SimpleNamespace()
@@ -762,6 +769,331 @@ def add_allam(n, costs):
)
+def add_biomass_to_methanol(n, costs):
+
+ if len(spatial.biomass.nodes) <= 1 and len(spatial.methanol.nodes) > 1:
+ link_names = nodes + " " + spatial.biomass.nodes
+ else:
+ link_names = spatial.biomass.nodes
+
+ n.madd(
+ "Link",
+ link_names,
+ suffix=" biomass-to-methanol",
+ bus0=spatial.biomass.nodes,
+ bus1=spatial.methanol.nodes,
+ bus2="co2 atmosphere",
+ carrier="biomass-to-methanol",
+ lifetime=costs.at["biomass-to-methanol", "lifetime"],
+ efficiency=costs.at["biomass-to-methanol", "efficiency"],
+ efficiency2=-costs.at["solid biomass", "CO2 intensity"]
+ + costs.at["biomass-to-methanol", "CO2 stored"],
+ p_nom_extendable=True,
+ capital_cost=costs.at["biomass-to-methanol", "fixed"]
+ / costs.at["biomass-to-methanol", "efficiency"],
+ marginal_cost=costs.loc["biomass-to-methanol", "VOM"]
+ / costs.at["biomass-to-methanol", "efficiency"],
+ )
+
+
+def add_biomass_to_methanol_cc(n, costs):
+
+ if len(spatial.biomass.nodes) <= 1 and len(spatial.methanol.nodes) > 1:
+ link_names = nodes + " " + spatial.biomass.nodes
+ else:
+ link_names = spatial.biomass.nodes
+
+ n.madd(
+ "Link",
+ link_names,
+ suffix=" biomass-to-methanol CC",
+ bus0=spatial.biomass.nodes,
+ bus1=spatial.methanol.nodes,
+ bus2="co2 atmosphere",
+ bus3=spatial.co2.nodes,
+ carrier="biomass-to-methanol CC",
+ lifetime=costs.at["biomass-to-methanol", "lifetime"],
+ efficiency=costs.at["biomass-to-methanol", "efficiency"],
+ efficiency2=-costs.at["solid biomass", "CO2 intensity"]
+ + costs.at["biomass-to-methanol", "CO2 stored"]
+ * (1 - costs.at["biomass-to-methanol", "capture rate"]),
+ efficiency3=costs.at["biomass-to-methanol", "CO2 stored"]
+ * costs.at["biomass-to-methanol", "capture rate"],
+ p_nom_extendable=True,
+ capital_cost=costs.at["biomass-to-methanol", "fixed"]
+ / costs.at["biomass-to-methanol", "efficiency"]
+ + costs.at["biomass CHP capture", "fixed"]
+ * costs.at["biomass-to-methanol", "CO2 stored"],
+ marginal_cost=costs.loc["biomass-to-methanol", "VOM"]
+ / costs.at["biomass-to-methanol", "efficiency"],
+ )
+
+
+def add_methanol_to_power(n, costs, types={}):
+ # TODO: add costs to technology-data
+
+ nodes = pop_layout.index
+
+ if types["allam"]:
+ logger.info("Adding Allam cycle methanol power plants.")
+
+ n.madd(
+ "Link",
+ nodes,
+ suffix=" allam methanol",
+ bus0=spatial.methanol.nodes,
+ bus1=nodes,
+ bus2=spatial.co2.df.loc[nodes, "nodes"].values,
+ bus3="co2 atmosphere",
+ carrier="allam methanol",
+ p_nom_extendable=True,
+ capital_cost=0.59
+ * 1.832e6
+ * calculate_annuity(25, 0.07), # efficiency * EUR/MW * annuity
+ marginal_cost=2,
+ efficiency=0.59,
+ efficiency2=0.98 * costs.at["methanolisation", "carbondioxide-input"],
+ efficiency3=0.02 * costs.at["methanolisation", "carbondioxide-input"],
+ lifetime=25,
+ )
+
+ if types["ccgt"]:
+ logger.info("Adding methanol CCGT power plants.")
+
+ # efficiency * EUR/MW * (annuity + FOM)
+ capital_cost = 0.58 * 916e3 * (calculate_annuity(25, 0.07) + 0.035)
+
+ n.madd(
+ "Link",
+ nodes,
+ suffix=" CCGT methanol",
+ bus0=spatial.methanol.nodes,
+ bus1=nodes,
+ bus2="co2 atmosphere",
+ carrier="CCGT methanol",
+ p_nom_extendable=True,
+ capital_cost=capital_cost,
+ marginal_cost=2,
+ efficiency=0.58,
+ efficiency2=costs.at["methanolisation", "carbondioxide-input"],
+ lifetime=25,
+ )
+
+ if types["ccgt_cc"]:
+ logger.info(
+ "Adding methanol CCGT power plants with post-combustion carbon capture."
+ )
+
+ # TODO consider efficiency changes / energy inputs for CC
+
+ # efficiency * EUR/MW * (annuity + FOM)
+ capital_cost = 0.58 * 916e3 * (calculate_annuity(25, 0.07) + 0.035)
+
+ capital_cost_cc = (
+ capital_cost
+ + costs.at["cement capture", "fixed"]
+ * costs.at["methanolisation", "carbondioxide-input"]
+ )
+
+ n.madd(
+ "Link",
+ nodes,
+ suffix=" CCGT methanol CC",
+ bus0=spatial.methanol.nodes,
+ bus1=nodes,
+ bus2=spatial.co2.df.loc[nodes, "nodes"].values,
+ bus3="co2 atmosphere",
+ carrier="CCGT methanol CC",
+ p_nom_extendable=True,
+ capital_cost=capital_cost_cc,
+ marginal_cost=2,
+ efficiency=0.58,
+ efficiency2=costs.at["cement capture", "capture_rate"]
+ * costs.at["methanolisation", "carbondioxide-input"],
+ efficiency3=(1 - costs.at["cement capture", "capture_rate"])
+ * costs.at["methanolisation", "carbondioxide-input"],
+ lifetime=25,
+ )
+
+ if types["ocgt"]:
+ logger.info("Adding methanol OCGT power plants.")
+
+ n.madd(
+ "Link",
+ nodes,
+ suffix=" OCGT methanol",
+ bus0=spatial.methanol.nodes,
+ bus1=nodes,
+ bus2="co2 atmosphere",
+ carrier="OCGT methanol",
+ p_nom_extendable=True,
+ capital_cost=0.35
+ * 458e3
+ * (
+ calculate_annuity(25, 0.07) + 0.035
+ ), # efficiency * EUR/MW * (annuity + FOM)
+ marginal_cost=2,
+ efficiency=0.35,
+ efficiency2=costs.at["methanolisation", "carbondioxide-input"],
+ lifetime=25,
+ )
+
+
+def add_methanol_to_olefins(n, costs):
+ nodes = pop_layout.index
+ nhours = n.snapshot_weightings.generators.sum()
+ nyears = nhours / 8760
+
+ tech = "methanol-to-olefins/aromatics"
+
+ logger.info(f"Adding {tech}.")
+
+ demand_factor = options["HVC_demand_factor"]
+
+ industrial_production = (
+ pd.read_csv(snakemake.input.industrial_production, index_col=0)
+ * 1e3
+ * nyears # kt/a -> t/a
+ )
+
+ p_nom_max = (
+ demand_factor
+ * industrial_production.loc[nodes, "HVC"]
+ / nhours
+ * costs.at[tech, "methanol-input"]
+ )
+
+ co2_release = (
+ costs.at[tech, "carbondioxide-output"] / costs.at[tech, "methanol-input"]
+ + costs.at["methanolisation", "carbondioxide-input"]
+ )
+
+ n.madd(
+ "Link",
+ spatial.methanol.locations,
+ suffix=f" {tech}",
+ carrier=tech,
+ capital_cost=costs.at[tech, "fixed"] / costs.at[tech, "methanol-input"],
+ marginal_cost=costs.at[tech, "VOM"] / costs.at[tech, "methanol-input"],
+ p_nom_extendable=True,
+ bus0=spatial.methanol.nodes,
+ bus1=spatial.oil.naphtha,
+ bus2=nodes,
+ bus3="co2 atmosphere",
+ p_min_pu=1,
+ p_nom_max=p_nom_max.values,
+ efficiency=1 / costs.at[tech, "methanol-input"],
+ efficiency2=-costs.at[tech, "electricity-input"]
+ / costs.at[tech, "methanol-input"],
+ efficiency3=co2_release,
+ )
+
+
+def add_methanol_to_kerosene(n, costs):
+ nodes = pop_layout.index
+ nhours = n.snapshot_weightings.generators.sum()
+ nyears = nhours / 8760
+
+ demand_factor = options["aviation_demand_factor"]
+
+ tech = "methanol-to-kerosene"
+
+ logger.info(f"Adding {tech}.")
+
+ all_aviation = ["total international aviation", "total domestic aviation"]
+
+ p_nom_max = (
+ demand_factor
+ * pop_weighted_energy_totals.loc[nodes, all_aviation].sum(axis=1)
+ * 1e6
+ / nhours
+ * costs.at[tech, "methanol-input"]
+ )
+
+ # cost data available at https://www.concawe.eu/wp-content/uploads/Rpt_22-17.pdf table 94
+
+ n.madd(
+ "Link",
+ spatial.methanol.locations,
+ suffix=f" {tech}",
+ carrier=tech,
+ # capital_cost= ,
+ bus0=spatial.methanol.nodes,
+ bus1=spatial.oil.kerosene,
+ bus2=spatial.h2.nodes,
+ efficiency=costs.at[tech, "methanol-input"],
+ efficiency2=-costs.at[tech, "hydrogen-input"]
+ / costs.at[tech, "methanol-input"],
+ p_nom_extendable=True,
+ p_min_pu=1,
+ p_nom_max=p_nom_max.values,
+ )
+
+
+def add_methanol_reforming(n, costs):
+ logger.info("Adding methanol steam reforming.")
+
+ nodes = pop_layout.index
+
+ tech = "Methanol steam reforming"
+
+ capital_cost = costs.at[tech, "fixed"] / costs.at[tech, "methanol-input"]
+
+ n.madd(
+ "Link",
+ spatial.methanol.locations,
+ suffix=f" {tech}",
+ bus0=spatial.methanol.nodes,
+ bus1=spatial.h2.nodes,
+ bus2="co2 atmosphere",
+ p_nom_extendable=True,
+ capital_cost=capital_cost,
+ efficiency=1 / costs.at[tech, "methanol-input"],
+ efficiency2=costs.at["methanolisation", "carbondioxide-input"],
+ carrier=tech,
+ lifetime=costs.at[tech, "lifetime"],
+ )
+
+
+def add_methanol_reforming_cc(n, costs):
+ logger.info("Adding methanol steam reforming with carbon capture.")
+
+ nodes = pop_layout.index
+
+ tech = "Methanol steam reforming"
+
+ # TODO: heat release and electricity demand for process and carbon capture
+ # but the energy demands for carbon capture have not yet been added for other CC processes
+ # 10.1016/j.rser.2020.110171: 0.129 kWh_e/kWh_H2, -0.09 kWh_heat/kWh_H2
+
+ capital_cost = costs.at[tech, "fixed"] / costs.at[tech, "methanol-input"]
+
+ capital_cost_cc = (
+ capital_cost
+ + costs.at["cement capture", "fixed"]
+ * costs.at["methanolisation", "carbondioxide-input"]
+ )
+
+ n.madd(
+ "Link",
+ nodes,
+ suffix=f" {tech} CC",
+ bus0=spatial.methanol.nodes,
+ bus1=spatial.h2.nodes,
+ bus2="co2 atmosphere",
+ bus3=spatial.co2.nodes,
+ p_nom_extendable=True,
+ capital_cost=capital_cost_cc,
+ efficiency=1 / costs.at[tech, "methanol-input"],
+ efficiency2=(1 - costs.at["cement capture", "capture_rate"])
+ * costs.at["methanolisation", "carbondioxide-input"],
+ efficiency3=costs.at["cement capture", "capture_rate"]
+ * costs.at["methanolisation", "carbondioxide-input"],
+ carrier=f"{tech} CC",
+ lifetime=costs.at[tech, "lifetime"],
+ )
+
+
def add_dac(n, costs):
heat_carriers = ["urban central heat", "services urban decentral heat"]
heat_buses = n.buses.index[n.buses.carrier.isin(heat_carriers)]
@@ -2226,6 +2558,64 @@ def add_heat(n, costs):
)
+def add_methanol(n, costs):
+ logger.info("Add methanol")
+
+ n.add("Carrier", "methanol")
+
+ n.madd(
+ "Bus",
+ spatial.methanol.nodes,
+ location=spatial.methanol.locations,
+ carrier="methanol",
+ unit="MWh_LHV",
+ )
+
+ n.madd(
+ "Store",
+ spatial.methanol.nodes,
+ suffix=" Store",
+ bus=spatial.methanol.nodes,
+ e_nom_extendable=True,
+ e_cyclic=True,
+ carrier="methanol",
+ capital_cost=0.02,
+ )
+
+ if options["methanol_transport"]:
+ methanol_transport = create_network_topology(
+ n, "methanol transport ", bidirectional=True
+ )
+ n.madd(
+ "Link",
+ methanol_transport.index,
+ bus0=methanol_transport.bus0 + " methanol",
+ bus1=methanol_transport.bus1 + " methanol",
+ p_nom_extendable=False,
+ p_nom=5e4,
+ length=methanol_transport.length.values,
+ marginal_cost=0.027
+ * methanol_transport.length.values, # assuming 0.15€/ton-km and 0.183t/1000MWhMeOH
+ carrier="methanol transport",
+ )
+
+ if "biomass" in n.buses.carrier.unique():
+ if options["biomass_to_methanol"]:
+ add_biomass_to_methanol(n, costs)
+
+ if options["biomass_to_methanol"]:
+ add_biomass_to_methanol_cc(n, costs)
+
+ if options["methanol_to_power"]:
+ add_methanol_to_power(n, costs, types=options["methanol_to_power"])
+
+ if options["methanol_reforming"]:
+ add_methanol_reforming(n, costs)
+
+ if options["methanol_reforming_cc"]:
+ add_methanol_reforming_cc(n, costs)
+
+
def add_biomass(n, costs):
logger.info("Add biomass")
@@ -2685,25 +3075,26 @@ def add_industry(n, costs):
)
# methanol for industry
+ # add methanol nodes if not already added
+ if "methanol" not in n.buses.carrier.unique():
+ n.madd(
+ "Bus",
+ spatial.methanol.nodes,
+ carrier="methanol",
+ location=spatial.methanol.locations,
+ unit="MWh_LHV",
+ )
- n.madd(
- "Bus",
- spatial.methanol.nodes,
- carrier="methanol",
- location=spatial.methanol.locations,
- unit="MWh_LHV",
- )
-
- n.madd(
- "Store",
- spatial.methanol.nodes,
- suffix=" Store",
- bus=spatial.methanol.nodes,
- e_nom_extendable=True,
- e_cyclic=True,
- carrier="methanol",
- capital_cost=0.02,
- )
+ n.madd(
+ "Store",
+ spatial.methanol.nodes,
+ suffix=" Store",
+ bus=spatial.methanol.nodes,
+ e_nom_extendable=True,
+ e_cyclic=True,
+ carrier="methanol",
+ capital_cost=0.02,
+ )
n.madd(
"Bus",
@@ -2718,7 +3109,7 @@ def add_industry(n, costs):
/ nhours
)
- if not options["regional_methanol_demand"]:
+ if not options["regional_methanol_demand"] or not options["methanol_spatial"]:
p_set_methanol = p_set_methanol.sum()
n.madd(
@@ -2840,7 +3231,7 @@ def add_industry(n, costs):
* efficiency
)
- if not options["regional_methanol_demand"]:
+ if not options["regional_methanol_demand"] or not options["methanol_spatial"]:
p_set_methanol_shipping = p_set_methanol_shipping.sum()
n.madd(
@@ -3129,6 +3520,9 @@ def add_industry(n, costs):
efficiency3=process_co2_per_naphtha,
)
+ if options["methanol_to_olefins"]:
+ add_methanol_to_olefins(n, costs)
+
# aviation
demand_factor = options.get("aviation_demand_factor", 1)
if demand_factor != 1:
@@ -3173,6 +3567,9 @@ def add_industry(n, costs):
efficiency2=costs.at["oil", "CO2 intensity"],
)
+ if options["methanol_to_kerosene"]:
+ add_methanol_to_kerosene(n, costs)
+
# TODO simplify bus expression
n.madd(
"Load",
@@ -3947,9 +4344,10 @@ if __name__ == "__main__":
simpl="",
opts="",
clusters="37",
- ll="v1.0",
- sector_opts="730H-T-H-B-I-A-dist1",
+ ll="vopt",
+ sector_opts="",
planning_horizons="2050",
+ run="enable_all",
)
configure_logging(snakemake)
@@ -4012,6 +4410,9 @@ if __name__ == "__main__":
if options["ammonia"]:
add_ammonia(n, costs)
+ if options["methanol"]:
+ add_methanol(n, costs)
+
if options["industry"]:
add_industry(n, costs)