diff --git a/docs/sphinx/source/whatsnew/v0.4.6.rst b/docs/sphinx/source/whatsnew/v0.4.6.rst index 37f968d851..f94318ebea 100644 --- a/docs/sphinx/source/whatsnew/v0.4.6.rst +++ b/docs/sphinx/source/whatsnew/v0.4.6.rst @@ -12,15 +12,21 @@ 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 ~~~~~~~~~~~~ * Added default values to docstrings of all functions (:issue:`336`) + API Changes ~~~~~~~~~~~ * Removed parameter w from _calc_d (:issue:`344`) +* SingleAxisTracker.get_aoi and SingleAxisTracker.get_irradiance + now require surface_zenith and surface_azimuth (:issue:`351`) + Documentation ~~~~~~~~~~~~~ diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 2d24cbb443..36736644b8 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,13 +783,17 @@ def prepare_inputs(self, times=None, irradiance=None, weather=None): self.tracking['surface_azimuth'] = ( self.tracking['surface_azimuth'] .fillna(self.system.axis_azimuth)) + self.aoi = self.tracking['aoi'] 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: + self.aoi = 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'], diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py index e2199a283f..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([ 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..a1ac8cbca4 100644 --- a/pvlib/test/test_tracking.py +++ b/pvlib/test/test_tracking.py @@ -6,9 +6,9 @@ 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 from pvlib import tracking @@ -246,6 +246,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, @@ -260,19 +275,17 @@ 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, - 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'], @@ -284,7 +297,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 +308,6 @@ 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 c61dd905dd..c23c9a9da5 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,60 @@ 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`. 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 + ---------- + 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 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, + solar_zenith, solar_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,23 +178,9 @@ 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) - dni_extra = pd.Series(dni_extra, index=solar_zenith.index) if airmass is None: airmass = atmosphere.relativeairmass(solar_zenith)