Battery and Solar Investment#
1. Load packages
We are using the gurobipy package to formulate a mathematical model and solve it.
import pandas as pd
import numpy as np
import gurobipy as gp
from gurobipy import GRB
import matplotlib.pyplot as plt
import plotly.express as px
2. Load and inspect data
# Data source:
# https://www.eia.gov/consumption/commercial/
# actual 1-year data for a commercial building in NJ
# solar data modified from PECAN streeet
# https://www.pecanstreet.org/dataport/
# load data
building_data_file = 'building1h.csv'
building_df = pd.read_csv(building_data_file)
elec = building_df['elec'].to_numpy() # in kWh
solar = building_df['solar'].to_numpy() # relative avalability [1]
is_peak = building_df['is_peak'].to_numpy() # binary
building_df.head()
hour | is_peak | elec | solar | |
---|---|---|---|---|
0 | 0 | 0 | 1.4369 | 0.0 |
1 | 1 | 0 | 1.3840 | 0.0 |
2 | 2 | 0 | 1.4896 | 0.0 |
3 | 3 | 0 | 1.5717 | 0.0 |
4 | 4 | 0 | 2.2712 | 0.0 |
# inspect electricity data
fig = px.line(elec)
fig.update_layout(
width=900,
height=600,
xaxis_title="15-min timestep",
yaxis_title="kWh"
)
fig.show()
# inspect solar data
fig = px.line(solar)
fig.update_layout(
width=900,
height=600,
xaxis_title="15-min timestep",
yaxis_title="solar availability [1]"
)
fig.update_traces(line=dict(color='orange'))
fig.show()
3. Define Parameters
Parameter |
Parameter |
Value |
Unit |
---|---|---|---|
\(C^{pv}\) |
Cost of solar |
2,750 |
$/kW |
\(C^{bat}\) |
Cost for battery |
700 |
$/kWh |
\(p^{pv,max}\) |
Maximum space for solar |
20 |
kW |
\(B\) |
Budget |
50,000 |
$ |
\(C^{off}\) |
Off peak price |
0.1 |
$/kWh |
\(C^{peak}\) |
Peak price |
0.24 |
$/kWh |
\(f\) |
grid_fee |
0.08 |
$/kWh |
# cost for solar: $2,750 per kW
# maximum space for solar: 20kW
# cost for battery: $700 per kWh (capacity, 2h storage)
# we assume that batteries can be installed continously
# we assume that batteries are ideal
# total budget:
# write parameters
n_t = len(elec)
cost_solar = 2750 # per kW
cost_battery = 700 # per kWh
budget = 50000 # $
off_peak = 0.1 # $/kWh
peak = 0.24 # $/kWh
solar_max = 20 #kW
grid_fee = 0.08 # $/kWh
4. Mathematical Model
Objective function:
Optimizing the investment in solar and storage capacity to minimize the grid tariff cost in the objective function.
Parameters:
\(C^{\rm bat}\) investment cost of battery.
\(C^{\rm pv}\) investment cost of PV.
H battery capacity value.
\(A_t\) solar output in t given as a percentage.
Decision variables:
\(p_{t}^{\text{c}}\) production of conventional generator i in t.
\(p_{t}^{\text{w}}\) production of wind generator i in t.
\(p_{t}^{\text{pv}}\) production of PV i in t.
\(p^{grid}_{t}\) exchange with grid, negative is load
\(p^{solar}_t\) solar output in t
\(p^{ch}_t\) charging in t
\(p^{dis}_t\) discharging in t
\(e_t\) battery state of charge in t
Constraints:
Energy balance: power from the grid is equal to the disbalance between load PV production the load and charging/discharging of storage.
Solar output is equal to the installed capacity multiplied by the availability factor of time t.
Storage energy balance constraint: state of charge is equal to the previous state of charge and all charging/discharging decisions.
Storage capacity: the state of charge is limited by the battery storage capacity.
Charging and discharging are limited by the storage capacity multiplied by 1 over the battery capacity value.
Storage capacity is limited by the maximum possible storage capacity \(e^{ max}\).
PV capacity is limited by the maximum possible PV capacity \(p^{pv,max}\) .
Budget constraint: the investments are limited by the budget.
# build model
m = gp.Model()
m.setParam("OutputFlag", 0)
# variables
p_grid = m.addVars(n_t, lb=-GRB.INFINITY, ub=GRB.INFINITY, name='p_grid') # exchange with grid, negative is load
p_solar = m.addVars(n_t, lb=0, ub=GRB.INFINITY, name='p_solar')
p_charge = m.addVars(n_t, lb=0, ub=GRB.INFINITY, name='p_charge')
p_discharge = m.addVars(n_t, lb=0, ub=GRB.INFINITY, name='p_discharge')
e_bat = m.addVars(n_t, lb=0, ub=GRB.INFINITY, name='e_bat')
c_solar = m.addVar(lb=0, ub=GRB.INFINITY, name='c_solar')
c_battery = m.addVar(lb=0, ub=GRB.INFINITY, name='c_battery')
p_grid_pos = m.addVars(n_t, lb=0, ub=GRB.INFINITY, name='p_grid_pos')
# constraints
m.addConstrs((p_grid[t] == p_solar[t] - elec[t] - p_charge[t] + p_discharge[t] for t in range(n_t)))
# solar capacity
m.addConstrs((p_solar[t] == c_solar*solar[t] for t in range(n_t)))
m.addConstr(c_solar <= solar_max)
# battery capacity and constraints
m.addConstrs((e_bat[t+1] == e_bat[t] + p_charge[t] - p_discharge[t] for t in range(n_t-1)))
m.addConstr(e_bat[0] == 0)
m.addConstrs(e_bat[t] <= c_battery for t in range(n_t))
m.addConstrs(p_charge[t] <= 0.5*c_battery for t in range(n_t)) # 2h storage
m.addConstrs(p_discharge[t] <= 0.5*c_battery for t in range(n_t))
# budget constraint
investment_cost = c_battery*cost_battery + c_solar*cost_solar
m.addConstr(investment_cost <= budget)
# yearly energy cost
energy_cost = sum(-p_grid[t]*(off_peak*(1-is_peak[t]) + peak*is_peak[t]) for t in range(n_t))
m.addConstrs(p_grid_pos[t] >= p_grid[t] for t in range(n_t))
feed_in_cost = sum(p_grid_pos[t] * grid_fee for t in range(n_t))
# set_objective
m.setObjective((energy_cost + feed_in_cost)*15 + investment_cost, GRB.MINIMIZE)
# run
m.optimize()
print(f"Optimal Battery capacity: {c_battery.X:.1f} kWh")
print(f"Optimal Solar capacity: {c_solar.X:.1f} kW")
total_cost = energy_cost.getValue() + feed_in_cost.getValue()
print(f"Total cost of opertation: {total_cost:.2f} $")
Set parameter Username
Academic license - for non-commercial use only - expires 2025-11-15
Optimal Battery capacity: 8.8 kWh
Optimal Solar capacity: 15.9 kW
Total cost of opertation: 4517.91 $
pgrid_res = [p_grid[t].X for t in range(n_t)]
fig = px.line(pgrid_res)
fig.update_layout(
width=900,
height=600,
)
fig.show()