From 44a3a92e6a4fb1bf2d3d7c06e6ee3ce30983aeee Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Tue, 25 Jul 2017 08:35:40 -0700 Subject: [PATCH 1/5] add SingleAxisTracker.get_aoi, update ModelChain.prepare_inputs to use it --- pvlib/modelchain.py | 26 ++++++++++++------ pvlib/tracking.py | 66 ++++++++++++++++++++++++++++++++------------- 2 files changed, 65 insertions(+), 27 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 2d24cbb443..3f89caa728 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -757,9 +757,6 @@ def prepare_inputs(self, times=None, irradiance=None, weather=None): self.airmass = self.location.get_airmass( solar_position=self.solar_position, model=self.airmass_model) - self.aoi = self.system.get_aoi(self.solar_position['apparent_zenith'], - self.solar_position['azimuth']) - 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, @@ -773,7 +770,8 @@ def prepare_inputs(self, times=None, irradiance=None, weather=None): "Detected data: {0}".format(list(self.weather.columns))) # PVSystem.get_irradiance and SingleAxisTracker.get_irradiance - # have different method signatures, so use partial to handle + # and PVSystem.get_aoi and SingleAxisTracker.get_aoi + # have different method signatures. Use partial to handle # the differences. if isinstance(self.system, SingleAxisTracker): self.tracking = self.system.singleaxis( @@ -785,18 +783,30 @@ def prepare_inputs(self, times=None, irradiance=None, weather=None): self.tracking['surface_azimuth'] = ( self.tracking['surface_azimuth'] .fillna(self.system.axis_azimuth)) + get_aoi = partial( + self.system.get_aoi, + self.tracking['surface_tilt'], + self.tracking['surface_azimuth'], + self.solar_position['apparent_zenith'], + self.solar_position['azimuth']) get_irradiance = partial( self.system.get_irradiance, - surface_tilt=self.tracking['surface_tilt'], - surface_azimuth=self.tracking['surface_azimuth'], - solar_zenith=self.solar_position['apparent_zenith'], - solar_azimuth=self.solar_position['azimuth']) + self.tracking['surface_tilt'], + self.tracking['surface_azimuth'], + self.solar_position['apparent_zenith'], + self.solar_position['azimuth']) else: + get_aoi = partial( + self.system.get_aoi, + self.solar_position['apparent_zenith'], + self.solar_position['azimuth']) get_irradiance = partial( self.system.get_irradiance, self.solar_position['apparent_zenith'], self.solar_position['azimuth']) + self.aoi = get_aoi() + self.total_irrad = get_irradiance( self.weather['dni'], self.weather['ghi'], diff --git a/pvlib/tracking.py b/pvlib/tracking.py index c61dd905dd..9a589acbe8 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -14,7 +14,7 @@ class SingleAxisTracker(PVSystem): """ - Inherits all of the PV modeling methods from PVSystem. + Inherits the PV modeling methods from :ref:PVSystem:. axis_tilt : float, default 0 The tilt of the axis of rotation (i.e, the y-axis defined by @@ -54,6 +54,9 @@ def __init__(self, axis_tilt=0, axis_azimuth=0, self.backtrack = backtrack self.gcr = gcr + kwargs['surface_tilt'] = None + kwargs['surface_azimuth'] = None + super(SingleAxisTracker, self).__init__(**kwargs) def __repr__(self): @@ -98,20 +101,58 @@ def localize(self, location=None, latitude=None, longitude=None, return LocalizedSingleAxisTracker(pvsystem=self, location=location) - def get_irradiance(self, dni, ghi, dhi, + + def get_aoi(self, surface_tilt, surface_azimuth, solar_zenith, + solar_azimuth): + """Get the angle of incidence on the system. + + For a given set of solar zenith and azimuth angles, the + surface tilt and azimuth parameters are typically determined + by :py:method:`~SingleAxisTracker.singleaxis`. + + Parameters + ---------- + surface_tilt : numeric + Panel tilt from horizontal. + surface_azimuth : numeric + Panel azimuth from north + solar_zenith : float or Series. + Solar zenith angle. + solar_azimuth : float or Series. + Solar azimuth angle. + + Returns + ------- + aoi : Series + The angle of incidence + """ + + aoi = irradiance.aoi(surface_tilt, surface_azimuth, + solar_zenith, solar_azimuth) + + + def get_irradiance(self, surface_tilt, surface_azimuth, + apparent_zenith, azimuth, dni, ghi, dhi, dni_extra=None, airmass=None, model='haydavies', **kwargs): """ Uses the :func:`irradiance.total_irrad` function to calculate the plane of array irradiance components on a tilted surface - defined by ``self.surface_tilt``, ``self.surface_azimuth``, and - ``self.albedo``. + defined by the input data and ``self.albedo``. + + For a given set of solar zenith and azimuth angles, the + surface tilt and azimuth parameters are typically determined + by :py:method:`~SingleAxisTracker.singleaxis`. Parameters ---------- - solar_zenith : float or Series. + surface_tilt : numeric + Panel tilt from horizontal. + surface_azimuth : numeric + Panel azimuth from north + solar_zenith : numeric Solar zenith angle. - solar_azimuth : float or Series. + solar_azimuth : numeric Solar azimuth angle. dni : float or Series Direct Normal Irradiance @@ -135,19 +176,6 @@ def get_irradiance(self, dni, ghi, dhi, Column names are: ``total, beam, sky, ground``. """ - surface_tilt = kwargs.pop('surface_tilt', self.surface_tilt) - surface_azimuth = kwargs.pop('surface_azimuth', self.surface_azimuth) - - try: - solar_zenith = kwargs['solar_zenith'] - except KeyError: - solar_zenith = self.solar_zenith - - try: - solar_azimuth = kwargs['solar_azimuth'] - except KeyError: - solar_azimuth = self.solar_azimuth - # not needed for all models, but this is easier if dni_extra is None: dni_extra = irradiance.extraradiation(solar_zenith.index) From 23fe66097af13f7d8aa66bc987ccf6649ab703cc Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Wed, 26 Jul 2017 19:51:54 -0700 Subject: [PATCH 2/5] get tests to pass --- pvlib/modelchain.py | 12 ++---------- pvlib/test/test_modelchain.py | 2 +- pvlib/test/test_tracking.py | 16 ++++++++-------- pvlib/tracking.py | 9 ++++----- 4 files changed, 15 insertions(+), 24 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 3f89caa728..36736644b8 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -783,12 +783,7 @@ def prepare_inputs(self, times=None, irradiance=None, weather=None): self.tracking['surface_azimuth'] = ( self.tracking['surface_azimuth'] .fillna(self.system.axis_azimuth)) - get_aoi = partial( - self.system.get_aoi, - self.tracking['surface_tilt'], - self.tracking['surface_azimuth'], - self.solar_position['apparent_zenith'], - self.solar_position['azimuth']) + self.aoi = self.tracking['aoi'] get_irradiance = partial( self.system.get_irradiance, self.tracking['surface_tilt'], @@ -796,8 +791,7 @@ def prepare_inputs(self, times=None, irradiance=None, weather=None): self.solar_position['apparent_zenith'], self.solar_position['azimuth']) else: - get_aoi = partial( - self.system.get_aoi, + self.aoi = self.system.get_aoi( self.solar_position['apparent_zenith'], self.solar_position['azimuth']) get_irradiance = partial( @@ -805,8 +799,6 @@ def prepare_inputs(self, times=None, irradiance=None, weather=None): self.solar_position['apparent_zenith'], self.solar_position['azimuth']) - self.aoi = get_aoi() - self.total_irrad = get_irradiance( self.weather['dni'], self.weather['ghi'], diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index e2199a283f..ffdbf40d81 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -168,7 +168,7 @@ def test_run_model_tracker(system, location): times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') ac = mc.run_model(times).ac - expected = pd.Series(np.array([ 122.333764454, -2.00000000e-02]), + expected = pd.Series(np.array([ 119.067713606, nan]), index=times) assert_series_equal(ac, expected, check_less_precise=2) diff --git a/pvlib/test/test_tracking.py b/pvlib/test/test_tracking.py index 360927a29c..c491683da3 100644 --- a/pvlib/test/test_tracking.py +++ b/pvlib/test/test_tracking.py @@ -260,13 +260,13 @@ def test_get_irradiance(): solar_azimuth = solar_position['azimuth'] tracker_data = system.singleaxis(solar_zenith, solar_azimuth) - irradiance = system.get_irradiance(irrads['dni'], + irradiance = system.get_irradiance(tracker_data['surface_tilt'], + tracker_data['surface_azimuth'], + solar_zenith, + solar_azimuth, + irrads['dni'], irrads['ghi'], - irrads['dhi'], - solar_zenith=solar_zenith, - solar_azimuth=solar_azimuth, - surface_tilt=tracker_data['surface_tilt'], - surface_azimuth=tracker_data['surface_azimuth']) + irrads['dhi']) expected = pd.DataFrame(data=np.array( [[ 961.80070, 815.94490, 145.85580, 135.32820, @@ -284,7 +284,7 @@ def test_get_irradiance(): def test_SingleAxisTracker___repr__(): system = tracking.SingleAxisTracker(max_angle=45, gcr=.25, module='blah', inverter='blarg') - expected = 'SingleAxisTracker: \n axis_tilt: 0\n axis_azimuth: 0\n max_angle: 45\n backtrack: True\n gcr: 0.25\n name: None\n surface_tilt: 0\n surface_azimuth: 180\n module: blah\n inverter: blarg\n albedo: 0.25\n racking_model: open_rack_cell_glassback' + expected = 'SingleAxisTracker: \n axis_tilt: 0\n axis_azimuth: 0\n max_angle: 45\n backtrack: True\n gcr: 0.25\n name: None\n surface_tilt: None\n surface_azimuth: None\n module: blah\n inverter: blarg\n albedo: 0.25\n racking_model: open_rack_cell_glassback' assert system.__repr__() == expected @@ -295,7 +295,7 @@ def test_LocalizedSingleAxisTracker___repr__(): inverter='blarg', gcr=0.25) - expected = 'LocalizedSingleAxisTracker: \n axis_tilt: 0\n axis_azimuth: 0\n max_angle: 90\n backtrack: True\n gcr: 0.25\n name: None\n surface_tilt: 0\n surface_azimuth: 180\n module: blah\n inverter: blarg\n albedo: 0.25\n racking_model: open_rack_cell_glassback\n latitude: 32\n longitude: -111\n altitude: 0\n tz: UTC' + expected = 'LocalizedSingleAxisTracker: \n axis_tilt: 0\n axis_azimuth: 0\n max_angle: 90\n backtrack: True\n gcr: 0.25\n name: None\n surface_tilt: None\n surface_azimuth: None\n module: blah\n inverter: blarg\n albedo: 0.25\n racking_model: open_rack_cell_glassback\n latitude: 32\n longitude: -111\n altitude: 0\n tz: UTC' assert localized_system.__repr__() == expected diff --git a/pvlib/tracking.py b/pvlib/tracking.py index 9a589acbe8..33b7b43c69 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -178,16 +178,15 @@ def get_irradiance(self, surface_tilt, surface_azimuth, # not needed for all models, but this is easier if dni_extra is None: - dni_extra = irradiance.extraradiation(solar_zenith.index) - dni_extra = pd.Series(dni_extra, index=solar_zenith.index) + dni_extra = irradiance.extraradiation(apparent_zenith.index) if airmass is None: - airmass = atmosphere.relativeairmass(solar_zenith) + airmass = atmosphere.relativeairmass(apparent_zenith) return irradiance.total_irrad(surface_tilt, surface_azimuth, - solar_zenith, - solar_azimuth, + apparent_zenith, + azimuth, dni, ghi, dhi, dni_extra=dni_extra, airmass=airmass, model=model, From 356b8d00b9e06c2b42bb3035ab0d47a57258833d Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Thu, 27 Jul 2017 10:31:07 -0700 Subject: [PATCH 3/5] update whatsnew --- docs/sphinx/source/whatsnew/v0.4.6.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/sphinx/source/whatsnew/v0.4.6.rst b/docs/sphinx/source/whatsnew/v0.4.6.rst index 37f968d851..9410521fbb 100644 --- a/docs/sphinx/source/whatsnew/v0.4.6.rst +++ b/docs/sphinx/source/whatsnew/v0.4.6.rst @@ -12,6 +12,8 @@ Bug fixes (:issue:`330`) * Fix the `__repr__` method of `ModelChain`, crashing when `orientation_strategy` is set to `'None'` (:issue:`352`) +* Fix the `ModelChain`'s angle of incidence calculation for + SingleAxisTracker objects (:issue:`351`) Enhancements @@ -21,6 +23,8 @@ Enhancements API Changes ~~~~~~~~~~~ * Removed parameter w from _calc_d (:issue:`344`) +* SingleAxisTracker.get_aoi now requires surface_zenith and surface_azimuth + (:issue:`351`) Documentation ~~~~~~~~~~~~~ From c5652a0423127bf135d57564cd42dda1c4e3bade Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Thu, 27 Jul 2017 12:07:28 -0700 Subject: [PATCH 4/5] add test, clarify things --- docs/sphinx/source/whatsnew/v0.4.6.rst | 6 ++++-- pvlib/test/test_tracking.py | 16 ++++++++++++++++ pvlib/tracking.py | 19 +++++++++++-------- 3 files changed, 31 insertions(+), 10 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.4.6.rst b/docs/sphinx/source/whatsnew/v0.4.6.rst index 9410521fbb..f94318ebea 100644 --- a/docs/sphinx/source/whatsnew/v0.4.6.rst +++ b/docs/sphinx/source/whatsnew/v0.4.6.rst @@ -20,11 +20,13 @@ Enhancements ~~~~~~~~~~~~ * Added default values to docstrings of all functions (:issue:`336`) + API Changes ~~~~~~~~~~~ * Removed parameter w from _calc_d (:issue:`344`) -* SingleAxisTracker.get_aoi now requires surface_zenith and surface_azimuth - (:issue:`351`) +* SingleAxisTracker.get_aoi and SingleAxisTracker.get_irradiance + now require surface_zenith and surface_azimuth (:issue:`351`) + Documentation ~~~~~~~~~~~~~ diff --git a/pvlib/test/test_tracking.py b/pvlib/test/test_tracking.py index c491683da3..8d7c8bb82e 100644 --- a/pvlib/test/test_tracking.py +++ b/pvlib/test/test_tracking.py @@ -6,6 +6,7 @@ import pytest from pandas.util.testing import assert_frame_equal +from numpy.testing import assert_allclose from pvlib.location import Location from pvlib import solarposition @@ -246,6 +247,21 @@ def test_SingleAxisTracker_localize_location(): assert localized_system.longitude == -111 +# see test_irradiance for more thorough testing +def test_get_aoi(): + system = tracking.SingleAxisTracker(max_angle=90, axis_tilt=30, + axis_azimuth=180, gcr=2.0/7.0, + backtrack=True) + surface_tilt = np.array([30, 0]) + surface_azimuth = np.array([90, 270]) + solar_zenith = np.array([70, 10]) + solar_azimuth = np.array([100, 180]) + out = system.get_aoi(surface_tilt, surface_azimuth, + solar_zenith, solar_azimuth) + expected = np.array([40.632115, 10.]) + assert_allclose(out, expected, atol=0.000001) + + def test_get_irradiance(): system = tracking.SingleAxisTracker(max_angle=90, axis_tilt=30, axis_azimuth=180, gcr=2.0/7.0, diff --git a/pvlib/tracking.py b/pvlib/tracking.py index 33b7b43c69..8561f0dc1e 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -108,7 +108,10 @@ def get_aoi(self, surface_tilt, surface_azimuth, solar_zenith, For a given set of solar zenith and azimuth angles, the surface tilt and azimuth parameters are typically determined - by :py:method:`~SingleAxisTracker.singleaxis`. + by :py:method:`~SingleAxisTracker.singleaxis`. The + :py:method:`~SingleAxisTracker.singleaxis` method also returns + the angle of incidence, so this method is only needed + if using a different tracking algorithm. Parameters ---------- @@ -124,15 +127,15 @@ def get_aoi(self, surface_tilt, surface_azimuth, solar_zenith, Returns ------- aoi : Series - The angle of incidence + The angle of incidence in degrees from normal. """ aoi = irradiance.aoi(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth) - + return aoi def get_irradiance(self, surface_tilt, surface_azimuth, - apparent_zenith, azimuth, dni, ghi, dhi, + solar_zenith, solar_azimuth, dni, ghi, dhi, dni_extra=None, airmass=None, model='haydavies', **kwargs): """ @@ -178,15 +181,15 @@ def get_irradiance(self, surface_tilt, surface_azimuth, # not needed for all models, but this is easier if dni_extra is None: - dni_extra = irradiance.extraradiation(apparent_zenith.index) + dni_extra = irradiance.extraradiation(solar_zenith.index) if airmass is None: - airmass = atmosphere.relativeairmass(apparent_zenith) + airmass = atmosphere.relativeairmass(solar_zenith) return irradiance.total_irrad(surface_tilt, surface_azimuth, - apparent_zenith, - azimuth, + solar_zenith, + solar_azimuth, dni, ghi, dhi, dni_extra=dni_extra, airmass=airmass, model=model, From 7b66ff65026dc989ac19bd959ef81ff8d738c2b3 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Thu, 27 Jul 2017 12:12:39 -0700 Subject: [PATCH 5/5] flake8 --- pvlib/test/test_modelchain.py | 2 +- pvlib/test/test_tracking.py | 8 ++------ pvlib/tracking.py | 1 - 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index ffdbf40d81..1712a05f68 100644 --- a/pvlib/test/test_modelchain.py +++ b/pvlib/test/test_modelchain.py @@ -168,7 +168,7 @@ def test_run_model_tracker(system, location): times = pd.date_range('20160101 1200-0700', periods=2, freq='6H') ac = mc.run_model(times).ac - expected = pd.Series(np.array([ 119.067713606, nan]), + expected = pd.Series(np.array([119.067713606, nan]), index=times) assert_series_equal(ac, expected, check_less_precise=2) diff --git a/pvlib/test/test_tracking.py b/pvlib/test/test_tracking.py index 8d7c8bb82e..a1ac8cbca4 100644 --- a/pvlib/test/test_tracking.py +++ b/pvlib/test/test_tracking.py @@ -9,7 +9,6 @@ from numpy.testing import assert_allclose from pvlib.location import Location -from pvlib import solarposition from pvlib import tracking @@ -285,10 +284,8 @@ def test_get_irradiance(): irrads['dhi']) expected = pd.DataFrame(data=np.array( - [[ 961.80070, 815.94490, 145.85580, 135.32820, - 10.52757492], - [ nan, nan, nan, nan, - nan]]), + [[961.80070, 815.94490, 145.85580, 135.32820, 10.52757492], + [nan, nan, nan, nan, nan]]), columns=['poa_global', 'poa_direct', 'poa_diffuse', 'poa_sky_diffuse', 'poa_ground_diffuse'], @@ -314,4 +311,3 @@ def test_LocalizedSingleAxisTracker___repr__(): expected = 'LocalizedSingleAxisTracker: \n axis_tilt: 0\n axis_azimuth: 0\n max_angle: 90\n backtrack: True\n gcr: 0.25\n name: None\n surface_tilt: None\n surface_azimuth: None\n module: blah\n inverter: blarg\n albedo: 0.25\n racking_model: open_rack_cell_glassback\n latitude: 32\n longitude: -111\n altitude: 0\n tz: UTC' assert localized_system.__repr__() == expected - diff --git a/pvlib/tracking.py b/pvlib/tracking.py index 8561f0dc1e..c23c9a9da5 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -101,7 +101,6 @@ def localize(self, location=None, latitude=None, longitude=None, return LocalizedSingleAxisTracker(pvsystem=self, location=location) - def get_aoi(self, surface_tilt, surface_azimuth, solar_zenith, solar_azimuth): """Get the angle of incidence on the system.