Source code for relsad.network.components.Battery

from enum import Enum
from numbers import Number

import numpy as np

from relsad.Time import Time, TimeUnit
from relsad.utils import INF

from .Bus import Bus
from .Component import Component
from .MicrogridController import MicrogridMode


[docs]class BatteryType(Enum): """ Battery type Attributes ---------- REGULAR : int Regular battery type EV : int Electric vehicle battery type """ REGULAR = 1 EV = 2
[docs]class BatteryState(Enum): """ Battery state Attributes ---------- ACTIVE : int Battery is active INACTIVE : int Battery is inactive """ ACTIVE = 1 INACTIVE = 2
[docs]class Battery(Component): """ Common class for batteries ... Attributes ---------- name : string Name of the battery mode : str Microgrid mode survival_time : Time Total amount of hours the microgrid should survive on battery capacity remaining_survival_time : Time The time left for the battery to ensure energy for the microgrid load standard_SOC_min : float The minimum State of Charge for the battery which can change based on wanted battery capacity bus : Bus The bus the battery is connected to inj_p_max : float The maximum active power that the battery can inject [MW] inj_q_max : float The maximum reactive power that the battery can inject [MVar] inj_max : float The maximum apperent power that the battery can inject [MVa] f_inj_p : float Active power capacity fraction f_inj_q : float Reactive power capacity fraction E_max : float The maximum capacity of the battery [MWh] SOC_min : float The minimal state of charge level in the battery SOC_max : float The maximum state of charge level in the battery n_battery : float The battery efficiency SOC_start : float The iteration start value for the state of charge level in the battery p_inj : float The injected active power in the battery [MW] q_inj : float The injected reactive power in the battery [MVar] SOC : float The state of charge of the battery E_battery : float The amount of energy stored in the battery [MWh] battery_type : BatteryType The battery type state : BatteryState Battery state history : dict Dictonary attribute that stores the historic variables Methods ---------- update_SOC() Updates the SOC in the battery charge(P_ch, dt) Charge the battery. Decides how much the battery can charge based on the desired charging power. Restricted by the amount of power that can be stored, the maximum power that can be injected, and the maximum state of charge of the battery. Updates the state of charge of the battery. Returns a float telling how much power the battery is not able to charge discharge(P_dis, q_dis, dt) Discharge the battery. Decides how much the battery can discharge based on the available energy in the battery. Limited by the state of charge, the maximum power that can be injected, and the wanted amount of power from the battery. Updates the state of charge of the battery. Returns a float telling how much power the battery is not able to discharge print_status() Prints the status of the battery update_bus_load_and_prod(system_load_balance_p, system_load_balance_q) Updates the load and production on the bus based on the system load balance. If the balance is negative, there is a surplus of production, and the battery will charge. If the balance is positive, there is a shortage of production, and the battery will discharge. Returns the remaining surplus/shortage of power initialize_history() Initializes the history variables update_history(prev_time, curr_time, dt, save_flag) Updates the history variables get_history(attribute) Returns the history variables of an attribute update_fail_status(dt) Locks and unlocks the battery functionality based on failure states of the basestation add_random_instance(random_gen) Adds global random seed reset_status() Resets and sets the status of the system parameters set_mode(mode) Sets the microgrid mode start_survival_time() Starts the timer for how long the battery should focus on supporting own load Only for when a microgrid is added and follows a survival mode set_SOC_state() Sets the SOC state draw_SOC_state() Draws the SOC state based on a uniform distribution update(p, q, fail_duration, dt) Updates the battery status for the current time step """ ## Random instance ps_random: np.random.Generator = None def __init__( self, name: str, bus: Bus, inj_p_max: float = 0.5, inj_q_max: float = 0.5, E_max: float = 1, SOC_min: float = 0.1, SOC_max: float = 1, n_battery: float = 0.95, battery_type: BatteryType = BatteryType.REGULAR, random_instance: np.random.Generator = None, SOC_start: float = None, ): # Verify input if bus is None: raise Exception("Battery must be connected to a Bus") if bus.parent_network is not None and random_instance is None: raise Exception( "Battery must be created before the bus is connected to a network" ) if inj_p_max < 0: raise Exception("The active power injection must be positive") if inj_q_max < 0: raise Exception("The reactive power injection must be positive") if E_max < 0: raise Exception("The energy capacity must be positive") if SOC_min < 0 or SOC_max > 1: raise Exception("The SOC limits must be between 0 and 1") if n_battery < 0 or n_battery > 1: raise Exception("The efficiency must be between 0 and 1") if ( SOC_start is not None and not isinstance(SOC_start, Number) and (SOC_start < 0 or SOC_start > 1) ): raise Exception( "The SOC start value must be a number between 0 and 1" ) self.name = name self.ps_random = random_instance self.mode = None self.survival_time = Time(4, TimeUnit.HOUR) self.remaining_survival_time = Time(0) self.standard_SOC_min = SOC_min self.bus = bus self.battery_type = battery_type if battery_type == BatteryType.REGULAR: # Link battery to parent bus bus.battery = self self.inj_p_max = inj_p_max # MW self.inj_q_max = inj_q_max # MVar self.inj_max = self.inj_p_max # active capacity fraction self.f_inj_p = 1 # reactive capacity fraction self.f_inj_q = self.inj_q_max / self.inj_max self.E_max = E_max # MWh self.SOC_min = SOC_min self.E_min = E_max * SOC_min # MWh self.SOC_max = SOC_max self.n_battery = n_battery if SOC_start is None: self.set_SOC_state(SOC_state=SOC_min) else: self.set_SOC_state(SOC_state=SOC_start) self.SOC_start = SOC_start self.p_inj = 0.0 # MW self.q_inj = 0.0 # MVar self.state = BatteryState.ACTIVE ## History self.history = {} self.initialize_history() def __str__(self): return self.name def __repr__(self): return f"Battery(name={self.name})" def __eq__(self, other): if hasattr(other, "name"): return self.name == other.name and isinstance(other, Battery) else: return False def __hash__(self): return hash(self.name)
[docs] def update_SOC(self): """ Updates the SOC in the battery Parameters ---------- None Returns ---------- None """ self.SOC = self.E_battery / self.E_max
[docs] def charge(self, p_ch, dt: Time): """ Charge the battery Decides how much the battery can charge based on the desired charging power. Restricted by the amount of power that can be stored, the maximum power that can be injected, and the maximum state of charge of the battery Updates the state of charge of the battery Returns a float telling how much power the battery is not able to charge Parameters ---------- p_ch : float Desired amount of active power to charge the battery [MW] dt : Time The current time step Returns ---------- P_ch_remaining : float Amount of active power exceeding the charging capacity [MW] """ p_ch_remaining = 0 if p_ch > self.inj_p_max: p_ch_remaining += p_ch - self.inj_p_max if p_ch >= INF: # "Infinite" power source available p_ch = self.inj_p_max else: p_ch -= p_ch_remaining # Change in energy dE = self.n_battery * p_ch * dt.get_hours() # MWh # Energy level, trial step E_tr = self.E_battery + dE SOC_tr = E_tr / self.E_max dSOC = dE / self.E_max if SOC_tr > self.SOC_max: # Correcting step # # Factor to scale the energy level f = 1 - (SOC_tr - self.SOC_max) / dSOC self.E_battery += f * dE p_ch_remaining += (1 - f) * p_ch else: self.E_battery += dE p_ch_remaining += 0 self.update_SOC() return p_ch_remaining
[docs] def discharge(self, p_dis, q_dis, dt: Time): """ Discharge the battery Decides how much the battery can discharge based on the available energy in the battery. Limited by the state of charge, the maximum power that can be injected, and the wanted amount of power from the battery Updates the state of charge of the battery Returns a float telling how much power the battery is not able to discharge Parameters ---------- p_dis : float Amount of wanted active power from the network [MW] q_dis : float Amount of wanted reactive power from the network [MVar] dt : Time The current time step Returns ---------- p_dis_remaining : float Amount of wanted active power from the network exceeding battery discharging capacity [MW] q_dis_remaining : float Amount of wanted reactive power from the network exceeding battery discharging capacity [MVar] """ if ( self.remaining_survival_time > Time(0) and self.mode == MicrogridMode.SURVIVAL ): self.remaining_survival_time -= dt self.SOC_min = min( self.bus.parent_network.get_max_load()[0] * self.remaining_survival_time.get_hours() / self.E_max + self.standard_SOC_min, self.SOC_max, ) else: self.SOC_min = self.standard_SOC_min p_dis_remaining = 0 q_dis_remaining = 0 if p_dis > self.inj_p_max: p_dis_remaining += p_dis - self.inj_p_max p_dis -= p_dis_remaining if q_dis > self.inj_q_max: q_dis_remaining += q_dis - self.inj_q_max q_dis -= q_dis_remaining if p_dis + q_dis > self.inj_max: f_p = p_dis / (p_dis + q_dis) # active fraction f_q = 1 - f_p # reactive fraction diff = p_dis + q_dis - self.inj_max p_dis_remaining += diff * (1 - f_p) q_dis_remaining += diff * (1 - f_q) p_dis -= diff * (1 - f_p) q_dis -= diff * (1 - f_q) dE = 1 / self.n_battery * (p_dis + q_dis) * dt.get_hours() # MWh/MVarh E_tr = self.E_battery - dE SOC_tr = E_tr / self.E_max dSOC = dE / self.E_max if SOC_tr < self.SOC_min and dSOC > 0: f = 1 - (self.SOC_min - SOC_tr) / dSOC self.E_battery -= f * dE p_dis_remaining += (1 - f) * p_dis q_dis_remaining += (1 - f) * q_dis else: self.E_battery -= dE self.update_SOC() return p_dis_remaining, q_dis_remaining
[docs] def print_status(self): """ Prints the status of the battery Parameters ---------- None Returns ---------- None """ print("Battery status") print("name: {}".format(self.name)) print("parent bus: {}".format(self.bus.name)) print("inj_p_max: {} MW".format(self.inj_p_max)) print("inj_q_max: {} MVar".format(self.inj_q_max)) print("E_max: {} MWh".format(self.E_max)) print("Efficiency: {} %".format(self.n_battery * 100)) print("SOC_min: {}".format(self.SOC_min)) print("SOC_max: {}".format(self.SOC_max)) print("SOC: {:.2f}".format(self.SOC))
[docs] def update_bus_load_and_prod( self, system_load_balance_p: float, system_load_balance_q: float, dt: Time, ): """ Updates the load and production on the bus based on the system load balance. If the balance is negative, there is a surplus of production, and the battery will charge. If the balance is positive, there is a shortage of production, and the battery will discharge. Returns the remaining surplus/shortage of power Parameters ---------- system_load_balance_p : float Active power system load balance in MW system_load_balance_q : float Reactive power system load balance in MW dt : Time The current time step Returns ---------- p_rem : float Remaining surplus/shortage active power q_rem : float Remianing surplus/shortage reactive power """ p, q = system_load_balance_p, system_load_balance_q if self.state == BatteryState.INACTIVE: # Trafo has failed p_rem, q_rem = p, q return p_rem, q_rem pprod, qprod, pload, qload = 0, 0, 0, 0 if p >= 0 and q >= 0: p_rem, q_rem = self.discharge(p, q, dt) pprod = p - p_rem qprod = q - q_rem if p < 0 and q >= 0: p_rem = self.charge(-p, dt) q_rem = self.discharge(0, q, dt)[1] pload = -p - p_rem qprod = q - q_rem if p >= 0 and q < 0: p_rem = self.discharge(p, 0, dt)[0] pprod = p - p_rem if p < 0 and q < 0: p_rem = self.charge(-p, dt) pload = -p - p_rem # Add production to bus self.bus.pprod += pprod # MW self.bus.qprod += qprod # MVar self.bus.pprod_pu += pprod / self.bus.s_ref # PU self.bus.qprod_pu += qprod / self.bus.s_ref # PU # Add load to bus self.bus.add_load( pload=pload, qload=qload, ) # Battery power injection self.p_inj = pload - pprod self.q_inj = qload - qprod # Remaining power p_rem = p + pload - pprod q_rem = q + qload - qprod return p_rem, q_rem
[docs] def initialize_history(self): """ Initializes the storage of the history variables Parameters ---------- None Returns ---------- None """ self.history["SOC"] = {} self.history["SOC_min"] = {} self.history["remaining_survival_time"] = {}
[docs] def update_history( self, prev_time: Time, curr_time: Time, save_flag: bool ): """ Updates the history variables Parameters ---------- prev_time : Time The previous time curr_time : Time Current time save_flag : bool Indicates if saving is on or off Returns ---------- None """ if save_flag: time = curr_time.get_unit_quantity(curr_time.unit) self.history["SOC"][time] = self.SOC self.history["SOC_min"][time] = self.SOC_min self.history["remaining_survival_time"][ time ] = self.remaining_survival_time.get_unit_quantity(curr_time.unit)
[docs] def get_history(self, attribute: str): """ Returns the history variables of an attribute Parameters ---------- attribute : str Battery attribute Returns ---------- history[attribute] : dict Returns the history variables of an attribute """ return self.history[attribute]
[docs] def update_fail_status(self, dt: Time): """ Locks og unlocks the battery functionality based on failure states of the basestation Parameters ---------- dt : Time The current time step Returns ---------- None """ if self.bus.trafo_failed: self.state = BatteryState.INACTIVE else: self.state = BatteryState.ACTIVE
[docs] def add_random_instance(self, random_gen): """ Adds global random seed Parameters ---------- random_gen : int Random number generator Returns ---------- None """ self.ps_random = random_gen
[docs] def reset_status(self, save_flag: bool): """ Resets and sets the status of the class parameters Parameters ---------- save_flag : bool Indicates if saving is on or off Returns ---------- None """ if self.SOC_start is None: self.set_SOC_state(SOC_state=self.standard_SOC_min) else: self.set_SOC_state(SOC_state=self.SOC_start) self.state = BatteryState.ACTIVE if save_flag: self.initialize_history()
[docs] def set_mode(self, mode): """ Sets the microgrid mode Parameters ---------- mode : str The microgrid mode Returns ---------- None """ self.mode = mode
[docs] def start_survival_time(self): """ Starts the timer for how long the battery should focus on supporting own load Only for when a microgrid is added and follows a survival mode Parameters ---------- None Returns ---------- None """ self.remaining_survival_time = self.survival_time
[docs] def set_SOC_state(self, SOC_state: float): """ Sets the SOC state Parameters ---------- SOC_state : float SOC value, between SOC_min and SOC_max Returns ---------- None """ if SOC_state < self.SOC_min or SOC_state > self.SOC_max: raise Exception("Not a valid SOC state") self.E_battery = SOC_state * self.E_max self.update_SOC()
[docs] def draw_SOC_state(self): """ Draws the SOC state based on a uniform distribution Parameters ---------- None Returns ---------- None """ self.E_battery = self.ps_random.uniform( low=self.E_min, high=self.E_max, ) self.update_SOC()
[docs] def update(self, p: float, q: float, fail_duration: Time, dt: Time): """ Updates the battery status for the current time step Parameters ---------- p : float Active power balance of the parent power system q : float Reactive power balance of the parent power system fail_duration : Time The duration of the current failure dt : Time The current time step Returns ---------- p : float Remaining active power balance of the parent power system q : float Remaining reactive power balance of the parent power system """ if ( self.mode in [MicrogridMode.SURVIVAL, MicrogridMode.FULL_SUPPORT] and fail_duration == dt ): # Special handling for microgrid modes # Failure occurred in current time step self.draw_SOC_state() p, q = self.update_bus_load_and_prod(p, q, dt) return p, q
if __name__ == "__main__": pass