Skip to content

Commit 23a4d1a

Browse files
uvchikwholmgren
authored andcommitted
add complete_irradiance method to ModelChain and deprecate irradiance parameter (#239)
* try to use weather DataFrame for irradiance if irradiance DataFrame is None * try to determine missing columns of the irradiance DataFrame * remove prepare_irradiance call from run_model * replace irradiance DF with weather DF without breaking the API * add future warning * add error if irradiation data is incomplete * rename method * replace non existing function with set set comparison * fix assigned attributes in the docstring * fix self.weather assignment * fix layout * change existing tests to new API * add new tests * add columns or update columns of existing weather data * add attributes to class * adapt docstrings * make time an optional argument if self.times is already set * complete temporary version (beta) of "complete_irradiance" * remove duplicate import * update docstring * fix typo * add deprecated parameter to docstring * set default self.weather to None instead of empty DataFrame * fix typos, layout changes * avoid mutation of input data (irradiance) * overwrite self.weather completly with its parameter instead of column-wise * add more IDE config files to gitignore * add whatsnew entry * copy doctstring changes to run_model method * make parameters optional if attributes are already set * add example to docstring (not working and excluded from doctest) * return self and describe it in the docstring * add bug fix to whatsnew * add empty DataFrame directly to avoid if clauses * make error message clearer
1 parent 44742c5 commit 23a4d1a

File tree

5 files changed

+233
-38
lines changed

5 files changed

+233
-38
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,12 @@ coverage.xml
5252
# Translations
5353
*.mo
5454

55-
# Mr Developer
55+
# IDE's (Mr Developer, pydev, pycharm...)
5656
.mr.developer.cfg
5757
.project
5858
.pydevproject
59+
.spyderproject
60+
.idea/
5961

6062
# Rope
6163
.ropeproject

docs/sphinx/source/whatsnew.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ What's New
66

77
These are new features and improvements of note in each release.
88

9+
.. include:: whatsnew/v0.4.2.txt
910
.. include:: whatsnew/v0.4.1.txt
1011
.. include:: whatsnew/v0.4.0.txt
1112
.. include:: whatsnew/v0.3.3.txt
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
.. _whatsnew_0420:
2+
3+
v0.4.2 ()
4+
------------------------
5+
6+
This is a minor release from 0.4.0. ....
7+
8+
9+
Bug fixes
10+
~~~~~~~~~
11+
12+
* Fixed typo in __repr__ method of ModelChain and in its regarding test. (commit: b691358b)
13+
14+
15+
API Changes
16+
~~~~~~~~~~~
17+
18+
* The run_model method of the ModelChain will use the weather parameter of all weather data instead of splitting it to irradiation and weather. The irradiation parameter still works but will be removed soon.(:issue:`239`)
19+
20+
21+
Enhancements
22+
~~~~~~~~~~~~
23+
24+
* Adding a complete_irradiance method to the ModelChain to make it possible to calculate missing irradiation data from the existing columns [beta] (:issue:`239`)
25+
26+
27+
Code Contributors
28+
~~~~~~~~~~~~~~~~~
29+
30+
* Uwe Krien

pvlib/modelchain.py

Lines changed: 138 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
"""
88

99
from functools import partial
10-
10+
import logging
11+
import warnings
1112
import pandas as pd
1213

13-
from pvlib import solarposition, pvsystem, clearsky, atmosphere
14+
from pvlib import (solarposition, pvsystem, clearsky, atmosphere, tools)
1415
from pvlib.tracking import SingleAxisTracker
1516
import pvlib.irradiance # avoid name conflict with full import
1617

@@ -316,9 +317,13 @@ def __init__(self, system, location,
316317
self.losses_model = losses_model
317318
self.orientation_strategy = orientation_strategy
318319

320+
self.weather = None
321+
self.times = None
322+
self.solar_position = None
323+
319324
def __repr__(self):
320325
return ('ModelChain for: ' + str(self.system) +
321-
' orientation_startegy: ' + str(self.orientation_strategy) +
326+
' orientation_strategy: ' + str(self.orientation_strategy) +
322327
' clearsky_model: ' + str(self.clearsky_model) +
323328
' transposition_model: ' + str(self.transposition_model) +
324329
' solar_position_method: ' + str(self.solar_position_method) +
@@ -602,32 +607,125 @@ def effective_irradiance_model(self):
602607
fd*self.total_irrad['poa_diffuse'])
603608
return self
604609

605-
def prepare_inputs(self, times, irradiance=None, weather=None):
610+
def complete_irradiance(self, times=None, weather=None):
611+
"""
612+
Determine the missing irradiation columns. Only two of the following
613+
data columns (dni, ghi, dhi) are needed to calculate the missing data.
614+
615+
This function is not safe at the moment. Results can be too high or
616+
negative. Please contribute and help to improve this function on
617+
https://github.com/pvlib/pvlib-python
618+
619+
Parameters
620+
----------
621+
times : DatetimeIndex
622+
Times at which to evaluate the model. Can be None if attribute
623+
`times` is already set.
624+
weather : pandas.DataFrame
625+
Table with at least two columns containing one of the following data
626+
sets: dni, dhi, ghi. Can be None if attribute `weather` is already
627+
set.
628+
629+
Returns
630+
-------
631+
self
632+
633+
Assigns attributes: times, weather
634+
635+
Examples
636+
--------
637+
This example does not work until the parameters `my_system`,
638+
`my_location`, `my_datetime` and `my_weather` are not defined properly
639+
but shows the basic idea how this method can be used.
640+
641+
>>> from pvlib.modelchain import ModelChain
642+
643+
>>> # my_weather containing 'dhi' and 'ghi'.
644+
>>> mc = ModelChain(my_system, my_location) # doctest: +SKIP
645+
>>> mc.complete_irradiance(my_datetime, my_weather) # doctest: +SKIP
646+
>>> mc.run_model() # doctest: +SKIP
647+
648+
>>> # my_weather containing 'dhi', 'ghi' and 'dni'.
649+
>>> mc = ModelChain(my_system, my_location) # doctest: +SKIP
650+
>>> mc.run_model(my_datetime, my_weather) # doctest: +SKIP
651+
"""
652+
if weather is not None:
653+
self.weather = weather
654+
if times is not None:
655+
self.times = times
656+
self.solar_position = self.location.get_solarposition(self.times)
657+
icolumns = set(self.weather.columns)
658+
wrn_txt = ("This function is not safe at the moment.\n" +
659+
"Results can be too high or negative.\n" +
660+
"Help to improve this function on github:\n" +
661+
"https://github.com/pvlib/pvlib-python \n")
662+
warnings.warn(wrn_txt, UserWarning)
663+
if {'ghi', 'dhi'} <= icolumns and 'dni' not in icolumns:
664+
logging.debug('Estimate dni from ghi and dhi')
665+
self.weather.loc[:, 'dni'] = (
666+
(self.weather.loc[:, 'ghi'] - self.weather.loc[:, 'dhi']) /
667+
tools.cosd(self.solar_position.loc[:, 'zenith']))
668+
elif {'dni', 'dhi'} <= icolumns and 'ghi' not in icolumns:
669+
logging.debug('Estimate ghi from dni and dhi')
670+
self.weather.loc[:, 'ghi'] = (
671+
self.weather.dni * tools.cosd(self.solar_position.zenith) +
672+
self.weather.dhi)
673+
elif {'dni', 'ghi'} <= icolumns and 'dhi' not in icolumns:
674+
logging.debug('Estimate dhi from dni and ghi')
675+
self.weather.loc[:, 'dhi'] = (
676+
self.weather.ghi - self.weather.dni *
677+
tools.cosd(self.solar_position.zenith))
678+
679+
return self
680+
681+
def prepare_inputs(self, times=None, irradiance=None, weather=None):
606682
"""
607683
Prepare the solar position, irradiance, and weather inputs to
608684
the model.
609685
610686
Parameters
611687
----------
612688
times : DatetimeIndex
613-
Times at which to evaluate the model.
689+
Times at which to evaluate the model. Can be None if attribute
690+
`times` is already set.
614691
irradiance : None or DataFrame
615-
If None, calculates clear sky data.
616-
Columns must be 'dni', 'ghi', 'dhi'.
692+
This parameter is deprecated. Please use `weather` instead.
617693
weather : None or DataFrame
618-
If None, assumes air temperature is 20 C and
619-
wind speed is 0 m/s.
620-
Columns must be 'wind_speed', 'temp_air'.
694+
If None, the weather attribute is used. If the weather attribute is
695+
also None assumes air temperature is 20 C, wind speed is 0 m/s and
696+
irradiation calculated from clear sky data.
697+
Column names must be 'wind_speed', 'temp_air', 'dni', 'ghi', 'dhi'.
698+
Do not pass incomplete irradiation data.
699+
Use method
700+
:py:meth:`~pvlib.modelchain.ModelChain.complete_irradiance`
701+
instead.
621702
622703
Returns
623704
-------
624705
self
625706
626-
Assigns attributes: times, solar_position, airmass, irradiance,
627-
total_irrad, weather, aoi
707+
Assigns attributes: times, solar_position, airmass, total_irrad, aoi
628708
"""
629-
630-
self.times = times
709+
if weather is not None:
710+
self.weather = weather
711+
if self.weather is None:
712+
self.weather = pd.DataFrame()
713+
714+
# The following part could be removed together with the irradiance
715+
# parameter at version v0.5 or v0.6.
716+
# **** Begin ****
717+
wrn_txt = ("The irradiance parameter will be removed soon.\n" +
718+
"Please use the weather parameter to pass a DataFrame " +
719+
"with irradiance (ghi, dni, dhi), wind speed and " +
720+
"temp_air.\n")
721+
if irradiance is not None:
722+
warnings.warn(wrn_txt, FutureWarning)
723+
for column in irradiance.columns:
724+
self.weather[column] = irradiance[column]
725+
# **** End ****
726+
727+
if times is not None:
728+
self.times = times
631729

632730
self.solar_position = self.location.get_solarposition(self.times)
633731

@@ -637,12 +735,17 @@ def prepare_inputs(self, times, irradiance=None, weather=None):
637735
self.aoi = self.system.get_aoi(self.solar_position['apparent_zenith'],
638736
self.solar_position['azimuth'])
639737

640-
if irradiance is None:
641-
irradiance = self.location.get_clearsky(
738+
if not any([x in ['ghi', 'dni', 'dhi'] for x in self.weather.columns]):
739+
self.weather[['ghi', 'dni', 'dhi']] = self.location.get_clearsky(
642740
self.solar_position.index, self.clearsky_model,
643741
zenith_data=self.solar_position['apparent_zenith'],
644742
airmass_data=self.airmass['airmass_absolute'])
645-
self.irradiance = irradiance
743+
744+
if not {'ghi', 'dni', 'dhi'} <= set(self.weather.columns):
745+
raise ValueError(
746+
"Uncompleted irradiance data set. Please check you input " +
747+
"data.\nData set needs to have 'dni', 'dhi' and 'ghi'.\n" +
748+
"Detected data: {0}".format(list(self.weather.columns)))
646749

647750
# PVSystem.get_irradiance and SingleAxisTracker.get_irradiance
648751
# have different method signatures, so use partial to handle
@@ -670,35 +773,37 @@ def prepare_inputs(self, times, irradiance=None, weather=None):
670773
self.solar_position['azimuth'])
671774

672775
self.total_irrad = get_irradiance(
673-
self.irradiance['dni'],
674-
self.irradiance['ghi'],
675-
self.irradiance['dhi'],
776+
self.weather['dni'],
777+
self.weather['ghi'],
778+
self.weather['dhi'],
676779
airmass=self.airmass['airmass_relative'],
677780
model=self.transposition_model)
678781

679-
if weather is None:
680-
weather = {'wind_speed': 0, 'temp_air': 20}
681-
self.weather = weather
682-
782+
if self.weather.get('wind_speed') is None:
783+
self.weather['wind_speed'] = 0
784+
if self.weather.get('temp_air') is None:
785+
self.weather['temp_air'] = 20
683786
return self
684787

685-
def run_model(self, times, irradiance=None, weather=None):
788+
def run_model(self, times=None, irradiance=None, weather=None):
686789
"""
687790
Run the model.
688791
689792
Parameters
690793
----------
691794
times : DatetimeIndex
692-
Times at which to evaluate the model.
693-
795+
Times at which to evaluate the model. Can be None if attribute
796+
`times` is already set.
694797
irradiance : None or DataFrame
695-
If None, calculates clear sky data.
696-
Columns must be 'dni', 'ghi', 'dhi'.
697-
798+
This parameter is deprecated. Please use `weather` instead.
698799
weather : None or DataFrame
699-
If None, assumes air temperature is 20 C and
700-
wind speed is 0 m/s.
701-
Columns must be 'wind_speed', 'temp_air'.
800+
If None, assumes air temperature is 20 C, wind speed is 0 m/s and
801+
irradiation calculated from clear sky data.
802+
Column names must be 'wind_speed', 'temp_air', 'dni', 'ghi', 'dhi'.
803+
Do not pass incomplete irradiation data.
804+
Use method
805+
:py:meth:`~pvlib.modelchain.ModelChain.complete_irradiance`
806+
instead.
702807
703808
Returns
704809
-------

pvlib/test/test_modelchain.py

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def test_run_model_with_irradiance(system, location):
102102
times = pd.date_range('20160101 1200-0700', periods=2, freq='6H')
103103
irradiance = pd.DataFrame({'dni':900, 'ghi':600, 'dhi':150},
104104
index=times)
105-
ac = mc.run_model(times, irradiance=irradiance).ac
105+
ac = mc.run_model(times, weather=irradiance).ac
106106

107107
expected = pd.Series(np.array([ 1.90054749e+02, -2.00000000e-02]),
108108
index=times)
@@ -114,7 +114,7 @@ def test_run_model_perez(system, location):
114114
times = pd.date_range('20160101 1200-0700', periods=2, freq='6H')
115115
irradiance = pd.DataFrame({'dni':900, 'ghi':600, 'dhi':150},
116116
index=times)
117-
ac = mc.run_model(times, irradiance=irradiance).ac
117+
ac = mc.run_model(times, weather=irradiance).ac
118118

119119
expected = pd.Series(np.array([ 190.194545796, -2.00000000e-02]),
120120
index=times)
@@ -127,7 +127,7 @@ def test_run_model_gueymard_perez(system, location):
127127
times = pd.date_range('20160101 1200-0700', periods=2, freq='6H')
128128
irradiance = pd.DataFrame({'dni':900, 'ghi':600, 'dhi':150},
129129
index=times)
130-
ac = mc.run_model(times, irradiance=irradiance).ac
130+
ac = mc.run_model(times, weather=irradiance).ac
131131

132132
expected = pd.Series(np.array([ 190.194760203, -2.00000000e-02]),
133133
index=times)
@@ -412,6 +412,63 @@ def test_ModelChain___repr__(system, location):
412412

413413
assert mc.__repr__() == ('ModelChain for: PVSystem with tilt:32.2 and '+
414414
'azimuth: 180 with Module: None and Inverter: None '+
415-
'orientation_startegy: south_at_latitude_tilt clearsky_model: '+
415+
'orientation_strategy: south_at_latitude_tilt clearsky_model: '+
416416
'ineichen transposition_model: haydavies solar_position_method: '+
417417
'nrel_numpy airmass_model: kastenyoung1989')
418+
419+
420+
@requires_scipy
421+
def test_weather_irradiance_input(system, location):
422+
"""Test will raise a warning and should be removed in future versions."""
423+
mc = ModelChain(system, location)
424+
times = pd.date_range('2012-06-01 12:00:00', periods=2, freq='H')
425+
i = pd.DataFrame({'dni': [2, 3], 'dhi': [4, 6], 'ghi': [9, 5]}, index=times)
426+
w = pd.DataFrame({'wind_speed': [11, 5], 'temp_air': [30, 32]}, index=times)
427+
mc.run_model(times, irradiance=i, weather=w)
428+
429+
assert_series_equal(mc.weather['dni'],
430+
pd.Series([2, 3], index=times, name='dni'))
431+
assert_series_equal(mc.weather['wind_speed'],
432+
pd.Series([11, 5], index=times, name='wind_speed'))
433+
434+
435+
@requires_scipy
436+
def test_complete_irradiance_clean_run(system, location):
437+
"""The DataFrame should not change if all columns are passed"""
438+
mc = ModelChain(system, location)
439+
times = pd.date_range('2010-07-05 9:00:00', periods=2, freq='H')
440+
i = pd.DataFrame({'dni': [2, 3], 'dhi': [4, 6], 'ghi': [9, 5]}, index=times)
441+
442+
mc.complete_irradiance(times, weather=i)
443+
444+
assert_series_equal(mc.weather['dni'],
445+
pd.Series([2, 3], index=times, name='dni'))
446+
assert_series_equal(mc.weather['dhi'],
447+
pd.Series([4, 6], index=times, name='dhi'))
448+
assert_series_equal(mc.weather['ghi'],
449+
pd.Series([9, 5], index=times, name='ghi'))
450+
451+
452+
@requires_scipy
453+
def test_complete_irradiance(system, location):
454+
"""Check calculations"""
455+
mc = ModelChain(system, location)
456+
times = pd.date_range('2010-07-05 9:00:00', periods=2, freq='H')
457+
i = pd.DataFrame({'dni': [30.354455, 77.22822],
458+
'dhi': [372.103976116, 497.087579068],
459+
'ghi': [356.543700, 465.44400]}, index=times)
460+
461+
mc.complete_irradiance(times, weather=i[['ghi', 'dni']])
462+
assert_series_equal(mc.weather['dhi'],
463+
pd.Series([372.103976116, 497.087579068],
464+
index=times, name='dhi'))
465+
466+
mc.complete_irradiance(times, weather=i[['dhi', 'dni']])
467+
assert_series_equal(mc.weather['ghi'],
468+
pd.Series([356.543700, 465.44400],
469+
index=times, name='ghi'))
470+
471+
mc.complete_irradiance(times, weather=i[['dhi', 'ghi']])
472+
assert_series_equal(mc.weather['dni'],
473+
pd.Series([30.354455, 77.22822],
474+
index=times, name='dni'))

0 commit comments

Comments
 (0)