├── .DS_Store
├── Data
└── .DS_Store
├── Parameters
├── .DS_Store
├── Basic_H2_plant
│ ├── .DS_Store
│ ├── storage_units.csv
│ ├── buses.csv
│ ├── generators.csv
│ ├── stores.csv
│ └── links.csv
└── NA
│ ├── demand_parameters.xlsx
│ ├── country_parameters.xlsx
│ ├── pipeline_parameters.xlsx
│ ├── transport_parameters.xlsx
│ ├── conversion_parameters.xlsx
│ └── technology_parameters.xlsx
├── __pycache__
├── .DS_Store
├── p_H2_aux.cpython-310.pyc
└── functions.cpython-310.pyc
├── .gitignore
├── config.yaml
├── environment.yaml
├── Scripts
├── assign_country.py
├── total_hydrogen_cost.py
├── get_weather_data.py
├── water_cost.py
├── costs_by_component.py
├── p_H2_aux.py
├── plot_dispatch.py
├── optimize_transport_and_conversion.py
├── map_costs.py
├── optimize_hydrogen_plant.py
└── functions.py
├── LICENSE
├── CITATION.cff
├── Snakefile
└── README.md
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ClimateCompatibleGrowth/GeoH2/HEAD/.DS_Store
--------------------------------------------------------------------------------
/Data/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ClimateCompatibleGrowth/GeoH2/HEAD/Data/.DS_Store
--------------------------------------------------------------------------------
/Parameters/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ClimateCompatibleGrowth/GeoH2/HEAD/Parameters/.DS_Store
--------------------------------------------------------------------------------
/__pycache__/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ClimateCompatibleGrowth/GeoH2/HEAD/__pycache__/.DS_Store
--------------------------------------------------------------------------------
/Parameters/Basic_H2_plant/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ClimateCompatibleGrowth/GeoH2/HEAD/Parameters/Basic_H2_plant/.DS_Store
--------------------------------------------------------------------------------
/Parameters/Basic_H2_plant/storage_units.csv:
--------------------------------------------------------------------------------
1 | name,bus,p_nom_extendable,carrier,capital_cost,max_hours
2 | Battery,Power,TRUE,AC,95000,1
--------------------------------------------------------------------------------
/Parameters/NA/demand_parameters.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ClimateCompatibleGrowth/GeoH2/HEAD/Parameters/NA/demand_parameters.xlsx
--------------------------------------------------------------------------------
/__pycache__/p_H2_aux.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ClimateCompatibleGrowth/GeoH2/HEAD/__pycache__/p_H2_aux.cpython-310.pyc
--------------------------------------------------------------------------------
/Parameters/NA/country_parameters.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ClimateCompatibleGrowth/GeoH2/HEAD/Parameters/NA/country_parameters.xlsx
--------------------------------------------------------------------------------
/Parameters/NA/pipeline_parameters.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ClimateCompatibleGrowth/GeoH2/HEAD/Parameters/NA/pipeline_parameters.xlsx
--------------------------------------------------------------------------------
/Parameters/NA/transport_parameters.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ClimateCompatibleGrowth/GeoH2/HEAD/Parameters/NA/transport_parameters.xlsx
--------------------------------------------------------------------------------
/__pycache__/functions.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ClimateCompatibleGrowth/GeoH2/HEAD/__pycache__/functions.cpython-310.pyc
--------------------------------------------------------------------------------
/Parameters/Basic_H2_plant/buses.csv:
--------------------------------------------------------------------------------
1 | name,control,generator,carrier
2 | Power,Slack,Wind,AC
3 | Hydrogen,Slack,,H2
4 | Hydrogen Storage,Slack,,H2
5 |
--------------------------------------------------------------------------------
/Parameters/NA/conversion_parameters.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ClimateCompatibleGrowth/GeoH2/HEAD/Parameters/NA/conversion_parameters.xlsx
--------------------------------------------------------------------------------
/Parameters/NA/technology_parameters.xlsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ClimateCompatibleGrowth/GeoH2/HEAD/Parameters/NA/technology_parameters.xlsx
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | Cutouts/
3 | Data/
4 | Resources/
5 | Results/
6 | Plots/
7 | **/.DS_Store
8 | .DS_Store
9 | Temp/
10 | .snakemake/
11 | gurobi.log
--------------------------------------------------------------------------------
/Parameters/Basic_H2_plant/generators.csv:
--------------------------------------------------------------------------------
1 | name,bus,control,p_nom_extendable,p_nom,capital_cost,marginal_cost
2 | Wind,Power,Slack,TRUE,0,1.58E+06,0
3 | Solar,Power,PQ,TRUE,0,1.47E+06,0
--------------------------------------------------------------------------------
/Parameters/Basic_H2_plant/stores.csv:
--------------------------------------------------------------------------------
1 | name,bus,carrier,e_nom,e_nom_extendable,e_nom_max,e_cyclic,e_initial,capital_cost
2 | Compressed H2 Store,Hydrogen Storage,H2,0,TRUE,1.00E+07,TRUE,0,2.17E+04
--------------------------------------------------------------------------------
/config.yaml:
--------------------------------------------------------------------------------
1 | scenario:
2 | country: ['NA']
3 | weather_year: [2023]
4 |
5 | generators: ['Solar','Wind']
6 |
7 | transport:
8 | pipeline_construction: true
9 | road_construction: true
--------------------------------------------------------------------------------
/Parameters/Basic_H2_plant/links.csv:
--------------------------------------------------------------------------------
1 | name,bus0,bus1,carrier,efficiency,p_nom_extendable,p_min_pu,capital_cost,ramp_limit_up,ramp_limit_down,bus2,efficiency2,p_nom_max
2 | Electrolysis,Power,Hydrogen,AC,0.59,TRUE,0,1.25E+06,,,,,1.00E+06
3 | Hydrogen Compression,Hydrogen,Hydrogen Storage,H2,1,TRUE,0,0,,,Power,-0.051,1.00E+06
4 | Hydrogen from storage,Hydrogen Storage,Hydrogen,H2,1,TRUE,0,0,,,,,1.00E+06
--------------------------------------------------------------------------------
/environment.yaml:
--------------------------------------------------------------------------------
1 | name: geoh2
2 | channels:
3 | - conda-forge
4 | - nodefaults
5 | - bioconda
6 |
7 | dependencies:
8 | - atlite=0.2.14
9 | - cartopy
10 | - gdal=3
11 | - geopandas
12 | - geopy
13 | - matplotlib
14 | - numpy
15 | - openpyxl
16 | - pandas=2.1.4
17 | - pip
18 | - pypsa=0.26.0
19 | - python
20 | - shapely=1.8.4
21 | - snakemake
22 | - xarray
23 |
--------------------------------------------------------------------------------
/Scripts/assign_country.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | """
4 | Created on Sun Apr 2 12:05:51 2023
5 |
6 | @author: Claire Halloran, University of Oxford
7 |
8 | Assigns interest rate to different hexagons for different technology categories
9 | based on their country.
10 |
11 | Just add to optimize_hydrogen_plant.py?
12 |
13 | """
14 | import geopandas as gpd
15 |
16 | if __name__ == "__main__":
17 | hexagons = gpd.read_file(str(snakemake.input))
18 | world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres')) # may need to switch to higher res
19 | hexagons.to_crs(world.crs, inplace=True)
20 | countries = world.drop(columns=['pop_est', 'continent', 'iso_a3', 'gdp_md_est'])
21 | countries = countries.rename(columns={'name':'country'})
22 | hexagons_with_country = gpd.sjoin(hexagons, countries, op='intersects') # changed from "within"
23 | hexagons_with_country.to_file(str(snakemake.output), driver='GeoJSON')
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Climate Compatible Growth
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/CITATION.cff:
--------------------------------------------------------------------------------
1 | cff-version: 1.2.0
2 | message: "If you use this package, please cite the corresponding manuscript in MethodsX."
3 | authors:
4 | - family-names: "Halloran"
5 | given-names: "Claire"
6 | - family-names: "Leonard"
7 | given-names: "Alycia"
8 | - family-names: "Salmon"
9 | given-names: "Nicholas"
10 | - family-names: "Müller"
11 | given-names: "Leander"
12 | - family-names: "Hirmer"
13 | given-names: "Stephanie"
14 | title: "GeoH2"
15 | url: "https://github.com/ClimateCompatibleGrowth/GeoH2"
16 | version: 1.0
17 | preferred-citation:
18 | type: article
19 | authors:
20 | - family-names: "Halloran"
21 | given-names: "Claire"
22 | - family-names: "Leonard"
23 | given-names: "Alycia"
24 | - family-names: "Salmon"
25 | given-names: "Nicholas"
26 | - family-names: "Müller"
27 | given-names: "Leander"
28 | - family-names: "Hirmer"
29 | given-names: "Stephanie"
30 | title: "GeoH2 model: Geospatial cost optimization of green hydrogen production including storage and transportation"
31 | journal: "MethodsX"
32 | year: 2024
33 | doi: 10.1016/j.mex.2024.102660
34 | month: 6
35 | start: 102660 # First page number
36 | volume: 12
--------------------------------------------------------------------------------
/Scripts/total_hydrogen_cost.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | """
4 | Created on Wed Apr 5 13:44:32 2023
5 |
6 | @author: Claire Halloran, University of Oxford
7 |
8 | Total hydrogen cost
9 |
10 | Bring together all previous data to calculate lowest-cost hydrogen
11 | """
12 |
13 | #%% identify lowest-cost strategy: trucking vs. pipeline
14 |
15 | import geopandas as gpd
16 | import pandas as pd
17 | import numpy as np
18 |
19 | hexagons = gpd.read_file(str(snakemake.input.hexagons))
20 | demand_excel_path = str(snakemake.input.demand_parameters)
21 | demand_parameters = pd.read_excel(demand_excel_path,
22 | index_col='Demand center',
23 | )
24 |
25 | demand_centers = demand_parameters.index
26 | for demand_center in demand_centers:
27 | hexagons[f'{demand_center} trucking total cost'] =\
28 | hexagons[f'{demand_center} road construction costs']\
29 | +hexagons[f'{demand_center} trucking transport and conversion costs']\
30 | +hexagons[f'{demand_center} trucking production cost']\
31 | +hexagons['Lowest water cost']
32 | hexagons[f'{demand_center} pipeline total cost'] =\
33 | hexagons[f'{demand_center} pipeline transport and conversion costs']\
34 | +hexagons[f'{demand_center} pipeline production cost']\
35 | +hexagons['Lowest water cost']
36 |
37 | for hexagon in hexagons.index:
38 | hexagons.loc[hexagon,f'{demand_center} lowest cost'] = np.nanmin(
39 | [hexagons.loc[hexagon,f'{demand_center} trucking total cost'],
40 | hexagons.loc[hexagon,f'{demand_center} pipeline total cost']
41 | ])
42 |
43 | hexagons.to_file(str(snakemake.output), driver='GeoJSON', encoding='utf-8')
44 |
--------------------------------------------------------------------------------
/Scripts/get_weather_data.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Created on Sat Feb 25 15:11:47 2023
4 |
5 | @author: Claire Halloran, University of Oxford
6 |
7 | get_weather_data.py
8 |
9 | This script fetches historical weather data to calculate wind and solar potential
10 | from the ERA-5 reanalysis dataset using Atlite.
11 |
12 | Create cutouts with `atlite `_.
13 |
14 | For this rule to work you must have
15 |
16 | - installed the `Copernicus Climate Data Store `_ ``cdsapi`` package (`install with `pip``) and
17 | - registered and setup your CDS API key as described on their website
18 |
19 | """
20 | import logging
21 | import atlite
22 | import geopandas as gpd
23 | import os
24 | if __name__ == "__main__":
25 |
26 | logging.basicConfig(level=logging.INFO)
27 |
28 | # calculate min and max coordinates from hexagons
29 | hexagons = gpd.read_file(str(snakemake.input.hexagons))
30 |
31 | hexagon_bounds = hexagons.geometry.bounds
32 | min_lon, min_lat = hexagon_bounds[['minx','miny']].min()
33 | max_lon, max_lat = hexagon_bounds[['maxx','maxy']].max()
34 |
35 | weather_year = snakemake.wildcards.weather_year
36 | end_weather_year = int(snakemake.wildcards.weather_year)+1
37 | start_date = f'{weather_year}-01-01'
38 | end_date = f'{end_weather_year}-01-01'
39 |
40 | # Create folders for final cutouts and temporary files
41 | if not os.path.exists('Cutouts'):
42 | os.makedirs('Cutouts')
43 | if not os.path.exists('temp'):
44 | os.makedirs('temp')
45 |
46 | cutout = atlite.Cutout(
47 | path=str(snakemake.output),
48 | module="era5",
49 | x=slice(min_lon, max_lon),
50 | y=slice(min_lat, max_lat),
51 | time=slice(start_date, end_date),
52 | )
53 |
54 | cutout.prepare(tmpdir="temp") # TEMPDIR DEFINITION IS NEW TO FIX ERROR
--------------------------------------------------------------------------------
/Scripts/water_cost.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | """
4 | Created on Wed Apr 5 13:26:19 2023
5 |
6 | @author: Claire Halloran, University of Oxford
7 |
8 | Water costs for hydrogen production in each hexagon
9 |
10 |
11 | """
12 |
13 | import geopandas as gpd
14 | import pandas as pd
15 | import numpy as np
16 |
17 | hexagons = gpd.read_file(str(snakemake.input.hexagons))
18 | technology_parameters = str(snakemake.input.technology_parameters)
19 | country_excel_path = str(snakemake.input.country_parameters)
20 |
21 | water_data = pd.read_excel(technology_parameters,
22 | sheet_name='Water',
23 | index_col='Parameter'
24 | ).squeeze("columns")
25 | country_parameters = pd.read_excel(country_excel_path,
26 | index_col='Country')
27 |
28 | #%% water cost for each hexagon for each kg hydrogen produced
29 |
30 | h2o_costs_dom_water_bodies = np.empty(len(hexagons))
31 | h2o_costs_ocean = np.empty(len(hexagons))
32 | h2o_costs = np.empty(len(hexagons))
33 |
34 | electricity_demand_h2o_treatment = water_data['Freshwater treatment electricity demand (kWh/m3)']
35 | electricity_demand_h2o_ocean_treatment = water_data['Ocean water treatment electricity demand (kWh/m3)']
36 | water_transport_costs = water_data['Water transport cost (euros/100 km/m3)']
37 | water_spec_cost = water_data['Water specific cost (euros/m3)']
38 | water_demand = water_data['Water demand (L/kg H2)']
39 |
40 | for i in range(len(hexagons)):
41 | h2o_costs_dom_water_bodies[i] =(water_spec_cost
42 | + (water_transport_costs/100)*min(hexagons['waterbody_dist'][i],
43 | hexagons['waterway_dist'][i])
44 | + electricity_demand_h2o_treatment*\
45 | country_parameters.loc[hexagons.country[i],'Electricity price (euros/kWh)']
46 | )*water_demand/1000
47 | h2o_costs_ocean[i] =(water_spec_cost
48 | + (water_transport_costs/100)*hexagons['ocean_dist'][i]
49 | + electricity_demand_h2o_ocean_treatment*\
50 | country_parameters.loc[hexagons.country[i],'Electricity price (euros/kWh)']
51 | )*water_demand/1000
52 | h2o_costs[i] = min(h2o_costs_dom_water_bodies[i],h2o_costs_ocean[i])
53 |
54 | hexagons['Ocean water costs'] = h2o_costs_ocean
55 | hexagons['Freshwater costs'] = h2o_costs_dom_water_bodies
56 | hexagons['Lowest water cost'] = h2o_costs
57 |
58 | hexagons.to_file(str(snakemake.output), driver='GeoJSON', encoding='utf-8')
59 |
60 |
--------------------------------------------------------------------------------
/Snakefile:
--------------------------------------------------------------------------------
1 | configfile: "config.yaml"
2 |
3 | wildcard_constraints:
4 | # ISO alpha-2 country code
5 | country="[A-Z]{2}",
6 | # ERA5 weather year (1940-2023)
7 | weather_year="(19[4-9]\d|20[0-1]\d|202[0-3])",
8 |
9 | # rule to delete all necessary files to allow reruns
10 | rule clean:
11 | shell: 'rm -r Cutouts/*.nc Data/*.geojson Resources/*.geojson Results/*.geojson temp/*.nc Results/*.csv Plots/'
12 |
13 | # bulk run rule to run all countries and years listed in config file
14 | rule optimise_all:
15 | input:
16 | expand('Results/hex_total_cost_{country}_{weather_year}.geojson',
17 | **config["scenario"]
18 | ),
19 |
20 | # bulk run rule to map all countries and years listed in config file
21 | rule map_all:
22 | input:
23 | expand('Plots/{country}_{weather_year}',
24 | **config["scenario"]
25 | ),
26 |
27 | rule assign_country:
28 | input:
29 | "Data/hex_final_{country}.geojson",
30 | output:
31 | "Data/hexagons_with_country_{country}.geojson",
32 | script:
33 | "Scripts/assign_country.py"
34 |
35 | rule get_weather_data:
36 | input:
37 | hexagons = "Data/hexagons_with_country_{country}.geojson",
38 | output:
39 | "Cutouts/{country}_{weather_year}.nc",
40 | script:
41 | 'Scripts/get_weather_data.py'
42 |
43 | rule optimize_transport_and_conversion:
44 | input:
45 | hexagons = 'Data/hexagons_with_country_{country}.geojson',
46 | technology_parameters = "Parameters/{country}/technology_parameters.xlsx",
47 | demand_parameters = 'Parameters/{country}/demand_parameters.xlsx',
48 | country_parameters = 'Parameters/{country}/country_parameters.xlsx',
49 | conversion_parameters = "Parameters/{country}/conversion_parameters.xlsx",
50 | transport_parameters = "Parameters/{country}/transport_parameters.xlsx",
51 | pipeline_parameters = "Parameters/{country}/pipeline_parameters.xlsx"
52 | output:
53 | 'Resources/hex_transport_{country}.geojson'
54 | script:
55 | 'Scripts/optimize_transport_and_conversion.py'
56 |
57 | rule calculate_water_costs:
58 | input:
59 | technology_parameters = "Parameters/{country}/technology_parameters.xlsx",
60 | country_parameters = 'Parameters/{country}/country_parameters.xlsx',
61 | hexagons = 'Resources/hex_transport_{country}.geojson'
62 | output:
63 | 'Resources/hex_water_{country}.geojson'
64 | script:
65 | 'Scripts/water_cost.py'
66 |
67 |
68 | rule optimize_hydrogen_plant:
69 | input:
70 | transport_parameters = "Parameters/{country}/transport_parameters.xlsx",
71 | country_parameters = 'Parameters/{country}/country_parameters.xlsx',
72 | demand_parameters = 'Parameters/{country}/demand_parameters.xlsx',
73 | # cutout = "Cutouts/{country}_{weather_year}.nc",
74 | hexagons = 'Resources/hex_water_{country}.geojson'
75 | output:
76 | 'Resources/hex_lcoh_{country}_{weather_year}.geojson'
77 | script:
78 | 'Scripts/optimize_hydrogen_plant.py'
79 |
80 | rule calculate_total_hydrogen_cost:
81 | input:
82 | hexagons = 'Resources/hex_lcoh_{country}_{weather_year}.geojson',
83 | demand_parameters = 'Parameters/{country}/demand_parameters.xlsx'
84 | output:
85 | 'Results/hex_total_cost_{country}_{weather_year}.geojson'
86 | script:
87 | 'Scripts/total_hydrogen_cost.py'
88 |
89 | rule calculate_cost_components:
90 | input:
91 | hexagons = 'Results/hex_total_cost_{country}_{weather_year}.geojson',
92 | demand_parameters = 'Parameters/{country}/demand_parameters.xlsx',
93 | country_parameters = 'Parameters/{country}/country_parameters.xlsx',
94 | stores_parameters = 'Parameters/Basic_H2_plant/stores.csv',
95 | storage_parameters = 'Parameters/Basic_H2_plant/storage_units.csv',
96 | links_parameters = 'Parameters/Basic_H2_plant/links.csv',
97 | generators_parameters = 'Parameters/Basic_H2_plant/generators.csv'
98 | output:
99 | 'Results/hex_cost_components_{country}_{weather_year}.geojson',
100 | 'Results/hex_cost_components_{country}_{weather_year}.csv'
101 | script:
102 | 'Scripts/costs_by_component.py'
103 |
104 | rule map_costs:
105 | input:
106 | hexagons = 'Results/hex_cost_components_{country}_{weather_year}.geojson',
107 | demand_parameters = 'Parameters/{country}/demand_parameters.xlsx'
108 | output:
109 | directory('Plots/{country}_{weather_year}')
110 | script:
111 | 'Scripts/map_costs.py'
112 |
113 |
--------------------------------------------------------------------------------
/Scripts/costs_by_component.py:
--------------------------------------------------------------------------------
1 | """
2 | Created on Tue Aug 29th 2023
3 |
4 | @author: Alycia Leonard, University of Oxford, alycia.leonard@eng.ox.ac.uk
5 |
6 | Cost by hydrogen plant component
7 |
8 | Add attributes to hex file for cost of each component
9 | """
10 |
11 | # %% identify lowest-cost strategy: trucking vs. pipeline
12 |
13 | import geopandas as gpd
14 | import pandas as pd
15 | from geopy.geocoders import Photon
16 | import functions
17 |
18 | # Load hexagons
19 | hexagons = gpd.read_file(str(snakemake.input.hexagons))
20 |
21 | # Load necessary parameters
22 | demand_excel_path = str(snakemake.input.demand_parameters)
23 | demand_parameters = pd.read_excel(demand_excel_path, index_col='Demand center')
24 | country_excel_path = str(snakemake.input.country_parameters)
25 | country_parameters = pd.read_excel(country_excel_path, index_col='Country')
26 | stores_csv_path = str(snakemake.input.stores_parameters) # H2 storage
27 | stores_parameters = pd.read_csv(stores_csv_path, index_col='name')
28 | storage_csv_path = str(snakemake.input.storage_parameters) # Battery
29 | storage_parameters = pd.read_csv(storage_csv_path, index_col='name')
30 | links_csv_path = str(snakemake.input.links_parameters) # Electrolyzer
31 | links_parameters = pd.read_csv(links_csv_path, index_col='name')
32 | generators_csv_path = str(snakemake.input.generators_parameters) # Solar and generator
33 | generators_parameters = pd.read_csv(generators_csv_path, index_col='name')
34 |
35 | # For each demand center, get costs for each component
36 |
37 | demand_centers = demand_parameters.index
38 | transport_methods = ['pipeline', 'trucking']
39 | for demand_center in demand_centers:
40 | # Get location of demand center
41 | lat = demand_parameters.loc[demand_center, 'Lat [deg]']
42 | lon = demand_parameters.loc[demand_center, 'Lon [deg]']
43 | coordinates = str(lat) + ", " + str(lon)
44 | # Get country where the demand center is
45 | geolocator = Photon(user_agent="MyApp")
46 | location = geolocator.reverse(coordinates, language="en")
47 | country = location.raw['properties']['country']
48 |
49 | # Get CRF and then cost for each component using the data for the country you are looking at
50 | for transport_method in transport_methods:
51 | # Battery
52 | interest_battery = country_parameters.loc[country, 'Plant interest rate']
53 | lifetime_battery = country_parameters.loc[country, 'Plant lifetime (years)']
54 | crf_battery = functions.CRF(interest_battery, lifetime_battery)
55 | capital_cost_battery = storage_parameters.loc['Battery', 'capital_cost']
56 | hexagons[f'{demand_center} {transport_method} battery costs'] = \
57 | hexagons[f'{demand_center} {transport_method} battery capacity'] * capital_cost_battery * crf_battery
58 | hexagons[f'{demand_center} LCOH - {transport_method} battery costs portion'] = \
59 | hexagons[f'{demand_center} {transport_method} battery costs'] \
60 | / demand_parameters.loc[demand_center, 'Annual demand [kg/a]']
61 |
62 | # Electrolyzer
63 | interest_electrolyzer = country_parameters.loc[country, 'Plant interest rate']
64 | lifetime_electrolyzer = country_parameters.loc[country, 'Plant lifetime (years)']
65 | crf_electrolyzer = functions.CRF(interest_electrolyzer, lifetime_electrolyzer)
66 | capital_cost_electrolyzer = links_parameters.loc['Electrolysis', 'capital_cost']
67 | hexagons[f'{demand_center} {transport_method} electrolyzer costs'] = \
68 | hexagons[f'{demand_center} {transport_method} electrolyzer capacity'] * capital_cost_electrolyzer * crf_electrolyzer
69 | hexagons[f'{demand_center} LCOH - {transport_method} electrolyzer portion'] = \
70 | hexagons[f'{demand_center} {transport_method} electrolyzer costs'] / demand_parameters.loc[demand_center, 'Annual demand [kg/a]']
71 |
72 | # H2 Storage
73 | interest_h2_storage = country_parameters.loc[country, 'Plant interest rate']
74 | lifetime_h2_storage = country_parameters.loc[country, 'Plant lifetime (years)']
75 | crf_h2_storage = functions.CRF(interest_h2_storage, lifetime_h2_storage)
76 | capital_cost_h2_storage = stores_parameters.loc['Compressed H2 Store', 'capital_cost']
77 | hexagons[f'{demand_center} {transport_method} H2 storage costs'] = \
78 | hexagons[f'{demand_center} {transport_method} H2 storage capacity'] * capital_cost_h2_storage * crf_h2_storage
79 | hexagons[f'{demand_center} LCOH - {transport_method} H2 storage portion'] = \
80 | hexagons[f'{demand_center} {transport_method} H2 storage costs'] / demand_parameters.loc[demand_center, 'Annual demand [kg/a]']
81 |
82 | for generator in snakemake.config['generators']:
83 | generator_lower = generator.lower()
84 | interest_generator = country_parameters.loc[country, f'{generator} interest rate']
85 | lifetime_generator = country_parameters.loc[country, f'{generator} lifetime (years)']
86 | crf_generator = functions.CRF(interest_generator, lifetime_generator)
87 | capital_cost_generator = generators_parameters.loc[f'{generator}', 'capital_cost']
88 | hexagons[f'{demand_center} {transport_method} {generator_lower} costs'] = \
89 | hexagons[f'{demand_center} {transport_method} {generator_lower} capacity'] * capital_cost_generator * crf_generator
90 | hexagons[f'{demand_center} LCOH - {transport_method} {generator_lower} portion'] = \
91 | hexagons[f'{demand_center} {transport_method} {generator_lower} costs'] / demand_parameters.loc[demand_center, 'Annual demand [kg/a]']
92 |
93 | hexagons.to_file(str(snakemake.output[0]), driver='GeoJSON', encoding='utf-8')
94 | hexagons.to_csv(str(snakemake.output[1]), encoding='latin-1')
--------------------------------------------------------------------------------
/Scripts/p_H2_aux.py:
--------------------------------------------------------------------------------
1 | import pypsa
2 | import numpy as np
3 | import pandas as pd
4 | import logging
5 |
6 |
7 | def create_override_components():
8 | """Set up new component attributes as required"""
9 | # Modify the capacity of a link so that it can attach to 2 buses.
10 | override_component_attrs = pypsa.descriptors.Dict(
11 | {k: v.copy() for k, v in pypsa.components.component_attrs.items()}
12 | )
13 |
14 | override_component_attrs["Link"].loc["bus2"] = [
15 | "string",
16 | np.nan,
17 | np.nan,
18 | "2nd bus",
19 | "Input (optional)",
20 | ]
21 | override_component_attrs["Link"].loc["efficiency2"] = [
22 | "static or series",
23 | "per unit",
24 | 1.0,
25 | "2nd bus efficiency",
26 | "Input (optional)",
27 | ]
28 | override_component_attrs["Link"].loc["p2"] = [
29 | "series",
30 | "MW",
31 | 0.0,
32 | "2nd bus output",
33 | "Output",
34 | ]
35 | return override_component_attrs
36 |
37 |
38 | def get_col_widths(dataframe):
39 | # First we find the maximum length of the index column
40 | idx_max = max([len(str(s)) for s in dataframe.index.values] + [len(str(dataframe.index.name))])
41 | # Then, we concatenate this to the max of the lengths of column name and its values for each column, left to right
42 | return [idx_max] + [max([len(str(s)) for s in dataframe[col].values] + [len(col)]) for col in dataframe.columns]
43 |
44 |
45 |
46 | def extra_functionalities(n, snapshots):
47 | """Might be needed if additional constraints are needed later"""
48 | pass
49 |
50 |
51 | def get_weather_data():
52 | """Asks the user where the weather data is, and pulls it in. Keeps asking until it gets a file."""
53 | # import data
54 | input_check = True
55 | while input_check:
56 | try:
57 | input_check = False
58 | file = input("What is the name of your weather data file? "
59 | "It must be a CSV, but don't include the file extension >> ")
60 | weather_data = pd.read_csv(file + '.csv')
61 | weather_data.drop(weather_data.columns[0], axis=1, inplace=True)
62 | except FileNotFoundError:
63 | input_check = True
64 | print("There's no input file there! Try again.")
65 |
66 | # Check the weather data is a year long
67 | if len(weather_data) < 8700 or len(weather_data) > 8760 + 48:
68 | logging.warning('Your weather data seems not to be one year long in hourly intervals. \n'
69 | 'Are you sure the input data is correct? If not, exit the code using ctrl+c and start again.')
70 |
71 | return weather_data
72 |
73 | def check_CAPEX():
74 | """Checks if the user has put the CAPEX into annualised format. If not, it helps them do so."""
75 | check_CAPEX = input('Are your capital costs in the generators.csv, components.csv and stores.csv files annualised?'
76 | '\n (i.e. have you converted them from their upfront capital cost'
77 | ' to the cost that accrues each year under the chosen financial conditions? \n'
78 | '(Y/N) >> ')
79 | if check_CAPEX != 'Y':
80 | print('You have selected no annualisation, which means you have entered the upfront capital cost'
81 | ' of the equipment. \n We have to ask you a few questions to convert these to annualised costs.')
82 | check = True
83 | while check:
84 | try:
85 | discount = float(input('Enter the weighted average cost of capital in percent (i.e. 7 not 0.07)'))
86 | years = float(input('Enter the plant operating years.'))
87 | O_and_M = float(input('Enter the fixed O & M costs as a percentage of installed CAPEX '
88 | '(i.e. 2 not 0.02)'))
89 | check = False
90 | except ValueError:
91 | logging.warning('You have to enter a number! Try again.')
92 | check = True
93 |
94 | crf = discount * (1 + discount) ** years / ((1 + discount) ** years - 1)
95 | if crf < 2 or crf > 20 or O_and_M < 0 or O_and_M > 8:
96 | print('Your financial parameter inputs are giving some strange results. \n'
97 | 'You might want to exit the code using ctrl + c and try re-entering them.')
98 |
99 | return crf, O_and_M
100 | else:
101 | print('You have selected the annualised capital cost entry. \n'
102 | 'Make sure that the annualised capital cost data includes any operating costs that you '
103 | 'estimate based on plant CAPEX.')
104 | return None
105 |
106 | def get_solving_info():
107 | """Prompts the user for information about the solver and the problem formulation"""
108 | solver = input('What solver would you like to use? '
109 | 'If you leave this blank, the glpk default will be used >> ')
110 | if solver == '':
111 | solver = 'glpk'
112 |
113 | formulator = input("Would you like to formulate the problem using pyomo or linopt? \n"
114 | "Linopt can be faster but it is less user friendly. \n"
115 | "The answer only matters if you've added some extra constraints. (p/l) >> ")
116 | return solver, formulator
117 |
118 | def get_scale(n):
119 | """Gives the user some information about the solution, and asks if they'd like it to be scaled"""
120 | print('\nThe unscaled generation capacities are:')
121 | print(n.generators.rename(columns={'p_nom_opt': 'Rated Capacity (MW)'})[['Rated Capacity (MW)']])
122 | print('The unscaled hydrogen production is {a} t/year\n'.format(a=n.loads.p_set.values[0]/39.4*8760))
123 | scale = input('Enter a scaling factor for the results, to adjust the production. \n'
124 | "If you don't want to scale the results, enter a value of 1 >> ")
125 | try:
126 | scale = float(scale)
127 | except ValueError:
128 | scale = 1
129 | print("You didn't enter a number! The results won't be scaled.")
130 | return scale
131 |
132 | def get_results_dict_for_excel(n, scale):
133 | """Takes the results and puts them in a dictionary ready to be sent to Excel"""
134 | # Rename the components:
135 | links_name_dct = {'p_nom_opt': 'Rated Capacity (MW)',
136 | 'carrier': 'Carrier',
137 | 'bus0': 'Primary Energy Source',
138 | 'bus2': 'Secondary Energy Source'}
139 | comps = n.links.rename(columns=links_name_dct)[[i for i in links_name_dct.values()]]
140 | comps["Rated Capacity (MW)"] *= scale
141 |
142 | # Get the energy flows
143 | primary = n.links_t.p0*scale
144 | secondary = (n.links_t.p2*scale).drop(columns=['Hydrogen from storage', 'Electrolysis'])
145 |
146 | # Rescale the energy flows:
147 | primary['Hydrogen Compression'] /= 39.4
148 | primary['Hydrogen from storage'] /= 39.4
149 |
150 | # Rename the energy flows
151 | primary.rename(columns={
152 | 'Electrolysis': 'Electrolysis (MW)',
153 | 'Hydrogen Compression': 'Hydrogen to storage (t/h)',
154 | 'Hydrogen from storage': 'Hydrogen from storage (t/h)'
155 | }, inplace=True)
156 | secondary.rename(columns={'Hydrogen Compression': 'H2 Compression Power Consumption (MW)'}, inplace=True)
157 |
158 | consumption = pd.merge(primary, secondary, left_index=True, right_index=True)
159 |
160 | output = {
161 | 'Headlines': pd.DataFrame({
162 | 'Objective function (USD/kg)': [n.objective/(n.loads.p_set.values[0]/39.4*8760*1000)],
163 | 'Production (t/year)': n.loads.p_set.values[0]/39.4*8760*scale}, index=['LCOH (USD/kg)']),
164 | 'Generators': n.generators.rename(columns={'p_nom_opt': 'Rated Capacity (MW)'})[['Rated Capacity (MW)']]*scale,
165 | 'Components': comps,
166 | 'Stores': n.stores.rename(columns={'e_nom_opt': 'Storage Capacity (MWh)'})[['Storage Capacity (MWh)']]*scale,
167 | 'Energy generation (MW)': n.generators_t.p*scale,
168 | 'Energy consumption': consumption,
169 | 'Stored energy capacity (MWh)': n.stores_t.e*scale
170 | }
171 | return output
172 |
173 |
174 | def write_results_to_excel(output):
175 | """Takes results dictionary and puts them in an excel file. User determines the file name"""
176 | incomplete = True
177 | while incomplete:
178 | output_file = input("Enter the name of your output data file. \n"
179 | "Don't include the file extension. >> ") + '.xlsx'
180 | try:
181 | incomplete = False
182 | with pd.ExcelWriter(output_file, engine='xlsxwriter') as writer:
183 | for key in output.keys():
184 | dataframe = output[key]
185 | dataframe.to_excel(writer, sheet_name=key)
186 | worksheet = writer.sheets[key]
187 | for i, width in enumerate(get_col_widths(dataframe)):
188 | worksheet.set_column(i, i, width)
189 | except PermissionError:
190 | incomplete = True
191 | print('There is a problem writing on that file. Try another excel file name.')
192 |
--------------------------------------------------------------------------------
/Scripts/plot_dispatch.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | """
4 | Created on Mon Aug 28 13:57:02 2023
5 |
6 | @author: Claire Halloran, University of Oxford
7 |
8 | This script plots the temporal results of hydrogen plant optimization for specified
9 | hexagons
10 |
11 | Includes code from Nicholas Salmon, University of Oxford, for optimizing
12 | hydrogen plant capacity.
13 |
14 | """
15 |
16 | import atlite
17 | import geopandas as gpd
18 | import pypsa
19 | import matplotlib.pyplot as plt
20 | import pandas as pd
21 | import cartopy.crs as ccrs
22 | import p_H2_aux as aux
23 | from functions import CRF
24 | import numpy as np
25 | import logging
26 | import time
27 | from optimize_hydrogen_plant import *
28 |
29 | logging.basicConfig(level=logging.ERROR)
30 | # list of hexagons of interest
31 | hexagon_list = [372, 9]
32 | # hexagon_list = [9]
33 |
34 | # in the future, may want to make hexagons a class with different features
35 | def optimize_hydrogen_plant(wind_potential, pv_potential, times, demand_profile,
36 | wind_max_capacity, pv_max_capacity,
37 | country_series, water_limit = None):
38 | '''
39 | Optimizes the size of green hydrogen plant components based on renewable potential, hydrogen demand, and country parameters.
40 |
41 | Parameters
42 | ----------
43 | wind_potential : xarray DataArray
44 | 1D dataarray of per-unit wind potential in hexagon.
45 | pv_potential : xarray DataArray
46 | 1D dataarray of per-unit solar potential in hexagon.
47 | times : xarray DataArray
48 | 1D dataarray with timestamps for wind and solar potential.
49 | demand_profile : pandas DataFrame
50 | hourly dataframe of hydrogen demand in kg.
51 | country_series : pandas Series
52 | interest rate and lifetime information.
53 | water_limit : float
54 | annual limit on water available for electrolysis in hexagon, in cubic meters. Default is None.
55 |
56 | Returns
57 | -------
58 | lcoh : float
59 | levelized cost per kg hydrogen.
60 | wind_capacity: float
61 | optimal wind capacity in MW.
62 | solar_capacity: float
63 | optimal solar capacity in MW.
64 | electrolyzer_capacity: float
65 | optimal electrolyzer capacity in MW.
66 | battery_capacity: float
67 | optimal battery storage capacity in MW/MWh (1 hour batteries).
68 | h2_storage: float
69 | optimal hydrogen storage capacity in MWh.
70 |
71 | '''
72 |
73 | # if a water limit is given, check if hydrogen demand can be met
74 | if water_limit != None:
75 | # total hydrogen demand in kg
76 | total_hydrogen_demand = demand_profile['Demand'].sum()
77 | # check if hydrogen demand can be met based on hexagon water availability
78 | water_constraint = total_hydrogen_demand <= water_limit * 111.57 # kg H2 per cubic meter of water
79 | if water_constraint == False:
80 | print('Not enough water to meet hydrogen demand!')
81 | # return null values
82 | lcoh = np.nan
83 | wind_capacity = np.nan
84 | solar_capacity = np.nan
85 | electrolyzer_capacity = np.nan
86 | battery_capacity = np.nan
87 | h2_storage = np.nan
88 | return lcoh, wind_capacity, solar_capacity, electrolyzer_capacity, battery_capacity, h2_storage
89 |
90 | # Set up network
91 | # Import a generic network
92 | n = pypsa.Network(override_component_attrs=aux.create_override_components())
93 |
94 | # Set the time values for the network
95 | n.set_snapshots(times)
96 |
97 | # Import the design of the H2 plant into the network
98 | n.import_from_csv_folder("Parameters/Basic_H2_plant")
99 |
100 | # Import demand profile
101 | # Note: All flows are in MW or MWh, conversions for hydrogen done using HHVs. Hydrogen HHV = 39.4 MWh/t
102 | # hydrogen_demand = pd.read_excel(demand_path,index_col = 0) # Excel file in kg hydrogen, convert to MWh
103 | n.add('Load',
104 | 'Hydrogen demand',
105 | bus = 'Hydrogen',
106 | p_set = demand_profile['Demand']/1000*39.4,
107 | )
108 |
109 | # Send the weather data to the model
110 | n.generators_t.p_max_pu['Wind'] = wind_potential
111 | n.generators_t.p_max_pu['Solar'] = pv_potential
112 |
113 | # specify maximum capacity based on land use
114 | n.generators.loc['Wind','p_nom_max'] = wind_max_capacity
115 | n.generators.loc['Solar','p_nom_max'] = pv_max_capacity
116 |
117 | # specify technology-specific and country-specific WACC and lifetime here
118 | n.generators.loc['Wind','capital_cost'] = n.generators.loc['Wind','capital_cost']\
119 | * CRF(country_series['Wind interest rate'], country_series['Wind lifetime (years)'])
120 | n.generators.loc['Solar','capital_cost'] = n.generators.loc['Solar','capital_cost']\
121 | * CRF(country_series['Solar interest rate'], country_series['Solar lifetime (years)'])
122 | for item in [n.links, n.stores,n.storage_units]:
123 | item.capital_cost = item.capital_cost * CRF(country_series['Plant interest rate'],country_series['Plant lifetime (years)'])
124 |
125 | # Solve the model
126 | solver = 'gurobi'
127 | n.lopf(solver_name=solver,
128 | solver_options = {'LogToConsole':0, 'OutputFlag':0},
129 | pyomo=False,
130 | extra_functionality=aux.extra_functionalities,
131 | )
132 | # Output results
133 |
134 | lcoh = n.objective/(n.loads_t.p_set.sum()[0]/39.4*1000) # convert back to kg H2
135 | # wind_capacity = n.generators.p_nom_opt['Wind']
136 | # solar_capacity = n.generators.p_nom_opt['Solar']
137 | # electrolyzer_capacity = n.links.p_nom_opt['Electrolysis']
138 | # battery_capacity = n.storage_units.p_nom_opt['Battery']
139 | # h2_storage = n.stores.e_nom_opt['Compressed H2 Store']
140 | print(lcoh)
141 |
142 |
143 | # instead of saving capacities, want to plot temporal profile of input and output
144 | # for testing, use time window of July
145 | return n
146 |
147 |
148 | def plot_dispatch(n, time, demand, hex):
149 | fig, ax = plt.subplots(figsize=(6, 3))
150 |
151 | p_by_carrier = n.generators_t.p.loc[time]
152 |
153 | if not n.storage_units.empty:
154 |
155 | sto = (
156 | n.storage_units_t.p.loc[time]
157 | )
158 |
159 | p_by_carrier = pd.concat([p_by_carrier, sto], axis=1)
160 | if not n.stores.empty:
161 | sto = (
162 | n.stores_t.p.loc[time]
163 | )
164 | p_by_carrier = pd.concat([p_by_carrier, sto], axis=1)
165 |
166 | p_by_carrier.where(p_by_carrier > 0).loc[time].plot.area(
167 | ax=ax,
168 | linewidth=0,
169 | )
170 |
171 | charge = p_by_carrier.where(p_by_carrier < 0).dropna(how="all", axis=1).loc[time]
172 |
173 | if not charge.empty:
174 | charge.plot.area(
175 | ax=ax,
176 | linewidth=0,
177 | )
178 |
179 | n.loads_t.p_set.sum(axis=1).loc[time].plot(ax=ax, c="k")
180 |
181 | plt.legend(loc=(1.05, 0.2))
182 | ax.set_ylabel("MW")
183 | ax.set_ylim(-1500, 2000)
184 | fig.savefig(f'Resources\\{demand} hexagon {hex} temporal - October.png', bbox_inches='tight')
185 | plt.close()
186 |
187 | # %%
188 | time = slice('2022-10-01','2022-10-15')
189 |
190 |
191 | transport_excel_path = "Parameters/transport_parameters.xlsx"
192 | weather_excel_path = "Parameters/weather_parameters.xlsx"
193 | country_excel_path = 'Parameters/country_parameters.xlsx'
194 | country_parameters = pd.read_excel(country_excel_path,
195 | index_col='Country')
196 | demand_excel_path = 'Parameters/demand_parameters.xlsx'
197 | demand_parameters = pd.read_excel(demand_excel_path,
198 | index_col='Demand center',
199 | ).squeeze("columns")
200 | demand_centers = demand_parameters.index
201 | weather_parameters = pd.read_excel(weather_excel_path,
202 | index_col = 'Parameters'
203 | ).squeeze('columns')
204 | weather_filename = weather_parameters['Filename']
205 |
206 | hexagons = gpd.read_file('Resources/hex_transport.geojson')
207 | # !!! change to name of cutout in weather
208 | cutout = atlite.Cutout('Cutouts/' + weather_filename +'.nc')
209 | layout = cutout.uniform_layout()
210 |
211 | pv_profile = cutout.pv(
212 | panel= 'CSi',
213 | orientation='latitude_optimal',
214 | layout = layout,
215 | shapes = hexagons,
216 | per_unit = True
217 | )
218 | pv_profile = pv_profile.rename(dict(dim_0='hexagon'))
219 |
220 | wind_profile = cutout.wind(
221 | # Changed turbine type - was Vestas_V80_2MW_gridstreamer in first run
222 | # Other option being explored: NREL_ReferenceTurbine_2020ATB_4MW, Enercon_E126_7500kW
223 | turbine = 'NREL_ReferenceTurbine_2020ATB_4MW',
224 | layout = layout,
225 | shapes = hexagons,
226 | per_unit = True
227 | )
228 | wind_profile = wind_profile.rename(dict(dim_0='hexagon'))
229 | # %%
230 | for location in demand_centers:
231 | for hexagon in hexagon_list:
232 | hydrogen_demand_trucking, hydrogen_demand_pipeline = demand_schedule(
233 | demand_parameters.loc[location,'Annual demand [kg/a]'],
234 | hexagons.loc[hexagon,f'{location} trucking state'],
235 | transport_excel_path,
236 | weather_excel_path)
237 | country_series = country_parameters.loc[hexagons.country[hexagon]]
238 | # trucking demand
239 | n = optimize_hydrogen_plant(wind_profile.sel(hexagon = hexagon),
240 | pv_profile.sel(hexagon = hexagon),
241 | wind_profile.time,
242 | hydrogen_demand_trucking,
243 | hexagons.loc[hexagon,'theo_turbines'],
244 | hexagons.loc[hexagon,'theo_pv'],
245 | country_series,
246 | # water_limit = hexagons.loc[hexagon,'delta_water_m3']
247 | )
248 | plot_dispatch(n, time, location, hexagon)
249 | # pipeline demand
250 | n = optimize_hydrogen_plant(wind_profile.sel(hexagon = hexagon),
251 | pv_profile.sel(hexagon = hexagon),
252 | wind_profile.time,
253 | hydrogen_demand_pipeline,
254 | hexagons.loc[hexagon,'theo_turbines'],
255 | hexagons.loc[hexagon,'theo_pv'],
256 | country_series,
257 | # water_limit = hexagons.loc[hexagon,'delta_water_m3'],
258 | )
259 | plot_dispatch(n, time, location, hexagon)
260 |
261 |
--------------------------------------------------------------------------------
/Scripts/optimize_transport_and_conversion.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | """
4 | Created on Sun Mar 26 16:52:59 2023
5 |
6 | @author: Claire Halloran, University of Oxford
7 | Contains code originally written by Leander Müller, RWTH Aachen University
8 |
9 | Calculates the cost-optimal hydrogen transportation strategy to the nearest demand center.
10 |
11 | Calculate cost of pipeline transport and demand profile based on optimal size
12 |
13 |
14 |
15 | """
16 |
17 | import geopandas as gpd
18 | import numpy as np
19 | import pandas as pd
20 | from functions import CRF, cheapest_trucking_strategy, h2_conversion_stand, cheapest_pipeline_strategy
21 | from shapely.geometry import Point
22 | import shapely.geometry
23 | import shapely.wkt
24 | import geopy.distance
25 | import os
26 | import json
27 |
28 | #%% Data Input
29 |
30 | # Excel file with technology parameters
31 | technology_parameters = str(snakemake.input.technology_parameters)
32 | demand_parameters = str(snakemake.input.demand_parameters)
33 | country_parameters = str(snakemake.input.country_parameters)
34 | conversion_parameters = str(snakemake.input.conversion_parameters)
35 | transport_parameters = str(snakemake.input.transport_parameters)
36 | pipeline_parameters = str(snakemake.input.pipeline_parameters)
37 |
38 | #%% load data from technology parameters Excel file
39 |
40 | infra_data = pd.read_excel(technology_parameters,
41 | sheet_name='Infra',
42 | index_col='Infrastructure')
43 |
44 | global_data = pd.read_excel(technology_parameters,
45 | sheet_name='Global',
46 | index_col='Parameter'
47 | ).squeeze("columns")
48 |
49 | demand_center_list = pd.read_excel(demand_parameters,
50 | sheet_name='Demand centers',
51 | index_col='Demand center',
52 | )
53 | country_parameters = pd.read_excel(country_parameters,
54 | index_col='Country')
55 |
56 | pipeline_construction = snakemake.config["transport"]["pipeline_construction"]
57 | road_construction = snakemake.config["transport"]["road_construction"]
58 |
59 | road_capex_long = infra_data.at['Long road','CAPEX']
60 | road_capex_short = infra_data.at['Short road','CAPEX']
61 | road_opex = infra_data.at['Short road','OPEX']
62 |
63 | #%% Handle any hexagons at edges in the geojson which are labelled with a country we aren't analyzing
64 | hexagon_path = str(snakemake.input.hexagons)
65 | # Read the GeoJSON file
66 | with open(hexagon_path, 'r') as file:
67 | data = json.load(file)
68 |
69 | copied_list = data["features"].copy()
70 |
71 | # iterates through hexagons and removes ones that have a different country than the one we want
72 | for feature in copied_list:
73 | # Access and modify properties
74 | if feature['properties']['country'] != country_parameters.index.values[0]:
75 | data['features'].remove(feature)
76 |
77 | # Write the modified GeoJSON back to the file
78 | with open(hexagon_path, 'w') as file:
79 | json.dump(data, file)
80 |
81 | # Now, load the Hexagon file in geopandas
82 | hexagon = gpd.read_file(hexagon_path)
83 |
84 | # Create Resources folder to save results if it doesn't already exist
85 | if not os.path.exists('Resources'):
86 | os.makedirs('Resources')
87 |
88 | #%% calculate cost of hydrogen state conversion and transportation for demand
89 | # loop through all demand centers-- limit this on continential scale
90 | for d in demand_center_list.index:
91 | demand_location = Point(demand_center_list.loc[d,'Lon [deg]'], demand_center_list.loc[d,'Lat [deg]'])
92 | distance_to_demand = np.empty(len(hexagon))
93 | hydrogen_quantity = demand_center_list.loc[d,'Annual demand [kg/a]']
94 | road_construction_costs = np.empty(len(hexagon))
95 | trucking_states = np.empty(len(hexagon),dtype='0:
196 | trucking_costs[i] = np.nan
197 | trucking_states[i] = np.nan
198 | # pipeline costs
199 | if pipeline_construction== True:
200 |
201 | pipeline_cost, pipeline_type =\
202 | cheapest_pipeline_strategy(demand_state,
203 | hydrogen_quantity,
204 | distance_to_demand[i],
205 | country_parameters.loc[hexagon.country[i],'Electricity price (euros/kWh)'],
206 | country_parameters.loc[hexagon.country[i],'Heat price (euros/kWh)'],
207 | country_parameters.loc[hexagon['country'][i],'Infrastructure interest rate'],
208 | conversion_parameters,
209 | pipeline_parameters,
210 | country_parameters.loc[hexagon.country[demand_fid],'Electricity price (euros/kWh)'],
211 | )
212 | pipeline_costs[i] = pipeline_cost
213 | else:
214 | pipeline_costs[i] = np.nan
215 |
216 | # variables to save for each demand scenario
217 | hexagon[f'{d} road construction costs'] = road_construction_costs/hydrogen_quantity
218 | hexagon[f'{d} trucking transport and conversion costs'] = trucking_costs # cost of road construction, supply conversion, trucking transport, and demand conversion
219 | hexagon[f'{d} trucking state'] = trucking_states # cost of road construction, supply conversion, trucking transport, and demand conversion
220 | hexagon[f'{d} pipeline transport and conversion costs'] = pipeline_costs # cost of supply conversion, pipeline transport, and demand conversion
221 |
222 | # Added force to UTF-8 encoding.
223 | hexagon.to_file(str(snakemake.output), driver='GeoJSON', encoding='utf-8')
--------------------------------------------------------------------------------
/Scripts/map_costs.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | """
4 | Created on Thu Apr 27 11:14:48 2023
5 |
6 | @author: Claire Halloran, University of Oxford
7 |
8 | This script visualizes the spatial cost of hydrogen for each demand center.
9 | """
10 |
11 | import geopandas as gpd
12 | import cartopy.crs as ccrs
13 | import matplotlib.pyplot as plt
14 | import pandas as pd
15 | import os
16 |
17 | hexagons = gpd.read_file(str(snakemake.input.hexagons))
18 | demand_excel_path = str(snakemake.input.demand_parameters)
19 | demand_parameters = pd.read_excel(demand_excel_path,index_col='Demand center')
20 | demand_centers = demand_parameters.index
21 | transport_methods = ['pipeline', 'trucking']
22 |
23 | # plot LCOH for each hexagon
24 | # update central coordinates for area considered
25 | hexagon_bounds = hexagons.geometry.bounds
26 | min_lon, min_lat = hexagon_bounds[['minx','miny']].min()
27 | max_lon, max_lat = hexagon_bounds[['maxx','maxy']].max()
28 |
29 | central_lon = (min_lon + max_lon)/2
30 | central_lat = (min_lat + max_lat)/2
31 |
32 | crs = ccrs.Orthographic(central_longitude = central_lon, central_latitude= central_lat)
33 |
34 | output_folder = str(snakemake.output)
35 | if not os.path.exists(output_folder):
36 | os.makedirs(output_folder)
37 |
38 | for demand_center in demand_centers:
39 | # plot lowest LCOH in each location
40 | fig = plt.figure(figsize=(10,5))
41 |
42 | ax = plt.axes(projection=crs)
43 | ax.set_axis_off()
44 |
45 | hexagons.to_crs(crs.proj4_init).plot(
46 | ax=ax,
47 | column = f'{demand_center} lowest cost',
48 | legend = True,
49 | cmap = 'viridis_r',
50 | legend_kwds={'label':'LCOH [euros/kg]'},
51 | missing_kwds={
52 | "color": "lightgrey",
53 | "label": "Missing values",
54 | },
55 | )
56 | ax.set_title(f'{demand_center} LCOH')
57 | fig.savefig(output_folder + f'/{demand_center} LCOH.png', bbox_inches='tight')
58 | plt.close()
59 |
60 | for transport_method in transport_methods:
61 | fig = plt.figure(figsize=(10,5))
62 |
63 | ax = plt.axes(projection=crs)
64 | ax.set_axis_off()
65 |
66 | hexagons.to_crs(crs.proj4_init).plot(
67 | ax=ax,
68 | column = f'{demand_center} {transport_method} production cost',
69 | legend = True,
70 | cmap = 'viridis_r',
71 | legend_kwds={'label':'Production LCOH [euros/kg]'},
72 | missing_kwds={
73 | "color": "lightgrey",
74 | "label": "Missing values",
75 | },
76 | )
77 | ax.set_title(f'{demand_center} {transport_method} production cost')
78 | fig.savefig(output_folder + f'/{demand_center} {transport_method} production cost.png', bbox_inches='tight')
79 | plt.close()
80 |
81 | #%% plot transportation costs
82 | fig = plt.figure(figsize=(10,5))
83 |
84 | ax = plt.axes(projection=crs)
85 | ax.set_axis_off()
86 |
87 | hexagons[f'{demand_center} total {transport_method} cost'] =\
88 | hexagons[f'{demand_center} {transport_method} transport and conversion costs']+hexagons[f'{demand_center} road construction costs']
89 |
90 | hexagons.to_crs(crs.proj4_init).plot(
91 | ax=ax,
92 | column = f'{demand_center} total {transport_method} cost',
93 | legend = True,
94 | cmap = 'viridis_r',
95 | legend_kwds={'label':'{transport_method} cost [euros/kg]'},
96 | missing_kwds={
97 | "color": "lightgrey",
98 | "label": "Missing values",
99 | },
100 | )
101 | ax.set_title(f'{demand_center} {transport_method} transport costs')
102 | fig.savefig(output_folder + f'/{demand_center} {transport_method} transport cost.png',
103 | bbox_inches='tight')
104 | plt.close()
105 |
106 | # %% plot total costs
107 |
108 | fig = plt.figure(figsize=(10,5))
109 |
110 | ax = plt.axes(projection=crs)
111 | ax.set_axis_off()
112 |
113 | hexagons.to_crs(crs.proj4_init).plot(
114 | ax=ax,
115 | column = f'{demand_center} {transport_method} total cost',
116 | legend = True,
117 | cmap = 'viridis_r',
118 | legend_kwds={'label':'LCOH [euros/kg]'},
119 | missing_kwds={
120 | "color": "lightgrey",
121 | "label": "Missing values",
122 | },
123 | )
124 | ax.set_title(f'{demand_center} {transport_method} LCOH')
125 | fig.savefig(output_folder + f'/{demand_center} {transport_method} LCOH.png',
126 | bbox_inches='tight')
127 | plt.close()
128 |
129 | # Electrolyzer capacity
130 |
131 | fig = plt.figure(figsize=(10, 5))
132 |
133 | ax = plt.axes(projection=crs)
134 | ax.set_axis_off()
135 |
136 | hexagons.to_crs(crs.proj4_init).plot(
137 | ax=ax,
138 | column=f'{demand_center} {transport_method} electrolyzer capacity',
139 | legend=True,
140 | cmap='viridis_r',
141 | legend_kwds={'label': 'Size (MW)'},
142 | missing_kwds={
143 | "color": "lightgrey",
144 | "label": "Missing values",
145 | },
146 | )
147 | ax.set_title(f'{demand_center} {transport_method} electrolyzer capacity')
148 | fig.savefig(output_folder + f'/{demand_center} {transport_method} electrolyzer capacity.png',
149 | bbox_inches='tight')
150 | plt.close()
151 |
152 |
153 | # Electrolyzer costs
154 |
155 | fig = plt.figure(figsize=(10, 5))
156 |
157 | ax = plt.axes(projection=crs)
158 | ax.set_axis_off()
159 |
160 | hexagons.to_crs(crs.proj4_init).plot(
161 | ax=ax,
162 | column=f'{demand_center} {transport_method} electrolyzer costs',
163 | legend=True,
164 | cmap='viridis_r',
165 | legend_kwds={'label': '$'},
166 | missing_kwds={
167 | "color": "lightgrey",
168 | "label": "Missing values",
169 | },
170 | )
171 | ax.set_title(f'{demand_center} {transport_method} electrolyzer costs')
172 | fig.savefig(output_folder + f'/{demand_center} {transport_method} electrolyzer costs.png',
173 | bbox_inches='tight')
174 | plt.close()
175 |
176 | # H2 storage capacity
177 |
178 | fig = plt.figure(figsize=(10, 5))
179 |
180 | ax = plt.axes(projection=crs)
181 | ax.set_axis_off()
182 |
183 | hexagons.to_crs(crs.proj4_init).plot(
184 | ax=ax,
185 | column=f'{demand_center} {transport_method} H2 storage capacity',
186 | legend=True,
187 | cmap='viridis_r',
188 | legend_kwds={'label': 'Size (MW)'},
189 | missing_kwds={
190 | "color": "lightgrey",
191 | "label": "Missing values",
192 | },
193 | )
194 | ax.set_title(f'{demand_center} {transport_method} H2 storage capacity')
195 | fig.savefig(output_folder + f'/{demand_center} {transport_method} H2 storage capacity.png',
196 | bbox_inches='tight')
197 | plt.close()
198 |
199 | # H2 storage costs
200 |
201 | fig = plt.figure(figsize=(10, 5))
202 |
203 | ax = plt.axes(projection=crs)
204 | ax.set_axis_off()
205 |
206 | hexagons.to_crs(crs.proj4_init).plot(
207 | ax=ax,
208 | column=f'{demand_center} {transport_method} H2 storage costs',
209 | legend=True,
210 | cmap='viridis_r',
211 | legend_kwds={'label': '$'},
212 | missing_kwds={
213 | "color": "lightgrey",
214 | "label": "Missing values",
215 | },
216 | )
217 | ax.set_title(f'{demand_center} {transport_method} H2 storage costs')
218 | fig.savefig(output_folder + f'/{demand_center} {transport_method} H2 storage costs.png',
219 | bbox_inches='tight')
220 | plt.close()
221 |
222 | # Battery capcity
223 |
224 | fig = plt.figure(figsize=(10, 5))
225 |
226 | ax = plt.axes(projection=crs)
227 | ax.set_axis_off()
228 |
229 | hexagons.to_crs(crs.proj4_init).plot(
230 | ax=ax,
231 | column=f'{demand_center} {transport_method} battery capacity',
232 | legend=True,
233 | cmap='viridis_r',
234 | legend_kwds={'label': 'Size (MW)'},
235 | missing_kwds={
236 | "color": "lightgrey",
237 | "label": "Missing values",
238 | },
239 | )
240 | ax.set_title(f'{demand_center} {transport_method} battery capacity')
241 | fig.savefig(output_folder + f'/{demand_center} {transport_method} battery capacity.png',
242 | bbox_inches='tight')
243 | plt.close()
244 |
245 | # battery costs
246 |
247 | fig = plt.figure(figsize=(10, 5))
248 |
249 | ax = plt.axes(projection=crs)
250 | ax.set_axis_off()
251 |
252 | hexagons.to_crs(crs.proj4_init).plot(
253 | ax=ax,
254 | column=f'{demand_center} {transport_method} battery costs',
255 | legend=True,
256 | cmap='viridis_r',
257 | legend_kwds={'label': '$'},
258 | missing_kwds={
259 | "color": "lightgrey",
260 | "label": "Missing values",
261 | },
262 | )
263 | ax.set_title(f'{demand_center} {transport_method} battery costs')
264 | fig.savefig(output_folder + f'/{demand_center} {transport_method} battery costs.png',
265 | bbox_inches='tight')
266 | plt.close()
267 |
268 | for generator in snakemake.config['generators']:
269 | # generator capacity
270 | generator = generator.lower()
271 | fig = plt.figure(figsize=(10, 5))
272 |
273 | ax = plt.axes(projection=crs)
274 | ax.set_axis_off()
275 |
276 | hexagons.to_crs(crs.proj4_init).plot(
277 | ax=ax,
278 | column=f'{demand_center} {transport_method} {generator} capacity',
279 | legend=True,
280 | cmap='viridis_r',
281 | legend_kwds={'label': 'Capacity (MW)'},
282 | missing_kwds={
283 | "color": "lightgrey",
284 | "label": "Missing values",
285 | },
286 | )
287 | ax.set_title(f'{demand_center} {transport_method} {generator} capacity')
288 | fig.savefig(output_folder + f'/{demand_center} {transport_method} {generator} capacity.png',
289 | bbox_inches='tight')
290 | plt.close()
291 |
292 | # generator costs
293 |
294 | fig = plt.figure(figsize=(10, 5))
295 |
296 | ax = plt.axes(projection=crs)
297 | ax.set_axis_off()
298 |
299 | hexagons.to_crs(crs.proj4_init).plot(
300 | ax=ax,
301 | column=f'{demand_center} {transport_method} {generator} costs',
302 | legend=True,
303 | cmap='viridis_r',
304 | legend_kwds={'label': '$'}, #!!! correct label?
305 | missing_kwds={
306 | "color": "lightgrey",
307 | "label": "Missing values",
308 | },
309 | )
310 | ax.set_title(f'{demand_center} {transport_method} {generator} costs ')
311 | fig.savefig(output_folder + f'/{demand_center} {transport_method} {generator} costs.png',
312 | bbox_inches='tight')
313 | plt.close()
314 |
315 | # %% plot water costs
316 |
317 | fig = plt.figure(figsize=(10,5))
318 |
319 | ax = plt.axes(projection=crs)
320 | ax.set_axis_off()
321 |
322 | hexagons.to_crs(crs.proj4_init).plot(
323 | ax=ax,
324 | column = 'Ocean water costs',
325 | legend = True,
326 | cmap = 'viridis_r',
327 | legend_kwds={'label':'Water cost [euros/kg H2]'},
328 | missing_kwds={
329 | "color": "lightgrey",
330 | "label": "Missing values",
331 | },
332 | )
333 | plt.ticklabel_format(style='plain')
334 | ax.set_title('Ocean water costs')
335 | fig.savefig(output_folder +'/Ocean water costs.png', bbox_inches='tight')
336 | plt.close()
337 |
338 | fig = plt.figure(figsize=(10,5))
339 |
340 | ax = plt.axes(projection=crs)
341 | ax.set_axis_off()
342 |
343 | hexagons.to_crs(crs.proj4_init).plot(
344 | ax=ax,
345 | column = 'Freshwater costs',
346 | legend = True,
347 | cmap = 'viridis_r',
348 | legend_kwds={'label':'Water cost [euros/kg H2]'},
349 | missing_kwds={
350 | "color": "lightgrey",
351 | "label": "Missing values",
352 | },
353 | )
354 | plt.ticklabel_format(style='plain')
355 | ax.set_title('Freshwater costs')
356 | fig.savefig(output_folder +'/Freshwater costs.png', bbox_inches='tight')
357 | plt.close()
358 |
359 |
360 |
--------------------------------------------------------------------------------
/Scripts/optimize_hydrogen_plant.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | Created on Sun Feb 26 11:47:57 2023
4 |
5 | @author: Claire Halloran, University of Oxford
6 |
7 | Includes code from Nicholas Salmon, University of Oxford, for optimizing
8 | hydrogen plant capacity.
9 |
10 | """
11 |
12 | import atlite
13 | import geopandas as gpd
14 | import pypsa
15 | import pandas as pd
16 | import p_H2_aux as aux
17 | from functions import CRF
18 | import numpy as np
19 | import logging
20 | import time
21 |
22 | logging.basicConfig(level=logging.ERROR)
23 |
24 | def demand_schedule(quantity, start_date, end_date, transport_state, transport_excel_path):
25 | '''
26 | calculates hourly hydrogen demand for truck shipment and pipeline transport.
27 |
28 | Parameters
29 | ----------
30 | quantity : float
31 | annual amount of hydrogen to transport in kilograms.
32 | start_date: string
33 | start date for demand schedule in the format YYYY-MM-DD.
34 | end_date: string
35 | end date for demand schedule in the format YYYY-MM-DD.
36 | transport_state : string
37 | state hydrogen is transported in, one of '500 bar', 'LH2', 'LOHC', or 'NH3'.
38 | transport_excel_path : string
39 | path to transport_parameters.xlsx file
40 |
41 | Returns
42 | -------
43 | trucking_hourly_demand_schedule : pandas DataFrame
44 | hourly demand profile for hydrogen trucking.
45 | pipeline_hourly_demand_schedule : pandas DataFrame
46 | hourly demand profile for pipeline transport.
47 | '''
48 | # schedule for pipeline
49 | index = pd.date_range(start_date, end_date, freq = 'H')
50 | pipeline_hourly_quantity = quantity/index.size
51 | pipeline_hourly_demand_schedule = pd.DataFrame(pipeline_hourly_quantity, index=index, columns = ['Demand'])
52 |
53 | # if demand center is in hexagon
54 | if transport_state=="None":
55 | # schedule for trucking
56 | annual_deliveries = 365*24
57 | trucking_hourly_demand = quantity/annual_deliveries
58 | index = pd.date_range(start_date, end_date, periods=annual_deliveries)
59 | trucking_demand_schedule = pd.DataFrame(trucking_hourly_demand, index=index, columns = ['Demand'])
60 | trucking_hourly_demand_schedule = trucking_demand_schedule.resample('H').sum().fillna(0.)
61 |
62 | return trucking_hourly_demand_schedule, pipeline_hourly_demand_schedule
63 | else:
64 | transport_parameters = pd.read_excel(transport_excel_path,
65 | sheet_name = transport_state,
66 | index_col = 'Parameter'
67 | ).squeeze('columns')
68 |
69 | truck_capacity = transport_parameters['Net capacity (kg H2)']
70 |
71 | # schedule for trucking
72 | annual_deliveries = quantity/truck_capacity
73 | quantity_per_delivery = quantity/annual_deliveries
74 | index = pd.date_range(start_date, end_date, periods=annual_deliveries)
75 | trucking_demand_schedule = pd.DataFrame(quantity_per_delivery, index=index, columns=['Demand'])
76 | trucking_hourly_demand_schedule = trucking_demand_schedule.resample('H').sum().fillna(0.)
77 |
78 | return trucking_hourly_demand_schedule, pipeline_hourly_demand_schedule
79 |
80 | # in the future, may want to make hexagons a class with different features
81 | def optimize_hydrogen_plant(wind_potential, pv_potential, times, demand_profile,
82 | wind_max_capacity, pv_max_capacity,
83 | country_series, water_limit = None):
84 | '''
85 | Optimizes the size of green hydrogen plant components based on renewable potential, hydrogen demand, and country parameters.
86 |
87 | Parameters
88 | ----------
89 | wind_potential : xarray DataArray
90 | 1D dataarray of per-unit wind potential in hexagon.
91 | pv_potential : xarray DataArray
92 | 1D dataarray of per-unit solar potential in hexagon.
93 | times : xarray DataArray
94 | 1D dataarray with timestamps for wind and solar potential.
95 | demand_profile : pandas DataFrame
96 | hourly dataframe of hydrogen demand in kg.
97 | country_series : pandas Series
98 | interest rate and lifetime information.
99 | water_limit : float
100 | annual limit on water available for electrolysis in hexagon, in cubic meters. Default is None.
101 |
102 | Returns
103 | -------
104 | lcoh : float
105 | levelized cost per kg hydrogen.
106 | wind_capacity: float
107 | optimal wind capacity in MW.
108 | solar_capacity: float
109 | optimal solar capacity in MW.
110 | electrolyzer_capacity: float
111 | optimal electrolyzer capacity in MW.
112 | battery_capacity: float
113 | optimal battery storage capacity in MW/MWh (1 hour batteries).
114 | h2_storage: float
115 | optimal hydrogen storage capacity in MWh.
116 |
117 | '''
118 |
119 | # if a water limit is given, check if hydrogen demand can be met
120 | if water_limit != None:
121 | # total hydrogen demand in kg
122 | total_hydrogen_demand = demand_profile['Demand'].sum()
123 | # check if hydrogen demand can be met based on hexagon water availability
124 | water_constraint = total_hydrogen_demand <= water_limit * 111.57 # kg H2 per cubic meter of water
125 | if water_constraint == False:
126 | print('Not enough water to meet hydrogen demand!')
127 | # return null values
128 | lcoh = np.nan
129 | wind_capacity = np.nan
130 | solar_capacity = np.nan
131 | electrolyzer_capacity = np.nan
132 | battery_capacity = np.nan
133 | h2_storage = np.nan
134 | return lcoh, wind_capacity, solar_capacity, electrolyzer_capacity, battery_capacity, h2_storage
135 |
136 | # Set up network
137 | # Import a generic network
138 | n = pypsa.Network(override_component_attrs=aux.create_override_components())
139 |
140 | # Set the time values for the network
141 | n.set_snapshots(times)
142 |
143 | # Import the design of the H2 plant into the network
144 | n.import_from_csv_folder("Parameters/Basic_H2_plant")
145 |
146 | # Import demand profile
147 | # Note: All flows are in MW or MWh, conversions for hydrogen done using HHVs. Hydrogen HHV = 39.4 MWh/t
148 | n.add('Load',
149 | 'Hydrogen demand',
150 | bus = 'Hydrogen',
151 | p_set = demand_profile['Demand']/1000*39.4,
152 | )
153 |
154 | # Send the weather data to the model
155 | n.generators_t.p_max_pu['Wind'] = wind_potential
156 | n.generators_t.p_max_pu['Solar'] = pv_potential
157 |
158 | # specify maximum capacity based on land use
159 | n.generators.loc['Wind','p_nom_max'] = wind_max_capacity*4
160 | n.generators.loc['Solar','p_nom_max'] = pv_max_capacity
161 |
162 | # specify technology-specific and country-specific WACC and lifetime here
163 | n.generators.loc['Wind','capital_cost'] = n.generators.loc['Wind','capital_cost']\
164 | * CRF(country_series['Wind interest rate'], country_series['Wind lifetime (years)'])
165 | n.generators.loc['Solar','capital_cost'] = n.generators.loc['Solar','capital_cost']\
166 | * CRF(country_series['Solar interest rate'], country_series['Solar lifetime (years)'])
167 | for item in [n.links, n.stores,n.storage_units]:
168 | item.capital_cost = item.capital_cost * CRF(country_series['Plant interest rate'],country_series['Plant lifetime (years)'])
169 |
170 | # Solve the model
171 | solver = 'gurobi'
172 | n.lopf(solver_name=solver,
173 | solver_options = {'LogToConsole':0, 'OutputFlag':0},
174 | pyomo=False,
175 | extra_functionality=aux.extra_functionalities,
176 | )
177 | # Output results
178 |
179 | lcoh = n.objective/(n.loads_t.p_set.sum()[0]/39.4*1000) # convert back to kg H2
180 | wind_capacity = n.generators.p_nom_opt['Wind']
181 | solar_capacity = n.generators.p_nom_opt['Solar']
182 | electrolyzer_capacity = n.links.p_nom_opt['Electrolysis']
183 | battery_capacity = n.storage_units.p_nom_opt['Battery']
184 | h2_storage = n.stores.e_nom_opt['Compressed H2 Store']
185 | print(lcoh)
186 | return lcoh, wind_capacity, solar_capacity, electrolyzer_capacity, battery_capacity, h2_storage
187 |
188 |
189 | if __name__ == "__main__":
190 | transport_excel_path = str(snakemake.input.transport_parameters)
191 | country_excel_path = str(snakemake.input.country_parameters)
192 | demand_excel_path = str(snakemake.input.demand_parameters)
193 | country_parameters = pd.read_excel(country_excel_path,
194 | index_col='Country')
195 | demand_parameters = pd.read_excel(demand_excel_path,
196 | index_col='Demand center',
197 | ).squeeze("columns")
198 | demand_centers = demand_parameters.index
199 |
200 | weather_year = snakemake.wildcards.weather_year
201 | end_weather_year = int(snakemake.wildcards.weather_year)+1
202 | start_date = f'{weather_year}-01-01'
203 | end_date = f'{end_weather_year}-01-01'
204 |
205 | hexagons = gpd.read_file(str(snakemake.input.hexagons))
206 | cutout = atlite.Cutout(f'Cutouts/{snakemake.wildcards.country}_{snakemake.wildcards.weather_year}.nc')
207 | layout = cutout.uniform_layout()
208 |
209 | # can add hydro and other generators here
210 | pv_profile = cutout.pv(
211 | panel= 'CSi',
212 | orientation='latitude_optimal',
213 | layout = layout,
214 | shapes = hexagons,
215 | per_unit = True
216 | )
217 | pv_profile = pv_profile.rename(dict(dim_0='hexagon'))
218 |
219 | wind_profile = cutout.wind(
220 | turbine = 'NREL_ReferenceTurbine_2020ATB_4MW',
221 | layout = layout,
222 | shapes = hexagons,
223 | per_unit = True
224 | )
225 | wind_profile = wind_profile.rename(dict(dim_0='hexagon'))
226 |
227 | for location in demand_centers:
228 | # trucking variables
229 | lcohs_trucking = np.zeros(len(pv_profile.hexagon))
230 | t_solar_capacities= np.zeros(len(pv_profile.hexagon))
231 | t_wind_capacities= np.zeros(len(pv_profile.hexagon))
232 | t_electrolyzer_capacities= np.zeros(len(pv_profile.hexagon))
233 | t_battery_capacities = np.zeros(len(pv_profile.hexagon))
234 | t_h2_storages= np.zeros(len(pv_profile.hexagon))
235 |
236 | # pipeline variables
237 | lcohs_pipeline = np.zeros(len(pv_profile.hexagon))
238 | p_solar_capacities= np.zeros(len(pv_profile.hexagon))
239 | p_wind_capacities= np.zeros(len(pv_profile.hexagon))
240 | p_electrolyzer_capacities= np.zeros(len(pv_profile.hexagon))
241 | p_battery_capacities = np.zeros(len(pv_profile.hexagon))
242 | p_h2_storages= np.zeros(len(pv_profile.hexagon))
243 |
244 | # function
245 | for i in pv_profile.hexagon.data:
246 | hydrogen_demand_trucking, hydrogen_demand_pipeline =\
247 | demand_schedule(demand_parameters.loc[location,'Annual demand [kg/a]'],
248 | start_date,
249 | end_date,
250 | hexagons.loc[i,f'{location} trucking state'],
251 | transport_excel_path)
252 |
253 | country_series = country_parameters.loc[hexagons.country[i]]
254 |
255 | transport_types = ["trucking", "pipeline"]
256 | for j in transport_types:
257 | if j == "trucking":
258 | hydrogen_demand = hydrogen_demand_trucking
259 | else:
260 | hydrogen_demand = hydrogen_demand_pipeline
261 |
262 | lcoh, wind_capacity, solar_capacity, electrolyzer_capacity, battery_capacity, h2_storage =\
263 | optimize_hydrogen_plant(wind_profile.sel(hexagon = i),
264 | pv_profile.sel(hexagon = i),
265 | wind_profile.time,
266 | hydrogen_demand,
267 | hexagons.loc[i,'theo_turbines'],
268 | hexagons.loc[i,'theo_pv'],
269 | country_series,
270 | # water_limit = hexagons.loc[hexagon,'delta_water_m3']
271 | )
272 |
273 | if j == "trucking":
274 | lcohs_trucking[i] = lcoh
275 | t_solar_capacities[i] = solar_capacity
276 | t_wind_capacities[i] = wind_capacity
277 | t_electrolyzer_capacities[i] = electrolyzer_capacity
278 | t_battery_capacities[i] = battery_capacity
279 | t_h2_storages[i] = h2_storage
280 | else:
281 | lcohs_pipeline[i]=lcoh
282 | p_solar_capacities[i] = solar_capacity
283 | p_wind_capacities[i] = wind_capacity
284 | p_electrolyzer_capacities[i] = electrolyzer_capacity
285 | p_battery_capacities[i] = battery_capacity
286 | p_h2_storages[i] = h2_storage
287 |
288 | # updating trucking hexagons
289 | hexagons[f'{location} trucking solar capacity'] = t_solar_capacities
290 | hexagons[f'{location} trucking wind capacity'] = t_wind_capacities
291 | hexagons[f'{location} trucking electrolyzer capacity'] = t_electrolyzer_capacities
292 | hexagons[f'{location} trucking battery capacity'] = t_battery_capacities
293 | hexagons[f'{location} trucking H2 storage capacity'] = t_h2_storages
294 | # save trucking LCOH
295 | hexagons[f'{location} trucking production cost'] = lcohs_trucking
296 |
297 | # updating pipeline hexagons
298 | hexagons[f'{location} pipeline solar capacity'] = p_solar_capacities
299 | hexagons[f'{location} pipeline wind capacity'] = p_wind_capacities
300 | hexagons[f'{location} pipeline electrolyzer capacity'] = p_electrolyzer_capacities
301 | hexagons[f'{location} pipeline battery capacity'] = p_battery_capacities
302 | hexagons[f'{location} pipeline H2 storage capacity'] = p_h2_storages
303 |
304 | # add optimal LCOH for each hexagon to hexagon file
305 | hexagons[f'{location} pipeline production cost'] = lcohs_pipeline
306 |
307 | hexagons.to_file(str(snakemake.output), driver='GeoJSON', encoding='utf-8')
308 |
--------------------------------------------------------------------------------
/Scripts/functions.py:
--------------------------------------------------------------------------------
1 |
2 | import pandas as pd
3 | import numpy as np
4 |
5 | def CRF(interest,lifetime):
6 | '''
7 | Calculates the capital recovery factor of a capital investment.
8 |
9 | Parameters
10 | ----------
11 | interest : float
12 | interest rate.
13 | lifetime : float or integer
14 | lifetime of asset.
15 |
16 | Returns
17 | -------
18 | CRF : float
19 | present value factor.
20 |
21 | '''
22 | interest = float(interest)
23 | lifetime = float(lifetime)
24 |
25 | CRF = (((1+interest)**lifetime)*interest)/(((1+interest)**lifetime)-1)
26 | return CRF
27 |
28 | def trucking_costs(transport_state, distance, quantity, interest, transport_excel_path):
29 | '''
30 | calculates the annual cost of transporting hydrogen by truck.
31 |
32 | Parameters
33 | ----------
34 | transport_state : string
35 | state hydrogen is transported in, one of '500 bar', 'LH2', 'LOHC', or 'NH3'.
36 | distance : float
37 | distance between hydrogen production site and demand site.
38 | quantity : float
39 | annual amount of hydrogen to transport.
40 | interest : float
41 | interest rate on capital investments.
42 | excel_path : string
43 | path to transport_parameters.xlsx file
44 |
45 | Returns
46 | -------
47 | annual_costs : float
48 | annual cost of hydrogen transport with specified method.
49 | '''
50 | daily_quantity = quantity/365
51 |
52 | transport_parameters = pd.read_excel(transport_excel_path,
53 | sheet_name = transport_state,
54 | index_col = 'Parameter'
55 | ).squeeze('columns')
56 |
57 | average_truck_speed = transport_parameters['Average truck speed (km/h)']
58 | working_hours = transport_parameters['Working hours (h/day)']
59 | diesel_price = transport_parameters['Diesel price (euros/L)']
60 | costs_for_driver = transport_parameters['Costs for driver (euros/h)']
61 | working_days = transport_parameters['Working days (per year)']
62 | max_driving_dist = transport_parameters['Max driving distance (km/a)']
63 |
64 | spec_capex_truck = transport_parameters['Spec capex truck (euros)']
65 | spec_opex_truck = transport_parameters['Spec opex truck (% of capex/a)']
66 | diesel_consumption = transport_parameters['Diesel consumption (L/100 km)']
67 | truck_lifetime = transport_parameters['Truck lifetime (a)']
68 |
69 | spec_capex_trailor = transport_parameters['Spec capex trailer (euros)']
70 | spec_opex_trailor =transport_parameters['Spec opex trailer (% of capex/a)']
71 | net_capacity = transport_parameters['Net capacity (kg H2)']
72 | trailor_lifetime = transport_parameters['Trailer lifetime (a)']
73 | loading_unloading_time = transport_parameters['Loading unloading time (h)']
74 |
75 |
76 | amount_deliveries_needed = daily_quantity/net_capacity
77 | deliveries_per_truck = working_hours/(loading_unloading_time+(2*distance/average_truck_speed))
78 | trailors_needed = round((amount_deliveries_needed/deliveries_per_truck)+0.5,0)
79 | total_drives_day = round(amount_deliveries_needed+0.5,0) # not in ammonia calculation
80 | if transport_state == 'NH3':
81 | trucks_needed = trailors_needed
82 | else:
83 | trucks_needed = max(round((total_drives_day*2*distance*working_days/max_driving_dist)+0.5,0),trailors_needed)
84 |
85 | capex_trucks = trucks_needed * spec_capex_truck
86 | capex_trailor = trailors_needed * spec_capex_trailor
87 | if amount_deliveries_needed < 1:
88 | fuel_costs = (amount_deliveries_needed*2*distance*365/100)*diesel_consumption*diesel_price
89 | wages = amount_deliveries_needed * ((distance/average_truck_speed)*2+loading_unloading_time) * working_days * costs_for_driver
90 |
91 | else:
92 | fuel_costs = (round(amount_deliveries_needed+0.5)*2*distance*365/100)*diesel_consumption*diesel_price
93 | wages = round(amount_deliveries_needed+0.5) * ((distance/average_truck_speed)*2+loading_unloading_time) * working_days * costs_for_driver
94 |
95 | annual_costs = (capex_trucks*CRF(interest,truck_lifetime)+capex_trailor*CRF(interest,trailor_lifetime))\
96 | + capex_trucks*spec_opex_truck + capex_trailor*spec_opex_trailor + fuel_costs + wages
97 | return annual_costs
98 |
99 |
100 | def h2_conversion_stand(final_state, quantity, electricity_costs, heat_costs, interest,
101 | conversion_excel_path):
102 | '''
103 | calculates the annual cost and electricity and heating demand for converting
104 | hydrogen to a given state
105 |
106 | Parameters
107 | ----------
108 | final_state : string
109 | final state to convert hydrogen to, one of 'standard condition', '500 bar',
110 | 'LH2', 'LOHC_load', 'LOHC_unload', 'NH3_load', or 'NH3_unload'.
111 | quantity : float
112 | annual quantity of hydrogen to convert in kg.
113 | electricity_costs : float
114 | unit price for electricity.
115 | heat_costs : float
116 | unit costs for heat.
117 | interest : float
118 | interest rate applicable to hydrogen converter investments.
119 | conversion_excel_path: string
120 | path to conversion parameters excel sheet.
121 |
122 | Returns
123 | -------
124 | elec_demand : float
125 | annual electricity demand.
126 | heat_demand : float
127 | annual heat demand.
128 | annual_costs : float
129 | annual hydrogen conversion costs.
130 |
131 | '''
132 |
133 | daily_throughput = quantity/365
134 |
135 | if final_state != 'standard condition':
136 | conversion_parameters = pd.read_excel(conversion_excel_path,
137 | sheet_name = final_state,
138 | index_col = 'Parameter'
139 | ).squeeze('columns')
140 |
141 | if final_state == 'standard condition':
142 | elec_demand = 0
143 | heat_demand = 0
144 | annual_costs = 0
145 | return elec_demand, heat_demand, annual_costs
146 |
147 | elif final_state == '500 bar':
148 | cp = conversion_parameters['Heat capacity']
149 | Tein = conversion_parameters['Input temperature (K)']
150 | pein = conversion_parameters['Input pressure (bar)']
151 | k = conversion_parameters['Isentropic exponent']
152 | n_isentrop = conversion_parameters['Isentropic efficiency']
153 |
154 | compressor_lifetime = conversion_parameters['Compressor lifetime (a)']
155 | capex_coefficient = conversion_parameters['Compressor capex coefficient (euros per kilograms H2 per day)']
156 | opex_compressor = conversion_parameters['Compressor opex (% capex)']
157 |
158 | elec_demand_per_kg_h2 = (cp*Tein*(((500/pein)**((k-1)/k))-1))/n_isentrop
159 | elec_demand = elec_demand_per_kg_h2 * quantity
160 | heat_demand = 0
161 |
162 | capex_compressor = capex_coefficient * ((daily_throughput)**0.6038)
163 |
164 | annual_costs = (capex_compressor*CRF(interest,compressor_lifetime))\
165 | + (capex_compressor*opex_compressor)\
166 | + elec_demand * electricity_costs\
167 | + heat_demand*heat_costs
168 |
169 | return elec_demand, heat_demand, annual_costs
170 |
171 | elif final_state == 'LH2':
172 |
173 | electricity_unit_demand = conversion_parameters['Electricity demand (kWh per kg H2)']
174 | capex_quadratic_coefficient = conversion_parameters['Capex quadratic coefficient (euros (kg H2)-2)']
175 | capex_linear_coefficient = conversion_parameters['Capex linear coefficient (euros per kg H2)']
176 | capex_constant = conversion_parameters['Capex constant (euros)']
177 | opex_liquid_plant = conversion_parameters['Opex (% of capex)']
178 | liquid_plant_lifetime = conversion_parameters['Plant lifetime (a)']
179 |
180 | heat_demand = 0
181 | elec_demand = electricity_unit_demand * quantity
182 | capex_liquid_plant = capex_quadratic_coefficient *(daily_throughput**2)\
183 | +capex_linear_coefficient*daily_throughput\
184 | +capex_constant
185 |
186 | annual_costs = (capex_liquid_plant*CRF(interest,liquid_plant_lifetime))\
187 | + (capex_liquid_plant*opex_liquid_plant)\
188 | + elec_demand * electricity_costs\
189 | + heat_demand*heat_costs
190 | return elec_demand, heat_demand, annual_costs
191 |
192 | elif final_state == 'LOHC_load':
193 |
194 | electricity_unit_demand = conversion_parameters['Electricity demand (kWh per kg H2)']
195 | heat_unit_demand = conversion_parameters['Heat demand (kWh per kg H2)']
196 | capex_coefficient = conversion_parameters['Capex coefficient (euros per kilograms H2 per year)']
197 | opex_hydrogenation = conversion_parameters['Opex (% of capex)']
198 | hydrogenation_lifetime = conversion_parameters['Hydrogenation lifetime (a)']
199 | costs_carrier = conversion_parameters['Carrier costs (euros per kg carrier)']
200 | ratio_carrier = conversion_parameters['Carrier ratio (kg carrier: kg hydrogen)']
201 |
202 | elec_demand = electricity_unit_demand * quantity
203 | heat_demand = heat_unit_demand * quantity
204 | capex_hydrogenation = capex_coefficient * quantity
205 |
206 | # why are daily carrier costs included in net present value calculation?
207 | annual_costs = (capex_hydrogenation+costs_carrier*ratio_carrier*daily_throughput)*CRF(interest, hydrogenation_lifetime)\
208 | + capex_hydrogenation*opex_hydrogenation\
209 | + elec_demand * electricity_costs \
210 | + heat_demand*heat_costs
211 |
212 | return elec_demand, heat_demand, annual_costs
213 |
214 | elif final_state == 'LOHC_unload':
215 |
216 | electricity_unit_demand = conversion_parameters['Electricity demand (kWh per kg H2)']
217 | heat_unit_demand = conversion_parameters['Heat demand (kWh per kg H2)']
218 | capex_coefficient = conversion_parameters['Capex coefficient (euros per kilograms H2 per year)']
219 | opex_dehydrogenation = conversion_parameters['Opex (% of capex)']
220 | dehydrogenation_lifetime = conversion_parameters['Hydrogenation lifetime (a)']
221 |
222 | elec_demand = electricity_unit_demand * quantity
223 | heat_demand = heat_unit_demand * quantity
224 | capex_dehydrogenation = capex_coefficient * quantity
225 |
226 | annual_costs = (capex_dehydrogenation*CRF(interest, dehydrogenation_lifetime))\
227 | + (capex_dehydrogenation*opex_dehydrogenation)\
228 | + elec_demand * electricity_costs\
229 | + heat_demand*heat_costs
230 |
231 | return elec_demand, heat_demand, annual_costs
232 |
233 | elif final_state == 'NH3_load':
234 |
235 | electricity_unit_demand = conversion_parameters['Electricity demand (kWh per kg H2)']
236 | heat_unit_demand = conversion_parameters['Heat demand (kWh per kg H2)']
237 | capex_coefficient = conversion_parameters['Capex coefficient (euros per annual g H2)']
238 | opex_NH3_plant = conversion_parameters['Opex (% of capex)']
239 | NH3_plant_lifetime =conversion_parameters['Plant lifetime (a)']
240 |
241 |
242 | elec_demand = electricity_unit_demand * quantity
243 | heat_demand = heat_unit_demand * quantity
244 | capex_NH3_plant = capex_coefficient * quantity
245 |
246 | annual_costs = capex_NH3_plant*CRF(interest,NH3_plant_lifetime)\
247 | + capex_NH3_plant*opex_NH3_plant\
248 | + elec_demand * electricity_costs\
249 | + heat_demand*heat_costs
250 |
251 | return elec_demand, heat_demand, annual_costs
252 |
253 | elif final_state == 'NH3_unload':
254 |
255 | electricity_unit_demand = conversion_parameters['Electricity demand (kWh per kg H2)']
256 | heat_unit_demand = conversion_parameters['Heat demand (kWh per kg H2)']
257 | capex_coefficient = conversion_parameters['Capex coefficient (euros per hourly g H2)']
258 | opex_NH3_plant = conversion_parameters['Opex (% of capex)']
259 | NH3_plant_lifetime = conversion_parameters['Plant lifetime (a)']
260 |
261 | elec_demand = electricity_unit_demand * quantity
262 | heat_demand = heat_unit_demand * quantity
263 |
264 | capex_NH3_plant = capex_coefficient * ((quantity/1000/365/24) ** 0.7451)
265 |
266 | annual_costs = capex_NH3_plant*CRF(interest,NH3_plant_lifetime) + capex_NH3_plant*opex_NH3_plant \
267 | + elec_demand * electricity_costs + heat_demand*heat_costs
268 |
269 | return elec_demand, heat_demand, annual_costs
270 |
271 | else:
272 | raise NotImplementedError(f'Conversion costs for {final_state} not currently supported.')
273 |
274 | def cheapest_trucking_strategy(final_state, quantity, distance,
275 | elec_costs, heat_costs, interest,
276 | conversion_excel_path, transport_excel_path,
277 | elec_costs_demand, elec_cost_grid = 0.):
278 | '''
279 | calculates the lowest-cost state to transport hydrogen by truck
280 |
281 | Parameters
282 | ----------
283 | final_state : string
284 | final state for hydrogen demand.
285 | quantity : float
286 | annual demand for hydrogen in kg.
287 | distance : float
288 | distance to transport hydrogen.
289 | elec_costs : float
290 | cost per kWh of electricity at hydrogen production site.
291 | heat_costs : float
292 | cost per kWh of heat.
293 | interest : float
294 | interest on conversion and trucking capital investments (not including roads).
295 | conversion_excel_path: string
296 | path to conversion parameters excel sheet.
297 | elec_costs_demand : float
298 | cost per kWh of electricity at hydrogen demand site.
299 | elec_cost_grid : float
300 | grid electricity costs that pipeline compressors pay. Default 0.
301 |
302 | Returns
303 | -------
304 | costs_per_unit : float
305 | storage, conversion, and transport costs for the cheapest trucking option.
306 | cheapest_option : string
307 | the lowest-cost state in which to transport hydrogen by truck.
308 |
309 | '''
310 |
311 | if final_state == '500 bar':
312 | dist_costs_500bar = h2_conversion_stand('500 bar', quantity, elec_costs, heat_costs, interest, conversion_excel_path)[2]\
313 | + trucking_costs('500 bar',distance,quantity,interest,transport_excel_path)
314 | elif final_state == 'NH3':
315 | dist_costs_500bar = h2_conversion_stand('500 bar', quantity, elec_costs, heat_costs, interest, conversion_excel_path)[2]\
316 | + trucking_costs('500 bar',distance,quantity,interest,transport_excel_path)\
317 | + h2_conversion_stand(final_state+'_load', quantity, elec_costs, heat_costs, interest, conversion_excel_path)[2]
318 | else:
319 | dist_costs_500bar = h2_conversion_stand('500 bar', quantity, elec_costs, heat_costs, interest, conversion_excel_path)[2]\
320 | + trucking_costs('500 bar',distance,quantity,interest,transport_excel_path)\
321 | + h2_conversion_stand(final_state, quantity, elec_costs, heat_costs, interest, conversion_excel_path)[2]
322 | if final_state == 'LH2':
323 | dist_costs_lh2 = h2_conversion_stand('LH2', quantity, elec_costs, heat_costs, interest, conversion_excel_path)[2]\
324 | + trucking_costs('LH2',distance, quantity,interest,transport_excel_path)
325 | elif final_state == 'NH3':
326 | dist_costs_lh2 = h2_conversion_stand('500 bar', quantity, elec_costs, heat_costs, interest, conversion_excel_path)[2]\
327 | + trucking_costs('500 bar',distance,quantity,interest,transport_excel_path)\
328 | + h2_conversion_stand(final_state+'_load', quantity, elec_costs, heat_costs, interest, conversion_excel_path)[2]
329 | else:
330 | dist_costs_lh2 = h2_conversion_stand('LH2', quantity, elec_costs, heat_costs, interest, conversion_excel_path)[2]\
331 | + trucking_costs('LH2',distance, quantity,interest,transport_excel_path)\
332 | + h2_conversion_stand(final_state, quantity, elec_costs_demand, heat_costs, interest, conversion_excel_path)[2]
333 | if final_state == 'NH3':
334 | dist_costs_nh3 = h2_conversion_stand('NH3_load', quantity, elec_costs, heat_costs, interest, conversion_excel_path)[2]\
335 | + trucking_costs('NH3',distance, quantity, interest,transport_excel_path)
336 | dist_costs_lohc = h2_conversion_stand('LOHC_load', quantity, elec_costs, heat_costs, interest, conversion_excel_path)[2]\
337 | + trucking_costs('LOHC',distance, quantity, interest,transport_excel_path)\
338 | + h2_conversion_stand('LOHC_unload', quantity, elec_costs_demand, heat_costs, interest, conversion_excel_path)[2]\
339 | + h2_conversion_stand('NH3_load', quantity, elec_costs_demand, heat_costs, interest, conversion_excel_path)[2]
340 | else:
341 | dist_costs_nh3 = h2_conversion_stand('NH3_load', quantity, elec_costs, heat_costs, interest, conversion_excel_path)[2]\
342 | + trucking_costs('NH3',distance, quantity,interest,transport_excel_path)\
343 | + h2_conversion_stand('NH3_unload', quantity, elec_costs_demand, heat_costs, interest, conversion_excel_path)[2]\
344 | + h2_conversion_stand(final_state, quantity, elec_costs_demand, heat_costs, interest, conversion_excel_path)[2]
345 | dist_costs_lohc = h2_conversion_stand('LOHC_load', quantity, elec_costs, heat_costs, interest, conversion_excel_path)[2]\
346 | + trucking_costs('LOHC',distance, quantity,interest,transport_excel_path)\
347 | + h2_conversion_stand('LOHC_unload', quantity, elec_costs_demand, heat_costs, interest, conversion_excel_path)[2]\
348 | + h2_conversion_stand(final_state, quantity, elec_costs_demand, heat_costs, interest, conversion_excel_path)[2]
349 |
350 | lowest_cost = np.nanmin([dist_costs_500bar, dist_costs_lh2, dist_costs_lohc, dist_costs_nh3])
351 |
352 | if dist_costs_500bar == lowest_cost:
353 | cheapest_option = '500 bar'
354 | elif dist_costs_lh2 == lowest_cost:
355 | cheapest_option = 'LH2'
356 | elif dist_costs_lohc == lowest_cost:
357 | cheapest_option = 'LOHC'
358 | elif dist_costs_nh3 == lowest_cost:
359 | cheapest_option = 'NH3'
360 |
361 | costs_per_unit = lowest_cost/quantity
362 |
363 | return costs_per_unit, cheapest_option
364 |
365 |
366 |
367 | def cheapest_pipeline_strategy(final_state, quantity, distance,
368 | elec_costs, heat_costs,interest,
369 | conversion_excel_path,
370 | pipeline_excel_path,
371 | elec_costs_demand,
372 | elec_cost_grid = 0.):
373 | '''
374 | calculates the lowest-cost way to transport hydrogen via pipeline
375 |
376 | Parameters
377 | ----------
378 | final_state : string
379 | final state for hydrogen demand.
380 | quantity : float
381 | annual demand for hydrogen in kg.
382 | distance : float
383 | distance to transport hydrogen.
384 | elec_costs : float
385 | cost per kWh of electricity at hydrogen production site.
386 | heat_costs : float
387 | cost per kWh of heat.
388 | interest : float
389 | interest on pipeline capital investments.
390 | conversion_excel_path: string
391 | path to conversion parameters excel sheet.
392 | elec_costs_demand : float
393 | cost per kWh of electricity at hydrogen demand site.
394 | elec_cost_grid : float
395 | grid electricity costs that pipeline compressors pay. Default 0.
396 |
397 | Returns
398 | -------
399 | costs_per_unit : float
400 | storage, conversion, and transport costs for the cheapest option.
401 | cheapest_option : string
402 | the lowest-cost state in which to transport hydrogen by truck.
403 |
404 | '''
405 |
406 | if final_state == 'NH3':
407 | dist_costs_pipeline = pipeline_costs(distance,quantity,elec_cost_grid, pipeline_excel_path, interest)[0]\
408 | + h2_conversion_stand(final_state+'_load', quantity, elec_costs_demand, heat_costs, interest, conversion_excel_path)[2]
409 | else:
410 | dist_costs_pipeline = pipeline_costs(distance,quantity,elec_cost_grid,pipeline_excel_path,interest)[0]\
411 | + h2_conversion_stand(final_state, quantity, elec_costs_demand, heat_costs, interest, conversion_excel_path)[2]
412 |
413 | costs_per_unit = dist_costs_pipeline/quantity
414 | cheapest_option = pipeline_costs(distance, quantity, elec_cost_grid, pipeline_excel_path, interest)[1]
415 |
416 | return costs_per_unit, cheapest_option
417 |
418 |
419 | #Only new pipelines
420 | def pipeline_costs(distance, quantity, elec_cost, pipeline_excel_path, interest):
421 | '''
422 | calculates the annualized cost of building a pipeline.
423 |
424 | Parameters
425 | ----------
426 | distance : float
427 | distance from production site to demand site in km.
428 | quantity : float
429 | annual quantity of hydrogen demanded in kg.
430 | elec_cost : float
431 | price of electricity along pipeline in euros.
432 | pipeline_excel_path: string
433 | path to conversion parameters excel sheet.
434 | interest : float
435 | interest rate on capital investments.
436 |
437 | Returns
438 | -------
439 | float
440 | annual costs for pipeline.
441 | string
442 | size of pipeline to build
443 |
444 | '''
445 | all_parameters = pd.read_excel(pipeline_excel_path,
446 | sheet_name='All',
447 | index_col = 'Parameter'
448 | ).squeeze('columns')
449 | opex = all_parameters['Opex (% of capex)']
450 | availability = all_parameters['Availability']
451 | lifetime_pipeline = all_parameters['Pipeline lifetime (a)']
452 | lifetime_compressors = all_parameters['Compressor lifetime (a)']
453 | electricity_demand = all_parameters['Electricity demand (kWh/kg*km)']
454 | max_capacity_big = all_parameters['Large pipeline max capacity (GW)']
455 | max_capacity_med = all_parameters['Medium pipeline max capacity (GW)']
456 | max_capacity_sml = all_parameters['Small pipeline max capcity (GW)']
457 |
458 | max_throughput_big = (((max_capacity_big*(10**6))/33.333))*8760*availability
459 | max_throughput_med = (((max_capacity_med*(10**6))/33.333))*8760*availability
460 | max_throughput_sml = (((max_capacity_sml*(10**6))/33.333))*8760*availability
461 |
462 | if quantity <= max_throughput_sml:
463 | pipeline_type = 'Small'
464 |
465 | elif quantity > max_throughput_sml and quantity <= max_throughput_med:
466 | pipeline_type = 'Medium'
467 |
468 | elif quantity > max_throughput_med and quantity <= max_throughput_big:
469 | pipeline_type = 'Large'
470 |
471 | else:
472 | return np.nan,'No Pipeline big enough'
473 |
474 | pipeline_parameters = pd.read_excel(pipeline_excel_path,
475 | sheet_name=pipeline_type,
476 | index_col = 'Parameter'
477 | ).squeeze('columns')
478 | capex_pipeline = pipeline_parameters['Pipeline capex (euros)']
479 | capex_compressor = pipeline_parameters['Compressor capex (euros)']
480 |
481 | capex_annual = ((capex_pipeline*distance)*CRF(interest,lifetime_pipeline))\
482 | + ((capex_compressor*distance)*CRF(interest,lifetime_compressors))
483 | opex_annual = opex*(capex_pipeline+capex_compressor)*distance
484 | electricity_costs = electricity_demand * distance * quantity * elec_cost
485 |
486 | annual_costs = capex_annual + opex_annual + electricity_costs
487 |
488 | return annual_costs, f"{pipeline_type} Pipeline"
489 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | >[!NOTE]
2 | >We now have a new repository for you to use! The new [GeoX](https://github.com/ClimateCompatibleGrowth/GeoX.git) repository contains both this repository and the [GeoNH3](https://github.com/ClimateCompatibleGrowth/GeoNH3.git) repository.
3 |
4 |
5 | # GEOH2
6 | **Geospatial analysis of hydrogen production costs**
7 |
8 | GEOH2 calculates the locational cost of green hydrogen production, storage, transport, and conversion to meet demand in a specified location. These costs can be compared to current or projected prices for energy and chemical feedstocks in the region to assess the competitiveness of green hydrogen. Currently, different end-uses, such as fertilizer production, export shipping, and steel production, are not modeled.
9 |
10 | The model outputs the levelized cost of hydrogen (LCOH) at the demand location including production, storage, transport, and conversion costs.
11 |
12 | In the code provided, the specific use case of Namibia is investigated.
13 | Parameter references for this case are attached.
14 | However, as the code is written in a generalized way, it is possible to analyse all sorts of regions.
15 |
16 | GeoH2 builds upon a preliminary code iteration produced by Leander Müller, available under a CC-BY-4.0 licence: [https://github.com/leandermue/GEOH2](https://github.com/leandermue/GEOH2).
17 | It also integrates code produced by Nick Salmon under an MIT licence:
18 | [https://github.com/nsalmon11/LCOH_Optimisation](https://github.com/nsalmon11/LCOH_Optimisation)
19 | ___
20 |
21 | # Setup instructions
22 |
23 | ## Clone the repository
24 | First, clone the GeoH2 repository using `git`.
25 |
26 | `... % git clone https://github.com/ClimateCompatibleGrowth/GeoH2.git`
27 |
28 | ## Environment setup
29 | The python package requirements are in the `environment.yaml` file. You can install these requirements in a new environment using `mamba` package and environment manager (installation instructions [here](https://mamba.readthedocs.io/en/latest/installation/mamba-installation.html)):
30 |
31 | ` .../GEOH2 % mamba env create -f environment.yaml`
32 |
33 | Then activate this new environment using
34 |
35 | `.../GEOH2 % mamba activate geoh2`
36 |
37 | ## CDS API setup
38 | The `get_weather_data` rule downloads the relevant historical weather data from the ERA-5 reanalysis dataset using [Atlite](https://atlite.readthedocs.io/en/latest/) to create a cutout. For this process to work, you need to register and set up your CDS API key as described on the [Climate Data Store website](https://cds.climate.copernicus.eu/api-how-to).
39 |
40 | **Note:** Ensure the API key and URL are affiliated with CDS-Beta.
41 |
42 | ## Solver setup
43 | For the `optimize_hydrogen_plant` rule to work, you will need a solver installed on your computer. You can use any solver that works with [PyPSA](https://pypsa.readthedocs.io/en/latest/installation.html), such as [Cbc](https://github.com/coin-or/Cbc), a free, open-source solver, or [Gurobi](https://www.gurobi.com/), a commerical solver with free academic licenses available. Install your solver of choice following the instructions for use with Python and your operating system in the solver's documentation.
44 |
45 | In `Scripts/optimize_hydrogen_plant.py` line 160, the solver is set to `gurobi`. This must be changed if you choose to use a different solver.
46 |
47 | **Note**: Snakemake uses Cbc, which will be installed upon environment setup. To check, activate your environment and enter `mamba list` in your terminal for the environment's list of packages.
48 | ___
49 |
50 | # Preparing input data
51 |
52 | ## Hexagons
53 | To analyse a different area of interest, the input hexagon file needs to be changed, but needs to follow the logic of the one provided.
54 |
55 | A full walkthrough on all the tools to create these hexagons are in the [GeoH2-data-prep](https://github.com/ClimateCompatibleGrowth/GeoH2-data-prep) repo.
56 |
57 | An explanation of how to create a H3-Hexagon file can be found in the following repo:
58 |
59 | https://github.com/carderne/ccg-spider
60 |
61 | The hexagon file needs to filled with the following attributes:
62 |
63 | - waterbody_dist: Distance to selected waterbodies in area of interest
64 | - waterway_dist: Distance to selected waterways in area of interest
65 | - ocean_dist: Distance to ocean coastline
66 | - grid_dist: Distance to transmission network
67 | - road_dist: Distance to road network
68 | - theo_pv: Theoretical PV potential --> Possible to investigate with: https://github.com/FZJ-IEK3-VSA/glaes. Note that this value should be in MW.
69 | - theo_wind: Theoretical wind turbine potential --> Possible to investigate with: https://github.com/FZJ-IEK3-VSA/glaes. Note that this value should be in MW.
70 |
71 | Once you have created a hexagon file with these features, save it in the `Data` folder as `hex_final_[COUNTRY ISO CODE].geojson`.
72 |
73 | **Note:** `COUNTRY ISO CODE` is the country's ISO standard 2-letter abbreviation.
74 |
75 | ## Input parameter Excel files
76 |
77 | Required input parameters include the spatial area of interest, total annual demand for hydrogen, and prices and cost of capital for infrastructure investments. These values can be either current values or projected values for a single snapshot in time. The parameter values for running the model can be specified in a set of Excel files in the Parameters folder.
78 |
79 | - **Basic H2 plant:** In this folder, there are several csv files containing the global parameters for optimizing the plant design. All power units are MW and all energy units are MWh. For more information on these parameters, refer to the [PyPSA documentation](https://pypsa.readthedocs.io/en/latest/components.html).
80 |
81 | **Note:** The excel files must be kept in a folder with the title matching the Country ISO Code. From the use case of Namibia, we have them in a folder titled "NA"
82 |
83 | - **Conversion parameters:** `conversion_parameters.xlsx` includes parameters related to converting between states of hydrogen.
84 |
85 | - **Country parameters:** `country_parameters.xlsx` includes country- and technology-specific interest rates, heat and electricity costs, and asset lifetimes.
86 | - Interest rates should be expressed as a decimal, e.g. 5% as 0.05.
87 | - Asset lifetimes should be in years.
88 |
89 | - **Demand parameters:** `demand_parameters.xlsx` includes a list of demand centers. For each demand center, its lat-lon location, annual demand, and hydrogen state for that demand must be specified. If multiple forms of hydrogen are demanded in one location, differentiate the demand center name (e.g. Nairobi LH2 and Nairobi NH3) to avoid problems from duplicate demand center names.
90 |
91 | - **Pipeline parameters:** `pipeline_parameters.xlsx` includes the price, capacity, and lifetime data for different sizes of hydrogen pipeline.
92 |
93 | - **Technology parameters:** ` technology_parameters.xlsx` includes water parameters, road infrastructure parameters, and whether road and hydrogen pipeline construction is allowed.
94 |
95 | - **Transport parameters:** `transport_parameters.xlsx` includes the parameters related to road transport of hydrogen, including truck speed, cost, lifetime, and capacity.
96 | ___
97 |
98 | # Snakemake
99 |
100 | This repository uses [Snakemake](https://snakemake.readthedocs.io/en/stable/) to automate its workflow (for a gentle introduction to Snakemake, see [Getting Started with Snakemake](https://carpentries-incubator.github.io/workflows-snakemake/) on The Carpentries Incubator).
101 |
102 | ## Wildcards
103 | Wildcards specify the data used in the workflow. This workflow uses two wildcards: `country` (an ISO standard 2-letter abbreviation) and `weather_year` (a 4-digit year between 1940 and 2023 included in the ERA5 dataset).
104 |
105 | ## Config file
106 |
107 | High-level workflow settings are controlled in the config file: `config.yaml`.
108 |
109 | Multiple wildcard values are specified in the `scenario` section. These can be changed to match the `country` and `weather_year` you are analysing.
110 |
111 | Renewable generators considered for hydrogen plant construction are included in the `generators` section.
112 |
113 | In the `transport` section, `pipeline_construction` and `road_construction` can be switched from `True` to `False`, as needed.
114 |
115 | **Note:** `country` and `weather_year` can be a list of more than one, depending on how many countries and years you are analysing. You must ensure all other files that need for each country run are where they should be.
116 |
117 | ## Rules
118 |
119 | Rules can be run multiple ways using Snakemake. Below, you will be able to run rules by entering the rule name or their output in the terminal. Snakemake will run all necessary rules and their corresponding scripts to create an output. While all rules are discussed here for completeness, **you do not need to enter each rule one-by-one and can simply enter the output you're interested in or one of the run all rules.** Rules are defined in the `Snakefile`.
120 |
121 | Snakemake requires a specification of the `number of cores to be used`; this can be up to 4.
122 |
123 | ### Run time
124 |
125 | The `get_weather_data` rule, depending on country size and your internet connection, could take from a few minutes to several hours to run. Ensure that you have space on your computer to store the data, which can be several GB.
126 |
127 | The `optimize_hydrogen_plant` rule, depending on country size and the number of demand centers, could take from several minutes to several hours to run.
128 |
129 | The `optimize_transport_and_conversion` rule, depending on country size, should take a few minutes to run.
130 |
131 | All other rules take a few seconds to run.
132 |
133 | ### Rule to remove all files
134 |
135 | **Note:** This rule does not work on Windows, as of yet. Please manually remove the files you need to.
136 |
137 | This rule is important to know first, as it will remove all the files that the below rules will create as well as the file you initially saved into the `Data` folder as `hex_final_[COUNTRY ISO CODE].geojson`.
138 |
139 | This is to allow for a quicker transition to analyse more data and to clear up space. Make sure you save the created files that you need elsewhere before running the following rule into the terminal:
140 | ```
141 | snakemake -j [NUMBER OF CORES TO BE USED] clean
142 | ```
143 |
144 | ### Run optimisation and mapping rules
145 |
146 | This section can be used to run most rules, without having to run exact output files. If any files are changed after a completed run, the same command can be used again and Snakemake will only run the necessary scripts to ensure the results are up to date.
147 |
148 | The total hydrogen cost for all scenarios can be run by entering the following rule into the terminal (make sure you have the necessary weather file in the Cutouts folder before running, you might have to run the get_weather_data rule):
149 | ```
150 | snakemake -j [NUMBER OF CORES TO BE USED] optimise_all
151 | ```
152 | Similarly, you can map hydrogen costs for all scenarios with the following rule:
153 | ```
154 | snakemake -j [NUMBER OF CORES TO BE USED] map_all
155 | ```
156 |
157 | ### `assign_country` rule
158 |
159 | Assign country-specific interest rates, technology lifetimes, and heat and electricity prices from `country_parameters.xlsx` to different hexagons based on their country.
160 |
161 | You can run this rule by entering the following command in your terminal:
162 | ```
163 | snakemake -j [NUMBER OF CORES TO BE USED] Data/hexagons_with_country_[COUNTRY ISO CODE].geojson
164 | ```
165 |
166 | ### `get_weather_data` rule
167 |
168 | **Note:** This rule will also create the assign_country rule's output, as it uses that file.
169 | You can run this rule by entering the following command in your terminal:
170 | ```
171 | snakemake -j [NUMBER OF CORES TO BE USED] Cutouts/[COUNTRY ISO CODE]_[WEATHER YEAR].nc
172 | ```
173 |
174 | ### `optimize_transport_and_conversion` rule
175 |
176 | Calculate the cost of the optimal hydrogen transportation and conversion strategy from each hexagon to each demand center, using both pipelines and road transport, using parameters from `technology_parameters.xlsx`, `demand_parameters.xlsx`, and `country_parameters.xlsx`.
177 |
178 | You can run this rule by entering the following command in your terminal:
179 | ```
180 | snakemake -j [NUMBER OF CORES TO BE USED] Resources/hex_transport_[COUNTRY ISO CODE].geojson
181 | ```
182 |
183 | ### `calculate_water_costs` rule
184 |
185 | Calculate water costs from the ocean and freshwater bodies for hydrogen production in each hexagon using `Parameters/technology_parameters.xlsx` and `Parameters/country_parameters.xlsx`.
186 |
187 | You can run this rule by entering the following command in your terminal:
188 | ```
189 | snakemake -j [NUMBER OF CORES TO BE USED] Resources/hex_water_[COUNTRY ISO CODE].geojson
190 | ```
191 |
192 | ### `optimize_hydrogen_plant` rule
193 |
194 | Design green hydrogen plant to meet the hydrogen demand profile for each demand center for each transportation method to each demand center using the `optimize_hydrogen_plant.py` script. Ensure that you have specified your hydrogen plant parameters in the CSV files in the `Parameters/Basic_H2_plant` folder, your investment parameters in `Parameters/investment_parameters.xlsx`, and your demand centers in `Parameters/demand_parameters.xlsx`.
195 |
196 | You can run this rule by entering the following command in your terminal:
197 | ```
198 | snakemake -j [NUMBER OF CORES TO BE USED] Resources/hex_lcoh_[COUNTRY ISO CODE]_[WEATHER YEAR].geojson
199 | ```
200 |
201 | ### `calculate_total_hydrogen_cost` rule
202 | Combine results to find the lowest-cost method of producing, transporting, and converting hydrogen for each demand center.
203 |
204 | You can run this rule by entering the following command in your terminal:
205 | ```
206 | snakemake -j [NUMBER OF CORES TO BE USED] Results/hex_total_cost_[COUNTRY ISO CODE]_[WEATHER YEAR].geojson
207 | ```
208 |
209 | ### `calculate_cost_components` rule
210 |
211 | Calculate the cost for each type of equipment in each polygon.
212 |
213 | You can run this rule by entering the following command in your terminal:
214 | ```
215 | snakemake -j [NUMBER OF CORES TO BE USED] Results/hex_cost_components_[COUNTRY ISO CODE]_[WEATHER YEAR].geojson
216 | ```
217 |
218 | ### `map_costs` rule
219 |
220 | Visualize the spatial variation in different costs per kilogram of hydrogen.
221 |
222 | You can run this rule by entering the following command in your terminal:
223 | ```
224 | snakemake -j [NUMBER OF CORES TO BE USED] Plots/[COUNTRY ISO CODE]_[WEATHER YEAR]
225 | ```
226 | ___
227 |
228 | # Limitations
229 |
230 | This model considers only greenfield wind and solar plants for hydrogen production. Therefore it does not consider using grid electricity or existing generation for hydrogen production. The model further assumes that all excess electricity is curtailed.
231 |
232 | While the design of the green hydrogen plant is convex and therefore guarenteed to find the global optimum solution if it exists, the selection of the trucking strategy is greedy to avoid the long computation times and potential computational intractability associated with a mixed-integer optimization problem.
233 |
234 | Currently, only land transport is considered in the model. To calculate the cost of hydrogen production for export, any additional costs for conversion and transport via ship or undersea pipeline must be added in post-processing.
235 |
236 | Transport costs are calculated from the center of the hexagon to the demand center. When using large hexagon sizes, this assumption may over- or underestimate transportation costs significantly. Additionally, only path length is considered when calculating the cost of road and pipeline construction. Additional costs due to terrain are not considered.
237 |
238 | The availability of water for electrolysis is not limited in regions that could potentially face drought, and a single prices for freshwater and ocean water are used throughout the modeled area.
239 | ___
240 |
241 | # Citation
242 |
243 | If you decide to use GeoH2, please kindly cite us using the following:
244 |
245 | *Halloran, C., Leonard, A., Salmon, N., Müller, L., & Hirmer, S. (2024).
246 | GeoH2 model: Geospatial cost optimization of green hydrogen production including storage and transportation.
247 | MethodsX, 12, 102660. https://doi.org/10.1016/j.mex.2024.102660.*
248 |
249 | ```commandline
250 | @article{Halloran_GeoH2_model_Geospatial_2024,
251 | author = {Halloran, Claire and Leonard, Alycia and Salmon, Nicholas and Müller, Leander and Hirmer, Stephanie},
252 | doi = {10.1016/j.mex.2024.102660},
253 | journal = {MethodsX},
254 | month = jun,
255 | pages = {102660},
256 | title = {{GeoH2 model: Geospatial cost optimization of green hydrogen production including storage and transportation}},
257 | volume = {12},
258 | year = {2024}
259 | }
260 | ```
261 | ___
262 |
263 | # Case study parameters
264 |
265 | This repository includes sample parameters for a hydrogen production case in Namibia.
266 | References for these parameters are included in the tables below for reference.
267 | For the results of this case, please refer to the model MethodsX article: https://doi.org/10.1016/j.mex.2024.102660.
268 |
269 | **Green hydrogen plant parameters:**
270 |
271 | | Hardware | Parameter | Value | Units | Ref. |
272 | |----------------------------|-----------------------|-----------|----------------------|---------------------------------------------------------------------------------------------------------------------------------|
273 | | Solar photovoltaic | Capex | 1,470,000 | €/MW | [Allington et al., 2021](https://doi.org/10.1016/j.dib.2022.108021) |
274 | | Wind turbines | Capex | 1,580,000 | €/MW | [Allington et al., 2021](https://doi.org/10.1016/j.dib.2022.108021) |
275 | | Hydrogen electrolysis | Capex | 1,250,000 | €/MW | [Müller et al., 2022](https://doi.org/10.1016/j.apenergy.2023.121219) |
276 | | Hydrogen electrolysis | Efficiency | 0.59 | MWh H2/MWh el | [Taibi et al., 2020](https://www.irena.org/-/media/Files/IRENA/Agency/Publication/2020/Dec/IRENA_Green_hydrogen_cost_2020.pdf) |
277 | | Hydrogen compression | Isentropic efficiency | 0.051 | MWh el/MWh H2 | [Müller et al., 2022](https://doi.org/10.1016/j.apenergy.2023.121219) |
278 | | Hydrogen storage unloading | Efficiency | 1 | MWh H2/MWh H2-stored | Assumption |
279 | | Battery | Capex | 95,000 | €/MW | [BloombergNEF, 2022](https://about.bnef.com/blog/lithium-ion-battery-pack-prices-rise-for-first-time-to-an-average-of-151-kwh/) |
280 | | Hydrogen storage | Capex | 21,700 | €/MWh | [Müller et al., 2022](https://doi.org/10.1016/j.apenergy.2023.121219) |
281 |
282 | **Conversion parameters:**
283 |
284 | | Process | Parameter | Value | Units | Ref. |
285 | |------------------------|------------------------------|-------------|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------|
286 | | 500 bar compression | Heat capacity | 0.0039444 | kWh/kg/K | Kurzweil and Dietlmeier, 2016 |
287 | | 500 bar compression | Input temperature | 298.15 | K | [Müller et al., 2022](https://doi.org/10.1016/j.apenergy.2023.121219) |
288 | | 500 bar compression | Input pressure | 25 | bar | [Müller et al., 2022](https://doi.org/10.1016/j.apenergy.2023.121219) |
289 | | 500 bar compression | Isentropic exponent | 1.402 | | Kurzweil and Dietlmeier, 2016 |
290 | | 500 bar compression | Isentropic efficiency | 0.8 | | [Müller et al., 2022](https://doi.org/10.1016/j.apenergy.2023.121219) |
291 | | 500 bar compression | Compressor lifetime | 15 | years | [Cerniauskas, 2021](https://juser.fz-juelich.de/record/906356) |
292 | | 500 bar compression | Compressor capex coefficient | 40,035 | €/kg H2/day | [Cerniauskas, 2021](https://juser.fz-juelich.de/record/906356) |
293 | | 500 bar compression | Compressor opex | 4 | % capex/year | [Cerniauskas, 2021](https://juser.fz-juelich.de/record/906356) |
294 | | Hydrogen liquification | Electricity demand | 9.93 | kWh/kg H2 | [Ausfelder and Dura](https://dechema.de/dechema_media/Downloads/Positionspapiere/2021_DEC_P2X_III_Technischer_Anhang.pdf) |
295 | | Hydrogen liquification | Capex quadratic coefficient | -0.0002 | €/(kg H2)^2 | [Müller et al., 2022](https://doi.org/10.1016/j.apenergy.2023.121219) |
296 | | Hydrogen liquification | Capex linear coefficient | 1,781.9 | €/kg H2 | [Müller et al., 2022](https://doi.org/10.1016/j.apenergy.2023.121219) |
297 | | Hydrogen liquification | Capex constant | 300,000,000 | € | [Müller et al., 2022](https://doi.org/10.1016/j.apenergy.2023.121219) |
298 | | Hydrogen liquification | Opex | 8 | % capex/year | [Cerniauskas, 2021](https://juser.fz-juelich.de/record/906356) |
299 | | Hydrogen liquification | Plant lifetime | 20 | years | [Cerniauskas, 2021](https://juser.fz-juelich.de/record/906356) |
300 | | LOHC hydrogenation | Electricity demand | 0.35 | kWh/kg H2 | [Andersson and Grönkvist, 2019](https://doi.org/10.1016/j.ijhydene.2019.03.063) |
301 | | LOHC hydrogenation | Heat demand | -9 | kWh/kg H2 | [Hydrogenious, 2022](https://hydrogenious.net/how/) |
302 | | LOHC hydrogenation | Capex coefficient | 0.84 | kWh/kg H2/year | [IEA, 2020](https://iea.blob.core.windows.net/assets/29b027e5-fefc-47df-aed0-456b1bb38844/IEA-The-Future-of-Hydrogen-Assumptions-Annex_CORR.pdf) |
303 | | LOHC hydrogenation | Opex | 4 | % capex/year | [IEA, 2020](https://iea.blob.core.windows.net/assets/29b027e5-fefc-47df-aed0-456b1bb38844/IEA-The-Future-of-Hydrogen-Assumptions-Annex_CORR.pdf) |
304 | | LOHC hydrogenation | Plant lifetime | 25 | years | [IEA, 2020](https://iea.blob.core.windows.net/assets/29b027e5-fefc-47df-aed0-456b1bb38844/IEA-The-Future-of-Hydrogen-Assumptions-Annex_CORR.pdf) |
305 | | LOHC hydrogenation | Carrier costs | 2 | €/kg carrier | [Clark, 2020](https://hydrogenious.net/lohc-global-hydrogen-opportunity/) |
306 | | LOHC hydrogenation | Carrier ratio | 16.1 | kg carrier/kg H2 | [Arlt and Obermeier, 2017](https://www.encn.de/fileadmin/user_upload/Studie_Wasserstoff_und_Schwerlastverkehr_WEB.pdf) |
307 | | LOHC dehydrogenation | Electricity demand | 0.35 | kWh/kg H2 | [Andersson and Grönkvist, 2019](https://doi.org/10.1016/j.ijhydene.2019.03.063) |
308 | | LOHC dehydrogenation | Heat demand | 12 | kWh/kg H2 | [Hydrogenious, 2022](https://hydrogenious.net/how/) |
309 | | LOHC dehydrogenation | Capex coefficient | 2.46 | kWh/kg H2 | [IEA, 2020](https://iea.blob.core.windows.net/assets/29b027e5-fefc-47df-aed0-456b1bb38844/IEA-The-Future-of-Hydrogen-Assumptions-Annex_CORR.pdf) |
310 | | LOHC dehydrogenation | Opex | 4 | % capex/year | [IEA, 2020](https://iea.blob.core.windows.net/assets/29b027e5-fefc-47df-aed0-456b1bb38844/IEA-The-Future-of-Hydrogen-Assumptions-Annex_CORR.pdf) |
311 | | LOHC dehydrogenation | Plant lifetime | 25 | years | [IEA, 2020](https://iea.blob.core.windows.net/assets/29b027e5-fefc-47df-aed0-456b1bb38844/IEA-The-Future-of-Hydrogen-Assumptions-Annex_CORR.pdf) |
312 | | Ammonia synthesis | Electricity demand | 2.809 | kWh/kg H2 | [IEA, 2021](https://www.iea.org/reports/ammonia-technology-roadmap) |
313 | | Ammonia synthesis | Capex coefficient | 0.75717 | kWh/g H2/year | [IEA, 2021](https://www.iea.org/reports/ammonia-technology-roadmap) |
314 | | Ammonia synthesis | Opex | 1.5 | % capex/year | [IEA, 2020](https://iea.blob.core.windows.net/assets/29b027e5-fefc-47df-aed0-456b1bb38844/IEA-The-Future-of-Hydrogen-Assumptions-Annex_CORR.pdf) |
315 | | Ammonia synthesis | Plant lifetime | 25 | years | [IEA, 2020](https://iea.blob.core.windows.net/assets/29b027e5-fefc-47df-aed0-456b1bb38844/IEA-The-Future-of-Hydrogen-Assumptions-Annex_CORR.pdf) |
316 | | Ammonia cracking | Heat demand | 4.2 | kWh/kg H2 | [Andersson and Grönkvist, 2019](https://doi.org/10.1016/j.ijhydene.2019.03.063) |
317 | | Ammonia cracking | Capex coefficient | 17,262,450 | kWh/g H2/hour | [Cesaro et al., 2021](https://doi.org/10.1016/j.apenergy.2020.116009) |
318 | | Ammonia cracking | Opex | 2 | % capex/year | [Müller et al., 2022](https://doi.org/10.1016/j.apenergy.2023.121219) |
319 | | Ammonia cracking | Plant lifetime | 25 | years | [Müller et al., 2022](https://doi.org/10.1016/j.apenergy.2023.121219) |
320 |
321 | **Trucking parameters:**
322 |
323 | | Hardware | Parameter | Value | Units | Ref. |
324 | |--------------------------|----------------------------|---------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------|
325 | | All trucks | Average truck speed | 70 | km/h | Assumption |
326 | | All trucks | Working hours | 24 | h/day | Assumption |
327 | | All trucks | Diesel price | 1.5 | €/L | Assumption |
328 | | All trucks | Driver wage | 2.85 | €/h | [Müller et al., 2022](https://doi.org/10.1016/j.apenergy.2023.121219) |
329 | | All trucks | Working days | 365 | days/year | Assumption |
330 | | All trucks | Max driving distance | 160,000 | km/year | [Müller et al., 2022](https://doi.org/10.1016/j.apenergy.2023.121219) |
331 | | All trucks | Truck capex | 160,000 | € | [Reuss et al., 2017](https://doi.org/10.1016/j.apenergy.2017.05.050) |
332 | | All trucks | Truck Opex | 12 | % capex/year | [Reuss et al., 2017](https://doi.org/10.1016/j.apenergy.2017.05.050) |
333 | | All trucks | Diesel consumption | 35 | L/100 km | [Reuss et al., 2017](https://doi.org/10.1016/j.apenergy.2017.05.050) |
334 | | All trucks | Truck lifetime | 8 | years | [Reuss et al., 2017](https://doi.org/10.1016/j.apenergy.2017.05.050) |
335 | | All trucks | Trailer lifetime | 12 | years | [Reuss et al., 2017](https://doi.org/10.1016/j.apenergy.2017.05.050) |
336 | | 500 bar hydrogen trailer | Trailer capex | 660,000 | € | [Cerniauskas, 2021](https://juser.fz-juelich.de/record/906356) |
337 | | 500 bar hydrogen trailer | Trailer opex | 2 | % capex/year | [Cerniauskas, 2021](https://juser.fz-juelich.de/record/906356) |
338 | | 500 bar hydrogen trailer | Trailer capacity | 1,100 | kg H2 | [Cerniauskas, 2021](https://juser.fz-juelich.de/record/906356) |
339 | | 500 bar hydrogen trailer | Loading and unloading time | 1.5 | hours | [Cerniauskas, 2021](https://juser.fz-juelich.de/record/906356) |
340 | | Liquid hydrogen trailer | Trailer capex | 860,000 | € | [Reuss et al., 2017](https://doi.org/10.1016/j.apenergy.2017.05.050) |
341 | | Liquid hydrogen trailer | Trailer opex | 2 | % capex/year | [Reuss et al., 2017](https://doi.org/10.1016/j.apenergy.2017.05.050) |
342 | | Liquid hydrogen trailer | Trailer capacity | 4,300 | kg H2 | [Reuss et al., 2017](https://doi.org/10.1016/j.apenergy.2017.05.050) |
343 | | Liquid hydrogen trailer | Loading and unloading time | 3 | hours | [Reuss et al., 2017](https://doi.org/10.1016/j.apenergy.2017.05.050) |
344 | | LOHC trailer | Trailer capex | 660,000 | € | [IEA, 2020](https://iea.blob.core.windows.net/assets/29b027e5-fefc-47df-aed0-456b1bb38844/IEA-The-Future-of-Hydrogen-Assumptions-Annex_CORR.pdf) |
345 | | LOHC trailer | Trailer opex | 2 | % capex/year | [Reuss et al., 2017](https://doi.org/10.1016/j.apenergy.2017.05.050) |
346 | | LOHC trailer | Trailer capacity | 1,800 | kg H2 | [Reuss et al., 2017](https://doi.org/10.1016/j.apenergy.2017.05.050) |
347 | | LOHC trailer | Loading and unloading time | 1.5 | hours | [Reuss et al., 2017](https://doi.org/10.1016/j.apenergy.2017.05.050) |
348 | | Ammonia trailer | Trailer capex | 210,000 | € | [IEA, 2020](https://iea.blob.core.windows.net/assets/29b027e5-fefc-47df-aed0-456b1bb38844/IEA-The-Future-of-Hydrogen-Assumptions-Annex_CORR.pdf) |
349 | | Ammonia trailer | Trailer opex | 2 | % capex/year | [IEA, 2020](https://iea.blob.core.windows.net/assets/29b027e5-fefc-47df-aed0-456b1bb38844/IEA-The-Future-of-Hydrogen-Assumptions-Annex_CORR.pdf) |
350 | | Ammonia trailer | Trailer capacity | 2,600 | kg H2 | [IEA, 2020](https://iea.blob.core.windows.net/assets/29b027e5-fefc-47df-aed0-456b1bb38844/IEA-The-Future-of-Hydrogen-Assumptions-Annex_CORR.pdf) |
351 | | Ammonia trailer | Loading and unloading time | 1.5 | hours | [IEA, 2020](https://iea.blob.core.windows.net/assets/29b027e5-fefc-47df-aed0-456b1bb38844/IEA-The-Future-of-Hydrogen-Assumptions-Annex_CORR.pdf) |
352 |
353 | **Road parameters:**
354 |
355 | | Road length | Parameter | Value | Units | Ref. |
356 | |---------------------|-----------|------------|-----------|-----------------------------------------------------------------------|
357 | | Short road (<10 km) | Capex | 626,478.45 | €/km | [Müller et al., 2022](https://doi.org/10.1016/j.apenergy.2023.121219) |
358 | | Long road (>10 km) | Capex | 481,866.6 | €/km | [Müller et al., 2022](https://doi.org/10.1016/j.apenergy.2023.121219) |
359 | | All roads | Opex | 7,149.7 | €/km/year | [Müller et al., 2022](https://doi.org/10.1016/j.apenergy.2023.121219) |
360 |
361 | **Pipeline parameters:**
362 |
363 | | Pipeline size | Parameter | Value | Units | Ref. |
364 | |-----------------|---------------------|-----------|--------------|--------------------------------------------------------------------------------------------------|
365 | | All pipelines | Opex | 1.25 | % capex/year | [Jens et al., 2021](https://ehb.eu/files/downloads/European-Hydrogen-Backbone-April-2021-V3.pdf) |
366 | | All pipelines | Availability | 95 | % | [Müller et al., 2022](https://doi.org/10.1016/j.apenergy.2023.121219) |
367 | | All pipelines | Pipeline lifetime | 42.5 | years | [Jens et al., 2021](https://ehb.eu/files/downloads/European-Hydrogen-Backbone-April-2021-V3.pdf) |
368 | | All pipelines | Compressor lifetime | 24 | years | [Jens et al., 2021](https://ehb.eu/files/downloads/European-Hydrogen-Backbone-April-2021-V3.pdf) |
369 | | All pipelines | Electricity demand | 0.000614 | kWh/kg H2/km | [Jens et al., 2021](https://ehb.eu/files/downloads/European-Hydrogen-Backbone-April-2021-V3.pdf) |
370 | | Large pipeline | Maximum capacity | 13 | GW | [Jens et al., 2021](https://ehb.eu/files/downloads/European-Hydrogen-Backbone-April-2021-V3.pdf) |
371 | | Large pipeline | Pipeline capex | 2,800,000 | €/km | [Jens et al., 2021](https://ehb.eu/files/downloads/European-Hydrogen-Backbone-April-2021-V3.pdf) |
372 | | Large pipeline | Compressor capex | 620,000 | €/km | [Jens et al., 2021](https://ehb.eu/files/downloads/European-Hydrogen-Backbone-April-2021-V3.pdf) |
373 | | Medium pipeline | Maximum capacity | 4.7 | GW | [Jens et al., 2021](https://ehb.eu/files/downloads/European-Hydrogen-Backbone-April-2021-V3.pdf) |
374 | | Medium pipeline | Pipeline capex | 2,200,000 | €/km | [Jens et al., 2021](https://ehb.eu/files/downloads/European-Hydrogen-Backbone-April-2021-V3.pdf) |
375 | | Medium pipeline | Compressor capex | 310,000 | €/km | [Jens et al., 2021](https://ehb.eu/files/downloads/European-Hydrogen-Backbone-April-2021-V3.pdf) |
376 | | Small pipeline | Maximum capacity | 1.2 | GW | [Jens et al., 2021](https://ehb.eu/files/downloads/European-Hydrogen-Backbone-April-2021-V3.pdf) |
377 | | Small pipeline | Pipeline capex | 90,000 | €/km | [Jens et al., 2021](https://ehb.eu/files/downloads/European-Hydrogen-Backbone-April-2021-V3.pdf) |
378 | | Small pipeline | Compressor capex | 90,000 | €/km | [Jens et al., 2021](https://ehb.eu/files/downloads/European-Hydrogen-Backbone-April-2021-V3.pdf) |
379 |
380 | **Water parameters:**
381 |
382 | | Type | Parameter | Value | Units | Ref. |
383 | |-------------|------------------------------|-------|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
384 | | Freshwater | Treatment electricity demand | 0.4 | kWh/m^3 water | [US Dept. of Energy, 2016](https://betterbuildingssolutioncenter.energy.gov/sites/default/files/Primer%20on%20energy%20efficiency%20in%20water%20and%20wastewater%20plants_0.pdf) |
385 | | Ocean water | Treatment electricity demand | 3.7 | kWh/m^3 water | [Patterson et al., 2019](https://doi.org/10.1073/pnas.1902335116) |
386 | | All water | Transport cost | 0.1 | €/100 km/m^3 water | [Zhou and Tol, 2005](https://doi.org/10.1029/2004WR003749) |
387 | | All water | Water specific cost | 1.25 | €/m^3 water | [Wasreb, 2019](https://wasreb.go.ke/wasrebsystems/tariffs/about-us.html) |
388 | | All water | Water demand | 21 | L water/kg H2 | [Taibi et al., 2020](https://www.irena.org/-/media/Files/IRENA/Agency/Publication/2020/Dec/IRENA_Green_hydrogen_cost_2020.pdf) |
389 |
390 | **Country-specific parameters:**
391 |
392 | | Country | Parameter | Value | Units | Ref. |
393 | |---------|------------------------------|---------|-------|------------------------------------------------------------------------------------|
394 | | Namibia | Electricity price | 0.10465 | €/kWh | [GlobalPetrolPrices.com]({https://www.globalpetrolprices.com/electricity_prices/}) |
395 | | Namibia | Heat price | 0.02 | €/kWh | Assumption |
396 | | Namibia | Solar interest rate | 6 | % | Assumption |
397 | | Namibia | Solar lifetime | 20 | years | Assumption |
398 | | Namibia | Wind interest rate | 6 | % | Assumption |
399 | | Namibia | Wind lifetime | 20 | years | Assumption |
400 | | Namibia | Plant interest rate | 6 | % | Assumption |
401 | | Namibia | Plant lifetime | 20 | years | Assumption |
402 | | Namibia | Infrastructure interest rate | 6 | % | Assumption |
403 | | Namibia | Infrastructure lifetime | 50 | years | [Müller et al., 2022](https://doi.org/10.1016/j.apenergy.2023.121219) |
404 |
--------------------------------------------------------------------------------