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..75c0d6d04 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: @@ -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/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..ed526b134 --- /dev/null +++ b/h2integrate/storage/battery/battery_performance.py @@ -0,0 +1,185 @@ +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 degradation: add additional parameters for degradation here + cop: float = field(validator=gt_zero) + + 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 + + self.add_discrete_input( + "solar_resource_data", + val={}, + 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() + + 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 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 + ) + + # TODO degradation: add degradation method here + 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 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..7f0641c82 --- /dev/null +++ b/h2integrate/storage/battery/test/test_battery_performance.py @@ -0,0 +1 @@ +# TODO degradation: tests for battery performance