From 81e55f86fcd1e24b06a5fe27120eae6091aac2f3 Mon Sep 17 00:00:00 2001 From: kbrunik Date: Fri, 7 Jun 2024 05:45:38 -0700 Subject: [PATCH] clean up hydrogen mgmt logic --- .../plant/greenheart_config_onshore_mn.yaml | 2 +- .../plant/greenheart_config_onshore_tx.yaml | 2 +- .../plant/greenheart_config_offshore_gom.yaml | 2 +- greenheart/tools/eco/hydrogen_mgmt.py | 272 ++++++++++-------- 4 files changed, 152 insertions(+), 126 deletions(-) diff --git a/examples/reference_plants/01-onshore-steel-mn/input/plant/greenheart_config_onshore_mn.yaml b/examples/reference_plants/01-onshore-steel-mn/input/plant/greenheart_config_onshore_mn.yaml index 7b98f4f20..53f72b68a 100644 --- a/examples/reference_plants/01-onshore-steel-mn/input/plant/greenheart_config_onshore_mn.yaml +++ b/examples/reference_plants/01-onshore-steel-mn/input/plant/greenheart_config_onshore_mn.yaml @@ -85,7 +85,7 @@ h2_storage: flag: True # If True, then storage is sized to provide steady-state storage capacity_from_max_on_turbine_storage: False # if True, then days of storage is ignored and storage capacity is based on how much h2 storage fits on the turbines in the plant using Kottenstete 2003. type: "lined_rock_cavern" # can be one of ["none", "pipe", "turbine", "pressure_vessel", "salt_cavern", "lined_rock_cavern"] - days: -1 #8.57267 # from `hydrogen_storage_duration_hr` = 205.74419987482239 [days] how many days worth of production we should be able to store (this is ignored if `capacity_from_max_on_turbine_storage` is set to True) + days: 0 #8.57267 # from `hydrogen_storage_duration_hr` = 205.74419987482239 [days] how many days worth of production we should be able to store (this is ignored if `capacity_from_max_on_turbine_storage` is set to True) policy_parameters: # these should be adjusted for inflation prior to application - order of operations: rate in 1992 $, #then prevailing wage multiplier if applicable, then inflation diff --git a/examples/reference_plants/02-onshore-ammonia-tx/input/plant/greenheart_config_onshore_tx.yaml b/examples/reference_plants/02-onshore-ammonia-tx/input/plant/greenheart_config_onshore_tx.yaml index 8ea5ecf52..f8801dcfc 100644 --- a/examples/reference_plants/02-onshore-ammonia-tx/input/plant/greenheart_config_onshore_tx.yaml +++ b/examples/reference_plants/02-onshore-ammonia-tx/input/plant/greenheart_config_onshore_tx.yaml @@ -85,7 +85,7 @@ h2_storage: flag: True # If True, then storage is sized to provide steady-state storage capacity_from_max_on_turbine_storage: False # if True, then days of storage is ignored and storage capacity is based on how much h2 storage fits on the turbines in the plant using Kottenstete 2003. type: "salt_cavern" # can be one of ["none", "pipe", "turbine", "pressure_vessel", "salt_cavern", "lined_rock_cavern"] - days: -1 #19.783 # from `hydrogen_storage_duration_hr` = 474.7948370015298 [days] how many days worth of production we should be able to store (this is ignored if `capacity_from_max_on_turbine_storage` is set to True) + days: 0 #19.783 # from `hydrogen_storage_duration_hr` = 474.7948370015298 [days] how many days worth of production we should be able to store (this is ignored if `capacity_from_max_on_turbine_storage` is set to True) policy_parameters: # these should be adjusted for inflation prior to application - order of operations: rate in 1992 $, #then prevailing wage multiplier if applicable, then inflation diff --git a/examples/reference_plants/03-offshore-hydrogen-gom/input/plant/greenheart_config_offshore_gom.yaml b/examples/reference_plants/03-offshore-hydrogen-gom/input/plant/greenheart_config_offshore_gom.yaml index 9daf5d2d0..8281fb7d4 100644 --- a/examples/reference_plants/03-offshore-hydrogen-gom/input/plant/greenheart_config_offshore_gom.yaml +++ b/examples/reference_plants/03-offshore-hydrogen-gom/input/plant/greenheart_config_offshore_gom.yaml @@ -84,7 +84,7 @@ h2_storage: flag: True # If True, then storage is sized to provide steady-state storage capacity_from_max_on_turbine_storage: False # if True, then days of storage is ignored and storage capacity is based on how much h2 storage fits on the turbines in the plant using Kottenstete 2003. type: "salt_cavern" # can be one of ["none", "pipe", "turbine", "pressure_vessel", "salt_cavern", "lined_rock_cavern"] - days: -1 # [days] how many days worth of production we should be able to store (this is ignored if `capacity_from_max_on_turbine_storage` is set to True) + days: 0 # [days] how many days worth of production we should be able to store (this is ignored if `capacity_from_max_on_turbine_storage` is set to True) # platform: # opex_rate: 0.0111 # % of capex to determine opex (see table 5 in https://www.acm.nl/sites/default/files/documents/study-on-estimation-method-for-additional-efficient-offshore-grid-opex.pdf) # installation_days: 14 # days diff --git a/greenheart/tools/eco/hydrogen_mgmt.py b/greenheart/tools/eco/hydrogen_mgmt.py index 44a82e678..9fac3d621 100644 --- a/greenheart/tools/eco/hydrogen_mgmt.py +++ b/greenheart/tools/eco/hydrogen_mgmt.py @@ -44,7 +44,9 @@ ) from greenheart.simulation.technologies.offshore.all_platforms import calc_platform_opex -from greenheart.simulation.technologies.hydrogen.h2_storage.storage_sizing import hydrogen_storage_capacity +from greenheart.simulation.technologies.hydrogen.h2_storage.storage_sizing import ( + hydrogen_storage_capacity, +) def run_h2_pipe_array( @@ -248,7 +250,6 @@ def run_h2_storage( design_scenario, verbose=False, ): - nturbines = hopp_config["technologies"]["wind"]["num_turbines"] if design_scenario["h2_storage_location"] == "platform": if ( @@ -259,16 +260,31 @@ def run_h2_storage( "Only pressure vessel storage can be used on the off shore platform" ) - # initialize output dictionary + if design_scenario["h2_storage_location"] == "turbine": + if ( + greenheart_config["h2_storage"]["type"] != "turbine" + and greenheart_config["h2_storage"]["type"] != "pressure_vessel" + and greenheart_config["h2_storage"]["type"] != "none" + ): + raise ValueError( + "Only turbine or pressure vessel storage can be used for turbine hydrogen storage location" + ) + ########### initialize output dictionary ########### h2_storage_results = dict() - storage_hours = greenheart_config["h2_storage"]["days"] * 24 storage_max_fill_rate = np.max( electrolyzer_physics_results["H2_Results"]["Hydrogen Hourly Production [kg/hr]"] ) + ########### get hydrogen storage size in kilograms ########### + ##################### no hydrogen storage + if greenheart_config["h2_storage"]["type"] == "none": + h2_storage_capacity_kg = 0.0 + storage_max_fill_rate = 0.0 + ##################### get storage capacity from turbine storage model - if greenheart_config["h2_storage"]["capacity_from_max_on_turbine_storage"]: + elif greenheart_config["h2_storage"]["capacity_from_max_on_turbine_storage"]: + nturbines = hopp_config["technologies"]["wind"]["num_turbines"] turbine = { "tower_length": turbine_config["tower"]["length"], "section_diameters": turbine_config["tower"]["section_diameters"], @@ -282,28 +298,39 @@ def run_h2_storage( h2_storage_capacity_single_turbine = h2_storage.get_capacity_H2() # kg - h2_capacity = nturbines * h2_storage_capacity_single_turbine # in kg - ################################### - else: - h2_capacity = round(storage_hours * storage_max_fill_rate) + h2_storage_capacity_kg = nturbines * h2_storage_capacity_single_turbine # in kg - if greenheart_config["h2_storage"]["type"] == "none": - greenheart_config["h2_capacity"] = 0.0 - h2_storage_results["h2_storage_kg"] = 0.0 + ##################### get storage capacity from hydrogen storage demand + elif greenheart_config["h2_storage"]["size_capacity_from_demand"]["flag"]: + hydrogen_storage_demand = np.mean( + electrolyzer_physics_results["H2_Results"][ + "Hydrogen Hourly Production [kg/hr]" + ] + ) # TODO: update demand based on end-use needs + ( + hydrogen_storage_capacity_kg, + hydrogen_storage_duration_hr, + hydrogen_storage_soc, + ) = hydrogen_storage_capacity( + electrolyzer_physics_results["H2_Results"], + greenheart_config["electrolyzer"]["rating"], + hydrogen_storage_demand, + ) + h2_storage_capacity_kg = hydrogen_storage_capacity_kg + h2_storage_results["hydrogen_storage_duration_hr"] = ( + hydrogen_storage_duration_hr + ) + h2_storage_results["hydrogen_storage_soc"] = hydrogen_storage_soc + + ##################### get storage capacity based on storage days in config else: - if greenheart_config['h2_storage']['size_capacity_from_demand']['flag']: - hydrogen_storage_demand = np.mean(electrolyzer_physics_results["H2_Results"][ - "Hydrogen Hourly Production [kg/hr]" - ]) # TODO: update demand based on end-use needs - hydrogen_storage_capacity_kg, hydrogen_storage_duration_hr, hydrogen_storage_soc = hydrogen_storage_capacity(electrolyzer_physics_results['H2_Results'], greenheart_config['electrolyzer']['rating'], hydrogen_storage_demand) - greenheart_config["h2_capacity"] = hydrogen_storage_capacity_kg - h2_storage_results["h2_storage_kg"] = hydrogen_storage_capacity_kg + storage_hours = greenheart_config["h2_storage"]["days"] * 24 + h2_storage_capacity_kg = round(storage_hours * storage_max_fill_rate) - else: - greenheart_config["h2_capacity"] = h2_capacity - h2_storage_results["h2_storage_kg"] = h2_capacity + h2_storage_results["h2_storage_capacity_kg"] = h2_storage_capacity_kg + h2_storage_results["h2_storage_max_fill_rate_kg_hr"] = storage_max_fill_rate - # if storage_hours == 0: + ########### run specific hydrogen storage models for costs and energy use ########### if ( greenheart_config["h2_storage"]["type"] == "none" or design_scenario["h2_storage_location"] == "none" @@ -314,8 +341,8 @@ def run_h2_storage( h2_storage = None - elif design_scenario["h2_storage_location"] == "turbine": - if greenheart_config["h2_storage"]["type"] == "turbine": + elif greenheart_config["h2_storage"]["type"] == "turbine": + if design_scenario["h2_storage_location"] == "turbine": turbine = { "tower_length": turbine_config["tower"]["length"], "section_diameters": turbine_config["tower"]["section_diameters"], @@ -338,12 +365,41 @@ def run_h2_storage( ) print("storage pressure: ", h2_storage.get_pressure_H2()) - h2_storage_results[ - "storage_energy" - ] = 0.0 # low pressure, so no additional compression needed beyond electolyzer + h2_storage_results["storage_energy"] = ( + 0.0 # low pressure, so no additional compression needed beyond electolyzer + ) + else: + raise ValueError( + "`h2_storage_location` must be set to 'turbine' to use 'turbine' for h2 storage type." + ) - elif greenheart_config["h2_storage"]["type"] == "pressure_vessel": + elif greenheart_config["h2_storage"]["type"] == "pipe": + # for more information, see https://www.nrel.gov/docs/fy14osti/58564.pdf + # initialize dictionary for pipe storage parameters + storage_input = dict() + # pull parameters from plat_config file + storage_input["h2_storage_kg"] = h2_storage_capacity_kg + storage_input["compressor_output_pressure"] = greenheart_config[ + "h2_storage_compressor" + ]["output_pressure"] + storage_input["system_flow_rate"] = storage_max_fill_rate + storage_input["model"] = "papadias" + + # run pipe storage model + h2_storage = UndergroundPipeStorage(storage_input) + + h2_storage.pipe_storage_capex() + h2_storage.pipe_storage_opex() + + h2_storage_results["storage_capex"] = h2_storage.output_dict[ + "pipe_storage_capex" + ] + h2_storage_results["storage_opex"] = h2_storage.output_dict["pipe_storage_opex"] + h2_storage_results["storage_energy"] = 0.0 + + elif greenheart_config["h2_storage"]["type"] == "pressure_vessel": + if design_scenario["h2_storage_location"] == "turbine": energy_cost = 0.0 h2_storage = PressureVessel(Energy_cost=energy_cost) @@ -356,7 +412,7 @@ def run_h2_storage( area_site, mass_tank_empty_site, _, - ) = h2_storage.distributed_storage_vessels(greenheart_config["h2_capacity"], 1) + ) = h2_storage.distributed_storage_vessels(h2_storage_capacity_kg, 1) # ) = h2_storage.distributed_storage_vessels(h2_capacity, nturbines) # capex, opex, energy = h2_storage.calculate_from_fit(h2_capacity) @@ -369,15 +425,18 @@ def run_h2_storage( ] ) # total in kWh h2_storage_results["tank_mass_full_kg"] = ( - h2_storage.get_tank_mass(greenheart_config["h2_capacity"])[1] + greenheart_config["h2_capacity"] + h2_storage.get_tank_mass(h2_storage_capacity_kg)[1] + + h2_storage_capacity_kg ) h2_storage_results["tank_footprint_m2"] = h2_storage.get_tank_footprint( - greenheart_config["h2_capacity"], upright=True + h2_storage_capacity_kg, upright=True )[1] - h2_storage_results[ - "tank volume (m^3)" - ] = h2_storage.compressed_gas_function.Vtank - h2_storage_results["Number of tanks"] = h2_storage.get_tanks(greenheart_config["h2_capacity"]) + h2_storage_results["tank volume (m^3)"] = ( + h2_storage.compressed_gas_function.Vtank + ) + h2_storage_results["Number of tanks"] = h2_storage.get_tanks( + h2_storage_capacity_kg + ) if verbose: print("ENERGY FOR STORAGE: ", energy * 1e-3 / (365 * 24), " MW") print("Tank volume (M^3): ", h2_storage_results["tank volume (m^3)"]) @@ -388,82 +447,53 @@ def run_h2_storage( print("N Tanks: ", h2_storage_results["Number of tanks"]) else: - ValueError( - "with storage location set to tower, only 'pressure_vessel' and 'tower' types are implemented." - ) - - elif greenheart_config["h2_storage"]["type"] == "pipe": - # for more information, see https://www.nrel.gov/docs/fy14osti/58564.pdf - # initialize dictionary for pipe storage parameters - storage_input = dict() - - # pull parameters from plat_config file - storage_input["h2_storage_kg"] = greenheart_config["h2_capacity"] - storage_input["compressor_output_pressure"] = greenheart_config[ - "h2_storage_compressor" - ]["output_pressure"] - storage_input["system_flow_rate"] = storage_max_fill_rate - storage_input["model"] = "papadias" + # if plant_config["project_parameters"]["grid_connection"]: + # energy_cost = plant_config["project_parameters"]["ppa_price"] + # else: + # energy_cost = 0.0 + energy_cost = 0.0 # energy cost is now handled outside the storage model - # run pipe storage model - h2_storage = UndergroundPipeStorage(storage_input) - - h2_storage.pipe_storage_capex() - h2_storage.pipe_storage_opex() - - h2_storage_results["storage_capex"] = h2_storage.output_dict[ - "pipe_storage_capex" - ] - h2_storage_results["storage_opex"] = h2_storage.output_dict["pipe_storage_opex"] - h2_storage_results["storage_energy"] = 0.0 - - elif greenheart_config["h2_storage"]["type"] == "pressure_vessel": - # if plant_config["project_parameters"]["grid_connection"]: - # energy_cost = plant_config["project_parameters"]["ppa_price"] - # else: - # energy_cost = 0.0 - energy_cost = 0.0 # energy cost is now handled outside the storage model - - h2_storage = PressureVessel(Energy_cost=energy_cost) - h2_storage.run() + h2_storage = PressureVessel(Energy_cost=energy_cost) + h2_storage.run() - capex, opex, energy = h2_storage.calculate_from_fit(greenheart_config["h2_capacity"]) + capex, opex, energy = h2_storage.calculate_from_fit(h2_storage_capacity_kg) - h2_storage_results["storage_capex"] = capex - h2_storage_results["storage_opex"] = opex - h2_storage_results["storage_energy"] = ( - energy - * electrolyzer_physics_results["H2_Results"][ - "Life: Annual H2 production [kg/year]" - ] - ) # total in kWh - h2_storage_results["tank_mass_full_kg"] = ( - h2_storage.get_tank_mass(greenheart_config["h2_capacity"])[1] + greenheart_config["h2_capacity"] - ) - h2_storage_results["tank_footprint_m2"] = h2_storage.get_tank_footprint( - greenheart_config["h2_capacity"], upright=True - )[1] - h2_storage_results[ - "tank volume (m^3)" - ] = h2_storage.compressed_gas_function.Vtank - h2_storage_results[ - "Number of tanks" - ] = h2_storage.compressed_gas_function.number_of_tanks - if verbose: - print("ENERGY FOR STORAGE: ", energy * 1e-3 / (365 * 24), " MW") - print("Tank volume (M^3): ", h2_storage_results["tank volume (m^3)"]) - print( - "Single Tank capacity (kg): ", - h2_storage.compressed_gas_function.single_tank_h2_capacity_kg, + h2_storage_results["storage_capex"] = capex + h2_storage_results["storage_opex"] = opex + h2_storage_results["storage_energy"] = ( + energy + * electrolyzer_physics_results["H2_Results"][ + "Life: Annual H2 production [kg/year]" + ] + ) # total in kWh + h2_storage_results["tank_mass_full_kg"] = ( + h2_storage.get_tank_mass(h2_storage_capacity_kg)[1] + + h2_storage_capacity_kg + ) + h2_storage_results["tank_footprint_m2"] = h2_storage.get_tank_footprint( + h2_storage_capacity_kg, upright=True + )[1] + h2_storage_results["tank volume (m^3)"] = ( + h2_storage.compressed_gas_function.Vtank + ) + h2_storage_results["Number of tanks"] = ( + h2_storage.compressed_gas_function.number_of_tanks ) - print("N Tanks: ", h2_storage_results["Number of tanks"]) + if verbose: + print("ENERGY FOR STORAGE: ", energy * 1e-3 / (365 * 24), " MW") + print("Tank volume (M^3): ", h2_storage_results["tank volume (m^3)"]) + print( + "Single Tank capacity (kg): ", + h2_storage.compressed_gas_function.single_tank_h2_capacity_kg, + ) + print("N Tanks: ", h2_storage_results["Number of tanks"]) elif greenheart_config["h2_storage"]["type"] == "salt_cavern": # initialize dictionary for salt cavern storage parameters storage_input = dict() # pull parameters from plant_config file - storage_input["h2_storage_kg"] = greenheart_config["h2_capacity"] + storage_input["h2_storage_kg"] = h2_storage_capacity_kg storage_input["system_flow_rate"] = storage_max_fill_rate storage_input["model"] = "papadias" @@ -480,23 +510,13 @@ def run_h2_storage( "salt_cavern_storage_opex" ] h2_storage_results["storage_energy"] = 0.0 - # TODO replace this rough estimate with real numbers - # h2_storage = None - # capex = 36.0 * h2_capacity # based on Papadias 2021 table 7 - # opex = ( - # 0.021 * capex - # ) # based on https://www.pnnl.gov/sites/default/files/media/file/Hydrogen_Methodology.pdf - - # h2_storage_results["storage_capex"] = capex - # h2_storage_results["storage_opex"] = opex - # h2_storage_results["storage_energy"] = 0.0 elif greenheart_config["h2_storage"]["type"] == "lined_rock_cavern": # initialize dictionary for salt cavern storage parameters storage_input = dict() # pull parameters from plat_config file - storage_input["h2_storage_kg"] = greenheart_config["h2_capacity"] + storage_input["h2_storage_kg"] = h2_storage_capacity_kg storage_input["system_flow_rate"] = storage_max_fill_rate storage_input["model"] = "papadias" @@ -516,13 +536,12 @@ def run_h2_storage( else: raise ( ValueError( - "H2 storage type %s was given, but must be one of ['none', 'pipe', 'pressure_vessel', 'salt_cavern', 'lined_rock_cavern']" + "H2 storage type %s was given, but must be one of ['none', 'turbine', 'pipe', 'pressure_vessel', 'salt_cavern', 'lined_rock_cavern']" ) ) if verbose: print("\nH2 Storage Results:") - print("H2 Storage capacity (kg): ",greenheart_config["h2_capacity"]) print("H2 storage capex: ${0:,.0f}".format(h2_storage_results["storage_capex"])) print( "H2 storage annual opex: ${0:,.0f}/yr".format( @@ -530,13 +549,14 @@ def run_h2_storage( ) ) print( - "H2 storage capacity (tonnes): ", h2_storage_results["h2_storage_kg"] / 1000 + "H2 storage capacity (tonnes): ", + h2_storage_results["h2_storage_capacity_kg"] / 1000, ) - if h2_storage_results["h2_storage_kg"] > 0: + if h2_storage_results["h2_storage_capacity_kg"] > 0: print( "H2 storage cost $/kg of H2: ", h2_storage_results["storage_capex"] - / h2_storage_results["h2_storage_kg"], + / h2_storage_results["h2_storage_capacity_kg"], ) return h2_storage, h2_storage_results @@ -592,12 +612,18 @@ def run_equipment_platform( topmass += battery_mass toparea += battery_area - if hopp_config["site"]["solar"] and design_scenario["pv_location"] == "platform": - pv_area = hopp_results['hybrid_plant'].pv.footprint_area - solar_mass = hopp_results['hybrid_plant'].pv.system_mass - + if ( + hopp_config["site"]["solar"] + and design_scenario["pv_location"] == "platform" + ): + pv_area = hopp_results["hybrid_plant"].pv.footprint_area + solar_mass = hopp_results["hybrid_plant"].pv.system_mass + if pv_area > toparea: - warnings.warn(f"Solar area ({pv_area} m^2) must be smaller than platform area ({toparea} m^2)", UserWarning) + warnings.warn( + f"Solar area ({pv_area} m^2) must be smaller than platform area ({toparea} m^2)", + UserWarning, + ) topmass += solar_mass #### initialize