diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index 0af200e67c..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/ @@ -21,6 +22,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/modelchain.rst b/docs/sphinx/source/modelchain.rst index 00b1d2c6fd..8c7c87486f 100644 --- a/docs/sphinx/source/modelchain.rst +++ b/docs/sphinx/source/modelchain.rst @@ -1,3 +1,4 @@ +.. _modelchaindoc: ModelChain ========== @@ -40,7 +41,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,8 +54,8 @@ 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 @@ -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 25b9a1c9e6..221b78dafb 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([array.module_parameters for array in system_two_arrays.arrays]) @@ -148,7 +149,7 @@ provided for each array, and the arrays are provided to 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`. @@ -179,27 +180,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.arrays[0].surface_tilt, - system_one_array.arrays[0].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 - [(array.surface_tilt, array.surface_azimuth) for array in system.arrays] + for array in system.arrays: + print(array.mount) The `surface_tilt` and `surface_azimuth` attributes are used in PVSystem @@ -215,8 +219,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.arrays[0].surface_tilt, - system_one_array.arrays[0].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) @@ -229,7 +232,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) @@ -240,7 +243,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 @@ -315,8 +318,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. diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst index c8d8b04c45..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 ~~~~~~~~~~~~ @@ -116,6 +120,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 diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 06230d6c95..8b7ae0e992 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -11,6 +11,9 @@ from urllib.request import urlopen import numpy as np import pandas as pd +from dataclasses import dataclass +from abc import ABC, abstractmethod +from typing import Optional from pvlib._deprecation import deprecated @@ -234,8 +237,7 @@ def __init__(self, array_losses_parameters = _build_kwargs(['dc_ohmic_percent'], losses_parameters) self.arrays = (Array( - surface_tilt, - surface_azimuth, + FixedMount(surface_tilt, surface_azimuth, racking_model), albedo, surface_type, module, @@ -244,7 +246,6 @@ def __init__(self, temperature_model_parameters, modules_per_string, strings_per_inverter, - racking_model, array_losses_parameters, ),) elif len(arrays) == 0: @@ -299,7 +300,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 @@ -813,11 +813,13 @@ 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 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` 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 @@ -867,7 +869,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 @@ -1165,25 +1166,25 @@ def temperature_model_parameters(self, value): @_unwrap_single_value @_check_deprecated_passthrough def surface_tilt(self): - return tuple(array.surface_tilt for array in self.arrays) + return tuple(array.mount.surface_tilt for array in self.arrays) @surface_tilt.setter @_check_deprecated_passthrough def surface_tilt(self, value): for array in self.arrays: - array.surface_tilt = value + array.mount.surface_tilt = value @property @_unwrap_single_value @_check_deprecated_passthrough def surface_azimuth(self): - return tuple(array.surface_azimuth for array in self.arrays) + return tuple(array.mount.surface_azimuth for array in self.arrays) @surface_azimuth.setter @_check_deprecated_passthrough def surface_azimuth(self, value): for array in self.arrays: - array.surface_azimuth = value + array.mount.surface_azimuth = value @property @_unwrap_single_value @@ -1201,13 +1202,13 @@ def albedo(self, value): @_unwrap_single_value @_check_deprecated_passthrough def racking_model(self): - return tuple(array.racking_model for array in self.arrays) + return tuple(array.mount.racking_model for array in self.arrays) @racking_model.setter @_check_deprecated_passthrough def racking_model(self, value): for array in self.arrays: - array.racking_model = value + array.mount.racking_model = value @property @_unwrap_single_value @@ -1243,20 +1244,16 @@ 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 + 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 The ground albedo. If ``None``, will attempt to use @@ -1290,26 +1287,22 @@ 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, - surface_tilt=0, surface_azimuth=180, + 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, array_losses_parameters=None, + array_losses_parameters=None, name=None): - self.surface_tilt = surface_tilt - self.surface_azimuth = surface_azimuth + self.mount = mount self.surface_type = surface_type if albedo is None: @@ -1324,7 +1317,6 @@ def __init__(self, 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 @@ -1343,10 +1335,11 @@ def __init__(self, self.name = name def __repr__(self): - attrs = ['name', 'surface_tilt', 'surface_azimuth', 'module', - 'albedo', 'racking_model', 'module_type', + attrs = ['name', 'mount', 'module', + 'albedo', 'module_type', 'temperature_model_parameters', 'strings', 'modules_per_string'] + return 'Array:\n ' + '\n '.join( f'{attr}: {getattr(self, attr)}' for attr in attrs ) @@ -1354,7 +1347,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: @@ -1424,7 +1417,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, @@ -1473,8 +1468,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, @@ -1595,13 +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) - # default to using the Array attribute, but allow user to override - # with a custom surface_tilt value in temperature_model_parameters - if 'surface_tilt' not in optional: - optional['surface_tilt'] = self.surface_tilt + 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) @@ -1683,6 +1677,145 @@ def dc_ohms_from_percent(self): self.strings) +@dataclass +class AbstractMount(ABC): + """ + A base class for Mount classes to extend. It is not intended to be + instantiated directly. + """ + + @abstractmethod + def get_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'` + (typically a dict or pandas.DataFrame) + """ + + +@dataclass +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] + + 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 + 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 + return { + 'surface_tilt': self.surface_tilt, + 'surface_azimuth': self.surface_azimuth, + } + + +@dataclass +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] + + 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.0/7.0 + 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 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_modelchain.py b/pvlib/tests/test_modelchain.py index 4d2edbccae..e52bda72bd 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() ) @@ -288,13 +288,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, @@ -312,13 +314,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, @@ -365,12 +369,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 ) @@ -696,7 +700,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.arrays[0].temperature_model_parameters = { - 'noct_installed': 45 + 'noct_installed': 45, 'surface_tilt': 30, } mc = ModelChain(sapm_dc_snl_ac_system, location) mc.temperature_model = 'fuentes' @@ -730,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, # noqa: E501 + 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) @@ -751,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, # noqa: E501 + 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]) @@ -1023,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, # noqa: E501 + 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 @@ -1439,6 +1446,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} @@ -1953,10 +1961,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): @@ -1964,16 +1975,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 4533e58567..505016a7ce 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 @@ -36,8 +37,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 +229,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 +279,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 +315,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 +372,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) @@ -397,10 +408,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 ) @@ -457,8 +470,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.get_cell_temperature( (1000, 1000), 25, 1, model='sapm' @@ -555,6 +570,7 @@ def test_PVSystem_multi_array_celltemp_functions(model, two_array_system): irrad_two = pd.Series(500, index=times) temp_air = pd.Series(25, index=times) wind_speed = pd.Series(1, index=times) + temp_one, temp_two = two_array_system.get_cell_temperature( (irrad_one, irrad_two), temp_air, wind_speed, model=model) assert (temp_one != temp_two).all() @@ -667,7 +683,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) @@ -683,47 +699,38 @@ 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 attribute of the PVSystem +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') - - 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 + _ = 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(module_parameters={}, - 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(module_parameters={}, - 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(module_parameters={}, - racking_model='insulated', + array = pvsystem.Array(mount=FixedMount(0, 180, + racking_model='insulated'), + module_parameters={}, module_type=None) expected = temperature.TEMPERATURE_MODEL_PARAMETERS[ 'pvsyst']['insulated'] @@ -731,7 +738,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 @@ -1434,8 +1442,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 @@ -1482,7 +1492,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, ) @@ -1529,7 +1540,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.]) @@ -1571,7 +1583,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) @@ -1597,7 +1609,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]) @@ -1625,9 +1638,12 @@ 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(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.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) @@ -1641,8 +1657,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(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 @@ -1703,8 +1721,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(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', @@ -1743,8 +1763,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', @@ -1804,40 +1824,6 @@ def test_PVSystem_multi_array_get_irradiance_multi_irrad(): assert not array_irrad[0].equals(array_irrad[1]) -def test_PVSystem_change_surface_azimuth(): - system = pvsystem.PVSystem(surface_azimuth=180) - with pytest.warns(pvlibDeprecationWarning): - assert system.surface_azimuth == 180 - with pytest.warns(pvlibDeprecationWarning): - system.surface_azimuth = 90 - with pytest.warns(pvlibDeprecationWarning): - assert system.surface_azimuth == 90 - - -def test_PVSystem_get_albedo(): - system = pvsystem.PVSystem( - arrays=[pvsystem.Array(albedo=0.5)] - ) - with pytest.warns(pvlibDeprecationWarning): - assert system.albedo == 0.5 - - -def test_PVSystem_modules_per_string(): - system = pvsystem.PVSystem( - arrays=[pvsystem.Array(modules_per_string=5)] - ) - with pytest.warns(pvlibDeprecationWarning): - assert system.modules_per_string == 5 - - -def test_PVSystem_strings_per_inverter(): - system = pvsystem.PVSystem( - arrays=[pvsystem.Array(strings=5)] - ) - with pytest.warns(pvlibDeprecationWarning): - assert system.strings_per_inverter == 5 - - @fail_on_pvlib_version('0.10') @pytest.mark.parametrize('attr', ['module_parameters', 'module', 'module_type', 'temperature_model_parameters', 'albedo', @@ -1845,8 +1831,8 @@ def test_PVSystem_strings_per_inverter(): '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) @@ -1871,23 +1857,23 @@ def test_PVSystem___repr__(): name: pv ftw Array: name: None - 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 modules_per_string: 1 - inverter: blarg""" + inverter: blarg""" # noqa: E501 assert system.__repr__() == expected 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(pvsystem.FixedMount(surface_tilt=30, + surface_azimuth=100)), + pvsystem.Array(pvsystem.FixedMount(surface_tilt=20, + surface_azimuth=220), name='foo')], inverter='blarg', ) @@ -1895,36 +1881,32 @@ 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, 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 - 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( - 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', @@ -1932,15 +1914,13 @@ def test_Array___repr__(): ) expected = """Array: name: biz - 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 @@ -2035,12 +2015,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]) @@ -2060,7 +2042,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): @@ -2118,7 +2102,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 @@ -2146,6 +2132,49 @@ def test_no_extra_kwargs(): pvsystem.PVSystem(arbitrary_kwarg='value') +def test_AbstractMount_constructor(): + match = "Can't instantiate abstract class AbstractMount" + with pytest.raises(TypeError, match=match): + _ = pvsystem.AbstractMount() + + +@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.get_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.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 + + def test_dc_ohms_from_percent(): expected = .1425 out = pvsystem.dc_ohms_from_percent(38, 8, 3, 1, 1) @@ -2183,7 +2212,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() @@ -2196,7 +2226,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() @@ -2209,7 +2240,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() @@ -2230,7 +2262,8 @@ 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() @@ -2261,7 +2294,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) diff --git a/pvlib/tests/test_tracking.py b/pvlib/tests/test_tracking.py index ee4a9b8d12..9a589e403a 100644 --- a/pvlib/tests/test_tracking.py +++ b/pvlib/tests/test_tracking.py @@ -6,8 +6,9 @@ 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 +from pvlib._deprecation import pvlibDeprecationWarning SINGLEAXIS_COL_ORDER = ['tracker_theta', 'aoi', 'surface_azimuth', 'surface_tilt'] @@ -289,51 +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_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, - 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]) @@ -353,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]) @@ -371,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]) @@ -385,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 @@ -431,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 @@ -444,16 +409,14 @@ 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, 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 modules_per_string: 1 - inverter: blarg""" + inverter: blarg""" # noqa: E501 assert system.__repr__() == expected diff --git a/pvlib/tracking.py b/pvlib/tracking.py index 9aa4919198..032b35d44a 100644 --- a/pvlib/tracking.py +++ b/pvlib/tracking.py @@ -2,10 +2,14 @@ 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 +from pvlib._deprecation import deprecated +@deprecated('0.9.0', alternative='PVSystem with SingleAxisTrackerMount') class SingleAxisTracker(PVSystem): """ A class for single-axis trackers that inherits the PV modeling methods from @@ -76,20 +80,31 @@ 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_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, + **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, + } + 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 +112,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',