-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
LCOH Update and Custom Electrolyzer Costs #79
base: develop
Are you sure you want to change the base?
Changes from all commits
85c663d
7d28785
35cc318
f7f00c1
a500504
5fe5173
6c5188e
166b44a
76c543b
19e38c2
1038f8b
61a1a6b
d3dfe11
b869798
b299828
1d4c105
90c63dc
375e7c9
fc7b32e
8c91b8f
59d1d26
51a983f
39bbc1a
2382eee
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,6 +29,9 @@ | |
AmmoniaCapacityModelOutputs, | ||
run_ammonia_full_model, | ||
) | ||
from greenheart.simulation.technologies.hydrogen.electrolysis.pem_cost_tools import ( | ||
ElectrolyzerLCOHInputConfig, | ||
) | ||
|
||
|
||
pd.options.mode.chained_assignment = None # default='warn' | ||
|
@@ -124,6 +127,11 @@ def __attrs_post_init__(self): | |
|
||
# if design_scenario["h2_storage_location"] == "turbine": | ||
# plant_config["h2_storage"]["type"] = "turbine" | ||
if "analysis_start_year" not in self.greenheart_config["finance_parameters"]: | ||
analysis_start_year = self.greenheart_config["project_parameters"]["atb_year"] + 2 | ||
self.greenheart_config["finance_parameters"].update( | ||
{"analysis_start_year": analysis_start_year} | ||
) | ||
|
||
if self.electrolyzer_rating_mw is not None: | ||
self.greenheart_config["electrolyzer"]["flag"] = True | ||
|
@@ -316,9 +324,15 @@ def setup_greenheart_simulation(config: GreenHeartSimulationConfig): | |
wind_cost_results = he_fin.run_wind_cost_model( | ||
wind_cost_inputs=wind_config, verbose=config.verbose | ||
) | ||
if "installation_time" not in config.greenheart_config["project_parameters"].keys(): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this should check the value so we can allow the user to specify the time or pull from orbit, but require the installation time to be in the input file regardless. If from orbit, let's just allow a flag that says "orbit" in place of the actual time. |
||
config.greenheart_config["project_parameters"].update( | ||
{"installation_time": wind_cost_results.installation_time} | ||
) | ||
else: | ||
wind_cost_results = None | ||
|
||
if "installation_time" not in config.greenheart_config["project_parameters"].keys(): | ||
config.greenheart_config["project_parameters"].update({"installation_time": 0}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I lean towards not having hidden defaults like this. I'd rather throw an error |
||
# override individual fin_model values with cost_info values | ||
if "wind" in config.hopp_config["technologies"]: | ||
if ("wind_om_per_kw" in config.hopp_config["config"]["cost_info"]) and ( | ||
|
@@ -874,7 +888,7 @@ def simple_solver(initial_guess=0.0): | |
) | ||
|
||
# TODO double check full-system OPEX | ||
opex_annual, opex_breakdown_annual = he_fin.run_opex( | ||
opex_annual, opex_breakdown_annual = he_fin.run_fixed_opex( | ||
hopp_results, | ||
wind_cost_results, | ||
electrolyzer_cost_results, | ||
|
@@ -890,6 +904,15 @@ def simple_solver(initial_guess=0.0): | |
total_export_system_cost=capex_breakdown["electrical_export_system"], | ||
) | ||
|
||
vopex_breakdown_annual = he_fin.run_variable_opex( | ||
electrolyzer_cost_results, config.greenheart_config | ||
) | ||
|
||
opex_breakdown_total = { | ||
"fixed_om": opex_breakdown_annual, | ||
"variable_om": vopex_breakdown_annual, | ||
} | ||
|
||
if config.verbose: | ||
print( | ||
"hybrid plant capacity factor: ", | ||
|
@@ -914,12 +937,24 @@ def simple_solver(initial_guess=0.0): | |
save_plots=config.save_plots, | ||
output_dir=config.output_dir, | ||
) | ||
|
||
electrolyzer_performance_results = ElectrolyzerLCOHInputConfig( | ||
electrolyzer_physics_results=electrolyzer_physics_results, | ||
electrolyzer_config=config.greenheart_config["electrolyzer"], | ||
analysis_start_year=config.greenheart_config["finance_parameters"][ | ||
"analysis_start_year" | ||
], | ||
installation_period_months=config.greenheart_config["project_parameters"][ | ||
"installation_time" | ||
], | ||
) | ||
|
||
lcoh_grid_only, pf_grid_only = he_fin.run_profast_grid_only( | ||
config.greenheart_config, | ||
wind_cost_results, | ||
electrolyzer_physics_results, | ||
electrolyzer_performance_results, | ||
capex_breakdown, | ||
opex_breakdown_annual, | ||
opex_breakdown_total, | ||
hopp_results, | ||
config.design_scenario, | ||
total_accessory_power_renewable_kw, | ||
|
@@ -932,9 +967,9 @@ def simple_solver(initial_guess=0.0): | |
lcoh, pf_lcoh = he_fin.run_profast_full_plant_model( | ||
config.greenheart_config, | ||
wind_cost_results, | ||
electrolyzer_physics_results, | ||
electrolyzer_performance_results, | ||
capex_breakdown, | ||
opex_breakdown_annual, | ||
opex_breakdown_total, | ||
hopp_results, | ||
config.incentive_option, | ||
config.design_scenario, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
def calc_custom_electrolysis_capex_fom(electrolyzer_capacity_kW, electrolyzer_config): | ||
"""calculates electrolyzer total installed capex and fixed O&M based on user-input values. | ||
Only used if greenheart_config["electrolyzer"]["cost_model"] is set to "custom" | ||
Requires additional inputs in greenheart_config["electrolyzer"]: | ||
- fixed_om: electrolyzer fixed o&m in $/kW-year | ||
- electrolyzer_capex: electrolyzer capex in $/kW | ||
|
||
Args: | ||
electrolyzer_capacity_kW (float or int): electrolyzer capacity in kW | ||
electrolyzer_config (dict): ``greenheart_config["electrolyzer"]`` | ||
|
||
Returns: | ||
list(float): ``[capex,fixed_om]``:: | ||
|
||
``capex``: electrolyzer overnight capex in $ | ||
``fixed_om``: electrolyzer fixed O&M in $/year | ||
""" | ||
electrolyzer_capex = electrolyzer_config["electrolyzer_capex"] * electrolyzer_capacity_kW | ||
if "fixed_om" in electrolyzer_config.keys(): | ||
electrolyzer_fopex = electrolyzer_config["fixed_om"] * electrolyzer_capacity_kW | ||
else: | ||
electrolyzer_fopex = 0.0 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should not hardcode a default of 0 here. Let's require the input. also, let's put the units in the name There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. didn't remove the default value of 0 here. but did change doc string and dictionary key value. |
||
return electrolyzer_capex, electrolyzer_fopex |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
import numpy as np | ||
from attrs import field, define | ||
|
||
from greenheart.tools.profast_tools import create_years_of_operation | ||
|
||
|
||
@define | ||
class ElectrolyzerLCOHInputConfig: | ||
"""C | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like the description did not get saved. Please include the description along with any relevant citations if necessary. |
||
|
||
Args: | ||
electrolyzer_physics_results (dict): results from run_electrolyzer_physics() | ||
electrolyzer_config (dict): sub-dictionary of greenheart_config | ||
analysis_start_year (int): analysis start year | ||
installation_period_months (int|float|None): installation period in months. defaults to 36. | ||
""" | ||
|
||
electrolyzer_physics_results: dict | ||
electrolyzer_config: dict | ||
analysis_start_year: int | ||
installation_period_months: int | float | None = field(default=36) | ||
|
||
electrolyzer_capacity_kW: int | float = field(init=False) | ||
project_lifetime_years: int = field(init=False) | ||
long_term_utilization: dict = field(init=False) | ||
rated_capacity_kg_pr_day: float = field(init=False) | ||
water_usage_gal_pr_kg: float = field(init=False) | ||
|
||
electrolyzer_annual_energy_usage_kWh: list[float] = field(init=False) | ||
electrolyzer_eff_kWh_pr_kg: list[float] = field(init=False) | ||
electrolyzer_annual_h2_production_kg: list[float] = field(init=False) | ||
|
||
# simple_replacement_schedule: list[float] = field(init=False) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove commented out code? |
||
# complex_replacement_schedule: list[float] = field(init=False) | ||
|
||
# simple_refurb_cost_percent: list[float] = field(init=False) | ||
# complex_refurb_cost_percent: list[float] = field(init=False) | ||
|
||
refurb_cost_percent: list[float] = field(init=False) | ||
replacement_schedule: list[float] = field(init=False) | ||
|
||
def __attrs_post_init__(self): | ||
annual_performance = self.electrolyzer_physics_results["H2_Results"][ | ||
"Performance Schedules" | ||
] | ||
|
||
#: electrolyzer system capacity in kW | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a mix of units in parentheses and some in text, maybe pick one for consistency? |
||
self.electrolyzer_capacity_kW = self.electrolyzer_physics_results["H2_Results"][ | ||
"system capacity [kW]" | ||
] | ||
|
||
#: int: lifetime of project in years | ||
self.project_lifetime_years = len(annual_performance) | ||
|
||
#: float: electrolyzer beginnning-of-life rated H2 production capacity (kg/day) | ||
self.rated_capacity_kg_pr_day = ( | ||
self.electrolyzer_physics_results["H2_Results"]["Rated BOL: H2 Production [kg/hr]"] * 24 | ||
) | ||
|
||
#: float: water usage in gallons of water per kg of H2 | ||
self.water_usage_gal_pr_kg = self.electrolyzer_physics_results["H2_Results"][ | ||
"Rated BOL: Gal H2O per kg-H2" | ||
] | ||
#: list(float): annual energy consumed by electrolyzer for each year of operation (kWh/year) | ||
self.electrolyzer_annual_energy_usage_kWh = annual_performance[ | ||
"Annual Energy Used [kWh/year]" | ||
].to_list() | ||
#: list(float): annual avg efficiency of electrolyzer for each year of operation (kWh/kg) | ||
self.electrolyzer_eff_kWh_pr_kg = annual_performance[ | ||
"Annual Average Efficiency [kWh/kg]" | ||
].to_list() | ||
#: list(float): annual hydrogen production for each year of operation (kg/year) | ||
self.electrolyzer_annual_h2_production_kg = annual_performance[ | ||
"Annual H2 Production [kg/year]" | ||
] | ||
#: dict: annual capacity factor of electrolyzer for each year of operation | ||
self.long_term_utilization = self.make_lifetime_utilization() | ||
|
||
use_complex_refurb = False | ||
if "complex_refurb" in self.electrolyzer_config.keys(): | ||
if self.electrolyzer_config["complex_refurb"]: | ||
use_complex_refurb = True | ||
|
||
# complex schedule assumes stacks are replaced in the year they reach EOL | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we spell out end-of-life? make it easier for people not hip with the lingo |
||
if use_complex_refurb: | ||
self.replacement_schedule = self.calc_complex_refurb_schedule() | ||
self.refurb_cost_percent = list( | ||
np.array( | ||
self.replacement_schedule * self.electrolyzer_config["replacement_cost_percent"] | ||
) | ||
) | ||
|
||
# simple schedule assumes all stacks are replaced in the same year | ||
else: | ||
self.replacement_schedule = self.calc_simple_refurb_schedule() | ||
self.refurb_cost_percent = list( | ||
np.array( | ||
self.replacement_schedule * self.electrolyzer_config["replacement_cost_percent"] | ||
) | ||
) | ||
|
||
def calc_simple_refurb_schedule(self): | ||
annual_performance = self.electrolyzer_physics_results["H2_Results"][ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. doc string |
||
"Performance Schedules" | ||
] | ||
refurb_simple = np.zeros(len(annual_performance)) | ||
refurb_period = int( | ||
round( | ||
self.electrolyzer_physics_results["H2_Results"]["Time Until Replacement [hrs]"] | ||
/ 8760 | ||
) | ||
) | ||
refurb_simple[refurb_period : len(annual_performance) : refurb_period] = 1.0 | ||
|
||
return refurb_simple | ||
|
||
def calc_complex_refurb_schedule(self): | ||
annual_performance = self.electrolyzer_physics_results["H2_Results"][ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. doc string |
||
"Performance Schedules" | ||
] | ||
refurb_complex = annual_performance["Refurbishment Schedule [MW replaced/year]"].values / ( | ||
self.electrolyzer_capacity_kW / 1e3 | ||
) | ||
return refurb_complex | ||
|
||
def make_lifetime_utilization(self): | ||
annual_performance = self.electrolyzer_physics_results["H2_Results"][ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. doc string |
||
"Performance Schedules" | ||
] | ||
|
||
years_of_operation = create_years_of_operation( | ||
self.project_lifetime_years, | ||
self.analysis_start_year, | ||
self.installation_period_months, | ||
) | ||
|
||
cf_per_year = annual_performance["Capacity Factor [-]"].to_list() | ||
utilization_dict = dict(zip(years_of_operation, cf_per_year)) | ||
return utilization_dict | ||
|
||
|
||
def calc_electrolyzer_variable_om(electrolyzer_physics_results, greenheart_config): | ||
electrolyzer_config = greenheart_config["electrolyzer"] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. doc string |
||
annual_performance = electrolyzer_physics_results["H2_Results"]["Performance Schedules"] | ||
|
||
if "var_om" in electrolyzer_config.keys(): | ||
electrolyzer_vopex_pr_kg = ( | ||
electrolyzer_config["var_om"] | ||
* annual_performance["Annual Average Efficiency [kWh/kg]"].values | ||
) | ||
|
||
if "analysis_start_year" not in greenheart_config["finance_parameters"]: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Adding a note here, since I agree with @jaredthomas68. I think we should throw an error if these aren't set in the greenheart config |
||
analysis_start_year = greenheart_config["project_parameters"]["atb_year"] + 2 | ||
else: | ||
analysis_start_year = greenheart_config["finance_parameters"]["analysis_start_year"] | ||
if "installation_time" not in greenheart_config["project_parameters"]: | ||
installation_period_months = 36 | ||
else: | ||
installation_period_months = greenheart_config["project_parameters"][ | ||
"installation_time" | ||
] | ||
|
||
years_of_operation = create_years_of_operation( | ||
greenheart_config["project_parameters"]["project_lifetime"], | ||
analysis_start_year, | ||
installation_period_months, | ||
) | ||
# $/kg-year | ||
vopex_elec = dict(zip(years_of_operation, electrolyzer_vopex_pr_kg)) | ||
|
||
else: | ||
vopex_elec = 0.0 | ||
return vopex_elec |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather throw an error here than have a hidden default