Skip to content

Commit b6ae4db

Browse files
committed
Implement initial storage support (pvlib#1333)
1 parent b3c1b5e commit b6ae4db

File tree

4 files changed

+300
-0
lines changed

4 files changed

+300
-0
lines changed

docs/sphinx/source/user_guide/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ User Guide
1414
timetimezones
1515
clearsky
1616
forecasts
17+
storage
1718
comparison_pvlib_matlab
1819
variables_style_rules
1920
singlediode
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
.. _storage:
2+
3+
Storage
4+
=======
5+
6+
Storage is a way of transforming energy that is available at a given instant,
7+
for use at a later time. The way in which this energy is stored can vary
8+
depending on the storage technology. It can be potential energy, heat or
9+
chemical, among others. Storage is literally everywhere, in small electronic
10+
devices like mobile phones or laptops, to electric vehicles, to huge dams.
11+
12+
While humanity shifts from fossil fuels to renewable energy, it faces the
13+
challenges of integrating more energy sources that are not always stable nor
14+
predictable. The energy transition requires not only to replace current
15+
electricity generation plants to be renewable, but also to replace heating
16+
systems and combustion engines in the whole transportation sector to be
17+
electric.
18+
19+
Unfortunately, electrical energy cannot easily be stored so, if electricity is
20+
becoming the main way of generating and consuming energy, energy storage
21+
systems need to be capable of storing the excess electrical energy that is
22+
produced when the generation is higher than the demand. Storage sysems need to
23+
be:
24+
25+
- Efficient: in the round-trip conversion from electrical to other type of
26+
energy, and back to electrical again.
27+
28+
- Capable: to store large quantities of energy.
29+
30+
- Durable: to last longer.
31+
32+
There are many specific use cases in which storage can be beneficial. In all of
33+
them the underlying effect is the same: to make the grid more stable and
34+
predictable.
35+
36+
Some use cases are not necessarily always coupled with PV. For instance, with
37+
power peak shaving, storage can be fed from the grid without a renewable energy
38+
source directly connected to the system. Other use cases, however, are tightly
39+
coupled with PV and hence, are of high interest for this project.
40+
41+
Power versus energy
42+
-------------------
43+
44+
Module and inverter models in pvlib compute the power generation (DC and AC
45+
respectively). This means the computation happens as an instant, without taking
46+
into account the previous state nor the duration of the calculated power. It
47+
is, in general, the way most models work in pvlib, with the exception of some
48+
cell temperature models. It also means that time, or timestamps associated to
49+
the power values, are not taken into consideration for the calculations.
50+
51+
When dealing with storage systems, state plays a fundamental role. Degradation
52+
and parameters like the state of charge (SOC) greatly affect how the systems
53+
operates. This means that power computation is not sufficient. Energy is what
54+
really matters and, in order to compute energy, time needs to be well defined.
55+
56+
Conventions
57+
***********
58+
59+
In order to work with time series pvlib relies on pandas and pytz to handle
60+
time and time zones. See "Time and time zones" section for a brief
61+
introduction.
62+
63+
Also, when dealing with storage systems and energy flow, you need to take into
64+
account the following conventions:
65+
66+
- Timestamps are associated to the beginning of the interval
67+
68+
- The time series frequency needs to be well defined in the time series
69+
70+
- Values represent power throughout the interval, in W
71+
72+
- Positive values represent power provided by the storage system (i.e.:
73+
discharging), hence negative values represent power into the storage system
74+
(i.e.: charging) NOTE: should we use values representing the energy instead?
75+
sounds more intuitive to me but if NREL chose to use power instead there may
76+
be a good reason
77+
78+
The left-labelling of the bins can be in conflict with energy meter series data
79+
provided by the electricity retailer companies, where the timestamp represents
80+
the time of the reading (the end of the interval). However, using labels at the
81+
beginning of the interval eases typical group operations with time series like
82+
resampling, where Pandas will assume by default that the label is at the
83+
beginning of the interval.
84+
85+
As an example, here you can see 15-minutes-period time series representing 1000
86+
W power throughout all the periods:
87+
88+
.. ipython:: python
89+
90+
from pandas import date_range
91+
from pandas import Series
92+
93+
index = date_range(
94+
"2022-01",
95+
"2022-02",
96+
closed="left",
97+
freq="15T",
98+
tz="Europe/Madrid",
99+
)
100+
power = Series(1000.0, index=index)
101+
power[0:2]
102+
103+
Energy series
104+
*************
105+
106+
You can convert the power series into energy series very easily:
107+
108+
.. ipython:: python
109+
110+
from pvlib.battery import power_to_energy
111+
112+
energy = power_to_energy(power)
113+
energy[0:2]
114+
115+
And just as easily, you can resample the energy series to use a different
116+
period thanks to Pandas built-in methods:
117+
118+
.. ipython:: python
119+
120+
daily = energy.resample("D").sum()
121+
daily[0:2]
122+
123+
Batteries
124+
---------
125+
126+
TODO
127+
128+
Energy flow
129+
-----------
130+
131+
TODO

pvlib/battery.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
"""
2+
This module contains functions for modeling batteries.
3+
4+
TODO:
5+
6+
- boc() # Bag of coulombs
7+
- fit_boc_datasheet()
8+
- battwatts() # Simplified battery storage model
9+
- fit_battwatts_datasheet()
10+
- sam_stateful()
11+
- fit_sam_stateful_datasheet()
12+
"""
13+
from pandas import DataFrame
14+
15+
16+
def power_to_energy(power):
17+
'''
18+
Converts a power series to an energy series.
19+
20+
Assuming Watts as the input power unit, the output energy unit will be
21+
Watt Hours.
22+
23+
Parameters
24+
----------
25+
power : Series
26+
The input power series. [W]
27+
28+
Returns
29+
-------
30+
The converted energy Series. [Wh]
31+
'''
32+
if power.index.freq.name == "H":
33+
return power * power.index.freq.n
34+
if power.index.freq.name == "T":
35+
return power * power.index.freq.n / 60
36+
raise ValueError("Unsupported frequency {}".format(power.index.freq))
37+
38+
39+
def fit_sam_datasheet(datasheet):
40+
'''
41+
Determine the SAM BatteryStateful model mathing the given characteristics.
42+
43+
Parameters
44+
----------
45+
datasheet : dict_like
46+
The datasheet parameters of the battery.
47+
48+
Returns
49+
-------
50+
object
51+
The SAM BatteryStateful object.
52+
53+
Notes
54+
-----
55+
This function does not really perform a fitting procedure. Instead, it just
56+
calculates the model parameters that match the provided informatio from a
57+
datasheet.
58+
'''
59+
try:
60+
from PySAM.BatteryStateful import default
61+
from PySAM.BatteryTools import battery_model_sizing
62+
except ImportError:
63+
raise ImportError("Requires NREL's PySAM package at "
64+
"https://pypi.org/project/NREL-PySAM/.")
65+
datasheet = {
66+
"brand": "BYD",
67+
"model": "HVS 5.1",
68+
"width": 0.585,
69+
"height": 0.712,
70+
"depth": 0.298,
71+
"weight": 91,
72+
"chemistry": "LFP",
73+
"modules": 2,
74+
"energy_wh": 5120,
75+
"voltage": 204,
76+
"max_power_w": 5100,
77+
}
78+
# TODO: validate/normalize datasheet
79+
chemistry = {
80+
"LFP": "LFPGraphite",
81+
}
82+
model = default(chemistry[datasheet["chemistry"]])
83+
battery_model_sizing(
84+
model=model,
85+
desired_power=datasheet["max_power_w"] / 1000,
86+
desired_capacity=datasheet["energy_wh"] / 1000,
87+
desired_voltage=datasheet["voltage"],
88+
)
89+
model.ParamsCell.minimum_SOC = 10
90+
model.ParamsCell.maximum_SOC = 90
91+
model.ParamsCell.initial_SOC = 50
92+
return model.export()
93+
94+
95+
def sam(model, power):
96+
'''
97+
'''
98+
try:
99+
from PySAM.BatteryStateful import new
100+
except ImportError:
101+
raise ImportError("Requires NREL's PySAM package at "
102+
"https://pypi.org/project/NREL-PySAM/.")
103+
battery = new()
104+
battery.assign(model)
105+
if power.index.freq.name == "H":
106+
battery.Controls.dt_hr = power.index.freq.n
107+
elif power.index.freq.name == "T":
108+
battery.Controls.dt_hr = power.index.freq.n / 60
109+
battery.Controls.control_mode = 1
110+
battery.Controls.input_power = 0
111+
battery.setup()
112+
113+
states = []
114+
for p in power:
115+
battery.Controls.input_power = p
116+
battery.execute(0)
117+
states.append((battery.StatePack.P, battery.StatePack.SOC))
118+
119+
results = DataFrame(states, index=power.index, columns=["Power", "SOC"])
120+
export = battery.export()
121+
122+
return (export, results * 1000)

pvlib/tests/test_battery.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from pandas import Series
2+
from pandas import date_range
3+
from pytest import approx
4+
from pytest import mark
5+
from pytest import raises
6+
7+
from pvlib.battery import power_to_energy
8+
9+
@mark.parametrize("power_value,frequency,energy_value", [
10+
(1000, "H", 1000),
11+
(1000, "2H", 2000),
12+
(1000, "15T", 250),
13+
(1000, "60T", 1000),
14+
])
15+
def test_power_to_energy(power_value, frequency, energy_value):
16+
'''
17+
The function should be able to convert power to energy for different power
18+
series' frequencies.
19+
'''
20+
index = date_range(
21+
start="2022-01-01",
22+
periods=10,
23+
freq=frequency,
24+
tz="Europe/Madrid",
25+
closed="left",
26+
)
27+
power = Series(power_value, index=index)
28+
energy = power_to_energy(power)
29+
assert approx(energy) == energy_value
30+
31+
32+
def test_power_to_energy_unsupported_frequency():
33+
'''
34+
When the power series' frequency is unsupported, the function raises an
35+
exception.
36+
'''
37+
index = date_range(
38+
start="2022-01-01",
39+
periods=10,
40+
freq="1M",
41+
tz="Europe/Madrid",
42+
closed="left",
43+
)
44+
power = Series(1000, index=index)
45+
with raises(ValueError, match=r"Unsupported frequency") as error:
46+
power_to_energy(power)

0 commit comments

Comments
 (0)