├── .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 | --------------------------------------------------------------------------------