diff --git a/CHANGELOG.md b/CHANGELOG.md index 67b48699f..eb89bdcb2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ - Added base class (`StorageOpenLoopControlBase`) and base configuration class (`StorageOpenLoopControlBaseConfig`) for open-loop storage control strategies and updated the existing open-loop storage control strategies to inherit these [PR 619](https://github.com/NatLabRockies/H2Integrate/pull/619) - Added a generic cost model for converters [PR 622](https://github.com/NatLabRockies/H2Integrate/pull/622) - Updated the `StorageAutoSizingModel` model to be compatible with Pyomo control strategies [PR 621](https://github.com/NatLabRockies/H2Integrate/pull/621) +- Removed a few usages of `shape_by_conn` due to issues with OpenMDAO v3.43.0 release on some computers [PR 632](https://github.com/NatLabRockies/H2Integrate/pull/632) ## 0.7.1 [March 13, 2026] diff --git a/h2integrate/core/h2integrate_model.py b/h2integrate/core/h2integrate_model.py index 0b211c385..2e97b426c 100644 --- a/h2integrate/core/h2integrate_model.py +++ b/h2integrate/core/h2integrate_model.py @@ -1052,7 +1052,7 @@ def connect_technologies(self): pass else: connection_component = self.supported_models[transport_type]( - transport_item=transport_item + transport_item=transport_item, plant_config=self.plant_config ) # Add the connection component to the model diff --git a/h2integrate/transporters/cable.py b/h2integrate/transporters/cable.py index 5b369367b..1883cf957 100644 --- a/h2integrate/transporters/cable.py +++ b/h2integrate/transporters/cable.py @@ -8,22 +8,22 @@ class CablePerformanceModel(om.ExplicitComponent): def initialize(self): self.options.declare("transport_item", values=["electricity"]) + self.options.declare("plant_config", types=dict) def setup(self): + n_timesteps = int(self.options["plant_config"]["plant"]["simulation"]["n_timesteps"]) self.input_name = self.options["transport_item"] + "_in" self.output_name = self.options["transport_item"] + "_out" self.add_input( self.input_name, val=-1.0, - shape_by_conn=True, - copy_shape=self.output_name, + shape=n_timesteps, units="kW", ) self.add_output( self.output_name, val=-1.0, - shape_by_conn=True, - copy_shape=self.input_name, + shape=n_timesteps, units="kW", ) diff --git a/h2integrate/transporters/generic_splitter.py b/h2integrate/transporters/generic_splitter.py index ddfe4df6d..185c4684f 100644 --- a/h2integrate/transporters/generic_splitter.py +++ b/h2integrate/transporters/generic_splitter.py @@ -75,10 +75,12 @@ def setup(self): additional_cls_name=self.__class__.__name__, ) + n_timesteps = int(self.options["plant_config"]["plant"]["simulation"]["n_timesteps"]) + self.add_input( f"{self.config.commodity}_in", val=0.0, - shape_by_conn=True, + shape=n_timesteps, units=self.config.commodity_rate_units, ) @@ -94,7 +96,7 @@ def setup(self): self.add_input( "prescribed_commodity_to_priority_tech", val=self.config.prescribed_commodity_to_priority_tech, - copy_shape=f"{self.config.commodity}_in", + shape=n_timesteps, units=self.config.commodity_rate_units, desc="Prescribed amount of commodity to send to the priority technology", ) @@ -102,14 +104,14 @@ def setup(self): self.add_output( f"{self.config.commodity}_out1", val=0.0, - copy_shape=f"{self.config.commodity}_in", + shape=n_timesteps, units=self.config.commodity_rate_units, desc=f"{self.config.commodity} output to the first technology", ) self.add_output( f"{self.config.commodity}_out2", val=0.0, - copy_shape=f"{self.config.commodity}_in", + shape=n_timesteps, units=self.config.commodity_rate_units, desc=f"{self.config.commodity} output to the second technology", ) diff --git a/h2integrate/transporters/pipe.py b/h2integrate/transporters/pipe.py index f0edbf669..69a67c13c 100644 --- a/h2integrate/transporters/pipe.py +++ b/h2integrate/transporters/pipe.py @@ -20,12 +20,15 @@ def initialize(self): "water", ], ) + self.options.declare("plant_config", types=dict) def setup(self): transport_item = self.options["transport_item"] self.input_name = transport_item + "_in" self.output_name = transport_item + "_out" + n_timesteps = int(self.options["plant_config"]["plant"]["simulation"]["n_timesteps"]) + if transport_item == "natural_gas": units = "MMBtu/h" elif transport_item == "water": @@ -38,15 +41,13 @@ def setup(self): self.add_input( self.input_name, val=-1.0, - shape_by_conn=True, - copy_shape=self.output_name, + shape=n_timesteps, units=units, ) self.add_output( self.output_name, val=-1.0, - shape_by_conn=True, - copy_shape=self.input_name, + shape=n_timesteps, units=units, ) diff --git a/h2integrate/transporters/test/test_generic_splitter.py b/h2integrate/transporters/test/test_generic_splitter.py index ee0c7411f..7f9f2cfb3 100644 --- a/h2integrate/transporters/test/test_generic_splitter.py +++ b/h2integrate/transporters/test/test_generic_splitter.py @@ -25,10 +25,16 @@ def splitter_tech_config_hydrogen(): rng = np.random.default_rng(seed=0) +N_TIMESTEPS = 10 + + +@fixture +def plant_config(): + return {"plant": {"simulation": {"n_timesteps": N_TIMESTEPS}}} @pytest.mark.regression -def test_splitter_ratio_mode_edge_cases_electricity(splitter_tech_config_electricity): +def test_splitter_ratio_mode_edge_cases_electricity(splitter_tech_config_electricity, plant_config): """Test the splitter in fraction mode with edge case fractions.""" performance_config = { "split_mode": "fraction", @@ -40,45 +46,45 @@ def test_splitter_ratio_mode_edge_cases_electricity(splitter_tech_config_electri tech_config = {"model_inputs": {"performance_parameters": performance_config}} prob = om.Problem() - comp = GenericSplitterPerformanceModel(tech_config=tech_config) + comp = GenericSplitterPerformanceModel(tech_config=tech_config, plant_config=plant_config) prob.model.add_subsystem("comp", comp, promotes=["*"]) ivc = om.IndepVarComp() - ivc.add_output("electricity_in", val=100.0, units="kW") + ivc.add_output("electricity_in", val=np.full(N_TIMESTEPS, 100.0), units="kW") ivc.add_output("fraction_to_priority_tech", val=0.0) prob.model.add_subsystem("ivc", ivc, promotes=["*"]) prob.setup() - electricity_input = 100.0 + electricity_input = np.full(N_TIMESTEPS, 100.0) prob.set_val("electricity_in", electricity_input, units="kW") prob.set_val("fraction_to_priority_tech", 0.0) prob.run_model() - assert prob.get_val("electricity_out1", units="kW") == approx(0.0, abs=1e-10) + assert prob.get_val("electricity_out1", units="kW") == approx(np.zeros(N_TIMESTEPS), abs=1e-10) assert prob.get_val("electricity_out2", units="kW") == approx(electricity_input, rel=1e-5) prob.set_val("fraction_to_priority_tech", 1.0) prob.run_model() assert prob.get_val("electricity_out1", units="kW") == approx(electricity_input, rel=1e-5) - assert prob.get_val("electricity_out2", units="kW") == approx(0.0, abs=1e-10) + assert prob.get_val("electricity_out2", units="kW") == approx(np.zeros(N_TIMESTEPS), abs=1e-10) prob.set_val("fraction_to_priority_tech", 1.5) prob.run_model() assert prob.get_val("electricity_out1", units="kW") == approx(electricity_input, rel=1e-5) - assert prob.get_val("electricity_out2", units="kW") == approx(0.0, abs=1e-10) + assert prob.get_val("electricity_out2", units="kW") == approx(np.zeros(N_TIMESTEPS), abs=1e-10) prob.set_val("fraction_to_priority_tech", -0.5) prob.run_model() - assert prob.get_val("electricity_out1", units="kW") == approx(0.0, abs=1e-10) + assert prob.get_val("electricity_out1", units="kW") == approx(np.zeros(N_TIMESTEPS), abs=1e-10) assert prob.get_val("electricity_out2", units="kW") == approx(electricity_input, rel=1e-5) @pytest.mark.regression -def test_splitter_prescribed_electricity_mode(splitter_tech_config_electricity): +def test_splitter_prescribed_electricity_mode(splitter_tech_config_electricity, plant_config): """Test the splitter in prescribed_electricity mode.""" performance_config = { "split_mode": "prescribed_commodity", @@ -90,17 +96,17 @@ def test_splitter_prescribed_electricity_mode(splitter_tech_config_electricity): tech_config = {"model_inputs": {"performance_parameters": performance_config}} prob = om.Problem() - comp = GenericSplitterPerformanceModel(tech_config=tech_config) + comp = GenericSplitterPerformanceModel(tech_config=tech_config, plant_config=plant_config) prob.model.add_subsystem("comp", comp, promotes=["*"]) ivc = om.IndepVarComp() - ivc.add_output("electricity_in", val=np.zeros(8760), units="kW") - ivc.add_output("prescribed_commodity_to_priority_tech", val=np.zeros(8760), units="kW") + ivc.add_output("electricity_in", val=np.zeros(N_TIMESTEPS), units="kW") + ivc.add_output("prescribed_commodity_to_priority_tech", val=np.zeros(N_TIMESTEPS), units="kW") prob.model.add_subsystem("ivc", ivc, promotes=["*"]) prob.setup() - electricity_input = rng.random(8760) * 500 + 300 - prescribed_electricity = np.full(8760, 200.0) + electricity_input = rng.random(N_TIMESTEPS) * 500 + 300 + prescribed_electricity = np.full(N_TIMESTEPS, 200.0) prob.set_val("electricity_in", electricity_input, units="kW") prob.set_val("prescribed_commodity_to_priority_tech", prescribed_electricity, units="kW") @@ -119,7 +125,9 @@ def test_splitter_prescribed_electricity_mode(splitter_tech_config_electricity): @pytest.mark.regression -def test_splitter_prescribed_electricity_mode_limited_input(splitter_tech_config_electricity): +def test_splitter_prescribed_electricity_mode_limited_input( + splitter_tech_config_electricity, plant_config +): """ Test the splitter in prescribed_electricity mode when input is less than prescribed electricity. @@ -135,31 +143,31 @@ def test_splitter_prescribed_electricity_mode_limited_input(splitter_tech_config tech_config = {"model_inputs": {"performance_parameters": performance_config}} prob = om.Problem() - comp = GenericSplitterPerformanceModel(tech_config=tech_config) + comp = GenericSplitterPerformanceModel(tech_config=tech_config, plant_config=plant_config) prob.model.add_subsystem("comp", comp, promotes=["*"]) ivc = om.IndepVarComp() - ivc.add_output("electricity_in", val=np.zeros(8760), units="kW") - ivc.add_output("prescribed_commodity_to_priority_tech", val=np.zeros(8760), units="kW") + ivc.add_output("electricity_in", val=np.zeros(N_TIMESTEPS), units="kW") + ivc.add_output("prescribed_commodity_to_priority_tech", val=np.zeros(N_TIMESTEPS), units="kW") prob.model.add_subsystem("ivc", ivc, promotes=["*"]) prob.setup() - electricity_input = np.full(8760, 100.0) - prescribed_electricity = np.full(8760, 150.0) + electricity_input = np.full(N_TIMESTEPS, 100.0) + prescribed_electricity = np.full(N_TIMESTEPS, 150.0) prob.set_val("electricity_in", electricity_input, units="kW") prob.set_val("prescribed_commodity_to_priority_tech", prescribed_electricity, units="kW") prob.run_model() expected_output1 = electricity_input - expected_output2 = np.zeros(8760) + expected_output2 = np.zeros(N_TIMESTEPS) assert prob.get_val("electricity_out1", units="kW") == approx(expected_output1, rel=1e-5) assert prob.get_val("electricity_out2", units="kW") == approx(expected_output2, abs=1e-10) @pytest.mark.unit -def test_splitter_invalid_mode(splitter_tech_config_electricity): +def test_splitter_invalid_mode(splitter_tech_config_electricity, plant_config): """Test that an invalid split mode raises an error.""" performance_config = { "split_mode": "invalid_mode", @@ -174,13 +182,13 @@ def test_splitter_invalid_mode(splitter_tech_config_electricity): match="Item invalid_mode not found in list", ): prob = om.Problem() - comp = GenericSplitterPerformanceModel(tech_config=tech_config) + comp = GenericSplitterPerformanceModel(tech_config=tech_config, plant_config=plant_config) prob.model.add_subsystem("comp", comp, promotes=["*"]) prob.setup() @pytest.mark.regression -def test_splitter_scalar_inputs(splitter_tech_config_electricity): +def test_splitter_scalar_inputs(splitter_tech_config_electricity, plant_config): """Test the splitter with scalar inputs instead of arrays.""" performance_config_ratio = { "split_mode": "fraction", @@ -191,18 +199,22 @@ def test_splitter_scalar_inputs(splitter_tech_config_electricity): tech_config_ratio = {"model_inputs": {"performance_parameters": performance_config_ratio}} prob = om.Problem() - comp = GenericSplitterPerformanceModel(tech_config=tech_config_ratio) + comp = GenericSplitterPerformanceModel(tech_config=tech_config_ratio, plant_config=plant_config) prob.model.add_subsystem("comp", comp, promotes=["*"]) ivc = om.IndepVarComp() - ivc.add_output("electricity_in", val=100.0, units="kW") + ivc.add_output("electricity_in", val=np.full(N_TIMESTEPS, 100.0), units="kW") ivc.add_output("fraction_to_priority_tech", val=0.4) prob.model.add_subsystem("ivc", ivc, promotes=["*"]) prob.setup() prob.run_model() - assert prob.get_val("electricity_out1", units="kW") == approx(40.0, rel=1e-5) - assert prob.get_val("electricity_out2", units="kW") == approx(60.0, rel=1e-5) + assert prob.get_val("electricity_out1", units="kW") == approx( + np.full(N_TIMESTEPS, 40.0), rel=1e-5 + ) + assert prob.get_val("electricity_out2", units="kW") == approx( + np.full(N_TIMESTEPS, 60.0), rel=1e-5 + ) performance_config_prescribed = { "split_mode": "prescribed_commodity", @@ -216,22 +228,32 @@ def test_splitter_scalar_inputs(splitter_tech_config_electricity): } prob2 = om.Problem() - comp2 = GenericSplitterPerformanceModel(tech_config=tech_config_prescribed) + comp2 = GenericSplitterPerformanceModel( + tech_config=tech_config_prescribed, plant_config=plant_config + ) prob2.model.add_subsystem("comp", comp2, promotes=["*"]) ivc2 = om.IndepVarComp() - ivc2.add_output("electricity_in", val=100.0, units="kW") - ivc2.add_output("prescribed_commodity_to_priority_tech", val=30.0, units="kW") + ivc2.add_output("electricity_in", val=np.full(N_TIMESTEPS, 100.0), units="kW") + ivc2.add_output( + "prescribed_commodity_to_priority_tech", val=np.full(N_TIMESTEPS, 30.0), units="kW" + ) prob2.model.add_subsystem("ivc", ivc2, promotes=["*"]) prob2.setup() prob2.run_model() - assert prob2.get_val("electricity_out1", units="kW") == approx(30.0, rel=1e-5) - assert prob2.get_val("electricity_out2", units="kW") == approx(70.0, rel=1e-5) + assert prob2.get_val("electricity_out1", units="kW") == approx( + np.full(N_TIMESTEPS, 30.0), rel=1e-5 + ) + assert prob2.get_val("electricity_out2", units="kW") == approx( + np.full(N_TIMESTEPS, 70.0), rel=1e-5 + ) @pytest.mark.regression -def test_splitter_prescribed_electricity_varied_array(splitter_tech_config_electricity): +def test_splitter_prescribed_electricity_varied_array( + splitter_tech_config_electricity, plant_config +): """Test the splitter in prescribed_electricity mode with a varied array (50-100 MW).""" performance_config = { "split_mode": "prescribed_commodity", @@ -245,20 +267,20 @@ def test_splitter_prescribed_electricity_varied_array(splitter_tech_config_elect tech_config.update(splitter_tech_config_electricity) prob = om.Problem() - comp = GenericSplitterPerformanceModel(tech_config=tech_config) + comp = GenericSplitterPerformanceModel(tech_config=tech_config, plant_config=plant_config) prob.model.add_subsystem("comp", comp, promotes=["*"]) ivc = om.IndepVarComp() - ivc.add_output("electricity_in", val=np.zeros(8760), units="kW") - ivc.add_output("prescribed_commodity_to_priority_tech", val=np.zeros(8760), units="kW") + ivc.add_output("electricity_in", val=np.zeros(N_TIMESTEPS), units="kW") + ivc.add_output("prescribed_commodity_to_priority_tech", val=np.zeros(N_TIMESTEPS), units="kW") prob.model.add_subsystem("ivc", ivc, promotes=["*"]) prob.setup() # Generate varied prescribed electricity array between 50-100 MW (50,000-100,000 kW) - prescribed_electricity = rng.random(8760) * 50000 + 50000 # 50-100 MW range + prescribed_electricity = rng.random(N_TIMESTEPS) * 50000 + 50000 # 50-100 MW range # Input electricity should be higher than prescribed to test both scenarios - electricity_input = rng.random(8760) * 30000 + 120000 # 120-150 MW range + electricity_input = rng.random(N_TIMESTEPS) * 30000 + 120000 # 120-150 MW range prob.set_val("electricity_in", electricity_input, units="kW") prob.set_val("prescribed_commodity_to_priority_tech", prescribed_electricity, units="kW") @@ -278,7 +300,7 @@ def test_splitter_prescribed_electricity_varied_array(splitter_tech_config_elect assert total_output == approx(electricity_input, rel=1e-5) # Test with some time steps where prescribed > available - electricity_input_limited = rng.random(8760) * 30000 + 20000 # 20-50 MW range + electricity_input_limited = rng.random(N_TIMESTEPS) * 30000 + 20000 # 20-50 MW range prob.set_val("electricity_in", electricity_input_limited, units="kW") prob.run_model() diff --git a/h2integrate/transporters/test/test_pipe.py b/h2integrate/transporters/test/test_pipe.py index 638f216c4..ab139ddbb 100644 --- a/h2integrate/transporters/test/test_pipe.py +++ b/h2integrate/transporters/test/test_pipe.py @@ -1,16 +1,28 @@ +import numpy as np import pytest import openmdao.api as om -from pytest import approx +from pytest import approx, fixture from h2integrate.transporters.pipe import PipePerformanceModel +@fixture +def plant_config(): + plant_dict = { + "plant": { + "plant_life": 30, + "simulation": {"n_timesteps": 8760, "dt": 3600}, + } + } + return plant_dict + + @pytest.mark.unit -def test_pipe_with_hydrogen(): +def test_pipe_with_hydrogen(plant_config): """Test the pipe transport with hydrogen as transport_item.""" # Create the pipe component with hydrogen as transport item - pipe = PipePerformanceModel(transport_item="hydrogen") + pipe = PipePerformanceModel(plant_config=plant_config, transport_item="hydrogen") # Create OpenMDAO problem and add the component prob = om.Problem() @@ -18,12 +30,13 @@ def test_pipe_with_hydrogen(): # Add independent variable component for input ivc = om.IndepVarComp() - ivc.add_output("hydrogen_in", val=10.0, units="kg/s") + hydrogen_profile = np.full(8760, 10.0) + ivc.add_output("hydrogen_in", val=hydrogen_profile, units="kg/s") prob.model.add_subsystem("ivc", ivc, promotes=["*"]) # Setup and run the model prob.setup() - prob.set_val("hydrogen_in", 10.0, units="kg/s") + prob.set_val("hydrogen_in", val=hydrogen_profile, units="kg/s") prob.run_model() # Check that output equals input (pass-through pipe with no losses)