Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions examples/33_peak_load_management/plant_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
5 changes: 4 additions & 1 deletion examples/33_peak_load_management/tech_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions h2integrate/core/supported_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions h2integrate/storage/battery/__init__.py
Original file line number Diff line number Diff line change
@@ -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
185 changes: 185 additions & 0 deletions h2integrate/storage/battery/battery_performance.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# TODO degradation: tests for battery performance
Loading