Analysis of Demand-side Storage Model

This notebook executes several analyses to assess the feasibility of purchasing batteries for electricity storage. Annual electricity costs are reduced by opting into a time-of-use pricing plan and purchasing electricity during less expensive off-peak hours.

Created by Justin Elszasz.

In [1]:
# Full descriptions of these models can be found in the docstrings for each module.

#TOU_pricing.py contains functions that determine the season, period (peak, off-peak, intermediate), 
#and cost of electricity for each hour of a timestampd dataframe containing hourly residential 
#electricity consumption.
import TOU_pricing


#storage_logic.py contains functions that implement a control algorithm for the purchase of electricity
#based on available storage and period of the day (peak, off-peak). 
import storage_logic

#calculations.py contains functions which return key metrics for analyzing the model output.
import calculations

import pandas as pd
import numpy as np
import time
/Users/Justin/Library/Enthought/Canopy_64bit/User/lib/python2.7/site-packages/pandas/io/excel.py:626: UserWarning: Installed openpyxl is not supported at this time. Use >=1.6.1 and <2.0.0.
  .format(openpyxl_compat.start_ver, openpyxl_compat.stop_ver))
In [8]:
# Reload modules during development
reload(TOU_pricing)
reload(storage_logic)
reload(calculations)
Out[8]:
<module 'calculations' from 'calculations.pyc'>

Battery Size Analysis

This section analyzes the performance of the model based on the size of batteries in the system in kilowatt-hours.

Define efficiencies of inverter and battery.

In [9]:
def inverter_efficiency(direction):
    if direction == 'charging':
        eff = .85
    elif direction == 'discharging':
        eff = .85
    return eff

def battery_efficiency(direction):
    if direction == 'charging':
        eff = .85
    elif direction == 'discharging':
        eff = .85
    return eff

Create array of all the battery sizes to be analyzed.

In [10]:
battery_sizes = [1., 2., 5., 7.5, 10., 15., 20., 25., 30.]
max_dod = .2

Calculate the output and annual cost with normal (static) BGE electricity prices.

In [39]:
R_output = TOU_pricing.main('R',False)
R_annual_cost = np.sum(R_output['USAGE'] * R_output['cost'])
print R_annual_cost
729.88896351

This loop determines the model output for each battery size and the relevant metrics for each output. If save_results is True, a csv for each output (all hourly flows for one year) is saved for each battery size along with a csv containing the metrics for each battery size.

In [11]:
save_results = False

t0 = time.time()

for i, bat_size in enumerate(battery_sizes):

    system_param = {
        'Inverter Cost' : 1500., # ballpark
        'Storage Cost' : 200. / 1.2, # cost per kilowatt-hour based on link above
        'Storage Size' : bat_size,  # storage size in kilowatt-hours
        'Max Charge Rate' : bat_size / 8.,
        'Max DOD' : max_dod, # DOD
        'Bat Depleted' : max_dod * bat_size,
        'Inverter Efficiency' : inverter_efficiency,
        'Battery Efficiency' : battery_efficiency,
    }

    results = storage_logic.main(TOU_pricing.main('EV', False), system_param)
    filename = 'results/storage_size/output_%s_kWh.csv'%bat_size
    if save_results == True: results.to_csv(filename)
    
    metrics = calculations.calc_metrics(results, system_param)

    if i == 0: all_metrics = pd.DataFrame(index=battery_sizes, columns=metrics.keys()) 
        
    all_metrics['Total kWh Purchased'][bat_size] = metrics['Total kWh Purchased']
    all_metrics['% Purchased During Peak'][bat_size] = metrics['% Purchased During Peak']
    all_metrics['% Purchased During Off-Peak'][bat_size] =  metrics['% Purchased During Off-Peak']
    all_metrics['Hours on Battery Only'][bat_size] = metrics['Hours on Battery Only']
    all_metrics['% Peak Demand Battery'][bat_size] = metrics['% Peak Demand Battery']
    all_metrics['Hours Battery Full'][bat_size] = metrics['Hours Battery Full']
    all_metrics['Hours Battery Depleted'][bat_size] = metrics['Hours Battery Depleted']
    all_metrics['Annual System Eff'][bat_size] = metrics['Annual System Eff']
    all_metrics['Annual Var Cost'][bat_size] = metrics['Annual Var Cost']
    all_metrics['Initial Cost'][bat_size] = metrics['Initial Cost']
    all_metrics['Peak kWh Shaved'][bat_size] = metrics['Peak kWh Shaved']
    all_metrics['% Peak kWh Shaved'][bat_size] = metrics['% Peak kWh Shaved']
    all_metrics['PBP'][bat_size] = metrics['PBP']
    all_metrics['Annual Savings'][bat_size] = metrics['Annual Savings']
    all_metrics['% Annual Cost Savings'][bat_size] = metrics['% Annual Cost Savings']

if save_results == True: all_metrics.to_csv('results/storage_size/all_metrics_storage_size.csv')    
    
t1 = time.time()
print 'Time for model run:',round(t1-t0,1),'s'
Time for model run: 151.5 s
In [40]:
# We now have a Pandas DataFrame containing metrics that describe the system performance
# indexed by battery size.
all_metrics.head()
Out[40]:
PBP Annual Var Cost % Peak kWh Shaved Total kWh Purchased % Purchased During Off-Peak Peak kWh Shaved % Purchased During Peak % Annual Cost Savings % Peak Demand Battery Annual Savings Initial Cost Hours Battery Full Annual System Eff Hours Battery Depleted Hours on Battery Only
1.0 18.55998 640.09 0.1353311 8566.259 0.8183945 243.4825 0.1816055 0.123031 0.1957039 89.79896 1666.667 3914 0.9739731 2224 32
2.0 16.92688 621.58 0.2706621 8789.213 0.8507037 486.965 0.1492963 0.148391 0.367376 108.309 1833.333 3914 0.9492666 1799 457
5.0 14.52985 569.3 0.6551423 9423.424 0.9341582 1178.707 0.06584177 0.2200183 0.7265188 160.589 2333.333 1 0.8853795 795 1461
7.5 15.39935 551.31 0.8048847 9673.588 0.9637111 1448.118 0.03628891 0.2446659 0.8335197 178.579 2750 2 0.8624831 382 1874
10.0 17.0978 544.68 0.8771617 9792.536 0.9774312 1578.156 0.02256882 0.2537495 0.8975529 185.209 3166.667 1 0.8520066 248 2008

Simple Payback Period

Plot the simple payback period as a function of battery size.

In [178]:
save_fig = True

a = 6.
fig = plt.figure(figsize=[1.3*a,a])
ax = subplot(111)

# turn off square border around plot
ax.spines["top"].set_visible(False)  
ax.spines["bottom"].set_visible(False)  
ax.spines["right"].set_visible(False)  
ax.spines["left"].set_visible(False)

# ensure only ticks on bottom and left, not top and right (unnecessary)
#ax.get_xaxis().tick_bottom()  
#ax.get_yaxis().tick_left()  

all_metrics['PBP'].plot(grid='off',color='#FF3300',linewidth=2,marker='.',markersize=14)
title('Payback Period in Years',fontsize=16)
xlabel('Storage Size (kWh)',fontsize=14)
xticks(fontsize=14)
yticks(fontsize=14)

# turn off tick marks
plt.tick_params(axis="both", which="both", bottom="off", top="off",
                labelbottom="on", left="off", right="off", labelleft="on")  

ylabel('Years',fontsize=14)

if save_fig == True: fig.savefig('results/storage_size/PBP.png',bbox_inches='tight')

Cost Savings

Plot the cost savings ($ and %) compared to a normal BGE plan without time-of-use pricing.

In [177]:
save_fig = True

a = 6.
fig = plt.figure(figsize=[1.3*a,a])
ax1 = plt.subplot(111)


# Ticks
ticks_left = np.array(range(10,30,2))/100.
ticks_right_ymin = min(ticks_left) * R_annual_cost
ticks_right_ymax = max(ticks_left) * R_annual_cost
ticks_right = np.array(range(80,200,20))

# turn off square border around plot
ax1.spines["top"].set_visible(False)  
ax1.spines["bottom"].set_visible(False)  
ax1.spines["right"].set_visible(False)  
ax1.spines["left"].set_visible(False)

title('Annual Electricity Bill Cost Savings',fontsize=16)
xlabel('Storage Size (kWh)',fontsize=14)
xticks(fontsize=14)

ax1.plot(all_metrics.index, all_metrics['% Annual Cost Savings'],color='#FF3300',linewidth=2,marker='.',markersize=14)
ax1.set_ylabel('% Annual Savings', fontsize=14)

ax2 = ax1.twinx()
ax2.set_ylim(min(all_metrics['Annual Savings']), max(all_metrics['Annual Savings']))
ax2.set_yticklabels(ticks_right, fontsize=14)
ax2.set_ylabel('Annual Savings (USD)', fontsize=14)


# turn off ticks
ax1.tick_params(axis="both", which="both", bottom="off", top="off",
               labelbottom="on", left="off", right="off", labelleft="on",labelsize=14) 
ax2.tick_params(axis="y", which="both", bottom="off", top="off",
               labelbottom="on", left="off", right="off", labelleft="off",labelsize=14) 



if save_fig == True: fig.savefig('results/storage_size/cost_savings.png',bbox_inches='tight')

Peak Load Shaving

Number/percent of kilowatt-hours no longer purchased during peak hours compared to normal BGE cost plan and no storage.

In [243]:
save_fig = True

a = 6.
fig = plt.figure(figsize=[1.3*a,a])
ax1 = plt.subplot(111)

# Ticks

ticks_right = np.array(range(0,2000,200))

# turn off square border around plot
ax1.spines["top"].set_visible(False)  
ax1.spines["bottom"].set_visible(False)  
ax1.spines["right"].set_visible(False)  
ax1.spines["left"].set_visible(False)

title('Demand Shaved From Peak Hours',fontsize=16)
xlabel('Storage Size (kWh)',fontsize=14)
xticks(fontsize=14)

ax1.plot(all_metrics.index, all_metrics['% Peak kWh Shaved'],color='#FF3300',linewidth=2,marker='.',markersize=14)
ax1.set_ylabel('% Peak Demand Shaved', fontsize=14)
ax1.set_ylim(0,1.1)

ax2 = ax1.twinx()
ax2.set_yticks(ticks_right)
ax2.set_yticklabels(ticks_right, fontsize=14)
ax2.set_ylim(0,1.1*max(all_metrics['Peak kWh Shaved']))
ax2.set_ylabel('Peak Demand Shaved (kWh)', fontsize=14)

# turn off ticks
ax1.tick_params(axis="both", which="both", bottom="off", top="off",
               labelbottom="on", left="off", right="off", labelleft="on",labelsize=14) 
ax2.tick_params(axis="y", which="both", bottom="off", top="off",
               labelbottom="on", left="off", right="off", labelleft="off",labelsize=14) 

if save_fig == True: fig.savefig('results/storage_size/peak_shaved_stor_cap.png',bbox_inches='tight')

System Efficiency

Plot of total annual kilowatt-hours purchased and the resulting overall system efficiency.

In [247]:
save_fig = True

a = 6.
fig = plt.figure(figsize=[1.3*a,a])
ax1 = plt.subplot(111)

# Ticks
ticks_right = np.array(range(0,2000,200))

# turn off square border around plot
ax1.spines["top"].set_visible(False)  
ax1.spines["bottom"].set_visible(False)  
ax1.spines["right"].set_visible(False)  
ax1.spines["left"].set_visible(False)

title('Annual System Efficiency',fontsize=16)
xlabel('Storage Size (kWh)',fontsize=14)
xticks(fontsize=14)

ax1.plot(all_metrics.index, all_metrics['Total kWh Purchased'],color='#FF3300',linewidth=2,marker='.',markersize=14)
ax1.set_ylabel('Total kWh Purchased', fontsize=14, color='#FF3300')
#ax1.set_ylim(0,1.1)

ax2 = ax1.twinx()
ax2.plot(all_metrics.index, all_metrics['Annual System Eff'],color='#00CCFF',linewidth=2,marker='.',markersize=14)
#ax2.set_yticks([0,.25,.5,1])
#ax2.set_yticklabels(, fontsize=14)
ax2.set_ylim(0,1)
ax2.set_ylabel('System Efficiency', fontsize=14, color='#00CCFF')

# turn off ticks
ax1.tick_params(axis="both", which="both", bottom="off", top="off",
               labelbottom="on", left="off", right="off", labelleft="on",labelsize=14) 
ax2.tick_params(axis="y", which="both", bottom="off", top="off",
               labelbottom="on", left="off", right="off", labelleft="off",labelsize=14) 

if save_fig == True: fig.savefig('results/storage_size/sys_eff_stor_cap.png',bbox_inches='tight')