Skip to content
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

Open
wants to merge 24 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
85c663d
updated lcoh calc for to be more aligned with GS methodology
elenya-grant Jan 31, 2025
7d28785
fixed circular import error with electrolyzer cost tools
elenya-grant Jan 31, 2025
35cc318
ran formatter on new file
elenya-grant Jan 31, 2025
f7f00c1
ran formatter on modified files
elenya-grant Feb 1, 2025
a500504
formatted
elenya-grant Feb 3, 2025
5fe5173
actually fixed for tests
elenya-grant Feb 3, 2025
6c5188e
updated tests with new values
elenya-grant Feb 3, 2025
166b44a
Merge branch 'develop' into dev/lcoh_fix
johnjasa Feb 4, 2025
76c543b
modified adjust_dollar_year to be able to take in dictionaries or lis…
elenya-grant Feb 4, 2025
19e38c2
updated electrolyzer cost tools integrated variable om
elenya-grant Feb 4, 2025
1038f8b
updated example input files
elenya-grant Feb 4, 2025
61a1a6b
updated test input files for test_greensteel
elenya-grant Feb 4, 2025
d3dfe11
Merge branch 'dev/lcoh_fix' of github.com:elenya-grant/GreenHEART int…
elenya-grant Feb 4, 2025
b869798
added test for custom PEM cost function
elenya-grant Feb 4, 2025
b299828
changed input for custom pem cost
elenya-grant Feb 4, 2025
1d4c105
update test input file
elenya-grant Feb 4, 2025
90c63dc
adjusted openmdao test assert
elenya-grant Feb 4, 2025
375e7c9
added profast_reverse_tools functions
elenya-grant Feb 4, 2025
fc7b32e
added some docstrings to new functions
elenya-grant Feb 4, 2025
8c91b8f
added more doc strings to profast tools
elenya-grant Feb 5, 2025
59d1d26
updated input file to fix tests
elenya-grant Feb 5, 2025
51a983f
updated doc strings and refurb period in electrolyzer output config
elenya-grant Feb 5, 2025
39bbc1a
moved setting default installation time to setup_greenheart
elenya-grant Feb 5, 2025
2382eee
Merge branch 'develop' into dev/lcoh_fix
johnjasa Feb 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ finance_parameters:
profast_general_inflation: 0.025 # based on 2022 ATB
discount_rate: 0.0948 # nominal return based on 2022 ATB basline workbook
debt_equity_split: 68.5 # 2022 ATB uses 68.5% debt
debt_equity_ratio: False
property_tax: 0.01 # percent of CAPEX # combined with property insurance then between H2A and H2FAST defaults
property_insurance: 0.005 # percent of CAPEX # combined with property tax then between H2A and H2FAST defaults
total_income_tax_rate: 0.2574 # 0.257 tax rate in 2022 atb baseline workbook # current federal income tax rate, but proposed 2023 rate is 0.28. No state income tax in Texas
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ finance_parameters:
profast_general_inflation: 0.025 # based on 2022 ATB
discount_rate: 0.0948 # nominal return based on 2022 ATB basline workbook
debt_equity_split: 68.5 # 2022 ATB uses 68.5% debt
debt_equity_ratio: False
property_tax: 0.01 # percent of CAPEX # combined with property insurance then between H2A and H2FAST defaults
property_insurance: 0.005 # percent of CAPEX # combined with property tax then between H2A and H2FAST defaults
total_income_tax_rate: 0.2574 # 0.257 tax rate in 2022 atb baseline workbook # current federal income tax rate, but proposed 2023 rate is 0.28. No state income tax in Texas
Expand Down
1 change: 0 additions & 1 deletion examples/inputs/plant/greenheart_config_onshore-steel.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ finance_parameters:
profast_general_inflation: 0.025 # based on 2022 ATB
discount_rate: 0.0948 # nominal return based on 2022 ATB basline workbook
debt_equity_split: 68.5 # 2022 ATB uses 68.5% debt
debt_equity_ratio: False
property_tax: 0.01 # percent of CAPEX # combined with property insurance then between H2A and H2FAST defaults
property_insurance: 0.005 # percent of CAPEX # combined with property tax then between H2A and H2FAST defaults
total_income_tax_rate: 0.2574 # 0.257 tax rate in 2022 atb baseline workbook # current federal income tax rate, but proposed 2023 rate is 0.28. No state income tax in Texas
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ finance_parameters:
costing_general_inflation: 0.025 # used to adjust modeled costs to cost_year
profast_general_inflation: 0 # based on 2022 ATB
discount_rate: 0.0948 # nominal return based on 2022 ATB basline workbook
debt_equity_split: False # 2022 ATB uses 68.5% debt
debt_equity_ratio: 1.72
property_tax: 0.01 # percent of CAPEX # combined with property insurance then between H2A and H2FAST defaults
property_insurance: 0.005 # percent of CAPEX # combined with property tax then between H2A and H2FAST defaults
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ finance_parameters:
costing_general_inflation: 0.025 # used to adjust modeled costs to cost_year
profast_general_inflation: 0 # based on 2022 ATB
discount_rate: 0.0948 # nominal return based on 2022 ATB basline workbook
debt_equity_split: False # 2022 ATB uses 68.5% debt
debt_equity_ratio: 1.72
property_tax: 0.01 # percent of CAPEX # combined with property insurance then between H2A and H2FAST defaults
property_insurance: 0.005 # percent of CAPEX # combined with property tax then between H2A and H2FAST defaults
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ finance_parameters:
costing_general_inflation: 0.025 # used to adjust modeled costs to cost_year
profast_general_inflation: 0 # based on 2022 ATB
discount_rate: 0.11 # nominal return based on 2022 ATB basline workbook
debt_equity_split: False # 2022 ATB uses 68.5% debt
debt_equity_ratio: 1.72
property_tax: 0.01 # percent of CAPEX # combined with property insurance then between H2A and H2FAST defaults
property_insurance: 0.005 # percent of CAPEX # combined with property tax then between H2A and H2FAST defaults
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ finance_parameters:
costing_general_inflation: 0.025 # used to adjust modeled costs to cost_year
profast_general_inflation: 0 # based on 2022 ATB
discount_rate: 0.11 # nominal return based on 2022 ATB basline workbook
debt_equity_split: False # 2022 ATB uses 68.5% debt
debt_equity_ratio: 1.72
property_tax: 0.01 # percent of CAPEX # combined with property insurance then between H2A and H2FAST defaults
property_insurance: 0.005 # percent of CAPEX # combined with property tax then between H2A and H2FAST defaults
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ finance_parameters:
costing_general_inflation: 0.025 # used to adjust modeled costs to cost_year
profast_general_inflation: 0 # based on 2022 ATB
discount_rate: 0.11 # nominal return based on 2022 ATB basline workbook
debt_equity_split: False # 2022 ATB uses 68.5% debt
debt_equity_ratio: 1.72
property_tax: 0.01 # percent of CAPEX # combined with property insurance then between H2A and H2FAST defaults
property_insurance: 0.005 # percent of CAPEX # combined with property tax then between H2A and H2FAST defaults
Expand Down
45 changes: 40 additions & 5 deletions greenheart/simulation/greenheart_simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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"]:
Copy link
Collaborator

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

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
Expand Down Expand Up @@ -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():
Copy link
Collaborator

Choose a reason for hiding this comment

The 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})
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 (
Expand Down Expand Up @@ -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,
Expand All @@ -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: ",
Expand All @@ -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,
Expand All @@ -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,
Expand Down
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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 fixed_om_per_kw - note that the doc string is not fully correct on the units, or the usage in the function is wrong

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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)
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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"][
Copy link
Collaborator

Choose a reason for hiding this comment

The 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"][
Copy link
Collaborator

Choose a reason for hiding this comment

The 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"][
Copy link
Collaborator

Choose a reason for hiding this comment

The 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"]
Copy link
Collaborator

Choose a reason for hiding this comment

The 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"]:
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Loading