Source code for osier.technology

import unyt
from unyt import MW, hr, kg, km, m, megatonnes, MWh
from unyt import unyt_quantity, unyt_array
from unyt.exceptions import UnitParseError
from collections import OrderedDict

import numpy as np
import pandas as pd


_dim_opts = {'time': hr,
             'power': MW,
             'energy': MWh,
             'mass': kg,
             'length': km,
             'area': km**2,
             'volume': m**3,
             'specific_time': hr**-1,
             'specific_mass': kg**-1,
             'specific_power': MW**-1,
             'specific_energy': (MWh)**-1,
             'mass_per_energy': megatonnes * (MWh)**-1,
             'area_per_power': km**2 * MW**-1}

_constant_types = (int, float, unyt_quantity)
_array_types = (unyt.unyt_array, pd.core.series.Series, np.ndarray, list)


def _validate_unit(value, dimension):
    """
    This function checks that a unit has the correct
    dimensions. Used in :class:`Technology` to set
    units.

    Parameters
    ----------
    value : string, float, int, or :class:`unyt.unit_object.Unit`
        The value being tested. Should be a unit symbol.
    dimension : string
        The expected dimensions of `value`.
        Accepted values listed in `_dim_opts`.

    Returns
    -------
    valid_unit : :class:`unyt.unit_object.Unit`
        The validated unit.
    """
    try:
        exp_dim = _dim_opts[dimension]
    except KeyError:
        raise KeyError(f"Key <{dimension}> not accepted. Try: {_dim_opts}")

    valid_unit = None
    if isinstance(value, unyt.unit_object.Unit):
        assert value.same_dimensions_as(exp_dim)
        valid_unit = value
    elif isinstance(value, str):
        try:
            unit = unyt_quantity.from_string(value).units
            assert unit.same_dimensions_as(exp_dim)
            valid_unit = unit
        except UnitParseError:
            raise UnitParseError(f"Could not interpret <{value}>.")
        except AssertionError:
            raise AssertionError(f"{value} lacks units of {dimension}.")
    else:
        raise ValueError(f"Value of type <{type(value)}> passed.")

    return valid_unit


def _validate_quantity(value, dimension):
    """
    This function checks that a quantity has the correct
    dimensions. Used in :class:`Technology` to set
    data attributess.

    Parameters
    ----------
    value : string, float, int, or :class:`unyt.unyt_quantity`
        The value being tested. Should be something like

        >>> _validate_quantity("10 MW", dimension='power')
        unyt_quantity(10., 'MW')

    dimension : string
        The expected dimensions of `value`.
        Accepted values listed in `_dim_opts`.

    Returns
    -------
    valid_quantity : :class:`unyt.unyt_quantity`
        The validated quantity.
    """
    try:
        exp_dim = _dim_opts[dimension]
    except KeyError:
        raise KeyError(f"Key <{dimension}> not accepted. Try: {_dim_opts}")

    valid_quantity = None
    if isinstance(value, unyt_quantity):
        try:
            assert value.units.same_dimensions_as(exp_dim)
            valid_quantity = value
        except AssertionError:
            raise TypeError(
                f"{value} has dimensions {value.units.dimensions}. "
                f"Expected {exp_dim.dimensions}")
    elif isinstance(value, unyt_array):
        try:
            assert value.units.same_dimensions_as(exp_dim)
            valid_quantity = value
        except AssertionError:
            raise TypeError(
                f"{value} has dimensions {value.units.dimensions}. "
                f"Expected {exp_dim.dimensions}")
    elif isinstance(value, np.ndarray):
        valid_quantity = value * exp_dim
    elif isinstance(value, pd.core.series.Series):
        valid_quantity = value.values * exp_dim
    elif isinstance(value, list):
        valid_quantity = np.array(value) * exp_dim
    elif isinstance(value, float):
        valid_quantity = value * exp_dim
    elif isinstance(value, int):
        valid_quantity = value * exp_dim
    elif isinstance(value, str):
        try:
            valid_quantity = float(value) * exp_dim
        except ValueError:
            try:
                unyt_value = unyt_quantity.from_string(value)
                assert unyt_value.units.same_dimensions_as(exp_dim)
                valid_quantity = unyt_value
            except UnitParseError:
                raise UnitParseError(f"Could not interpret <{value}>.")
            except AssertionError:
                raise AssertionError(f"{value} lacks units of {dimension}.")
    else:
        raise ValueError(f"Value of type <{type(value)}> passed.")
    return valid_quantity


[docs] class Technology(object): """ The :class:`Technology` base class contains the minimum required data to solve an energy systems problem. Many optional data are included here as well. All other technologies in :mod:`osier` inherit from this class. Parameters ---------- technology_name : str The name identifier of the technology. technology_type : str The string identifier for the type of technology. Two common types are: ["production", "storage"]. technology_category : str The string identifier the the technology category. For example: "renewable," "fossil," or "nuclear." dispatchable : bool Indicates whether the technology can be dispatched by a grid operator, or if it produces variable electricity that must be used or stored the moment it is produced. For example, solar panels and wind turbines are not dispatchable, but nuclear and biopower are dispatchable. Default value is true. renewable : bool Indicates whether the technology is considered "renewable." Useful for determining if a technology will contribute to a renewable portfolio standard (RPS). capital_cost : float or :class:`unyt.array.unyt_quantity` Specifies the capital cost. If float, the default unit is $/MW. om_cost_fixed : float or :class:`unyt.array.unyt_quantity` Specifies the fixed operating costs. If float, the default unit is $/MW. om_cost_variable : float, :class:`unyt.array.unyt_quantity`, or array-like Specifies the variable operating costs. Users may pass timeseries data. However, :class:`pandas.DataFrame` is not supported by this feature. If float, the default unit is $/MWh. fuel_cost : float, :class:`unyt.array.unyt_quantity`, or array-like Specifies the fuel costs. Users may pass timeseries data. However, :class:`pandas.DataFrame` is not supported by this feature. If float, the default unit is $/MWh. fuel_type : str Specifies the type of fuel consumed by the technology. capacity : float or :class:`unyt.array.unyt_quantity` Specifies the technology capacity. If float, the default unit is MW capacity_factor : Optional, float Specifies the 'usable' fraction of a technology's capacity. Default is 1.0, i.e. all of the technology's capacity is usable all of the time. capacity_credit : Optional, float Specifies the fraction of a technology's capacity that counts towards reliability requirements. Most frequently used for renewable technologies. For example, a solar farm might have a capacity credit of 0.2. This means that in order to meet a capacity requirement of 1 GW, 1.25 GW of solar would need to be installed. Default is 1.0, i.e. all of the technology's capacity contributes to capacity requirements. co2_rate : float or :class:`unyt.array.unyt_quantity` Specifies the rate at which carbon dioxide is emitted during operation. Generally only applicable for fossil fueled plants. If float, the default units are megatonnes per MWh lifecycle_co2_rate : float or :class:`unyt.array.unyt_quantity` Specifies the rate at which of CO2eq emissions over a typical lifetime. Unless you are reading this in a future where the economy is fully decarbonized, all technologies should have a non-zero value for this attribute. If float, the default units are megatonnes per MWh land_intensity : float or :class:`unyt.array.unyt_quantity` The amount of land required per unit capacity. May be either lifecycle land use or from direct use. However, consistency between technologies is incumbent on the user. efficiency : float The technology's energy conversion efficiency expressed as a fraction. Default is 1.0. lifetime : float The technology's operational lifetime in years. Default is 25 years. default_power_units : str or :class:`unyt.unit_object.Unit` An optional parameter, specifies the units for power. Default is megawatts [MW]. default_time_units : str or :class:`unyt.unit_object.Unit` An optional parameter, specifies the units for time. Default is hours [hr]. default_mass_units : str or :class:`unyt.unit_object.Unit` An optional parameter, specifies the units for mass. Default is hours [kg]. default_energy_units : str or :class:`unyt.unit_object.Unit` An optional parameter, specifies the units for energy. Default is megawatt-hours [MWh] Currently, `default_energy_units` is derived from the time and power units. Notes ----- Cost values are listed in the docs as [$ / physical unit]. However, :class:`osier` does not currently have a currency handler, therefore the units are technically [1 / physical unit]. The :class:`unyt` library may not be able to interpret strings for inverse units. For example: >>> my_unit = "10 / MW" >>> my_unit = unyt_quantity.from_string(my_unit) ValueError: Received invalid quantity expression '10/MW'. Instead, try the more explicit approach: >>> my_unit = "10 MW**-1" >>> my_unit = unyt_quantity.from_string(my_unit) unyt_quantity(10., '1/MW') However, inverse MWh cannot be converted from a string. """ def __init__(self, technology_name, technology_type='production', technology_category='base', dispatchable=True, renewable=False, capital_cost=0.0, om_cost_fixed=0.0, om_cost_variable=0.0, fuel_cost=0.0, fuel_type=None, capacity=0.0, capacity_factor=1.0, capacity_credit=1.0, co2_rate=0.0, lifecycle_co2_rate=0.0, land_intensity=0.0, efficiency=1.0, lifetime=25.0, default_power_units=MW, default_time_units=hr, default_energy_units=None, default_length_units=km, default_volume_units=m**3, default_mass_units=megatonnes) -> None: self.technology_name = technology_name self.technology_type = technology_type self.technology_category = technology_category self.dispatchable = dispatchable self.renewable = renewable self.fuel_type = fuel_type self.lifetime = lifetime self.unit_power = default_power_units self.unit_time = default_time_units self.unit_energy = default_energy_units self.unit_length = default_length_units self.unit_volume = default_volume_units self.unit_mass = default_mass_units self.capacity = capacity self.capacity_factor = capacity_factor self.capacity_credit = capacity_credit self.efficiency = efficiency self.capital_cost = capital_cost self.om_cost_fixed = om_cost_fixed self.om_cost_variable = om_cost_variable self.fuel_cost = fuel_cost self.power_level = self.capacity self.co2_rate = co2_rate self.lifecycle_co2_rate = lifecycle_co2_rate self.land_intensity = land_intensity self.power_history = [] def __repr__(self) -> str: return (f"{self.technology_name}: {self.capacity}") def __eq__(self, tech) -> bool: """Test technology equality""" if ((self.technology_name == tech.technology_name) and (self.capacity == tech.capacity) and (self.variable_cost == tech.variable_cost)): return True else: return False def __ge__(self, tech) -> bool: """Tests greater or equal to.""" if (self.variable_cost == tech.variable_cost): return self.efficiency >= tech.efficiency else: return self.variable_cost >= tech.variable_cost def __le__(self, tech) -> bool: """Tests less or equal to.""" if (self.variable_cost == tech.variable_cost): return self.efficiency <= tech.efficiency else: return self.variable_cost <= tech.variable_cost def __lt__(self, tech) -> bool: """Tests less than.""" if (self.variable_cost == tech.variable_cost): return self.efficiency < tech.efficiency else: return self.variable_cost < tech.variable_cost def __gt__(self, tech) -> bool: """Tests greater than.""" if (self.variable_cost == tech.variable_cost): return self.efficiency > tech.efficiency else: return self.variable_cost > tech.variable_cost @property def unit_power(self): return self._unit_power @unit_power.setter def unit_power(self, value): self._unit_power = _validate_unit(value, dimension="power") @property def unit_time(self): return self._unit_time @unit_time.setter def unit_time(self, value): self._unit_time = _validate_unit(value, dimension="time") @property def unit_mass(self): return self._unit_mass @unit_mass.setter def unit_mass(self, value): self._unit_mass = _validate_unit(value, dimension="mass") @property def unit_length(self): return self._unit_length @unit_length.setter def unit_length(self, value): self._unit_length = _validate_unit(value, dimension="length") @property def unit_area(self): return self._unit_length**2 @unit_area.setter def unit_area(self, value): self._unit_area = self._unit_length**2 @property def unit_volume(self): return self._unit_volume @unit_volume.setter def unit_volume(self, value): self._unit_volume = _validate_unit(value, dimension="volume") @property def unit_energy(self): return self._unit_power * self._unit_time @unit_energy.setter def unit_energy(self, value): self._unit_energy = self._unit_power * self._unit_time @property def capacity(self): return self._capacity.to(self._unit_power) @capacity.setter def capacity(self, value): valid_quantity = _validate_quantity(value, dimension="power") self._capacity = valid_quantity.to(self._unit_power) self.power_level = self._capacity @property def capital_cost(self): return self._capital_cost.to(self._unit_power**-1) @capital_cost.setter def capital_cost(self, value): self._capital_cost = _validate_quantity( value, dimension="specific_power") @property def om_cost_fixed(self): return self._om_cost_fixed.to(self._unit_power**-1) @om_cost_fixed.setter def om_cost_fixed(self, value): self._om_cost_fixed = _validate_quantity( value, dimension="specific_power") @property def om_cost_variable(self): if isinstance(self._om_cost_variable, _constant_types): return self._om_cost_variable.to(self.unit_energy**-1) elif isinstance(self._om_cost_variable, _array_types): if isinstance(self._om_cost_variable, unyt.unyt_array): return self._om_cost_variable.to(self.unit_energy**-1) else: return np.array(self._om_cost_variable) * \ (self.unit_energy**-1) @om_cost_variable.setter def om_cost_variable(self, value): self._om_cost_variable = _validate_quantity( value, dimension="specific_energy") @property def fuel_cost(self): if isinstance(self._fuel_cost, _constant_types): return self._fuel_cost.to(self.unit_energy**-1) elif isinstance(self._fuel_cost, _array_types): if isinstance(self._fuel_cost, unyt.unyt_array): return self._fuel_cost.to(self.unit_energy**-1) else: return np.array(self._fuel_cost) * (self.unit_energy**-1) @fuel_cost.setter def fuel_cost(self, value): self._fuel_cost = _validate_quantity( value, dimension="specific_energy") @property def co2_rate(self): return self._co2_rate.to(self.unit_mass * self.unit_energy**-1) @co2_rate.setter def co2_rate(self, value): self._co2_rate = _validate_quantity(value, dimension="mass_per_energy") @property def lifecycle_co2_rate(self): return self._lifecycle_co2_rate.to( self.unit_mass * self.unit_energy**-1) @lifecycle_co2_rate.setter def lifecycle_co2_rate(self, value): self._lifecycle_co2_rate = _validate_quantity( value, dimension="mass_per_energy") @property def land_intensity(self): return self._land_intensity.to(self.unit_area * self.unit_power**-1) @land_intensity.setter def land_intensity(self, value): self._land_intensity = _validate_quantity( value, dimension="area_per_power") @property def total_capital_cost(self): return self.capacity * self.capital_cost @property def annual_fixed_cost(self): return self.capacity * self.om_cost_fixed @property def variable_cost(self): """ Combines the fuel and variable operating costs into a total variable cost associated with technology usage. Notes ----- This function will attempt to merge the two values, even if they have different sizes and types. Therefore it is recommended that users pass values of the same size and type to prevent unexpected behavior. """ if (isinstance(self.fuel_cost, _constant_types) and isinstance(self.om_cost_variable, _constant_types)): return self.fuel_cost + self.om_cost_variable elif (isinstance(self.fuel_cost, _array_types) and isinstance(self.om_cost_variable, _constant_types)): return self.fuel_cost + \ np.ones(len(self.fuel_cost)) * self.om_cost_variable elif (isinstance(self.fuel_cost, _constant_types) and isinstance(self.om_cost_variable, _array_types)): return self.fuel_cost * \ np.ones(len(self.om_cost_variable)) + self.om_cost_variable elif (isinstance(self.fuel_cost, _constant_types) and isinstance(self.om_cost_variable, _array_types)): return self.fuel_cost * \ np.ones(len(self.om_cost_variable)) + self.om_cost_variable elif (isinstance(self.fuel_cost, _array_types) and isinstance(self.om_cost_variable, _array_types)): min_len = min(len(self.fuel_cost), len(self.om_cost_variable)) return self.fuel_cost[:min_len] + self.om_cost_variable[:min_len] else: raise TypeError( f"Fuel cost has type <{type(self.fuel_cost)}>.\n" + f"OM variable cost has type <{type(self.om_cost_variable)}>.\n" "One or both of these types are unknown.")
[docs] def variable_cost_ts(self, size): """ Returns the total variable cost as an array of length :attr:`size`. .. warning:: The current implementation will only select the first N values, where N = `size`. It is recommended that users only pass the subset of data they wish to use. Parameters ---------- size : int The number of periods, i.e. length, of the time series. Returns ------- var_cost_ts : :class:`numpy.ndarray` The variable cost time series. """ if isinstance(self.variable_cost, _constant_types): var_cost_ts = np.ones(size) * self.variable_cost return var_cost_ts elif isinstance(self.variable_cost, _array_types): try: var_cost_ts = self.variable_cost[:size] assert len(var_cost_ts) == size except AssertionError as e: raise AssertionError( f"Variable cost data too short ({len(var_cost_ts)} < {size})") return var_cost_ts
[docs] def to_dataframe(self, cast_to_string=True): """ Writes all technology attributes to a :class:`pandas.DataFrame` for export and manipulation. """ tech_data = OrderedDict() tech_data['technology_name'] = [self.technology_name] tech_data['technology_category'] = [self.technology_category] tech_data['technology_type'] = [self.technology_type] tech_data['dispatchable'] = [str(self.dispatchable)] tech_data['renewable'] = [str(self.renewable)] tech_data['fuel_type'] = [str(self.fuel_type)] for key, value in self.__dict__.items(): if key in tech_data: continue elif value is None: col = key.strip('_') tech_data[col] = [str(value)] else: if isinstance(value, unyt.unit_object.Unit): continue elif isinstance(value, unyt_quantity): col = f"{key.strip('_')} ({value.units})" if cast_to_string: tech_data[col] = ["{:.3g}".format(value.to_value())] else: tech_data[col] = [np.round(value.to_value(), 10)] elif isinstance(value, (int, float)): col = key.strip('_') if cast_to_string: tech_data[col] = ["{:.3g}".format(value)] else: tech_data[col] = [np.round(value, 10)] else: continue tech_dataframe = pd.DataFrame(tech_data).set_index('technology_name') return tech_dataframe
[docs] def reset_history(self): """ Resets the technology's power history for a new simulation. """ self.power_history = [] self.power_level = self.capacity
[docs] def power_output(self, demand: unyt_quantity, **kwargs): """ Raise or lower the power level to meet demand. Returns current power level and appends to power history. Parameters ---------- demand : :class:`unyt.unyt_quantity` The demand at a particular timestep. Must be a :class:`unyt.unyt_quantity` to avoid ambiguity. Returns ------- power_level : :class:`unyt.unyt_quantity` The current power level of the technology. """ assert isinstance(demand, unyt_quantity) self.power_level = max(0 * demand.units, min(demand, self.capacity)) self.power_history.append(self.power_level.copy()) return self.power_level
[docs] class RampingTechnology(Technology): """ The :class:`RampingTechnology` class extends the :class:`Technology` class by adding ramping attributes that correspond to a technology's ability to increase or decrease its power level at a specified rate. Parameters ---------- ramp_up_rate : float or :class:`unyt_quantity` The rate at which a technology can increase its power, expressed as a percentage of its capacity. For example, if `ramp_up_rate` equals 0.5, then the technology may ramp up its power level by 50% per unit time. The default is 1.0 (i.e. there is no constraint on ramping up). ramp_down_rate : float or :class:`unyt_quantity` The rate at which a technology can decrease its power, expressed as a percentage of its capacity. For example, if `ramp_down_rate` equals 0.5, then the technology may ramp down its power level by 50% per unit time. The default is 1.0 (i.e. there is no constraint on ramping down). Notes ----- It is common for a ramping technology to have different ramp up and ramp down rates. Consider a light-water nuclear reactor that can quickly reduce its power level by inserting control rods, but must wait much longer to increase its power by the same amount due to a build up of neutron absorbing isotopes. """ def __init__( self, technology_type='production', technology_category='ramping', ramp_up_rate=1.0 * hr**-1, ramp_down_rate=1.0 * hr**-1, *args, **kwargs) -> None: self.ramp_up_rate = _validate_quantity(ramp_up_rate, dimension='specific_time') self.ramp_down_rate = _validate_quantity(ramp_down_rate, dimension='specific_time') super().__init__(technology_type=technology_type, technology_category=technology_category, *args, **kwargs) @property def ramp_up(self): return ( self.capacity * self.ramp_up_rate).to( self.unit_power * self.unit_time**-1 ) @property def ramp_down(self): return ( self.capacity * self.ramp_down_rate).to( self.unit_power * self.unit_time**-1 )
[docs] def max_power(self, time_delta: unyt_quantity = 1 * hr): """ Calculates the maximum achievable power for a technology in the next timestep. Parameters ---------- time_delta : :class:`unyt.unyt_quantity` The difference between two timesteps. Default is one hour. Returns ------- max_power : :class:`unyt.unyt_quantity` The maximum achievable power level. """ output = self.power_level + self.ramp_up * time_delta return min(self.capacity, output)
[docs] def min_power(self, time_delta: unyt_quantity = 1 * hr): """ Calculates the minimum achievable power for a technology in the next timestep. Parameters ---------- time_delta : :class:`unyt.unyt_quantity` The difference between two timesteps. Default is one hour. Returns ------- min_power : :class:`unyt.unyt_quantity` The minimum achievable power level. """ output = self.power_level - self.ramp_down * time_delta return max(0 * self.unit_power, output)
[docs] def power_output(self, demand: unyt_quantity, time_delta: unyt_quantity = 1 * hr): """ Raise or lower the power level to meet demand. Returns current power level and appends to power history. Checks if the power level can be achieved given the technology's ramp rate. Parameters ---------- demand : :class:`unyt.unyt_quantity` The demand at a particular timestep. Must be a :class:`unyt.unyt_quantity` to avoid ambiguity. time_delta : :class:`unyt.unyt_quantity` The difference between two timesteps. Default is one hour. Returns ------- power_level : :class:`unyt.unyt_quantity` The current power level of the technology. """ assert isinstance(demand, unyt_quantity) if self.power_level > demand: # power must be lowered self.power_level = max( self.min_power(time_delta), demand).to( demand.units) elif (self.power_level <= demand) and \ (self.capacity >= demand): # power must be raised self.power_level = (min(self.max_power(time_delta), demand)).to(demand.units) elif (self.power_level <= demand) and \ (self.capacity <= demand): self.power_level = self.max_power(time_delta).to(demand.units) self.power_history.append(self.power_level) return self.power_level
[docs] class ThermalTechnology(RampingTechnology): """ The :class:`ThermalTechnology` class extends the :class:`RampingTechnology` class by adding a heat rate. Parameters ---------- heat_rate : int or float The heat rate of a given technology. """ def __init__( self, heat_rate=None, technology_type='production', technology_category='thermal', *args, **kwargs) -> None: super().__init__(technology_type=technology_type, technology_category=technology_category, *args, **kwargs) self.heat_rate = heat_rate self.power_level = self.capacity
[docs] class StorageTechnology(Technology): """ The :class:`StorageTechnology` extends the :class:`Technology` by adding storage parameters. Parameters ---------- storage_duration : float or :class:`unyt.array.unyt_quantity` The amount of time the battery could discharge continuously when full. Used to calculate the storage capacity. initial_storage : float or :class:`unyt.array.unyt_quantity` The initial stored energy. Cannot exceed :attr:`storage_capacity`. """ def __init__( self, technology_type='storage', storage_duration=0, initial_storage=0, *args, **kwargs) -> None: super().__init__(technology_type=technology_type, *args, **kwargs) self.storage_duration = storage_duration self.initial_storage = initial_storage self.storage_level = self.initial_storage self.storage_history = [] self.charge_history = [] @property def storage_duration(self): return self._storage_duration @storage_duration.setter def storage_duration(self, value): valid_quantity = _validate_quantity(value, dimension='time') self._storage_duration = valid_quantity @property def storage_capacity(self): return self._storage_duration * self._capacity @property def initial_storage(self): return self._initial_storage @initial_storage.setter def initial_storage(self, value): valid_quantity = _validate_quantity(value, dimension='energy') try: assert valid_quantity <= self.storage_capacity except AssertionError: raise AssertionError("Initial storage exceeds storage capacity.") self._initial_storage = valid_quantity self.storage_level = valid_quantity @property def max_rate(self): return self.capacity * self.unit_time
[docs] def reset_history(self): """ Resets the technology's power history for a new simulation. """ self.storage_history = [] self.storage_level = self._initial_storage self.power_history = [] self.power_level = self.capacity self.charge_history = []
[docs] def discharge(self, demand: unyt_quantity, time_delta=1 * hr): """ Discharges the battery if there is a surplus of energy. Parameters ---------- demand : :class:`unyt.unyt_quantity` Amount of surplus. time_delta : :class:`unyt.unyt_quantity` The real time passed between modeled timesteps. Returns ------- power_level : :class:`unyt.unyt_quantity` The current power level of the technology. """ # check that the battery has power to discharge fully. power_out = max(0 * demand.units, min(demand, self.capacity)) # check that the battery has enough energy to meet demand. energy_out = min(power_out * time_delta, self.storage_level) out = self.storage_level - energy_out self.storage_level = out self.storage_history.append(out) self.power_level = energy_out / time_delta self.power_history.append(self.power_level) self.charge_history.append(0 * demand.units) return self.power_level.to(demand.units)
[docs] def charge(self, surplus, time_delta=1 * hr): """ Charges the battery if there is a surplus of energy. Parameters ---------- surplus : :class:`unyt.unyt_quantity` Amount of surplus. time_delta : :class:`unyt.unyt_quantity` The real time passed between modeled timesteps. Returns ------- power_level : :class:`unyt.unyt_quantity` The current power level of the technology. """ # check that the battery has enough power to consume surplus. power_in = min(np.abs(min(0 * surplus.units, surplus)), self.capacity) # check that the battery has enough space to store surplus. energy_in = min((self.storage_capacity - self.storage_level), power_in * time_delta) out = self.storage_level + energy_in self.storage_level = out self.storage_history.append(out) self.power_level = -energy_in / time_delta self.charge_history.append(self.power_level) self.power_history.append(0 * surplus.units) return self.power_level.to(surplus.units)
[docs] def power_output(self, v, time_delta=1 * hr): """ Calculates the power output given a demand value. Parameters ---------- v : :class:`unyt.unyt_quantity` Voltage representing a demand or a surplus of energy. time_delta : :class:`unyt.unyt_quantity` The real time passed between modeled timesteps. Returns ------- output : :class:`unyt.unyt_quantity` The current power level of the technology. """ if v >= 0: output = self.discharge(demand=v, time_delta=time_delta) else: output = self.charge(surplus=v, time_delta=time_delta) return output