From 499e3c1de8363e9e63211b9d034d2866d3f54dfd Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Mon, 25 Jan 2021 10:38:04 -0700 Subject: [PATCH 1/4] add SingleAxisArray. Array to FixedTiltArray --- pvlib/pvsystem.py | 431 ++++++++++++++++++++++++++++++++++++++++------ pvlib/tracking.py | 2 +- 2 files changed, 381 insertions(+), 52 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index afc1c195cf..e5b8655ee0 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -15,7 +15,7 @@ from pvlib._deprecation import deprecated from pvlib import (atmosphere, iam, inverter, irradiance, - singlediode as _singlediode, temperature) + singlediode as _singlediode, temperature, tracking) from pvlib.tools import _build_kwargs from pvlib._deprecation import pvlibDeprecationWarning @@ -96,9 +96,9 @@ class PVSystem: Parameters ---------- - arrays : iterable of Array, optional + arrays : iterable of FixedTiltArray or SingleAxisArray, optional List of arrays that are part of the system. If not specified - a single array is created from the other parameters (e.g. + a single FixedTiltArray is created from the other parameters (e.g. `surface_tilt`, `surface_azimuth`). If `arrays` is specified the following parameters are ignored: @@ -191,7 +191,7 @@ def __init__(self, racking_model=None, losses_parameters=None, name=None): if arrays is None: - self.arrays = (Array( + self.arrays = (FixedTiltArray( surface_tilt, surface_azimuth, albedo, @@ -1047,9 +1047,9 @@ def num_arrays(self): return len(self.arrays) -class Array: +class BaseArray: """ - An Array is a set of of modules at the same orientation. + A set of modules described by common parameters. Specifically, an array is defined by tilt, azimuth, the module parameters, the number of parallel strings of modules @@ -1057,15 +1057,6 @@ class Array: 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. - albedo : None or float, default None The ground albedo. If ``None``, will attempt to use ``surface_type`` to look up an albedo value in @@ -1105,15 +1096,12 @@ class Array: """ def __init__(self, - surface_tilt=0, surface_azimuth=180, 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 self.surface_type = surface_type if albedo is None: @@ -1142,14 +1130,24 @@ def __init__(self, self.name = name def __repr__(self): - attrs = ['name', 'surface_tilt', 'surface_azimuth', 'module', + attrs = ['name', 'module', 'albedo', 'racking_model', 'module_type', 'temperature_model_parameters', 'strings', 'modules_per_string'] - return 'Array:\n ' + '\n '.join( + return 'BaseArray:\n ' + '\n '.join( f'{attr}: {getattr(self, attr)}' for attr in attrs ) + def _extend_repr(self, name, attributes): + """refactor into non-cringy super() gymnastics""" + this_repr = (f'{name}:\n ' + '\n '.join( + f'{attr}: {getattr(self, attr)}' for attr in attributes)) + # get the parent BaseArray info + basearray_repr = super().__repr__() + # remove the first line (contains 'BaseArray: \n') + basearray_repr = '\n'.join(basearray_repr.split('\n')[1:]) + return this_repr + '\n' + basearray_repr + def _infer_temperature_model_params(self): # try to infer temperature model parameters from from racking_model # and module_type @@ -1208,6 +1206,142 @@ def _infer_cell_type(self): return cell_type + def get_iam(self, aoi, iam_model='physical'): + """ + Determine the incidence angle modifier using the method specified by + ``iam_model``. + + Parameters for the selected IAM model are expected to be in + ``Array.module_parameters``. Default parameters are available for + the 'physical', 'ashrae' and 'martin_ruiz' models. + + Parameters + ---------- + aoi : numeric + The angle of incidence in degrees. + + aoi_model : string, default 'physical' + The IAM model to be used. Valid strings are 'physical', 'ashrae', + 'martin_ruiz' and 'sapm'. + + Returns + ------- + iam : numeric + The AOI modifier. + + Raises + ------ + ValueError + if `iam_model` is not a valid model name. + """ + model = iam_model.lower() + if model in ['ashrae', 'physical', 'martin_ruiz']: + param_names = iam._IAM_MODEL_PARAMS[model] + kwargs = _build_kwargs(param_names, self.module_parameters) + func = getattr(iam, model) + return func(aoi, **kwargs) + elif model == 'sapm': + return iam.sapm(aoi, self.module_parameters) + elif model == 'interp': + raise ValueError(model + ' is not implemented as an IAM model ' + 'option for Array') + else: + raise ValueError(model + ' is not a valid IAM model') + + +class FixedTiltArray(BaseArray): + """ + A set of of modules at a fixed orientation. + + Specifically, a FixedTiltArray is defined by tilt, azimuth, 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. + + albedo : None or float, default None + The ground albedo. If ``None``, will attempt to use + ``surface_type`` to look up an albedo value in + ``irradiance.SURFACE_ALBEDOS``. If a surface albedo + cannot be found then 0.25 is used. + + surface_type : None or string, default None + The ground surface type. See ``irradiance.SURFACE_ALBEDOS`` + for valid values. + + module : None or string, default None + The model name of the modules. + May be used to look up the module_parameters dictionary + via some other method. + + module_type : None or string, default None + Describes the module's construction. Valid strings are 'glass_polymer' + and 'glass_glass'. Used for cell and module temperature calculations. + + module_parameters : None, dict or Series, default None + Parameters for the module model, e.g., SAPM, CEC, or other. + + temperature_model_parameters : None, dict or Series, default None. + Parameters for the module temperature model, e.g., SAPM, Pvsyst, or + other. + + modules_per_string: int, default 1 + Number of modules per string in the 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. + + """ + + def __init__( + self, + surface_tilt=0, + surface_azimuth=180, + 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 + + super().__init__( + albedo=albedo, + surface_type=surface_type, + module=module, + module_type=module_type, + module_parameters=module_parameters, + temperature_model_parameters=temperature_model_parameters, + modules_per_string=modules_per_string, + strings=strings, + racking_model=racking_model, + name=name + ) + + def __repr__(self): + attrs = ['surface_tilt', 'surface_azimuth'] + return self._extend_repr('FixedTiltArray', attrs) + def get_aoi(self, solar_zenith, solar_azimuth): """ Get the angle of incidence on the array. @@ -1283,47 +1417,242 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, albedo=self.albedo, **kwargs) - def get_iam(self, aoi, iam_model='physical'): - """ - Determine the incidence angle modifier using the method specified by - ``iam_model``. - Parameters for the selected IAM model are expected to be in - ``Array.module_parameters``. Default parameters are available for - the 'physical', 'ashrae' and 'martin_ruiz' models. +class SingleAxisArray(BaseArray): + """ + A set of of modules that track the sun along a single axis. + + 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, in decimal degrees. + + axis_azimuth : float, default 0 + A value denoting the compass direction along which the axis of + rotation lies. Measured in decimal degrees east of north. + + max_angle : float, default 90 + A value denoting the maximum rotation angle, in decimal degrees, + 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. + + 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. + + 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] + + albedo : None or float, default None + The ground albedo. If ``None``, will attempt to use + ``surface_type`` to look up an albedo value in + ``irradiance.SURFACE_ALBEDOS``. If a surface albedo + cannot be found then 0.25 is used. + + surface_type : None or string, default None + The ground surface type. See ``irradiance.SURFACE_ALBEDOS`` + for valid values. + + module : None or string, default None + The model name of the modules. + May be used to look up the module_parameters dictionary + via some other method. + + module_type : None or string, default None + Describes the module's construction. Valid strings are 'glass_polymer' + and 'glass_glass'. Used for cell and module temperature calculations. + + module_parameters : None, dict or Series, default None + Parameters for the module model, e.g., SAPM, CEC, or other. + + temperature_model_parameters : None, dict or Series, default None. + Parameters for the module temperature model, e.g., SAPM, Pvsyst, or + other. + + modules_per_string: int, default 1 + Number of modules per string in the 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. + + """ + + def __init__( + self, + axis_tilt=0, + axis_azimuth=0, + max_angle=90, + backtrack=True, + gcr=2.0/7.0, + cross_axis_tilt=0.0, + 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.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 + + super().__init__( + albedo=albedo, + surface_type=surface_type, + module=module, + module_type=module_type, + module_parameters=module_parameters, + temperature_model_parameters=temperature_model_parameters, + modules_per_string=modules_per_string, + strings=strings, + racking_model=racking_model, + name=name + ) + + def __repr__(self): + attrs = ['axis_tilt', 'axis_azimuth', 'max_angle', 'backtrack', 'gcr', + 'cross_axis_tilt'] + return self._extend_repr('SingleAxisArray', attrs) + + def _singleaxis(self, apparent_zenith, apparent_azimuth): + """ + Get tracking data. See :py:func:`pvlib.tracking.singleaxis` more + detail. Parameters ---------- - aoi : numeric - The angle of incidence in degrees. + apparent_zenith : float, 1d array, or Series + Solar apparent zenith angles in decimal degrees. - aoi_model : string, default 'physical' - The IAM model to be used. Valid strings are 'physical', 'ashrae', - 'martin_ruiz' and 'sapm'. + apparent_azimuth : float, 1d array, or Series + Solar apparent azimuth angles in decimal degrees. Returns ------- - iam : numeric - The AOI modifier. + tracking data + """ + tracking_data = tracking.singleaxis( + apparent_zenith, apparent_azimuth, + self.axis_tilt, self.axis_azimuth, + self.max_angle, self.backtrack, + self.gcr, self.cross_axis_tilt + ) + return tracking_data - Raises - ------ - ValueError - if `iam_model` is not a valid model name. + def get_aoi(self, solar_zenith, solar_azimuth): """ - model = iam_model.lower() - if model in ['ashrae', 'physical', 'martin_ruiz']: - param_names = iam._IAM_MODEL_PARAMS[model] - kwargs = _build_kwargs(param_names, self.module_parameters) - func = getattr(iam, model) - return func(aoi, **kwargs) - elif model == 'sapm': - return iam.sapm(aoi, self.module_parameters) - elif model == 'interp': - raise ValueError(model + ' is not implemented as an IAM model ' - 'option for Array') - else: - raise ValueError(model + ' is not a valid IAM model') + Get the angle of incidence on the array. + + Parameters + ---------- + solar_zenith : float or Series + Solar zenith angle. + solar_azimuth : float or Series + Solar azimuth angle + + Returns + ------- + aoi : Series + Then angle of incidence. + """ + tracking = self._singleaxis(solar_zenith, solar_azimuth) + return irradiance.aoi( + tracking['surface_tilt'], + tracking['surface_azimuth'], + solar_zenith, + solar_azimuth + ) + + def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, + dni_extra=None, airmass=None, model='haydavies', + **kwargs): + """ + Get plane of array irradiance components. + + Uses the :py:func:`pvlib.irradiance.get_total_irradiance` function to + calculate the plane of array irradiance components for a surface + defined by ``self.surface_tilt`` and ``self.surface_azimuth`` with + albedo ``self.albedo``. + + Parameters + ---------- + solar_zenith : float or Series. + Solar zenith angle. + solar_azimuth : float or Series. + Solar azimuth angle. + dni : float or Series + Direct Normal Irradiance + ghi : float or Series + Global horizontal irradiance + dhi : float or Series + Diffuse horizontal irradiance + dni_extra : None, float or Series, default None + Extraterrestrial direct normal irradiance + airmass : None, float or Series, default None + Airmass + model : String, default 'haydavies' + Irradiance model. + + kwargs + Extra parameters passed to + :py:func:`pvlib.irradiance.get_total_irradiance`. + + Returns + ------- + poa_irradiance : DataFrame + Column names are: ``total, beam, sky, ground``. + """ + # not needed for all models, but this is easier + if dni_extra is None: + dni_extra = irradiance.get_extra_radiation(solar_zenith.index) + + if airmass is None: + airmass = atmosphere.get_relative_airmass(solar_zenith) + + tracking = self._singleaxis(solar_zenith, solar_azimuth) + + return irradiance.get_total_irradiance( + tracking['surface_tilt'], + tracking['surface_azimuth'], + solar_zenith, solar_azimuth, + dni, ghi, dhi, + dni_extra=dni_extra, + airmass=airmass, + model=model, + albedo=self.albedo, + **kwargs) def calcparams_desoto(effective_irradiance, temp_cell, diff --git a/pvlib/tracking.py b/pvlib/tracking.py index 5c3a160aa9..4e4308c048 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -6,6 +6,7 @@ from pvlib import irradiance, atmosphere +# add deprecation warning? class SingleAxisTracker(PVSystem): """ A class for single-axis trackers that inherits the PV modeling methods from @@ -75,7 +76,6 @@ 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 " From 7377dab17ff8e41a1b16622c1c8bad6bbda1c86d Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Mon, 25 Jan 2021 10:42:19 -0700 Subject: [PATCH 2/4] fix aoi method --- pvlib/pvsystem.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index e5b8655ee0..9c48f093b9 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1588,12 +1588,7 @@ def get_aoi(self, solar_zenith, solar_azimuth): Then angle of incidence. """ tracking = self._singleaxis(solar_zenith, solar_azimuth) - return irradiance.aoi( - tracking['surface_tilt'], - tracking['surface_azimuth'], - solar_zenith, - solar_azimuth - ) + return tracking['aoi'] def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, dni_extra=None, airmass=None, model='haydavies', From 6b770a1a68b572c0bd2cbaf34354e7630c1cd730 Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Mon, 25 Jan 2021 10:44:26 -0700 Subject: [PATCH 3/4] public method --- pvlib/pvsystem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 9c48f093b9..5e84648bde 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -1546,7 +1546,7 @@ def __repr__(self): 'cross_axis_tilt'] return self._extend_repr('SingleAxisArray', attrs) - def _singleaxis(self, apparent_zenith, apparent_azimuth): + def singleaxis(self, apparent_zenith, apparent_azimuth): """ Get tracking data. See :py:func:`pvlib.tracking.singleaxis` more detail. @@ -1587,7 +1587,7 @@ def get_aoi(self, solar_zenith, solar_azimuth): aoi : Series Then angle of incidence. """ - tracking = self._singleaxis(solar_zenith, solar_azimuth) + tracking = self.singleaxis(solar_zenith, solar_azimuth) return tracking['aoi'] def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, @@ -1636,7 +1636,7 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi, if airmass is None: airmass = atmosphere.get_relative_airmass(solar_zenith) - tracking = self._singleaxis(solar_zenith, solar_azimuth) + tracking = self.singleaxis(solar_zenith, solar_azimuth) return irradiance.get_total_irradiance( tracking['surface_tilt'], From 07e47eccfa04b9d28fd6f807fa94937e5d01a86a Mon Sep 17 00:00:00 2001 From: Will Holmgren Date: Tue, 26 Jan 2021 10:34:19 -0700 Subject: [PATCH 4/4] reprs --- pvlib/pvsystem.py | 26 +++++++++++++++----------- pvlib/tools.py | 12 ++++++++++++ 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 5e84648bde..549b08ffc2 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -15,7 +15,7 @@ from pvlib._deprecation import deprecated from pvlib import (atmosphere, iam, inverter, irradiance, - singlediode as _singlediode, temperature, tracking) + singlediode as _singlediode, temperature, tools) from pvlib.tools import _build_kwargs from pvlib._deprecation import pvlibDeprecationWarning @@ -1129,14 +1129,12 @@ def __init__(self, self.name = name - def __repr__(self): + def __repr__(self, name='BaseArray'): attrs = ['name', 'module', 'albedo', 'racking_model', 'module_type', 'temperature_model_parameters', 'strings', 'modules_per_string'] - return 'BaseArray:\n ' + '\n '.join( - f'{attr}: {getattr(self, attr)}' for attr in attrs - ) + return tools.repr(self, attrs, name=name, level=1) def _extend_repr(self, name, attributes): """refactor into non-cringy super() gymnastics""" @@ -1321,7 +1319,7 @@ def __init__( racking_model=None, name=None ): - + self.name = name self.surface_tilt = surface_tilt self.surface_azimuth = surface_azimuth @@ -1339,8 +1337,10 @@ def __init__( ) def __repr__(self): - attrs = ['surface_tilt', 'surface_azimuth'] - return self._extend_repr('FixedTiltArray', attrs) + parent_repr = super().__repr__(name='FixedArray') + attrs = ['name', 'surface_tilt', 'surface_azimuth'] + this_repr = tools.repr(self, attrs, level=1) + return parent_repr + '\n' + this_repr def get_aoi(self, solar_zenith, solar_azimuth): """ @@ -1521,6 +1521,7 @@ def __init__( name=None ): + self.name = name self.axis_tilt = axis_tilt self.axis_azimuth = axis_azimuth self.max_angle = max_angle @@ -1542,9 +1543,11 @@ def __init__( ) def __repr__(self): - attrs = ['axis_tilt', 'axis_azimuth', 'max_angle', 'backtrack', 'gcr', - 'cross_axis_tilt'] - return self._extend_repr('SingleAxisArray', attrs) + parent_repr = super().__repr__(name='SingleAxisArray') + attrs = ['name', 'axis_tilt', 'axis_azimuth', 'max_angle', 'backtrack', + 'gcr', 'cross_axis_tilt'] + this_repr = tools.repr(self, attrs, level=1) + return parent_repr + '\n' + this_repr def singleaxis(self, apparent_zenith, apparent_azimuth): """ @@ -1563,6 +1566,7 @@ def singleaxis(self, apparent_zenith, apparent_azimuth): ------- tracking data """ + from pvlib import tracking tracking_data = tracking.singleaxis( apparent_zenith, apparent_azimuth, self.axis_tilt, self.axis_azimuth, diff --git a/pvlib/tools.py b/pvlib/tools.py index b6ee3e7c3a..8d546eafca 100644 --- a/pvlib/tools.py +++ b/pvlib/tools.py @@ -318,3 +318,15 @@ def _golden_sect_DataFrame(params, VL, VH, func): raise Exception("EXCEPTION:iterations exceeded maximum (50)") return func(df, 'V1'), df['V1'] + + +def repr(obj, attributes, name=None, level=1): + if name is not None: + prefix = ' ' * (level - 1) + this_repr = f'{prefix}{name}:\n' + else: + this_repr = '' + indent = ' ' * level + this_repr += '\n'.join( + f'{indent}{attr}: {getattr(obj, attr)}' for attr in attributes) + return this_repr