From 553742ee95b9624ee5eed74287f92af436144cbf Mon Sep 17 00:00:00 2001 From: Jared Thomas Date: Thu, 28 May 2026 12:55:19 -0600 Subject: [PATCH 1/7] create battery model files for including degradation --- h2integrate/core/supported_models.py | 1 + h2integrate/storage/battery/__init__.py | 1 + .../storage/battery/battery_performance.py | 164 ++++++++++++++++++ .../battery/test/test_battery_performance.py | 1 + 4 files changed, 167 insertions(+) create mode 100644 h2integrate/storage/battery/battery_performance.py create mode 100644 h2integrate/storage/battery/test/test_battery_performance.py diff --git a/h2integrate/core/supported_models.py b/h2integrate/core/supported_models.py index a22ae427e..429d60bae 100644 --- a/h2integrate/core/supported_models.py +++ b/h2integrate/core/supported_models.py @@ -141,6 +141,7 @@ def copy(self): "GenericSummerPerformanceModel": "transporters:GenericSummerPerformanceModel", # Storage "PySAMBatteryPerformanceModel": "storage.battery:PySAMBatteryPerformanceModel", + "BatteryPerformanceModel": "storage.battery:BatteryPerformanceModel", "StoragePerformanceModel": "storage:StoragePerformanceModel", "StorageAutoSizingModel": "storage:StorageAutoSizingModel", "LinedRockCavernStorageCostModel": "storage.hydrogen:LinedRockCavernStorageCostModel", diff --git a/h2integrate/storage/battery/__init__.py b/h2integrate/storage/battery/__init__.py index 1a32933c3..432e095a4 100644 --- a/h2integrate/storage/battery/__init__.py +++ b/h2integrate/storage/battery/__init__.py @@ -1,2 +1,3 @@ from h2integrate.storage.battery.pysam_battery import PySAMBatteryPerformanceModel from h2integrate.storage.battery.atb_battery_cost import ATBBatteryCostModel +from h2integrate.storage.battery.battery_performance import BatteryPerformanceModel diff --git a/h2integrate/storage/battery/battery_performance.py b/h2integrate/storage/battery/battery_performance.py new file mode 100644 index 000000000..d2b676a59 --- /dev/null +++ b/h2integrate/storage/battery/battery_performance.py @@ -0,0 +1,164 @@ +import numpy as np +from attrs import field, define + +from h2integrate.core.utilities import merge_shared_inputs +from h2integrate.core.validators import gt_zero, range_val, range_val_or_none +from h2integrate.storage.storage_baseclass import ( + StoragePerformanceBase, + StoragePerformanceBaseConfig, +) + + +@define(kw_only=True) +class BatteryPerformanceModelConfig(StoragePerformanceBaseConfig): + """Configuration class for storage performance models. + + This class defines configuration parameters for simulating storage + performance with the Pyomo controllers. It includes + specifications such as capacity, charge rate, state-of-charge limits, + and charge/discharge efficiencies. + + Attributes: + commodity (str): name of commodity + commodity_rate_units (str): Units of the commodity (e.g., "kg/h"). + 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. + max_capacity (float): Maximum storage energy capacity in commodity_amount_units. + Must be greater than zero. + max_charge_rate (float): Rated commodity capacity of the storage in commodity_rate_units. + Must be greater than zero. + min_soc_fraction (float): Minimum allowable state of charge as a fraction (0 to 1). + max_soc_fraction (float): Maximum allowable state of charge as a fraction (0 to 1). + init_soc_fraction (float): Initial state of charge as a fraction (0 to 1). + 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. + max_discharge_rate (float | None, optional): Maximum rate at which the commodity can be + discharged (in units per time step, e.g., "kg/time step"). This rate does not include + the discharge_efficiency. Only required if `charge_equals_discharge` is False. + charge_equals_discharge (bool, optional): If True, set the max_discharge_rate equal to the + max_charge_rate. If False, specify the max_discharge_rate as a value different than + the max_charge_rate. Defaults to True. + charge_efficiency (float | None, optional): Efficiency of charging the storage, represented + as a decimal between 0 and 1 (e.g., 0.9 for 90% efficiency). Optional if + `round_trip_efficiency` is provided. + discharge_efficiency (float | None, optional): Efficiency of discharging the storage, + represented as a decimal between 0 and 1 (e.g., 0.9 for 90% efficiency). Optional if + `round_trip_efficiency` is provided. + round_trip_efficiency (float | None, optional): Combined efficiency of charging and + discharging the storage, represented as a decimal between 0 and 1 (e.g., 0.81 for + 81% efficiency). Optional if `charge_efficiency` and `discharge_efficiency` are + provided. + + """ + + commodity: str = field() + commodity_rate_units: str = field() + + max_capacity: float = field(validator=gt_zero) + max_charge_rate: float = field(validator=gt_zero) + + init_soc_fraction: float = field(validator=range_val(0, 1)) + + commodity_amount_units: str = field(default=None) + max_discharge_rate: float | None = field(default=None) + charge_equals_discharge: bool = field(default=True) + + charge_efficiency: float | None = field(default=None, validator=range_val_or_none(0, 1)) + discharge_efficiency: float | None = field(default=None, validator=range_val_or_none(0, 1)) + round_trip_efficiency: float | None = field(default=None, validator=range_val_or_none(0, 1)) + + # TODO degredation: add additional parameters for degradation here + + def __attrs_post_init__(self): + """ + Post-initialization logic to validate and calculate efficiencies. + + Ensures that either `charge_efficiency` and `discharge_efficiency` are provided, + or `round_trip_efficiency` is provided. If `round_trip_efficiency` is provided, + it calculates `charge_efficiency` and `discharge_efficiency` as the square root + of `round_trip_efficiency`. + """ + if (self.round_trip_efficiency is not None) and ( + self.charge_efficiency is None and self.discharge_efficiency is None + ): + # Calculate charge and discharge efficiencies from round-trip efficiency + self.charge_efficiency = np.sqrt(self.round_trip_efficiency) + self.discharge_efficiency = np.sqrt(self.round_trip_efficiency) + + if self.charge_efficiency is None or self.discharge_efficiency is None: + raise ValueError( + "Exactly one of the following sets of parameters must be set: (a) " + "`round_trip_efficiency`, or (b) both `charge_efficiency` " + "and `discharge_efficiency`." + ) + + if self.charge_equals_discharge: + if ( + self.max_discharge_rate is not None + and self.max_discharge_rate != self.max_charge_rate + ): + msg = ( + "Max discharge rate does not equal max charge rate but charge_equals_discharge " + f"is True. Discharge rate is {self.max_discharge_rate} and charge rate " + f"is {self.max_charge_rate}." + ) + raise ValueError(msg) + + self.max_discharge_rate = self.max_charge_rate + + if not self.charge_equals_discharge and self.max_discharge_rate is None: + msg = ( + "max_discharge_rate is required when charge_equals_discharge is False. " + "Please input the discharge rate using the key `max_discharge_rate`." + ) + raise ValueError(msg) + + if self.commodity_amount_units is None: + self.commodity_amount_units = f"({self.commodity_rate_units})*h" + + +class BatteryPerformanceModel(StoragePerformanceBase): + """OpenMDAO component for a storage component.""" + + _time_step_bounds = ( + 1, + 3600, + ) # (min, max) time step lengths (in seconds) compatible with this model + + def setup(self): + self.config = BatteryPerformanceModelConfig.from_dict( + merge_shared_inputs(self.options["tech_config"]["model_inputs"], "performance"), + strict=False, + additional_cls_name=self.__class__.__name__, + ) + + self.commodity = self.config.commodity + self.commodity_rate_units = self.config.commodity_rate_units + self.commodity_amount_units = self.config.commodity_amount_units + + # TODO degredation: adjustments for degradation + + super().setup() + + def compute(self, inputs, outputs, discrete_inputs=[], discrete_outputs=[]): + """Run the storage performance model.""" + self.current_soc = self.config.init_soc_fraction + + charge_rate = inputs["max_charge_rate"][0] + if "max_discharge_rate" in inputs: + discharge_rate = inputs["max_discharge_rate"][0] + else: + discharge_rate = inputs["max_charge_rate"][0] + storage_capacity = inputs["storage_capacity"][0] + + # TODO degredation: adjust compute method for degradation as needed + outputs = self.run_storage( + charge_rate, discharge_rate, storage_capacity, inputs, outputs, discrete_inputs + ) + + # TODO degredation: add degradation method here + def degradation( + self, + ): + return diff --git a/h2integrate/storage/battery/test/test_battery_performance.py b/h2integrate/storage/battery/test/test_battery_performance.py new file mode 100644 index 000000000..83808572c --- /dev/null +++ b/h2integrate/storage/battery/test/test_battery_performance.py @@ -0,0 +1 @@ +# TODO degredation: tests for battery performance From bdcfcc29889c04e0ef6b3a03f53b8243953369c0 Mon Sep 17 00:00:00 2001 From: Jared Thomas Date: Thu, 28 May 2026 14:52:30 -0600 Subject: [PATCH 2/7] spelling correction --- h2integrate/storage/battery/battery_performance.py | 8 ++++---- .../storage/battery/test/test_battery_performance.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/h2integrate/storage/battery/battery_performance.py b/h2integrate/storage/battery/battery_performance.py index d2b676a59..40cd5a353 100644 --- a/h2integrate/storage/battery/battery_performance.py +++ b/h2integrate/storage/battery/battery_performance.py @@ -68,7 +68,7 @@ class BatteryPerformanceModelConfig(StoragePerformanceBaseConfig): discharge_efficiency: float | None = field(default=None, validator=range_val_or_none(0, 1)) round_trip_efficiency: float | None = field(default=None, validator=range_val_or_none(0, 1)) - # TODO degredation: add additional parameters for degradation here + # TODO degradation: add additional parameters for degradation here def __attrs_post_init__(self): """ @@ -137,7 +137,7 @@ def setup(self): self.commodity_rate_units = self.config.commodity_rate_units self.commodity_amount_units = self.config.commodity_amount_units - # TODO degredation: adjustments for degradation + # TODO degradation: adjustments for degradation super().setup() @@ -152,12 +152,12 @@ def compute(self, inputs, outputs, discrete_inputs=[], discrete_outputs=[]): discharge_rate = inputs["max_charge_rate"][0] storage_capacity = inputs["storage_capacity"][0] - # TODO degredation: adjust compute method for degradation as needed + # TODO degradation: adjust compute method for degradation as needed outputs = self.run_storage( charge_rate, discharge_rate, storage_capacity, inputs, outputs, discrete_inputs ) - # TODO degredation: add degradation method here + # TODO degradation: add degradation method here def degradation( self, ): diff --git a/h2integrate/storage/battery/test/test_battery_performance.py b/h2integrate/storage/battery/test/test_battery_performance.py index 83808572c..7f0641c82 100644 --- a/h2integrate/storage/battery/test/test_battery_performance.py +++ b/h2integrate/storage/battery/test/test_battery_performance.py @@ -1 +1 @@ -# TODO degredation: tests for battery performance +# TODO degradation: tests for battery performance From 3c48f03d50d1fb7739a4933189a28c94e0876a73 Mon Sep 17 00:00:00 2001 From: Jared Thomas Date: Thu, 28 May 2026 17:38:45 -0600 Subject: [PATCH 3/7] make solar resource data available to the battery performance model --- examples/24_solar_battery_grid/plant_config.yaml | 4 +++- examples/24_solar_battery_grid/tech_config.yaml | 2 +- h2integrate/storage/battery/battery_performance.py | 8 ++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/examples/24_solar_battery_grid/plant_config.yaml b/examples/24_solar_battery_grid/plant_config.yaml index 3fed5d46d..114bccce4 100644 --- a/examples/24_solar_battery_grid/plant_config.yaml +++ b/examples/24_solar_battery_grid/plant_config.yaml @@ -34,8 +34,10 @@ technology_interconnections: - [solar, combiner, electricity, cable] - [grid_buy, combiner, electricity, cable] resource_to_tech_connections: - # connect the wind resource to the wind technology + # connect the solar resource to the pv solar technology - [site.solar_resource, solar, solar_resource_data] + # connect the solar resource to the battery technology + - [site.solar_resource, battery, solar_resource_data] plant: plant_life: 30 simulation: diff --git a/examples/24_solar_battery_grid/tech_config.yaml b/examples/24_solar_battery_grid/tech_config.yaml index 969cfbf63..d16665938 100644 --- a/examples/24_solar_battery_grid/tech_config.yaml +++ b/examples/24_solar_battery_grid/tech_config.yaml @@ -31,7 +31,7 @@ technologies: cost_year: 2024 battery: performance_model: - model: StoragePerformanceModel + model: BatteryPerformanceModel cost_model: model: ATBBatteryCostModel control_strategy: diff --git a/h2integrate/storage/battery/battery_performance.py b/h2integrate/storage/battery/battery_performance.py index 40cd5a353..8344241b5 100644 --- a/h2integrate/storage/battery/battery_performance.py +++ b/h2integrate/storage/battery/battery_performance.py @@ -137,6 +137,12 @@ def setup(self): self.commodity_rate_units = self.config.commodity_rate_units self.commodity_amount_units = self.config.commodity_amount_units + self.add_discrete_input( + "solar_resource_data", + val={}, + desc="Solar resource data dictionary", + ) + # TODO degradation: adjustments for degradation super().setup() @@ -153,6 +159,7 @@ def compute(self, inputs, outputs, discrete_inputs=[], discrete_outputs=[]): storage_capacity = inputs["storage_capacity"][0] # TODO degradation: adjust compute method for degradation as needed + self.degradation(discrete_inputs["solar_resource_data"]) outputs = self.run_storage( charge_rate, discharge_rate, storage_capacity, inputs, outputs, discrete_inputs ) @@ -160,5 +167,6 @@ def compute(self, inputs, outputs, discrete_inputs=[], discrete_outputs=[]): # TODO degradation: add degradation method here def degradation( self, + solar_resource_data, ): return From f62b25f6df635c954b0aec52687190ea73fadece Mon Sep 17 00:00:00 2001 From: Jared Thomas Date: Thu, 28 May 2026 17:46:08 -0600 Subject: [PATCH 4/7] add doc string comment --- h2integrate/storage/battery/battery_performance.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/h2integrate/storage/battery/battery_performance.py b/h2integrate/storage/battery/battery_performance.py index 8344241b5..d5ffd168d 100644 --- a/h2integrate/storage/battery/battery_performance.py +++ b/h2integrate/storage/battery/battery_performance.py @@ -169,4 +169,10 @@ def degradation( self, solar_resource_data, ): + """_summary_ + + Args: + solar_resource_data (_type_): a dictionary of hourly (or by timestep) solar resource + information including irradiance and temperature + """ return From 37b813a44b326b0066826141ca2773e156a8efe7 Mon Sep 17 00:00:00 2001 From: Jared Thomas Date: Fri, 29 May 2026 09:15:38 -0600 Subject: [PATCH 5/7] use plm example 33 for degradation instead of example 24 --- examples/24_solar_battery_grid/plant_config.yaml | 4 +--- examples/24_solar_battery_grid/tech_config.yaml | 2 +- examples/33_peak_load_management/plant_config.yaml | 10 ++++++++++ examples/33_peak_load_management/tech_config.yaml | 2 +- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/examples/24_solar_battery_grid/plant_config.yaml b/examples/24_solar_battery_grid/plant_config.yaml index 114bccce4..3fed5d46d 100644 --- a/examples/24_solar_battery_grid/plant_config.yaml +++ b/examples/24_solar_battery_grid/plant_config.yaml @@ -34,10 +34,8 @@ technology_interconnections: - [solar, combiner, electricity, cable] - [grid_buy, combiner, electricity, cable] resource_to_tech_connections: - # connect the solar resource to the pv solar technology + # connect the wind resource to the wind technology - [site.solar_resource, solar, solar_resource_data] - # connect the solar resource to the battery technology - - [site.solar_resource, battery, solar_resource_data] plant: plant_life: 30 simulation: diff --git a/examples/24_solar_battery_grid/tech_config.yaml b/examples/24_solar_battery_grid/tech_config.yaml index d16665938..969cfbf63 100644 --- a/examples/24_solar_battery_grid/tech_config.yaml +++ b/examples/24_solar_battery_grid/tech_config.yaml @@ -31,7 +31,7 @@ technologies: cost_year: 2024 battery: performance_model: - model: BatteryPerformanceModel + model: StoragePerformanceModel cost_model: model: ATBBatteryCostModel control_strategy: diff --git a/examples/33_peak_load_management/plant_config.yaml b/examples/33_peak_load_management/plant_config.yaml index 93be1cecf..26ef6faad 100644 --- a/examples/33_peak_load_management/plant_config.yaml +++ b/examples/33_peak_load_management/plant_config.yaml @@ -2,14 +2,24 @@ name: plant_config description: Demonstrates multivariable streams with a gas combiner plant: plant_life: 30 + latitude: 30.6617 + longitude: -101.7096 simulation: n_timesteps: 8760 dt: 3600 timezone: -6 start_time: 2025/07/01 00:00:00 + resources: + solar_resource: + resource_model: GOESAggregatedSolarAPI + resource_parameters: + resource_year: 2025 technology_interconnections: - [grid_buy, battery, electricity, cable] # include battery charge/discharge in the load - [battery, electrical_load_demand, [electricity_out, electricity_in]] # buy power from the grid to fulfill demand including to accommodate battery operation - [electrical_load_demand, grid_buy, [unmet_electricity_demand_out, electricity_set_point]] +resource_to_tech_connections: + # connect the solar resource to the battery technology + - [site.solar_resource, battery, solar_resource_data] diff --git a/examples/33_peak_load_management/tech_config.yaml b/examples/33_peak_load_management/tech_config.yaml index e1a9ff88f..9f247d33b 100644 --- a/examples/33_peak_load_management/tech_config.yaml +++ b/examples/33_peak_load_management/tech_config.yaml @@ -3,7 +3,7 @@ description: This plant charges a battery from the grid to reduce peak demand technologies: battery: performance_model: - model: StoragePerformanceModel + model: BatteryPerformanceModel cost_model: model: ATBBatteryCostModel control_strategy: From 185c6d67dfb061bd0d03a43d78a089d6d72852c7 Mon Sep 17 00:00:00 2001 From: Jared Thomas Date: Thu, 4 Jun 2026 10:56:00 -0600 Subject: [PATCH 6/7] add performance param example --- examples/33_peak_load_management/tech_config.yaml | 3 +++ h2integrate/storage/battery/battery_performance.py | 1 + 2 files changed, 4 insertions(+) diff --git a/examples/33_peak_load_management/tech_config.yaml b/examples/33_peak_load_management/tech_config.yaml index 9f247d33b..75c0d6d04 100644 --- a/examples/33_peak_load_management/tech_config.yaml +++ b/examples/33_peak_load_management/tech_config.yaml @@ -22,6 +22,9 @@ technologies: charge_efficiency: 1.0 # percent as decimal discharge_efficiency: 1.0 # percent as decimal demand_profile: !include demand_profiles/demand_profile.yaml + performance_parameters: + cop: 0.3 + # TODO: add remaining performance parameters here control_parameters: demand_profile_upstream: !include demand_profiles/demand_profile_upstream.yaml # demand used to define when the supervisor commands battery dispatch. This may represent an upstream load. dispatch_priority_demand_profile: demand_profile_upstream # demand profile the controller prioritizes when deciding dispatch diff --git a/h2integrate/storage/battery/battery_performance.py b/h2integrate/storage/battery/battery_performance.py index d5ffd168d..971fb380e 100644 --- a/h2integrate/storage/battery/battery_performance.py +++ b/h2integrate/storage/battery/battery_performance.py @@ -69,6 +69,7 @@ class BatteryPerformanceModelConfig(StoragePerformanceBaseConfig): round_trip_efficiency: float | None = field(default=None, validator=range_val_or_none(0, 1)) # TODO degradation: add additional parameters for degradation here + cop: float = field(validator=gt_zero) def __attrs_post_init__(self): """ From fd86452cd4a018684502f5836a48e19fd0957bfd Mon Sep 17 00:00:00 2001 From: Jared Thomas Date: Fri, 12 Jun 2026 11:02:56 -0600 Subject: [PATCH 7/7] add battery auxiliary power output --- h2integrate/storage/battery/battery_performance.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/h2integrate/storage/battery/battery_performance.py b/h2integrate/storage/battery/battery_performance.py index 971fb380e..ed526b134 100644 --- a/h2integrate/storage/battery/battery_performance.py +++ b/h2integrate/storage/battery/battery_performance.py @@ -144,6 +144,12 @@ def setup(self): desc="Solar resource data dictionary", ) + self.add_output( + f"{self.commodity}_auxiliary_demand", + shape=self.n_timesteps, + desc="Electricity demand for running battery auxiliary systems", + ) + # TODO degradation: adjustments for degradation super().setup()