From 7d61a1c5ce8ddd58c06d38d89183cf76ae50fe6c Mon Sep 17 00:00:00 2001 From: Jared Thomas Date: Fri, 3 Apr 2026 17:51:47 -0600 Subject: [PATCH 01/18] add draft for variable timestep framework --- h2integrate/converters/grid/grid.py | 10 +++- h2integrate/core/h2integrate_model.py | 26 +++++++++++ h2integrate/core/inputs/validation.py | 8 +--- h2integrate/core/model_baseclasses.py | 3 ++ h2integrate/core/test/test_framework.py | 62 +++++++++++++++++++++++-- 5 files changed, 97 insertions(+), 12 deletions(-) diff --git a/h2integrate/converters/grid/grid.py b/h2integrate/converters/grid/grid.py index 0765ac426..9b2e49dce 100644 --- a/h2integrate/converters/grid/grid.py +++ b/h2integrate/converters/grid/grid.py @@ -45,6 +45,9 @@ class GridPerformanceModel(PerformanceModelBaseClass): electricity_out (array): Power flowing out of the grid (buying) (kW). """ + _time_step_min = 300 + _time_step_max = 3600 + def initialize(self): super().initialize() self.commodity = "electricity" @@ -178,6 +181,9 @@ class GridCostModel(CostModelBaseClass): this model assumes that each timestep represents 1 hour. """ + _time_step_min = 300 + _time_step_max = 3600 + def setup(self): self.config = GridCostModelConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost"), @@ -277,13 +283,13 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): electricity_out = inputs["electricity_out"] buy_price = inputs["electricity_buy_price"] # Buying costs money (positive VarOpEx) - varopex += np.sum(electricity_out * buy_price) + varopex += np.sum((self.dt / 3600) * electricity_out * buy_price) # Add selling revenue if sell price is configured # electricity_sold represents power flowing INTO grid (selling) if self.config.electricity_sell_price is not None: sell_price = inputs["electricity_sell_price"] # Selling generates revenue (negative VarOpEx) - varopex -= np.sum(inputs["electricity_sold"] * sell_price) + varopex -= np.sum((self.dt / 3600) * inputs["electricity_sold"] * sell_price) outputs["VarOpEx"] = varopex diff --git a/h2integrate/core/h2integrate_model.py b/h2integrate/core/h2integrate_model.py index ba1569c91..3ef94ce75 100644 --- a/h2integrate/core/h2integrate_model.py +++ b/h2integrate/core/h2integrate_model.py @@ -496,6 +496,7 @@ def create_technology_models(self): # and in combined_performance_and_cost_models perf_model = individual_tech_config.get("performance_model", {}).get("model") cost_model = individual_tech_config.get("cost_model", {}).get("model") + individual_tech_config.get("finance_model", {}).get("model") if ( perf_model @@ -570,6 +571,7 @@ def create_technology_models(self): for tech_name, individual_tech_config in self.technology_config["technologies"].items(): cost_model = individual_tech_config.get("cost_model", {}).get("model") + if cost_model == "FeedstockCostModel": comp = self.supported_models[cost_model]( driver_config=self.driver_config, @@ -582,6 +584,9 @@ def _process_model(self, model_type, individual_tech_config, tech_group): # Generalized function to process model definitions model_name = individual_tech_config[model_type]["model"] model_object = self.supported_models[model_name] + + self._check_time_step(model_name, model_object) + om_model_object = tech_group.add_subsystem( model_name, model_object( @@ -591,8 +596,29 @@ def _process_model(self, model_type, individual_tech_config, tech_group): ), promotes=["*"], ) + return om_model_object + def _check_time_step(self, model_name, model_object): + dt = int(self.plant_config["plant"]["simulation"]["dt"]) + + if hasattr(model_object, "_time_step_min") or hasattr(model_object, "_time_step_max"): + if dt < model_object._time_step_min or dt > model_object._time_step_max: + msg = ( + f"Performance model {model_name} is compatible with time steps " + f"between {model_object._time_step_min} and {model_object._time_step_max} " + f"but a time step of {dt} (s) was specified" + ) + raise ValueError(msg) + + elif dt != 3600: + msg = ( + f"Performance model {model_name} does not currently support simulations with a " + "time step that is less than or greater than 1-hour. Please ensure that " + "plant_config['plant']['simulation']['dt'] is set to 3600." + ) + raise ValueError(msg) + def create_finance_model(self): """ Create and configure the finance model(s) for the plant. diff --git a/h2integrate/core/inputs/validation.py b/h2integrate/core/inputs/validation.py index d9cc6f90d..c99f2dc9f 100644 --- a/h2integrate/core/inputs/validation.py +++ b/h2integrate/core/inputs/validation.py @@ -147,13 +147,7 @@ def load_plant_yaml(finput): "plant_config['plant']['simulation']['n_timesteps'] is set to 8760." ) raise ValueError(msg) - if int(plant_config["plant"]["simulation"]["dt"]) != 3600: - msg = ( - "H2Integrate does not currently support simulations with a time step that is " - "less than or greater than 1-hour. Please ensure that " - "plant_config['plant']['simulation']['dt'] is set to 3600." - ) - raise ValueError(msg) + return plant_config diff --git a/h2integrate/core/model_baseclasses.py b/h2integrate/core/model_baseclasses.py index 60226ce08..236d33265 100644 --- a/h2integrate/core/model_baseclasses.py +++ b/h2integrate/core/model_baseclasses.py @@ -151,6 +151,9 @@ def setup(self): "cost_year", val=self.config.cost_year, desc="Dollar year for costs" ) + # dt is seconds per timestep + self.dt = self.options["plant_config"]["plant"]["simulation"]["dt"] + def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): """ Computation for the OM component. diff --git a/h2integrate/core/test/test_framework.py b/h2integrate/core/test/test_framework.py index d9540984d..1ebc76fb0 100644 --- a/h2integrate/core/test/test_framework.py +++ b/h2integrate/core/test/test_framework.py @@ -182,9 +182,65 @@ def test_unsupported_simulation_parameters(temp_dir): with pytest.raises(ValueError, match="greater than 1-year"): load_plant_yaml(plant_config_data_ntimesteps) - # check that error is thrown when loading config with invalid time interval - with pytest.raises(ValueError, match="with a time step that"): - load_plant_yaml(plant_config_data_dt) + +@pytest.mark.unit +def test_check_time_step_with_model_bounds_allows_supported_dt(): + class DummyModel: + _time_step_min = 900 + _time_step_max = 3600 + + model = object.__new__(H2IntegrateModel) + model.plant_config = {"plant": {"simulation": {"dt": 1800}}} + + model._check_time_step("DummyModel", DummyModel) + + +@pytest.mark.unit +def test_check_time_step_with_model_bounds_raises_for_unsupported_dt(): + class DummyModel: + _time_step_min = 900 + _time_step_max = 3600 + + model = object.__new__(H2IntegrateModel) + model.plant_config = {"plant": {"simulation": {"dt": 7200}}} + + with pytest.raises( + ValueError, + match=( + r"Performance model DummyModel is compatible with time steps between " + r"900 and 3600 but a time step of 7200 \(s\) was specified" + ), + ): + model._check_time_step("DummyModel", DummyModel) + + +@pytest.mark.unit +def test_check_time_step_without_bounds_requires_one_hour_dt(): + class DummyModelNoBounds: + pass + + model = object.__new__(H2IntegrateModel) + model.plant_config = {"plant": {"simulation": {"dt": 1800}}} + + with pytest.raises( + ValueError, + match=( + r"Performance model DummyModelNoBounds does not currently support simulations " + r"with a time step that is less than or greater than 1-hour" + ), + ): + model._check_time_step("DummyModelNoBounds", DummyModelNoBounds) + + +@pytest.mark.unit +def test_check_time_step_without_bounds_allows_one_hour_dt(): + class DummyModelNoBounds: + pass + + model = object.__new__(H2IntegrateModel) + model.plant_config = {"plant": {"simulation": {"dt": 3600}}} + + model._check_time_step("DummyModelNoBounds", DummyModelNoBounds) @pytest.mark.unit From c36dfb89fa157973a3586ffd7c9bfd219ee1f78e Mon Sep 17 00:00:00 2001 From: John Jasa Date: Thu, 9 Apr 2026 11:35:57 -0600 Subject: [PATCH 02/18] Changed error wording --- h2integrate/core/h2integrate_model.py | 6 +++--- h2integrate/core/test/test_framework.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/h2integrate/core/h2integrate_model.py b/h2integrate/core/h2integrate_model.py index 9fadd3215..82f71d86a 100644 --- a/h2integrate/core/h2integrate_model.py +++ b/h2integrate/core/h2integrate_model.py @@ -617,9 +617,9 @@ def _check_time_step(self, model_name, model_object): elif dt != 3600: msg = ( - f"Performance model {model_name} does not currently support simulations with a " - "time step that is less than or greater than 1-hour. Please ensure that " - "plant_config['plant']['simulation']['dt'] is set to 3600." + f"Performance model '{model_name}' only supports a 1-hour time step (dt=3600), " + f"but dt={dt} was specified. Please set " + "plant_config['plant']['simulation']['dt'] to 3600." ) raise ValueError(msg) diff --git a/h2integrate/core/test/test_framework.py b/h2integrate/core/test/test_framework.py index 1ebc76fb0..7cd7a818a 100644 --- a/h2integrate/core/test/test_framework.py +++ b/h2integrate/core/test/test_framework.py @@ -225,8 +225,8 @@ class DummyModelNoBounds: with pytest.raises( ValueError, match=( - r"Performance model DummyModelNoBounds does not currently support simulations " - r"with a time step that is less than or greater than 1-hour" + r"Performance model 'DummyModelNoBounds' only supports a 1-hour time step " + r"\(dt=3600\), but dt=1800 was specified" ), ): model._check_time_step("DummyModelNoBounds", DummyModelNoBounds) From 0774f31bd5bc9d15de9102e124ddd6014c5455bc Mon Sep 17 00:00:00 2001 From: Jared Thomas Date: Fri, 10 Apr 2026 15:11:20 -0600 Subject: [PATCH 03/18] switch bounds to tuple --- h2integrate/converters/grid/grid.py | 6 ++---- h2integrate/core/h2integrate_model.py | 10 ++++++---- h2integrate/core/test/test_framework.py | 6 ++---- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/h2integrate/converters/grid/grid.py b/h2integrate/converters/grid/grid.py index 9b2e49dce..fa0f631f7 100644 --- a/h2integrate/converters/grid/grid.py +++ b/h2integrate/converters/grid/grid.py @@ -45,8 +45,7 @@ class GridPerformanceModel(PerformanceModelBaseClass): electricity_out (array): Power flowing out of the grid (buying) (kW). """ - _time_step_min = 300 - _time_step_max = 3600 + _time_step_bounds = (300, 3600) # (min, max) time step lengths compatible with this model def initialize(self): super().initialize() @@ -181,8 +180,7 @@ class GridCostModel(CostModelBaseClass): this model assumes that each timestep represents 1 hour. """ - _time_step_min = 300 - _time_step_max = 3600 + _time_step_bounds = (300, 3600) # (min, max) time step lengths compatible with this model def setup(self): self.config = GridCostModelConfig.from_dict( diff --git a/h2integrate/core/h2integrate_model.py b/h2integrate/core/h2integrate_model.py index 5f40c0724..47d8d1c65 100644 --- a/h2integrate/core/h2integrate_model.py +++ b/h2integrate/core/h2integrate_model.py @@ -613,12 +613,14 @@ def _process_model(self, model_type, individual_tech_config, tech_group): def _check_time_step(self, model_name, model_object): dt = int(self.plant_config["plant"]["simulation"]["dt"]) - if hasattr(model_object, "_time_step_min") or hasattr(model_object, "_time_step_max"): - if dt < model_object._time_step_min or dt > model_object._time_step_max: + if hasattr(model_object, "_time_step_bounds"): + min_ts = model_object._time_step_bounds[0] + max_ts = model_object._time_step_bounds[1] + if dt < min_ts or dt > max_ts: msg = ( f"Performance model {model_name} is compatible with time steps " - f"between {model_object._time_step_min} and {model_object._time_step_max} " - f"but a time step of {dt} (s) was specified" + f"between {min_ts} (s) and {max_ts} (s), but a time step of {dt} (s) " + "was specified" ) raise ValueError(msg) diff --git a/h2integrate/core/test/test_framework.py b/h2integrate/core/test/test_framework.py index 0077cb20f..406ef3a72 100644 --- a/h2integrate/core/test/test_framework.py +++ b/h2integrate/core/test/test_framework.py @@ -186,8 +186,7 @@ def test_unsupported_simulation_parameters(temp_dir): @pytest.mark.unit def test_check_time_step_with_model_bounds_allows_supported_dt(): class DummyModel: - _time_step_min = 900 - _time_step_max = 3600 + _time_step_bounds = (900, 3600) model = object.__new__(H2IntegrateModel) model.plant_config = {"plant": {"simulation": {"dt": 1800}}} @@ -198,8 +197,7 @@ class DummyModel: @pytest.mark.unit def test_check_time_step_with_model_bounds_raises_for_unsupported_dt(): class DummyModel: - _time_step_min = 900 - _time_step_max = 3600 + _time_step_bounds = (900, 3600) # (min, max) time step lengths compatible with this model model = object.__new__(H2IntegrateModel) model.plant_config = {"plant": {"simulation": {"dt": 7200}}} From 1adab8ca9837d3ecf4ccc542a0afc926b69078da Mon Sep 17 00:00:00 2001 From: Jared Thomas Date: Fri, 10 Apr 2026 15:13:54 -0600 Subject: [PATCH 04/18] minor test correction --- h2integrate/core/test/test_framework.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/h2integrate/core/test/test_framework.py b/h2integrate/core/test/test_framework.py index 406ef3a72..73b98163d 100644 --- a/h2integrate/core/test/test_framework.py +++ b/h2integrate/core/test/test_framework.py @@ -206,7 +206,7 @@ class DummyModel: ValueError, match=( r"Performance model DummyModel is compatible with time steps between " - r"900 and 3600 but a time step of 7200 \(s\) was specified" + r"900 \(s\) and 3600 \(s\), but a time step of 7200 \(s\) was specified" ), ): model._check_time_step("DummyModel", DummyModel) From c74968e0b0d767c1129733460bc2210ab2495b39 Mon Sep 17 00:00:00 2001 From: Jared Thomas Date: Fri, 10 Apr 2026 15:29:24 -0600 Subject: [PATCH 05/18] include time-series generation functions --- h2integrate/core/utilities.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/h2integrate/core/utilities.py b/h2integrate/core/utilities.py index 2a6852f82..1244c0b5f 100644 --- a/h2integrate/core/utilities.py +++ b/h2integrate/core/utilities.py @@ -3,6 +3,7 @@ import attrs import numpy as np +import pandas as pd from attrs import Attribute, define @@ -191,3 +192,30 @@ def attr_filter(inst: Attribute, value: Any) -> bool: if value.size == 0: return False return True + + +def build_time_series_from_plant_config(plant_config): + """Build simulation timestamps from plant_config simulation settings.""" + simulation_cfg = plant_config["plant"]["simulation"] + n_timesteps = int(simulation_cfg["n_timesteps"]) + dt_seconds = int(simulation_cfg["dt"]) + tz = int(simulation_cfg["timezone"]) + start_time = simulation_cfg["start_time"] + + return build_time_series( + start_time=start_time, dt_seconds=dt_seconds, n_timesteps=n_timesteps, time_zone=tz + ) + + +def build_time_series( + start_time: str, + dt_seconds: int, + n_timesteps: int, + time_zone: int, + start_year: int | None = None, +): + # Optional start_time in config; default to a fixed reference timestamp. + start_timestamp = pd.Timestamp(start_time, tz=time_zone, year=start_year) + freq = pd.to_timedelta(dt_seconds, unit="s") + + return pd.date_range(start=start_timestamp, periods=n_timesteps, freq=freq).to_pydatetime() From 935ef26deba4ac96eea9c02375592c2a8d14bfdb Mon Sep 17 00:00:00 2001 From: Jared Thomas Date: Fri, 10 Apr 2026 16:35:52 -0600 Subject: [PATCH 06/18] add tests for variable dt and update simulation length check for non-hourly dt --- h2integrate/converters/grid/test/test_grid.py | 127 ++++++++++++++++++ h2integrate/core/inputs/validation.py | 8 +- 2 files changed, 133 insertions(+), 2 deletions(-) diff --git a/h2integrate/converters/grid/test/test_grid.py b/h2integrate/converters/grid/test/test_grid.py index 5af52832c..00bb1ff13 100644 --- a/h2integrate/converters/grid/test/test_grid.py +++ b/h2integrate/converters/grid/test/test_grid.py @@ -4,6 +4,7 @@ from pytest import fixture from h2integrate.converters.grid.grid import GridCostModel, GridPerformanceModel +from h2integrate.core.h2integrate_model import H2IntegrateModel @fixture @@ -281,6 +282,132 @@ def test_varying_demand_profile(plant_config, n_timesteps): np.testing.assert_array_almost_equal(electricity_out, expected) +@pytest.mark.unit +@pytest.mark.parametrize("n_timesteps", [10]) +def test_non_hourly_dt_demand_profile(subtests, plant_config, n_timesteps): + """Test with time-varying demand profile.""" + plant_config["plant"]["simulation"]["dt"] = 300 + + prob = om.Problem() + commodity = "electricity" + + tech_config = {"model_inputs": {"shared_parameters": {"interconnection_size": 100000.0}}} + + prob.model.add_subsystem( + "grid", + GridPerformanceModel(driver_config={}, plant_config=plant_config, tech_config=tech_config), + ) + + prob.setup() + + # Create varying demand profile + demand = np.array([10000, 20000, 30000, 50000, 70000, 90000, 110000, 80000, 60000, 40000]) + prob.set_val("grid.electricity_demand", demand, units="kW") + + prob.run_model() + + with subtests.test(f"annual_{commodity}_produced length"): + electricity_out = prob.get_val("grid.electricity_out", units="kW") + # Values above 100000 should be clipped + expected = np.clip(demand, 0, 100000) + np.testing.assert_array_almost_equal(electricity_out, expected) + + with subtests.test("cf"): + cf = prob.get_val("grid.capacity_factor", units="unitless") + expected = np.full(30, 0.55) + np.testing.assert_array_almost_equal(cf, expected) + + with subtests.test("total production"): + total_energy = prob.get_val(f"grid.total_{commodity}_produced", units="kW*h") + expected = 45833.33 # (demand.sum()-10000)*(300/3600) to adjust to non-hourly + assert total_energy == pytest.approx(expected) + + with subtests.test("annual production"): + annual_energy = prob.get_val(f"grid.annual_{commodity}_produced", units="kW*h/year") + expected = 481799999.99999994 # total_energy*(min/year)/(min in simulation) + assert annual_energy == pytest.approx(expected) + + +@pytest.mark.integration +def test_grid_integration_dt_1800(subtests, tmp_path): + """Integration test: run an H2IntegrateModel with only grid technology at dt=1800 s.""" + n_timesteps = 8760 * 2 + dt_seconds = 1800 + interconnection_size = 100000.0 + demand_kw = 40000.0 + + driver_config = { + "name": "driver_config", + "description": "Integration test driver config", + "general": { + "folder_output": str(tmp_path / "output"), + "create_om_reports": False, + }, + } + + tech_config = { + "name": "technology_config", + "description": "Grid-only integration test", + "technologies": { + "grid": { + "performance_model": {"model": "GridPerformanceModel"}, + "model_inputs": { + "shared_parameters": { + "interconnection_size": interconnection_size, + } + }, + } + }, + } + + plant_config = { + "name": "plant_config", + "description": "Grid-only integration test plant", + "plant": { + "plant_life": 30, + "simulation": { + "n_timesteps": n_timesteps, + "dt": dt_seconds, + }, + }, + } + + h2i = H2IntegrateModel( + { + "name": "h2i_grid_integration_test", + "system_summary": "Grid-only integration model", + "driver_config": driver_config, + "technology_config": tech_config, + "plant_config": plant_config, + } + ) + h2i.setup() + + demand = np.full(n_timesteps, demand_kw) + h2i.prob.set_val("grid.electricity_demand", demand, units="kW") + h2i.prob.run_model() + + expected_out = np.full(n_timesteps, demand_kw) + expected_total = expected_out.sum() * (dt_seconds / 3600) + expected_annual = expected_total * (365 * 24 * 3600) / (n_timesteps * dt_seconds) + + with subtests.test("electricity_out equals demand when below interconnection limit"): + electricity_out = h2i.prob.get_val("grid.electricity_out", units="kW") + np.testing.assert_array_almost_equal(electricity_out, expected_out) + + with subtests.test("capacity factor reflects 40 percent loading"): + capacity_factor = h2i.prob.get_val("grid.capacity_factor", units="unitless") + np.testing.assert_array_almost_equal(capacity_factor, np.full_like(capacity_factor, 0.4)) + + with subtests.test("total electricity produced scales with 1800 second timestep"): + total_energy = h2i.prob.get_val("grid.total_electricity_produced", units="kW*h") + assert total_energy == pytest.approx(expected_total) + + with subtests.test("annual electricity produced scales from simulated fraction of year"): + annual_energy = h2i.prob.get_val("grid.annual_electricity_produced", units="kW*h/year") + assert annual_energy == pytest.approx(expected_annual) + + @pytest.mark.unit @pytest.mark.parametrize("n_timesteps", [24]) def test_buy_only_mode(plant_config, n_timesteps): diff --git a/h2integrate/core/inputs/validation.py b/h2integrate/core/inputs/validation.py index f98908d9f..f59dfabdd 100644 --- a/h2integrate/core/inputs/validation.py +++ b/h2integrate/core/inputs/validation.py @@ -140,11 +140,15 @@ def load_tech_yaml(finput): def load_plant_yaml(finput): plant_config = _validate(finput, fschema_plant) - if int(plant_config["plant"]["simulation"]["n_timesteps"]) != 8760: + n_timesteps = plant_config["plant"]["simulation"]["n_timesteps"] + dt = plant_config["plant"]["simulation"]["dt"] + + if int(n_timesteps * dt) != 31536000: # seconds in simulation must be seconds/year msg = ( "H2Integrate does not currently support simulations that are less than or " "greater than 1-year. Please ensure that " - "plant_config['plant']['simulation']['n_timesteps'] is set to 8760." + "plant_config['plant']['simulation']['n_timesteps'] times " + "plant_config['plant']['simulation']['dt'] equals 31536000 (s)." ) raise ValueError(msg) From 82e8bff6179b592efb262d7bc8f5832f48daa3ce Mon Sep 17 00:00:00 2001 From: Jared Thomas Date: Fri, 10 Apr 2026 17:18:28 -0600 Subject: [PATCH 07/18] update docs --- docs/developer_guide/adding_a_new_technology.md | 13 +++++++++++++ docs/technology_models/grid.md | 4 ++++ 2 files changed, 17 insertions(+) diff --git a/docs/developer_guide/adding_a_new_technology.md b/docs/developer_guide/adding_a_new_technology.md index 9d30d320b..5c22e6d19 100644 --- a/docs/developer_guide/adding_a_new_technology.md +++ b/docs/developer_guide/adding_a_new_technology.md @@ -258,6 +258,19 @@ In the middle-ground case where the models might use a shared object that is com This would require additional logic to first check if the cached object exists and is valid before attempting to load it, otherwise it would create the object from scratch. There is an example of this in the `hopp_wrapper.py` file. +### Specifying allowable time step for your model +If you want your model to run with time steps other than 1 hour (3600 s), then you must specify the `_time_step_bounds` as a class attribute in each of your model classes. To run a simulation with a given time step, all models in the plant must be compatible with the desired time step. + +```python +class ECOElectrolyzerPerformanceModel(ElectrolyzerPerformanceBaseClass): + """ + An OpenMDAO component that wraps the PEM electrolyzer model. + Takes electricity input and outputs hydrogen and oxygen generation rates. + """ + + # (min, max) time step lengths (in seconds) compatible with this model + _time_step_bounds = (300, 3600) # (5-min, 1-hour) +``` ### Other cases diff --git a/docs/technology_models/grid.md b/docs/technology_models/grid.md index befb8e3f8..ce0ca4902 100644 --- a/docs/technology_models/grid.md +++ b/docs/technology_models/grid.md @@ -67,3 +67,7 @@ The **revenue** of selling electricity to the grid is represented as a variable ```{note} If you're using a price-maker financial model (e.g., calculating the LCOE) and selling all of the electricity to the grid, then the `electricity_sell_price` should most likely be set to 0. since you want to know the breakeven price of selling that electricity. ``` + +```{note} +The grid components are currently compatible with 5-minute (300-second) to 1-hour (3600-second) time steps. +``` From ee2a46f5a7298b7938cfc9cf3a2063fbb9da2445 Mon Sep 17 00:00:00 2001 From: Jared Thomas Date: Fri, 10 Apr 2026 17:20:24 -0600 Subject: [PATCH 08/18] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11cf18f49..d4a05c93a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ - Adds `H2IntegrateModel.state` as an `IntEnum` to handle setup and run status checks. [PR 590](https://github.com/NatLabRockies/H2Integrate/pull/590) - Modified CI setup so Windows is temporarily disabled and also so unit, regression, and integration tests are run in separate jobs to speed up testing and provide more information on test failures. [PR 668](https://github.com/NatLabRockies/H2Integrate/pull/668) +- Added infrastructure for running models with non-hourly time steps via a class attribute `_time_step_bounds` and sets new time step bounds of 5-minutes to 1-hour for the grid components. [PR 653](https://github.com/NatLabRockies/H2Integrate/pull/653) ## 0.7.2 [April 9, 2026] From b9dc16713cca4be25221375cd5fad81324bf0efc Mon Sep 17 00:00:00 2001 From: Jared Thomas Date: Fri, 10 Apr 2026 17:40:35 -0600 Subject: [PATCH 09/18] update docs and doc strings --- docs/intro.md | 6 +++++- h2integrate/converters/grid/grid.py | 6 ++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/intro.md b/docs/intro.md index 734fddc85..d2824aa3e 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -64,11 +64,15 @@ H2Integrate was previously known as GreenHEART. The name was updated to H2Integr ## How does H2Integrate work? -H2Integrate models energy systems on a yearly basis using hourly timesteps (i.e., 8760 operational data points across a year). +H2Integrate typically models energy systems on a yearly basis using hourly timesteps (i.e., 8760 operational data points across a year). Results from these simulations are then processed across the project's lifecycle to provide insights into the system's performance, costs, and financial viability. Depending on the models used and the size of the system, H2Integrate can simulate systems ranging from the kW to GW scale in seconds on a personal computer. Additionally, H2Integrate tracks the flow of electricity, molecules (e.g., hydrogen, ammonia, methanol), and other products (e.g., steel) between different technologies in the energy system. +```{note} + Some models are now able to operate with non-hourly time steps. Appropriate time step bounds are included as class attributes when non-hourly time steps are permitted. Check individual model docs and definitions for time step bounds for individual models. All models in a given simulation must be compatible with the specified time step. +``` + For each technology there are 4 different types of models: control, performance, cost, and finance. These model categories allow for modular pieces to be brought in or re-used throughout H2Integrate, as well as ease of development and organization. Note that the only required models for a technology are performance and cost, while control and finance are optional. The figure below shows these four categories and some of the technologies included in H2Integrate. For a full list of models available, please see [Model Overview](user_guide/model_overview.md). ![A representation of a single technology model in H2Integrate](tech-model.png) diff --git a/h2integrate/converters/grid/grid.py b/h2integrate/converters/grid/grid.py index fa0f631f7..b40ca6f5b 100644 --- a/h2integrate/converters/grid/grid.py +++ b/h2integrate/converters/grid/grid.py @@ -36,6 +36,8 @@ class GridPerformanceModel(PerformanceModelBaseClass): different grid connection points (for example, one for buying upstream and another for selling downstream). + This model is compatible with time steps ranging from 5-minutes to 1-hour. + Inputs interconnection_size (float): Maximum power capacity for grid connection (kW). electricity_in (array): Power flowing into the grid (selling) (kW). @@ -176,8 +178,8 @@ class GridCostModel(CostModelBaseClass): - Revenue from electricity sales (sell mode) - Support for time-varying electricity prices - Note: Although the electricity units are in kW and the prices are in USD/kWh, - this model assumes that each timestep represents 1 hour. + This model is compatible with time steps ranging from 5-minutes to 1-hour. + """ _time_step_bounds = (300, 3600) # (min, max) time step lengths compatible with this model From e866dd64e6807545b1df7a4c3c479abe0818e2e6 Mon Sep 17 00:00:00 2001 From: Jared Thomas Date: Fri, 10 Apr 2026 17:56:50 -0600 Subject: [PATCH 10/18] restore develop version of utilities.py --- h2integrate/core/utilities.py | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/h2integrate/core/utilities.py b/h2integrate/core/utilities.py index 1244c0b5f..2a6852f82 100644 --- a/h2integrate/core/utilities.py +++ b/h2integrate/core/utilities.py @@ -3,7 +3,6 @@ import attrs import numpy as np -import pandas as pd from attrs import Attribute, define @@ -192,30 +191,3 @@ def attr_filter(inst: Attribute, value: Any) -> bool: if value.size == 0: return False return True - - -def build_time_series_from_plant_config(plant_config): - """Build simulation timestamps from plant_config simulation settings.""" - simulation_cfg = plant_config["plant"]["simulation"] - n_timesteps = int(simulation_cfg["n_timesteps"]) - dt_seconds = int(simulation_cfg["dt"]) - tz = int(simulation_cfg["timezone"]) - start_time = simulation_cfg["start_time"] - - return build_time_series( - start_time=start_time, dt_seconds=dt_seconds, n_timesteps=n_timesteps, time_zone=tz - ) - - -def build_time_series( - start_time: str, - dt_seconds: int, - n_timesteps: int, - time_zone: int, - start_year: int | None = None, -): - # Optional start_time in config; default to a fixed reference timestamp. - start_timestamp = pd.Timestamp(start_time, tz=time_zone, year=start_year) - freq = pd.to_timedelta(dt_seconds, unit="s") - - return pd.date_range(start=start_timestamp, periods=n_timesteps, freq=freq).to_pydatetime() From 9eeec641e841549834dc45b0eab4923cc8f9fc46 Mon Sep 17 00:00:00 2001 From: Jared Thomas Date: Fri, 10 Apr 2026 18:01:29 -0600 Subject: [PATCH 11/18] update dt bounds error --- h2integrate/core/h2integrate_model.py | 4 ++-- h2integrate/core/test/test_framework.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/h2integrate/core/h2integrate_model.py b/h2integrate/core/h2integrate_model.py index 01beeae42..acb04e47e 100644 --- a/h2integrate/core/h2integrate_model.py +++ b/h2integrate/core/h2integrate_model.py @@ -619,7 +619,7 @@ def _check_time_step(self, model_name, model_object): max_ts = model_object._time_step_bounds[1] if dt < min_ts or dt > max_ts: msg = ( - f"Performance model {model_name} is compatible with time steps " + f"Model {model_name} is compatible with time steps " f"between {min_ts} (s) and {max_ts} (s), but a time step of {dt} (s) " "was specified" ) @@ -627,7 +627,7 @@ def _check_time_step(self, model_name, model_object): elif dt != 3600: msg = ( - f"Performance model '{model_name}' only supports a 1-hour time step (dt=3600), " + f"Model '{model_name}' only supports a 1-hour time step (dt=3600), " f"but dt={dt} was specified. Please set " "plant_config['plant']['simulation']['dt'] to 3600." ) diff --git a/h2integrate/core/test/test_framework.py b/h2integrate/core/test/test_framework.py index 73b98163d..cf8b6a81e 100644 --- a/h2integrate/core/test/test_framework.py +++ b/h2integrate/core/test/test_framework.py @@ -205,7 +205,7 @@ class DummyModel: with pytest.raises( ValueError, match=( - r"Performance model DummyModel is compatible with time steps between " + r"Model DummyModel is compatible with time steps between " r"900 \(s\) and 3600 \(s\), but a time step of 7200 \(s\) was specified" ), ): @@ -223,7 +223,7 @@ class DummyModelNoBounds: with pytest.raises( ValueError, match=( - r"Performance model 'DummyModelNoBounds' only supports a 1-hour time step " + r"Model 'DummyModelNoBounds' only supports a 1-hour time step " r"\(dt=3600\), but dt=1800 was specified" ), ): From edd178b7434074da377ff40291c18d1430ca460c Mon Sep 17 00:00:00 2001 From: John Jasa Date: Sat, 11 Apr 2026 11:42:02 -0600 Subject: [PATCH 12/18] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/intro.md | 4 +++- h2integrate/converters/grid/test/test_grid.py | 22 ++++++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/docs/intro.md b/docs/intro.md index d2824aa3e..de798f43c 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -70,7 +70,9 @@ Depending on the models used and the size of the system, H2Integrate can simulat Additionally, H2Integrate tracks the flow of electricity, molecules (e.g., hydrogen, ammonia, methanol), and other products (e.g., steel) between different technologies in the energy system. ```{note} - Some models are now able to operate with non-hourly time steps. Appropriate time step bounds are included as class attributes when non-hourly time steps are permitted. Check individual model docs and definitions for time step bounds for individual models. All models in a given simulation must be compatible with the specified time step. +Some models are now able to operate with non-hourly time steps. +Appropriate time step bounds are included as class attributes when non-hourly time steps are permitted. +Check individual model docs and definitions for time step bounds for individual models. All models in a given simulation must be compatible with the specified time step. ``` For each technology there are 4 different types of models: control, performance, cost, and finance. These model categories allow for modular pieces to be brought in or re-used throughout H2Integrate, as well as ease of development and organization. Note that the only required models for a technology are performance and cost, while control and finance are optional. The figure below shows these four categories and some of the technologies included in H2Integrate. For a full list of models available, please see [Model Overview](user_guide/model_overview.md). diff --git a/h2integrate/converters/grid/test/test_grid.py b/h2integrate/converters/grid/test/test_grid.py index 00bb1ff13..ee6f54121 100644 --- a/h2integrate/converters/grid/test/test_grid.py +++ b/h2integrate/converters/grid/test/test_grid.py @@ -306,26 +306,32 @@ def test_non_hourly_dt_demand_profile(subtests, plant_config, n_timesteps): prob.run_model() + interconnection_size = tech_config["model_inputs"]["shared_parameters"]["interconnection_size"] + dt_seconds = plant_config["plant"]["simulation"]["dt"] + expected = np.clip(demand, 0, interconnection_size) + expected_total = expected.sum() * dt_seconds / 3600.0 + fraction_of_year = n_timesteps * dt_seconds / 31536000.0 + expected_annual_value = expected_total / fraction_of_year + with subtests.test(f"annual_{commodity}_produced length"): electricity_out = prob.get_val("grid.electricity_out", units="kW") - # Values above 100000 should be clipped - expected = np.clip(demand, 0, 100000) np.testing.assert_array_almost_equal(electricity_out, expected) with subtests.test("cf"): cf = prob.get_val("grid.capacity_factor", units="unitless") - expected = np.full(30, 0.55) - np.testing.assert_array_almost_equal(cf, expected) + expected_capacity_factor = expected.mean() / interconnection_size + assert cf == pytest.approx(expected_capacity_factor) with subtests.test("total production"): total_energy = prob.get_val(f"grid.total_{commodity}_produced", units="kW*h") - expected = 45833.33 # (demand.sum()-10000)*(300/3600) to adjust to non-hourly - assert total_energy == pytest.approx(expected) + np.testing.assert_allclose(np.atleast_1d(total_energy), [expected_total]) with subtests.test("annual production"): annual_energy = prob.get_val(f"grid.annual_{commodity}_produced", units="kW*h/year") - expected = 481799999.99999994 # total_energy*(min/year)/(min in simulation) - assert annual_energy == pytest.approx(expected) + np.testing.assert_allclose( + np.atleast_1d(annual_energy), + np.full(np.atleast_1d(annual_energy).shape, expected_annual_value), + ) @pytest.mark.integration From ac3ddcc0089966b49125f0b1117bf23aa01764ad Mon Sep 17 00:00:00 2001 From: John Jasa Date: Sat, 11 Apr 2026 11:58:17 -0600 Subject: [PATCH 13/18] Fixing tests --- .../hydrogen/test/test_basic_cost_model.py | 1 + .../test/test_singlitico_cost_model.py | 1 + h2integrate/core/h2integrate_model.py | 22 ++++++------------- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/h2integrate/converters/hydrogen/test/test_basic_cost_model.py b/h2integrate/converters/hydrogen/test/test_basic_cost_model.py index c11cdb69b..bce2205ea 100644 --- a/h2integrate/converters/hydrogen/test/test_basic_cost_model.py +++ b/h2integrate/converters/hydrogen/test/test_basic_cost_model.py @@ -34,6 +34,7 @@ def _create_problem(self, location, electrolyzer_size_mw, electrical_generation_ "plant_life": self.useful_life, "simulation": { "n_timesteps": self.n_timesteps, + "dt": 3600, }, }, }, diff --git a/h2integrate/converters/hydrogen/test/test_singlitico_cost_model.py b/h2integrate/converters/hydrogen/test/test_singlitico_cost_model.py index 5fa41ab82..76b448726 100644 --- a/h2integrate/converters/hydrogen/test/test_singlitico_cost_model.py +++ b/h2integrate/converters/hydrogen/test/test_singlitico_cost_model.py @@ -38,6 +38,7 @@ def _create_problem(self, location): "plant_life": 30, "simulation": { "n_timesteps": 8760, + "dt": 3600, }, }, }, diff --git a/h2integrate/core/h2integrate_model.py b/h2integrate/core/h2integrate_model.py index f81b024a5..126f1b072 100644 --- a/h2integrate/core/h2integrate_model.py +++ b/h2integrate/core/h2integrate_model.py @@ -614,22 +614,14 @@ def _process_model(self, model_type, individual_tech_config, tech_group): def _check_time_step(self, model_name, model_object): dt = int(self.plant_config["plant"]["simulation"]["dt"]) - if hasattr(model_object, "_time_step_bounds"): - min_ts = model_object._time_step_bounds[0] - max_ts = model_object._time_step_bounds[1] - if dt < min_ts or dt > max_ts: - msg = ( - f"Model {model_name} is compatible with time steps " - f"between {min_ts} (s) and {max_ts} (s), but a time step of {dt} (s) " - "was specified" - ) - raise ValueError(msg) - - elif dt != 3600: + min_ts = model_object._time_step_bounds[0] + max_ts = model_object._time_step_bounds[1] + if dt < min_ts or dt > max_ts: msg = ( - f"Model '{model_name}' only supports a 1-hour time step (dt=3600), " - f"but dt={dt} was specified. Please set " - "plant_config['plant']['simulation']['dt'] to 3600." + f"Model {model_name} is compatible with time steps " + f"between {min_ts} (s) and {max_ts} (s), but a time step of {dt} (s) " + "was specified. Please set plant_config['plant']['simulation']['dt'] to a" + f" value within the range [{min_ts}, {max_ts}]." ) raise ValueError(msg) From 733450752f4ec59a5f6a1c809e5db089b7f2ac8d Mon Sep 17 00:00:00 2001 From: John Jasa Date: Sat, 11 Apr 2026 12:02:08 -0600 Subject: [PATCH 14/18] Adding time step bounds to all models --- .../control/control_rules/converters/generic_converter.py | 2 ++ h2integrate/control/control_rules/pyomo_rule_baseclass.py | 2 ++ .../control_rules/storage/pyomo_storage_rule_baseclass.py | 2 ++ .../control_strategies/heuristic_pyomo_controller.py | 2 ++ .../control_strategies/optimized_pyomo_controller.py | 2 ++ .../control_strategies/pyomo_controller_baseclass.py | 2 ++ .../storage/demand_openloop_storage_controller.py | 2 ++ .../storage/openloop_storage_control_base.py | 2 ++ .../storage/simple_openloop_controller.py | 2 ++ h2integrate/converters/ammonia/ammonia_synloop.py | 4 ++++ h2integrate/converters/ammonia/simple_ammonia_model.py | 4 ++++ h2integrate/converters/co2/marine/direct_ocean_capture.py | 4 ++++ .../converters/co2/marine/ocean_alkalinity_enhancement.py | 6 ++++++ h2integrate/converters/generic_converter_cost.py | 2 ++ h2integrate/converters/hopp/hopp_wrapper.py | 2 ++ h2integrate/converters/hydrogen/basic_cost_model.py | 2 ++ .../converters/hydrogen/custom_electrolyzer_cost_model.py | 2 ++ h2integrate/converters/hydrogen/electrolyzer_baseclass.py | 4 ++++ .../hydrogen/geologic/aspen_surface_processing.py | 4 ++++ .../hydrogen/geologic/h2_well_subsurface_baseclass.py | 4 ++++ .../hydrogen/geologic/h2_well_surface_baseclass.py | 4 ++++ .../converters/hydrogen/geologic/simple_natural_geoh2.py | 2 ++ .../hydrogen/geologic/templeton_serpentinization.py | 2 ++ h2integrate/converters/hydrogen/h2_fuel_cell.py | 4 ++++ h2integrate/converters/hydrogen/pem_electrolyzer.py | 2 ++ h2integrate/converters/hydrogen/singlitico_cost_model.py | 2 ++ h2integrate/converters/hydrogen/steam_methane_reformer.py | 4 ++++ h2integrate/converters/hydrogen/wombat_model.py | 2 ++ h2integrate/converters/iron/humbert_ewin_perf.py | 2 ++ h2integrate/converters/iron/humbert_stinn_ewin_cost.py | 2 ++ h2integrate/converters/iron/iron_dri_base.py | 4 ++++ h2integrate/converters/iron/iron_dri_plant.py | 8 ++++++++ h2integrate/converters/iron/iron_transport.py | 4 ++++ h2integrate/converters/iron/martin_mine_cost_model.py | 2 ++ h2integrate/converters/iron/martin_mine_perf_model.py | 2 ++ h2integrate/converters/methanol/co2h_methanol_plant.py | 4 ++++ h2integrate/converters/methanol/methanol_baseclass.py | 4 ++++ h2integrate/converters/methanol/smr_methanol_plant.py | 4 ++++ .../converters/natural_gas/dummy_gas_components.py | 8 ++++++++ h2integrate/converters/natural_gas/natural_gas_cc_ct.py | 4 ++++ h2integrate/converters/nitrogen/simple_ASU.py | 4 ++++ h2integrate/converters/nuclear/nuclear_plant.py | 4 ++++ h2integrate/converters/solar/atb_res_com_pv_cost.py | 2 ++ h2integrate/converters/solar/atb_utility_pv_cost.py | 2 ++ h2integrate/converters/solar/solar_baseclass.py | 2 ++ h2integrate/converters/solar/solar_pysam.py | 2 ++ h2integrate/converters/steel/steel.py | 4 ++++ h2integrate/converters/steel/steel_baseclass.py | 4 ++++ h2integrate/converters/steel/steel_eaf_base.py | 4 ++++ h2integrate/converters/steel/steel_eaf_plant.py | 8 ++++++++ h2integrate/converters/water/desal/desalination.py | 4 ++++ .../converters/water/desal/desalination_baseclass.py | 4 ++++ .../converters/water_power/hydro_plant_run_of_river.py | 4 ++++ h2integrate/converters/water_power/pysam_marine_cost.py | 2 ++ h2integrate/converters/water_power/tidal_pysam.py | 2 ++ h2integrate/converters/wind/atb_wind_cost.py | 2 ++ h2integrate/converters/wind/floris.py | 2 ++ h2integrate/converters/wind/wind_plant_ard.py | 4 ++++ h2integrate/converters/wind/wind_plant_baseclass.py | 2 ++ h2integrate/converters/wind/wind_pysam.py | 2 ++ h2integrate/storage/battery/atb_battery_cost.py | 2 ++ h2integrate/storage/battery/pysam_battery.py | 2 ++ h2integrate/storage/generic_storage_cost.py | 2 ++ h2integrate/storage/hydrogen/h2_storage_cost.py | 8 ++++++++ h2integrate/storage/hydrogen/mch_storage.py | 2 ++ h2integrate/storage/simple_storage_auto_sizing.py | 2 ++ h2integrate/storage/storage_baseclass.py | 2 ++ h2integrate/storage/storage_performance_model.py | 2 ++ 68 files changed, 212 insertions(+) diff --git a/h2integrate/control/control_rules/converters/generic_converter.py b/h2integrate/control/control_rules/converters/generic_converter.py index 92fa2bd82..53ecf41f3 100644 --- a/h2integrate/control/control_rules/converters/generic_converter.py +++ b/h2integrate/control/control_rules/converters/generic_converter.py @@ -9,6 +9,8 @@ class PyomoDispatchGenericConverter(PyomoRuleBaseClass): + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = PyomoRuleBaseConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "dispatch_rule"), diff --git a/h2integrate/control/control_rules/pyomo_rule_baseclass.py b/h2integrate/control/control_rules/pyomo_rule_baseclass.py index b8f512e57..e26de6a5b 100644 --- a/h2integrate/control/control_rules/pyomo_rule_baseclass.py +++ b/h2integrate/control/control_rules/pyomo_rule_baseclass.py @@ -22,6 +22,8 @@ class PyomoRuleBaseConfig(BaseConfig): class PyomoRuleBaseClass(om.ExplicitComponent): + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): self.options.declare("driver_config", types=dict) self.options.declare("plant_config", types=dict) diff --git a/h2integrate/control/control_rules/storage/pyomo_storage_rule_baseclass.py b/h2integrate/control/control_rules/storage/pyomo_storage_rule_baseclass.py index 0e996fb2b..91618decf 100644 --- a/h2integrate/control/control_rules/storage/pyomo_storage_rule_baseclass.py +++ b/h2integrate/control/control_rules/storage/pyomo_storage_rule_baseclass.py @@ -24,6 +24,8 @@ class PyomoStorageRuleBaseConfig(PyomoRuleBaseConfig): class PyomoRuleStorageBaseclass(PyomoRuleBaseClass): """Base class defining Pyomo rules for generic commodity storage components.""" + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = PyomoStorageRuleBaseConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "dispatch_rule"), diff --git a/h2integrate/control/control_strategies/heuristic_pyomo_controller.py b/h2integrate/control/control_strategies/heuristic_pyomo_controller.py index 3a40b5bec..2ed60d585 100644 --- a/h2integrate/control/control_strategies/heuristic_pyomo_controller.py +++ b/h2integrate/control/control_strategies/heuristic_pyomo_controller.py @@ -70,6 +70,8 @@ class HeuristicLoadFollowingController(PyomoControllerBaseClass): """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): """Initialize the heuristic load-following controller.""" self.config = HeuristicLoadFollowingControllerConfig.from_dict( diff --git a/h2integrate/control/control_strategies/optimized_pyomo_controller.py b/h2integrate/control/control_strategies/optimized_pyomo_controller.py index 0bc66dad8..41140a669 100644 --- a/h2integrate/control/control_strategies/optimized_pyomo_controller.py +++ b/h2integrate/control/control_strategies/optimized_pyomo_controller.py @@ -100,6 +100,8 @@ class OptimizedDispatchController(PyomoControllerBaseClass): """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): """Initialize the optimized dispatch controller.""" self.config = OptimizedDispatchControllerConfig.from_dict( diff --git a/h2integrate/control/control_strategies/pyomo_controller_baseclass.py b/h2integrate/control/control_strategies/pyomo_controller_baseclass.py index ae8b7737f..6627b0541 100644 --- a/h2integrate/control/control_strategies/pyomo_controller_baseclass.py +++ b/h2integrate/control/control_strategies/pyomo_controller_baseclass.py @@ -75,6 +75,8 @@ def __attrs_post_init__(self): class PyomoControllerBaseClass(om.ExplicitComponent): + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): """ Declare options for the component. See "Attributes" section in class doc strings for diff --git a/h2integrate/control/control_strategies/storage/demand_openloop_storage_controller.py b/h2integrate/control/control_strategies/storage/demand_openloop_storage_controller.py index 6fc9cfab9..906fbd8fb 100644 --- a/h2integrate/control/control_strategies/storage/demand_openloop_storage_controller.py +++ b/h2integrate/control/control_strategies/storage/demand_openloop_storage_controller.py @@ -115,6 +115,8 @@ class DemandOpenLoopStorageController(StorageOpenLoopControlBase): commodity to charge, discharge, or curtail at each time step. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = DemandOpenLoopStorageControllerConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "control"), diff --git a/h2integrate/control/control_strategies/storage/openloop_storage_control_base.py b/h2integrate/control/control_strategies/storage/openloop_storage_control_base.py index 108cdabfd..0f58b01e4 100644 --- a/h2integrate/control/control_strategies/storage/openloop_storage_control_base.py +++ b/h2integrate/control/control_strategies/storage/openloop_storage_control_base.py @@ -38,6 +38,8 @@ class StorageOpenLoopControlBase(om.ExplicitComponent): dispatch command profile. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): self.options.declare("driver_config", types=dict) self.options.declare("plant_config", types=dict) diff --git a/h2integrate/control/control_strategies/storage/simple_openloop_controller.py b/h2integrate/control/control_strategies/storage/simple_openloop_controller.py index 750e7b4d4..6972f49ba 100644 --- a/h2integrate/control/control_strategies/storage/simple_openloop_controller.py +++ b/h2integrate/control/control_strategies/storage/simple_openloop_controller.py @@ -56,6 +56,8 @@ class SimpleStorageOpenLoopController(StorageOpenLoopControlBase): uncontrolled frameworks. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = SimpleStorageOpenLoopControllerConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "control"), diff --git a/h2integrate/converters/ammonia/ammonia_synloop.py b/h2integrate/converters/ammonia/ammonia_synloop.py index 3e360478d..e38bad3f4 100644 --- a/h2integrate/converters/ammonia/ammonia_synloop.py +++ b/h2integrate/converters/ammonia/ammonia_synloop.py @@ -139,6 +139,8 @@ class AmmoniaSynLoopPerformanceModel(ResizeablePerformanceModelBaseClass): conversion efficiency up to the limiting reagent or energy input. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "ammonia" @@ -423,6 +425,8 @@ class AmmoniaSynLoopCostModel(CostModelBaseClass): Annual maintenance cost """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): target_cost_year = self.options["plant_config"]["finance_parameters"][ "cost_adjustment_parameters" diff --git a/h2integrate/converters/ammonia/simple_ammonia_model.py b/h2integrate/converters/ammonia/simple_ammonia_model.py index 9f609d232..29b2714fb 100644 --- a/h2integrate/converters/ammonia/simple_ammonia_model.py +++ b/h2integrate/converters/ammonia/simple_ammonia_model.py @@ -30,6 +30,8 @@ class SimpleAmmoniaPerformanceModel(PerformanceModelBaseClass): Computes annual ammonia production based on plant capacity and capacity factor. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "ammonia" @@ -108,6 +110,8 @@ class SimpleAmmoniaCostModel(CostModelBaseClass): Includes CapEx, OpEx, and byproduct credits, and exposes all detailed cost outputs. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = AmmoniaCostModelConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost"), diff --git a/h2integrate/converters/co2/marine/direct_ocean_capture.py b/h2integrate/converters/co2/marine/direct_ocean_capture.py index 5e1670ce5..b4abffe16 100644 --- a/h2integrate/converters/co2/marine/direct_ocean_capture.py +++ b/h2integrate/converters/co2/marine/direct_ocean_capture.py @@ -76,6 +76,8 @@ class DOCPerformanceModel(PerformanceModelBaseClass): An OpenMDAO component for modeling the performance of a Direct Ocean Capture (DOC) plant. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "co2" @@ -153,6 +155,8 @@ class DOCCostModel(CostModelBaseClass): direct ocean capture (DOC) system. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() diff --git a/h2integrate/converters/co2/marine/ocean_alkalinity_enhancement.py b/h2integrate/converters/co2/marine/ocean_alkalinity_enhancement.py index 99b8f4fec..8b1c56b71 100644 --- a/h2integrate/converters/co2/marine/ocean_alkalinity_enhancement.py +++ b/h2integrate/converters/co2/marine/ocean_alkalinity_enhancement.py @@ -69,6 +69,8 @@ class OAEPerformanceConfig(BaseConfig): class OAEPerformanceModel(PerformanceModelBaseClass): """OpenMDAO component for modeling Ocean Alkalinity Enhancement (OAE) performance.""" + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "co2" @@ -255,6 +257,8 @@ class OAECostModel(CostModelBaseClass): ocean alkalinity enhancement (OAE) system. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() @@ -347,6 +351,8 @@ class OAECostAndFinancialModel(CostModelBaseClass): - Carbon Credit Value """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() diff --git a/h2integrate/converters/generic_converter_cost.py b/h2integrate/converters/generic_converter_cost.py index 166525949..9e7fa86fd 100644 --- a/h2integrate/converters/generic_converter_cost.py +++ b/h2integrate/converters/generic_converter_cost.py @@ -50,6 +50,8 @@ def __attrs_post_init__(self): class GenericConverterCostModel(CostModelBaseClass): + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = GenericConverterCostConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost"), diff --git a/h2integrate/converters/hopp/hopp_wrapper.py b/h2integrate/converters/hopp/hopp_wrapper.py index 5cb5f9351..b968726db 100644 --- a/h2integrate/converters/hopp/hopp_wrapper.py +++ b/h2integrate/converters/hopp/hopp_wrapper.py @@ -28,6 +28,8 @@ class HOPPComponent(PerformanceModelBaseClass, CacheBaseClass): computed results when the same configuration is encountered. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "electricity" diff --git a/h2integrate/converters/hydrogen/basic_cost_model.py b/h2integrate/converters/hydrogen/basic_cost_model.py index 574993abb..37520914b 100644 --- a/h2integrate/converters/hydrogen/basic_cost_model.py +++ b/h2integrate/converters/hydrogen/basic_cost_model.py @@ -35,6 +35,8 @@ class BasicElectrolyzerCostModel(ElectrolyzerCostBaseClass): An OpenMDAO component that computes the cost of a PEM electrolyzer. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = BasicElectrolyzerCostModelConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost"), diff --git a/h2integrate/converters/hydrogen/custom_electrolyzer_cost_model.py b/h2integrate/converters/hydrogen/custom_electrolyzer_cost_model.py index 851b2538c..cdd899c20 100644 --- a/h2integrate/converters/hydrogen/custom_electrolyzer_cost_model.py +++ b/h2integrate/converters/hydrogen/custom_electrolyzer_cost_model.py @@ -27,6 +27,8 @@ class CustomElectrolyzerCostModel(ElectrolyzerCostBaseClass): An OpenMDAO component that computes the cost of a PEM electrolyzer. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = CustomElectrolyzerCostModelConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost"), diff --git a/h2integrate/converters/hydrogen/electrolyzer_baseclass.py b/h2integrate/converters/hydrogen/electrolyzer_baseclass.py index 39112d648..122f01d6a 100644 --- a/h2integrate/converters/hydrogen/electrolyzer_baseclass.py +++ b/h2integrate/converters/hydrogen/electrolyzer_baseclass.py @@ -5,6 +5,8 @@ class ElectrolyzerPerformanceBaseClass(ResizeablePerformanceModelBaseClass): + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "hydrogen" @@ -28,6 +30,8 @@ def compute(self, inputs, outputs): class ElectrolyzerCostBaseClass(CostModelBaseClass): + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] super().setup() diff --git a/h2integrate/converters/hydrogen/geologic/aspen_surface_processing.py b/h2integrate/converters/hydrogen/geologic/aspen_surface_processing.py index e929950ed..cdcd89c44 100644 --- a/h2integrate/converters/hydrogen/geologic/aspen_surface_processing.py +++ b/h2integrate/converters/hydrogen/geologic/aspen_surface_processing.py @@ -76,6 +76,8 @@ class AspenGeoH2SurfacePerformanceModel(GeoH2SurfacePerformanceBaseClass): Hourly steam production profile (8760 hours), in kW thermal. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = AspenGeoH2SurfacePerformanceConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), @@ -244,6 +246,8 @@ class AspenGeoH2SurfaceCostModel(GeoH2SurfaceCostBaseClass): All inherited from GeoH2SurfaceCostBaseClass """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = AspenGeoH2SurfaceCostConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost"), diff --git a/h2integrate/converters/hydrogen/geologic/h2_well_subsurface_baseclass.py b/h2integrate/converters/hydrogen/geologic/h2_well_subsurface_baseclass.py index f22dd3010..0837c8033 100644 --- a/h2integrate/converters/hydrogen/geologic/h2_well_subsurface_baseclass.py +++ b/h2integrate/converters/hydrogen/geologic/h2_well_subsurface_baseclass.py @@ -86,6 +86,8 @@ class GeoH2SubsurfacePerformanceBaseClass(PerformanceModelBaseClass): The total hydrogen produced over the plant lifetime, in kilograms per year. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "hydrogen" @@ -169,6 +171,8 @@ class GeoH2SubsurfaceCostBaseClass(CostModelBaseClass): Variable OPEX per kilogram of wellhead gas produced, in USD/kg. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): super().setup() n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] diff --git a/h2integrate/converters/hydrogen/geologic/h2_well_surface_baseclass.py b/h2integrate/converters/hydrogen/geologic/h2_well_surface_baseclass.py index 3fca88df0..45d258f99 100644 --- a/h2integrate/converters/hydrogen/geologic/h2_well_surface_baseclass.py +++ b/h2integrate/converters/hydrogen/geologic/h2_well_surface_baseclass.py @@ -66,6 +66,8 @@ class GeoH2SurfacePerformanceBaseClass(PerformanceModelBaseClass): The wellhead gas flow in kg/hour used for sizing the system - passed to the cost model. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "hydrogen" @@ -147,6 +149,8 @@ class GeoH2SurfaceCostBaseClass(CostModelBaseClass): Variable OPEX per kilogram of hydrogen produced, in USD/year. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): super().setup() n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] diff --git a/h2integrate/converters/hydrogen/geologic/simple_natural_geoh2.py b/h2integrate/converters/hydrogen/geologic/simple_natural_geoh2.py index 8f26ddacd..e82f16842 100644 --- a/h2integrate/converters/hydrogen/geologic/simple_natural_geoh2.py +++ b/h2integrate/converters/hydrogen/geologic/simple_natural_geoh2.py @@ -141,6 +141,8 @@ class NaturalGeoH2PerformanceModel(GeoH2SubsurfacePerformanceBaseClass): of annual hydrogen production to the maximum hydrogen production of the well. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = NaturalGeoH2PerformanceConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), diff --git a/h2integrate/converters/hydrogen/geologic/templeton_serpentinization.py b/h2integrate/converters/hydrogen/geologic/templeton_serpentinization.py index bc99d3c3d..9e372baf1 100644 --- a/h2integrate/converters/hydrogen/geologic/templeton_serpentinization.py +++ b/h2integrate/converters/hydrogen/geologic/templeton_serpentinization.py @@ -73,6 +73,8 @@ class StimulatedGeoH2PerformanceModel(GeoH2SubsurfacePerformanceBaseClass): over one year (8760 hours) [kg/h]. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] self.config = StimulatedGeoH2PerformanceConfig.from_dict( diff --git a/h2integrate/converters/hydrogen/h2_fuel_cell.py b/h2integrate/converters/hydrogen/h2_fuel_cell.py index e7851a94b..24ef0d8ed 100644 --- a/h2integrate/converters/hydrogen/h2_fuel_cell.py +++ b/h2integrate/converters/hydrogen/h2_fuel_cell.py @@ -38,6 +38,8 @@ class LinearH2FuelCellPerformanceModel(PerformanceModelBaseClass): - HHV_hydrogen is the higher heating value of hydrogen (approximately 142 MJ/kg) """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "electricity" @@ -144,6 +146,8 @@ class H2FuelCellCostModel(CostModelBaseClass): specified cost parameters. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = H2FuelCellCostConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost"), diff --git a/h2integrate/converters/hydrogen/pem_electrolyzer.py b/h2integrate/converters/hydrogen/pem_electrolyzer.py index 8142aa7c5..3f4114796 100644 --- a/h2integrate/converters/hydrogen/pem_electrolyzer.py +++ b/h2integrate/converters/hydrogen/pem_electrolyzer.py @@ -59,6 +59,8 @@ class ECOElectrolyzerPerformanceModel(ElectrolyzerPerformanceBaseClass): Takes electricity input and outputs hydrogen and oxygen generation rates. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = ECOElectrolyzerPerformanceModelConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), diff --git a/h2integrate/converters/hydrogen/singlitico_cost_model.py b/h2integrate/converters/hydrogen/singlitico_cost_model.py index 8c36a7d9d..6eccf8284 100644 --- a/h2integrate/converters/hydrogen/singlitico_cost_model.py +++ b/h2integrate/converters/hydrogen/singlitico_cost_model.py @@ -29,6 +29,8 @@ class SingliticoCostModel(ElectrolyzerCostBaseClass): An OpenMDAO component that computes the cost of a PEM electrolyzer. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = SingliticoCostModelConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost"), diff --git a/h2integrate/converters/hydrogen/steam_methane_reformer.py b/h2integrate/converters/hydrogen/steam_methane_reformer.py index a52b73754..a997778d2 100644 --- a/h2integrate/converters/hydrogen/steam_methane_reformer.py +++ b/h2integrate/converters/hydrogen/steam_methane_reformer.py @@ -44,6 +44,8 @@ class SteamMethaneReformerPerformanceModel(PerformanceModelBaseClass): unmet_hydrogen_demand (array): Unmet hydrogen demand in kg/h for each timestep """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "hydrogen" @@ -271,6 +273,8 @@ class SteamMethaneReformerCostModel(CostModelBaseClass): cost_year (int): Dollar year for the costs """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = SteamMethaneReformerCostModelConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost"), diff --git a/h2integrate/converters/hydrogen/wombat_model.py b/h2integrate/converters/hydrogen/wombat_model.py index 84fdc04be..d747be6b6 100644 --- a/h2integrate/converters/hydrogen/wombat_model.py +++ b/h2integrate/converters/hydrogen/wombat_model.py @@ -35,6 +35,8 @@ class WOMBATElectrolyzerModel(ECOElectrolyzerPerformanceModel): lost due to operations and maintenance (O&M), and electrolyzer availability. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): super().setup() self.config = WOMBATModelConfig.from_dict( diff --git a/h2integrate/converters/iron/humbert_ewin_perf.py b/h2integrate/converters/iron/humbert_ewin_perf.py index 7a8888933..c820064e6 100644 --- a/h2integrate/converters/iron/humbert_ewin_perf.py +++ b/h2integrate/converters/iron/humbert_ewin_perf.py @@ -75,6 +75,8 @@ class HumbertEwinPerformanceComponent(PerformanceModelBaseClass): """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): self.commodity = "sponge_iron" self.commodity_rate_units = "kg/h" diff --git a/h2integrate/converters/iron/humbert_stinn_ewin_cost.py b/h2integrate/converters/iron/humbert_stinn_ewin_cost.py index 374ee1732..1967452ef 100644 --- a/h2integrate/converters/iron/humbert_stinn_ewin_cost.py +++ b/h2integrate/converters/iron/humbert_stinn_ewin_cost.py @@ -117,6 +117,8 @@ class HumbertStinnEwinCostComponent(CostModelBaseClass): """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): self.options.declare("driver_config", types=dict) self.options.declare("plant_config", types=dict) diff --git a/h2integrate/converters/iron/iron_dri_base.py b/h2integrate/converters/iron/iron_dri_base.py index 9887b76d7..b8577c599 100644 --- a/h2integrate/converters/iron/iron_dri_base.py +++ b/h2integrate/converters/iron/iron_dri_base.py @@ -30,6 +30,8 @@ class IronReductionPerformanceBaseConfig(BaseConfig): class IronReductionPlantBasePerformanceComponent(PerformanceModelBaseClass): + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "pig_iron" @@ -295,6 +297,8 @@ class IronReductionPlantBaseCostComponent(CostModelBaseClass): steel_to_iron_ratio (float): steel/pig iron ratio """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] diff --git a/h2integrate/converters/iron/iron_dri_plant.py b/h2integrate/converters/iron/iron_dri_plant.py index 25a631ff9..3d46f7555 100644 --- a/h2integrate/converters/iron/iron_dri_plant.py +++ b/h2integrate/converters/iron/iron_dri_plant.py @@ -15,6 +15,8 @@ class HydrogenIronReductionPlantCostComponent(IronReductionPlantBaseCostComponen steel_to_iron_ratio (float): steel/pig iron ratio """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.product = "h2_dri" super().setup() @@ -31,6 +33,8 @@ class NaturalGasIronReductionPlantCostComponent(IronReductionPlantBaseCostCompon steel_to_iron_ratio (float): steel/pig iron ratio """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.product = "ng_dri" super().setup() @@ -47,6 +51,8 @@ class HydrogenIronReductionPlantPerformanceComponent(IronReductionPlantBasePerfo steel_to_iron_ratio (float): steel/pig iron ratio """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.product = "h2_dri" self.feedstocks_to_units = { @@ -70,6 +76,8 @@ class NaturalGasIronReductionPlantPerformanceComponent(IronReductionPlantBasePer steel_to_iron_ratio (float): steel/pig iron ratio """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.feedstocks_to_units = { "natural_gas": "MMBtu/h", diff --git a/h2integrate/converters/iron/iron_transport.py b/h2integrate/converters/iron/iron_transport.py index 1502cdbc9..8dc17ace2 100644 --- a/h2integrate/converters/iron/iron_transport.py +++ b/h2integrate/converters/iron/iron_transport.py @@ -29,6 +29,8 @@ def __attrs_post_init__(self): class IronTransportPerformanceComponent(om.ExplicitComponent): + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): self.options.declare("driver_config", types=dict) self.options.declare("plant_config", types=dict) @@ -163,6 +165,8 @@ class IronTransportCostConfig(BaseConfig): class IronTransportCostComponent(CostModelBaseClass): + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): self.options.declare("driver_config", types=dict) self.options.declare("plant_config", types=dict) diff --git a/h2integrate/converters/iron/martin_mine_cost_model.py b/h2integrate/converters/iron/martin_mine_cost_model.py index 2f5554b57..3434b5a34 100644 --- a/h2integrate/converters/iron/martin_mine_cost_model.py +++ b/h2integrate/converters/iron/martin_mine_cost_model.py @@ -39,6 +39,8 @@ class MartinIronMineCostConfig(BaseConfig): class MartinIronMineCostComponent(CostModelBaseClass): + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): # merge inputs from performance parameters and cost parameters config_dict = merge_shared_inputs( diff --git a/h2integrate/converters/iron/martin_mine_perf_model.py b/h2integrate/converters/iron/martin_mine_perf_model.py index b886ca7cf..f625877a7 100644 --- a/h2integrate/converters/iron/martin_mine_perf_model.py +++ b/h2integrate/converters/iron/martin_mine_perf_model.py @@ -31,6 +31,8 @@ class MartinIronMinePerformanceConfig(BaseConfig): class MartinIronMinePerformanceComponent(PerformanceModelBaseClass): + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "iron_ore" diff --git a/h2integrate/converters/methanol/co2h_methanol_plant.py b/h2integrate/converters/methanol/co2h_methanol_plant.py index abf2cbbce..df8ba7ae7 100644 --- a/h2integrate/converters/methanol/co2h_methanol_plant.py +++ b/h2integrate/converters/methanol/co2h_methanol_plant.py @@ -49,6 +49,8 @@ class CO2HMethanolPlantPerformanceModel(MethanolPerformanceBaseClass): - methanol_out: methanol produced in kg/h """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] self.config = CO2HPerformanceConfig.from_dict( @@ -161,6 +163,8 @@ class CO2HMethanolPlantCostModel(MethanolCostBaseClass): co2_cost: annual cost of CO2 (USD/year) """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] self.config = CO2HCostConfig.from_dict( diff --git a/h2integrate/converters/methanol/methanol_baseclass.py b/h2integrate/converters/methanol/methanol_baseclass.py index ad0babc2a..0cd53d466 100644 --- a/h2integrate/converters/methanol/methanol_baseclass.py +++ b/h2integrate/converters/methanol/methanol_baseclass.py @@ -34,6 +34,8 @@ class MethanolPerformanceBaseClass(PerformanceModelBaseClass): - h2o_consumption: h2o consumption in kg/h """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "methanol" @@ -87,6 +89,8 @@ class MethanolCostBaseClass(CostModelBaseClass): - Variable_OpEx: all methanol plant variable operating expenses (vary with production rate) """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] super().setup() diff --git a/h2integrate/converters/methanol/smr_methanol_plant.py b/h2integrate/converters/methanol/smr_methanol_plant.py index 9ab9fe600..11e85166f 100644 --- a/h2integrate/converters/methanol/smr_methanol_plant.py +++ b/h2integrate/converters/methanol/smr_methanol_plant.py @@ -42,6 +42,8 @@ class SMRMethanolPlantPerformanceModel(MethanolPerformanceBaseClass): - electricity_out: hourly electricity production (kW*h/h) """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] self.config = SMRPerformanceConfig.from_dict( @@ -145,6 +147,8 @@ class SMRMethanolPlantCostModel(MethanolCostBaseClass): cost_year: dollar year for output costs """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = SMRCostConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost"), diff --git a/h2integrate/converters/natural_gas/dummy_gas_components.py b/h2integrate/converters/natural_gas/dummy_gas_components.py index 0249bfb23..9d49e6a4a 100644 --- a/h2integrate/converters/natural_gas/dummy_gas_components.py +++ b/h2integrate/converters/natural_gas/dummy_gas_components.py @@ -61,6 +61,8 @@ class SimpleGasProducerPerformance(PerformanceModelBaseClass): The outputs use random variations around base values. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "gas" @@ -139,6 +141,8 @@ class SimpleGasConsumerPerformance(PerformanceModelBaseClass): The primary commodity output is hydrogen (extracted from the gas stream). """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "hydrogen" @@ -207,6 +211,8 @@ class SimpleGasProducerCost(CostModelBaseClass): Simple cost model for the dummy gas producer. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = SimpleGasProducerCostConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost") @@ -238,6 +244,8 @@ class SimpleGasConsumerCost(CostModelBaseClass): Simple cost model for the dummy gas consumer. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = SimpleGasConsumerCostConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost") diff --git a/h2integrate/converters/natural_gas/natural_gas_cc_ct.py b/h2integrate/converters/natural_gas/natural_gas_cc_ct.py index a2d7bd985..5fe27f532 100644 --- a/h2integrate/converters/natural_gas/natural_gas_cc_ct.py +++ b/h2integrate/converters/natural_gas/natural_gas_cc_ct.py @@ -56,6 +56,8 @@ class NaturalGasPerformanceModel(PerformanceModelBaseClass): """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "electricity" @@ -246,6 +248,8 @@ class NaturalGasCostModel(CostModelBaseClass): cost_year (int): Dollar year for the costs """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = NaturalGasCostModelConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost"), diff --git a/h2integrate/converters/nitrogen/simple_ASU.py b/h2integrate/converters/nitrogen/simple_ASU.py index d32399a98..57c963c92 100644 --- a/h2integrate/converters/nitrogen/simple_ASU.py +++ b/h2integrate/converters/nitrogen/simple_ASU.py @@ -65,6 +65,8 @@ class SimpleASUPerformanceModel(PerformanceModelBaseClass): Air Separation Unit. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "nitrogen" @@ -278,6 +280,8 @@ def __attrs_post_init__(self): class SimpleASUCostModel(CostModelBaseClass): + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): self.options.declare("plant_config", types=dict) self.options.declare("tech_config", types=dict) diff --git a/h2integrate/converters/nuclear/nuclear_plant.py b/h2integrate/converters/nuclear/nuclear_plant.py index 624a3ecad..4ce3deb1f 100644 --- a/h2integrate/converters/nuclear/nuclear_plant.py +++ b/h2integrate/converters/nuclear/nuclear_plant.py @@ -33,6 +33,8 @@ class QuinnNuclearPerformanceModel(PerformanceModelBaseClass): https://doi.org/10.1016/j.apenergy.2023.120669 """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "electricity" @@ -124,6 +126,8 @@ class QuinnNuclearCostModel(CostModelBaseClass): https://doi.org/10.1016/j.apenergy.2023.120669 """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = QuinnNuclearCostModelConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost"), diff --git a/h2integrate/converters/solar/atb_res_com_pv_cost.py b/h2integrate/converters/solar/atb_res_com_pv_cost.py index a29f69e9e..1405720dd 100644 --- a/h2integrate/converters/solar/atb_res_com_pv_cost.py +++ b/h2integrate/converters/solar/atb_res_com_pv_cost.py @@ -29,6 +29,8 @@ class ATBResComPVCostModelConfig(CostModelBaseConfig): class ATBResComPVCostModel(CostModelBaseClass): + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = ATBResComPVCostModelConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost"), diff --git a/h2integrate/converters/solar/atb_utility_pv_cost.py b/h2integrate/converters/solar/atb_utility_pv_cost.py index aa00eb0ba..db87dbb94 100644 --- a/h2integrate/converters/solar/atb_utility_pv_cost.py +++ b/h2integrate/converters/solar/atb_utility_pv_cost.py @@ -26,6 +26,8 @@ class ATBUtilityPVCostModelConfig(CostModelBaseConfig): class ATBUtilityPVCostModel(CostModelBaseClass): + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = ATBUtilityPVCostModelConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost"), diff --git a/h2integrate/converters/solar/solar_baseclass.py b/h2integrate/converters/solar/solar_baseclass.py index c27ebb322..6f6b62bdc 100644 --- a/h2integrate/converters/solar/solar_baseclass.py +++ b/h2integrate/converters/solar/solar_baseclass.py @@ -2,6 +2,8 @@ class SolarPerformanceBaseClass(PerformanceModelBaseClass): + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "electricity" diff --git a/h2integrate/converters/solar/solar_pysam.py b/h2integrate/converters/solar/solar_pysam.py index b47811694..ba5eda2c8 100644 --- a/h2integrate/converters/solar/solar_pysam.py +++ b/h2integrate/converters/solar/solar_pysam.py @@ -142,6 +142,8 @@ class PYSAMSolarPlantPerformanceModel(SolarPerformanceBaseClass): It takes solar parameters as input and outputs power generation data. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): super().setup() diff --git a/h2integrate/converters/steel/steel.py b/h2integrate/converters/steel/steel.py index 753c26fd1..077fdf137 100644 --- a/h2integrate/converters/steel/steel.py +++ b/h2integrate/converters/steel/steel.py @@ -21,6 +21,8 @@ class SteelPerformanceModel(SteelPerformanceBaseClass): Computes annual steel production based on plant capacity and capacity factor. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): super().setup() self.config = SteelPerformanceModelConfig.from_dict( @@ -84,6 +86,8 @@ class SteelCostAndFinancialModel(SteelCostBaseClass): Includes CapEx, OpEx, and byproduct credits. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = SteelCostAndFinancialModelConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost"), diff --git a/h2integrate/converters/steel/steel_baseclass.py b/h2integrate/converters/steel/steel_baseclass.py index 16c11c032..4793638f1 100644 --- a/h2integrate/converters/steel/steel_baseclass.py +++ b/h2integrate/converters/steel/steel_baseclass.py @@ -2,6 +2,8 @@ class SteelPerformanceBaseClass(PerformanceModelBaseClass): + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "steel" @@ -26,6 +28,8 @@ def compute(self, inputs, outputs): class SteelCostBaseClass(CostModelBaseClass): + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): # Inputs for cost model configuration super().setup() diff --git a/h2integrate/converters/steel/steel_eaf_base.py b/h2integrate/converters/steel/steel_eaf_base.py index c4b2c8354..331d795e0 100644 --- a/h2integrate/converters/steel/steel_eaf_base.py +++ b/h2integrate/converters/steel/steel_eaf_base.py @@ -30,6 +30,8 @@ class ElectricArcFurnacePerformanceBaseConfig(BaseConfig): class ElectricArcFurnacePlantBasePerformanceComponent(PerformanceModelBaseClass): + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "steel" @@ -300,6 +302,8 @@ class ElectricArcFurnacePlantBaseCostComponent(CostModelBaseClass): coeff_df (pd.DataFrame): cost coefficient dataframe """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): n_timesteps = self.options["plant_config"]["plant"]["simulation"]["n_timesteps"] diff --git a/h2integrate/converters/steel/steel_eaf_plant.py b/h2integrate/converters/steel/steel_eaf_plant.py index 6aa5b5e93..277b72b1a 100644 --- a/h2integrate/converters/steel/steel_eaf_plant.py +++ b/h2integrate/converters/steel/steel_eaf_plant.py @@ -14,6 +14,8 @@ class HydrogenEAFPlantCostComponent(ElectricArcFurnacePlantBaseCostComponent): coeff_df (pd.DataFrame): cost coefficient dataframe """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.product = "h2_eaf" super().setup() @@ -29,6 +31,8 @@ class NaturalGasEAFPlantCostComponent(ElectricArcFurnacePlantBaseCostComponent): coeff_df (pd.DataFrame): cost coefficient dataframe """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.product = "ng_eaf" super().setup() @@ -44,6 +48,8 @@ class HydrogenEAFPlantPerformanceComponent(ElectricArcFurnacePlantBasePerformanc coeff_df (pd.DataFrame): performance coefficient dataframe """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.product = "h2_eaf" self.feedstocks_to_units = { @@ -67,6 +73,8 @@ class NaturalGasEAFPlantPerformanceComponent(ElectricArcFurnacePlantBasePerforma coeff_df (pd.DataFrame): performance coefficient dataframe """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.feedstocks_to_units = { "natural_gas": "MMBtu/h", diff --git a/h2integrate/converters/water/desal/desalination.py b/h2integrate/converters/water/desal/desalination.py index 230be70ed..b7399f4c8 100644 --- a/h2integrate/converters/water/desal/desalination.py +++ b/h2integrate/converters/water/desal/desalination.py @@ -32,6 +32,8 @@ class ReverseOsmosisPerformanceModel(DesalinationPerformanceBaseClass): Takes plantcapacitykgph input and outputs fresh water and electricity required. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): super().setup() self.config = ReverseOsmosisPerformanceModelConfig.from_dict( @@ -132,6 +134,8 @@ class ReverseOsmosisCostModel(DesalinationCostBaseClass): An OpenMDAO component that computes the cost of a reverse osmosis desalination system. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = ReverseOsmosisCostModelConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost"), diff --git a/h2integrate/converters/water/desal/desalination_baseclass.py b/h2integrate/converters/water/desal/desalination_baseclass.py index 55fd58c4c..08639bf98 100644 --- a/h2integrate/converters/water/desal/desalination_baseclass.py +++ b/h2integrate/converters/water/desal/desalination_baseclass.py @@ -2,6 +2,8 @@ class DesalinationPerformanceBaseClass(PerformanceModelBaseClass): + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "water" @@ -25,6 +27,8 @@ def compute(self, inputs, outputs): class DesalinationCostBaseClass(CostModelBaseClass): + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): super().setup() # Inputs for cost model configuration 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 780b5b950..62826cc7e 100644 --- a/h2integrate/converters/water_power/hydro_plant_run_of_river.py +++ b/h2integrate/converters/water_power/hydro_plant_run_of_river.py @@ -36,6 +36,8 @@ class RunOfRiverHydroPerformanceModel(PerformanceModelBaseClass): Computes annual electricity production based on water flow rate and turbine efficiency. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "electricity" @@ -107,6 +109,8 @@ class RunOfRiverHydroCostModel(CostModelBaseClass): Just a placeholder for now, but can be extended with more detailed cost models. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = RunOfRiverHydroCostConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost"), diff --git a/h2integrate/converters/water_power/pysam_marine_cost.py b/h2integrate/converters/water_power/pysam_marine_cost.py index 4e9553f1a..a082b0b45 100644 --- a/h2integrate/converters/water_power/pysam_marine_cost.py +++ b/h2integrate/converters/water_power/pysam_marine_cost.py @@ -159,6 +159,8 @@ class PySAMMarineCostModel(CostModelBaseClass): """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = PySAMMarineCostConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost"), diff --git a/h2integrate/converters/water_power/tidal_pysam.py b/h2integrate/converters/water_power/tidal_pysam.py index 63933d8bc..cc63323b5 100644 --- a/h2integrate/converters/water_power/tidal_pysam.py +++ b/h2integrate/converters/water_power/tidal_pysam.py @@ -120,6 +120,8 @@ class PySAMTidalPerformanceModel(PerformanceModelBaseClass): It takes tidal parameters as input and outputs power generation data. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "electricity" diff --git a/h2integrate/converters/wind/atb_wind_cost.py b/h2integrate/converters/wind/atb_wind_cost.py index 6065ce155..f4bb45336 100644 --- a/h2integrate/converters/wind/atb_wind_cost.py +++ b/h2integrate/converters/wind/atb_wind_cost.py @@ -48,6 +48,8 @@ class ATBWindPlantCostModel(CostModelBaseClass): Annual operating expenditure of the wind plant. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = ATBWindPlantCostModelConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost"), diff --git a/h2integrate/converters/wind/floris.py b/h2integrate/converters/wind/floris.py index 773e73a2a..25826f079 100644 --- a/h2integrate/converters/wind/floris.py +++ b/h2integrate/converters/wind/floris.py @@ -104,6 +104,8 @@ class FlorisWindPlantPerformanceModel(WindPerformanceBaseClass, CacheBaseClass): outputs power generation data. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.n_timesteps = int(self.options["plant_config"]["plant"]["simulation"]["n_timesteps"]) diff --git a/h2integrate/converters/wind/wind_plant_ard.py b/h2integrate/converters/wind/wind_plant_ard.py index 1e5de8b85..2115ef440 100644 --- a/h2integrate/converters/wind/wind_plant_ard.py +++ b/h2integrate/converters/wind/wind_plant_ard.py @@ -37,6 +37,8 @@ class WindArdPerformanceCompatibilityComponent(PerformanceModelBaseClass): H2Integrate. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "electricity" @@ -91,6 +93,8 @@ class WindArdCostCompatibilityComponent(CostModelBaseClass): requires a self.config attribute to be defined, so we create this minimal subclass. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = CostModelBaseConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost") diff --git a/h2integrate/converters/wind/wind_plant_baseclass.py b/h2integrate/converters/wind/wind_plant_baseclass.py index 0355996fb..69680c5d5 100644 --- a/h2integrate/converters/wind/wind_plant_baseclass.py +++ b/h2integrate/converters/wind/wind_plant_baseclass.py @@ -2,6 +2,8 @@ class WindPerformanceBaseClass(PerformanceModelBaseClass): + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "electricity" diff --git a/h2integrate/converters/wind/wind_pysam.py b/h2integrate/converters/wind/wind_pysam.py index c729395c9..ec703e6fa 100644 --- a/h2integrate/converters/wind/wind_pysam.py +++ b/h2integrate/converters/wind/wind_pysam.py @@ -182,6 +182,8 @@ class PYSAMWindPlantPerformanceModel(WindPerformanceBaseClass): It takes wind parameters as input and outputs power generation data. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): super().setup() diff --git a/h2integrate/storage/battery/atb_battery_cost.py b/h2integrate/storage/battery/atb_battery_cost.py index aeb15c598..cc55c78ee 100644 --- a/h2integrate/storage/battery/atb_battery_cost.py +++ b/h2integrate/storage/battery/atb_battery_cost.py @@ -61,6 +61,8 @@ class ATBBatteryCostModel(CostModelBaseClass): """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = ATBBatteryCostConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost"), diff --git a/h2integrate/storage/battery/pysam_battery.py b/h2integrate/storage/battery/pysam_battery.py index c20bc7b32..019ef46f1 100644 --- a/h2integrate/storage/battery/pysam_battery.py +++ b/h2integrate/storage/battery/pysam_battery.py @@ -95,6 +95,8 @@ class PySAMBatteryPerformanceModel(StoragePerformanceBase): Sets the battery control mode (power or current). """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() self.commodity = "electricity" diff --git a/h2integrate/storage/generic_storage_cost.py b/h2integrate/storage/generic_storage_cost.py index 92d03d083..667be6abf 100644 --- a/h2integrate/storage/generic_storage_cost.py +++ b/h2integrate/storage/generic_storage_cost.py @@ -50,6 +50,8 @@ class GenericStorageCostModel(CostModelBaseClass): """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = GenericStorageCostConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost"), diff --git a/h2integrate/storage/hydrogen/h2_storage_cost.py b/h2integrate/storage/hydrogen/h2_storage_cost.py index 509b13491..0b34b3782 100644 --- a/h2integrate/storage/hydrogen/h2_storage_cost.py +++ b/h2integrate/storage/hydrogen/h2_storage_cost.py @@ -74,6 +74,8 @@ def make_model_dict(self): class HydrogenStorageBaseCostModel(CostModelBaseClass): + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() @@ -151,6 +153,8 @@ class LinedRockCavernStorageCostModel(HydrogenStorageBaseCostModel): [3] HDSAM V4.0 Gaseous H2 Geologic Storage sheet """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): """Calculate installed capital and O&M costs for lined rock cavern hydrogen storage. @@ -276,6 +280,8 @@ class SaltCavernStorageCostModel(HydrogenStorageBaseCostModel): [3] HDSAM V4.0 Gaseous H2 Geologic Storage sheet """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): """Calculate installed capital and O&M costs for salt cavern hydrogen storage. @@ -406,6 +412,8 @@ class PipeStorageCostModel(HydrogenStorageBaseCostModel): [3] HDSAM V4.0 Gaseous H2 Geologic Storage sheet """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): """Calculate installed capital and O&M costs for underground pipe hydrogen storage. diff --git a/h2integrate/storage/hydrogen/mch_storage.py b/h2integrate/storage/hydrogen/mch_storage.py index f6a53b663..ceef6320e 100644 --- a/h2integrate/storage/hydrogen/mch_storage.py +++ b/h2integrate/storage/hydrogen/mch_storage.py @@ -54,6 +54,8 @@ class MCHTOLStorageCostModel(CostModelBaseClass): """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): super().initialize() diff --git a/h2integrate/storage/simple_storage_auto_sizing.py b/h2integrate/storage/simple_storage_auto_sizing.py index 59dc47d54..712eafa72 100644 --- a/h2integrate/storage/simple_storage_auto_sizing.py +++ b/h2integrate/storage/simple_storage_auto_sizing.py @@ -91,6 +91,8 @@ class StorageAutoSizingModel(StoragePerformanceBase): capacity calculated. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = StorageSizingModelConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), diff --git a/h2integrate/storage/storage_baseclass.py b/h2integrate/storage/storage_baseclass.py index 65fa00f3e..34fd43863 100644 --- a/h2integrate/storage/storage_baseclass.py +++ b/h2integrate/storage/storage_baseclass.py @@ -45,6 +45,8 @@ class StoragePerformanceBase(PerformanceModelBaseClass): dispatch decisions using solver inputs. """ + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): """Set up the storage performance model in OpenMDAO. diff --git a/h2integrate/storage/storage_performance_model.py b/h2integrate/storage/storage_performance_model.py index a58137235..90159bf26 100644 --- a/h2integrate/storage/storage_performance_model.py +++ b/h2integrate/storage/storage_performance_model.py @@ -119,6 +119,8 @@ def __attrs_post_init__(self): class StoragePerformanceModel(StoragePerformanceBase): """OpenMDAO component for a storage component.""" + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = StoragePerformanceModelConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), From 22b6cf77120fb5e65fb0d894456fb599388518b1 Mon Sep 17 00:00:00 2001 From: John Jasa Date: Sat, 11 Apr 2026 12:12:48 -0600 Subject: [PATCH 15/18] Adding time step bounds to the combiner/splitter/transporters --- h2integrate/transporters/generic_combiner.py | 2 ++ h2integrate/transporters/generic_splitter.py | 2 ++ h2integrate/transporters/generic_summer.py | 2 ++ h2integrate/transporters/generic_transporter.py | 2 ++ 4 files changed, 8 insertions(+) diff --git a/h2integrate/transporters/generic_combiner.py b/h2integrate/transporters/generic_combiner.py index d6aea2e4b..e51d71b46 100644 --- a/h2integrate/transporters/generic_combiner.py +++ b/h2integrate/transporters/generic_combiner.py @@ -38,6 +38,8 @@ class GenericCombinerPerformanceModel(om.ExplicitComponent): the output commodity profile is the element-wise sum of all input profiles. """ + _time_step_bounds = (1, float("inf")) # (min, max) time step lengths compatible with this model + def initialize(self): self.options.declare("driver_config", types=dict) self.options.declare("plant_config", types=dict) diff --git a/h2integrate/transporters/generic_splitter.py b/h2integrate/transporters/generic_splitter.py index 185c4684f..22d067c6c 100644 --- a/h2integrate/transporters/generic_splitter.py +++ b/h2integrate/transporters/generic_splitter.py @@ -63,6 +63,8 @@ class GenericSplitterPerformanceModel(om.ExplicitComponent): losses or other considerations from system components. """ + _time_step_bounds = (1, float("inf")) # (min, max) time step lengths compatible with this model + def initialize(self): self.options.declare("driver_config", types=dict, default={}) self.options.declare("plant_config", types=dict, default={}) diff --git a/h2integrate/transporters/generic_summer.py b/h2integrate/transporters/generic_summer.py index 32e27c338..99a2a8b47 100644 --- a/h2integrate/transporters/generic_summer.py +++ b/h2integrate/transporters/generic_summer.py @@ -26,6 +26,8 @@ class GenericSummerPerformanceModel(om.ExplicitComponent): Sum the production or consumption profile of some commodity from a single source. """ + _time_step_bounds = (1, float("inf")) # (min, max) time step lengths compatible with this model + def initialize(self): self.options.declare("driver_config", types=dict) self.options.declare("plant_config", types=dict) diff --git a/h2integrate/transporters/generic_transporter.py b/h2integrate/transporters/generic_transporter.py index c94606ac7..af08e938e 100644 --- a/h2integrate/transporters/generic_transporter.py +++ b/h2integrate/transporters/generic_transporter.py @@ -23,6 +23,8 @@ class GenericTransporterPerformanceModel(om.ExplicitComponent): losses or other considerations from system components. """ + _time_step_bounds = (1, float("inf")) # (min, max) time step lengths compatible with this model + def initialize(self): self.options.declare("driver_config", types=dict) self.options.declare("plant_config", types=dict) From 4b50db02295938c52628818980b3984d81ebba57 Mon Sep 17 00:00:00 2001 From: John Jasa Date: Sat, 11 Apr 2026 12:32:43 -0600 Subject: [PATCH 16/18] Updates for tests --- .../test_custom_electrolyzer_cost_model.py | 1 + h2integrate/core/test/test_framework.py | 29 ------------------- .../transporters/gas_stream_combiner.py | 2 ++ h2integrate/transporters/generic_combiner.py | 2 +- h2integrate/transporters/generic_splitter.py | 2 +- h2integrate/transporters/generic_summer.py | 2 +- .../transporters/generic_transporter.py | 2 +- 7 files changed, 7 insertions(+), 33 deletions(-) diff --git a/h2integrate/converters/hydrogen/test/test_custom_electrolyzer_cost_model.py b/h2integrate/converters/hydrogen/test/test_custom_electrolyzer_cost_model.py index 3a4771fd8..e4aa77f05 100644 --- a/h2integrate/converters/hydrogen/test/test_custom_electrolyzer_cost_model.py +++ b/h2integrate/converters/hydrogen/test/test_custom_electrolyzer_cost_model.py @@ -26,6 +26,7 @@ def test_custom_electrolyzer_cost_model(subtests): "plant_life": 30, "simulation": { "n_timesteps": 8760, # Default number of timesteps for the simulation + "dt": 3600, # Default time step length in seconds (1 hour) }, }, } diff --git a/h2integrate/core/test/test_framework.py b/h2integrate/core/test/test_framework.py index cf8b6a81e..1819f51b9 100644 --- a/h2integrate/core/test/test_framework.py +++ b/h2integrate/core/test/test_framework.py @@ -212,35 +212,6 @@ class DummyModel: model._check_time_step("DummyModel", DummyModel) -@pytest.mark.unit -def test_check_time_step_without_bounds_requires_one_hour_dt(): - class DummyModelNoBounds: - pass - - model = object.__new__(H2IntegrateModel) - model.plant_config = {"plant": {"simulation": {"dt": 1800}}} - - with pytest.raises( - ValueError, - match=( - r"Model 'DummyModelNoBounds' only supports a 1-hour time step " - r"\(dt=3600\), but dt=1800 was specified" - ), - ): - model._check_time_step("DummyModelNoBounds", DummyModelNoBounds) - - -@pytest.mark.unit -def test_check_time_step_without_bounds_allows_one_hour_dt(): - class DummyModelNoBounds: - pass - - model = object.__new__(H2IntegrateModel) - model.plant_config = {"plant": {"simulation": {"dt": 3600}}} - - model._check_time_step("DummyModelNoBounds", DummyModelNoBounds) - - @pytest.mark.unit def test_technology_connections(temp_dir): # Path to the original plant_config.yaml and high-level yaml in the example directory diff --git a/h2integrate/transporters/gas_stream_combiner.py b/h2integrate/transporters/gas_stream_combiner.py index 008199af0..f5fe23a63 100644 --- a/h2integrate/transporters/gas_stream_combiner.py +++ b/h2integrate/transporters/gas_stream_combiner.py @@ -41,6 +41,8 @@ class GasStreamCombinerPerformanceModel(om.ExplicitComponent): mass-weighted averages of the input streams. """ + _time_step_bounds = (1, 1e9) # (min, max) time step lengths compatible with this model + def initialize(self): self.options.declare("driver_config", types=dict) self.options.declare("plant_config", types=dict) diff --git a/h2integrate/transporters/generic_combiner.py b/h2integrate/transporters/generic_combiner.py index e51d71b46..3404b6fbb 100644 --- a/h2integrate/transporters/generic_combiner.py +++ b/h2integrate/transporters/generic_combiner.py @@ -38,7 +38,7 @@ class GenericCombinerPerformanceModel(om.ExplicitComponent): the output commodity profile is the element-wise sum of all input profiles. """ - _time_step_bounds = (1, float("inf")) # (min, max) time step lengths compatible with this model + _time_step_bounds = (1, 1e9) # (min, max) time step lengths compatible with this model def initialize(self): self.options.declare("driver_config", types=dict) diff --git a/h2integrate/transporters/generic_splitter.py b/h2integrate/transporters/generic_splitter.py index 22d067c6c..4431dbb71 100644 --- a/h2integrate/transporters/generic_splitter.py +++ b/h2integrate/transporters/generic_splitter.py @@ -63,7 +63,7 @@ class GenericSplitterPerformanceModel(om.ExplicitComponent): losses or other considerations from system components. """ - _time_step_bounds = (1, float("inf")) # (min, max) time step lengths compatible with this model + _time_step_bounds = (1, 1e9) # (min, max) time step lengths compatible with this model def initialize(self): self.options.declare("driver_config", types=dict, default={}) diff --git a/h2integrate/transporters/generic_summer.py b/h2integrate/transporters/generic_summer.py index 99a2a8b47..2e3f64906 100644 --- a/h2integrate/transporters/generic_summer.py +++ b/h2integrate/transporters/generic_summer.py @@ -26,7 +26,7 @@ class GenericSummerPerformanceModel(om.ExplicitComponent): Sum the production or consumption profile of some commodity from a single source. """ - _time_step_bounds = (1, float("inf")) # (min, max) time step lengths compatible with this model + _time_step_bounds = (1, 1e9) # (min, max) time step lengths compatible with this model def initialize(self): self.options.declare("driver_config", types=dict) diff --git a/h2integrate/transporters/generic_transporter.py b/h2integrate/transporters/generic_transporter.py index af08e938e..efdef85f3 100644 --- a/h2integrate/transporters/generic_transporter.py +++ b/h2integrate/transporters/generic_transporter.py @@ -23,7 +23,7 @@ class GenericTransporterPerformanceModel(om.ExplicitComponent): losses or other considerations from system components. """ - _time_step_bounds = (1, float("inf")) # (min, max) time step lengths compatible with this model + _time_step_bounds = (1, 1e9) # (min, max) time step lengths compatible with this model def initialize(self): self.options.declare("driver_config", types=dict) From 467caeab567c5cd087305cec6480e1818a48277b Mon Sep 17 00:00:00 2001 From: John Jasa Date: Sat, 11 Apr 2026 13:04:36 -0600 Subject: [PATCH 17/18] Adding time step bounds to the custom paper mill performance model --- examples/06_custom_tech/user_defined_model/paper_mill.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/06_custom_tech/user_defined_model/paper_mill.py b/examples/06_custom_tech/user_defined_model/paper_mill.py index 34adef7b3..ab4e9a6d6 100644 --- a/examples/06_custom_tech/user_defined_model/paper_mill.py +++ b/examples/06_custom_tech/user_defined_model/paper_mill.py @@ -16,6 +16,8 @@ class PaperMillConfig(BaseConfig): class PaperMillPerformance(om.ExplicitComponent): + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def initialize(self): self.options.declare("driver_config", types=dict) self.options.declare("plant_config", types=dict) From 5bc9b5e77af32e1c50177c558aabcf636b75cfd9 Mon Sep 17 00:00:00 2001 From: John Jasa Date: Sat, 11 Apr 2026 15:20:51 -0600 Subject: [PATCH 18/18] Added time step bounds to paper mill cost --- examples/06_custom_tech/user_defined_model/paper_mill.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/06_custom_tech/user_defined_model/paper_mill.py b/examples/06_custom_tech/user_defined_model/paper_mill.py index ab4e9a6d6..8fbc9beb0 100644 --- a/examples/06_custom_tech/user_defined_model/paper_mill.py +++ b/examples/06_custom_tech/user_defined_model/paper_mill.py @@ -55,6 +55,8 @@ class PaperMillCostConfig(CostModelBaseConfig): class PaperMillCost(CostModelBaseClass): + _time_step_bounds = (3600, 3600) # (min, max) time step lengths compatible with this model + def setup(self): self.config = PaperMillCostConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "cost")