From a862e696640640af3967f864578e5322c7bfaa32 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:27:16 -0700 Subject: [PATCH 01/63] added draft of performance model baseclass to set outputs --- h2integrate/core/model_baseclasses.py | 68 +++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/h2integrate/core/model_baseclasses.py b/h2integrate/core/model_baseclasses.py index 296cc214a..092641143 100644 --- a/h2integrate/core/model_baseclasses.py +++ b/h2integrate/core/model_baseclasses.py @@ -382,3 +382,71 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): # self.cache_outputs(inputs, outputs, discrete_inputs, discrete_outputs) raise NotImplementedError("This method should be implemented in a subclass.") + + +class PerformanceModelBaseClass(om.ExplicitComponent): + def initialize(self): + self.options.declare("driver_config", types=dict) + self.options.declare("plant_config", types=dict) + self.options.declare("tech_config", types=dict) + + def setup(self): + # Below should be done in subclass that produces hydrogen + # self.commodity = "hydrogen" + # self.commodity_rate_units = "kg/h" + # self.commodity_amount_units = "kg" + # super().setup() + + # Below should be done in subclass that produces electricity + # self.commodity = "electricity" + # self.commodity_rate_units = "kW" + # self.commodity_amount_units = "kW*h" + # super().setup() + + self.n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] + self.dt = self.options["plant_config"]["plant"]["simulation"]["dt"] + self.plant_life = int(self.options["plant_config"]["plant"]["plant_life"]) + + self.set_outputs() + + def set_outputs(self): + # timeseries profiles + self.add_output( + f"{self.commodity}_out", + val=0.0, + shape=self.n_timesteps, + units=self.commodity_rate_units, + ) + # sum over simulation + self.add_output( + f"total_{self.commodity}_produced", val=0.0, units=self.commodity_amount_units + ) + # annual performance estimates + self.add_output( + f"annual_{self.commodity}_produced", + val=0.0, + shape=self.plant_life, + units=f"({self.commodity_amount_units})/year", + ) + self.add_output("replacement_schedule", val=0.0, shape=self.plant_life, units="unitless") + self.add_output( + "capacity_factor", + val=0.0, + shape=self.plant_life, + units="unitless", + desc="Capacity factor", + ) + # system design info + self.add_output( + f"rated_{self.commodity}_production", val=0.0, units=self.commodity_rate_units + ) + self.add_output("operational_life", val=self.plant_life, units="yr") + + def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): + """ + Computation for the OM component. + + For a template class this is not implement and raises an error. + """ + + raise NotImplementedError("This method should be implemented in a subclass.") From bc6a7989d0a2dcc2d7607ec0a1fc9b474db79101 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:49:18 -0700 Subject: [PATCH 02/63] updated wind models for standardized outputs --- h2integrate/converters/wind/atb_wind_cost.py | 13 +++- h2integrate/converters/wind/floris.py | 34 +++++---- .../wind/test/test_atb_wind_with_pysam.py | 8 +- .../converters/wind/test/test_floris_wind.py | 20 +++-- .../converters/wind/test/test_pysam_wind.py | 75 ++++++++++--------- .../converters/wind/wind_plant_baseclass.py | 43 ++--------- h2integrate/converters/wind/wind_pysam.py | 32 +++++--- 7 files changed, 112 insertions(+), 113 deletions(-) diff --git a/h2integrate/converters/wind/atb_wind_cost.py b/h2integrate/converters/wind/atb_wind_cost.py index 63ae5d35a..9db7b1671 100644 --- a/h2integrate/converters/wind/atb_wind_cost.py +++ b/h2integrate/converters/wind/atb_wind_cost.py @@ -38,7 +38,7 @@ class ATBWindPlantCostModel(CostModelBaseClass): Configuration object containing per-kW cost parameters for CapEx and OpEx. Inputs: - total_capacity (float): + rated_electricity_production (float): Rated capacity of the wind farm [kW]. Outputs: @@ -54,11 +54,16 @@ def setup(self): ) super().setup() - self.add_input("total_capacity", val=0.0, units="kW", desc="Wind farm rated capacity in kW") + self.add_input( + "rated_electricity_production", + val=0.0, + units="kW", + desc="Wind farm rated capacity in kW", + ) def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): - capex = self.config.capex_per_kW * inputs["total_capacity"] - opex = self.config.opex_per_kW_per_year * inputs["total_capacity"] + capex = self.config.capex_per_kW * inputs["rated_electricity_production"] + opex = self.config.opex_per_kW_per_year * inputs["rated_electricity_production"] outputs["CapEx"] = capex outputs["OpEx"] = opex diff --git a/h2integrate/converters/wind/floris.py b/h2integrate/converters/wind/floris.py index 623b68322..e1d3c6b46 100644 --- a/h2integrate/converters/wind/floris.py +++ b/h2integrate/converters/wind/floris.py @@ -145,17 +145,17 @@ def setup(self): desc="turbine hub-height", ) - self.add_output( - "total_electricity_produced", - val=0.0, - units="kW*h/year", - desc="Annual energy production from WindPlant", - ) - self.add_output("total_capacity", val=0.0, units="kW", desc="Wind farm rated capacity") - - self.add_output( - "capacity_factor", val=0.0, units="unitless", desc="Wind farm capacity factor" - ) + # self.add_output( + # "total_electricity_produced", + # val=0.0, + # units="kW*h/year", + # desc="Annual energy production from WindPlant", + # ) + # self.add_output("total_capacity", val=0.0, units="kW", desc="Wind farm rated capacity") + + # self.add_output( + # "capacity_factor", val=0.0, units="unitless", desc="Wind farm capacity factor" + # ) super().setup() @@ -282,11 +282,17 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): # set outputs outputs["electricity_out"] = gen - outputs["total_capacity"] = n_turbs * self.wind_turbine_rating_kW + outputs["rated_electricity_production"] = n_turbs * self.wind_turbine_rating_kW - max_production = n_turbs * self.wind_turbine_rating_kW * len(gen) - outputs["total_electricity_produced"] = np.sum(gen) + max_production = n_turbs * self.wind_turbine_rating_kW * len(gen) * (self.dt / 3600) + outputs["total_electricity_produced"] = np.sum(gen) * (self.dt / 3600) outputs["capacity_factor"] = np.sum(gen) / max_production + # NOTE: below is not flexible + hours_per_year = 8760 + hours_simulated = (self.dt / 3600) * self.n_timesteps + outputs["annual_electricity_produced"] = outputs["total_electricity_produced"] * ( + hours_simulated / hours_per_year + ) # 3. Cache the results for future use if enabled self.cache_outputs( diff --git a/h2integrate/converters/wind/test/test_atb_wind_with_pysam.py b/h2integrate/converters/wind/test/test_atb_wind_with_pysam.py index 90ab543d0..8eb392ced 100644 --- a/h2integrate/converters/wind/test/test_atb_wind_with_pysam.py +++ b/h2integrate/converters/wind/test/test_atb_wind_with_pysam.py @@ -112,16 +112,14 @@ def test_wind_plant_costs_with_pysam( wind_plant_config["num_turbines"] * wind_plant_config["turbine_rating_kw"] / 1e3 ) - prob.get_val("wind_plant.electricity_out") - prob.get_val("wind_plant.annual_energy") - prob.get_val("wind_plant.total_capacity") - capex = prob.get_val("wind_cost.CapEx") opex = prob.get_val("wind_cost.OpEx") with subtests.test("wind farm capacity"): assert ( - pytest.approx(prob.get_val("wind_plant.total_capacity", units="MW")[0], rel=1e-6) + pytest.approx( + prob.get_val("wind_plant.rated_electricity_production", units="MW")[0], rel=1e-6 + ) == expected_farm_capacity_MW ) diff --git a/h2integrate/converters/wind/test/test_floris_wind.py b/h2integrate/converters/wind/test/test_floris_wind.py index 4a0e0d0d5..080497c02 100644 --- a/h2integrate/converters/wind/test/test_floris_wind.py +++ b/h2integrate/converters/wind/test/test_floris_wind.py @@ -115,14 +115,16 @@ def test_floris_wind_performance(plant_config_openmeteo, floris_config, subtests with subtests.test("wind farm capacity"): assert ( - pytest.approx(prob.get_val("wind_plant.total_capacity", units="kW")[0], rel=1e-6) + pytest.approx( + prob.get_val("wind_plant.rated_electricity_production", units="kW")[0], rel=1e-6 + ) == 660 * 20 ) with subtests.test("AEP"): assert ( pytest.approx( - prob.get_val("wind_plant.total_electricity_produced", units="kW*h/year")[0], + prob.get_val("wind_plant.annual_electricity_produced", units="kW*h/year")[0], rel=1e-6, ) == 36471.03023616864 * 1e3 @@ -131,7 +133,7 @@ def test_floris_wind_performance(plant_config_openmeteo, floris_config, subtests with subtests.test("total electricity_out"): assert pytest.approx( np.sum(prob.get_val("wind_plant.electricity_out", units="kW")), rel=1e-6 - ) == prob.get_val("wind_plant.total_electricity_produced", units="kW*h/year") + ) == prob.get_val("wind_plant.annual_electricity_produced", units="kW*h/year") def test_floris_caching_changed_config(plant_config_openmeteo, floris_config, subtests): @@ -318,17 +320,19 @@ def test_floris_wind_performance_air_dens(plant_config_wtk, floris_config, subte wind_resource_data = dict(prob.get_val("wind_resource.wind_resource_data")) - initial_aep = prob.get_val("wind_plant.total_electricity_produced", units="kW*h/year")[0] + initial_aep = prob.get_val("wind_plant.annual_electricity_produced", units="kW*h/year")[0] with subtests.test("wind farm capacity"): assert ( - pytest.approx(prob.get_val("wind_plant.total_capacity", units="kW")[0], rel=1e-6) + pytest.approx( + prob.get_val("wind_plant.rated_electricity_production", units="kW")[0], rel=1e-6 + ) == 660 * 20 ) with subtests.test("AEP"): assert ( pytest.approx( - prob.get_val("wind_plant.total_electricity_produced", units="kW*h/year")[0], + prob.get_val("wind_plant.annual_electricity_produced", units="kW*h/year")[0], rel=1e-6, ) == 37007.33639643173 * 1e3 @@ -337,7 +341,7 @@ def test_floris_wind_performance_air_dens(plant_config_wtk, floris_config, subte with subtests.test("total electricity_out"): assert pytest.approx( np.sum(prob.get_val("wind_plant.electricity_out", units="kW")), rel=1e-6 - ) == prob.get_val("wind_plant.total_electricity_produced", units="kW*h/year") + ) == prob.get_val("wind_plant.annual_electricity_produced", units="kW*h/year") # Add elevation to the resource data and rerun floris floris_config["adjust_air_density_for_elevation"] = True @@ -356,7 +360,7 @@ def test_floris_wind_performance_air_dens(plant_config_wtk, floris_config, subte prob.set_val("wind_plant.wind_resource_data", wind_resource_data) prob.run_model() - adjusted_aep = prob.get_val("wind_plant.total_electricity_produced", units="kW*h/year")[0] + adjusted_aep = prob.get_val("wind_plant.annual_electricity_produced", units="kW*h/year")[0] with subtests.test("reduced AEP with air density adjustment"): assert adjusted_aep < initial_aep diff --git a/h2integrate/converters/wind/test/test_pysam_wind.py b/h2integrate/converters/wind/test/test_pysam_wind.py index 44b78c9ab..a15b7a85f 100644 --- a/h2integrate/converters/wind/test/test_pysam_wind.py +++ b/h2integrate/converters/wind/test/test_pysam_wind.py @@ -93,24 +93,25 @@ def test_wind_plant_pysam_no_changes_from_setup( wind_plant_config["num_turbines"] * wind_plant_config["turbine_rating_kw"] / 1e3 ) - prob.get_val("wind_plant.electricity_out") - prob.get_val("wind_plant.annual_energy") - prob.get_val("wind_plant.total_capacity") - with subtests.test("wind farm capacity"): assert ( - pytest.approx(prob.get_val("wind_plant.total_capacity", units="MW")[0], rel=1e-6) + pytest.approx( + prob.get_val("wind_plant.rated_electricity_production", units="MW")[0], rel=1e-6 + ) == expected_farm_capacity_MW ) with subtests.test("wind AEP matches electricity out"): assert pytest.approx( - prob.get_val("wind_plant.annual_energy", units="MW*h/year")[0], rel=1e-6 + prob.get_val("wind_plant.annual_electricity_produced", units="MW*h/year")[0], rel=1e-6 ) == np.sum(prob.get_val("wind_plant.electricity_out", units="MW")) with subtests.test("wind AEP value"): assert ( - pytest.approx(prob.get_val("wind_plant.annual_energy", units="MW*h/year")[0], rel=1e-6) + pytest.approx( + prob.get_val("wind_plant.annual_electricity_produced", units="MW*h/year")[0], + rel=1e-6, + ) == 1014129.048439629 ) @@ -144,24 +145,25 @@ def test_wind_plant_pysam_change_hub_height( wind_plant_config["num_turbines"] * wind_plant_config["turbine_rating_kw"] / 1e3 ) - prob.get_val("wind_plant.electricity_out") - prob.get_val("wind_plant.annual_energy") - prob.get_val("wind_plant.total_capacity") - with subtests.test("wind farm capacity"): assert ( - pytest.approx(prob.get_val("wind_plant.total_capacity", units="MW")[0], rel=1e-6) + pytest.approx( + prob.get_val("wind_plant.rated_electricity_production", units="MW")[0], rel=1e-6 + ) == expected_farm_capacity_MW ) with subtests.test("wind AEP matches electricity out"): assert pytest.approx( - prob.get_val("wind_plant.annual_energy", units="MW*h/year")[0], rel=1e-6 + prob.get_val("wind_plant.annual_electricity_produced", units="MW*h/year")[0], rel=1e-6 ) == np.sum(prob.get_val("wind_plant.electricity_out", units="MW")) with subtests.test("wind AEP value"): assert ( - pytest.approx(prob.get_val("wind_plant.annual_energy", units="MW*h/year")[0], rel=1e-6) + pytest.approx( + prob.get_val("wind_plant.annual_electricity_produced", units="MW*h/year")[0], + rel=1e-6, + ) == 1037360.7950548842 ) @@ -195,24 +197,25 @@ def test_wind_plant_pysam_change_rotor_diameter( wind_plant_config["num_turbines"] * wind_plant_config["turbine_rating_kw"] / 1e3 ) - prob.get_val("wind_plant.electricity_out") - prob.get_val("wind_plant.annual_energy") - prob.get_val("wind_plant.total_capacity") - with subtests.test("wind farm capacity"): assert ( - pytest.approx(prob.get_val("wind_plant.total_capacity", units="MW")[0], rel=1e-6) + pytest.approx( + prob.get_val("wind_plant.rated_electricity_production", units="MW")[0], rel=1e-6 + ) == expected_farm_capacity_MW ) with subtests.test("wind AEP matches electricity out"): assert pytest.approx( - prob.get_val("wind_plant.annual_energy", units="MW*h/year")[0], rel=1e-6 + prob.get_val("wind_plant.annual_electricity_produced", units="MW*h/year")[0], rel=1e-6 ) == np.sum(prob.get_val("wind_plant.electricity_out", units="MW")) with subtests.test("wind AEP value"): assert ( - pytest.approx(prob.get_val("wind_plant.annual_energy", units="MW*h/year")[0], rel=1e-6) + pytest.approx( + prob.get_val("wind_plant.annual_electricity_produced", units="MW*h/year")[0], + rel=1e-6, + ) == 916820.0472438652 ) @@ -245,24 +248,25 @@ def test_wind_plant_pysam_change_turbine_rating( expected_farm_capacity_MW = wind_plant_config["num_turbines"] * new_rating_MW - prob.get_val("wind_plant.electricity_out") - prob.get_val("wind_plant.annual_energy") - prob.get_val("wind_plant.total_capacity") - with subtests.test("wind farm capacity"): assert ( - pytest.approx(prob.get_val("wind_plant.total_capacity", units="MW")[0], rel=1e-6) + pytest.approx( + prob.get_val("wind_plant.rated_electricity_production", units="MW")[0], rel=1e-6 + ) == expected_farm_capacity_MW ) with subtests.test("wind AEP matches electricity out"): assert pytest.approx( - prob.get_val("wind_plant.annual_energy", units="MW*h/year")[0], rel=1e-6 + prob.get_val("wind_plant.annual_electricity_produced", units="MW*h/year")[0], rel=1e-6 ) == np.sum(prob.get_val("wind_plant.electricity_out", units="MW")) with subtests.test("wind AEP value"): assert ( - pytest.approx(prob.get_val("wind_plant.annual_energy", units="MW*h/year")[0], rel=1e-6) + pytest.approx( + prob.get_val("wind_plant.annual_electricity_produced", units="MW*h/year")[0], + rel=1e-6, + ) == 968681.3512372728 ) @@ -295,23 +299,24 @@ def test_wind_plant_pysam_change_n_turbines( expected_farm_capacity_MW = new_num_turbines * wind_plant_config["turbine_rating_kw"] / 1e3 - prob.get_val("wind_plant.electricity_out") - prob.get_val("wind_plant.annual_energy") - prob.get_val("wind_plant.total_capacity") - with subtests.test("wind farm capacity"): assert ( - pytest.approx(prob.get_val("wind_plant.total_capacity", units="MW")[0], rel=1e-6) + pytest.approx( + prob.get_val("wind_plant.rated_electricity_production", units="MW")[0], rel=1e-6 + ) == expected_farm_capacity_MW ) with subtests.test("wind AEP matches electricity out"): assert pytest.approx( - prob.get_val("wind_plant.annual_energy", units="MW*h/year")[0], rel=1e-6 + prob.get_val("wind_plant.annual_electricity_produced", units="MW*h/year")[0], rel=1e-6 ) == np.sum(prob.get_val("wind_plant.electricity_out", units="MW")) with subtests.test("wind AEP value"): assert ( - pytest.approx(prob.get_val("wind_plant.annual_energy", units="MW*h/year")[0], rel=1e-6) + pytest.approx( + prob.get_val("wind_plant.annual_electricity_produced", units="MW*h/year")[0], + rel=1e-6, + ) == 2027210.444644157 ) diff --git a/h2integrate/converters/wind/wind_plant_baseclass.py b/h2integrate/converters/wind/wind_plant_baseclass.py index eb35ca5b8..9ee56e200 100644 --- a/h2integrate/converters/wind/wind_plant_baseclass.py +++ b/h2integrate/converters/wind/wind_plant_baseclass.py @@ -1,21 +1,13 @@ -import openmdao.api as om +from h2integrate.core.model_baseclasses import PerformanceModelBaseClass -class WindPerformanceBaseClass(om.ExplicitComponent): - def initialize(self): - self.options.declare("driver_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - +class WindPerformanceBaseClass(PerformanceModelBaseClass): def setup(self): - n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] - self.add_output( - "electricity_out", - val=0.0, - shape=n_timesteps, - units="kW", - desc="Power output from WindPlant", - ) + self.commodity = "electricity" + self.commodity_rate_units = "kW" + self.commodity_amount_units = "kW*h" + super().setup() + self.add_discrete_input( "wind_resource_data", val={}, @@ -85,24 +77,3 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): """ raise NotImplementedError("This method should be implemented in a subclass.") - - -class WindFinanceBaseClass(om.ExplicitComponent): - def initialize(self): - self.options.declare("driver_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - - def setup(self): - self.add_input("CapEx", val=0.0, units="USD") - self.add_input("OpEx", val=0.0, units="USD/year") - self.add_output("NPV", val=0.0, units="USD", desc="Net present value") - - def compute(self, inputs, outputs): - """ - Computation for the OM component. - - For a template class this is not implement and raises an error. - """ - - raise NotImplementedError("This method should be implemented in a subclass.") diff --git a/h2integrate/converters/wind/wind_pysam.py b/h2integrate/converters/wind/wind_pysam.py index db312f333..4fd3f45b5 100644 --- a/h2integrate/converters/wind/wind_pysam.py +++ b/h2integrate/converters/wind/wind_pysam.py @@ -240,15 +240,15 @@ def setup(self): desc="turbine hub-height in meters", ) - self.add_output( - "annual_energy", - val=0.0, - units="kW*h/year", - desc="Annual energy production from WindPlant in kW", - ) - self.add_output( - "total_capacity", val=0.0, units="kW", desc="Wind farm rated capacity in kW" - ) + # self.add_output( + # "annual_energy", + # val=0.0, + # units="kW*h/year", + # desc="Annual energy production from WindPlant in kW", + # ) + # self.add_output( + # "total_capacity", val=0.0, units="kW", desc="Wind farm rated capacity in kW" + # ) if self.config.create_model_from == "default": self.system_model = Windpower.default(self.config.config_name) @@ -468,8 +468,18 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): self.system_model.execute(0) outputs["electricity_out"] = self.system_model.Outputs.gen - outputs["total_capacity"] = self.system_model.Farm.system_capacity - outputs["annual_energy"] = self.system_model.Outputs.annual_energy + outputs["rated_electricity_production"] = self.system_model.Farm.system_capacity + + # outputs["total_capacity"] = self.system_model.Farm.system_capacity + # outputs["annual_energy"] = self.system_model.Outputs.annual_energy + outputs["total_electricity_produced"] = outputs["electricity_out"].sum() * (self.dt / 3600) + outputs["annual_electricity_produced"] = self.system_model.Outputs.annual_energy + rated_electricity_production_for_sim = ( + self.n_timesteps * outputs["rated_electricity_production"] * (self.dt / 3600) + ) + outputs["capacity_factor"] = ( + outputs["total_electricity_produced"] / rated_electricity_production_for_sim + ) def post_process(self, show_plots=False): def plot_turbine_points( From 73edcd064b7904b02bdb1edc436307fcfa48bd76 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:52:24 -0700 Subject: [PATCH 03/63] minor cleanup to wind pysam --- h2integrate/converters/wind/wind_pysam.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/h2integrate/converters/wind/wind_pysam.py b/h2integrate/converters/wind/wind_pysam.py index 4fd3f45b5..b9b2c4321 100644 --- a/h2integrate/converters/wind/wind_pysam.py +++ b/h2integrate/converters/wind/wind_pysam.py @@ -474,12 +474,10 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): # outputs["annual_energy"] = self.system_model.Outputs.annual_energy outputs["total_electricity_produced"] = outputs["electricity_out"].sum() * (self.dt / 3600) outputs["annual_electricity_produced"] = self.system_model.Outputs.annual_energy - rated_electricity_production_for_sim = ( + max_production = ( self.n_timesteps * outputs["rated_electricity_production"] * (self.dt / 3600) ) - outputs["capacity_factor"] = ( - outputs["total_electricity_produced"] / rated_electricity_production_for_sim - ) + outputs["capacity_factor"] = outputs["total_electricity_produced"] / max_production def post_process(self, show_plots=False): def plot_turbine_points( From d3d5616bef280f89b6ba263f2152c96c761ecd67 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:00:06 -0700 Subject: [PATCH 04/63] minor cleanup to floris outputs --- h2integrate/converters/wind/floris.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/h2integrate/converters/wind/floris.py b/h2integrate/converters/wind/floris.py index e1d3c6b46..a59613650 100644 --- a/h2integrate/converters/wind/floris.py +++ b/h2integrate/converters/wind/floris.py @@ -286,7 +286,7 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): max_production = n_turbs * self.wind_turbine_rating_kW * len(gen) * (self.dt / 3600) outputs["total_electricity_produced"] = np.sum(gen) * (self.dt / 3600) - outputs["capacity_factor"] = np.sum(gen) / max_production + outputs["capacity_factor"] = outputs["total_electricity_produced"].sum() / max_production # NOTE: below is not flexible hours_per_year = 8760 hours_simulated = (self.dt / 3600) * self.n_timesteps From 3a8ad3546ab8ee97611d61cbd63e2a4c00242f09 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:01:07 -0700 Subject: [PATCH 05/63] updated outputs of hydroplant --- .../water_power/hydro_plant_baseclass.py | 48 ------------------- .../water_power/hydro_plant_run_of_river.py | 28 ++++++++--- 2 files changed, 22 insertions(+), 54 deletions(-) delete mode 100644 h2integrate/converters/water_power/hydro_plant_baseclass.py diff --git a/h2integrate/converters/water_power/hydro_plant_baseclass.py b/h2integrate/converters/water_power/hydro_plant_baseclass.py deleted file mode 100644 index 2a6760616..000000000 --- a/h2integrate/converters/water_power/hydro_plant_baseclass.py +++ /dev/null @@ -1,48 +0,0 @@ -import openmdao.api as om - - -class HydroPerformanceBaseClass(om.ExplicitComponent): - def initialize(self): - self.options.declare("driver_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - - def setup(self): - n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] - self.add_output( - "electricity_out", - val=0.0, - shape=n_timesteps, - units="kW", - desc="Power output from HydroPlant", - ) - - def compute(self, inputs, outputs): - """ - Computation for the OM component. - - For a template class this is not implemented and raises an error. - """ - - raise NotImplementedError("This method should be implemented in a subclass.") - - -class HydroFinanceBaseClass(om.ExplicitComponent): - def initialize(self): - self.options.declare("driver_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - - def setup(self): - self.add_input("CapEx", val=0.0, units="USD") - self.add_input("OpEx", val=0.0, units="USD/year") - self.add_output("NPV", val=0.0, units="USD", desc="Net present value") - - def compute(self, inputs, outputs): - """ - Computation for the OM component. - - For a template class this is not implemented and raises an error. - """ - - raise NotImplementedError("This method should be implemented in a subclass.") diff --git a/h2integrate/converters/water_power/hydro_plant_run_of_river.py b/h2integrate/converters/water_power/hydro_plant_run_of_river.py index c4ddd2575..13f1400b5 100644 --- a/h2integrate/converters/water_power/hydro_plant_run_of_river.py +++ b/h2integrate/converters/water_power/hydro_plant_run_of_river.py @@ -2,8 +2,11 @@ from attrs import field, define from h2integrate.core.utilities import BaseConfig, merge_shared_inputs -from h2integrate.core.model_baseclasses import CostModelBaseClass, CostModelBaseConfig -from h2integrate.converters.water_power.hydro_plant_baseclass import HydroPerformanceBaseClass +from h2integrate.core.model_baseclasses import ( + CostModelBaseClass, + CostModelBaseConfig, + PerformanceModelBaseClass, +) @define(kw_only=True) @@ -27,16 +30,17 @@ class RunOfRiverHydroPerformanceConfig(BaseConfig): head: float = field() -class RunOfRiverHydroPerformanceModel(HydroPerformanceBaseClass): +class RunOfRiverHydroPerformanceModel(PerformanceModelBaseClass): """ An OpenMDAO component for modeling the performance of a run-of-river hydropower plant. Computes annual electricity production based on water flow rate and turbine efficiency. """ - def initialize(self): - super().initialize() - def setup(self): + self.commodity = "electricity" + self.commodity_rate_units = "kW" + self.commodity_amount_units = "kW*h" + super().setup() n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] self.config = RunOfRiverHydroPerformanceConfig.from_dict( @@ -61,6 +65,18 @@ def compute(self, inputs, outputs): # Distribute the power output over the number of time steps outputs["electricity_out"] = power_output + outputs["rated_electricity_production"] = plant_capacity_kw + + outputs["total_electricity_produced"] = outputs["electricity_out"].sum() * (self.dt / 3600) + # Estimate annual electricity production + hours_per_year = 8760 + hours_simulated = (self.dt / 3600) * self.n_timesteps + outputs["annual_electricity_produced"] = outputs["total_electricity_produced"] * ( + hours_simulated / hours_per_year + ) + # Calculate capacity factor + max_production = plant_capacity_kw * self.n_timesteps * (self.dt / 3600) + outputs["capacity_factor"] = outputs["total_electricity_produced"].sum() / max_production @define(kw_only=True) From 682959ee7de0ff0116b2d2455bc1bc37494a4411 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:08:30 -0700 Subject: [PATCH 06/63] updated solar pv models for standardized outputs --- .../converters/solar/solar_baseclass.py | 43 +++---------------- h2integrate/converters/solar/solar_pysam.py | 23 +++++++--- .../converters/solar/test/test_pysam_solar.py | 6 +-- 3 files changed, 26 insertions(+), 46 deletions(-) diff --git a/h2integrate/converters/solar/solar_baseclass.py b/h2integrate/converters/solar/solar_baseclass.py index 013881d65..f491c2e6e 100644 --- a/h2integrate/converters/solar/solar_baseclass.py +++ b/h2integrate/converters/solar/solar_baseclass.py @@ -1,26 +1,18 @@ -import openmdao.api as om +from h2integrate.core.model_baseclasses import PerformanceModelBaseClass -class SolarPerformanceBaseClass(om.ExplicitComponent): - def initialize(self): - self.options.declare("driver_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - +class SolarPerformanceBaseClass(PerformanceModelBaseClass): def setup(self): + self.commodity = "electricity" + self.commodity_rate_units = "kW" + self.commodity_amount_units = "kW*h" + super().setup() + self.add_discrete_input( "solar_resource_data", val={}, desc="Solar resource data dictionary", ) - n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] - self.add_output( - "electricity_out", - val=0.0, - shape=n_timesteps, - units="kW", - desc="Power output from SolarPlant", - ) def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): """ @@ -30,24 +22,3 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): """ raise NotImplementedError("This method should be implemented in a subclass.") - - -class SolarFinanceBaseClass(om.ExplicitComponent): - def initialize(self): - self.options.declare("driver_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - - def setup(self): - self.add_input("CapEx", val=0.0, units="USD") - self.add_input("OpEx", val=0.0, units="USD/year") - self.add_output("NPV", val=0.0, units="USD", desc="Net present value") - - def compute(self, inputs, outputs): - """ - Computation for the OM component. - - For a template class this is not implement and raises an error. - """ - - raise NotImplementedError("This method should be implemented in a subclass.") diff --git a/h2integrate/converters/solar/solar_pysam.py b/h2integrate/converters/solar/solar_pysam.py index a61b1b6d3..e6ebd3bdb 100644 --- a/h2integrate/converters/solar/solar_pysam.py +++ b/h2integrate/converters/solar/solar_pysam.py @@ -155,12 +155,12 @@ def setup(self): desc="PV rated capacity in DC", ) self.add_output("system_capacity_AC", val=0.0, units="kW", desc="PV rated capacity in AC") - self.add_output( - "annual_energy", - val=0.0, - units="kW*h/year", - desc="Annual energy production in kWac", - ) + # self.add_output( + # "annual_energy", + # val=0.0, + # units="kW*h/year", + # desc="Annual energy production in kWac", + # ) if self.design_config.create_model_from == "default": self.system_model = Pvwatts.default(self.design_config.config_name) @@ -297,4 +297,13 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): pv_capacity_kWdc = self.system_model.value("system_capacity") dc_ac_ratio = self.system_model.value("dc_ac_ratio") outputs["system_capacity_AC"] = pv_capacity_kWdc / dc_ac_ratio - outputs["annual_energy"] = self.system_model.value("ac_annual") + outputs["rated_electricity_production"] = outputs["system_capacity_AC"] + outputs["total_electricity_produced"] = outputs["electricity_out"].sum() * (self.dt / 3600) + + max_production = ( + outputs["rated_electricity_production"] * self.n_timesteps * (self.dt / 3600) + ) + + outputs["capacity_factor"] = outputs["total_electricity_produced"] / max_production + outputs["annual_electricity_produced"] = self.system_model.value("ac_annual") + # outputs["annual_energy"] = self.system_model.value("ac_annual") diff --git a/h2integrate/converters/solar/test/test_pysam_solar.py b/h2integrate/converters/solar/test/test_pysam_solar.py index 2a0d966cc..93cf844dd 100644 --- a/h2integrate/converters/solar/test/test_pysam_solar.py +++ b/h2integrate/converters/solar/test/test_pysam_solar.py @@ -96,7 +96,7 @@ def test_pvwatts_singleowner_notilt( prob.setup() prob.run_model() - aep = prob.get_val("pv_perf.annual_energy")[0] + aep = prob.get_val("pv_perf.annual_electricity_produced")[0] system_capacity_AC = prob.get_val("pv_perf.system_capacity_AC")[0] system_capacity_DC = prob.get_val("pv_perf.system_capacity_DC")[0] @@ -177,7 +177,7 @@ def test_pvwatts_singleowner_notilt_different_site(basic_pysam_options, plant_co prob.model.set_val("solar_resource.longitude", -102.75) prob.run_model() - aep = prob.get_val("pv_perf.annual_energy")[0] + aep = prob.get_val("pv_perf.annual_electricity_produced")[0] system_capacity_AC = prob.get_val("pv_perf.system_capacity_AC")[0] system_capacity_DC = prob.get_val("pv_perf.system_capacity_DC")[0] @@ -240,7 +240,7 @@ def test_pvwatts_singleowner_withtilt( prob.setup() prob.run_model() - aep = prob.get_val("pv_perf.annual_energy")[0] + aep = prob.get_val("pv_perf.annual_electricity_produced")[0] system_capacity_AC = prob.get_val("pv_perf.system_capacity_AC")[0] system_capacity_DC = prob.get_val("pv_perf.system_capacity_DC")[0] From 0aef02aab85253ec97559eb1b86f05f2163a19d0 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:09:50 -0700 Subject: [PATCH 07/63] updated solar resource integration tests with updated solar model outputs --- .../resource/solar/test/test_pvwatts_integration.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/h2integrate/resource/solar/test/test_pvwatts_integration.py b/h2integrate/resource/solar/test/test_pvwatts_integration.py index dffcfeea6..cf2fe3f83 100644 --- a/h2integrate/resource/solar/test/test_pvwatts_integration.py +++ b/h2integrate/resource/solar/test/test_pvwatts_integration.py @@ -196,7 +196,7 @@ def test_pvwatts_with_himawari7( prob.setup() prob.run_model() - aep = prob.get_val("pv_perf.annual_energy", units="MW*h/year")[0] + aep = prob.get_val("pv_perf.annual_electricity_produced", units="MW*h/year")[0] with subtests.test("AEP"): assert pytest.approx(aep, rel=1e-6) == 473577.280269 @@ -230,7 +230,7 @@ def test_pvwatts_with_himawari8( prob.setup() prob.run_model() - aep = prob.get_val("pv_perf.annual_energy", units="MW*h/year")[0] + aep = prob.get_val("pv_perf.annual_electricity_produced", units="MW*h/year")[0] with subtests.test("AEP"): assert pytest.approx(aep, rel=1e-6) == 411251.781327 @@ -264,7 +264,7 @@ def test_pvwatts_with_meteosat_pm( prob.setup() prob.run_model() - aep = prob.get_val("pv_perf.annual_energy", units="MW*h/year")[0] + aep = prob.get_val("pv_perf.annual_electricity_produced", units="MW*h/year")[0] with subtests.test("AEP"): assert pytest.approx(aep, rel=1e-6) == 410211.9419 @@ -300,7 +300,7 @@ def test_pvwatts_with_himawari_tmy( prob.setup() prob.run_model() - aep = prob.get_val("pv_perf.annual_energy", units="MW*h/year")[0] + aep = prob.get_val("pv_perf.annual_electricity_produced", units="MW*h/year")[0] with subtests.test("AEP"): assert pytest.approx(aep, rel=1e-6) == 510709.633402 @@ -334,7 +334,7 @@ def test_pvwatts_with_meteosat_pm_tmy( prob.setup() prob.run_model() - aep = prob.get_val("pv_perf.annual_energy", units="MW*h/year")[0] + aep = prob.get_val("pv_perf.annual_electricity_produced", units="MW*h/year")[0] with subtests.test("AEP"): assert pytest.approx(aep, rel=1e-6) == 510709.633402 From 8f993d44cdc6bd91f2c0cff18f8f27c88991545a Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:52:27 -0700 Subject: [PATCH 08/63] added fraction_of_year_simulated attribute to performance baseclass --- .../water_power/hydro_plant_run_of_river.py | 7 +++---- h2integrate/converters/wind/floris.py | 6 ++---- h2integrate/core/model_baseclasses.py | 18 +++++++++++++++--- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/h2integrate/converters/water_power/hydro_plant_run_of_river.py b/h2integrate/converters/water_power/hydro_plant_run_of_river.py index 13f1400b5..655927928 100644 --- a/h2integrate/converters/water_power/hydro_plant_run_of_river.py +++ b/h2integrate/converters/water_power/hydro_plant_run_of_river.py @@ -69,11 +69,10 @@ def compute(self, inputs, outputs): outputs["total_electricity_produced"] = outputs["electricity_out"].sum() * (self.dt / 3600) # Estimate annual electricity production - hours_per_year = 8760 - hours_simulated = (self.dt / 3600) * self.n_timesteps - outputs["annual_electricity_produced"] = outputs["total_electricity_produced"] * ( - hours_simulated / hours_per_year + outputs["annual_electricity_produced"] = ( + outputs["total_electricity_produced"] * self.fraction_of_year_simulated ) + # Calculate capacity factor max_production = plant_capacity_kw * self.n_timesteps * (self.dt / 3600) outputs["capacity_factor"] = outputs["total_electricity_produced"].sum() / max_production diff --git a/h2integrate/converters/wind/floris.py b/h2integrate/converters/wind/floris.py index a59613650..ebcac29cc 100644 --- a/h2integrate/converters/wind/floris.py +++ b/h2integrate/converters/wind/floris.py @@ -288,10 +288,8 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): outputs["total_electricity_produced"] = np.sum(gen) * (self.dt / 3600) outputs["capacity_factor"] = outputs["total_electricity_produced"].sum() / max_production # NOTE: below is not flexible - hours_per_year = 8760 - hours_simulated = (self.dt / 3600) * self.n_timesteps - outputs["annual_electricity_produced"] = outputs["total_electricity_produced"] * ( - hours_simulated / hours_per_year + outputs["annual_electricity_produced"] = ( + outputs["total_electricity_produced"] * self.fraction_of_year_simulated ) # 3. Cache the results for future use if enabled diff --git a/h2integrate/core/model_baseclasses.py b/h2integrate/core/model_baseclasses.py index 092641143..3c709344a 100644 --- a/h2integrate/core/model_baseclasses.py +++ b/h2integrate/core/model_baseclasses.py @@ -403,10 +403,18 @@ def setup(self): # self.commodity_amount_units = "kW*h" # super().setup() + # n_timesteps is number of timesteps in a simulation self.n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] + # dt is seconds per timestep self.dt = self.options["plant_config"]["plant"]["simulation"]["dt"] + # plant_life is number of years the plant is expected to operate for self.plant_life = int(self.options["plant_config"]["plant"]["plant_life"]) - + hours_per_year = 8760 + # hours simulated is the number of hours in a simulation + hours_simulated = (self.dt / 3600) * self.n_timesteps + # fraction_of_year_simulated is the ratio of simulation length to length of year + # and may be used to estimate annual performance from simulation performance + self.fraction_of_year_simulated = hours_simulated / hours_per_year self.set_outputs() def set_outputs(self): @@ -421,14 +429,16 @@ def set_outputs(self): self.add_output( f"total_{self.commodity}_produced", val=0.0, units=self.commodity_amount_units ) - # annual performance estimates + # annual performance estimate for commodity produced self.add_output( f"annual_{self.commodity}_produced", val=0.0, shape=self.plant_life, units=f"({self.commodity_amount_units})/year", ) + # lifetime estimate of item replacements, represented as a fraction of the capacity. self.add_output("replacement_schedule", val=0.0, shape=self.plant_life, units="unitless") + # capacity factor is the ratio of actual production / maximum production possible self.add_output( "capacity_factor", val=0.0, @@ -436,10 +446,12 @@ def set_outputs(self): units="unitless", desc="Capacity factor", ) - # system design info + # rated/maximum commodity production, this would be used to calculate the maximum + # production possible over the simulation self.add_output( f"rated_{self.commodity}_production", val=0.0, units=self.commodity_rate_units ) + # operational life of the technology if the technology cannot be replaced self.add_output("operational_life", val=self.plant_life, units="yr") def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): From a660e2100ea40b900c5cd5e1a7f331544c763fbe Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 20 Jan 2026 17:09:59 -0700 Subject: [PATCH 09/63] updated tests for turbine preprocessing tools --- .../test/test_wind_turbine_file_tools.py | 34 ++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/h2integrate/preprocess/test/test_wind_turbine_file_tools.py b/h2integrate/preprocess/test/test_wind_turbine_file_tools.py index 73a717c03..179de8a29 100644 --- a/h2integrate/preprocess/test/test_wind_turbine_file_tools.py +++ b/h2integrate/preprocess/test/test_wind_turbine_file_tools.py @@ -1,3 +1,5 @@ +import shutil + import numpy as np import pytest import openmdao.api as om @@ -74,7 +76,10 @@ def test_pysam_turbine_export(subtests): with subtests.test("File runs with WindPower, check total capacity"): assert ( - pytest.approx(prob.get_val("wind_plant.total_capacity", units="MW"), rel=1e-6) == 300.0 + pytest.approx( + prob.get_val("wind_plant.rated_electricity_production", units="MW"), rel=1e-6 + ) + == 300.0 ) with subtests.test("File runs with WindPower, check turbine size"): @@ -85,7 +90,9 @@ def test_pysam_turbine_export(subtests): with subtests.test("File runs with WindPower, check AEP"): assert ( - pytest.approx(prob.get_val("wind_plant.annual_energy", units="MW*h/yr")[0], rel=1e-6) + pytest.approx( + prob.get_val("wind_plant.annual_electricity_produced", units="MW*h/yr")[0], rel=1e-6 + ) == 1391425.64 ) @@ -106,9 +113,13 @@ def test_floris_turbine_export(subtests): plant_config = load_plant_yaml(plant_config_path) tech_config = load_tech_yaml(tech_config_path) + cache_dir = tech_config_path.parent / "test_cache" + updated_parameters = { "hub_height": -1, "floris_turbine_config": floris_options, + "enable_caching": True, + "cache_dir": cache_dir, } tech_config["technologies"]["wind"]["model_inputs"]["performance_parameters"].update( @@ -135,7 +146,10 @@ def test_floris_turbine_export(subtests): with subtests.test("File runs with Floris, check total capacity"): assert ( - pytest.approx(prob.get_val("wind_plant.total_capacity", units="MW"), rel=1e-6) == 600.0 + pytest.approx( + prob.get_val("wind_plant.rated_electricity_production", units="MW"), rel=1e-6 + ) + == 600.0 ) with subtests.test("File runs with Floris, check turbine size"): @@ -153,10 +167,22 @@ def test_floris_turbine_export(subtests): == 53.556784 ) + with subtests.test("File runs with Floris, check total electricity produced"): + assert ( + pytest.approx( + prob.get_val("wind_plant.total_electricity_produced", units="MW*h")[0], rel=1e-6 + ) + == 2814944.574 + ) + with subtests.test("File runs with Floris, check AEP"): assert ( pytest.approx( - prob.get_val("wind_plant.total_electricity_produced", units="MW*h/yr")[0], rel=1e-6 + prob.get_val("wind_plant.annual_electricity_produced", units="MW*h/yr")[0], rel=1e-6 ) == 2814944.574 ) + + # delete cache dir if it exists + if cache_dir.exists(): + shutil.rmtree(cache_dir) From 1f9788ac143d4086a790574e94b3123d9153c21c Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 20 Jan 2026 17:16:10 -0700 Subject: [PATCH 10/63] fixed variable naming in test_all_examples for wind and solar --- examples/test/test_all_examples.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/examples/test/test_all_examples.py b/examples/test/test_all_examples.py index 5e90dfb41..598645a1f 100644 --- a/examples/test/test_all_examples.py +++ b/examples/test/test_all_examples.py @@ -1245,9 +1245,9 @@ def test_sweeping_solar_sites_doe(subtests): for ci, case in enumerate(cases): solar_resource_data = case.get_val("site.solar_resource.solar_resource_data") lat_lon = f"{case.get_val('site.latitude')[0]} {case.get_val('site.longitude')[0]}" - solar_capacity = case.get_design_vars()["solar.system_capacity_DC"] - aep = case.get_val("solar.annual_energy", units="MW*h/yr") - lcoe = case.get_val("finance_subgroup_electricity.LCOE_optimistic", units="USD/(MW*h)") + solar_capacity = case.get_design_vars()["solar.system_capacity_DC"][0] + aep = case.get_val("solar.annual_electricity_produced", units="MW*h/yr")[0] + lcoe = case.get_val("finance_subgroup_electricity.LCOE_optimistic", units="USD/(MW*h)")[0] site_res = pd.DataFrame( [aep, lcoe, solar_capacity], index=["AEP", "LCOE", "solar_capacity"], columns=[lat_lon] @@ -1312,12 +1312,17 @@ def test_floris_example(subtests): ) with subtests.test("Wind plant capacity"): - assert pytest.approx(h2i.prob.get_val("wind.total_capacity", units="MW"), rel=1e-6) == 66.0 + assert ( + pytest.approx( + h2i.prob.get_val("wind.rated_electricity_production", units="MW"), rel=1e-6 + ) + == 66.0 + ) with subtests.test("Total electricity production"): assert ( pytest.approx( - np.sum(h2i.prob.get_val("wind.total_electricity_produced", units="MW*h/yr")), + np.sum(h2i.prob.get_val("wind.total_electricity_produced", units="MW*h")), rel=1e-6, ) == 128948.21977 From d1f238747be1c94291abbda36629e0417423752d Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Wed, 21 Jan 2026 09:38:58 -0700 Subject: [PATCH 11/63] started updating electrolyzer model outputs --- .../hydrogen/electrolyzer_baseclass.py | 38 +++++++++++++++---- .../converters/hydrogen/pem_electrolyzer.py | 38 +++++++++++++++---- .../hydrogen/test/test_wombat_model.py | 4 +- .../converters/hydrogen/wombat_model.py | 2 +- h2integrate/core/h2integrate_model.py | 2 +- h2integrate/finances/profast_base.py | 1 + 6 files changed, 66 insertions(+), 19 deletions(-) diff --git a/h2integrate/converters/hydrogen/electrolyzer_baseclass.py b/h2integrate/converters/hydrogen/electrolyzer_baseclass.py index 09692255d..11972f752 100644 --- a/h2integrate/converters/hydrogen/electrolyzer_baseclass.py +++ b/h2integrate/converters/hydrogen/electrolyzer_baseclass.py @@ -2,22 +2,42 @@ from h2integrate.core.model_baseclasses import ( CostModelBaseClass, + PerformanceModelBaseClass, ResizeablePerformanceModelBaseClass, ) -class ElectrolyzerPerformanceBaseClass(ResizeablePerformanceModelBaseClass): +class ElectrolyzerPerformanceBaseClass( + ResizeablePerformanceModelBaseClass, PerformanceModelBaseClass +): def setup(self): + self.commodity = "hydrogen" + self.commodity_rate_units = "kg/h" + self.commodity_amount_units = "kg" n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] super().setup() + + # n_timesteps is number of timesteps in a simulation + self.n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] + # dt is seconds per timestep + self.dt = self.options["plant_config"]["plant"]["simulation"]["dt"] + # plant_life is number of years the plant is expected to operate for + self.plant_life = int(self.options["plant_config"]["plant"]["plant_life"]) + hours_per_year = 8760 + # hours simulated is the number of hours in a simulation + hours_simulated = (self.dt / 3600) * self.n_timesteps + # fraction_of_year_simulated is the ratio of simulation length to length of year + # and may be used to estimate annual performance from simulation performance + self.fraction_of_year_simulated = hours_simulated / hours_per_year + + self.set_outputs() # Define inputs for electricity and outputs for hydrogen and oxygen generation self.add_input("electricity_in", val=0.0, shape=n_timesteps, units="kW") - self.add_output("hydrogen_out", val=0.0, shape=n_timesteps, units="kg/h") - self.add_output( - "time_until_replacement", val=80000.0, units="h", desc="Time until replacement" - ) - - self.add_output("total_hydrogen_produced", val=0.0, units="kg/year") + # self.add_output("hydrogen_out", val=0.0, shape=n_timesteps, units="kg/h") + # self.add_output( + # "time_until_replacement", val=80000.0, units="h", desc="Time until replacement" + # ) + # self.add_output("total_hydrogen_produced", val=0.0, units="kg/year") def compute(self, inputs, outputs): """ @@ -33,7 +53,9 @@ class ElectrolyzerCostBaseClass(CostModelBaseClass): def setup(self): n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] super().setup() - self.add_input("total_hydrogen_produced", val=0.0, units="kg/year") + self.add_input( + "total_hydrogen_produced", val=0.0, units="kg" + ) # NOTE: unsure if this is used self.add_input("electricity_in", val=0.0, shape=n_timesteps, units="kW") diff --git a/h2integrate/converters/hydrogen/pem_electrolyzer.py b/h2integrate/converters/hydrogen/pem_electrolyzer.py index faa7fe86d..1051b6e1f 100644 --- a/h2integrate/converters/hydrogen/pem_electrolyzer.py +++ b/h2integrate/converters/hydrogen/pem_electrolyzer.py @@ -66,11 +66,14 @@ def setup(self): ) super().setup() self.add_output("efficiency", val=0.0, desc="Average efficiency of the electrolyzer") + # self.add_output( + # "rated_h2_production_kg_pr_hr", + # val=0.0, + # units="kg/h", + # desc="Rated hydrogen production of system in kg/hour", + # ) self.add_output( - "rated_h2_production_kg_pr_hr", - val=0.0, - units="kg/h", - desc="Rated hydrogen production of system in kg/hour", + "time_until_replacement", val=80000.0, units="h", desc="Time until replacement" ) self.add_input( @@ -88,7 +91,8 @@ def setup(self): ) self.add_input("cluster_size", val=-1.0, units="MW") self.add_input("max_hydrogen_capacity", val=1000.0, units="kg/h") - self.add_output("hydrogen_capacity_factor", val=0.0, units="unitless") + # TODO: add feedstock inputs and consumption outputs + # self.add_output("hydrogen_capacity_factor", val=0.0, units="unitless") def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): plant_life = self.options["plant_config"]["plant"]["plant_life"] @@ -155,9 +159,27 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): # Assuming `h2_results` includes hydrogen and oxygen rates per timestep outputs["hydrogen_out"] = H2_Results["Hydrogen Hourly Production [kg/hr]"] - outputs["total_hydrogen_produced"] = H2_Results["Life: Annual H2 production [kg/year]"] + outputs["total_hydrogen_produced"] = outputs["hydrogen_out"].sum() outputs["efficiency"] = H2_Results["Sim: Average Efficiency [%-HHV]"] + refurb_schedule = np.zeros(self.plant_life) + refurb_period = round(float(H2_Results["Time Until Replacement [hrs]"]) / (24 * 365)) + refurb_schedule[refurb_period : self.plant_life : refurb_period] = 1 + + outputs["replacement_schedule"] = refurb_schedule + # NOTE: could replace above with line with below: + # outputs["replacement_schedule"] = (H2_Results["Performance Schedules"] + # ['Refurbishment Schedule [MW replaced/year]'].values + # /electrolyzer_actual_capacity_MW + # ) + + # TODO: remove time_until_replacement as output after finance model(s) have been updated to not use it outputs["time_until_replacement"] = H2_Results["Time Until Replacement [hrs]"] - outputs["rated_h2_production_kg_pr_hr"] = H2_Results["Rated BOL: H2 Production [kg/hr]"] + + outputs["rated_hydrogen_production"] = H2_Results["Rated BOL: H2 Production [kg/hr]"] outputs["electrolyzer_size_mw"] = electrolyzer_actual_capacity_MW - outputs["hydrogen_capacity_factor"] = H2_Results["Life: Capacity Factor"] + outputs["capacity_factor"] = H2_Results["Performance Schedules"][ + "Capacity Factor [-]" + ].values + outputs["annual_hydrogen_produced"] = H2_Results["Performance Schedules"][ + "Annual H2 Production [kg/year]" + ].values diff --git a/h2integrate/converters/hydrogen/test/test_wombat_model.py b/h2integrate/converters/hydrogen/test/test_wombat_model.py index d675f9dcf..12d293516 100644 --- a/h2integrate/converters/hydrogen/test/test_wombat_model.py +++ b/h2integrate/converters/hydrogen/test/test_wombat_model.py @@ -16,6 +16,7 @@ def test_wombat_model_outputs(subtests): "plant_life": 20, "simulation": { "n_timesteps": 8760, + "dt": 3600, }, }, }, @@ -50,7 +51,7 @@ def test_wombat_model_outputs(subtests): with subtests.test("efficiency"): assert prob["efficiency"] == approx(0.76733639, rel=1e-2) with subtests.test("rated_h2_production_kg_pr_hr"): - assert prob["rated_h2_production_kg_pr_hr"] == approx(784.3544736, rel=1e-2) + assert prob["rated_hydrogen_production"] == approx(784.3544736, rel=1e-2) with subtests.test("capacity_factor"): assert prob["capacity_factor"] == approx(0.75637315, rel=1e-2) with subtests.test("CapEx"): @@ -73,6 +74,7 @@ def test_wombat_error(subtests): "plant_life": 20, "simulation": { "n_timesteps": 8760, + "dt": 3600, }, }, }, diff --git a/h2integrate/converters/hydrogen/wombat_model.py b/h2integrate/converters/hydrogen/wombat_model.py index 5ab991c62..74572341c 100644 --- a/h2integrate/converters/hydrogen/wombat_model.py +++ b/h2integrate/converters/hydrogen/wombat_model.py @@ -41,7 +41,7 @@ def setup(self): 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("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( diff --git a/h2integrate/core/h2integrate_model.py b/h2integrate/core/h2integrate_model.py index 930e6ba48..848dd66d5 100644 --- a/h2integrate/core/h2integrate_model.py +++ b/h2integrate/core/h2integrate_model.py @@ -1095,7 +1095,7 @@ def connect_technologies(self): if "electrolyzer" in tech_name: if primary_commodity_type == "hydrogen": self.plant.connect( - f"{tech_name}.total_hydrogen_produced", + f"{tech_name}.annual_hydrogen_produced", f"finance_subgroup_{group_id}.total_hydrogen_produced", ) diff --git a/h2integrate/finances/profast_base.py b/h2integrate/finances/profast_base.py index 31643ea76..bc501fa5b 100644 --- a/h2integrate/finances/profast_base.py +++ b/h2integrate/finances/profast_base.py @@ -529,6 +529,7 @@ def setup(self): f"total_{self.options['commodity_type']}_produced", val=-1.0, units=commodity_units, + shape_by_conn=True, require_connection=True, ) From 4227c1309ed99b0f4dcf46abba7c7f5955a6ae45 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:11:20 -0700 Subject: [PATCH 12/63] minor bugfix to test_sql_timeseries_to_csv for example 2 --- h2integrate/converters/hydrogen/pem_electrolyzer.py | 9 +++++++-- .../postprocess/test/test_sql_timeseries_to_csv.py | 6 ++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/h2integrate/converters/hydrogen/pem_electrolyzer.py b/h2integrate/converters/hydrogen/pem_electrolyzer.py index 1051b6e1f..ed29c6e68 100644 --- a/h2integrate/converters/hydrogen/pem_electrolyzer.py +++ b/h2integrate/converters/hydrogen/pem_electrolyzer.py @@ -162,7 +162,10 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): outputs["total_hydrogen_produced"] = outputs["hydrogen_out"].sum() outputs["efficiency"] = H2_Results["Sim: Average Efficiency [%-HHV]"] refurb_schedule = np.zeros(self.plant_life) - refurb_period = round(float(H2_Results["Time Until Replacement [hrs]"]) / (24 * 365)) + if np.isnan(H2_Results["Time Until Replacement [hrs]"]): + refurb_period = 80000 + else: + refurb_period = round(float(H2_Results["Time Until Replacement [hrs]"]) / (24 * 365)) refurb_schedule[refurb_period : self.plant_life : refurb_period] = 1 outputs["replacement_schedule"] = refurb_schedule @@ -173,7 +176,9 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): # ) # TODO: remove time_until_replacement as output after finance model(s) have been updated to not use it - outputs["time_until_replacement"] = H2_Results["Time Until Replacement [hrs]"] + outputs["time_until_replacement"] = ( + refurb_period # H2_Results["Time Until Replacement [hrs]"] + ) outputs["rated_hydrogen_production"] = H2_Results["Rated BOL: H2 Production [kg/hr]"] outputs["electrolyzer_size_mw"] = electrolyzer_actual_capacity_MW diff --git a/h2integrate/postprocess/test/test_sql_timeseries_to_csv.py b/h2integrate/postprocess/test/test_sql_timeseries_to_csv.py index 993c31764..314dc65d3 100644 --- a/h2integrate/postprocess/test/test_sql_timeseries_to_csv.py +++ b/h2integrate/postprocess/test/test_sql_timeseries_to_csv.py @@ -1,5 +1,6 @@ import os +import numpy as np from pytest import fixture from h2integrate import EXAMPLE_DIR @@ -18,6 +19,11 @@ def run_example_02_sql_fpath(): # Create a H2Integrate model h2i = H2IntegrateModel("02_texas_ammonia.yaml") + # Set the demand profile + demand_profile = np.ones(8760) * 640.0 + h2i.setup() + h2i.prob.set_val("battery.electricity_demand", demand_profile, units="MW") + # Run the model h2i.run() From ff3de30b1eac315b6d32a0802f61e33bce8936ed Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:40:52 -0700 Subject: [PATCH 13/63] fixed save_case_timeseries_as_csv --- .../converters/hydrogen/pem_electrolyzer.py | 4 +-- .../postprocess/sql_timeseries_to_csv.py | 26 +++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/h2integrate/converters/hydrogen/pem_electrolyzer.py b/h2integrate/converters/hydrogen/pem_electrolyzer.py index ed29c6e68..2d79bc03c 100644 --- a/h2integrate/converters/hydrogen/pem_electrolyzer.py +++ b/h2integrate/converters/hydrogen/pem_electrolyzer.py @@ -176,9 +176,7 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): # ) # TODO: remove time_until_replacement as output after finance model(s) have been updated to not use it - outputs["time_until_replacement"] = ( - refurb_period # H2_Results["Time Until Replacement [hrs]"] - ) + outputs["time_until_replacement"] = H2_Results["Time Until Replacement [hrs]"] outputs["rated_hydrogen_production"] = H2_Results["Rated BOL: H2 Production [kg/hr]"] outputs["electrolyzer_size_mw"] = electrolyzer_actual_capacity_MW diff --git a/h2integrate/postprocess/sql_timeseries_to_csv.py b/h2integrate/postprocess/sql_timeseries_to_csv.py index c8dd2d62c..d983c309a 100644 --- a/h2integrate/postprocess/sql_timeseries_to_csv.py +++ b/h2integrate/postprocess/sql_timeseries_to_csv.py @@ -212,6 +212,32 @@ def save_case_timeseries_as_csv( var_to_values = {alt_name_mapper[k]: v for k, v in var_to_values.items()} var_to_units = {alt_name_mapper[k]: v for k, v in var_to_units.items()} + # get length of timeseries profiles (n_timesteps) + timeseries_lengths = list( + {len(v) for k, v in var_to_values.items() if k.endswith("_out") or k.endswith("_in")} + ) + if len(timeseries_lengths) != 1: + msg = ( + "Unexpected: found zero or multiple lengths for timeseries variables " + f"{timeseries_lengths}. Try specifying the variables to save using the " + "vars_to_save input." + ) + raise ValueError(msg) + + # check for any values for variables that aren't timeseries profiles + if any(len(v) != timeseries_lengths[0] for k, v in var_to_values.items()): + # drop variables that aren't timeseries profiles + var_to_values = { + alt_name_mapper[k]: v + for k, v in var_to_values.items() + if len(v) == timeseries_lengths[0] + } + var_to_units = { + alt_name_mapper[k]: v + for k, v in var_to_units.items() + if alt_name_mapper[k] in var_to_values + } + # rename columns to include units column_rename_mapper = { v_name: f"{v_name} ({v_units})" for v_name, v_units in var_to_units.items() From d84c10af9ecfac0bc8a1a64d93be202ae835b18f Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:55:54 -0700 Subject: [PATCH 14/63] update to sql_to_csv function just in case --- h2integrate/postprocess/sql_to_csv.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/h2integrate/postprocess/sql_to_csv.py b/h2integrate/postprocess/sql_to_csv.py index 120028b86..fb0335b1a 100644 --- a/h2integrate/postprocess/sql_to_csv.py +++ b/h2integrate/postprocess/sql_to_csv.py @@ -58,6 +58,18 @@ def summarize_case(case, return_units=False): var_to_values.update({var: val[0]}) var_to_units.update({var: case._get_units(var)}) + # save average capacity factor and annual production + lifetime_prod_var = var.lower().split(".")[-1].startswith("annual") and var.lower().split( + "." + )[-1].endsswith("production") + if "capacity_factor" in var.lower() or lifetime_prod_var: + if isinstance(val, np.ndarray): + if not np.all(val == val[0]): + # take the average value if not all years are equal + val = [np.mean(val)] + else: + continue + if isinstance(val, np.ndarray): # dont save information for non-scalar values if len(val) > 1: From 40ae0900f24ab7f2bc010369dd020d9561bf45d2 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Mon, 26 Jan 2026 13:05:43 -0700 Subject: [PATCH 15/63] added attribute check in PerformanceModelBaseClass --- h2integrate/core/model_baseclasses.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/h2integrate/core/model_baseclasses.py b/h2integrate/core/model_baseclasses.py index 3c709344a..81d3bd584 100644 --- a/h2integrate/core/model_baseclasses.py +++ b/h2integrate/core/model_baseclasses.py @@ -418,6 +418,23 @@ def setup(self): self.set_outputs() def set_outputs(self): + # Check that the required attributes have been instantiated + required = ("commodity", "commodity_rate_units", "commodity_amount_units") + missing = [el for el in required if not hasattr(self, el)] + + if missing: + # Throw error if any attributes are missing. + cls_name = self.msginfo.split("") + missing = ", ".join(missing) + msg = ( + f"{cls_name} is missing the following required attributes: {missing}." + f"Please ensure that the attributes: {missing}" + f"are set in the `setup()` method of {cls_name}." + "Further documentation can be found in the `PerformanceModelBaseClass` " + "documentation." + ) + raise NotImplementedError(msg) + # timeseries profiles self.add_output( f"{self.commodity}_out", From 82116bc5524b46ec9a056b28588fb279c19f36f0 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Mon, 26 Jan 2026 15:38:32 -0700 Subject: [PATCH 16/63] typo bugfix in refurb period calc in electrolyzer model --- h2integrate/converters/hydrogen/pem_electrolyzer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/h2integrate/converters/hydrogen/pem_electrolyzer.py b/h2integrate/converters/hydrogen/pem_electrolyzer.py index 2d79bc03c..5ea196d7c 100644 --- a/h2integrate/converters/hydrogen/pem_electrolyzer.py +++ b/h2integrate/converters/hydrogen/pem_electrolyzer.py @@ -163,7 +163,7 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): outputs["efficiency"] = H2_Results["Sim: Average Efficiency [%-HHV]"] refurb_schedule = np.zeros(self.plant_life) if np.isnan(H2_Results["Time Until Replacement [hrs]"]): - refurb_period = 80000 + refurb_period = 80000 / (24 * 365) else: refurb_period = round(float(H2_Results["Time Until Replacement [hrs]"]) / (24 * 365)) refurb_schedule[refurb_period : self.plant_life : refurb_period] = 1 From 08f94ef2e00052c5126d55f4d4973bda2debb59f Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 27 Jan 2026 09:48:02 -0700 Subject: [PATCH 17/63] added test for solar performance baseclass --- .../solar/test/test_solar_baseclass.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 h2integrate/converters/solar/test/test_solar_baseclass.py diff --git a/h2integrate/converters/solar/test/test_solar_baseclass.py b/h2integrate/converters/solar/test/test_solar_baseclass.py new file mode 100644 index 000000000..6ba190951 --- /dev/null +++ b/h2integrate/converters/solar/test/test_solar_baseclass.py @@ -0,0 +1,37 @@ +from pytest import fixture + +from h2integrate.converters.solar.solar_baseclass import SolarPerformanceBaseClass + + +@fixture +def plant_config(): + plant = { + "plant_life": 30, + "simulation": { + "dt": 3600, + "n_timesteps": 8760, + "start_time": "01/01/1900 00:30:00", + "timezone": 0, + }, + } + + return {"plant": plant, "site": {"latitude": 30.6617, "longitude": -101.7096, "resources": {}}} + + +def test_solar_baseclass_initialization(plant_config, subtests): + solar_base = SolarPerformanceBaseClass( + plant_config=plant_config, + tech_config={}, + driver_config={}, + ) + + # At this point, the commodity attributes haven't been set + + solar_base.setup() + + with subtests.test("commodity"): + assert solar_base.commodity == "electricity" + with subtests.test("commodity_amount_units"): + assert solar_base.commodity_amount_units == "kW*h" + with subtests.test("commodity_rate_units"): + assert solar_base.commodity_rate_units == "kW" From 8df11d0654fc4b1f0395f971e8adda9ab2118a0f Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:23:21 -0700 Subject: [PATCH 18/63] added solar test to check that all outputs are set in parent class --- .../converters/solar/test/test_pysam_solar.py | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/h2integrate/converters/solar/test/test_pysam_solar.py b/h2integrate/converters/solar/test/test_pysam_solar.py index 93cf844dd..9526da08b 100644 --- a/h2integrate/converters/solar/test/test_pysam_solar.py +++ b/h2integrate/converters/solar/test/test_pysam_solar.py @@ -1,3 +1,4 @@ +import numpy as np import pytest import openmdao.api as om from pytest import fixture @@ -51,6 +52,117 @@ def basic_pysam_options(): return pysam_options +def test_pvwatts_outputs(basic_pysam_options, solar_resource_dict, plant_config, subtests): + basic_pysam_options["SystemDesign"].update({"tilt": 0.0}) + pv_design_dict = { + "pv_capacity_kWdc": 250000.0, + "dc_ac_ratio": 1.23, + "create_model_from": "default", + "config_name": "PVWattsSingleOwner", + "tilt": 0.0, + "tilt_angle_func": "none", # "lat-func", + "pysam_options": basic_pysam_options, + } + + tech_config_dict = { + "model_inputs": { + "performance_parameters": pv_design_dict, + } + } + + prob = om.Problem() + solar_resource = GOESAggregatedSolarAPI( + plant_config=plant_config, + resource_config=solar_resource_dict, + driver_config={}, + ) + comp = PYSAMSolarPlantPerformanceModel( + plant_config=plant_config, + tech_config=tech_config_dict, + driver_config={}, + ) + prob.model.add_subsystem("solar_resource", solar_resource, promotes=["*"]) + prob.model.add_subsystem("pv_perf", comp, promotes=["*"]) + prob.setup() + prob.run_model() + + commodity = "electricity" + commodity_amount_units = "kW*h" + commodity_rate_units = "kW" + plant_life = int(plant_config["plant"]["plant_life"]) + n_timesteps = int(plant_config["plant"]["simulation"]["n_timesteps"]) + + base_outputs = ["capacity_factor", "replacement_schedule", "operational_life"] + base_outputs += [ + f"rated_{commodity}_production", + f"annual_{commodity}_produced", + f"total_{commodity}_produced", + f"{commodity}_out", + ] + + # Check that replacement schedule is between 0 and 1 + with subtests.test("0 <= replacement_schedule <=1"): + assert np.all(prob.get_val("replacement_schedule", units="unitless") >= 0) + assert np.all(prob.get_val("replacement_schedule", units="unitless") <= 1) + + with subtests.test("replacement_schedule length"): + assert len(prob.get_val("replacement_schedule", units="unitless")) == plant_life + + # Check that capacity factor is between 0 and 1 with units of "unitless" + with subtests.test("0 <= capacity_factor (unitless) <=1"): + assert np.all(prob.get_val("capacity_factor", units="unitless") >= 0) + assert np.all(prob.get_val("capacity_factor", units="unitless") <= 1) + + # Check that capacity factor is between 1 and 100 with units of "percent" + with subtests.test("1 <= capacity_factor (percent) <=1"): + assert np.all(prob.get_val("capacity_factor", units="percent") >= 1) + assert np.all(prob.get_val("capacity_factor", units="percent") <= 100) + + with subtests.test("capacity_factor length"): + assert len(prob.get_val("capacity_factor", units="unitless")) == plant_life + + # Test that rated commodity production is greater than zero + with subtests.test(f"rated_{commodity}_production > 0"): + assert np.all(prob.get_val(f"rated_{commodity}_production", units=commodity_rate_units) > 0) + + with subtests.test(f"rated_{commodity}_production length"): + assert len(prob.get_val(f"rated_{commodity}_production", units=commodity_rate_units)) == 1 + + # Test that total commodity production is greater than zero + with subtests.test(f"total_{commodity}_produced > 0"): + assert np.all(prob.get_val(f"total_{commodity}_produced", units=commodity_amount_units) > 0) + with subtests.test(f"total_{commodity}_produced length"): + assert len(prob.get_val(f"total_{commodity}_produced", units=commodity_amount_units)) == 1 + + # Test that annual commodity production is greater than zero + with subtests.test(f"annual_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"annual_{commodity}_produced", units=f"{commodity_amount_units}/yr") > 0 + ) + + with subtests.test(f"annual_{commodity}_produced[1:] == annual_{commodity}_produced[0]"): + annual_production = prob.get_val( + f"annual_{commodity}_produced", units=f"{commodity_amount_units}/yr" + ) + assert np.all(annual_production[1:] == annual_production[0]) + + with subtests.test(f"annual_{commodity}_produced length"): + assert len(annual_production) == plant_life + + # Test that commodity output has some values greater than zero + with subtests.test(f"Some of {commodity}_out > 0"): + assert np.any(prob.get_val(f"{commodity}_out", units=commodity_rate_units) > 0) + + with subtests.test(f"{commodity}_out length"): + assert len(prob.get_val(f"{commodity}_out", units=commodity_rate_units)) == n_timesteps + + # Test default values + with subtests.test("operational_life default value"): + assert prob.get_val("operational_life", units="yr") == plant_life + with subtests.test("replacement_schedule value"): + assert np.all(prob.get_val("replacement_schedule", units="unitless") == 0) + + def test_pvwatts_singleowner_notilt( basic_pysam_options, solar_resource_dict, plant_config, subtests ): From 5a38e8faf4f195d4b97a5dcf0e986ed29bf6635a Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:24:20 -0700 Subject: [PATCH 19/63] commented out unused variables in new solar test --- .../converters/solar/test/test_pysam_solar.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/h2integrate/converters/solar/test/test_pysam_solar.py b/h2integrate/converters/solar/test/test_pysam_solar.py index 9526da08b..638f33d18 100644 --- a/h2integrate/converters/solar/test/test_pysam_solar.py +++ b/h2integrate/converters/solar/test/test_pysam_solar.py @@ -92,13 +92,14 @@ def test_pvwatts_outputs(basic_pysam_options, solar_resource_dict, plant_config, plant_life = int(plant_config["plant"]["plant_life"]) n_timesteps = int(plant_config["plant"]["simulation"]["n_timesteps"]) - base_outputs = ["capacity_factor", "replacement_schedule", "operational_life"] - base_outputs += [ - f"rated_{commodity}_production", - f"annual_{commodity}_produced", - f"total_{commodity}_produced", - f"{commodity}_out", - ] + # Below are the base outputs that should be tested + # base_outputs = ["capacity_factor", "replacement_schedule", "operational_life"] + # base_outputs += [ + # f"rated_{commodity}_production", + # f"annual_{commodity}_produced", + # f"total_{commodity}_produced", + # f"{commodity}_out", + # ] # Check that replacement schedule is between 0 and 1 with subtests.test("0 <= replacement_schedule <=1"): From 1a51e7b78deea92f495541269737f86eeff913d7 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 27 Jan 2026 11:30:13 -0700 Subject: [PATCH 20/63] generalized solar unit test so it can be easily used for other components --- .../converters/solar/test/test_pysam_solar.py | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/h2integrate/converters/solar/test/test_pysam_solar.py b/h2integrate/converters/solar/test/test_pysam_solar.py index 638f33d18..9bf85b1ab 100644 --- a/h2integrate/converters/solar/test/test_pysam_solar.py +++ b/h2integrate/converters/solar/test/test_pysam_solar.py @@ -82,7 +82,7 @@ def test_pvwatts_outputs(basic_pysam_options, solar_resource_dict, plant_config, driver_config={}, ) prob.model.add_subsystem("solar_resource", solar_resource, promotes=["*"]) - prob.model.add_subsystem("pv_perf", comp, promotes=["*"]) + prob.model.add_subsystem("comp", comp, promotes=["*"]) prob.setup() prob.run_model() @@ -103,47 +103,56 @@ def test_pvwatts_outputs(basic_pysam_options, solar_resource_dict, plant_config, # Check that replacement schedule is between 0 and 1 with subtests.test("0 <= replacement_schedule <=1"): - assert np.all(prob.get_val("replacement_schedule", units="unitless") >= 0) - assert np.all(prob.get_val("replacement_schedule", units="unitless") <= 1) + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") >= 0) + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") <= 1) with subtests.test("replacement_schedule length"): - assert len(prob.get_val("replacement_schedule", units="unitless")) == plant_life + assert len(prob.get_val("comp.replacement_schedule", units="unitless")) == plant_life # Check that capacity factor is between 0 and 1 with units of "unitless" with subtests.test("0 <= capacity_factor (unitless) <=1"): - assert np.all(prob.get_val("capacity_factor", units="unitless") >= 0) + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") >= 0) assert np.all(prob.get_val("capacity_factor", units="unitless") <= 1) # Check that capacity factor is between 1 and 100 with units of "percent" with subtests.test("1 <= capacity_factor (percent) <=1"): - assert np.all(prob.get_val("capacity_factor", units="percent") >= 1) - assert np.all(prob.get_val("capacity_factor", units="percent") <= 100) + assert np.all(prob.get_val("comp.capacity_factor", units="percent") >= 1) + assert np.all(prob.get_val("comp.capacity_factor", units="percent") <= 100) with subtests.test("capacity_factor length"): - assert len(prob.get_val("capacity_factor", units="unitless")) == plant_life + assert len(prob.get_val("comp.capacity_factor", units="unitless")) == plant_life # Test that rated commodity production is greater than zero with subtests.test(f"rated_{commodity}_production > 0"): - assert np.all(prob.get_val(f"rated_{commodity}_production", units=commodity_rate_units) > 0) + assert np.all( + prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units) > 0 + ) with subtests.test(f"rated_{commodity}_production length"): - assert len(prob.get_val(f"rated_{commodity}_production", units=commodity_rate_units)) == 1 + assert ( + len(prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units)) == 1 + ) # Test that total commodity production is greater than zero with subtests.test(f"total_{commodity}_produced > 0"): - assert np.all(prob.get_val(f"total_{commodity}_produced", units=commodity_amount_units) > 0) + assert np.all( + prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units) > 0 + ) with subtests.test(f"total_{commodity}_produced length"): - assert len(prob.get_val(f"total_{commodity}_produced", units=commodity_amount_units)) == 1 + assert ( + len(prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units)) == 1 + ) # Test that annual commodity production is greater than zero with subtests.test(f"annual_{commodity}_produced > 0"): assert np.all( - prob.get_val(f"annual_{commodity}_produced", units=f"{commodity_amount_units}/yr") > 0 + prob.get_val(f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr") + > 0 ) with subtests.test(f"annual_{commodity}_produced[1:] == annual_{commodity}_produced[0]"): annual_production = prob.get_val( - f"annual_{commodity}_produced", units=f"{commodity_amount_units}/yr" + f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr" ) assert np.all(annual_production[1:] == annual_production[0]) @@ -152,16 +161,16 @@ def test_pvwatts_outputs(basic_pysam_options, solar_resource_dict, plant_config, # Test that commodity output has some values greater than zero with subtests.test(f"Some of {commodity}_out > 0"): - assert np.any(prob.get_val(f"{commodity}_out", units=commodity_rate_units) > 0) + assert np.any(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units) > 0) with subtests.test(f"{commodity}_out length"): - assert len(prob.get_val(f"{commodity}_out", units=commodity_rate_units)) == n_timesteps + assert len(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units)) == n_timesteps # Test default values with subtests.test("operational_life default value"): - assert prob.get_val("operational_life", units="yr") == plant_life + assert prob.get_val("comp.operational_life", units="yr") == plant_life with subtests.test("replacement_schedule value"): - assert np.all(prob.get_val("replacement_schedule", units="unitless") == 0) + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") == 0) def test_pvwatts_singleowner_notilt( From 3ea3568b07756bd8a88af5a3e1375877f87de5cc Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 27 Jan 2026 13:41:07 -0700 Subject: [PATCH 21/63] updated natural gas plant --- .../natural_gas/natural_gas_baseclass.py | 41 ------------------ .../natural_gas/natural_gas_cc_ct.py | 42 +++++++++++++------ 2 files changed, 30 insertions(+), 53 deletions(-) delete mode 100644 h2integrate/converters/natural_gas/natural_gas_baseclass.py diff --git a/h2integrate/converters/natural_gas/natural_gas_baseclass.py b/h2integrate/converters/natural_gas/natural_gas_baseclass.py deleted file mode 100644 index e8f80c3cf..000000000 --- a/h2integrate/converters/natural_gas/natural_gas_baseclass.py +++ /dev/null @@ -1,41 +0,0 @@ -import openmdao.api as om - - -class NaturalGasPerformanceBaseClass(om.ExplicitComponent): - """ - Base class for natural gas plant performance models. - - This base class defines the common interface for natural gas combustion - turbine (NGCT) and natural gas combined cycle (NGCC) performance models. - """ - - def initialize(self): - self.options.declare("driver_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - - def setup(self): - n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] - self.add_input( - "natural_gas_in", - val=0.0, - shape=n_timesteps, - units="MMBtu", - desc="Natural gas input energy", - ) - self.add_output( - "electricity_out", - val=0.0, - shape=n_timesteps, - units="MW", - desc="Electricity output from natural gas plant", - ) - - def compute(self, inputs, outputs): - """ - Computation for the OM component. - - For a template class this is not implemented and raises an error. - """ - - raise NotImplementedError("This method should be implemented in a subclass.") diff --git a/h2integrate/converters/natural_gas/natural_gas_cc_ct.py b/h2integrate/converters/natural_gas/natural_gas_cc_ct.py index 183aefac0..60de24a35 100644 --- a/h2integrate/converters/natural_gas/natural_gas_cc_ct.py +++ b/h2integrate/converters/natural_gas/natural_gas_cc_ct.py @@ -1,10 +1,13 @@ import numpy as np -import openmdao.api as om from attrs import field, define from h2integrate.core.utilities import BaseConfig, merge_shared_inputs from h2integrate.core.validators import gt_zero, gte_zero -from h2integrate.core.model_baseclasses import CostModelBaseClass, CostModelBaseConfig +from h2integrate.core.model_baseclasses import ( + CostModelBaseClass, + CostModelBaseConfig, + PerformanceModelBaseClass, +) @define(kw_only=True) @@ -29,7 +32,7 @@ class NaturalGasPerformanceConfig(BaseConfig): heat_rate_mmbtu_per_mwh: float = field(validator=gt_zero) -class NaturalGasPerformanceModel(om.ExplicitComponent): +class NaturalGasPerformanceModel(PerformanceModelBaseClass): """ Performance model for natural gas power plants. @@ -59,6 +62,11 @@ def initialize(self): self.options.declare("tech_config", types=dict) def setup(self): + self.commodity = "electricity" + self.commodity_rate_units = "MW" + self.commodity_amount_units = "MW*h" + super().setup() + self.config = NaturalGasPerformanceConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance") ) @@ -74,13 +82,13 @@ def setup(self): ) # Add electricity output - self.add_output( - "electricity_out", - val=0.0, - shape=n_timesteps, - units="MW", - desc="Electricity output from natural gas plant", - ) + # self.add_output( + # "electricity_out", + # val=0.0, + # shape=n_timesteps, + # units="MW", + # desc="Electricity output from natural gas plant", + # ) # Add heat_rate as an OpenMDAO input with config value as default self.add_input( @@ -100,10 +108,10 @@ def setup(self): # Default the electricity demand input as the rated capacity self.add_input( - "electricity_demand", + f"{self.commodity}_demand", val=self.config.system_capacity_mw, shape=n_timesteps, - units="MW", + units=self.commodity_rate_units, desc="Electricity demand for natural gas plant", ) @@ -159,6 +167,16 @@ def compute(self, inputs, outputs): outputs["electricity_out"] = electricity_out outputs["natural_gas_consumed"] = natural_gas_consumed + outputs["rated_electricity_production"] = inputs["system_capacity"] + + max_production = inputs["system_capacity"] * len(electricity_out) * (self.dt / 3600) + + outputs["total_electricity_produced"] = np.sum(electricity_out) * (self.dt / 3600) + outputs["capacity_factor"] = outputs["total_electricity_produced"].sum() / max_production + outputs["annual_electricity_produced"] = ( + outputs["total_electricity_produced"] * self.fraction_of_year_simulated + ) + @define(kw_only=True) class NaturalGasCostModelConfig(CostModelBaseConfig): From d02ec1b432ab0918d236baa26dafaf06f2740fce Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:01:03 -0700 Subject: [PATCH 22/63] started updating co2 models --- .../co2/marine/direct_ocean_capture.py | 25 +++++++++++---- .../marine/marine_carbon_capture_baseclass.py | 31 ++++++++++--------- .../marine/ocean_alkalinity_enhancement.py | 19 +++++++++--- 3 files changed, 50 insertions(+), 25 deletions(-) diff --git a/h2integrate/converters/co2/marine/direct_ocean_capture.py b/h2integrate/converters/co2/marine/direct_ocean_capture.py index 4d4365157..9fd1fe0cd 100644 --- a/h2integrate/converters/co2/marine/direct_ocean_capture.py +++ b/h2integrate/converters/co2/marine/direct_ocean_capture.py @@ -128,10 +128,21 @@ def compute(self, inputs, outputs): plot_range=[3910, 4030], ) - outputs["co2_out"] = ed_outputs.ED_outputs["mCC"] * 1000 - outputs["co2_capture_mtpy"] = max(ed_outputs.mCC_yr, 1e-6) # Must be >0 + outputs["co2_out"] = ed_outputs.ED_outputs["mCC"] * 1000 # kg/h + outputs["co2_capture_mtpy"] = max(ed_outputs.mCC_yr, 1e-6) # Must be >0 #TODO: remove outputs["total_tank_volume_m3"] = range_outputs.V_aT_max + range_outputs.V_bT_max - outputs["plant_mCC_capacity_mtph"] = max(range_outputs.S1["mCC"]) + outputs["plant_mCC_capacity_mtph"] = max(range_outputs.S1["mCC"]) # TODO: remove + + outputs["rated_co2_production"] = ( + max(range_outputs.S1["mCC"]) * 1e3 + ) # convert from t/h to kg/h + outputs["total_co2_produced"] = outputs["co2_out"].sum() + + max_production = outputs["rated_co2_production"] * len(outputs["co2_out"]) + outputs["annual_co2_produced"] = max( + ed_outputs.mCC_yr * 1e3, 1e-6 + ) # convert from metric tons/year to kg/year + outputs["capacity_factor"] = outputs["total_co2_produced"] / max_production @define(kw_only=True) @@ -179,7 +190,7 @@ def setup(self): ) self.add_input( - "plant_mCC_capacity_mtph", + "plant_mCC_capacity_mtph", # TODO: replace with rated_co2_production val=0.0, units="t/h", desc="Theoretical plant maximum CO₂ capture (t/h)", @@ -192,10 +203,12 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): res = echem_mcc.electrodialysis_cost_model( echem_mcc.ElectrodialysisCostInputs( electrodialysis_inputs=ED_inputs, - mCC_yr=inputs["co2_capture_mtpy"], + mCC_yr=inputs["co2_capture_mtpy"], # TODO: replace with annual_co2_produced total_tank_volume=inputs["total_tank_volume_m3"], infrastructure_type=self.config.infrastructure_type, - max_theoretical_mCC=inputs["plant_mCC_capacity_mtph"], + max_theoretical_mCC=inputs[ + "plant_mCC_capacity_mtph" + ], # TODO: replaced with rated_co2_production ) ) diff --git a/h2integrate/converters/co2/marine/marine_carbon_capture_baseclass.py b/h2integrate/converters/co2/marine/marine_carbon_capture_baseclass.py index 0c0228764..6cc2ddd47 100644 --- a/h2integrate/converters/co2/marine/marine_carbon_capture_baseclass.py +++ b/h2integrate/converters/co2/marine/marine_carbon_capture_baseclass.py @@ -1,8 +1,7 @@ -import openmdao.api as om from attrs import field, define from h2integrate.core.utilities import BaseConfig -from h2integrate.core.model_baseclasses import CostModelBaseClass +from h2integrate.core.model_baseclasses import CostModelBaseClass, PerformanceModelBaseClass @define(kw_only=True) @@ -22,7 +21,7 @@ class MarineCarbonCapturePerformanceConfig(BaseConfig): store_hours: float = field() -class MarineCarbonCapturePerformanceBaseClass(om.ExplicitComponent): +class MarineCarbonCapturePerformanceBaseClass(PerformanceModelBaseClass): """Base OpenMDAO component for modeling marine carbon capture performance. This class provides the basic input/output setup and requires subclassing to @@ -33,22 +32,23 @@ class MarineCarbonCapturePerformanceBaseClass(om.ExplicitComponent): tech_config (dict): Configuration dictionary for technology-specific parameters. """ - def initialize(self): - self.options.declare("driver_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - def setup(self): + self.commodity = "co2" + self.commodity_rate_units = "kg/h" + self.commodity_amount_units = "kg" + super().setup() + self.add_input( "electricity_in", val=0.0, shape=8760, units="W", desc="Hourly input electricity (W)" ) - self.add_output( - "co2_out", - val=0.0, - shape=8760, - units="kg/h", - desc="Hourly CO₂ capture rate (kg/h)", - ) + # self.add_output( + # "co2_out", + # val=0.0, + # shape=8760, + # units="kg/h", + # desc="Hourly CO₂ capture rate (kg/h)", + # ) + # TODO: remove this output once finance models are updated self.add_output("co2_capture_mtpy", units="t/year", desc="Annual CO₂ captured (t/year)") @@ -68,6 +68,7 @@ def setup(self): self.add_input( "electricity_in", val=0.0, shape=8760, units="W", desc="Hourly input electricity (W)" ) + # TODO: replaced with annual_co2_produced self.add_input( "co2_capture_mtpy", val=0.0, diff --git a/h2integrate/converters/co2/marine/ocean_alkalinity_enhancement.py b/h2integrate/converters/co2/marine/ocean_alkalinity_enhancement.py index e9b343dfb..253451377 100644 --- a/h2integrate/converters/co2/marine/ocean_alkalinity_enhancement.py +++ b/h2integrate/converters/co2/marine/ocean_alkalinity_enhancement.py @@ -215,8 +215,19 @@ def compute(self, inputs, outputs): ) outputs["co2_out"] = oae_outputs.OAE_outputs["mass_CO2_absorbed"] - outputs["co2_capture_mtpy"] = oae_outputs.M_co2est - outputs["plant_mCC_capacity_mtph"] = max(range_outputs.S1["mass_CO2_absorbed"] / 1000) + outputs["rated_co2_production"] = max(range_outputs.S1["mass_CO2_absorbed"]) # kg/h + outputs["total_co2_produced"] = outputs["co2_out"] + + max_production = outputs["rated_co2_production"] * len(outputs["co2_out"]) + outputs["annual_co2_produced"] = ( + oae_outputs.M_co2est * 1e3 + ) # convert from metric tons/year to kg/year + outputs["capacity_factor"] = outputs["total_co2_produced"] / max_production + + outputs["co2_capture_mtpy"] = oae_outputs.M_co2est # TODO: remove + outputs["plant_mCC_capacity_mtph"] = max( + range_outputs.S1["mass_CO2_absorbed"] / 1000 + ) # TODO: remove outputs["alkaline_seawater_flow_rate"] = oae_outputs.OAE_outputs["Qout"] outputs["alkaline_seawater_pH"] = oae_outputs.OAE_outputs["pH_f"] outputs["alkaline_seawater_dic"] = oae_outputs.OAE_outputs["dic_f"] @@ -313,7 +324,7 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): value_product=inputs["value_products"], waste_mass=inputs["mass_acid_disposed"], waste_disposal_cost=inputs["cost_acid_disposal"], - estimated_cdr=inputs["co2_capture_mtpy"], + estimated_cdr=inputs["co2_capture_mtpy"], # TODO: replace with annual_co2_produced base_added_seawater_max_power=inputs["based_added_seawater_max_power"], mass_rca=inputs["mass_rca"], annual_energy_cost=0, # Energy costs are calculated within H2I and added to LCOC calc @@ -435,7 +446,7 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): value_product=inputs["value_products"], waste_mass=inputs["mass_acid_disposed"], waste_disposal_cost=inputs["cost_acid_disposal"], - estimated_cdr=inputs["co2_capture_mtpy"], + estimated_cdr=inputs["co2_capture_mtpy"], # TODO: replace with annual_co2_produced base_added_seawater_max_power=inputs["based_added_seawater_max_power"], mass_rca=inputs["mass_rca"], annual_energy_cost=annual_energy_cost_usd_yr, From e868d821af6ec71b34fd869007090c096b0971b4 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:06:58 -0700 Subject: [PATCH 23/63] updated grid model --- h2integrate/converters/grid/grid.py | 49 ++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/h2integrate/converters/grid/grid.py b/h2integrate/converters/grid/grid.py index 959abd8f2..bd6991f74 100644 --- a/h2integrate/converters/grid/grid.py +++ b/h2integrate/converters/grid/grid.py @@ -1,9 +1,12 @@ import numpy as np -import openmdao.api as om from attrs import field, define from h2integrate.core.utilities import BaseConfig, merge_shared_inputs -from h2integrate.core.model_baseclasses import CostModelBaseClass, CostModelBaseConfig +from h2integrate.core.model_baseclasses import ( + CostModelBaseClass, + CostModelBaseConfig, + PerformanceModelBaseClass, +) @define(kw_only=True) @@ -17,7 +20,7 @@ class GridPerformanceModelConfig(BaseConfig): interconnection_size: float = field() # kW -class GridPerformanceModel(om.ExplicitComponent): +class GridPerformanceModel(PerformanceModelBaseClass): """Model a grid interconnection point. The grid is treated as the interconnection point itself: @@ -48,6 +51,10 @@ def initialize(self): self.options.declare("tech_config", types=dict) def setup(self): + self.commodity = "electricity" + self.commodity_rate_units = "kW" + self.commodity_amount_units = "kW*h" + super().setup() self.config = GridPerformanceModelConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance") ) @@ -58,7 +65,7 @@ def setup(self): self.add_input( "interconnection_size", val=self.config.interconnection_size, - units="kW", + units=self.commodity_rate_units, desc="Maximum power capacity for grid connection", ) @@ -67,7 +74,7 @@ def setup(self): "electricity_in", val=0.0, shape=n_timesteps, - units="kW", + units=self.commodity_rate_units, desc="Electricity flowing into grid interconnection point (selling to grid)", ) @@ -76,18 +83,18 @@ def setup(self): "electricity_demand", val=0.0, shape=n_timesteps, - units="kW", + units=self.commodity_rate_units, desc="Electricity demand from downstream technologies", ) # Electricity flowing OUT OF the grid (buying from grid) - self.add_output( - "electricity_out", - val=0.0, - shape=n_timesteps, - units="kW", - desc="Electricity flowing out of grid interconnection point (buying from grid)", - ) + # self.add_output( + # "electricity_out", + # val=0.0, + # shape=n_timesteps, + # units=self.commodity_rate_units, + # desc="Electricity flowing out of grid interconnection point (buying from grid)", + # ) self.add_output( "electricity_sold", @@ -101,7 +108,7 @@ def setup(self): "electricity_unmet_demand", val=0.0, shape=n_timesteps, - units="kW", + units=self.commodity_rate_units, desc="Electricity demand that is not met", ) @@ -109,7 +116,7 @@ def setup(self): "electricity_excess", val=0.0, shape=n_timesteps, - units="kW", + units=self.commodity_rate_units, desc="Electricity that was not sold due to interconnection limits", ) @@ -130,6 +137,18 @@ def compute(self, inputs, outputs): # Not sold electricity if demand exceeds interconnection size outputs["electricity_excess"] = inputs["electricity_in"] - electricity_sold + max_production = ( + inputs["interconnection_size"] * len(outputs["electricity_out"]) * (self.dt / 3600) + ) + outputs["rated_electricity_production"] = inputs["interconnection_size"] + outputs["total_electricity_produced"] = np.sum(outputs["electricity_out"]) * ( + self.dt / 3600 + ) + outputs["capacity_factor"] = outputs["total_electricity_produced"].sum() / max_production + outputs["annual_electricity_produced"] = ( + outputs["total_electricity_produced"] * self.fraction_of_year_simulated + ) + @define(kw_only=True) class GridCostModelConfig(CostModelBaseConfig): From 16a44c5c2ae4ee6a706348b4a9b1aec32c3d64e3 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:17:36 -0700 Subject: [PATCH 24/63] updated asu model --- h2integrate/converters/nitrogen/simple_ASU.py | 74 +++++++++++-------- .../nitrogen/test/test_simple_asu_model.py | 11 +-- 2 files changed, 48 insertions(+), 37 deletions(-) diff --git a/h2integrate/converters/nitrogen/simple_ASU.py b/h2integrate/converters/nitrogen/simple_ASU.py index 71d8e3592..837e6518b 100644 --- a/h2integrate/converters/nitrogen/simple_ASU.py +++ b/h2integrate/converters/nitrogen/simple_ASU.py @@ -1,11 +1,14 @@ import numpy as np -import openmdao.api as om from attrs import field, define from h2integrate.core.utilities import BaseConfig, merge_shared_inputs from h2integrate.core.validators import contains, range_val from h2integrate.tools.constants import N_MW, AR_MW, O2_MW -from h2integrate.core.model_baseclasses import CostModelBaseClass, CostModelBaseConfig +from h2integrate.core.model_baseclasses import ( + CostModelBaseClass, + CostModelBaseConfig, + PerformanceModelBaseClass, +) @define(kw_only=True) @@ -56,7 +59,7 @@ def __attrs_post_init__(self): raise ValueError(msg) -class SimpleASUPerformanceModel(om.ExplicitComponent): +class SimpleASUPerformanceModel(PerformanceModelBaseClass): """Simple linear converter to model nitrogen production from an Air Separation Unit. """ @@ -67,6 +70,11 @@ def initialize(self): self.options.declare("driver_config", types=dict) def setup(self): + self.commodity = "nitrogen" + self.commodity_amount_units = "kg" + self.commodity_rate_units = "kg/h" + super().setup() + n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] self.config = SimpleASUPerformanceConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance") @@ -80,9 +88,9 @@ def setup(self): self.add_output("air_in", val=0.0, shape=n_timesteps, units="kg/h") self.add_output("ASU_capacity_kW", val=0.0, units="kW", desc="ASU rated capacity in kW") - self.add_output( - "rated_N2_kg_pr_hr", val=0.0, units="kg/h", desc="ASU rated capacity in kg-N2/hour" - ) + # self.add_output( + # "rated_N2_kg_pr_hr", val=0.0, units="kg/h", desc="ASU rated capacity in kg-N2/hour" + # ) self.add_output( "annual_electricity_consumption", @@ -90,26 +98,26 @@ def setup(self): units="kW", desc="ASU annual electricity consumption in kWh/year", ) - self.add_output( - "total_nitrogen_produced", - val=0.0, - units="kg/year", - desc="ASU annual nitrogen production in kg-N2/year", - ) - self.add_output( - "annual_max_nitrogen_production", - val=0.0, - units="kg/year", - desc="ASU maximum annual nitrogen production in kg-N2/year", - ) - self.add_output( - "nitrogen_production_capacity_factor", - val=0.0, - units=None, - desc="ASU annual nitrogen production in kg-N2/year", - ) - - self.add_output("nitrogen_out", val=0.0, shape=n_timesteps, units="kg/h") + # self.add_output( + # "total_nitrogen_produced", + # val=0.0, + # units="kg/year", + # desc="ASU annual nitrogen production in kg-N2/year", + # ) + # self.add_output( + # "annual_max_nitrogen_production", + # val=0.0, + # units="kg/year", + # desc="ASU maximum annual nitrogen production in kg-N2/year", + # ) + # self.add_output( + # "nitrogen_production_capacity_factor", + # val=0.0, + # units=None, + # desc="ASU annual nitrogen production in kg-N2/year", + # ) + + # self.add_output("nitrogen_out", val=0.0, shape=n_timesteps, units="kg/h") self.add_output("oxygen_out", val=0.0, shape=n_timesteps, units="kg/h") self.add_output("argon_out", val=0.0, shape=n_timesteps, units="kg/h") @@ -201,7 +209,7 @@ def compute(self, inputs, outputs): # calculate the annual rated production of nitrogen in kg-N2/year max_annual_N2 = rated_N2_kg_pr_hr * len(n2_profile_out_kg) - outputs["rated_N2_kg_pr_hr"] = rated_N2_kg_pr_hr # rated ASU capacity in kg-N2/hour + outputs["rated_nitrogen_production"] = rated_N2_kg_pr_hr # rated ASU capacity in kg-N2/hour outputs["ASU_capacity_kW"] = ASU_rated_power_kW # rated ASU capacity in kW outputs["air_in"] = air_profile_kg # air feedstock profile in kg/hour outputs["oxygen_out"] = o2_profile_kg # O2 secondary output profile in kg/hour @@ -209,11 +217,13 @@ def compute(self, inputs, outputs): outputs["nitrogen_out"] = n2_profile_out_kg # N2 primary output profile in kg/hour # capacity factor of ASU - outputs["nitrogen_production_capacity_factor"] = sum(n2_profile_out_kg) / max_annual_N2 + outputs["capacity_factor"] = sum(n2_profile_out_kg) / max_annual_N2 # annual N2 production in kg-N2/year outputs["total_nitrogen_produced"] = sum(n2_profile_out_kg) # maximum annual N2 production in kg-N2/year - outputs["annual_max_nitrogen_production"] = max_annual_N2 + outputs["annual_nitrogen_produced"] = ( + outputs["total_nitrogen_produced"] * self.fraction_of_year_simulated + ) # annual electricity consumption in kWh/year outputs["annual_electricity_consumption"] = sum(electricity_kWh) @@ -302,7 +312,7 @@ def setup(self): super().setup() self.add_input("ASU_capacity_kW", val=0.0, units="kW") - self.add_input("rated_N2_kg_pr_hr", val=0.0, units="kg/h") + self.add_input("rated_nitrogen_production", val=0.0, units="kg/h") def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): # Get config values @@ -311,14 +321,14 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): if capex_based_unit == "power": capex_usd = unit_capex * inputs["ASU_capacity_kW"] else: - capex_usd = unit_capex * inputs["rated_N2_kg_pr_hr"] + capex_usd = unit_capex * inputs["rated_nitrogen_production"] opex_k, opex_based_unit = make_cost_unit_multiplier(self.config.opex_unit) unit_opex = self.config.opex_usd_per_unit_per_year * opex_k if opex_based_unit == "power": opex_usd_per_year = unit_opex * inputs["ASU_capacity_kW"] else: - opex_usd_per_year = unit_opex * inputs["rated_N2_kg_pr_hr"] + opex_usd_per_year = unit_opex * inputs["rated_nitrogen_production"] outputs["CapEx"] = capex_usd outputs["OpEx"] = opex_usd_per_year diff --git a/h2integrate/converters/nitrogen/test/test_simple_asu_model.py b/h2integrate/converters/nitrogen/test/test_simple_asu_model.py index c81631ba4..40bc4fb4f 100644 --- a/h2integrate/converters/nitrogen/test/test_simple_asu_model.py +++ b/h2integrate/converters/nitrogen/test/test_simple_asu_model.py @@ -10,6 +10,7 @@ "plant_life": 30, "simulation": { "n_timesteps": 8760, # Default number of timesteps for the simulation + "dt": 3600, }, }, } @@ -41,7 +42,7 @@ def test_simple_ASU_performance_model_set_capacity_kW(subtests): prob.set_val("asu_perf.electricity_in", e_profile_in_kW.tolist(), units="kW") prob.run_model() # Dummy expected values - max_n2_mfr = prob.get_val("asu_perf.rated_N2_kg_pr_hr")[0] + max_n2_mfr = prob.get_val("asu_perf.rated_nitrogen_production", units="kg/h")[0] max_pwr_kw = prob.get_val("asu_perf.ASU_capacity_kW")[0] max_eff = max_pwr_kw / max_n2_mfr @@ -80,7 +81,7 @@ def test_simple_ASU_performance_model_size_for_demand(subtests): prob.set_val("asu_perf.nitrogen_in", n2_dmd_kg_pr_hr.tolist(), units="kg/h") prob.run_model() # Dummy expected values - max_n2_mfr = prob.get_val("asu_perf.rated_N2_kg_pr_hr")[0] + max_n2_mfr = prob.get_val("asu_perf.rated_nitrogen_production", units="kg/h")[0] max_pwr_kw = prob.get_val("asu_perf.ASU_capacity_kW")[0] max_eff = max_pwr_kw / max_n2_mfr @@ -132,7 +133,7 @@ def test_simple_ASU_cost_model_usd_pr_kw(subtests): # Set required inputs prob.set_val("asu_cost.ASU_capacity_kW", rated_power_kW, units="kW") - prob.set_val("asu_cost.rated_N2_kg_pr_hr", rated_N2_mfr, units="kg/h") + prob.set_val("asu_cost.rated_nitrogen_production", rated_N2_mfr, units="kg/h") prob.run_model() expected_outputs = { @@ -177,7 +178,7 @@ def test_simple_ASU_cost_model_usd_pr_mw(subtests): # Set required inputs prob.set_val("asu_cost.ASU_capacity_kW", rated_power_kW, units="kW") - prob.set_val("asu_cost.rated_N2_kg_pr_hr", rated_N2_mfr, units="kg/h") + prob.set_val("asu_cost.rated_nitrogen_production", rated_N2_mfr, units="kg/h") prob.run_model() expected_outputs = { @@ -233,7 +234,7 @@ def test_simple_ASU_performance_and_cost_size_for_demand(subtests): prob.set_val("asu_perf.nitrogen_in", n2_dmd_kg_pr_hr.tolist(), units="kg/h") prob.run_model() # Dummy expected values - max_n2_mfr = prob.get_val("asu_perf.rated_N2_kg_pr_hr")[0] + max_n2_mfr = prob.get_val("asu_perf.rated_nitrogen_production")[0] max_pwr_kw = prob.get_val("asu_perf.ASU_capacity_kW")[0] max_eff = max_pwr_kw / max_n2_mfr From d8fda021002bbb4c6ca654e8865f8127b97c23d8 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:27:30 -0700 Subject: [PATCH 25/63] updated grid tests --- h2integrate/converters/grid/test/test_grid.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/h2integrate/converters/grid/test/test_grid.py b/h2integrate/converters/grid/test/test_grid.py index d1868d024..d70974054 100644 --- a/h2integrate/converters/grid/test/test_grid.py +++ b/h2integrate/converters/grid/test/test_grid.py @@ -12,7 +12,15 @@ class TestGridPerformanceModel(unittest.TestCase): def setUp(self): """Set up test fixtures.""" self.n_timesteps = 10 - self.plant_config = {"plant": {"simulation": {"n_timesteps": self.n_timesteps}}} + self.plant_config = { + "plant": { + "plant_life": 30, + "simulation": { + "n_timesteps": self.n_timesteps, + "dt": 3600, + }, + } + } def test_buying_electricity(self): """Test buying electricity from grid (electricity flows out).""" From c32e752c5842cfa16589a4d2912609637f06e88a Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:40:57 -0700 Subject: [PATCH 26/63] updated desal model --- .../converters/water/desal/desalination.py | 11 +++++- .../water/desal/desalination_baseclass.py | 38 ++++--------------- .../water/desal/test/test_ro_desalination.py | 28 ++++++++++---- 3 files changed, 38 insertions(+), 39 deletions(-) diff --git a/h2integrate/converters/water/desal/desalination.py b/h2integrate/converters/water/desal/desalination.py index 2e31a63a7..cc812670f 100644 --- a/h2integrate/converters/water/desal/desalination.py +++ b/h2integrate/converters/water/desal/desalination.py @@ -94,7 +94,16 @@ def compute(self, inputs, outputs): desal_mass_kg = freshwater_m3_per_hr * 346.7 # [kg] desal_size_m2 = freshwater_m3_per_hr * 0.467 # [m^2] - outputs["water"] = freshwater_m3_per_hr + outputs["water_out"] = freshwater_m3_per_hr + outputs["total_water_produced"] = outputs["water_out"].sum() + outputs["rated_water_production"] = outputs["water_out"].max() + outputs["capacity_factor"] = outputs["total_water_produced"] / ( + outputs["rated_water_production"] * len(outputs["water_out"]) + ) + outputs["annual_water_produced"] = ( + outputs["total_water_produced"] * self.fraction_of_year_simulated + ) + outputs["electricity_in"] = desal_power outputs["feedwater"] = feedwater_m3_per_hr outputs["mass"] = desal_mass_kg diff --git a/h2integrate/converters/water/desal/desalination_baseclass.py b/h2integrate/converters/water/desal/desalination_baseclass.py index 71665a37c..fc515453d 100644 --- a/h2integrate/converters/water/desal/desalination_baseclass.py +++ b/h2integrate/converters/water/desal/desalination_baseclass.py @@ -1,16 +1,13 @@ -import openmdao.api as om +from h2integrate.core.model_baseclasses import CostModelBaseClass, PerformanceModelBaseClass -from h2integrate.core.model_baseclasses import CostModelBaseClass - - -class DesalinationPerformanceBaseClass(om.ExplicitComponent): - def initialize(self): - self.options.declare("driver_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) +class DesalinationPerformanceBaseClass(PerformanceModelBaseClass): def setup(self): - self.add_output("water", val=0.0, units="m**3/h", desc="Fresh water") + self.commodity = "water" + self.commodity_amount_units = "m**3" + self.commodity_rate_units = "m**3/h" + super().setup() + # self.add_output("water", val=0.0, units="m**3/h", desc="Fresh water") self.add_output("mass", val=0.0, units="kg", desc="Mass of desalination system") self.add_output("footprint", val=0.0, units="m**2", desc="Footprint of desalination system") @@ -31,24 +28,3 @@ def setup(self): self.add_input( "plant_capacity_kgph", val=0.0, units="kg/h", desc="Desired freshwater flow rate" ) - - -class DesalinationFinanceBaseClass(om.ExplicitComponent): - def initialize(self): - self.options.declare("driver_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - - def setup(self): - self.add_input("CapEx", val=0.0, units="USD") - self.add_input("OpEx", val=0.0, units="USD/year") - self.add_output("NPV", val=0.0, units="USD", desc="Net present value") - - def compute(self, inputs, outputs): - """ - Computation for the OM component. - - For a template class this is not implement and raises an error. - """ - - raise NotImplementedError("This method should be implemented in a subclass.") diff --git a/h2integrate/converters/water/desal/test/test_ro_desalination.py b/h2integrate/converters/water/desal/test/test_ro_desalination.py index a4da3ee28..48b5c9305 100644 --- a/h2integrate/converters/water/desal/test/test_ro_desalination.py +++ b/h2integrate/converters/water/desal/test/test_ro_desalination.py @@ -1,5 +1,5 @@ import openmdao.api as om -from pytest import approx +from pytest import approx, fixture from h2integrate.converters.water.desal.desalination import ( ReverseOsmosisCostModel, @@ -7,7 +7,20 @@ ) -def test_brackish_performance(subtests): +@fixture +def plant_config(): + plant = { + "plant_life": 30, + "simulation": { + "dt": 3600, + "n_timesteps": 8760, + }, + } + + return {"plant": plant} + + +def test_brackish_performance(plant_config, subtests): tech_config = { "model_inputs": { "performance_parameters": { @@ -19,14 +32,14 @@ def test_brackish_performance(subtests): } prob = om.Problem() - comp = ReverseOsmosisPerformanceModel(tech_config=tech_config) + comp = ReverseOsmosisPerformanceModel(plant_config=plant_config, tech_config=tech_config) prob.model.add_subsystem("comp", comp, promotes=["*"]) prob.setup() prob.run_model() with subtests.test("fresh water"): - assert prob["water"] == approx(10.03, rel=1e-5) + assert prob["water_out"] == approx(10.03, rel=1e-5) with subtests.test("mass"): assert prob["mass"] == approx(3477.43, rel=1e-3) with subtests.test("footprint"): @@ -37,7 +50,7 @@ def test_brackish_performance(subtests): assert prob["electricity_in"] == approx(15.04, rel=1e-3) -def test_seawater_performance(subtests): +def test_seawater_performance(plant_config, subtests): tech_config = { "model_inputs": { "performance_parameters": { @@ -49,14 +62,14 @@ def test_seawater_performance(subtests): } prob = om.Problem() - comp = ReverseOsmosisPerformanceModel(tech_config=tech_config) + comp = ReverseOsmosisPerformanceModel(plant_config=plant_config, tech_config=tech_config) prob.model.add_subsystem("comp", comp, promotes=["*"]) prob.setup() prob.run_model() with subtests.test("fresh water"): - assert prob["water"] == approx(10.03, rel=1e-5) + assert prob["water_out"] == approx(10.03, rel=1e-5) with subtests.test("mass"): assert prob["mass"] == approx(3477.43, rel=1e-3) with subtests.test("footprint"): @@ -82,6 +95,7 @@ def test_ro_desalination_cost(subtests): "plant_life": 30, "simulation": { "n_timesteps": 8760, + "dt": 3600, }, }, } From 910a374730232e0e9968d8b4a1d7c63742511551 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:41:15 -0700 Subject: [PATCH 27/63] updated co2 models and tests --- .../marine/ocean_alkalinity_enhancement.py | 2 +- .../converters/co2/marine/test/test_doc.py | 22 +++++++++++++++++-- .../converters/co2/marine/test/test_oae.py | 14 ++++++++++-- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/h2integrate/converters/co2/marine/ocean_alkalinity_enhancement.py b/h2integrate/converters/co2/marine/ocean_alkalinity_enhancement.py index 253451377..e362480c3 100644 --- a/h2integrate/converters/co2/marine/ocean_alkalinity_enhancement.py +++ b/h2integrate/converters/co2/marine/ocean_alkalinity_enhancement.py @@ -216,7 +216,7 @@ def compute(self, inputs, outputs): outputs["co2_out"] = oae_outputs.OAE_outputs["mass_CO2_absorbed"] outputs["rated_co2_production"] = max(range_outputs.S1["mass_CO2_absorbed"]) # kg/h - outputs["total_co2_produced"] = outputs["co2_out"] + outputs["total_co2_produced"] = outputs["co2_out"].sum() max_production = outputs["rated_co2_production"] * len(outputs["co2_out"]) outputs["annual_co2_produced"] = ( diff --git a/h2integrate/converters/co2/marine/test/test_doc.py b/h2integrate/converters/co2/marine/test/test_doc.py index 5cf8f52a9..06b5bcb88 100644 --- a/h2integrate/converters/co2/marine/test/test_doc.py +++ b/h2integrate/converters/co2/marine/test/test_doc.py @@ -34,6 +34,15 @@ def setUp(self): }, }, } + plant_config = { + "plant": { + "plant_life": 30, + "simulation": { + "n_timesteps": 8760, # Default number of timesteps for the simulation + "dt": 3600, + }, + }, + } driver_config = { "general": { @@ -42,7 +51,7 @@ def setUp(self): } doc_model = DOCPerformanceModel( - driver_config=driver_config, plant_config={}, tech_config=self.config + driver_config=driver_config, plant_config=plant_config, tech_config=self.config ) self.prob = om.Problem(model=om.Group()) self.prob.model.add_subsystem("DOC", doc_model, promotes=["*"]) @@ -78,7 +87,16 @@ def test_no_mcm_import(self): from h2integrate.converters.co2.marine.direct_ocean_capture import DOCPerformanceModel try: - self.model = DOCPerformanceModel(plant_config={}, tech_config={}) + plant_config = { + "plant": { + "plant_life": 30, + "simulation": { + "n_timesteps": 8760, # Default number of timesteps for the simulation + "dt": 3600, + }, + }, + } + self.model = DOCPerformanceModel(plant_config=plant_config, tech_config={}) except ImportError as e: self.assertIn( "The `mcm` package is required to use the Direct Ocean Capture model." diff --git a/h2integrate/converters/co2/marine/test/test_oae.py b/h2integrate/converters/co2/marine/test/test_oae.py index bdf380dd0..2e3c97c4b 100644 --- a/h2integrate/converters/co2/marine/test/test_oae.py +++ b/h2integrate/converters/co2/marine/test/test_oae.py @@ -41,10 +41,11 @@ def setUp(self): plant_config = { "plant": { + "plant_life": 30, "simulation": { "n_timesteps": 8760, "dt": 3600, - } + }, } } @@ -91,7 +92,16 @@ def test_no_mcm_import(self): ) try: - self.model = OAEPerformanceModel(plant_config={}, tech_config={}) + plant_config = { + "plant": { + "plant_life": 30, + "simulation": { + "n_timesteps": 8760, + "dt": 3600, + }, + } + } + self.model = OAEPerformanceModel(plant_config=plant_config, tech_config={}) except ImportError as e: self.assertIn( "The `mcm` package is required to use the Ocean Alkalinity Enhancement model." From cca9e4bc11b1f5312cbf3c307cc387998a351d9a Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:50:52 -0700 Subject: [PATCH 28/63] updated newest steel models --- .../converters/steel/steel_eaf_base.py | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/h2integrate/converters/steel/steel_eaf_base.py b/h2integrate/converters/steel/steel_eaf_base.py index 0f8ecf014..8aa8abf4a 100644 --- a/h2integrate/converters/steel/steel_eaf_base.py +++ b/h2integrate/converters/steel/steel_eaf_base.py @@ -1,13 +1,16 @@ import numpy as np import pandas as pd -import openmdao.api as om from attrs import field, define from openmdao.utils import units from h2integrate import ROOT_DIR from h2integrate.core.utilities import BaseConfig, merge_shared_inputs from h2integrate.core.validators import gte_zero -from h2integrate.core.model_baseclasses import CostModelBaseClass, CostModelBaseConfig +from h2integrate.core.model_baseclasses import ( + CostModelBaseClass, + CostModelBaseConfig, + PerformanceModelBaseClass, +) from h2integrate.tools.inflation.inflate import inflate_cpi, inflate_cepci @@ -26,13 +29,13 @@ class ElectricArcFurnacePerformanceBaseConfig(BaseConfig): water_density: float = field(default=1000) # kg/m3 -class ElectricArcFurnacePlantBasePerformanceComponent(om.ExplicitComponent): - def initialize(self): - self.options.declare("driver_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - +class ElectricArcFurnacePlantBasePerformanceComponent(PerformanceModelBaseClass): def setup(self): + self.commodity = "steel" + self.commodity_rate_units = "t/h" + self.commodity_amount_units = "t" + super().setup() + n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] self.config = ElectricArcFurnacePerformanceBaseConfig.from_dict( @@ -43,7 +46,7 @@ def setup(self): self.add_input( "system_capacity", val=self.config.steel_production_rate_tonnes_per_hr, - units="t/h", + units=self.commodity_rate_units, desc="Rated steel production capacity", ) @@ -69,17 +72,17 @@ def setup(self): "steel_demand", val=self.config.steel_production_rate_tonnes_per_hr, shape=n_timesteps, - units="t/h", + units=self.commodity_rate_units, desc="Steel demand for steel plant", ) - self.add_output( - "steel_out", - val=0.0, - shape=n_timesteps, - units="t/h", - desc="Steel produced", - ) + # self.add_output( + # "steel_out", + # val=0.0, + # shape=n_timesteps, + # units="t/h", + # desc="Steel produced", + # ) coeff_fpath = ROOT_DIR / "converters" / "iron" / "rosner" / "perf_coeffs.csv" # rosner dri performance model @@ -257,6 +260,14 @@ def compute(self, inputs, outputs): # output is minimum between available feedstocks and output demand steel_production = np.minimum.reduce(steel_from_feedstocks) outputs["steel_out"] = steel_production + outputs["rated_steel_production"] = inputs["system_capacity"] + outputs["total_steel_produced"] = outputs["steel_out"].sum() + outputs["annual_steel_produced"] = ( + outputs["total_steel_produced"] * self.fraction_of_year_simulated + ) + outputs["capacity_factor"] = outputs["total_steel_produced"] / ( + outputs["rated_steel_production"] * len(outputs["steel_out"]) + ) # feedstock consumption based on actual steel produced for feedstock_type, consumption_rate in feedstocks_usage_rates.items(): From b899fae1380f47144f2dcf092546ed24a2c0e232 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:56:46 -0700 Subject: [PATCH 29/63] started updating methanol models but not tested --- .../converters/methanol/co2h_methanol_plant.py | 8 ++++++++ h2integrate/converters/methanol/methanol_baseclass.py | 11 ++++++++--- h2integrate/converters/methanol/smr_methanol_plant.py | 8 ++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/h2integrate/converters/methanol/co2h_methanol_plant.py b/h2integrate/converters/methanol/co2h_methanol_plant.py index eb7d817bf..113f422aa 100644 --- a/h2integrate/converters/methanol/co2h_methanol_plant.py +++ b/h2integrate/converters/methanol/co2h_methanol_plant.py @@ -121,6 +121,14 @@ def compute(self, inputs, outputs): outputs["hydrogen_consume"] = meoh_prod * h2_ratio outputs["electricity_consume"] = meoh_prod * elec_ratio + outputs["rated_methanol_production"] = inputs["plant_capacity_kgpy"] / 8760 + outputs["total_methanol_produced"] = outputs["methanol_out"].sum() + max_production = len(meoh_prod) * inputs["plant_capacity_kgpy"] / 8760 + outputs["capacity_factor"] = outputs["total_methanol_produced"] / max_production + outputs["annual_methanol_produced"] = ( + outputs["total_methanol_produced"] * self.fraction_of_year_simulated + ) + @define(kw_only=True) class CO2HCostConfig(MethanolCostConfig): diff --git a/h2integrate/converters/methanol/methanol_baseclass.py b/h2integrate/converters/methanol/methanol_baseclass.py index 99bd94f6d..4fabf2c7e 100644 --- a/h2integrate/converters/methanol/methanol_baseclass.py +++ b/h2integrate/converters/methanol/methanol_baseclass.py @@ -3,7 +3,7 @@ from h2integrate.core.utilities import BaseConfig from h2integrate.core.validators import contains -from h2integrate.core.model_baseclasses import CostModelBaseClass +from h2integrate.core.model_baseclasses import CostModelBaseClass, PerformanceModelBaseClass @define(kw_only=True) @@ -15,7 +15,7 @@ class MethanolPerformanceConfig(BaseConfig): h2o_consume_ratio: float = field() -class MethanolPerformanceBaseClass(om.ExplicitComponent): +class MethanolPerformanceBaseClass(PerformanceModelBaseClass): """ An OpenMDAO component for modeling the performance of a methanol plant. Computes annual methanol and co-product production, feedstock consumption, and emissions @@ -40,13 +40,18 @@ def initialize(self): self.options.declare("tech_config", types=dict) def setup(self): + self.commodity = "methanol" + self.commodity_rate_units = "kg/h" + self.commodity_amount_units = "kg" + super().setup() + n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] self.add_input("plant_capacity_kgpy", units="kg/year", val=self.config.plant_capacity_kgpy) self.add_input("capacity_factor", units="unitless", val=self.config.capacity_factor) self.add_input("co2e_emit_ratio", units="kg/kg", val=self.config.co2e_emit_ratio) self.add_input("h2o_consume_ratio", units="kg/kg", val=self.config.h2o_consume_ratio) - self.add_output("methanol_out", units="kg/h", shape=n_timesteps) + # self.add_output("methanol_out", units="kg/h", shape=n_timesteps) self.add_output("total_methanol_produced", units="kg/year") self.add_output("co2e_emissions", units="kg/h", shape=n_timesteps) self.add_output("h2o_consumption", units="kg/h", shape=n_timesteps) diff --git a/h2integrate/converters/methanol/smr_methanol_plant.py b/h2integrate/converters/methanol/smr_methanol_plant.py index e30aec6eb..8b134f3bb 100644 --- a/h2integrate/converters/methanol/smr_methanol_plant.py +++ b/h2integrate/converters/methanol/smr_methanol_plant.py @@ -105,6 +105,14 @@ def compute(self, inputs, outputs): outputs["total_methanol_produced"] = np.sum(meoh_prod) outputs["electricity_out"] = meoh_prod * elec_ratio + outputs["rated_methanol_production"] = inputs["plant_capacity_kgpy"] / 8760 + outputs["total_methanol_produced"] = outputs["methanol_out"].sum() + max_production = len(meoh_prod) * inputs["plant_capacity_kgpy"] / 8760 + outputs["capacity_factor"] = outputs["total_methanol_produced"] / max_production + outputs["annual_methanol_produced"] = ( + outputs["total_methanol_produced"] * self.fraction_of_year_simulated + ) + @define(kw_only=True) class SMRCostConfig(MethanolCostConfig): From e570263b82424b7446806757707b69f2b37fe9b2 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:00:18 -0700 Subject: [PATCH 30/63] made it so ResizablePerformanceModelBaseClass inherits PerformanceModelBaseClass --- h2integrate/core/model_baseclasses.py | 202 +++++++++++++------------- 1 file changed, 99 insertions(+), 103 deletions(-) diff --git a/h2integrate/core/model_baseclasses.py b/h2integrate/core/model_baseclasses.py index 81d3bd584..721423527 100644 --- a/h2integrate/core/model_baseclasses.py +++ b/h2integrate/core/model_baseclasses.py @@ -9,6 +9,103 @@ from h2integrate.core.utilities import BaseConfig +class PerformanceModelBaseClass(om.ExplicitComponent): + def initialize(self): + self.options.declare("driver_config", types=dict) + self.options.declare("plant_config", types=dict) + self.options.declare("tech_config", types=dict) + + def setup(self): + # Below should be done in subclass that produces hydrogen + # self.commodity = "hydrogen" + # self.commodity_rate_units = "kg/h" + # self.commodity_amount_units = "kg" + # super().setup() + + # Below should be done in subclass that produces electricity + # self.commodity = "electricity" + # self.commodity_rate_units = "kW" + # self.commodity_amount_units = "kW*h" + # super().setup() + + # n_timesteps is number of timesteps in a simulation + self.n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] + # dt is seconds per timestep + self.dt = self.options["plant_config"]["plant"]["simulation"]["dt"] + # plant_life is number of years the plant is expected to operate for + self.plant_life = int(self.options["plant_config"]["plant"]["plant_life"]) + hours_per_year = 8760 + # hours simulated is the number of hours in a simulation + hours_simulated = (self.dt / 3600) * self.n_timesteps + # fraction_of_year_simulated is the ratio of simulation length to length of year + # and may be used to estimate annual performance from simulation performance + self.fraction_of_year_simulated = hours_simulated / hours_per_year + self.set_outputs() + + def set_outputs(self): + # Check that the required attributes have been instantiated + required = ("commodity", "commodity_rate_units", "commodity_amount_units") + missing = [el for el in required if not hasattr(self, el)] + + if missing: + # Throw error if any attributes are missing. + cls_name = self.msginfo.split("") + missing = ", ".join(missing) + msg = ( + f"{cls_name} is missing the following required attributes: {missing}." + f"Please ensure that the attributes: {missing}" + f"are set in the `setup()` method of {cls_name}." + "Further documentation can be found in the `PerformanceModelBaseClass` " + "documentation." + ) + raise NotImplementedError(msg) + + # timeseries profiles + self.add_output( + f"{self.commodity}_out", + val=0.0, + shape=self.n_timesteps, + units=self.commodity_rate_units, + ) + # sum over simulation + self.add_output( + f"total_{self.commodity}_produced", val=0.0, units=self.commodity_amount_units + ) + # annual performance estimate for commodity produced + self.add_output( + f"annual_{self.commodity}_produced", + val=0.0, + shape=self.plant_life, + units=f"({self.commodity_amount_units})/year", + ) + # lifetime estimate of item replacements, represented as a fraction of the capacity. + self.add_output("replacement_schedule", val=0.0, shape=self.plant_life, units="unitless") + # capacity factor is the ratio of actual production / maximum production possible + self.add_output( + "capacity_factor", + val=0.0, + shape=self.plant_life, + units="unitless", + desc="Capacity factor", + ) + # rated/maximum commodity production, this would be used to calculate the maximum + # production possible over the simulation + self.add_output( + f"rated_{self.commodity}_production", val=0.0, units=self.commodity_rate_units + ) + # operational life of the technology if the technology cannot be replaced + self.add_output("operational_life", val=self.plant_life, units="yr") + + def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): + """ + Computation for the OM component. + + For a template class this is not implement and raises an error. + """ + + raise NotImplementedError("This method should be implemented in a subclass.") + + @define(kw_only=True) class CostModelBaseConfig(BaseConfig): cost_year: int = field(converter=int) @@ -86,7 +183,7 @@ def __attrs_post_init__(self): ) -class ResizeablePerformanceModelBaseClass(om.ExplicitComponent): +class ResizeablePerformanceModelBaseClass(PerformanceModelBaseClass): """Baseclass to be used for all resizeable performance models. The built-in inputs are used by the performance models to resize themselves. @@ -110,12 +207,8 @@ class ResizeablePerformanceModelBaseClass(om.ExplicitComponent): this component to the max commodity consumed by the downstream tech. """ - def initialize(self): - self.options.declare("driver_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - def setup(self): + super().setup() # Parse in sizing parameters size_mode = self.config.size_mode self.add_discrete_input("size_mode", val=size_mode) @@ -382,100 +475,3 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): # self.cache_outputs(inputs, outputs, discrete_inputs, discrete_outputs) raise NotImplementedError("This method should be implemented in a subclass.") - - -class PerformanceModelBaseClass(om.ExplicitComponent): - def initialize(self): - self.options.declare("driver_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - - def setup(self): - # Below should be done in subclass that produces hydrogen - # self.commodity = "hydrogen" - # self.commodity_rate_units = "kg/h" - # self.commodity_amount_units = "kg" - # super().setup() - - # Below should be done in subclass that produces electricity - # self.commodity = "electricity" - # self.commodity_rate_units = "kW" - # self.commodity_amount_units = "kW*h" - # super().setup() - - # n_timesteps is number of timesteps in a simulation - self.n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] - # dt is seconds per timestep - self.dt = self.options["plant_config"]["plant"]["simulation"]["dt"] - # plant_life is number of years the plant is expected to operate for - self.plant_life = int(self.options["plant_config"]["plant"]["plant_life"]) - hours_per_year = 8760 - # hours simulated is the number of hours in a simulation - hours_simulated = (self.dt / 3600) * self.n_timesteps - # fraction_of_year_simulated is the ratio of simulation length to length of year - # and may be used to estimate annual performance from simulation performance - self.fraction_of_year_simulated = hours_simulated / hours_per_year - self.set_outputs() - - def set_outputs(self): - # Check that the required attributes have been instantiated - required = ("commodity", "commodity_rate_units", "commodity_amount_units") - missing = [el for el in required if not hasattr(self, el)] - - if missing: - # Throw error if any attributes are missing. - cls_name = self.msginfo.split("") - missing = ", ".join(missing) - msg = ( - f"{cls_name} is missing the following required attributes: {missing}." - f"Please ensure that the attributes: {missing}" - f"are set in the `setup()` method of {cls_name}." - "Further documentation can be found in the `PerformanceModelBaseClass` " - "documentation." - ) - raise NotImplementedError(msg) - - # timeseries profiles - self.add_output( - f"{self.commodity}_out", - val=0.0, - shape=self.n_timesteps, - units=self.commodity_rate_units, - ) - # sum over simulation - self.add_output( - f"total_{self.commodity}_produced", val=0.0, units=self.commodity_amount_units - ) - # annual performance estimate for commodity produced - self.add_output( - f"annual_{self.commodity}_produced", - val=0.0, - shape=self.plant_life, - units=f"({self.commodity_amount_units})/year", - ) - # lifetime estimate of item replacements, represented as a fraction of the capacity. - self.add_output("replacement_schedule", val=0.0, shape=self.plant_life, units="unitless") - # capacity factor is the ratio of actual production / maximum production possible - self.add_output( - "capacity_factor", - val=0.0, - shape=self.plant_life, - units="unitless", - desc="Capacity factor", - ) - # rated/maximum commodity production, this would be used to calculate the maximum - # production possible over the simulation - self.add_output( - f"rated_{self.commodity}_production", val=0.0, units=self.commodity_rate_units - ) - # operational life of the technology if the technology cannot be replaced - self.add_output("operational_life", val=self.plant_life, units="yr") - - def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): - """ - Computation for the OM component. - - For a template class this is not implement and raises an error. - """ - - raise NotImplementedError("This method should be implemented in a subclass.") From ee220e516e91341c782a425d03a990cd18f6141f Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:23:50 -0700 Subject: [PATCH 31/63] updated electrolyzer model and ammonia synloop model --- .../converters/ammonia/ammonia_synloop.py | 19 ++++++--- .../test/test_ammonia_synloop_model.py | 3 +- .../hydrogen/electrolyzer_baseclass.py | 41 +------------------ .../hydrogen/test/test_basic_cost_model.py | 12 +++--- .../test/test_singlitico_cost_model.py | 2 +- 5 files changed, 25 insertions(+), 52 deletions(-) diff --git a/h2integrate/converters/ammonia/ammonia_synloop.py b/h2integrate/converters/ammonia/ammonia_synloop.py index b567f895d..b920d2151 100644 --- a/h2integrate/converters/ammonia/ammonia_synloop.py +++ b/h2integrate/converters/ammonia/ammonia_synloop.py @@ -140,6 +140,9 @@ class AmmoniaSynLoopPerformanceModel(ResizeablePerformanceModelBaseClass): """ def setup(self): + self.commodity = "ammonia" + self.commodity_rate_units = "kg/h" + self.commodity_amount_units = "kg" n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] self.config = AmmoniaSynLoopPerformanceConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance") @@ -150,13 +153,13 @@ def setup(self): self.add_input("nitrogen_in", val=0.0, shape=n_timesteps, units="kg/h") self.add_input("electricity_in", val=0.0, shape=n_timesteps, units="MW") - self.add_output("ammonia_out", val=0.0, shape=n_timesteps, units="kg/h") + # self.add_output("ammonia_out", val=0.0, shape=n_timesteps, units="kg/h") self.add_output("nitrogen_out", val=0.0, shape=n_timesteps, units="kg/h") self.add_output("hydrogen_out", val=0.0, shape=n_timesteps, units="kg/h") self.add_output("electricity_out", val=0.0, shape=n_timesteps, units="MW") self.add_output("heat_out", val=0.0, shape=n_timesteps, units="kW*h/kg") self.add_output("catalyst_mass", val=0.0, units="kg") - self.add_output("total_ammonia_produced", val=0.0, units="kg/year") + # self.add_output("total_ammonia_produced", val=0.0, units="kg/year") self.add_output("total_hydrogen_consumed", val=0.0, units="kg/year") self.add_output("total_nitrogen_consumed", val=0.0, units="kg/year") self.add_output("total_electricity_consumed", val=0.0, units="kW*h/year") @@ -164,7 +167,7 @@ def setup(self): "limiting_input", val=0, shape_by_conn=True, copy_shape="hydrogen_in", units=None ) self.add_output("max_hydrogen_capacity", val=1000.0, units="kg/h") - self.add_output("ammonia_capacity_factor", val=0.0, units="unitless") + # self.add_output("ammonia_capacity_factor", val=0.0, units="unitless") def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): # Get config values @@ -273,7 +276,12 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): outputs["max_hydrogen_capacity"] = h2_cap # Calculate capacity factor - outputs["ammonia_capacity_factor"] = np.mean(nh3_prod) / nh3_cap + outputs["capacity_factor"] = np.mean(nh3_prod) / nh3_cap + + outputs["rated_ammonia_production"] = nh3_cap + outputs["annual_ammonia_produced"] = ( + outputs["total_ammonia_produced"] * self.fraction_of_year_simulated + ) @define(kw_only=True) @@ -419,8 +427,9 @@ def setup(self): merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost") ) super().setup() + plant_life = int(self.options["plant_config"]["plant"]["plant_life"]) - self.add_input("total_ammonia_produced", val=0.0, units="kg/year") + self.add_input("annual_ammonia_produced", val=0.0, shape=plant_life, units="kg/year") self.add_input("total_hydrogen_consumed", val=0.0, units="kg/year") self.add_input("total_nitrogen_consumed", val=0.0, units="kg/year") self.add_input("total_electricity_consumed", val=0.0, units="kW*h/year") diff --git a/h2integrate/converters/ammonia/test/test_ammonia_synloop_model.py b/h2integrate/converters/ammonia/test/test_ammonia_synloop_model.py index ae342a097..570ecb5ef 100644 --- a/h2integrate/converters/ammonia/test/test_ammonia_synloop_model.py +++ b/h2integrate/converters/ammonia/test/test_ammonia_synloop_model.py @@ -43,10 +43,11 @@ def make_synloop_config(): def test_ammonia_synloop_limiting_cases(subtests): config = make_synloop_config() plant_info = { + "plant_life": 30, "simulation": { "n_timesteps": 4, # Using 4 timesteps for this test "dt": 3600, - } + }, } # Each test is a single array of 4 hours, each with a different limiting case diff --git a/h2integrate/converters/hydrogen/electrolyzer_baseclass.py b/h2integrate/converters/hydrogen/electrolyzer_baseclass.py index 11972f752..65218db40 100644 --- a/h2integrate/converters/hydrogen/electrolyzer_baseclass.py +++ b/h2integrate/converters/hydrogen/electrolyzer_baseclass.py @@ -1,5 +1,3 @@ -import openmdao.api as om - from h2integrate.core.model_baseclasses import ( CostModelBaseClass, PerformanceModelBaseClass, @@ -14,25 +12,11 @@ def setup(self): self.commodity = "hydrogen" self.commodity_rate_units = "kg/h" self.commodity_amount_units = "kg" - n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] - super().setup() - # n_timesteps is number of timesteps in a simulation - self.n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] - # dt is seconds per timestep - self.dt = self.options["plant_config"]["plant"]["simulation"]["dt"] - # plant_life is number of years the plant is expected to operate for - self.plant_life = int(self.options["plant_config"]["plant"]["plant_life"]) - hours_per_year = 8760 - # hours simulated is the number of hours in a simulation - hours_simulated = (self.dt / 3600) * self.n_timesteps - # fraction_of_year_simulated is the ratio of simulation length to length of year - # and may be used to estimate annual performance from simulation performance - self.fraction_of_year_simulated = hours_simulated / hours_per_year + super().setup() - self.set_outputs() # Define inputs for electricity and outputs for hydrogen and oxygen generation - self.add_input("electricity_in", val=0.0, shape=n_timesteps, units="kW") + self.add_input("electricity_in", val=0.0, shape=self.n_timesteps, units="kW") # self.add_output("hydrogen_out", val=0.0, shape=n_timesteps, units="kg/h") # self.add_output( # "time_until_replacement", val=80000.0, units="h", desc="Time until replacement" @@ -57,24 +41,3 @@ def setup(self): "total_hydrogen_produced", val=0.0, units="kg" ) # NOTE: unsure if this is used self.add_input("electricity_in", val=0.0, shape=n_timesteps, units="kW") - - -class ElectrolyzerFinanceBaseClass(om.ExplicitComponent): - def initialize(self): - self.options.declare("driver_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - - def setup(self): - self.add_input("CapEx", val=0.0, units="USD") - self.add_input("OpEx", val=0.0, units="USD/year") - self.add_output("NPV", val=0.0, units="USD", desc="Net present value") - - def compute(self, inputs, outputs): - """ - Computation for the OM component. - - For a template class this is not implement and raises an error. - """ - - raise NotImplementedError("This method should be implemented in a subclass.") diff --git a/h2integrate/converters/hydrogen/test/test_basic_cost_model.py b/h2integrate/converters/hydrogen/test/test_basic_cost_model.py index 6b3969f78..7bfbb6a83 100644 --- a/h2integrate/converters/hydrogen/test/test_basic_cost_model.py +++ b/h2integrate/converters/hydrogen/test/test_basic_cost_model.py @@ -59,7 +59,7 @@ def test_on_turbine_capex(self): self.per_turb_electrolyzer_size_mw, self.per_turb_electrical_generation_timeseries, ) - prob.set_val("total_hydrogen_produced", self.per_turb_h2_annual_output, units="kg/year") + prob.set_val("total_hydrogen_produced", self.per_turb_h2_annual_output, units="kg") prob.run_model() per_turb_electrolyzer_total_capital_cost = prob["CapEx"] @@ -71,7 +71,7 @@ def test_on_platform_capex(self): prob = self._create_problem( "offshore", self.electrolyzer_size_mw, self.electrical_generation_timeseries ) - prob.set_val("total_hydrogen_produced", self.h2_annual_output, units="kg/year") + prob.set_val("total_hydrogen_produced", self.h2_annual_output, units="kg") prob.run_model() electrolyzer_total_capital_cost = prob["CapEx"] @@ -84,7 +84,7 @@ def test_on_land_capex(self): self.per_turb_electrolyzer_size_mw, self.per_turb_electrical_generation_timeseries, ) - prob.set_val("total_hydrogen_produced", self.per_turb_h2_annual_output, units="kg/year") + prob.set_val("total_hydrogen_produced", self.per_turb_h2_annual_output, units="kg") prob.run_model() per_turb_electrolyzer_total_capital_cost = prob["CapEx"] @@ -98,7 +98,7 @@ def test_on_turbine_opex(self): self.per_turb_electrolyzer_size_mw, self.per_turb_electrical_generation_timeseries, ) - prob.set_val("total_hydrogen_produced", self.per_turb_h2_annual_output, units="kg/year") + prob.set_val("total_hydrogen_produced", self.per_turb_h2_annual_output, units="kg") prob.run_model() per_turb_electrolyzer_OM_cost = prob["OpEx"] @@ -110,7 +110,7 @@ def test_on_platform_opex(self): prob = self._create_problem( "offshore", self.electrolyzer_size_mw, self.electrical_generation_timeseries ) - prob.set_val("total_hydrogen_produced", self.h2_annual_output, units="kg/year") + prob.set_val("total_hydrogen_produced", self.h2_annual_output, units="kg") prob.run_model() electrolyzer_OM_cost = prob["OpEx"] @@ -123,7 +123,7 @@ def test_on_land_opex(self): self.per_turb_electrolyzer_size_mw, self.per_turb_electrical_generation_timeseries, ) - prob.set_val("total_hydrogen_produced", self.per_turb_h2_annual_output, units="kg/year") + prob.set_val("total_hydrogen_produced", self.per_turb_h2_annual_output, units="kg") prob.run_model() per_turb_electrolyzer_OM_cost = prob["OpEx"] diff --git a/h2integrate/converters/hydrogen/test/test_singlitico_cost_model.py b/h2integrate/converters/hydrogen/test/test_singlitico_cost_model.py index abe06af03..dd283b6bf 100644 --- a/h2integrate/converters/hydrogen/test/test_singlitico_cost_model.py +++ b/h2integrate/converters/hydrogen/test/test_singlitico_cost_model.py @@ -53,7 +53,7 @@ def _create_problem(self, location): prob.setup() prob.set_val("electrolyzer_size_mw", self.P_elec_mw, units="MW") prob.set_val("electricity_in", np.ones(8760) * self.P_elec_mw, units="kW") - prob.set_val("total_hydrogen_produced", 1000.0, units="kg/year") + prob.set_val("total_hydrogen_produced", 1000.0, units="kg") return prob def test_calc_capex_onshore(self): From 1b6440663689bf9227a495479e99eb4c681e7d08 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:28:08 -0700 Subject: [PATCH 32/63] updated simple ammonia model --- .../ammonia/simple_ammonia_model.py | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/h2integrate/converters/ammonia/simple_ammonia_model.py b/h2integrate/converters/ammonia/simple_ammonia_model.py index 934c04c05..7aae5f6fd 100644 --- a/h2integrate/converters/ammonia/simple_ammonia_model.py +++ b/h2integrate/converters/ammonia/simple_ammonia_model.py @@ -1,9 +1,12 @@ -import openmdao.api as om from attrs import field, define from h2integrate.core.utilities import BaseConfig, merge_shared_inputs from h2integrate.core.validators import must_equal -from h2integrate.core.model_baseclasses import CostModelBaseClass, CostModelBaseConfig +from h2integrate.core.model_baseclasses import ( + CostModelBaseClass, + CostModelBaseConfig, + PerformanceModelBaseClass, +) @define(kw_only=True) @@ -21,7 +24,7 @@ class AmmoniaPerformanceModelConfig(BaseConfig): plant_capacity_factor: float = field() -class SimpleAmmoniaPerformanceModel(om.ExplicitComponent): +class SimpleAmmoniaPerformanceModel(PerformanceModelBaseClass): """ An OpenMDAO component for modeling the performance of an ammonia plant. Computes annual ammonia production based on plant capacity and capacity factor. @@ -33,20 +36,30 @@ def initialize(self): self.options.declare("driver_config", types=dict) def setup(self): + self.commodity = "ammonia" + self.commodity_rate_units = "kg/h" + self.commodity_amount_units = "kg" + super().setup() n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] self.config = AmmoniaPerformanceModelConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance") ) self.add_input("hydrogen_in", val=0.0, shape=n_timesteps, units="kg/h") - self.add_output("ammonia_out", val=0.0, shape=n_timesteps, units="kg/h") - self.add_output("total_ammonia_produced", val=0.0, units="kg/year") + # self.add_output("ammonia_out", val=0.0, shape=n_timesteps, units="kg/h") + # self.add_output("total_ammonia_produced", val=0.0, units="kg/year") def compute(self, inputs, outputs): ammonia_production_kgpy = ( self.config.plant_capacity_kgpy * self.config.plant_capacity_factor ) outputs["ammonia_out"] = ammonia_production_kgpy / len(inputs["hydrogen_in"]) + outputs["capacity_factor"] = self.config.plant_capacity_factor + outputs["total_ammonia_produced"] = ammonia_production_kgpy + outputs["annual_ammonia_produced"] = ( + outputs["total_ammonia_produced"] * self.fraction_of_year_simulated + ) + outputs["rated_ammonia_production"] = self.config.plant_capacity_kgpy / 8760 @define(kw_only=True) From 496fba0a59420868b73b4e040b3796364217d9fa Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:44:42 -0700 Subject: [PATCH 33/63] updated hopp wrapper --- h2integrate/converters/hopp/hopp_wrapper.py | 41 +++++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/h2integrate/converters/hopp/hopp_wrapper.py b/h2integrate/converters/hopp/hopp_wrapper.py index 2732ca594..b07638f1d 100644 --- a/h2integrate/converters/hopp/hopp_wrapper.py +++ b/h2integrate/converters/hopp/hopp_wrapper.py @@ -3,7 +3,11 @@ from hopp.tools.dispatch.plot_tools import plot_battery_output, plot_generation_profile from h2integrate.core.utilities import merge_shared_inputs -from h2integrate.core.model_baseclasses import CacheBaseClass, CacheBaseConfig, CostModelBaseClass +from h2integrate.core.model_baseclasses import ( + CacheBaseClass, + CacheBaseConfig, + PerformanceModelBaseClass, +) from h2integrate.converters.hopp.hopp_mgmt import run_hopp, setup_hopp @@ -14,7 +18,7 @@ class HOPPComponentModelConfig(CacheBaseConfig): electrolyzer_rating: int | float | None = field(default=None) -class HOPPComponent(CostModelBaseClass, CacheBaseClass): +class HOPPComponent(PerformanceModelBaseClass, CacheBaseClass): """ A simple OpenMDAO component that represents a HOPP model. @@ -25,7 +29,11 @@ class HOPPComponent(CostModelBaseClass, CacheBaseClass): """ def setup(self): - n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] + self.commodity = "electricity" + self.commodity_rate_units = "kW" + self.commodity_amount_units = "kW*h" + + # n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] self.config = HOPPComponentModelConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), @@ -61,9 +69,9 @@ def setup(self): self.add_output("percent_load_missed", units="percent", val=0.0) self.add_output("curtailment_percent", units="percent", val=0.0) self.add_output("aep", units="kW*h", val=0.0) - self.add_output( - "electricity_out", val=np.zeros(n_timesteps), units="kW", desc="Power output" - ) + # self.add_output( + # "electricity_out", val=np.zeros(n_timesteps), units="kW", desc="Power output" + # ) self.add_output("battery_duration", val=0.0, units="h", desc="Battery duration") self.add_output( "annual_energy_to_interconnect_potential_ratio", @@ -77,6 +85,19 @@ def setup(self): units="unitless", desc="Power capacity to interconnect ratio", ) + 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( + "VarOpEx", + val=0.0, + shape=self.plant_life, + units="USD/year", + 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" + ) def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): # Check if the results for the current configuration are already cached @@ -143,10 +164,16 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): # Set the outputs outputs["percent_load_missed"] = subset_of_hopp_results["percent_load_missed"] outputs["curtailment_percent"] = subset_of_hopp_results["curtailment_percent"] - outputs["aep"] = subset_of_hopp_results["annual_energies"]["hybrid"] + # outputs["aep"] = subset_of_hopp_results["annual_energies"]["hybrid"] outputs["electricity_out"] = subset_of_hopp_results["combined_hybrid_power_production_hopp"] outputs["CapEx"] = subset_of_hopp_results["capex"] outputs["OpEx"] = subset_of_hopp_results["opex"] + outputs["rated_electricity_production"] = hopp_results["hybrid_plant"].system_capacity_kw[ + "hybrid" + ] # this includes battery + outputs["total_electricity_produced"] = outputs["electricity_out"].sum() + outputs["annual_electricity_produced"] = subset_of_hopp_results["annual_energies"]["hybrid"] + outputs["capacity_factor"] = hopp_results["hybrid_plant"].capacity_factors["hybrid"] / 100 if "battery" in self.config.hopp_config["technologies"]: outputs["battery_duration"] = ( From 811703455849879dccecf7e07bd2deb284238e85 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:57:51 -0700 Subject: [PATCH 34/63] updated steel.py --- h2integrate/converters/steel/steel.py | 12 +++++- .../converters/steel/steel_baseclass.py | 40 +++++-------------- 2 files changed, 19 insertions(+), 33 deletions(-) diff --git a/h2integrate/converters/steel/steel.py b/h2integrate/converters/steel/steel.py index c87f13759..2fa7e2a3b 100644 --- a/h2integrate/converters/steel/steel.py +++ b/h2integrate/converters/steel/steel.py @@ -29,7 +29,13 @@ def setup(self): def compute(self, inputs, outputs): steel_production_mtpy = self.config.plant_capacity_mtpy * self.config.capacity_factor - outputs["steel"] = steel_production_mtpy / len(inputs["electricity_in"]) + outputs["steel_out"] = steel_production_mtpy / len(inputs["electricity_in"]) + outputs["rated_steel_production"] = self.config.plant_capacity_mtpy / 8760 + outputs["capacity_factor"] = self.config.capacity_factor + outputs["total_steel_produced"] = outputs["steel_out"].sum() + outputs["annual_steel_produced"] = ( + outputs["total_steel_produced"] * self.fraction_of_year_simulated + ) @define(kw_only=True) @@ -83,7 +89,9 @@ def setup(self): ) super().setup() - self.add_input("steel_production_mtpy", val=0.0, units="t/year") + self.add_input( + "steel_production_mtpy", val=0.0, units="t/year" + ) # TODO: update with rated_steel_production self.add_output("LCOS", val=0.0, units="USD/t") def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): diff --git a/h2integrate/converters/steel/steel_baseclass.py b/h2integrate/converters/steel/steel_baseclass.py index 956df63b7..7a22365ca 100644 --- a/h2integrate/converters/steel/steel_baseclass.py +++ b/h2integrate/converters/steel/steel_baseclass.py @@ -1,19 +1,18 @@ -import openmdao.api as om +from h2integrate.core.model_baseclasses import CostModelBaseClass, PerformanceModelBaseClass -from h2integrate.core.model_baseclasses import CostModelBaseClass - - -class SteelPerformanceBaseClass(om.ExplicitComponent): - def initialize(self): - self.options.declare("driver_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) +class SteelPerformanceBaseClass(PerformanceModelBaseClass): def setup(self): + self.commodity = "steel" + self.commodity_amount_units = "t" + self.commodity_rate_units = "t/h" + + super().setup() n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] + # NOTE: the SteelPerformanceModel does not use electricity or hydrogen in its calc self.add_input("electricity_in", val=0.0, shape=n_timesteps, units="kW") self.add_input("hydrogen_in", val=0.0, shape=n_timesteps, units="kg/h") - self.add_output("steel", val=0.0, shape=n_timesteps, units="t/year") + # self.add_output("steel", val=0.0, shape=n_timesteps, units="t/year") def compute(self, inputs, outputs): """ @@ -35,24 +34,3 @@ def setup(self): self.add_input( "electricity_cost", val=0.0, units="USD/(MW*h)", desc="Levelized cost of electricity" ) - - -class SteelFinanceBaseClass(om.ExplicitComponent): - def initialize(self): - self.options.declare("driver_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - - def setup(self): - self.add_input("CapEx", val=0.0, units="USD") - self.add_input("OpEx", val=0.0, units="USD/year") - self.add_output("NPV", val=0.0, units="USD", desc="Net present value") - - def compute(self, inputs, outputs): - """ - Computation for the OM component. - - For a template class this is not implement and raises an error. - """ - - raise NotImplementedError("This method should be implemented in a subclass.") From 6c7790c3a9ebb5d1c7d8517ed9ddba3c4ea885dc Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:58:12 -0700 Subject: [PATCH 35/63] updated iron mine and dri models --- h2integrate/converters/iron/iron_dri_base.py | 35 +++++++++---- .../converters/iron/martin_mine_perf_model.py | 50 +++++++++++-------- 2 files changed, 53 insertions(+), 32 deletions(-) diff --git a/h2integrate/converters/iron/iron_dri_base.py b/h2integrate/converters/iron/iron_dri_base.py index f70e505af..0fb53d0ed 100644 --- a/h2integrate/converters/iron/iron_dri_base.py +++ b/h2integrate/converters/iron/iron_dri_base.py @@ -1,13 +1,16 @@ import numpy as np import pandas as pd -import openmdao.api as om from attrs import field, define from openmdao.utils import units from h2integrate import ROOT_DIR from h2integrate.core.utilities import BaseConfig, merge_shared_inputs from h2integrate.core.validators import gte_zero -from h2integrate.core.model_baseclasses import CostModelBaseClass, CostModelBaseConfig +from h2integrate.core.model_baseclasses import ( + CostModelBaseClass, + CostModelBaseConfig, + PerformanceModelBaseClass, +) from h2integrate.tools.inflation.inflate import inflate_cpi, inflate_cepci @@ -26,13 +29,17 @@ class IronReductionPerformanceBaseConfig(BaseConfig): water_density: float = field(default=1000) # kg/m3 -class IronReductionPlantBasePerformanceComponent(om.ExplicitComponent): +class IronReductionPlantBasePerformanceComponent(PerformanceModelBaseClass): def initialize(self): self.options.declare("driver_config", types=dict) self.options.declare("plant_config", types=dict) self.options.declare("tech_config", types=dict) def setup(self): + self.commodity = "pig_iron" + self.commodity_rate_units = "t/h" + self.commodity_amount_units = "t" + super().setup() n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] self.config = IronReductionPerformanceBaseConfig.from_dict( @@ -73,13 +80,13 @@ def setup(self): desc="Pig iron demand for iron plant", ) - self.add_output( - "pig_iron_out", - val=0.0, - shape=n_timesteps, - units="t/h", - desc="Pig iron produced", - ) + # self.add_output( + # "pig_iron_out", + # val=0.0, + # shape=n_timesteps, + # units="t/h", + # desc="Pig iron produced", + # ) coeff_fpath = ROOT_DIR / "converters" / "iron" / "rosner" / "perf_coeffs.csv" # rosner dri performance model @@ -252,6 +259,14 @@ def compute(self, inputs, outputs): # output is minimum between available feedstocks and output demand pig_iron_production = np.minimum.reduce(pig_iron_from_feedstocks) outputs["pig_iron_out"] = pig_iron_production + outputs["total_pig_iron_produced"] = np.sum(pig_iron_production) + outputs["capacity_factor"] = outputs["total_pig_iron_produced"] / ( + inputs["system_capacity"] * self.n_timesteps + ) + outputs["rated_pig_iron_production"] = inputs["system_capacity"] + outputs["annual_pig_iron_produced"] = ( + outputs["total_pig_iron_produced"] * self.fraction_of_year_simulated + ) # feedstock consumption based on actual pig iron produced for feedstock_type, consumption_rate in feedstocks_usage_rates.items(): diff --git a/h2integrate/converters/iron/martin_mine_perf_model.py b/h2integrate/converters/iron/martin_mine_perf_model.py index d39bf17ae..0e352b10d 100644 --- a/h2integrate/converters/iron/martin_mine_perf_model.py +++ b/h2integrate/converters/iron/martin_mine_perf_model.py @@ -1,12 +1,12 @@ import numpy as np import pandas as pd -import openmdao.api as om from attrs import field, define from openmdao.utils import units from h2integrate import ROOT_DIR from h2integrate.core.utilities import BaseConfig, merge_shared_inputs from h2integrate.core.validators import contains +from h2integrate.core.model_baseclasses import PerformanceModelBaseClass @define(kw_only=True) @@ -30,13 +30,12 @@ class MartinIronMinePerformanceConfig(BaseConfig): mine: str = field(validator=contains(["Hibbing", "Northshore", "United", "Minorca", "Tilden"])) -class MartinIronMinePerformanceComponent(om.ExplicitComponent): - def initialize(self): - self.options.declare("driver_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - +class MartinIronMinePerformanceComponent(PerformanceModelBaseClass): def setup(self): + self.commodity = "iron_ore" + self.commodity_rate_units = "t/h" + self.commodity_amount_units = "t" + super().setup() n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] self.config = MartinIronMinePerformanceConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), @@ -47,7 +46,7 @@ def setup(self): "system_capacity", val=self.config.max_ore_production_rate_tonnes_per_hr, units="t/h", - desc="Annual ore production capacity", + desc="Ore production capacity", ) # Add electricity input, default to 0 --> set using feedstock component @@ -93,20 +92,20 @@ def setup(self): desc="Electricity consumed", ) - self.add_output( - "iron_ore_out", - val=0.0, - shape=n_timesteps, - units="t/h", - desc="Iron ore pellets produced", - ) - - self.add_output( - "total_iron_ore_produced", - val=1.0, - units="t/year", - desc="Total iron ore pellets produced anually", - ) + # self.add_output( + # "iron_ore_out", + # val=0.0, + # shape=n_timesteps, + # units="t/h", + # desc="Iron ore pellets produced", + # ) + + # self.add_output( + # "total_iron_ore_produced", + # val=1.0, + # units="t/year", + # desc="Total iron ore pellets produced anually", + # ) coeff_fpath = ROOT_DIR / "converters" / "iron" / "martin_ore" / "perf_coeffs.csv" # martin ore performance model @@ -246,5 +245,12 @@ def compute(self, inputs, outputs): outputs["iron_ore_out"] = processed_ore_production outputs["total_iron_ore_produced"] = np.sum(processed_ore_production) + outputs["annual_iron_ore_produced"] = ( + outputs["total_iron_ore_produced"] * self.fraction_of_year_simulated + ) + outputs["rated_iron_ore_production"] = inputs["system_capacity"] + outputs["capacity_factor"] = outputs["total_iron_ore_produced"] / ( + outputs["rated_iron_ore_production"] * self.n_timesteps + ) outputs["electricity_consumed"] = energy_consumed outputs["crude_ore_consumed"] = crude_ore_consumption From 1d0b1eac501c02886dc24d2092111c8516091570 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:07:23 -0700 Subject: [PATCH 36/63] updated geoh2 models --- .../geologic/aspen_surface_processing.py | 7 ++++++ .../geologic/h2_well_subsurface_baseclass.py | 23 ++++++++++-------- .../geologic/h2_well_surface_baseclass.py | 24 ++++++++++--------- .../hydrogen/geologic/simple_natural_geoh2.py | 7 ++++++ .../geologic/templeton_serpentinization.py | 8 +++++++ 5 files changed, 48 insertions(+), 21 deletions(-) diff --git a/h2integrate/converters/hydrogen/geologic/aspen_surface_processing.py b/h2integrate/converters/hydrogen/geologic/aspen_surface_processing.py index 49fc3a083..8ab314208 100644 --- a/h2integrate/converters/hydrogen/geologic/aspen_surface_processing.py +++ b/h2integrate/converters/hydrogen/geologic/aspen_surface_processing.py @@ -148,6 +148,13 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): outputs["water_consumed"] = water_in_kt_h outputs["steam_out"] = steam_out_kt_h outputs["total_hydrogen_produced"] = np.sum(h2_out_kg_hr) + outputs["annual_hydrogen_produced"] = ( + outputs["total_hydrogen_produced"] * self.fraction_of_year_simulated + ) + outputs["rated_hydrogen_production"] = wellhead_cap_kg_hr # TODO: double check + outputs["capacity_factor"] = outputs["total_hydrogen_produced"] / ( + outputs["rated_hydrogen_production"] * self.n_timesteps + ) @define diff --git a/h2integrate/converters/hydrogen/geologic/h2_well_subsurface_baseclass.py b/h2integrate/converters/hydrogen/geologic/h2_well_subsurface_baseclass.py index 470a800d4..213017059 100644 --- a/h2integrate/converters/hydrogen/geologic/h2_well_subsurface_baseclass.py +++ b/h2integrate/converters/hydrogen/geologic/h2_well_subsurface_baseclass.py @@ -1,9 +1,12 @@ -import openmdao.api as om from attrs import field, define from h2integrate.core.utilities import BaseConfig from h2integrate.core.validators import contains -from h2integrate.core.model_baseclasses import CostModelBaseClass, CostModelBaseConfig +from h2integrate.core.model_baseclasses import ( + CostModelBaseClass, + CostModelBaseConfig, + PerformanceModelBaseClass, +) from h2integrate.tools.inflation.inflate import inflate_cepci @@ -42,7 +45,7 @@ class GeoH2SubsurfacePerformanceConfig(BaseConfig): grain_size: float = field() -class GeoH2SubsurfacePerformanceBaseClass(om.ExplicitComponent): +class GeoH2SubsurfacePerformanceBaseClass(PerformanceModelBaseClass): """OpenMDAO component for modeling the performance of the well subsurface for geologic hydrogen. @@ -83,21 +86,21 @@ class GeoH2SubsurfacePerformanceBaseClass(om.ExplicitComponent): The total hydrogen produced over the plant lifetime, in kilograms per year. """ - def initialize(self): - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - self.options.declare("driver_config", types=dict) - def setup(self): + self.commodity = "hydrogen" + self.commodity_rate_units = "kg/h" + self.commodity_amount_units = "kg" + super().setup() + # inputs self.add_input("borehole_depth", units="m", val=self.config.borehole_depth) self.add_input("grain_size", units="m", val=self.config.grain_size) # outputs self.add_output("wellhead_gas_out", units="kg/h", shape=(8760,)) - self.add_output("hydrogen_out", units="kg/h", shape=(8760,)) + # self.add_output("hydrogen_out", units="kg/h", shape=(8760,)) self.add_output("total_wellhead_gas_produced", val=0.0, units="kg/year") - self.add_output("total_hydrogen_produced", val=0.0, units="kg/year") + # self.add_output("total_hydrogen_produced", val=0.0, units="kg/year") @define(kw_only=True) diff --git a/h2integrate/converters/hydrogen/geologic/h2_well_surface_baseclass.py b/h2integrate/converters/hydrogen/geologic/h2_well_surface_baseclass.py index 5acbe3035..38064e888 100644 --- a/h2integrate/converters/hydrogen/geologic/h2_well_surface_baseclass.py +++ b/h2integrate/converters/hydrogen/geologic/h2_well_surface_baseclass.py @@ -1,8 +1,11 @@ -import openmdao.api as om from attrs import field, define from h2integrate.core.utilities import BaseConfig -from h2integrate.core.model_baseclasses import CostModelBaseClass, CostModelBaseConfig +from h2integrate.core.model_baseclasses import ( + CostModelBaseClass, + CostModelBaseConfig, + PerformanceModelBaseClass, +) @define @@ -24,7 +27,7 @@ class GeoH2SurfacePerformanceConfig(BaseConfig): max_flow_in: float = field() -class GeoH2SurfacePerformanceBaseClass(om.ExplicitComponent): +class GeoH2SurfacePerformanceBaseClass(PerformanceModelBaseClass): """OpenMDAO component for modeling the performance of the wellhead surface processing for geologic hydrogen. @@ -63,12 +66,11 @@ class GeoH2SurfacePerformanceBaseClass(om.ExplicitComponent): The wellhead gas flow in kg/hour used for sizing the system - passed to the cost model. """ - def initialize(self): - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - self.options.declare("driver_config", types=dict) - def setup(self): + self.commodity = "hydrogen" + self.commodity_rate_units = "kg/h" + self.commodity_amount_units = "kg" + super().setup() n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] # inputs @@ -77,9 +79,9 @@ def setup(self): self.add_input("wellhead_h2_concentration_mol", units="mol/mol", val=-1.0) # outputs - self.add_output("hydrogen_out", units="kg/h", shape=(n_timesteps,)) + # self.add_output("hydrogen_out", units="kg/h", shape=(n_timesteps,)) self.add_output("hydrogen_concentration_out", units="mol/mol", val=-1.0) - self.add_output("total_hydrogen_produced", val=-1.0, units="kg/year") + # self.add_output("total_hydrogen_produced", val=-1.0, units="kg/year") self.add_output("max_flow_size", units="kg/h", val=self.config.max_flow_in) @@ -159,7 +161,7 @@ def setup(self): units="kg/h", desc=f"Hydrogen production rate in kg/h over {n_timesteps} hours.", ) - self.add_input("total_hydrogen_produced", val=0.0, units="kg/year") + self.add_input("total_hydrogen_produced", val=0.0, units="kg") self.add_input("custom_capex", val=self.config.custom_capex, units="USD") self.add_input("custom_opex", val=self.config.custom_opex, units="USD/year") diff --git a/h2integrate/converters/hydrogen/geologic/simple_natural_geoh2.py b/h2integrate/converters/hydrogen/geologic/simple_natural_geoh2.py index 0710244d2..8ca6d5fe7 100644 --- a/h2integrate/converters/hydrogen/geologic/simple_natural_geoh2.py +++ b/h2integrate/converters/hydrogen/geologic/simple_natural_geoh2.py @@ -149,3 +149,10 @@ def compute(self, inputs, outputs): outputs["max_wellhead_gas"] = init_wh_flow outputs["total_wellhead_gas_produced"] = np.sum(outputs["wellhead_gas_out"]) outputs["total_hydrogen_produced"] = np.sum(outputs["hydrogen_out"]) + outputs["annual_hydrogen_produced"] = ( + outputs["total_hydrogen_produced"] * self.fraction_of_year_simulated + ) + outputs["rated_hydrogen_production"] = init_wh_flow # TODO: double check + outputs["capacity_factor"] = outputs["total_hydrogen_produced"] / ( + outputs["rated_hydrogen_production"] * self.n_timesteps + ) diff --git a/h2integrate/converters/hydrogen/geologic/templeton_serpentinization.py b/h2integrate/converters/hydrogen/geologic/templeton_serpentinization.py index fcf5b94f9..11063eded 100644 --- a/h2integrate/converters/hydrogen/geologic/templeton_serpentinization.py +++ b/h2integrate/converters/hydrogen/geologic/templeton_serpentinization.py @@ -135,3 +135,11 @@ def compute(self, inputs, outputs): outputs["wellhead_gas_out"] = h2_prod_avg outputs["total_hydrogen_produced"] = np.sum(outputs["hydrogen_out"]) outputs["total_wellhead_gas_produced"] = np.sum(outputs["wellhead_gas_out"]) + + outputs["annual_hydrogen_produced"] = ( + outputs["total_hydrogen_produced"] * self.fraction_of_year_simulated + ) + outputs["rated_hydrogen_production"] = np.max(h2_prod_avg) # TODO: double check + outputs["capacity_factor"] = outputs["total_hydrogen_produced"] / ( + outputs["rated_hydrogen_production"] * self.n_timesteps + ) From f582c4c62371c36ac40a3baa95e1f72a66310366 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 27 Jan 2026 17:18:46 -0700 Subject: [PATCH 37/63] updated battery --- .../storage/battery/battery_baseclass.py | 28 +++++++++---------- h2integrate/storage/battery/pysam_battery.py | 12 +++++++- .../storage/battery/test/test_battery_cost.py | 1 + .../test_battery/test_pysam_battery.py | 21 +++++++++++--- 4 files changed, 43 insertions(+), 19 deletions(-) diff --git a/h2integrate/storage/battery/battery_baseclass.py b/h2integrate/storage/battery/battery_baseclass.py index 6affdcaf5..f56d326e3 100644 --- a/h2integrate/storage/battery/battery_baseclass.py +++ b/h2integrate/storage/battery/battery_baseclass.py @@ -1,13 +1,13 @@ -import openmdao.api as om +from h2integrate.core.model_baseclasses import PerformanceModelBaseClass -class BatteryPerformanceBaseClass(om.ExplicitComponent): - def initialize(self): - self.options.declare("driver_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - +class BatteryPerformanceBaseClass(PerformanceModelBaseClass): def setup(self): + self.commodity = "electricity" + self.commodity_rate_units = "kW" + self.commodity_amount_units = "kW*h" + super().setup() + self.add_input( "electricity_in", val=0.0, @@ -16,13 +16,13 @@ def setup(self): desc="Power input to Battery", ) - self.add_output( - "electricity_out", - val=0.0, - copy_shape="electricity_in", - units="kW", - desc="Total electricity out of Battery", - ) + # self.add_output( + # "electricity_out", + # val=0.0, + # copy_shape="electricity_in", + # units="kW", + # desc="Total electricity out of Battery", + # ) self.add_output( "SOC", diff --git a/h2integrate/storage/battery/pysam_battery.py b/h2integrate/storage/battery/pysam_battery.py index 6b1b551fd..a8a9f6520 100644 --- a/h2integrate/storage/battery/pysam_battery.py +++ b/h2integrate/storage/battery/pysam_battery.py @@ -239,7 +239,8 @@ def setup(self): desc="Battery storage capacity", ) - BatteryPerformanceBaseClass.setup(self) + super().setup() + # BatteryPerformanceBaseClass.setup(self) self.add_input( "electricity_demand", @@ -427,6 +428,15 @@ def compute(self, inputs, outputs, discrete_inputs=[], discrete_outputs=[]): outputs["SOC"] = soc outputs["P_chargeable"] = self.outputs.P_chargeable outputs["P_dischargeable"] = self.outputs.P_dischargeable + outputs["rated_electricity_production"] = inputs["max_charge_rate"] + + outputs["total_electricity_produced"] = np.sum(total_power_out) + outputs["annual_electricity_produced"] = ( + outputs["total_electricity_produced"] * self.fraction_of_year_simulated + ) + outputs["capacity_factor"] = outputs["total_electricity_produced"] / ( + outputs["rated_electricity_production"] * self.n_timesteps + ) def simulate( self, diff --git a/h2integrate/storage/battery/test/test_battery_cost.py b/h2integrate/storage/battery/test/test_battery_cost.py index 2fdd8fa84..6d115fa04 100644 --- a/h2integrate/storage/battery/test/test_battery_cost.py +++ b/h2integrate/storage/battery/test/test_battery_cost.py @@ -15,6 +15,7 @@ def plant_config(): "plant_life": 30, "simulation": { "n_timesteps": 8760, + "dt": 3600, }, }, } diff --git a/h2integrate/storage/battery/test_battery/test_pysam_battery.py b/h2integrate/storage/battery/test_battery/test_pysam_battery.py index 6f6467a0f..eac9541f2 100644 --- a/h2integrate/storage/battery/test_battery/test_pysam_battery.py +++ b/h2integrate/storage/battery/test_battery/test_pysam_battery.py @@ -5,6 +5,7 @@ import numpy as np import pytest import openmdao.api as om +from pytest import fixture from h2integrate.storage.battery.pysam_battery import ( PySAMBatteryPerformanceModel, @@ -12,7 +13,19 @@ ) -def test_pysam_battery_performance_model_without_controller(subtests): +@fixture +def plant_config(): + plant = { + "plant_life": 30, + "simulation": { + "dt": 3600, + "n_timesteps": 24, + }, + } + return {"plant": plant} + + +def test_pysam_battery_performance_model_without_controller(plant_config, subtests): # Get the directory of the current script current_dir = Path(__file__).parent @@ -57,7 +70,7 @@ def test_pysam_battery_performance_model_without_controller(subtests): prob.model.add_subsystem( "pysam_battery", PySAMBatteryPerformanceModel( - plant_config={"plant": {"simulation": {"dt": 3600, "n_timesteps": 24}}}, + plant_config=plant_config, tech_config=tech_config["technologies"]["battery"], ), promotes=["*"], @@ -240,7 +253,7 @@ def test_battery_config(subtests): PySAMBatteryPerformanceModelConfig.from_dict(data) -def test_battery_initialization(subtests): +def test_battery_initialization(plant_config, subtests): # Get the directory of the current script current_dir = Path(__file__).parent @@ -252,7 +265,7 @@ def test_battery_initialization(subtests): tech_config = yaml.safe_load(file) battery = PySAMBatteryPerformanceModel( - plant_config={"plant": {"simulation": {"dt": 3600, "n_timesteps": 24}}}, + plant_config=plant_config, tech_config=tech_config["technologies"]["battery"], ) From 71a1b858f99e62b7c259fb320e4e7a3acca80933 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Wed, 28 Jan 2026 09:36:57 -0700 Subject: [PATCH 38/63] added todo comments to storage models --- h2integrate/storage/generic_storage_cost.py | 2 +- h2integrate/storage/simple_generic_storage.py | 2 +- h2integrate/storage/simple_storage_auto_sizing.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/h2integrate/storage/generic_storage_cost.py b/h2integrate/storage/generic_storage_cost.py index 4ec34e200..b4d6bd999 100644 --- a/h2integrate/storage/generic_storage_cost.py +++ b/h2integrate/storage/generic_storage_cost.py @@ -35,7 +35,7 @@ class GenericStorageCostConfig(CostModelBaseConfig): max_charge_rate: float = field() commodity_units: str = field( validator=contains(["W", "kW", "MW", "GW", "TW", "g/h", "kg/h", "t/h", "MMBtu/h"]) - ) + ) # TODO: udpate to commodity_rate_units class GenericStorageCostModel(CostModelBaseClass): diff --git a/h2integrate/storage/simple_generic_storage.py b/h2integrate/storage/simple_generic_storage.py index 2f8c7b73e..3a47d5dc6 100644 --- a/h2integrate/storage/simple_generic_storage.py +++ b/h2integrate/storage/simple_generic_storage.py @@ -7,7 +7,7 @@ @define(kw_only=True) class SimpleGenericStorageConfig(BaseConfig): commodity_name: str = field() - commodity_units: str = field() + commodity_units: str = field() # TODO: update to commodity_rate_units class SimpleGenericStorage(om.ExplicitComponent): diff --git a/h2integrate/storage/simple_storage_auto_sizing.py b/h2integrate/storage/simple_storage_auto_sizing.py index a16cb070a..7443874b9 100644 --- a/h2integrate/storage/simple_storage_auto_sizing.py +++ b/h2integrate/storage/simple_storage_auto_sizing.py @@ -18,7 +18,7 @@ class StorageSizingModelConfig(BaseConfig): """ commodity_name: str = field(default="hydrogen") - commodity_units: str = field(default="kg/h") + commodity_units: str = field(default="kg/h") # TODO: update to commodity_rate_units demand_profile: int | float | list = field(default=0.0) From be535a30044d51ee2689bef34ea1fada29c1df4c Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Wed, 28 Jan 2026 11:22:42 -0700 Subject: [PATCH 39/63] added unit tests to check that outputs are populated --- .../test/test_ammonia_synloop_model.py | 120 ++++++++- .../ammonia/test/test_simple_ammonia_model.py | 187 ++++++++++---- .../converters/co2/marine/test/test_doc.py | 151 +++++++++++ .../converters/co2/marine/test/test_oae.py | 148 +++++++++++ h2integrate/converters/grid/test/test_grid.py | 115 +++++++++ .../converters/hopp/test/test_hopp_caching.py | 94 +++++++ .../geologic/test/test_geologic_h2.py | 99 ++++++++ .../test/test_pem_electrolyzer_performance.py | 129 ++++++++++ .../converters/iron/test/test_martin_mine.py | 99 ++++++++ .../converters/iron/test/test_rosner_dri.py | 98 +++++++ .../converters/methanol/methanol_baseclass.py | 4 +- .../converters/methanol/test/__init__.py | 0 .../converters/methanol/test/test_methanol.py | 239 ++++++++++++++++++ .../test/test_natural_gas_models.py | 125 ++++++++- .../nitrogen/test/test_simple_asu_model.py | 135 +++++++++- .../converters/solar/test/test_pysam_solar.py | 2 +- .../converters/steel/test/test_rosner_eaf.py | 98 +++++++ .../steel/test/test_simple_steel.py | 119 +++++++++ .../water/desal/test/test_ro_desalination.py | 97 +++++++ .../converters/water_power/test/__init__.py | 0 .../water_power/test/test_hydro_power.py | 142 +++++++++++ .../converters/wind/test/test_floris_wind.py | 107 ++++++++ .../converters/wind/test/test_pysam_wind.py | 100 ++++++++ 23 files changed, 2334 insertions(+), 74 deletions(-) create mode 100644 h2integrate/converters/hydrogen/test/test_pem_electrolyzer_performance.py create mode 100644 h2integrate/converters/methanol/test/__init__.py create mode 100644 h2integrate/converters/methanol/test/test_methanol.py create mode 100644 h2integrate/converters/steel/test/test_simple_steel.py create mode 100644 h2integrate/converters/water_power/test/__init__.py create mode 100644 h2integrate/converters/water_power/test/test_hydro_power.py diff --git a/h2integrate/converters/ammonia/test/test_ammonia_synloop_model.py b/h2integrate/converters/ammonia/test/test_ammonia_synloop_model.py index 570ecb5ef..911cf1ddf 100644 --- a/h2integrate/converters/ammonia/test/test_ammonia_synloop_model.py +++ b/h2integrate/converters/ammonia/test/test_ammonia_synloop_model.py @@ -3,6 +3,7 @@ import numpy as np import pytest import openmdao.api as om +from pytest import fixture from h2integrate import EXAMPLE_DIR from h2integrate.core.h2integrate_model import H2IntegrateModel @@ -10,7 +11,8 @@ from h2integrate.converters.ammonia.ammonia_synloop import AmmoniaSynLoopPerformanceModel -def make_synloop_config(): +@fixture +def synloop_config(): return { "model_inputs": { "shared_parameters": { @@ -40,8 +42,116 @@ def make_synloop_config(): } -def test_ammonia_synloop_limiting_cases(subtests): - config = make_synloop_config() +def test_ammonia_synloop_outputs(synloop_config, subtests): + plant_config = { + "plant": { + "plant_life": 30, + "simulation": { + "n_timesteps": 8760, + "dt": 3600, + }, + } + } + + cap_mult = 10.0e3 + # none of these inputs are limiting + n2 = np.ones(plant_config["plant"]["simulation"]["n_timesteps"]) * 5.0 * cap_mult # kg + h2 = np.ones(plant_config["plant"]["simulation"]["n_timesteps"]) * 2.0 * cap_mult + elec = np.ones(plant_config["plant"]["simulation"]["n_timesteps"]) * 0.006 * cap_mult + prob = om.Problem() + + comp = AmmoniaSynLoopPerformanceModel( + plant_config=plant_config, + tech_config=synloop_config, + driver_config={}, + ) + prob.model.add_subsystem("comp", comp, promotes=["*"]) + prob.setup() + prob.set_val("comp.hydrogen_in", h2, units="kg/h") + prob.set_val("comp.nitrogen_in", n2, units="kg/h") + prob.set_val("comp.electricity_in", elec, units="MW") + + prob.run_model() + + commodity = "ammonia" + commodity_amount_units = "kg" + commodity_rate_units = "kg/h" + plant_life = int(plant_config["plant"]["plant_life"]) + n_timesteps = int(plant_config["plant"]["simulation"]["n_timesteps"]) + + # Check that replacement schedule is between 0 and 1 + with subtests.test("0 <= replacement_schedule <=1"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") >= 0) + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") <= 1) + + with subtests.test("replacement_schedule length"): + assert len(prob.get_val("comp.replacement_schedule", units="unitless")) == plant_life + + # Check that capacity factor is between 0 and 1 with units of "unitless" + with subtests.test("0 <= capacity_factor (unitless) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") >= 0) + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") <= 1) + + # Check that capacity factor is between 1 and 100 with units of "percent" + with subtests.test("1 <= capacity_factor (percent) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="percent") >= 1) + assert np.all(prob.get_val("comp.capacity_factor", units="percent") <= 100) + + with subtests.test("capacity_factor length"): + assert len(prob.get_val("comp.capacity_factor", units="unitless")) == plant_life + + # Test that rated commodity production is greater than zero + with subtests.test(f"rated_{commodity}_production > 0"): + assert np.all( + prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units) > 0 + ) + + with subtests.test(f"rated_{commodity}_production length"): + assert ( + len(prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units)) == 1 + ) + + # Test that total commodity production is greater than zero + with subtests.test(f"total_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units) > 0 + ) + with subtests.test(f"total_{commodity}_produced length"): + assert ( + len(prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units)) == 1 + ) + + # Test that annual commodity production is greater than zero + with subtests.test(f"annual_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr") + > 0 + ) + + with subtests.test(f"annual_{commodity}_produced[1:] == annual_{commodity}_produced[0]"): + annual_production = prob.get_val( + f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr" + ) + assert np.all(annual_production[1:] == annual_production[0]) + + with subtests.test(f"annual_{commodity}_produced length"): + assert len(annual_production) == plant_life + + # Test that commodity output has some values greater than zero + with subtests.test(f"Some of {commodity}_out > 0"): + assert np.any(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units) > 0) + + with subtests.test(f"{commodity}_out length"): + assert len(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units)) == n_timesteps + + # Test default values + with subtests.test("operational_life default value"): + assert prob.get_val("comp.operational_life", units="yr") == plant_life + with subtests.test("replacement_schedule value"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") == 0) + + +def test_ammonia_synloop_limiting_cases(synloop_config, subtests): plant_info = { "plant_life": 30, "simulation": { @@ -68,7 +178,9 @@ def test_ammonia_synloop_limiting_cases(subtests): ) prob = om.Problem() - comp = AmmoniaSynLoopPerformanceModel(plant_config={"plant": plant_info}, tech_config=config) + comp = AmmoniaSynLoopPerformanceModel( + plant_config={"plant": plant_info}, tech_config=synloop_config + ) prob.model.add_subsystem("synloop", comp) prob.setup() prob.set_val("synloop.hydrogen_in", h2, units="kg/h") diff --git a/h2integrate/converters/ammonia/test/test_simple_ammonia_model.py b/h2integrate/converters/ammonia/test/test_simple_ammonia_model.py index 231e5eced..7652e6386 100644 --- a/h2integrate/converters/ammonia/test/test_simple_ammonia_model.py +++ b/h2integrate/converters/ammonia/test/test_simple_ammonia_model.py @@ -1,5 +1,7 @@ +import numpy as np import pytest import openmdao.api as om +from pytest import fixture from h2integrate.converters.ammonia.simple_ammonia_model import ( SimpleAmmoniaCostModel, @@ -7,40 +9,139 @@ ) -plant_config = { - "plant": { - "plant_life": 30, - "simulation": { - "n_timesteps": 8760, - }, - }, -} - -tech_config_dict = { - "model_inputs": { - "shared_parameters": { - "plant_capacity_kgpy": 1000000.0, - "plant_capacity_factor": 0.9, - }, - "cost_parameters": { - "electricity_cost": 91, - # "hydrogen_cost": 4.023963541079105, - "cooling_water_cost": 0.00516275276753, - "iron_based_catalyst_cost": 25, - "oxygen_cost": 0, - "electricity_consumption": 0.0001207, - "hydrogen_consumption": 0.197284403, - "cooling_water_consumption": 0.049236824, - "iron_based_catalyst_consumption": 0.000091295354067341, - "oxygen_byproduct": 0.29405077250145, - "capex_scaling_exponent": 0.6, - "cost_year": 2022, +@fixture +def plant_config(): + plant_config = { + "plant": { + "plant_life": 30, + "simulation": { + "n_timesteps": 8760, + "dt": 3600, + }, }, } -} + return plant_config + + +@fixture +def tech_config(): + tech_config_dict = { + "model_inputs": { + "shared_parameters": { + "plant_capacity_kgpy": 1000000.0, + "plant_capacity_factor": 0.9, + }, + "cost_parameters": { + "electricity_cost": 91, + # "hydrogen_cost": 4.023963541079105, + "cooling_water_cost": 0.00516275276753, + "iron_based_catalyst_cost": 25, + "oxygen_cost": 0, + "electricity_consumption": 0.0001207, + "hydrogen_consumption": 0.197284403, + "cooling_water_consumption": 0.049236824, + "iron_based_catalyst_consumption": 0.000091295354067341, + "oxygen_byproduct": 0.29405077250145, + "capex_scaling_exponent": 0.6, + "cost_year": 2022, + }, + } + } + return tech_config_dict + + +def test_simple_ammonia_performance_model_outputs(plant_config, tech_config, subtests): + prob = om.Problem() + comp = SimpleAmmoniaPerformanceModel( + plant_config=plant_config, + tech_config=tech_config, + ) + + plant_life = int(plant_config["plant"]["plant_life"]) + n_timesteps = int(plant_config["plant"]["simulation"]["n_timesteps"]) + + prob.model.add_subsystem("comp", comp) + prob.setup() + # Set dummy hydrogen input (array of n_timesteps for shape test) + prob.set_val("comp.hydrogen_in", [10.0] * n_timesteps, units="kg/h") + prob.run_model() + commodity = "ammonia" + commodity_amount_units = "kg" + commodity_rate_units = "kg/h" + + # Check that replacement schedule is between 0 and 1 + with subtests.test("0 <= replacement_schedule <=1"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") >= 0) + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") <= 1) + + with subtests.test("replacement_schedule length"): + assert len(prob.get_val("comp.replacement_schedule", units="unitless")) == plant_life + + # Check that capacity factor is between 0 and 1 with units of "unitless" + with subtests.test("0 <= capacity_factor (unitless) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") >= 0) + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") <= 1) + # Check that capacity factor is between 1 and 100 with units of "percent" + with subtests.test("1 <= capacity_factor (percent) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="percent") >= 1) + assert np.all(prob.get_val("comp.capacity_factor", units="percent") <= 100) -def test_simple_ammonia_performance_model(): + with subtests.test("capacity_factor length"): + assert len(prob.get_val("comp.capacity_factor", units="unitless")) == plant_life + + # Test that rated commodity production is greater than zero + with subtests.test(f"rated_{commodity}_production > 0"): + assert np.all( + prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units) > 0 + ) + + with subtests.test(f"rated_{commodity}_production length"): + assert ( + len(prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units)) == 1 + ) + + # Test that total commodity production is greater than zero + with subtests.test(f"total_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units) > 0 + ) + with subtests.test(f"total_{commodity}_produced length"): + assert ( + len(prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units)) == 1 + ) + + # Test that annual commodity production is greater than zero + with subtests.test(f"annual_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr") + > 0 + ) + + with subtests.test(f"annual_{commodity}_produced[1:] == annual_{commodity}_produced[0]"): + annual_production = prob.get_val( + f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr" + ) + assert np.all(annual_production[1:] == annual_production[0]) + + with subtests.test(f"annual_{commodity}_produced length"): + assert len(annual_production) == plant_life + + # Test that commodity output has some values greater than zero + with subtests.test(f"Some of {commodity}_out > 0"): + assert np.any(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units) > 0) + + with subtests.test(f"{commodity}_out length"): + assert len(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units)) == n_timesteps + + # Test default values + with subtests.test("operational_life default value"): + assert prob.get_val("comp.operational_life", units="yr") == plant_life + with subtests.test("replacement_schedule value"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") == 0) + + +def test_simple_ammonia_performance_model(tech_config, subtests): plant_info = { "plant_life": 30, "simulation": { @@ -52,7 +153,7 @@ def test_simple_ammonia_performance_model(): prob = om.Problem() comp = SimpleAmmoniaPerformanceModel( plant_config={"plant": plant_info}, - tech_config=tech_config_dict, + tech_config=tech_config, ) prob.model.add_subsystem("ammonia_perf", comp) prob.setup() @@ -62,23 +163,23 @@ def test_simple_ammonia_performance_model(): # Dummy expected values expected_total = 1000000.0 * 0.9 expected_out = expected_total / 2 - assert pytest.approx(prob.get_val("ammonia_perf.total_ammonia_produced")) == expected_total - assert all(pytest.approx(x) == expected_out for x in prob.get_val("ammonia_perf.ammonia_out")) + with subtests.test("total ammonia produced"): + assert pytest.approx(prob.get_val("ammonia_perf.total_ammonia_produced")) == expected_total + + with subtests.test("performance output"): + assert all( + pytest.approx(x) == expected_out for x in prob.get_val("ammonia_perf.ammonia_out") + ) -def test_simple_ammonia_cost_model(subtests): - plant_info = { - "plant_life": 30, - "simulation": { - "n_timesteps": 8760, - "dt": 3600, - }, - } + +def test_simple_ammonia_cost_model(plant_config, tech_config, subtests): + plant_config["plant"]["simulation"] prob = om.Problem() comp = SimpleAmmoniaCostModel( - plant_config={"plant": plant_info}, - tech_config=tech_config_dict, + plant_config=plant_config, + tech_config=tech_config, ) prob.model.add_subsystem("ammonia_cost", comp) diff --git a/h2integrate/converters/co2/marine/test/test_doc.py b/h2integrate/converters/co2/marine/test/test_doc.py index 06b5bcb88..3e48b8fd7 100644 --- a/h2integrate/converters/co2/marine/test/test_doc.py +++ b/h2integrate/converters/co2/marine/test/test_doc.py @@ -2,10 +2,161 @@ import importlib import numpy as np +import pytest import openmdao.api as om +from pytest import fixture from openmdao.utils.assert_utils import assert_near_equal +@fixture +def plant_config(): + plant_config = { + "plant": { + "plant_life": 30, + "simulation": { + "n_timesteps": 8760, + "dt": 3600, + }, + }, + } + return plant_config + + +@fixture +def tech_config(): + return { + "model_inputs": { + "performance_parameters": { + "power_single_ed_w": 24000000.0, # W + "flow_rate_single_ed_m3s": 0.6, # m^3/s + "number_ed_min": 1, + "number_ed_max": 10, + "E_HCl": 0.05, # kWh/mol + "E_NaOH": 0.05, # kWh/mol + "y_ext": 0.9, + "y_pur": 0.2, + "y_vac": 0.6, + "frac_ed_flow": 0.01, + "use_storage_tanks": True, + "initial_tank_volume_m3": 0.0, # m^3 + "store_hours": 12.0, # hours + "sal": 33.0, # ppt + "temp_C": 12.0, # degrees Celsius + "dic_i": 0.0022, # mol/L + "pH_i": 8.1, # initial pH + }, + }, + } + + +@fixture +def driver_config(): + driver_config = { + "general": { + "folder_output": "output", + }, + } + return driver_config + + +@pytest.mark.skipif(importlib.util.find_spec("mcm") is None, reason="mcm is not installed") +def test_doc_outputs(driver_config, plant_config, tech_config, subtests): + from h2integrate.converters.co2.marine.direct_ocean_capture import DOCPerformanceModel + + doc_model = DOCPerformanceModel( + driver_config=driver_config, plant_config=plant_config, tech_config=tech_config + ) + prob = om.Problem(model=om.Group()) + prob.model.add_subsystem("comp", doc_model, promotes=["*"]) + prob.setup() + rng = np.random.default_rng(seed=42) + base_power = np.linspace(3.0e8, 2.0e8, 8760) # 5 MW to 10 MW over 8760 hours + noise = rng.normal(loc=0, scale=0.5e8, size=8760) # ±0.5 MW noise + power_profile = base_power + noise + prob.set_val("comp.electricity_in", power_profile, units="W") + + # Run the model + prob.run_model() + + plant_life = int(plant_config["plant"]["plant_life"]) + n_timesteps = int(plant_config["plant"]["simulation"]["n_timesteps"]) + + commodity = "co2" + commodity_amount_units = "kg" + commodity_rate_units = "kg/h" + + # Check that replacement schedule is between 0 and 1 + with subtests.test("0 <= replacement_schedule <=1"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") >= 0) + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") <= 1) + + with subtests.test("replacement_schedule length"): + assert len(prob.get_val("comp.replacement_schedule", units="unitless")) == plant_life + + # Check that capacity factor is between 0 and 1 with units of "unitless" + with subtests.test("0 <= capacity_factor (unitless) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") >= 0) + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") <= 1) + + # Check that capacity factor is between 1 and 100 with units of "percent" + with subtests.test("1 <= capacity_factor (percent) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="percent") >= 1) + assert np.all(prob.get_val("comp.capacity_factor", units="percent") <= 100) + + with subtests.test("capacity_factor length"): + assert len(prob.get_val("comp.capacity_factor", units="unitless")) == plant_life + + # Test that rated commodity production is greater than zero + with subtests.test(f"rated_{commodity}_production > 0"): + assert np.all( + prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units) > 0 + ) + + with subtests.test(f"rated_{commodity}_production length"): + assert ( + len(prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units)) == 1 + ) + + # Test that total commodity production is greater than zero + with subtests.test(f"total_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units) > 0 + ) + with subtests.test(f"total_{commodity}_produced length"): + assert ( + len(prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units)) == 1 + ) + + # Test that annual commodity production is greater than zero + with subtests.test(f"annual_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr") + > 0 + ) + + with subtests.test(f"annual_{commodity}_produced[1:] == annual_{commodity}_produced[0]"): + annual_production = prob.get_val( + f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr" + ) + assert np.all(annual_production[1:] == annual_production[0]) + + with subtests.test(f"annual_{commodity}_produced length"): + assert len(annual_production) == plant_life + + # Test that commodity output has some values greater than zero + with subtests.test(f"Some of {commodity}_out > 0"): + assert np.any(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units) > 0) + + with subtests.test(f"{commodity}_out length"): + assert len(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units)) == n_timesteps + + # Test default values + with subtests.test("operational_life default value"): + assert prob.get_val("comp.operational_life", units="yr") == plant_life + with subtests.test("replacement_schedule value"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") == 0) + + @unittest.skipUnless(importlib.util.find_spec("mcm") is not None, "mcm is not installed") class TestDOCPerformanceModel(unittest.TestCase): def setUp(self): diff --git a/h2integrate/converters/co2/marine/test/test_oae.py b/h2integrate/converters/co2/marine/test/test_oae.py index 2e3c97c4b..a69106b51 100644 --- a/h2integrate/converters/co2/marine/test/test_oae.py +++ b/h2integrate/converters/co2/marine/test/test_oae.py @@ -2,10 +2,158 @@ import importlib import numpy as np +import pytest import openmdao.api as om +from pytest import fixture from openmdao.utils.assert_utils import assert_near_equal +@fixture +def plant_config(): + plant_config = { + "plant": { + "plant_life": 30, + "simulation": { + "n_timesteps": 8760, + "dt": 3600, + }, + }, + } + return plant_config + + +@fixture +def tech_config(): + return { + "model_inputs": { + "performance_parameters": { + "number_ed_min": 1, + "number_ed_max": 10, + "max_ed_system_flow_rate_m3s": 0.0324, # m^3/s + "frac_base_flow": 0.5, + "assumed_CDR_rate": 0.8, # mol CO2/mol NaOH + "use_storage_tanks": True, + "initial_tank_volume_m3": 0.0, # m^3 + "store_hours": 12.0, # hours + "acid_disposal_method": "sell rca", + "initial_salinity_ppt": 73.76, # ppt + "initial_temp_C": 10.0, # degrees Celsius + "initial_dic_mol_per_L": 0.0044, # mol/L + "initial_pH": 8.1, # initial pH + }, + }, + } + + +@fixture +def driver_config(): + driver_config = { + "general": { + "folder_output": "output", + }, + } + return driver_config + + +@pytest.mark.skipif(importlib.util.find_spec("mcm") is None, reason="mcm is not installed") +def test_doc_outputs(driver_config, plant_config, tech_config, subtests): + from h2integrate.converters.co2.marine.ocean_alkalinity_enhancement import OAEPerformanceModel + + doc_model = OAEPerformanceModel( + driver_config=driver_config, plant_config=plant_config, tech_config=tech_config + ) + prob = om.Problem(model=om.Group()) + prob.model.add_subsystem("comp", doc_model, promotes=["*"]) + prob.setup() + + rng = np.random.default_rng(seed=42) + base_power = np.linspace(3.0e8, 2.0e8, 8760) # 300 MW to 200 MW over 8760 hours + noise = rng.normal(loc=0, scale=0.5e8, size=8760) # ±50 MW noise + power_profile = base_power + noise + prob.set_val("comp.electricity_in", power_profile, units="W") + + # Run the model + prob.run_model() + + plant_life = int(plant_config["plant"]["plant_life"]) + n_timesteps = int(plant_config["plant"]["simulation"]["n_timesteps"]) + + commodity = "co2" + commodity_amount_units = "kg" + commodity_rate_units = "kg/h" + + # Check that replacement schedule is between 0 and 1 + with subtests.test("0 <= replacement_schedule <=1"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") >= 0) + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") <= 1) + + with subtests.test("replacement_schedule length"): + assert len(prob.get_val("comp.replacement_schedule", units="unitless")) == plant_life + + # Check that capacity factor is between 0 and 1 with units of "unitless" + with subtests.test("0 <= capacity_factor (unitless) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") >= 0) + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") <= 1) + + # Check that capacity factor is between 1 and 100 with units of "percent" + with subtests.test("1 <= capacity_factor (percent) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="percent") >= 1) + assert np.all(prob.get_val("comp.capacity_factor", units="percent") <= 100) + + with subtests.test("capacity_factor length"): + assert len(prob.get_val("comp.capacity_factor", units="unitless")) == plant_life + + # Test that rated commodity production is greater than zero + with subtests.test(f"rated_{commodity}_production > 0"): + assert np.all( + prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units) > 0 + ) + + with subtests.test(f"rated_{commodity}_production length"): + assert ( + len(prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units)) == 1 + ) + + # Test that total commodity production is greater than zero + with subtests.test(f"total_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units) > 0 + ) + with subtests.test(f"total_{commodity}_produced length"): + assert ( + len(prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units)) == 1 + ) + + # Test that annual commodity production is greater than zero + with subtests.test(f"annual_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr") + > 0 + ) + + with subtests.test(f"annual_{commodity}_produced[1:] == annual_{commodity}_produced[0]"): + annual_production = prob.get_val( + f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr" + ) + assert np.all(annual_production[1:] == annual_production[0]) + + with subtests.test(f"annual_{commodity}_produced length"): + assert len(annual_production) == plant_life + + # Test that commodity output has some values greater than zero + with subtests.test(f"Some of {commodity}_out > 0"): + assert np.any(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units) > 0) + + with subtests.test(f"{commodity}_out length"): + assert len(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units)) == n_timesteps + + # Test default values + with subtests.test("operational_life default value"): + assert prob.get_val("comp.operational_life", units="yr") == plant_life + with subtests.test("replacement_schedule value"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") == 0) + + @unittest.skipUnless(importlib.util.find_spec("mcm") is not None, "mcm is not installed") class TestOAEPerformanceModel(unittest.TestCase): def setUp(self): diff --git a/h2integrate/converters/grid/test/test_grid.py b/h2integrate/converters/grid/test/test_grid.py index d70974054..992505b0b 100644 --- a/h2integrate/converters/grid/test/test_grid.py +++ b/h2integrate/converters/grid/test/test_grid.py @@ -2,10 +2,125 @@ import numpy as np import openmdao.api as om +from pytest import fixture from h2integrate.converters.grid.grid import GridCostModel, GridPerformanceModel +@fixture +def plant_config(): + return { + "plant": { + "plant_life": 30, + "simulation": { + "n_timesteps": 8760, + "dt": 3600, + }, + } + } + + +def test_grid_performance_outputs(plant_config, subtests): + prob = om.Problem() + plant_life = int(plant_config["plant"]["plant_life"]) + n_timesteps = int(plant_config["plant"]["simulation"]["n_timesteps"]) + tech_config = { + "model_inputs": { + "shared_parameters": { + "interconnection_size": 50000.0 # 50 MW + } + } + } + + prob.model.add_subsystem( + "comp", + GridPerformanceModel(driver_config={}, plant_config=plant_config, tech_config=tech_config), + ) + + prob.setup() + + # Set demand below interconnection limit + demand = np.full(n_timesteps, 30000.0) # 30 MW demand + prob.set_val("comp.electricity_demand", demand) + + prob.run_model() + + commodity = "electricity" + commodity_amount_units = "kW*h" + commodity_rate_units = "kW" + + # Check that replacement schedule is between 0 and 1 + with subtests.test("0 <= replacement_schedule <=1"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") >= 0) + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") <= 1) + + with subtests.test("replacement_schedule length"): + assert len(prob.get_val("comp.replacement_schedule", units="unitless")) == plant_life + + # Check that capacity factor is between 0 and 1 with units of "unitless" + with subtests.test("0 <= capacity_factor (unitless) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") >= 0) + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") <= 1) + + # Check that capacity factor is between 1 and 100 with units of "percent" + with subtests.test("1 <= capacity_factor (percent) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="percent") >= 1) + assert np.all(prob.get_val("comp.capacity_factor", units="percent") <= 100) + + with subtests.test("capacity_factor length"): + assert len(prob.get_val("comp.capacity_factor", units="unitless")) == plant_life + + # Test that rated commodity production is greater than zero + with subtests.test(f"rated_{commodity}_production > 0"): + assert np.all( + prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units) > 0 + ) + + with subtests.test(f"rated_{commodity}_production length"): + assert ( + len(prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units)) == 1 + ) + + # Test that total commodity production is greater than zero + with subtests.test(f"total_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units) > 0 + ) + with subtests.test(f"total_{commodity}_produced length"): + assert ( + len(prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units)) == 1 + ) + + # Test that annual commodity production is greater than zero + with subtests.test(f"annual_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr") + > 0 + ) + + with subtests.test(f"annual_{commodity}_produced[1:] == annual_{commodity}_produced[0]"): + annual_production = prob.get_val( + f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr" + ) + assert np.all(annual_production[1:] == annual_production[0]) + + with subtests.test(f"annual_{commodity}_produced length"): + assert len(annual_production) == plant_life + + # Test that commodity output has some values greater than zero + with subtests.test(f"Some of {commodity}_out > 0"): + assert np.any(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units) > 0) + + with subtests.test(f"{commodity}_out length"): + assert len(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units)) == n_timesteps + + # Test default values + with subtests.test("operational_life default value"): + assert prob.get_val("comp.operational_life", units="yr") == plant_life + with subtests.test("replacement_schedule value"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") == 0) + + class TestGridPerformanceModel(unittest.TestCase): """Test cases for GridPerformanceModel.""" diff --git a/h2integrate/converters/hopp/test/test_hopp_caching.py b/h2integrate/converters/hopp/test/test_hopp_caching.py index 2579826ed..b45c10960 100644 --- a/h2integrate/converters/hopp/test/test_hopp_caching.py +++ b/h2integrate/converters/hopp/test/test_hopp_caching.py @@ -2,6 +2,7 @@ import shutil from pathlib import Path +import numpy as np import openmdao.api as om from pytest import fixture @@ -25,6 +26,99 @@ def tech_config(): return hopp_tech_config +def test_hopp_wrapper_outputs(subtests, plant_config, tech_config): + tech_config["model_inputs"]["performance_parameters"]["enable_caching"] = False + tech_config["model_inputs"]["performance_parameters"]["hopp_config"]["technologies"]["wind"][ + "num_turbines" + ] = 4 + prob = om.Problem() + + hopp_perf = HOPPComponent( + plant_config=plant_config, + tech_config=tech_config, + driver_config={}, + ) + prob.model.add_subsystem("comp", hopp_perf, promotes=["*"]) + prob.setup() + prob.run_model() + plant_life = int(plant_config["plant"]["plant_life"]) + n_timesteps = int(plant_config["plant"]["simulation"]["n_timesteps"]) + commodity = "electricity" + commodity_amount_units = "kW*h" + commodity_rate_units = "kW" + + # Check that replacement schedule is between 0 and 1 + with subtests.test("0 <= replacement_schedule <=1"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") >= 0) + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") <= 1) + + with subtests.test("replacement_schedule length"): + assert len(prob.get_val("comp.replacement_schedule", units="unitless")) == plant_life + + # Check that capacity factor is between 0 and 1 with units of "unitless" + with subtests.test("0 <= capacity_factor (unitless) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") >= 0) + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") <= 1) + + # Check that capacity factor is between 1 and 100 with units of "percent" + with subtests.test("1 <= capacity_factor (percent) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="percent") >= 1) + assert np.all(prob.get_val("comp.capacity_factor", units="percent") <= 100) + + with subtests.test("capacity_factor length"): + assert len(prob.get_val("comp.capacity_factor", units="unitless")) == plant_life + + # Test that rated commodity production is greater than zero + with subtests.test(f"rated_{commodity}_production > 0"): + assert np.all( + prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units) > 0 + ) + + with subtests.test(f"rated_{commodity}_production length"): + assert ( + len(prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units)) == 1 + ) + + # Test that total commodity production is greater than zero + with subtests.test(f"total_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units) > 0 + ) + with subtests.test(f"total_{commodity}_produced length"): + assert ( + len(prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units)) == 1 + ) + + # Test that annual commodity production is greater than zero + with subtests.test(f"annual_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr") + > 0 + ) + + with subtests.test(f"annual_{commodity}_produced[1:] == annual_{commodity}_produced[0]"): + annual_production = prob.get_val( + f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr" + ) + assert np.all(annual_production[1:] == annual_production[0]) + + with subtests.test(f"annual_{commodity}_produced length"): + assert len(annual_production) == plant_life + + # Test that commodity output has some values greater than zero + with subtests.test(f"Some of {commodity}_out > 0"): + assert np.any(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units) > 0) + + with subtests.test(f"{commodity}_out length"): + assert len(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units)) == n_timesteps + + # Test default values + with subtests.test("operational_life default value"): + assert prob.get_val("comp.operational_life", units="yr") == plant_life + with subtests.test("replacement_schedule value"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") == 0) + + def test_hopp_wrapper_cache_filenames(subtests, plant_config, tech_config): cache_dir = EXAMPLE_DIR / "25_sizing_modes" / "test_cache" diff --git a/h2integrate/converters/hydrogen/geologic/test/test_geologic_h2.py b/h2integrate/converters/hydrogen/geologic/test/test_geologic_h2.py index 417d5c1ab..130a5e7f4 100644 --- a/h2integrate/converters/hydrogen/geologic/test/test_geologic_h2.py +++ b/h2integrate/converters/hydrogen/geologic/test/test_geologic_h2.py @@ -91,6 +91,105 @@ def aspen_geoh2_config(): return {"model_inputs": model_inputs} +def test_aspen_geoh2_performance_outputs( + subtests, plant_config, geoh2_subsurface_well, aspen_geoh2_config +): + prob = om.Problem() + perf_comp = AspenGeoH2SurfacePerformanceModel( + plant_config=plant_config, + tech_config=aspen_geoh2_config, + driver_config={}, + ) + + well_group = prob.model.add_subsystem("well", om.Group()) + well_group.add_subsystem("perf", geoh2_subsurface_well, promotes=["*"]) + + tech_group = prob.model.add_subsystem("comp", om.Group()) + tech_group.add_subsystem("perf", perf_comp, promotes=["*"]) + + prob.model.connect("well.wellhead_gas_out", "comp.wellhead_gas_in") + prob.model.connect("well.wellhead_h2_concentration_mol", "comp.wellhead_h2_concentration_mol") + + prob.setup() + prob.run_model() + commodity = "hydrogen" + commodity_rate_units = "kg/h" + commodity_amount_units = "kg" + plant_life = int(plant_config["plant"]["plant_life"]) + n_timesteps = int(plant_config["plant"]["simulation"]["n_timesteps"]) + + # Check that replacement schedule is between 0 and 1 + with subtests.test("0 <= replacement_schedule <=1"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") >= 0) + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") <= 1) + + with subtests.test("replacement_schedule length"): + assert len(prob.get_val("comp.replacement_schedule", units="unitless")) == plant_life + + # Check that capacity factor is between 0 and 1 with units of "unitless" + with subtests.test("0 <= capacity_factor (unitless) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") >= 0) + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") <= 1) + + # Check that capacity factor is between 1 and 100 with units of "percent" + with subtests.test("1 <= capacity_factor (percent) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="percent") >= 1) + assert np.all(prob.get_val("comp.capacity_factor", units="percent") <= 100) + + with subtests.test("capacity_factor length"): + assert len(prob.get_val("comp.capacity_factor", units="unitless")) == plant_life + + # Test that rated commodity production is greater than zero + with subtests.test(f"rated_{commodity}_production > 0"): + assert np.all( + prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units) > 0 + ) + + with subtests.test(f"rated_{commodity}_production length"): + assert ( + len(prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units)) == 1 + ) + + # Test that total commodity production is greater than zero + with subtests.test(f"total_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units) > 0 + ) + with subtests.test(f"total_{commodity}_produced length"): + assert ( + len(prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units)) == 1 + ) + + # Test that annual commodity production is greater than zero + with subtests.test(f"annual_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr") + > 0 + ) + + with subtests.test(f"annual_{commodity}_produced[1:] == annual_{commodity}_produced[0]"): + annual_production = prob.get_val( + f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr" + ) + assert np.all(annual_production[1:] == annual_production[0]) + + with subtests.test(f"annual_{commodity}_produced length"): + assert len(annual_production) == plant_life + + # Test that commodity output has some values greater than zero + with subtests.test(f"Some of {commodity}_out > 0"): + assert np.any(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units) > 0) + + with subtests.test(f"{commodity}_out length"): + assert len(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units)) == n_timesteps + + # Test default values + with subtests.test("operational_life default value"): + assert prob.get_val("comp.operational_life", units="yr") == plant_life + with subtests.test("replacement_schedule value"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") == 0) + + def test_aspen_geoh2_performance(subtests, plant_config, geoh2_subsurface_well, aspen_geoh2_config): prob = om.Problem() perf_comp = AspenGeoH2SurfacePerformanceModel( diff --git a/h2integrate/converters/hydrogen/test/test_pem_electrolyzer_performance.py b/h2integrate/converters/hydrogen/test/test_pem_electrolyzer_performance.py new file mode 100644 index 000000000..2d209e974 --- /dev/null +++ b/h2integrate/converters/hydrogen/test/test_pem_electrolyzer_performance.py @@ -0,0 +1,129 @@ +import numpy as np +import openmdao.api as om +from pytest import fixture + +from h2integrate.converters.hydrogen.pem_electrolyzer import ECOElectrolyzerPerformanceModel + + +@fixture +def plant_config(): + plant_config = { + "plant": { + "plant_life": 30, + "simulation": { + "n_timesteps": 8760, + "dt": 3600, + }, + }, + } + return plant_config + + +@fixture +def tech_config(): + config = { + "model_inputs": { + "performance_parameters": { + "n_clusters": 4.0, + "location": "onshore", + "cluster_rating_MW": 10, + "eol_eff_percent_loss": 10.0, + "uptime_hours_until_eol": 8000, + "include_degradation_penalty": True, + "turndown_ratio": 0.1, + "electrolyzer_capex": 10.0, + } + } + } + return config + + +def test_electrolyzer_outputs(tech_config, plant_config, subtests): + plant_life = int(plant_config["plant"]["plant_life"]) + n_timesteps = int(plant_config["plant"]["simulation"]["n_timesteps"]) + + prob = om.Problem() + comp = ECOElectrolyzerPerformanceModel( + plant_config=plant_config, tech_config=tech_config, driver_config={} + ) + prob.model.add_subsystem("comp", comp, promotes=["*"]) + prob.setup() + power_profile = np.ones(n_timesteps) * 32.0 + prob.set_val("comp.electricity_in", power_profile, units="MW") + + prob.run_model() + + commodity = "hydrogen" + commodity_amount_units = "kg" + commodity_rate_units = "kg/h" + + # Check that replacement schedule is between 0 and 1 + with subtests.test("0 <= replacement_schedule <=1"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") >= 0) + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") <= 1) + + with subtests.test("replacement_schedule length"): + assert len(prob.get_val("comp.replacement_schedule", units="unitless")) == plant_life + + # Check that capacity factor is between 0 and 1 with units of "unitless" + with subtests.test("0 <= capacity_factor (unitless) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") >= 0) + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") <= 1) + + # Check that capacity factor is between 1 and 100 with units of "percent" + with subtests.test("1 <= capacity_factor (percent) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="percent") >= 1) + assert np.all(prob.get_val("comp.capacity_factor", units="percent") <= 100) + + with subtests.test("capacity_factor length"): + assert len(prob.get_val("comp.capacity_factor", units="unitless")) == plant_life + + # Test that rated commodity production is greater than zero + with subtests.test(f"rated_{commodity}_production > 0"): + assert np.all( + prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units) > 0 + ) + + with subtests.test(f"rated_{commodity}_production length"): + assert ( + len(prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units)) == 1 + ) + + # Test that total commodity production is greater than zero + with subtests.test(f"total_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units) > 0 + ) + with subtests.test(f"total_{commodity}_produced length"): + assert ( + len(prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units)) == 1 + ) + + # Test that annual commodity production is greater than zero + with subtests.test(f"annual_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr") + > 0 + ) + + with subtests.test(f"annual_{commodity}_produced[1:] == annual_{commodity}_produced[0]"): + annual_production = prob.get_val( + f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr" + ) + assert np.all(annual_production[1:] == annual_production[0]) + + with subtests.test(f"annual_{commodity}_produced length"): + assert len(annual_production) == plant_life + + # Test that commodity output has some values greater than zero + with subtests.test(f"Some of {commodity}_out > 0"): + assert np.any(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units) > 0) + + with subtests.test(f"{commodity}_out length"): + assert len(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units)) == n_timesteps + + # Test default values + with subtests.test("operational_life default value"): + assert prob.get_val("comp.operational_life", units="yr") == plant_life + with subtests.test("replacement_schedule value"): + assert np.any(prob.get_val("comp.replacement_schedule", units="unitless") == 0) diff --git a/h2integrate/converters/iron/test/test_martin_mine.py b/h2integrate/converters/iron/test/test_martin_mine.py index a69ad025f..749f43035 100644 --- a/h2integrate/converters/iron/test/test_martin_mine.py +++ b/h2integrate/converters/iron/test/test_martin_mine.py @@ -50,6 +50,105 @@ def driver_config(): return driver_config +def test_iron_mine_performance_outputs( + plant_config, driver_config, iron_ore_config_martin_om, subtests +): + prob = om.Problem() + iron_ore_perf = MartinIronMinePerformanceComponent( + plant_config=plant_config, + tech_config=iron_ore_config_martin_om, + driver_config=driver_config, + ) + prob.model.add_subsystem("comp", iron_ore_perf, promotes=["*"]) + prob.setup() + + annual_crude_ore = 25.0 * 1e6 + annual_electricity = 1030.0 * 1e6 + ore_rated_capacity = 516.0497610311598 + + prob.set_val("comp.electricity_in", [annual_electricity / 8760] * 8760, units="kW") + prob.set_val("comp.crude_ore_in", [annual_crude_ore / 8760] * 8760, units="t/h") + prob.set_val("comp.iron_ore_demand", [ore_rated_capacity] * 8760, units="t/h") + + prob.run_model() + commodity = "iron_ore" + commodity_rate_units = "kg/h" + commodity_amount_units = "kg" + plant_life = int(plant_config["plant"]["plant_life"]) + n_timesteps = int(plant_config["plant"]["simulation"]["n_timesteps"]) + + # Check that replacement schedule is between 0 and 1 + with subtests.test("0 <= replacement_schedule <=1"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") >= 0) + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") <= 1) + + with subtests.test("replacement_schedule length"): + assert len(prob.get_val("comp.replacement_schedule", units="unitless")) == plant_life + + # Check that capacity factor is between 0 and 1 with units of "unitless" + with subtests.test("0 <= capacity_factor (unitless) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") >= 0) + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") <= 1) + + # Check that capacity factor is between 1 and 100 with units of "percent" + with subtests.test("1 <= capacity_factor (percent) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="percent") >= 1) + assert np.all(prob.get_val("comp.capacity_factor", units="percent") <= 100) + + with subtests.test("capacity_factor length"): + assert len(prob.get_val("comp.capacity_factor", units="unitless")) == plant_life + + # Test that rated commodity production is greater than zero + with subtests.test(f"rated_{commodity}_production > 0"): + assert np.all( + prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units) > 0 + ) + + with subtests.test(f"rated_{commodity}_production length"): + assert ( + len(prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units)) == 1 + ) + + # Test that total commodity production is greater than zero + with subtests.test(f"total_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units) > 0 + ) + with subtests.test(f"total_{commodity}_produced length"): + assert ( + len(prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units)) == 1 + ) + + # Test that annual commodity production is greater than zero + with subtests.test(f"annual_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr") + > 0 + ) + + with subtests.test(f"annual_{commodity}_produced[1:] == annual_{commodity}_produced[0]"): + annual_production = prob.get_val( + f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr" + ) + assert np.all(annual_production[1:] == annual_production[0]) + + with subtests.test(f"annual_{commodity}_produced length"): + assert len(annual_production) == plant_life + + # Test that commodity output has some values greater than zero + with subtests.test(f"Some of {commodity}_out > 0"): + assert np.any(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units) > 0) + + with subtests.test(f"{commodity}_out length"): + assert len(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units)) == n_timesteps + + # Test default values + with subtests.test("operational_life default value"): + assert prob.get_val("comp.operational_life", units="yr") == plant_life + with subtests.test("replacement_schedule value"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") == 0) + + def test_baseline_iron_ore_costs(plant_config, driver_config, iron_ore_config_martin_om, subtests): martin_ore_capex = 1221599018.626594 martin_ore_fixed_om = 0.0 diff --git a/h2integrate/converters/iron/test/test_rosner_dri.py b/h2integrate/converters/iron/test/test_rosner_dri.py index f8cc00168..e04bfe7e7 100644 --- a/h2integrate/converters/iron/test/test_rosner_dri.py +++ b/h2integrate/converters/iron/test/test_rosner_dri.py @@ -120,6 +120,104 @@ def h2_feedstock_availability_costs(): return feedstocks_dict +def test_ng_dri_performance_outputs( + plant_config, ng_dri_base_config, ng_feedstock_availability_costs, subtests +): + prob = om.Problem() + + iron_dri_perf = NaturalGasIronReductionPlantPerformanceComponent( + plant_config=plant_config, + tech_config=ng_dri_base_config, + driver_config={}, + ) + prob.model.add_subsystem("comp", iron_dri_perf, promotes=["*"]) + prob.setup() + + for feedstock_name, feedstock_info in ng_feedstock_availability_costs.items(): + prob.set_val( + f"comp.{feedstock_name}_in", + feedstock_info["rated_capacity"], + units=feedstock_info["units"], + ) + prob.run_model() + commodity = "pig_iron" + commodity_rate_units = "kg/h" + commodity_amount_units = "kg" + plant_life = int(plant_config["plant"]["plant_life"]) + n_timesteps = int(plant_config["plant"]["simulation"]["n_timesteps"]) + + # Check that replacement schedule is between 0 and 1 + with subtests.test("0 <= replacement_schedule <=1"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") >= 0) + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") <= 1) + + with subtests.test("replacement_schedule length"): + assert len(prob.get_val("comp.replacement_schedule", units="unitless")) == plant_life + + # Check that capacity factor is between 0 and 1 with units of "unitless" + with subtests.test("0 <= capacity_factor (unitless) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") >= 0) + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") <= 1) + + # Check that capacity factor is between 1 and 100 with units of "percent" + with subtests.test("1 <= capacity_factor (percent) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="percent") >= 1) + assert np.all(prob.get_val("comp.capacity_factor", units="percent") <= 100) + + with subtests.test("capacity_factor length"): + assert len(prob.get_val("comp.capacity_factor", units="unitless")) == plant_life + + # Test that rated commodity production is greater than zero + with subtests.test(f"rated_{commodity}_production > 0"): + assert np.all( + prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units) > 0 + ) + + with subtests.test(f"rated_{commodity}_production length"): + assert ( + len(prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units)) == 1 + ) + + # Test that total commodity production is greater than zero + with subtests.test(f"total_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units) > 0 + ) + with subtests.test(f"total_{commodity}_produced length"): + assert ( + len(prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units)) == 1 + ) + + # Test that annual commodity production is greater than zero + with subtests.test(f"annual_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr") + > 0 + ) + + with subtests.test(f"annual_{commodity}_produced[1:] == annual_{commodity}_produced[0]"): + annual_production = prob.get_val( + f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr" + ) + assert np.all(annual_production[1:] == annual_production[0]) + + with subtests.test(f"annual_{commodity}_produced length"): + assert len(annual_production) == plant_life + + # Test that commodity output has some values greater than zero + with subtests.test(f"Some of {commodity}_out > 0"): + assert np.any(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units) > 0) + + with subtests.test(f"{commodity}_out length"): + assert len(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units)) == n_timesteps + + # Test default values + with subtests.test("operational_life default value"): + assert prob.get_val("comp.operational_life", units="yr") == plant_life + with subtests.test("replacement_schedule value"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") == 0) + + def test_ng_dri_performance( plant_config, ng_dri_base_config, ng_feedstock_availability_costs, subtests ): diff --git a/h2integrate/converters/methanol/methanol_baseclass.py b/h2integrate/converters/methanol/methanol_baseclass.py index 4fabf2c7e..db0043d5b 100644 --- a/h2integrate/converters/methanol/methanol_baseclass.py +++ b/h2integrate/converters/methanol/methanol_baseclass.py @@ -47,12 +47,12 @@ def setup(self): n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] self.add_input("plant_capacity_kgpy", units="kg/year", val=self.config.plant_capacity_kgpy) - self.add_input("capacity_factor", units="unitless", val=self.config.capacity_factor) + self.add_input("input_capacity_factor", units="unitless", val=self.config.capacity_factor) self.add_input("co2e_emit_ratio", units="kg/kg", val=self.config.co2e_emit_ratio) self.add_input("h2o_consume_ratio", units="kg/kg", val=self.config.h2o_consume_ratio) # self.add_output("methanol_out", units="kg/h", shape=n_timesteps) - self.add_output("total_methanol_produced", units="kg/year") + # self.add_output("total_methanol_produced", units="kg/year") self.add_output("co2e_emissions", units="kg/h", shape=n_timesteps) self.add_output("h2o_consumption", units="kg/h", shape=n_timesteps) diff --git a/h2integrate/converters/methanol/test/__init__.py b/h2integrate/converters/methanol/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/h2integrate/converters/methanol/test/test_methanol.py b/h2integrate/converters/methanol/test/test_methanol.py new file mode 100644 index 000000000..192899871 --- /dev/null +++ b/h2integrate/converters/methanol/test/test_methanol.py @@ -0,0 +1,239 @@ +import numpy as np +import openmdao.api as om +from pytest import fixture + +from h2integrate.converters.methanol.smr_methanol_plant import SMRMethanolPlantPerformanceModel +from h2integrate.converters.methanol.co2h_methanol_plant import CO2HMethanolPlantPerformanceModel + + +@fixture +def plant_config(): + return { + "plant": { + "plant_life": 30, + "simulation": { + "n_timesteps": 8760, + "dt": 3600, + }, + } + } + + +@fixture +def co2h_tech_config(): + config = { + "model_inputs": { + "performance_parameters": { + "plant_capacity_kgpy": 127893196.8, + "plant_capacity_flow": "methanol", + "capacity_factor": 0.9, + "co2e_emit_ratio": 0.020296, + "h2o_consume_ratio": 0.988, + "h2_consume_ratio": 0.195, + "co2_consume_ratio": 1.423, + "elec_consume_ratio": 0.09466667, + "meoh_syn_cat_consume_ratio": 0.00000128398, + "ng_consume_ratio": 0.073511601, + } + } + } + return config + + +@fixture +def smr_tech_config(): + config = { + "model_inputs": { + "performance_parameters": { + "plant_capacity_kgpy": 127893196.8, + "plant_capacity_flow": "methanol", + "capacity_factor": 0.9, + "co2e_emit_ratio": 1.13442, + "h2o_consume_ratio": 2.669877132, + "meoh_syn_cat_consume_ratio": 0.00000036322492251, + "meoh_atr_cat_consume_ratio": 0.0000013078433938, + "ng_consume_ratio": 0.7674355312, + "elec_produce_ratio": 0.338415339, + } + } + } + + return config + + +def test_co2h_model_outputs(plant_config, co2h_tech_config, subtests): + prob = om.Problem() + + comp = CO2HMethanolPlantPerformanceModel( + plant_config=plant_config, tech_config=co2h_tech_config, driver_config={} + ) + + prob.model.add_subsystem("comp", comp, promotes=["*"]) + prob.setup() + + prob.run_model() + plant_life = int(plant_config["plant"]["plant_life"]) + n_timesteps = int(plant_config["plant"]["simulation"]["n_timesteps"]) + commodity = "methanol" + commodity_amount_units = "kg" + commodity_rate_units = "kg/h" + + # Check that replacement schedule is between 0 and 1 + with subtests.test("0 <= replacement_schedule <=1"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") >= 0) + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") <= 1) + + with subtests.test("replacement_schedule length"): + assert len(prob.get_val("comp.replacement_schedule", units="unitless")) == plant_life + + # Check that capacity factor is between 0 and 1 with units of "unitless" + with subtests.test("0 <= capacity_factor (unitless) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") >= 0) + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") <= 1) + + # Check that capacity factor is between 1 and 100 with units of "percent" + with subtests.test("1 <= capacity_factor (percent) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="percent") >= 1) + assert np.all(prob.get_val("comp.capacity_factor", units="percent") <= 100) + + with subtests.test("capacity_factor length"): + assert len(prob.get_val("comp.capacity_factor", units="unitless")) == plant_life + + # Test that rated commodity production is greater than zero + with subtests.test(f"rated_{commodity}_production > 0"): + assert np.all( + prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units) > 0 + ) + + with subtests.test(f"rated_{commodity}_production length"): + assert ( + len(prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units)) == 1 + ) + + # Test that total commodity production is greater than zero + with subtests.test(f"total_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units) > 0 + ) + with subtests.test(f"total_{commodity}_produced length"): + assert ( + len(prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units)) == 1 + ) + + # Test that annual commodity production is greater than zero + with subtests.test(f"annual_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr") + > 0 + ) + + with subtests.test(f"annual_{commodity}_produced[1:] == annual_{commodity}_produced[0]"): + annual_production = prob.get_val( + f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr" + ) + assert np.all(annual_production[1:] == annual_production[0]) + + with subtests.test(f"annual_{commodity}_produced length"): + assert len(annual_production) == plant_life + + # Test that commodity output has some values greater than zero + with subtests.test(f"Some of {commodity}_out > 0"): + assert np.any(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units) > 0) + + with subtests.test(f"{commodity}_out length"): + assert len(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units)) == n_timesteps + + # Test default values + with subtests.test("operational_life default value"): + assert prob.get_val("comp.operational_life", units="yr") == plant_life + with subtests.test("replacement_schedule value"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") == 0) + + +def test_smr_model_outputs(plant_config, smr_tech_config, subtests): + prob = om.Problem() + + comp = SMRMethanolPlantPerformanceModel( + plant_config=plant_config, tech_config=smr_tech_config, driver_config={} + ) + + prob.model.add_subsystem("comp", comp, promotes=["*"]) + prob.setup() + + prob.run_model() + plant_life = int(plant_config["plant"]["plant_life"]) + n_timesteps = int(plant_config["plant"]["simulation"]["n_timesteps"]) + commodity = "methanol" + commodity_amount_units = "kg" + commodity_rate_units = "kg/h" + + # Check that replacement schedule is between 0 and 1 + with subtests.test("0 <= replacement_schedule <=1"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") >= 0) + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") <= 1) + + with subtests.test("replacement_schedule length"): + assert len(prob.get_val("comp.replacement_schedule", units="unitless")) == plant_life + + # Check that capacity factor is between 0 and 1 with units of "unitless" + with subtests.test("0 <= capacity_factor (unitless) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") >= 0) + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") <= 1) + + # Check that capacity factor is between 1 and 100 with units of "percent" + with subtests.test("1 <= capacity_factor (percent) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="percent") >= 1) + assert np.all(prob.get_val("comp.capacity_factor", units="percent") <= 100) + + with subtests.test("capacity_factor length"): + assert len(prob.get_val("comp.capacity_factor", units="unitless")) == plant_life + + # Test that rated commodity production is greater than zero + with subtests.test(f"rated_{commodity}_production > 0"): + assert np.all( + prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units) > 0 + ) + + with subtests.test(f"rated_{commodity}_production length"): + assert ( + len(prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units)) == 1 + ) + + # Test that total commodity production is greater than zero + with subtests.test(f"total_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units) > 0 + ) + with subtests.test(f"total_{commodity}_produced length"): + assert ( + len(prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units)) == 1 + ) + + # Test that annual commodity production is greater than zero + with subtests.test(f"annual_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr") + > 0 + ) + + with subtests.test(f"annual_{commodity}_produced[1:] == annual_{commodity}_produced[0]"): + annual_production = prob.get_val( + f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr" + ) + assert np.all(annual_production[1:] == annual_production[0]) + + with subtests.test(f"annual_{commodity}_produced length"): + assert len(annual_production) == plant_life + + # Test that commodity output has some values greater than zero + with subtests.test(f"Some of {commodity}_out > 0"): + assert np.any(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units) > 0) + + with subtests.test(f"{commodity}_out length"): + assert len(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units)) == n_timesteps + + # Test default values + with subtests.test("operational_life default value"): + assert prob.get_val("comp.operational_life", units="yr") == plant_life + with subtests.test("replacement_schedule value"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") == 0) diff --git a/h2integrate/converters/natural_gas/test/test_natural_gas_models.py b/h2integrate/converters/natural_gas/test/test_natural_gas_models.py index 9246213a1..9f77c79b5 100644 --- a/h2integrate/converters/natural_gas/test/test_natural_gas_models.py +++ b/h2integrate/converters/natural_gas/test/test_natural_gas_models.py @@ -57,7 +57,8 @@ def ngct_cost_params(): return cost_params -def get_plant_config(): +@fixture +def plant_config(): """Fixture to get plant configuration.""" return { "plant": { @@ -70,7 +71,109 @@ def get_plant_config(): } -def test_ngcc_performance(ngcc_performance_params, subtests): +def test_ngcc_performance_outputs(plant_config, ngcc_performance_params, subtests): + """Test NGCC performance model with typical operating conditions.""" + tech_config_dict = { + "model_inputs": { + "performance_parameters": ngcc_performance_params, + } + } + + # Create a simple natural gas input profile (constant 750 MMBtu/h for 100 MW plant) + natural_gas_input = np.full(8760, 750.0) # MMBtu + + prob = om.Problem() + perf_comp = NaturalGasPerformanceModel( + plant_config=plant_config, + tech_config=tech_config_dict, + ) + + prob.model.add_subsystem("comp", perf_comp, promotes=["*"]) + prob.setup() + + # Set the natural gas input + prob.set_val("comp.natural_gas_in", natural_gas_input) + prob.run_model() + + commodity = "electricity" + commodity_amount_units = "kW*h" + commodity_rate_units = "kW" + plant_life = int(plant_config["plant"]["plant_life"]) + n_timesteps = int(plant_config["plant"]["simulation"]["n_timesteps"]) + + # Check that replacement schedule is between 0 and 1 + with subtests.test("0 <= replacement_schedule <=1"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") >= 0) + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") <= 1) + + with subtests.test("replacement_schedule length"): + assert len(prob.get_val("comp.replacement_schedule", units="unitless")) == plant_life + + # Check that capacity factor is between 0 and 1 with units of "unitless" + with subtests.test("0 <= capacity_factor (unitless) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") >= 0) + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") <= 1) + + # Check that capacity factor is between 1 and 100 with units of "percent" + with subtests.test("1 <= capacity_factor (percent) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="percent") >= 1) + assert np.all(prob.get_val("comp.capacity_factor", units="percent") <= 100) + + with subtests.test("capacity_factor length"): + assert len(prob.get_val("comp.capacity_factor", units="unitless")) == plant_life + + # Test that rated commodity production is greater than zero + with subtests.test(f"rated_{commodity}_production > 0"): + assert np.all( + prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units) > 0 + ) + + with subtests.test(f"rated_{commodity}_production length"): + assert ( + len(prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units)) == 1 + ) + + # Test that total commodity production is greater than zero + with subtests.test(f"total_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units) > 0 + ) + with subtests.test(f"total_{commodity}_produced length"): + assert ( + len(prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units)) == 1 + ) + + # Test that annual commodity production is greater than zero + with subtests.test(f"annual_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr") + > 0 + ) + + with subtests.test(f"annual_{commodity}_produced[1:] == annual_{commodity}_produced[0]"): + annual_production = prob.get_val( + f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr" + ) + assert np.all(annual_production[1:] == annual_production[0]) + + with subtests.test(f"annual_{commodity}_produced length"): + assert len(annual_production) == plant_life + + # Test that commodity output has some values greater than zero + with subtests.test(f"Some of {commodity}_out > 0"): + assert np.any(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units) > 0) + + with subtests.test(f"{commodity}_out length"): + assert len(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units)) == n_timesteps + + # Test default values + with subtests.test("operational_life default value"): + assert prob.get_val("comp.operational_life", units="yr") == plant_life + with subtests.test("replacement_schedule value"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") == 0) + + +def test_ngcc_performance(plant_config, ngcc_performance_params, subtests): """Test NGCC performance model with typical operating conditions.""" tech_config_dict = { "model_inputs": { @@ -83,7 +186,7 @@ def test_ngcc_performance(ngcc_performance_params, subtests): prob = om.Problem() perf_comp = NaturalGasPerformanceModel( - plant_config=get_plant_config(), + plant_config=plant_config, tech_config=tech_config_dict, ) @@ -106,7 +209,7 @@ def test_ngcc_performance(ngcc_performance_params, subtests): assert pytest.approx(np.mean(electricity_out), rel=1e-6) == 100.0 -def test_ngct_performance(ngct_performance_params, subtests): +def test_ngct_performance(plant_config, ngct_performance_params, subtests): """Test NGCT performance model with typical operating conditions.""" tech_config_dict = { "model_inputs": { @@ -119,7 +222,7 @@ def test_ngct_performance(ngct_performance_params, subtests): prob = om.Problem() perf_comp = NaturalGasPerformanceModel( - plant_config=get_plant_config(), + plant_config=plant_config, tech_config=tech_config_dict, ) @@ -142,7 +245,7 @@ def test_ngct_performance(ngct_performance_params, subtests): assert pytest.approx(np.mean(electricity_out), rel=1e-6) == 50.0 -def test_ngcc_cost(ngcc_cost_params, subtests): +def test_ngcc_cost(plant_config, ngcc_cost_params, subtests): """Test NGCC cost model calculations.""" tech_config_dict = { "model_inputs": { @@ -159,7 +262,7 @@ def test_ngcc_cost(ngcc_cost_params, subtests): prob = om.Problem() cost_comp = NaturalGasCostModel( - plant_config=get_plant_config(), + plant_config=plant_config, tech_config=tech_config_dict, ) @@ -191,7 +294,7 @@ def test_ngcc_cost(ngcc_cost_params, subtests): assert cost_year == ngcc_cost_params["cost_year"] -def test_ngct_cost(ngct_cost_params, subtests): +def test_ngct_cost(plant_config, ngct_cost_params, subtests): """Test NGCT cost model calculations.""" tech_config_dict = { "model_inputs": { @@ -208,7 +311,7 @@ def test_ngct_cost(ngct_cost_params, subtests): prob = om.Problem() cost_comp = NaturalGasCostModel( - plant_config=get_plant_config(), + plant_config=plant_config, tech_config=tech_config_dict, ) @@ -240,7 +343,7 @@ def test_ngct_cost(ngct_cost_params, subtests): assert cost_year == ngct_cost_params["cost_year"] -def test_ngcc_performance_demand(ngcc_performance_params, subtests): +def test_ngcc_performance_demand(plant_config, ngcc_performance_params, subtests): """Test NGCC performance model with typical operating conditions.""" tech_config_dict = { "model_inputs": { @@ -257,7 +360,7 @@ def test_ngcc_performance_demand(ngcc_performance_params, subtests): prob = om.Problem() perf_comp = NaturalGasPerformanceModel( - plant_config=get_plant_config(), + plant_config=plant_config, tech_config=tech_config_dict, ) diff --git a/h2integrate/converters/nitrogen/test/test_simple_asu_model.py b/h2integrate/converters/nitrogen/test/test_simple_asu_model.py index 40bc4fb4f..57c504a26 100644 --- a/h2integrate/converters/nitrogen/test/test_simple_asu_model.py +++ b/h2integrate/converters/nitrogen/test/test_simple_asu_model.py @@ -1,22 +1,131 @@ import numpy as np import pytest import openmdao.api as om +from pytest import fixture from h2integrate.converters.nitrogen.simple_ASU import SimpleASUCostModel, SimpleASUPerformanceModel -plant_config = { - "plant": { - "plant_life": 30, - "simulation": { - "n_timesteps": 8760, # Default number of timesteps for the simulation - "dt": 3600, +@fixture +def plant_config(): + plant_config = { + "plant": { + "plant_life": 30, + "simulation": { + "n_timesteps": 8760, # Default number of timesteps for the simulation + "dt": 3600, + }, }, - }, -} + } + return plant_config + + +def test_simple_ASU_performance_model_outputs(plant_config, subtests): + """Test user-defined capacity in kW and user input electricity profile""" + p_max_kW = 1000.0 + e_profile_in_kW = np.tile(np.linspace(0.0, p_max_kW * 1.2, 876), 10) + tech_config_dict = { + "model_inputs": { + "performance_parameters": { + "size_from_N2_demand": False, + "ASU_rated_power_kW": p_max_kW, + "efficiency_kWh_pr_kg_N2": 0.119, + }, + } + } + + prob = om.Problem() + comp = SimpleASUPerformanceModel( + plant_config=plant_config, + tech_config=tech_config_dict, + driver_config={}, + ) + prob.model.add_subsystem("comp", comp) + prob.setup() + + # Set dummy electricity input + prob.set_val("comp.electricity_in", e_profile_in_kW.tolist(), units="kW") + prob.run_model() + + commodity = "nitrogen" + commodity_amount_units = "kg" + commodity_rate_units = "kg/h" + plant_life = int(plant_config["plant"]["plant_life"]) + n_timesteps = int(plant_config["plant"]["simulation"]["n_timesteps"]) + + # Check that replacement schedule is between 0 and 1 + with subtests.test("0 <= replacement_schedule <=1"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") >= 0) + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") <= 1) + + with subtests.test("replacement_schedule length"): + assert len(prob.get_val("comp.replacement_schedule", units="unitless")) == plant_life + + # Check that capacity factor is between 0 and 1 with units of "unitless" + with subtests.test("0 <= capacity_factor (unitless) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") >= 0) + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") <= 1) + + # Check that capacity factor is between 1 and 100 with units of "percent" + with subtests.test("1 <= capacity_factor (percent) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="percent") >= 1) + assert np.all(prob.get_val("comp.capacity_factor", units="percent") <= 100) + + with subtests.test("capacity_factor length"): + assert len(prob.get_val("comp.capacity_factor", units="unitless")) == plant_life + + # Test that rated commodity production is greater than zero + with subtests.test(f"rated_{commodity}_production > 0"): + assert np.all( + prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units) > 0 + ) + + with subtests.test(f"rated_{commodity}_production length"): + assert ( + len(prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units)) == 1 + ) + + # Test that total commodity production is greater than zero + with subtests.test(f"total_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units) > 0 + ) + with subtests.test(f"total_{commodity}_produced length"): + assert ( + len(prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units)) == 1 + ) + + # Test that annual commodity production is greater than zero + with subtests.test(f"annual_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr") + > 0 + ) + + with subtests.test(f"annual_{commodity}_produced[1:] == annual_{commodity}_produced[0]"): + annual_production = prob.get_val( + f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr" + ) + assert np.all(annual_production[1:] == annual_production[0]) + + with subtests.test(f"annual_{commodity}_produced length"): + assert len(annual_production) == plant_life + + # Test that commodity output has some values greater than zero + with subtests.test(f"Some of {commodity}_out > 0"): + assert np.any(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units) > 0) + + with subtests.test(f"{commodity}_out length"): + assert len(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units)) == n_timesteps + + # Test default values + with subtests.test("operational_life default value"): + assert prob.get_val("comp.operational_life", units="yr") == plant_life + with subtests.test("replacement_schedule value"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") == 0) -def test_simple_ASU_performance_model_set_capacity_kW(subtests): +def test_simple_ASU_performance_model_set_capacity_kW(plant_config, subtests): """Test user-defined capacity in kW and user input electricity profile""" p_max_kW = 1000.0 e_profile_in_kW = np.tile(np.linspace(0.0, p_max_kW * 1.2, 876), 10) @@ -56,7 +165,7 @@ def test_simple_ASU_performance_model_set_capacity_kW(subtests): assert max(prob.get_val("asu_perf.annual_electricity_consumption")) <= sum(e_profile_in_kW) -def test_simple_ASU_performance_model_size_for_demand(subtests): +def test_simple_ASU_performance_model_size_for_demand(plant_config, subtests): """Test user-defined capacity in kW and user input electricity profile""" n2_dmd_max_kg_pr_hr = 1000.0 n2_dmd_kg_pr_hr = np.tile(np.linspace(0.0, n2_dmd_max_kg_pr_hr, 876), 10) @@ -103,7 +212,7 @@ def test_simple_ASU_performance_model_size_for_demand(subtests): ) -def test_simple_ASU_cost_model_usd_pr_kw(subtests): +def test_simple_ASU_cost_model_usd_pr_kw(plant_config, subtests): capex_usd_per_kw = 10.0 opex_usd_per_kw = 5.0 @@ -147,7 +256,7 @@ def test_simple_ASU_cost_model_usd_pr_kw(subtests): assert pytest.approx(val, rel=1e-6) == expected[0] -def test_simple_ASU_cost_model_usd_pr_mw(subtests): +def test_simple_ASU_cost_model_usd_pr_mw(plant_config, subtests): capex_usd_per_kw = 10.0 opex_usd_per_kw = 5.0 capex_usd_per_mw = capex_usd_per_kw * 1e3 @@ -192,7 +301,7 @@ def test_simple_ASU_cost_model_usd_pr_mw(subtests): assert pytest.approx(val, rel=1e-6) == expected[0] -def test_simple_ASU_performance_and_cost_size_for_demand(subtests): +def test_simple_ASU_performance_and_cost_size_for_demand(plant_config, subtests): """Test user-defined capacity in kW and user input electricity profile""" cpx_usd_per_mw = 10.0 # dummy number opex_usd_per_mw = 5.0 # dummy number diff --git a/h2integrate/converters/solar/test/test_pysam_solar.py b/h2integrate/converters/solar/test/test_pysam_solar.py index 9bf85b1ab..6b5accbe8 100644 --- a/h2integrate/converters/solar/test/test_pysam_solar.py +++ b/h2integrate/converters/solar/test/test_pysam_solar.py @@ -112,7 +112,7 @@ def test_pvwatts_outputs(basic_pysam_options, solar_resource_dict, plant_config, # Check that capacity factor is between 0 and 1 with units of "unitless" with subtests.test("0 <= capacity_factor (unitless) <=1"): assert np.all(prob.get_val("comp.capacity_factor", units="unitless") >= 0) - assert np.all(prob.get_val("capacity_factor", units="unitless") <= 1) + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") <= 1) # Check that capacity factor is between 1 and 100 with units of "percent" with subtests.test("1 <= capacity_factor (percent) <=1"): diff --git a/h2integrate/converters/steel/test/test_rosner_eaf.py b/h2integrate/converters/steel/test/test_rosner_eaf.py index 48e093426..f5e0ab1a5 100644 --- a/h2integrate/converters/steel/test/test_rosner_eaf.py +++ b/h2integrate/converters/steel/test/test_rosner_eaf.py @@ -120,6 +120,104 @@ def h2_feedstock_availability_costs(): return feedstocks_dict +def test_ng_eaf_performance_outputs( + plant_config, ng_eaf_base_config, ng_feedstock_availability_costs, subtests +): + prob = om.Problem() + + iron_dri_perf = NaturalGasEAFPlantPerformanceComponent( + plant_config=plant_config, + tech_config=ng_eaf_base_config, + driver_config={}, + ) + prob.model.add_subsystem("comp", iron_dri_perf, promotes=["*"]) + prob.setup() + + for feedstock_name, feedstock_info in ng_feedstock_availability_costs.items(): + prob.set_val( + f"comp.{feedstock_name}_in", + feedstock_info["rated_capacity"], + units=feedstock_info["units"], + ) + prob.run_model() + plant_life = int(plant_config["plant"]["plant_life"]) + n_timesteps = int(plant_config["plant"]["simulation"]["n_timesteps"]) + commodity = "steel" + commodity_amount_units = "kg" + commodity_rate_units = "kg/h" + + # Check that replacement schedule is between 0 and 1 + with subtests.test("0 <= replacement_schedule <=1"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") >= 0) + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") <= 1) + + with subtests.test("replacement_schedule length"): + assert len(prob.get_val("comp.replacement_schedule", units="unitless")) == plant_life + + # Check that capacity factor is between 0 and 1 with units of "unitless" + with subtests.test("0 <= capacity_factor (unitless) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") >= 0) + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") <= 1) + + # Check that capacity factor is between 1 and 100 with units of "percent" + with subtests.test("1 <= capacity_factor (percent) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="percent") >= 1) + assert np.all(prob.get_val("comp.capacity_factor", units="percent") <= 100) + + with subtests.test("capacity_factor length"): + assert len(prob.get_val("comp.capacity_factor", units="unitless")) == plant_life + + # Test that rated commodity production is greater than zero + with subtests.test(f"rated_{commodity}_production > 0"): + assert np.all( + prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units) > 0 + ) + + with subtests.test(f"rated_{commodity}_production length"): + assert ( + len(prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units)) == 1 + ) + + # Test that total commodity production is greater than zero + with subtests.test(f"total_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units) > 0 + ) + with subtests.test(f"total_{commodity}_produced length"): + assert ( + len(prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units)) == 1 + ) + + # Test that annual commodity production is greater than zero + with subtests.test(f"annual_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr") + > 0 + ) + + with subtests.test(f"annual_{commodity}_produced[1:] == annual_{commodity}_produced[0]"): + annual_production = prob.get_val( + f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr" + ) + assert np.all(annual_production[1:] == annual_production[0]) + + with subtests.test(f"annual_{commodity}_produced length"): + assert len(annual_production) == plant_life + + # Test that commodity output has some values greater than zero + with subtests.test(f"Some of {commodity}_out > 0"): + assert np.any(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units) > 0) + + with subtests.test(f"{commodity}_out length"): + assert len(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units)) == n_timesteps + + # Test default values + with subtests.test("operational_life default value"): + assert prob.get_val("comp.operational_life", units="yr") == plant_life + with subtests.test("replacement_schedule value"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") == 0) + + def test_ng_eaf_performance( plant_config, ng_eaf_base_config, ng_feedstock_availability_costs, subtests ): diff --git a/h2integrate/converters/steel/test/test_simple_steel.py b/h2integrate/converters/steel/test/test_simple_steel.py new file mode 100644 index 000000000..bc002d97b --- /dev/null +++ b/h2integrate/converters/steel/test/test_simple_steel.py @@ -0,0 +1,119 @@ +import numpy as np +import openmdao.api as om +from pytest import fixture + +from h2integrate.converters.steel.steel import SteelPerformanceModel + + +@fixture +def tech_config(): + config = { + "model_inputs": { + "performance_parameters": {"plant_capacity_mtpy": 8760, "capacity_factor": 0.9} + } + } + return config + + +@fixture +def plant_config(): + plant_config = { + "plant": { + "plant_life": 30, + "simulation": { + "n_timesteps": 8760, + "dt": 3600, + }, + }, + } + return plant_config + + +def test_simple_steel_performance_outputs(tech_config, plant_config, subtests): + prob = om.Problem() + + # START HERE + comp = SteelPerformanceModel( + plant_config=plant_config, + tech_config=tech_config, + driver_config={}, + ) + prob.model.add_subsystem("comp", comp, promotes=["*"]) + prob.setup() + prob.run_model() + plant_life = int(plant_config["plant"]["plant_life"]) + n_timesteps = int(plant_config["plant"]["simulation"]["n_timesteps"]) + commodity = "steel" + commodity_amount_units = "kg" + commodity_rate_units = "kg/h" + + # Check that replacement schedule is between 0 and 1 + with subtests.test("0 <= replacement_schedule <=1"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") >= 0) + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") <= 1) + + with subtests.test("replacement_schedule length"): + assert len(prob.get_val("comp.replacement_schedule", units="unitless")) == plant_life + + # Check that capacity factor is between 0 and 1 with units of "unitless" + with subtests.test("0 <= capacity_factor (unitless) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") >= 0) + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") <= 1) + + # Check that capacity factor is between 1 and 100 with units of "percent" + with subtests.test("1 <= capacity_factor (percent) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="percent") >= 1) + assert np.all(prob.get_val("comp.capacity_factor", units="percent") <= 100) + + with subtests.test("capacity_factor length"): + assert len(prob.get_val("comp.capacity_factor", units="unitless")) == plant_life + + # Test that rated commodity production is greater than zero + with subtests.test(f"rated_{commodity}_production > 0"): + assert np.all( + prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units) > 0 + ) + + with subtests.test(f"rated_{commodity}_production length"): + assert ( + len(prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units)) == 1 + ) + + # Test that total commodity production is greater than zero + with subtests.test(f"total_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units) > 0 + ) + with subtests.test(f"total_{commodity}_produced length"): + assert ( + len(prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units)) == 1 + ) + + # Test that annual commodity production is greater than zero + with subtests.test(f"annual_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr") + > 0 + ) + + with subtests.test(f"annual_{commodity}_produced[1:] == annual_{commodity}_produced[0]"): + annual_production = prob.get_val( + f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr" + ) + assert np.all(annual_production[1:] == annual_production[0]) + + with subtests.test(f"annual_{commodity}_produced length"): + assert len(annual_production) == plant_life + + # Test that commodity output has some values greater than zero + with subtests.test(f"Some of {commodity}_out > 0"): + assert np.any(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units) > 0) + + with subtests.test(f"{commodity}_out length"): + assert len(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units)) == n_timesteps + + # Test default values + with subtests.test("operational_life default value"): + assert prob.get_val("comp.operational_life", units="yr") == plant_life + with subtests.test("replacement_schedule value"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") == 0) diff --git a/h2integrate/converters/water/desal/test/test_ro_desalination.py b/h2integrate/converters/water/desal/test/test_ro_desalination.py index 48b5c9305..7ad09476e 100644 --- a/h2integrate/converters/water/desal/test/test_ro_desalination.py +++ b/h2integrate/converters/water/desal/test/test_ro_desalination.py @@ -1,3 +1,4 @@ +import numpy as np import openmdao.api as om from pytest import approx, fixture @@ -20,6 +21,102 @@ def plant_config(): return {"plant": plant} +def test_brackish_desal_outputs(plant_config, subtests): + tech_config = { + "model_inputs": { + "performance_parameters": { + "freshwater_kg_per_hour": 10000, + "salinity": "brackish", + "freshwater_density": 997, + }, + } + } + + prob = om.Problem() + comp = ReverseOsmosisPerformanceModel(plant_config=plant_config, tech_config=tech_config) + prob.model.add_subsystem("comp", comp, promotes=["*"]) + + prob.setup() + prob.run_model() + + commodity = "water" + commodity_amount_units = "m**3" + commodity_rate_units = "m**3/h" + plant_life = int(plant_config["plant"]["plant_life"]) + n_timesteps = int(plant_config["plant"]["simulation"]["n_timesteps"]) + + # Check that replacement schedule is between 0 and 1 + with subtests.test("0 <= replacement_schedule <=1"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") >= 0) + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") <= 1) + + with subtests.test("replacement_schedule length"): + assert len(prob.get_val("comp.replacement_schedule", units="unitless")) == plant_life + + # Check that capacity factor is between 0 and 1 with units of "unitless" + with subtests.test("0 <= capacity_factor (unitless) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") >= 0) + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") <= 1) + + # Check that capacity factor is between 1 and 100 with units of "percent" + with subtests.test("1 <= capacity_factor (percent) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="percent") >= 1) + assert np.all(prob.get_val("comp.capacity_factor", units="percent") <= 100) + + with subtests.test("capacity_factor length"): + assert len(prob.get_val("comp.capacity_factor", units="unitless")) == plant_life + + # Test that rated commodity production is greater than zero + with subtests.test(f"rated_{commodity}_production > 0"): + assert np.all( + prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units) > 0 + ) + + with subtests.test(f"rated_{commodity}_production length"): + assert ( + len(prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units)) == 1 + ) + + # Test that total commodity production is greater than zero + with subtests.test(f"total_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units) > 0 + ) + with subtests.test(f"total_{commodity}_produced length"): + assert ( + len(prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units)) == 1 + ) + + # Test that annual commodity production is greater than zero + with subtests.test(f"annual_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr") + > 0 + ) + + with subtests.test(f"annual_{commodity}_produced[1:] == annual_{commodity}_produced[0]"): + annual_production = prob.get_val( + f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr" + ) + assert np.all(annual_production[1:] == annual_production[0]) + + with subtests.test(f"annual_{commodity}_produced length"): + assert len(annual_production) == plant_life + + # Test that commodity output has some values greater than zero + with subtests.test(f"Some of {commodity}_out > 0"): + assert np.any(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units) > 0) + + with subtests.test(f"{commodity}_out length"): + assert len(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units)) == n_timesteps + + # Test default values + with subtests.test("operational_life default value"): + assert prob.get_val("comp.operational_life", units="yr") == plant_life + with subtests.test("replacement_schedule value"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") == 0) + + def test_brackish_performance(plant_config, subtests): tech_config = { "model_inputs": { diff --git a/h2integrate/converters/water_power/test/__init__.py b/h2integrate/converters/water_power/test/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/h2integrate/converters/water_power/test/test_hydro_power.py b/h2integrate/converters/water_power/test/test_hydro_power.py new file mode 100644 index 000000000..905daa840 --- /dev/null +++ b/h2integrate/converters/water_power/test/test_hydro_power.py @@ -0,0 +1,142 @@ +import numpy as np +import openmdao.api as om +from pytest import fixture + +from h2integrate import EXAMPLE_DIR +from h2integrate.resource.river import RiverResource +from h2integrate.converters.water_power.hydro_plant_run_of_river import ( + RunOfRiverHydroPerformanceModel, +) + + +@fixture +def plant_config(): + plant_config = { + "plant": { + "plant_life": 30, + "simulation": { + "n_timesteps": 8760, + "dt": 3600, + }, + }, + "site": { + "resources": { + "river_resource": { + "resource_parameters": { + "filename": EXAMPLE_DIR / "07_run_of_river_plant" / "river_data.csv" + } + } + } + }, + } + return plant_config + + +@fixture +def tech_config(): + model_inputs = { + "plant_capacity_mw": 10.0, + "water_density": 998, + "acceleration_gravity": 9.81, + "turbine_efficiency": 0.9, + "head": 10.0, # m + } + return {"model_inputs": {"performance_parameters": model_inputs}} + + +def test_hydro_power_performance_outputs(tech_config, plant_config, subtests): + prob = om.Problem() + + river_resource = RiverResource( + plant_config=plant_config, + resource_config=plant_config["site"]["resources"]["river_resource"]["resource_parameters"], + driver_config={}, + ) + prob.model.add_subsystem("river_resource", river_resource, promotes=["*"]) + + # START HERE + comp = RunOfRiverHydroPerformanceModel( + plant_config=plant_config, + tech_config=tech_config, + driver_config={}, + ) + prob.model.add_subsystem("comp", comp, promotes=["*"]) + prob.setup() + prob.run_model() + + commodity = "electricity" + commodity_amount_units = "kW*h" + commodity_rate_units = "kW" + plant_life = int(plant_config["plant"]["plant_life"]) + n_timesteps = int(plant_config["plant"]["simulation"]["n_timesteps"]) + + # Check that replacement schedule is between 0 and 1 + with subtests.test("0 <= replacement_schedule <=1"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") >= 0) + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") <= 1) + + with subtests.test("replacement_schedule length"): + assert len(prob.get_val("comp.replacement_schedule", units="unitless")) == plant_life + + # Check that capacity factor is between 0 and 1 with units of "unitless" + with subtests.test("0 <= capacity_factor (unitless) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") >= 0) + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") <= 1) + + # Check that capacity factor is between 1 and 100 with units of "percent" + with subtests.test("1 <= capacity_factor (percent) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="percent") >= 1) + assert np.all(prob.get_val("comp.capacity_factor", units="percent") <= 100) + + with subtests.test("capacity_factor length"): + assert len(prob.get_val("comp.capacity_factor", units="unitless")) == plant_life + + # Test that rated commodity production is greater than zero + with subtests.test(f"rated_{commodity}_production > 0"): + assert np.all( + prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units) > 0 + ) + + with subtests.test(f"rated_{commodity}_production length"): + assert ( + len(prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units)) == 1 + ) + + # Test that total commodity production is greater than zero + with subtests.test(f"total_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units) > 0 + ) + with subtests.test(f"total_{commodity}_produced length"): + assert ( + len(prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units)) == 1 + ) + + # Test that annual commodity production is greater than zero + with subtests.test(f"annual_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr") + > 0 + ) + + with subtests.test(f"annual_{commodity}_produced[1:] == annual_{commodity}_produced[0]"): + annual_production = prob.get_val( + f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr" + ) + assert np.all(annual_production[1:] == annual_production[0]) + + with subtests.test(f"annual_{commodity}_produced length"): + assert len(annual_production) == plant_life + + # Test that commodity output has some values greater than zero + with subtests.test(f"Some of {commodity}_out > 0"): + assert np.any(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units) > 0) + + with subtests.test(f"{commodity}_out length"): + assert len(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units)) == n_timesteps + + # Test default values + with subtests.test("operational_life default value"): + assert prob.get_val("comp.operational_life", units="yr") == plant_life + with subtests.test("replacement_schedule value"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") == 0) diff --git a/h2integrate/converters/wind/test/test_floris_wind.py b/h2integrate/converters/wind/test/test_floris_wind.py index 080497c02..cfb727f9e 100644 --- a/h2integrate/converters/wind/test/test_floris_wind.py +++ b/h2integrate/converters/wind/test/test_floris_wind.py @@ -84,6 +84,113 @@ def plant_config_wtk(): return d +def test_floris_outputs(plant_config_openmeteo, floris_config, subtests): + tech_config_dict = { + "model_inputs": { + "performance_parameters": floris_config, + } + } + + prob = om.Problem() + + wind_resource_config = plant_config_openmeteo["site"]["resource"]["wind_resource"][ + "resource_parameters" + ] + wind_resource = OpenMeteoHistoricalWindResource( + plant_config=plant_config_openmeteo, + resource_config=wind_resource_config, + driver_config={}, + ) + + wind_plant = FlorisWindPlantPerformanceModel( + plant_config=plant_config_openmeteo, + tech_config=tech_config_dict, + driver_config={}, + ) + + prob.model.add_subsystem("wind_resource", wind_resource, promotes=["*"]) + prob.model.add_subsystem("comp", wind_plant, promotes=["*"]) + prob.setup() + prob.run_model() + + commodity = "electricity" + commodity_amount_units = "kW*h" + commodity_rate_units = "kW" + plant_life = int(plant_config_openmeteo["plant"]["plant_life"]) + n_timesteps = int(plant_config_openmeteo["plant"]["simulation"]["n_timesteps"]) + + # Check that replacement schedule is between 0 and 1 + with subtests.test("0 <= replacement_schedule <=1"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") >= 0) + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") <= 1) + + with subtests.test("replacement_schedule length"): + assert len(prob.get_val("comp.replacement_schedule", units="unitless")) == plant_life + + # Check that capacity factor is between 0 and 1 with units of "unitless" + with subtests.test("0 <= capacity_factor (unitless) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") >= 0) + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") <= 1) + + # Check that capacity factor is between 1 and 100 with units of "percent" + with subtests.test("1 <= capacity_factor (percent) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="percent") >= 1) + assert np.all(prob.get_val("comp.capacity_factor", units="percent") <= 100) + + with subtests.test("capacity_factor length"): + assert len(prob.get_val("comp.capacity_factor", units="unitless")) == plant_life + + # Test that rated commodity production is greater than zero + with subtests.test(f"rated_{commodity}_production > 0"): + assert np.all( + prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units) > 0 + ) + + with subtests.test(f"rated_{commodity}_production length"): + assert ( + len(prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units)) == 1 + ) + + # Test that total commodity production is greater than zero + with subtests.test(f"total_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units) > 0 + ) + with subtests.test(f"total_{commodity}_produced length"): + assert ( + len(prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units)) == 1 + ) + + # Test that annual commodity production is greater than zero + with subtests.test(f"annual_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr") + > 0 + ) + + with subtests.test(f"annual_{commodity}_produced[1:] == annual_{commodity}_produced[0]"): + annual_production = prob.get_val( + f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr" + ) + assert np.all(annual_production[1:] == annual_production[0]) + + with subtests.test(f"annual_{commodity}_produced length"): + assert len(annual_production) == plant_life + + # Test that commodity output has some values greater than zero + with subtests.test(f"Some of {commodity}_out > 0"): + assert np.any(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units) > 0) + + with subtests.test(f"{commodity}_out length"): + assert len(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units)) == n_timesteps + + # Test default values + with subtests.test("operational_life default value"): + assert prob.get_val("comp.operational_life", units="yr") == plant_life + with subtests.test("replacement_schedule value"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") == 0) + + def test_floris_wind_performance(plant_config_openmeteo, floris_config, subtests): tech_config_dict = { "model_inputs": { diff --git a/h2integrate/converters/wind/test/test_pysam_wind.py b/h2integrate/converters/wind/test/test_pysam_wind.py index a15b7a85f..6ebcceb7c 100644 --- a/h2integrate/converters/wind/test/test_pysam_wind.py +++ b/h2integrate/converters/wind/test/test_pysam_wind.py @@ -65,6 +65,106 @@ def wind_plant_config(): return design_config +def test_pysam_wind_outputs(wind_resource_config, plant_config, wind_plant_config, subtests): + prob = om.Problem() + + plant_config["site"].update({"resources": {"wind_resource": wind_resource_config}}) + + wind_resource = WTKNRELDeveloperAPIWindResource( + plant_config=plant_config, + resource_config=wind_resource_config, + driver_config={}, + ) + + wind_plant = PYSAMWindPlantPerformanceModel( + plant_config=plant_config, + tech_config={"model_inputs": {"performance_parameters": wind_plant_config}}, + driver_config={}, + ) + + prob.model.add_subsystem("wind_resource", wind_resource, promotes=["*"]) + prob.model.add_subsystem("comp", wind_plant, promotes=["*"]) + prob.setup() + prob.run_model() + + commodity = "electricity" + commodity_amount_units = "kW*h" + commodity_rate_units = "kW" + plant_life = int(plant_config["plant"]["plant_life"]) + n_timesteps = int(plant_config["plant"]["simulation"]["n_timesteps"]) + + # Check that replacement schedule is between 0 and 1 + with subtests.test("0 <= replacement_schedule <=1"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") >= 0) + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") <= 1) + + with subtests.test("replacement_schedule length"): + assert len(prob.get_val("comp.replacement_schedule", units="unitless")) == plant_life + + # Check that capacity factor is between 0 and 1 with units of "unitless" + with subtests.test("0 <= capacity_factor (unitless) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") >= 0) + assert np.all(prob.get_val("comp.capacity_factor", units="unitless") <= 1) + + # Check that capacity factor is between 1 and 100 with units of "percent" + with subtests.test("1 <= capacity_factor (percent) <=1"): + assert np.all(prob.get_val("comp.capacity_factor", units="percent") >= 1) + assert np.all(prob.get_val("comp.capacity_factor", units="percent") <= 100) + + with subtests.test("capacity_factor length"): + assert len(prob.get_val("comp.capacity_factor", units="unitless")) == plant_life + + # Test that rated commodity production is greater than zero + with subtests.test(f"rated_{commodity}_production > 0"): + assert np.all( + prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units) > 0 + ) + + with subtests.test(f"rated_{commodity}_production length"): + assert ( + len(prob.get_val(f"comp.rated_{commodity}_production", units=commodity_rate_units)) == 1 + ) + + # Test that total commodity production is greater than zero + with subtests.test(f"total_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units) > 0 + ) + with subtests.test(f"total_{commodity}_produced length"): + assert ( + len(prob.get_val(f"comp.total_{commodity}_produced", units=commodity_amount_units)) == 1 + ) + + # Test that annual commodity production is greater than zero + with subtests.test(f"annual_{commodity}_produced > 0"): + assert np.all( + prob.get_val(f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr") + > 0 + ) + + with subtests.test(f"annual_{commodity}_produced[1:] == annual_{commodity}_produced[0]"): + annual_production = prob.get_val( + f"comp.annual_{commodity}_produced", units=f"{commodity_amount_units}/yr" + ) + assert np.all(annual_production[1:] == annual_production[0]) + + with subtests.test(f"annual_{commodity}_produced length"): + assert len(annual_production) == plant_life + + # Test that commodity output has some values greater than zero + with subtests.test(f"Some of {commodity}_out > 0"): + assert np.any(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units) > 0) + + with subtests.test(f"{commodity}_out length"): + assert len(prob.get_val(f"comp.{commodity}_out", units=commodity_rate_units)) == n_timesteps + + # Test default values + with subtests.test("operational_life default value"): + assert prob.get_val("comp.operational_life", units="yr") == plant_life + with subtests.test("replacement_schedule value"): + assert np.all(prob.get_val("comp.replacement_schedule", units="unitless") == 0) + + def test_wind_plant_pysam_no_changes_from_setup( wind_resource_config, plant_config, wind_plant_config, subtests ): From b54f32f4e1ed114961bd95e84be07c89f7331eaa Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Wed, 28 Jan 2026 14:11:49 -0700 Subject: [PATCH 40/63] working on updating combiners and h2imodel --- examples/08_wind_electrolyzer/plant_config.yaml | 3 +++ .../user_finance_model/simple_lco.py | 3 +++ .../converters/hydrogen/pem_electrolyzer.py | 9 ++++++--- h2integrate/core/h2integrate_model.py | 12 ++++++------ h2integrate/core/resource_summer.py | 4 ++++ h2integrate/finances/profast_base.py | 7 +++++-- h2integrate/transporters/generic_summer.py | 15 +++++++++++++-- 7 files changed, 40 insertions(+), 13 deletions(-) diff --git a/examples/08_wind_electrolyzer/plant_config.yaml b/examples/08_wind_electrolyzer/plant_config.yaml index 39dd90b37..d5803f5c4 100644 --- a/examples/08_wind_electrolyzer/plant_config.yaml +++ b/examples/08_wind_electrolyzer/plant_config.yaml @@ -61,14 +61,17 @@ finance_parameters: finance_subgroups: electricity_profast: commodity: "electricity" + # commodity_stream: "wind" finance_groups: ["profast_model"] technologies: ["wind"] electricity_custom: commodity: "electricity" + # commodity_stream: "wind" finance_groups: ["custom_model"] technologies: ["wind"] hydrogen: commodity: "hydrogen" + # commodity_stream: "electrolyzer" commodity_desc: "produced" finance_groups: ["custom_model","profast_model"] technologies: ["wind", "electrolyzer"] diff --git a/examples/08_wind_electrolyzer/user_finance_model/simple_lco.py b/examples/08_wind_electrolyzer/user_finance_model/simple_lco.py index e2e8cfe7e..ad8f5de4f 100644 --- a/examples/08_wind_electrolyzer/user_finance_model/simple_lco.py +++ b/examples/08_wind_electrolyzer/user_finance_model/simple_lco.py @@ -41,10 +41,13 @@ def setup(self): else f"{LCO_base_str}_{self.options['description']}" ) + plant_life = int(self.options["plant_config"]["plant"]["plant_life"]) + # add inputs for commodity production and costs self.add_input( f"total_{self.options['commodity_type']}_produced", val=0.0, + shape=plant_life, units=commodity_units, ) tech_config = self.tech_config = self.options["tech_config"] diff --git a/h2integrate/converters/hydrogen/pem_electrolyzer.py b/h2integrate/converters/hydrogen/pem_electrolyzer.py index 5ea196d7c..f29555fe4 100644 --- a/h2integrate/converters/hydrogen/pem_electrolyzer.py +++ b/h2integrate/converters/hydrogen/pem_electrolyzer.py @@ -183,6 +183,9 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): outputs["capacity_factor"] = H2_Results["Performance Schedules"][ "Capacity Factor [-]" ].values - outputs["annual_hydrogen_produced"] = H2_Results["Performance Schedules"][ - "Annual H2 Production [kg/year]" - ].values + outputs["annual_hydrogen_produced"] = outputs["hydrogen_out"].sum() + + # TODO: replace above line w below + # outputs["annual_hydrogen_produced"] = H2_Results["Performance Schedules"][ + # "Annual H2 Production [kg/year]" + # ].values diff --git a/h2integrate/core/h2integrate_model.py b/h2integrate/core/h2integrate_model.py index 10eb69f9a..a016ec474 100644 --- a/h2integrate/core/h2integrate_model.py +++ b/h2integrate/core/h2integrate_model.py @@ -763,7 +763,8 @@ def create_finance_model(self): if commodity_stream is None and commodity == "electricity": finance_subgroup.add_subsystem( - "electricity_sum", ElectricitySumComp(tech_configs=tech_configs) + "electricity_sum", + ElectricitySumComp(plant_config=self.plant_config, tech_configs=tech_configs), ) # Add adjusted capex/opex @@ -1055,7 +1056,6 @@ def connect_technologies(self): tech_configs = group_configs.get("tech_configs") primary_commodity_type = group_configs.get("commodity") commodity_stream = group_configs.get("commodity_stream") - if commodity_stream is not None: # connect commodity stream output to summer input self.plant.connect( @@ -1133,13 +1133,13 @@ def connect_technologies(self): if "geoh2" in tech_name: if primary_commodity_type == "hydrogen": self.plant.connect( - f"{tech_name}.total_hydrogen_produced", + f"{tech_name}.annual_hydrogen_produced", f"finance_subgroup_{group_id}.total_hydrogen_produced", ) if "ammonia" in tech_name and primary_commodity_type == "ammonia": self.plant.connect( - f"{tech_name}.total_ammonia_produced", + f"{tech_name}.annual_ammonia_produced", f"finance_subgroup_{group_id}.total_ammonia_produced", ) @@ -1153,13 +1153,13 @@ def connect_technologies(self): if "methanol" in tech_name and primary_commodity_type == "methanol": self.plant.connect( - f"{tech_name}.total_methanol_produced", + f"{tech_name}.annual_methanol_produced", f"finance_subgroup_{group_id}.total_methanol_produced", ) if "air_separator" in tech_name and primary_commodity_type == "nitrogen": self.plant.connect( - f"{tech_name}.total_nitrogen_produced", + f"{tech_name}.annual_nitrogen_produced", f"finance_subgroup_{group_id}.total_nitrogen_produced", ) diff --git a/h2integrate/core/resource_summer.py b/h2integrate/core/resource_summer.py index 00aa8541c..993f61932 100644 --- a/h2integrate/core/resource_summer.py +++ b/h2integrate/core/resource_summer.py @@ -11,8 +11,11 @@ class ElectricitySumComp(om.ExplicitComponent): def initialize(self): self.options.declare("tech_configs", types=dict, desc="Configuration for each technology") + self.options.declare("plant_config", types=dict) def setup(self): + plant_life = int(self.options["plant_config"]["plant"]["plant_life"]) + # Add inputs for each electricity producing technology for tech in self.options["tech_configs"]: if is_electricity_producer(tech): @@ -28,6 +31,7 @@ def setup(self): self.add_output( "total_electricity_produced", val=0.0, + shape=plant_life, units="kW*h/year", desc="Total electricity produced", ) diff --git a/h2integrate/finances/profast_base.py b/h2integrate/finances/profast_base.py index bc501fa5b..df29f5abb 100644 --- a/h2integrate/finances/profast_base.py +++ b/h2integrate/finances/profast_base.py @@ -521,6 +521,8 @@ def setup(self): # Add model-specific outputs defined by subclass self.add_model_specific_outputs() + plant_life = int(self.options["plant_config"]["plant"]["plant_life"]) + # Add production input (CO2 capture or total commodity produced) if self.options["commodity_type"] == "co2": self.add_input("co2_capture_kgpy", val=0.0, units="kg/year", require_connection=True) @@ -529,12 +531,13 @@ def setup(self): f"total_{self.options['commodity_type']}_produced", val=-1.0, units=commodity_units, - shape_by_conn=True, + shape=plant_life, + # shape_by_conn=True, require_connection=True, ) # Add inputs for CapEx, OpEx, and variable OpEx for each technology - plant_life = int(self.options["plant_config"]["plant"]["plant_life"]) + tech_config = self.tech_config = self.options["tech_config"] for tech in tech_config: self.add_input(f"capex_adjusted_{tech}", val=0.0, units="USD") diff --git a/h2integrate/transporters/generic_summer.py b/h2integrate/transporters/generic_summer.py index 6926b1670..e74334e67 100644 --- a/h2integrate/transporters/generic_summer.py +++ b/h2integrate/transporters/generic_summer.py @@ -40,6 +40,7 @@ def setup(self): ) n_timesteps = int(self.options["plant_config"]["plant"]["simulation"]["n_timesteps"]) + plant_life = int(self.options["plant_config"]["plant"]["plant_life"]) if self.config.commodity == "electricity": # NOTE: this should be updated in overhaul required for flexible dt @@ -56,9 +57,19 @@ def setup(self): ) if self.config.operation_mode == "consumption": - self.add_output(f"total_{self.config.commodity}_consumed", val=0.0, units=summed_units) + self.add_output( + f"total_{self.config.commodity}_consumed", + val=0.0, + shape=plant_life, + units=summed_units, + ) else: # production mode (default) - self.add_output(f"total_{self.config.commodity}_produced", val=0.0, units=summed_units) + self.add_output( + f"total_{self.config.commodity}_produced", + val=0.0, + shape=plant_life, + units=summed_units, + ) def compute(self, inputs, outputs): if self.config.operation_mode == "consumption": From 834916359d49cf3e4a6319ccc9934df702cdf86e Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Wed, 28 Jan 2026 16:27:06 -0700 Subject: [PATCH 41/63] updated electrolyzer so test values dont change and other bugfixes so examples run --- examples/test/test_all_examples.py | 6 ++++-- h2integrate/converters/ammonia/ammonia_synloop.py | 2 +- h2integrate/converters/hydrogen/pem_electrolyzer.py | 2 +- h2integrate/converters/hydrogen/wombat_model.py | 6 ++++-- h2integrate/finances/numpy_financial_npv.py | 2 ++ h2integrate/postprocess/sql_to_csv.py | 2 +- 6 files changed, 13 insertions(+), 7 deletions(-) diff --git a/examples/test/test_all_examples.py b/examples/test/test_all_examples.py index b31bb9300..4a1388229 100644 --- a/examples/test/test_all_examples.py +++ b/examples/test/test_all_examples.py @@ -356,7 +356,9 @@ def test_wind_h2_opt_example(subtests): model_init.post_process() - annual_h20 = model_init.prob.get_val("electrolyzer.total_hydrogen_produced", units="kg/year")[0] + annual_h20 = model_init.prob.get_val("electrolyzer.annual_hydrogen_produced", units="kg/year")[ + 0 + ] # Create a H2Integrate model model = H2IntegrateModel(Path.cwd() / "wind_plant_electrolyzer.yaml") @@ -422,7 +424,7 @@ def test_wind_h2_opt_example(subtests): with subtests.test("Check minimum total hydrogen produced"): assert ( pytest.approx( - model.prob.get_val("electrolyzer.total_hydrogen_produced", units="kg/year")[0], + model.prob.get_val("electrolyzer.annual_hydrogen_produced", units="kg/year")[0], abs=15000, ) == 29028700 diff --git a/h2integrate/converters/ammonia/ammonia_synloop.py b/h2integrate/converters/ammonia/ammonia_synloop.py index b920d2151..3354101d5 100644 --- a/h2integrate/converters/ammonia/ammonia_synloop.py +++ b/h2integrate/converters/ammonia/ammonia_synloop.py @@ -548,7 +548,7 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): o2_price_base = self.config.oxygen_price_base # USD / kg O2 # Get total production/consumption - nh3_prod = inputs["total_ammonia_produced"] # kg NH3 /year + nh3_prod = inputs["annual_ammonia_produced"].mean() # kg NH3 /year # Apply scaling rebuild_cost = rebuild_cost_base * capex_ratio * cepci_ratio diff --git a/h2integrate/converters/hydrogen/pem_electrolyzer.py b/h2integrate/converters/hydrogen/pem_electrolyzer.py index f29555fe4..af9ef9c7c 100644 --- a/h2integrate/converters/hydrogen/pem_electrolyzer.py +++ b/h2integrate/converters/hydrogen/pem_electrolyzer.py @@ -183,7 +183,7 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): outputs["capacity_factor"] = H2_Results["Performance Schedules"][ "Capacity Factor [-]" ].values - outputs["annual_hydrogen_produced"] = outputs["hydrogen_out"].sum() + outputs["annual_hydrogen_produced"] = H2_Results["Life: Annual H2 production [kg/year]"] # TODO: replace above line w below # outputs["annual_hydrogen_produced"] = H2_Results["Performance Schedules"][ diff --git a/h2integrate/converters/hydrogen/wombat_model.py b/h2integrate/converters/hydrogen/wombat_model.py index 74572341c..26eb39759 100644 --- a/h2integrate/converters/hydrogen/wombat_model.py +++ b/h2integrate/converters/hydrogen/wombat_model.py @@ -121,11 +121,13 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): outputs["hydrogen_out"] = hydrogen_out_with_availability # Compute total hydrogen produced (sum over the year) - outputs["total_hydrogen_produced"] = np.sum(hydrogen_out_with_availability) + # TODO: make below total rather than annual + outputs["annual_hydrogen_produced"] = np.sum(hydrogen_out_with_availability) # Compute percent hydrogen lost due to O&M maintenance + # TODO: make below total rather than annual percent_hydrogen_lost = 100 * ( - 1 - outputs["total_hydrogen_produced"] / np.sum(original_hydrogen_out) + 1 - outputs["annual_hydrogen_produced"][0] / np.sum(original_hydrogen_out) ) outputs["percent_hydrogen_lost"] = percent_hydrogen_lost diff --git a/h2integrate/finances/numpy_financial_npv.py b/h2integrate/finances/numpy_financial_npv.py index edaef5837..df4b4635d 100644 --- a/h2integrate/finances/numpy_financial_npv.py +++ b/h2integrate/finances/numpy_financial_npv.py @@ -77,6 +77,7 @@ def initialize(self): def setup(self): commodity_type = self.options["commodity_type"] description = self.options["description"].strip() if "description" in self.options else "" + plant_life = int(self.options["plant_config"]["plant"]["plant_life"]) # Use description only if non-empty suffix = f"_{description}" if description else "" @@ -100,6 +101,7 @@ def setup(self): self.add_input( f"total_{self.options['commodity_type']}_produced", val=0.0, + shape=plant_life, units=commodity_units, ) diff --git a/h2integrate/postprocess/sql_to_csv.py b/h2integrate/postprocess/sql_to_csv.py index 68c2843c7..33c75466b 100644 --- a/h2integrate/postprocess/sql_to_csv.py +++ b/h2integrate/postprocess/sql_to_csv.py @@ -61,7 +61,7 @@ def summarize_case(case, return_units=False): # save average capacity factor and annual production lifetime_prod_var = var.lower().split(".")[-1].startswith("annual") and var.lower().split( "." - )[-1].endsswith("production") + )[-1].endswith("production") if "capacity_factor" in var.lower() or lifetime_prod_var: if isinstance(val, np.ndarray): if not np.all(val == val[0]): From 199c6fd667fe50f40cb1ad1725c9c80c47ddb1b5 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Wed, 28 Jan 2026 16:41:42 -0700 Subject: [PATCH 42/63] updated how_to_set_up_an_analysis.md --- docs/user_guide/how_to_set_up_an_analysis.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/user_guide/how_to_set_up_an_analysis.md b/docs/user_guide/how_to_set_up_an_analysis.md index 1babbfb37..aea859827 100644 --- a/docs/user_guide/how_to_set_up_an_analysis.md +++ b/docs/user_guide/how_to_set_up_an_analysis.md @@ -142,9 +142,9 @@ h2i_model.run() # h2i_model.post_process() -# Print the total hydrogen produced by the electrolyzer in kg/year -total_hydrogen = h2i_model.model.get_val("electrolyzer.total_hydrogen_produced", units="kg/year")[0] -print(f"Total hydrogen produced by the electrolyzer: {total_hydrogen:.2f} kg/year") +# Print the average annual hydrogen produced by the electrolyzer in kg/year +annual_hydrogen = h2i_model.model.get_val("electrolyzer.annual_hydrogen_produced", units="kg/year").mean() +print(f"Total hydrogen produced by the electrolyzer: {annual_hydrogen:.2f} kg/year") ``` This will run the analysis defined in the config files and generate the output files in the through the `post_process` method. @@ -169,9 +169,9 @@ h2i_model.run() # Post-process the results # h2i_model.post_process() -# Print the total hydrogen produced by the electrolyzer in kg/year -total_hydrogen = h2i_model.model.get_val("electrolyzer.total_hydrogen_produced", units="kg/year")[0] -print(f"Total hydrogen produced by the electrolyzer: {total_hydrogen:.2f} kg/year") +# Print the average annual hydrogen produced by the electrolyzer in kg/year +annual_hydrogen = h2i_model.model.get_val("electrolyzer.annual_hydrogen_produced", units="kg/year").mean() +print(f"Total hydrogen produced by the electrolyzer: {annual_hydrogen:.2f} kg/year") ``` This is especially useful when you want to run an H2I model as a script and modify parameters dynamically without changing the original YAML configuration file. From d0ac7d8739e435befc2828318bb21bcfc64ff90e Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Wed, 28 Jan 2026 17:05:03 -0700 Subject: [PATCH 43/63] removed init file from new hydro power test folder --- h2integrate/converters/water_power/test/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 h2integrate/converters/water_power/test/__init__.py diff --git a/h2integrate/converters/water_power/test/__init__.py b/h2integrate/converters/water_power/test/__init__.py deleted file mode 100644 index e69de29bb..000000000 From 95fe55915211470d74d284e7661008f3600aac9e Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Thu, 29 Jan 2026 11:54:53 -0700 Subject: [PATCH 44/63] updated remaining failing tests --- h2integrate/finances/test/test_finances.py | 8 ++++---- .../finances/test/test_profast_finance.py | 10 ++++++++-- h2integrate/finances/test/test_profast_npv.py | 16 +++++++++++++--- .../solar/test/test_pvwatts_integration.py | 2 +- .../transporters/test/test_generic_combiner.py | 7 ++++++- 5 files changed, 32 insertions(+), 11 deletions(-) diff --git a/h2integrate/finances/test/test_finances.py b/h2integrate/finances/test/test_finances.py index a15017710..14a3179bc 100644 --- a/h2integrate/finances/test/test_finances.py +++ b/h2integrate/finances/test/test_finances.py @@ -87,7 +87,7 @@ def test_electrolyzer_refurb_results(self): commodity_type="hydrogen", ) ivc = om.IndepVarComp() - ivc.add_output("total_hydrogen_produced", 4.0e5, units="kg/year") + ivc.add_output("total_hydrogen_produced", [4.0e5] * 30, units="kg/year") prob.model.add_subsystem("ivc", ivc, promotes=["*"]) prob.model.add_subsystem("comp", comp, promotes=["*"]) @@ -121,7 +121,7 @@ def test_modified_lcoe_calc(self): commodity_type="electricity", ) ivc = om.IndepVarComp() - ivc.add_output("total_electricity_produced", 2.0e7, units="kW*h/year") + ivc.add_output("total_electricity_produced", [2.0e7] * 30, units="kW*h/year") prob.model.add_subsystem("ivc", ivc, promotes=["*"]) prob.model.add_subsystem("comp", comp, promotes=["*"]) @@ -166,7 +166,7 @@ def test_lcoe_with_selected_technologies(self): commodity_type="electricity", ) ivc = om.IndepVarComp() - ivc.add_output("total_electricity_produced", 2.0e7, units="kW*h/year") + ivc.add_output("total_electricity_produced", [2.0e7] * 30, units="kW*h/year") prob.model.add_subsystem("ivc", ivc, promotes=["*"]) prob.model.add_subsystem("comp", comp, promotes=["*"]) @@ -297,7 +297,7 @@ def test_profast_config_provided(): commodity_type="hydrogen", ) ivc = om.IndepVarComp() - ivc.add_output("total_hydrogen_produced", 4.0e5, units="kg/year") + ivc.add_output("total_hydrogen_produced", [4.0e5] * 30, units="kg/year") prob.model.add_subsystem("ivc", ivc, promotes=["*"]) prob.model.add_subsystem("comp", comp, promotes=["*"]) diff --git a/h2integrate/finances/test/test_profast_finance.py b/h2integrate/finances/test/test_profast_finance.py index 2a3a76906..60b11d851 100644 --- a/h2integrate/finances/test/test_profast_finance.py +++ b/h2integrate/finances/test/test_profast_finance.py @@ -76,7 +76,10 @@ def test_profast_comp(profast_inputs_no1, fake_filtered_tech_config, fake_cost_d description="no1", ) ivc = om.IndepVarComp() - ivc.add_output("total_electricity_produced", mean_hourly_production * 8760, units="kW*h/year") + annual_electricity_produced = [mean_hourly_production * 8760] * plant_config["plant"][ + "plant_life" + ] + ivc.add_output("total_electricity_produced", annual_electricity_produced, units="kW*h/year") prob.model.add_subsystem("ivc", ivc, promotes=["*"]) prob.model.add_subsystem("pf", pf, promotes=["total_electricity_produced"]) prob.setup() @@ -145,7 +148,10 @@ def test_profast_comp_coproduct( description="no1", ) ivc = om.IndepVarComp() - ivc.add_output("total_electricity_produced", mean_hourly_production * 8760, units="kW*h/year") + annual_electricity_produced = [mean_hourly_production * 8760] * plant_config["plant"][ + "plant_life" + ] + ivc.add_output("total_electricity_produced", annual_electricity_produced, units="kW*h/year") prob.model.add_subsystem("ivc", ivc, promotes=["*"]) prob.model.add_subsystem("pf", pf, promotes=["total_electricity_produced"]) prob.setup() diff --git a/h2integrate/finances/test/test_profast_npv.py b/h2integrate/finances/test/test_profast_npv.py index f0760cbea..4007475a5 100644 --- a/h2integrate/finances/test/test_profast_npv.py +++ b/h2integrate/finances/test/test_profast_npv.py @@ -109,7 +109,10 @@ def test_profast_npv_no1(profast_inputs_no1, fake_filtered_tech_config, fake_cos description="no1", ) ivc = om.IndepVarComp() - ivc.add_output("total_electricity_produced", mean_hourly_production * 8760, units="kW*h/year") + annual_electricity_produced = [mean_hourly_production * 8760] * plant_config["plant"][ + "plant_life" + ] + ivc.add_output("total_electricity_produced", annual_electricity_produced, units="kW*h/year") prob.model.add_subsystem("ivc", ivc, promotes=["*"]) prob.model.add_subsystem("pf", pf, promotes=["total_electricity_produced"]) prob.setup() @@ -162,7 +165,11 @@ def test_profast_npv_no1_change_sell_price( ) ivc = om.IndepVarComp() - ivc.add_output("total_electricity_produced", mean_hourly_production * 8760, units="kW*h/year") + annual_electricity_produced = [mean_hourly_production * 8760] * plant_config["plant"][ + "plant_life" + ] + + ivc.add_output("total_electricity_produced", annual_electricity_produced, units="kW*h/year") prob.model.add_subsystem("ivc", ivc, promotes=["*"]) prob.model.add_subsystem("pf", pf, promotes=["total_electricity_produced"]) prob.model.add_subsystem("pf2", pf2, promotes=["total_electricity_produced"]) @@ -235,8 +242,11 @@ def test_profast_npv_no2(profast_inputs_no2, fake_filtered_tech_config, fake_cos commodity_type="electricity", description="no2", ) + annual_electricity_produced = [mean_hourly_production * 8760] * plant_config["plant"][ + "plant_life" + ] ivc = om.IndepVarComp() - ivc.add_output("total_electricity_produced", mean_hourly_production * 8760, units="kW*h/year") + ivc.add_output("total_electricity_produced", annual_electricity_produced, units="kW*h/year") prob.model.add_subsystem("ivc", ivc, promotes=["*"]) prob.model.add_subsystem("pf", pf, promotes=["total_electricity_produced"]) prob.setup() diff --git a/h2integrate/resource/solar/test/test_pvwatts_integration.py b/h2integrate/resource/solar/test/test_pvwatts_integration.py index f395807ea..b9458705b 100644 --- a/h2integrate/resource/solar/test/test_pvwatts_integration.py +++ b/h2integrate/resource/solar/test/test_pvwatts_integration.py @@ -386,7 +386,7 @@ def test_pvwatts_with_openmeteo_solar( prob.setup() prob.run_model() - aep = prob.get_val("pv_perf.annual_energy", units="MW*h/year")[0] + aep = prob.get_val("pv_perf.annual_electricity_produced", units="MW*h/year")[0] with subtests.test("AEP"): assert pytest.approx(aep, rel=1e-6) == 443558.17053592583 diff --git a/h2integrate/transporters/test/test_generic_combiner.py b/h2integrate/transporters/test/test_generic_combiner.py index 58ff18c45..640ba4aa5 100644 --- a/h2integrate/transporters/test/test_generic_combiner.py +++ b/h2integrate/transporters/test/test_generic_combiner.py @@ -11,7 +11,12 @@ @fixture def plant_config(): - plant_dict = {"plant": {"simulation": {"n_timesteps": 8760, "dt": 3600}}} + plant_dict = { + "plant": { + "plant_life": 30, + "simulation": {"n_timesteps": 8760, "dt": 3600}, + } + } return plant_dict From 9503a1bb8a9d32a2782d6ac8c614ad62876b657c Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Thu, 29 Jan 2026 12:04:34 -0700 Subject: [PATCH 45/63] updated example 28 and iron_wrapper --- examples/28_iron_map/driver_config.yaml | 2 +- examples/28_iron_map/plant_config.yaml | 19 ++++--------------- h2integrate/converters/iron/iron_wrapper.py | 6 ++++-- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/examples/28_iron_map/driver_config.yaml b/examples/28_iron_map/driver_config.yaml index 97c22f9e2..3bf03b33c 100644 --- a/examples/28_iron_map/driver_config.yaml +++ b/examples/28_iron_map/driver_config.yaml @@ -10,7 +10,7 @@ general: driver: design_of_experiments: flag: true - debug_print: true + debug_print: false generator: "csvgen" filename: "ned_reduced_sitelist.csv" run_parallel: False diff --git a/examples/28_iron_map/plant_config.yaml b/examples/28_iron_map/plant_config.yaml index 7217f3109..ba8251c7e 100644 --- a/examples/28_iron_map/plant_config.yaml +++ b/examples/28_iron_map/plant_config.yaml @@ -1,21 +1,10 @@ name: "plant_config" description: "This plant is located in MN, USA..." -site: - latitude: 41.717 - longitude: -88.398 - - # array of polygons defining boundaries with x/y coords - boundaries: [ - { - x: [0.0, 1000.0, 1000.0, 0.0], - y: [0.0, 0.0, 100.0, 1000.0], - }, - { - x: [2000.0, 2500.0, 2000.0], - y: [2000.0, 2000.0, 2500.0], - } - ] +sites: + site: + latitude: 41.717 + longitude: -88.398 # array of arrays containing left-to-right technology # interconnections; can support bidirectional connections diff --git a/h2integrate/converters/iron/iron_wrapper.py b/h2integrate/converters/iron/iron_wrapper.py index 2fdf50c38..b94b4c650 100644 --- a/h2integrate/converters/iron/iron_wrapper.py +++ b/h2integrate/converters/iron/iron_wrapper.py @@ -79,6 +79,8 @@ class IronComponent(CostModelBaseClass): """ def setup(self): + plant_life = self.options["plant_config"]["plant"]["plant_life"] + n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] self.config = IronConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost"), @@ -96,7 +98,7 @@ def setup(self): self.add_input("LCOE", val=self.config.LCOE, units="USD/MW/h") self.add_input("LCOH", val=self.config.LCOH, units="USD/kg") - self.add_output("total_iron_produced", val=0.0, units="kg/year") + self.add_output("annual_iron_produced", val=0.0, shape=plant_life, units="kg/year") self.add_output("LCOI", val=0.0, units="USD/kg") def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): @@ -236,7 +238,7 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): # ABOVE: Copy-pasted from ye olde h2integrate_simulation.py (the 1000+ line monster) outputs["iron_out"] = iron_mtpy * 1000 / 8760 - outputs["total_iron_produced"] = iron_mtpy * 1000 + outputs["annual_iron_produced"] = iron_mtpy * 1000 cost_df = iron_costs.costs_df capex = 0 From d98ed88e57b524d749fa2304f54e856d9bd98f05 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Thu, 29 Jan 2026 12:09:55 -0700 Subject: [PATCH 46/63] updated capacity factor strings in run_size_modes files --- docs/user_guide/run_size_modes.md | 16 ++++++++-------- examples/25_sizing_modes/run_size_modes.py | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/user_guide/run_size_modes.md b/docs/user_guide/run_size_modes.md index 9936d652c..c52915e8f 100644 --- a/docs/user_guide/run_size_modes.md +++ b/docs/user_guide/run_size_modes.md @@ -188,10 +188,10 @@ model.run() for value in [ "electrolyzer.electricity_in", "electrolyzer.electrolyzer_size_mw", - "electrolyzer.hydrogen_capacity_factor", + "electrolyzer.capacity_factor", "ammonia.hydrogen_in", "ammonia.max_hydrogen_capacity", - "ammonia.ammonia_capacity_factor", + "ammonia.capacity_factor", "finance_subgroup_h2.LCOH", "finance_subgroup_nh3.LCOA", ]: @@ -225,10 +225,10 @@ model.run() for value in [ "electrolyzer.electricity_in", "electrolyzer.electrolyzer_size_mw", - "electrolyzer.hydrogen_capacity_factor", + "electrolyzer.capacity_factor", "ammonia.hydrogen_in", "ammonia.max_hydrogen_capacity", - "ammonia.ammonia_capacity_factor", + "ammonia.capacity_factor", "finance_subgroup_h2.LCOH", "finance_subgroup_nh3.LCOA", ]: @@ -270,10 +270,10 @@ model.run() for value in [ "electrolyzer.electricity_in", "electrolyzer.electrolyzer_size_mw", - "electrolyzer.hydrogen_capacity_factor", + "electrolyzer.capacity_factor", "ammonia.hydrogen_in", "ammonia.max_hydrogen_capacity", - "ammonia.ammonia_capacity_factor", + "ammonia.capacity_factor", "finance_subgroup_h2.LCOH", "finance_subgroup_nh3.LCOA", ]: @@ -323,10 +323,10 @@ model.run() for value in [ "electrolyzer.electricity_in", "electrolyzer.electrolyzer_size_mw", - "electrolyzer.hydrogen_capacity_factor", + "electrolyzer.capacity_factor", "ammonia.hydrogen_in", "ammonia.max_hydrogen_capacity", - "ammonia.ammonia_capacity_factor", + "ammonia.capacity_factor", "finance_subgroup_h2.LCOH", "finance_subgroup_nh3.LCOA", ]: diff --git a/examples/25_sizing_modes/run_size_modes.py b/examples/25_sizing_modes/run_size_modes.py index 1ae8eb344..4073c75dd 100644 --- a/examples/25_sizing_modes/run_size_modes.py +++ b/examples/25_sizing_modes/run_size_modes.py @@ -82,10 +82,10 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): for value in [ "electrolyzer.electricity_in", "electrolyzer.electrolyzer_size_mw", - "electrolyzer.hydrogen_capacity_factor", + "electrolyzer.capacity_factor", "ammonia.hydrogen_in", "ammonia.max_hydrogen_capacity", - "ammonia.ammonia_capacity_factor", + "ammonia.capacity_factor", "finance_subgroup_h2.LCOH", "finance_subgroup_nh3.LCOA", ]: @@ -112,10 +112,10 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): for value in [ "electrolyzer.electricity_in", "electrolyzer.electrolyzer_size_mw", - "electrolyzer.hydrogen_capacity_factor", + "electrolyzer.capacity_factor", "ammonia.hydrogen_in", "ammonia.max_hydrogen_capacity", - "ammonia.ammonia_capacity_factor", + "ammonia.capacity_factor", "finance_subgroup_h2.LCOH", "finance_subgroup_nh3.LCOA", ]: @@ -153,10 +153,10 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): for value in [ "electrolyzer.electricity_in", "electrolyzer.electrolyzer_size_mw", - "electrolyzer.hydrogen_capacity_factor", + "electrolyzer.capacity_factor", "ammonia.hydrogen_in", "ammonia.max_hydrogen_capacity", - "ammonia.ammonia_capacity_factor", + "ammonia.capacity_factor", "finance_subgroup_h2.LCOH", "finance_subgroup_nh3.LCOA", ]: @@ -198,10 +198,10 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): for value in [ "electrolyzer.electricity_in", "electrolyzer.electrolyzer_size_mw", - "electrolyzer.hydrogen_capacity_factor", + "electrolyzer.capacity_factor", "ammonia.hydrogen_in", "ammonia.max_hydrogen_capacity", - "ammonia.ammonia_capacity_factor", + "ammonia.capacity_factor", "finance_subgroup_h2.LCOH", "finance_subgroup_nh3.LCOA", ]: From 11d45b2cca3f32b2770617d9e526ab9c7ac40320 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:06:42 -0700 Subject: [PATCH 47/63] removed commented out outputs --- .../converters/ammonia/ammonia_synloop.py | 4 +--- .../ammonia/simple_ammonia_model.py | 2 -- .../marine/marine_carbon_capture_baseclass.py | 8 +------ h2integrate/converters/grid/grid.py | 9 +------ h2integrate/converters/hopp/hopp_wrapper.py | 5 ---- .../geologic/h2_well_subsurface_baseclass.py | 4 +--- .../geologic/h2_well_surface_baseclass.py | 2 -- .../converters/hydrogen/pem_electrolyzer.py | 8 +------ .../converters/hydrogen/wombat_model.py | 1 - h2integrate/converters/iron/iron_dri_base.py | 8 ------- .../converters/iron/martin_mine_perf_model.py | 15 ------------ .../converters/methanol/methanol_baseclass.py | 7 ++---- .../natural_gas/natural_gas_cc_ct.py | 9 ------- h2integrate/converters/nitrogen/simple_ASU.py | 24 +------------------ h2integrate/converters/solar/solar_pysam.py | 7 ------ .../converters/steel/steel_baseclass.py | 1 - .../converters/steel/steel_eaf_base.py | 8 ------- .../water/desal/desalination_baseclass.py | 2 +- h2integrate/converters/wind/floris.py | 12 ---------- h2integrate/converters/wind/wind_pysam.py | 10 -------- 20 files changed, 9 insertions(+), 137 deletions(-) diff --git a/h2integrate/converters/ammonia/ammonia_synloop.py b/h2integrate/converters/ammonia/ammonia_synloop.py index 3354101d5..9bfdbbc5e 100644 --- a/h2integrate/converters/ammonia/ammonia_synloop.py +++ b/h2integrate/converters/ammonia/ammonia_synloop.py @@ -153,13 +153,12 @@ def setup(self): self.add_input("nitrogen_in", val=0.0, shape=n_timesteps, units="kg/h") self.add_input("electricity_in", val=0.0, shape=n_timesteps, units="MW") - # self.add_output("ammonia_out", val=0.0, shape=n_timesteps, units="kg/h") self.add_output("nitrogen_out", val=0.0, shape=n_timesteps, units="kg/h") self.add_output("hydrogen_out", val=0.0, shape=n_timesteps, units="kg/h") self.add_output("electricity_out", val=0.0, shape=n_timesteps, units="MW") self.add_output("heat_out", val=0.0, shape=n_timesteps, units="kW*h/kg") self.add_output("catalyst_mass", val=0.0, units="kg") - # self.add_output("total_ammonia_produced", val=0.0, units="kg/year") + self.add_output("total_hydrogen_consumed", val=0.0, units="kg/year") self.add_output("total_nitrogen_consumed", val=0.0, units="kg/year") self.add_output("total_electricity_consumed", val=0.0, units="kW*h/year") @@ -167,7 +166,6 @@ def setup(self): "limiting_input", val=0, shape_by_conn=True, copy_shape="hydrogen_in", units=None ) self.add_output("max_hydrogen_capacity", val=1000.0, units="kg/h") - # self.add_output("ammonia_capacity_factor", val=0.0, units="unitless") def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): # Get config values diff --git a/h2integrate/converters/ammonia/simple_ammonia_model.py b/h2integrate/converters/ammonia/simple_ammonia_model.py index 7aae5f6fd..1e979f053 100644 --- a/h2integrate/converters/ammonia/simple_ammonia_model.py +++ b/h2integrate/converters/ammonia/simple_ammonia_model.py @@ -45,8 +45,6 @@ def setup(self): merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance") ) self.add_input("hydrogen_in", val=0.0, shape=n_timesteps, units="kg/h") - # self.add_output("ammonia_out", val=0.0, shape=n_timesteps, units="kg/h") - # self.add_output("total_ammonia_produced", val=0.0, units="kg/year") def compute(self, inputs, outputs): ammonia_production_kgpy = ( diff --git a/h2integrate/converters/co2/marine/marine_carbon_capture_baseclass.py b/h2integrate/converters/co2/marine/marine_carbon_capture_baseclass.py index 6cc2ddd47..8e56b1623 100644 --- a/h2integrate/converters/co2/marine/marine_carbon_capture_baseclass.py +++ b/h2integrate/converters/co2/marine/marine_carbon_capture_baseclass.py @@ -41,13 +41,7 @@ def setup(self): self.add_input( "electricity_in", val=0.0, shape=8760, units="W", desc="Hourly input electricity (W)" ) - # self.add_output( - # "co2_out", - # val=0.0, - # shape=8760, - # units="kg/h", - # desc="Hourly CO₂ capture rate (kg/h)", - # ) + # TODO: remove this output once finance models are updated self.add_output("co2_capture_mtpy", units="t/year", desc="Annual CO₂ captured (t/year)") diff --git a/h2integrate/converters/grid/grid.py b/h2integrate/converters/grid/grid.py index bd6991f74..5f5411a47 100644 --- a/h2integrate/converters/grid/grid.py +++ b/h2integrate/converters/grid/grid.py @@ -87,14 +87,7 @@ def setup(self): desc="Electricity demand from downstream technologies", ) - # Electricity flowing OUT OF the grid (buying from grid) - # self.add_output( - # "electricity_out", - # val=0.0, - # shape=n_timesteps, - # units=self.commodity_rate_units, - # desc="Electricity flowing out of grid interconnection point (buying from grid)", - # ) + # electricity_out is electricity flowing OUT OF the grid (buying from grid) self.add_output( "electricity_sold", diff --git a/h2integrate/converters/hopp/hopp_wrapper.py b/h2integrate/converters/hopp/hopp_wrapper.py index b07638f1d..7a944a998 100644 --- a/h2integrate/converters/hopp/hopp_wrapper.py +++ b/h2integrate/converters/hopp/hopp_wrapper.py @@ -33,8 +33,6 @@ def setup(self): self.commodity_rate_units = "kW" self.commodity_amount_units = "kW*h" - # n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] - self.config = HOPPComponentModelConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), strict=False, @@ -69,9 +67,6 @@ def setup(self): self.add_output("percent_load_missed", units="percent", val=0.0) self.add_output("curtailment_percent", units="percent", val=0.0) self.add_output("aep", units="kW*h", val=0.0) - # self.add_output( - # "electricity_out", val=np.zeros(n_timesteps), units="kW", desc="Power output" - # ) self.add_output("battery_duration", val=0.0, units="h", desc="Battery duration") self.add_output( "annual_energy_to_interconnect_potential_ratio", diff --git a/h2integrate/converters/hydrogen/geologic/h2_well_subsurface_baseclass.py b/h2integrate/converters/hydrogen/geologic/h2_well_subsurface_baseclass.py index 213017059..86f7b72a2 100644 --- a/h2integrate/converters/hydrogen/geologic/h2_well_subsurface_baseclass.py +++ b/h2integrate/converters/hydrogen/geologic/h2_well_subsurface_baseclass.py @@ -97,10 +97,8 @@ def setup(self): self.add_input("grain_size", units="m", val=self.config.grain_size) # outputs - self.add_output("wellhead_gas_out", units="kg/h", shape=(8760,)) - # self.add_output("hydrogen_out", units="kg/h", shape=(8760,)) + self.add_output("wellhead_gas_out", units="kg/h", shape=(self.n_timesteps,)) self.add_output("total_wellhead_gas_produced", val=0.0, units="kg/year") - # self.add_output("total_hydrogen_produced", val=0.0, units="kg/year") @define(kw_only=True) diff --git a/h2integrate/converters/hydrogen/geologic/h2_well_surface_baseclass.py b/h2integrate/converters/hydrogen/geologic/h2_well_surface_baseclass.py index 38064e888..ff7cbf90c 100644 --- a/h2integrate/converters/hydrogen/geologic/h2_well_surface_baseclass.py +++ b/h2integrate/converters/hydrogen/geologic/h2_well_surface_baseclass.py @@ -79,9 +79,7 @@ def setup(self): self.add_input("wellhead_h2_concentration_mol", units="mol/mol", val=-1.0) # outputs - # self.add_output("hydrogen_out", units="kg/h", shape=(n_timesteps,)) self.add_output("hydrogen_concentration_out", units="mol/mol", val=-1.0) - # self.add_output("total_hydrogen_produced", val=-1.0, units="kg/year") self.add_output("max_flow_size", units="kg/h", val=self.config.max_flow_in) diff --git a/h2integrate/converters/hydrogen/pem_electrolyzer.py b/h2integrate/converters/hydrogen/pem_electrolyzer.py index af9ef9c7c..36f20e59b 100644 --- a/h2integrate/converters/hydrogen/pem_electrolyzer.py +++ b/h2integrate/converters/hydrogen/pem_electrolyzer.py @@ -66,12 +66,7 @@ def setup(self): ) super().setup() self.add_output("efficiency", val=0.0, desc="Average efficiency of the electrolyzer") - # self.add_output( - # "rated_h2_production_kg_pr_hr", - # val=0.0, - # units="kg/h", - # desc="Rated hydrogen production of system in kg/hour", - # ) + self.add_output( "time_until_replacement", val=80000.0, units="h", desc="Time until replacement" ) @@ -92,7 +87,6 @@ def setup(self): self.add_input("cluster_size", val=-1.0, units="MW") self.add_input("max_hydrogen_capacity", val=1000.0, units="kg/h") # TODO: add feedstock inputs and consumption outputs - # self.add_output("hydrogen_capacity_factor", val=0.0, units="unitless") def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): plant_life = self.options["plant_config"]["plant"]["plant_life"] diff --git a/h2integrate/converters/hydrogen/wombat_model.py b/h2integrate/converters/hydrogen/wombat_model.py index 26eb39759..70d181686 100644 --- a/h2integrate/converters/hydrogen/wombat_model.py +++ b/h2integrate/converters/hydrogen/wombat_model.py @@ -41,7 +41,6 @@ def setup(self): 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( diff --git a/h2integrate/converters/iron/iron_dri_base.py b/h2integrate/converters/iron/iron_dri_base.py index 0fb53d0ed..217d67c85 100644 --- a/h2integrate/converters/iron/iron_dri_base.py +++ b/h2integrate/converters/iron/iron_dri_base.py @@ -80,14 +80,6 @@ def setup(self): desc="Pig iron demand for iron plant", ) - # self.add_output( - # "pig_iron_out", - # val=0.0, - # shape=n_timesteps, - # units="t/h", - # desc="Pig iron produced", - # ) - coeff_fpath = ROOT_DIR / "converters" / "iron" / "rosner" / "perf_coeffs.csv" # rosner dri performance model coeff_df = pd.read_csv(coeff_fpath, index_col=0) diff --git a/h2integrate/converters/iron/martin_mine_perf_model.py b/h2integrate/converters/iron/martin_mine_perf_model.py index 0e352b10d..befb71ab3 100644 --- a/h2integrate/converters/iron/martin_mine_perf_model.py +++ b/h2integrate/converters/iron/martin_mine_perf_model.py @@ -92,21 +92,6 @@ def setup(self): desc="Electricity consumed", ) - # self.add_output( - # "iron_ore_out", - # val=0.0, - # shape=n_timesteps, - # units="t/h", - # desc="Iron ore pellets produced", - # ) - - # self.add_output( - # "total_iron_ore_produced", - # val=1.0, - # units="t/year", - # desc="Total iron ore pellets produced anually", - # ) - coeff_fpath = ROOT_DIR / "converters" / "iron" / "martin_ore" / "perf_coeffs.csv" # martin ore performance model coeff_df = pd.read_csv(coeff_fpath, index_col=0) diff --git a/h2integrate/converters/methanol/methanol_baseclass.py b/h2integrate/converters/methanol/methanol_baseclass.py index db0043d5b..4ef55c97b 100644 --- a/h2integrate/converters/methanol/methanol_baseclass.py +++ b/h2integrate/converters/methanol/methanol_baseclass.py @@ -45,16 +45,13 @@ def setup(self): self.commodity_amount_units = "kg" super().setup() - n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] self.add_input("plant_capacity_kgpy", units="kg/year", val=self.config.plant_capacity_kgpy) self.add_input("input_capacity_factor", units="unitless", val=self.config.capacity_factor) self.add_input("co2e_emit_ratio", units="kg/kg", val=self.config.co2e_emit_ratio) self.add_input("h2o_consume_ratio", units="kg/kg", val=self.config.h2o_consume_ratio) - # self.add_output("methanol_out", units="kg/h", shape=n_timesteps) - # self.add_output("total_methanol_produced", units="kg/year") - self.add_output("co2e_emissions", units="kg/h", shape=n_timesteps) - self.add_output("h2o_consumption", units="kg/h", shape=n_timesteps) + self.add_output("co2e_emissions", units="kg/h", shape=self.n_timesteps) + self.add_output("h2o_consumption", units="kg/h", shape=self.n_timesteps) @define(kw_only=True) diff --git a/h2integrate/converters/natural_gas/natural_gas_cc_ct.py b/h2integrate/converters/natural_gas/natural_gas_cc_ct.py index 60de24a35..8d7b693e8 100644 --- a/h2integrate/converters/natural_gas/natural_gas_cc_ct.py +++ b/h2integrate/converters/natural_gas/natural_gas_cc_ct.py @@ -81,15 +81,6 @@ def setup(self): desc="Natural gas consumed by the plant", ) - # Add electricity output - # self.add_output( - # "electricity_out", - # val=0.0, - # shape=n_timesteps, - # units="MW", - # desc="Electricity output from natural gas plant", - # ) - # Add heat_rate as an OpenMDAO input with config value as default self.add_input( "heat_rate_mmbtu_per_mwh", diff --git a/h2integrate/converters/nitrogen/simple_ASU.py b/h2integrate/converters/nitrogen/simple_ASU.py index 837e6518b..483cb15e1 100644 --- a/h2integrate/converters/nitrogen/simple_ASU.py +++ b/h2integrate/converters/nitrogen/simple_ASU.py @@ -88,9 +88,6 @@ def setup(self): self.add_output("air_in", val=0.0, shape=n_timesteps, units="kg/h") self.add_output("ASU_capacity_kW", val=0.0, units="kW", desc="ASU rated capacity in kW") - # self.add_output( - # "rated_N2_kg_pr_hr", val=0.0, units="kg/h", desc="ASU rated capacity in kg-N2/hour" - # ) self.add_output( "annual_electricity_consumption", @@ -98,26 +95,7 @@ def setup(self): units="kW", desc="ASU annual electricity consumption in kWh/year", ) - # self.add_output( - # "total_nitrogen_produced", - # val=0.0, - # units="kg/year", - # desc="ASU annual nitrogen production in kg-N2/year", - # ) - # self.add_output( - # "annual_max_nitrogen_production", - # val=0.0, - # units="kg/year", - # desc="ASU maximum annual nitrogen production in kg-N2/year", - # ) - # self.add_output( - # "nitrogen_production_capacity_factor", - # val=0.0, - # units=None, - # desc="ASU annual nitrogen production in kg-N2/year", - # ) - - # self.add_output("nitrogen_out", val=0.0, shape=n_timesteps, units="kg/h") + self.add_output("oxygen_out", val=0.0, shape=n_timesteps, units="kg/h") self.add_output("argon_out", val=0.0, shape=n_timesteps, units="kg/h") diff --git a/h2integrate/converters/solar/solar_pysam.py b/h2integrate/converters/solar/solar_pysam.py index 84df0c2c9..8ea0a651a 100644 --- a/h2integrate/converters/solar/solar_pysam.py +++ b/h2integrate/converters/solar/solar_pysam.py @@ -155,12 +155,6 @@ def setup(self): desc="PV rated capacity in DC", ) self.add_output("system_capacity_AC", val=0.0, units="kW", desc="PV rated capacity in AC") - # self.add_output( - # "annual_energy", - # val=0.0, - # units="kW*h/year", - # desc="Annual energy production in kWac", - # ) if self.design_config.create_model_from == "default": self.system_model = Pvwatts.default(self.design_config.config_name) @@ -306,4 +300,3 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): outputs["capacity_factor"] = outputs["total_electricity_produced"] / max_production outputs["annual_electricity_produced"] = self.system_model.value("ac_annual") - # outputs["annual_energy"] = self.system_model.value("ac_annual") diff --git a/h2integrate/converters/steel/steel_baseclass.py b/h2integrate/converters/steel/steel_baseclass.py index 7a22365ca..2d930b6e7 100644 --- a/h2integrate/converters/steel/steel_baseclass.py +++ b/h2integrate/converters/steel/steel_baseclass.py @@ -12,7 +12,6 @@ def setup(self): # NOTE: the SteelPerformanceModel does not use electricity or hydrogen in its calc self.add_input("electricity_in", val=0.0, shape=n_timesteps, units="kW") self.add_input("hydrogen_in", val=0.0, shape=n_timesteps, units="kg/h") - # self.add_output("steel", val=0.0, shape=n_timesteps, units="t/year") def compute(self, inputs, outputs): """ diff --git a/h2integrate/converters/steel/steel_eaf_base.py b/h2integrate/converters/steel/steel_eaf_base.py index 8aa8abf4a..1a602aec7 100644 --- a/h2integrate/converters/steel/steel_eaf_base.py +++ b/h2integrate/converters/steel/steel_eaf_base.py @@ -76,14 +76,6 @@ def setup(self): desc="Steel demand for steel plant", ) - # self.add_output( - # "steel_out", - # val=0.0, - # shape=n_timesteps, - # units="t/h", - # desc="Steel produced", - # ) - coeff_fpath = ROOT_DIR / "converters" / "iron" / "rosner" / "perf_coeffs.csv" # rosner dri performance model coeff_df = pd.read_csv(coeff_fpath, index_col=0) diff --git a/h2integrate/converters/water/desal/desalination_baseclass.py b/h2integrate/converters/water/desal/desalination_baseclass.py index fc515453d..4039491a3 100644 --- a/h2integrate/converters/water/desal/desalination_baseclass.py +++ b/h2integrate/converters/water/desal/desalination_baseclass.py @@ -7,7 +7,7 @@ def setup(self): self.commodity_amount_units = "m**3" self.commodity_rate_units = "m**3/h" super().setup() - # self.add_output("water", val=0.0, units="m**3/h", desc="Fresh water") + self.add_output("mass", val=0.0, units="kg", desc="Mass of desalination system") self.add_output("footprint", val=0.0, units="m**2", desc="Footprint of desalination system") diff --git a/h2integrate/converters/wind/floris.py b/h2integrate/converters/wind/floris.py index ebcac29cc..e75fb8fb3 100644 --- a/h2integrate/converters/wind/floris.py +++ b/h2integrate/converters/wind/floris.py @@ -145,18 +145,6 @@ def setup(self): desc="turbine hub-height", ) - # self.add_output( - # "total_electricity_produced", - # val=0.0, - # units="kW*h/year", - # desc="Annual energy production from WindPlant", - # ) - # self.add_output("total_capacity", val=0.0, units="kW", desc="Wind farm rated capacity") - - # self.add_output( - # "capacity_factor", val=0.0, units="unitless", desc="Wind farm capacity factor" - # ) - super().setup() power_curve = self.config.floris_turbine_config.get("power_thrust_table").get("power") diff --git a/h2integrate/converters/wind/wind_pysam.py b/h2integrate/converters/wind/wind_pysam.py index b9b2c4321..8c81904c1 100644 --- a/h2integrate/converters/wind/wind_pysam.py +++ b/h2integrate/converters/wind/wind_pysam.py @@ -240,16 +240,6 @@ def setup(self): desc="turbine hub-height in meters", ) - # self.add_output( - # "annual_energy", - # val=0.0, - # units="kW*h/year", - # desc="Annual energy production from WindPlant in kW", - # ) - # self.add_output( - # "total_capacity", val=0.0, units="kW", desc="Wind farm rated capacity in kW" - # ) - if self.config.create_model_from == "default": self.system_model = Windpower.default(self.config.config_name) elif self.config.create_model_from == "new": From 01f4124b09fc005a6779ffc1edae720401e33d02 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:47:12 -0700 Subject: [PATCH 48/63] updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 486553cc9..d340106cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - Remove unused dependencies. - Fixes typos for skipped folders. - Fixes missing dependencies for `gis` modifier used in new iron mapping tests. +- Added `PerformanceModelBaseClass` and standardized outputs of converter performance models ## 0.5.1 [December 18, 2025] From 9c8dd0eb457e0b1181268fc7c7e2cb9b87c99c7d Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Thu, 29 Jan 2026 15:57:42 -0700 Subject: [PATCH 49/63] removed duplicate inheritance of PerformanceModelBaseClass in electrolyzer performance baseclass --- .../hydrogen/electrolyzer_baseclass.py | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/h2integrate/converters/hydrogen/electrolyzer_baseclass.py b/h2integrate/converters/hydrogen/electrolyzer_baseclass.py index 65218db40..1eeeeb6cd 100644 --- a/h2integrate/converters/hydrogen/electrolyzer_baseclass.py +++ b/h2integrate/converters/hydrogen/electrolyzer_baseclass.py @@ -1,13 +1,10 @@ from h2integrate.core.model_baseclasses import ( CostModelBaseClass, - PerformanceModelBaseClass, ResizeablePerformanceModelBaseClass, ) -class ElectrolyzerPerformanceBaseClass( - ResizeablePerformanceModelBaseClass, PerformanceModelBaseClass -): +class ElectrolyzerPerformanceBaseClass(ResizeablePerformanceModelBaseClass): def setup(self): self.commodity = "hydrogen" self.commodity_rate_units = "kg/h" @@ -15,13 +12,8 @@ def setup(self): super().setup() - # Define inputs for electricity and outputs for hydrogen and oxygen generation + # Define inputs for electricity self.add_input("electricity_in", val=0.0, shape=self.n_timesteps, units="kW") - # self.add_output("hydrogen_out", val=0.0, shape=n_timesteps, units="kg/h") - # self.add_output( - # "time_until_replacement", val=80000.0, units="h", desc="Time until replacement" - # ) - # self.add_output("total_hydrogen_produced", val=0.0, units="kg/year") def compute(self, inputs, outputs): """ @@ -37,7 +29,5 @@ class ElectrolyzerCostBaseClass(CostModelBaseClass): def setup(self): n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] super().setup() - self.add_input( - "total_hydrogen_produced", val=0.0, units="kg" - ) # NOTE: unsure if this is used + self.add_input("total_hydrogen_produced", val=0.0, units="kg") self.add_input("electricity_in", val=0.0, shape=n_timesteps, units="kW") From 10116869e587d4c60419bf766c7256f37ffc779e Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:58:24 -0700 Subject: [PATCH 50/63] updated pysam battery outputs --- h2integrate/storage/battery/battery_baseclass.py | 8 -------- h2integrate/storage/battery/pysam_battery.py | 1 - 2 files changed, 9 deletions(-) diff --git a/h2integrate/storage/battery/battery_baseclass.py b/h2integrate/storage/battery/battery_baseclass.py index f56d326e3..8163cd71b 100644 --- a/h2integrate/storage/battery/battery_baseclass.py +++ b/h2integrate/storage/battery/battery_baseclass.py @@ -16,14 +16,6 @@ def setup(self): desc="Power input to Battery", ) - # self.add_output( - # "electricity_out", - # val=0.0, - # copy_shape="electricity_in", - # units="kW", - # desc="Total electricity out of Battery", - # ) - self.add_output( "SOC", val=0.0, diff --git a/h2integrate/storage/battery/pysam_battery.py b/h2integrate/storage/battery/pysam_battery.py index a8a9f6520..9f3b64317 100644 --- a/h2integrate/storage/battery/pysam_battery.py +++ b/h2integrate/storage/battery/pysam_battery.py @@ -240,7 +240,6 @@ def setup(self): ) super().setup() - # BatteryPerformanceBaseClass.setup(self) self.add_input( "electricity_demand", From 36c077280234b1171283f73d195a9ab3261e1172 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:51:40 -0700 Subject: [PATCH 51/63] updated annual outputs to properly account for fraction of year simulated --- h2integrate/converters/ammonia/ammonia_synloop.py | 4 ++-- h2integrate/converters/ammonia/simple_ammonia_model.py | 4 ++-- h2integrate/converters/grid/grid.py | 4 ++-- .../converters/hydrogen/geologic/aspen_surface_processing.py | 4 ++-- .../converters/hydrogen/geologic/simple_natural_geoh2.py | 4 ++-- .../hydrogen/geologic/templeton_serpentinization.py | 4 ++-- h2integrate/converters/iron/iron_dri_base.py | 4 ++-- h2integrate/converters/iron/martin_mine_perf_model.py | 4 ++-- h2integrate/converters/methanol/co2h_methanol_plant.py | 4 ++-- h2integrate/converters/methanol/smr_methanol_plant.py | 4 ++-- h2integrate/converters/natural_gas/natural_gas_cc_ct.py | 4 ++-- h2integrate/converters/nitrogen/simple_ASU.py | 4 ++-- h2integrate/converters/steel/steel.py | 4 ++-- h2integrate/converters/steel/steel_eaf_base.py | 4 ++-- h2integrate/converters/water/desal/desalination.py | 4 ++-- .../converters/water_power/hydro_plant_run_of_river.py | 4 ++-- h2integrate/converters/wind/floris.py | 4 ++-- h2integrate/storage/battery/pysam_battery.py | 4 ++-- 18 files changed, 36 insertions(+), 36 deletions(-) diff --git a/h2integrate/converters/ammonia/ammonia_synloop.py b/h2integrate/converters/ammonia/ammonia_synloop.py index 9bfdbbc5e..11978ad85 100644 --- a/h2integrate/converters/ammonia/ammonia_synloop.py +++ b/h2integrate/converters/ammonia/ammonia_synloop.py @@ -277,8 +277,8 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): outputs["capacity_factor"] = np.mean(nh3_prod) / nh3_cap outputs["rated_ammonia_production"] = nh3_cap - outputs["annual_ammonia_produced"] = ( - outputs["total_ammonia_produced"] * self.fraction_of_year_simulated + outputs["annual_ammonia_produced"] = outputs["total_ammonia_produced"] * ( + 1 / self.fraction_of_year_simulated ) diff --git a/h2integrate/converters/ammonia/simple_ammonia_model.py b/h2integrate/converters/ammonia/simple_ammonia_model.py index 1e979f053..4ebac8a9d 100644 --- a/h2integrate/converters/ammonia/simple_ammonia_model.py +++ b/h2integrate/converters/ammonia/simple_ammonia_model.py @@ -54,8 +54,8 @@ def compute(self, inputs, outputs): outputs["capacity_factor"] = self.config.plant_capacity_factor outputs["total_ammonia_produced"] = ammonia_production_kgpy - outputs["annual_ammonia_produced"] = ( - outputs["total_ammonia_produced"] * self.fraction_of_year_simulated + outputs["annual_ammonia_produced"] = outputs["total_ammonia_produced"] * ( + 1 / self.fraction_of_year_simulated ) outputs["rated_ammonia_production"] = self.config.plant_capacity_kgpy / 8760 diff --git a/h2integrate/converters/grid/grid.py b/h2integrate/converters/grid/grid.py index 5f5411a47..9fbdfcbad 100644 --- a/h2integrate/converters/grid/grid.py +++ b/h2integrate/converters/grid/grid.py @@ -138,8 +138,8 @@ def compute(self, inputs, outputs): self.dt / 3600 ) outputs["capacity_factor"] = outputs["total_electricity_produced"].sum() / max_production - outputs["annual_electricity_produced"] = ( - outputs["total_electricity_produced"] * self.fraction_of_year_simulated + outputs["annual_electricity_produced"] = outputs["total_electricity_produced"] * ( + 1 / self.fraction_of_year_simulated ) diff --git a/h2integrate/converters/hydrogen/geologic/aspen_surface_processing.py b/h2integrate/converters/hydrogen/geologic/aspen_surface_processing.py index 8ab314208..030977f23 100644 --- a/h2integrate/converters/hydrogen/geologic/aspen_surface_processing.py +++ b/h2integrate/converters/hydrogen/geologic/aspen_surface_processing.py @@ -148,8 +148,8 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): outputs["water_consumed"] = water_in_kt_h outputs["steam_out"] = steam_out_kt_h outputs["total_hydrogen_produced"] = np.sum(h2_out_kg_hr) - outputs["annual_hydrogen_produced"] = ( - outputs["total_hydrogen_produced"] * self.fraction_of_year_simulated + outputs["annual_hydrogen_produced"] = outputs["total_hydrogen_produced"] * ( + 1 / self.fraction_of_year_simulated ) outputs["rated_hydrogen_production"] = wellhead_cap_kg_hr # TODO: double check outputs["capacity_factor"] = outputs["total_hydrogen_produced"] / ( diff --git a/h2integrate/converters/hydrogen/geologic/simple_natural_geoh2.py b/h2integrate/converters/hydrogen/geologic/simple_natural_geoh2.py index 8ca6d5fe7..2d1997891 100644 --- a/h2integrate/converters/hydrogen/geologic/simple_natural_geoh2.py +++ b/h2integrate/converters/hydrogen/geologic/simple_natural_geoh2.py @@ -149,8 +149,8 @@ def compute(self, inputs, outputs): outputs["max_wellhead_gas"] = init_wh_flow outputs["total_wellhead_gas_produced"] = np.sum(outputs["wellhead_gas_out"]) outputs["total_hydrogen_produced"] = np.sum(outputs["hydrogen_out"]) - outputs["annual_hydrogen_produced"] = ( - outputs["total_hydrogen_produced"] * self.fraction_of_year_simulated + outputs["annual_hydrogen_produced"] = outputs["total_hydrogen_produced"] * ( + 1 / self.fraction_of_year_simulated ) outputs["rated_hydrogen_production"] = init_wh_flow # TODO: double check outputs["capacity_factor"] = outputs["total_hydrogen_produced"] / ( diff --git a/h2integrate/converters/hydrogen/geologic/templeton_serpentinization.py b/h2integrate/converters/hydrogen/geologic/templeton_serpentinization.py index 11063eded..4baf125cd 100644 --- a/h2integrate/converters/hydrogen/geologic/templeton_serpentinization.py +++ b/h2integrate/converters/hydrogen/geologic/templeton_serpentinization.py @@ -136,8 +136,8 @@ def compute(self, inputs, outputs): outputs["total_hydrogen_produced"] = np.sum(outputs["hydrogen_out"]) outputs["total_wellhead_gas_produced"] = np.sum(outputs["wellhead_gas_out"]) - outputs["annual_hydrogen_produced"] = ( - outputs["total_hydrogen_produced"] * self.fraction_of_year_simulated + outputs["annual_hydrogen_produced"] = outputs["total_hydrogen_produced"] * ( + 1 / self.fraction_of_year_simulated ) outputs["rated_hydrogen_production"] = np.max(h2_prod_avg) # TODO: double check outputs["capacity_factor"] = outputs["total_hydrogen_produced"] / ( diff --git a/h2integrate/converters/iron/iron_dri_base.py b/h2integrate/converters/iron/iron_dri_base.py index 217d67c85..fd5da6863 100644 --- a/h2integrate/converters/iron/iron_dri_base.py +++ b/h2integrate/converters/iron/iron_dri_base.py @@ -256,8 +256,8 @@ def compute(self, inputs, outputs): inputs["system_capacity"] * self.n_timesteps ) outputs["rated_pig_iron_production"] = inputs["system_capacity"] - outputs["annual_pig_iron_produced"] = ( - outputs["total_pig_iron_produced"] * self.fraction_of_year_simulated + outputs["annual_pig_iron_produced"] = outputs["total_pig_iron_produced"] * ( + 1 / self.fraction_of_year_simulated ) # feedstock consumption based on actual pig iron produced diff --git a/h2integrate/converters/iron/martin_mine_perf_model.py b/h2integrate/converters/iron/martin_mine_perf_model.py index befb71ab3..5423a8374 100644 --- a/h2integrate/converters/iron/martin_mine_perf_model.py +++ b/h2integrate/converters/iron/martin_mine_perf_model.py @@ -230,8 +230,8 @@ def compute(self, inputs, outputs): outputs["iron_ore_out"] = processed_ore_production outputs["total_iron_ore_produced"] = np.sum(processed_ore_production) - outputs["annual_iron_ore_produced"] = ( - outputs["total_iron_ore_produced"] * self.fraction_of_year_simulated + outputs["annual_iron_ore_produced"] = outputs["total_iron_ore_produced"] * ( + 1 / self.fraction_of_year_simulated ) outputs["rated_iron_ore_production"] = inputs["system_capacity"] outputs["capacity_factor"] = outputs["total_iron_ore_produced"] / ( diff --git a/h2integrate/converters/methanol/co2h_methanol_plant.py b/h2integrate/converters/methanol/co2h_methanol_plant.py index 113f422aa..08fd28fe4 100644 --- a/h2integrate/converters/methanol/co2h_methanol_plant.py +++ b/h2integrate/converters/methanol/co2h_methanol_plant.py @@ -125,8 +125,8 @@ def compute(self, inputs, outputs): outputs["total_methanol_produced"] = outputs["methanol_out"].sum() max_production = len(meoh_prod) * inputs["plant_capacity_kgpy"] / 8760 outputs["capacity_factor"] = outputs["total_methanol_produced"] / max_production - outputs["annual_methanol_produced"] = ( - outputs["total_methanol_produced"] * self.fraction_of_year_simulated + outputs["annual_methanol_produced"] = outputs["total_methanol_produced"] * ( + 1 / self.fraction_of_year_simulated ) diff --git a/h2integrate/converters/methanol/smr_methanol_plant.py b/h2integrate/converters/methanol/smr_methanol_plant.py index 8b134f3bb..356e34b7a 100644 --- a/h2integrate/converters/methanol/smr_methanol_plant.py +++ b/h2integrate/converters/methanol/smr_methanol_plant.py @@ -109,8 +109,8 @@ def compute(self, inputs, outputs): outputs["total_methanol_produced"] = outputs["methanol_out"].sum() max_production = len(meoh_prod) * inputs["plant_capacity_kgpy"] / 8760 outputs["capacity_factor"] = outputs["total_methanol_produced"] / max_production - outputs["annual_methanol_produced"] = ( - outputs["total_methanol_produced"] * self.fraction_of_year_simulated + outputs["annual_methanol_produced"] = outputs["total_methanol_produced"] * ( + 1 / self.fraction_of_year_simulated ) diff --git a/h2integrate/converters/natural_gas/natural_gas_cc_ct.py b/h2integrate/converters/natural_gas/natural_gas_cc_ct.py index 8d7b693e8..587ee5ce3 100644 --- a/h2integrate/converters/natural_gas/natural_gas_cc_ct.py +++ b/h2integrate/converters/natural_gas/natural_gas_cc_ct.py @@ -164,8 +164,8 @@ def compute(self, inputs, outputs): outputs["total_electricity_produced"] = np.sum(electricity_out) * (self.dt / 3600) outputs["capacity_factor"] = outputs["total_electricity_produced"].sum() / max_production - outputs["annual_electricity_produced"] = ( - outputs["total_electricity_produced"] * self.fraction_of_year_simulated + outputs["annual_electricity_produced"] = outputs["total_electricity_produced"] * ( + 1 / self.fraction_of_year_simulated ) diff --git a/h2integrate/converters/nitrogen/simple_ASU.py b/h2integrate/converters/nitrogen/simple_ASU.py index 483cb15e1..799dc5197 100644 --- a/h2integrate/converters/nitrogen/simple_ASU.py +++ b/h2integrate/converters/nitrogen/simple_ASU.py @@ -199,8 +199,8 @@ def compute(self, inputs, outputs): # annual N2 production in kg-N2/year outputs["total_nitrogen_produced"] = sum(n2_profile_out_kg) # maximum annual N2 production in kg-N2/year - outputs["annual_nitrogen_produced"] = ( - outputs["total_nitrogen_produced"] * self.fraction_of_year_simulated + outputs["annual_nitrogen_produced"] = outputs["total_nitrogen_produced"] * ( + 1 / self.fraction_of_year_simulated ) # annual electricity consumption in kWh/year outputs["annual_electricity_consumption"] = sum(electricity_kWh) diff --git a/h2integrate/converters/steel/steel.py b/h2integrate/converters/steel/steel.py index 2fa7e2a3b..77abc9d71 100644 --- a/h2integrate/converters/steel/steel.py +++ b/h2integrate/converters/steel/steel.py @@ -33,8 +33,8 @@ def compute(self, inputs, outputs): outputs["rated_steel_production"] = self.config.plant_capacity_mtpy / 8760 outputs["capacity_factor"] = self.config.capacity_factor outputs["total_steel_produced"] = outputs["steel_out"].sum() - outputs["annual_steel_produced"] = ( - outputs["total_steel_produced"] * self.fraction_of_year_simulated + outputs["annual_steel_produced"] = outputs["total_steel_produced"] * ( + 1 / self.fraction_of_year_simulated ) diff --git a/h2integrate/converters/steel/steel_eaf_base.py b/h2integrate/converters/steel/steel_eaf_base.py index 1a602aec7..4743ee546 100644 --- a/h2integrate/converters/steel/steel_eaf_base.py +++ b/h2integrate/converters/steel/steel_eaf_base.py @@ -254,8 +254,8 @@ def compute(self, inputs, outputs): outputs["steel_out"] = steel_production outputs["rated_steel_production"] = inputs["system_capacity"] outputs["total_steel_produced"] = outputs["steel_out"].sum() - outputs["annual_steel_produced"] = ( - outputs["total_steel_produced"] * self.fraction_of_year_simulated + outputs["annual_steel_produced"] = outputs["total_steel_produced"] * ( + 1 / self.fraction_of_year_simulated ) outputs["capacity_factor"] = outputs["total_steel_produced"] / ( outputs["rated_steel_production"] * len(outputs["steel_out"]) diff --git a/h2integrate/converters/water/desal/desalination.py b/h2integrate/converters/water/desal/desalination.py index cc812670f..a6e1acc21 100644 --- a/h2integrate/converters/water/desal/desalination.py +++ b/h2integrate/converters/water/desal/desalination.py @@ -100,8 +100,8 @@ def compute(self, inputs, outputs): outputs["capacity_factor"] = outputs["total_water_produced"] / ( outputs["rated_water_production"] * len(outputs["water_out"]) ) - outputs["annual_water_produced"] = ( - outputs["total_water_produced"] * self.fraction_of_year_simulated + outputs["annual_water_produced"] = outputs["total_water_produced"] * ( + 1 / self.fraction_of_year_simulated ) outputs["electricity_in"] = desal_power diff --git a/h2integrate/converters/water_power/hydro_plant_run_of_river.py b/h2integrate/converters/water_power/hydro_plant_run_of_river.py index 655927928..be2103e63 100644 --- a/h2integrate/converters/water_power/hydro_plant_run_of_river.py +++ b/h2integrate/converters/water_power/hydro_plant_run_of_river.py @@ -69,8 +69,8 @@ def compute(self, inputs, outputs): outputs["total_electricity_produced"] = outputs["electricity_out"].sum() * (self.dt / 3600) # Estimate annual electricity production - outputs["annual_electricity_produced"] = ( - outputs["total_electricity_produced"] * self.fraction_of_year_simulated + outputs["annual_electricity_produced"] = outputs["total_electricity_produced"] * ( + 1 / self.fraction_of_year_simulated ) # Calculate capacity factor diff --git a/h2integrate/converters/wind/floris.py b/h2integrate/converters/wind/floris.py index e75fb8fb3..73a276fab 100644 --- a/h2integrate/converters/wind/floris.py +++ b/h2integrate/converters/wind/floris.py @@ -276,8 +276,8 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): outputs["total_electricity_produced"] = np.sum(gen) * (self.dt / 3600) outputs["capacity_factor"] = outputs["total_electricity_produced"].sum() / max_production # NOTE: below is not flexible - outputs["annual_electricity_produced"] = ( - outputs["total_electricity_produced"] * self.fraction_of_year_simulated + outputs["annual_electricity_produced"] = outputs["total_electricity_produced"] * ( + 1 / self.fraction_of_year_simulated ) # 3. Cache the results for future use if enabled diff --git a/h2integrate/storage/battery/pysam_battery.py b/h2integrate/storage/battery/pysam_battery.py index 9f3b64317..ed993e365 100644 --- a/h2integrate/storage/battery/pysam_battery.py +++ b/h2integrate/storage/battery/pysam_battery.py @@ -430,8 +430,8 @@ def compute(self, inputs, outputs, discrete_inputs=[], discrete_outputs=[]): outputs["rated_electricity_production"] = inputs["max_charge_rate"] outputs["total_electricity_produced"] = np.sum(total_power_out) - outputs["annual_electricity_produced"] = ( - outputs["total_electricity_produced"] * self.fraction_of_year_simulated + outputs["annual_electricity_produced"] = outputs["total_electricity_produced"] * ( + 1 / self.fraction_of_year_simulated ) outputs["capacity_factor"] = outputs["total_electricity_produced"] / ( outputs["rated_electricity_production"] * self.n_timesteps From 632d02803ff8a3a018a6d350e1b56197de981d94 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Fri, 30 Jan 2026 11:15:41 -0700 Subject: [PATCH 52/63] moved commodity defn to initialize --- .../converters/ammonia/ammonia_synloop.py | 20 ++++++++++--------- .../ammonia/simple_ammonia_model.py | 8 +++----- .../marine/marine_carbon_capture_baseclass.py | 11 ++++++++-- h2integrate/converters/grid/grid.py | 8 +++----- h2integrate/converters/hopp/hopp_wrapper.py | 4 +++- .../hydrogen/electrolyzer_baseclass.py | 4 +++- .../geologic/h2_well_subsurface_baseclass.py | 5 ++++- .../geologic/h2_well_surface_baseclass.py | 4 +++- h2integrate/converters/iron/iron_dri_base.py | 8 +++----- .../converters/iron/martin_mine_perf_model.py | 5 ++++- .../converters/methanol/methanol_baseclass.py | 8 +++----- .../natural_gas/natural_gas_cc_ct.py | 8 +++----- h2integrate/converters/nitrogen/simple_ASU.py | 8 +++----- .../converters/solar/solar_baseclass.py | 5 ++++- .../converters/steel/steel_baseclass.py | 4 +++- .../converters/steel/steel_eaf_base.py | 5 ++++- .../water/desal/desalination_baseclass.py | 5 ++++- .../water_power/hydro_plant_run_of_river.py | 4 +++- .../converters/wind/wind_plant_baseclass.py | 5 ++++- .../storage/battery/battery_baseclass.py | 5 ++++- 20 files changed, 81 insertions(+), 53 deletions(-) diff --git a/h2integrate/converters/ammonia/ammonia_synloop.py b/h2integrate/converters/ammonia/ammonia_synloop.py index 11978ad85..fffa63636 100644 --- a/h2integrate/converters/ammonia/ammonia_synloop.py +++ b/h2integrate/converters/ammonia/ammonia_synloop.py @@ -139,24 +139,26 @@ class AmmoniaSynLoopPerformanceModel(ResizeablePerformanceModelBaseClass): conversion efficiency up to the limiting reagent or energy input. """ - def setup(self): + def initialize(self): + super().initialize() self.commodity = "ammonia" self.commodity_rate_units = "kg/h" self.commodity_amount_units = "kg" - n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] + + def setup(self): self.config = AmmoniaSynLoopPerformanceConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance") ) super().setup() - self.add_input("hydrogen_in", val=0.0, shape=n_timesteps, units="kg/h") - self.add_input("nitrogen_in", val=0.0, shape=n_timesteps, units="kg/h") - self.add_input("electricity_in", val=0.0, shape=n_timesteps, units="MW") + self.add_input("hydrogen_in", val=0.0, shape=self.n_timesteps, units="kg/h") + self.add_input("nitrogen_in", val=0.0, shape=self.n_timesteps, units="kg/h") + self.add_input("electricity_in", val=0.0, shape=self.n_timesteps, units="MW") - self.add_output("nitrogen_out", val=0.0, shape=n_timesteps, units="kg/h") - self.add_output("hydrogen_out", val=0.0, shape=n_timesteps, units="kg/h") - self.add_output("electricity_out", val=0.0, shape=n_timesteps, units="MW") - self.add_output("heat_out", val=0.0, shape=n_timesteps, units="kW*h/kg") + self.add_output("nitrogen_out", val=0.0, shape=self.n_timesteps, units="kg/h") + self.add_output("hydrogen_out", val=0.0, shape=self.n_timesteps, units="kg/h") + self.add_output("electricity_out", val=0.0, shape=self.n_timesteps, units="MW") + self.add_output("heat_out", val=0.0, shape=self.n_timesteps, units="kW*h/kg") self.add_output("catalyst_mass", val=0.0, units="kg") self.add_output("total_hydrogen_consumed", val=0.0, units="kg/year") diff --git a/h2integrate/converters/ammonia/simple_ammonia_model.py b/h2integrate/converters/ammonia/simple_ammonia_model.py index 4ebac8a9d..e618d903f 100644 --- a/h2integrate/converters/ammonia/simple_ammonia_model.py +++ b/h2integrate/converters/ammonia/simple_ammonia_model.py @@ -31,14 +31,12 @@ class SimpleAmmoniaPerformanceModel(PerformanceModelBaseClass): """ def initialize(self): - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - self.options.declare("driver_config", types=dict) - - def setup(self): + super().initialize() self.commodity = "ammonia" self.commodity_rate_units = "kg/h" self.commodity_amount_units = "kg" + + def setup(self): super().setup() n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] self.config = AmmoniaPerformanceModelConfig.from_dict( diff --git a/h2integrate/converters/co2/marine/marine_carbon_capture_baseclass.py b/h2integrate/converters/co2/marine/marine_carbon_capture_baseclass.py index 8e56b1623..805ced48f 100644 --- a/h2integrate/converters/co2/marine/marine_carbon_capture_baseclass.py +++ b/h2integrate/converters/co2/marine/marine_carbon_capture_baseclass.py @@ -32,14 +32,21 @@ class MarineCarbonCapturePerformanceBaseClass(PerformanceModelBaseClass): tech_config (dict): Configuration dictionary for technology-specific parameters. """ - def setup(self): + def initialize(self): + super().initialize() self.commodity = "co2" self.commodity_rate_units = "kg/h" self.commodity_amount_units = "kg" + + def setup(self): super().setup() self.add_input( - "electricity_in", val=0.0, shape=8760, units="W", desc="Hourly input electricity (W)" + "electricity_in", + val=0.0, + shape=self.n_timesteps, + units="W", + desc="Hourly input electricity (W)", ) # TODO: remove this output once finance models are updated diff --git a/h2integrate/converters/grid/grid.py b/h2integrate/converters/grid/grid.py index 9fbdfcbad..2e14a3427 100644 --- a/h2integrate/converters/grid/grid.py +++ b/h2integrate/converters/grid/grid.py @@ -46,14 +46,12 @@ class GridPerformanceModel(PerformanceModelBaseClass): """ def initialize(self): - self.options.declare("driver_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - - def setup(self): + super().initialize() self.commodity = "electricity" self.commodity_rate_units = "kW" self.commodity_amount_units = "kW*h" + + def setup(self): super().setup() self.config = GridPerformanceModelConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance") diff --git a/h2integrate/converters/hopp/hopp_wrapper.py b/h2integrate/converters/hopp/hopp_wrapper.py index 7a944a998..f33f10dbc 100644 --- a/h2integrate/converters/hopp/hopp_wrapper.py +++ b/h2integrate/converters/hopp/hopp_wrapper.py @@ -28,11 +28,13 @@ class HOPPComponent(PerformanceModelBaseClass, CacheBaseClass): computed results when the same configuration is encountered. """ - def setup(self): + def initialize(self): + super().initialize() self.commodity = "electricity" self.commodity_rate_units = "kW" self.commodity_amount_units = "kW*h" + def setup(self): self.config = HOPPComponentModelConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), strict=False, diff --git a/h2integrate/converters/hydrogen/electrolyzer_baseclass.py b/h2integrate/converters/hydrogen/electrolyzer_baseclass.py index 1eeeeb6cd..39112d648 100644 --- a/h2integrate/converters/hydrogen/electrolyzer_baseclass.py +++ b/h2integrate/converters/hydrogen/electrolyzer_baseclass.py @@ -5,11 +5,13 @@ class ElectrolyzerPerformanceBaseClass(ResizeablePerformanceModelBaseClass): - def setup(self): + def initialize(self): + super().initialize() self.commodity = "hydrogen" self.commodity_rate_units = "kg/h" self.commodity_amount_units = "kg" + def setup(self): super().setup() # Define inputs for electricity diff --git a/h2integrate/converters/hydrogen/geologic/h2_well_subsurface_baseclass.py b/h2integrate/converters/hydrogen/geologic/h2_well_subsurface_baseclass.py index 86f7b72a2..f22dd3010 100644 --- a/h2integrate/converters/hydrogen/geologic/h2_well_subsurface_baseclass.py +++ b/h2integrate/converters/hydrogen/geologic/h2_well_subsurface_baseclass.py @@ -86,10 +86,13 @@ class GeoH2SubsurfacePerformanceBaseClass(PerformanceModelBaseClass): The total hydrogen produced over the plant lifetime, in kilograms per year. """ - def setup(self): + def initialize(self): + super().initialize() self.commodity = "hydrogen" self.commodity_rate_units = "kg/h" self.commodity_amount_units = "kg" + + def setup(self): super().setup() # inputs diff --git a/h2integrate/converters/hydrogen/geologic/h2_well_surface_baseclass.py b/h2integrate/converters/hydrogen/geologic/h2_well_surface_baseclass.py index ff7cbf90c..3fca88df0 100644 --- a/h2integrate/converters/hydrogen/geologic/h2_well_surface_baseclass.py +++ b/h2integrate/converters/hydrogen/geologic/h2_well_surface_baseclass.py @@ -66,11 +66,13 @@ class GeoH2SurfacePerformanceBaseClass(PerformanceModelBaseClass): The wellhead gas flow in kg/hour used for sizing the system - passed to the cost model. """ - def setup(self): + def initialize(self): + super().initialize() self.commodity = "hydrogen" self.commodity_rate_units = "kg/h" self.commodity_amount_units = "kg" + def setup(self): super().setup() n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] # inputs diff --git a/h2integrate/converters/iron/iron_dri_base.py b/h2integrate/converters/iron/iron_dri_base.py index fd5da6863..e09c2e564 100644 --- a/h2integrate/converters/iron/iron_dri_base.py +++ b/h2integrate/converters/iron/iron_dri_base.py @@ -31,14 +31,12 @@ class IronReductionPerformanceBaseConfig(BaseConfig): class IronReductionPlantBasePerformanceComponent(PerformanceModelBaseClass): def initialize(self): - self.options.declare("driver_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - - def setup(self): + super().initialize() self.commodity = "pig_iron" self.commodity_rate_units = "t/h" self.commodity_amount_units = "t" + + def setup(self): super().setup() n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] diff --git a/h2integrate/converters/iron/martin_mine_perf_model.py b/h2integrate/converters/iron/martin_mine_perf_model.py index 5423a8374..e12bc97eb 100644 --- a/h2integrate/converters/iron/martin_mine_perf_model.py +++ b/h2integrate/converters/iron/martin_mine_perf_model.py @@ -31,10 +31,13 @@ class MartinIronMinePerformanceConfig(BaseConfig): class MartinIronMinePerformanceComponent(PerformanceModelBaseClass): - def setup(self): + def initialize(self): + super().initialize() self.commodity = "iron_ore" self.commodity_rate_units = "t/h" self.commodity_amount_units = "t" + + def setup(self): super().setup() n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] self.config = MartinIronMinePerformanceConfig.from_dict( diff --git a/h2integrate/converters/methanol/methanol_baseclass.py b/h2integrate/converters/methanol/methanol_baseclass.py index 4ef55c97b..ad0babc2a 100644 --- a/h2integrate/converters/methanol/methanol_baseclass.py +++ b/h2integrate/converters/methanol/methanol_baseclass.py @@ -35,14 +35,12 @@ class MethanolPerformanceBaseClass(PerformanceModelBaseClass): """ def initialize(self): - self.options.declare("driver_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - - def setup(self): + super().initialize() self.commodity = "methanol" self.commodity_rate_units = "kg/h" self.commodity_amount_units = "kg" + + def setup(self): super().setup() self.add_input("plant_capacity_kgpy", units="kg/year", val=self.config.plant_capacity_kgpy) diff --git a/h2integrate/converters/natural_gas/natural_gas_cc_ct.py b/h2integrate/converters/natural_gas/natural_gas_cc_ct.py index 587ee5ce3..0663b0d16 100644 --- a/h2integrate/converters/natural_gas/natural_gas_cc_ct.py +++ b/h2integrate/converters/natural_gas/natural_gas_cc_ct.py @@ -57,14 +57,12 @@ class NaturalGasPerformanceModel(PerformanceModelBaseClass): """ def initialize(self): - self.options.declare("driver_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - - def setup(self): + super().initialize() self.commodity = "electricity" self.commodity_rate_units = "MW" self.commodity_amount_units = "MW*h" + + def setup(self): super().setup() self.config = NaturalGasPerformanceConfig.from_dict( diff --git a/h2integrate/converters/nitrogen/simple_ASU.py b/h2integrate/converters/nitrogen/simple_ASU.py index 799dc5197..66da17779 100644 --- a/h2integrate/converters/nitrogen/simple_ASU.py +++ b/h2integrate/converters/nitrogen/simple_ASU.py @@ -65,14 +65,12 @@ class SimpleASUPerformanceModel(PerformanceModelBaseClass): """ def initialize(self): - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - self.options.declare("driver_config", types=dict) - - def setup(self): + super().initialize() self.commodity = "nitrogen" self.commodity_amount_units = "kg" self.commodity_rate_units = "kg/h" + + def setup(self): super().setup() n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] diff --git a/h2integrate/converters/solar/solar_baseclass.py b/h2integrate/converters/solar/solar_baseclass.py index f491c2e6e..c27ebb322 100644 --- a/h2integrate/converters/solar/solar_baseclass.py +++ b/h2integrate/converters/solar/solar_baseclass.py @@ -2,10 +2,13 @@ class SolarPerformanceBaseClass(PerformanceModelBaseClass): - def setup(self): + def initialize(self): + super().initialize() self.commodity = "electricity" self.commodity_rate_units = "kW" self.commodity_amount_units = "kW*h" + + def setup(self): super().setup() self.add_discrete_input( diff --git a/h2integrate/converters/steel/steel_baseclass.py b/h2integrate/converters/steel/steel_baseclass.py index 2d930b6e7..16c11c032 100644 --- a/h2integrate/converters/steel/steel_baseclass.py +++ b/h2integrate/converters/steel/steel_baseclass.py @@ -2,11 +2,13 @@ class SteelPerformanceBaseClass(PerformanceModelBaseClass): - def setup(self): + def initialize(self): + super().initialize() self.commodity = "steel" self.commodity_amount_units = "t" self.commodity_rate_units = "t/h" + def setup(self): super().setup() n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] # NOTE: the SteelPerformanceModel does not use electricity or hydrogen in its calc diff --git a/h2integrate/converters/steel/steel_eaf_base.py b/h2integrate/converters/steel/steel_eaf_base.py index 4743ee546..94670cec7 100644 --- a/h2integrate/converters/steel/steel_eaf_base.py +++ b/h2integrate/converters/steel/steel_eaf_base.py @@ -30,10 +30,13 @@ class ElectricArcFurnacePerformanceBaseConfig(BaseConfig): class ElectricArcFurnacePlantBasePerformanceComponent(PerformanceModelBaseClass): - def setup(self): + def initialize(self): + super().initialize() self.commodity = "steel" self.commodity_rate_units = "t/h" self.commodity_amount_units = "t" + + def setup(self): super().setup() n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] diff --git a/h2integrate/converters/water/desal/desalination_baseclass.py b/h2integrate/converters/water/desal/desalination_baseclass.py index 4039491a3..55fd58c4c 100644 --- a/h2integrate/converters/water/desal/desalination_baseclass.py +++ b/h2integrate/converters/water/desal/desalination_baseclass.py @@ -2,10 +2,13 @@ class DesalinationPerformanceBaseClass(PerformanceModelBaseClass): - def setup(self): + def initialize(self): + super().initialize() self.commodity = "water" self.commodity_amount_units = "m**3" self.commodity_rate_units = "m**3/h" + + def setup(self): super().setup() self.add_output("mass", val=0.0, units="kg", desc="Mass of desalination system") diff --git a/h2integrate/converters/water_power/hydro_plant_run_of_river.py b/h2integrate/converters/water_power/hydro_plant_run_of_river.py index be2103e63..7da19094b 100644 --- a/h2integrate/converters/water_power/hydro_plant_run_of_river.py +++ b/h2integrate/converters/water_power/hydro_plant_run_of_river.py @@ -36,11 +36,13 @@ class RunOfRiverHydroPerformanceModel(PerformanceModelBaseClass): Computes annual electricity production based on water flow rate and turbine efficiency. """ - def setup(self): + def initialize(self): + super().initialize() self.commodity = "electricity" self.commodity_rate_units = "kW" self.commodity_amount_units = "kW*h" + def setup(self): super().setup() n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] self.config = RunOfRiverHydroPerformanceConfig.from_dict( diff --git a/h2integrate/converters/wind/wind_plant_baseclass.py b/h2integrate/converters/wind/wind_plant_baseclass.py index 9ee56e200..0355996fb 100644 --- a/h2integrate/converters/wind/wind_plant_baseclass.py +++ b/h2integrate/converters/wind/wind_plant_baseclass.py @@ -2,10 +2,13 @@ class WindPerformanceBaseClass(PerformanceModelBaseClass): - def setup(self): + def initialize(self): + super().initialize() self.commodity = "electricity" self.commodity_rate_units = "kW" self.commodity_amount_units = "kW*h" + + def setup(self): super().setup() self.add_discrete_input( diff --git a/h2integrate/storage/battery/battery_baseclass.py b/h2integrate/storage/battery/battery_baseclass.py index 8163cd71b..fffdcfdff 100644 --- a/h2integrate/storage/battery/battery_baseclass.py +++ b/h2integrate/storage/battery/battery_baseclass.py @@ -2,10 +2,13 @@ class BatteryPerformanceBaseClass(PerformanceModelBaseClass): - def setup(self): + def initialize(self): + super().initialize() self.commodity = "electricity" self.commodity_rate_units = "kW" self.commodity_amount_units = "kW*h" + + def setup(self): super().setup() self.add_input( From 6c9e6ba9fc28309ee2ef6eb3459fa4159974c72e Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Fri, 30 Jan 2026 13:39:15 -0700 Subject: [PATCH 53/63] updates based on reviewer feedback --- h2integrate/converters/hydrogen/pem_electrolyzer.py | 4 ++++ .../hydrogen/test/test_basic_cost_model.py | 12 ++++++------ .../hydrogen/test/test_singlitico_cost_model.py | 2 +- h2integrate/finances/profast_base.py | 1 - 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/h2integrate/converters/hydrogen/pem_electrolyzer.py b/h2integrate/converters/hydrogen/pem_electrolyzer.py index 36f20e59b..1cc2aba1e 100644 --- a/h2integrate/converters/hydrogen/pem_electrolyzer.py +++ b/h2integrate/converters/hydrogen/pem_electrolyzer.py @@ -162,6 +162,10 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): refurb_period = round(float(H2_Results["Time Until Replacement [hrs]"]) / (24 * 365)) refurb_schedule[refurb_period : self.plant_life : refurb_period] = 1 + # The replacement_schedule is the fraction of the total capacity that is replaced per year + # The replacement_schedule may be used in the finance model if the replacement_cost_percent + # is specified in the tech_config under + # ['model_inputs']['finance_parameters']['capital_items']['replacement_cost_percent'] outputs["replacement_schedule"] = refurb_schedule # NOTE: could replace above with line with below: # outputs["replacement_schedule"] = (H2_Results["Performance Schedules"] diff --git a/h2integrate/converters/hydrogen/test/test_basic_cost_model.py b/h2integrate/converters/hydrogen/test/test_basic_cost_model.py index 7bfbb6a83..d4a1f74b6 100644 --- a/h2integrate/converters/hydrogen/test/test_basic_cost_model.py +++ b/h2integrate/converters/hydrogen/test/test_basic_cost_model.py @@ -59,7 +59,7 @@ def test_on_turbine_capex(self): self.per_turb_electrolyzer_size_mw, self.per_turb_electrical_generation_timeseries, ) - prob.set_val("total_hydrogen_produced", self.per_turb_h2_annual_output, units="kg") + prob.run_model() per_turb_electrolyzer_total_capital_cost = prob["CapEx"] @@ -71,7 +71,7 @@ def test_on_platform_capex(self): prob = self._create_problem( "offshore", self.electrolyzer_size_mw, self.electrical_generation_timeseries ) - prob.set_val("total_hydrogen_produced", self.h2_annual_output, units="kg") + prob.run_model() electrolyzer_total_capital_cost = prob["CapEx"] @@ -84,7 +84,7 @@ def test_on_land_capex(self): self.per_turb_electrolyzer_size_mw, self.per_turb_electrical_generation_timeseries, ) - prob.set_val("total_hydrogen_produced", self.per_turb_h2_annual_output, units="kg") + prob.run_model() per_turb_electrolyzer_total_capital_cost = prob["CapEx"] @@ -98,7 +98,7 @@ def test_on_turbine_opex(self): self.per_turb_electrolyzer_size_mw, self.per_turb_electrical_generation_timeseries, ) - prob.set_val("total_hydrogen_produced", self.per_turb_h2_annual_output, units="kg") + prob.run_model() per_turb_electrolyzer_OM_cost = prob["OpEx"] @@ -110,7 +110,7 @@ def test_on_platform_opex(self): prob = self._create_problem( "offshore", self.electrolyzer_size_mw, self.electrical_generation_timeseries ) - prob.set_val("total_hydrogen_produced", self.h2_annual_output, units="kg") + prob.run_model() electrolyzer_OM_cost = prob["OpEx"] @@ -123,7 +123,7 @@ def test_on_land_opex(self): self.per_turb_electrolyzer_size_mw, self.per_turb_electrical_generation_timeseries, ) - prob.set_val("total_hydrogen_produced", self.per_turb_h2_annual_output, units="kg") + prob.run_model() per_turb_electrolyzer_OM_cost = prob["OpEx"] diff --git a/h2integrate/converters/hydrogen/test/test_singlitico_cost_model.py b/h2integrate/converters/hydrogen/test/test_singlitico_cost_model.py index dd283b6bf..27d57f31f 100644 --- a/h2integrate/converters/hydrogen/test/test_singlitico_cost_model.py +++ b/h2integrate/converters/hydrogen/test/test_singlitico_cost_model.py @@ -53,7 +53,7 @@ def _create_problem(self, location): prob.setup() prob.set_val("electrolyzer_size_mw", self.P_elec_mw, units="MW") prob.set_val("electricity_in", np.ones(8760) * self.P_elec_mw, units="kW") - prob.set_val("total_hydrogen_produced", 1000.0, units="kg") + return prob def test_calc_capex_onshore(self): diff --git a/h2integrate/finances/profast_base.py b/h2integrate/finances/profast_base.py index df29f5abb..4f1efa1e0 100644 --- a/h2integrate/finances/profast_base.py +++ b/h2integrate/finances/profast_base.py @@ -532,7 +532,6 @@ def setup(self): val=-1.0, units=commodity_units, shape=plant_life, - # shape_by_conn=True, require_connection=True, ) From ed4988c6f82cd25b993d85259cfa38be5e610772 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:29:25 -0700 Subject: [PATCH 54/63] updated generic storage models and simple controllers --- .../converters/demand_openloop_controller.py | 2 +- .../flexible_demand_openloop_controller.py | 2 +- .../demand_openloop_controller.py | 2 +- .../passthrough_openloop_controller.py | 8 +- .../storage/demand_openloop_controller.py | 4 +- .../control/test/test_openloop_controllers.py | 14 +-- h2integrate/storage/simple_generic_storage.py | 41 +++++--- .../storage/simple_storage_auto_sizing.py | 95 ++++++++++++++----- 8 files changed, 119 insertions(+), 49 deletions(-) diff --git a/h2integrate/control/control_strategies/converters/demand_openloop_controller.py b/h2integrate/control/control_strategies/converters/demand_openloop_controller.py index b9125e7ec..faa67d4a3 100644 --- a/h2integrate/control/control_strategies/converters/demand_openloop_controller.py +++ b/h2integrate/control/control_strategies/converters/demand_openloop_controller.py @@ -70,6 +70,6 @@ def compute(self, inputs, outputs): ) # Calculate actual output based on demand met and curtailment - outputs[f"{commodity}_out"] = ( + outputs[f"{commodity}_set_point"] = ( inputs[f"{commodity}_in"] - outputs[f"{commodity}_unused_commodity"] ) diff --git a/h2integrate/control/control_strategies/converters/flexible_demand_openloop_controller.py b/h2integrate/control/control_strategies/converters/flexible_demand_openloop_controller.py index 398c89f85..43d7923a5 100644 --- a/h2integrate/control/control_strategies/converters/flexible_demand_openloop_controller.py +++ b/h2integrate/control/control_strategies/converters/flexible_demand_openloop_controller.py @@ -297,6 +297,6 @@ def compute(self, inputs, outputs): ) # Calculate actual output based on demand met and curtailment - outputs[f"{commodity}_out"] = ( + outputs[f"{commodity}_set_point"] = ( inputs[f"{commodity}_in"] - outputs[f"{commodity}_unused_commodity"] ) diff --git a/h2integrate/control/control_strategies/demand_openloop_controller.py b/h2integrate/control/control_strategies/demand_openloop_controller.py index f0724d922..bbab59687 100644 --- a/h2integrate/control/control_strategies/demand_openloop_controller.py +++ b/h2integrate/control/control_strategies/demand_openloop_controller.py @@ -97,7 +97,7 @@ def setup(self): ) self.add_output( - f"{commodity}_out", + f"{commodity}_set_point", val=0.0, shape=(n_timesteps), units=self.config.commodity_units, diff --git a/h2integrate/control/control_strategies/passthrough_openloop_controller.py b/h2integrate/control/control_strategies/passthrough_openloop_controller.py index 9f3261449..9dafa6082 100644 --- a/h2integrate/control/control_strategies/passthrough_openloop_controller.py +++ b/h2integrate/control/control_strategies/passthrough_openloop_controller.py @@ -35,7 +35,7 @@ def setup(self): ) self.add_output( - f"{self.config.commodity_name}_out", + f"{self.config.commodity_name}_set_point", copy_shape=f"{self.config.commodity_name}_in", units=self.config.commodity_units, desc=f"{self.config.commodity_name} output timeseries from plant after storage", @@ -53,7 +53,9 @@ def compute(self, inputs, outputs): """ # Assign the input to the output - outputs[f"{self.config.commodity_name}_out"] = inputs[f"{self.config.commodity_name}_in"] + outputs[f"{self.config.commodity_name}_set_point"] = inputs[ + f"{self.config.commodity_name}_in" + ] def setup_partials(self): """ @@ -74,7 +76,7 @@ def setup_partials(self): # Declare partials sparsely for all elements as an identity matrix # (diagonal elements are 1.0, others are 0.0) self.declare_partials( - of=f"{self.config.commodity_name}_out", + of=f"{self.config.commodity_name}_set_point", wrt=f"{self.config.commodity_name}_in", rows=np.arange(size), cols=np.arange(size), diff --git a/h2integrate/control/control_strategies/storage/demand_openloop_controller.py b/h2integrate/control/control_strategies/storage/demand_openloop_controller.py index 64cac74f0..a07853f0d 100644 --- a/h2integrate/control/control_strategies/storage/demand_openloop_controller.py +++ b/h2integrate/control/control_strategies/storage/demand_openloop_controller.py @@ -279,7 +279,7 @@ def compute(self, inputs, outputs): # initialize outputs soc_array = outputs[f"{commodity}_soc"] unused_commodity_array = outputs[f"{commodity}_unused_commodity"] - output_array = outputs[f"{commodity}_out"] + output_array = outputs[f"{commodity}_set_point"] unmet_demand_array = outputs[f"{commodity}_unmet_demand"] # Loop through each time step @@ -338,7 +338,7 @@ def compute(self, inputs, outputs): # Record the missed load at the current time step unmet_demand_array[t] = max(0.0, (demand_t - output_array[t])) - outputs[f"{commodity}_out"] = output_array + outputs[f"{commodity}_set_point"] = output_array # Return the SOC outputs[f"{commodity}_soc"] = soc_array diff --git a/h2integrate/control/test/test_openloop_controllers.py b/h2integrate/control/test/test_openloop_controllers.py index 8c06259e1..c50af9116 100644 --- a/h2integrate/control/test/test_openloop_controllers.py +++ b/h2integrate/control/test/test_openloop_controllers.py @@ -72,7 +72,7 @@ def test_pass_through_controller(subtests): # Run the test with subtests.test("Check output"): - assert pytest.approx(prob.get_val("hydrogen_out"), rel=1e-3) == np.arange(10) + assert pytest.approx(prob.get_val("hydrogen_set_point"), rel=1e-3) == np.arange(10) # Run the test with subtests.test("Check derivatives"): @@ -80,7 +80,7 @@ def test_pass_through_controller(subtests): assert_check_totals( prob.check_totals( of=[ - "hydrogen_out", + "hydrogen_set_point", ], wrt=[ "hydrogen_in", @@ -149,7 +149,7 @@ def test_storage_demand_controller(subtests): # Run the test with subtests.test("Check output"): assert pytest.approx([0.5, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]) == prob.get_val( - "hydrogen_out" + "hydrogen_set_point" ) with subtests.test("Check curtailment"): @@ -241,7 +241,9 @@ def set_up_and_run_problem(config): # Run the test with subtests.test("Check output"): - assert pytest.approx(prob_ioe.get_val("hydrogen_out")) == prob_rte.get_val("hydrogen_out") + assert pytest.approx(prob_ioe.get_val("hydrogen_set_point")) == prob_rte.get_val( + "hydrogen_set_point" + ) with subtests.test("Check curtailment"): assert pytest.approx(prob_ioe.get_val("hydrogen_unused_commodity")) == prob_rte.get_val( @@ -324,7 +326,7 @@ def test_generic_storage_demand_controller(subtests): # # Run the test with subtests.test("Check output"): assert pytest.approx([0.5, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]) == prob.get_val( - "hydrogen_out" + "hydrogen_set_point" ) with subtests.test("Check curtailment"): @@ -396,7 +398,7 @@ def test_demand_converter_controller(subtests): # # Run the test with subtests.test("Check output"): assert pytest.approx([0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 5.0, 5.0, 5.0, 5.0]) == prob.get_val( - "hydrogen_out" + "hydrogen_set_point" ) with subtests.test("Check curtailment"): diff --git a/h2integrate/storage/simple_generic_storage.py b/h2integrate/storage/simple_generic_storage.py index b378e5a9d..606c0f955 100644 --- a/h2integrate/storage/simple_generic_storage.py +++ b/h2integrate/storage/simple_generic_storage.py @@ -1,7 +1,7 @@ -import openmdao.api as om from attrs import field, define from h2integrate.core.utilities import BaseConfig, merge_shared_inputs +from h2integrate.core.model_baseclasses import PerformanceModelBaseClass @define(kw_only=True) @@ -10,26 +10,45 @@ class SimpleGenericStorageConfig(BaseConfig): commodity_units: str = field() # TODO: update to commodity_rate_units -class SimpleGenericStorage(om.ExplicitComponent): +class SimpleGenericStorage(PerformanceModelBaseClass): """ Simple generic storage model. """ - def initialize(self): - self.options.declare("tech_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("driver_config", types=dict) + # def initialize(self): + # self.options.declare("tech_config", types=dict) + # self.options.declare("plant_config", types=dict) + # self.options.declare("driver_config", types=dict) def setup(self): - n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] + # n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] self.config = SimpleGenericStorageConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), strict=False, additional_cls_name=self.__class__.__name__, ) - commodity_name = self.config.commodity_name - commodity_units = self.config.commodity_units - self.add_input(f"{commodity_name}_in", val=0.0, shape=n_timesteps, units=commodity_units) + self.commodity = self.config.commodity_name + self.commodity_rate_units = self.config.commodity_units + self.commodity_amount_units = f"({self.commodity_rate_units})*h" + super().setup() + self.add_input( + f"{self.commodity}_set_point", + val=0.0, + shape=self.n_timesteps, + units=self.commodity_rate_units, + ) def compute(self, inputs, outputs): - pass + outputs[f"{self.commodity}_out"] = inputs[f"{self.commodity}_set_point"] + + outputs[f"rated_{self.commodity}_production"] = inputs[f"{self.commodity}_set_point"].max() + outputs[f"total_{self.commodity}_produced"] = outputs[f"{self.commodity}_out"].sum() + outputs[f"annual_{self.commodity}_produced"] = outputs[ + f"total_{self.commodity}_produced" + ] * (1 / self.fraction_of_year_simulated) + + max_production = ( + inputs[f"{self.commodity}_set_point"].max() * self.n_timesteps * (self.dt / 3600) + ) + + outputs["capacity_factor"] = outputs[f"total_{self.commodity}_produced"] / max_production diff --git a/h2integrate/storage/simple_storage_auto_sizing.py b/h2integrate/storage/simple_storage_auto_sizing.py index 7d6fb32db..af89fe66a 100644 --- a/h2integrate/storage/simple_storage_auto_sizing.py +++ b/h2integrate/storage/simple_storage_auto_sizing.py @@ -1,8 +1,10 @@ +from copy import deepcopy + import numpy as np -import openmdao.api as om from attrs import field, define from h2integrate.core.utilities import BaseConfig, merge_shared_inputs +from h2integrate.core.model_baseclasses import PerformanceModelBaseClass @define(kw_only=True) @@ -22,7 +24,7 @@ class StorageSizingModelConfig(BaseConfig): demand_profile: int | float | list = field(default=0.0) -class StorageAutoSizingModel(om.ExplicitComponent): +class StorageAutoSizingModel(PerformanceModelBaseClass): """Performance model that calculates the storage charge rate and capacity needed to either: @@ -30,9 +32,9 @@ class StorageAutoSizingModel(om.ExplicitComponent): 2. try to meet the commodity demand with the given commodity production profile. Inputs: - {commodity_name}_in (float): Input commodity flow timeseries (e.g., hydrogen production). + self.commodity_in (float): Input commodity flow timeseries (e.g., hydrogen production). - Units: Defined in `commodity_units` (e.g., "kg/h"). - {commodity_name}_demand_profile (float): Demand profile of commodity. + self.commodity_demand_profile (float): Demand profile of commodity. - Units: Defined in `commodity_units` (e.g., "kg/h"). Outputs: @@ -42,10 +44,10 @@ class StorageAutoSizingModel(om.ExplicitComponent): - Units: Defined in `commodity_units` (e.g., "kg/h"). """ - def initialize(self): - self.options.declare("driver_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) + # def initialize(self): + # self.options.declare("driver_config", types=dict) + # self.options.declare("plant_config", types=dict) + # self.options.declare("tech_config", types=dict) def setup(self): self.config = StorageSizingModelConfig.from_dict( @@ -54,25 +56,32 @@ def setup(self): additional_cls_name=self.__class__.__name__, ) - super().setup() - - n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] + self.commodity = self.config.commodity_name + self.commodity_rate_units = self.config.commodity_units + self.commodity_amount_units = f"({self.commodity_rate_units})*h" - commodity_name = self.config.commodity_name + super().setup() self.add_input( - f"{commodity_name}_demand_profile", + f"{self.commodity}_demand_profile", units=f"{self.config.commodity_units}", val=self.config.demand_profile, - shape=n_timesteps, - desc=f"{commodity_name} demand profile timeseries", + shape=self.n_timesteps, + desc=f"{self.commodity} demand profile timeseries", + ) + + self.add_input( + f"{self.commodity}_in", + shape_by_conn=True, + units=f"{self.config.commodity_units}", + desc=f"{self.commodity} input timeseries from production to storage", ) self.add_input( - f"{commodity_name}_in", + f"{self.commodity}_set_point", shape_by_conn=True, units=f"{self.config.commodity_units}", - desc=f"{commodity_name} input timeseries from production to storage", + desc=f"{self.commodity} input timeseries from production to storage", ) self.add_output( @@ -90,18 +99,17 @@ def setup(self): ) def compute(self, inputs, outputs): - commodity_name = self.config.commodity_name - storage_max_fill_rate = np.max(inputs[f"{commodity_name}_in"]) + storage_max_fill_rate = np.max(inputs[f"{self.commodity}_in"]) ########### get storage size ########### - if np.sum(inputs[f"{commodity_name}_demand_profile"]) > 0: - commodity_demand = inputs[f"{commodity_name}_demand_profile"] + if np.sum(inputs[f"{self.commodity}_demand_profile"]) > 0: + commodity_demand = inputs[f"{self.commodity}_demand_profile"] else: - commodity_demand = np.mean( - inputs[f"{commodity_name}_in"] + commodity_demand = np.mean(inputs[f"{self.commodity}_in"]) * np.ones( + self.n_timesteps ) # TODO: update demand based on end-use needs - commodity_production = inputs[f"{commodity_name}_in"] + commodity_production = inputs[f"{self.commodity}_set_point"] # TODO: SOC is just an absolute value and is not a percentage. Ideally would calculate as shortfall in future. commodity_storage_soc = [] @@ -123,5 +131,44 @@ def compute(self, inputs, outputs): commodity_storage_soc ) + discharge_storage = np.zeros(self.n_timesteps) + charge_storage = np.zeros(self.n_timesteps) + soc = deepcopy(commodity_storage_soc[0]) + output_array = np.zeros(self.n_timesteps) + for t, demand_t in enumerate(commodity_demand): + input_flow = commodity_production[t] + available_charge = float(commodity_storage_capacity_kg - soc) + available_discharge = float(soc - commodity_storage_soc[t]) + + if demand_t > input_flow: + # Discharge storage to meet demand. + discharge_needed = demand_t - input_flow + discharge = min(discharge_needed, available_discharge, storage_max_fill_rate) + soc -= discharge + + discharge_storage[t] = discharge + output_array[t] = input_flow + discharge + + else: + # Charge storage with unused input + unused_input = input_flow - demand_t + charge = min(unused_input, available_charge, storage_max_fill_rate) + soc += charge + + charge_storage[t] = charge + output_array[t] = demand_t + outputs["max_charge_rate"] = storage_max_fill_rate outputs["max_capacity"] = commodity_storage_capacity_kg + + outputs[f"{self.commodity}_out"] = output_array + + outputs[f"rated_{self.commodity}_production"] = storage_max_fill_rate + outputs[f"total_{self.commodity}_produced"] = outputs[f"{self.commodity}_out"].sum() + outputs[f"annual_{self.commodity}_produced"] = outputs[ + f"total_{self.commodity}_produced" + ] * (1 / self.fraction_of_year_simulated) + + max_production = storage_max_fill_rate * self.n_timesteps * (self.dt / 3600) + + outputs["capacity_factor"] = outputs[f"total_{self.commodity}_produced"] / max_production From 0cca96d5a71717c44c2b2fe385d95494b5e4069d Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Mon, 2 Feb 2026 16:37:02 -0700 Subject: [PATCH 55/63] bugfix in simple_storage_autosizing --- h2integrate/storage/simple_storage_auto_sizing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/h2integrate/storage/simple_storage_auto_sizing.py b/h2integrate/storage/simple_storage_auto_sizing.py index af89fe66a..6934b3ccd 100644 --- a/h2integrate/storage/simple_storage_auto_sizing.py +++ b/h2integrate/storage/simple_storage_auto_sizing.py @@ -115,10 +115,10 @@ def compute(self, inputs, outputs): commodity_storage_soc = [] for j in range(len(commodity_production)): if j == 0: - commodity_storage_soc.append(commodity_production[j] - commodity_demand) + commodity_storage_soc.append(commodity_production[j] - commodity_demand[j]) else: commodity_storage_soc.append( - commodity_storage_soc[j - 1] + commodity_production[j] - commodity_demand + commodity_storage_soc[j - 1] + commodity_production[j] - commodity_demand[j] ) minimum_soc = np.min(commodity_storage_soc) @@ -138,7 +138,7 @@ def compute(self, inputs, outputs): for t, demand_t in enumerate(commodity_demand): input_flow = commodity_production[t] available_charge = float(commodity_storage_capacity_kg - soc) - available_discharge = float(soc - commodity_storage_soc[t]) + available_discharge = float(soc) if demand_t > input_flow: # Discharge storage to meet demand. From 5d0832aae92619bc83e22aceb86ca5a828cda9da Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 3 Feb 2026 10:03:06 -0700 Subject: [PATCH 56/63] updated test for post-processing timeseries --- h2integrate/postprocess/test/test_sql_timeseries_to_csv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/h2integrate/postprocess/test/test_sql_timeseries_to_csv.py b/h2integrate/postprocess/test/test_sql_timeseries_to_csv.py index e9c7a4893..1dd80538e 100644 --- a/h2integrate/postprocess/test/test_sql_timeseries_to_csv.py +++ b/h2integrate/postprocess/test/test_sql_timeseries_to_csv.py @@ -35,7 +35,7 @@ def test_save_csv_all_results(subtests, run_example_02_sql_fpath): res = save_case_timeseries_as_csv(run_example_02_sql_fpath, save_to_file=True) with subtests.test("Check number of columns"): - assert len(res.columns.to_list()) == 35 + assert len(res.columns.to_list()) == 36 with subtests.test("Check number of rows"): assert len(res) == 8760 From 0a83daf9b2061799965d5a823db0ee3b3457a699 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 3 Feb 2026 10:57:52 -0700 Subject: [PATCH 57/63] added subtest for ex 12 --- examples/test/test_all_examples.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/examples/test/test_all_examples.py b/examples/test/test_all_examples.py index 4a1388229..ef2a59090 100644 --- a/examples/test/test_all_examples.py +++ b/examples/test/test_all_examples.py @@ -258,7 +258,15 @@ def test_ammonia_synloop_example(subtests): assert pytest.approx(model.prob.get_val("ammonia.CapEx"), rel=1e-6) == 1.15173753e09 with subtests.test("Check ammonia OpEx"): - assert pytest.approx(model.prob.get_val("ammonia.OpEx"), rel=1e-4) == 25737370.661763854 + assert pytest.approx(model.prob.get_val("ammonia.OpEx")[0], rel=1e-4) == 25737370.661763854 + + with subtests.test("Check ammonia production"): + assert ( + pytest.approx( + model.prob.get_val("ammonia.annual_ammonia_produced", units="t/yr").mean(), rel=1e-4 + ) + == 406333.161 + ) with subtests.test("Check total adjusted CapEx"): assert ( From 35ab37d88ed8a8dd8cbee82a8a120fa22e7001ca Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 3 Feb 2026 11:14:31 -0700 Subject: [PATCH 58/63] updated ex 12 test values --- examples/test/test_all_examples.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/test/test_all_examples.py b/examples/test/test_all_examples.py index ef2a59090..80e0761ce 100644 --- a/examples/test/test_all_examples.py +++ b/examples/test/test_all_examples.py @@ -258,7 +258,7 @@ def test_ammonia_synloop_example(subtests): assert pytest.approx(model.prob.get_val("ammonia.CapEx"), rel=1e-6) == 1.15173753e09 with subtests.test("Check ammonia OpEx"): - assert pytest.approx(model.prob.get_val("ammonia.OpEx")[0], rel=1e-4) == 25737370.661763854 + assert pytest.approx(model.prob.get_val("ammonia.OpEx")[0], rel=1e-4) == 25414748.989416014 with subtests.test("Check ammonia production"): assert ( @@ -281,7 +281,7 @@ def test_ammonia_synloop_example(subtests): pytest.approx( model.prob.get_val("finance_subgroup_nh3.total_opex_adjusted")[0], rel=1e-6 ) - == 79744581.00552343 + == 79421959.33317558 ) with subtests.test("Check LCOH"): @@ -293,7 +293,7 @@ def test_ammonia_synloop_example(subtests): with subtests.test("Check LCOA"): assert ( pytest.approx(model.prob.get_val("finance_subgroup_nh3.LCOA")[0], rel=1e-6) - == 1.2310335361130984 + == 1.1022714567388747 ) From 6bfdb07598c1eccfd61d7ad3a4835e79ae16e75b Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 3 Feb 2026 12:05:39 -0700 Subject: [PATCH 59/63] removed commented out code --- h2integrate/storage/simple_generic_storage.py | 6 ------ h2integrate/storage/simple_storage_auto_sizing.py | 5 ----- 2 files changed, 11 deletions(-) diff --git a/h2integrate/storage/simple_generic_storage.py b/h2integrate/storage/simple_generic_storage.py index 606c0f955..38f3c1539 100644 --- a/h2integrate/storage/simple_generic_storage.py +++ b/h2integrate/storage/simple_generic_storage.py @@ -15,13 +15,7 @@ class SimpleGenericStorage(PerformanceModelBaseClass): Simple generic storage model. """ - # def initialize(self): - # self.options.declare("tech_config", types=dict) - # self.options.declare("plant_config", types=dict) - # self.options.declare("driver_config", types=dict) - def setup(self): - # n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] self.config = SimpleGenericStorageConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), strict=False, diff --git a/h2integrate/storage/simple_storage_auto_sizing.py b/h2integrate/storage/simple_storage_auto_sizing.py index 6934b3ccd..46ec16b00 100644 --- a/h2integrate/storage/simple_storage_auto_sizing.py +++ b/h2integrate/storage/simple_storage_auto_sizing.py @@ -44,11 +44,6 @@ class StorageAutoSizingModel(PerformanceModelBaseClass): - Units: Defined in `commodity_units` (e.g., "kg/h"). """ - # def initialize(self): - # self.options.declare("driver_config", types=dict) - # self.options.declare("plant_config", types=dict) - # self.options.declare("tech_config", types=dict) - def setup(self): self.config = StorageSizingModelConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), From 9976fcc4008ac6b2a8e2c86bae3748a41e5949fa Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 3 Feb 2026 12:06:39 -0700 Subject: [PATCH 60/63] updated changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f562fcf0..a2da635ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ enforced by a newly added test to ensure adherence. - Remove `pytest-subtests` as it's incorporated into pytest as of v9, and is an archived project. - Added `PerformanceModelBaseClass` and standardized outputs of converter performance models +- Updated storage control strategies to output `commodity_set_point` and generic storage performance models to output `commodity_out` and input `commodity_set_point`. ## 0.5.1 [December 18, 2025] From 2ac03d26f36a09c98b05fd86ac7ed92092193b59 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 3 Feb 2026 14:01:32 -0700 Subject: [PATCH 61/63] added some comments to generic storage performance models --- h2integrate/storage/simple_generic_storage.py | 2 +- .../storage/simple_storage_auto_sizing.py | 52 ++++++++++++++++--- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/h2integrate/storage/simple_generic_storage.py b/h2integrate/storage/simple_generic_storage.py index 38f3c1539..6fbf3100f 100644 --- a/h2integrate/storage/simple_generic_storage.py +++ b/h2integrate/storage/simple_generic_storage.py @@ -12,7 +12,7 @@ class SimpleGenericStorageConfig(BaseConfig): class SimpleGenericStorage(PerformanceModelBaseClass): """ - Simple generic storage model. + Simple generic storage model that acts as a pass-through component. """ def setup(self): diff --git a/h2integrate/storage/simple_storage_auto_sizing.py b/h2integrate/storage/simple_storage_auto_sizing.py index 46ec16b00..a8541eec9 100644 --- a/h2integrate/storage/simple_storage_auto_sizing.py +++ b/h2integrate/storage/simple_storage_auto_sizing.py @@ -31,10 +31,13 @@ class StorageAutoSizingModel(PerformanceModelBaseClass): 1. supply the comodity at a constant rate based on the commodity production profile or 2. try to meet the commodity demand with the given commodity production profile. + Then simulates performance of a basic storage component using the charge rate and + capacity calculated. + Inputs: - self.commodity_in (float): Input commodity flow timeseries (e.g., hydrogen production). + commodity_in (float): Input commodity flow timeseries (e.g., hydrogen production). - Units: Defined in `commodity_units` (e.g., "kg/h"). - self.commodity_demand_profile (float): Demand profile of commodity. + commodity_demand_profile (float): Demand profile of commodity. - Units: Defined in `commodity_units` (e.g., "kg/h"). Outputs: @@ -42,6 +45,13 @@ class StorageAutoSizingModel(PerformanceModelBaseClass): - Units: in non-rate units, e.g., "kg" if `commodity_units` is "kg/h" max_charge_rate (float): Maximum rate at which the commodity can be charged - Units: Defined in `commodity_units` (e.g., "kg/h"). + Assumed to also be the discharge rate. + commodity_out (np.ndarray): + total_commodity_produced (float): + rated_commodity_production (float): + annual_commodity_produced (np.ndarray): + capacity_factor (np.ndarray): + """ def setup(self): @@ -76,7 +86,7 @@ def setup(self): f"{self.commodity}_set_point", shape_by_conn=True, units=f"{self.config.commodity_units}", - desc=f"{self.commodity} input timeseries from production to storage", + desc=f"{self.commodity} input set point from controller", ) self.add_output( @@ -94,19 +104,26 @@ def setup(self): ) def compute(self, inputs, outputs): + # Step 1: Auto-size the storage to meet the demand + + # Auto-size the fill rate as the max of the input commodity storage_max_fill_rate = np.max(inputs[f"{self.commodity}_in"]) - ########### get storage size ########### + # Set the demand profile if np.sum(inputs[f"{self.commodity}_demand_profile"]) > 0: commodity_demand = inputs[f"{self.commodity}_demand_profile"] else: + # If the commodity_demand_profile is zero, use the average + # commodity_in as the demand commodity_demand = np.mean(inputs[f"{self.commodity}_in"]) * np.ones( self.n_timesteps ) # TODO: update demand based on end-use needs + # The commodity_set_point is the production set by the controller commodity_production = inputs[f"{self.commodity}_set_point"] # TODO: SOC is just an absolute value and is not a percentage. Ideally would calculate as shortfall in future. + # Size the storage capacity to meet the demand as much as possible commodity_storage_soc = [] for j in range(len(commodity_production)): if j == 0: @@ -118,52 +135,73 @@ def compute(self, inputs, outputs): minimum_soc = np.min(commodity_storage_soc) - # adjust soc so it's not negative. + # Adjust soc so it's not negative. if minimum_soc < 0: commodity_storage_soc = [x + np.abs(minimum_soc) for x in commodity_storage_soc] + # Calculate the maximum hydrogen storage capacity needed to meet the demand commodity_storage_capacity_kg = np.max(commodity_storage_soc) - np.min( commodity_storage_soc ) + # Step 2: Simulate the storage performance based on the sizes calculated + + # Initialize output arrays of charge and discharge discharge_storage = np.zeros(self.n_timesteps) charge_storage = np.zeros(self.n_timesteps) - soc = deepcopy(commodity_storage_soc[0]) output_array = np.zeros(self.n_timesteps) + + # Initialize state-of-charge value as the soc at t=0 + soc = deepcopy(commodity_storage_soc[0]) + + # Simulate a basic storage component for t, demand_t in enumerate(commodity_demand): input_flow = commodity_production[t] available_charge = float(commodity_storage_capacity_kg - soc) available_discharge = float(soc) + # If demand is greater than the input, discharge storage if demand_t > input_flow: # Discharge storage to meet demand. discharge_needed = demand_t - input_flow discharge = min(discharge_needed, available_discharge, storage_max_fill_rate) + # Update SOC soc -= discharge discharge_storage[t] = discharge output_array[t] = input_flow + discharge + # If input is greater than the demand, charge storage else: # Charge storage with unused input unused_input = input_flow - demand_t charge = min(unused_input, available_charge, storage_max_fill_rate) + # Update SOC soc += charge charge_storage[t] = charge output_array[t] = demand_t + # Output the storage sizes (charge rate and capacity) outputs["max_charge_rate"] = storage_max_fill_rate outputs["max_capacity"] = commodity_storage_capacity_kg + # commodity_out is the commodity_set_point - charge_storage + discharge_storage outputs[f"{self.commodity}_out"] = output_array + # The rated_commodity_production is based on the discharge rate + # (which is assumed equal to the charge rate) outputs[f"rated_{self.commodity}_production"] = storage_max_fill_rate - outputs[f"total_{self.commodity}_produced"] = outputs[f"{self.commodity}_out"].sum() + + # The total_commodity_produced is the sum of the commodity discharged from storage + outputs[f"total_{self.commodity}_produced"] = discharge_storage.sum() + # Adjust the total_commodity_produced to a year-long simulation outputs[f"annual_{self.commodity}_produced"] = outputs[ f"total_{self.commodity}_produced" ] * (1 / self.fraction_of_year_simulated) + # The maximum production is based on the charge/discharge rate max_production = storage_max_fill_rate * self.n_timesteps * (self.dt / 3600) + # Capacity factor is total discharged commodity / maximum discharged commodity possible outputs["capacity_factor"] = outputs[f"total_{self.commodity}_produced"] / max_production From 6221d3e1da047dc183d89b19968ad524421b3354 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 3 Feb 2026 14:49:32 -0700 Subject: [PATCH 62/63] added more inline and docstring comments --- h2integrate/storage/simple_generic_storage.py | 9 ++++++ .../storage/simple_storage_auto_sizing.py | 28 ++++++++++++++----- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/h2integrate/storage/simple_generic_storage.py b/h2integrate/storage/simple_generic_storage.py index 6fbf3100f..f67471200 100644 --- a/h2integrate/storage/simple_generic_storage.py +++ b/h2integrate/storage/simple_generic_storage.py @@ -13,6 +13,10 @@ class SimpleGenericStorageConfig(BaseConfig): class SimpleGenericStorage(PerformanceModelBaseClass): """ Simple generic storage model that acts as a pass-through component. + + Note: this storage performance model is intended to be used with the + `DemandOpenLoopStorageController` controller. + """ def setup(self): @@ -33,14 +37,19 @@ def setup(self): ) def compute(self, inputs, outputs): + # Pass the commodity_out as the commodity_set_point outputs[f"{self.commodity}_out"] = inputs[f"{self.commodity}_set_point"] + # Estimate the rated commodity production as the maximum value from the commodity_set_point outputs[f"rated_{self.commodity}_production"] = inputs[f"{self.commodity}_set_point"].max() + + # Calculate the total and annual commodity produced outputs[f"total_{self.commodity}_produced"] = outputs[f"{self.commodity}_out"].sum() outputs[f"annual_{self.commodity}_produced"] = outputs[ f"total_{self.commodity}_produced" ] * (1 / self.fraction_of_year_simulated) + # Calculate the maximum commodity production over the simulation max_production = ( inputs[f"{self.commodity}_set_point"].max() * self.n_timesteps * (self.dt / 3600) ) diff --git a/h2integrate/storage/simple_storage_auto_sizing.py b/h2integrate/storage/simple_storage_auto_sizing.py index a8541eec9..414090dcd 100644 --- a/h2integrate/storage/simple_storage_auto_sizing.py +++ b/h2integrate/storage/simple_storage_auto_sizing.py @@ -34,23 +34,37 @@ class StorageAutoSizingModel(PerformanceModelBaseClass): Then simulates performance of a basic storage component using the charge rate and capacity calculated. + Note: this storage performance model is intended to be used with the + `PassThroughOpenLoopController` controller and is not compatible with the + `DemandOpenLoopStorageController` controller. + Inputs: - commodity_in (float): Input commodity flow timeseries (e.g., hydrogen production). + {commodity}_in (float): Input commodity flow timeseries (e.g., hydrogen production) + used to estimate the demand if `commodity_demand_profile` is zero. - Units: Defined in `commodity_units` (e.g., "kg/h"). - commodity_demand_profile (float): Demand profile of commodity. + {commodity}_set_point (float): Input commodity flow timeseries (e.g., hydrogen production) + used as the available input commodity to meet the demand. + {commodity}_demand_profile (float): Demand profile of commodity. - Units: Defined in `commodity_units` (e.g., "kg/h"). + Outputs: max_capacity (float): Maximum storage capacity of the commodity. - Units: in non-rate units, e.g., "kg" if `commodity_units` is "kg/h" max_charge_rate (float): Maximum rate at which the commodity can be charged - Units: Defined in `commodity_units` (e.g., "kg/h"). Assumed to also be the discharge rate. - commodity_out (np.ndarray): - total_commodity_produced (float): - rated_commodity_production (float): - annual_commodity_produced (np.ndarray): - capacity_factor (np.ndarray): + {commodity}_out (np.ndarray): the commodity used to meet demand from the available + input commodity and storage component. Defined in `commodity_units`. + total_{commodity}_produced (float): sum of commodity discharged from storage over + the simulation. Defined in `commodity_units*h` + rated_{commodity}_production (float): maximum commodity that could be discharged + in a timestep. Defined in `commodity_units` + annual_{commodity}_produced (np.ndarray): total commodity discharged per year. + Defined in `commodity_units*h/year` + capacity_factor (np.ndarray): ratio of commodity discharged to the maximum + commodity that could be discharged over the simulation. + Defined as a ratio (units of `unitless`) """ From 4804f8b41f597214e846e2213d40c33e15206169 Mon Sep 17 00:00:00 2001 From: John Jasa Date: Thu, 5 Feb 2026 10:57:16 -0700 Subject: [PATCH 63/63] Minor refactoring changes for clarity --- h2integrate/core/model_baseclasses.py | 8 +++++--- h2integrate/storage/simple_storage_auto_sizing.py | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/h2integrate/core/model_baseclasses.py b/h2integrate/core/model_baseclasses.py index 721423527..60226ce08 100644 --- a/h2integrate/core/model_baseclasses.py +++ b/h2integrate/core/model_baseclasses.py @@ -30,19 +30,21 @@ def setup(self): # n_timesteps is number of timesteps in a simulation self.n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] + # dt is seconds per timestep self.dt = self.options["plant_config"]["plant"]["simulation"]["dt"] + # plant_life is number of years the plant is expected to operate for self.plant_life = int(self.options["plant_config"]["plant"]["plant_life"]) - hours_per_year = 8760 + # hours simulated is the number of hours in a simulation hours_simulated = (self.dt / 3600) * self.n_timesteps + # fraction_of_year_simulated is the ratio of simulation length to length of year # and may be used to estimate annual performance from simulation performance + hours_per_year = 8760 self.fraction_of_year_simulated = hours_simulated / hours_per_year - self.set_outputs() - def set_outputs(self): # Check that the required attributes have been instantiated required = ("commodity", "commodity_rate_units", "commodity_amount_units") missing = [el for el in required if not hasattr(self, el)] diff --git a/h2integrate/storage/simple_storage_auto_sizing.py b/h2integrate/storage/simple_storage_auto_sizing.py index 414090dcd..4b7b7287b 100644 --- a/h2integrate/storage/simple_storage_auto_sizing.py +++ b/h2integrate/storage/simple_storage_auto_sizing.py @@ -28,7 +28,7 @@ class StorageAutoSizingModel(PerformanceModelBaseClass): """Performance model that calculates the storage charge rate and capacity needed to either: - 1. supply the comodity at a constant rate based on the commodity production profile or + 1. supply the commodity at a constant rate based on the commodity production profile or 2. try to meet the commodity demand with the given commodity production profile. Then simulates performance of a basic storage component using the charge rate and