Skip to content

Commit c7026ee

Browse files
committed
fixup! Implement initial storage support
1 parent 520a4e7 commit c7026ee

File tree

5 files changed

+358
-17566
lines changed

5 files changed

+358
-17566
lines changed

docs/sphinx/source/user_guide/storage.rst

Lines changed: 169 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,81 @@ Power flow
253253

254254
With pvlib you can simulate power flow for different scenarios and use cases.
255255

256+
In order to start playing with these use cases, you can start off by creating a
257+
model chain containing the results of the PV generation for a particular
258+
location and configuration:
259+
260+
.. ipython:: python
261+
262+
from pvlib.iotools import get_pvgis_tmy
263+
from pvlib.location import Location
264+
from pvlib.modelchain import ModelChain
265+
from pvlib.pvsystem import Array
266+
from pvlib.pvsystem import FixedMount
267+
from pvlib.pvsystem import PVSystem
268+
from pvlib.pvsystem import retrieve_sam
269+
from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS as tmodel
270+
271+
def run_model_chain():
272+
name = 'Madrid'
273+
latitude = 40.31672645215922
274+
longitude = -3.674695061062714
275+
altitude = 603
276+
timezone = 'Europe/Madrid'
277+
module = retrieve_sam('SandiaMod')['Canadian_Solar_CS5P_220M___2009_']
278+
inverter = retrieve_sam('cecinverter')['Powercom__SLK_1500__240V_']
279+
weather = get_pvgis_tmy(latitude, longitude, map_variables=True)[0]
280+
weather.index = date_range(
281+
start="2021-01-01 00:00:00",
282+
end="2022-01-01 00:00:00",
283+
closed="left",
284+
freq="H",
285+
)
286+
weather.index.name = "Timestamp"
287+
location = Location(
288+
latitude,
289+
longitude,
290+
name=name,
291+
altitude=altitude,
292+
tz=timezone,
293+
)
294+
mount = FixedMount(surface_tilt=latitude, surface_azimuth=180)
295+
temperature_model_parameters = tmodel['sapm']['open_rack_glass_glass']
296+
array = Array(
297+
mount=mount,
298+
module_parameters=module,
299+
modules_per_string=16,
300+
strings=1,
301+
temperature_model_parameters=temperature_model_parameters,
302+
)
303+
system = PVSystem(arrays=[array], inverter_parameters=inverter)
304+
mc = ModelChain(system, location)
305+
mc.run_model(weather)
306+
return mc
307+
308+
mc = run_model_chain()
309+
310+
311+
And a syntethic load profile for the experiment:
312+
313+
.. ipython:: python
314+
315+
from numpy import nan
316+
from numpy.random import uniform
317+
318+
def create_synthetic_load_profile(index):
319+
load = Series(data=nan, index=index)
320+
load[load.index.hour == 0] = 600
321+
load[load.index.hour == 4] = 400
322+
load[load.index.hour == 13] = 1100
323+
load[load.index.hour == 17] = 800
324+
load[load.index.hour == 21] = 1300
325+
load *= uniform(low=0.6, high=1.4, size=len(load))
326+
return load.interpolate(method="spline", order=2)
327+
328+
load = create_synthetic_load_profile(mc.results.ac.index)
329+
330+
256331
Self consumption
257332
****************
258333

@@ -279,38 +354,14 @@ The self-consumption use case is defined with the following assumptions:
279354
- The grid will provide power to the system if required (i.e.: during night
280355
hours)
281356

282-
To simulate a system like this, you first need to start with the well-known PV
283-
system generation and load profiles:
284-
285-
.. ipython:: python
286-
287-
import pkgutil
288-
from io import BytesIO
289-
290-
from pandas import Series
291-
from pandas import read_csv
292-
from pandas import to_datetime
293-
294-
295-
def read_file(fname):
296-
df = read_csv(BytesIO(pkgutil.get_data("pvlib", fname)))
297-
df.columns = ["Timestamp", "Power"]
298-
df["Timestamp"] = to_datetime(df["Timestamp"], format="%Y-%m-%dT%H:%M:%S%z", utc=True)
299-
s = df.set_index("Timestamp")["Power"]
300-
s = s.asfreq("H")
301-
return s.tz_convert("Europe/Madrid")
302-
303-
generation = read_file("data/generated.csv")
304-
load = read_file("data/consumed.csv")
305-
306-
307-
You can use these profiles to solve the energy/power flow for the
357+
You can use the system and load profiles to solve the energy/power flow for the
308358
self-consumption use case:
309359

310360
.. ipython:: python
311361
312362
from pvlib.powerflow import self_consumption
313363
364+
generation = mc.results.ac
314365
self_consumption_flow = self_consumption(generation, load)
315366
self_consumption_flow.head()
316367
@@ -321,12 +372,7 @@ from grid to load/system:
321372
.. ipython:: python
322373
323374
@savefig power_flow_self_consumption_load.png
324-
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")
325-
@suppress
326-
plt.close()
327-
328-
@savefig power_flow_self_consumption_system.png
329-
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")
375+
self_consumption_flow.groupby(self_consumption_flow.index.hour).mean()[["System to load", "Grid to load", "System to grid"]].plot.bar(stacked=True, xlabel="Hour", ylabel="Power (W)", title="Average power flow")
330376
@suppress
331377
plt.close()
332378
@@ -412,28 +458,110 @@ load/battery/grid, from battery to load and from grid to load/system:
412458
.. ipython:: python
413459
414460
@savefig flow_self_consumption_ac_battery_load.png
415-
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")
461+
flow.groupby(flow.index.hour).mean()[["System to load", "Battery to load", "Grid to load", "System to battery", "System to grid"]].plot.bar(stacked=True, legend=True, xlabel="Hour", ylabel="Power (W)", title="Average power flow")
416462
@suppress
417463
plt.close()
418464
419-
@savefig flow_self_consumption_ac_battery_system.png
420-
flow.groupby(flow.index.hour).mean()[["System to load", "System to battery", "System to grid"]].plot.bar(stacked=True, legend=True, xlabel="Hour", ylabel="Power (W)", title="Average system power flow")
465+
466+
Self consumption with DC-connected battery
467+
******************************************
468+
469+
The self-consumption with DC-connected battery is a completely different use
470+
case. As opposed to the AC-connected battery use case, you cannot just take
471+
into account the system's AC ouput and the load. Instead, you need to note that
472+
the battery is connected to a single inverter, and the inverter imposes some
473+
restrictions on how the power can flow from PV and from the battery.
474+
475+
Hence, for this use case, you need to consider the PV power output (DC
476+
generation) and the inverter model as well in order to be able to calculate the
477+
inverter's output power (AC).
478+
479+
The restrictions are as follow:
480+
481+
- PV and battery are DC-connected to the same single inverter
482+
483+
- Battery can only charge from PV
484+
485+
- The PV generation (DC) is well-known
486+
487+
- A custom dispatch series must be defined, but there is no guarantee that it
488+
will be followed
489+
490+
- The battery cannot charge with higher power than PV can provide
491+
492+
- The battery cannot discharge with higher power if the inverter cannot
493+
handle the total power provided by PV and the battery
494+
495+
- The battery will be charged to avoid clipping AC power
496+
497+
For this use case, you can start with the same self-consumption power flow
498+
solution and dispatch series as in the AC battery use case:
499+
500+
.. ipython:: python
501+
502+
self_consumption_flow = self_consumption(generation, load)
503+
dispatch = self_consumption_flow["Grid to load"] - self_consumption_flow["System to grid"]
504+
505+
506+
TODO:
507+
508+
.. ipython:: python
509+
510+
from pvlib.powerflow import multi_dc_battery
511+
512+
battery_datasheet = {
513+
"chemistry": "LFP",
514+
"mode": "DC",
515+
"charge_efficiency": 0.98,
516+
"discharge_efficiency": 0.98,
517+
"min_soc_percent": 5,
518+
"max_soc_percent": 95,
519+
"dc_modules": 1,
520+
"dc_modules_in_series": 1,
521+
"dc_energy_wh": 5500,
522+
"dc_nominal_voltage": 102.4,
523+
"dc_max_power_w": 3400,
524+
}
525+
results = multi_dc_battery(
526+
v_dc=[mc.results.dc["v_mp"]],
527+
p_dc=[mc.results.dc["p_mp"]],
528+
inverter=mc.system.inverter_parameters,
529+
battery_dispatch=dispatch,
530+
battery_parameters=fit_sam(battery_datasheet),
531+
battery_model=sam,
532+
)
533+
dc_battery_flow = self_consumption(results["AC power"], load)
534+
dc_battery_flow["PV to load"] = dc_battery_flow["System to load"] * (1 - results["Battery factor"])
535+
dc_battery_flow["Battery to load"] = dc_battery_flow["System to load"] * results["Battery factor"]
536+
@savefig flow_self_consumption_dc_battery_load.png
537+
dc_battery_flow.groupby(dc_battery_flow.index.hour).mean()[["PV to load", "Battery to load", "Grid to load", "System to grid"]].plot.bar(stacked=True, legend=True, xlabel="Hour", ylabel="Power (W)", title="Average power flow")
421538
@suppress
422539
plt.close()
423540
424541
425-
While the self-consumption with AC-connected battery use case imposes many
426-
restrictions to the power flow, it still allows some flexibility to decide when
427-
to allow charging and discharging. If you wanted to simulate a use case where
428-
discharging should be avoided from 00:00 to 08:00, you could do that by simply:
542+
Dispatching strategies
543+
**********************
544+
545+
While the self-consumption-with-battery use case imposes many restrictions to
546+
the power flow, it still allows some flexibility to decide when to charge and
547+
discharge the battery.
548+
549+
An example of a different dispatching strategy is to set a well-defined
550+
schedule that determines when the energy should flow into or out of the
551+
battery. This is a common use case when you want to tie the battery flow to the
552+
grid energy rates (i.e.: avoid discharging when the energy rates are low).
553+
554+
For example, if you wanted to simulate an AC-connected battery in a system
555+
where discharging should be avoided between 21:00 and 00:00, you could do that
556+
by simply:
429557

430558
.. ipython:: python
431559
432560
dispatch = self_consumption_flow["Grid to load"] - self_consumption_flow["System to grid"]
433-
dispatch.loc[dispatch.index.hour < 8] = 0
561+
dispatch.loc[dispatch.index.hour >= 21] = 0
434562
state, flow = self_consumption_ac_battery_custom_dispatch(self_consumption_flow, dispatch, battery, sam)
435563
436-
@savefig flow_self_consumption_ac_battery_load_custom_dispatch_restricted.png
564+
@savefig flow_self_consumption_ac_battery_restricted_dispatch_load.png
437565
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")
438566
@suppress
439567
plt.close()

0 commit comments

Comments
 (0)