Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
63190d6
added variable into cost and finance models
elenya-grant Aug 28, 2025
e8b1317
added plant life input to cost model tests that were missing it
elenya-grant Aug 28, 2025
ce816a5
added varom output to wombat model
elenya-grant Aug 28, 2025
10f05e5
updated CostModelBaseClass doc string
elenya-grant Aug 28, 2025
cd5ac48
updated changelog
elenya-grant Aug 28, 2025
57575aa
added initial test for variable om in profast and updated profast script
elenya-grant Sep 2, 2025
8e3a5f5
updated added finance tests
elenya-grant Sep 2, 2025
585cd45
updated units in new finance tests
elenya-grant Sep 2, 2025
f21b12c
merged upstream
elenya-grant Sep 2, 2025
a0fcda9
added plant life to test plant configs
elenya-grant Sep 2, 2025
91899f0
Merge branch 'develop' into dev/var_om
johnjasa Sep 5, 2025
8db5a9c
merged in develop
elenya-grant Sep 10, 2025
e116d23
updated feedstock cost model and natgas test
elenya-grant Sep 10, 2025
e3134d7
fixed failing tests in feedstock and custom elec cost
elenya-grant Sep 11, 2025
bc9824f
merged develop into branch
elenya-grant Sep 16, 2025
1518b68
updated variable om tests in test_finance.py
elenya-grant Sep 16, 2025
14df797
Merge branch 'develop' into dev/var_om
johnjasa Sep 18, 2025
68c785d
renamed attr_filter
johnjasa Sep 19, 2025
3cfaeaf
Using create_years_of_operation in tests
johnjasa Sep 19, 2025
34f57f9
Naming fix
johnjasa Sep 19, 2025
2d3259a
Fixing order of args
johnjasa Sep 19, 2025
352e344
Mods due to review feedback
johnjasa Sep 19, 2025
44d5bf5
Merge branch 'develop' into dev/var_om
johnjasa Sep 20, 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
- Added `"custom_electrolyzer_cost"` model, an electrolyzer cost model that allows for user-defined capex and opex values
- Made `pipe` and `cable` substance-agnostic rather than hard-coded for `hydrogen` and `electricity`
- Change finance handling to use `finance_subgroups` and `finance_groups` defined in the `plant_config` rather than previous `financial_groups` in the `tech_config` and `technologies_to_include_in_metrics` in `plant_config`
- Added variable O&M to `CostModelBaseClass` and integrated into finance-related models
- Added generic storage model, useful for battery, hydrogen, CO2, or other resource storage.
- Added wind resource model, API baseclasses, updated examples, and documentation.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,11 @@

def test_simple_ammonia_performance_model():
plant_info = {
"plant_life": 30,
"simulation": {
"n_timesteps": 2,
"dt": 3600,
}
},
}

prob = om.Problem()
Expand All @@ -67,10 +68,11 @@ def test_simple_ammonia_performance_model():

def test_simple_ammonia_cost_model(subtests):
plant_info = {
"plant_life": 30,
"simulation": {
"n_timesteps": 8760,
"dt": 3600,
}
},
}

prob = om.Problem()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def test_custom_electrolyzer_cost_model(subtests):

plant_config = {
"plant": {
"plant_life": 30,
"simulation": {
"n_timesteps": 8760, # Default number of timesteps for the simulation
},
Expand Down
9 changes: 8 additions & 1 deletion h2integrate/converters/hydrogen/wombat_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,17 @@ def setup(self):
self.config = WOMBATModelConfig.from_dict(
merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance")
)

plant_life = int(self.options["plant_config"]["plant"]["plant_life"])
self.add_output("capacity_factor", val=0.0, units=None)
self.add_output("CapEx", val=0.0, units="USD", desc="Capital expenditure")
self.add_output("OpEx", val=0.0, units="USD/year", desc="Operational expenditure")
self.add_output(
"VarOpEx",
val=0.0,
shape=plant_life,
units="USD/year",
desc="Variable operational expenditure",
)
Comment on lines +47 to +53

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're adding this output as part of this PR but not setting it in the compute. Is this intentional and will it be part of the follow-on PRs you mention in the body?

self.add_discrete_output("cost_year", val=0, desc="Dollar year for costs")
self.add_output(
"percent_hydrogen_lost",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

plant_config = {
"plant": {
"plant_life": 30,
"simulation": {
"n_timesteps": 8760, # Default number of timesteps for the simulation
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,11 @@ def test_utility_pv_cost(
}

plant_info = {
"plant_life": 30,
"simulation": {
"n_timesteps": 8760,
"dt": 3600,
}
},
}

prob = om.Problem()
Expand Down Expand Up @@ -189,10 +190,11 @@ def test_commercial_pv_cost(
}

plant_info = {
"plant_life": 30,
"simulation": {
"n_timesteps": 8760,
"dt": 3600,
}
},
}

prob = om.Problem()
Expand Down Expand Up @@ -243,10 +245,11 @@ def test_residential_pv_cost(
}

plant_info = {
"plant_life": 30,
"simulation": {
"n_timesteps": 8760,
"dt": 3600,
}
},
}

prob = om.Problem()
Expand Down
3 changes: 2 additions & 1 deletion h2integrate/core/feedstocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,5 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs):
cost_per_year = sum(price * hourly_consumption)

outputs["CapEx"] = self.config.start_up_cost
outputs["OpEx"] = self.config.annual_cost + cost_per_year
outputs["OpEx"] = self.config.annual_cost
outputs["VarOpEx"] = cost_per_year
3 changes: 3 additions & 0 deletions h2integrate/core/h2integrate_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,9 @@ def connect_technologies(self):
self.plant.connect(
f"{tech_name}.OpEx", f"finance_subgroup_{group_id}.opex_{tech_name}"
)
self.plant.connect(
f"{tech_name}.VarOpEx", f"finance_subgroup_{group_id}.varopex_{tech_name}"
)
self.plant.connect(
f"{tech_name}.cost_year",
f"finance_subgroup_{group_id}.cost_year_{tech_name}",
Expand Down
11 changes: 7 additions & 4 deletions h2integrate/core/model_baseclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ class CostModelBaseClass(om.ExplicitComponent):

Outputs:
- CapEx (float): capital expenditure costs in $
- OpEx (float): annual operating expenditure costs in $/year
- OpEx (float): annual fixed operating expenditure costs in $/year
- VarOpEx (float): annual variable operating expenditure costs in $/year

Discrete Outputs:
- cost_year (int): dollar-year corresponding to CapEx and OpEx values.
Expand All @@ -20,15 +21,17 @@ def initialize(self):
self.options.declare("tech_config", types=dict)

def setup(self):
plant_life = int(self.options["plant_config"]["plant"]["plant_life"])
# Define outputs: CapEx and OpEx costs
self.add_output("CapEx", val=0.0, units="USD", desc="Capital expenditure")
self.add_output("OpEx", val=0.0, units="USD/year", desc="Fixed operational expenditure")
self.add_output(
"OpEx",
"VarOpEx",
val=0.0,
shape=plant_life,
units="USD/year",
desc="Total (fixed and variable) operational expenditure",
desc="Variable operational expenditure",
)

# Define discrete outputs: cost_year
self.add_discrete_output(
"cost_year", val=self.config.cost_year, desc="Dollar year for costs"
Expand Down
4 changes: 2 additions & 2 deletions h2integrate/core/test/test_feedstocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def test_single_feedstock_natural_gas(self):

# Check outputs
capex = prob_cost.get_val("feedstock_cost.CapEx")[0]
opex = prob_cost.get_val("feedstock_cost.OpEx")[0]
opex = prob_cost.get_val("feedstock_cost.VarOpEx")[0]

self.assertEqual(capex, 100000.0) # start_up_cost
expected_opex = 0.0 + 4.2 * consumption.sum() # annual_cost + price * consumption
Expand Down Expand Up @@ -235,7 +235,7 @@ def test_variable_pricing(self):
prob.run_model()

# Check that OpEx reflects variable pricing
opex = prob.get_val("feedstock_cost.OpEx")[0]
opex = prob.get_val("feedstock_cost.VarOpEx")[0]
expected_opex = 0.0 + np.sum(hourly_prices * consumption)
self.assertAlmostEqual(opex, expected_opex, places=5)

Expand Down
4 changes: 2 additions & 2 deletions h2integrate/core/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def as_dict(self) -> dict:
Returns:
dict: All key, value pairs required for class re-creation.
"""
return attrs.asdict(self, filter=attr_hopp_filter, value_serializer=attr_serializer)
return attrs.asdict(self, filter=attr_filter, value_serializer=attr_serializer)


@define
Expand All @@ -174,7 +174,7 @@ def attr_serializer(inst: type, field: Attribute, value: Any):
return value


def attr_hopp_filter(inst: Attribute, value: Any) -> bool:
def attr_filter(inst: Attribute, value: Any) -> bool:
if inst.init is False:
return False
if value is None:
Expand Down
16 changes: 14 additions & 2 deletions h2integrate/finances/finances.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import numpy as np
import openmdao.api as om
import numpy_financial as npf

Expand Down Expand Up @@ -44,32 +45,43 @@ def setup(self):
self.target_dollar_year = plant_config["finance_parameters"]["cost_adjustment_parameters"][
"target_dollar_year"
]
plant_life = self.plant_life = int(self.options["plant_config"]["plant"]["plant_life"])

for tech in tech_configs:
self.add_input(f"capex_{tech}", val=0.0, units="USD")
self.add_input(f"opex_{tech}", val=0.0, units="USD/year")
self.add_input(f"varopex_{tech}", val=0.0, shape=plant_life, units="USD/year")

self.add_discrete_input(f"cost_year_{tech}", val=0, desc="Dollar year for costs")

self.add_output(f"capex_adjusted_{tech}", val=0.0, units="USD")
self.add_output(f"opex_adjusted_{tech}", val=0.0, units="USD/year")
self.add_output(f"varopex_adjusted_{tech}", val=0.0, shape=plant_life, units="USD/year")

self.add_output("total_capex_adjusted", val=0.0, units="USD")
self.add_output("total_opex_adjusted", val=0.0, units="USD/year")
self.add_output("total_varopex_adjusted", val=0.0, shape=plant_life, units="USD/year")

def compute(self, inputs, outputs, discrete_inputs, discrete_outputs):
total_capex_adjusted = 0.0
total_opex_adjusted = 0.0
total_varopex_adjusted = np.zeros(self.plant_life)
for tech in self.options["tech_configs"]:
capex = float(inputs[f"capex_{tech}"][0])
opex = float(inputs[f"opex_{tech}"][0])
capex = float(inputs[f"capex_{tech}"])
opex = float(inputs[f"opex_{tech}"])
varopex = inputs[f"varopex_{tech}"]
cost_year = int(discrete_inputs[f"cost_year_{tech}"])
periods = self.target_dollar_year - cost_year
adjusted_capex = -npf.fv(self.inflation_rate, periods, 0.0, capex)
adjusted_opex = -npf.fv(self.inflation_rate, periods, 0.0, opex)
adjusted_varopex = -npf.fv(self.inflation_rate, periods, 0.0, varopex)
outputs[f"capex_adjusted_{tech}"] = adjusted_capex
outputs[f"opex_adjusted_{tech}"] = adjusted_opex
outputs[f"varopex_adjusted_{tech}"] = adjusted_varopex
total_capex_adjusted += adjusted_capex
total_opex_adjusted += adjusted_opex
total_varopex_adjusted += adjusted_varopex

outputs["total_capex_adjusted"] = total_capex_adjusted
outputs["total_opex_adjusted"] = total_opex_adjusted
outputs["total_varopex_adjusted"] = total_varopex_adjusted
Loading