Skip to content

Commit 6458257

Browse files
committed
fixup! Implement initial storage support
1 parent ca2d908 commit 6458257

File tree

3 files changed

+114
-28
lines changed

3 files changed

+114
-28
lines changed

docs/sphinx/source/user_guide/storage.rst

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,7 @@ account the following conventions:
7474

7575
- Positive values represent power provided by the storage system (i.e.:
7676
discharging), hence negative values represent power into the storage system
77-
(i.e.: charging) NOTE: should we use values representing the energy instead?
78-
sounds more intuitive to me but if NREL chose to use power instead there may
79-
be a good reason
77+
(i.e.: charging)
8078

8179
.. note:: The left-labelling of the bins can be in conflict with energy meter
8280
series data provided by the electricity retailer companies, where the
@@ -307,24 +305,22 @@ self-consumption use case:
307305
308306
from pvlib.powerflow import self_consumption
309307
310-
power_flow = self_consumption(generation, load)
311-
power_flow.head()
308+
self_consumption_flow = self_consumption(generation, load)
309+
self_consumption_flow.head()
312310
313311
314312
The function will return the power flow series from system to load/grid and
315313
from grid to load/system:
316314

317315
.. ipython:: python
318316
319-
from pvlib.powerflow import self_consumption
320-
321317
@savefig power_flow_self_consumption_load.png
322-
power_flow.groupby(power_flow.index.hour).mean()[["System to load", "Grid to load"]].plot.bar(stacked=True, xlabel="Hour", ylabel="Power (W)", title="Average power flow to load")
318+
self_consumption_flow.groupby(self_consumption_flow.index.hour).mean()[["System to load", "Grid to load"]].plot.bar(stacked=True, xlabel="Hour", ylabel="Power (W)", title="Average power flow to load")
323319
@suppress
324320
plt.close()
325321
326322
@savefig power_flow_self_consumption_system.png
327-
power_flow.groupby(power_flow.index.hour).mean()[["System to load", "System to grid"]].plot.bar(stacked=True, xlabel="Hour", ylabel="Power (W)", title="Average system power flow")
323+
self_consumption_flow.groupby(self_consumption_flow.index.hour).mean()[["System to load", "System to grid"]].plot.bar(stacked=True, xlabel="Hour", ylabel="Power (W)", title="Average system power flow")
328324
@suppress
329325
plt.close()
330326
@@ -350,34 +346,58 @@ following assumptions:
350346
- The load is provided with power from the system, when possible
351347

352348
- When the system is unable to provide sufficient power to the load, the
353-
battery will try to fill the load requirements
349+
battery may try to fill the load requirements, if the dispatching activates
350+
the discharge
354351

355352
- When both the system and the battery are unable to provide sufficient power
356353
to the load, the grid will fill the load requirements
357354

358-
- When the system produces more power than the required by the load, it will be
359-
fed to the battery
355+
- When the system produces more power than the required by the load, it may be
356+
fed to the battery, if the dispatching activates the charge
360357

361-
- When the excess power cannot be fed into the battery, it will be fed into the
362-
grid
358+
- When the excess power from the system (after feeding the load) is not fed
359+
into the battery, it will be fed into the grid
363360

364361
- The grid will provide power to the system if required (i.e.: during night
365362
hours)
366363

367-
For this use case, you need to provide, appart from the well-known generation
368-
and load profiles, the battery parameters and battery model simulation
369-
function:
364+
For this use case, you need to start with the self-consumption power flow
365+
solution:
366+
367+
.. ipython:: python
368+
369+
from pvlib.powerflow import self_consumption
370+
371+
self_consumption_flow = self_consumption(generation, load)
372+
self_consumption_flow.head()
373+
374+
375+
Then you need to provide a dispatch series, which could easily be defined so
376+
that the battery always charges from the excess energy by the system and always
377+
discharges when the load requires energy from the grid:
378+
379+
.. ipython:: python
380+
381+
dispatch = self_consumption_flow["Grid to load"] - self_consumption_flow["System to grid"]
382+
383+
384+
.. note:: Note how the positive values represent power provided by the storage
385+
system (i.e.: discharging) while negative values represent power into the
386+
storage system (i.e.: charging)
387+
388+
389+
The last step is to use the self-consumption power flow solution and the
390+
dispatch series to solve the new power flow scenario:
370391

371392
.. ipython:: python
372393
373-
from pvlib.powerflow import self_consumption_ac_battery
394+
from pvlib.powerflow import self_consumption_ac_battery_custom_dispatch
374395
375396
battery = fit_sam(parameters)
376-
state, flow = self_consumption_ac_battery(generation, load, battery, sam)
377-
flow.head()
397+
state, flow = self_consumption_ac_battery_custom_dispatch(self_consumption_flow, dispatch, battery, sam)
378398
379399
380-
The function will return the power flow series from system to
400+
The new power flow results now include the flow series from system to
381401
load/battery/grid, from battery to load and from grid to load/system:
382402

383403
.. ipython:: python
@@ -393,16 +413,28 @@ load/battery/grid, from battery to load and from grid to load/system:
393413
plt.close()
394414
395415
396-
The returned ``state`` represents the state of the battery after running the
397-
simulation. You can use this state to call
398-
:py:func:`~pvlib.flow.self_consumption_ac_battery`.
399-
400-
401416
.. note:: The :py:func:`~pvlib.flow.self_consumption_ac_battery` function
402417
allows you to define the AC-DC losses, if you would rather avoid the default
403418
values.
404419

405420

421+
While the self-consumption with AC-connected battery use case imposes many
422+
restrictions to the power flow, it still allows some flexibility to decide when
423+
to allow charging and discharging. If you wanted to simulate a use case where
424+
discharging should be avoided from 00:00 to 08:00, you could do that by simply:
425+
426+
.. ipython:: python
427+
428+
dispatch = self_consumption_flow["Grid to load"] - self_consumption_flow["System to grid"]
429+
dispatch.loc[dispatch.index.hour < 8] = 0
430+
state, flow = self_consumption_ac_battery_custom_dispatch(self_consumption_flow, dispatch, battery, sam)
431+
432+
@savefig flow_self_consumption_ac_battery_load_custom_dispatch_restricted.png
433+
flow.groupby(flow.index.hour).mean()[["System to load", "Battery to load", "Grid to load"]].plot.bar(stacked=True, legend=True, xlabel="Hour", ylabel="Power (W)", title="Average power flow to load")
434+
@suppress
435+
plt.close()
436+
437+
406438
Energy flow
407439
-----------
408440

pvlib/powerflow.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,57 @@ def self_consumption_ac_battery(
8585
axis=1, skipna=False
8686
)
8787
return final_state, df
88+
89+
90+
def self_consumption_ac_battery_custom_dispatch(
91+
df, dispatch, battery, model, ac_dc_loss=4, dc_ac_loss=4
92+
):
93+
"""
94+
Calculate the power flow for a self-consumption use case with an
95+
AC-connected battery and a custom dispatch series. It assumes the system is
96+
connected to the grid.
97+
98+
Parameters
99+
----------
100+
df : DataFrame
101+
The self-consumption power flow solution. [W]
102+
dispatch : Series
103+
The battery model to use.
104+
battery : dict
105+
The battery parameters.
106+
model : str
107+
The battery model to use.
108+
ac_dc_loss : float
109+
The fixed loss when converting AC to DC (i.e.: charging). [%]
110+
dc_ac_loss : float
111+
The fixed loss when converting DC to AC (i.e.: discharging). [%]
112+
113+
Returns
114+
-------
115+
DataFrame
116+
The resulting power flow provided by the system, the grid and the
117+
battery into the system, grid, battery and load. [W]
118+
"""
119+
# TODO: should we move this to the battery model? (i.e.: the dispatch
120+
# series adjustment and the conversion losses
121+
ac_dc_efficiency = 1 - ac_dc_loss / 100
122+
dc_ac_efficiency = 1 - dc_ac_loss / 100
123+
dc_dispatch = dispatch.copy()
124+
dc_dispatch.loc[dispatch < 0] *= dc_ac_efficiency
125+
dc_dispatch.loc[dispatch > 0] /= ac_dc_efficiency
126+
final_state, results = model(battery, dc_dispatch)
127+
df = df.copy()
128+
df["System to battery"] = -results["Power"]
129+
df["System to battery"].loc[df["System to battery"] < 0] = 0.0
130+
df["System to battery"] /= ac_dc_efficiency
131+
df["System to battery"] = df[["System to battery", "System to grid"]].min(axis=1)
132+
df["System to grid"] -= df["System to battery"]
133+
df["Battery to load"] = results["Power"]
134+
df["Battery to load"].loc[df["Battery to load"] < 0] = 0.0
135+
df["Battery to load"] *= dc_ac_efficiency
136+
df["Battery to load"] = df[["Battery to load", "Grid to load"]].min(axis=1)
137+
df["Grid to load"] -= df["Battery to load"]
138+
df["Grid"] = df[["Grid to system", "Grid to load"]].sum(
139+
axis=1, skipna=False
140+
)
141+
return final_state, df

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,13 @@
5353
'requests-mock', 'pytest-timeout', 'pytest-rerunfailures',
5454
'pytest-remotedata']
5555
EXTRAS_REQUIRE = {
56-
'optional': ['cython', 'ephem', 'netcdf4', 'nrel-pysam >= 3.0', 'numba',
56+
'optional': ['cython', 'ephem', 'netcdf4', 'nrel-pysam >= 3.0.1', 'numba',
5757
'pvfactors', 'siphon', 'statsmodels',
5858
'cftime >= 1.1.1'],
5959
'doc': ['ipython', 'matplotlib', 'sphinx == 3.1.2',
6060
'pydata-sphinx-theme', 'sphinx-gallery', 'docutils == 0.15.2',
6161
'pillow', 'netcdf4', 'siphon',
62-
'sphinx-toggleprompt >= 0.0.5', 'nrel-pysam >= 3.0'],
62+
'sphinx-toggleprompt >= 0.0.5', 'nrel-pysam >= 3.0.1'],
6363
'test': TESTS_REQUIRE
6464
}
6565
EXTRAS_REQUIRE['all'] = sorted(set(sum(EXTRAS_REQUIRE.values(), [])))

0 commit comments

Comments
 (0)