A Computational Essay by Owen Dray, Rachel Rabayeva, and Isabel Serrato

1

In [1]:

# Configure Jupyter so figures appear in the notebook %matplotlib inline # Configure Jupyter to display the assigned value after an assignment %config InteractiveShell.ast_node_interactivity='last_expr_or_assign' # import functions from the modsim library from modsim import * import pandas as pd import numpy as np import matplotlib.pyplot as mp

3

The goal of this project was to determine the optimal battery capacity needed to store enough power to meet the electricity demand in the city of Boston. Our question refers to the design of a possible energy grid in the future. The model provide us with the optimal battery storage in order to compensate for the fluctuations in both power generation and demand over the course of a year. Boston will need to know the optimal amount of battery storage required to power the city if it wants to to become completely powered by renewable energy and meet its goal of being carbon neutral by 2050. Due to the variability of the most popular types of renewable energy, wind and solar, it is challenging to meet surge demands without an energy storage system. Battery storage helps supplement the grid by allowing production to remain lower than demand, as excess is stored for the future rather than wasted.

4

Our model is based loosely off of the Olin-Wellesley bikeshare model, as we are expanding off of a transfer system. It will create a power transfer system by routing power from a renewable energy producer and direct it to either the city of Boston or a battery storage system, which can then route energy to the city if the production is less than demanded.

5

In [2]:

""" energyCon is base energy consumed batterySize is number of kwH per battery bastteryCost is the price of 1 battery This is the system that we are functioning in """ batterySystem = System(energyCon=2, batterySize=210, batteryCost=398)

6

values | |
---|---|

energyCon | 2 |

batterySize | 210 |

batteryCost | 398 |

In [3]:

""" t is time in months failure is the number of times the demand exceeds power production and storage batteryCharge1 is the charge of the battery This is the state of the system """ powerstate = State(t=1, failure=0, batteryCharge1=1000)

7

values | |
---|---|

t | 1 |

failure | 0 |

batteryCharge1 | 1000 |

We obtained access to the city of Boston's utility data over the last 8 years organized per building per month and used it to create gaussian predictions of energy consumption per month in 2018. We are assuming that the typical standard deviation of consumption is within ten percent of the median in order to account for fluctuations in demand.

8

In [4]:

def demand(t): #demand of electricity for the city per months in kWh, standard deviation assumed to be ~10%. Data is from actual Boston consumption averages in 2018 pDemand = 0 if(t==1): pDemand = np.random.normal(1.37 * 10**7, 1.37 * 10**6) elif(t==2): pDemand = np.random.normal(1.04 * 10**7, 1.04 * 10**6) elif(t==3): pDemand = np.random.normal(1.24 * 10**7, 1.24 * 10**6) elif(t==4): pDemand = np.random.normal(9.69 * 10**6, 9.69 * 10**5) elif(t==5): pDemand = np.random.normal(1.18 * 10**7, 1.18 * 10**6) elif(t==6): pDemand = np.random.normal(1.19 * 10**7, 1.19 * 10**6) elif(t==7): pDemand = np.random.normal(1.31 * 10**7, 1.31 * 10**6) elif(t==8): pDemand = np.random.normal(1.37 * 10**7, 1.37 * 10**6) elif(t==9): pDemand = np.random.normal(1.40 * 10**7, 1.40 * 10**6) elif(t==10): pDemand = np.random.normal(1.26 * 10**7, 1.26 * 10**6) elif(t==11): pDemand = np.random.normal(1.2 * 10**7, 1.2 * 10**6) elif(t==12): pDemand = np.random.normal(1.18 * 10**7, 1.18 * 10**6) return(pDemand) demand(2)

9

10923502.896620696

We then found Boston's average weather data and used it to estimate the levels of production of electricity between months. Due to the fact that Massachusetts has a goal to be carbon neutral by 2050, we assumed that the electricity production in the future would be entirely renewable and, on average, be roughly equal to energy demand. We then organized months in order of their predicted energy production by factoring in the average amount of sun and wind per month using the weather data, weighting months with a higher percentage of sunny days as higher producers due to the fact that Massachusetts uses more solar farms than wind farms. As for the energy production values, we kept the medians in a range of slightly over the maximum energy demand to slightly below the minimum energy demand, again assuming that the production also varies by ten percent of the median.

10

In [5]:

def produce_electricity(t): # how much power is being produced by the grid renewable = 0 if(t==1): #t=1=January renewable = np.random.normal(1.25*10**7,1.25*10**6) elif(t==2): renewable = np.random.normal(1.2*10**7, 1.2*10**6) elif(t==3): renewable = np.random.normal(1.1*10**7,1.1*10**6) elif(t==4): renewable = np.random.normal(1.15*10**7,1.15*10**6) elif(t==5): renewable = np.random.normal(1*10**7,1*10**6) elif(t==6): renewable = np.random.normal(1.3*10**7,1.3*10**6) elif(t==7): renewable = np.random.normal(1.4*10**7,1.4*10**6) elif(t==8): renewable = np.random.normal(1.5*10**7,1.5*10**6) elif(t==9): renewable = np.random.normal(1.6*10**7,1.6*10**6) elif(t==10): renewable = np.random.normal(1.55*10**7,1.55*10**6) elif(t==11): renewable = np.random.normal(1.45*10**7,1.45*10**6) elif(t==12): renewable = np.random.normal(1.35*10**7,1.35*10**6) #this generates a gaussian distribution of values with a mean of 7 and a standard deviation of 2 return(renewable) produce_electricity(2)

11

13806496.458680872

In [6]:

def pdifference(t): #calculates the difference between electricity demanded and electricity produced pdifference=produce_electricity(t)-demand(t) return(pdifference) #print (produce_electricity(3)) #print (demand(4)) pdifference(4)

12

1491616.1907780347

In [7]:

def currentcap(tbatteryCap, t): pdifference2=pdifference(t) if (tbatteryCap > powerstate.batteryCharge1): #If the battery isn't full, add surplus or subtract required additional demand #print ("First") powerstate.batteryCharge1 = powerstate.batteryCharge1 + pdifference2 else: if (pdifference2<0): #If the battery if full and electricity is demanded, subtract the amount demanded #print ("Second") powerstate.batteryCharge1= powerstate.batteryCharge1 + pdifference2 else: #If the battery is full and there is a surplus, do nothing #print ("Third") powerstate.batteryCharge1 = tbatteryCap if (powerstate.batteryCharge1>tbatteryCap): #If the battery is charged above the over capacity, set it to the capactity powerstate.batteryCharge1=tbatteryCap if (powerstate.batteryCharge1<0): #if the battery charge is negative, set it to 0 powerstate.batteryCharge1=0 powerstate.failure +=1 # print (powerstate.batteryCharge1) # print (tbatteryCap) # print (pdifference2) return(powerstate.batteryCharge1)

13

As the simulation runs, the metrics recorded are the number of failures and battery charge at a given month. The model sweeps through a total battery capacity of 1,000 kWh to 10,000,000 kWh with 51 divisions tested. The model averages the number of times demand exceeds the power production and storage over 1000 repititions as a metric to determine how successful a particular amount of storage is. By varying the total battery capacity used in the model, we are able to determine the minimum battery capacity required to store enough renewable energy to power the city of Boston with the lowest reasonable number of failures. The model also creates a graph of the charge of each battery over a single year as a visual representation of how the model runs.

14

In [8]:

#Run simulation takes a system variable and sweeps through a year of energy production with a different number of batteries. It then plots the charge of the battery over a year. def runSimulation(system,tbatteryCap): #the current state of the system # t is the month of the year, failure is the number of times the power grid can't supply enough power, and batterCharge is the current amount of power stored in the battery totalfail = 0 for i in range(1000): results=TimeSeries() failure=TimeSeries() t=0 powerstate.batteryCharge1=0 powerstate.failure=0 results[0] = 0 # failure[0]=0 for t in linrange(1, 12): results[t+1] = currentcap(tbatteryCap,t) #print(results[t+1]) #print ("-------") totalfail+=powerstate.failure return (results,totalfail)

15

In [9]:

def model(): mp.figure(figsize=(15,15)) for tbatteryCap in linspace(1000,10000000,10): results,failure = runSimulation(batterySystem,tbatteryCap) mp.plot(results,label=('Battery Cap:'+ str(tbatteryCap))) decorate (title='Battery Storage over time for different battery capacities', xlabel='Time (months)', ylabel='Power (kWh)') print(tbatteryCap) print(failure/1000) print('------') mp.legend(fontsize='x-small')

16

In [10]:

model();

17

1000.0
3.807
------
1112000.0
3.077
------
2223000.0
2.419
------
3334000.0
2.149
------
4445000.0
2.05
------

The results indicate that there are generally fewer failures associated with higher total battery capacities. Any battery capacity between 3.8 million kWh and 10 million kWh will have on average two failures, as opposed to three to four as associated with other capacities. Once the the model sweeps capacities past 3.8 million kilowatt hours, the number of average failures remains roughly constant at around two per year.

18

Therefore, the optimal battery capacity for Boston is 3.8 million kilowatt hours, as any battery capacity larger than that has the additional cost of requiring higher storage and no additional benefits regarding the number of failures.

19

Due to the stochastic nature of our model, the optimal battery capacity required may vary slightly between runs of the simulation, but by running the simulation repeatedly we are be able to find the battery capacity that most often meets the demand of the system. One of the most impactful limitations to our model is the energy production data. The consumption data comes from actual Boston consumption data, whereas the production data is estimated based off of a combination of consumption data and weather data and is assumed to come from 100% renewable sources. However, due to disparities in the data, our results are not accurate for every energy system. The production data and the demand data are subject to change in the next 1000 years that the model runs, and as such cannot be entirely accurate. Due to the fact that the average energy produced is not based on historical data, there is no way of ensuring that the results are entirely accurate. Using different electricity demand data or electricity produced data would greatly affect the results of the model. The model is also limited in the fact that the data used to determine demand is over the interval of a full month and effectively does not include the possibilities that some days or even hours the power demand will be much higher than the average demand. Additionally, the energy production and the amount of power stored in the batteries will likely vary much more than our model represents, as their values can change far more drastically in smaller time periods to produce a steady monthly average. The limit at two failures also indicates that there are two months where production is significantly lower and consumption is significantly higher, in addition to a limited surplus being created in the months prior. This could also indicate that production as a whole needs to be slightly higher, but varying production is currently beyond the scope of the project.

20