def aoi_projection(surf_tilt, surf_az, sun_zen, sun_az): """ Calculates the dot product of the solar vector and the surface normal. Input all angles in degrees. Parameters ========== surf_tilt : float or Series. Panel tilt from horizontal. surf_az : float or Series. Panel azimuth from north. sun_zen : float or Series. Solar zenith angle. sun_az : float or Series. Solar azimuth angle. Returns ======= float or Series. Dot product of panel normal and solar angle. """ projection = ( tools.cosd(surf_tilt) * tools.cosd(sun_zen) + tools.sind(surf_tilt) * tools.sind(sun_zen) * tools.cosd(sun_az - surf_az)) try: projection.name = 'aoi_projection' except AttributeError: pass return projection
def singleaxis(apparent_zenith, apparent_azimuth, axis_tilt=0, axis_azimuth=0, max_angle=90, backtrack=True, gcr=2.0/7.0): """ Determine the rotation angle of a single axis tracker using the equations in [1] when given a particular sun zenith and azimuth angle. backtracking may be specified, and if so, a ground coverage ratio is required. Rotation angle is determined in a panel-oriented coordinate system. The tracker azimuth axis_azimuth defines the positive y-axis; the positive x-axis is 90 degress clockwise from the y-axis and parallel to the earth surface, and the positive z-axis is normal and oriented towards the sun. Rotation angle tracker_theta indicates tracker position relative to horizontal: tracker_theta = 0 is horizontal, and positive tracker_theta is a clockwise rotation around the y axis in the x, y, z coordinate system. For example, if tracker azimuth axis_azimuth is 180 (oriented south), tracker_theta = 30 is a rotation of 30 degrees towards the west, and tracker_theta = -90 is a rotation to the vertical plane facing east. Parameters ---------- apparent_zenith : float, 1d array, or Series Solar apparent zenith angles in decimal degrees. apparent_azimuth : float, 1d array, or Series Solar apparent azimuth angles in decimal degrees. 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. Returns ------- dict or DataFrame with the following columns: * tracker_theta: The rotation angle of the tracker. tracker_theta = 0 is horizontal, and positive rotation angles are clockwise. * aoi: The angle-of-incidence of direct irradiance onto the rotated panel surface. * surface_tilt: The angle between the panel surface and the earth surface, accounting for panel rotation. * surface_azimuth: The azimuth of the rotated panel, determined by projecting the vector normal to the panel's surface to the earth's surface. References ---------- [1] Lorenzo, E et al., 2011, "Tracking and back-tracking", Prog. in Photovoltaics: Research and Applications, v. 19, pp. 747-753. """ # MATLAB to Python conversion by # Will Holmgren (@wholmgren), U. Arizona. March, 2015. if isinstance(apparent_zenith, pd.Series): index = apparent_zenith.index else: index = None # convert scalars to arrays apparent_azimuth = np.atleast_1d(apparent_azimuth) apparent_zenith = np.atleast_1d(apparent_zenith) if apparent_azimuth.ndim > 1 or apparent_zenith.ndim > 1: raise ValueError('Input dimensions must not exceed 1') # Calculate sun position x, y, z using coordinate system as in [1], Eq 2. # Positive y axis is oriented parallel to earth surface along tracking axis # (for the purpose of illustration, assume y is oriented to the south); # positive x axis is orthogonal, 90 deg clockwise from y-axis, and parallel # to the earth's surface (if y axis is south, x axis is west); # positive z axis is normal to x, y axes, pointed upward. # Equations in [1] assume solar azimuth is relative to reference vector # pointed south, with clockwise positive. # Here, the input solar azimuth is degrees East of North, # i.e., relative to a reference vector pointed # north with clockwise positive. # Rotate sun azimuth to coordinate system as in [1] # to calculate sun position. az = apparent_azimuth - 180 apparent_elevation = 90 - apparent_zenith x = cosd(apparent_elevation) * sind(az) y = cosd(apparent_elevation) * cosd(az) z = sind(apparent_elevation) # translate array azimuth from compass bearing to [1] coord system # wholmgren: strange to see axis_azimuth calculated differently from az, # (not that it matters, or at least it shouldn't...). axis_azimuth_south = axis_azimuth - 180 # translate input array tilt angle axis_tilt to [1] coordinate system. # In [1] coordinates, axis_tilt is a rotation about the x-axis. # For a system with array azimuth (y-axis) oriented south, # the x-axis is oriented west, and a positive axis_tilt is a # counterclockwise rotation, i.e, lifting the north edge of the panel. # Thus, in [1] coordinate system, in the northern hemisphere a positive # axis_tilt indicates a rotation toward the equator, # whereas in the southern hemisphere rotation toward the equator is # indicated by axis_tilt<0. Here, the input axis_tilt is # always positive and is a rotation toward the equator. # Calculate sun position (xp, yp, zp) in panel-oriented coordinate system: # positive y-axis is oriented along tracking axis at panel tilt; # positive x-axis is orthogonal, clockwise, parallel to earth surface; # positive z-axis is normal to x-y axes, pointed upward. # Calculate sun position (xp,yp,zp) in panel coordinates using [1] Eq 11 # note that equation for yp (y' in Eq. 11 of Lorenzo et al 2011) is # corrected, after conversation with paper's authors. xp = x*cosd(axis_azimuth_south) - y*sind(axis_azimuth_south) yp = (x*cosd(axis_tilt)*sind(axis_azimuth_south) + y*cosd(axis_tilt)*cosd(axis_azimuth_south) - z*sind(axis_tilt)) zp = (x*sind(axis_tilt)*sind(axis_azimuth_south) + y*sind(axis_tilt)*cosd(axis_azimuth_south) + z*cosd(axis_tilt)) # The ideal tracking angle wid is the rotation to place the sun position # vector (xp, yp, zp) in the (y, z) plane; i.e., normal to the panel and # containing the axis of rotation. wid = 0 indicates that the panel is # horizontal. Here, our convention is that a clockwise rotation is # positive, to view rotation angles in the same frame of reference as # azimuth. For example, for a system with tracking axis oriented south, # a rotation toward the east is negative, and a rotation to the west is # positive. # Use arctan2 and avoid the tmp corrections. # angle from x-y plane to projection of sun vector onto x-z plane # tmp = np.degrees(np.arctan(zp/xp)) # Obtain wid by translating tmp to convention for rotation angles. # Have to account for which quadrant of the x-z plane in which the sun # vector lies. Complete solution here but probably not necessary to # consider QIII and QIV. # wid = pd.Series(index=times) # wid[(xp>=0) & (zp>=0)] = 90 - tmp[(xp>=0) & (zp>=0)] # QI # wid[(xp<0) & (zp>=0)] = -90 - tmp[(xp<0) & (zp>=0)] # QII # wid[(xp<0) & (zp<0)] = -90 - tmp[(xp<0) & (zp<0)] # QIII # wid[(xp>=0) & (zp<0)] = 90 - tmp[(xp>=0) & (zp<0)] # QIV # Calculate angle from x-y plane to projection of sun vector onto x-z plane # and then obtain wid by translating tmp to convention for rotation angles. wid = 90 - np.degrees(np.arctan2(zp, xp)) # filter for sun above panel horizon zen_gt_90 = apparent_zenith > 90 wid[zen_gt_90] = np.nan # Account for backtracking; modified from [1] to account for rotation # angle convention being used here. if backtrack: axes_distance = 1/gcr temp = np.minimum(axes_distance*cosd(wid), 1) # backtrack angle # (always positive b/c acosd returns values between 0 and 180) wc = np.degrees(np.arccos(temp)) # Eq 4 applied when wid in QIV (wid < 0 evalulates True), QI tracker_theta = np.where(wid < 0, wid + wc, wid - wc) else: tracker_theta = wid tracker_theta[tracker_theta > max_angle] = max_angle tracker_theta[tracker_theta < -max_angle] = -max_angle # calculate panel normal vector in panel-oriented x, y, z coordinates. # y-axis is axis of tracker rotation. tracker_theta is a compass angle # (clockwise is positive) rather than a trigonometric angle. # the *0 is a trick to preserve NaN values. panel_norm = np.array([sind(tracker_theta), tracker_theta*0, cosd(tracker_theta)]) # sun position in vector format in panel-oriented x, y, z coordinates sun_vec = np.array([xp, yp, zp]) # calculate angle-of-incidence on panel aoi = np.degrees(np.arccos(np.abs(np.sum(sun_vec*panel_norm, axis=0)))) # calculate panel tilt and azimuth # in a coordinate system where the panel tilt is the # angle from horizontal, and the panel azimuth is # the compass angle (clockwise from north) to the projection # of the panel's normal to the earth's surface. # These outputs are provided for convenience and comparison # with other PV software which use these angle conventions. # project normal vector to earth surface. # First rotate about x-axis by angle -axis_tilt so that y-axis is # also parallel to earth surface, then project. # Calculate standard rotation matrix rot_x = np.array([[1, 0, 0], [0, cosd(-axis_tilt), -sind(-axis_tilt)], [0, sind(-axis_tilt), cosd(-axis_tilt)]]) # panel_norm_earth contains the normal vector # expressed in earth-surface coordinates # (z normal to surface, y aligned with tracker axis parallel to earth) panel_norm_earth = np.dot(rot_x, panel_norm).T # projection to plane tangent to earth surface, # in earth surface coordinates projected_normal = np.array([panel_norm_earth[:, 0], panel_norm_earth[:, 1], panel_norm_earth[:, 2]*0]).T # calculate vector magnitudes projected_normal_mag = np.sqrt(np.nansum(projected_normal**2, axis=1)) # renormalize the projected vector # avoid creating nan values. non_zeros = projected_normal_mag != 0 projected_normal[non_zeros] = (projected_normal[non_zeros].T / projected_normal_mag[non_zeros]).T # calculation of surface_azimuth # 1. Find the angle. # surface_azimuth = pd.Series( # np.degrees(np.arctan(projected_normal[:,1]/projected_normal[:,0])), # index=times) surface_azimuth = \ np.degrees(np.arctan2(projected_normal[:, 1], projected_normal[:, 0])) # 2. Clean up atan when x-coord or y-coord is zero # surface_azimuth[(projected_normal[:,0]==0) & (projected_normal[:,1]>0)] = 90 # surface_azimuth[(projected_normal[:,0]==0) & (projected_normal[:,1]<0)] = -90 # surface_azimuth[(projected_normal[:,1]==0) & (projected_normal[:,0]>0)] = 0 # surface_azimuth[(projected_normal[:,1]==0) & (projected_normal[:,0]<0)] = 180 # 3. Correct atan for QII and QIII # surface_azimuth[(projected_normal[:,0]<0) & (projected_normal[:,1]>0)] += 180 # QII # surface_azimuth[(projected_normal[:,0]<0) & (projected_normal[:,1]<0)] += 180 # QIII # 4. Skip to below # at this point surface_azimuth contains angles between -90 and +270, # where 0 is along the positive x-axis, # the y-axis is in the direction of the tracker azimuth, # and positive angles are rotations from the positive x axis towards # the positive y-axis. # Adjust to compass angles # (clockwise rotation from 0 along the positive y-axis) # surface_azimuth[surface_azimuth<=90] = 90 - surface_azimuth[surface_azimuth<=90] # surface_azimuth[surface_azimuth>90] = 450 - surface_azimuth[surface_azimuth>90] # finally rotate to align y-axis with true north # PVLIB_MATLAB has this latitude correction, # but I don't think it's latitude dependent if you always # specify axis_azimuth with respect to North. # if latitude > 0 or True: # surface_azimuth = surface_azimuth - axis_azimuth # else: # surface_azimuth = surface_azimuth - axis_azimuth - 180 # surface_azimuth[surface_azimuth<0] = 360 + surface_azimuth[surface_azimuth<0] # the commented code above is mostly part of PVLIB_MATLAB. # My (wholmgren) take is that it can be done more simply. # Say that we're pointing along the postive x axis (likely west). # We just need to rotate 90 degrees to get from the x axis # to the y axis (likely south), # and then add the axis_azimuth to get back to North. # Anything left over is the azimuth that we want, # and we can map it into the [0,360) domain. # 4. Rotate 0 reference from panel's x axis to it's y axis and # then back to North. surface_azimuth = 90 - surface_azimuth + axis_azimuth # 5. Map azimuth into [0,360) domain. surface_azimuth[surface_azimuth < 0] += 360 surface_azimuth[surface_azimuth >= 360] -= 360 # Calculate surface_tilt dotproduct = (panel_norm_earth * projected_normal).sum(axis=1) surface_tilt = 90 - np.degrees(np.arccos(dotproduct)) # Bundle DataFrame for return values and filter for sun below horizon. out = {'tracker_theta': tracker_theta, 'aoi': aoi, 'surface_azimuth': surface_azimuth, 'surface_tilt': surface_tilt} if index is not None: out = pd.DataFrame(out, index=index) out = out[['tracker_theta', 'aoi', 'surface_azimuth', 'surface_tilt']] out[zen_gt_90] = np.nan else: out = {k: np.where(zen_gt_90, np.nan, v) for k, v in out.items()} return out
def singleaxis(apparent_zenith, apparent_azimuth, axis_tilt=0, axis_azimuth=0, max_angle=90, backtrack=True, gcr=2.0 / 7.0, cross_axis_tilt=0): """ Determine the rotation angle of a single-axis tracker when given particular solar zenith and azimuth angles. See [1]_ for details about the equations. Backtracking may be specified, and if so, a ground coverage ratio is required. Rotation angle is determined in a right-handed coordinate system. The tracker `axis_azimuth` defines the positive y-axis, the positive x-axis is 90 degrees clockwise from the y-axis and parallel to the Earth's surface, and the positive z-axis is normal to both x & y-axes and oriented skyward. Rotation angle `tracker_theta` is a right-handed rotation around the y-axis in the x, y, z coordinate system and indicates tracker position relative to horizontal. For example, if tracker `axis_azimuth` is 180 (oriented south) and `axis_tilt` is zero, then a `tracker_theta` of zero is horizontal, a `tracker_theta` of 30 degrees is a rotation of 30 degrees towards the west, and a `tracker_theta` of -90 degrees is a rotation to the vertical plane facing east. Parameters ---------- apparent_zenith : float, 1d array, or Series Solar apparent zenith angles in decimal degrees. apparent_azimuth : float, 1d array, or Series Solar apparent azimuth angles in decimal degrees. 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] Returns ------- dict or DataFrame with the following columns: * `tracker_theta`: The rotation angle of the tracker. tracker_theta = 0 is horizontal, and positive rotation angles are clockwise. [degrees] * `aoi`: The angle-of-incidence of direct irradiance onto the rotated panel surface. [degrees] * `surface_tilt`: The angle between the panel surface and the earth surface, accounting for panel rotation. [degrees] * `surface_azimuth`: The azimuth of the rotated panel, determined by projecting the vector normal to the panel's surface to the earth's surface. [degrees] See also -------- pvlib.tracking.calc_axis_tilt pvlib.tracking.calc_cross_axis_tilt References ---------- .. [1] Kevin Anderson and Mark Mikofski, "Slope-Aware Backtracking for Single-Axis Trackers", Technical Report NREL/TP-5K00-76626, July 2020. https://www.nrel.gov/docs/fy20osti/76626.pdf """ # MATLAB to Python conversion by # Will Holmgren (@wholmgren), U. Arizona. March, 2015. if isinstance(apparent_zenith, pd.Series): index = apparent_zenith.index else: index = None # convert scalars to arrays apparent_azimuth = np.atleast_1d(apparent_azimuth) apparent_zenith = np.atleast_1d(apparent_zenith) if apparent_azimuth.ndim > 1 or apparent_zenith.ndim > 1: raise ValueError('Input dimensions must not exceed 1') # Calculate sun position x, y, z using coordinate system as in [1], Eq 1. # NOTE: solar elevation = 90 - solar zenith, then use trig identities: # sin(90-x) = cos(x) & cos(90-x) = sin(x) sin_zenith = sind(apparent_zenith) x = sin_zenith * sind(apparent_azimuth) y = sin_zenith * cosd(apparent_azimuth) z = cosd(apparent_zenith) # Assume the tracker reference frame is right-handed. Positive y-axis is # oriented along tracking axis; from north, the y-axis is rotated clockwise # by the axis azimuth and tilted from horizontal by the axis tilt. The # positive x-axis is 90 deg clockwise from the y-axis and parallel to # horizontal (e.g., if the y-axis is south, the x-axis is west); the # positive z-axis is normal to the x and y axes, pointed upward. # Calculate sun position (xp, yp, zp) in tracker coordinate system using # [1] Eq 4. cos_axis_azimuth = cosd(axis_azimuth) sin_axis_azimuth = sind(axis_azimuth) cos_axis_tilt = cosd(axis_tilt) sin_axis_tilt = sind(axis_tilt) xp = x * cos_axis_azimuth - y * sin_axis_azimuth yp = (x * cos_axis_tilt * sin_axis_azimuth + y * cos_axis_tilt * cos_axis_azimuth - z * sin_axis_tilt) zp = (x * sin_axis_tilt * sin_axis_azimuth + y * sin_axis_tilt * cos_axis_azimuth + z * cos_axis_tilt) # The ideal tracking angle wid is the rotation to place the sun position # vector (xp, yp, zp) in the (y, z) plane, which is normal to the panel and # contains the axis of rotation. wid = 0 indicates that the panel is # horizontal. Here, our convention is that a clockwise rotation is # positive, to view rotation angles in the same frame of reference as # azimuth. For example, for a system with tracking axis oriented south, a # rotation toward the east is negative, and a rotation to the west is # positive. This is a right-handed rotation around the tracker y-axis. # Calculate angle from x-y plane to projection of sun vector onto x-z plane # using [1] Eq. 5. wid = np.degrees(np.arctan2(xp, zp)) # filter for sun above panel horizon zen_gt_90 = apparent_zenith > 90 wid[zen_gt_90] = np.nan # Account for backtracking if backtrack: # distance between rows in terms of rack lengths relative to cross-axis # tilt axes_distance = 1 / (gcr * cosd(cross_axis_tilt)) # NOTE: account for rare angles below array, see GH 824 temp = np.abs(axes_distance * cosd(wid - cross_axis_tilt)) # backtrack angle using [1], Eq. 14 with np.errstate(invalid='ignore'): wc = np.degrees(-np.sign(wid) * np.arccos(temp)) # NOTE: in the middle of the day, arccos(temp) is out of range because # there's no row-to-row shade to avoid, & backtracking is unnecessary # [1], Eqs. 15-16 with np.errstate(invalid='ignore'): tracker_theta = wid + np.where(temp < 1, wc, 0) else: tracker_theta = wid # NOTE: max_angle defined relative to zero-point rotation, not the # system-plane normal tracker_theta = np.clip(tracker_theta, -max_angle, max_angle) # Calculate panel normal vector in panel-oriented x, y, z coordinates. # y-axis is axis of tracker rotation. tracker_theta is a compass angle # (clockwise is positive) rather than a trigonometric angle. # NOTE: the *0 is a trick to preserve NaN values. panel_norm = np.array( [sind(tracker_theta), tracker_theta * 0, cosd(tracker_theta)]) # sun position in vector format in panel-oriented x, y, z coordinates sun_vec = np.array([xp, yp, zp]) # calculate angle-of-incidence on panel # TODO: use irradiance.aoi projection = np.clip(np.sum(sun_vec * panel_norm, axis=0), -1, 1) aoi = np.degrees(np.arccos(projection)) # Calculate panel tilt and azimuth in a coordinate system where the panel # tilt is the angle from horizontal, and the panel azimuth is the compass # angle (clockwise from north) to the projection of the panel's normal to # the earth's surface. These outputs are provided for convenience and # comparison with other PV software which use these angle conventions. # Project normal vector to earth surface. First rotate about x-axis by # angle -axis_tilt so that y-axis is also parallel to earth surface, then # project. # Calculate standard rotation matrix rot_x = np.array([[1, 0, 0], [0, cosd(-axis_tilt), -sind(-axis_tilt)], [0, sind(-axis_tilt), cosd(-axis_tilt)]]) # panel_norm_earth contains the normal vector expressed in earth-surface # coordinates (z normal to surface, y aligned with tracker axis parallel to # earth) panel_norm_earth = np.dot(rot_x, panel_norm).T # projection to plane tangent to earth surface, in earth surface # coordinates projected_normal = np.array([ panel_norm_earth[:, 0], panel_norm_earth[:, 1], panel_norm_earth[:, 2] * 0 ]).T # calculate vector magnitudes projected_normal_mag = np.sqrt(np.nansum(projected_normal**2, axis=1)) # renormalize the projected vector, avoid creating nan values. non_zeros = projected_normal_mag != 0 projected_normal[non_zeros] = (projected_normal[non_zeros].T / projected_normal_mag[non_zeros]).T # calculation of surface_azimuth surface_azimuth = \ np.degrees(np.arctan2(projected_normal[:, 1], projected_normal[:, 0])) # Rotate 0 reference from panel's x-axis to its y-axis and then back to # north. surface_azimuth = 90 - surface_azimuth + axis_azimuth # Map azimuth into [0,360) domain. with np.errstate(invalid='ignore'): surface_azimuth = surface_azimuth % 360 # Calculate surface_tilt dotproduct = (panel_norm_earth * projected_normal).sum(axis=1) surface_tilt = 90 - np.degrees(np.arccos(dotproduct)) # Bundle DataFrame for return values and filter for sun below horizon. out = { 'tracker_theta': tracker_theta, 'aoi': aoi, 'surface_tilt': surface_tilt, 'surface_azimuth': surface_azimuth } if index is not None: out = pd.DataFrame(out, index=index) out = out[['tracker_theta', 'aoi', 'surface_azimuth', 'surface_tilt']] out[zen_gt_90] = np.nan else: out = {k: np.where(zen_gt_90, np.nan, v) for k, v in out.items()} return out
def from_raw_inputs(cls, xy_center, width, rotation_vec, cut, shaded_length, n_vector=None, param_names=None): """Create timeseries side using raw PV row inputs. Note: shading will always be zero when PV rows are flat. Parameters ---------- xy_center : tuple of float x and y coordinates of the PV row center point (invariant) width : float width of the PV rows [m] rotation_vec : np.ndarray Timeseries rotation values of the PV row [deg] cut : int Discretization scheme of the PV side. Will create segments of equal length. shaded_length : np.ndarray Timeseries values of side shaded length from lowest point [m] n_vector : np.ndarray, optional Timeseries normal vectors of the side param_names : list of str, optional List of names of surface parameters to use when creating geometries (Default = None) Returns ------- New timeseries side object """ mask_tilted_to_left = rotation_vec >= 0 # Create Ts segments x_center, y_center = xy_center radius = width / 2. segment_length = width / cut is_not_flat = rotation_vec != 0. # Calculate coords of shading point r_shade = radius - shaded_length x_sh = np.where( mask_tilted_to_left, r_shade * cosd(rotation_vec + 180.) + x_center, r_shade * cosd(rotation_vec) + x_center) y_sh = np.where( mask_tilted_to_left, r_shade * sind(rotation_vec + 180.) + y_center, r_shade * sind(rotation_vec) + y_center) # Calculate coords list_segments = [] for i in range(cut): # Calculate segment coords r1 = radius - i * segment_length r2 = radius - (i + 1) * segment_length x1 = r1 * cosd(rotation_vec + 180.) + x_center y1 = r1 * sind(rotation_vec + 180.) + y_center x2 = r2 * cosd(rotation_vec + 180) + x_center y2 = r2 * sind(rotation_vec + 180) + y_center segment_coords = TsLineCoords.from_array( np.array([[x1, y1], [x2, y2]])) # Determine lowest and highest points of segment x_highest = np.where(mask_tilted_to_left, x2, x1) y_highest = np.where(mask_tilted_to_left, y2, y1) x_lowest = np.where(mask_tilted_to_left, x1, x2) y_lowest = np.where(mask_tilted_to_left, y1, y2) # Calculate illum and shaded coords x2_illum, y2_illum = x_highest, y_highest x1_shaded, y1_shaded, x2_shaded, y2_shaded = \ x_lowest, y_lowest, x_lowest, y_lowest mask_all_shaded = (y_sh > y_highest) & (is_not_flat) mask_partial_shaded = (y_sh > y_lowest) & (~ mask_all_shaded) \ & (is_not_flat) # Calculate second boundary point of shade x2_shaded = np.where(mask_all_shaded, x_highest, x2_shaded) x2_shaded = np.where(mask_partial_shaded, x_sh, x2_shaded) y2_shaded = np.where(mask_all_shaded, y_highest, y2_shaded) y2_shaded = np.where(mask_partial_shaded, y_sh, y2_shaded) x1_illum = x2_shaded y1_illum = y2_shaded illum_coords = TsLineCoords.from_array( np.array([[x1_illum, y1_illum], [x2_illum, y2_illum]])) shaded_coords = TsLineCoords.from_array( np.array([[x1_shaded, y1_shaded], [x2_shaded, y2_shaded]])) # Create illuminated and shaded surfaces illum = TsSurface(illum_coords, n_vector=n_vector, param_names=param_names) shaded = TsSurface(shaded_coords, n_vector=n_vector, param_names=param_names) # Create dual segment segment = TsDualSegment(segment_coords, illum, shaded, n_vector=n_vector) list_segments.append(segment) return cls(list_segments, n_vector=n_vector)
def physical(aoi, n=1.526, K=4., L=0.002): r""" Determine the incidence angle modifier using refractive index ``n``, extinction coefficient ``K``, and glazing thickness ``L``. ``iam.physical`` calculates the incidence angle modifier as described in [1]_, Section 3. The calculation is based on a physical model of absorbtion and transmission through a transparent cover. Parameters ---------- aoi : numeric The angle of incidence between the module normal vector and the sun-beam vector in degrees. Angles of 0 are replaced with 1e-06 to ensure non-nan results. Angles of nan will result in nan. n : numeric, default 1.526 The effective index of refraction (unitless). Reference [1]_ indicates that a value of 1.526 is acceptable for glass. K : numeric, default 4.0 The glazing extinction coefficient in units of 1/meters. Reference [1] indicates that a value of 4 is reasonable for "water white" glass. L : numeric, default 0.002 The glazing thickness in units of meters. Reference [1]_ indicates that 0.002 meters (2 mm) is reasonable for most glass-covered PV panels. Returns ------- iam : numeric The incident angle modifier Notes ----- The pvlib python authors believe that Eqn. 14 in [1]_ is incorrect, which presents :math:`\theta_{r} = \arcsin(n \sin(AOI))`. Here, :math:`\theta_{r} = \arcsin(1/n \times \sin(AOI))` References ---------- .. [1] W. De Soto et al., "Improvement and validation of a model for photovoltaic array performance", Solar Energy, vol 80, pp. 78-88, 2006. .. [2] Duffie, John A. & Beckman, William A.. (2006). Solar Engineering of Thermal Processes, third edition. [Books24x7 version] Available from http://common.books24x7.com/toc.aspx?bookid=17160. See Also -------- pvlib.iam.martin_ruiz pvlib.iam.ashrae pvlib.iam.interp pvlib.iam.sapm """ zeroang = 1e-06 # hold a new reference to the input aoi object since we're going to # overwrite the aoi reference below, but we'll need it for the # series check at the end of the function aoi_input = aoi aoi = np.where(aoi == 0, zeroang, aoi) # angle of reflection thetar_deg = asind(1.0 / n * (sind(aoi))) # reflectance and transmittance for normal incidence light rho_zero = ((1 - n) / (1 + n))**2 tau_zero = np.exp(-K * L) # reflectance for parallel and perpendicular polarized light rho_para = (tand(thetar_deg - aoi) / tand(thetar_deg + aoi))**2 rho_perp = (sind(thetar_deg - aoi) / sind(thetar_deg + aoi))**2 # transmittance for non-normal light tau = np.exp(-K * L / cosd(thetar_deg)) # iam is ratio of non-normal to normal incidence transmitted light # after deducting the reflected portion of each iam = ((1 - (rho_para + rho_perp) / 2) / (1 - rho_zero) * tau / tau_zero) with np.errstate(invalid='ignore'): # angles near zero produce nan, but iam is defined as one small_angle = 1e-06 iam = np.where(np.abs(aoi) < small_angle, 1.0, iam) # angles at 90 degrees can produce tiny negative values, # which should be zero. this is a result of calculation precision # rather than the physical model iam = np.where(iam < 0, 0, iam) # for light coming from behind the plane, none can enter the module iam = np.where(aoi > 90, 0, iam) if isinstance(aoi_input, pd.Series): iam = pd.Series(iam, index=aoi_input.index) return iam
def perez(surf_tilt, surf_az, DHI, DNI, DNI_ET, sun_zen, sun_az, AM, modelt='allsitescomposite1990'): ''' Determine diffuse irradiance from the sky on a tilted surface using one of the Perez models. Perez models determine the diffuse irradiance from the sky (ground reflected irradiance is not included in this algorithm) on a tilted surface using the surface tilt angle, surface azimuth angle, diffuse horizontal irradiance, direct normal irradiance, extraterrestrial irradiance, sun zenith angle, sun azimuth angle, and relative (not pressure-corrected) airmass. Optionally a selector may be used to use any of Perez's model coefficient sets. Parameters ---------- surf_tilt : float or Series Surface tilt angles in decimal degrees. surf_tilt must be >=0 and <=180. The tilt angle is defined as degrees from horizontal (e.g. surface facing up = 0, surface facing horizon = 90) surf_az : float or Series Surface azimuth angles in decimal degrees. surf_az must be >=0 and <=360. The Azimuth convention is defined as degrees east of north (e.g. North = 0, South=180 East = 90, West = 270). DHI : float or Series diffuse horizontal irradiance in W/m^2. DHI must be >=0. DNI : float or Series direct normal irradiance in W/m^2. DNI must be >=0. DNI_ET : float or Series extraterrestrial normal irradiance in W/m^2. DNI_ET must be >=0. sun_zen : float or Series apparent (refraction-corrected) zenith angles in decimal degrees. sun_zen must be >=0 and <=180. sun_az : float or Series Sun azimuth angles in decimal degrees. sun_az must be >=0 and <=360. The Azimuth convention is defined as degrees east of north (e.g. North = 0, East = 90, West = 270). AM : float or Series relative (not pressure-corrected) airmass values. If AM is a DataFrame it must be of the same size as all other DataFrame inputs. AM must be >=0 (careful using the 1/sec(z) model of AM generation) Other Parameters ---------------- model : string (optional, default='allsitescomposite1990') a character string which selects the desired set of Perez coefficients. If model is not provided as an input, the default, '1990' will be used. All possible model selections are: * '1990' * 'allsitescomposite1990' (same as '1990') * 'allsitescomposite1988' * 'sandiacomposite1988' * 'usacomposite1988' * 'france1988' * 'phoenix1988' * 'elmonte1988' * 'osage1988' * 'albuquerque1988' * 'capecanaveral1988' * 'albany1988' Returns -------- float or Series the diffuse component of the solar radiation on an arbitrarily tilted surface defined by the Perez model as given in reference [3]. SkyDiffuse is the diffuse component ONLY and does not include the ground reflected irradiance or the irradiance due to the beam. References ---------- [1] Loutzenhiser P.G. et. al. "Empirical validation of models to compute solar irradiance on inclined surfaces for building energy simulation" 2007, Solar Energy vol. 81. pp. 254-267 [2] Perez, R., Seals, R., Ineichen, P., Stewart, R., Menicucci, D., 1987. A new simplified version of the Perez diffuse irradiance model for tilted surfaces. Solar Energy 39(3), 221-232. [3] Perez, R., Ineichen, P., Seals, R., Michalsky, J., Stewart, R., 1990. Modeling daylight availability and irradiance components from direct and global irradiance. Solar Energy 44 (5), 271-289. [4] Perez, R. et. al 1988. "The Development and Verification of the Perez Diffuse Radiation Model". SAND88-7030 ''' pvl_logger.debug('diffuse_sky.perez()') kappa = 1.041 # for sun_zen in radians z = np.radians(sun_zen) # convert to radians # epsilon is the sky's "clearness" eps = ((DHI + DNI) / DHI + kappa * (z ** 3)) / (1 + kappa * (z ** 3)) # Perez et al define clearness bins according to the following rules. # 1 = overcast ... 8 = clear # (these names really only make sense for small zenith angles, but...) # these values will eventually be used as indicies for coeffecient look ups ebin = eps.copy() ebin[(eps < 1.065)] = 1 ebin[(eps >= 1.065) & (eps < 1.23)] = 2 ebin[(eps >= 1.23) & (eps < 1.5)] = 3 ebin[(eps >= 1.5) & (eps < 1.95)] = 4 ebin[(eps >= 1.95) & (eps < 2.8)] = 5 ebin[(eps >= 2.8) & (eps < 4.5)] = 6 ebin[(eps >= 4.5) & (eps < 6.2)] = 7 ebin[eps >= 6.2] = 8 ebin = ebin - 1 # correct for 0 indexing in coeffecient lookup # remove night time values ebin = ebin.dropna().astype(int) # This is added because in cases where the sun is below the horizon # (var.sun_zen > 90) but there is still diffuse horizontal light # (var.DHI>0), it is possible that the airmass (var.AM) could be NaN, which # messes up later calculations. Instead, if the sun is down, and there is # still var.DHI, we set the airmass to the airmass value on the horizon # (approximately 37-38). # var.AM(var.sun_zen >=90 & var.DHI >0) = 37; # var.DNI_ET[var.DNI_ET==0] = .00000001 #very hacky, fix this # delta is the sky's "brightness" delta = DHI * AM / DNI_ET # keep only valid times delta = delta[ebin.index] z = z[ebin.index] # The various possible sets of Perez coefficients are contained # in a subfunction to clean up the code. F1c, F2c = _get_perez_coefficients(modelt) F1 = F1c[ebin, 0] + F1c[ebin, 1] * delta + F1c[ebin, 2] * z F1[F1 < 0] = 0 F1 = F1.astype(float) F2 = F2c[ebin, 0] + F2c[ebin, 1] * delta + F2c[ebin, 2] * z F2[F2 < 0] = 0 F2 = F2.astype(float) A = aoi_projection(surf_tilt, surf_az, sun_zen, sun_az) A[A < 0] = 0 B = tools.cosd(sun_zen) B[B < tools.cosd(85)] = tools.cosd(85) # Calculate Diffuse POA from sky dome term1 = 0.5 * (1 - F1) * (1 + tools.cosd(surf_tilt)) term2 = F1 * A[ebin.index] / B[ebin.index] term3 = F2 * tools.sind(surf_tilt) sky_diffuse = DHI[ebin.index] * (term1 + term2 + term3) sky_diffuse[sky_diffuse < 0] = 0 return sky_diffuse
def reindl(surf_tilt, surf_az, DHI, DNI, GHI, DNI_ET, sun_zen, sun_az): r''' Determine diffuse irradiance from the sky on a tilted surface using Reindl's 1990 model .. math:: I_{d} = DHI (A R_b + (1 - A) (\frac{1 + \cos\beta}{2}) (1 + \sqrt{\frac{I_{hb}}{I_h}} \sin^3(\beta/2)) ) Reindl's 1990 model determines the diffuse irradiance from the sky (ground reflected irradiance is not included in this algorithm) on a tilted surface using the surface tilt angle, surface azimuth angle, diffuse horizontal irradiance, direct normal irradiance, global horizontal irradiance, extraterrestrial irradiance, sun zenith angle, and sun azimuth angle. Parameters ---------- surf_tilt : float or Series. 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) surf_az : float or Series. Surface azimuth angles in decimal degrees. The Azimuth convention is defined as degrees east of north (e.g. North = 0, South=180 East = 90, West = 270). DHI : float or Series. diffuse horizontal irradiance in W/m^2. DNI : float or Series. direct normal irradiance in W/m^2. GHI: float or Series. Global irradiance in W/m^2. DNI_ET : float or Series. extraterrestrial normal irradiance in W/m^2. sun_zen : float or Series. apparent (refraction-corrected) zenith angles in decimal degrees. sun_az : float or Series. Sun azimuth angles in decimal degrees. The Azimuth convention is defined as degrees east of north (e.g. North = 0, East = 90, West = 270). Returns ------- SkyDiffuse : float or Series. The diffuse component of the solar radiation on an arbitrarily tilted surface defined by the Reindl model as given in Loutzenhiser et. al (2007) equation 8. SkyDiffuse is the diffuse component ONLY and does not include the ground reflected irradiance or the irradiance due to the beam. SkyDiffuse is a column vector vector with a number of elements equal to the input vector(s). Notes ----- The POAskydiffuse calculation is generated from the Loutzenhiser et al. (2007) paper, equation 8. Note that I have removed the beam and ground reflectance portion of the equation and this generates ONLY the diffuse radiation from the sky and circumsolar, so the form of the equation varies slightly from equation 8. References ---------- [1] Loutzenhiser P.G. et. al. "Empirical validation of models to compute solar irradiance on inclined surfaces for building energy simulation" 2007, Solar Energy vol. 81. pp. 254-267 [2] Reindl, D.T., Beckmann, W.A., Duffie, J.A., 1990a. Diffuse fraction correlations. Solar Energy 45(1), 1-7. [3] Reindl, D.T., Beckmann, W.A., Duffie, J.A., 1990b. Evaluation of hourly tilted surface radiation models. Solar Energy 45(1), 9-17. ''' pvl_logger.debug('diffuse_sky.reindl()') cos_tt = aoi_projection(surf_tilt, surf_az, sun_zen, sun_az) cos_sun_zen = tools.cosd(sun_zen) # ratio of titled and horizontal beam irradiance Rb = cos_tt / cos_sun_zen # Anisotropy Index AI = DNI / DNI_ET # DNI projected onto horizontal HB = DNI * cos_sun_zen HB[HB < 0] = 0 # these are actually the () and [] sub-terms of the second term of eqn 8 term1 = 1 - AI term2 = 0.5 * (1 + tools.cosd(surf_tilt)) term3 = 1 + np.sqrt(HB / GHI) * (tools.sind(0.5 * surf_tilt) ** 3) sky_diffuse = DHI * (AI * Rb + term1 * term2 * term3) sky_diffuse[sky_diffuse < 0] = 0 return sky_diffuse
def physicaliam(K, L, n, aoi): ''' Determine the incidence angle modifier using refractive index, glazing thickness, and extinction coefficient physicaliam calculates the incidence angle modifier as described in De Soto et al. "Improvement and validation of a model for photovoltaic array performance", section 3. The calculation is based upon a physical model of absorbtion and transmission through a cover. Required information includes, incident angle, cover extinction coefficient, cover thickness Note: The authors of this function believe that eqn. 14 in [1] is incorrect. This function uses the following equation in its place: theta_r = arcsin(1/n * sin(theta)) Parameters ---------- K : float The glazing extinction coefficient in units of 1/meters. Reference [1] indicates that a value of 4 is reasonable for "water white" glass. K must be a numeric scalar or vector with all values >=0. If K is a vector, it must be the same size as all other input vectors. L : float The glazing thickness in units of meters. Reference [1] indicates that 0.002 meters (2 mm) is reasonable for most glass-covered PV panels. L must be a numeric scalar or vector with all values >=0. If L is a vector, it must be the same size as all other input vectors. n : float The effective index of refraction (unitless). Reference [1] indicates that a value of 1.526 is acceptable for glass. n must be a numeric scalar or vector with all values >=0. If n is a vector, it must be the same size as all other input vectors. aoi : Series The angle of incidence between the module normal vector and the sun-beam vector in degrees. Returns ------- IAM : float or Series The incident angle modifier as specified in eqns. 14-16 of [1]. IAM is a column vector with the same number of elements as the largest input vector. Theta must be a numeric scalar or vector. For any values of theta where abs(aoi)>90, IAM is set to 0. For any values of aoi where -90 < aoi < 0, theta is set to abs(aoi) and evaluated. References ---------- [1] W. De Soto et al., "Improvement and validation of a model for photovoltaic array performance", Solar Energy, vol 80, pp. 78-88, 2006. [2] Duffie, John A. & Beckman, William A.. (2006). Solar Engineering of Thermal Processes, third edition. [Books24x7 version] Available from http://common.books24x7.com/toc.aspx?bookid=17160. See Also -------- getaoi ephemeris spa ashraeiam ''' thetar_deg = tools.asind(1.0 / n*(tools.sind(aoi))) tau = ( np.exp(- 1.0 * (K*L / tools.cosd(thetar_deg))) * ((1 - 0.5*((((tools.sind(thetar_deg - aoi)) ** 2) / ((tools.sind(thetar_deg + aoi)) ** 2) + ((tools.tand(thetar_deg - aoi)) ** 2) / ((tools.tand(thetar_deg + aoi)) ** 2))))) ) zeroang = 1e-06 thetar_deg0 = tools.asind(1.0 / n*(tools.sind(zeroang))) tau0 = ( np.exp(- 1.0 * (K*L / tools.cosd(thetar_deg0))) * ((1 - 0.5*((((tools.sind(thetar_deg0 - zeroang)) ** 2) / ((tools.sind(thetar_deg0 + zeroang)) ** 2) + ((tools.tand(thetar_deg0 - zeroang)) ** 2) / ((tools.tand(thetar_deg0 + zeroang)) ** 2))))) ) IAM = tau / tau0 IAM[abs(aoi) >= 90] = np.nan IAM[IAM < 0] = np.nan return IAM
def perez(surf_tilt, surf_az, DHI, DNI, DNI_ET, sun_zen, sun_az, AM, modelt='allsitescomposite1990'): ''' Determine diffuse irradiance from the sky on a tilted surface using one of the Perez models. Perez models determine the diffuse irradiance from the sky (ground reflected irradiance is not included in this algorithm) on a tilted surface using the surface tilt angle, surface azimuth angle, diffuse horizontal irradiance, direct normal irradiance, extraterrestrial irradiance, sun zenith angle, sun azimuth angle, and relative (not pressure-corrected) airmass. Optionally a selector may be used to use any of Perez's model coefficient sets. Parameters ---------- surf_tilt : float or Series Surface tilt angles in decimal degrees. surf_tilt must be >=0 and <=180. The tilt angle is defined as degrees from horizontal (e.g. surface facing up = 0, surface facing horizon = 90) surf_az : float or Series Surface azimuth angles in decimal degrees. surf_az must be >=0 and <=360. The Azimuth convention is defined as degrees east of north (e.g. North = 0, South=180 East = 90, West = 270). DHI : float or Series diffuse horizontal irradiance in W/m^2. DHI must be >=0. DNI : float or Series direct normal irradiance in W/m^2. DNI must be >=0. DNI_ET : float or Series extraterrestrial normal irradiance in W/m^2. DNI_ET must be >=0. sun_zen : float or Series apparent (refraction-corrected) zenith angles in decimal degrees. sun_zen must be >=0 and <=180. sun_az : float or Series Sun azimuth angles in decimal degrees. sun_az must be >=0 and <=360. The Azimuth convention is defined as degrees east of north (e.g. North = 0, East = 90, West = 270). AM : float or Series relative (not pressure-corrected) airmass values. If AM is a DataFrame it must be of the same size as all other DataFrame inputs. AM must be >=0 (careful using the 1/sec(z) model of AM generation) Other Parameters ---------------- model : string (optional, default='allsitescomposite1990') a character string which selects the desired set of Perez coefficients. If model is not provided as an input, the default, '1990' will be used. All possible model selections are: * '1990' * 'allsitescomposite1990' (same as '1990') * 'allsitescomposite1988' * 'sandiacomposite1988' * 'usacomposite1988' * 'france1988' * 'phoenix1988' * 'elmonte1988' * 'osage1988' * 'albuquerque1988' * 'capecanaveral1988' * 'albany1988' Returns -------- float or Series the diffuse component of the solar radiation on an arbitrarily tilted surface defined by the Perez model as given in reference [3]. SkyDiffuse is the diffuse component ONLY and does not include the ground reflected irradiance or the irradiance due to the beam. References ---------- [1] Loutzenhiser P.G. et. al. "Empirical validation of models to compute solar irradiance on inclined surfaces for building energy simulation" 2007, Solar Energy vol. 81. pp. 254-267 [2] Perez, R., Seals, R., Ineichen, P., Stewart, R., Menicucci, D., 1987. A new simplified version of the Perez diffuse irradiance model for tilted surfaces. Solar Energy 39(3), 221-232. [3] Perez, R., Ineichen, P., Seals, R., Michalsky, J., Stewart, R., 1990. Modeling daylight availability and irradiance components from direct and global irradiance. Solar Energy 44 (5), 271-289. [4] Perez, R. et. al 1988. "The Development and Verification of the Perez Diffuse Radiation Model". SAND88-7030 ''' pvl_logger.debug('diffuse_sky.perez()') kappa = 1.041 # for sun_zen in radians z = np.radians(sun_zen) # convert to radians # epsilon is the sky's "clearness" eps = ((DHI + DNI) / DHI + kappa * (z**3)) / (1 + kappa * (z**3)) # Perez et al define clearness bins according to the following rules. # 1 = overcast ... 8 = clear # (these names really only make sense for small zenith angles, but...) # these values will eventually be used as indicies for coeffecient look ups ebin = eps.copy() ebin[(eps < 1.065)] = 1 ebin[(eps >= 1.065) & (eps < 1.23)] = 2 ebin[(eps >= 1.23) & (eps < 1.5)] = 3 ebin[(eps >= 1.5) & (eps < 1.95)] = 4 ebin[(eps >= 1.95) & (eps < 2.8)] = 5 ebin[(eps >= 2.8) & (eps < 4.5)] = 6 ebin[(eps >= 4.5) & (eps < 6.2)] = 7 ebin[eps >= 6.2] = 8 ebin = ebin - 1 # correct for 0 indexing in coeffecient lookup # remove night time values ebin = ebin.dropna().astype(int) # This is added because in cases where the sun is below the horizon # (var.sun_zen > 90) but there is still diffuse horizontal light # (var.DHI>0), it is possible that the airmass (var.AM) could be NaN, which # messes up later calculations. Instead, if the sun is down, and there is # still var.DHI, we set the airmass to the airmass value on the horizon # (approximately 37-38). # var.AM(var.sun_zen >=90 & var.DHI >0) = 37; # var.DNI_ET[var.DNI_ET==0] = .00000001 #very hacky, fix this # delta is the sky's "brightness" delta = DHI * AM / DNI_ET # keep only valid times delta = delta[ebin.index] z = z[ebin.index] # The various possible sets of Perez coefficients are contained # in a subfunction to clean up the code. F1c, F2c = _get_perez_coefficients(modelt) F1 = F1c[ebin, 0] + F1c[ebin, 1] * delta + F1c[ebin, 2] * z F1[F1 < 0] = 0 F1 = F1.astype(float) F2 = F2c[ebin, 0] + F2c[ebin, 1] * delta + F2c[ebin, 2] * z F2[F2 < 0] = 0 F2 = F2.astype(float) A = aoi_projection(surf_tilt, surf_az, sun_zen, sun_az) A[A < 0] = 0 B = tools.cosd(sun_zen) B[B < tools.cosd(85)] = tools.cosd(85) # Calculate Diffuse POA from sky dome term1 = 0.5 * (1 - F1) * (1 + tools.cosd(surf_tilt)) term2 = F1 * A[ebin.index] / B[ebin.index] term3 = F2 * tools.sind(surf_tilt) sky_diffuse = DHI[ebin.index] * (term1 + term2 + term3) sky_diffuse[sky_diffuse < 0] = 0 return sky_diffuse
def klucher(surf_tilt, surf_az, DHI, GHI, sun_zen, sun_az): r''' Determine diffuse irradiance from the sky on a tilted surface using Klucher's 1979 model .. math:: I_{d} = DHI \frac{1 + \cos\beta}{2} (1 + F' \sin^3(\beta/2)) (1 + F' \cos^2\theta\sin^3\theta_z) where .. math:: F' = 1 - (I_{d0} / GHI) Klucher's 1979 model determines the diffuse irradiance from the sky (ground reflected irradiance is not included in this algorithm) on a tilted surface using the surface tilt angle, surface azimuth angle, diffuse horizontal irradiance, direct normal irradiance, global horizontal irradiance, extraterrestrial irradiance, sun zenith angle, and sun azimuth angle. Parameters ---------- surf_tilt : float or Series Surface tilt angles in decimal degrees. surf_tilt must be >=0 and <=180. The tilt angle is defined as degrees from horizontal (e.g. surface facing up = 0, surface facing horizon = 90) surf_az : float or Series Surface azimuth angles in decimal degrees. surf_az must be >=0 and <=360. The Azimuth convention is defined as degrees east of north (e.g. North = 0, South=180 East = 90, West = 270). DHI : float or Series diffuse horizontal irradiance in W/m^2. DHI must be >=0. GHI : float or Series Global irradiance in W/m^2. DNI must be >=0. sun_zen : float or Series apparent (refraction-corrected) zenith angles in decimal degrees. sun_zen must be >=0 and <=180. sun_az : float or Series Sun azimuth angles in decimal degrees. sun_az must be >=0 and <=360. The Azimuth convention is defined as degrees east of north (e.g. North = 0, East = 90, West = 270). Returns ------- float or Series. The diffuse component of the solar radiation on an arbitrarily tilted surface defined by the Klucher model as given in Loutzenhiser et. al (2007) equation 4. SkyDiffuse is the diffuse component ONLY and does not include the ground reflected irradiance or the irradiance due to the beam. SkyDiffuse is a column vector vector with a number of elements equal to the input vector(s). References ---------- [1] Loutzenhiser P.G. et. al. "Empirical validation of models to compute solar irradiance on inclined surfaces for building energy simulation" 2007, Solar Energy vol. 81. pp. 254-267 [2] Klucher, T.M., 1979. Evaluation of models to predict insolation on tilted surfaces. Solar Energy 23 (2), 111-114. ''' pvl_logger.debug('diffuse_sky.klucher()') # zenith angle with respect to panel normal. cos_tt = aoi_projection(surf_tilt, surf_az, sun_zen, sun_az) F = 1 - ((DHI / GHI)**2) try: # fails with single point input F.fillna(0, inplace=True) except AttributeError: F = 0 term1 = 0.5 * (1 + tools.cosd(surf_tilt)) term2 = 1 + F * (tools.sind(0.5 * surf_tilt)**3) term3 = 1 + F * (cos_tt**2) * (tools.sind(sun_zen)**3) sky_diffuse = DHI * term1 * term2 * term3 return sky_diffuse
def reindl(surf_tilt, surf_az, DHI, DNI, GHI, DNI_ET, sun_zen, sun_az): r''' Determine diffuse irradiance from the sky on a tilted surface using Reindl's 1990 model .. math:: I_{d} = DHI (A R_b + (1 - A) (\frac{1 + \cos\beta}{2}) (1 + \sqrt{\frac{I_{hb}}{I_h}} \sin^3(\beta/2)) ) Reindl's 1990 model determines the diffuse irradiance from the sky (ground reflected irradiance is not included in this algorithm) on a tilted surface using the surface tilt angle, surface azimuth angle, diffuse horizontal irradiance, direct normal irradiance, global horizontal irradiance, extraterrestrial irradiance, sun zenith angle, and sun azimuth angle. Parameters ---------- surf_tilt : float or Series. 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) surf_az : float or Series. Surface azimuth angles in decimal degrees. The Azimuth convention is defined as degrees east of north (e.g. North = 0, South=180 East = 90, West = 270). DHI : float or Series. diffuse horizontal irradiance in W/m^2. DNI : float or Series. direct normal irradiance in W/m^2. GHI: float or Series. Global irradiance in W/m^2. DNI_ET : float or Series. extraterrestrial normal irradiance in W/m^2. sun_zen : float or Series. apparent (refraction-corrected) zenith angles in decimal degrees. sun_az : float or Series. Sun azimuth angles in decimal degrees. The Azimuth convention is defined as degrees east of north (e.g. North = 0, East = 90, West = 270). Returns ------- SkyDiffuse : float or Series. The diffuse component of the solar radiation on an arbitrarily tilted surface defined by the Reindl model as given in Loutzenhiser et. al (2007) equation 8. SkyDiffuse is the diffuse component ONLY and does not include the ground reflected irradiance or the irradiance due to the beam. SkyDiffuse is a column vector vector with a number of elements equal to the input vector(s). Notes ----- The POAskydiffuse calculation is generated from the Loutzenhiser et al. (2007) paper, equation 8. Note that I have removed the beam and ground reflectance portion of the equation and this generates ONLY the diffuse radiation from the sky and circumsolar, so the form of the equation varies slightly from equation 8. References ---------- [1] Loutzenhiser P.G. et. al. "Empirical validation of models to compute solar irradiance on inclined surfaces for building energy simulation" 2007, Solar Energy vol. 81. pp. 254-267 [2] Reindl, D.T., Beckmann, W.A., Duffie, J.A., 1990a. Diffuse fraction correlations. Solar Energy 45(1), 1-7. [3] Reindl, D.T., Beckmann, W.A., Duffie, J.A., 1990b. Evaluation of hourly tilted surface radiation models. Solar Energy 45(1), 9-17. ''' pvl_logger.debug('diffuse_sky.reindl()') cos_tt = aoi_projection(surf_tilt, surf_az, sun_zen, sun_az) cos_sun_zen = tools.cosd(sun_zen) # ratio of titled and horizontal beam irradiance Rb = cos_tt / cos_sun_zen # Anisotropy Index AI = DNI / DNI_ET # DNI projected onto horizontal HB = DNI * cos_sun_zen HB[HB < 0] = 0 # these are actually the () and [] sub-terms of the second term of eqn 8 term1 = 1 - AI term2 = 0.5 * (1 + tools.cosd(surf_tilt)) term3 = 1 + np.sqrt(HB / GHI) * (tools.sind(0.5 * surf_tilt)**3) sky_diffuse = DHI * (AI * Rb + term1 * term2 * term3) sky_diffuse[sky_diffuse < 0] = 0 return sky_diffuse
def coverage_nrel(snowfall, poa_irradiance, temp_air, surface_tilt, initial_coverage=0, threshold_snowfall=1., can_slide_coefficient=-80., slide_amount_coefficient=0.197): ''' Calculates the fraction of the slant height of a row of modules covered by snow at every time step. Implements the model described in [1]_ with minor improvements in [2]_, with the change that the output is in fraction of the row's slant height rather than in tenths of the row slant height. As described in [1]_, model validation focused on fixed tilt systems. Parameters ---------- snowfall : Series Accumulated snowfall within each time period. [cm] poa_irradiance : Series Total in-plane irradiance [W/m^2] temp_air : Series Ambient air temperature [C] surface_tilt : numeric Tilt of module's from horizontal, e.g. surface facing up = 0, surface facing horizon = 90. [degrees] initial_coverage : float, default 0 Fraction of row's slant height that is covered with snow at the beginning of the simulation. [unitless] threshold_snowfall : float, default 1.0 Hourly snowfall above which snow coverage is set to the row's slant height. [cm/hr] can_slide_coefficient : float, default -80. Coefficient to determine if snow can slide given irradiance and air temperature. [W/(m^2 C)] slide_amount_coefficient : float, default 0.197 Coefficient to determine fraction of snow that slides off in one hour. [unitless] Returns ------- snow_coverage : Series The fraction of the slant height of a row of modules that is covered by snow at each time step. Notes ----- In [1]_, `can_slide_coefficient` is termed `m`, and the value of `slide_amount_coefficient` is given in tenths of a module's slant height. References ---------- .. [1] Marion, B.; Schaefer, R.; Caine, H.; Sanchez, G. (2013). "Measured and modeled photovoltaic system energy losses from snow for Colorado and Wisconsin locations." Solar Energy 97; pp.112-121. .. [2] Ryberg, D; Freeman, J. (2017). "Integration, Validation, and Application of a PV Snow Coverage Model in SAM" NREL Technical Report NREL/TP-6A20-68705 ''' # find times with new snowfall new_snowfall = fully_covered_nrel(snowfall, threshold_snowfall) # set up output Series snow_coverage = pd.Series(np.nan, index=poa_irradiance.index) # determine amount that snow can slide in each timestep can_slide = temp_air > poa_irradiance / can_slide_coefficient slide_amt = slide_amount_coefficient * sind(surface_tilt) * \ _time_delta_in_hours(poa_irradiance.index) slide_amt[~can_slide] = 0. # don't slide during snow events slide_amt[new_snowfall] = 0. # don't slide in the interval preceding the snowfall data slide_amt.iloc[0] = 0 # build time series of cumulative slide amounts sliding_period_ID = new_snowfall.cumsum() cumulative_sliding = slide_amt.groupby(sliding_period_ID).cumsum() # set up time series of snow coverage without any sliding applied snow_coverage[new_snowfall] = 1.0 if np.isnan(snow_coverage.iloc[0]): snow_coverage.iloc[0] = initial_coverage snow_coverage.ffill(inplace=True) snow_coverage -= cumulative_sliding # clean up periods where row is completely uncovered return snow_coverage.clip(lower=0)
def perez_diffuse_luminance(timestamps, array_tilt, array_azimuth, solar_zenith, solar_azimuth, dni, dhi): """ Function used to calculate the luminance and the view factor terms from the Perez diffuse light transposition model, as implemented in the ``pvlib-python`` library. This function was custom made to allow the calculation of the circumsolar component on the back surface as well. Otherwise, the ``pvlib`` implementation would ignore it. :param array-like timestamps: simulation timestamps :param array-like array_tilt: pv module tilt angles :param array-like array_azimuth: pv array azimuth angles :param array-like solar_zenith: solar zenith angles :param array-like solar_azimuth: solar azimuth angles :param array-like dni: values for direct normal irradiance :param array-like dhi: values for diffuse horizontal irradiance :return: ``df_inputs``, dataframe with the following columns: ['solar_zenith', 'solar_azimuth', 'array_tilt', 'array_azimuth', 'dhi', 'dni', 'vf_horizon', 'vf_circumsolar', 'vf_isotropic', 'luminance_horizon', 'luminance_circumsolar', 'luminance_isotropic', 'poa_isotropic', 'poa_circumsolar', 'poa_horizon', 'poa_total_diffuse'] :rtype: class:`pandas.DataFrame` """ # Create a dataframe to help filtering on all arrays df_inputs = pd.DataFrame( { 'array_tilt': array_tilt, 'array_azimuth': array_azimuth, 'solar_zenith': solar_zenith, 'solar_azimuth': solar_azimuth, 'dni': dni, 'dhi': dhi }, index=pd.DatetimeIndex(timestamps)) dni_et = irradiance.extraradiation(df_inputs.index.dayofyear) am = atmosphere.relativeairmass(df_inputs.solar_zenith) # Need to treat the case when the sun is hitting the back surface of pvrow aoi_proj = aoi_projection(df_inputs.array_tilt, df_inputs.array_azimuth, df_inputs.solar_zenith, df_inputs.solar_azimuth) sun_hitting_back_surface = ((aoi_proj < 0) & (df_inputs.solar_zenith <= 90)) df_inputs_back_surface = df_inputs.loc[sun_hitting_back_surface] # Reverse the surface normal to switch to back-surface circumsolar calc df_inputs_back_surface.loc[:, 'array_azimuth'] -= 180. df_inputs_back_surface.loc[:, 'array_azimuth'] = np.mod( df_inputs_back_surface.loc[:, 'array_azimuth'], 360.) df_inputs_back_surface.loc[:, 'array_tilt'] = ( 180. - df_inputs_back_surface.array_tilt) if df_inputs_back_surface.shape[0] > 0: # Use recursion to calculate circumsolar luminance for back surface df_inputs_back_surface = perez_diffuse_luminance( *breakup_df_inputs(df_inputs_back_surface)) # Calculate Perez diffuse components diffuse_poa, components = irradiance.perez(df_inputs.array_tilt, df_inputs.array_azimuth, df_inputs.dhi, df_inputs.dni, dni_et, df_inputs.solar_zenith, df_inputs.solar_azimuth, am, return_components=True) # Calculate Perez view factors: a = aoi_projection(df_inputs.array_tilt, df_inputs.array_azimuth, df_inputs.solar_zenith, df_inputs.solar_azimuth) a = np.maximum(a, 0) b = cosd(df_inputs.solar_zenith) b = np.maximum(b, cosd(85)) vf_perez = pd.DataFrame( np.array([ sind(df_inputs.array_tilt), a / b, (1. + cosd(df_inputs.array_tilt)) / 2. ]).T, index=df_inputs.index, columns=['vf_horizon', 'vf_circumsolar', 'vf_isotropic']) # Calculate diffuse luminance luminance = pd.DataFrame(np.array([ components['horizon'] / vf_perez['vf_horizon'], components['circumsolar'] / vf_perez['vf_circumsolar'], components['isotropic'] / vf_perez['vf_isotropic'] ]).T, index=df_inputs.index, columns=[ 'luminance_horizon', 'luminance_circumsolar', 'luminance_isotropic' ]) luminance.loc[diffuse_poa == 0, :] = 0. # Format components column names components = components.rename( columns={ 'isotropic': 'poa_isotropic', 'circumsolar': 'poa_circumsolar', 'horizon': 'poa_horizon' }) df_inputs = pd.concat( [df_inputs, components, vf_perez, luminance, diffuse_poa], axis=1, join='outer') df_inputs = df_inputs.rename(columns={0: 'poa_total_diffuse'}) # Adjust the circumsolar luminance when it hits the back surface if df_inputs_back_surface.shape[0] > 0: df_inputs.loc[sun_hitting_back_surface, 'luminance_circumsolar'] = ( df_inputs_back_surface.loc[:, 'luminance_circumsolar']) return df_inputs
def perez_diffuse_luminance(timestamps, surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, dni, dhi): """Function used to calculate the luminance and the view factor terms from the Perez diffuse light transposition model, as implemented in the ``pvlib-python`` library. This function was custom made to allow the calculation of the circumsolar component on the back surface as well. Otherwise, the ``pvlib`` implementation would ignore it. Parameters ---------- timestamps : array-like simulation timestamps surface_tilt : array-like Surface tilt angles in decimal degrees. surface_tilt must be >=0 and <=180. The tilt angle is defined as degrees from horizontal (e.g. surface facing up = 0, surface facing horizon = 90) surface_azimuth : array-like The azimuth of the rotated panel, determined by projecting the vector normal to the panel's surface to the earth's surface [degrees]. solar_zenith : array-like solar zenith angles solar_azimuth : array-like solar azimuth angles dni : array-like values for direct normal irradiance dhi : array-like values for diffuse horizontal irradiance Returns ------- df_inputs : `pandas.DataFrame` Dataframe with the following columns: ['solar_zenith', 'solar_azimuth', 'surface_tilt', 'surface_azimuth', 'dhi', 'dni', 'vf_horizon', 'vf_circumsolar', 'vf_isotropic', 'luminance_horizon', 'luminance_circuqmsolar', 'luminance_isotropic', 'poa_isotropic', 'poa_circumsolar', 'poa_horizon', 'poa_total_diffuse'] """ # Create a dataframe to help filtering on all arrays df_inputs = pd.DataFrame( { 'surface_tilt': surface_tilt, 'surface_azimuth': surface_azimuth, 'solar_zenith': solar_zenith, 'solar_azimuth': solar_azimuth, 'dni': dni, 'dhi': dhi }, index=pd.DatetimeIndex(timestamps)) dni_et = irradiance.get_extra_radiation(df_inputs.index.dayofyear) am = atmosphere.get_relative_airmass(df_inputs.solar_zenith) # Need to treat the case when the sun is hitting the back surface of pvrow aoi_proj = irradiance.aoi_projection(df_inputs.surface_tilt, df_inputs.surface_azimuth, df_inputs.solar_zenith, df_inputs.solar_azimuth) sun_hitting_back_surface = ((aoi_proj < 0) & (df_inputs.solar_zenith <= 90)) df_inputs_back_surface = df_inputs.loc[sun_hitting_back_surface].copy() # Reverse the surface normal to switch to back-surface circumsolar calc df_inputs_back_surface.loc[:, 'surface_azimuth'] = ( df_inputs_back_surface.loc[:, 'surface_azimuth'] - 180.) df_inputs_back_surface.loc[:, 'surface_azimuth'] = np.mod( df_inputs_back_surface.loc[:, 'surface_azimuth'], 360.) df_inputs_back_surface.loc[:, 'surface_tilt'] = ( 180. - df_inputs_back_surface.surface_tilt) if df_inputs_back_surface.shape[0] > 0: # Use recursion to calculate circumsolar luminance for back surface df_inputs_back_surface = perez_diffuse_luminance( *breakup_df_inputs(df_inputs_back_surface)) # Calculate Perez diffuse components components = irradiance.perez(df_inputs.surface_tilt, df_inputs.surface_azimuth, df_inputs.dhi, df_inputs.dni, dni_et, df_inputs.solar_zenith, df_inputs.solar_azimuth, am, return_components=True) # Calculate Perez view factors: a = irradiance.aoi_projection(df_inputs.surface_tilt, df_inputs.surface_azimuth, df_inputs.solar_zenith, df_inputs.solar_azimuth) a = np.maximum(a, 0) b = cosd(df_inputs.solar_zenith) b = np.maximum(b, cosd(85)) vf_perez = pd.DataFrame( { 'vf_horizon': sind(df_inputs.surface_tilt), 'vf_circumsolar': a / b, 'vf_isotropic': (1. + cosd(df_inputs.surface_tilt)) / 2. }, index=df_inputs.index) # Calculate diffuse luminance luminance = pd.DataFrame(np.array([ components['horizon'] / vf_perez['vf_horizon'], components['circumsolar'] / vf_perez['vf_circumsolar'], components['isotropic'] / vf_perez['vf_isotropic'] ]).T, index=df_inputs.index, columns=[ 'luminance_horizon', 'luminance_circumsolar', 'luminance_isotropic' ]) luminance.loc[components['sky_diffuse'] == 0, :] = 0. # Format components column names components = components.rename( columns={ 'isotropic': 'poa_isotropic', 'circumsolar': 'poa_circumsolar', 'horizon': 'poa_horizon' }) df_inputs = pd.concat([df_inputs, components, vf_perez, luminance], axis=1, join='outer') df_inputs = df_inputs.rename(columns={'sky_diffuse': 'poa_total_diffuse'}) # Adjust the circumsolar luminance when it hits the back surface if df_inputs_back_surface.shape[0] > 0: df_inputs.loc[sun_hitting_back_surface, 'luminance_circumsolar'] = ( df_inputs_back_surface.loc[:, 'luminance_circumsolar']) return df_inputs
def singleaxis(apparent_zenith, apparent_azimuth, axis_tilt=0, axis_azimuth=0, max_angle=90, backtrack=True, gcr=2.0 / 7.0): """ Determine the rotation angle of a single axis tracker using the equations in [1] when given a particular sun zenith and azimuth angle. backtracking may be specified, and if so, a ground coverage ratio is required. Rotation angle is determined in a panel-oriented coordinate system. The tracker azimuth axis_azimuth defines the positive y-axis; the positive x-axis is 90 degress clockwise from the y-axis and parallel to the earth surface, and the positive z-axis is normal and oriented towards the sun. Rotation angle tracker_theta indicates tracker position relative to horizontal: tracker_theta = 0 is horizontal, and positive tracker_theta is a clockwise rotation around the y axis in the x, y, z coordinate system. For example, if tracker azimuth axis_azimuth is 180 (oriented south), tracker_theta = 30 is a rotation of 30 degrees towards the west, and tracker_theta = -90 is a rotation to the vertical plane facing east. Parameters ---------- apparent_zenith : float, 1d array, or Series Solar apparent zenith angles in decimal degrees. apparent_azimuth : float, 1d array, or Series Solar apparent azimuth angles in decimal degrees. 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. Returns ------- dict or DataFrame with the following columns: * tracker_theta: The rotation angle of the tracker. tracker_theta = 0 is horizontal, and positive rotation angles are clockwise. * aoi: The angle-of-incidence of direct irradiance onto the rotated panel surface. * surface_tilt: The angle between the panel surface and the earth surface, accounting for panel rotation. * surface_azimuth: The azimuth of the rotated panel, determined by projecting the vector normal to the panel's surface to the earth's surface. References ---------- [1] Lorenzo, E et al., 2011, "Tracking and back-tracking", Prog. in Photovoltaics: Research and Applications, v. 19, pp. 747-753. """ # MATLAB to Python conversion by # Will Holmgren (@wholmgren), U. Arizona. March, 2015. if isinstance(apparent_zenith, pd.Series): index = apparent_zenith.index else: index = None # convert scalars to arrays apparent_azimuth = np.atleast_1d(apparent_azimuth) apparent_zenith = np.atleast_1d(apparent_zenith) if apparent_azimuth.ndim > 1 or apparent_zenith.ndim > 1: raise ValueError('Input dimensions must not exceed 1') # Calculate sun position x, y, z using coordinate system as in [1], Eq 2. # Positive y axis is oriented parallel to earth surface along tracking axis # (for the purpose of illustration, assume y is oriented to the south); # positive x axis is orthogonal, 90 deg clockwise from y-axis, and parallel # to the earth's surface (if y axis is south, x axis is west); # positive z axis is normal to x, y axes, pointed upward. # Equations in [1] assume solar azimuth is relative to reference vector # pointed south, with clockwise positive. # Here, the input solar azimuth is degrees East of North, # i.e., relative to a reference vector pointed # north with clockwise positive. # Rotate sun azimuth to coordinate system as in [1] # to calculate sun position. az = apparent_azimuth - 180 apparent_elevation = 90 - apparent_zenith x = cosd(apparent_elevation) * sind(az) y = cosd(apparent_elevation) * cosd(az) z = sind(apparent_elevation) # translate array azimuth from compass bearing to [1] coord system # wholmgren: strange to see axis_azimuth calculated differently from az, # (not that it matters, or at least it shouldn't...). axis_azimuth_south = axis_azimuth - 180 # translate input array tilt angle axis_tilt to [1] coordinate system. # In [1] coordinates, axis_tilt is a rotation about the x-axis. # For a system with array azimuth (y-axis) oriented south, # the x-axis is oriented west, and a positive axis_tilt is a # counterclockwise rotation, i.e, lifting the north edge of the panel. # Thus, in [1] coordinate system, in the northern hemisphere a positive # axis_tilt indicates a rotation toward the equator, # whereas in the southern hemisphere rotation toward the equator is # indicated by axis_tilt<0. Here, the input axis_tilt is # always positive and is a rotation toward the equator. # Calculate sun position (xp, yp, zp) in panel-oriented coordinate system: # positive y-axis is oriented along tracking axis at panel tilt; # positive x-axis is orthogonal, clockwise, parallel to earth surface; # positive z-axis is normal to x-y axes, pointed upward. # Calculate sun position (xp,yp,zp) in panel coordinates using [1] Eq 11 # note that equation for yp (y' in Eq. 11 of Lorenzo et al 2011) is # corrected, after conversation with paper's authors. xp = x * cosd(axis_azimuth_south) - y * sind(axis_azimuth_south) yp = (x * cosd(axis_tilt) * sind(axis_azimuth_south) + y * cosd(axis_tilt) * cosd(axis_azimuth_south) - z * sind(axis_tilt)) zp = (x * sind(axis_tilt) * sind(axis_azimuth_south) + y * sind(axis_tilt) * cosd(axis_azimuth_south) + z * cosd(axis_tilt)) # The ideal tracking angle wid is the rotation to place the sun position # vector (xp, yp, zp) in the (y, z) plane; i.e., normal to the panel and # containing the axis of rotation. wid = 0 indicates that the panel is # horizontal. Here, our convention is that a clockwise rotation is # positive, to view rotation angles in the same frame of reference as # azimuth. For example, for a system with tracking axis oriented south, # a rotation toward the east is negative, and a rotation to the west is # positive. # Use arctan2 and avoid the tmp corrections. # angle from x-y plane to projection of sun vector onto x-z plane # tmp = np.degrees(np.arctan(zp/xp)) # Obtain wid by translating tmp to convention for rotation angles. # Have to account for which quadrant of the x-z plane in which the sun # vector lies. Complete solution here but probably not necessary to # consider QIII and QIV. # wid = pd.Series(index=times) # wid[(xp>=0) & (zp>=0)] = 90 - tmp[(xp>=0) & (zp>=0)] # QI # wid[(xp<0) & (zp>=0)] = -90 - tmp[(xp<0) & (zp>=0)] # QII # wid[(xp<0) & (zp<0)] = -90 - tmp[(xp<0) & (zp<0)] # QIII # wid[(xp>=0) & (zp<0)] = 90 - tmp[(xp>=0) & (zp<0)] # QIV # Calculate angle from x-y plane to projection of sun vector onto x-z plane # and then obtain wid by translating tmp to convention for rotation angles. wid = 90 - np.degrees(np.arctan2(zp, xp)) # filter for sun above panel horizon zen_gt_90 = apparent_zenith > 90 wid[zen_gt_90] = np.nan # Account for backtracking; modified from [1] to account for rotation # angle convention being used here. if backtrack: axes_distance = 1 / gcr # clip needed for low angles. GH 656 temp = np.clip(axes_distance * cosd(wid), -1, 1) # backtrack angle # (always positive b/c acosd returns values between 0 and 180) wc = np.degrees(np.arccos(temp)) # Eq 4 applied when wid in QIV (wid < 0 evalulates True), QI with np.errstate(invalid='ignore'): # errstate for GH 622 tracker_theta = np.where(wid < 0, wid + wc, wid - wc) else: tracker_theta = wid tracker_theta = np.minimum(tracker_theta, max_angle) tracker_theta = np.maximum(tracker_theta, -max_angle) # calculate panel normal vector in panel-oriented x, y, z coordinates. # y-axis is axis of tracker rotation. tracker_theta is a compass angle # (clockwise is positive) rather than a trigonometric angle. # the *0 is a trick to preserve NaN values. panel_norm = np.array( [sind(tracker_theta), tracker_theta * 0, cosd(tracker_theta)]) # sun position in vector format in panel-oriented x, y, z coordinates sun_vec = np.array([xp, yp, zp]) # calculate angle-of-incidence on panel aoi = np.degrees(np.arccos(np.abs(np.sum(sun_vec * panel_norm, axis=0)))) # calculate panel tilt and azimuth # in a coordinate system where the panel tilt is the # angle from horizontal, and the panel azimuth is # the compass angle (clockwise from north) to the projection # of the panel's normal to the earth's surface. # These outputs are provided for convenience and comparison # with other PV software which use these angle conventions. # project normal vector to earth surface. # First rotate about x-axis by angle -axis_tilt so that y-axis is # also parallel to earth surface, then project. # Calculate standard rotation matrix rot_x = np.array([[1, 0, 0], [0, cosd(-axis_tilt), -sind(-axis_tilt)], [0, sind(-axis_tilt), cosd(-axis_tilt)]]) # panel_norm_earth contains the normal vector # expressed in earth-surface coordinates # (z normal to surface, y aligned with tracker axis parallel to earth) panel_norm_earth = np.dot(rot_x, panel_norm).T # projection to plane tangent to earth surface, # in earth surface coordinates projected_normal = np.array([ panel_norm_earth[:, 0], panel_norm_earth[:, 1], panel_norm_earth[:, 2] * 0 ]).T # calculate vector magnitudes projected_normal_mag = np.sqrt(np.nansum(projected_normal**2, axis=1)) # renormalize the projected vector # avoid creating nan values. non_zeros = projected_normal_mag != 0 projected_normal[non_zeros] = (projected_normal[non_zeros].T / projected_normal_mag[non_zeros]).T # calculation of surface_azimuth # 1. Find the angle. # surface_azimuth = pd.Series( # np.degrees(np.arctan(projected_normal[:,1]/projected_normal[:,0])), # index=times) surface_azimuth = \ np.degrees(np.arctan2(projected_normal[:, 1], projected_normal[:, 0])) # 2. Clean up atan when x-coord or y-coord is zero # surface_azimuth[(projected_normal[:,0]==0) & (projected_normal[:,1]>0)] = 90 # surface_azimuth[(projected_normal[:,0]==0) & (projected_normal[:,1]<0)] = -90 # surface_azimuth[(projected_normal[:,1]==0) & (projected_normal[:,0]>0)] = 0 # surface_azimuth[(projected_normal[:,1]==0) & (projected_normal[:,0]<0)] = 180 # 3. Correct atan for QII and QIII # surface_azimuth[(projected_normal[:,0]<0) & (projected_normal[:,1]>0)] += 180 # QII # surface_azimuth[(projected_normal[:,0]<0) & (projected_normal[:,1]<0)] += 180 # QIII # 4. Skip to below # at this point surface_azimuth contains angles between -90 and +270, # where 0 is along the positive x-axis, # the y-axis is in the direction of the tracker azimuth, # and positive angles are rotations from the positive x axis towards # the positive y-axis. # Adjust to compass angles # (clockwise rotation from 0 along the positive y-axis) # surface_azimuth[surface_azimuth<=90] = 90 - surface_azimuth[surface_azimuth<=90] # surface_azimuth[surface_azimuth>90] = 450 - surface_azimuth[surface_azimuth>90] # finally rotate to align y-axis with true north # PVLIB_MATLAB has this latitude correction, # but I don't think it's latitude dependent if you always # specify axis_azimuth with respect to North. # if latitude > 0 or True: # surface_azimuth = surface_azimuth - axis_azimuth # else: # surface_azimuth = surface_azimuth - axis_azimuth - 180 # surface_azimuth[surface_azimuth<0] = 360 + surface_azimuth[surface_azimuth<0] # the commented code above is mostly part of PVLIB_MATLAB. # My (wholmgren) take is that it can be done more simply. # Say that we're pointing along the postive x axis (likely west). # We just need to rotate 90 degrees to get from the x axis # to the y axis (likely south), # and then add the axis_azimuth to get back to North. # Anything left over is the azimuth that we want, # and we can map it into the [0,360) domain. # 4. Rotate 0 reference from panel's x axis to it's y axis and # then back to North. surface_azimuth = 90 - surface_azimuth + axis_azimuth # 5. Map azimuth into [0,360) domain. # surface_azimuth[surface_azimuth < 0] += 360 # surface_azimuth[surface_azimuth >= 360] -= 360 surface_azimuth = surface_azimuth % 360 # Calculate surface_tilt dotproduct = (panel_norm_earth * projected_normal).sum(axis=1) surface_tilt = 90 - np.degrees(np.arccos(dotproduct)) # Bundle DataFrame for return values and filter for sun below horizon. out = { 'tracker_theta': tracker_theta, 'aoi': aoi, 'surface_azimuth': surface_azimuth, 'surface_tilt': surface_tilt } if index is not None: out = pd.DataFrame(out, index=index) out = out[['tracker_theta', 'aoi', 'surface_azimuth', 'surface_tilt']] out[zen_gt_90] = np.nan else: out = {k: np.where(zen_gt_90, np.nan, v) for k, v in out.items()} return out
def klucher(surf_tilt, surf_az, DHI, GHI, sun_zen, sun_az): r''' Determine diffuse irradiance from the sky on a tilted surface using Klucher's 1979 model .. math:: I_{d} = DHI \frac{1 + \cos\beta}{2} (1 + F' \sin^3(\beta/2)) (1 + F' \cos^2\theta\sin^3\theta_z) where .. math:: F' = 1 - (I_{d0} / GHI) Klucher's 1979 model determines the diffuse irradiance from the sky (ground reflected irradiance is not included in this algorithm) on a tilted surface using the surface tilt angle, surface azimuth angle, diffuse horizontal irradiance, direct normal irradiance, global horizontal irradiance, extraterrestrial irradiance, sun zenith angle, and sun azimuth angle. Parameters ---------- surf_tilt : float or Series Surface tilt angles in decimal degrees. surf_tilt must be >=0 and <=180. The tilt angle is defined as degrees from horizontal (e.g. surface facing up = 0, surface facing horizon = 90) surf_az : float or Series Surface azimuth angles in decimal degrees. surf_az must be >=0 and <=360. The Azimuth convention is defined as degrees east of north (e.g. North = 0, South=180 East = 90, West = 270). DHI : float or Series diffuse horizontal irradiance in W/m^2. DHI must be >=0. GHI : float or Series Global irradiance in W/m^2. DNI must be >=0. sun_zen : float or Series apparent (refraction-corrected) zenith angles in decimal degrees. sun_zen must be >=0 and <=180. sun_az : float or Series Sun azimuth angles in decimal degrees. sun_az must be >=0 and <=360. The Azimuth convention is defined as degrees east of north (e.g. North = 0, East = 90, West = 270). Returns ------- float or Series. The diffuse component of the solar radiation on an arbitrarily tilted surface defined by the Klucher model as given in Loutzenhiser et. al (2007) equation 4. SkyDiffuse is the diffuse component ONLY and does not include the ground reflected irradiance or the irradiance due to the beam. SkyDiffuse is a column vector vector with a number of elements equal to the input vector(s). References ---------- [1] Loutzenhiser P.G. et. al. "Empirical validation of models to compute solar irradiance on inclined surfaces for building energy simulation" 2007, Solar Energy vol. 81. pp. 254-267 [2] Klucher, T.M., 1979. Evaluation of models to predict insolation on tilted surfaces. Solar Energy 23 (2), 111-114. ''' pvl_logger.debug('diffuse_sky.klucher()') # zenith angle with respect to panel normal. cos_tt = aoi_projection(surf_tilt, surf_az, sun_zen, sun_az) F = 1 - ((DHI / GHI) ** 2) try: # fails with single point input F.fillna(0, inplace=True) except AttributeError: F = 0 term1 = 0.5 * (1 + tools.cosd(surf_tilt)) term2 = 1 + F * (tools.sind(0.5 * surf_tilt) ** 3) term3 = 1 + F * (cos_tt ** 2) * (tools.sind(sun_zen) ** 3) sky_diffuse = DHI * term1 * term2 * term3 return sky_diffuse
def perez_diffuse_luminance(df_inputs): """ Function used to calculate the luminance and the view factor terms from the Perez diffuse light transposition model, as implemented in the ``pvlib-python`` library. :param df_inputs: class:`pandas.DataFrame` with following columns: ['solar_zenith', 'solar_azimuth', 'array_tilt', 'array_azimuth', 'dhi', 'dni']. Units are: ['deg', 'deg', 'deg', 'deg', 'W/m2', 'W/m2'] :return: class:`pandas.DataFrame` with the following columns: ['solar_zenith', 'solar_azimuth', 'array_tilt', 'array_azimuth', 'dhi', 'dni', 'vf_horizon', 'vf_circumsolar', 'vf_isotropic', 'luminance_horizon', 'luminance_circumsolar', 'luminance_isotropic', 'poa_isotropic', 'poa_circumsolar', 'poa_horizon', 'poa_total_diffuse'] """ dni_et = irradiance.extraradiation(df_inputs.index.dayofyear) am = atmosphere.relativeairmass(df_inputs.solar_zenith) # Need to treat the case when the sun is hitting the back surface of pvrow aoi_proj = aoi_projection(df_inputs.array_tilt, df_inputs.array_azimuth, df_inputs.solar_zenith, df_inputs.solar_azimuth) sun_hitting_back_surface = ((aoi_proj < 0) & (df_inputs.solar_zenith <= 90)) df_inputs_back_surface = df_inputs.loc[sun_hitting_back_surface] # Reverse the surface normal to switch to back-surface circumsolar calc df_inputs_back_surface.loc[:, 'array_azimuth'] -= 180. df_inputs_back_surface.loc[:, 'array_azimuth'] = np.mod( df_inputs_back_surface.loc[:, 'array_azimuth'], 360. ) df_inputs_back_surface.loc[:, 'array_tilt'] = ( 180. - df_inputs_back_surface.array_tilt) if df_inputs_back_surface.shape[0] > 0: # Use recursion to calculate circumsolar luminance for back surface df_inputs_back_surface = perez_diffuse_luminance( df_inputs_back_surface) # Calculate Perez diffuse components diffuse_poa, components = irradiance.perez(df_inputs.array_tilt, df_inputs.array_azimuth, df_inputs.dhi, df_inputs.dni, dni_et, df_inputs.solar_zenith, df_inputs.solar_azimuth, am, return_components=True) # Calculate Perez view factors: a = aoi_projection(df_inputs.array_tilt, df_inputs.array_azimuth, df_inputs.solar_zenith, df_inputs.solar_azimuth) a = np.maximum(a, 0) b = cosd(df_inputs.solar_zenith) b = np.maximum(b, cosd(85)) vf_perez = pd.DataFrame( np.array([ sind(df_inputs.array_tilt), a / b, (1. + cosd(df_inputs.array_tilt)) / 2. ]).T, index=df_inputs.index, columns=['vf_horizon', 'vf_circumsolar', 'vf_isotropic'] ) # Calculate diffuse luminance luminance = pd.DataFrame( np.array([ components['horizon'] / vf_perez['vf_horizon'], components['circumsolar'] / vf_perez['vf_circumsolar'], components['isotropic'] / vf_perez['vf_isotropic'] ]).T, index=df_inputs.index, columns=['luminance_horizon', 'luminance_circumsolar', 'luminance_isotropic'] ) luminance.loc[diffuse_poa == 0, :] = 0. # Format components column names components = components.rename(columns={'isotropic': 'poa_isotropic', 'circumsolar': 'poa_circumsolar', 'horizon': 'poa_horizon'}) df_inputs = pd.concat([df_inputs, components, vf_perez, luminance, diffuse_poa], axis=1, join='outer') df_inputs = df_inputs.rename(columns={0: 'poa_total_diffuse'}) # Adjust the circumsolar luminance when it hits the back surface if df_inputs_back_surface.shape[0] > 0: df_inputs.loc[sun_hitting_back_surface, 'luminance_circumsolar'] = ( df_inputs_back_surface.loc[:, 'luminance_circumsolar'] ) return df_inputs