From cb3c207398683aaccb8620d1ed8bc1b86baa3302 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 23 Feb 2021 10:20:30 -0700 Subject: [PATCH 01/47] add Mount classes, incorporate into Array and PVSystem --- pvlib/__init__.py | 1 + pvlib/mounts.py | 48 ++++++++++++++++++++++++++++++++++++++++ pvlib/pvsystem.py | 56 +++++++++++++++++++++++++++-------------------- 3 files changed, 81 insertions(+), 24 deletions(-) create mode 100644 pvlib/mounts.py diff --git a/pvlib/__init__.py b/pvlib/__init__.py index ff6b375017..98a935c546 100644 --- a/pvlib/__init__.py +++ b/pvlib/__init__.py @@ -12,6 +12,7 @@ ivtools, location, modelchain, + mounts, pvsystem, scaling, shading, diff --git a/pvlib/mounts.py b/pvlib/mounts.py new file mode 100644 index 0000000000..b91b3f4049 --- /dev/null +++ b/pvlib/mounts.py @@ -0,0 +1,48 @@ +from pvlib import tracking + + +class FixedMount: + def __init__(self, surface_tilt=0, surface_azimuth=180): + self.surface_tilt = surface_tilt + self.surface_azimuth = surface_azimuth + + def __repr__(self): + return ( + 'FixedMount:' + f'\n surface_tilt: {self.surface_tilt}' + f'\n surface_azimuth: {self.surface_azimuth}' + ) + + def get_orientation(self, solar_zenith, solar_azimuth): + return { + 'surface_tilt': self.surface_tilt, + 'surface_azimuth': self.surface_azimuth, + } + + +class SingleAxisTrackerMount: + def __init__(self, axis_tilt, axis_azimuth, max_angle, backtrack, gcr, + cross_axis_tilt): + self.axis_tilt = axis_tilt + self.axis_azimuth = axis_azimuth + self.max_angle = max_angle + self.backtrack = backtrack + self.gcr = gcr + self.cross_axis_tilt = cross_axis_tilt + + def __repr__(self): + attrs = ['axis_tilt', 'axis_azimuth', 'max_angle', + 'backtrack', 'gcr', 'cross_axis_tilt'] + + return 'SingleAxisTrackerMount:\n ' + '\n '.join( + f'{attr}: {getattr(self, attr)}' for attr in attrs + ) + + def get_orientation(self, solar_zenith, solar_azimuth): + tracking_data = tracking.singleaxis( + solar_zenith, solar_azimuth, + self.axis_tilt, self.axis_azimuth, + self.max_angle, self.backtrack, + self.gcr, self.cross_axis_tilt + ) + return tracking_data diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index a727673265..8185411125 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -190,9 +190,9 @@ def __init__(self, racking_model=None, losses_parameters=None, name=None): if arrays is None: + from pvlib import mounts self.arrays = (Array( - surface_tilt, - surface_azimuth, + mounts.FixedMount(surface_tilt, surface_azimuth), albedo, surface_type, module, @@ -765,7 +765,10 @@ def fuentes_celltemp(self, poa_global, temp_air, wind_speed): wind_speed = self._validate_per_array(wind_speed, system_wide=True) def _build_kwargs_fuentes(array): - kwargs = {'surface_tilt': array.surface_tilt} + # TODO: I think there should be an interface function so that + # directly accessing surface_tilt isn't necessary. Doesn't this + # break for SAT? + kwargs = {'surface_tilt': array.mount.surface_tilt} temp_model_kwargs = _build_kwargs([ 'noct_installed', 'module_height', 'wind_height', 'emissivity', 'absorption', 'surface_tilt', 'module_width', 'module_length'], @@ -1046,22 +1049,26 @@ def temperature_model_parameters(self, value): @property @_unwrap_single_value def surface_tilt(self): - return tuple(array.surface_tilt for array in self.arrays) + # TODO: don't access mount attributes directly? + return tuple(array.mount.surface_tilt for array in self.arrays) @surface_tilt.setter def surface_tilt(self, value): + # TODO: don't access mount attributes directly? for array in self.arrays: - array.surface_tilt = value + array.mount.surface_tilt = value @property @_unwrap_single_value def surface_azimuth(self): - return tuple(array.surface_azimuth for array in self.arrays) + # TODO: don't access mount attributes directly? + return tuple(array.mount.surface_azimuth for array in self.arrays) @surface_azimuth.setter def surface_azimuth(self, value): + # TODO: don't access mount attributes directly? for array in self.arrays: - array.surface_azimuth = value + array.mount.surface_azimuth = value @property @_unwrap_single_value @@ -1098,20 +1105,15 @@ class Array: """ An Array is a set of of modules at the same orientation. - Specifically, an array is defined by tilt, azimuth, the + Specifically, an array is defined by its mount, the module parameters, the number of parallel strings of modules and the number of modules on each string. Parameters ---------- - surface_tilt: float or array-like, default 0 - Surface tilt angles in decimal degrees. - The tilt angle is defined as degrees from horizontal - (e.g. surface facing up = 0, surface facing horizon = 90) - - surface_azimuth: float or array-like, default 180 - Azimuth angle of the module surface. - North=0, East=90, South=180, West=270. + mount: FixedMount, SingleAxisTrackerMount, or other, default None + Mounting strategy for the array, used to determine module orientation. + If not provided, a FixedMount with zero tilt is used. albedo : None or float, default None The ground albedo. If ``None``, will attempt to use @@ -1152,15 +1154,18 @@ class Array: """ def __init__(self, - surface_tilt=0, surface_azimuth=180, + mount=None, albedo=None, surface_type=None, module=None, module_type=None, module_parameters=None, temperature_model_parameters=None, modules_per_string=1, strings=1, racking_model=None, name=None): - self.surface_tilt = surface_tilt - self.surface_azimuth = surface_azimuth + if mount is None: + from pvlib import mounts # avoid circular import issue + self.mount = mounts.FixedMount(0, 180) + else: + self.mount = mount self.surface_type = surface_type if albedo is None: @@ -1189,10 +1194,11 @@ def __init__(self, self.name = name def __repr__(self): - attrs = ['name', 'surface_tilt', 'surface_azimuth', 'module', + attrs = ['name', 'mount', 'module', 'albedo', 'racking_model', 'module_type', 'temperature_model_parameters', 'strings', 'modules_per_string'] + return 'Array:\n ' + '\n '.join( f'{attr}: {getattr(self, attr)}' for attr in attrs ) @@ -1213,7 +1219,6 @@ def _infer_temperature_model_params(self): return {} def _infer_cell_type(self): - """ Examines module_parameters and maps the Technology key for the CEC database and the Material key for the Sandia database to a common @@ -1271,7 +1276,9 @@ def get_aoi(self, solar_zenith, solar_azimuth): aoi : Series Then angle of incidence. """ - return irradiance.aoi(self.surface_tilt, self.surface_azimuth, + orientation = self.mount.get_orientation(solar_zenith, solar_azimuth) + return irradiance.aoi(orientation['surface_tilt'], + orientation['surface_azimuth'], solar_zenith, solar_azimuth) def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, @@ -1320,8 +1327,9 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, if airmass is None: airmass = atmosphere.get_relative_airmass(solar_zenith) - return irradiance.get_total_irradiance(self.surface_tilt, - self.surface_azimuth, + orientation = self.mount.get_orientation(solar_zenith, solar_azimuth) + return irradiance.get_total_irradiance(orientation['surface_tilt'], + orientation['surface_azimuth'], solar_zenith, solar_azimuth, dni, ghi, dhi, dni_extra=dni_extra, From 8a6d0e63953b7a5cb1dbf9f5f07aeec21d0e3dbc Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 23 Feb 2021 10:20:37 -0700 Subject: [PATCH 02/47] update pvsystem tests --- pvlib/tests/test_pvsystem.py | 47 ++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 5c0ef7b4a2..ea14aa8a6e 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -10,7 +10,7 @@ from numpy.testing import assert_allclose import unittest.mock as mock -from pvlib import inverter, pvsystem +from pvlib import inverter, pvsystem, mounts from pvlib import atmosphere from pvlib import iam as _iam from pvlib import irradiance @@ -1548,8 +1548,9 @@ def test_PVSystem_creation(): def test_PVSystem_multiple_array_creation(): - array_one = pvsystem.Array(surface_tilt=32) - array_two = pvsystem.Array(surface_tilt=15, module_parameters={'pdc0': 1}) + array_one = pvsystem.Array(mounts.FixedMount(surface_tilt=32)) + array_two = pvsystem.Array(mounts.FixedMount(surface_tilt=15), + module_parameters={'pdc0': 1}) pv_system = pvsystem.PVSystem(arrays=[array_one, array_two]) assert pv_system.surface_tilt == (32, 15) assert pv_system.surface_azimuth == (180, 180) @@ -1567,8 +1568,10 @@ def test_PVSystem_get_aoi(): def test_PVSystem_multiple_array_get_aoi(): system = pvsystem.PVSystem( - arrays=[pvsystem.Array(surface_tilt=15, surface_azimuth=135), - pvsystem.Array(surface_tilt=32, surface_azimuth=135)] + arrays=[pvsystem.Array(mounts.FixedMount(surface_tilt=15, + surface_azimuth=135)), + pvsystem.Array(mounts.FixedMount(surface_tilt=32, + surface_azimuth=135))] ) aoi_one, aoi_two = system.get_aoi(30, 225) assert np.round(aoi_two, 4) == 42.7408 @@ -1629,8 +1632,10 @@ def test_PVSystem_get_irradiance_model(mocker): def test_PVSystem_multi_array_get_irradiance(): - array_one = pvsystem.Array(surface_tilt=32, surface_azimuth=135) - array_two = pvsystem.Array(surface_tilt=5, surface_azimuth=150) + array_one = pvsystem.Array(mounts.FixedMount(surface_tilt=32, + surface_azimuth=135)) + array_two = pvsystem.Array(mounts.FixedMount(surface_tilt=5, + surface_azimuth=150)) system = pvsystem.PVSystem(arrays=[array_one, array_two]) location = Location(latitude=32, longitude=-111) times = pd.date_range(start='20160101 1200-0700', @@ -1778,8 +1783,9 @@ def test_PVSystem___repr__(): name: pv ftw Array: name: None - surface_tilt: 0 - surface_azimuth: 180 + mount: FixedMount: + surface_tilt: 0 + surface_azimuth: 180 module: blah albedo: 0.25 racking_model: None @@ -1793,8 +1799,10 @@ def test_PVSystem___repr__(): def test_PVSystem_multi_array___repr__(): system = pvsystem.PVSystem( - arrays=[pvsystem.Array(surface_tilt=30, surface_azimuth=100), - pvsystem.Array(surface_tilt=20, surface_azimuth=220, + arrays=[pvsystem.Array(mounts.FixedMount(surface_tilt=30, + surface_azimuth=100)), + pvsystem.Array(mounts.FixedMount(surface_tilt=20, + surface_azimuth=220), name='foo')], inverter='blarg', ) @@ -1802,8 +1810,9 @@ def test_PVSystem_multi_array___repr__(): name: None Array: name: None - surface_tilt: 30 - surface_azimuth: 100 + mount: FixedMount: + surface_tilt: 30 + surface_azimuth: 100 module: None albedo: 0.25 racking_model: None @@ -1813,8 +1822,9 @@ def test_PVSystem_multi_array___repr__(): modules_per_string: 1 Array: name: foo - surface_tilt: 20 - surface_azimuth: 220 + mount: FixedMount: + surface_tilt: 20 + surface_azimuth: 220 module: None albedo: 0.25 racking_model: None @@ -1828,7 +1838,7 @@ def test_PVSystem_multi_array___repr__(): def test_Array___repr__(): array = pvsystem.Array( - surface_tilt=10, surface_azimuth=100, + mount=mounts.FixedMount(surface_tilt=10, surface_azimuth=100), albedo=0.15, module_type='glass_glass', temperature_model_parameters={'a': -3.56}, racking_model='close_mount', @@ -1839,8 +1849,9 @@ def test_Array___repr__(): ) expected = """Array: name: biz - surface_tilt: 10 - surface_azimuth: 100 + mount: FixedMount: + surface_tilt: 10 + surface_azimuth: 100 module: baz albedo: 0.15 racking_model: close_mount From 6d4240e974b049a16e9cae8fd3390b38829c1259 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 23 Feb 2021 15:46:02 -0700 Subject: [PATCH 03/47] delete mounts module --- pvlib/__init__.py | 1 - pvlib/mounts.py | 48 -------------------------------- pvlib/pvsystem.py | 54 +++++++++++++++++++++++++++++++++--- pvlib/tests/test_pvsystem.py | 32 ++++++++++----------- 4 files changed, 66 insertions(+), 69 deletions(-) delete mode 100644 pvlib/mounts.py diff --git a/pvlib/__init__.py b/pvlib/__init__.py index 98a935c546..ff6b375017 100644 --- a/pvlib/__init__.py +++ b/pvlib/__init__.py @@ -12,7 +12,6 @@ ivtools, location, modelchain, - mounts, pvsystem, scaling, shading, diff --git a/pvlib/mounts.py b/pvlib/mounts.py deleted file mode 100644 index b91b3f4049..0000000000 --- a/pvlib/mounts.py +++ /dev/null @@ -1,48 +0,0 @@ -from pvlib import tracking - - -class FixedMount: - def __init__(self, surface_tilt=0, surface_azimuth=180): - self.surface_tilt = surface_tilt - self.surface_azimuth = surface_azimuth - - def __repr__(self): - return ( - 'FixedMount:' - f'\n surface_tilt: {self.surface_tilt}' - f'\n surface_azimuth: {self.surface_azimuth}' - ) - - def get_orientation(self, solar_zenith, solar_azimuth): - return { - 'surface_tilt': self.surface_tilt, - 'surface_azimuth': self.surface_azimuth, - } - - -class SingleAxisTrackerMount: - def __init__(self, axis_tilt, axis_azimuth, max_angle, backtrack, gcr, - cross_axis_tilt): - self.axis_tilt = axis_tilt - self.axis_azimuth = axis_azimuth - self.max_angle = max_angle - self.backtrack = backtrack - self.gcr = gcr - self.cross_axis_tilt = cross_axis_tilt - - def __repr__(self): - attrs = ['axis_tilt', 'axis_azimuth', 'max_angle', - 'backtrack', 'gcr', 'cross_axis_tilt'] - - return 'SingleAxisTrackerMount:\n ' + '\n '.join( - f'{attr}: {getattr(self, attr)}' for attr in attrs - ) - - def get_orientation(self, solar_zenith, solar_azimuth): - tracking_data = tracking.singleaxis( - solar_zenith, solar_azimuth, - self.axis_tilt, self.axis_azimuth, - self.max_angle, self.backtrack, - self.gcr, self.cross_axis_tilt - ) - return tracking_data diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 8185411125..fcc12c3593 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -190,9 +190,8 @@ def __init__(self, racking_model=None, losses_parameters=None, name=None): if arrays is None: - from pvlib import mounts self.arrays = (Array( - mounts.FixedMount(surface_tilt, surface_azimuth), + FixedMount(surface_tilt, surface_azimuth), albedo, surface_type, module, @@ -1162,8 +1161,7 @@ def __init__(self, modules_per_string=1, strings=1, racking_model=None, name=None): if mount is None: - from pvlib import mounts # avoid circular import issue - self.mount = mounts.FixedMount(0, 180) + self.mount = FixedMount(0, 180) else: self.mount = mount @@ -1381,6 +1379,54 @@ def get_iam(self, aoi, iam_model='physical'): raise ValueError(model + ' is not a valid IAM model') +class FixedMount: + def __init__(self, surface_tilt=0, surface_azimuth=180): + self.surface_tilt = surface_tilt + self.surface_azimuth = surface_azimuth + + def __repr__(self): + return ( + 'FixedMount:' + f'\n surface_tilt: {self.surface_tilt}' + f'\n surface_azimuth: {self.surface_azimuth}' + ) + + def get_orientation(self, solar_zenith, solar_azimuth): + return { + 'surface_tilt': self.surface_tilt, + 'surface_azimuth': self.surface_azimuth, + } + + +class SingleAxisTrackerMount: + def __init__(self, axis_tilt, axis_azimuth, max_angle, backtrack, gcr, + cross_axis_tilt): + self.axis_tilt = axis_tilt + self.axis_azimuth = axis_azimuth + self.max_angle = max_angle + self.backtrack = backtrack + self.gcr = gcr + self.cross_axis_tilt = cross_axis_tilt + + def __repr__(self): + attrs = ['axis_tilt', 'axis_azimuth', 'max_angle', + 'backtrack', 'gcr', 'cross_axis_tilt'] + + return 'SingleAxisTrackerMount:\n ' + '\n '.join( + f'{attr}: {getattr(self, attr)}' for attr in attrs + ) + + def get_orientation(self, solar_zenith, solar_azimuth): + from pvlib import tracking # avoid circular import issue + tracking_data = tracking.singleaxis( + solar_zenith, solar_azimuth, + self.axis_tilt, self.axis_azimuth, + self.max_angle, self.backtrack, + self.gcr, self.cross_axis_tilt + ) + return tracking_data + + def calcparams_desoto(effective_irradiance, temp_cell, alpha_sc, a_ref, I_L_ref, I_o_ref, R_sh_ref, R_s, EgRef=1.121, dEgdT=-0.0002677, diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index ea14aa8a6e..2cab5e5988 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -10,7 +10,7 @@ from numpy.testing import assert_allclose import unittest.mock as mock -from pvlib import inverter, pvsystem, mounts +from pvlib import inverter, pvsystem from pvlib import atmosphere from pvlib import iam as _iam from pvlib import irradiance @@ -1548,8 +1548,8 @@ def test_PVSystem_creation(): def test_PVSystem_multiple_array_creation(): - array_one = pvsystem.Array(mounts.FixedMount(surface_tilt=32)) - array_two = pvsystem.Array(mounts.FixedMount(surface_tilt=15), + array_one = pvsystem.Array(pvsystem.FixedMount(surface_tilt=32)) + array_two = pvsystem.Array(pvsystem.FixedMount(surface_tilt=15), module_parameters={'pdc0': 1}) pv_system = pvsystem.PVSystem(arrays=[array_one, array_two]) assert pv_system.surface_tilt == (32, 15) @@ -1568,10 +1568,10 @@ def test_PVSystem_get_aoi(): def test_PVSystem_multiple_array_get_aoi(): system = pvsystem.PVSystem( - arrays=[pvsystem.Array(mounts.FixedMount(surface_tilt=15, - surface_azimuth=135)), - pvsystem.Array(mounts.FixedMount(surface_tilt=32, - surface_azimuth=135))] + arrays=[pvsystem.Array(pvsystem.FixedMount(surface_tilt=15, + surface_azimuth=135)), + pvsystem.Array(pvsystem.FixedMount(surface_tilt=32, + surface_azimuth=135))] ) aoi_one, aoi_two = system.get_aoi(30, 225) assert np.round(aoi_two, 4) == 42.7408 @@ -1632,10 +1632,10 @@ def test_PVSystem_get_irradiance_model(mocker): def test_PVSystem_multi_array_get_irradiance(): - array_one = pvsystem.Array(mounts.FixedMount(surface_tilt=32, - surface_azimuth=135)) - array_two = pvsystem.Array(mounts.FixedMount(surface_tilt=5, - surface_azimuth=150)) + array_one = pvsystem.Array(pvsystem.FixedMount(surface_tilt=32, + surface_azimuth=135)) + array_two = pvsystem.Array(pvsystem.FixedMount(surface_tilt=5, + surface_azimuth=150)) system = pvsystem.PVSystem(arrays=[array_one, array_two]) location = Location(latitude=32, longitude=-111) times = pd.date_range(start='20160101 1200-0700', @@ -1799,10 +1799,10 @@ def test_PVSystem___repr__(): def test_PVSystem_multi_array___repr__(): system = pvsystem.PVSystem( - arrays=[pvsystem.Array(mounts.FixedMount(surface_tilt=30, - surface_azimuth=100)), - pvsystem.Array(mounts.FixedMount(surface_tilt=20, - surface_azimuth=220), + arrays=[pvsystem.Array(pvsystem.FixedMount(surface_tilt=30, + surface_azimuth=100)), + pvsystem.Array(pvsystem.FixedMount(surface_tilt=20, + surface_azimuth=220), name='foo')], inverter='blarg', ) @@ -1838,7 +1838,7 @@ def test_PVSystem_multi_array___repr__(): def test_Array___repr__(): array = pvsystem.Array( - mount=mounts.FixedMount(surface_tilt=10, surface_azimuth=100), + mount=pvsystem.FixedMount(surface_tilt=10, surface_azimuth=100), albedo=0.15, module_type='glass_glass', temperature_model_parameters={'a': -3.56}, racking_model='close_mount', From fc4064b27f2dbccb400e782a82ca4431aa3dbb50 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 23 Feb 2021 17:02:57 -0700 Subject: [PATCH 04/47] fix modelchain tests --- pvlib/tests/test_modelchain.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index c45fd5026f..bef95ffd9a 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -55,13 +55,13 @@ def cec_dc_snl_ac_arrays(cec_module_cs5p_220m, cec_inverter_parameters, module_parameters['dEgdT'] = -0.0002677 temp_model_params = sapm_temperature_cs5p_220m.copy() array_one = pvsystem.Array( - surface_tilt=32.2, surface_azimuth=180, + mount=pvsystem.FixedMount(surface_tilt=32.2, surface_azimuth=180), module=module_parameters['Name'], module_parameters=module_parameters.copy(), temperature_model_parameters=temp_model_params.copy() ) array_two = pvsystem.Array( - surface_tilt=42.2, surface_azimuth=220, + mount=pvsystem.FixedMount(surface_tilt=42.2, surface_azimuth=220), module=module_parameters['Name'], module_parameters=module_parameters.copy(), temperature_model_parameters=temp_model_params.copy() @@ -109,13 +109,13 @@ def pvsyst_dc_snl_ac_arrays(pvsyst_module_params, cec_inverter_parameters, module_parameters['b'] = 0.05 temp_model_params = sapm_temperature_cs5p_220m.copy() array_one = pvsystem.Array( - surface_tilt=32.2, surface_azimuth=180, + mount=pvsystem.FixedMount(surface_tilt=32.2, surface_azimuth=180), module=module, module_parameters=module_parameters.copy(), temperature_model_parameters=temp_model_params.copy() ) array_two = pvsystem.Array( - surface_tilt=42.2, surface_azimuth=220, + mount=pvsystem.FixedMount(surface_tilt=42.2, surface_azimuth=220), module=module, module_parameters=module_parameters.copy(), temperature_model_parameters=temp_model_params.copy() @@ -172,12 +172,12 @@ def pvwatts_dc_pvwatts_ac_system_arrays(sapm_temperature_cs5p_220m): temp_model_params = sapm_temperature_cs5p_220m.copy() inverter_parameters = {'pdc0': 220, 'eta_inv_nom': 0.95} array_one = pvsystem.Array( - surface_tilt=32.2, surface_azimuth=180, + mount=pvsystem.FixedMount(surface_tilt=32.2, surface_azimuth=180), module_parameters=module_parameters.copy(), temperature_model_parameters=temp_model_params.copy() ) array_two = pvsystem.Array( - surface_tilt=42.2, surface_azimuth=220, + mount=pvsystem.FixedMount(surface_tilt=42.2, surface_azimuth=220), module_parameters=module_parameters.copy(), temperature_model_parameters=temp_model_params.copy() ) @@ -276,13 +276,15 @@ def sapm_dc_snl_ac_system_Array(sapm_module_params, cec_inverter_parameters, module = 'Canadian_Solar_CS5P_220M___2009_' module_parameters = sapm_module_params.copy() temp_model_params = sapm_temperature_cs5p_220m.copy() - array_one = pvsystem.Array(surface_tilt=32, surface_azimuth=180, + array_one = pvsystem.Array(mount=pvsystem.FixedMount(surface_tilt=32, + surface_azimuth=180), albedo=0.2, module=module, module_parameters=module_parameters, temperature_model_parameters=temp_model_params, modules_per_string=1, strings=1) - array_two = pvsystem.Array(surface_tilt=15, surface_azimuth=180, + array_two = pvsystem.Array(mount=pvsystem.FixedMount(surface_tilt=15, + surface_azimuth=180), albedo=0.2, module=module, module_parameters=module_parameters, temperature_model_parameters=temp_model_params, @@ -300,13 +302,15 @@ def sapm_dc_snl_ac_system_same_arrays(sapm_module_params, module = 'Canadian_Solar_CS5P_220M___2009_' module_parameters = sapm_module_params.copy() temp_model_params = sapm_temperature_cs5p_220m.copy() - array_one = pvsystem.Array(surface_tilt=32.2, surface_azimuth=180, + array_one = pvsystem.Array(mount=pvsystem.FixedMount(surface_tilt=32.2, + surface_azimuth=180), module=module, module_parameters=module_parameters, temperature_model_parameters=temp_model_params, modules_per_string=1, strings=1) - array_two = pvsystem.Array(surface_tilt=32.2, surface_azimuth=180, + array_two = pvsystem.Array(mount=pvsystem.FixedMount(surface_tilt=32.2, + surface_azimuth=180), module=module, module_parameters=module_parameters, temperature_model_parameters=temp_model_params, @@ -369,12 +373,12 @@ def multi_array_sapm_dc_snl_ac_system( temp_model_parameters = sapm_temperature_cs5p_220m.copy() inverter_parameters = cec_inverter_parameters array_one = pvsystem.Array( - surface_tilt=32.2, surface_azimuth=180, + mount=pvsystem.FixedMount(surface_tilt=32.2, surface_azimuth=180), module_parameters=module_parameters, temperature_model_parameters=temp_model_parameters ) array_two = pvsystem.Array( - surface_tilt=32.2, surface_azimuth=220, + mount=pvsystem.FixedMount(surface_tilt=32.2, surface_azimuth=220), module_parameters=module_parameters, temperature_model_parameters=temp_model_parameters ) From e8f1404734b57af35e6d12349a61ea41e7f0a629 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 23 Feb 2021 17:52:46 -0700 Subject: [PATCH 05/47] some modifications to SingleAxisTracker --- pvlib/tests/test_tracking.py | 38 ++++++------------------------ pvlib/tracking.py | 45 +++++++++++++++++++++--------------- 2 files changed, 34 insertions(+), 49 deletions(-) diff --git a/pvlib/tests/test_tracking.py b/pvlib/tests/test_tracking.py index 248e3e3d53..f723e15fca 100644 --- a/pvlib/tests/test_tracking.py +++ b/pvlib/tests/test_tracking.py @@ -301,35 +301,6 @@ def test_SingleAxisTracker_creation(): assert system.inverter == 'blarg' -def test_SingleAxisTracker_one_array_only(): - system = tracking.SingleAxisTracker( - arrays=[pvsystem.Array( - module='foo', - surface_tilt=None, - surface_azimuth=None - )] - ) - assert system.module == 'foo' - with pytest.raises(ValueError, - match="SingleAxisTracker does not support " - r"multiple arrays\."): - tracking.SingleAxisTracker( - arrays=[pvsystem.Array(module='foo'), - pvsystem.Array(module='bar')] - ) - with pytest.raises(ValueError, - match="Array must not have surface_tilt "): - tracking.SingleAxisTracker(arrays=[pvsystem.Array(module='foo')]) - with pytest.raises(ValueError, - match="Array must not have surface_tilt "): - tracking.SingleAxisTracker( - arrays=[pvsystem.Array(surface_azimuth=None)]) - with pytest.raises(ValueError, - match="Array must not have surface_tilt "): - tracking.SingleAxisTracker( - arrays=[pvsystem.Array(surface_tilt=None)]) - - def test_SingleAxisTracker_tracking(): system = tracking.SingleAxisTracker(max_angle=90, axis_tilt=30, axis_azimuth=180, gcr=2.0/7.0, @@ -444,8 +415,13 @@ def test_SingleAxisTracker___repr__(): name: None Array: name: None - surface_tilt: None - surface_azimuth: None + mount: SingleAxisTrackerMount: + axis_tilt: 0 + axis_azimuth: 0 + max_angle: 45 + backtrack: True + gcr: 0.25 + cross_axis_tilt: 0.0 module: blah albedo: 0.25 racking_model: None diff --git a/pvlib/tracking.py b/pvlib/tracking.py index 8dd9e43461..bdf9809958 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -2,7 +2,9 @@ import pandas as pd from pvlib.tools import cosd, sind, tand -from pvlib.pvsystem import PVSystem, _unwrap_single_value +from pvlib.pvsystem import ( + PVSystem, Array, SingleAxisTrackerMount, _unwrap_single_value +) from pvlib import irradiance, atmosphere @@ -76,20 +78,27 @@ class SingleAxisTracker(PVSystem): def __init__(self, axis_tilt=0, axis_azimuth=0, max_angle=90, backtrack=True, gcr=2.0/7.0, cross_axis_tilt=0.0, **kwargs): - arrays = kwargs.get('arrays', []) - if len(arrays) > 1: - raise ValueError("SingleAxisTracker does not support " - "multiple arrays.") - elif len(arrays) == 1: - surface_tilt = arrays[0].surface_tilt - surface_azimuth = arrays[0].surface_azimuth - if surface_tilt is not None or surface_azimuth is not None: - raise ValueError( - "Array must not have surface_tilt or " - "surface_azimuth assigned. You must pass an " - "Array with these fields set to None." - ) - + mount = SingleAxisTrackerMount(axis_tilt, axis_azimuth, max_angle, + backtrack, gcr, cross_axis_tilt) + + array_defaults = { + 'albedo': None, 'surface_type': None, 'module': None, + 'module_type': None, 'module_parameters': None, + 'temperature_model_parameters': None, + 'modules_per_string': 1, + 'racking_model': None, + } + array_kwargs = { + key: kwargs.get(key, array_defaults[key]) for key in array_defaults + } + # strings/strings_per_inverter is a special case + array_kwargs['strings'] = kwargs.get('strings_per_inverter', 1) + + array = Array(mount=mount, **array_kwargs) + pass_through_kwargs = { # other args to pass to PVSystem() + k: v for k, v in kwargs.items() if k not in array_defaults + } + # leave these in case someone is using them self.axis_tilt = axis_tilt self.axis_azimuth = axis_azimuth self.max_angle = max_angle @@ -97,10 +106,10 @@ def __init__(self, axis_tilt=0, axis_azimuth=0, max_angle=90, self.gcr = gcr self.cross_axis_tilt = cross_axis_tilt - kwargs['surface_tilt'] = None - kwargs['surface_azimuth'] = None + pass_through_kwargs['surface_tilt'] = None + pass_through_kwargs['surface_azimuth'] = None - super().__init__(**kwargs) + super().__init__(arrays=[array], **pass_through_kwargs) def __repr__(self): attrs = ['axis_tilt', 'axis_azimuth', 'max_angle', 'backtrack', 'gcr', From 02a2b2791a7342d9fc155d5f068b9166ff80b660 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sat, 27 Feb 2021 15:11:04 -0700 Subject: [PATCH 06/47] changes from review --- pvlib/modelchain.py | 4 +- pvlib/pvsystem.py | 194 +++++++++++++++++++++++++-------- pvlib/tests/test_modelchain.py | 11 +- pvlib/tests/test_pvsystem.py | 111 +++++++++++++------ 4 files changed, 239 insertions(+), 81 deletions(-) diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py index 1c07857745..8fe4dc0c70 100644 --- a/pvlib/modelchain.py +++ b/pvlib/modelchain.py @@ -644,7 +644,9 @@ def orientation_strategy(self, strategy): strategy = None if strategy is not None: - self.system.surface_tilt, self.system.surface_azimuth = \ + # TODO: this is probably not what we want to do here + (self.system.arrays[0].mount.surface_tilt, + self.system.arrays[0].mount.surface_azimuth) = \ get_orientation(strategy, latitude=self.location.latitude) self._orientation_strategy = strategy diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index fcc12c3593..62544ba479 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -11,8 +11,10 @@ from urllib.request import urlopen import numpy as np import pandas as pd +from abc import ABC, abstractmethod -from pvlib._deprecation import deprecated +import warnings +from pvlib._deprecation import deprecated, pvlibDeprecationWarning from pvlib import (atmosphere, iam, inverter, irradiance, singlediode as _singlediode, temperature) @@ -248,7 +250,6 @@ def _validate_per_array(self, values, system_wide=False): @_unwrap_single_value def _infer_cell_type(self): - """ Examines module_parameters and maps the Technology key for the CEC database and the Material key for the Sandia database to a common @@ -1046,28 +1047,60 @@ def temperature_model_parameters(self, value): array.temperature_model_parameters = value @property - @_unwrap_single_value def surface_tilt(self): - # TODO: don't access mount attributes directly? - return tuple(array.mount.surface_tilt for array in self.arrays) + if len(self.arrays) == 1: + msg = ( + 'PVSystem.surface_tilt attribute is deprecated. ' + 'Use PVSystem.arrays[0].mount.surface_tilt.' + ) + warnings.warn(msg, pvlibDeprecationWarning) + else: + raise AttributeError( + 'PVSystem.surface_tilt not supported for multi-array systems. ' + 'Use PVSystem.arrays[i].mount.surface_tilt.') + return self.arrays[0].mount.surface_tilt @surface_tilt.setter def surface_tilt(self, value): - # TODO: don't access mount attributes directly? - for array in self.arrays: - array.mount.surface_tilt = value + if len(self.arrays) == 1: + msg = ( + 'PVSystem.surface_tilt attribute is deprecated. ' + 'Use PVSystem.arrays[0].mount.surface_tilt.' + ) + warnings.warn(msg, pvlibDeprecationWarning) + else: + raise AttributeError( + 'PVSystem.surface_tilt not supported for multi-array systems. ' + 'Use PVSystem.arrays[i].mount.surface_tilt.') + self.arrays[0].mount.surface_tilt = value @property - @_unwrap_single_value def surface_azimuth(self): - # TODO: don't access mount attributes directly? - return tuple(array.mount.surface_azimuth for array in self.arrays) + if len(self.arrays) == 1: + msg = ( + 'PVSystem.surface_azimuth attribute is deprecated. ' + 'Use PVSystem.arrays[0].mount.surface_azimuth.' + ) + warnings.warn(msg, pvlibDeprecationWarning) + else: + raise AttributeError( + 'PVSystem.surface_azimuth not supported for multi-array ' + 'systems. Use PVSystem.arrays[i].mount.surface_azimuth.') + return self.arrays[0].mount.surface_azimuth @surface_azimuth.setter def surface_azimuth(self, value): - # TODO: don't access mount attributes directly? - for array in self.arrays: - array.mount.surface_azimuth = value + if len(self.arrays) == 1: + msg = ( + 'PVSystem.surface_azimuth attribute is deprecated. ' + 'Use PVSystem.arrays[0].mount.surface_azimuth.' + ) + warnings.warn(msg, pvlibDeprecationWarning) + else: + raise AttributeError( + 'PVSystem.surface_azimuth not supported for multi-array ' + 'systems. Use PVSystem.arrays[i].mount.surface_azimuth.') + self.arrays[0].mount.surface_azimuth = value @property @_unwrap_single_value @@ -1110,7 +1143,7 @@ class Array: Parameters ---------- - mount: FixedMount, SingleAxisTrackerMount, or other, default None + mount: FixedMount, SingleAxisTrackerMount, or other Mounting strategy for the array, used to determine module orientation. If not provided, a FixedMount with zero tilt is used. @@ -1152,19 +1185,14 @@ class Array: """ - def __init__(self, - mount=None, + def __init__(self, mount, albedo=None, surface_type=None, module=None, module_type=None, module_parameters=None, temperature_model_parameters=None, modules_per_string=1, strings=1, racking_model=None, name=None): - if mount is None: - self.mount = FixedMount(0, 180) - else: - self.mount = mount - + self.mount = mount self.surface_type = surface_type if albedo is None: self.albedo = irradiance.SURFACE_ALBEDOS.get(surface_type, 0.25) @@ -1274,7 +1302,7 @@ def get_aoi(self, solar_zenith, solar_azimuth): aoi : Series Then angle of incidence. """ - orientation = self.mount.get_orientation(solar_zenith, solar_azimuth) + orientation = self.mount.calculate_orientation(solar_zenith, solar_azimuth) return irradiance.aoi(orientation['surface_tilt'], orientation['surface_azimuth'], solar_zenith, solar_azimuth) @@ -1325,7 +1353,7 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, if airmass is None: airmass = atmosphere.get_relative_airmass(solar_zenith) - orientation = self.mount.get_orientation(solar_zenith, solar_azimuth) + orientation = self.mount.calculate_orientation(solar_zenith, solar_azimuth) return irradiance.get_total_irradiance(orientation['surface_tilt'], orientation['surface_azimuth'], solar_zenith, solar_azimuth, @@ -1379,44 +1407,120 @@ def get_iam(self, aoi, iam_model='physical'): raise ValueError(model + ' is not a valid IAM model') -class FixedMount: +class AbstractMount(ABC): + def __repr__(self): + classname = self.__class__.__name__ + return f'{classname}:\n ' + '\n '.join( + f'{attr}: {getattr(self, attr)}' for attr in self._repr_attrs + ) + + @abstractmethod + def calculate_orientation(self, solar_zenith, solar_azimuth): + """ + Determine module orientation. + + Parameters + ---------- + solar_zenith : numeric + Solar apparent zenith angle [degrees] + solar_azimuth : numeric + Solar azimuth angle [degrees] + + Returns + ------- + orientation : dict-like + A dict-like object with keys `'surface_tilt', 'surface_azimuth'` + """ + pass + + +class FixedMount(AbstractMount): + """ + Racking at fixed (static) orientation. + + Parameters + ---------- + surface_tilt : float, default 0 + Surface tilt angle. The tilt angle is defined as angle from horizontal + (e.g. surface facing up = 0, surface facing horizon = 90) [degrees] + + surface_azimuth : float, default 180 + Azimuth angle of the module surface. North=0, East=90, South=180, + West=270. [degrees] + """ + def __init__(self, surface_tilt=0, surface_azimuth=180): self.surface_tilt = surface_tilt self.surface_azimuth = surface_azimuth + self._repr_attrs = ['surface_tilt', 'surface_azimuth'] - def __repr__(self): - return ( - 'FixedMount:' - f'\n surface_tilt: {self.surface_tilt}' - f'\n surface_azimuth: {self.surface_azimuth}' - ) - - def get_orientation(self, solar_zenith, solar_azimuth): + def calculate_orientation(self, solar_zenith, solar_azimuth): + # note -- docstring is automatically inherited from AbstractMount return { 'surface_tilt': self.surface_tilt, 'surface_azimuth': self.surface_azimuth, } -class SingleAxisTrackerMount: - def __init__(self, axis_tilt, axis_azimuth, max_angle, backtrack, gcr, - cross_axis_tilt): +class SingleAxisTrackerMount(AbstractMount): + """ + Single-axis tracker racking for dynamic solar tracking. + + Parameters + ---------- + axis_tilt : float, default 0 + The tilt of the axis of rotation (i.e, the y-axis defined by + axis_azimuth) with respect to horizontal. [degrees] + + axis_azimuth : float, default 180 + A value denoting the compass direction along which the axis of + rotation lies, measured east of north. [degrees] + + max_angle : float, default 90 + A value denoting the maximum rotation angle + of the one-axis tracker from its horizontal position (horizontal + if axis_tilt = 0). A max_angle of 90 degrees allows the tracker + to rotate to a vertical position to point the panel towards a + horizon. max_angle of 180 degrees allows for full rotation. [degrees] + + backtrack : bool, default True + Controls whether the tracker has the capability to "backtrack" + to avoid row-to-row shading. False denotes no backtrack + capability. True denotes backtrack capability. + + gcr : float, default 2.0/7.0 + A value denoting the ground coverage ratio of a tracker system + which utilizes backtracking; i.e. the ratio between the PV array + surface area to total ground area. A tracker system with modules + 2 meters wide, centered on the tracking axis, with 6 meters + between the tracking axes has a gcr of 2/6=0.333. If gcr is not + provided, a gcr of 2/7 is default. gcr must be <=1. [unitless] + + cross_axis_tilt : float, default 0.0 + The angle, relative to horizontal, of the line formed by the + intersection between the slope containing the tracker axes and a plane + perpendicular to the tracker axes. Cross-axis tilt should be specified + using a right-handed convention. For example, trackers with axis + azimuth of 180 degrees (heading south) will have a negative cross-axis + tilt if the tracker axes plane slopes down to the east and positive + cross-axis tilt if the tracker axes plane slopes up to the east. Use + :func:`~pvlib.tracking.calc_cross_axis_tilt` to calculate + `cross_axis_tilt`. [degrees] + """ + + def __init__(self, axis_tilt=0, axis_azimuth=180, max_angle=90, + backtrack=True, gcr=2.0/7.0, cross_axis_tilt=0): self.axis_tilt = axis_tilt self.axis_azimuth = axis_azimuth self.max_angle = max_angle self.backtrack = backtrack self.gcr = gcr self.cross_axis_tilt = cross_axis_tilt + self._repr_attrs = ['axis_tilt', 'axis_azimuth', 'max_angle', + 'backtrack', 'gcr', 'cross_axis_tilt'] - def __repr__(self): - attrs = ['axis_tilt', 'axis_azimuth', 'max_angle', - 'backtrack', 'gcr', 'cross_axis_tilt'] - - return 'SingleAxisTrackerMount:\n ' + '\n '.join( - f'{attr}: {getattr(self, attr)}' for attr in attrs - ) - - def get_orientation(self, solar_zenith, solar_azimuth): + def calculate_orientation(self, solar_zenith, solar_azimuth): + # note -- docstring is automatically inherited from AbstractMount from pvlib import tracking # avoid circular import issue tracking_data = tracking.singleaxis( solar_zenith, solar_azimuth, diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index bef95ffd9a..06159dbfdb 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -349,8 +349,8 @@ def test_orientation_strategy(strategy, expected, sapm_dc_snl_ac_system, # the || accounts for the coercion of 'None' to None assert (mc.orientation_strategy == strategy or mc.orientation_strategy is None) - assert sapm_dc_snl_ac_system.surface_tilt == expected[0] - assert sapm_dc_snl_ac_system.surface_azimuth == expected[1] + assert sapm_dc_snl_ac_system.arrays[0].mount.surface_tilt == expected[0] + assert sapm_dc_snl_ac_system.arrays[0].mount.surface_azimuth == expected[1] def test_run_model_with_irradiance(sapm_dc_snl_ac_system, location): @@ -1373,6 +1373,7 @@ def test_infer_ac_model_invalid_params(location): module_parameters = {'pdc0': 1, 'gamma_pdc': 1} system = pvsystem.PVSystem( arrays=[pvsystem.Array( + mount=pvsystem.FixedMount(0, 180), module_parameters=module_parameters )], inverter_parameters={'foo': 1, 'bar': 2} @@ -1869,10 +1870,13 @@ def test_inconsistent_array_params(location, different_module_system = pvsystem.PVSystem( arrays=[ pvsystem.Array( + mount=pvsystem.FixedMount(0, 180), module_parameters=sapm_module_params), pvsystem.Array( + mount=pvsystem.FixedMount(0, 180), module_parameters=cec_module_params), pvsystem.Array( + mount=pvsystem.FixedMount(0, 180), module_parameters=cec_module_params)] ) with pytest.raises(ValueError, match=module_error): @@ -1880,16 +1884,19 @@ def test_inconsistent_array_params(location, different_temp_system = pvsystem.PVSystem( arrays=[ pvsystem.Array( + mount=pvsystem.FixedMount(0, 180), module_parameters=cec_module_params, temperature_model_parameters={'a': 1, 'b': 1, 'deltaT': 1}), pvsystem.Array( + mount=pvsystem.FixedMount(0, 180), module_parameters=cec_module_params, temperature_model_parameters={'a': 2, 'b': 2, 'deltaT': 2}), pvsystem.Array( + mount=pvsystem.FixedMount(0, 180), module_parameters=cec_module_params, temperature_model_parameters={'b': 3, 'deltaT': 3})] ) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 2cab5e5988..ba29c06f90 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -36,8 +36,10 @@ def test_PVSystem_get_iam(mocker, iam_model, model_params): def test_PVSystem_multi_array_get_iam(): model_params = {'b': 0.05} system = pvsystem.PVSystem( - arrays=[pvsystem.Array(module_parameters=model_params), - pvsystem.Array(module_parameters=model_params)] + arrays=[pvsystem.Array(mount=pvsystem.FixedMount(0, 180), + module_parameters=model_params), + pvsystem.Array(mount=pvsystem.FixedMount(0, 180), + module_parameters=model_params)] ) iam = system.get_iam((1, 5), iam_model='ashrae') assert len(iam) == 2 @@ -226,8 +228,10 @@ def test_PVSystem_sapm(sapm_module_params, mocker): def test_PVSystem_multi_array_sapm(sapm_module_params): system = pvsystem.PVSystem( - arrays=[pvsystem.Array(module_parameters=sapm_module_params), - pvsystem.Array(module_parameters=sapm_module_params)] + arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180), + module_parameters=sapm_module_params), + pvsystem.Array(pvsystem.FixedMount(0, 180), + module_parameters=sapm_module_params)] ) effective_irradiance = (100, 500) temp_cell = (15, 25) @@ -274,8 +278,10 @@ def test_PVSystem_sapm_spectral_loss(sapm_module_params, mocker): def test_PVSystem_multi_array_sapm_spectral_loss(sapm_module_params): system = pvsystem.PVSystem( - arrays=[pvsystem.Array(module_parameters=sapm_module_params), - pvsystem.Array(module_parameters=sapm_module_params)] + arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180), + module_parameters=sapm_module_params), + pvsystem.Array(pvsystem.FixedMount(0, 180), + module_parameters=sapm_module_params)] ) loss_one, loss_two = system.sapm_spectral_loss(2) assert loss_one == loss_two @@ -308,10 +314,12 @@ def test_PVSystem_multi_array_first_solar_spectral_loss(): system = pvsystem.PVSystem( arrays=[ pvsystem.Array( + mount=pvsystem.FixedMount(0, 180), module_parameters={'Technology': 'mc-Si'}, module_type='multisi' ), pvsystem.Array( + mount=pvsystem.FixedMount(0, 180), module_parameters={'Technology': 'mc-Si'}, module_type='multisi' ) @@ -363,8 +371,10 @@ def test_PVSystem_sapm_effective_irradiance(sapm_module_params, mocker): def test_PVSystem_multi_array_sapm_effective_irradiance(sapm_module_params): system = pvsystem.PVSystem( - arrays=[pvsystem.Array(module_parameters=sapm_module_params), - pvsystem.Array(module_parameters=sapm_module_params)] + arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180), + module_parameters=sapm_module_params), + pvsystem.Array(pvsystem.FixedMount(0, 180), + module_parameters=sapm_module_params)] ) poa_direct = (500, 900) poa_diffuse = (50, 100) @@ -393,10 +403,12 @@ def two_array_system(pvsyst_module_params, cec_module_params): return pvsystem.PVSystem( arrays=[ pvsystem.Array( + mount=pvsystem.FixedMount(0, 180), temperature_model_parameters=temperature_model, module_parameters=module_params ), pvsystem.Array( + mount=pvsystem.FixedMount(0, 180), temperature_model_parameters=temperature_model, module_parameters=module_params ) @@ -453,8 +465,10 @@ def test_PVSystem_multi_array_sapm_celltemp_different_arrays(): temp_model_two = temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][ 'close_mount_glass_glass'] system = pvsystem.PVSystem( - arrays=[pvsystem.Array(temperature_model_parameters=temp_model_one), - pvsystem.Array(temperature_model_parameters=temp_model_two)] + arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180), + temperature_model_parameters=temp_model_one), + pvsystem.Array(pvsystem.FixedMount(0, 180), + temperature_model_parameters=temp_model_two)] ) temp_one, temp_two = system.sapm_celltemp( (1000, 1000), 25, 1 @@ -672,19 +686,22 @@ def test_PVSystem_fuentes_celltemp_override(mocker): def test_Array__infer_temperature_model_params(): - array = pvsystem.Array(module_parameters={}, + array = pvsystem.Array(mount=pvsystem.FixedMount(0, 180), + module_parameters={}, racking_model='open_rack', module_type='glass_polymer') expected = temperature.TEMPERATURE_MODEL_PARAMETERS[ 'sapm']['open_rack_glass_polymer'] assert expected == array._infer_temperature_model_params() - array = pvsystem.Array(module_parameters={}, + array = pvsystem.Array(mount=pvsystem.FixedMount(0, 180), + module_parameters={}, racking_model='freestanding', module_type='glass_polymer') expected = temperature.TEMPERATURE_MODEL_PARAMETERS[ 'pvsyst']['freestanding'] assert expected == array._infer_temperature_model_params() - array = pvsystem.Array(module_parameters={}, + array = pvsystem.Array(mount=pvsystem.FixedMount(0, 180), + module_parameters={}, racking_model='insulated', module_type=None) expected = temperature.TEMPERATURE_MODEL_PARAMETERS[ @@ -693,7 +710,8 @@ def test_Array__infer_temperature_model_params(): def test_Array__infer_cell_type(): - array = pvsystem.Array(module_parameters={}) + array = pvsystem.Array(mount=pvsystem.FixedMount(0, 180), + module_parameters={}) assert array._infer_cell_type() is None @@ -1359,8 +1377,10 @@ def test_PVSystem_scale_voltage_current_power(mocker): def test_PVSystem_multi_scale_voltage_current_power(mocker): data = (1, 2) system = pvsystem.PVSystem( - arrays=[pvsystem.Array(modules_per_string=2, strings=3), - pvsystem.Array(modules_per_string=3, strings=5)] + arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180), + modules_per_string=2, strings=3), + pvsystem.Array(pvsystem.FixedMount(0, 180), + modules_per_string=3, strings=5)] ) m = mocker.patch( 'pvlib.pvsystem.scale_voltage_current_power', autospec=True @@ -1407,7 +1427,8 @@ def test_PVSystem_snlinverter(cec_inverter_parameters): def test_PVSystem_get_ac_sandia_multi(cec_inverter_parameters, mocker): inv_fun = mocker.spy(inverter, 'sandia_multi') system = pvsystem.PVSystem( - arrays=[pvsystem.Array(), pvsystem.Array()], + arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180)), + pvsystem.Array(pvsystem.FixedMount(0, 180))], inverter=cec_inverter_parameters['Name'], inverter_parameters=cec_inverter_parameters, ) @@ -1454,7 +1475,8 @@ def test_PVSystem_get_ac_pvwatts_multi( systems = [pvwatts_system_defaults, pvwatts_system_kwargs] for base_sys, exp in zip(systems, expected): system = pvsystem.PVSystem( - arrays=[pvsystem.Array(), pvsystem.Array()], + arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180)), + pvsystem.Array(pvsystem.FixedMount(0, 180),)], inverter_parameters=base_sys.inverter_parameters, ) pdcs = pd.Series([0., 25., 50.]) @@ -1496,7 +1518,7 @@ def test_PVSystem_get_ac_single_array_tuple_input( 'sandia': pd.Series([-0.020000, 132.004308, 250.000000]) } system = pvsystem.PVSystem( - arrays=[pvsystem.Array()], + arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180))], inverter_parameters=inverter_parameters[model] ) ac = system.get_ac(p_dc=(pdcs[model],), v_dc=(vdcs[model],), model=model) @@ -1522,7 +1544,8 @@ def test_PVSystem_get_ac_adr(adr_inverter_parameters, mocker): def test_PVSystem_get_ac_adr_multi(adr_inverter_parameters): system = pvsystem.PVSystem( - arrays=[pvsystem.Array(), pvsystem.Array()], + arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180)), + pvsystem.Array(pvsystem.FixedMount(0, 180))], inverter_parameters=adr_inverter_parameters, ) pdcs = pd.Series([135, 1232, 1170, 420, 551]) @@ -1552,8 +1575,6 @@ def test_PVSystem_multiple_array_creation(): array_two = pvsystem.Array(pvsystem.FixedMount(surface_tilt=15), module_parameters={'pdc0': 1}) pv_system = pvsystem.PVSystem(arrays=[array_one, array_two]) - assert pv_system.surface_tilt == (32, 15) - assert pv_system.surface_azimuth == (180, 180) assert pv_system.module_parameters == ({}, {'pdc0': 1}) assert pv_system.arrays == (array_one, array_two) with pytest.raises(TypeError): @@ -1674,8 +1695,8 @@ def test_PVSystem_multi_array_get_irradiance_multi_irrad(): for each array when different GHI/DHI/DNI input is given. For the later case we verify that the correct irradiance data is passed to each array. """ - array_one = pvsystem.Array() - array_two = pvsystem.Array() + array_one = pvsystem.Array(pvsystem.FixedMount(0, 180)) + array_two = pvsystem.Array(pvsystem.FixedMount(0, 180)) system = pvsystem.PVSystem(arrays=[array_one, array_two]) location = Location(latitude=32, longitude=-111) times = pd.date_range(start='20160101 1200-0700', @@ -1744,7 +1765,7 @@ def test_PVSystem_change_surface_azimuth(): def test_PVSystem_get_albedo(two_array_system): system = pvsystem.PVSystem( - arrays=[pvsystem.Array(albedo=0.5)] + arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180), albedo=0.5)] ) assert system.albedo == 0.5 assert two_array_system.albedo == (0.25, 0.25) @@ -1752,24 +1773,26 @@ def test_PVSystem_get_albedo(two_array_system): def test_PVSystem_modules_per_string(): system = pvsystem.PVSystem( - arrays=[pvsystem.Array(modules_per_string=1), - pvsystem.Array(modules_per_string=2)] + arrays=[ + pvsystem.Array(pvsystem.FixedMount(0, 180), modules_per_string=1), + pvsystem.Array(pvsystem.FixedMount(0, 180), modules_per_string=2)] ) assert system.modules_per_string == (1, 2) system = pvsystem.PVSystem( - arrays=[pvsystem.Array(modules_per_string=5)] + arrays=[ + pvsystem.Array(pvsystem.FixedMount(0, 180), modules_per_string=5)] ) assert system.modules_per_string == 5 def test_PVSystem_strings_per_inverter(): system = pvsystem.PVSystem( - arrays=[pvsystem.Array(strings=2), - pvsystem.Array(strings=1)] + arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180), strings=2), + pvsystem.Array(pvsystem.FixedMount(0, 180), strings=1)] ) assert system.strings_per_inverter == (2, 1) system = pvsystem.PVSystem( - arrays=[pvsystem.Array(strings=5)] + arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180), strings=5)] ) assert system.strings_per_inverter == 5 @@ -1952,12 +1975,14 @@ def test_PVSystem_multiple_array_pvwatts_dc(): 'pdc0': 100, 'gamma_pdc': -0.003, 'temp_ref': 20 } array_one = pvsystem.Array( + pvsystem.FixedMount(0, 180), module_parameters=array_one_module_parameters ) array_two_module_parameters = { 'pdc0': 150, 'gamma_pdc': -0.002, 'temp_ref': 25 } array_two = pvsystem.Array( + pvsystem.FixedMount(0, 180), module_parameters=array_two_module_parameters ) system = pvsystem.PVSystem(arrays=[array_one, array_two]) @@ -1977,7 +2002,9 @@ def test_PVSystem_multiple_array_pvwatts_dc(): def test_PVSystem_multiple_array_pvwatts_dc_value_error(): system = pvsystem.PVSystem( - arrays=[pvsystem.Array(), pvsystem.Array(), pvsystem.Array()] + arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180)), + pvsystem.Array(pvsystem.FixedMount(0, 180)), + pvsystem.Array(pvsystem.FixedMount(0, 180))] ) error_message = 'Length mismatch for per-array parameter' with pytest.raises(ValueError, match=error_message): @@ -2035,7 +2062,9 @@ def test_PVSystem_pvwatts_ac_kwargs(pvwatts_system_kwargs, mocker): def test_PVSystem_num_arrays(): system_one = pvsystem.PVSystem() - system_two = pvsystem.PVSystem(arrays=[pvsystem.Array(), pvsystem.Array()]) + system_two = pvsystem.PVSystem(arrays=[ + pvsystem.Array(pvsystem.FixedMount(0, 180)), + pvsystem.Array(pvsystem.FixedMount(0, 180))]) assert system_one.num_arrays == 1 assert system_two.num_arrays == 2 @@ -2055,3 +2084,19 @@ def test_combine_loss_factors(): def test_no_extra_kwargs(): with pytest.raises(TypeError, match="arbitrary_kwarg"): pvsystem.PVSystem(arbitrary_kwarg='value') + + +def test_deprecated_attributes_single(pvwatts_system_defaults): + match = r"Use PVSystem.arrays\[0\].mount.surface_(tilt|azimuth)" + with pytest.warns(pvlibDeprecationWarning, match=match): + pvwatts_system_defaults.surface_tilt + with pytest.warns(pvlibDeprecationWarning, match=match): + pvwatts_system_defaults.surface_azimuth + + +def test_deprecated_attributes_multi(two_array_system): + match = "not supported for multi-array systems" + with pytest.raises(AttributeError, match=match): + two_array_system.surface_tilt + with pytest.raises(AttributeError, match=match): + two_array_system.surface_azimuth From 0be45aef2e6603a470c9318fdc04cd490c66a8ca Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sat, 27 Feb 2021 15:12:23 -0700 Subject: [PATCH 07/47] stickler --- pvlib/pvsystem.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 62544ba479..c90d0c6d24 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1302,7 +1302,8 @@ def get_aoi(self, solar_zenith, solar_azimuth): aoi : Series Then angle of incidence. """ - orientation = self.mount.calculate_orientation(solar_zenith, solar_azimuth) + orientation = self.mount.calculate_orientation(solar_zenith, + solar_azimuth) return irradiance.aoi(orientation['surface_tilt'], orientation['surface_azimuth'], solar_zenith, solar_azimuth) @@ -1353,7 +1354,8 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, if airmass is None: airmass = atmosphere.get_relative_airmass(solar_zenith) - orientation = self.mount.calculate_orientation(solar_zenith, solar_azimuth) + orientation = self.mount.calculate_orientation(solar_zenith, + solar_azimuth) return irradiance.get_total_irradiance(orientation['surface_tilt'], orientation['surface_azimuth'], solar_zenith, solar_azimuth, From e1bdc67e05f0d07a135f44c74caf91530794085f Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sun, 7 Mar 2021 08:37:51 -0700 Subject: [PATCH 08/47] use dataclasses for mounts --- pvlib/pvsystem.py | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index c90d0c6d24..0181621bf3 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -11,6 +11,7 @@ from urllib.request import urlopen import numpy as np import pandas as pd +from dataclasses import dataclass, field from abc import ABC, abstractmethod import warnings @@ -1409,12 +1410,8 @@ def get_iam(self, aoi, iam_model='physical'): raise ValueError(model + ' is not a valid IAM model') +@dataclass class AbstractMount(ABC): - def __repr__(self): - classname = self.__class__.__name__ - return f'{classname}:\n ' + '\n '.join( - f'{attr}: {getattr(self, attr)}' for attr in self._repr_attrs - ) @abstractmethod def calculate_orientation(self, solar_zenith, solar_azimuth): @@ -1436,6 +1433,7 @@ def calculate_orientation(self, solar_zenith, solar_azimuth): pass +@dataclass class FixedMount(AbstractMount): """ Racking at fixed (static) orientation. @@ -1451,10 +1449,8 @@ class FixedMount(AbstractMount): West=270. [degrees] """ - def __init__(self, surface_tilt=0, surface_azimuth=180): - self.surface_tilt = surface_tilt - self.surface_azimuth = surface_azimuth - self._repr_attrs = ['surface_tilt', 'surface_azimuth'] + surface_tilt: float = field(default=0) + surface_azimuth: float = field(default=180) def calculate_orientation(self, solar_zenith, solar_azimuth): # note -- docstring is automatically inherited from AbstractMount @@ -1464,6 +1460,7 @@ def calculate_orientation(self, solar_zenith, solar_azimuth): } +@dataclass class SingleAxisTrackerMount(AbstractMount): """ Single-axis tracker racking for dynamic solar tracking. @@ -1509,17 +1506,12 @@ class SingleAxisTrackerMount(AbstractMount): :func:`~pvlib.tracking.calc_cross_axis_tilt` to calculate `cross_axis_tilt`. [degrees] """ - - def __init__(self, axis_tilt=0, axis_azimuth=180, max_angle=90, - backtrack=True, gcr=2.0/7.0, cross_axis_tilt=0): - self.axis_tilt = axis_tilt - self.axis_azimuth = axis_azimuth - self.max_angle = max_angle - self.backtrack = backtrack - self.gcr = gcr - self.cross_axis_tilt = cross_axis_tilt - self._repr_attrs = ['axis_tilt', 'axis_azimuth', 'max_angle', - 'backtrack', 'gcr', 'cross_axis_tilt'] + axis_tilt: float = field(default=0) + axis_azimuth: float = field(default=0) + max_angle: float = field(default=90) + backtrack: bool = field(default=True) + gcr: float = field(default=2/7) + cross_axis_tilt: float = field(default=0) def calculate_orientation(self, solar_zenith, solar_azimuth): # note -- docstring is automatically inherited from AbstractMount From 61650e9e0dc73ef863dc30c0eadfaeaef057c70b Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sun, 7 Mar 2021 08:38:05 -0700 Subject: [PATCH 09/47] update tests --- pvlib/tests/test_pvsystem.py | 16 ++++------------ pvlib/tests/test_tracking.py | 12 +++--------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index ba29c06f90..ebcda1fd2b 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1806,9 +1806,7 @@ def test_PVSystem___repr__(): name: pv ftw Array: name: None - mount: FixedMount: - surface_tilt: 0 - surface_azimuth: 180 + mount: FixedMount(surface_tilt=0, surface_azimuth=180) module: blah albedo: 0.25 racking_model: None @@ -1833,9 +1831,7 @@ def test_PVSystem_multi_array___repr__(): name: None Array: name: None - mount: FixedMount: - surface_tilt: 30 - surface_azimuth: 100 + mount: FixedMount(surface_tilt=30, surface_azimuth=100) module: None albedo: 0.25 racking_model: None @@ -1845,9 +1841,7 @@ def test_PVSystem_multi_array___repr__(): modules_per_string: 1 Array: name: foo - mount: FixedMount: - surface_tilt: 20 - surface_azimuth: 220 + mount: FixedMount(surface_tilt=20, surface_azimuth=220) module: None albedo: 0.25 racking_model: None @@ -1872,9 +1866,7 @@ def test_Array___repr__(): ) expected = """Array: name: biz - mount: FixedMount: - surface_tilt: 10 - surface_azimuth: 100 + mount: FixedMount(surface_tilt=10, surface_azimuth=100) module: baz albedo: 0.15 racking_model: close_mount diff --git a/pvlib/tests/test_tracking.py b/pvlib/tests/test_tracking.py index f723e15fca..90fd97ecc1 100644 --- a/pvlib/tests/test_tracking.py +++ b/pvlib/tests/test_tracking.py @@ -6,7 +6,7 @@ from numpy.testing import assert_allclose import pvlib -from pvlib import tracking, pvsystem +from pvlib import tracking from conftest import DATA_DIR, assert_frame_equal SINGLEAXIS_COL_ORDER = ['tracker_theta', 'aoi', @@ -415,13 +415,7 @@ def test_SingleAxisTracker___repr__(): name: None Array: name: None - mount: SingleAxisTrackerMount: - axis_tilt: 0 - axis_azimuth: 0 - max_angle: 45 - backtrack: True - gcr: 0.25 - cross_axis_tilt: 0.0 + mount: SingleAxisTrackerMount(axis_tilt=0, axis_azimuth=0, max_angle=45, backtrack=True, gcr=0.25, cross_axis_tilt=0.0) module: blah albedo: 0.25 racking_model: None @@ -429,7 +423,7 @@ def test_SingleAxisTracker___repr__(): temperature_model_parameters: {'a': -3.56} strings: 1 modules_per_string: 1 - inverter: blarg""" + inverter: blarg""" # noqa: E501 assert system.__repr__() == expected From fc47003ccd7da29e21e27b2ca4c1be07306c1f52 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sun, 7 Mar 2021 10:30:30 -0700 Subject: [PATCH 10/47] update docs --- docs/sphinx/source/api.rst | 2 ++ docs/sphinx/source/pvsystem.rst | 36 ++++++++++++++++++--------------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index e88cb66dde..8e74e003ea 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -21,6 +21,8 @@ corresponding procedural code. location.Location pvsystem.PVSystem pvsystem.Array + pvsystem.FixedMount + pvsystem.SingleAxisTrackerMount tracking.SingleAxisTracker modelchain.ModelChain modelchain.ModelChainResult diff --git a/docs/sphinx/source/pvsystem.rst b/docs/sphinx/source/pvsystem.rst index 58d943ac79..a43971f37c 100644 --- a/docs/sphinx/source/pvsystem.rst +++ b/docs/sphinx/source/pvsystem.rst @@ -138,8 +138,9 @@ provided for each array, and the arrays are provided to .. ipython:: python module_parameters = {'pdc0': 5000, 'gamma_pdc': -0.004} - array_one = pvsystem.Array(module_parameters=module_parameters) - array_two = pvsystem.Array(module_parameters=module_parameters) + mount = pvsystem.FixedMount(surface_tilt=20, surface_azimuth=180) + array_one = pvsystem.Array(mount=mount, module_parameters=module_parameters) + array_two = pvsystem.Array(mount=mount, module_parameters=module_parameters) system_two_arrays = pvsystem.PVSystem(arrays=[array_one, array_two], inverter_parameters=inverter_parameters) print(system_two_arrays.module_parameters) @@ -151,7 +152,7 @@ a tuple with the `module_parameters` for each array. The :py:class:`~pvlib.pvsystem.Array` class includes those :py:class:`~pvlib.pvsystem.PVSystem` attributes that may vary from array -to array. These attributes include `surface_tilt`, `surface_azimuth`, +to array. These attributes include `module_parameters`, `temperature_model_parameters`, `modules_per_string`, `strings_per_inverter`, `albedo`, `surface_type`, `module_type`, and `racking_model`. @@ -182,27 +183,30 @@ Tilt and azimuth The first parameters which describe the DC part of a PV system are the tilt and azimuth of the modules. In the case of a PV system with a single array, these parameters can be specified using the `PVSystem.surface_tilt` and -`PVSystem.surface_azimuth` attributes. +`PVSystem.surface_azimuth` attributes. This will automatically create +an :py:class:`~pvlib.pvsystem.Array` with a :py:class:`~pvlib.pvsystem.FixedMount` +at the specified tilt and azimuth: .. ipython:: python # single south-facing array at 20 deg tilt system_one_array = pvsystem.PVSystem(surface_tilt=20, surface_azimuth=180) - print(system_one_array.surface_tilt, system_one_array.surface_azimuth) + print(system_one_array.arrays[0].mount) In the case of a PV system with several arrays, the parameters are specified -for each array using the attributes `Array.surface_tilt` and `Array.surface_azimuth`. +for each array by passing a different :py:class:`~pvlib.pvsystem.FixedMount` +(or another `Mount` class): .. ipython:: python - array_one = pvsystem.Array(surface_tilt=30, surface_azimuth=90) - print(array_one.surface_tilt, array_one.surface_azimuth) - array_two = pvsystem.Array(surface_tilt=30, surface_azimuth=220) + array_one = pvsystem.Array(pvsystem.FixedMount(surface_tilt=30, surface_azimuth=90)) + print(array_one.mount.surface_tilt, array_one.mount.surface_azimuth) + array_two = pvsystem.Array(pvsystem.FixedMount(surface_tilt=30, surface_azimuth=220)) system = pvsystem.PVSystem(arrays=[array_one, array_two]) system.num_arrays - system.surface_tilt - system.surface_azimuth + for array in system.arrays: + print(array.mount) The `surface_tilt` and `surface_azimuth` attributes are used in PVSystem @@ -218,7 +222,7 @@ and `solar_azimuth` as arguments. # single south-facing array at 20 deg tilt system_one_array = pvsystem.PVSystem(surface_tilt=20, surface_azimuth=180) - print(system_one_array.surface_tilt, system_one_array.surface_azimuth) + print(system_one_array.arrays[0].mount) # call get_aoi with solar_zenith, solar_azimuth aoi = system_one_array.get_aoi(solar_zenith=30, solar_azimuth=180) @@ -231,7 +235,7 @@ operates in a similar manner. .. ipython:: python # two arrays each at 30 deg tilt with different facing - array_one = pvsystem.Array(surface_tilt=30, surface_azimuth=90) + array_one = pvsystem.Array(pvsystem.FixedMount(surface_tilt=30, surface_azimuth=90)) array_one_aoi = array_one.get_aoi(solar_zenith=30, solar_azimuth=180) print(array_one_aoi) @@ -242,7 +246,7 @@ operates on all `Array` instances in the `PVSystem`, whereas the the .. ipython:: python - array_two = pvsystem.Array(surface_tilt=30, surface_azimuth=220) + array_two = pvsystem.Array(pvsystem.FixedMount(surface_tilt=30, surface_azimuth=220)) system_multiarray = pvsystem.PVSystem(arrays=[array_one, array_two]) print(system_multiarray.num_arrays) # call get_aoi with solar_zenith, solar_azimuth @@ -317,8 +321,8 @@ Losses The `losses_parameters` attribute contains data that may be used with methods that calculate system losses. At present, these methods include -only :py:meth:`PVSystem.pvwatts_losses` and -:py:func:`pvsystem.pvwatts_losses`, but we hope to add more related functions +only :py:meth:`pvlib.pvsystem.PVSystem.pvwatts_losses` and +:py:func:`pvlib.pvsystem.pvwatts_losses`, but we hope to add more related functions and methods in the future. From 887fd3af47bc4d77344973ad63010b4a9eafdff0 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sun, 7 Mar 2021 10:32:55 -0700 Subject: [PATCH 11/47] whatsnew --- docs/sphinx/source/whatsnew/v0.9.0.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst index 22c1a3ba3d..68787eb401 100644 --- a/docs/sphinx/source/whatsnew/v0.9.0.rst +++ b/docs/sphinx/source/whatsnew/v0.9.0.rst @@ -75,6 +75,9 @@ Enhancements * Added :py:class:`~pvlib.pvsystem.Array` class to represent an array of modules separately from a :py:class:`~pvlib.pvsystem.PVSystem`. (:pull:`1076`, :issue:`1067`) +* Added :py:class:`~pvlib.pvsystem.FixedMount` and + :py:class:`~pvlib.pvsystem.SingleAxisTrackerMount` classes to use with + the new :py:class:`~pvlib.pvsystem.Array` class (:pull:`1176`) * Added capability for modeling a PV system with multiple arrays in :py:class:`~pvlib.pvsystem.PVSystem`. Updates the ``PVSystem`` API to operate on and return tuples where each element of the tuple corresponds From ecc4737d958c5dc77b8b369b5ab62146986dc417 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sun, 7 Mar 2021 10:51:33 -0700 Subject: [PATCH 12/47] test mount classes --- pvlib/tests/test_pvsystem.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index ebcda1fd2b..3b436dd49b 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -2092,3 +2092,39 @@ def test_deprecated_attributes_multi(two_array_system): two_array_system.surface_tilt with pytest.raises(AttributeError, match=match): two_array_system.surface_azimuth + + +@pytest.fixture +def fixed_mount(): + return pvsystem.FixedMount(20, 180) + + +@pytest.fixture +def single_axis_tracker_mount(): + return pvsystem.SingleAxisTrackerMount(axis_tilt=10, axis_azimuth=170, + max_angle=45, backtrack=False, + gcr=0.4, cross_axis_tilt=-5) + + +def test_FixedMount_constructor(fixed_mount): + assert fixed_mount.surface_tilt == 20 + assert fixed_mount.surface_azimuth == 180 + + +def test_FixedMount_get_orientation(fixed_mount): + expected = {'surface_tilt': 20, 'surface_azimuth': 180} + assert fixed_mount.calculate_orientation(45, 130) == expected + + +def test_SingleAxisTrackerMount_constructor(single_axis_tracker_mount): + expected = dict(axis_tilt=10, axis_azimuth=170, max_angle=45, backtrack=False, + gcr=0.4, cross_axis_tilt=-5) + for attr_name, expected_value in expected.items(): + assert getattr(single_axis_tracker_mount, attr_name) == expected_value + + +def test_SingleAxisTrackerMount_get_orientation(single_axis_tracker_mount): + expected = {'surface_tilt': 19.29835284, 'surface_azimuth': 229.7643755} + actual = single_axis_tracker_mount.calculate_orientation(45, 190) + for key, expected_value in expected.items(): + assert actual[key] == pytest.approx(expected_value), f"{key} value incorrect" From cbb41e16ee59574c3274ccc91b1d59469e393d96 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sun, 7 Mar 2021 10:52:46 -0700 Subject: [PATCH 13/47] stickler --- pvlib/tests/test_pvsystem.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 3b436dd49b..4370e97380 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -2117,8 +2117,8 @@ def test_FixedMount_get_orientation(fixed_mount): def test_SingleAxisTrackerMount_constructor(single_axis_tracker_mount): - expected = dict(axis_tilt=10, axis_azimuth=170, max_angle=45, backtrack=False, - gcr=0.4, cross_axis_tilt=-5) + expected = dict(axis_tilt=10, axis_azimuth=170, max_angle=45, + backtrack=False, gcr=0.4, cross_axis_tilt=-5) for attr_name, expected_value in expected.items(): assert getattr(single_axis_tracker_mount, attr_name) == expected_value @@ -2127,4 +2127,5 @@ def test_SingleAxisTrackerMount_get_orientation(single_axis_tracker_mount): expected = {'surface_tilt': 19.29835284, 'surface_azimuth': 229.7643755} actual = single_axis_tracker_mount.calculate_orientation(45, 190) for key, expected_value in expected.items(): - assert actual[key] == pytest.approx(expected_value), f"{key} value incorrect" + err_msg = f"{key} value incorrect" + assert actual[key] == pytest.approx(expected_value), err_msg From 9e663d23e3f5ee7316a37a5561b55c83c47b6fed Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sun, 7 Mar 2021 11:10:35 -0700 Subject: [PATCH 14/47] more tests --- pvlib/tests/test_pvsystem.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 4370e97380..627e0f57fe 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1756,13 +1756,35 @@ def test_PVSystem_multi_array_get_irradiance_multi_irrad(): assert not array_irrad[0].equals(array_irrad[1]) +def test_PVSystem_change_surface_tilt(): + system = pvsystem.PVSystem(surface_tilt=30) + assert system.surface_tilt == 30 + match = "PVSystem.surface_tilt attribute is deprecated" + with pytest.warns(UserWarning, match=match): + system.surface_tilt = 10 + assert system.surface_tilt == 10 + + def test_PVSystem_change_surface_azimuth(): system = pvsystem.PVSystem(surface_azimuth=180) assert system.surface_azimuth == 180 - system.surface_azimuth = 90 + match = "PVSystem.surface_azimuth attribute is deprecated" + with pytest.warns(UserWarning, match=match): + system.surface_azimuth = 90 assert system.surface_azimuth == 90 +@pytest.mark.parametrize('attr', ['surface_tilt', 'surface_azimuth']) +def test_PVSystem_change_surface_tilt_azimuth_multi(attr, two_array_system): + # getting fails + with pytest.raises(AttributeError, match='not supported for multi-array'): + getattr(two_array_system, attr) + + # setting fails + with pytest.raises(AttributeError, match='not supported for multi-array'): + setattr(two_array_system, attr, 0) + + def test_PVSystem_get_albedo(two_array_system): system = pvsystem.PVSystem( arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180), albedo=0.5)] From 105edb77924f121a70ea6368f2cd82de88c870ac Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sun, 7 Mar 2021 11:12:25 -0700 Subject: [PATCH 15/47] another test --- pvlib/tests/test_pvsystem.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 627e0f57fe..e9606c9278 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -2116,6 +2116,12 @@ def test_deprecated_attributes_multi(two_array_system): two_array_system.surface_azimuth +def test_AbstractMount_constructor(): + match = "Can't instantiate abstract base class AbstractMount" + with pytest.raises(TypeError, match=match): + _ = pvsystem.AbstractMount() + + @pytest.fixture def fixed_mount(): return pvsystem.FixedMount(20, 180) From 6d32c414983152868a1c1c9206ab8b0e5aa85a45 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sun, 7 Mar 2021 11:15:31 -0700 Subject: [PATCH 16/47] fix typo --- pvlib/tests/test_pvsystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index e9606c9278..1627b99b06 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -2117,7 +2117,7 @@ def test_deprecated_attributes_multi(two_array_system): def test_AbstractMount_constructor(): - match = "Can't instantiate abstract base class AbstractMount" + match = "Can't instantiate abstract class AbstractMount" with pytest.raises(TypeError, match=match): _ = pvsystem.AbstractMount() From 4a6347e4e642fa85d05da7c17d435c6f79dcbd5b Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sat, 13 Mar 2021 08:46:52 -0700 Subject: [PATCH 17/47] clean up AbstractMount --- pvlib/pvsystem.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 0181621bf3..e2124b594a 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1412,6 +1412,10 @@ def get_iam(self, aoi, iam_model='physical'): @dataclass class AbstractMount(ABC): + """ + A base class for Mount classes to extend. It is not intended to be + instantiated directly. + """ @abstractmethod def calculate_orientation(self, solar_zenith, solar_azimuth): @@ -1430,7 +1434,6 @@ def calculate_orientation(self, solar_zenith, solar_azimuth): orientation : dict-like A dict-like object with keys `'surface_tilt', 'surface_azimuth'` """ - pass @dataclass From 1e063baf7d207be1f6d548168b2f9fc51d43bdbf Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sat, 13 Mar 2021 08:47:46 -0700 Subject: [PATCH 18/47] remove unnecessary use of dataclasses.field --- pvlib/pvsystem.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index e2124b594a..cba07a7908 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -11,7 +11,7 @@ from urllib.request import urlopen import numpy as np import pandas as pd -from dataclasses import dataclass, field +from dataclasses import dataclass from abc import ABC, abstractmethod import warnings @@ -1452,8 +1452,8 @@ class FixedMount(AbstractMount): West=270. [degrees] """ - surface_tilt: float = field(default=0) - surface_azimuth: float = field(default=180) + surface_tilt: float = 0.0 + surface_azimuth: float = 180.0 def calculate_orientation(self, solar_zenith, solar_azimuth): # note -- docstring is automatically inherited from AbstractMount @@ -1509,12 +1509,12 @@ class SingleAxisTrackerMount(AbstractMount): :func:`~pvlib.tracking.calc_cross_axis_tilt` to calculate `cross_axis_tilt`. [degrees] """ - axis_tilt: float = field(default=0) - axis_azimuth: float = field(default=0) - max_angle: float = field(default=90) - backtrack: bool = field(default=True) - gcr: float = field(default=2/7) - cross_axis_tilt: float = field(default=0) + axis_tilt: float = 0.0 + axis_azimuth: float = 0.0 + max_angle: float = 90.0 + backtrack: bool = True + gcr: float = 2/7 + cross_axis_tilt: float = 0.0 def calculate_orientation(self, solar_zenith, solar_azimuth): # note -- docstring is automatically inherited from AbstractMount From 3dcf106b887c4e2201c4d2aad1aec2924125942e Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sat, 13 Mar 2021 09:19:28 -0700 Subject: [PATCH 19/47] calculate -> get --- pvlib/pvsystem.py | 12 +++++------- pvlib/tests/test_pvsystem.py | 4 ++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index cba07a7908..ca8c44cfc9 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1303,8 +1303,7 @@ def get_aoi(self, solar_zenith, solar_azimuth): aoi : Series Then angle of incidence. """ - orientation = self.mount.calculate_orientation(solar_zenith, - solar_azimuth) + orientation = self.mount.get_orientation(solar_zenith, solar_azimuth) return irradiance.aoi(orientation['surface_tilt'], orientation['surface_azimuth'], solar_zenith, solar_azimuth) @@ -1355,8 +1354,7 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, if airmass is None: airmass = atmosphere.get_relative_airmass(solar_zenith) - orientation = self.mount.calculate_orientation(solar_zenith, - solar_azimuth) + orientation = self.mount.get_orientation(solar_zenith, solar_azimuth) return irradiance.get_total_irradiance(orientation['surface_tilt'], orientation['surface_azimuth'], solar_zenith, solar_azimuth, @@ -1418,7 +1416,7 @@ class AbstractMount(ABC): """ @abstractmethod - def calculate_orientation(self, solar_zenith, solar_azimuth): + def get_orientation(self, solar_zenith, solar_azimuth): """ Determine module orientation. @@ -1455,7 +1453,7 @@ class FixedMount(AbstractMount): surface_tilt: float = 0.0 surface_azimuth: float = 180.0 - def calculate_orientation(self, solar_zenith, solar_azimuth): + def get_orientation(self, solar_zenith, solar_azimuth): # note -- docstring is automatically inherited from AbstractMount return { 'surface_tilt': self.surface_tilt, @@ -1516,7 +1514,7 @@ class SingleAxisTrackerMount(AbstractMount): gcr: float = 2/7 cross_axis_tilt: float = 0.0 - def calculate_orientation(self, solar_zenith, solar_azimuth): + def get_orientation(self, solar_zenith, solar_azimuth): # note -- docstring is automatically inherited from AbstractMount from pvlib import tracking # avoid circular import issue tracking_data = tracking.singleaxis( diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 1627b99b06..88c58f5196 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -2141,7 +2141,7 @@ def test_FixedMount_constructor(fixed_mount): def test_FixedMount_get_orientation(fixed_mount): expected = {'surface_tilt': 20, 'surface_azimuth': 180} - assert fixed_mount.calculate_orientation(45, 130) == expected + assert fixed_mount.get_orientation(45, 130) == expected def test_SingleAxisTrackerMount_constructor(single_axis_tracker_mount): @@ -2153,7 +2153,7 @@ def test_SingleAxisTrackerMount_constructor(single_axis_tracker_mount): def test_SingleAxisTrackerMount_get_orientation(single_axis_tracker_mount): expected = {'surface_tilt': 19.29835284, 'surface_azimuth': 229.7643755} - actual = single_axis_tracker_mount.calculate_orientation(45, 190) + actual = single_axis_tracker_mount.get_orientation(45, 190) for key, expected_value in expected.items(): err_msg = f"{key} value incorrect" assert actual[key] == pytest.approx(expected_value), err_msg From b0b551ff983e81eff43e09f3cd52fb582d2f93a3 Mon Sep 17 00:00:00 2001 From: Kevin Anderson <57452607+kanderso-nrel@users.noreply.github.com> Date: Sun, 21 Mar 2021 11:38:56 -0600 Subject: [PATCH 20/47] Update pvlib/pvsystem.py Co-authored-by: Cliff Hansen --- pvlib/pvsystem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index a346377eba..2b8f5579dc 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1237,7 +1237,8 @@ class Array: Parameters ---------- mount: FixedMount, SingleAxisTrackerMount, or other - Mounting strategy for the array, used to determine module orientation. + Mounting for the array, either on fixed-tilt racking or horizontal single axis + tracker. Mounting is used to determine module orientation. If not provided, a FixedMount with zero tilt is used. albedo : None or float, default None From 515a359b7e986acaacaca2b0031004aef1b10400 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sun, 21 Mar 2021 11:40:12 -0600 Subject: [PATCH 21/47] stickler --- pvlib/pvsystem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 2b8f5579dc..8038ad8081 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1237,8 +1237,8 @@ class Array: Parameters ---------- mount: FixedMount, SingleAxisTrackerMount, or other - Mounting for the array, either on fixed-tilt racking or horizontal single axis - tracker. Mounting is used to determine module orientation. + Mounting for the array, either on fixed-tilt racking or horizontal + single axis tracker. Mounting is used to determine module orientation. If not provided, a FixedMount with zero tilt is used. albedo : None or float, default None From a874b11413464679e61a154653eea8bebcf32a1b Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sun, 21 Mar 2021 11:57:40 -0600 Subject: [PATCH 22/47] test fixes --- pvlib/tests/test_pvsystem.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 946c3cdc09..a1deb924be 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -2211,6 +2211,7 @@ def test_SingleAxisTrackerMount_get_orientation(single_axis_tracker_mount): err_msg = f"{key} value incorrect" assert actual[key] == pytest.approx(expected_value), err_msg + def test_dc_ohms_from_percent(): expected = .1425 out = pvsystem.dc_ohms_from_percent(38, 8, 3, 1, 1) @@ -2248,7 +2249,8 @@ def test_Array_dc_ohms_from_percent(mocker): expected = .1425 - array = pvsystem.Array(array_losses_parameters={'dc_ohmic_percent': 3}, + array = pvsystem.Array(pvsystem.FixedMount(0, 180), + array_losses_parameters={'dc_ohmic_percent': 3}, module_parameters={'I_mp_ref': 8, 'V_mp_ref': 38}) out = array.dc_ohms_from_percent() @@ -2261,7 +2263,8 @@ def test_Array_dc_ohms_from_percent(mocker): ) assert_allclose(out, expected) - array = pvsystem.Array(array_losses_parameters={'dc_ohmic_percent': 3}, + array = pvsystem.Array(pvsystem.FixedMount(0, 180), + array_losses_parameters={'dc_ohmic_percent': 3}, module_parameters={'Impo': 8, 'Vmpo': 38}) out = array.dc_ohms_from_percent() @@ -2274,7 +2277,8 @@ def test_Array_dc_ohms_from_percent(mocker): ) assert_allclose(out, expected) - array = pvsystem.Array(array_losses_parameters={'dc_ohmic_percent': 3}, + array = pvsystem.Array(pvsystem.FixedMount(0, 180), + array_losses_parameters={'dc_ohmic_percent': 3}, module_parameters={'Impp': 8, 'Vmpp': 38}) out = array.dc_ohms_from_percent() @@ -2295,5 +2299,6 @@ def test_Array_dc_ohms_from_percent(mocker): '{"V_mp_ref", "I_mp_Ref"}, ' '{"Vmpo", "Impo"}, or ' '{"Vmpp", "Impp"}.')): - array = pvsystem.Array(array_losses_parameters={'dc_ohmic_percent': 3}) + array = pvsystem.Array(pvsystem.FixedMount(0, 180), + array_losses_parameters={'dc_ohmic_percent': 3}) out = array.dc_ohms_from_percent() From 17195aef0a5dc77bbb5593eecbf3fd5c7cc3a742 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sun, 21 Mar 2021 12:57:28 -0600 Subject: [PATCH 23/47] add optional surface_tilt parameter to PVSystem.fuentes_celltemp --- pvlib/pvsystem.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 8038ad8081..bc41d129aa 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -729,7 +729,8 @@ def faiman_celltemp(self, poa_global, temp_air, wind_speed=1.0): ) @_unwrap_single_value - def fuentes_celltemp(self, poa_global, temp_air, wind_speed): + def fuentes_celltemp(self, poa_global, temp_air, wind_speed, + surface_tilt=None): """ Use :py:func:`temperature.fuentes` to calculate cell temperature. @@ -744,6 +745,11 @@ def fuentes_celltemp(self, poa_global, temp_air, wind_speed): wind_speed : pandas Series or tuple of Series Wind speed [m/s] + surface_tilt : pandas Series or tuple of Series, optional + Panel tilt from horizontal. Superseded by ``surface_tilt`` + values in ``self.arrays[i].temperature_model_parameters`` + (see Notes). [degrees] + Returns ------- temperature_cell : Series or tuple of Series @@ -754,11 +760,11 @@ def fuentes_celltemp(self, poa_global, temp_air, wind_speed): The Fuentes thermal model uses the module surface tilt for convection modeling. The SAM implementation of PVWatts hardcodes the surface tilt value at 30 degrees, ignoring whatever value is used for irradiance - transposition. This method defaults to using ``self.surface_tilt``, but - if you want to match the PVWatts behavior, you can override it by - including a ``surface_tilt`` value in ``temperature_model_parameters``. + transposition. If you want to match the PVWatts behavior, specify a + ``surface_tilt`` value in the Array's ``temperature_model_parameters``. - The `temp_air` and `wind_speed` parameters may be passed as tuples + The `temp_air`, `wind_speed`, and `surface_tilt` parameters may be + passed as tuples to provide different values for each Array in the system. If not passed as a tuple then the same value is used for input to each Array. If passed as a tuple the length must be the same as the number of @@ -769,12 +775,10 @@ def fuentes_celltemp(self, poa_global, temp_air, wind_speed): poa_global = self._validate_per_array(poa_global) temp_air = self._validate_per_array(temp_air, system_wide=True) wind_speed = self._validate_per_array(wind_speed, system_wide=True) + surface_tilt = self._validate_per_array(wind_speed, system_wide=True) - def _build_kwargs_fuentes(array): - # TODO: I think there should be an interface function so that - # directly accessing surface_tilt isn't necessary. Doesn't this - # break for SAT? - kwargs = {'surface_tilt': array.mount.surface_tilt} + def _build_kwargs_fuentes(array, user_tilt): + kwargs = {'surface_tilt': user_tilt} temp_model_kwargs = _build_kwargs([ 'noct_installed', 'module_height', 'wind_height', 'emissivity', 'absorption', 'surface_tilt', 'module_width', 'module_length'], @@ -784,9 +788,9 @@ def _build_kwargs_fuentes(array): return tuple( temperature.fuentes( poa_global, temp_air, wind_speed, - **_build_kwargs_fuentes(array)) - for array, poa_global, temp_air, wind_speed in zip( - self.arrays, poa_global, temp_air, wind_speed + **_build_kwargs_fuentes(array, user_tilt)) + for array, poa_global, temp_air, wind_speed, user_tilt in zip( + self.arrays, poa_global, temp_air, wind_speed, surface_tilt ) ) From 93716bb63762b5f33a9db6b59caf7197c0504058 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sun, 21 Mar 2021 13:59:58 -0600 Subject: [PATCH 24/47] move racking_model and module_height to the Mounts --- pvlib/pvsystem.py | 42 ++++++++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index bc41d129aa..fabfb7c223 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -13,6 +13,7 @@ import pandas as pd from dataclasses import dataclass from abc import ABC, abstractmethod +from typing import Optional import warnings from pvlib._deprecation import deprecated, pvlibDeprecationWarning @@ -200,7 +201,7 @@ def __init__(self, array_losses_parameters = _build_kwargs(['dc_ohmic_percent'], losses_parameters) self.arrays = (Array( - FixedMount(surface_tilt, surface_azimuth), + FixedMount(surface_tilt, surface_azimuth, racking_model), albedo, surface_type, module, @@ -209,7 +210,6 @@ def __init__(self, temperature_model_parameters, modules_per_string, strings_per_inverter, - racking_model, array_losses_parameters, ),) else: @@ -775,15 +775,22 @@ def fuentes_celltemp(self, poa_global, temp_air, wind_speed, poa_global = self._validate_per_array(poa_global) temp_air = self._validate_per_array(temp_air, system_wide=True) wind_speed = self._validate_per_array(wind_speed, system_wide=True) - surface_tilt = self._validate_per_array(wind_speed, system_wide=True) + surface_tilt = self._validate_per_array(surface_tilt, system_wide=True) def _build_kwargs_fuentes(array, user_tilt): kwargs = {'surface_tilt': user_tilt} + if array.mount.module_height is not None: + kwargs['module_height'] = array.mount.module_height temp_model_kwargs = _build_kwargs([ - 'noct_installed', 'module_height', 'wind_height', 'emissivity', + 'noct_installed', 'wind_height', 'emissivity', 'absorption', 'surface_tilt', 'module_width', 'module_length'], array.temperature_model_parameters) kwargs.update(temp_model_kwargs) + if kwargs['surface_tilt'] is None: + raise ValueError( + "surface_tilt must be specified in " + "PVSystem.fuentes_celltemp if not providing a " + "default in the Array's temperature_model_parameters") return kwargs return tuple( temperature.fuentes( @@ -1145,6 +1152,7 @@ def temperature_model_parameters(self, value): @property def surface_tilt(self): + # TODO: make sure this is merged correctly with #1196 if len(self.arrays) == 1: msg = ( 'PVSystem.surface_tilt attribute is deprecated. ' @@ -1159,6 +1167,7 @@ def surface_tilt(self): @surface_tilt.setter def surface_tilt(self, value): + # TODO: make sure this is merged correctly with #1196 if len(self.arrays) == 1: msg = ( 'PVSystem.surface_tilt attribute is deprecated. ' @@ -1173,6 +1182,7 @@ def surface_tilt(self, value): @property def surface_azimuth(self): + # TODO: make sure this is merged correctly with #1196 if len(self.arrays) == 1: msg = ( 'PVSystem.surface_azimuth attribute is deprecated. ' @@ -1187,6 +1197,7 @@ def surface_azimuth(self): @surface_azimuth.setter def surface_azimuth(self, value): + # TODO: make sure this is merged correctly with #1196 if len(self.arrays) == 1: msg = ( 'PVSystem.surface_azimuth attribute is deprecated. ' @@ -1207,12 +1218,14 @@ def albedo(self): @property @_unwrap_single_value def racking_model(self): - return tuple(array.racking_model for array in self.arrays) + # TODO: make sure this is merged correctly with #1196 + return tuple(array.mount.racking_model for array in self.arrays) @racking_model.setter def racking_model(self, value): + # TODO: make sure this is merged correctly with #1196 for array in self.arrays: - array.racking_model = value + array.mount.racking_model = value @property @_unwrap_single_value @@ -1277,13 +1290,11 @@ class Array: strings: int, default 1 Number of parallel strings in the array. - racking_model : None or string, default None - Valid strings are 'open_rack', 'close_mount', and 'insulated_back'. - Used to identify a parameter set for the SAPM cell temperature model. - array_losses_parameters: None, dict or Series, default None. Supported keys are 'dc_ohmic_percent'. + name: None or str, default None + Name of Array instance. """ def __init__(self, mount, @@ -1292,7 +1303,7 @@ def __init__(self, mount, module_parameters=None, temperature_model_parameters=None, modules_per_string=1, strings=1, - racking_model=None, array_losses_parameters=None, + array_losses_parameters=None, name=None): self.mount = mount @@ -1309,7 +1320,6 @@ def __init__(self, mount, self.module_parameters = module_parameters self.module_type = module_type - self.racking_model = racking_model self.strings = strings self.modules_per_string = modules_per_string @@ -1329,7 +1339,7 @@ def __init__(self, mount, def __repr__(self): attrs = ['name', 'mount', 'module', - 'albedo', 'racking_model', 'module_type', + 'albedo', 'module_type', 'temperature_model_parameters', 'strings', 'modules_per_string'] @@ -1340,7 +1350,7 @@ def __repr__(self): def _infer_temperature_model_params(self): # try to infer temperature model parameters from from racking_model # and module_type - param_set = f'{self.racking_model}_{self.module_type}' + param_set = f'{self.mount.racking_model}_{self.module_type}' if param_set in temperature.TEMPERATURE_MODEL_PARAMETERS['sapm']: return temperature._temperature_model_params('sapm', param_set) elif 'freestanding' in param_set: @@ -1625,6 +1635,8 @@ class FixedMount(AbstractMount): surface_tilt: float = 0.0 surface_azimuth: float = 180.0 + racking_model: Optional[str] = None + module_height: Optional[float] = None def get_orientation(self, solar_zenith, solar_azimuth): # note -- docstring is automatically inherited from AbstractMount @@ -1686,6 +1698,8 @@ class SingleAxisTrackerMount(AbstractMount): backtrack: bool = True gcr: float = 2/7 cross_axis_tilt: float = 0.0 + racking_model: Optional[str] = None + module_height: Optional[float] = None def get_orientation(self, solar_zenith, solar_azimuth): # note -- docstring is automatically inherited from AbstractMount From efe6b3be24c577a4cf5db6460fae0a43b0ebfc51 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sun, 21 Mar 2021 14:23:31 -0600 Subject: [PATCH 25/47] fix some tests --- pvlib/pvsystem.py | 6 ++- pvlib/tests/test_modelchain.py | 2 +- pvlib/tests/test_pvsystem.py | 79 +++++++++++++++++++--------------- pvlib/tracking.py | 9 +++- 4 files changed, 57 insertions(+), 39 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index fabfb7c223..26e536d536 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -760,8 +760,10 @@ def fuentes_celltemp(self, poa_global, temp_air, wind_speed, The Fuentes thermal model uses the module surface tilt for convection modeling. The SAM implementation of PVWatts hardcodes the surface tilt value at 30 degrees, ignoring whatever value is used for irradiance - transposition. If you want to match the PVWatts behavior, specify a - ``surface_tilt`` value in the Array's ``temperature_model_parameters``. + transposition. If you want to match the PVWatts behavior you can + either leave ``surface_tilt`` unspecified to use the PVWatts default + of 30, or specify a ``surface_tilt`` value in the Array's + ``temperature_model_parameters``. The `temp_air`, `wind_speed`, and `surface_tilt` parameters may be passed as tuples diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 0dae8025bb..7b5b72aa99 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -697,7 +697,7 @@ def test_run_model_with_weather_fuentes_temp(sapm_dc_snl_ac_system, location, weather['wind_speed'] = 5 weather['temp_air'] = 10 sapm_dc_snl_ac_system.temperature_model_parameters = { - 'noct_installed': 45 + 'noct_installed': 45, 'surface_tilt': 30, } mc = ModelChain(sapm_dc_snl_ac_system, location) mc.temperature_model = 'fuentes' diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index a1deb924be..7af546c0de 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -566,8 +566,13 @@ def test_PVSystem_multi_array_celltemp_functions(celltemp, two_array_system): irrad_two = pd.Series(500, index=times) temp_air = pd.Series(25, index=times) wind_speed = pd.Series(1, index=times) + kwargs = {} + if celltemp == pvsystem.PVSystem.fuentes_celltemp: + kwargs['surface_tilt'] = 30 + temp_one, temp_two = celltemp( - two_array_system, (irrad_one, irrad_two), temp_air, wind_speed) + two_array_system, (irrad_one, irrad_two), temp_air, wind_speed, + **kwargs) assert (temp_one != temp_two).all() @@ -583,18 +588,24 @@ def test_PVSystem_multi_array_celltemp_multi_temp(celltemp, two_array_system): temp_air_one = pd.Series(25, index=times) temp_air_two = pd.Series(5, index=times) wind_speed = pd.Series(1, index=times) + kwargs = {} + if celltemp == pvsystem.PVSystem.fuentes_celltemp: + kwargs['surface_tilt'] = 30 + temp_one, temp_two = celltemp( two_array_system, (irrad, irrad), (temp_air_one, temp_air_two), - wind_speed + wind_speed, + **kwargs ) assert (temp_one != temp_two).all() temp_one_swtich, temp_two_switch = celltemp( two_array_system, (irrad, irrad), (temp_air_two, temp_air_one), - wind_speed + wind_speed, + **kwargs ) assert_series_equal(temp_one, temp_two_switch) assert_series_equal(temp_two, temp_one_swtich) @@ -612,18 +623,24 @@ def test_PVSystem_multi_array_celltemp_multi_wind(celltemp, two_array_system): temp_air = pd.Series(25, index=times) wind_speed_one = pd.Series(1, index=times) wind_speed_two = pd.Series(5, index=times) + kwargs = {} + if celltemp == pvsystem.PVSystem.fuentes_celltemp: + kwargs['surface_tilt'] = 30 + temp_one, temp_two = celltemp( two_array_system, (irrad, irrad), temp_air, - (wind_speed_one, wind_speed_two) + (wind_speed_one, wind_speed_two), + **kwargs ) assert (temp_one != temp_two).all() temp_one_swtich, temp_two_switch = celltemp( two_array_system, (irrad, irrad), temp_air, - (wind_speed_two, wind_speed_one) + (wind_speed_two, wind_speed_one), + **kwargs ) assert_series_equal(temp_one, temp_two_switch) assert_series_equal(temp_two, temp_one_swtich) @@ -703,10 +720,12 @@ def test_PVSystem_fuentes_celltemp(mocker): temps = pd.Series(25, index) irrads = pd.Series(1000, index) winds = pd.Series(1, index) - out = system.fuentes_celltemp(irrads, temps, winds) + + out = system.fuentes_celltemp(irrads, temps, winds, surface_tilt=0) assert_series_equal(spy.call_args[0][0], irrads) assert_series_equal(spy.call_args[0][1], temps) assert_series_equal(spy.call_args[0][2], winds) + assert spy.call_args[1]['surface_tilt'] == 0 assert spy.call_args[1]['noct_installed'] == noct_installed assert_series_equal(out, pd.Series([52.85, 55.85, 55.85], index, name='tmod')) @@ -714,7 +733,8 @@ def test_PVSystem_fuentes_celltemp(mocker): def test_PVSystem_fuentes_celltemp_override(mocker): # test that the surface_tilt value in the cell temp calculation can be - # overridden but defaults to the surface_tilt attribute of the PVSystem + # overridden but defaults to the surface_tilt value in + # array.temperature_model_parameters spy = mocker.spy(temperature, 'fuentes') noct_installed = 45 @@ -724,38 +744,33 @@ def test_PVSystem_fuentes_celltemp_override(mocker): winds = pd.Series(1, index) # uses default value - temp_model_params = {'noct_installed': noct_installed} - system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params, - surface_tilt=20) + temp_model_params = {'noct_installed': noct_installed, 'surface_tilt': 20} + system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params) system.fuentes_celltemp(irrads, temps, winds) assert spy.call_args[1]['surface_tilt'] == 20 - # can be overridden - temp_model_params = {'noct_installed': noct_installed, 'surface_tilt': 30} - system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params, - surface_tilt=20) - system.fuentes_celltemp(irrads, temps, winds) - assert spy.call_args[1]['surface_tilt'] == 30 + # if no default, uses the user-specified values + temp_model_params = {'noct_installed': noct_installed} + system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params) + system.fuentes_celltemp(irrads, temps, winds, surface_tilt=40) + assert spy.call_args[1]['surface_tilt'] == 40 def test_Array__infer_temperature_model_params(): - array = pvsystem.Array(mount=pvsystem.FixedMount(0, 180), + array = pvsystem.Array(mount=pvsystem.FixedMount(0, 180, racking_model='open_rack'), module_parameters={}, - racking_model='open_rack', module_type='glass_polymer') expected = temperature.TEMPERATURE_MODEL_PARAMETERS[ 'sapm']['open_rack_glass_polymer'] assert expected == array._infer_temperature_model_params() - array = pvsystem.Array(mount=pvsystem.FixedMount(0, 180), + array = pvsystem.Array(mount=pvsystem.FixedMount(0, 180, racking_model='freestanding'), module_parameters={}, - racking_model='freestanding', module_type='glass_polymer') expected = temperature.TEMPERATURE_MODEL_PARAMETERS[ 'pvsyst']['freestanding'] assert expected == array._infer_temperature_model_params() - array = pvsystem.Array(mount=pvsystem.FixedMount(0, 180), + array = pvsystem.Array(mount=pvsystem.FixedMount(0, 180, racking_model='insulated'), module_parameters={}, - racking_model='insulated', module_type=None) expected = temperature.TEMPERATURE_MODEL_PARAMETERS[ 'pvsyst']['insulated'] @@ -1881,10 +1896,9 @@ def test_PVSystem___repr__(): name: pv ftw Array: name: None - mount: FixedMount(surface_tilt=0, surface_azimuth=180) + mount: FixedMount(surface_tilt=0, surface_azimuth=180, racking_model=None, module_height=None) module: blah albedo: 0.25 - racking_model: None module_type: None temperature_model_parameters: {'a': -3.56} strings: 1 @@ -1906,34 +1920,32 @@ def test_PVSystem_multi_array___repr__(): name: None Array: name: None - mount: FixedMount(surface_tilt=30, surface_azimuth=100) + mount: FixedMount(surface_tilt=30, surface_azimuth=100, racking_model=None, module_height=None) module: None albedo: 0.25 - racking_model: None module_type: None temperature_model_parameters: {} strings: 1 modules_per_string: 1 Array: name: foo - mount: FixedMount(surface_tilt=20, surface_azimuth=220) + mount: FixedMount(surface_tilt=20, surface_azimuth=220, racking_model=None, module_height=None) module: None albedo: 0.25 - racking_model: None module_type: None temperature_model_parameters: {} strings: 1 modules_per_string: 1 - inverter: blarg""" + inverter: blarg""" # noqa: E501 assert expected == system.__repr__() def test_Array___repr__(): array = pvsystem.Array( - mount=pvsystem.FixedMount(surface_tilt=10, surface_azimuth=100), + mount=pvsystem.FixedMount(surface_tilt=10, surface_azimuth=100, + racking_model='close_mount'), albedo=0.15, module_type='glass_glass', temperature_model_parameters={'a': -3.56}, - racking_model='close_mount', module_parameters={'foo': 'bar'}, modules_per_string=100, strings=10, module='baz', @@ -1941,14 +1953,13 @@ def test_Array___repr__(): ) expected = """Array: name: biz - mount: FixedMount(surface_tilt=10, surface_azimuth=100) + mount: FixedMount(surface_tilt=10, surface_azimuth=100, racking_model='close_mount', module_height=None) module: baz albedo: 0.15 - racking_model: close_mount module_type: glass_glass temperature_model_parameters: {'a': -3.56} strings: 10 - modules_per_string: 100""" + modules_per_string: 100""" # noqa: E501 assert array.__repr__() == expected diff --git a/pvlib/tracking.py b/pvlib/tracking.py index bdf9809958..47fe18bebb 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -2,6 +2,7 @@ import pandas as pd from pvlib.tools import cosd, sind, tand +from pvlib.tools import _build_kwargs from pvlib.pvsystem import ( PVSystem, Array, SingleAxisTrackerMount, _unwrap_single_value ) @@ -78,15 +79,19 @@ class SingleAxisTracker(PVSystem): def __init__(self, axis_tilt=0, axis_azimuth=0, max_angle=90, backtrack=True, gcr=2.0/7.0, cross_axis_tilt=0.0, **kwargs): + mount_kwargs = { + k: kwargs.pop(k) for k in ['racking_model', 'module_height'] + if k in kwargs + } mount = SingleAxisTrackerMount(axis_tilt, axis_azimuth, max_angle, - backtrack, gcr, cross_axis_tilt) + backtrack, gcr, cross_axis_tilt, + **mount_kwargs) array_defaults = { 'albedo': None, 'surface_type': None, 'module': None, 'module_type': None, 'module_parameters': None, 'temperature_model_parameters': None, 'modules_per_string': 1, - 'racking_model': None, } array_kwargs = { key: kwargs.get(key, array_defaults[key]) for key in array_defaults From 74a6be4c5e30aa245a129719246a0211dd607160 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Sun, 28 Mar 2021 15:54:57 -0600 Subject: [PATCH 26/47] remove unnecessary fixture --- pvlib/tests/test_pvsystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 7af546c0de..2073b03111 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1853,7 +1853,7 @@ def test_PVSystem_change_surface_tilt_azimuth_multi(attr, two_array_system): setattr(two_array_system, attr, 0) -def test_PVSystem_get_albedo(two_array_system): +def test_PVSystem_get_albedo(): system = pvsystem.PVSystem( arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180), albedo=0.5)] ) From 9da9eb46244c244a90d9c1c84daa52db5ab603d8 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Mon, 29 Mar 2021 20:36:08 -0600 Subject: [PATCH 27/47] Revert "remove unnecessary fixture" This reverts commit 74a6be4c5e30aa245a129719246a0211dd607160. --- pvlib/tests/test_pvsystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 2073b03111..7af546c0de 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1853,7 +1853,7 @@ def test_PVSystem_change_surface_tilt_azimuth_multi(attr, two_array_system): setattr(two_array_system, attr, 0) -def test_PVSystem_get_albedo(): +def test_PVSystem_get_albedo(two_array_system): system = pvsystem.PVSystem( arrays=[pvsystem.Array(pvsystem.FixedMount(0, 180), albedo=0.5)] ) From 637014365bd6367f47dba6b8d7e5cdd198541155 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 14 May 2021 11:51:23 -0600 Subject: [PATCH 28/47] update merged test --- pvlib/tests/test_pvsystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 9c5578ea32..894c0683b0 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -2311,7 +2311,7 @@ def test_PVSystem_temperature_deprecated(funcname): ]) def test_Array_temperature_missing_parameters(model, keys): # test that a nice error is raised when required temp params are missing - array = pvsystem.Array() + array = pvsystem.Array(pvsystem.FixedMount(0, 180)) index = pd.date_range('2019-01-01', freq='h', periods=5) temps = pd.Series(25, index) irrads = pd.Series(1000, index) From 6181e96bfaa40da991fed08ebea87ee1e8ac9d24 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 14 May 2021 11:51:47 -0600 Subject: [PATCH 29/47] fix fuentes issue, sort of --- pvlib/pvsystem.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 9c5b5928fb..c53ed19761 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1551,10 +1551,12 @@ def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, 'module_height', 'wind_height', 'emissivity', 'absorption', 'surface_tilt', 'module_width', 'module_length'], self.temperature_model_parameters) - # default to using the Array attribute, but allow user to override - # with a custom surface_tilt value in temperature_model_parameters + # use surface_tilt from temp_model_params if available, otherwise + # fall back to the mount value. + # TODO: should this use mount.get_orientation? we don't have + # solar position available here... if 'surface_tilt' not in optional: - optional['surface_tilt'] = self.surface_tilt + optional['surface_tilt'] = self.mount.surface_tilt elif model == 'noct_sam': func = functools.partial(temperature.noct_sam, effective_irradiance=effective_irradiance) From 47b6884a436c5e31d7223372ad83b687c462a8bf Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 14 May 2021 12:22:31 -0600 Subject: [PATCH 30/47] pep8 --- pvlib/tracking.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pvlib/tracking.py b/pvlib/tracking.py index 47fe18bebb..327dd304cd 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -2,7 +2,6 @@ import pandas as pd from pvlib.tools import cosd, sind, tand -from pvlib.tools import _build_kwargs from pvlib.pvsystem import ( PVSystem, Array, SingleAxisTrackerMount, _unwrap_single_value ) From dbc1193734d1481bc6ea4d56c9765a05e52721e8 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 14 May 2021 12:22:58 -0600 Subject: [PATCH 31/47] pep8 --- pvlib/tests/test_tracking.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/tests/test_tracking.py b/pvlib/tests/test_tracking.py index a932b1a3b9..009e453bcb 100644 --- a/pvlib/tests/test_tracking.py +++ b/pvlib/tests/test_tracking.py @@ -6,7 +6,7 @@ from numpy.testing import assert_allclose import pvlib -from pvlib import tracking, pvsystem +from pvlib import tracking from .conftest import DATA_DIR, assert_frame_equal SINGLEAXIS_COL_ORDER = ['tracker_theta', 'aoi', From e0eeef456879ab24071f93b5981955f63f5ac3f2 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 14 May 2021 12:25:56 -0600 Subject: [PATCH 32/47] remove PVSystem.fuentes_celltemp surface_tilt parameter --- pvlib/pvsystem.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index c53ed19761..85c2d98071 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -746,8 +746,7 @@ def faiman_celltemp(self, poa_global, temp_air, wind_speed=1.0): @deprecated('0.9', alternative='PVSystem.get_cell_temperature', removal='0.10.0') - def fuentes_celltemp(self, poa_global, temp_air, wind_speed, - surface_tilt=None): + def fuentes_celltemp(self, poa_global, temp_air, wind_speed): """ Use :py:func:`temperature.fuentes` to calculate cell temperature. @@ -762,11 +761,6 @@ def fuentes_celltemp(self, poa_global, temp_air, wind_speed, wind_speed : pandas Series or tuple of Series Wind speed [m/s] - surface_tilt : pandas Series or tuple of Series, optional - Panel tilt from horizontal. Superseded by ``surface_tilt`` - values in ``self.arrays[i].temperature_model_parameters`` - (see Notes). [degrees] - Returns ------- temperature_cell : Series or tuple of Series From cf3fe2048013a507f3af1d78923a7670b81acbb5 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Fri, 14 May 2021 14:18:26 -0600 Subject: [PATCH 33/47] placeholder fuentes surface_tilt logic --- pvlib/pvsystem.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 85c2d98071..e80d29f551 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -827,7 +827,6 @@ def noct_sam_celltemp(self, poa_global, temp_air, wind_speed, @_unwrap_single_value def first_solar_spectral_loss(self, pw, airmass_absolute): - """ Use the :py:func:`first_solar_spectral_correction` function to calculate the spectral loss modifier. The model coefficients are @@ -1547,10 +1546,19 @@ def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, self.temperature_model_parameters) # use surface_tilt from temp_model_params if available, otherwise # fall back to the mount value. - # TODO: should this use mount.get_orientation? we don't have - # solar position available here... + # TODO: this logic is probably not what we want. if 'surface_tilt' not in optional: - optional['surface_tilt'] = self.mount.surface_tilt + # should this use mount.get_orientation? we don't have + # solar position available here... + try: + optional['surface_tilt'] = self.mount.surface_tilt + except AttributeError: + msg = ( + "surface_tilt is required for the fuentes model; " + "specify it as a key in temperature_model_parameters " + "or as an attribute of the Array's Mount." + ) + raise ValueError(msg) elif model == 'noct_sam': func = functools.partial(temperature.noct_sam, effective_irradiance=effective_irradiance) From 86292c5d60516ede7915b0d36c069679fe0fb657 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 20 May 2021 21:14:40 -0600 Subject: [PATCH 34/47] test updates --- pvlib/tests/test_pvsystem.py | 23 ++++------------------- pvlib/tests/test_tracking.py | 32 +------------------------------- 2 files changed, 5 insertions(+), 50 deletions(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 94c2905024..12e6a22743 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1614,7 +1614,8 @@ def test_PVSystem_multiple_array_creation(): array_two = pvsystem.Array(pvsystem.FixedMount(surface_tilt=15), module_parameters={'pdc0': 1}) pv_system = pvsystem.PVSystem(arrays=[array_one, array_two]) - assert pv_system.module_parameters == ({}, {'pdc0': 1}) + assert pv_system.arrays[0].module_parameters == {} + assert pv_system.arrays[1].module_parameters == {'pdc0': 1} assert pv_system.arrays == (array_one, array_two) with pytest.raises(TypeError): pvsystem.PVSystem(arrays=array_one) @@ -1802,8 +1803,8 @@ def test_PVSystem_multi_array_get_irradiance_multi_irrad(): 'racking_model', 'modules_per_string', 'strings_per_inverter']) def test_PVSystem_multi_array_attributes(attr): - array_one = pvsystem.Array() - array_two = pvsystem.Array() + array_one = pvsystem.Array(pvsystem.FixedMount()) + array_two = pvsystem.Array(pvsystem.FixedMount()) system = pvsystem.PVSystem(arrays=[array_one, array_two]) with pytest.raises(AttributeError): getattr(system, attr) @@ -2103,22 +2104,6 @@ def test_no_extra_kwargs(): pvsystem.PVSystem(arbitrary_kwarg='value') -def test_deprecated_attributes_single(pvwatts_system_defaults): - match = r"Use PVSystem.arrays\[0\].mount.surface_(tilt|azimuth)" - with pytest.warns(pvlibDeprecationWarning, match=match): - pvwatts_system_defaults.surface_tilt - with pytest.warns(pvlibDeprecationWarning, match=match): - pvwatts_system_defaults.surface_azimuth - - -def test_deprecated_attributes_multi(two_array_system): - match = "not supported for multi-array systems" - with pytest.raises(AttributeError, match=match): - two_array_system.surface_tilt - with pytest.raises(AttributeError, match=match): - two_array_system.surface_azimuth - - def test_AbstractMount_constructor(): match = "Can't instantiate abstract class AbstractMount" with pytest.raises(TypeError, match=match): diff --git a/pvlib/tests/test_tracking.py b/pvlib/tests/test_tracking.py index 5e8278bf3d..485895d7a2 100644 --- a/pvlib/tests/test_tracking.py +++ b/pvlib/tests/test_tracking.py @@ -301,35 +301,6 @@ def test_SingleAxisTracker_creation(): assert system.inverter == 'blarg' -def test_SingleAxisTracker_one_array_only(): - system = tracking.SingleAxisTracker( - arrays=[pvsystem.Array( - module='foo', - surface_tilt=None, - surface_azimuth=None - )] - ) - assert system.arrays[0].module == 'foo' - with pytest.raises(ValueError, - match="SingleAxisTracker does not support " - r"multiple arrays\."): - tracking.SingleAxisTracker( - arrays=[pvsystem.Array(module='foo'), - pvsystem.Array(module='bar')] - ) - with pytest.raises(ValueError, - match="Array must not have surface_tilt "): - tracking.SingleAxisTracker(arrays=[pvsystem.Array(module='foo')]) - with pytest.raises(ValueError, - match="Array must not have surface_tilt "): - tracking.SingleAxisTracker( - arrays=[pvsystem.Array(surface_azimuth=None)]) - with pytest.raises(ValueError, - match="Array must not have surface_tilt "): - tracking.SingleAxisTracker( - arrays=[pvsystem.Array(surface_tilt=None)]) - - def test_SingleAxisTracker_tracking(): system = tracking.SingleAxisTracker(max_angle=90, axis_tilt=30, axis_azimuth=180, gcr=2.0/7.0, @@ -444,10 +415,9 @@ def test_SingleAxisTracker___repr__(): name: None Array: name: None - mount: SingleAxisTrackerMount(axis_tilt=0, axis_azimuth=0, max_angle=45, backtrack=True, gcr=0.25, cross_axis_tilt=0.0) + mount: SingleAxisTrackerMount(axis_tilt=0, axis_azimuth=0, max_angle=45, backtrack=True, gcr=0.25, cross_axis_tilt=0.0, racking_model=None, module_height=None) module: blah albedo: 0.25 - racking_model: None module_type: None temperature_model_parameters: {'a': -3.56} strings: 1 From 739359eaa03d48fd451edbaab2080b1fb49211fa Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 17 Jun 2021 08:53:10 -0600 Subject: [PATCH 35/47] remove unused imports --- pvlib/pvsystem.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index caf4825ef0..328a8d5118 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -15,8 +15,7 @@ from abc import ABC, abstractmethod from typing import Optional -import warnings -from pvlib._deprecation import deprecated, pvlibDeprecationWarning +from pvlib._deprecation import deprecated from pvlib import (atmosphere, iam, inverter, irradiance, singlediode as _singlediode, temperature) From 59547f5c63a95b61f549a00b9b532f20f7e9a208 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 23 Jun 2021 16:51:15 -0600 Subject: [PATCH 36/47] remove fuentes override complexity --- pvlib/pvsystem.py | 15 --------------- pvlib/tests/test_pvsystem.py | 29 +---------------------------- 2 files changed, 1 insertion(+), 43 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 328a8d5118..fee4596bf0 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1594,21 +1594,6 @@ def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, 'module_height', 'wind_height', 'emissivity', 'absorption', 'surface_tilt', 'module_width', 'module_length'], self.temperature_model_parameters) - # use surface_tilt from temp_model_params if available, otherwise - # fall back to the mount value. - # TODO: this logic is probably not what we want. - if 'surface_tilt' not in optional: - # should this use mount.get_orientation? we don't have - # solar position available here... - try: - optional['surface_tilt'] = self.mount.surface_tilt - except AttributeError: - msg = ( - "surface_tilt is required for the fuentes model; " - "specify it as a key in temperature_model_parameters " - "or as an attribute of the Array's Mount." - ) - raise ValueError(msg) elif model == 'noct_sam': func = functools.partial(temperature.noct_sam, effective_irradiance=effective_irradiance) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 8530c6a800..c39729faf4 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -682,7 +682,7 @@ def test_PVSystem_multi_array_celltemp_poa_length_mismatch( def test_PVSystem_fuentes_celltemp(mocker): noct_installed = 45 - temp_model_params = {'noct_installed': noct_installed} + temp_model_params = {'noct_installed': noct_installed, 'surface_tilt': 0} system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params) spy = mocker.spy(temperature, 'fuentes') index = pd.date_range('2019-01-01 11:00', freq='h', periods=3) @@ -698,33 +698,6 @@ def test_PVSystem_fuentes_celltemp(mocker): name='tmod')) -def test_PVSystem_fuentes_celltemp_override(mocker): - # test that the surface_tilt value in the cell temp calculation can be - # overridden but defaults to the surface_tilt value in - # array.temperature_model_parameters - spy = mocker.spy(temperature, 'fuentes') - - noct_installed = 45 - index = pd.date_range('2019-01-01 11:00', freq='h', periods=3) - temps = pd.Series(25, index) - irrads = pd.Series(1000, index) - winds = pd.Series(1, index) - - # uses default value - temp_model_params = {'noct_installed': noct_installed} - system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params, - surface_tilt=20) - system.get_cell_temperature(irrads, temps, winds, model='fuentes') - assert spy.call_args[1]['surface_tilt'] == 20 - - # can be overridden - temp_model_params = {'noct_installed': noct_installed, 'surface_tilt': 30} - system = pvsystem.PVSystem(temperature_model_parameters=temp_model_params, - surface_tilt=20) - system.get_cell_temperature(irrads, temps, winds, model='fuentes') - assert spy.call_args[1]['surface_tilt'] == 30 - - def test_Array__infer_temperature_model_params(): array = pvsystem.Array(mount=pvsystem.FixedMount(0, 180, racking_model='open_rack'), module_parameters={}, From bed27c9c5e42cf0dd5ee8332ce7518f5e7feb3f2 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 23 Jun 2021 16:53:48 -0600 Subject: [PATCH 37/47] stickler --- pvlib/tests/test_pvsystem.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index c39729faf4..f28f687227 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -15,6 +15,7 @@ from pvlib import iam as _iam from pvlib import irradiance from pvlib.location import Location +from pvlib.pvsystem import FixedMount from pvlib import temperature from pvlib._deprecation import pvlibDeprecationWarning @@ -699,19 +700,22 @@ def test_PVSystem_fuentes_celltemp(mocker): def test_Array__infer_temperature_model_params(): - array = pvsystem.Array(mount=pvsystem.FixedMount(0, 180, racking_model='open_rack'), + array = pvsystem.Array(mount=FixedMount(0, 180, + racking_model='open_rack'), module_parameters={}, module_type='glass_polymer') expected = temperature.TEMPERATURE_MODEL_PARAMETERS[ 'sapm']['open_rack_glass_polymer'] assert expected == array._infer_temperature_model_params() - array = pvsystem.Array(mount=pvsystem.FixedMount(0, 180, racking_model='freestanding'), + array = pvsystem.Array(mount=FixedMount(0, 180, + racking_model='freestanding'), module_parameters={}, module_type='glass_polymer') expected = temperature.TEMPERATURE_MODEL_PARAMETERS[ 'pvsyst']['freestanding'] assert expected == array._infer_temperature_model_params() - array = pvsystem.Array(mount=pvsystem.FixedMount(0, 180, racking_model='insulated'), + array = pvsystem.Array(mount=FixedMount(0, 180, + racking_model='insulated'), module_parameters={}, module_type=None) expected = temperature.TEMPERATURE_MODEL_PARAMETERS[ @@ -1846,7 +1850,7 @@ def test_PVSystem___repr__(): temperature_model_parameters: {'a': -3.56} strings: 1 modules_per_string: 1 - inverter: blarg""" + inverter: blarg""" # noqa: E501 assert system.__repr__() == expected From 1103f99d05a73b29602808838e0ae35bd5648550 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 23 Jun 2021 17:10:46 -0600 Subject: [PATCH 38/47] update RST pages --- docs/sphinx/source/modelchain.rst | 13 +++++++------ docs/sphinx/source/pvsystem.rst | 2 -- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/sphinx/source/modelchain.rst b/docs/sphinx/source/modelchain.rst index 00b1d2c6fd..d2acc45dff 100644 --- a/docs/sphinx/source/modelchain.rst +++ b/docs/sphinx/source/modelchain.rst @@ -40,7 +40,7 @@ objects, module data, and inverter data. # pvlib imports import pvlib - from pvlib.pvsystem import PVSystem + from pvlib.pvsystem import PVSystem, FixedMount from pvlib.location import Location from pvlib.modelchain import ModelChain from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS @@ -53,13 +53,14 @@ objects, module data, and inverter data. sandia_module = sandia_modules['Canadian_Solar_CS5P_220M___2009_'] cec_inverter = cec_inverters['ABB__MICRO_0_25_I_OUTD_US_208__208V_'] -Now we create a Location object, a PVSystem object, and a ModelChain -object. +Now we create a Location object, a Mount object, a PVSystem object, and a +ModelChain object. .. ipython:: python location = Location(latitude=32.2, longitude=-110.9) - system = PVSystem(surface_tilt=20, surface_azimuth=200, + mount = FixedMount(surface_tilt=20, surface_azimuth=200) + system = PVSystem(mount=mount, module_parameters=sandia_module, inverter_parameters=cec_inverter, temperature_model_parameters=temperature_model_parameters) @@ -448,11 +449,11 @@ are in the same order as in ``PVSystem.arrays``. location = Location(latitude=32.2, longitude=-110.9) inverter_parameters = {'pdc0': 10000, 'eta_inv_nom': 0.96} module_parameters = {'pdc0': 250, 'gamma_pdc': -0.004} - array_one = Array(surface_tilt=20, surface_azimuth=200, + array_one = Array(mount=FixedMount(surface_tilt=20, surface_azimuth=200), module_parameters=module_parameters, temperature_model_parameters=temperature_model_parameters, modules_per_string=10, strings=2) - array_two = Array(surface_tilt=20, surface_azimuth=160, + array_two = Array(mount=FixedMount(surface_tilt=20, surface_azimuth=160), module_parameters=module_parameters, temperature_model_parameters=temperature_model_parameters, modules_per_string=10, strings=2) diff --git a/docs/sphinx/source/pvsystem.rst b/docs/sphinx/source/pvsystem.rst index 149a614279..221b78dafb 100644 --- a/docs/sphinx/source/pvsystem.rst +++ b/docs/sphinx/source/pvsystem.rst @@ -189,8 +189,6 @@ at the specified tilt and azimuth: # single south-facing array at 20 deg tilt system_one_array = pvsystem.PVSystem(surface_tilt=20, surface_azimuth=180) print(system_one_array.arrays[0].mount) - print(system_one_array.arrays[0].surface_tilt, - system_one_array.arrays[0].surface_azimuth) In the case of a PV system with several arrays, the parameters are specified From 73aba9ac61187379b006b708545af3720a04903a Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 23 Jun 2021 17:23:10 -0600 Subject: [PATCH 39/47] revert unnecessary docs change --- docs/sphinx/source/modelchain.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/sphinx/source/modelchain.rst b/docs/sphinx/source/modelchain.rst index d2acc45dff..27ee68e022 100644 --- a/docs/sphinx/source/modelchain.rst +++ b/docs/sphinx/source/modelchain.rst @@ -59,8 +59,7 @@ ModelChain object. .. ipython:: python location = Location(latitude=32.2, longitude=-110.9) - mount = FixedMount(surface_tilt=20, surface_azimuth=200) - system = PVSystem(mount=mount, + system = PVSystem(surface_tilt=20, surface_azimuth=200, module_parameters=sandia_module, inverter_parameters=cec_inverter, temperature_model_parameters=temperature_model_parameters) From f3ff722bace6fb47122a898003fbfb1a58a13c22 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Mon, 28 Jun 2021 20:54:20 -0600 Subject: [PATCH 40/47] add link to pvsystem and modelchain pages in api listing --- docs/sphinx/source/api.rst | 3 ++- docs/sphinx/source/modelchain.rst | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index 853dfbf628..eed2c03595 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -13,7 +13,8 @@ object-oriented programming. These classes can help users keep track of data in a more organized way, and can help to simplify the modeling process. The classes do not add any functionality beyond the procedural code. Most of the object methods are simple wrappers around the -corresponding procedural code. +corresponding procedural code. For examples of using these classes, see +the :ref:`pvsystemdoc` and :ref:`modelchaindoc` pages. .. autosummary:: :toctree: generated/ diff --git a/docs/sphinx/source/modelchain.rst b/docs/sphinx/source/modelchain.rst index 27ee68e022..8c7c87486f 100644 --- a/docs/sphinx/source/modelchain.rst +++ b/docs/sphinx/source/modelchain.rst @@ -1,3 +1,4 @@ +.. _modelchaindoc: ModelChain ========== From 9c0c3ffc756a94767bf247d79e611a4963aaa415 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Mon, 28 Jun 2021 20:55:49 -0600 Subject: [PATCH 41/47] other changes from review --- pvlib/pvsystem.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index fee4596bf0..1be0c68a20 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1698,6 +1698,7 @@ def get_orientation(self, solar_zenith, solar_azimuth): ------- orientation : dict-like A dict-like object with keys `'surface_tilt', 'surface_azimuth'` + (typically a dict or pandas.DataFrame) """ @@ -1715,6 +1716,14 @@ class FixedMount(AbstractMount): surface_azimuth : float, default 180 Azimuth angle of the module surface. North=0, East=90, South=180, West=270. [degrees] + + racking_model : str, optional + Valid strings are 'open_rack', 'close_mount', and 'insulated_back'. + Used to identify a parameter set for the SAPM cell temperature model. + + module_height : float, optional + The height above ground of the center of the module [m]. Used for + the Fuentes cell temperature model. """ surface_tilt: float = 0.0 @@ -1775,12 +1784,20 @@ class SingleAxisTrackerMount(AbstractMount): cross-axis tilt if the tracker axes plane slopes up to the east. Use :func:`~pvlib.tracking.calc_cross_axis_tilt` to calculate `cross_axis_tilt`. [degrees] + + racking_model : str, optional + Valid strings are 'open_rack', 'close_mount', and 'insulated_back'. + Used to identify a parameter set for the SAPM cell temperature model. + + module_height : float, optional + The height above ground of the center of the module [m]. Used for + the Fuentes cell temperature model. """ axis_tilt: float = 0.0 axis_azimuth: float = 0.0 max_angle: float = 90.0 backtrack: bool = True - gcr: float = 2/7 + gcr: float = 2.0/7.0 cross_axis_tilt: float = 0.0 racking_model: Optional[str] = None module_height: Optional[float] = None From 35da280224e2e84c386c4a30df1924d3a95a6c98 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Mon, 28 Jun 2021 21:07:40 -0600 Subject: [PATCH 42/47] get module_height from mount instead of temperature_model_parameters --- pvlib/pvsystem.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 1be0c68a20..8b7ae0e992 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1591,9 +1591,11 @@ def get_cell_temperature(self, poa_global, temp_air, wind_speed, model, func = temperature.fuentes required = _build_tcell_args(['noct_installed']) optional = _build_kwargs([ - 'module_height', 'wind_height', 'emissivity', 'absorption', + 'wind_height', 'emissivity', 'absorption', 'surface_tilt', 'module_width', 'module_length'], self.temperature_model_parameters) + if self.mount.module_height is not None: + optional['module_height'] = self.mount.module_height elif model == 'noct_sam': func = functools.partial(temperature.noct_sam, effective_irradiance=effective_irradiance) From 1a43ea83a916480b5881bc9450dcfa408317be2b Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 29 Jun 2021 08:42:56 -0600 Subject: [PATCH 43/47] coverage for fuentes module_height parameter --- pvlib/tests/test_pvsystem.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index f28f687227..505016a7ce 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -699,6 +699,20 @@ def test_PVSystem_fuentes_celltemp(mocker): name='tmod')) +def test_PVSystem_fuentes_module_height(mocker): + # check that fuentes picks up Array.mount.module_height correctly + # (temperature.fuentes defaults to 5 for module_height) + array = pvsystem.Array(mount=FixedMount(module_height=3), + temperature_model_parameters={'noct_installed': 45}) + spy = mocker.spy(temperature, 'fuentes') + index = pd.date_range('2019-01-01 11:00', freq='h', periods=3) + temps = pd.Series(25, index) + irrads = pd.Series(1000, index) + winds = pd.Series(1, index) + _ = array.get_cell_temperature(irrads, temps, winds, model='fuentes') + assert spy.call_args[1]['module_height'] == 3 + + def test_Array__infer_temperature_model_params(): array = pvsystem.Array(mount=FixedMount(0, 180, racking_model='open_rack'), From c1738e9c83ea2bab9e510c7a79936b9ecf3a7f14 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 21 Jul 2021 12:09:07 -0600 Subject: [PATCH 44/47] deprecate SingleAxisTracker --- docs/sphinx/source/whatsnew/v0.9.0.rst | 4 +++ pvlib/tests/test_tracking.py | 48 +++++++++++--------------- pvlib/tracking.py | 2 ++ 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst index b31989e084..24b4efb457 100644 --- a/docs/sphinx/source/whatsnew/v0.9.0.rst +++ b/docs/sphinx/source/whatsnew/v0.9.0.rst @@ -100,6 +100,10 @@ Deprecations * ``PVSystem.surface_azimuth`` * ``PVSystem.temperature_model_parameters`` +* The :py:class:`pvlib.tracking.SingleAxisTracker` class is deprecated and + replaced by using :py:class:`pvlib.pvsystem.PVSystem` with the new + :py:class:`pvlib.pvsystem.SingleAxisTrackerMount` (:pull:`1176`) + Enhancements ~~~~~~~~~~~~ diff --git a/pvlib/tests/test_tracking.py b/pvlib/tests/test_tracking.py index 485895d7a2..9a589e403a 100644 --- a/pvlib/tests/test_tracking.py +++ b/pvlib/tests/test_tracking.py @@ -8,6 +8,7 @@ import pvlib from pvlib import tracking from .conftest import DATA_DIR, assert_frame_equal +from pvlib._deprecation import pvlibDeprecationWarning SINGLEAXIS_COL_ORDER = ['tracker_theta', 'aoi', 'surface_azimuth', 'surface_tilt'] @@ -289,22 +290,11 @@ def test_low_sun_angles(): assert_allclose(expected[k], v) -def test_SingleAxisTracker_creation(): - system = tracking.SingleAxisTracker(max_angle=45, - gcr=.25, - module='blah', - inverter='blarg') - - assert system.max_angle == 45 - assert system.gcr == .25 - assert system.arrays[0].module == 'blah' - assert system.inverter == 'blarg' - - def test_SingleAxisTracker_tracking(): - system = tracking.SingleAxisTracker(max_angle=90, axis_tilt=30, - axis_azimuth=180, gcr=2.0/7.0, - backtrack=True) + with pytest.warns(pvlibDeprecationWarning): + system = tracking.SingleAxisTracker(max_angle=90, axis_tilt=30, + axis_azimuth=180, gcr=2.0/7.0, + backtrack=True) apparent_zenith = pd.Series([30]) apparent_azimuth = pd.Series([135]) @@ -324,9 +314,10 @@ def test_SingleAxisTracker_tracking(): pvsyst_solar_height = 27.315 pvsyst_axis_tilt = 20. pvsyst_axis_azimuth = 20. - pvsyst_system = tracking.SingleAxisTracker( - max_angle=60., axis_tilt=pvsyst_axis_tilt, - axis_azimuth=180+pvsyst_axis_azimuth, backtrack=False) + with pytest.warns(pvlibDeprecationWarning): + pvsyst_system = tracking.SingleAxisTracker( + max_angle=60., axis_tilt=pvsyst_axis_tilt, + axis_azimuth=180+pvsyst_axis_azimuth, backtrack=False) # the definition of azimuth is different from PYsyst apparent_azimuth = pd.Series([180+pvsyst_solar_azimuth]) apparent_zenith = pd.Series([90-pvsyst_solar_height]) @@ -342,9 +333,10 @@ def test_SingleAxisTracker_tracking(): # 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) + with pytest.warns(pvlibDeprecationWarning): + 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]) @@ -356,9 +348,10 @@ def test_get_aoi(): def test_get_irradiance(): - system = tracking.SingleAxisTracker(max_angle=90, axis_tilt=30, - axis_azimuth=180, gcr=2.0/7.0, - backtrack=True) + with pytest.warns(pvlibDeprecationWarning): + system = tracking.SingleAxisTracker(max_angle=90, axis_tilt=30, + axis_azimuth=180, gcr=2.0/7.0, + backtrack=True) times = pd.date_range(start='20160101 1200-0700', end='20160101 1800-0700', freq='6H') # latitude=32, longitude=-111 @@ -402,9 +395,10 @@ def test_get_irradiance(): def test_SingleAxisTracker___repr__(): - system = tracking.SingleAxisTracker( - max_angle=45, gcr=.25, module='blah', inverter='blarg', - temperature_model_parameters={'a': -3.56}) + with pytest.warns(pvlibDeprecationWarning): + system = tracking.SingleAxisTracker( + max_angle=45, gcr=.25, module='blah', inverter='blarg', + temperature_model_parameters={'a': -3.56}) expected = """SingleAxisTracker: axis_tilt: 0 axis_azimuth: 0 diff --git a/pvlib/tracking.py b/pvlib/tracking.py index 24542e3f39..67d9e52682 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -6,8 +6,10 @@ PVSystem, Array, SingleAxisTrackerMount, _unwrap_single_value ) from pvlib import irradiance, atmosphere +from pvlib._deprecation import deprecated +@deprecated('0.9.0', alternative='PVSystem with SingleAxisTrackingMount') class SingleAxisTracker(PVSystem): """ A class for single-axis trackers that inherits the PV modeling methods from From 781b13312d5cf0a37ee897083896ca120bfe742b Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Wed, 21 Jul 2021 12:33:03 -0600 Subject: [PATCH 45/47] suppress SAT deprecation warnings in tests --- pvlib/tests/test_modelchain.py | 39 ++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 0e2b4d0205..451df78216 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -734,12 +734,13 @@ def test_run_model_with_weather_noct_sam_temp(sapm_dc_snl_ac_system, location, def test_run_model_tracker(sapm_dc_snl_ac_system, location, weather, mocker): - system = SingleAxisTracker( - module_parameters=sapm_dc_snl_ac_system.arrays[0].module_parameters, - temperature_model_parameters=( - sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters - ), - inverter_parameters=sapm_dc_snl_ac_system.inverter_parameters) + with pytest.warns(pvlibDeprecationWarning): + system = SingleAxisTracker( + module_parameters=sapm_dc_snl_ac_system.arrays[0].module_parameters, + temperature_model_parameters=( + sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters + ), + inverter_parameters=sapm_dc_snl_ac_system.inverter_parameters) mocker.spy(system, 'singleaxis') mc = ModelChain(system, location) mc.run_model(weather) @@ -755,12 +756,13 @@ def test_run_model_tracker(sapm_dc_snl_ac_system, location, weather, mocker): def test_run_model_tracker_list( sapm_dc_snl_ac_system, location, weather, mocker): - system = SingleAxisTracker( - module_parameters=sapm_dc_snl_ac_system.arrays[0].module_parameters, - temperature_model_parameters=( - sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters - ), - inverter_parameters=sapm_dc_snl_ac_system.inverter_parameters) + with pytest.warns(pvlibDeprecationWarning): + system = SingleAxisTracker( + module_parameters=sapm_dc_snl_ac_system.arrays[0].module_parameters, + temperature_model_parameters=( + sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters + ), + inverter_parameters=sapm_dc_snl_ac_system.inverter_parameters) mocker.spy(system, 'singleaxis') mc = ModelChain(system, location) mc.run_model([weather]) @@ -1027,12 +1029,13 @@ def test_run_model_from_poa_arrays_solar_position_weather( def test_run_model_from_poa_tracking(sapm_dc_snl_ac_system, location, total_irrad): - system = SingleAxisTracker( - module_parameters=sapm_dc_snl_ac_system.arrays[0].module_parameters, - temperature_model_parameters=( - sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters - ), - inverter_parameters=sapm_dc_snl_ac_system.inverter_parameters) + with pytest.warns(pvlibDeprecationWarning): + system = SingleAxisTracker( + module_parameters=sapm_dc_snl_ac_system.arrays[0].module_parameters, + temperature_model_parameters=( + sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters + ), + inverter_parameters=sapm_dc_snl_ac_system.inverter_parameters) mc = ModelChain(system, location, aoi_model='no_loss', spectral_model='no_loss') ac = mc.run_model_from_poa(total_irrad).results.ac From 22341ec401d956efe5614b0848f6f8e3c326d4ee Mon Sep 17 00:00:00 2001 From: Kevin Anderson <57452607+kanderso-nrel@users.noreply.github.com> Date: Thu, 22 Jul 2021 09:43:58 -0600 Subject: [PATCH 46/47] Apply suggestions from code review Co-authored-by: Will Holmgren --- pvlib/tests/test_modelchain.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py index 451df78216..e52bda72bd 100644 --- a/pvlib/tests/test_modelchain.py +++ b/pvlib/tests/test_modelchain.py @@ -736,7 +736,7 @@ def test_run_model_with_weather_noct_sam_temp(sapm_dc_snl_ac_system, location, def test_run_model_tracker(sapm_dc_snl_ac_system, location, weather, mocker): with pytest.warns(pvlibDeprecationWarning): system = SingleAxisTracker( - module_parameters=sapm_dc_snl_ac_system.arrays[0].module_parameters, + module_parameters=sapm_dc_snl_ac_system.arrays[0].module_parameters, # noqa: E501 temperature_model_parameters=( sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters ), @@ -758,7 +758,7 @@ def test_run_model_tracker_list( sapm_dc_snl_ac_system, location, weather, mocker): with pytest.warns(pvlibDeprecationWarning): system = SingleAxisTracker( - module_parameters=sapm_dc_snl_ac_system.arrays[0].module_parameters, + module_parameters=sapm_dc_snl_ac_system.arrays[0].module_parameters, # noqa: E501 temperature_model_parameters=( sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters ), @@ -1031,7 +1031,7 @@ def test_run_model_from_poa_tracking(sapm_dc_snl_ac_system, location, total_irrad): with pytest.warns(pvlibDeprecationWarning): system = SingleAxisTracker( - module_parameters=sapm_dc_snl_ac_system.arrays[0].module_parameters, + module_parameters=sapm_dc_snl_ac_system.arrays[0].module_parameters, # noqa: E501 temperature_model_parameters=( sapm_dc_snl_ac_system.arrays[0].temperature_model_parameters ), From 6df84084b1d4039885ab6bf655fe3c9c6485c616 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 22 Jul 2021 10:12:53 -0600 Subject: [PATCH 47/47] Tracking -> Tracker --- pvlib/tracking.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/tracking.py b/pvlib/tracking.py index 67d9e52682..032b35d44a 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -9,7 +9,7 @@ from pvlib._deprecation import deprecated -@deprecated('0.9.0', alternative='PVSystem with SingleAxisTrackingMount') +@deprecated('0.9.0', alternative='PVSystem with SingleAxisTrackerMount') class SingleAxisTracker(PVSystem): """ A class for single-axis trackers that inherits the PV modeling methods from