Source code for assetra.units

from __future__ import annotations
from abc import abstractmethod, ABC
from logging import getLogger
from dataclasses import dataclass

# external
import numpy as np
import xarray as xr

LOG = getLogger(__name__)

# ENERGY UNIT(S)
MAX_CHUNK_SIZE = int(1e8)


[docs] @dataclass(frozen=True) class EnergyUnit(ABC): """Abstract base class for all energy units. Energy units are the fundamental building blocks of energy systems in the assetra model. This base class defines an interface which allows the assetra model to save/load pre-existing energy systems from files and run probabilistic simulations with unique energy unit types. Args: id (int): Unique identifying number, used to ensure energy units are not added more than once to an energy system nameplate_capacity (float) : Nameplate capacity of the energy unit in units of power (to be kept consistend between units). For some units defining the nameplate capacity may not make physical sense, e.g. demand units, in which case the nameplate capacity should be set to zero. """ id: int nameplate_capacity: float
[docs] @staticmethod @abstractmethod def to_unit_dataset(units: list[EnergyUnit]) -> xr.Dataset: """Convert a list of energy units of the derived class type into an xarray dataset. For different energy units, different dataset dimensions and coordinates may be appropriate. Args: units (list[EnergyUnit]): List of of energy units of the derived class type Returns: xr.Dataset: Dataset storing sufficient information to (1) fully reconstruct the list of energy units from which it is created and (2) generate hourly capacity time series with the EnergyUnit.get_probabilistic_capacity_matrix function """ pass
[docs] @staticmethod @abstractmethod def from_unit_dataset(unit_dataset: xr.Dataset) -> list[EnergyUnit]: """Convert a unit dataset to a list of energy units of the derived energy unit type. This is the inverse to the derived EnergyUnit.to_unit_dataset function Args: unit_dataset (xr.Dataset): Unit dataset with structure and content defined in the derived EnergyUnit.to_unit_dataset function Returns: list[EnergyUnit]: List of energy units of the derived class type """
[docs] @staticmethod @abstractmethod def get_probabilistic_capacity_matrix( unit_dataset: xr.Dataset, net_hourly_capacity_matrix: xr.DataArray ) -> xr.DataArray: """Return probabilistic hourly capacity matrix for a fleet of energy units of the derived energy unit type. Take the unit dataset and create a matrix representing the total hourly capacity of all energy units for some number of monte carlo trials. The hours and number of trials should match the net hourly capacity matrix. Args: unit_dataset (xr.Dataset): Unit dataset for the derived energy unit type, e.g. generated with the derived EnergyUnit.to_unit_dataset function net_hourly_capacity_matrix (xr.DataArray): Probabilistic net hourly capacity matrix with dimensions (trials, time) and shape (# of trials, # of hours) Returns: xr.DataArray: Combined hourly capacity for all units in the unit dataset for a determined number of Monte Carlo trials. The dimensions and coordinates of this matrix should match the net hourly capacity matrix """ pass
[docs] @dataclass(frozen=True) class StaticUnit(EnergyUnit): """Derived energy unit class. A static energy unit is neither stochastic nor responsive. A single hourly capacity profile is used in all probabilistic capacity trials. For example, a historical demand profile be fully accounted for in all trials of a probabilistic simulation. Args: id (int): Unique identifying number nameplate_capacity (float) : Nameplate capacity of the energy unit in units of power hourly_capacity (xr.DataArray) : Hourly capacity contained in DataArray with dimension (time) and datetime coordinates. """ hourly_capacity: xr.DataArray
[docs] @staticmethod def to_unit_dataset(units: list[StaticUnit]) -> xr.Dataset: """Convert a list of static energy units into an xarray dataset Args: units (list[StaticUnit]): List of of static energy units Returns: xr.Dataset: Dataset with dimensions (energy_unit, time) and variables (nameplate_capacity[energy_unit], hourly_capacity[energy_unit, time]). Coordinates for the energy_unit and time dimensions are energy unit IDs and hourly datetime indices, respectively. """ # build dataset unit_dataset = xr.Dataset( data_vars=dict( nameplate_capacity=( ["energy_unit"], [unit.nameplate_capacity for unit in units], ), hourly_capacity=( ["energy_unit", "time"], [unit.hourly_capacity for unit in units], ), ), coords=dict( energy_unit=[unit.id for unit in units], time=units[0].hourly_capacity.time if len(units) > 0 else [], ), ) return unit_dataset
[docs] @staticmethod def from_unit_dataset(unit_dataset: xr.Dataset) -> list[StaticUnit]: """Convert a static unit dataset to a list of static energy units. This is the inverse StaticUnit.to_unit_dataset function Args: unit_dataset (xr.Dataset): Unit dataset with structure and content defined in the derived StaticUnit.to_unit_dataset function Returns: list[StaticUnit]: List of static energy units """ # build list units = [] for id in unit_dataset.energy_unit: units.append( StaticUnit( int(id), int(unit_dataset.nameplate_capacity.loc[id]), unit_dataset.hourly_capacity.loc[id], ) ) return units
[docs] @staticmethod def get_probabilistic_capacity_matrix( unit_dataset: xr.Dataset, net_hourly_capacity_matrix: xr.DataArray ) -> xr.DataArray: """Return probabilistic hourly capacity matrix for a static unit dataset. For static units, combine hourly capacity profiles for all energy units in the unit dataset and broadcast the result across all trials Args: unit_dataset (xr.Dataset): Static unit dataset, as generated by StaticUnit.to_unit_dataset function net_hourly_capacity_matrix (xr.DataArray): Probabilistic net hourly capacity matrix with dimensions (trials, time) and shape (# of trials, # of hours) Returns: xr.DataArray: Combined hourly capacity for all units in the unit dataset with the same dimensions and shape as net hourly capacity matrix """ # time-indexing unit_dataset = unit_dataset.sel(time=net_hourly_capacity_matrix.time) # sum across capacity units probabilistic_capacity_matrix = unit_dataset["hourly_capacity"].sum( dim="energy_unit" ) # to xarray probabilistic_capacity_matrix = ( xr.zeros_like(net_hourly_capacity_matrix) + probabilistic_capacity_matrix ) return probabilistic_capacity_matrix
[docs] @dataclass(frozen=True) class StochasticUnit(EnergyUnit): """Derived energy unit class. A stochastic energy unit uses time-varying forced outage rates to sample indepenedent outages throughout the simulation period. Stochastic units are non-responsive, meaning that while hourly capacity profiles vary between trials in a probabilistic simulation, the profiles do not depend on system conditions and only need to be sampled once Args: id (int): Unique identifying number nameplate_capacity (float) : Nameplate capacity of the energy unit in units of power hourly_capacity (xr.DataArray) : Hourly capacity contained in DataArray with dimension (time) and datetime coordinates hourly_forced_outage_rate (xr.DataArray) : Hourly forced outage rate as decimal percents (e.g. 5% -> 0.05) contained in DataArray with dimension (time) and datetime coordinates. Should be a parallel matrix to hourly_capacity """ hourly_capacity: xr.DataArray hourly_forced_outage_rate: xr.DataArray
[docs] @staticmethod def to_unit_dataset(units: list[StochasticUnit]): """Convert a list of stochastic energy units into an xarray dataset Args: units (list[StochasticUnit]): List of of stochastic energy units Returns: xr.Dataset: Dataset with dimensions (energy_unit, time) and variables (nameplate_capacity[energy_unit], hourly_capacity[energy_unit, time]), hourly_forced_outage_rate[energy_unit, time]. Coordinates for the energy_unit and time dimensions are energy unit IDs and hourly datetime indices, respectively. """ unit_dataset = xr.Dataset( data_vars=dict( nameplate_capacity=( ["energy_unit"], [unit.nameplate_capacity for unit in units], ), hourly_capacity=( ["energy_unit", "time"], [unit.hourly_capacity for unit in units], ), hourly_forced_outage_rate=( ["energy_unit", "time"], [unit.hourly_forced_outage_rate for unit in units], ), ), coords=dict( energy_unit=[unit.id for unit in units], time=units[0].hourly_capacity.time if len(units) > 0 else [], ), ) return unit_dataset
[docs] @staticmethod def from_unit_dataset(unit_dataset: xr.Dataset) -> list[StochasticUnit]: """Convert a stochastic unit dataset to a list of stochastic energy units. This is the inverse to StochasticUnit.to_unit_dataset function Args: unit_dataset (xr.Dataset): Unit dataset with structure and content defined in the derived StochasticUnit.to_unit_dataset function Returns: list[StochasticUnit]: List of stochastic units """ # build list units = [] for id in unit_dataset.energy_unit: units.append( StochasticUnit( id, unit_dataset.nameplate_capacity.loc[id], unit_dataset.hourly_capacity.loc[id], unit_dataset.hourly_forced_outage_rate.loc[id], ) ) return units
[docs] @staticmethod def get_probabilistic_capacity_matrix( unit_dataset: xr.Dataset, net_hourly_capacity_matrix: xr.DataArray ) -> xr.DataArray: """Return probabilistic hourly capacity matrix for a stochastic unit dataset. For stochastic units, sample hourly independent outages in for units in all trials. Outages are sampled hourly for every unit and trial. Random numbers are drawn from the range 0 to 1, and where samples are less than the hourly forced outage rate, the effective capacity of that energy unit in that hour and trial is set to 0. The probabilistic capacity matrix is the aggregation of sampled capacities across energy units Args: unit_dataset (xr.Dataset): Static unit dataset, as generated by StaticUnit.to_unit_dataset function net_hourly_capacity_matrix (xr.DataArray): Probabilistic net hourly capacity matrix with dimensions (trials, time) and shape (# of trials, # of hours) Returns: xr.DataArray: Combined hourly capacity for all units in the unit dataset with the same dimensions and shape as net hourly capacity matrix """ # time-indexing unit_dataset = unit_dataset.sel(time=net_hourly_capacity_matrix.time) chunk_size = 1 + MAX_CHUNK_SIZE // ( net_hourly_capacity_matrix.sizes["trial"] * net_hourly_capacity_matrix.sizes["time"] ) LOG.info("Using chunk size " + str(chunk_size)) # Initialize the probabilistic capacity matrix probabilistic_capacity_matrix = xr.zeros_like( net_hourly_capacity_matrix ) # Loop over the energy unit dimension in chunks for unit_idx in range(0, unit_dataset.sizes["energy_unit"], chunk_size): unit_idx_end = min( unit_idx + chunk_size, unit_dataset.sizes["energy_unit"] ) LOG.info( "Sampling outages for units " + str(unit_idx + 1) + "-" + str(unit_idx_end) + " of " + str(unit_dataset.sizes["energy_unit"]) ) chunk = unit_dataset.isel(energy_unit=slice(unit_idx, unit_idx_end)) chunk_prob_matrix = np.where( np.random.random_sample( ( net_hourly_capacity_matrix.sizes["trial"], chunk.sizes["energy_unit"], net_hourly_capacity_matrix.sizes["time"], ) ) > chunk["hourly_forced_outage_rate"].values, chunk["hourly_capacity"].values, 0, ).sum(axis=1) # Update the main probabilistic capacity matrix with the results from the chunk probabilistic_capacity_matrix += chunk_prob_matrix return probabilistic_capacity_matrix
[docs] @dataclass(frozen=True) class StorageUnit(EnergyUnit): """Derived energy unit class. A storage unit is a state-dependent, responsive energy unit. The available capacity of a storage unit depends on its state of charge and on the needs of the system. As opposed to static and stochastic units, which require hourly time series, storage unit operation is characterized by a handful of scalar parameters Args: id (int): Unique identifying number nameplate_capacity (float) : Nameplate capacity in units of power. For storage, typically the discharge rate charge_rate (float) : Charge rate in units of power discharge_rate (float) : Discharge rate in units of power charge_capacity (float) : Maximum charge capacity in units of energy roundtrip_efficiency (float) : Roundtrip efficiency as decimal percent """ charge_rate: float discharge_rate: float charge_capacity: float roundtrip_efficiency: float def _get_hourly_capacity( charge_rate: float, discharge_rate: float, charge_capacity: float, roundtrip_efficiency: float, net_hourly_capacity: xr.DataArray, ) -> xr.DataArray: """Greedy storage dispatch Args: charge_rate (float) : Charge rate in units of power discharge_rate (float) : Discharge rate in units of power charge_capacity (float) : Maximum charge capacity in units of energy roundtrip_efficiency (float) : Roundtrip efficiency as decimal percent net_hourly_capacity (xr.DataArray): Net capacity contained in DataArray with dimension (time) and hourly datetime coordinates Returns: xr.DataArray: Hourly capacity contained in DataArray with same shape as net hourly capacity """ # TODO skip irrelevant days for average-case speed-up? # initialize full storage unit efficiency = roundtrip_efficiency**0.5 def charge_storage(excess_capacity: float, current_charge: float): capacity = -min( charge_rate, (charge_capacity - current_charge) / efficiency, excess_capacity, ) current_charge -= capacity * efficiency return capacity, current_charge def discharge_storage(unmet_demand: float, current_charge: float): capacity = min( discharge_rate / efficiency, current_charge, unmet_demand / efficiency, ) current_charge -= capacity return capacity * efficiency, current_charge def dispatch_storage(net_hourly_capacity: float): # initialize storage unit as full current_charge = float(charge_capacity) for net_capacity in net_hourly_capacity.values: capacity = 0 if net_capacity < 0: if current_charge > 0: # unmet demand and avaiable charge capacity, current_charge = discharge_storage( -net_capacity, current_charge ) elif current_charge < charge_capacity: # excess capacity and not fully charged capacity, current_charge = charge_storage( net_capacity, current_charge ) yield capacity # simulate dispatch hourly_capacity = net_hourly_capacity.copy( data=[ capacity for capacity in dispatch_storage(net_hourly_capacity) ] ) return hourly_capacity
[docs] @staticmethod def to_unit_dataset(units: list[StorageUnit]) -> xr.Dataset: """Convert a list of storage units into an xarray dataset Args: units (list[StorageUnit]): List of of storage energy units Returns: xr.Dataset: Dataset with dimensions (energy_unit) and variables (nameplate_capacity[energy_unit], charge_rate[energy_unit], discharge_rate[energy_unit], charge_capacity[energy_unit], roundtrip_efficiency[energy_unit] hourly_forced_outage_rate[energy_unit, time]). Coordinates for the energy_unit dimension are energy unit IDs """ # build dataset unit_dataset = xr.Dataset( data_vars=dict( nameplate_capacity=( ["energy_unit"], [unit.nameplate_capacity for unit in units], ), charge_rate=( ["energy_unit"], [unit.charge_rate for unit in units], ), discharge_rate=( ["energy_unit"], [unit.discharge_rate for unit in units], ), charge_capacity=( ["energy_unit"], [unit.charge_capacity for unit in units], ), roundtrip_efficiency=( ["energy_unit"], [unit.roundtrip_efficiency for unit in units], ), ), coords=dict(energy_unit=[unit.id for unit in units]), ) return unit_dataset
[docs] @staticmethod def from_unit_dataset(unit_dataset: xr.Dataset) -> list[StorageUnit]: """Convert a storage unit dataset to a list of storage units. This is the inverse StorageUnit.to_unit_dataset function Args: unit_dataset (xr.Dataset): Unit dataset with structure and content defined in the derived StorageUnit.to_unit_dataset function Returns: list[StorageUnit]: List of storage units """ # build list units = [] for id, nc, cr, dr, cc, re in zip( unit_dataset.energy_unit, unit_dataset.nameplate_capacity, unit_dataset.charge_rate, unit_dataset.discharge_rate, unit_dataset.charge_capacity, unit_dataset.roundtrip_efficiency, ): units.append( StorageUnit( id=int(id), nameplate_capacity=float(nc), charge_rate=float(cr), discharge_rate=float(dr), charge_capacity=float(cc), roundtrip_efficiency=float(re), ) ) return units
[docs] @staticmethod def get_probabilistic_capacity_matrix( unit_dataset: xr.Dataset, net_hourly_capacity_matrix: xr.DataArray ) -> xr.DataArray: """Return probabilistic hourly capacity matrix for a storage unit dataset. For storage units, it is necessary to dispatch units every hour and iteration sequentially. The dispatch policy implemented in StorageUnit._get_hourly_capacity is a greedy policy to minimize expected unserved energy. Units are dispatched according to the order they appear in the unit dataset Args: unit_dataset (xr.Dataset): Storage unit dataset, as generated by StorageUnit.to_unit_dataset function net_hourly_capacity_matrix (xr.DataArray): Probabilistic net hourly capacity matrix with dimensions (trials, time) and shape (# of trials, # of hours) Returns: xr.DataArray: Combined hourly capacity for all units in the unit dataset with the same dimensions and shape as net hourly capacity matrix """ units = StorageUnit.from_unit_dataset(unit_dataset) net_adj_hourly_capacity_matrix = net_hourly_capacity_matrix.copy() for idx, unit in enumerate(units): # print update LOG.info( "Dispatching storage unit " + str(idx) + " of " + str(len(units)) + " in all hours" ) for trial in net_adj_hourly_capacity_matrix: trial += StorageUnit._get_hourly_capacity( unit.charge_rate, unit.discharge_rate, unit.charge_capacity, unit.roundtrip_efficiency, trial, ) return net_adj_hourly_capacity_matrix - net_hourly_capacity_matrix
[docs] class DemandUnit(StaticUnit): """Derived energy unit class, providing a more meaningful interface for demand. Internally, demand is treated as a static unit with negative capacity and a nameplate rating of zero. This interface converts positive hourly demand into negative hourly capacity Args: id (int): Unique identifying number hourly_demand (xr.DataArray) : Hourly demand contained in DataArray with dimension (time) and datetime coordinates. """ def __init__(self, id: int, hourly_demand: xr.DataArray): StaticUnit.__init__( self, id, nameplate_capacity=0, hourly_capacity=-hourly_demand )
[docs] class HydroUnit(EnergyUnit): """Derived energy unit class. A hydro unit represents a hydropower generator that allocates its monthly expected generation by dispatching proportionally to unmet demand in each hour, subject to its nameplate capacity and (optionally) forced outages. Args: id (int): Unique identifying number. nameplate_capacity (float): Nameplate capacity of the hydro unit in units of power. monthly_expected_generation (xr.DataArray): Expected monthly generation, in units of energy, as a DataArray with dimension (month). hourly_forced_outage_rate (xr.DataArray, optional): Hourly forced outage rate as decimal percents, as a DataArray with dimension (time) and datetime coordinates. Defaults to None. """ def __init__(self, id, nameplate_capacity, monthly_expected_generation, hourly_forced_outage_rate=None): super().__init__(id, nameplate_capacity) self.monthly_expected_generation = monthly_expected_generation self.hourly_forced_outage_rate = hourly_forced_outage_rate def _get_hourly_capacity(self, net_hourly_capacity: xr.DataArray) -> xr.DataArray: """Calculate the hourly dispatchable capacity for the hydro unit for each hour. Capacity is distributed proportionally to unmet demand among hours in the same month, limited by nameplate capacity and the available monthly generation. Args: net_hourly_capacity (xr.DataArray): Net system capacity for each hour, with dimension (time). Returns: xr.DataArray: Hourly hydro capacity (same shape as net_hourly_capacity). """ #use numpy to use a vectorized calculation net_capacity_values = net_hourly_capacity.values hourly_capacity_values = np.zeros_like(net_capacity_values) #get months as numpy array months = net_hourly_capacity.time.dt.month.values unique_months = np.unique(months) for month in unique_months: #boolean mask for this month month_mask = months == month month_data = net_capacity_values[month_mask] #get current charge current_charge = float(self.monthly_expected_generation.sel(month=month, method = 'pad')) #calculate total monthly net capacity total_monthly_net_capacity = month_data[month_data <0].sum() if total_monthly_net_capacity == 0: continue #vectorized calc for distribution of hydro across month proportions = np.where(month_data < 0, (month_data/total_monthly_net_capacity), 0) dispatch_amounts = proportions*current_charge dispatch_amounts = np.minimum(dispatch_amounts, self.nameplate_capacity) hourly_capacity_values[month_mask] = dispatch_amounts #convert from numpy back to xarray on return hourly_capacity = xr.DataArray(hourly_capacity_values, coords = net_hourly_capacity.coords, dims=net_hourly_capacity.dims) return hourly_capacity
[docs] @staticmethod def to_unit_dataset(units: list["HydroUnit"]) -> xr.Dataset: """Convert a list of hydro units into an xarray dataset. Args: units (list[HydroUnit]): List of hydro units. Returns: xr.Dataset: Dataset storing sufficient information to reconstruct the list of hydro units, including nameplate capacity, monthly expected generation, and, if present, hourly forced outage rates. """ months = np.arange(1, 13) #only works for one year at a time hourly_time = units[0].hourly_forced_outage_rate.time if units[0].hourly_forced_outage_rate is not None else None dataset = xr.Dataset({ "nameplate_capacity": (["energy_unit"], [unit.nameplate_capacity for unit in units]), "monthly_expected_generation": (["energy_unit", "month"], [unit.monthly_expected_generation.values for unit in units]) }) if hourly_time is not None: dataset = dataset.assign_coords({"hourly_time": hourly_time}) if any(unit.hourly_forced_outage_rate is not None for unit in units): hourly_rates = [ unit.hourly_forced_outage_rate.values if unit.hourly_forced_outage_rate is not None else np.zeros(hourly_time.shape) for unit in units ] dataset["hourly_forced_outage_rate"] = (["energy_unit", "hourly_time"], hourly_rates) dataset = dataset.assign_coords({"month": months}) #print(dataset) return dataset
[docs] @staticmethod def from_unit_dataset(unit_dataset: xr.Dataset) -> list["HydroUnit"]: """Convert a hydro unit dataset to a list of hydro units. This is the inverse to HydroUnit.to_unit_dataset function. Args: unit_dataset (xr.Dataset): Unit dataset with structure defined in HydroUnit.to_unit_dataset. Returns: list[HydroUnit]: List of hydro units. """ units = [] for idx in range(len(unit_dataset.energy_unit)): hourly_forced_outage_rate = None if "hourly_forced_outage_rate" in unit_dataset: hourly_forced_outage_rate = xr.DataArray( unit_dataset["hourly_forced_outage_rate"].isel(energy_unit=idx).values, dims=["time"], coords={"time": unit_dataset.hourly_time.values} ) monthly_coords = np.arange(1, 13) monthly_expected_generation = xr.DataArray( unit_dataset["monthly_expected_generation"].isel(energy_unit=idx).values, dims=["month"], coords={"month": monthly_coords} ) units.append( HydroUnit( id=unit_dataset.energy_unit[idx].item(), nameplate_capacity=unit_dataset.nameplate_capacity[idx].item(), monthly_expected_generation=monthly_expected_generation, #hourly_forced_outage_rate=hourly_forced_outage_rate ) ) return units
[docs] @staticmethod def get_probabilistic_capacity_matrix(unit_dataset: xr.Dataset, net_hourly_capacity_matrix: xr.DataArray) -> xr.DataArray: """Return probabilistic hourly capacity matrix for a hydro unit dataset. For each hydro unit, simulate hourly capacity allocation in each trial, optionally accounting for forced outages if an hourly outage rate is present. Args: unit_dataset (xr.Dataset): Hydro unit dataset, as generated by HydroUnit.to_unit_dataset. net_hourly_capacity_matrix (xr.DataArray): Probabilistic net hourly capacity matrix with dimensions (trial, time) and shape (#trials, #hours). Returns: xr.DataArray: Combined hourly capacity for all units in the unit dataset, with the same dimensions and shape as net_hourly_capacity_matrix. """ units = HydroUnit.from_unit_dataset(unit_dataset) net_adj_hourly_capacity_matrix = net_hourly_capacity_matrix.copy() for idx, unit in enumerate(units): LOG.info("Dispatching hydro unit " + str(idx) + " of " + str(len(units)) + " in all hours") for trial in range(net_adj_hourly_capacity_matrix.sizes['trial']): net_capacity_trial = net_adj_hourly_capacity_matrix.isel(trial=trial) #print(f"Trial: {trial}, Net Hourly Capacity: {net_capacity_trial}") if unit.hourly_forced_outage_rate is not None: outage_mask = np.random.random_sample(net_capacity_trial.shape) > unit.hourly_forced_outage_rate[:-1] else: outage_mask = np.ones_like(net_capacity_trial) available_capacity = unit._get_hourly_capacity(net_capacity_trial) #print(f"Trial: {trial}, Available Capacity before outage: {available_capacity}") adjusted_capacity = available_capacity * outage_mask net_adj_hourly_capacity_matrix.loc[dict(trial=trial)] += adjusted_capacity #print(f"Trial: {trial}, Adjusted Capacity after outage: {adjusted_capacity}") #print(net_adj_hourly_capacity_matrix - net_hourly_capacity_matrix) return net_adj_hourly_capacity_matrix - net_hourly_capacity_matrix
# for successive simulations (e.g. ELCC), need to differentiate between # responsive and non-responsive units. # # these lists also serve to track all "valid" units that can be added # to an energy system NONRESPONSIVE_UNIT_TYPES = [DemandUnit, StaticUnit, HydroUnit, StochasticUnit] RESPONSIVE_UNIT_TYPES = [StorageUnit]