From 0f33d804bf968141821a545a7522adcc86d37cd4 Mon Sep 17 00:00:00 2001 From: Jared Thomas Date: Mon, 20 May 2024 06:35:53 -0600 Subject: [PATCH 1/6] make derivative specs optional for GreenHEART optimization interface --- .../tools/optimization/gc_PoseOptimization.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/greenheart/tools/optimization/gc_PoseOptimization.py b/greenheart/tools/optimization/gc_PoseOptimization.py index bd0cbee3d..220f23ada 100644 --- a/greenheart/tools/optimization/gc_PoseOptimization.py +++ b/greenheart/tools/optimization/gc_PoseOptimization.py @@ -159,11 +159,23 @@ def set_driver(self, opt_prob): opt_options = self.config.greenheart_config["opt_options"]["driver"]["optimization"] step_size = self._get_step_size() - if opt_options["step_calc"] == "None": + if "step_calc" in opt_options.keys(): + if opt_options["step_calc"] == "None": + step_calc = None + else: + step_calc = opt_options["step_calc"] + else: step_calc = None + + if "form" in opt_options.keys(): + if opt_options["form"] == "None": + form = None + else: + form = opt_options["form"] else: - step_calc = opt_options["step_calc"] - opt_prob.model.approx_totals(method="fd", step=step_size, form=opt_options["form"], step_calc=step_calc) + form = None + + opt_prob.model.approx_totals(method="fd", step=step_size, form=form, step_calc=step_calc) # Set optimization solver and options. First, Scipy's SLSQP and COBYLA if opt_options["solver"] in self.scipy_methods: From 2698362b71ddd1997e8e1a52420ed501ea3e661b Mon Sep 17 00:00:00 2001 From: Jared Thomas Date: Mon, 20 May 2024 07:12:50 -0600 Subject: [PATCH 2/6] make form keyword optional --- greenheart/tools/optimization/gc_PoseOptimization.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/greenheart/tools/optimization/gc_PoseOptimization.py b/greenheart/tools/optimization/gc_PoseOptimization.py index 220f23ada..0f00b069f 100644 --- a/greenheart/tools/optimization/gc_PoseOptimization.py +++ b/greenheart/tools/optimization/gc_PoseOptimization.py @@ -77,8 +77,9 @@ def get_number_design_variables(self): n_DV += self.config.hopp_config["technologies"]["wind"]["num_turbines"] # Wrap-up at end with multiplier for finite differencing - if self.config.greenheart_config["opt_options"]["driver"]["optimization"]["form"] == "central": # TODO this should probably be handled at the MPI point to avoid confusion with n_DV being double what would be expected - n_DV *= 2 + if "form" in self.config.greenheart_config["opt_options"]["driver"]["optimization"].keys(): + if self.config.greenheart_config["opt_options"]["driver"]["optimization"]["form"] == "central": # TODO this should probably be handled at the MPI point to avoid confusion with n_DV being double what would be expected + n_DV *= 2 return n_DV From 62cdb15986fd51346ed8fdf8a4c0101a3758af2c Mon Sep 17 00:00:00 2001 From: kbrunik Date: Wed, 5 Jun 2024 10:22:33 -0700 Subject: [PATCH 3/6] add h2 storage sizing for steady-state --- .../hydrogen/h2_storage/storage_sizing.py | 78 +++++++++++++++++++ greenheart/tools/eco/hydrogen_mgmt.py | 14 +++- .../input_files/plant/greenheart_config.yaml | 3 +- 3 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 greenheart/simulation/technologies/hydrogen/h2_storage/storage_sizing.py diff --git a/greenheart/simulation/technologies/hydrogen/h2_storage/storage_sizing.py b/greenheart/simulation/technologies/hydrogen/h2_storage/storage_sizing.py new file mode 100644 index 000000000..3e34be1d7 --- /dev/null +++ b/greenheart/simulation/technologies/hydrogen/h2_storage/storage_sizing.py @@ -0,0 +1,78 @@ +import math +import numpy as np + + +def hydrogen_storage_capacity(H2_Results, electrolyzer_size_mw, hydrogen_demand_kgphr): + """Calculate storage capacity based on hydrogen demand and production. + + Args: + H2_Results (dict): Dictionary including electrolyzer physics results. + electrolyzer_size_mw (float): Electrolyzer size in MW. + hydrogen_demand_kgphr (float): Hydrogen demand in kg/hr. + + Returns: + hydrogen_storage_capacity_kg (float): Hydrogen storage capacity in kilograms. + hydrogen_storage_duration_hr (float): Hydrogen storage duration in hours using HHV/LHV? + hydrogen_storage_soc (list): Timeseries of the hydrogen storage state of charge. + """ + + hydrogen_production_kgphr = H2_Results["Hydrogen Hourly Production [kg/hr]"] + + hydrogen_demand_kgphr = max( + hydrogen_demand_kgphr, np.mean(hydrogen_production_kgphr) + ) # TODO: potentially add buffer No buffer needed since we are already oversizing + + # TODO: SOC is currently allowed to go negative. Ideally would calculate as shortfall in future. + hydrogen_storage_soc = [] + for j in range(len(hydrogen_production_kgphr)): + if j == 0: + hydrogen_storage_soc.append( + hydrogen_production_kgphr[j] - hydrogen_demand_kgphr + ) + else: + hydrogen_storage_soc.append( + hydrogen_storage_soc[j - 1] + + hydrogen_production_kgphr[j] + - hydrogen_demand_kgphr + ) + + hydrogen_storage_capacity_kg = np.max(hydrogen_storage_soc) - np.min( + hydrogen_storage_soc + ) + h2_LHV = 119.96 # MJ/kg + h2_HHV = 141.88 # MJ/kg + hydrogen_storage_capacity_MWh_LHV = hydrogen_storage_capacity_kg * h2_LHV / 3600 + hydrogen_storage_capacity_MWh_HHV = hydrogen_storage_capacity_kg * h2_HHV / 3600 + + # Get max injection/withdrawal rate + hydrogen_injection_withdrawal_rate = [] + for j in range(len(hydrogen_production_kgphr)): + hydrogen_injection_withdrawal_rate.append( + hydrogen_production_kgphr[j] - hydrogen_demand_kgphr + ) + max_h2_injection_rate_kgphr = max(hydrogen_injection_withdrawal_rate) + + # Get storage compressor capacity. TODO: sync compressor calculation here with GreenHEART compressor model + compressor_total_capacity_kW = ( + max_h2_injection_rate_kgphr / 3600 / 2.0158 * 8641.678424 + ) + + compressor_max_capacity_kw = 16000 + n_comps = math.ceil(compressor_total_capacity_kW / compressor_max_capacity_kw) + + small_positive = 1e-6 + compressor_avg_capacity_kw = compressor_total_capacity_kW / ( + n_comps + small_positive + ) + + # Get average electrolyzer efficiency + electrolyzer_average_efficiency_HHV = H2_Results['Sim: Average Efficiency [%-HHV]'] + + # Calculate storage durationhyd + hydrogen_storage_duration_hr = ( + hydrogen_storage_capacity_MWh_LHV + / electrolyzer_size_mw + / electrolyzer_average_efficiency_HHV + ) + + return hydrogen_storage_capacity_kg, hydrogen_storage_duration_hr, hydrogen_storage_soc diff --git a/greenheart/tools/eco/hydrogen_mgmt.py b/greenheart/tools/eco/hydrogen_mgmt.py index 1eab9a782..ecad85cef 100644 --- a/greenheart/tools/eco/hydrogen_mgmt.py +++ b/greenheart/tools/eco/hydrogen_mgmt.py @@ -44,6 +44,8 @@ ) from greenheart.simulation.technologies.offshore.all_platforms import calc_platform_opex +from greenheart.simulation.technologies.hydrogen.h2_storage.storage_sizing import hydrogen_storage_capacity + def run_h2_pipe_array( greenheart_config, @@ -285,8 +287,16 @@ def run_h2_storage( greenheart_config["h2_capacity"] = 0.0 h2_storage_results["h2_storage_kg"] = 0.0 else: - greenheart_config["h2_capacity"] = h2_capacity - h2_storage_results["h2_storage_kg"] = h2_capacity + if greenheart_config['h2_storage']['demand_capacity']: + hydrogen_storage_demand = electrolyzer_physics_results["H2_Results"][ + "Life: Annual H2 production [kg/year]" + ] # 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 + else: + greenheart_config["h2_capacity"] = h2_capacity + h2_storage_results["h2_storage_kg"] = h2_capacity # if storage_hours == 0: if ( diff --git a/tests/greenheart/input_files/plant/greenheart_config.yaml b/tests/greenheart/input_files/plant/greenheart_config.yaml index ada6a0a86..9f706e214 100644 --- a/tests/greenheart/input_files/plant/greenheart_config.yaml +++ b/tests/greenheart/input_files/plant/greenheart_config.yaml @@ -82,9 +82,10 @@ h2_transport_pipe: outlet_pressure: 10 # bar - from example in code from Jamie #TODO check this value h2_storage: # capacity_kg: 18750 # kg + demand_capacity: False # 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: "none" # can be one of ["none", "pipe", "turbine", "pressure_vessel", "salt_cavern", "lined_rock_cavern"] - days: 3 # [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: 3 # [days] how many days worth of production we should be able to store (this is ignored if `capacity_from_max_on_turbine_storage` or `demand_capacity` 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) From 63c0374a8a63c916cb2401c687836933180fbf78 Mon Sep 17 00:00:00 2001 From: kbrunik Date: Wed, 5 Jun 2024 11:49:44 -0700 Subject: [PATCH 4/6] add ss h2 storage test --- tests/greenheart/test_greenheart_system.py | 52 +++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/tests/greenheart/test_greenheart_system.py b/tests/greenheart/test_greenheart_system.py index b9bb6ea74..3c514eb99 100644 --- a/tests/greenheart/test_greenheart_system.py +++ b/tests/greenheart/test_greenheart_system.py @@ -363,4 +363,54 @@ def test_simulation_wind_battery_pv_onshore_steel_ammonia(subtests): expected_length = 8760 for key in greenheart_output.hourly_energy_breakdown.keys(): - assert len(greenheart_output.hourly_energy_breakdown[key]) == expected_length \ No newline at end of file + assert len(greenheart_output.hourly_energy_breakdown[key]) == expected_length + +def test_simulation_wind_onshore_steel_ammonia_ss_h2storage(subtests): + + config = GreenHeartSimulationConfig( + filename_hopp_config=filename_hopp_config, + filename_greenheart_config=filename_greenheart_config_onshore, + filename_turbine_config=filename_turbine_config, + filename_floris_config=filename_floris_config, + verbose=False, + show_plots=False, + save_plots=True, + output_dir=os.path.abspath(pathlib.Path(__file__).parent.resolve()) + "/output/", + use_profast=True, + post_processing=True, + incentive_option=1, + plant_design_scenario=9, + output_level=7, + ) + + config.greenheart_config['h2_storage']['demand_capacity'] = True + config.greenheart_config['h2_storage']['type'] = 'pipe' + + # based on 2023 ATB moderate case for onshore wind + config.hopp_config["config"]["cost_info"]["wind_installed_cost_mw"] = 1434000.0 + # based on 2023 ATB moderate case for onshore wind + config.hopp_config["config"]["cost_info"]["wind_om_per_kw"] = 29.567 + config.hopp_config["technologies"]["wind"]["fin_model"]["system_costs"]["om_fixed"][0] = config.hopp_config["config"]["cost_info"]["wind_om_per_kw"] + # set skip_financial to false for onshore wind + config.hopp_config["config"]["simulation_options"]["wind"]["skip_financial"] = False + lcoe, lcoh, steel_finance, ammonia_finance = run_simulation(config) + + # TODO base this test value on something + with subtests.test("lcoh"): + assert lcoh == approx(4.023687007795485, rel=rtol) + + # TODO base this test value on something + with subtests.test("lcoe"): + assert lcoe == approx(0.03486192934806013, rel=rtol) + + # TODO base this test value on something + with subtests.test("steel_finance"): + lcos_expected = 1414.0330270955506 + + assert steel_finance.sol.get("price") == approx(lcos_expected, rel=rtol) + + # TODO base this test value on something + with subtests.test("ammonia_finance"): + lcoa_expected = 1.0419096226034346 + + assert ammonia_finance.sol.get("price") == approx(lcoa_expected, rel=rtol) \ No newline at end of file From d8ff547db45046d97953927b127e3ffc3f834cf5 Mon Sep 17 00:00:00 2001 From: kbrunik Date: Wed, 5 Jun 2024 14:25:21 -0700 Subject: [PATCH 5/6] set orbit requirement to specific commit --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 479ac8eb7..d8a2dfe9b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,7 +43,7 @@ openpyxl CoolProp attrs utm -orbit-nrel @ git+https://github.com/WISDEM/ORBIT.git@SemiTaut_Mooring_Update +orbit-nrel @ git+https://github.com/WISDEM/ORBIT.git@3baed36052d7503de6ea6f2db0b40945d637ea35 pyyaml-include <= 1.4.1 electrolyzer @ git+https://github.com/jaredthomas68/electrolyzer.git@smoothing ProFAST @ git+https://github.com/NREL/ProFAST.git From 2fb25f7ca5efaa45c1d6988f31e7b6a67737ef08 Mon Sep 17 00:00:00 2001 From: kbrunik Date: Wed, 5 Jun 2024 14:58:18 -0700 Subject: [PATCH 6/6] update h2 storage sizing parameters --- .../hydrogen/h2_storage/storage_sizing.py | 46 +++++++++++-------- greenheart/tools/eco/hydrogen_mgmt.py | 8 ++-- .../input_files/plant/greenheart_config.yaml | 6 +-- .../plant/greenheart_config_onshore.yaml | 3 +- tests/greenheart/test_greenheart_system.py | 2 +- 5 files changed, 36 insertions(+), 29 deletions(-) diff --git a/greenheart/simulation/technologies/hydrogen/h2_storage/storage_sizing.py b/greenheart/simulation/technologies/hydrogen/h2_storage/storage_sizing.py index 3e34be1d7..3728919b7 100644 --- a/greenheart/simulation/technologies/hydrogen/h2_storage/storage_sizing.py +++ b/greenheart/simulation/technologies/hydrogen/h2_storage/storage_sizing.py @@ -12,7 +12,7 @@ def hydrogen_storage_capacity(H2_Results, electrolyzer_size_mw, hydrogen_demand_ Returns: hydrogen_storage_capacity_kg (float): Hydrogen storage capacity in kilograms. - hydrogen_storage_duration_hr (float): Hydrogen storage duration in hours using HHV/LHV? + hydrogen_storage_duration_hr (float): Hydrogen storage duration in hours using HHV/LHV. hydrogen_storage_soc (list): Timeseries of the hydrogen storage state of charge. """ @@ -22,7 +22,7 @@ def hydrogen_storage_capacity(H2_Results, electrolyzer_size_mw, hydrogen_demand_ hydrogen_demand_kgphr, np.mean(hydrogen_production_kgphr) ) # TODO: potentially add buffer No buffer needed since we are already oversizing - # TODO: SOC is currently allowed to go negative. Ideally would calculate as shortfall in future. + # TODO: SOC is just an absolute value and is not a percentage. Ideally would calculate as shortfall in future. hydrogen_storage_soc = [] for j in range(len(hydrogen_production_kgphr)): if j == 0: @@ -36,6 +36,12 @@ def hydrogen_storage_capacity(H2_Results, electrolyzer_size_mw, hydrogen_demand_ - hydrogen_demand_kgphr ) + minimum_soc = np.min(hydrogen_storage_soc) + + #adjust soc so it's not negative. + if minimum_soc < 0: + hydrogen_storage_soc = [x + np.abs(minimum_soc) for x in hydrogen_storage_soc] + hydrogen_storage_capacity_kg = np.max(hydrogen_storage_soc) - np.min( hydrogen_storage_soc ) @@ -44,26 +50,26 @@ def hydrogen_storage_capacity(H2_Results, electrolyzer_size_mw, hydrogen_demand_ hydrogen_storage_capacity_MWh_LHV = hydrogen_storage_capacity_kg * h2_LHV / 3600 hydrogen_storage_capacity_MWh_HHV = hydrogen_storage_capacity_kg * h2_HHV / 3600 - # Get max injection/withdrawal rate - hydrogen_injection_withdrawal_rate = [] - for j in range(len(hydrogen_production_kgphr)): - hydrogen_injection_withdrawal_rate.append( - hydrogen_production_kgphr[j] - hydrogen_demand_kgphr - ) - max_h2_injection_rate_kgphr = max(hydrogen_injection_withdrawal_rate) - - # Get storage compressor capacity. TODO: sync compressor calculation here with GreenHEART compressor model - compressor_total_capacity_kW = ( - max_h2_injection_rate_kgphr / 3600 / 2.0158 * 8641.678424 - ) + # # Get max injection/withdrawal rate + # hydrogen_injection_withdrawal_rate = [] + # for j in range(len(hydrogen_production_kgphr)): + # hydrogen_injection_withdrawal_rate.append( + # hydrogen_production_kgphr[j] - hydrogen_demand_kgphr + # ) + # max_h2_injection_rate_kgphr = max(hydrogen_injection_withdrawal_rate) - compressor_max_capacity_kw = 16000 - n_comps = math.ceil(compressor_total_capacity_kW / compressor_max_capacity_kw) + # # Get storage compressor capacity. TODO: sync compressor calculation here with GreenHEART compressor model + # compressor_total_capacity_kW = ( + # max_h2_injection_rate_kgphr / 3600 / 2.0158 * 8641.678424 + # ) - small_positive = 1e-6 - compressor_avg_capacity_kw = compressor_total_capacity_kW / ( - n_comps + small_positive - ) + # compressor_max_capacity_kw = 16000 + # n_comps = math.ceil(compressor_total_capacity_kW / compressor_max_capacity_kw) + + # small_positive = 1e-6 + # compressor_avg_capacity_kw = compressor_total_capacity_kW / ( + # n_comps + small_positive + # ) # Get average electrolyzer efficiency electrolyzer_average_efficiency_HHV = H2_Results['Sim: Average Efficiency [%-HHV]'] diff --git a/greenheart/tools/eco/hydrogen_mgmt.py b/greenheart/tools/eco/hydrogen_mgmt.py index ecad85cef..8e2ef85ac 100644 --- a/greenheart/tools/eco/hydrogen_mgmt.py +++ b/greenheart/tools/eco/hydrogen_mgmt.py @@ -287,10 +287,10 @@ def run_h2_storage( greenheart_config["h2_capacity"] = 0.0 h2_storage_results["h2_storage_kg"] = 0.0 else: - if greenheart_config['h2_storage']['demand_capacity']: - hydrogen_storage_demand = electrolyzer_physics_results["H2_Results"][ - "Life: Annual H2 production [kg/year]" - ] # TODO: update demand based on end-use needs + 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 diff --git a/tests/greenheart/input_files/plant/greenheart_config.yaml b/tests/greenheart/input_files/plant/greenheart_config.yaml index 9f706e214..03d5256da 100644 --- a/tests/greenheart/input_files/plant/greenheart_config.yaml +++ b/tests/greenheart/input_files/plant/greenheart_config.yaml @@ -81,11 +81,11 @@ h2_storage_compressor: h2_transport_pipe: outlet_pressure: 10 # bar - from example in code from Jamie #TODO check this value h2_storage: - # capacity_kg: 18750 # kg - demand_capacity: False # If True, then storage is sized to provide steady-state storage + size_capacity_from_demand: + flag: False # 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: "none" # can be one of ["none", "pipe", "turbine", "pressure_vessel", "salt_cavern", "lined_rock_cavern"] - days: 3 # [days] how many days worth of production we should be able to store (this is ignored if `capacity_from_max_on_turbine_storage` or `demand_capacity` is set to True) + days: 3 # [days] how many days worth of production we should be able to store (this is ignored if `capacity_from_max_on_turbine_storage` or `size_capacity_from_demand` 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) diff --git a/tests/greenheart/input_files/plant/greenheart_config_onshore.yaml b/tests/greenheart/input_files/plant/greenheart_config_onshore.yaml index fd22b77bd..d5ca4133f 100644 --- a/tests/greenheart/input_files/plant/greenheart_config_onshore.yaml +++ b/tests/greenheart/input_files/plant/greenheart_config_onshore.yaml @@ -82,7 +82,8 @@ h2_storage_compressor: h2_transport_pipe: outlet_pressure: 10 # bar - from example in code from Jamie #TODO check this value h2_storage: - # capacity_kg: 18750 # kg + size_capacity_from_demand: + flag: False # 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: "none" # can be one of ["none", "pipe", "turbine", "pressure_vessel", "salt_cavern", "lined_rock_cavern"] days: 3 # [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) diff --git a/tests/greenheart/test_greenheart_system.py b/tests/greenheart/test_greenheart_system.py index 3c514eb99..ad172df1d 100644 --- a/tests/greenheart/test_greenheart_system.py +++ b/tests/greenheart/test_greenheart_system.py @@ -383,7 +383,7 @@ def test_simulation_wind_onshore_steel_ammonia_ss_h2storage(subtests): output_level=7, ) - config.greenheart_config['h2_storage']['demand_capacity'] = True + config.greenheart_config['h2_storage']['size_capacity_from_demand']['flag'] = True config.greenheart_config['h2_storage']['type'] = 'pipe' # based on 2023 ATB moderate case for onshore wind