From 1b9f88d371d266832cb7b215db601be4c7d47dfb Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:17:53 -0600 Subject: [PATCH 01/17] renamed outputs in converter openloop controllers --- .../demand_openloop_converter_controller.py | 8 +++++--- .../flexible_demand_openloop_controller.py | 10 +++++----- .../converters/openloop_controller_base.py | 14 +++++++------- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/h2integrate/control/control_strategies/converters/demand_openloop_converter_controller.py b/h2integrate/control/control_strategies/converters/demand_openloop_converter_controller.py index 7615e2c21..392f39b11 100644 --- a/h2integrate/control/control_strategies/converters/demand_openloop_converter_controller.py +++ b/h2integrate/control/control_strategies/converters/demand_openloop_converter_controller.py @@ -66,12 +66,14 @@ def compute(self, inputs, outputs): remaining_demand = inputs[f"{commodity}_demand"] - inputs[f"{commodity}_in"] # Calculate missed load and curtailed production - outputs[f"{commodity}_unmet_demand"] = np.where(remaining_demand > 0, remaining_demand, 0) - outputs[f"{commodity}_unused_commodity"] = np.where( + outputs[f"unmet_{commodity}_demand_out"] = np.where( + remaining_demand > 0, remaining_demand, 0 + ) + outputs[f"unused_{commodity}_out"] = np.where( remaining_demand < 0, -1 * remaining_demand, 0 ) # Calculate actual output based on demand met and curtailment outputs[f"{commodity}_set_point"] = ( - inputs[f"{commodity}_in"] - outputs[f"{commodity}_unused_commodity"] + inputs[f"{commodity}_in"] - outputs[f"unused_{commodity}_out"] ) diff --git a/h2integrate/control/control_strategies/converters/flexible_demand_openloop_controller.py b/h2integrate/control/control_strategies/converters/flexible_demand_openloop_controller.py index 1cc5e8bba..3ed9fae0d 100644 --- a/h2integrate/control/control_strategies/converters/flexible_demand_openloop_controller.py +++ b/h2integrate/control/control_strategies/converters/flexible_demand_openloop_controller.py @@ -268,10 +268,10 @@ def compute(self, inputs, outputs): if self.config.min_utilization == 1.0: # Calculate missed load and curtailed production - outputs[f"{commodity}_unmet_demand"] = np.where( + outputs[f"unmet_{commodity}_demand_out"] = np.where( remaining_demand > 0, remaining_demand, 0 ) - outputs[f"{commodity}_unused_commodity"] = np.where( + outputs[f"unused_{commodity}_out"] = np.where( remaining_demand < 0, -1 * remaining_demand, 0 ) else: @@ -289,14 +289,14 @@ def compute(self, inputs, outputs): outputs[f"{commodity}_flexible_demand_profile"] = flexible_demand_profile flexible_remaining_demand = flexible_demand_profile - inputs[f"{commodity}_in"] - outputs[f"{commodity}_unmet_demand"] = np.where( + outputs[f"unmet_{commodity}_demand_out"] = np.where( flexible_remaining_demand > 0, flexible_remaining_demand, 0 ) - outputs[f"{commodity}_unused_commodity"] = np.where( + outputs[f"unused_{commodity}_out"] = np.where( flexible_remaining_demand < 0, -1 * flexible_remaining_demand, 0 ) # Calculate actual output based on demand met and curtailment outputs[f"{commodity}_set_point"] = ( - inputs[f"{commodity}_in"] - outputs[f"{commodity}_unused_commodity"] + inputs[f"{commodity}_in"] - outputs[f"unused_{commodity}_out"] ) diff --git a/h2integrate/control/control_strategies/converters/openloop_controller_base.py b/h2integrate/control/control_strategies/converters/openloop_controller_base.py index a4818301d..993ba76c6 100644 --- a/h2integrate/control/control_strategies/converters/openloop_controller_base.py +++ b/h2integrate/control/control_strategies/converters/openloop_controller_base.py @@ -82,7 +82,7 @@ def setup(self): ) self.add_output( - f"{commodity}_unmet_demand", + f"unmet_{commodity}_demand_out", val=self.config.demand_profile, shape=(n_timesteps), units=self.config.commodity_rate_units, @@ -90,7 +90,7 @@ def setup(self): ) self.add_output( - f"{commodity}_unused_commodity", + f"unused_{commodity}_out", val=0.0, shape=(n_timesteps), units=self.config.commodity_rate_units, @@ -105,11 +105,11 @@ def setup(self): desc=f"Production profile of {commodity}", ) - self.add_output( - f"total_{commodity}_unmet_demand", - units=self.config.commodity_rate_units, - desc="Total unmet demand", - ) + # self.add_output( + # f"total_{commodity}_unmet_demand", + # units=self.config.commodity_rate_units, + # desc="Total unmet demand", + # ) def compute(): """This method must be implemented by subclasses to define the From 2b3d0c1480fb959e6425e8d421aa7eea77f60037 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:26:41 -0600 Subject: [PATCH 02/17] updated tests and examples with updated naming --- examples/19_simple_dispatch/run_wind_battery.py | 4 ++-- examples/23_solar_wind_ng_demand/plant_config.yaml | 2 +- examples/24_solar_battery_grid/README.md | 12 ++++++------ .../demand_openloop_converter_controller.py | 6 +++--- .../control/test/test_openloop_controllers.py | 10 +++++----- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/examples/19_simple_dispatch/run_wind_battery.py b/examples/19_simple_dispatch/run_wind_battery.py index 7fff85678..8e2ecdaf6 100644 --- a/examples/19_simple_dispatch/run_wind_battery.py +++ b/examples/19_simple_dispatch/run_wind_battery.py @@ -42,14 +42,14 @@ ) ax[1].plot( range(start_hour, end_hour), - model.prob.get_val("battery.electricity_unused_commodity", units="MW")[start_hour:end_hour], + model.prob.get_val("battery.unused_electricity_out", units="MW")[start_hour:end_hour], linestyle=":", label="Unused Electricity commodity (MW)", linewidth=2, ) ax[1].plot( range(start_hour, end_hour), - model.prob.get_val("battery.electricity_unmet_demand", units="MW")[start_hour:end_hour], + model.prob.get_val("battery.unmet_electricity_demand_out", units="MW")[start_hour:end_hour], linestyle=":", label="Electricity Unmet Demand (MW)", linewidth=2, diff --git a/examples/23_solar_wind_ng_demand/plant_config.yaml b/examples/23_solar_wind_ng_demand/plant_config.yaml index 555b9b13e..69fc71a98 100644 --- a/examples/23_solar_wind_ng_demand/plant_config.yaml +++ b/examples/23_solar_wind_ng_demand/plant_config.yaml @@ -27,7 +27,7 @@ technology_interconnections: # connect NG feedstock to NG plant - [combiner, electrical_load_demand, [electricity_out, electricity_in]] # subtract wind and solar from demand - - [electrical_load_demand, natural_gas_plant, [electricity_unmet_demand, electricity_demand]] + - [electrical_load_demand, natural_gas_plant, [unmet_electricity_demand_out, electricity_demand]] # give remaining load demand to natural gas plant - [combiner, fin_combiner, electricity, cable] - [natural_gas_plant, fin_combiner, electricity, cable] diff --git a/examples/24_solar_battery_grid/README.md b/examples/24_solar_battery_grid/README.md index 2cdf79ef6..cce607d62 100644 --- a/examples/24_solar_battery_grid/README.md +++ b/examples/24_solar_battery_grid/README.md @@ -53,8 +53,8 @@ The grid performance model handles: - `electricity_in`: Power flowing INTO the grid (selling to grid) - limited by interconnection size - `electricity_out`: Power flowing OUT OF the grid (buying from grid) - limited by interconnection size - `electricity_sold`: Actual electricity sold (up to interconnection limit) -- `electricity_unmet_demand`: Demand that couldn't be met due to interconnection limit -- `electricity_excess`: Electricity that couldn't be sold due to interconnection limit +- `unmet_electricity_demand_out`: Demand that couldn't be met due to interconnection limit +- `unused_electricity_out`: Electricity that couldn't be sold due to interconnection limit **Cost Model:** - CapEx: Based on interconnection size ($/kW) plus fixed costs @@ -72,16 +72,16 @@ Solar → Battery → [Grid Buy (purchases) | Grid Sell (sales)] - Solar generates electricity - Battery stores excess and follows 100 MW demand profile -- Grid Buy purchases electricity when battery cannot meet demand (via `electricity_unmet_demand` → `electricity_demand` connection) -- Grid Sell accepts excess electricity when battery has surplus (via `electricity_unused_commodity` → `electricity_in` connection) +- Grid Buy purchases electricity when battery cannot meet demand (via `unmet_electricity_demand_out` → `electricity_demand` connection) +- Grid Sell accepts excess electricity when battery has surplus (via `unused_electricity_out` → `electricity_in` connection) ### Technology Interconnections ```yaml technology_interconnections: [ ["solar", "battery", "electricity", "cable"], - ["battery", "grid_buy", ["electricity_unmet_demand", "electricity_demand"]], - ["battery", "grid_sell", ["electricity_unused_commodity", "electricity_in"]] + ["battery", "grid_buy", ["unmet_electricity_demand_out", "electricity_demand"]], + ["battery", "grid_sell", ["unused_electricity_out", "electricity_in"]] ] ``` diff --git a/h2integrate/control/control_strategies/converters/demand_openloop_converter_controller.py b/h2integrate/control/control_strategies/converters/demand_openloop_converter_controller.py index 392f39b11..8af224688 100644 --- a/h2integrate/control/control_strategies/converters/demand_openloop_converter_controller.py +++ b/h2integrate/control/control_strategies/converters/demand_openloop_converter_controller.py @@ -54,9 +54,9 @@ def compute(self, inputs, outputs): outputs (dict-like): Mapping of output variable names where results will be written, including: - * ``{commodity}_unmet_demand``: Unmet demand. - * ``{commodity}_unused_commodity``: Curtailed production. - * ``{commodity}_out``: Actual output delivered. + * ``unmet_{commodity}_demand_out``: Unmet demand. + * ``unused_{commodity}_out``: Curtailed production. + * ``{commodity}_set_point``: Actual output delivered. Notes: All variables operate on a per-timestep basis and typically have diff --git a/h2integrate/control/test/test_openloop_controllers.py b/h2integrate/control/test/test_openloop_controllers.py index 402f201b9..e96b8f76c 100644 --- a/h2integrate/control/test/test_openloop_controllers.py +++ b/h2integrate/control/test/test_openloop_controllers.py @@ -562,12 +562,12 @@ def test_demand_converter_controller(subtests): ) with subtests.test("Check curtailment"): - assert prob.get_val("hydrogen_unused_commodity") == pytest.approx( + assert prob.get_val("unused_hydrogen_out") == pytest.approx( [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 2.0, 3.0, 4.0] ) with subtests.test("Check missed load"): - assert prob.get_val("hydrogen_unmet_demand") == pytest.approx( + assert prob.get_val("unmet_hydrogen_demand_out") == pytest.approx( [5.0, 4.0, 3.0, 2.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0] ) @@ -644,7 +644,7 @@ def test_flexible_demand_converter_controller(subtests, variable_h2_production_p assert np.all(flexible_total_demand <= end_use_rated_demand) with subtests.test("Check curtailment"): # failed - assert np.sum(prob.get_val("hydrogen_unused_commodity", units="kg")) == pytest.approx(6.6) + assert np.sum(prob.get_val("unused_hydrogen_out", units="kg")) == pytest.approx(6.6) # check ramping constraints and turndown constraints are met with subtests.test("Check turndown ratio constraint"): @@ -672,13 +672,13 @@ def test_flexible_demand_converter_controller(subtests, variable_h2_production_p # any commodity in) with subtests.test("Check that flexible demand is greater than hydrogen_in"): hydrogen_available = variable_h2_production_profile - prob.get_val( - "hydrogen_unused_commodity", units="kg" + "unused_hydrogen_out", units="kg" ) assert np.all(flexible_total_demand >= hydrogen_available) with subtests.test("Check that remaining demand was calculated properly"): unmet_demand = flexible_total_demand - hydrogen_available - assert np.all(unmet_demand == prob.get_val("hydrogen_unmet_demand", units="kg")) + assert np.all(unmet_demand == prob.get_val("unmet_hydrogen_demand_out", units="kg")) @pytest.mark.regression From eab6d09ef2fc99be985f5f3e948226a39d950980 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:39:06 -0600 Subject: [PATCH 03/17] added performance model outputs to the converter control strategies --- .../demand_openloop_converter_controller.py | 29 ++++++-- .../flexible_demand_openloop_controller.py | 60 +++++++++++------ .../converters/openloop_controller_base.py | 67 ++++++++----------- 3 files changed, 89 insertions(+), 67 deletions(-) diff --git a/h2integrate/control/control_strategies/converters/demand_openloop_converter_controller.py b/h2integrate/control/control_strategies/converters/demand_openloop_converter_controller.py index 8af224688..f7d59b43a 100644 --- a/h2integrate/control/control_strategies/converters/demand_openloop_converter_controller.py +++ b/h2integrate/control/control_strategies/converters/demand_openloop_converter_controller.py @@ -62,18 +62,35 @@ def compute(self, inputs, outputs): All variables operate on a per-timestep basis and typically have array shape ``(n_timesteps,)``. """ - commodity = self.config.commodity - remaining_demand = inputs[f"{commodity}_demand"] - inputs[f"{commodity}_in"] + remaining_demand = inputs[f"{self.commodity}_demand"] - inputs[f"{self.commodity}_in"] # Calculate missed load and curtailed production - outputs[f"unmet_{commodity}_demand_out"] = np.where( + outputs[f"unmet_{self.commodity}_demand_out"] = np.where( remaining_demand > 0, remaining_demand, 0 ) - outputs[f"unused_{commodity}_out"] = np.where( + outputs[f"unused_{self.commodity}_out"] = np.where( remaining_demand < 0, -1 * remaining_demand, 0 ) # Calculate actual output based on demand met and curtailment - outputs[f"{commodity}_set_point"] = ( - inputs[f"{commodity}_in"] - outputs[f"unused_{commodity}_out"] + outputs[f"{self.commodity}_set_point"] = ( + inputs[f"{self.commodity}_in"] - outputs[f"unused_{self.commodity}_out"] + ) + + # Calculate performance model outputs + outputs[f"{self.commodity}_out"] = ( + inputs[f"{self.commodity}_in"] - outputs[f"unused_{self.commodity}_out"] + ) + + outputs[f"rated_{self.commodity}_production"] = inputs[f"{self.commodity}_demand"].mean() + + outputs[f"total_{self.commodity}_produced"] = np.sum(outputs[f"{self.commodity}_out"]) * ( + self.dt / 3600 + ) + outputs[f"annual_{self.commodity}_produced"] = ( + outputs[f"total_{self.commodity}_produced"] / self.fraction_of_year_simulated + ) + + outputs["capacity_factor"] = ( + outputs[f"{self.commodity}_set_point"].sum() / inputs[f"{self.commodity}_demand"].sum() ) diff --git a/h2integrate/control/control_strategies/converters/flexible_demand_openloop_controller.py b/h2integrate/control/control_strategies/converters/flexible_demand_openloop_controller.py index 3ed9fae0d..475c9caf8 100644 --- a/h2integrate/control/control_strategies/converters/flexible_demand_openloop_controller.py +++ b/h2integrate/control/control_strategies/converters/flexible_demand_openloop_controller.py @@ -64,15 +64,12 @@ def setup(self): ) super().setup() - n_timesteps = int(self.options["plant_config"]["plant"]["simulation"]["n_timesteps"]) - commodity = self.config.commodity - self.add_input( - f"rated_{commodity}_demand", + f"rated_{self.commodity}_demand", val=self.config.demand_profile, - shape=(n_timesteps), - units=self.config.commodity_rate_units, - desc=f"Rated demand of {commodity}", + shape=self.n_timesteps, + units=self.commodity_rate_units, + desc=f"Rated demand of {self.commodity}", ) self.add_input( @@ -104,11 +101,11 @@ def setup(self): ) self.add_output( - f"{commodity}_flexible_demand_profile", + f"{self.commodity}_flexible_demand_profile", val=0.0, - shape=(n_timesteps), + shape=self.n_timesteps, units=self.config.commodity_rate_units, - desc=f"Flexible demand profile of {commodity}", + desc=f"Flexible demand profile of {self.commodity}", ) def adjust_demand_for_ramping(self, pre_demand_met_clipped, demand_bounds, ramp_rate_bounds): @@ -263,15 +260,15 @@ def compute(self, inputs, outputs): outputs (dict-like): Mapping where computed outputs are written. """ - commodity = self.config.commodity - remaining_demand = inputs[f"{commodity}_demand"] - inputs[f"{commodity}_in"] + + remaining_demand = inputs[f"{self.commodity}_demand"] - inputs[f"{self.commodity}_in"] if self.config.min_utilization == 1.0: # Calculate missed load and curtailed production - outputs[f"unmet_{commodity}_demand_out"] = np.where( + outputs[f"unmet_{self.commodity}_demand_out"] = np.where( remaining_demand > 0, remaining_demand, 0 ) - outputs[f"unused_{commodity}_out"] = np.where( + outputs[f"unused_{self.commodity}_out"] = np.where( remaining_demand < 0, -1 * remaining_demand, 0 ) else: @@ -280,23 +277,42 @@ def compute(self, inputs, outputs): curtailed = np.where(remaining_demand < 0, -1 * remaining_demand, 0) # subtract out the excess input commodity - inflexible_out = inputs[f"{commodity}_in"] - curtailed + inflexible_out = inputs[f"{self.commodity}_in"] - curtailed flexible_demand_profile = self.make_flexible_demand( - inputs[f"{commodity}_demand"], inflexible_out, inputs + inputs[f"{self.commodity}_demand"], inflexible_out, inputs ) - outputs[f"{commodity}_flexible_demand_profile"] = flexible_demand_profile - flexible_remaining_demand = flexible_demand_profile - inputs[f"{commodity}_in"] + outputs[f"{self.commodity}_flexible_demand_profile"] = flexible_demand_profile + flexible_remaining_demand = flexible_demand_profile - inputs[f"{self.commodity}_in"] - outputs[f"unmet_{commodity}_demand_out"] = np.where( + outputs[f"unmet_{self.commodity}_demand_out"] = np.where( flexible_remaining_demand > 0, flexible_remaining_demand, 0 ) - outputs[f"unused_{commodity}_out"] = np.where( + outputs[f"unused_{self.commodity}_out"] = np.where( flexible_remaining_demand < 0, -1 * flexible_remaining_demand, 0 ) # Calculate actual output based on demand met and curtailment - outputs[f"{commodity}_set_point"] = ( - inputs[f"{commodity}_in"] - outputs[f"unused_{commodity}_out"] + outputs[f"{self.commodity}_set_point"] = ( + inputs[f"{self.commodity}_in"] - outputs[f"unused_{self.commodity}_out"] + ) + + # Calculate performance model outputs + outputs[f"{self.commodity}_out"] = ( + inputs[f"{self.commodity}_in"] - outputs[f"unused_{self.commodity}_out"] + ) + outputs[f"rated_{self.commodity}_production"] = inputs[ + f"rated_{self.commodity}_demand" + ].mean() + + outputs[f"total_{self.commodity}_produced"] = np.sum(outputs[f"{self.commodity}_out"]) * ( + self.dt / 3600 + ) + outputs[f"annual_{self.commodity}_produced"] = ( + outputs[f"total_{self.commodity}_produced"] / self.fraction_of_year_simulated + ) + + outputs["capacity_factor"] = ( + outputs[f"{self.commodity}_set_point"].sum() / inputs[f"{self.commodity}_demand"].sum() ) diff --git a/h2integrate/control/control_strategies/converters/openloop_controller_base.py b/h2integrate/control/control_strategies/converters/openloop_controller_base.py index 993ba76c6..abb3bb38c 100644 --- a/h2integrate/control/control_strategies/converters/openloop_controller_base.py +++ b/h2integrate/control/control_strategies/converters/openloop_controller_base.py @@ -1,7 +1,7 @@ -import openmdao.api as om from attrs import field, define from h2integrate.core.utilities import BaseConfig +from h2integrate.core.model_baseclasses import PerformanceModelBaseClass @define(kw_only=True) @@ -25,7 +25,7 @@ class ConverterOpenLoopControlBaseConfig(BaseConfig): demand_profile: int | float | list = field() -class ConverterOpenLoopControlBase(om.ExplicitComponent): +class ConverterOpenLoopControlBase(PerformanceModelBaseClass): """Base OpenMDAO component for open-loop demand tracking. This component defines the interfaces required for open-loop demand @@ -35,21 +35,6 @@ class ConverterOpenLoopControlBase(om.ExplicitComponent): controller behavior. """ - def initialize(self): - """Declare component options. - - Options: - driver_config (dict): Driver-level configuration parameters. - plant_config (dict): Plant-level configuration, including number of - simulation timesteps. - tech_config (dict): Technology-specific configuration, including - controller settings. - - """ - self.options.declare("driver_config", types=dict) - self.options.declare("plant_config", types=dict) - self.options.declare("tech_config", types=dict) - def setup(self): """Define inputs and outputs for demand control. @@ -61,48 +46,52 @@ def setup(self): KeyError: If required configuration keys are missing from ``plant_config`` or ``tech_config``. """ - n_timesteps = int(self.options["plant_config"]["plant"]["simulation"]["n_timesteps"]) + self.commodity = self.config.commodity + self.commodity_rate_units = self.config.commodity_rate_units + self.commodity_amount_units = getattr( + self.config, "commodity_amount_units", f"({self.config.commodity_rate_units})*h" + ) - commodity = self.config.commodity + super().setup() self.add_input( - f"{commodity}_demand", + f"{self.commodity}_demand", val=self.config.demand_profile, - shape=(n_timesteps), - units=self.config.commodity_rate_units, # NOTE: hardcoded to align with controllers - desc=f"Demand profile of {commodity}", + shape=self.n_timesteps, + units=self.commodity_rate_units, # NOTE: hardcoded to align with controllers + desc=f"Demand profile of {self.commodity}", ) self.add_input( - f"{commodity}_in", + f"{self.commodity}_in", val=0.0, - shape=(n_timesteps), - units=self.config.commodity_rate_units, - desc=f"Amount of {commodity} demand that has already been supplied", + shape=self.n_timesteps, + units=self.commodity_rate_units, + desc=f"Amount of {self.commodity} demand that has already been supplied", ) self.add_output( - f"unmet_{commodity}_demand_out", + f"unmet_{self.commodity}_demand_out", val=self.config.demand_profile, - shape=(n_timesteps), - units=self.config.commodity_rate_units, - desc=f"Remaining demand profile of {commodity}", + shape=self.n_timesteps, + units=self.commodity_rate_units, + desc=f"Remaining demand profile of {self.commodity}", ) self.add_output( - f"unused_{commodity}_out", + f"unused_{self.commodity}_out", val=0.0, - shape=(n_timesteps), - units=self.config.commodity_rate_units, - desc=f"Excess production of {commodity}", + shape=self.n_timesteps, + units=self.commodity_rate_units, + desc=f"Excess production of {self.commodity}", ) self.add_output( - f"{commodity}_set_point", + f"{self.commodity}_set_point", val=0.0, - shape=(n_timesteps), - units=self.config.commodity_rate_units, - desc=f"Production profile of {commodity}", + shape=self.n_timesteps, + units=self.commodity_rate_units, + desc=f"Production profile of {self.commodity}", ) # self.add_output( From 1e54ed2f5fc098eb9fdab6dc7c049de893d1fbfc Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Wed, 1 Apr 2026 11:52:50 -0600 Subject: [PATCH 04/17] udpated plant configs in test_openloop_controllers.py --- h2integrate/control/test/test_openloop_controllers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/h2integrate/control/test/test_openloop_controllers.py b/h2integrate/control/test/test_openloop_controllers.py index e96b8f76c..123808def 100644 --- a/h2integrate/control/test/test_openloop_controllers.py +++ b/h2integrate/control/test/test_openloop_controllers.py @@ -55,7 +55,7 @@ def test_pass_through_controller(subtests): # Set up the OpenMDAO problem prob = om.Problem() - plant_config = {"plant": {"plant_life": 30, "simulation": {"n_timesteps": 10}}} + plant_config = {"plant": {"plant_life": 30, "simulation": {"n_timesteps": 10, "dt": 3600}}} prob.model.add_subsystem( name="IVC", @@ -532,7 +532,7 @@ def test_demand_converter_controller(subtests): }, } - plant_config = {"plant": {"plant_life": 30, "simulation": {"n_timesteps": 10}}} + plant_config = {"plant": {"plant_life": 30, "simulation": {"n_timesteps": 10, "dt": 3600}}} # Set up OpenMDAO problem prob = om.Problem() @@ -611,7 +611,7 @@ def test_flexible_demand_converter_controller(subtests, variable_h2_production_p plant_config = { "plant": { "plant_life": 30, - "simulation": {"n_timesteps": len(variable_h2_production_profile)}, + "simulation": {"n_timesteps": len(variable_h2_production_profile), "dt": 3600}, } } @@ -721,7 +721,7 @@ def test_flexible_demand_converter_controller_min_utilization( plant_config = { "plant": { "plant_life": 30, - "simulation": {"n_timesteps": len(variable_h2_production_profile)}, + "simulation": {"n_timesteps": len(variable_h2_production_profile), "dt": 3600}, } } From b1b62c9abb91d7b4c4003844523918cd10e909d9 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:18:11 -0600 Subject: [PATCH 05/17] moved converter control strategies to separate file --- .../demand_openloop_converter_controller.py | 1 + .../test/test_converter_control_strategies.py | 281 ++++++++++++++++++ .../control/test/test_openloop_controllers.py | 260 ---------------- 3 files changed, 282 insertions(+), 260 deletions(-) create mode 100644 h2integrate/control/test/test_converter_control_strategies.py diff --git a/h2integrate/control/control_strategies/converters/demand_openloop_converter_controller.py b/h2integrate/control/control_strategies/converters/demand_openloop_converter_controller.py index f7d59b43a..e27a1cbfb 100644 --- a/h2integrate/control/control_strategies/converters/demand_openloop_converter_controller.py +++ b/h2integrate/control/control_strategies/converters/demand_openloop_converter_controller.py @@ -87,6 +87,7 @@ def compute(self, inputs, outputs): outputs[f"total_{self.commodity}_produced"] = np.sum(outputs[f"{self.commodity}_out"]) * ( self.dt / 3600 ) + outputs[f"annual_{self.commodity}_produced"] = ( outputs[f"total_{self.commodity}_produced"] / self.fraction_of_year_simulated ) diff --git a/h2integrate/control/test/test_converter_control_strategies.py b/h2integrate/control/test/test_converter_control_strategies.py new file mode 100644 index 000000000..08cb0c140 --- /dev/null +++ b/h2integrate/control/test/test_converter_control_strategies.py @@ -0,0 +1,281 @@ +from pathlib import Path + +import numpy as np +import pytest +import openmdao.api as om +from pytest import fixture + +from h2integrate.core.file_utils import load_yaml +from h2integrate.control.control_strategies.converters.flexible_demand_openloop_controller import ( + FlexibleDemandOpenLoopConverterController, +) +from h2integrate.control.control_strategies.converters.demand_openloop_converter_controller import ( + DemandOpenLoopConverterController, +) + + +@fixture +def variable_h2_production_profile(): + end_use_rated_demand = 10.0 # kg/h + ramp_up_rate_kg = 4.0 + ramp_down_rate_kg = 2.0 + slow_ramp_up = np.arange(0, end_use_rated_demand * 1.1, 0.5) + slow_ramp_down = np.arange(end_use_rated_demand * 1.1, -0.5, -0.5) + fast_ramp_up = np.arange(0, end_use_rated_demand, ramp_up_rate_kg * 1.2) + fast_ramp_down = np.arange(end_use_rated_demand, 0.0, ramp_down_rate_kg * 1.1) + variable_profile = np.concat( + [slow_ramp_up, fast_ramp_down, slow_ramp_up, slow_ramp_down, fast_ramp_up] + ) + variable_h2_profile = np.tile(variable_profile, 2) + return variable_h2_profile + + +@pytest.mark.regression +def test_demand_converter_controller(subtests): + # Test is the same as the demand controller test test_demand_controller for the "h2_storage" + # performance model but with the "StoragePerformanceModel" performance model + + # Get the directory of the current script + current_dir = Path(__file__).parent + + # Resolve the paths to the configuration files + tech_config_path = current_dir / "inputs" / "tech_config.yaml" + + # Load the technology configuration + tech_config = load_yaml(tech_config_path) + + tech_config["technologies"]["load"] = { + "control_strategy": { + "model": "DemandOpenLoopConverterController", + }, + "model_inputs": { + "control_parameters": { + "commodity": "hydrogen", + "commodity_rate_units": "kg", + "demand_profile": [5.0] * 10, # Example: 10 time steps with 5 kg/time step demand + }, + }, + } + + plant_config = {"plant": {"plant_life": 30, "simulation": {"n_timesteps": 10, "dt": 3600}}} + + # Set up OpenMDAO problem + prob = om.Problem() + + prob.model.add_subsystem( + name="IVC", + subsys=om.IndepVarComp(name="hydrogen_in", val=np.arange(10)), + promotes=["*"], + ) + + prob.model.add_subsystem( + "demand_open_loop_storage_controller", + DemandOpenLoopConverterController( + plant_config=plant_config, tech_config=tech_config["technologies"]["load"] + ), + promotes=["*"], + ) + + prob.setup() + + prob.run_model() + + # # Run the test + with subtests.test("Check output"): + assert prob.get_val("hydrogen_set_point") == pytest.approx( + [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 5.0, 5.0, 5.0, 5.0] + ) + + with subtests.test("Check curtailment"): + assert prob.get_val("unused_hydrogen_out") == pytest.approx( + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 2.0, 3.0, 4.0] + ) + + with subtests.test("Check missed load"): + assert prob.get_val("unmet_hydrogen_demand_out") == pytest.approx( + [5.0, 4.0, 3.0, 2.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0] + ) + + +@pytest.mark.unit +def test_flexible_demand_converter_controller(subtests, variable_h2_production_profile): + # Get the directory of the current script + current_dir = Path(__file__).parent + + # Resolve the paths to the configuration files + tech_config_path = current_dir / "inputs" / "tech_config.yaml" + + # Load the technology configuration + tech_config = load_yaml(tech_config_path) + + end_use_rated_demand = 10.0 # kg/h + ramp_up_rate_kg = 4.0 + ramp_down_rate_kg = 2.0 + min_demand_kg = 2.5 + tech_config["technologies"]["load"] = { + "control_strategy": { + "model": "FlexibleDemandOpenLoopConverterController", + }, + "model_inputs": { + "control_parameters": { + "commodity": "hydrogen", + "commodity_rate_units": "kg", + "rated_demand": end_use_rated_demand, + "demand_profile": end_use_rated_demand, # flat demand profile + "turndown_ratio": min_demand_kg / end_use_rated_demand, + "ramp_down_rate_fraction": ramp_down_rate_kg / end_use_rated_demand, + "ramp_up_rate_fraction": ramp_up_rate_kg / end_use_rated_demand, + "min_utilization": 0.1, + }, + }, + } + + plant_config = { + "plant": { + "plant_life": 30, + "simulation": {"n_timesteps": len(variable_h2_production_profile), "dt": 3600}, + } + } + + # Set up OpenMDAO problem + prob = om.Problem() + + prob.model.add_subsystem( + name="IVC", + subsys=om.IndepVarComp(name="hydrogen_in", val=variable_h2_production_profile), + promotes=["*"], + ) + + prob.model.add_subsystem( + "flexible_demand_open_loop_converter_controller", + FlexibleDemandOpenLoopConverterController( + plant_config=plant_config, tech_config=tech_config["technologies"]["load"] + ), + promotes=["*"], + ) + + prob.setup() + + prob.run_model() + + flexible_total_demand = prob.get_val("hydrogen_flexible_demand_profile", units="kg") + + rated_production = end_use_rated_demand * len(variable_h2_production_profile) + + with subtests.test("Check that total demand profile is less than rated"): + assert np.all(flexible_total_demand <= end_use_rated_demand) + + with subtests.test("Check curtailment"): # failed + assert np.sum(prob.get_val("unused_hydrogen_out", units="kg")) == pytest.approx(6.6) + + # check ramping constraints and turndown constraints are met + with subtests.test("Check turndown ratio constraint"): + assert np.all(flexible_total_demand >= min_demand_kg) + + ramping_down = np.where( + np.diff(flexible_total_demand) < 0, -1 * np.diff(flexible_total_demand), 0 + ) + ramping_up = np.where(np.diff(flexible_total_demand) > 0, np.diff(flexible_total_demand), 0) + + with subtests.test("Check ramping down constraint"): + assert np.max(ramping_down) == pytest.approx(ramp_down_rate_kg, rel=1e-6) + + with subtests.test("Check ramping up constraint"): # failed + assert np.max(ramping_up) == pytest.approx(ramp_up_rate_kg, rel=1e-6) + + with subtests.test("Check min utilization constraint"): + assert np.sum(flexible_total_demand) / rated_production >= 0.1 + + with subtests.test("Check min utilization value"): + flexible_demand_utilization = np.sum(flexible_total_demand) / rated_production + assert flexible_demand_utilization == pytest.approx(0.5822142857142857, rel=1e-6) + + # flexible_demand_profile[i] >= commodity_in[i] (as long as you are not curtailing + # any commodity in) + with subtests.test("Check that flexible demand is greater than hydrogen_in"): + hydrogen_available = variable_h2_production_profile - prob.get_val( + "unused_hydrogen_out", units="kg" + ) + assert np.all(flexible_total_demand >= hydrogen_available) + + with subtests.test("Check that remaining demand was calculated properly"): + unmet_demand = flexible_total_demand - hydrogen_available + assert np.all(unmet_demand == prob.get_val("unmet_hydrogen_demand_out", units="kg")) + + +@pytest.mark.regression +def test_flexible_demand_converter_controller_min_utilization( + subtests, variable_h2_production_profile +): + # give it a min utilization larger than utilization resulting from above test + + # Get the directory of the current script + current_dir = Path(__file__).parent + + # Resolve the paths to the configuration files + tech_config_path = current_dir / "inputs" / "tech_config.yaml" + + # Load the technology configuration + tech_config = load_yaml(tech_config_path) + + end_use_rated_demand = 10.0 # kg/h + ramp_up_rate_kg = 4.0 + ramp_down_rate_kg = 2.0 + min_demand_kg = 2.5 + tech_config["technologies"]["load"] = { + "control_strategy": { + "model": "FlexibleDemandOpenLoopConverterController", + }, + "model_inputs": { + "control_parameters": { + "commodity": "hydrogen", + "commodity_rate_units": "kg", + "rated_demand": end_use_rated_demand, + "demand_profile": end_use_rated_demand, # flat demand profile + "turndown_ratio": min_demand_kg / end_use_rated_demand, + "ramp_down_rate_fraction": ramp_down_rate_kg / end_use_rated_demand, + "ramp_up_rate_fraction": ramp_up_rate_kg / end_use_rated_demand, + "min_utilization": 0.8, + }, + }, + } + + plant_config = { + "plant": { + "plant_life": 30, + "simulation": {"n_timesteps": len(variable_h2_production_profile), "dt": 3600}, + } + } + + # Set up OpenMDAO problem + prob = om.Problem() + + prob.model.add_subsystem( + name="IVC", + subsys=om.IndepVarComp(name="hydrogen_in", val=variable_h2_production_profile), + promotes=["*"], + ) + + prob.model.add_subsystem( + "DemandOpenLoopStorageController", + FlexibleDemandOpenLoopConverterController( + plant_config=plant_config, tech_config=tech_config["technologies"]["load"] + ), + promotes=["*"], + ) + + prob.setup() + + prob.run_model() + + flexible_total_demand = prob.get_val("hydrogen_flexible_demand_profile", units="kg") + + rated_production = end_use_rated_demand * len(variable_h2_production_profile) + + flexible_demand_utilization = np.sum(flexible_total_demand) / rated_production + + with subtests.test("Check min utilization constraint"): + assert flexible_demand_utilization >= 0.8 + + with subtests.test("Check min utilization value"): + assert flexible_demand_utilization == pytest.approx(0.8010612244, rel=1e-6) diff --git a/h2integrate/control/test/test_openloop_controllers.py b/h2integrate/control/test/test_openloop_controllers.py index 123808def..2a9f90f2a 100644 --- a/h2integrate/control/test/test_openloop_controllers.py +++ b/h2integrate/control/test/test_openloop_controllers.py @@ -14,12 +14,6 @@ from h2integrate.control.control_strategies.storage.demand_openloop_storage_controller import ( DemandOpenLoopStorageController, ) -from h2integrate.control.control_strategies.converters.flexible_demand_openloop_controller import ( - FlexibleDemandOpenLoopConverterController, -) -from h2integrate.control.control_strategies.converters.demand_openloop_converter_controller import ( - DemandOpenLoopConverterController, -) @fixture @@ -503,257 +497,3 @@ def test_generic_storage_demand_controller(subtests): assert prob.get_val("unmet_hydrogen_demand_out", units="kg/h") == pytest.approx( [0.5, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] ) - - -@pytest.mark.regression -def test_demand_converter_controller(subtests): - # Test is the same as the demand controller test test_demand_controller for the "h2_storage" - # performance model but with the "StoragePerformanceModel" performance model - - # Get the directory of the current script - current_dir = Path(__file__).parent - - # Resolve the paths to the configuration files - tech_config_path = current_dir / "inputs" / "tech_config.yaml" - - # Load the technology configuration - tech_config = load_yaml(tech_config_path) - - tech_config["technologies"]["load"] = { - "control_strategy": { - "model": "DemandOpenLoopConverterController", - }, - "model_inputs": { - "control_parameters": { - "commodity": "hydrogen", - "commodity_rate_units": "kg", - "demand_profile": [5.0] * 10, # Example: 10 time steps with 5 kg/time step demand - }, - }, - } - - plant_config = {"plant": {"plant_life": 30, "simulation": {"n_timesteps": 10, "dt": 3600}}} - - # Set up OpenMDAO problem - prob = om.Problem() - - prob.model.add_subsystem( - name="IVC", - subsys=om.IndepVarComp(name="hydrogen_in", val=np.arange(10)), - promotes=["*"], - ) - - prob.model.add_subsystem( - "demand_open_loop_storage_controller", - DemandOpenLoopConverterController( - plant_config=plant_config, tech_config=tech_config["technologies"]["load"] - ), - promotes=["*"], - ) - - prob.setup() - - prob.run_model() - - # # Run the test - with subtests.test("Check output"): - assert prob.get_val("hydrogen_set_point") == pytest.approx( - [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 5.0, 5.0, 5.0, 5.0] - ) - - with subtests.test("Check curtailment"): - assert prob.get_val("unused_hydrogen_out") == pytest.approx( - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 2.0, 3.0, 4.0] - ) - - with subtests.test("Check missed load"): - assert prob.get_val("unmet_hydrogen_demand_out") == pytest.approx( - [5.0, 4.0, 3.0, 2.0, 1.0, 0.0, 0.0, 0.0, 0.0, 0.0] - ) - - -### Add test for flexible load demand controller here - - -@pytest.mark.unit -def test_flexible_demand_converter_controller(subtests, variable_h2_production_profile): - # Get the directory of the current script - current_dir = Path(__file__).parent - - # Resolve the paths to the configuration files - tech_config_path = current_dir / "inputs" / "tech_config.yaml" - - # Load the technology configuration - tech_config = load_yaml(tech_config_path) - - end_use_rated_demand = 10.0 # kg/h - ramp_up_rate_kg = 4.0 - ramp_down_rate_kg = 2.0 - min_demand_kg = 2.5 - tech_config["technologies"]["load"] = { - "control_strategy": { - "model": "FlexibleDemandOpenLoopConverterController", - }, - "model_inputs": { - "control_parameters": { - "commodity": "hydrogen", - "commodity_rate_units": "kg", - "rated_demand": end_use_rated_demand, - "demand_profile": end_use_rated_demand, # flat demand profile - "turndown_ratio": min_demand_kg / end_use_rated_demand, - "ramp_down_rate_fraction": ramp_down_rate_kg / end_use_rated_demand, - "ramp_up_rate_fraction": ramp_up_rate_kg / end_use_rated_demand, - "min_utilization": 0.1, - }, - }, - } - - plant_config = { - "plant": { - "plant_life": 30, - "simulation": {"n_timesteps": len(variable_h2_production_profile), "dt": 3600}, - } - } - - # Set up OpenMDAO problem - prob = om.Problem() - - prob.model.add_subsystem( - name="IVC", - subsys=om.IndepVarComp(name="hydrogen_in", val=variable_h2_production_profile), - promotes=["*"], - ) - - prob.model.add_subsystem( - "flexible_demand_open_loop_converter_controller", - FlexibleDemandOpenLoopConverterController( - plant_config=plant_config, tech_config=tech_config["technologies"]["load"] - ), - promotes=["*"], - ) - - prob.setup() - - prob.run_model() - - flexible_total_demand = prob.get_val("hydrogen_flexible_demand_profile", units="kg") - - rated_production = end_use_rated_demand * len(variable_h2_production_profile) - - with subtests.test("Check that total demand profile is less than rated"): - assert np.all(flexible_total_demand <= end_use_rated_demand) - - with subtests.test("Check curtailment"): # failed - assert np.sum(prob.get_val("unused_hydrogen_out", units="kg")) == pytest.approx(6.6) - - # check ramping constraints and turndown constraints are met - with subtests.test("Check turndown ratio constraint"): - assert np.all(flexible_total_demand >= min_demand_kg) - - ramping_down = np.where( - np.diff(flexible_total_demand) < 0, -1 * np.diff(flexible_total_demand), 0 - ) - ramping_up = np.where(np.diff(flexible_total_demand) > 0, np.diff(flexible_total_demand), 0) - - with subtests.test("Check ramping down constraint"): - assert np.max(ramping_down) == pytest.approx(ramp_down_rate_kg, rel=1e-6) - - with subtests.test("Check ramping up constraint"): # failed - assert np.max(ramping_up) == pytest.approx(ramp_up_rate_kg, rel=1e-6) - - with subtests.test("Check min utilization constraint"): - assert np.sum(flexible_total_demand) / rated_production >= 0.1 - - with subtests.test("Check min utilization value"): - flexible_demand_utilization = np.sum(flexible_total_demand) / rated_production - assert flexible_demand_utilization == pytest.approx(0.5822142857142857, rel=1e-6) - - # flexible_demand_profile[i] >= commodity_in[i] (as long as you are not curtailing - # any commodity in) - with subtests.test("Check that flexible demand is greater than hydrogen_in"): - hydrogen_available = variable_h2_production_profile - prob.get_val( - "unused_hydrogen_out", units="kg" - ) - assert np.all(flexible_total_demand >= hydrogen_available) - - with subtests.test("Check that remaining demand was calculated properly"): - unmet_demand = flexible_total_demand - hydrogen_available - assert np.all(unmet_demand == prob.get_val("unmet_hydrogen_demand_out", units="kg")) - - -@pytest.mark.regression -def test_flexible_demand_converter_controller_min_utilization( - subtests, variable_h2_production_profile -): - # give it a min utilization larger than utilization resulting from above test - - # Get the directory of the current script - current_dir = Path(__file__).parent - - # Resolve the paths to the configuration files - tech_config_path = current_dir / "inputs" / "tech_config.yaml" - - # Load the technology configuration - tech_config = load_yaml(tech_config_path) - - end_use_rated_demand = 10.0 # kg/h - ramp_up_rate_kg = 4.0 - ramp_down_rate_kg = 2.0 - min_demand_kg = 2.5 - tech_config["technologies"]["load"] = { - "control_strategy": { - "model": "FlexibleDemandOpenLoopConverterController", - }, - "model_inputs": { - "control_parameters": { - "commodity": "hydrogen", - "commodity_rate_units": "kg", - "rated_demand": end_use_rated_demand, - "demand_profile": end_use_rated_demand, # flat demand profile - "turndown_ratio": min_demand_kg / end_use_rated_demand, - "ramp_down_rate_fraction": ramp_down_rate_kg / end_use_rated_demand, - "ramp_up_rate_fraction": ramp_up_rate_kg / end_use_rated_demand, - "min_utilization": 0.8, - }, - }, - } - - plant_config = { - "plant": { - "plant_life": 30, - "simulation": {"n_timesteps": len(variable_h2_production_profile), "dt": 3600}, - } - } - - # Set up OpenMDAO problem - prob = om.Problem() - - prob.model.add_subsystem( - name="IVC", - subsys=om.IndepVarComp(name="hydrogen_in", val=variable_h2_production_profile), - promotes=["*"], - ) - - prob.model.add_subsystem( - "DemandOpenLoopStorageController", - FlexibleDemandOpenLoopConverterController( - plant_config=plant_config, tech_config=tech_config["technologies"]["load"] - ), - promotes=["*"], - ) - - prob.setup() - - prob.run_model() - - flexible_total_demand = prob.get_val("hydrogen_flexible_demand_profile", units="kg") - - rated_production = end_use_rated_demand * len(variable_h2_production_profile) - - flexible_demand_utilization = np.sum(flexible_total_demand) / rated_production - - with subtests.test("Check min utilization constraint"): - assert flexible_demand_utilization >= 0.8 - - with subtests.test("Check min utilization value"): - assert flexible_demand_utilization == pytest.approx(0.8010612244, rel=1e-6) From 2d6b0c5192b4dd118252acf4894164a9a6145445 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Mon, 6 Apr 2026 10:54:23 -0600 Subject: [PATCH 06/17] moved converter control strategies to demand folder --- .../control_strategies/converters => demand}/__init__.py | 0 .../demand_openloop_converter_controller.py | 2 +- .../flexible_demand_openloop_controller.py | 2 +- .../converters => demand}/openloop_controller_base.py | 0 .../test/test_demand_components.py} | 0 5 files changed, 2 insertions(+), 2 deletions(-) rename h2integrate/{control/control_strategies/converters => demand}/__init__.py (100%) rename h2integrate/{control/control_strategies/converters => demand}/demand_openloop_converter_controller.py (97%) rename h2integrate/{control/control_strategies/converters => demand}/flexible_demand_openloop_controller.py (99%) rename h2integrate/{control/control_strategies/converters => demand}/openloop_controller_base.py (100%) rename h2integrate/{control/test/test_converter_control_strategies.py => demand/test/test_demand_components.py} (100%) diff --git a/h2integrate/control/control_strategies/converters/__init__.py b/h2integrate/demand/__init__.py similarity index 100% rename from h2integrate/control/control_strategies/converters/__init__.py rename to h2integrate/demand/__init__.py diff --git a/h2integrate/control/control_strategies/converters/demand_openloop_converter_controller.py b/h2integrate/demand/demand_openloop_converter_controller.py similarity index 97% rename from h2integrate/control/control_strategies/converters/demand_openloop_converter_controller.py rename to h2integrate/demand/demand_openloop_converter_controller.py index e27a1cbfb..292dfc630 100644 --- a/h2integrate/control/control_strategies/converters/demand_openloop_converter_controller.py +++ b/h2integrate/demand/demand_openloop_converter_controller.py @@ -1,6 +1,6 @@ import numpy as np -from h2integrate.control.control_strategies.converters.openloop_controller_base import ( +from h2integrate.demand.openloop_controller_base import ( ConverterOpenLoopControlBase, ConverterOpenLoopControlBaseConfig, ) diff --git a/h2integrate/control/control_strategies/converters/flexible_demand_openloop_controller.py b/h2integrate/demand/flexible_demand_openloop_controller.py similarity index 99% rename from h2integrate/control/control_strategies/converters/flexible_demand_openloop_controller.py rename to h2integrate/demand/flexible_demand_openloop_controller.py index 475c9caf8..1d7dc79e2 100644 --- a/h2integrate/control/control_strategies/converters/flexible_demand_openloop_controller.py +++ b/h2integrate/demand/flexible_demand_openloop_controller.py @@ -2,7 +2,7 @@ from attrs import field, define from h2integrate.core.validators import gte_zero, range_val -from h2integrate.control.control_strategies.converters.openloop_controller_base import ( +from h2integrate.demand.openloop_controller_base import ( ConverterOpenLoopControlBase, ConverterOpenLoopControlBaseConfig, ) diff --git a/h2integrate/control/control_strategies/converters/openloop_controller_base.py b/h2integrate/demand/openloop_controller_base.py similarity index 100% rename from h2integrate/control/control_strategies/converters/openloop_controller_base.py rename to h2integrate/demand/openloop_controller_base.py diff --git a/h2integrate/control/test/test_converter_control_strategies.py b/h2integrate/demand/test/test_demand_components.py similarity index 100% rename from h2integrate/control/test/test_converter_control_strategies.py rename to h2integrate/demand/test/test_demand_components.py From 0e6b3e8c23bdd5b21fae2683657f7502fe78e796 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Mon, 6 Apr 2026 10:55:46 -0600 Subject: [PATCH 07/17] updated import paths for demand components --- h2integrate/core/supported_models.py | 12 ++++++------ h2integrate/demand/test/test_demand_components.py | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/h2integrate/core/supported_models.py b/h2integrate/core/supported_models.py index d559b0e05..e9f550f13 100644 --- a/h2integrate/core/supported_models.py +++ b/h2integrate/core/supported_models.py @@ -102,12 +102,18 @@ SteamMethaneReformerCostModel, SteamMethaneReformerPerformanceModel, ) +from h2integrate.demand.flexible_demand_openloop_controller import ( + FlexibleDemandOpenLoopConverterController, +) from h2integrate.converters.natural_gas.dummy_gas_components import ( SimpleGasConsumerCost, SimpleGasProducerCost, SimpleGasConsumerPerformance, SimpleGasProducerPerformance, ) +from h2integrate.demand.demand_openloop_converter_controller import ( + DemandOpenLoopConverterController, +) from h2integrate.converters.hydrogen.geologic.mathur_modified import GeoH2SubsurfaceCostModel from h2integrate.resource.solar.nlr_developer_goes_api_models import ( GOESTMYSolarAPI, @@ -170,12 +176,6 @@ from h2integrate.control.control_strategies.storage.demand_openloop_storage_controller import ( DemandOpenLoopStorageController, ) -from h2integrate.control.control_strategies.converters.flexible_demand_openloop_controller import ( - FlexibleDemandOpenLoopConverterController, -) -from h2integrate.control.control_strategies.converters.demand_openloop_converter_controller import ( - DemandOpenLoopConverterController, -) supported_models = { diff --git a/h2integrate/demand/test/test_demand_components.py b/h2integrate/demand/test/test_demand_components.py index 08cb0c140..f1c8b450f 100644 --- a/h2integrate/demand/test/test_demand_components.py +++ b/h2integrate/demand/test/test_demand_components.py @@ -6,10 +6,10 @@ from pytest import fixture from h2integrate.core.file_utils import load_yaml -from h2integrate.control.control_strategies.converters.flexible_demand_openloop_controller import ( +from h2integrate.demand.flexible_demand_openloop_controller import ( FlexibleDemandOpenLoopConverterController, ) -from h2integrate.control.control_strategies.converters.demand_openloop_converter_controller import ( +from h2integrate.demand.demand_openloop_converter_controller import ( DemandOpenLoopConverterController, ) From 2986dff78bedf1b382ba54a7421c57df75dfa169 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Mon, 6 Apr 2026 11:42:08 -0600 Subject: [PATCH 08/17] removed commodity_set_point as output --- .../demand_openloop_converter_controller.py | 7 +--- .../flexible_demand_openloop_controller.py | 7 +--- .../demand/openloop_controller_base.py | 18 ++-------- .../demand/test/test_demand_components.py | 35 ++++--------------- 4 files changed, 11 insertions(+), 56 deletions(-) diff --git a/h2integrate/demand/demand_openloop_converter_controller.py b/h2integrate/demand/demand_openloop_converter_controller.py index 292dfc630..304df72e2 100644 --- a/h2integrate/demand/demand_openloop_converter_controller.py +++ b/h2integrate/demand/demand_openloop_converter_controller.py @@ -73,11 +73,6 @@ def compute(self, inputs, outputs): ) # Calculate actual output based on demand met and curtailment - outputs[f"{self.commodity}_set_point"] = ( - inputs[f"{self.commodity}_in"] - outputs[f"unused_{self.commodity}_out"] - ) - - # Calculate performance model outputs outputs[f"{self.commodity}_out"] = ( inputs[f"{self.commodity}_in"] - outputs[f"unused_{self.commodity}_out"] ) @@ -93,5 +88,5 @@ def compute(self, inputs, outputs): ) outputs["capacity_factor"] = ( - outputs[f"{self.commodity}_set_point"].sum() / inputs[f"{self.commodity}_demand"].sum() + outputs[f"{self.commodity}_out"].sum() / inputs[f"{self.commodity}_demand"].sum() ) diff --git a/h2integrate/demand/flexible_demand_openloop_controller.py b/h2integrate/demand/flexible_demand_openloop_controller.py index 1d7dc79e2..26bfe5e42 100644 --- a/h2integrate/demand/flexible_demand_openloop_controller.py +++ b/h2integrate/demand/flexible_demand_openloop_controller.py @@ -294,11 +294,6 @@ def compute(self, inputs, outputs): ) # Calculate actual output based on demand met and curtailment - outputs[f"{self.commodity}_set_point"] = ( - inputs[f"{self.commodity}_in"] - outputs[f"unused_{self.commodity}_out"] - ) - - # Calculate performance model outputs outputs[f"{self.commodity}_out"] = ( inputs[f"{self.commodity}_in"] - outputs[f"unused_{self.commodity}_out"] ) @@ -314,5 +309,5 @@ def compute(self, inputs, outputs): ) outputs["capacity_factor"] = ( - outputs[f"{self.commodity}_set_point"].sum() / inputs[f"{self.commodity}_demand"].sum() + outputs[f"{self.commodity}_out"].sum() / inputs[f"{self.commodity}_demand"].sum() ) diff --git a/h2integrate/demand/openloop_controller_base.py b/h2integrate/demand/openloop_controller_base.py index abb3bb38c..f16c87a2a 100644 --- a/h2integrate/demand/openloop_controller_base.py +++ b/h2integrate/demand/openloop_controller_base.py @@ -20,7 +20,7 @@ class ConverterOpenLoopControlBaseConfig(BaseConfig): demand or a list/array for time-varying demand. """ - commodity: str = field(converter=(str.strip, str.lower)) + commodity: str = field(converter=str.strip) commodity_rate_units: str = field(converter=str.strip) demand_profile: int | float | list = field() @@ -58,7 +58,7 @@ def setup(self): f"{self.commodity}_demand", val=self.config.demand_profile, shape=self.n_timesteps, - units=self.commodity_rate_units, # NOTE: hardcoded to align with controllers + units=self.commodity_rate_units, desc=f"Demand profile of {self.commodity}", ) @@ -86,20 +86,6 @@ def setup(self): desc=f"Excess production of {self.commodity}", ) - self.add_output( - f"{self.commodity}_set_point", - val=0.0, - shape=self.n_timesteps, - units=self.commodity_rate_units, - desc=f"Production profile of {self.commodity}", - ) - - # self.add_output( - # f"total_{commodity}_unmet_demand", - # units=self.config.commodity_rate_units, - # desc="Total unmet demand", - # ) - def compute(): """This method must be implemented by subclasses to define the controller. diff --git a/h2integrate/demand/test/test_demand_components.py b/h2integrate/demand/test/test_demand_components.py index f1c8b450f..91c5b2340 100644 --- a/h2integrate/demand/test/test_demand_components.py +++ b/h2integrate/demand/test/test_demand_components.py @@ -1,11 +1,8 @@ -from pathlib import Path - import numpy as np import pytest import openmdao.api as om from pytest import fixture -from h2integrate.core.file_utils import load_yaml from h2integrate.demand.flexible_demand_openloop_controller import ( FlexibleDemandOpenLoopConverterController, ) @@ -35,14 +32,8 @@ def test_demand_converter_controller(subtests): # Test is the same as the demand controller test test_demand_controller for the "h2_storage" # performance model but with the "StoragePerformanceModel" performance model - # Get the directory of the current script - current_dir = Path(__file__).parent - - # Resolve the paths to the configuration files - tech_config_path = current_dir / "inputs" / "tech_config.yaml" - - # Load the technology configuration - tech_config = load_yaml(tech_config_path) + # Define the technology configuration + tech_config = {"technologies": {}} tech_config["technologies"]["load"] = { "control_strategy": { @@ -82,7 +73,7 @@ def test_demand_converter_controller(subtests): # # Run the test with subtests.test("Check output"): - assert prob.get_val("hydrogen_set_point") == pytest.approx( + assert prob.get_val("hydrogen_out") == pytest.approx( [0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 5.0, 5.0, 5.0, 5.0] ) @@ -99,14 +90,8 @@ def test_demand_converter_controller(subtests): @pytest.mark.unit def test_flexible_demand_converter_controller(subtests, variable_h2_production_profile): - # Get the directory of the current script - current_dir = Path(__file__).parent - - # Resolve the paths to the configuration files - tech_config_path = current_dir / "inputs" / "tech_config.yaml" - - # Load the technology configuration - tech_config = load_yaml(tech_config_path) + # Define the technology configuration + tech_config = {"technologies": {}} end_use_rated_demand = 10.0 # kg/h ramp_up_rate_kg = 4.0 @@ -209,14 +194,8 @@ def test_flexible_demand_converter_controller_min_utilization( ): # give it a min utilization larger than utilization resulting from above test - # Get the directory of the current script - current_dir = Path(__file__).parent - - # Resolve the paths to the configuration files - tech_config_path = current_dir / "inputs" / "tech_config.yaml" - - # Load the technology configuration - tech_config = load_yaml(tech_config_path) + # Define the technology configuration + tech_config = {"technologies": {}} end_use_rated_demand = 10.0 # kg/h ramp_up_rate_kg = 4.0 From 9672ecc7ab8e2e581dd74519f44b4a1cefa1f363 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:44:32 -0600 Subject: [PATCH 09/17] renamed demand files --- h2integrate/core/supported_models.py | 8 ++------ .../{openloop_controller_base.py => demand_base.py} | 0 ...e_demand_openloop_controller.py => flexible_demand.py} | 2 +- ...openloop_converter_controller.py => generic_demand.py} | 2 +- h2integrate/demand/test/test_demand_components.py | 8 ++------ 5 files changed, 6 insertions(+), 14 deletions(-) rename h2integrate/demand/{openloop_controller_base.py => demand_base.py} (100%) rename h2integrate/demand/{flexible_demand_openloop_controller.py => flexible_demand.py} (99%) rename h2integrate/demand/{demand_openloop_converter_controller.py => generic_demand.py} (98%) diff --git a/h2integrate/core/supported_models.py b/h2integrate/core/supported_models.py index 104fec57a..d082d8a71 100644 --- a/h2integrate/core/supported_models.py +++ b/h2integrate/core/supported_models.py @@ -6,8 +6,10 @@ from h2integrate.converters.grid.grid import GridCostModel, GridPerformanceModel from h2integrate.finances.profast_lco import ProFastLCO from h2integrate.finances.profast_npv import ProFastNPV +from h2integrate.demand.generic_demand import DemandOpenLoopConverterController from h2integrate.converters.steel.steel import SteelPerformanceModel, SteelCostAndFinancialModel from h2integrate.converters.wind.floris import FlorisWindPlantPerformanceModel +from h2integrate.demand.flexible_demand import FlexibleDemandOpenLoopConverterController from h2integrate.converters.wind.wind_pysam import PYSAMWindPlantPerformanceModel from h2integrate.transporters.generic_summer import GenericSummerPerformanceModel from h2integrate.converters.hopp.hopp_wrapper import HOPPComponent @@ -102,18 +104,12 @@ SteamMethaneReformerCostModel, SteamMethaneReformerPerformanceModel, ) -from h2integrate.demand.flexible_demand_openloop_controller import ( - FlexibleDemandOpenLoopConverterController, -) from h2integrate.converters.natural_gas.dummy_gas_components import ( SimpleGasConsumerCost, SimpleGasProducerCost, SimpleGasConsumerPerformance, SimpleGasProducerPerformance, ) -from h2integrate.demand.demand_openloop_converter_controller import ( - DemandOpenLoopConverterController, -) from h2integrate.converters.hydrogen.geologic.mathur_modified import GeoH2SubsurfaceCostModel from h2integrate.resource.solar.nlr_developer_goes_api_models import ( GOESTMYSolarAPI, diff --git a/h2integrate/demand/openloop_controller_base.py b/h2integrate/demand/demand_base.py similarity index 100% rename from h2integrate/demand/openloop_controller_base.py rename to h2integrate/demand/demand_base.py diff --git a/h2integrate/demand/flexible_demand_openloop_controller.py b/h2integrate/demand/flexible_demand.py similarity index 99% rename from h2integrate/demand/flexible_demand_openloop_controller.py rename to h2integrate/demand/flexible_demand.py index 26bfe5e42..2bf8c6ac6 100644 --- a/h2integrate/demand/flexible_demand_openloop_controller.py +++ b/h2integrate/demand/flexible_demand.py @@ -2,7 +2,7 @@ from attrs import field, define from h2integrate.core.validators import gte_zero, range_val -from h2integrate.demand.openloop_controller_base import ( +from h2integrate.demand.demand_base import ( ConverterOpenLoopControlBase, ConverterOpenLoopControlBaseConfig, ) diff --git a/h2integrate/demand/demand_openloop_converter_controller.py b/h2integrate/demand/generic_demand.py similarity index 98% rename from h2integrate/demand/demand_openloop_converter_controller.py rename to h2integrate/demand/generic_demand.py index 304df72e2..da7ad8a8d 100644 --- a/h2integrate/demand/demand_openloop_converter_controller.py +++ b/h2integrate/demand/generic_demand.py @@ -1,6 +1,6 @@ import numpy as np -from h2integrate.demand.openloop_controller_base import ( +from h2integrate.demand.demand_base import ( ConverterOpenLoopControlBase, ConverterOpenLoopControlBaseConfig, ) diff --git a/h2integrate/demand/test/test_demand_components.py b/h2integrate/demand/test/test_demand_components.py index 91c5b2340..a7aa156ae 100644 --- a/h2integrate/demand/test/test_demand_components.py +++ b/h2integrate/demand/test/test_demand_components.py @@ -3,12 +3,8 @@ import openmdao.api as om from pytest import fixture -from h2integrate.demand.flexible_demand_openloop_controller import ( - FlexibleDemandOpenLoopConverterController, -) -from h2integrate.demand.demand_openloop_converter_controller import ( - DemandOpenLoopConverterController, -) +from h2integrate.demand.generic_demand import DemandOpenLoopConverterController +from h2integrate.demand.flexible_demand import FlexibleDemandOpenLoopConverterController @fixture From 7ad7b59a32086267d9f1da7cd465049a71b8854d Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Tue, 7 Apr 2026 16:47:19 -0600 Subject: [PATCH 10/17] renamed demand classes and configs --- docs/control/control_overview.md | 4 ++-- docs/control/open-loop_controllers.md | 10 +++++----- docs/user_guide/model_overview.md | 4 ++-- .../flexible_demand_tech_config.yaml | 2 +- .../23_solar_wind_ng_demand/tech_config.yaml | 2 +- h2integrate/core/supported_models.py | 8 ++++---- h2integrate/demand/demand_base.py | 4 ++-- h2integrate/demand/flexible_demand.py | 11 ++++------- h2integrate/demand/generic_demand.py | 9 +++------ .../demand/test/test_demand_components.py | 16 ++++++++-------- 10 files changed, 32 insertions(+), 38 deletions(-) diff --git a/docs/control/control_overview.md b/docs/control/control_overview.md index 902e7462f..70bde5c9b 100644 --- a/docs/control/control_overview.md +++ b/docs/control/control_overview.md @@ -9,8 +9,8 @@ The first approach, [open-loop control](#open-loop-control), assumes no feedback Supported controllers: - [`SimpleStorageOpenLoopController`](#pass-through-controller) - [`DemandOpenLoopStorageController`](#demand-open-loop-storage-controller) -- [`DemandOpenLoopConverterController`](#demand-open-loop-converter-controller) -- [`FlexibleDemandOpenLoopConverterController`](#flexible-demand-open-loop-converter-controller) +- [`GenericDemandComponent`](#demand-open-loop-converter-controller) +- [`FlexibleDemandComponent`](#flexible-demand-open-loop-converter-controller) (pyomo-control-framework)= diff --git a/docs/control/open-loop_controllers.md b/docs/control/open-loop_controllers.md index 61210eabf..9241f1c26 100644 --- a/docs/control/open-loop_controllers.md +++ b/docs/control/open-loop_controllers.md @@ -37,7 +37,7 @@ This page documents two core controller types: (demand-open-loop-converter-controller)= ### Demand Open-Loop Converter Controller -The `DemandOpenLoopConverterController` allocates commodity input to meet a defined demand profile. It does not contain energy storage logic, only **instantaneous** matching of supply and demand. +The `GenericDemandComponent` allocates commodity input to meet a defined demand profile. It does not contain energy storage logic, only **instantaneous** matching of supply and demand. The controller computes each value per timestep: - Unmet demand (non-zero when supply < demand, otherwise 0.) @@ -57,19 +57,19 @@ The controller is defined within the `tech_config` and requires these inputs. ```yaml control_strategy: - model: DemandOpenLoopConverterController + model: GenericDemandComponent model_inputs: control_parameters: commodity_name: hydrogen commodity_units: kg/h demand_profile: [10, 10, 12, 15, 14] ``` -For an example of how to use the `DemandOpenLoopConverterController` open-loop control framework, see the following: +For an example of how to use the `GenericDemandComponent` open-loop control framework, see the following: - `examples/23_solar_wind_ng_demand` (flexible-demand-open-loop-converter-controller)= ### Flexible Demand Open-Loop Converter Controller -The `FlexibleDemandOpenLoopConverterController` extends the fixed-demand controller by allowing the actual demand to flex up or down within defined bounds. This is useful for demand-side management scenarios where: +The `FlexibleDemandComponent` extends the fixed-demand controller by allowing the actual demand to flex up or down within defined bounds. This is useful for demand-side management scenarios where: - Processes can defer demand (e.g., flexible industrial loads) - The system requires demand elasticity without dynamic optimization @@ -81,7 +81,7 @@ The controller computes: Everything remains open-loop no storage, no intertemporal coupling. -For an example of how to use the `FlexibleDemandOpenLoopConverterController` open-loop control framework, see the following: +For an example of how to use the `FlexibleDemandComponent` open-loop control framework, see the following: - `examples/23_solar_wind_ng_demand` The flexible demand component takes an input commodity production profile, the maximum demand profile, and various constraints (listed below), and creates a "flexible demand profile" that follows the original input commodity production profile while satisfying varying constraint. diff --git a/docs/user_guide/model_overview.md b/docs/user_guide/model_overview.md index f2074bdf7..53bc0f6a6 100644 --- a/docs/user_guide/model_overview.md +++ b/docs/user_guide/model_overview.md @@ -285,7 +285,7 @@ Below summarizes the available performance, cost, and financial models for each - `'DemandOpenLoopStorageController'`: open-loop control; manages resource flow based on demand and storage constraints - `'HeuristicLoadFollowingController'`: open-loop control that works on a time window basis to set dispatch commands; uses Pyomo - Converter Controllers: - - `'DemandOpenLoopConverterController'`: open-loop control; manages resource flow based on demand constraints - - `'FlexibleDemandOpenLoopConverterController'`: open-loop control; manages resource flow based on demand and flexibility constraints + - `'GenericDemandComponent'`: open-loop control; manages resource flow based on demand constraints + - `'FlexibleDemandComponent'`: open-loop control; manages resource flow based on demand and flexibility constraints - Optimized Dispatch: - `'OptimizedDispatchController'`: optimization-based dispatch using Pyomo diff --git a/examples/23_solar_wind_ng_demand/flexible_demand_tech_config.yaml b/examples/23_solar_wind_ng_demand/flexible_demand_tech_config.yaml index 4f10f7ddc..1fdb2403d 100644 --- a/examples/23_solar_wind_ng_demand/flexible_demand_tech_config.yaml +++ b/examples/23_solar_wind_ng_demand/flexible_demand_tech_config.yaml @@ -92,7 +92,7 @@ technologies: cost_year: 2023 electrical_load_demand: control_strategy: - model: FlexibleDemandOpenLoopConverterController + model: FlexibleDemandComponent model_inputs: control_parameters: commodity: electricity diff --git a/examples/23_solar_wind_ng_demand/tech_config.yaml b/examples/23_solar_wind_ng_demand/tech_config.yaml index 5fbabe17f..14e464da5 100644 --- a/examples/23_solar_wind_ng_demand/tech_config.yaml +++ b/examples/23_solar_wind_ng_demand/tech_config.yaml @@ -92,7 +92,7 @@ technologies: cost_year: 2023 electrical_load_demand: control_strategy: - model: DemandOpenLoopConverterController + model: GenericDemandComponent model_inputs: control_parameters: commodity: electricity diff --git a/h2integrate/core/supported_models.py b/h2integrate/core/supported_models.py index d082d8a71..b9fed1750 100644 --- a/h2integrate/core/supported_models.py +++ b/h2integrate/core/supported_models.py @@ -6,10 +6,10 @@ from h2integrate.converters.grid.grid import GridCostModel, GridPerformanceModel from h2integrate.finances.profast_lco import ProFastLCO from h2integrate.finances.profast_npv import ProFastNPV -from h2integrate.demand.generic_demand import DemandOpenLoopConverterController +from h2integrate.demand.generic_demand import GenericDemandComponent from h2integrate.converters.steel.steel import SteelPerformanceModel, SteelCostAndFinancialModel from h2integrate.converters.wind.floris import FlorisWindPlantPerformanceModel -from h2integrate.demand.flexible_demand import FlexibleDemandOpenLoopConverterController +from h2integrate.demand.flexible_demand import FlexibleDemandComponent from h2integrate.converters.wind.wind_pysam import PYSAMWindPlantPerformanceModel from h2integrate.transporters.generic_summer import GenericSummerPerformanceModel from h2integrate.converters.hopp.hopp_wrapper import HOPPComponent @@ -280,8 +280,8 @@ "DemandOpenLoopStorageController": DemandOpenLoopStorageController, "HeuristicLoadFollowingController": HeuristicLoadFollowingController, "OptimizedDispatchController": OptimizedDispatchController, - "DemandOpenLoopConverterController": DemandOpenLoopConverterController, - "FlexibleDemandOpenLoopConverterController": FlexibleDemandOpenLoopConverterController, + "GenericDemandComponent": GenericDemandComponent, + "FlexibleDemandComponent": FlexibleDemandComponent, # Dispatch "PyomoDispatchGenericConverter": PyomoDispatchGenericConverter, "PyomoRuleStorageBaseclass": PyomoRuleStorageBaseclass, diff --git a/h2integrate/demand/demand_base.py b/h2integrate/demand/demand_base.py index f16c87a2a..06e339923 100644 --- a/h2integrate/demand/demand_base.py +++ b/h2integrate/demand/demand_base.py @@ -5,7 +5,7 @@ @define(kw_only=True) -class ConverterOpenLoopControlBaseConfig(BaseConfig): +class DemandComponentBaseConfig(BaseConfig): """Configuration for defining an open-loop demand profile. This configuration object specifies the commodity being controlled and the @@ -25,7 +25,7 @@ class ConverterOpenLoopControlBaseConfig(BaseConfig): demand_profile: int | float | list = field() -class ConverterOpenLoopControlBase(PerformanceModelBaseClass): +class DemandComponentBase(PerformanceModelBaseClass): """Base OpenMDAO component for open-loop demand tracking. This component defines the interfaces required for open-loop demand diff --git a/h2integrate/demand/flexible_demand.py b/h2integrate/demand/flexible_demand.py index 2bf8c6ac6..5f36149a5 100644 --- a/h2integrate/demand/flexible_demand.py +++ b/h2integrate/demand/flexible_demand.py @@ -2,14 +2,11 @@ from attrs import field, define from h2integrate.core.validators import gte_zero, range_val -from h2integrate.demand.demand_base import ( - ConverterOpenLoopControlBase, - ConverterOpenLoopControlBaseConfig, -) +from h2integrate.demand.demand_base import DemandComponentBase, DemandComponentBaseConfig @define(kw_only=True) -class FlexibleDemandOpenLoopConverterControllerConfig(ConverterOpenLoopControlBaseConfig): +class FlexibleDemandComponentConfig(DemandComponentBaseConfig): """Configuration for defining a flexible demand open-loop controller. Extends :class:`DemandOpenLoopControlBaseConfig` with additional parameters @@ -42,7 +39,7 @@ class FlexibleDemandOpenLoopConverterControllerConfig(ConverterOpenLoopControlBa min_utilization: float = field(validator=range_val(0, 1.0)) -class FlexibleDemandOpenLoopConverterController(ConverterOpenLoopControlBase): +class FlexibleDemandComponent(DemandComponentBase): """Open-loop controller for flexible demand with ramping and utilization constraints. This controller extends the base demand controller by allowing the effective @@ -58,7 +55,7 @@ def setup(self): utilization, all expressed as fractions of maximum demand. Adds the flexible demand output profile, which will be populated in ``compute``. """ - self.config = FlexibleDemandOpenLoopConverterControllerConfig.from_dict( + self.config = FlexibleDemandComponentConfig.from_dict( self.options["tech_config"]["model_inputs"]["control_parameters"], additional_cls_name=self.__class__.__name__, ) diff --git a/h2integrate/demand/generic_demand.py b/h2integrate/demand/generic_demand.py index da7ad8a8d..f38da1729 100644 --- a/h2integrate/demand/generic_demand.py +++ b/h2integrate/demand/generic_demand.py @@ -1,12 +1,9 @@ import numpy as np -from h2integrate.demand.demand_base import ( - ConverterOpenLoopControlBase, - ConverterOpenLoopControlBaseConfig, -) +from h2integrate.demand.demand_base import DemandComponentBase, DemandComponentBaseConfig -class DemandOpenLoopConverterController(ConverterOpenLoopControlBase): +class GenericDemandComponent(DemandComponentBase): """Open-loop controller for converting input supply into met demand. This controller computes unmet demand, unused (curtailed) production, and @@ -32,7 +29,7 @@ def setup(self): KeyError: If the expected configuration keys are missing from ``tech_config``. """ - self.config = ConverterOpenLoopControlBaseConfig.from_dict( + self.config = DemandComponentBaseConfig.from_dict( self.options["tech_config"]["model_inputs"]["control_parameters"], additional_cls_name=self.__class__.__name__, ) diff --git a/h2integrate/demand/test/test_demand_components.py b/h2integrate/demand/test/test_demand_components.py index a7aa156ae..039ae4d85 100644 --- a/h2integrate/demand/test/test_demand_components.py +++ b/h2integrate/demand/test/test_demand_components.py @@ -3,8 +3,8 @@ import openmdao.api as om from pytest import fixture -from h2integrate.demand.generic_demand import DemandOpenLoopConverterController -from h2integrate.demand.flexible_demand import FlexibleDemandOpenLoopConverterController +from h2integrate.demand.generic_demand import GenericDemandComponent +from h2integrate.demand.flexible_demand import FlexibleDemandComponent @fixture @@ -33,7 +33,7 @@ def test_demand_converter_controller(subtests): tech_config["technologies"]["load"] = { "control_strategy": { - "model": "DemandOpenLoopConverterController", + "model": "GenericDemandComponent", }, "model_inputs": { "control_parameters": { @@ -57,7 +57,7 @@ def test_demand_converter_controller(subtests): prob.model.add_subsystem( "demand_open_loop_storage_controller", - DemandOpenLoopConverterController( + GenericDemandComponent( plant_config=plant_config, tech_config=tech_config["technologies"]["load"] ), promotes=["*"], @@ -95,7 +95,7 @@ def test_flexible_demand_converter_controller(subtests, variable_h2_production_p min_demand_kg = 2.5 tech_config["technologies"]["load"] = { "control_strategy": { - "model": "FlexibleDemandOpenLoopConverterController", + "model": "FlexibleDemandComponent", }, "model_inputs": { "control_parameters": { @@ -129,7 +129,7 @@ def test_flexible_demand_converter_controller(subtests, variable_h2_production_p prob.model.add_subsystem( "flexible_demand_open_loop_converter_controller", - FlexibleDemandOpenLoopConverterController( + FlexibleDemandComponent( plant_config=plant_config, tech_config=tech_config["technologies"]["load"] ), promotes=["*"], @@ -199,7 +199,7 @@ def test_flexible_demand_converter_controller_min_utilization( min_demand_kg = 2.5 tech_config["technologies"]["load"] = { "control_strategy": { - "model": "FlexibleDemandOpenLoopConverterController", + "model": "FlexibleDemandComponent", }, "model_inputs": { "control_parameters": { @@ -233,7 +233,7 @@ def test_flexible_demand_converter_controller_min_utilization( prob.model.add_subsystem( "DemandOpenLoopStorageController", - FlexibleDemandOpenLoopConverterController( + FlexibleDemandComponent( plant_config=plant_config, tech_config=tech_config["technologies"]["load"] ), promotes=["*"], From 8444cdf85ad1bd7414f97c778adf0fa7082bad36 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Wed, 8 Apr 2026 14:53:09 -0600 Subject: [PATCH 11/17] renamed demand component to be performance model instead of control strategy --- .../flexible_demand_tech_config.yaml | 4 ++-- examples/23_solar_wind_ng_demand/tech_config.yaml | 4 ++-- h2integrate/demand/demand_base.py | 11 ++++++++--- h2integrate/demand/flexible_demand.py | 4 +++- h2integrate/demand/generic_demand.py | 4 +++- h2integrate/demand/test/test_demand_components.py | 12 ++++++------ 6 files changed, 24 insertions(+), 15 deletions(-) diff --git a/examples/23_solar_wind_ng_demand/flexible_demand_tech_config.yaml b/examples/23_solar_wind_ng_demand/flexible_demand_tech_config.yaml index 1fdb2403d..d0c83fab0 100644 --- a/examples/23_solar_wind_ng_demand/flexible_demand_tech_config.yaml +++ b/examples/23_solar_wind_ng_demand/flexible_demand_tech_config.yaml @@ -91,10 +91,10 @@ technologies: variable_opex_per_mwh: 0.0 # $/MWh cost_year: 2023 electrical_load_demand: - control_strategy: + performance_model: model: FlexibleDemandComponent model_inputs: - control_parameters: + performance_parameters: commodity: electricity commodity_rate_units: kW rated_demand: 100000 diff --git a/examples/23_solar_wind_ng_demand/tech_config.yaml b/examples/23_solar_wind_ng_demand/tech_config.yaml index 14e464da5..e044c1788 100644 --- a/examples/23_solar_wind_ng_demand/tech_config.yaml +++ b/examples/23_solar_wind_ng_demand/tech_config.yaml @@ -91,10 +91,10 @@ technologies: variable_opex_per_mwh: 0.0 # $/MWh cost_year: 2023 electrical_load_demand: - control_strategy: + performance_model: model: GenericDemandComponent model_inputs: - control_parameters: + performance_parameters: commodity: electricity commodity_rate_units: kW demand_profile: 100000 # 100 MW diff --git a/h2integrate/demand/demand_base.py b/h2integrate/demand/demand_base.py index 06e339923..f8131b898 100644 --- a/h2integrate/demand/demand_base.py +++ b/h2integrate/demand/demand_base.py @@ -18,11 +18,18 @@ class DemandComponentBaseConfig(BaseConfig): demand_profile (int | float | list): Demand values for each timestep, in the same units as `commodity_rate_units`. May be a scalar for constant demand or a list/array for time-varying demand. + commodity_amount_units (str | None, optional): Units of the commodity as an amount + (i.e., kW*h or kg). If not provided, defaults to commodity_rate_units*h. """ commodity: str = field(converter=str.strip) commodity_rate_units: str = field(converter=str.strip) demand_profile: int | float | list = field() + commodity_amount_units: str = field(default=None) + + def __attrs_post_init__(self): + if self.commodity_amount_units is None: + self.commodity_amount_units = f"({self.commodity_rate_units})*h" class DemandComponentBase(PerformanceModelBaseClass): @@ -48,9 +55,7 @@ def setup(self): """ self.commodity = self.config.commodity self.commodity_rate_units = self.config.commodity_rate_units - self.commodity_amount_units = getattr( - self.config, "commodity_amount_units", f"({self.config.commodity_rate_units})*h" - ) + self.commodity_amount_units = self.config.commodity_amount_units super().setup() diff --git a/h2integrate/demand/flexible_demand.py b/h2integrate/demand/flexible_demand.py index 5f36149a5..1fc19c5c4 100644 --- a/h2integrate/demand/flexible_demand.py +++ b/h2integrate/demand/flexible_demand.py @@ -1,6 +1,7 @@ import numpy as np from attrs import field, define +from h2integrate.core.utilities import merge_shared_inputs from h2integrate.core.validators import gte_zero, range_val from h2integrate.demand.demand_base import DemandComponentBase, DemandComponentBaseConfig @@ -56,7 +57,8 @@ def setup(self): flexible demand output profile, which will be populated in ``compute``. """ self.config = FlexibleDemandComponentConfig.from_dict( - self.options["tech_config"]["model_inputs"]["control_parameters"], + merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), + strict=True, additional_cls_name=self.__class__.__name__, ) super().setup() diff --git a/h2integrate/demand/generic_demand.py b/h2integrate/demand/generic_demand.py index f38da1729..94834f742 100644 --- a/h2integrate/demand/generic_demand.py +++ b/h2integrate/demand/generic_demand.py @@ -1,5 +1,6 @@ import numpy as np +from h2integrate.core.utilities import merge_shared_inputs from h2integrate.demand.demand_base import DemandComponentBase, DemandComponentBaseConfig @@ -30,7 +31,8 @@ def setup(self): ``tech_config``. """ self.config = DemandComponentBaseConfig.from_dict( - self.options["tech_config"]["model_inputs"]["control_parameters"], + merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), + strict=True, additional_cls_name=self.__class__.__name__, ) super().setup() diff --git a/h2integrate/demand/test/test_demand_components.py b/h2integrate/demand/test/test_demand_components.py index 039ae4d85..45cbca3b9 100644 --- a/h2integrate/demand/test/test_demand_components.py +++ b/h2integrate/demand/test/test_demand_components.py @@ -32,11 +32,11 @@ def test_demand_converter_controller(subtests): tech_config = {"technologies": {}} tech_config["technologies"]["load"] = { - "control_strategy": { + "performance_model": { "model": "GenericDemandComponent", }, "model_inputs": { - "control_parameters": { + "performance_parameters": { "commodity": "hydrogen", "commodity_rate_units": "kg", "demand_profile": [5.0] * 10, # Example: 10 time steps with 5 kg/time step demand @@ -94,11 +94,11 @@ def test_flexible_demand_converter_controller(subtests, variable_h2_production_p ramp_down_rate_kg = 2.0 min_demand_kg = 2.5 tech_config["technologies"]["load"] = { - "control_strategy": { + "performance_model": { "model": "FlexibleDemandComponent", }, "model_inputs": { - "control_parameters": { + "performance_parameters": { "commodity": "hydrogen", "commodity_rate_units": "kg", "rated_demand": end_use_rated_demand, @@ -198,11 +198,11 @@ def test_flexible_demand_converter_controller_min_utilization( ramp_down_rate_kg = 2.0 min_demand_kg = 2.5 tech_config["technologies"]["load"] = { - "control_strategy": { + "performance_model": { "model": "FlexibleDemandComponent", }, "model_inputs": { - "control_parameters": { + "performance_parameters": { "commodity": "hydrogen", "commodity_rate_units": "kg", "rated_demand": end_use_rated_demand, From 2093b810499eeb31717ae54eb6fb1f6938dcc581 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:13:59 -0600 Subject: [PATCH 12/17] moved output calculations to shared method in baseclass --- h2integrate/demand/demand_base.py | 50 ++++++++++++++++++ h2integrate/demand/flexible_demand.py | 76 ++++++++++++++++----------- h2integrate/demand/generic_demand.py | 58 ++++++++++---------- 3 files changed, 126 insertions(+), 58 deletions(-) diff --git a/h2integrate/demand/demand_base.py b/h2integrate/demand/demand_base.py index f8131b898..0012dceae 100644 --- a/h2integrate/demand/demand_base.py +++ b/h2integrate/demand/demand_base.py @@ -1,3 +1,4 @@ +import numpy as np from attrs import field, define from h2integrate.core.utilities import BaseConfig @@ -99,3 +100,52 @@ def compute(): NotImplementedError: Always, unless implemented in a subclass. """ raise NotImplementedError("This method should be implemented in a subclass.") + + def calculate_outputs(self, commodity_in, commodity_demand, outputs): + """Compute unmet demand, unused commodity, and converter output. + + This method compares the demand profile to the supplied commodity for + each timestep and assigns unmet demand, curtailed production, and + actual delivered output. + + Args: + commodity_in (np.array): supplied commodity profile + commodity_demand (np.array): entire commodity demand profile + outputs (dict-like): Mapping of output variable names where results + will be written, including: + + * ``unmet_{commodity}_demand_out``: Unmet demand. + * ``unused_{commodity}_out``: Curtailed production. + * ``{commodity}_out``: Actual output delivered. + + Notes: + All variables operate on a per-timestep basis and typically have + array shape ``(n_timesteps,)``. + """ + + remaining_demand = commodity_demand - commodity_in + + # Calculate missed load and curtailed production + outputs[f"unmet_{self.commodity}_demand_out"] = np.where( + remaining_demand > 0, remaining_demand, 0 + ) + outputs[f"unused_{self.commodity}_out"] = np.where( + remaining_demand < 0, -1 * remaining_demand, 0 + ) + + # Calculate actual output based on demand met and curtailment + outputs[f"{self.commodity}_out"] = commodity_in - outputs[f"unused_{self.commodity}_out"] + + outputs[f"rated_{self.commodity}_production"] = commodity_demand.mean() + + outputs[f"total_{self.commodity}_produced"] = np.sum(outputs[f"{self.commodity}_out"]) * ( + self.dt / 3600 + ) + + outputs[f"annual_{self.commodity}_produced"] = ( + outputs[f"total_{self.commodity}_produced"] / self.fraction_of_year_simulated + ) + + outputs["capacity_factor"] = outputs[f"{self.commodity}_out"].sum() / commodity_demand.sum() + + return outputs diff --git a/h2integrate/demand/flexible_demand.py b/h2integrate/demand/flexible_demand.py index 1fc19c5c4..820ac6574 100644 --- a/h2integrate/demand/flexible_demand.py +++ b/h2integrate/demand/flexible_demand.py @@ -260,17 +260,28 @@ def compute(self, inputs, outputs): """ - remaining_demand = inputs[f"{self.commodity}_demand"] - inputs[f"{self.commodity}_in"] + # remaining_demand = inputs[f"{self.commodity}_demand"] - inputs[f"{self.commodity}_in"] if self.config.min_utilization == 1.0: # Calculate missed load and curtailed production - outputs[f"unmet_{self.commodity}_demand_out"] = np.where( - remaining_demand > 0, remaining_demand, 0 - ) - outputs[f"unused_{self.commodity}_out"] = np.where( - remaining_demand < 0, -1 * remaining_demand, 0 + # outputs[f"unmet_{self.commodity}_demand_out"] = np.where( + # remaining_demand > 0, remaining_demand, 0 + # ) + # outputs[f"unused_{self.commodity}_out"] = np.where( + # remaining_demand < 0, -1 * remaining_demand, 0 + # ) + + outputs[f"{self.commodity}_flexible_demand_profile"] = inputs[ + f"{self.commodity}_demand" + ] + + outputs = self.calculate_outputs( + inputs[f"{self.commodity}_in"], inputs[f"{self.commodity}_demand"], outputs ) + else: + remaining_demand = inputs[f"{self.commodity}_demand"] - inputs[f"{self.commodity}_in"] + # when remaining demand is less than 0, that means input exceeds demand # multiply by -1 to make it positive curtailed = np.where(remaining_demand < 0, -1 * remaining_demand, 0) @@ -283,30 +294,35 @@ def compute(self, inputs, outputs): ) outputs[f"{self.commodity}_flexible_demand_profile"] = flexible_demand_profile - flexible_remaining_demand = flexible_demand_profile - inputs[f"{self.commodity}_in"] - outputs[f"unmet_{self.commodity}_demand_out"] = np.where( - flexible_remaining_demand > 0, flexible_remaining_demand, 0 + outputs = self.calculate_outputs( + inputs[f"{self.commodity}_in"], flexible_demand_profile, outputs ) - outputs[f"unused_{self.commodity}_out"] = np.where( - flexible_remaining_demand < 0, -1 * flexible_remaining_demand, 0 - ) - - # Calculate actual output based on demand met and curtailment - outputs[f"{self.commodity}_out"] = ( - inputs[f"{self.commodity}_in"] - outputs[f"unused_{self.commodity}_out"] - ) - outputs[f"rated_{self.commodity}_production"] = inputs[ - f"rated_{self.commodity}_demand" - ].mean() - outputs[f"total_{self.commodity}_produced"] = np.sum(outputs[f"{self.commodity}_out"]) * ( - self.dt / 3600 - ) - outputs[f"annual_{self.commodity}_produced"] = ( - outputs[f"total_{self.commodity}_produced"] / self.fraction_of_year_simulated - ) - - outputs["capacity_factor"] = ( - outputs[f"{self.commodity}_out"].sum() / inputs[f"{self.commodity}_demand"].sum() - ) + # flexible_remaining_demand = flexible_demand_profile - inputs[f"{self.commodity}_in"] + + # outputs[f"unmet_{self.commodity}_demand_out"] = np.where( + # flexible_remaining_demand > 0, flexible_remaining_demand, 0 + # ) + # outputs[f"unused_{self.commodity}_out"] = np.where( + # flexible_remaining_demand < 0, -1 * flexible_remaining_demand, 0 + # ) + + # # Calculate actual output based on demand met and curtailment + # outputs[f"{self.commodity}_out"] = ( + # inputs[f"{self.commodity}_in"] - outputs[f"unused_{self.commodity}_out"] + # ) + # outputs[f"rated_{self.commodity}_production"] = inputs[ + # f"rated_{self.commodity}_demand" + # ].mean() + + # outputs[f"total_{self.commodity}_produced"] = np.sum(outputs[f"{self.commodity}_out"]) * ( + # self.dt / 3600 + # ) + # outputs[f"annual_{self.commodity}_produced"] = ( + # outputs[f"total_{self.commodity}_produced"] / self.fraction_of_year_simulated + # ) + + # outputs["capacity_factor"] = ( + # outputs[f"{self.commodity}_out"].sum() / inputs[f"{self.commodity}_demand"].sum() + # ) diff --git a/h2integrate/demand/generic_demand.py b/h2integrate/demand/generic_demand.py index 94834f742..0294c1f75 100644 --- a/h2integrate/demand/generic_demand.py +++ b/h2integrate/demand/generic_demand.py @@ -1,5 +1,3 @@ -import numpy as np - from h2integrate.core.utilities import merge_shared_inputs from h2integrate.demand.demand_base import DemandComponentBase, DemandComponentBaseConfig @@ -61,31 +59,35 @@ def compute(self, inputs, outputs): All variables operate on a per-timestep basis and typically have array shape ``(n_timesteps,)``. """ - remaining_demand = inputs[f"{self.commodity}_demand"] - inputs[f"{self.commodity}_in"] - - # Calculate missed load and curtailed production - outputs[f"unmet_{self.commodity}_demand_out"] = np.where( - remaining_demand > 0, remaining_demand, 0 - ) - outputs[f"unused_{self.commodity}_out"] = np.where( - remaining_demand < 0, -1 * remaining_demand, 0 - ) - - # Calculate actual output based on demand met and curtailment - outputs[f"{self.commodity}_out"] = ( - inputs[f"{self.commodity}_in"] - outputs[f"unused_{self.commodity}_out"] - ) - - outputs[f"rated_{self.commodity}_production"] = inputs[f"{self.commodity}_demand"].mean() - - outputs[f"total_{self.commodity}_produced"] = np.sum(outputs[f"{self.commodity}_out"]) * ( - self.dt / 3600 - ) - - outputs[f"annual_{self.commodity}_produced"] = ( - outputs[f"total_{self.commodity}_produced"] / self.fraction_of_year_simulated - ) - outputs["capacity_factor"] = ( - outputs[f"{self.commodity}_out"].sum() / inputs[f"{self.commodity}_demand"].sum() + outputs = self.calculate_outputs( + inputs[f"{self.commodity}_in"], inputs[f"{self.commodity}_demand"], outputs ) + # remaining_demand = inputs[f"{self.commodity}_demand"] - inputs[f"{self.commodity}_in"] + + # # Calculate missed load and curtailed production + # outputs[f"unmet_{self.commodity}_demand_out"] = np.where( + # remaining_demand > 0, remaining_demand, 0 + # ) + # outputs[f"unused_{self.commodity}_out"] = np.where( + # remaining_demand < 0, -1 * remaining_demand, 0 + # ) + + # # Calculate actual output based on demand met and curtailment + # outputs[f"{self.commodity}_out"] = ( + # inputs[f"{self.commodity}_in"] - outputs[f"unused_{self.commodity}_out"] + # ) + + # outputs[f"rated_{self.commodity}_production"] = inputs[f"{self.commodity}_demand"].mean() + + # outputs[f"total_{self.commodity}_produced"] = np.sum(outputs[f"{self.commodity}_out"]) * ( + # self.dt / 3600 + # ) + + # outputs[f"annual_{self.commodity}_produced"] = ( + # outputs[f"total_{self.commodity}_produced"] / self.fraction_of_year_simulated + # ) + + # outputs["capacity_factor"] = ( + # outputs[f"{self.commodity}_out"].sum() / inputs[f"{self.commodity}_demand"].sum() + # ) From d116ad89d4c72b8d8a35680c683db152f3585bc9 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:23:35 -0600 Subject: [PATCH 13/17] removed the word controller from docstrings --- h2integrate/demand/demand_base.py | 16 ++++++++-------- h2integrate/demand/flexible_demand.py | 8 ++++---- h2integrate/demand/generic_demand.py | 19 +++++-------------- 3 files changed, 17 insertions(+), 26 deletions(-) diff --git a/h2integrate/demand/demand_base.py b/h2integrate/demand/demand_base.py index 0012dceae..a9477db8f 100644 --- a/h2integrate/demand/demand_base.py +++ b/h2integrate/demand/demand_base.py @@ -7,9 +7,9 @@ @define(kw_only=True) class DemandComponentBaseConfig(BaseConfig): - """Configuration for defining an open-loop demand profile. + """Configuration for defining a demand profile. - This configuration object specifies the commodity being controlled and the + This configuration object specifies the commodity being demanded and the demand profile that should be met by downstream components. Attributes: @@ -36,19 +36,19 @@ def __attrs_post_init__(self): class DemandComponentBase(PerformanceModelBaseClass): """Base OpenMDAO component for open-loop demand tracking. - This component defines the interfaces required for open-loop demand - controllers, including inputs for demand, supplied commodity, and outputs + This component defines the interfaces required for demand + components, including inputs for demand, supplied commodity, and outputs tracking unmet demand, unused production, and total unmet demand. Subclasses must implement the :meth:`compute` method to define the - controller behavior. + demand component behavior. """ def setup(self): - """Define inputs and outputs for demand control. + """Define inputs and outputs for demand component. Creates time-series inputs and outputs for commodity demand, supply, unmet demand, unused commodity, and total unmet demand. Shapes and units - are determined by the plant configuration and controller configuration. + are determined by the plant configuration and load component configuration. Raises: KeyError: If required configuration keys are missing from @@ -94,7 +94,7 @@ def setup(self): def compute(): """This method must be implemented by subclasses to define the - controller. + demand component. Raises: NotImplementedError: Always, unless implemented in a subclass. diff --git a/h2integrate/demand/flexible_demand.py b/h2integrate/demand/flexible_demand.py index 820ac6574..48f85b78a 100644 --- a/h2integrate/demand/flexible_demand.py +++ b/h2integrate/demand/flexible_demand.py @@ -8,7 +8,7 @@ @define(kw_only=True) class FlexibleDemandComponentConfig(DemandComponentBaseConfig): - """Configuration for defining a flexible demand open-loop controller. + """Configuration for defining a flexible demand component. Extends :class:`DemandOpenLoopControlBaseConfig` with additional parameters required for dynamically adjusting demand based on turndown, ramping, and @@ -41,9 +41,9 @@ class FlexibleDemandComponentConfig(DemandComponentBaseConfig): class FlexibleDemandComponent(DemandComponentBase): - """Open-loop controller for flexible demand with ramping and utilization constraints. + """Demand component for flexible demand with ramping and utilization constraints. - This controller extends the base demand controller by allowing the effective + This component extends the base demand component by allowing the effective demand to vary dynamically based on turndown constraints, ramp-rate limits, and minimum-utilization requirements. A flexible demand profile is generated and used to compute unmet demand, unused commodity, and delivered output. @@ -246,7 +246,7 @@ def compute(self, inputs, outputs): """Compute unmet demand, unused commodity, and output under flexible demand. If ``min_utilization == 1.0``, the behavior matches the regular open-loop - controller with no flexible demand adjustments. + component with no flexible demand adjustments. Otherwise: * Construct a flexible demand profile. diff --git a/h2integrate/demand/generic_demand.py b/h2integrate/demand/generic_demand.py index 0294c1f75..37ef69fbe 100644 --- a/h2integrate/demand/generic_demand.py +++ b/h2integrate/demand/generic_demand.py @@ -3,9 +3,9 @@ class GenericDemandComponent(DemandComponentBase): - """Open-loop controller for converting input supply into met demand. + """Component for for converting input supply into met demand. - This controller computes unmet demand, unused (curtailed) production, and + This component computes unmet demand, unused (curtailed) production, and the resulting commodity output profile based on the incoming supply and an externally specified demand profile. It uses simple arithmetic rules: @@ -14,20 +14,11 @@ class GenericDemandComponent(DemandComponentBase): * Output equals supplied commodity minus curtailed commodity. This component relies on configuration provided through the - ``tech_config`` dictionary, which must define the controller's - ``control_parameters``. + ``tech_config`` dictionary, which must define the demand's + ``performance_parameters``. """ def setup(self): - """Set up the load controller configuration. - - Loads the controller configuration from ``tech_config`` and then calls - the base class ``setup`` to create inputs/outputs. - - Raises: - KeyError: If the expected configuration keys are missing from - ``tech_config``. - """ self.config = DemandComponentBaseConfig.from_dict( merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), strict=True, @@ -53,7 +44,7 @@ def compute(self, inputs, outputs): * ``unmet_{commodity}_demand_out``: Unmet demand. * ``unused_{commodity}_out``: Curtailed production. - * ``{commodity}_set_point``: Actual output delivered. + * ``{commodity}_out``: Actual output delivered. Notes: All variables operate on a per-timestep basis and typically have From 7c5b9e52f8278d03b0af58b5efa4b920b915592c Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Wed, 8 Apr 2026 15:27:58 -0600 Subject: [PATCH 14/17] draft update to docs --- docs/control/control_overview.md | 3 +- docs/control/open-loop_controllers.md | 90 ------------------------- docs/demand/demand_components.md | 94 +++++++++++++++++++++++++++ docs/user_guide/model_overview.md | 9 ++- 4 files changed, 101 insertions(+), 95 deletions(-) create mode 100644 docs/demand/demand_components.md diff --git a/docs/control/control_overview.md b/docs/control/control_overview.md index 70bde5c9b..9c98af5e9 100644 --- a/docs/control/control_overview.md +++ b/docs/control/control_overview.md @@ -9,8 +9,7 @@ The first approach, [open-loop control](#open-loop-control), assumes no feedback Supported controllers: - [`SimpleStorageOpenLoopController`](#pass-through-controller) - [`DemandOpenLoopStorageController`](#demand-open-loop-storage-controller) -- [`GenericDemandComponent`](#demand-open-loop-converter-controller) -- [`FlexibleDemandComponent`](#flexible-demand-open-loop-converter-controller) + (pyomo-control-framework)= diff --git a/docs/control/open-loop_controllers.md b/docs/control/open-loop_controllers.md index 9241f1c26..dfb7f241c 100644 --- a/docs/control/open-loop_controllers.md +++ b/docs/control/open-loop_controllers.md @@ -26,93 +26,3 @@ An example of an N2 diagram for a system using the open-loop control framework f For examples of how to use the `DemandOpenLoopStorageController` open-loop control framework, see the following: - `examples/14_wind_hydrogen_dispatch/` - `examples/19_simple_dispatch/` - -## Open-Loop Converter Controllers - -Open-loop converter controllers define rule-based logic for meeting commodity demand profiles without using dynamic system feedback. These controllers operate independently at each timestep. - -This page documents two core controller types: -1. Demand Open-Loop Converter Controller — meets a fixed demand profile. -2. Flexible Demand Open-Loop Converter Controller — adjusts demand up or down within flexible bounds. - -(demand-open-loop-converter-controller)= -### Demand Open-Loop Converter Controller -The `GenericDemandComponent` allocates commodity input to meet a defined demand profile. It does not contain energy storage logic, only **instantaneous** matching of supply and demand. - -The controller computes each value per timestep: -- Unmet demand (non-zero when supply < demand, otherwise 0.) -- Unused commodity (non-zero when supply > demand, otherwise 0.) -- Delivered output (commodity supplied to demand sink) - -This provides a simple baseline for understanding supply–demand balance before adding complex controls. - -#### Configuration -The controller is defined within the `tech_config` and requires these inputs. - -| Field | Type | Description | -| ----------------- | -------------- | ------------------------------------- | -| `commodity_name` | `str` | Commodity name (e.g., `"hydrogen"`). | -| `commodity_units` | `str` | Units (e.g., `"kg/h"`). | -| `demand_profile` | scalar or list | Timeseries demand or constant demand. | - -```yaml -control_strategy: - model: GenericDemandComponent -model_inputs: - control_parameters: - commodity_name: hydrogen - commodity_units: kg/h - demand_profile: [10, 10, 12, 15, 14] -``` -For an example of how to use the `GenericDemandComponent` open-loop control framework, see the following: -- `examples/23_solar_wind_ng_demand` - -(flexible-demand-open-loop-converter-controller)= -### Flexible Demand Open-Loop Converter Controller -The `FlexibleDemandComponent` extends the fixed-demand controller by allowing the actual demand to flex up or down within defined bounds. This is useful for demand-side management scenarios where: -- Processes can defer demand (e.g., flexible industrial loads) -- The system requires demand elasticity without dynamic optimization - -The controller computes: -- Flexible demand (clamped within allowable ranges) -- Unmet flexible demand -- Unused commodity -- Delivered output - -Everything remains open-loop no storage, no intertemporal coupling. - -For an example of how to use the `FlexibleDemandComponent` open-loop control framework, see the following: -- `examples/23_solar_wind_ng_demand` - -The flexible demand component takes an input commodity production profile, the maximum demand profile, and various constraints (listed below), and creates a "flexible demand profile" that follows the original input commodity production profile while satisfying varying constraint. -Please see the figure below for an example of how the flexible demand profile can vary from the original demand profile based on the input commodity production profile and the ramp rates. -The axes are unlabeled to allow for generalization to any commodity and unit type. - -| ![Flexible Demand Example](figures/flex_demand_fig.png) | -|-| - - -#### Configuration -The flexible demand controller is defined within the `tech_config` with the following parameters: - -| Field | Type | Description | -| ------------------- | -------------- | -------------------------------------------- | -| `commodity_name` | `str` | Commodity name. | -| `commodity_units` | `str` | Units for all values. | -| `demand_profile` | scalar or list | Default (nominal) demand profile. | -| `turndown_ratio` | float | Minimum fraction of baseline demand allowed. | -| `ramp_down_rate_fraction` | float | Maximum ramp-down rate per timestep expressed as a fraction of baseline demand. | -| `ramp_up_rate_fraction` | float | Maximum ramp-up rate per timestep expressed as a fraction of baseline demand. | -| `min_utilization` | float | Minimum total fraction of baseline demand that must be met over the entire simulation. | - -```yaml -model_inputs: - control_parameters: - commodity_name: hydrogen - commodity_units: kg/h - demand_profile: [10, 12, 10, 8] - turndown_ratio: 0.1 - ramp_down_rate_fraction: 0.5 - ramp_up_rate_fraction: 0.5 - min_utilization: 0 -``` diff --git a/docs/demand/demand_components.md b/docs/demand/demand_components.md new file mode 100644 index 000000000..ac972ae11 --- /dev/null +++ b/docs/demand/demand_components.md @@ -0,0 +1,94 @@ +# Demand components + +- [`GenericDemandComponent`](#demand-open-loop-converter-controller) +- [`FlexibleDemandComponent`](#flexible-demand-open-loop-converter-controller) + +## Open-Loop Converter Controllers + +Open-loop converter controllers define rule-based logic for meeting commodity demand profiles without using dynamic system feedback. These controllers operate independently at each timestep. + +This page documents two core controller types: +1. Demand Open-Loop Converter Controller — meets a fixed demand profile. +2. Flexible Demand Open-Loop Converter Controller — adjusts demand up or down within flexible bounds. + +(demand-open-loop-converter-controller)= +### Demand Open-Loop Converter Controller +The `GenericDemandComponent` allocates commodity input to meet a defined demand profile. It does not contain energy storage logic, only **instantaneous** matching of supply and demand. + +The controller computes each value per timestep: +- Unmet demand (non-zero when supply < demand, otherwise 0.) +- Unused commodity (non-zero when supply > demand, otherwise 0.) +- Delivered output (commodity supplied to demand sink) + +This provides a simple baseline for understanding supply–demand balance before adding complex controls. + +#### Configuration +The controller is defined within the `tech_config` and requires these inputs. + +| Field | Type | Description | +| ----------------- | -------------- | ------------------------------------- | +| `commodity_name` | `str` | Commodity name (e.g., `"hydrogen"`). | +| `commodity_units` | `str` | Units (e.g., `"kg/h"`). | +| `demand_profile` | scalar or list | Timeseries demand or constant demand. | + +```yaml +control_strategy: + model: GenericDemandComponent +model_inputs: + control_parameters: + commodity_name: hydrogen + commodity_units: kg/h + demand_profile: [10, 10, 12, 15, 14] +``` +For an example of how to use the `GenericDemandComponent` open-loop control framework, see the following: +- `examples/23_solar_wind_ng_demand` + +(flexible-demand-open-loop-converter-controller)= +### Flexible Demand Open-Loop Converter Controller +The `FlexibleDemandComponent` extends the fixed-demand controller by allowing the actual demand to flex up or down within defined bounds. This is useful for demand-side management scenarios where: +- Processes can defer demand (e.g., flexible industrial loads) +- The system requires demand elasticity without dynamic optimization + +The controller computes: +- Flexible demand (clamped within allowable ranges) +- Unmet flexible demand +- Unused commodity +- Delivered output + +Everything remains open-loop no storage, no intertemporal coupling. + +For an example of how to use the `FlexibleDemandComponent` open-loop control framework, see the following: +- `examples/23_solar_wind_ng_demand` + +The flexible demand component takes an input commodity production profile, the maximum demand profile, and various constraints (listed below), and creates a "flexible demand profile" that follows the original input commodity production profile while satisfying varying constraint. +Please see the figure below for an example of how the flexible demand profile can vary from the original demand profile based on the input commodity production profile and the ramp rates. +The axes are unlabeled to allow for generalization to any commodity and unit type. + +| ![Flexible Demand Example](figures/flex_demand_fig.png) | +|-| + + +#### Configuration +The flexible demand controller is defined within the `tech_config` with the following parameters: + +| Field | Type | Description | +| ------------------- | -------------- | -------------------------------------------- | +| `commodity_name` | `str` | Commodity name. | +| `commodity_units` | `str` | Units for all values. | +| `demand_profile` | scalar or list | Default (nominal) demand profile. | +| `turndown_ratio` | float | Minimum fraction of baseline demand allowed. | +| `ramp_down_rate_fraction` | float | Maximum ramp-down rate per timestep expressed as a fraction of baseline demand. | +| `ramp_up_rate_fraction` | float | Maximum ramp-up rate per timestep expressed as a fraction of baseline demand. | +| `min_utilization` | float | Minimum total fraction of baseline demand that must be met over the entire simulation. | + +```yaml +model_inputs: + control_parameters: + commodity_name: hydrogen + commodity_units: kg/h + demand_profile: [10, 12, 10, 8] + turndown_ratio: 0.1 + ramp_down_rate_fraction: 0.5 + ramp_up_rate_fraction: 0.5 + min_utilization: 0 +``` diff --git a/docs/user_guide/model_overview.md b/docs/user_guide/model_overview.md index 53bc0f6a6..492986ce8 100644 --- a/docs/user_guide/model_overview.md +++ b/docs/user_guide/model_overview.md @@ -67,6 +67,7 @@ Below summarizes the available performance, cost, and financial models for each - [Storage Models](#storage-models) - [Basic Operations](#basic-operations) - [Control Models](#control-models) + - [DemandModels](#demand-models) (resource-models)= ## Resource models @@ -284,8 +285,10 @@ Below summarizes the available performance, cost, and financial models for each - `'SimpleStorageOpenLoopController'`: open-loop control; manages resource flow based on demand and input commodity - `'DemandOpenLoopStorageController'`: open-loop control; manages resource flow based on demand and storage constraints - `'HeuristicLoadFollowingController'`: open-loop control that works on a time window basis to set dispatch commands; uses Pyomo -- Converter Controllers: - - `'GenericDemandComponent'`: open-loop control; manages resource flow based on demand constraints - - `'FlexibleDemandComponent'`: open-loop control; manages resource flow based on demand and flexibility constraints - Optimized Dispatch: - `'OptimizedDispatchController'`: optimization-based dispatch using Pyomo + +(demand-models)= +## Demand Models +- `'GenericDemandComponent'`: manages resource flow based on demand constraints +- `'FlexibleDemandComponent'`: manages resource flow based on demand and flexibility constraints From f8a55fb7efcccb5351e16efafc7f260bc35b6e79 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Fri, 10 Apr 2026 11:49:17 -0600 Subject: [PATCH 15/17] updated demand docs --- docs/_toc.yml | 3 +++ docs/demand/demand_components.md | 34 ++++++++++++++------------------ 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/docs/_toc.yml b/docs/_toc.yml index 5a1a3dd5e..e14090382 100644 --- a/docs/_toc.yml +++ b/docs/_toc.yml @@ -69,6 +69,9 @@ parts: - file: control/open-loop_controllers - file: control/pyomo_controllers - file: control/controller_demonstrations + - caption: Demand + chapters: + - file: demand/demand_components - caption: Finance Models chapters: - file: finance_models/financial_analyses diff --git a/docs/demand/demand_components.md b/docs/demand/demand_components.md index ac972ae11..213bb2c1f 100644 --- a/docs/demand/demand_components.md +++ b/docs/demand/demand_components.md @@ -1,29 +1,27 @@ # Demand components -- [`GenericDemandComponent`](#demand-open-loop-converter-controller) -- [`FlexibleDemandComponent`](#flexible-demand-open-loop-converter-controller) +- [`GenericDemandComponent`](#generic-demand-component) +- [`FlexibleDemandComponent`](#flexible-demand-component) -## Open-Loop Converter Controllers - -Open-loop converter controllers define rule-based logic for meeting commodity demand profiles without using dynamic system feedback. These controllers operate independently at each timestep. +Demand components define rule-based logic for meeting commodity demand profiles without using dynamic system feedback. These components operate independently at each timestep. This page documents two core controller types: 1. Demand Open-Loop Converter Controller — meets a fixed demand profile. 2. Flexible Demand Open-Loop Converter Controller — adjusts demand up or down within flexible bounds. -(demand-open-loop-converter-controller)= -### Demand Open-Loop Converter Controller +(generic-demand-component)= +### Generic Demand Component The `GenericDemandComponent` allocates commodity input to meet a defined demand profile. It does not contain energy storage logic, only **instantaneous** matching of supply and demand. -The controller computes each value per timestep: +The demand component computes each value per timestep: - Unmet demand (non-zero when supply < demand, otherwise 0.) - Unused commodity (non-zero when supply > demand, otherwise 0.) - Delivered output (commodity supplied to demand sink) -This provides a simple baseline for understanding supply–demand balance before adding complex controls. +This provides a simple baseline for understanding supply–demand balance. #### Configuration -The controller is defined within the `tech_config` and requires these inputs. +The demand is defined within the `tech_config` and requires these inputs. | Field | Type | Description | | ----------------- | -------------- | ------------------------------------- | @@ -32,10 +30,10 @@ The controller is defined within the `tech_config` and requires these inputs. | `demand_profile` | scalar or list | Timeseries demand or constant demand. | ```yaml -control_strategy: +performance_model: model: GenericDemandComponent model_inputs: - control_parameters: + performance_parameters: commodity_name: hydrogen commodity_units: kg/h demand_profile: [10, 10, 12, 15, 14] @@ -43,21 +41,19 @@ model_inputs: For an example of how to use the `GenericDemandComponent` open-loop control framework, see the following: - `examples/23_solar_wind_ng_demand` -(flexible-demand-open-loop-converter-controller)= -### Flexible Demand Open-Loop Converter Controller -The `FlexibleDemandComponent` extends the fixed-demand controller by allowing the actual demand to flex up or down within defined bounds. This is useful for demand-side management scenarios where: +(flexible-demand-component)= +### Flexible Demand Component +The `FlexibleDemandComponent` extends the generic demand component by allowing the actual demand to flex up or down within defined bounds. This is useful for demand-side management scenarios where: - Processes can defer demand (e.g., flexible industrial loads) - The system requires demand elasticity without dynamic optimization -The controller computes: +The component computes: - Flexible demand (clamped within allowable ranges) - Unmet flexible demand - Unused commodity - Delivered output -Everything remains open-loop no storage, no intertemporal coupling. - -For an example of how to use the `FlexibleDemandComponent` open-loop control framework, see the following: +For an example of how to use the `FlexibleDemandComponent` demand component, see the following: - `examples/23_solar_wind_ng_demand` The flexible demand component takes an input commodity production profile, the maximum demand profile, and various constraints (listed below), and creates a "flexible demand profile" that follows the original input commodity production profile while satisfying varying constraint. From 1fb531acedbad5ffc137a39af3f3385184af7d22 Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Fri, 10 Apr 2026 11:59:37 -0600 Subject: [PATCH 16/17] updated changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 002bb2f9b..7c52d2ffd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,9 @@ - Added oxygen production metrics and as outputs to `ECOElectrolyzerPerformanceModel` [PR 642](https://github.com/NatLabRockies/H2Integrate/pull/642) - Bugfix to allow for one resource to be connected to multiple technologies [PR 655](https://github.com/NatLabRockies/H2Integrate/pull/655) - Removed the last of the logic that was based on technology names rather than model classes [PR 654](https://github.com/NatLabRockies/H2Integrate/pull/654) +- Reclassified open-loop converter control strategies as demand components and updated output naming convention to align with output naming convention in storage performance models [PR 631](https://github.com/NatLabRockies/H2Integrate/pull/631). + - The `FlexibleDemandOpenLoopConverterController` has been renamed to `FlexibleDemandComponent` + - The `DemandOpenLoopConverterController` has been renamed to `GenericDemandComponent` ## 0.7.1 [March 13, 2026] From 8397cb083774c80526ec5b269641eafbeebb62fd Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:50:45 -0600 Subject: [PATCH 17/17] removed commented out code and other doc updates --- docs/demand/demand_components.md | 12 ++++---- h2integrate/demand/demand_base.py | 2 +- h2integrate/demand/flexible_demand.py | 43 ++------------------------- h2integrate/demand/generic_demand.py | 28 ----------------- 4 files changed, 10 insertions(+), 75 deletions(-) diff --git a/docs/demand/demand_components.md b/docs/demand/demand_components.md index 213bb2c1f..32e0e79ee 100644 --- a/docs/demand/demand_components.md +++ b/docs/demand/demand_components.md @@ -5,9 +5,9 @@ Demand components define rule-based logic for meeting commodity demand profiles without using dynamic system feedback. These components operate independently at each timestep. -This page documents two core controller types: -1. Demand Open-Loop Converter Controller — meets a fixed demand profile. -2. Flexible Demand Open-Loop Converter Controller — adjusts demand up or down within flexible bounds. +This page documents two core demand types: +1. Generic Demand Component — meets a fixed demand profile. +2. Flexible Demand Component — adjusts demand up or down within flexible bounds. (generic-demand-component)= ### Generic Demand Component @@ -38,7 +38,7 @@ model_inputs: commodity_units: kg/h demand_profile: [10, 10, 12, 15, 14] ``` -For an example of how to use the `GenericDemandComponent` open-loop control framework, see the following: +For an example of how to use the `GenericDemandComponent` framework, see the following: - `examples/23_solar_wind_ng_demand` (flexible-demand-component)= @@ -65,7 +65,7 @@ The axes are unlabeled to allow for generalization to any commodity and unit typ #### Configuration -The flexible demand controller is defined within the `tech_config` with the following parameters: +The flexible demand component is defined within the `tech_config` with the following parameters: | Field | Type | Description | | ------------------- | -------------- | -------------------------------------------- | @@ -79,7 +79,7 @@ The flexible demand controller is defined within the `tech_config` with the foll ```yaml model_inputs: - control_parameters: + performance_parameters: commodity_name: hydrogen commodity_units: kg/h demand_profile: [10, 12, 10, 8] diff --git a/h2integrate/demand/demand_base.py b/h2integrate/demand/demand_base.py index a9477db8f..129dee08c 100644 --- a/h2integrate/demand/demand_base.py +++ b/h2integrate/demand/demand_base.py @@ -13,7 +13,7 @@ class DemandComponentBaseConfig(BaseConfig): demand profile that should be met by downstream components. Attributes: - commodity (str): Name of the commodity being controlled + commodity (str): Name of the commodity being demanded (e.g., "hydrogen"). Converted to lowercase and stripped of whitespace. commodity_rate_units (str): Units of the commodity (e.g., "kg/h"). demand_profile (int | float | list): Demand values for each timestep, in diff --git a/h2integrate/demand/flexible_demand.py b/h2integrate/demand/flexible_demand.py index 48f85b78a..43af90d1a 100644 --- a/h2integrate/demand/flexible_demand.py +++ b/h2integrate/demand/flexible_demand.py @@ -10,7 +10,7 @@ class FlexibleDemandComponentConfig(DemandComponentBaseConfig): """Configuration for defining a flexible demand component. - Extends :class:`DemandOpenLoopControlBaseConfig` with additional parameters + Extends :class:`DemandComponentBaseConfig` with additional parameters required for dynamically adjusting demand based on turndown, ramping, and minimum utilization constraints. These parameters are expressed as fractions of ``maximum_demand`` and must lie within ``(0, 1)``. @@ -50,7 +50,7 @@ class FlexibleDemandComponent(DemandComponentBase): """ def setup(self): - """Set up component inputs and outputs for flexible demand control. + """Set up component inputs and outputs for flexible demand. Adds inputs for turndown ratio, ramp up/down rates, and minimum utilization, all expressed as fractions of maximum demand. Adds the @@ -260,21 +260,12 @@ def compute(self, inputs, outputs): """ - # remaining_demand = inputs[f"{self.commodity}_demand"] - inputs[f"{self.commodity}_in"] - if self.config.min_utilization == 1.0: - # Calculate missed load and curtailed production - # outputs[f"unmet_{self.commodity}_demand_out"] = np.where( - # remaining_demand > 0, remaining_demand, 0 - # ) - # outputs[f"unused_{self.commodity}_out"] = np.where( - # remaining_demand < 0, -1 * remaining_demand, 0 - # ) - outputs[f"{self.commodity}_flexible_demand_profile"] = inputs[ f"{self.commodity}_demand" ] + # Calculate missed load and curtailed production outputs = self.calculate_outputs( inputs[f"{self.commodity}_in"], inputs[f"{self.commodity}_demand"], outputs ) @@ -298,31 +289,3 @@ def compute(self, inputs, outputs): outputs = self.calculate_outputs( inputs[f"{self.commodity}_in"], flexible_demand_profile, outputs ) - - # flexible_remaining_demand = flexible_demand_profile - inputs[f"{self.commodity}_in"] - - # outputs[f"unmet_{self.commodity}_demand_out"] = np.where( - # flexible_remaining_demand > 0, flexible_remaining_demand, 0 - # ) - # outputs[f"unused_{self.commodity}_out"] = np.where( - # flexible_remaining_demand < 0, -1 * flexible_remaining_demand, 0 - # ) - - # # Calculate actual output based on demand met and curtailment - # outputs[f"{self.commodity}_out"] = ( - # inputs[f"{self.commodity}_in"] - outputs[f"unused_{self.commodity}_out"] - # ) - # outputs[f"rated_{self.commodity}_production"] = inputs[ - # f"rated_{self.commodity}_demand" - # ].mean() - - # outputs[f"total_{self.commodity}_produced"] = np.sum(outputs[f"{self.commodity}_out"]) * ( - # self.dt / 3600 - # ) - # outputs[f"annual_{self.commodity}_produced"] = ( - # outputs[f"total_{self.commodity}_produced"] / self.fraction_of_year_simulated - # ) - - # outputs["capacity_factor"] = ( - # outputs[f"{self.commodity}_out"].sum() / inputs[f"{self.commodity}_demand"].sum() - # ) diff --git a/h2integrate/demand/generic_demand.py b/h2integrate/demand/generic_demand.py index 37ef69fbe..b65e713f1 100644 --- a/h2integrate/demand/generic_demand.py +++ b/h2integrate/demand/generic_demand.py @@ -54,31 +54,3 @@ def compute(self, inputs, outputs): outputs = self.calculate_outputs( inputs[f"{self.commodity}_in"], inputs[f"{self.commodity}_demand"], outputs ) - # remaining_demand = inputs[f"{self.commodity}_demand"] - inputs[f"{self.commodity}_in"] - - # # Calculate missed load and curtailed production - # outputs[f"unmet_{self.commodity}_demand_out"] = np.where( - # remaining_demand > 0, remaining_demand, 0 - # ) - # outputs[f"unused_{self.commodity}_out"] = np.where( - # remaining_demand < 0, -1 * remaining_demand, 0 - # ) - - # # Calculate actual output based on demand met and curtailment - # outputs[f"{self.commodity}_out"] = ( - # inputs[f"{self.commodity}_in"] - outputs[f"unused_{self.commodity}_out"] - # ) - - # outputs[f"rated_{self.commodity}_production"] = inputs[f"{self.commodity}_demand"].mean() - - # outputs[f"total_{self.commodity}_produced"] = np.sum(outputs[f"{self.commodity}_out"]) * ( - # self.dt / 3600 - # ) - - # outputs[f"annual_{self.commodity}_produced"] = ( - # outputs[f"total_{self.commodity}_produced"] / self.fraction_of_year_simulated - # ) - - # outputs["capacity_factor"] = ( - # outputs[f"{self.commodity}_out"].sum() / inputs[f"{self.commodity}_demand"].sum() - # )