Source code for pvfactors.geometry.pvarray

"""Module containing PV array classes, which will use PV rows and ground
geometries."""

import numpy as np
from pvfactors.config import X_ORIGIN_PVROWS, DISTANCE_TOLERANCE
from pvfactors.geometry.base import \
    _get_solar_2d_vectors, BasePVArray, _get_rotation_from_tilt_azimuth
from pvfactors.geometry.pvrow import TsPVRow
from pvfactors.geometry.pvground import TsGround


[docs]class OrderedPVArray(BasePVArray): """An ordered PV array has a flat horizontal ground, and pv rows which are all at the same height, with the same surface tilt and azimuth angles, and also all equally spaced. These simplifications allow faster and easier calculations. In the ordered PV array, the list of PV rows must be ordered from left to right (along the x-axis) in the 2D geometry.""" y_ground = 0. # ground will be at height = 0 by default
[docs] def __init__(self, axis_azimuth=None, gcr=None, pvrow_height=None, n_pvrows=None, pvrow_width=None, param_names=None, cut=None): """Initialize ordered PV array. List of PV rows will be ordered from left to right. Parameters ---------- axis_azimuth : float, optional Azimuth angle of rotation axis [deg] (Default = None) gcr : float, optional Ground coverage ratio (Default = None) pvrow_height : float, optional Unique height of all PV rows in [m] (Default = None) n_pvrows : int, optional Number of PV rows in the PV array (Default = None) pvrow_width : float, optional Width of the PV rows in the 2D plane in [m] (Default = None) param_names : list of str, optional List of surface parameter names for the PV surfaces (Default = None) cut : dict, optional Nested dictionary that tells if some PV row sides need to be discretized, and how (Default = None). Example: {1: {'front': 5}}, will create 5 segments on the front side of the PV row with index 1 """ # Initialize base parameters: common to all sorts of PV arrays super(OrderedPVArray, self).__init__(axis_azimuth=axis_azimuth) # These are the invariant parameters of the PV array self.gcr = gcr self.height = pvrow_height self.distance = (pvrow_width / gcr if (pvrow_width is not None) and (gcr is not None) else None) self.width = pvrow_width self.n_pvrows = n_pvrows self.param_names = [] if param_names is None else param_names self.cut = {} if cut is None else cut # These attributes will be updated at fitting time self.solar_2d_vectors = None self.n_states = None self.has_direct_shading = None self.rotation_vec = None self.shaded_length_front = None self.shaded_length_back = None # These attributes will be updated by the engine self.ts_vf_matrix = None self.ts_vf_aoi_matrix = None
@classmethod def init_from_dict(cls, pvarray_params, param_names=None): """Instantiate ordered PV array from dictionary of parameters Parameters ---------- pvarray_params : dict The parameters defining the PV array param_names : list of str, optional List of parameter names to pass to surfaces (Default = None) Returns ------- OrderedPVArray Initialized Ordered PV Array """ return cls(axis_azimuth=pvarray_params['axis_azimuth'], gcr=pvarray_params['gcr'], pvrow_height=pvarray_params['pvrow_height'], n_pvrows=pvarray_params['n_pvrows'], pvrow_width=pvarray_params['pvrow_width'], cut=pvarray_params.get('cut', {}), param_names=param_names) @classmethod def fit_from_dict_of_scalars(cls, pvarray_params, param_names=None): """Instantiate, and fit ordered PV array using dictionary of scalar inputs. Parameters ---------- pvarray_params : dict The parameters used for instantiation, fitting, and transformation param_names : list of str, optional List of parameter names to pass to surfaces (Default = None) Returns ------- OrderedPVArray Initialized, and fitted Ordered PV Array """ # Create pv array pvarray = cls.init_from_dict(pvarray_params, param_names=param_names) # Fit pv array to scalar values solar_zenith = np.array([pvarray_params['solar_zenith']]) solar_azimuth = np.array([pvarray_params['solar_azimuth']]) surface_tilt = np.array([pvarray_params['surface_tilt']]) surface_azimuth = np.array([pvarray_params['surface_azimuth']]) pvarray.fit(solar_zenith, solar_azimuth, surface_tilt, surface_azimuth) return pvarray def fit(self, solar_zenith, solar_azimuth, surface_tilt, surface_azimuth): """Fit the ordered PV array to the list of solar and surface angles. All intermediate PV array results necessary to build the geometries will be calculated here using vectorization as much as possible. Intemediate results include: PV row coordinates for all timestamps, ground element coordinates for all timestamps, cases of direct shading, ... Parameters ---------- solar_zenith : array-like or float Solar zenith angles [deg] solar_azimuth : array-like or float Solar azimuth angles [deg] surface_tilt : array-like or float Surface tilt angles, from 0 to 180 [deg] surface_azimuth : array-like or float Surface azimuth angles [deg] """ self.n_states = len(solar_zenith) # Calculate rotation angles rotation_vec = _get_rotation_from_tilt_azimuth( surface_azimuth, self.axis_azimuth, surface_tilt) # Save rotation vector self.rotation_vec = rotation_vec # Calculate the solar 2D vectors for all timestamps self.solar_2d_vectors = _get_solar_2d_vectors( solar_zenith, solar_azimuth, self.axis_azimuth) # Calculate the angle made by 2D sun vector and x-axis alpha_vec = np.arctan2(self.solar_2d_vectors[1], self.solar_2d_vectors[0]) # Calculate the coordinates of all PV rows for all timestamps self._calculate_pvrow_elements_coords(alpha_vec, rotation_vec) # Calculate ground elements coordinates for all timestamps self.ts_ground = TsGround.from_ts_pvrows_and_angles( self.ts_pvrows, alpha_vec, rotation_vec, y_ground=self.y_ground, flag_overlap=self.has_direct_shading, param_names=self.param_names) # Save surface rotation angles self.rotation_vec = rotation_vec # Index all timeseries surfaces self._index_all_ts_surfaces() def _calculate_pvrow_elements_coords(self, alpha_vec, rotation_vec): """Calculate PV row coordinate elements in a vectorized way, such as PV row boundary coordinates and shaded lengths. Parameters ---------- alpha_vec : array-like or float Angle made by 2d solar vector and x-axis [rad] rotation_vec : array-like or float Rotation angle of the PV rows [deg] """ # Initialize timeseries pv rows self.ts_pvrows = [] # Calculate interrow direct shading lengths self._calculate_interrow_shading(alpha_vec, rotation_vec) # Calculate coordinates of segments of each pv row side xy_centers = [(X_ORIGIN_PVROWS + idx * self.distance, self.height + self.y_ground) for idx in range(self.n_pvrows)] tilted_to_left = rotation_vec > 0. for idx_pvrow, xy_center in enumerate(xy_centers): # A special treatment needs to be applied to shaded lengths for # the PV rows at the edge of the PV array if idx_pvrow == 0: # the leftmost row doesn't have left neighbors shaded_length_front = np.where(tilted_to_left, 0., self.shaded_length_front) shaded_length_back = np.where(tilted_to_left, self.shaded_length_back, 0.) elif idx_pvrow == (self.n_pvrows - 1): # the rightmost row does have right neighbors shaded_length_front = np.where(tilted_to_left, self.shaded_length_front, 0.) shaded_length_back = np.where(tilted_to_left, 0., self.shaded_length_back) else: # use calculated shaded lengths shaded_length_front = self.shaded_length_front shaded_length_back = self.shaded_length_back # Create timeseries PV rows and add it to the list self.ts_pvrows.append(TsPVRow.from_raw_inputs( xy_center, self.width, rotation_vec, self.cut.get(idx_pvrow, {}), shaded_length_front, shaded_length_back, index=idx_pvrow, param_names=self.param_names)) def _calculate_interrow_shading(self, alpha_vec, rotation_vec): """Calculate the shaded length on front and back side of PV rows when direct shading happens, and in a vectorized way. Parameters ---------- alpha_vec : array-like or float Angle made by 2d solar vector and x-axis [rad] rotation_vec : array-like or float Rotation angle of the PV rows [deg] """ if self.n_pvrows > 1: # Calculate intermediate values for direct shading alpha_vec_deg = np.rad2deg(alpha_vec) theta_t = 90. - rotation_vec theta_t_rad = np.deg2rad(theta_t) beta = theta_t + alpha_vec_deg beta_rad = np.deg2rad(beta) delta = self.distance * ( np.sin(theta_t_rad) - np.cos(theta_t_rad) * np.tan(beta_rad)) # Calculate temporary shaded lengths tmp_shaded_length_front = np.maximum(0, self.width - delta) tmp_shaded_length_back = np.maximum(0, self.width + delta) # The shaded length can't be longer than PV row (meaning sun can't # be under the horizon or something...) self.shaded_length_front = np.where( tmp_shaded_length_front > self.width, 0, tmp_shaded_length_front) self.shaded_length_back = np.where( tmp_shaded_length_back > self.width, 0, tmp_shaded_length_back) else: # Since there's 1 row, there can't be any direct shading self.shaded_length_front = np.zeros(self.n_states) self.shaded_length_back = np.zeros(self.n_states) # Flag times when there's direct shading self.has_direct_shading = ( (self.shaded_length_front > DISTANCE_TOLERANCE) | (self.shaded_length_back > DISTANCE_TOLERANCE))