diff --git a/.gitignore b/.gitignore index a8304db0b7..658fe2d026 100644 --- a/.gitignore +++ b/.gitignore @@ -52,10 +52,12 @@ coverage.xml # Translations *.mo -# Mr Developer +# IDE's (Mr Developer, pydev, pycharm...) .mr.developer.cfg .project .pydevproject +.spyderproject +.idea/ # Rope .ropeproject diff --git a/docs/sphinx/source/whatsnew.rst b/docs/sphinx/source/whatsnew.rst index e2439657bb..9b69bd4303 100644 --- a/docs/sphinx/source/whatsnew.rst +++ b/docs/sphinx/source/whatsnew.rst @@ -6,6 +6,7 @@ What's New These are new features and improvements of note in each release. +.. include:: whatsnew/v0.4.2.txt .. include:: whatsnew/v0.4.1.txt .. include:: whatsnew/v0.4.0.txt .. include:: whatsnew/v0.3.3.txt diff --git a/docs/sphinx/source/whatsnew/v0.4.2.txt b/docs/sphinx/source/whatsnew/v0.4.2.txt new file mode 100644 index 0000000000..9569a42db8 --- /dev/null +++ b/docs/sphinx/source/whatsnew/v0.4.2.txt @@ -0,0 +1,30 @@ +.. _whatsnew_0420: + +v0.4.2 () +------------------------ + +This is a minor release from 0.4.0. .... + + +Bug fixes +~~~~~~~~~ + +* Fixed typo in __repr__ method of ModelChain and in its regarding test. (commit: b691358b) + + +API Changes +~~~~~~~~~~~ + +* 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`) + + +Enhancements +~~~~~~~~~~~~ + +* Adding a complete_irradiance method to the ModelChain to make it possible to calculate missing irradiation data from the existing columns [beta] (:issue:`239`) + + +Code Contributors +~~~~~~~~~~~~~~~~~ + +* Uwe Krien diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 47b6b30992..e3625c3651 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -7,10 +7,11 @@ """ from functools import partial - +import logging +import warnings import pandas as pd -from pvlib import solarposition, pvsystem, clearsky, atmosphere +from pvlib import (solarposition, pvsystem, clearsky, atmosphere, tools) from pvlib.tracking import SingleAxisTracker import pvlib.irradiance # avoid name conflict with full import @@ -316,9 +317,13 @@ def __init__(self, system, location, self.losses_model = losses_model self.orientation_strategy = orientation_strategy + self.weather = None + self.times = None + self.solar_position = None + def __repr__(self): return ('ModelChain for: ' + str(self.system) + - ' orientation_startegy: ' + str(self.orientation_strategy) + + ' orientation_strategy: ' + str(self.orientation_strategy) + ' clearsky_model: ' + str(self.clearsky_model) + ' transposition_model: ' + str(self.transposition_model) + ' solar_position_method: ' + str(self.solar_position_method) + @@ -602,7 +607,78 @@ def effective_irradiance_model(self): fd*self.total_irrad['poa_diffuse']) return self - def prepare_inputs(self, times, irradiance=None, weather=None): + def complete_irradiance(self, times=None, weather=None): + """ + Determine the missing irradiation columns. Only two of the following + data columns (dni, ghi, dhi) are needed to calculate the missing data. + + This function is not safe at the moment. Results can be too high or + negative. Please contribute and help to improve this function on + https://github.com/pvlib/pvlib-python + + Parameters + ---------- + times : DatetimeIndex + Times at which to evaluate the model. Can be None if attribute + `times` is already set. + weather : pandas.DataFrame + Table with at least two columns containing one of the following data + sets: dni, dhi, ghi. Can be None if attribute `weather` is already + set. + + Returns + ------- + self + + Assigns attributes: times, weather + + Examples + -------- + This example does not work until the parameters `my_system`, + `my_location`, `my_datetime` and `my_weather` are not defined properly + but shows the basic idea how this method can be used. + + >>> from pvlib.modelchain import ModelChain + + >>> # my_weather containing 'dhi' and 'ghi'. + >>> mc = ModelChain(my_system, my_location) # doctest: +SKIP + >>> mc.complete_irradiance(my_datetime, my_weather) # doctest: +SKIP + >>> mc.run_model() # doctest: +SKIP + + >>> # my_weather containing 'dhi', 'ghi' and 'dni'. + >>> mc = ModelChain(my_system, my_location) # doctest: +SKIP + >>> mc.run_model(my_datetime, my_weather) # doctest: +SKIP + """ + if weather is not None: + self.weather = weather + if times is not None: + self.times = times + self.solar_position = self.location.get_solarposition(self.times) + icolumns = set(self.weather.columns) + wrn_txt = ("This function is not safe at the moment.\n" + + "Results can be too high or negative.\n" + + "Help to improve this function on github:\n" + + "https://github.com/pvlib/pvlib-python \n") + warnings.warn(wrn_txt, UserWarning) + if {'ghi', 'dhi'} <= icolumns and 'dni' not in icolumns: + logging.debug('Estimate dni from ghi and dhi') + self.weather.loc[:, 'dni'] = ( + (self.weather.loc[:, 'ghi'] - self.weather.loc[:, 'dhi']) / + tools.cosd(self.solar_position.loc[:, 'zenith'])) + elif {'dni', 'dhi'} <= icolumns and 'ghi' not in icolumns: + logging.debug('Estimate ghi from dni and dhi') + self.weather.loc[:, 'ghi'] = ( + self.weather.dni * tools.cosd(self.solar_position.zenith) + + self.weather.dhi) + elif {'dni', 'ghi'} <= icolumns and 'dhi' not in icolumns: + logging.debug('Estimate dhi from dni and ghi') + self.weather.loc[:, 'dhi'] = ( + self.weather.ghi - self.weather.dni * + tools.cosd(self.solar_position.zenith)) + + return self + + def prepare_inputs(self, times=None, irradiance=None, weather=None): """ Prepare the solar position, irradiance, and weather inputs to the model. @@ -610,24 +686,46 @@ def prepare_inputs(self, times, irradiance=None, weather=None): Parameters ---------- times : DatetimeIndex - Times at which to evaluate the model. + Times at which to evaluate the model. Can be None if attribute + `times` is already set. irradiance : None or DataFrame - If None, calculates clear sky data. - Columns must be 'dni', 'ghi', 'dhi'. + This parameter is deprecated. Please use `weather` instead. weather : None or DataFrame - If None, assumes air temperature is 20 C and - wind speed is 0 m/s. - Columns must be 'wind_speed', 'temp_air'. + If None, the weather attribute is used. If the weather attribute is + also None assumes air temperature is 20 C, wind speed is 0 m/s and + irradiation calculated from clear sky data. + Column names must be 'wind_speed', 'temp_air', 'dni', 'ghi', 'dhi'. + Do not pass incomplete irradiation data. + Use method + :py:meth:`~pvlib.modelchain.ModelChain.complete_irradiance` + instead. Returns ------- self - Assigns attributes: times, solar_position, airmass, irradiance, - total_irrad, weather, aoi + Assigns attributes: times, solar_position, airmass, total_irrad, aoi """ - - self.times = times + if weather is not None: + self.weather = weather + if self.weather is None: + self.weather = pd.DataFrame() + + # The following part could be removed together with the irradiance + # parameter at version v0.5 or v0.6. + # **** Begin **** + wrn_txt = ("The irradiance parameter will be removed soon.\n" + + "Please use the weather parameter to pass a DataFrame " + + "with irradiance (ghi, dni, dhi), wind speed and " + + "temp_air.\n") + if irradiance is not None: + warnings.warn(wrn_txt, FutureWarning) + for column in irradiance.columns: + self.weather[column] = irradiance[column] + # **** End **** + + if times is not None: + self.times = times self.solar_position = self.location.get_solarposition(self.times) @@ -637,12 +735,17 @@ def prepare_inputs(self, times, irradiance=None, weather=None): self.aoi = self.system.get_aoi(self.solar_position['apparent_zenith'], self.solar_position['azimuth']) - if irradiance is None: - irradiance = self.location.get_clearsky( + if not any([x in ['ghi', 'dni', 'dhi'] for x in self.weather.columns]): + self.weather[['ghi', 'dni', 'dhi']] = self.location.get_clearsky( self.solar_position.index, self.clearsky_model, zenith_data=self.solar_position['apparent_zenith'], airmass_data=self.airmass['airmass_absolute']) - self.irradiance = irradiance + + if not {'ghi', 'dni', 'dhi'} <= set(self.weather.columns): + raise ValueError( + "Uncompleted irradiance data set. Please check you input " + + "data.\nData set needs to have 'dni', 'dhi' and 'ghi'.\n" + + "Detected data: {0}".format(list(self.weather.columns))) # PVSystem.get_irradiance and SingleAxisTracker.get_irradiance # have different method signatures, so use partial to handle @@ -670,35 +773,37 @@ def prepare_inputs(self, times, irradiance=None, weather=None): self.solar_position['azimuth']) self.total_irrad = get_irradiance( - self.irradiance['dni'], - self.irradiance['ghi'], - self.irradiance['dhi'], + self.weather['dni'], + self.weather['ghi'], + self.weather['dhi'], airmass=self.airmass['airmass_relative'], model=self.transposition_model) - if weather is None: - weather = {'wind_speed': 0, 'temp_air': 20} - self.weather = weather - + if self.weather.get('wind_speed') is None: + self.weather['wind_speed'] = 0 + if self.weather.get('temp_air') is None: + self.weather['temp_air'] = 20 return self - def run_model(self, times, irradiance=None, weather=None): + def run_model(self, times=None, irradiance=None, weather=None): """ Run the model. Parameters ---------- times : DatetimeIndex - Times at which to evaluate the model. - + Times at which to evaluate the model. Can be None if attribute + `times` is already set. irradiance : None or DataFrame - If None, calculates clear sky data. - Columns must be 'dni', 'ghi', 'dhi'. - + This parameter is deprecated. Please use `weather` instead. weather : None or DataFrame - If None, assumes air temperature is 20 C and - wind speed is 0 m/s. - Columns must be 'wind_speed', 'temp_air'. + If None, assumes air temperature is 20 C, wind speed is 0 m/s and + irradiation calculated from clear sky data. + Column names must be 'wind_speed', 'temp_air', 'dni', 'ghi', 'dhi'. + Do not pass incomplete irradiation data. + Use method + :py:meth:`~pvlib.modelchain.ModelChain.complete_irradiance` + instead. Returns ------- diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index 1d05e1a5ee..40fd7f32a8 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -102,7 +102,7 @@ def test_run_model_with_irradiance(system, location): times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') irradiance = pd.DataFrame({'dni':900, 'ghi':600, 'dhi':150}, index=times) - ac = mc.run_model(times, irradiance=irradiance).ac + ac = mc.run_model(times, weather=irradiance).ac expected = pd.Series(np.array([ 1.90054749e+02, -2.00000000e-02]), index=times) @@ -114,7 +114,7 @@ def test_run_model_perez(system, location): times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') irradiance = pd.DataFrame({'dni':900, 'ghi':600, 'dhi':150}, index=times) - ac = mc.run_model(times, irradiance=irradiance).ac + ac = mc.run_model(times, weather=irradiance).ac expected = pd.Series(np.array([ 190.194545796, -2.00000000e-02]), index=times) @@ -127,7 +127,7 @@ def test_run_model_gueymard_perez(system, location): times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') irradiance = pd.DataFrame({'dni':900, 'ghi':600, 'dhi':150}, index=times) - ac = mc.run_model(times, irradiance=irradiance).ac + ac = mc.run_model(times, weather=irradiance).ac expected = pd.Series(np.array([ 190.194760203, -2.00000000e-02]), index=times) @@ -412,6 +412,63 @@ def test_ModelChain___repr__(system, location): assert mc.__repr__() == ('ModelChain for: PVSystem with tilt:32.2 and '+ 'azimuth: 180 with Module: None and Inverter: None '+ - 'orientation_startegy: south_at_latitude_tilt clearsky_model: '+ + 'orientation_strategy: south_at_latitude_tilt clearsky_model: '+ 'ineichen transposition_model: haydavies solar_position_method: '+ 'nrel_numpy airmass_model: kastenyoung1989') + + +@requires_scipy +def test_weather_irradiance_input(system, location): + """Test will raise a warning and should be removed in future versions.""" + mc = ModelChain(system, location) + times = pd.date_range('2012-06-01 12:00:00', periods=2, freq='H') + i = pd.DataFrame({'dni': [2, 3], 'dhi': [4, 6], 'ghi': [9, 5]}, index=times) + w = pd.DataFrame({'wind_speed': [11, 5], 'temp_air': [30, 32]}, index=times) + mc.run_model(times, irradiance=i, weather=w) + + assert_series_equal(mc.weather['dni'], + pd.Series([2, 3], index=times, name='dni')) + assert_series_equal(mc.weather['wind_speed'], + pd.Series([11, 5], index=times, name='wind_speed')) + + +@requires_scipy +def test_complete_irradiance_clean_run(system, location): + """The DataFrame should not change if all columns are passed""" + mc = ModelChain(system, location) + times = pd.date_range('2010-07-05 9:00:00', periods=2, freq='H') + i = pd.DataFrame({'dni': [2, 3], 'dhi': [4, 6], 'ghi': [9, 5]}, index=times) + + mc.complete_irradiance(times, weather=i) + + assert_series_equal(mc.weather['dni'], + pd.Series([2, 3], index=times, name='dni')) + assert_series_equal(mc.weather['dhi'], + pd.Series([4, 6], index=times, name='dhi')) + assert_series_equal(mc.weather['ghi'], + pd.Series([9, 5], index=times, name='ghi')) + + +@requires_scipy +def test_complete_irradiance(system, location): + """Check calculations""" + mc = ModelChain(system, location) + times = pd.date_range('2010-07-05 9:00:00', periods=2, freq='H') + i = pd.DataFrame({'dni': [30.354455, 77.22822], + 'dhi': [372.103976116, 497.087579068], + 'ghi': [356.543700, 465.44400]}, index=times) + + mc.complete_irradiance(times, weather=i[['ghi', 'dni']]) + assert_series_equal(mc.weather['dhi'], + pd.Series([372.103976116, 497.087579068], + index=times, name='dhi')) + + mc.complete_irradiance(times, weather=i[['dhi', 'dni']]) + assert_series_equal(mc.weather['ghi'], + pd.Series([356.543700, 465.44400], + index=times, name='ghi')) + + mc.complete_irradiance(times, weather=i[['dhi', 'ghi']]) + assert_series_equal(mc.weather['dni'], + pd.Series([30.354455, 77.22822], + index=times, name='dni'))