From 36f9de69d4a25d52349a6babbbaef8b4b8aa7fa2 Mon Sep 17 00:00:00 2001 From: kbrunik Date: Fri, 20 Sep 2024 16:50:11 -0500 Subject: [PATCH 01/11] PEM bop function --- .../simulation/greenheart_simulation.py | 46 ++++--- .../electrolysis/BOP_efficiency_BOL.csv | 87 ++++++++++++ .../hydrogen/electrolysis/PEM_BOP.py | 129 ++++++++++++++++++ greenheart/tools/eco/electrolysis.py | 17 +++ greenheart/tools/eco/finance.py | 4 +- greenheart/tools/eco/utilities.py | 5 +- 6 files changed, 267 insertions(+), 21 deletions(-) create mode 100644 greenheart/simulation/technologies/hydrogen/electrolysis/BOP_efficiency_BOL.csv create mode 100644 greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP.py diff --git a/greenheart/simulation/greenheart_simulation.py b/greenheart/simulation/greenheart_simulation.py index bb6ee79e9..bf239e884 100644 --- a/greenheart/simulation/greenheart_simulation.py +++ b/greenheart/simulation/greenheart_simulation.py @@ -587,13 +587,13 @@ def energy_internals( ) total_peripheral_energy = power_for_peripherals_kw_in * 365 * 24 distributed_peripheral_power = total_peripheral_energy / high_count - for i in range(len(hopp_results["combined_hybrid_power_production_hopp"])): - r = ( - hopp_results["combined_hybrid_power_production_hopp"][i] - - distributed_peripheral_power - ) - if r > 0: - remaining_power_profile_in[i] = r + remaining_power_profile_in = np.where( + hopp_results["combined_hybrid_power_production_hopp"] + - distributed_peripheral_power > 0, + hopp_results["combined_hybrid_power_production_hopp"] + - distributed_peripheral_power, + hopp_results["combined_hybrid_power_production_hopp"] + ) hopp_results_internal["combined_hybrid_power_production_hopp"] = tuple( remaining_power_profile_in @@ -620,6 +620,14 @@ def energy_internals( verbose=verbose, ) + # run electrolyzer bop model + electrolyzer_energy_consumption_bop_kw = he_elec.run_electrolyzer_bop( + greenheart_config, + electrolyzer_physics_results + ) + print("ElectrolyzEr bop",electrolyzer_energy_consumption_bop_kw[1]) + print("Electrolyzer Energy Consumption BOP",np.mean(electrolyzer_energy_consumption_bop_kw)) + desal_results = he_elec.run_desal( hopp_config, electrolyzer_physics_results, design_scenario, verbose ) @@ -685,16 +693,18 @@ def energy_internals( h2_storage_energy_kwh = h2_storage_results["storage_energy"] h2_storage_power_kw = h2_storage_energy_kwh * (1.0 / (365 * 24)) + total_accessory_power_renewable_kw = np.zeros(len(electrolyzer_energy_consumption_bop_kw)) + total_accessory_power_renewable_kw += electrolyzer_energy_consumption_bop_kw # if transport is not HVDC and h2 storage is on shore, then power the storage from the grid if (design_scenario["transportation"] == "pipeline") and ( design_scenario["h2_storage_location"] == "onshore" ): - total_accessory_power_renewable_kw = ( + total_accessory_power_renewable_kw += ( desal_power_kw + h2_transport_compressor_power_kw ) total_accessory_power_grid_kw = h2_storage_power_kw else: - total_accessory_power_renewable_kw = ( + total_accessory_power_renewable_kw += ( desal_power_kw + h2_transport_compressor_power_kw + h2_storage_power_kw ) total_accessory_power_grid_kw = 0.0 @@ -706,14 +716,13 @@ def energy_internals( grid_power_profile = np.zeros_like( hopp_results["combined_hybrid_power_production_hopp"] ) - for i in range(len(hopp_results["combined_hybrid_power_production_hopp"])): - r = ( - hopp_results["combined_hybrid_power_production_hopp"][i] - - total_accessory_power_renewable_kw - ) - grid_power_profile[i] = total_accessory_power_grid_kw - if r > 0: - remaining_power_profile[i] = r + remaining_power_profile = np.where( + hopp_results["combined_hybrid_power_production_hopp"] + - total_accessory_power_renewable_kw > 0, + hopp_results["combined_hybrid_power_production_hopp"] + - total_accessory_power_renewable_kw, + 0 + ) if verbose and not solver: print("\nEnergy/Power Results:") @@ -755,6 +764,7 @@ def energy_internals( desal_power_kw, h2_transport_compressor_power_kw, h2_storage_power_kw, + electrolyzer_energy_consumption_bop_kw, remaining_power_profile, ) else: @@ -799,6 +809,7 @@ def simple_solver(initial_guess=0.0): desal_power_kw, h2_transport_compressor_power_kw, h2_storage_power_kw, + electrolyzer_bop_kw, remaining_power_profile, ) = energy_internals( power_for_peripherals_kw_in=initial_guess, @@ -813,6 +824,7 @@ def simple_solver(initial_guess=0.0): desal_power_kw, h2_transport_compressor_power_kw, h2_storage_power_kw, + electrolyzer_bop_kw ) #################### solving for energy needed for non-electrolyzer components #################################### diff --git a/greenheart/simulation/technologies/hydrogen/electrolysis/BOP_efficiency_BOL.csv b/greenheart/simulation/technologies/hydrogen/electrolysis/BOP_efficiency_BOL.csv new file mode 100644 index 000000000..c70f2b77b --- /dev/null +++ b/greenheart/simulation/technologies/hydrogen/electrolysis/BOP_efficiency_BOL.csv @@ -0,0 +1,87 @@ +operating_ratio,efficiency +0.937732223,4.285714286 +0.933250382,4.258241758 +0.929665402,4.230769231 +0.925182329,4.217032967 +0.920700488,4.18956044 +0.916217415,4.175824176 +0.911735574,4.148351648 +0.904564382,4.107142857 +0.896495097,4.07967033 +0.887531415,4.024725275 +0.878566501,3.983516484 +0.870498448,3.942307692 +0.862430395,3.901098901 +0.856154832,3.873626374 +0.847189918,3.832417582 +0.837328143,3.791208791 +0.827466368,3.75 +0.818501454,3.708791209 +0.805950328,3.653846154 +0.796985414,3.612637363 +0.786226778,3.571428571 +0.775468142,3.53021978 +0.757537082,3.461538462 +0.743192234,3.406593407 +0.730641108,3.351648352 +0.717193121,3.296703297 +0.703743902,3.255494505 +0.687605332,3.200549451 +0.674156113,3.159340659 +0.661603755,3.118131868 +0.646362046,3.063186813 +0.632017198,3.008241758 +0.614979303,2.980769231 +0.588977726,2.898351648 +0.570147341,2.857142857 +0.550420096,2.815934066 +0.530694082,2.760989011 +0.513656187,2.733516484 +0.490341497,2.692307692 +0.475096092,2.678571429 +0.449089588,2.651098901 +0.415906963,2.637362637 +0.388104272,2.637362637 +0.372856404,2.651098901 +0.348638693,2.678571429 +0.329802149,2.706043956 +0.309169418,2.760989011 +0.286741734,2.82967033 +0.266102843,2.953296703 +0.245462721,3.090659341 +0.228410043,3.228021978 +0.207766225,3.406593407 +0.194302222,3.53021978 +0.181735081,3.653846154 +0.173653477,3.763736264 +0.167365594,3.873626374 +0.16107648,3.997252747 +0.153889272,4.134615385 +0.146698369,4.313186813 +0.138608141,4.519230769 +0.131417237,4.697802198 +0.125120731,4.903846154 +0.120620411,5.082417582 +0.114320209,5.32967033 +0.110717982,5.494505495 +0.107116986,5.645604396 +0.103513527,5.824175824 +0.099907604,6.03021978 +0.097199773,6.222527473 +0.093595082,6.414835165 +0.089987927,6.634615385 +0.086377076,6.895604396 +0.084566107,7.087912088 +0.081858276,7.28021978 +0.079147982,7.5 +0.078235106,7.678571429 +0.076422904,7.884615385 +0.074610703,8.090659341 +0.070994924,8.406593407 +0.069177795,8.667582418 +0.067361898,8.914835165 +0.064645444,9.203296703 +0.06372764,9.436813187 +0.061910511,9.697802198 +0.061894496,9.876373626 +0.060088454,10.01373626 \ No newline at end of file diff --git a/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP.py b/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP.py new file mode 100644 index 000000000..56abbf7a3 --- /dev/null +++ b/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP.py @@ -0,0 +1,129 @@ +import os +import numpy as np +import pandas as pd +import scipy.optimize +import matplotlib.pyplot as plt + +file_path = os.path.dirname(os.path.abspath(__file__)) + +def calc_efficiency(operating_ratio, a, b, c, d, min_ratio, max_ratio, min_efficiency, max_efficiency): + """Calculates efficiency [kwh/kg] given operation ratio with flattened end curves. + + Args: + operating_ratio (list or np.array): Operation ratios. + a (float): Coefficient a. + b (float): Coefficient b. + c (float): Coefficient c. + d (float): Coefficient d. + min_ratio (float): Minimum operating ratio from the CSV. + max_ratio (float): Maximum operating ratio from the CSV. + min_efficiency (float): Efficiency at the minimum operating ratio. + max_efficiency (float): Efficiency at the maximum operating ratio. + """ + # Ensure operating_ratio is an array for easy vectorized calculations + operating_ratio = np.array(operating_ratio) + + # Initialize efficiency array + efficiency = np.zeros_like(operating_ratio) + + # Handle operating ratios less than the min_ratio (flatten at min_efficiency) + efficiency[operating_ratio <= min_ratio] = min_efficiency + + # Handle operating ratios greater than the max_ratio (flatten at max_efficiency) + efficiency[operating_ratio >= max_ratio] = max_efficiency + + # For operating ratios within the bounds, calculate efficiency + within_bounds = (operating_ratio > min_ratio) & (operating_ratio < max_ratio) + efficiency[within_bounds] = ( + a + b * operating_ratio[within_bounds] + + c * operating_ratio[within_bounds]**2 + + d / operating_ratio[within_bounds] + ) + + # Ensure the calculated efficiency doesn't exceed the bounds + efficiency[within_bounds] = np.clip(efficiency[within_bounds], min_efficiency, max_efficiency) + + return efficiency + +def calc_curve_coefficients(): + """Calculates curve coefficients from BOP_efficiency_BOL.csv + """ + df = pd.read_csv(os.path.join(file_path, "BOP_efficiency_BOL.csv")) + operating_ratios = df["operating_ratio"].values + efficiency = df["efficiency"].values + + # Get min and max operating ratios + min_ratio_idx = df["operating_ratio"].idxmin() # Index of minimum operating ratio + max_ratio_idx = df["operating_ratio"].idxmax() # Index of maximum operating ratio + + # Get the efficiency at the min and max operating ratios + min_efficiency = df["efficiency"].iloc[min_ratio_idx] + max_efficiency = df["efficiency"].iloc[max_ratio_idx] + + # Get the actual min and max ratios + min_ratio = df["operating_ratio"].iloc[min_ratio_idx] + max_ratio = df["operating_ratio"].iloc[max_ratio_idx] + + # Fit curve with the modified calc_efficiency that flattens at min/max ratios + curve_coeff, curve_cov = scipy.optimize.curve_fit( + lambda or_, a, b, c, d: calc_efficiency(or_, a, b, c, d, min_ratio, max_ratio, min_efficiency, max_efficiency), + operating_ratios, efficiency, p0=(1.0, 1.0, 1.0, 1.0) + ) + + return curve_coeff, min_ratio, max_ratio, min_efficiency, max_efficiency + + return curve_coeff + +# def calc_efficiency( +# operating_ratio, a, b, c, d +# ): +# """Calculates efficiency [kwh/kg] given operation ratio. + +# Args: +# operating_ratio (_type_): _description_ +# a (_type_): _description_ +# b (_type_): _description_ +# c (_type_): _description_ +# d (_type_): _description_ +# """ +# efficiency = a + b * operating_ratio + c * operating_ratio**2 + d / operating_ratio +# return efficiency + +# def calc_curve_coefficients(): +# """_summary_ +# """ +# df = pd.read_csv(os.path.join(file_path, "BOP_efficiency_BOL.csv")) +# operating_ratios = df["operating_ratio"].values +# efficiency = df["efficiency"].values + +# curve_coeff, curve_cov = scipy.optimize.curve_fit( +# calc_efficiency, operating_ratios, efficiency, p0=(1.0, 1.0, 1.0, 1.0) +# ) +# return curve_coeff + +def pem_bop(power_profile_to_electrolyzer_kw, + electrolyzer_rated_mw): + from greenheart.tools.eco.electrolysis import get_electrolyzer_BOL_efficiency + """_summary_ + + Args: + power_profile_to_electrolyzer_kw (_type_): _description_ + electrolyzer_rated_mw (_type_): _description_ + + Returns: + _type_: _description_ + """ + operating_ratios = power_profile_to_electrolyzer_kw/(electrolyzer_rated_mw*1e3) + + curve_coeff, min_ratio, max_ratio, min_efficiency, max_efficiency = calc_curve_coefficients() + + efficiencies= calc_efficiency(operating_ratios,*curve_coeff,min_ratio,max_ratio,min_efficiency,max_efficiency) # kwh/kg + + BOL_efficiency = get_electrolyzer_BOL_efficiency() #kwh/kg + + BOL_kg = (electrolyzer_rated_mw*1000) / BOL_efficiency #kg/hr + + energy_consumption_bop = efficiencies*BOL_kg #kwh + + return energy_consumption_bop + diff --git a/greenheart/tools/eco/electrolysis.py b/greenheart/tools/eco/electrolysis.py index a9cc31bd0..4e69786ee 100644 --- a/greenheart/tools/eco/electrolysis.py +++ b/greenheart/tools/eco/electrolysis.py @@ -32,6 +32,8 @@ PEM_H2_Clusters as PEMClusters, ) +from greenheart.simulation.technologies.hydrogen.electrolysis.PEM_BOP import pem_bop + # from electrolyzer import run_electrolyzer @@ -443,6 +445,21 @@ def run_electrolyzer_cost( return electrolyzer_cost_results +def run_electrolyzer_bop( + plant_config, + electrolyzer_physics_results, + +): + if "include_bop_power" in plant_config["electrolyzer"]: + if plant_config['electrolyzer']['include_bop_power']: + energy_consumption_bop = pem_bop(electrolyzer_physics_results["power_to_electrolyzer_kw"], + plant_config['electrolyzer']["rating"]) + else: + energy_consumption_bop = np.zeros(len(electrolyzer_physics_results["power_to_electrolyzer_kw"])) + else: + energy_consumption_bop = np.zeros(len(electrolyzer_physics_results["power_to_electrolyzer_kw"])) + return energy_consumption_bop + def run_desal( plant_config, electrolyzer_physics_results, design_scenario, verbose=False diff --git a/greenheart/tools/eco/finance.py b/greenheart/tools/eco/finance.py index 221555861..2203fb667 100644 --- a/greenheart/tools/eco/finance.py +++ b/greenheart/tools/eco/finance.py @@ -1101,7 +1101,7 @@ def run_profast_grid_only( energy_purchase = ( 365 * 24 * greenheart_config["electrolyzer"]["rating"] * 1e3 - + total_accessory_power_renewable_kw + + sum(total_accessory_power_renewable_kw) + total_accessory_power_grid_kw ) @@ -1544,7 +1544,7 @@ def run_profast_full_plant_model( or total_accessory_power_grid_kw > 0 ): - energy_purchase = total_accessory_power_grid_kw * 365 * 24 + energy_purchase = sum(total_accessory_power_grid_kw) # * 365 * 24 if greenheart_config["project_parameters"]["grid_connection"]: annual_energy_shortfall = np.sum(hopp_results["energy_shortfall_hopp"]) diff --git a/greenheart/tools/eco/utilities.py b/greenheart/tools/eco/utilities.py index b131c3459..d2cc45ca6 100644 --- a/greenheart/tools/eco/utilities.py +++ b/greenheart/tools/eco/utilities.py @@ -1408,10 +1408,11 @@ def save_energy_flows( output.update({"battery charge [kW]": [-(int(p<0))*p*1E3 for p in battery_power_out_mw]}) # convert from MW to kW and extract only charging output.update({"battery state of charge [%]": hybrid_plant.battery.outputs.dispatch_SOC}) - output.update({"total renewable energy production hourly [kW]": [solver_results[0]]*simulation_length}) + output.update({"total accessory power required [kW]": solver_results[0]}) output.update({"grid energy usage hourly [kW]": [solver_results[1]]*simulation_length}) output.update({"desal energy hourly [kW]": [solver_results[2]]*simulation_length}) output.update({"electrolyzer energy hourly [kW]": electrolyzer_physics_results["power_to_electrolyzer_kw"]}) + output.update({"electrolyzer bop energy hourly [kW]":solver_results[5]}) output.update({"transport compressor energy hourly [kW]": [solver_results[3]]*simulation_length}) output.update({"storage energy hourly [kW]": [solver_results[4]]*simulation_length}) output.update({"h2 production hourly [kg]": electrolyzer_physics_results["H2_Results"]["Hydrogen Hourly Production [kg/hr]"]}) @@ -1563,7 +1564,7 @@ def post_process_simulation( "electrolyzer_kwh": sum( electrolyzer_physics_results["power_to_electrolyzer_kw"] ), - "renewable_kwh": solver_results[0] * hours, + "renewable_kwh": sum(solver_results[0]), "grid_power_kwh": solver_results[1] * hours, "desal_kwh": solver_results[2] * hours, "h2_transport_compressor_power_kwh": solver_results[3] * hours, From 319e8ce1a9f54fb71ffb6cc2b733724575f37f5c Mon Sep 17 00:00:00 2001 From: kbrunik Date: Wed, 25 Sep 2024 07:53:41 -0500 Subject: [PATCH 02/11] add pem bop tests --- .../simulation/greenheart_simulation.py | 2 - .../hydrogen/electrolysis/PEM_BOP.py | 129 ------------------ .../{ => PEM_BOP}/BOP_efficiency_BOL.csv | 0 .../hydrogen/electrolysis/PEM_BOP/PEM_BOP.py | 120 ++++++++++++++++ greenheart/tools/eco/electrolysis.py | 5 +- .../greenheart/test_hydrogen/test_PEM_bop.py | 45 ++++++ 6 files changed, 168 insertions(+), 133 deletions(-) delete mode 100644 greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP.py rename greenheart/simulation/technologies/hydrogen/electrolysis/{ => PEM_BOP}/BOP_efficiency_BOL.csv (100%) create mode 100644 greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/PEM_BOP.py create mode 100644 tests/greenheart/test_hydrogen/test_PEM_bop.py diff --git a/greenheart/simulation/greenheart_simulation.py b/greenheart/simulation/greenheart_simulation.py index bf239e884..62d30a86a 100644 --- a/greenheart/simulation/greenheart_simulation.py +++ b/greenheart/simulation/greenheart_simulation.py @@ -625,8 +625,6 @@ def energy_internals( greenheart_config, electrolyzer_physics_results ) - print("ElectrolyzEr bop",electrolyzer_energy_consumption_bop_kw[1]) - print("Electrolyzer Energy Consumption BOP",np.mean(electrolyzer_energy_consumption_bop_kw)) desal_results = he_elec.run_desal( hopp_config, electrolyzer_physics_results, design_scenario, verbose diff --git a/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP.py b/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP.py deleted file mode 100644 index 56abbf7a3..000000000 --- a/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP.py +++ /dev/null @@ -1,129 +0,0 @@ -import os -import numpy as np -import pandas as pd -import scipy.optimize -import matplotlib.pyplot as plt - -file_path = os.path.dirname(os.path.abspath(__file__)) - -def calc_efficiency(operating_ratio, a, b, c, d, min_ratio, max_ratio, min_efficiency, max_efficiency): - """Calculates efficiency [kwh/kg] given operation ratio with flattened end curves. - - Args: - operating_ratio (list or np.array): Operation ratios. - a (float): Coefficient a. - b (float): Coefficient b. - c (float): Coefficient c. - d (float): Coefficient d. - min_ratio (float): Minimum operating ratio from the CSV. - max_ratio (float): Maximum operating ratio from the CSV. - min_efficiency (float): Efficiency at the minimum operating ratio. - max_efficiency (float): Efficiency at the maximum operating ratio. - """ - # Ensure operating_ratio is an array for easy vectorized calculations - operating_ratio = np.array(operating_ratio) - - # Initialize efficiency array - efficiency = np.zeros_like(operating_ratio) - - # Handle operating ratios less than the min_ratio (flatten at min_efficiency) - efficiency[operating_ratio <= min_ratio] = min_efficiency - - # Handle operating ratios greater than the max_ratio (flatten at max_efficiency) - efficiency[operating_ratio >= max_ratio] = max_efficiency - - # For operating ratios within the bounds, calculate efficiency - within_bounds = (operating_ratio > min_ratio) & (operating_ratio < max_ratio) - efficiency[within_bounds] = ( - a + b * operating_ratio[within_bounds] - + c * operating_ratio[within_bounds]**2 - + d / operating_ratio[within_bounds] - ) - - # Ensure the calculated efficiency doesn't exceed the bounds - efficiency[within_bounds] = np.clip(efficiency[within_bounds], min_efficiency, max_efficiency) - - return efficiency - -def calc_curve_coefficients(): - """Calculates curve coefficients from BOP_efficiency_BOL.csv - """ - df = pd.read_csv(os.path.join(file_path, "BOP_efficiency_BOL.csv")) - operating_ratios = df["operating_ratio"].values - efficiency = df["efficiency"].values - - # Get min and max operating ratios - min_ratio_idx = df["operating_ratio"].idxmin() # Index of minimum operating ratio - max_ratio_idx = df["operating_ratio"].idxmax() # Index of maximum operating ratio - - # Get the efficiency at the min and max operating ratios - min_efficiency = df["efficiency"].iloc[min_ratio_idx] - max_efficiency = df["efficiency"].iloc[max_ratio_idx] - - # Get the actual min and max ratios - min_ratio = df["operating_ratio"].iloc[min_ratio_idx] - max_ratio = df["operating_ratio"].iloc[max_ratio_idx] - - # Fit curve with the modified calc_efficiency that flattens at min/max ratios - curve_coeff, curve_cov = scipy.optimize.curve_fit( - lambda or_, a, b, c, d: calc_efficiency(or_, a, b, c, d, min_ratio, max_ratio, min_efficiency, max_efficiency), - operating_ratios, efficiency, p0=(1.0, 1.0, 1.0, 1.0) - ) - - return curve_coeff, min_ratio, max_ratio, min_efficiency, max_efficiency - - return curve_coeff - -# def calc_efficiency( -# operating_ratio, a, b, c, d -# ): -# """Calculates efficiency [kwh/kg] given operation ratio. - -# Args: -# operating_ratio (_type_): _description_ -# a (_type_): _description_ -# b (_type_): _description_ -# c (_type_): _description_ -# d (_type_): _description_ -# """ -# efficiency = a + b * operating_ratio + c * operating_ratio**2 + d / operating_ratio -# return efficiency - -# def calc_curve_coefficients(): -# """_summary_ -# """ -# df = pd.read_csv(os.path.join(file_path, "BOP_efficiency_BOL.csv")) -# operating_ratios = df["operating_ratio"].values -# efficiency = df["efficiency"].values - -# curve_coeff, curve_cov = scipy.optimize.curve_fit( -# calc_efficiency, operating_ratios, efficiency, p0=(1.0, 1.0, 1.0, 1.0) -# ) -# return curve_coeff - -def pem_bop(power_profile_to_electrolyzer_kw, - electrolyzer_rated_mw): - from greenheart.tools.eco.electrolysis import get_electrolyzer_BOL_efficiency - """_summary_ - - Args: - power_profile_to_electrolyzer_kw (_type_): _description_ - electrolyzer_rated_mw (_type_): _description_ - - Returns: - _type_: _description_ - """ - operating_ratios = power_profile_to_electrolyzer_kw/(electrolyzer_rated_mw*1e3) - - curve_coeff, min_ratio, max_ratio, min_efficiency, max_efficiency = calc_curve_coefficients() - - efficiencies= calc_efficiency(operating_ratios,*curve_coeff,min_ratio,max_ratio,min_efficiency,max_efficiency) # kwh/kg - - BOL_efficiency = get_electrolyzer_BOL_efficiency() #kwh/kg - - BOL_kg = (electrolyzer_rated_mw*1000) / BOL_efficiency #kg/hr - - energy_consumption_bop = efficiencies*BOL_kg #kwh - - return energy_consumption_bop - diff --git a/greenheart/simulation/technologies/hydrogen/electrolysis/BOP_efficiency_BOL.csv b/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/BOP_efficiency_BOL.csv similarity index 100% rename from greenheart/simulation/technologies/hydrogen/electrolysis/BOP_efficiency_BOL.csv rename to greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/BOP_efficiency_BOL.csv diff --git a/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/PEM_BOP.py b/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/PEM_BOP.py new file mode 100644 index 000000000..721363f24 --- /dev/null +++ b/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/PEM_BOP.py @@ -0,0 +1,120 @@ +import os +import numpy as np +import pandas as pd +import scipy.optimize +import matplotlib.pyplot as plt + +file_path = os.path.dirname(os.path.abspath(__file__)) + + +def calc_efficiency_curve(operating_ratio, a, b, c, d): + """Calculates efficiency [kwh/kg] given operation ratio with flattened end curves. + + Args: + operating_ratio (list or np.array): Operation ratios. + a (float): Coefficient a. + b (float): Coefficient b. + c (float): Coefficient c. + d (float): Coefficient d. + """ + efficiency = a + b * operating_ratio + c * operating_ratio**2 + d / operating_ratio + + return efficiency + + +def calc_efficiency( + operating_ratio, efficiency, min_ratio, max_ratio, min_efficiency, max_efficiency +): + """Adjust efficiency list to not go above minimum or maximum operating ratios in BOP_efficiency_BOL.csv + + Args: + operating_ratio (list or np.array): Operation ratios. + efficiency (list or np.array): Efficiencies calculated using curve fit. + min_ratio (float): Minimum operating ratio from the CSV. + max_ratio (float): Maximum operating ratio from the CSV. + min_efficiency (float): Efficiency at the minimum operating ratio. + max_efficiency (float): Efficiency at the maximum operating ratio. + + Returns: + efficiency (list or np.array): Efficiencies limited with minimum and maximum values. + """ + efficiency = np.where(operating_ratio <= min_ratio, min_efficiency, efficiency) + + efficiency = np.where(operating_ratio >= max_ratio, max_efficiency, efficiency) + return efficiency + + +def calc_curve_coefficients(): + """Calculates curve coefficients from BOP_efficiency_BOL.csv""" + df = pd.read_csv(os.path.join(file_path, "BOP_efficiency_BOL.csv")) + operating_ratios = df["operating_ratio"].values + efficiency = df["efficiency"].values + + # Get min and max operating ratios + min_ratio_idx = df["operating_ratio"].idxmin() # Index of minimum operating ratio + max_ratio_idx = df["operating_ratio"].idxmax() # Index of maximum operating ratio + + # Get the efficiency at the min and max operating ratios + min_efficiency = df["efficiency"].iloc[min_ratio_idx] + max_efficiency = df["efficiency"].iloc[max_ratio_idx] + + # Get the actual min and max ratios + min_ratio = df["operating_ratio"].iloc[min_ratio_idx] + max_ratio = df["operating_ratio"].iloc[max_ratio_idx] + + curve_coeff, curve_cov = scipy.optimize.curve_fit( + calc_efficiency_curve, operating_ratios, efficiency, p0=(1.0, 1.0, 1.0, 1.0) + ) + return curve_coeff, min_ratio, max_ratio, min_efficiency, max_efficiency + + +def pem_bop( + power_profile_to_electrolyzer_kw, + electrolyzer_rated_mw, + electrolyzer_turn_down_ratio, +): + from greenheart.tools.eco.electrolysis import get_electrolyzer_BOL_efficiency + + """Calculate PEM balance of plant energy consumption based on power provided to the electrolyzer. + Args: + power_profile_to_electrolyzer_kw (list or np.array): Power profile to the electrolyzer in kW. + electrolyzer_rated_mw (float): The rating of the PEM electrolyzer in MW. + electrolyzer_turn_down_ratio (float): The electrolyzer turndown ratio. + + Returns: + energy_consumption_bop_kwh (list or np.array): Energy consumed by electrolyzer BOP in kWh. + """ + operating_ratios = power_profile_to_electrolyzer_kw / (electrolyzer_rated_mw * 1e3) + + curve_coeff, min_ratio, max_ratio, min_efficiency, max_efficiency = ( + calc_curve_coefficients() + ) + + efficiencies = calc_efficiency_curve( + operating_ratios, + *curve_coeff, + ) # kwh/kg + + efficiencies = calc_efficiency( + operating_ratios, + efficiencies, + min_ratio, + max_ratio, + min_efficiency, + max_efficiency, + ) + + BOL_efficiency = get_electrolyzer_BOL_efficiency() # kwh/kg + + BOL_kg = (electrolyzer_rated_mw * 1000) / BOL_efficiency # kg/hr + + energy_consumption_bop_kwh = efficiencies * BOL_kg # kwh + + energy_consumption_bop_kwh = np.where( + power_profile_to_electrolyzer_kw + < electrolyzer_turn_down_ratio * electrolyzer_rated_mw * 1000, + 0, + energy_consumption_bop_kwh, + ) + + return energy_consumption_bop_kwh diff --git a/greenheart/tools/eco/electrolysis.py b/greenheart/tools/eco/electrolysis.py index 4e69786ee..0ce05163e 100644 --- a/greenheart/tools/eco/electrolysis.py +++ b/greenheart/tools/eco/electrolysis.py @@ -32,7 +32,7 @@ PEM_H2_Clusters as PEMClusters, ) -from greenheart.simulation.technologies.hydrogen.electrolysis.PEM_BOP import pem_bop +from greenheart.simulation.technologies.hydrogen.electrolysis.PEM_BOP.PEM_BOP import pem_bop # from electrolyzer import run_electrolyzer @@ -453,7 +453,8 @@ def run_electrolyzer_bop( if "include_bop_power" in plant_config["electrolyzer"]: if plant_config['electrolyzer']['include_bop_power']: energy_consumption_bop = pem_bop(electrolyzer_physics_results["power_to_electrolyzer_kw"], - plant_config['electrolyzer']["rating"]) + plant_config["electrolyzer"]["rating"], + plant_config["electrolyzer"]["turndown_ratio"]) else: energy_consumption_bop = np.zeros(len(electrolyzer_physics_results["power_to_electrolyzer_kw"])) else: diff --git a/tests/greenheart/test_hydrogen/test_PEM_bop.py b/tests/greenheart/test_hydrogen/test_PEM_bop.py new file mode 100644 index 000000000..35d8a30ab --- /dev/null +++ b/tests/greenheart/test_hydrogen/test_PEM_bop.py @@ -0,0 +1,45 @@ +from pytest import approx, fixture +import numpy as np + +from greenheart.simulation.technologies.hydrogen.electrolysis.PEM_BOP.PEM_BOP import ( + pem_bop, +) + + +@fixture +def bop_energy(): + power_profile_kw = np.array( + [ + 0, + 9999, # just below turndown ratio + 10000, # exactly at turndown ratio + 82746, + 93774, # max power in csv. largest operating ratio + 100000, # full power + ] + ) + + electrolyzer_rating_mw = 100 # MW + turndown_ratio = 0.1 + + bop_energy = pem_bop( + power_profile_to_electrolyzer_kw=power_profile_kw, + electrolyzer_rated_mw=electrolyzer_rating_mw, + electrolyzer_turn_down_ratio=turndown_ratio, + ) + return bop_energy + + +def test_bop_energy(subtests, bop_energy): + with subtests.test("No power"): + assert bop_energy[0] == 0 + with subtests.test("below turndown"): + assert bop_energy[1] == 0 + with subtests.test("at turndown"): + assert bop_energy[2] == approx(11032.668, 1e-2) + with subtests.test("mid-range power"): + assert bop_energy[3] == approx(6866.87, 1e-2) + with subtests.test("max power in curve"): + assert bop_energy[4] == approx(7847.85) + with subtests.test("full power"): + assert bop_energy[5] == approx(7847.85) From e1ddafe1f5cffcd85e626d9d55456b4da8251759 Mon Sep 17 00:00:00 2001 From: kbrunik Date: Wed, 25 Sep 2024 07:56:42 -0500 Subject: [PATCH 03/11] pem bop readme --- .../hydrogen/electrolysis/PEM_BOP/README.md | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/README.md diff --git a/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/README.md b/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/README.md new file mode 100644 index 000000000..d2988a723 --- /dev/null +++ b/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/README.md @@ -0,0 +1,24 @@ +# Proton Exchange Membrane Water Electrolysis Balance-of-Plant + +This balance-of-plant (BOP) model is derived from Wang et. al (2023). It is represented as an electrical BOP efficiency curve (kWh/kg) at different operating ratios. The operating ratios are the percentage of rated power provided to the electrolyzer. + +The electrical BOP curve includes a transformer and a rectifier to condition alternating current power. + +**NOTE**: BOP assumes AC current as an input and assumes power electronics for AC to DC conversion. BOP efficiency curve has not been optimized for economies of scale or other electrical infrastructure connections. + + +Citation for BOP model. +``` +@Article{en16134964, +AUTHOR = {Wang, Xiaohua and Star, Andrew G. and Ahluwalia, Rajesh K.}, +TITLE = {Performance of Polymer Electrolyte Membrane Water Electrolysis Systems: Configuration, Stack Materials, Turndown and Efficiency}, +JOURNAL = {Energies}, +VOLUME = {16}, +YEAR = {2023}, +NUMBER = {13}, +ARTICLE-NUMBER = {4964}, +URL = {https://www.mdpi.com/1996-1073/16/13/4964}, +ISSN = {1996-1073}, +DOI = {10.3390/en16134964} +} +``` \ No newline at end of file From 6ca919e1c7ea585b199d65409c73b64ab63df59f Mon Sep 17 00:00:00 2001 From: kbrunik Date: Wed, 25 Sep 2024 08:01:45 -0500 Subject: [PATCH 04/11] pem bop readme update --- .../technologies/hydrogen/electrolysis/PEM_BOP/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/README.md b/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/README.md index d2988a723..f11146b0c 100644 --- a/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/README.md +++ b/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/README.md @@ -4,6 +4,8 @@ This balance-of-plant (BOP) model is derived from Wang et. al (2023). It is repr The electrical BOP curve includes a transformer and a rectifier to condition alternating current power. +The model in GreenHEART calculates a curve fit based on the provided CSV it also limits the calculated efficiencies to the maximum and minimum operating ratio values in the CSV, making the overall function a piecewise implementation. + **NOTE**: BOP assumes AC current as an input and assumes power electronics for AC to DC conversion. BOP efficiency curve has not been optimized for economies of scale or other electrical infrastructure connections. From a2045d0b94a22fba860e0b8c91c6ac8bc1a05104 Mon Sep 17 00:00:00 2001 From: kbrunik Date: Thu, 26 Sep 2024 09:02:29 -0500 Subject: [PATCH 05/11] update documentation --- .../hydrogen/electrolysis/PEM_BOP/PEM_BOP.py | 14 +++++++++----- .../hydrogen/electrolysis/PEM_BOP/README.md | 6 +++--- greenheart/tools/eco/electrolysis.py | 4 ++++ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/PEM_BOP.py b/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/PEM_BOP.py index 721363f24..776435318 100644 --- a/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/PEM_BOP.py +++ b/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/PEM_BOP.py @@ -2,13 +2,12 @@ import numpy as np import pandas as pd import scipy.optimize -import matplotlib.pyplot as plt file_path = os.path.dirname(os.path.abspath(__file__)) def calc_efficiency_curve(operating_ratio, a, b, c, d): - """Calculates efficiency [kwh/kg] given operation ratio with flattened end curves. + """Calculates efficiency [kWh/kg] given operation ratio with flattened end curves. Args: operating_ratio (list or np.array): Operation ratios. @@ -16,6 +15,9 @@ def calc_efficiency_curve(operating_ratio, a, b, c, d): b (float): Coefficient b. c (float): Coefficient c. d (float): Coefficient d. + + Returns: + efficiency (list or np.array): Efficiency of electrolyzer BOP in kWh/kg. """ efficiency = a + b * operating_ratio + c * operating_ratio**2 + d / operating_ratio @@ -36,7 +38,7 @@ def calc_efficiency( max_efficiency (float): Efficiency at the maximum operating ratio. Returns: - efficiency (list or np.array): Efficiencies limited with minimum and maximum values. + efficiency (list or np.array): Efficiencies limited with minimum and maximum values in kWh/kg. """ efficiency = np.where(operating_ratio <= min_ratio, min_efficiency, efficiency) @@ -73,9 +75,10 @@ def pem_bop( electrolyzer_rated_mw, electrolyzer_turn_down_ratio, ): - from greenheart.tools.eco.electrolysis import get_electrolyzer_BOL_efficiency + """ + Calculate PEM balance of plant energy consumption at the beginning-of-life + based on power provided to the electrolyzer. - """Calculate PEM balance of plant energy consumption based on power provided to the electrolyzer. Args: power_profile_to_electrolyzer_kw (list or np.array): Power profile to the electrolyzer in kW. electrolyzer_rated_mw (float): The rating of the PEM electrolyzer in MW. @@ -84,6 +87,7 @@ def pem_bop( Returns: energy_consumption_bop_kwh (list or np.array): Energy consumed by electrolyzer BOP in kWh. """ + from greenheart.tools.eco.electrolysis import get_electrolyzer_BOL_efficiency operating_ratios = power_profile_to_electrolyzer_kw / (electrolyzer_rated_mw * 1e3) curve_coeff, min_ratio, max_ratio, min_efficiency, max_efficiency = ( diff --git a/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/README.md b/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/README.md index f11146b0c..06192fd13 100644 --- a/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/README.md +++ b/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/README.md @@ -1,15 +1,15 @@ # Proton Exchange Membrane Water Electrolysis Balance-of-Plant -This balance-of-plant (BOP) model is derived from Wang et. al (2023). It is represented as an electrical BOP efficiency curve (kWh/kg) at different operating ratios. The operating ratios are the percentage of rated power provided to the electrolyzer. +This balance-of-plant (BOP) model is derived from Wang et. al (2023). It is represented as an overall BOP efficiency curve (kWh/kg) at different operating ratios at the beginning-of-life (BOL) of the electrolyzer. The operating ratios are the percentage of rated power provided to the electrolyzer. -The electrical BOP curve includes a transformer and a rectifier to condition alternating current power. +The BOP curve is largely dominated by electrical losses from the power electronics which includes a transformer and a rectifier to condition alternating current power, but there are additional mechanical and hydrogen losses. The model in GreenHEART calculates a curve fit based on the provided CSV it also limits the calculated efficiencies to the maximum and minimum operating ratio values in the CSV, making the overall function a piecewise implementation. **NOTE**: BOP assumes AC current as an input and assumes power electronics for AC to DC conversion. BOP efficiency curve has not been optimized for economies of scale or other electrical infrastructure connections. -Citation for BOP model. +Citation for BOP model. Losses from BOP are shown in Figure 8. in Wang et. al (2023). ``` @Article{en16134964, AUTHOR = {Wang, Xiaohua and Star, Andrew G. and Ahluwalia, Rajesh K.}, diff --git a/greenheart/tools/eco/electrolysis.py b/greenheart/tools/eco/electrolysis.py index 0ce05163e..810fea65f 100644 --- a/greenheart/tools/eco/electrolysis.py +++ b/greenheart/tools/eco/electrolysis.py @@ -455,6 +455,10 @@ def run_electrolyzer_bop( energy_consumption_bop = pem_bop(electrolyzer_physics_results["power_to_electrolyzer_kw"], plant_config["electrolyzer"]["rating"], plant_config["electrolyzer"]["turndown_ratio"]) + warnings.warn( + "Electrolyzer BOP energy consumption is dominated by power electronics (AC-DC conversion and step-down) if electrical system is different consider setting `include_bop_power` to False.", + UserWarning + ) else: energy_consumption_bop = np.zeros(len(electrolyzer_physics_results["power_to_electrolyzer_kw"])) else: From e0061782aaacd17dfd94ef6a2b1bf58d2e813d9b Mon Sep 17 00:00:00 2001 From: kbrunik Date: Fri, 27 Sep 2024 07:33:23 -0500 Subject: [PATCH 06/11] move imports, update docstring --- .../hydrogen/electrolysis/PEM_BOP/PEM_BOP.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/PEM_BOP.py b/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/PEM_BOP.py index 776435318..4dd0673e7 100644 --- a/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/PEM_BOP.py +++ b/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/PEM_BOP.py @@ -2,6 +2,7 @@ import numpy as np import pandas as pd import scipy.optimize +import greenheart.tools.eco.electrolysis as elec # import for get_electrolyzer_BOL_efficiency function file_path = os.path.dirname(os.path.abspath(__file__)) @@ -9,6 +10,13 @@ def calc_efficiency_curve(operating_ratio, a, b, c, d): """Calculates efficiency [kWh/kg] given operation ratio with flattened end curves. + Efficiency curve and general equation structure from Wang et. al (2023). See README.md + in PEM_BOP directory for more details. + + Wang, X.; Star, A.G.; Ahluwalia, R.K. Performance of Polymer Electrolyte Membrane Water Electrolysis Systems: + Configuration, Stack Materials, Turndown and Efficiency. Energies 2023, 16, 4964. + https://doi.org/10.3390/en16134964 + Args: operating_ratio (list or np.array): Operation ratios. a (float): Coefficient a. @@ -87,7 +95,7 @@ def pem_bop( Returns: energy_consumption_bop_kwh (list or np.array): Energy consumed by electrolyzer BOP in kWh. """ - from greenheart.tools.eco.electrolysis import get_electrolyzer_BOL_efficiency + operating_ratios = power_profile_to_electrolyzer_kw / (electrolyzer_rated_mw * 1e3) curve_coeff, min_ratio, max_ratio, min_efficiency, max_efficiency = ( @@ -108,7 +116,7 @@ def pem_bop( max_efficiency, ) - BOL_efficiency = get_electrolyzer_BOL_efficiency() # kwh/kg + BOL_efficiency = elec.get_electrolyzer_BOL_efficiency() # kwh/kg BOL_kg = (electrolyzer_rated_mw * 1000) / BOL_efficiency # kg/hr From 49abf5a3170906a1731a288b5e6ad8a1a26bd2d4 Mon Sep 17 00:00:00 2001 From: kbrunik Date: Fri, 27 Sep 2024 07:41:30 -0500 Subject: [PATCH 07/11] update remaining_power_profile_in calculation --- greenheart/simulation/greenheart_simulation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/greenheart/simulation/greenheart_simulation.py b/greenheart/simulation/greenheart_simulation.py index 62d30a86a..3f0c108de 100644 --- a/greenheart/simulation/greenheart_simulation.py +++ b/greenheart/simulation/greenheart_simulation.py @@ -587,12 +587,13 @@ def energy_internals( ) total_peripheral_energy = power_for_peripherals_kw_in * 365 * 24 distributed_peripheral_power = total_peripheral_energy / high_count + remaining_power_profile_in = np.where( hopp_results["combined_hybrid_power_production_hopp"] - distributed_peripheral_power > 0, hopp_results["combined_hybrid_power_production_hopp"] - distributed_peripheral_power, - hopp_results["combined_hybrid_power_production_hopp"] + 0 ) hopp_results_internal["combined_hybrid_power_production_hopp"] = tuple( From 6a837f9675ff2b38c81406831adbb02006cb6109 Mon Sep 17 00:00:00 2001 From: kbrunik Date: Fri, 27 Sep 2024 08:18:34 -0500 Subject: [PATCH 08/11] update imports to remove circularity --- .../hydrogen/electrolysis/PEM_BOP/PEM_BOP.py | 5 +- .../hydrogen/electrolysis/PEM_tools.py | 60 ++++++++++++++++++ .../hydrogen/electrolysis/__init__.py | 2 + greenheart/tools/eco/electrolysis.py | 61 ------------------- .../greenheart/test_hydrogen/test_PEM_bop.py | 47 ++++++++++++++ 5 files changed, 111 insertions(+), 64 deletions(-) create mode 100644 greenheart/simulation/technologies/hydrogen/electrolysis/PEM_tools.py diff --git a/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/PEM_BOP.py b/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/PEM_BOP.py index 4dd0673e7..a075b6a01 100644 --- a/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/PEM_BOP.py +++ b/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_BOP/PEM_BOP.py @@ -2,7 +2,7 @@ import numpy as np import pandas as pd import scipy.optimize -import greenheart.tools.eco.electrolysis as elec # import for get_electrolyzer_BOL_efficiency function +from greenheart.simulation.technologies.hydrogen.electrolysis.PEM_tools import get_electrolyzer_BOL_efficiency file_path = os.path.dirname(os.path.abspath(__file__)) @@ -95,7 +95,6 @@ def pem_bop( Returns: energy_consumption_bop_kwh (list or np.array): Energy consumed by electrolyzer BOP in kWh. """ - operating_ratios = power_profile_to_electrolyzer_kw / (electrolyzer_rated_mw * 1e3) curve_coeff, min_ratio, max_ratio, min_efficiency, max_efficiency = ( @@ -116,7 +115,7 @@ def pem_bop( max_efficiency, ) - BOL_efficiency = elec.get_electrolyzer_BOL_efficiency() # kwh/kg + BOL_efficiency = get_electrolyzer_BOL_efficiency() # kwh/kg BOL_kg = (electrolyzer_rated_mw * 1000) / BOL_efficiency # kg/hr diff --git a/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_tools.py b/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_tools.py new file mode 100644 index 000000000..6f59341aa --- /dev/null +++ b/greenheart/simulation/technologies/hydrogen/electrolysis/PEM_tools.py @@ -0,0 +1,60 @@ +import numpy as np +from greenheart.simulation.technologies.hydrogen.electrolysis.PEM_H2_LT_electrolyzer_Clusters import ( + PEM_H2_Clusters as PEMClusters, +) + +def create_1MW_reference_PEM(): + pem_param_dict = { + "eol_eff_percent_loss": 10, + "uptime_hours_until_eol": 77600, + "include_degradation_penalty": True, + "turndown_ratio": 0.1, + } + pem = PEMClusters(cluster_size_mw=1, plant_life=30, **pem_param_dict) + return pem + + +def get_electrolyzer_BOL_efficiency(): + pem_1MW = create_1MW_reference_PEM() + bol_eff = pem_1MW.output_dict["BOL Efficiency Curve Info"][ + "Efficiency [kWh/kg]" + ].values[-1] + + return np.round(bol_eff, 2) + +def size_electrolyzer_for_hydrogen_demand( + hydrogen_production_capacity_required_kgphr, + size_for="BOL", + electrolyzer_degradation_power_increase=None, +): + electrolyzer_energy_kWh_per_kg_estimate_BOL = get_electrolyzer_BOL_efficiency() + if size_for == "BOL": + electrolyzer_capacity_MW = ( + hydrogen_production_capacity_required_kgphr + * electrolyzer_energy_kWh_per_kg_estimate_BOL + / 1000 + ) + elif size_for == "EOL": + electrolyzer_energy_kWh_per_kg_estimate_EOL = ( + electrolyzer_energy_kWh_per_kg_estimate_BOL + * (1 + electrolyzer_degradation_power_increase) + ) + electrolyzer_capacity_MW = ( + hydrogen_production_capacity_required_kgphr + * electrolyzer_energy_kWh_per_kg_estimate_EOL + / 1000 + ) + + return electrolyzer_capacity_MW + + +def check_capacity_based_on_clusters(electrolyzer_capacity_BOL_MW, cluster_cap_mw): + + if electrolyzer_capacity_BOL_MW % cluster_cap_mw == 0: + n_pem_clusters_max = electrolyzer_capacity_BOL_MW // cluster_cap_mw + else: + n_pem_clusters_max = int( + np.ceil(np.ceil(electrolyzer_capacity_BOL_MW) / cluster_cap_mw) + ) + electrolyzer_size_mw = n_pem_clusters_max * cluster_cap_mw + return electrolyzer_size_mw \ No newline at end of file diff --git a/greenheart/simulation/technologies/hydrogen/electrolysis/__init__.py b/greenheart/simulation/technologies/hydrogen/electrolysis/__init__.py index 601d9b232..bb42f9352 100644 --- a/greenheart/simulation/technologies/hydrogen/electrolysis/__init__.py +++ b/greenheart/simulation/technologies/hydrogen/electrolysis/__init__.py @@ -1,3 +1,5 @@ from greenheart.simulation.technologies.hydrogen.electrolysis.PEM_costs_Singlitico_model import PEMCostsSingliticoModel from greenheart.simulation.technologies.hydrogen.electrolysis.PEM_electrolyzer_IVcurve import PEM_electrolyzer_LT from greenheart.simulation.technologies.hydrogen.electrolysis.PEM_H2_LT_electrolyzer import PEM_electrolyzer_LT +import greenheart.simulation.technologies.hydrogen.electrolysis.PEM_H2_LT_electrolyzer_Clusters +import greenheart.simulation.technologies.hydrogen.electrolysis.PEM_tools \ No newline at end of file diff --git a/greenheart/tools/eco/electrolysis.py b/greenheart/tools/eco/electrolysis.py index 810fea65f..31a21b187 100644 --- a/greenheart/tools/eco/electrolysis.py +++ b/greenheart/tools/eco/electrolysis.py @@ -28,9 +28,6 @@ from greenheart.simulation.technologies.hydrogen.electrolysis.run_h2_PEM import ( run_h2_PEM, ) -from greenheart.simulation.technologies.hydrogen.electrolysis.PEM_H2_LT_electrolyzer_Clusters import ( - PEM_H2_Clusters as PEMClusters, -) from greenheart.simulation.technologies.hydrogen.electrolysis.PEM_BOP.PEM_BOP import pem_bop @@ -561,61 +558,3 @@ def run_desal( print("\n") return desal_results - - -def create_1MW_reference_PEM(): - pem_param_dict = { - "eol_eff_percent_loss": 10, - "uptime_hours_until_eol": 77600, - "include_degradation_penalty": True, - "turndown_ratio": 0.1, - } - pem = PEMClusters(cluster_size_mw=1, plant_life=30, **pem_param_dict) - return pem - - -def get_electrolyzer_BOL_efficiency(): - pem_1MW = create_1MW_reference_PEM() - bol_eff = pem_1MW.output_dict["BOL Efficiency Curve Info"][ - "Efficiency [kWh/kg]" - ].values[-1] - - return np.round(bol_eff, 2) - - -def size_electrolyzer_for_hydrogen_demand( - hydrogen_production_capacity_required_kgphr, - size_for="BOL", - electrolyzer_degradation_power_increase=None, -): - electrolyzer_energy_kWh_per_kg_estimate_BOL = get_electrolyzer_BOL_efficiency() - if size_for == "BOL": - electrolyzer_capacity_MW = ( - hydrogen_production_capacity_required_kgphr - * electrolyzer_energy_kWh_per_kg_estimate_BOL - / 1000 - ) - elif size_for == "EOL": - electrolyzer_energy_kWh_per_kg_estimate_EOL = ( - electrolyzer_energy_kWh_per_kg_estimate_BOL - * (1 + electrolyzer_degradation_power_increase) - ) - electrolyzer_capacity_MW = ( - hydrogen_production_capacity_required_kgphr - * electrolyzer_energy_kWh_per_kg_estimate_EOL - / 1000 - ) - - return electrolyzer_capacity_MW - - -def check_capacity_based_on_clusters(electrolyzer_capacity_BOL_MW, cluster_cap_mw): - - if electrolyzer_capacity_BOL_MW % cluster_cap_mw == 0: - n_pem_clusters_max = electrolyzer_capacity_BOL_MW // cluster_cap_mw - else: - n_pem_clusters_max = int( - np.ceil(np.ceil(electrolyzer_capacity_BOL_MW) / cluster_cap_mw) - ) - electrolyzer_size_mw = n_pem_clusters_max * cluster_cap_mw - return electrolyzer_size_mw diff --git a/tests/greenheart/test_hydrogen/test_PEM_bop.py b/tests/greenheart/test_hydrogen/test_PEM_bop.py index 35d8a30ab..a49a132e4 100644 --- a/tests/greenheart/test_hydrogen/test_PEM_bop.py +++ b/tests/greenheart/test_hydrogen/test_PEM_bop.py @@ -1,3 +1,4 @@ +import os from pytest import approx, fixture import numpy as np @@ -5,6 +6,7 @@ pem_bop, ) +from ORBIT.core.library import initialize_library @fixture def bop_energy(): @@ -43,3 +45,48 @@ def test_bop_energy(subtests, bop_energy): assert bop_energy[4] == approx(7847.85) with subtests.test("full power"): assert bop_energy[5] == approx(7847.85) + +# dirname = os.path.dirname(__file__) +# library_path = os.path.join(dirname, "input_files/") + +# initialize_library(library_path) + +# turbine_model = "osw_18MW" +# filename_turbine_config = os.path.join( +# library_path, f"turbines/{turbine_model}.yaml" +# ) +# filename_orbit_config = os.path.join( +# library_path, f"plant/orbit-config-{turbine_model}-stripped.yaml" +# ) +# filename_floris_config = os.path.join( +# library_path, f"floris/floris_input_{turbine_model}.yaml" +# ) +# filename_greenheart_config = os.path.join( +# library_path, f"plant/greenheart_config.yaml" +# ) + +# filename_hopp_config = os.path.join( +# library_path, f"plant/hopp_config.yaml" +# ) + +# def test_greenheart_simulation_pem_bop(subtests): +# from greenheart.simulation.greenheart_simulation import ( +# run_simulation, +# GreenHeartSimulationConfig, +# ) +# config = GreenHeartSimulationConfig( +# filename_hopp_config=filename_hopp_config, +# filename_greenheart_config=filename_greenheart_config, +# filename_turbine_config=filename_turbine_config, +# filename_orbit_config=filename_orbit_config, +# filename_floris_config=filename_floris_config, +# verbose=False, +# show_plots=False, +# save_plots=False, +# use_profast=True, +# post_processing=True, +# incentive_option=1, +# plant_design_scenario=1, +# output_level=5, +# ) +# lcoe, lcoh, _, hi = run_simulation(config) \ No newline at end of file From 9604a826883be39f9725827496929366eea46af7 Mon Sep 17 00:00:00 2001 From: kbrunik Date: Fri, 27 Sep 2024 08:21:55 -0500 Subject: [PATCH 09/11] update plant sizing estimation imports --- greenheart/tools/plant_sizing_estimation.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/greenheart/tools/plant_sizing_estimation.py b/greenheart/tools/plant_sizing_estimation.py index 81e6e4692..96c0148c6 100644 --- a/greenheart/tools/plant_sizing_estimation.py +++ b/greenheart/tools/plant_sizing_estimation.py @@ -1,9 +1,7 @@ -import greenheart.tools.eco.electrolysis as he_elec +from greenheart.simulation.technologies.hydrogen.electrolysis import PEM_tools from greenheart.simulation.technologies.steel import steel from greenheart.simulation.technologies.ammonia import ammonia -import warnings -from hopp.utilities import load_yaml -import os + def size_electrolyzer_for_end_use(greenheart_config): @@ -34,8 +32,8 @@ def size_electrolyzer_for_end_use(greenheart_config): deg_power_inc = greenheart_config["electrolyzer"]['eol_eff_percent_loss']/100 bol_or_eol_sizing = greenheart_config["electrolyzer"]["sizing"]["size_for"] cluster_cap_mw = greenheart_config["electrolyzer"]["cluster_rating_MW"] - electrolyzer_capacity_BOL_MW = he_elec.size_electrolyzer_for_hydrogen_demand(hydrogen_production_capacity_required_kgphr, size_for = bol_or_eol_sizing,electrolyzer_degradation_power_increase=deg_power_inc) - electrolyzer_size_mw = he_elec.check_capacity_based_on_clusters(electrolyzer_capacity_BOL_MW,cluster_cap_mw) + electrolyzer_capacity_BOL_MW = PEM_tools.size_electrolyzer_for_hydrogen_demand(hydrogen_production_capacity_required_kgphr, size_for = bol_or_eol_sizing,electrolyzer_degradation_power_increase=deg_power_inc) + electrolyzer_size_mw = PEM_tools.check_capacity_based_on_clusters(electrolyzer_capacity_BOL_MW,cluster_cap_mw) greenheart_config["electrolyzer"]["rating"] = electrolyzer_size_mw greenheart_config["electrolyzer"]["sizing"]["hydrogen_dmd"] = hydrogen_production_capacity_required_kgphr @@ -49,7 +47,6 @@ def run_resizing_estimation(greenheart_config): if greenheart_config["project_parameters"]["grid_connection"]: if greenheart_config["project_parameters"]["hybrid_electricity_estimated_cf"]<1: print("hybrid_electricity_estimated_cf reset to 1 for grid-connected cases") - # warnings.warn("") greenheart_config["project_parameters"]["hybrid_electricity_estimated_cf"] = 1 if greenheart_config["electrolyzer"]["sizing"]["resize_for_enduse"]: From f6903ebc98760a7cd74ed0ac49ac1428ae93587c Mon Sep 17 00:00:00 2001 From: kbrunik Date: Fri, 27 Sep 2024 08:44:12 -0500 Subject: [PATCH 10/11] integration test --- greenheart/tools/eco/utilities.py | 1 + .../greenheart/test_hydrogen/test_PEM_bop.py | 104 ++++++++++-------- 2 files changed, 61 insertions(+), 44 deletions(-) diff --git a/greenheart/tools/eco/utilities.py b/greenheart/tools/eco/utilities.py index d2cc45ca6..ef95b7961 100644 --- a/greenheart/tools/eco/utilities.py +++ b/greenheart/tools/eco/utilities.py @@ -1569,6 +1569,7 @@ def post_process_simulation( "desal_kwh": solver_results[2] * hours, "h2_transport_compressor_power_kwh": solver_results[3] * hours, "h2_storage_power_kwh": solver_results[4] * hours, + "electrolyzer_bop_energy_kwh": sum(solver_results[5]) } diff --git a/tests/greenheart/test_hydrogen/test_PEM_bop.py b/tests/greenheart/test_hydrogen/test_PEM_bop.py index a49a132e4..3ec7ba5fe 100644 --- a/tests/greenheart/test_hydrogen/test_PEM_bop.py +++ b/tests/greenheart/test_hydrogen/test_PEM_bop.py @@ -5,6 +5,10 @@ from greenheart.simulation.technologies.hydrogen.electrolysis.PEM_BOP.PEM_BOP import ( pem_bop, ) +from greenheart.simulation.greenheart_simulation import ( + run_simulation, + GreenHeartSimulationConfig, +) from ORBIT.core.library import initialize_library @@ -46,47 +50,59 @@ def test_bop_energy(subtests, bop_energy): with subtests.test("full power"): assert bop_energy[5] == approx(7847.85) -# dirname = os.path.dirname(__file__) -# library_path = os.path.join(dirname, "input_files/") - -# initialize_library(library_path) - -# turbine_model = "osw_18MW" -# filename_turbine_config = os.path.join( -# library_path, f"turbines/{turbine_model}.yaml" -# ) -# filename_orbit_config = os.path.join( -# library_path, f"plant/orbit-config-{turbine_model}-stripped.yaml" -# ) -# filename_floris_config = os.path.join( -# library_path, f"floris/floris_input_{turbine_model}.yaml" -# ) -# filename_greenheart_config = os.path.join( -# library_path, f"plant/greenheart_config.yaml" -# ) - -# filename_hopp_config = os.path.join( -# library_path, f"plant/hopp_config.yaml" -# ) - -# def test_greenheart_simulation_pem_bop(subtests): -# from greenheart.simulation.greenheart_simulation import ( -# run_simulation, -# GreenHeartSimulationConfig, -# ) -# config = GreenHeartSimulationConfig( -# filename_hopp_config=filename_hopp_config, -# filename_greenheart_config=filename_greenheart_config, -# filename_turbine_config=filename_turbine_config, -# filename_orbit_config=filename_orbit_config, -# filename_floris_config=filename_floris_config, -# verbose=False, -# show_plots=False, -# save_plots=False, -# use_profast=True, -# post_processing=True, -# incentive_option=1, -# plant_design_scenario=1, -# output_level=5, -# ) -# lcoe, lcoh, _, hi = run_simulation(config) \ No newline at end of file +dirname = os.path.dirname(__file__) +parent_dir = os.path.dirname(dirname) + +library_path = os.path.join(parent_dir, "input_files/") + +initialize_library(library_path) + +turbine_model = "osw_18MW" +filename_turbine_config = os.path.join( + library_path, f"turbines/{turbine_model}.yaml" +) +filename_orbit_config = os.path.join( + library_path, f"plant/orbit-config-{turbine_model}-stripped.yaml" +) +filename_floris_config = os.path.join( + library_path, f"floris/floris_input_{turbine_model}.yaml" +) +filename_greenheart_config = os.path.join( + library_path, f"plant/greenheart_config.yaml" +) + +filename_hopp_config = os.path.join( + library_path, f"plant/hopp_config.yaml" +) + +def test_greenheart_simulation_pem_bop(subtests): + config = GreenHeartSimulationConfig( + filename_hopp_config=filename_hopp_config, + filename_greenheart_config=filename_greenheart_config, + filename_turbine_config=filename_turbine_config, + filename_orbit_config=filename_orbit_config, + filename_floris_config=filename_floris_config, + verbose=False, + show_plots=False, + save_plots=False, + use_profast=True, + post_processing=True, + incentive_option=1, + plant_design_scenario=1, + output_level=3, + ) + lcoh,_,_,_,_,_,_,annual_energy_breakdown = run_simulation(config) + + # include electrolyzer bop power consumption in greenheart simulation + config.greenheart_config['electrolyzer']["include_bop_power"] = True + + lcoh2,_,_,_,_,_,_,annual_energy_breakdown2 = run_simulation(config) + + with subtests.test("LCOH not equal"): + assert lcoh != lcoh2 + + with subtests.test("annual_energy_breakdown no electrolyzer bop"): + assert annual_energy_breakdown["electrolyzer_bop_energy_kwh"] == 0 + + with subtests.test("annual_energy_breakdown electrolyzer bop"): + assert annual_energy_breakdown2["electrolyzer_bop_energy_kwh"] == approx(79921217.81100339,1e-3) \ No newline at end of file From 4558d54fbe89b00f06f2c516f66a96cd56181f85 Mon Sep 17 00:00:00 2001 From: kbrunik Date: Fri, 27 Sep 2024 09:17:08 -0500 Subject: [PATCH 11/11] fix imports --- .../greenheart/test_hydrogen/test_electrolyzer_physics.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/greenheart/test_hydrogen/test_electrolyzer_physics.py b/tests/greenheart/test_hydrogen/test_electrolyzer_physics.py index 1dfe9b0c8..c9a97bfd2 100644 --- a/tests/greenheart/test_hydrogen/test_electrolyzer_physics.py +++ b/tests/greenheart/test_hydrogen/test_electrolyzer_physics.py @@ -4,6 +4,7 @@ import pandas as pd import greenheart.tools.eco.electrolysis as he_elec import greenheart.tools.plant_sizing_estimation as gh_sizing +from greenheart.simulation.technologies.hydrogen.electrolysis import PEM_tools from hopp.utilities import load_yaml import os import unittest @@ -222,9 +223,9 @@ def test_grid_electrolyzer_physics(grid_physics,grid_baseline_power_profile,subt def test_electrolyzer_tools(subtests): hydrogen_demand = 8366.311517 cluster_cap_mw = 40 - bol_eff = he_elec.get_electrolyzer_BOL_efficiency() - electrolyzer_capacity_BOL_MW = he_elec.size_electrolyzer_for_hydrogen_demand(hydrogen_demand) - electrolyzer_size_mw = he_elec.check_capacity_based_on_clusters(electrolyzer_capacity_BOL_MW,cluster_cap_mw) + bol_eff = PEM_tools.get_electrolyzer_BOL_efficiency() + electrolyzer_capacity_BOL_MW = PEM_tools.size_electrolyzer_for_hydrogen_demand(hydrogen_demand) + electrolyzer_size_mw = PEM_tools.check_capacity_based_on_clusters(electrolyzer_capacity_BOL_MW,cluster_cap_mw) with subtests.test("electrolyzer BOL efficiency"): assert bol_eff == 54.61