Source code for envs.battery_model

# Copyright (c) Meta Platforms, Inc. and affiliates.
# This source code is licensed under the CC-BY-NC license found in the
# LICENSE file in the root directory of this source tree.

import numpy as np

# class Battery:
#     capacity = 0 # Max MWh storage capacity
#     current_load = 0 # Current load in the battery, in MWh

#     def __init__(self, capacity, current_load=0):
#         self.capacity = capacity
#         self.current_load = current_load

#     # charge the battery based on an hourly load
#     # returns the total load after charging with input_load
#     def charge(self, input_load):
#         self.current_load = self.current_load + input_load
#         if(self.current_load > self.capacity):
#             self.current_load = self.capacity
#         return self.current_load

#     # returns how much energy is discharged when
#     # output_load is drawn from the battery in an hour
#     def discharge(self, output_load):
#         self.current_load = self.current_load - output_load
#         if(self.current_load < 0): # not enough battery load
#             lacking_amount = self.current_load
#             self.current_load = 0
#             return output_load + lacking_amount
#         return output_load

#     def is_full(self):
#         return (self.capacity == self.current_load)
    
#     # calculate the minimum battery capacity required
#     # to be able to charge it with input_load
#     # amount of energy within an hour and
#     # expand the existing capacity with that amount
#     def find_and_init_capacity(self, input_load):
#         self.capacity = self.capacity + input_load

# Battery model that includes efficiency and 
# linear charging/discharging rate limits with respect to battery capacity
# refer to C/L/C model in following reference for details: 
# "Tractable lithium-ion storage models for optimizing energy systems." 
# Energy Informatics 2.1 (2019): 1-22.
[docs] class Battery2: capacity = 0 # Max MWh storage capacity current_load = 0 # Current load in the battery, in MWh # charging and discharging efficiency, including DC-AC inverter loss eff_c = 1 eff_d = 1 c_lim = 3 d_lim = 3 # Maximum charging energy in one time step # is limited by (u * applied power) + v upper_lim_u = 0 upper_lim_v = 1 # Maximum discharged energy in one time step # is limited by (u * applied power) + v lower_lim_u = 0 lower_lim_v = 0 # defaults for lithium NMC cell def __init__(self, capacity, current_load=0, eff_c = 1, eff_d = 1, c_lim = 0.1, d_lim = 1, upper_u = -0.04, upper_v = 1, lower_u = 0.01, lower_v = 0): self.capacity = capacity self.current_load = current_load self.eff_c = eff_c self.eff_d = eff_d self.c_lim = c_lim self.d_lim = d_lim self.upper_lim_u = upper_u self.upper_lim_v = upper_v self.lower_lim_u = lower_u self.lower_lim_v = lower_v # Avisek added self.charging_load = 0
[docs] def reset(self): self.current_load = 0#np.round(np.random.uniform(0, self.capacity/4), 4)
[docs] def calc_max_charge(self, T_u): # energy content in current (next) time step: b_k (b_{k+1}, which is just b_k + p_k*eff_c) # charging power in current time step: p_k # b_{k+1} <= u * p_k + v is equivalent to # p_k <= (v - b_k) / (eff_c - u) max_charge = min((self.capacity/self.eff_c) * self.c_lim, (self.upper_lim_v*self.capacity - self.current_load)/((self.eff_c*T_u) - self.upper_lim_u)) return max_charge
[docs] def calc_max_discharge(self, T_u, dc_load): # energy content in current (next) time step: b_k (b_{k+1}, which is just b_k - p_k*eff_d) # charging power in current time step: p_k # b_{k+1} <= u * p_k + v is equivalent to # p_k <= (b_k - v) / (u + eff_d) max_discharge = min((self.capacity/self.eff_d) * self.d_lim, (self.current_load - self.lower_lim_v*self.capacity)/(self.lower_lim_u + (self.eff_d*T_u)), dc_load) return max_discharge
# charge the battery based on an hourly load # returns the total load after charging with input_load
[docs] def charge(self, input_load, T_u): max_charge = self.calc_max_charge(T_u) # Avisek added self.charging_load = min(max_charge, input_load) * self.eff_c * T_u self.current_load = np.round(self.current_load + self.charging_load, 8) # 4 decimals to round to avoid overflow in rounding numbers return self.current_load
# returns how much energy is discharged when # output_load is drawn from the battery over T_u hours (default T_u is 1/60)
[docs] def discharge(self, output_load, T_u, dc_load): max_discharge = self.calc_max_discharge(T_u, dc_load) self.current_load = np.round(self.current_load - (min(max_discharge, output_load) * self.eff_d * T_u), 8) if(max_discharge < output_load): # not enough battery load return max_discharge*T_u return output_load*T_u
[docs] def is_full(self): return (self.capacity == self.current_load)
# calculate the minimum battery capacity required # to be able to charge it with input_load # amount of energy within an hour and # expand the existing capacity with that amount
[docs] def find_and_init_capacity(self, input_load): self.capacity = self.capacity + input_load*self.eff_d # increase the capacity until we can discharge input_load # TODO: find analytical value for this new_capacity = input_load*self.eff_d while True: power_lim = (new_capacity - self.lower_lim_v*self.capacity)/(self.lower_lim_u + self.eff_d) if power_lim < input_load: self.capacity += 0.1 new_capacity += 0.1 else: break
# # return True if battery can meet all demand, False otherwise # def sim_battery_247(df_ren, df_dc_pow, b): # points_per_hour = 60 # for i in range(df_dc_pow.shape[0]): # ren_mw = df_ren[i] # df_dc = df_dc_pow["avg_dc_power_mw"][i] # net_load = ren_mw - df_dc # actual_discharge = 0 # # Apply the net power points_per_hour times # for j in range(points_per_hour): # # surplus, charge # if net_load > 0: # b.charge(net_load, 1/points_per_hour) # else: # # deficit, discharge # actual_discharge += b.discharge(-net_load, 1/points_per_hour) # # if we couldnt discharge enough, exit # # check if actual dicharge was sufficient to meet net load (with some tolerance for imprecision) # if net_load < 0 and actual_discharge < -net_load - 0.0001: # return False # return True # # binary search for smallest battery size that meets all demand # def calculate_247_battery_capacity_b2_sim(df_ren, df_dc_pow, max_bsize): # # first check special case, no battery: # if sim_battery_247(df_ren, df_dc_pow, Battery2(0,0)): # return 0 # l = 0 # u = max_bsize # while u - l > 0.1: # med = (u + l) / 2 # if sim_battery_247(df_ren, df_dc_pow, Battery2(med,med)): # u = med # else: # l = med # # check if max size was too small # if u == max_bsize: # return np.nan # return med # # binary search for smallest battery size that meets all demand # def calculate_247_battery_capacity_b1_sim(df_ren, df_dc_pow, max_bsize): # # first check special case, no battery: # if sim_battery_247(df_ren, df_dc_pow, Battery(0,0)): # return 0 # l = 0 # u = max_bsize # while u - l > 0.1: # med = (u + l) / 2 # if sim_battery_247(df_ren, df_dc_pow, Battery(med,med)): # u = med # else: # l = med # # check if max size was too small # if u == max_bsize: # return np.nan # return med # # Takes renewable supply and dc power as input dataframes # # returns how much battery capacity is needed to make # # dc operate on renewables 24/7 # def calculate_247_battery_capacity(df_ren, df_dc_pow): # battery_cap = 0 # return value stored here, capacity needed # b = Battery(0) # start with an empty battery # for i in range(df_dc_pow.shape[0]): # ren_mw = df_ren[i] # df_dc = df_dc_pow["avg_dc_power_mw"][i] # if df_dc > ren_mw: # if there's not enough renewable supply, need to discharge # if(b.capacity == 0): # b.find_and_init_capacity(df_dc - ren_mw) # find how much battery cap needs to be # else: # load_before = b.current_load # if(load_before == 0): # b.find_and_init_capacity(df_dc - ren_mw) # else: # drawn_amount = b.discharge(df_dc - ren_mw) # if(drawn_amount < (df_dc - ren_mw)): # b.find_and_init_capacity((df_dc - ren_mw) - drawn_amount) # else: # there's excess renewable supply, charge batteries # if b.capacity > 0: # b.charge(ren_mw-df_dc) # elif b.is_full(): # b = Battery(0) # if b.capacity > 0 and battery_cap != np.nan: # battery_cap = max(battery_cap, b.capacity) # return battery_cap # # Takes battery capacity, renewable supply and dc power as input dataframes # # and calculates how much battery can increase renewable coverage # # returns the non renewable amount that battery cannot cover # def apply_battery(battery_capacity, df_ren, df_dc_pow): # b = Battery2(battery_capacity, battery_capacity) # tot_non_ren_mw = 0 # store the mw amount battery cannot supply here # points_per_hour = 60 # for i in range(df_dc_pow.shape[0]): # ren_mw = df_ren[i] # df_dc = df_dc_pow["avg_dc_power_mw"][i] # gap = df_dc - ren_mw # discharged_amount = 0 # for j in range(points_per_hour): # # lack or excess renewable supply # if gap > 0: #discharging from battery # discharged_amount += b.discharge(gap, 1/points_per_hour) # else: # charging the battery # b.charge(-gap, 1/points_per_hour) # df_ren[i] += gap * (1 / points_per_hour) # decrease the available renewable energy # if gap > 0: # tot_non_ren_mw = tot_non_ren_mw + gap - discharged_amount # df_ren[i] += discharged_amount # increase the renewables available by the discharged # return tot_non_ren_mw, df_ren