def _get_solar_2d_vectors(solar_zenith, solar_azimuth, axis_azimuth): """Projection of 3d solar vector onto the cross section of the systems: which is the 2D plane we are considering. This is needed to calculate shadows. Remember that the 2D plane is such that the direction of the torque tube vector (or rotation axis) goes into (and normal to) the 2D plane, such that positive rotation angles will have the PV surfaces tilted to the LEFT and vice versa. Parameters ---------- solar_zenith : float or numpy array Solar zenith angle [deg] solar_azimuth : float or numpy array Solar azimuth angle [deg] axis_azimuth : float Axis azimuth of the PV surface, i.e. direction of axis of rotation [deg] Returns ------- solar_2d_vector : numpy array Two vector components of the solar vector in the 2D plane, with the form [x, y], where x and y can be arrays """ solar_2d_vector = np.array([ # a drawing really helps understand the following sind(solar_zenith) * cosd(solar_azimuth - axis_azimuth - 90.), cosd(solar_zenith) ]) return solar_2d_vector
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 _calculate_full_coords(xy_center, width, rotation): """Method to calculate the full PV row coordinaltes. 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 : np.ndarray Timeseries rotation values of the PV row [deg] Returns ------- coords: :py:class:`~pvfactors.geometry.timeseries.TsLineCoords` Timeseries coordinates of full PV row """ x_center, y_center = xy_center radius = width / 2. # Calculate coords x1 = radius * cosd(rotation + 180.) + x_center y1 = radius * sind(rotation + 180.) + y_center x2 = radius * cosd(rotation) + x_center y2 = radius * sind(rotation) + y_center coords = TsLineCoords.from_array(np.array([[x1, y1], [x2, y2]])) return coords
def test__vf_row_sky_integ(test_system): ts, _, _ = test_system gcr = ts['gcr'] surface_tilt = ts['surface_tilt'] f_x = np.array([0., 0.5, 1.]) shaded = [] noshade = [] for x in f_x: s, ns = infinite_sheds._vf_row_sky_integ(x, surface_tilt, gcr, npoints=100) shaded.append(s) noshade.append(ns) def analytic(gcr, surface_tilt, x): c = cosd(surface_tilt) a = 1. / gcr dx = np.sqrt(a**2 - 2 * a * c * x + x**2) return -a * (c**2 - 1) * np.arctanh((x - a * c) / dx) - c * dx expected_shade = 0.5 * (f_x * cosd(surface_tilt) - analytic( gcr, surface_tilt, 1 - f_x) + analytic(gcr, surface_tilt, 1.)) expected_noshade = 0.5 * ((1 - f_x) * cosd(surface_tilt) + analytic( gcr, surface_tilt, 1. - f_x) - analytic(gcr, surface_tilt, 0.)) shaded = np.array(shaded) noshade = np.array(noshade) assert np.allclose(shaded, expected_shade) assert np.allclose(noshade, expected_noshade)
def _calc_tracker_norm(ba, bg, dg): """ Calculate tracker normal, v, cross product of tracker axis and unit normal, N, to the system slope plane. Parameters ---------- ba : float axis tilt [degrees] bg : float ground tilt [degrees] dg : float delta gamma, difference between axis and ground azimuths [degrees] Returns ------- vector : tuple vx, vy, vz """ cos_ba = cosd(ba) cos_bg = cosd(bg) sin_bg = sind(bg) sin_dg = sind(dg) vx = sin_dg * cos_ba * cos_bg vy = sind(ba) * sin_bg + cosd(dg) * cos_ba * cos_bg vz = -sin_dg * sin_bg * cos_ba return vx, vy, vz
def _vf_row_ground(x, surface_tilt, gcr): """ View factor from a point x on the row to the ground. Parameters ---------- x : numeric Fraction of row slant height from the bottom. [unitless] surface_tilt : numeric Surface tilt angle in degrees from horizontal, e.g., surface facing up = 0, surface facing horizon = 90. [degree] gcr : float Ground coverage ratio, ratio of row slant length to row spacing. [unitless] Returns ------- vf : numeric View factor from the point at x to the ground. [unitless] """ cst = cosd(surface_tilt) # angle from horizontal at the point x on the row slant height to the # bottom of the facing row psi_t_shaded = _ground_angle(x, surface_tilt, gcr) # view factor from the point on the row to the ground return 0.5 * (cosd(psi_t_shaded) - cst)
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 _vf_row_sky_integ(f_x, surface_tilt, gcr, npoints=100): """ Integrated view factors from the shaded and unshaded parts of the row slant height to the sky. Parameters ---------- f_x : numeric Fraction of row slant height from the bottom that is shaded. [unitless] surface_tilt : numeric Surface tilt angle in degrees from horizontal, e.g., surface facing up = 0, surface facing horizon = 90. [degree] gcr : float Ratio of row slant length to row spacing (pitch). [unitless] npoints : int, default 100 Number of points for integration. [unitless] Returns ------- vf_shade_sky_integ : numeric Integrated view factor from the shaded part of the row to the sky. [unitless] vf_noshade_sky_integ : numeric Integrated view factor from the unshaded part of the row to the sky. [unitless] Notes ----- The view factor to the sky at a point x along the row slant height is given by .. math :: \\large{f_{sky} = \frac{1}{2} \\left(\\cos\\left(\\psi_t\\right) + \\cos \\left(\\beta\\right) \\right) where :math:`\\psi_t` is the angle from horizontal of the line from point x to the top of the facing row, and :math:`\\beta` is the surface tilt. View factors are integrated separately over shaded and unshaded portions of the row slant height. """ # handle Series inputs surface_tilt = np.array(surface_tilt) cst = cosd(surface_tilt) # shaded portion x = np.linspace(0, f_x, num=npoints) psi_t_shaded = masking_angle(surface_tilt, gcr, x) y = 0.5 * (cosd(psi_t_shaded) + cst) # integrate view factors from each point in the discretization. This is an # improvement over the algorithm described in [2] vf_shade_sky_integ = np.trapz(y, x, axis=0) # unshaded portion x = np.linspace(f_x, 1., num=npoints) psi_t_unshaded = masking_angle(surface_tilt, gcr, x) y = 0.5 * (cosd(psi_t_unshaded) + cst) vf_noshade_sky_integ = np.trapz(y, x, axis=0) return vf_shade_sky_integ, vf_noshade_sky_integ
def _vf_ground_sky_2d(x, rotation, gcr, pitch, height, max_rows=10): r""" Calculate the fraction of the sky dome visible from point x on the ground. The view factor accounts for the obstruction of the sky by array rows that are assumed to be infinitely long. View factors are thus calculated in a 2D geometry. The ground is assumed to be flat and level. Parameters ---------- x : numeric Position on the ground between two rows, as a fraction of the pitch. x = 0 corresponds to the point on the ground directly below the center point of a row. Positive x is towards the right. [unitless] rotation : float Rotation angle of the row's right edge relative to row center. [degree] gcr : float Ratio of the row slant length to the row spacing (pitch). [unitless] height : float Height of the center point of the row above the ground; must be in the same units as ``pitch``. pitch : float Distance between two rows; must be in the same units as ``height``. max_rows : int, default 10 Maximum number of rows to consider on either side of the current row. [unitless] Returns ------- vf : numeric Fraction of sky dome visible from each point on the ground. [unitless] wedge_angles : array Angles defining each wedge of sky that is blocked by a row. Shape is (2, len(x), 2*max_rows+1). ``wedge_angles[0,:,:]`` is the starting angle of each wedge, ``wedge_angles[1,:,:]`` is the end angle. [degree] """ x = np.atleast_1d(x) # handle float all_k = np.arange(-max_rows, max_rows + 1) width = gcr * pitch / 2. # angles from x to right edge of each row a1 = height + width * sind(rotation) b1 = (all_k - x[:, np.newaxis]) * pitch + width * cosd(rotation) phi_1 = np.degrees(np.arctan2(a1, b1)) # angles from x to left edge of each row a2 = height - width * sind(rotation) b2 = (all_k - x[:, np.newaxis]) * pitch - width * cosd(rotation) phi_2 = np.degrees(np.arctan2(a2, b2)) phi = np.stack([phi_1, phi_2]) swap = phi[0, :, :] > phi[1, :, :] # swap where phi_1 > phi_2 so that phi_1[0,:,:] is the lesser angle phi = np.where(swap, phi[::-1], phi) # right edge of next row - left edge of previous row wedge_vfs = 0.5 * (cosd(phi[1, :, 1:]) - cosd(phi[0, :, :-1])) vf = np.sum(np.where(wedge_vfs > 0, wedge_vfs, 0.), axis=1) return vf, phi
def _coords_from_center_tilt_length(xy_center, tilt, length, surface_azimuth, axis_azimuth): """Calculate ``shapely`` :py:class:`LineString` coordinates from center coords, surface angles and length of line. The axis azimuth indicates the axis of rotation of the pvrows (if single- axis trackers). In the 2D plane, the axis of rotation will be the vector normal to that 2D plane and going into the 2D plane (when plotting it). The surface azimuth should always be 90 degrees away from the axis azimuth, either in the positive or negative direction. For instance, a single axis trk with axis azimuth = 0 deg (North), will have surface azimuth values equal to 90 deg (East) or 270 deg (West). Tilt angles need to always be positive. Given the axis azimuth and surface azimuth, a rotation angle will be derived. Positive rotation angles will indicate pvrows pointing to the left, and negative rotation angles will indicate pvrows pointing to the right (no matter what the the axis azimuth is). All of these conventions are necessary to make sure that no matter what the tilt and surface angles are, we can still identify correctly the same pv rows: the leftmost PV row will have index 0, and the rightmost will have index -1. Parameters ---------- xy_center : tuple x, y coordinates of center point of desired linestring tilt : float or np.ndarray Surface tilt angles desired [deg]. Values should all be positive. length : float desired length of linestring [m] surface_azimuth : float or np.ndarray Surface azimuth angles of PV surface [deg] axis_azimuth : float Axis azimuth of the PV surface, i.e. direction of axis of rotation [deg] Returns ------- list List of linestring coordinates obtained from inputs (could be vectors) in the form of [[x1, y1], [x2, y2]], where xi and yi could be arrays or scalar values. """ # PV row params x_center, y_center = xy_center radius = length / 2. # Get rotation rotation = _get_rotation_from_tilt_azimuth(surface_azimuth, axis_azimuth, tilt) # Calculate coords x1 = radius * cosd(rotation + 180.) + x_center y1 = radius * sind(rotation + 180.) + y_center x2 = radius * cosd(rotation) + x_center y2 = radius * sind(rotation) + y_center return [[x1, y1], [x2, y2]]
def calc_surface_orientation(tracker_theta, axis_tilt=0, axis_azimuth=0): """ Calculate the surface tilt and azimuth angles for a given tracker rotation. Parameters ---------- tracker_theta : numeric Tracker rotation angle as a right-handed rotation around the axis defined by ``axis_tilt`` and ``axis_azimuth``. For example, with ``axis_tilt=0`` and ``axis_azimuth=180``, ``tracker_theta > 0`` results in ``surface_azimuth`` to the West while ``tracker_theta < 0`` results in ``surface_azimuth`` to the East. [degree] axis_tilt : float, default 0 The tilt of the axis of rotation with respect to horizontal. [degree] axis_azimuth : float, default 0 A value denoting the compass direction along which the axis of rotation lies. Measured east of north. [degree] Returns ------- dict or DataFrame Contains keys ``'surface_tilt'`` and ``'surface_azimuth'`` representing the module orientation accounting for tracker rotation and axis orientation. [degree] References ---------- .. [1] William F. Marion and Aron P. Dobos, "Rotation Angle for the Optimum Tracking of One-Axis Trackers", Technical Report NREL/TP-6A20-58891, July 2013. :doi:`10.2172/1089596` """ with np.errstate(invalid='ignore', divide='ignore'): surface_tilt = acosd(cosd(tracker_theta) * cosd(axis_tilt)) # clip(..., -1, +1) to prevent arcsin(1 + epsilon) issues: azimuth_delta = asind(np.clip(sind(tracker_theta) / sind(surface_tilt), a_min=-1, a_max=1)) # Combine Eqs 2, 3, and 4: azimuth_delta = np.where(abs(tracker_theta) < 90, azimuth_delta, -azimuth_delta + np.sign(tracker_theta) * 180) # handle surface_tilt=0 case: azimuth_delta = np.where(sind(surface_tilt) != 0, azimuth_delta, 90) surface_azimuth = (axis_azimuth + azimuth_delta) % 360 out = { 'surface_tilt': surface_tilt, 'surface_azimuth': surface_azimuth, } if hasattr(tracker_theta, 'index'): out = pd.DataFrame(out) return out
def king(surf_tilt, DHI, GHI, sun_zen): ''' Determine diffuse irradiance from the sky on a tilted surface using the King model. King's 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, diffuse horizontal irradiance, global horizontal irradiance, and sun zenith angle. Note that this model is not well documented and has not been published in any fashion (as of January 2012). 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) DHI : float or Series diffuse horizontal irradiance in W/m^2. GHI : float or Series global horizontal irradiance in W/m^2. sun_zen : float or Series apparent (refraction-corrected) zenith angles in decimal degrees. Returns -------- SkyDiffuse : float or Series the diffuse component of the solar radiation on an arbitrarily tilted surface as given by a model developed by David L. King at Sandia National Laboratories. ''' pvl_logger.debug('diffuse_sky.king()') sky_diffuse = (DHI * ((1 + tools.cosd(surf_tilt))) / 2 + GHI * ((0.012 * sun_zen - 0.04)) * ((1 - tools.cosd(surf_tilt))) / 2) sky_diffuse[sky_diffuse < 0] = 0 return sky_diffuse
def haurwitz(apparent_zenith): """Caluclate global horizontal irradiance from apparent zenith angle of sun using Haurwitz method. Parameters ---------- apparent_zenith : array_like Apparent zenith angle of sun in degrees Returns ------- array_like Global horizontal irradiance Notes ----- Based on `pvlib.clearsky.haurwitz` """ cos_zenith = tools.cosd(apparent_zenith) clearsky_ghi = np.zeros_like(apparent_zenith) cos_zen_gte_0 = cos_zenith > 0 clearsky_ghi[cos_zen_gte_0] = (1098.0 * cos_zenith[cos_zen_gte_0] * np.exp(-0.059 / cos_zenith[cos_zen_gte_0])) return clearsky_ghi
def test_noct_sam_against_sam(): # test is constructed to reproduce output from SAM v2020.11.29. # SAM calculation is the default Detailed PV System model (CEC diode model, # NOCT cell temperature model), with the only change being the soiling # loss is set to 0. Weather input is TMY3 for Phoenix AZ. # Values are taken from the Jan 1 12:00:00 timestamp. poa_total, temp_air, wind_speed, noct, module_efficiency = ( 860.673, 25, 3, 46.4, 0.20551) poa_total_after_refl = 851.458 # from SAM output # compute effective irradiance # spectral loss coefficients fixed in lib_cec6par.cpp a = np.flipud([0.918093, 0.086257, -0.024459, 0.002816, -0.000126]) # reproduce SAM air mass calculation zen = 56.4284 elev = 358 air_mass = 1. / (tools.cosd(zen) + 0.5057 * (96.080 - zen)**-1.634) air_mass *= np.exp(-0.0001184 * elev) f1 = np.polyval(a, air_mass) effective_irradiance = f1 * poa_total_after_refl transmittance_absorptance = 0.9 array_height = 1 mount_standoff = 4.0 result = temperature.noct_sam(poa_total, temp_air, wind_speed, noct, module_efficiency, effective_irradiance, transmittance_absorptance, array_height, mount_standoff) expected = 43.0655 # rtol from limited SAM output precision assert_allclose(result, expected, rtol=1e-5)
def _solar_projection_tangent(solar_zenith, solar_azimuth, surface_azimuth): """ Tangent of the angle between the zenith vector and the sun vector projected to the plane defined by the zenith vector and surface_azimuth. .. math:: \\tan \\phi = \\cos\\left(\\text{solar azimuth}-\\text{system azimuth} \\right)\\tan\\left(\\text{solar zenith}\\right) Parameters ---------- solar_zenith : numeric Solar zenith angle. [degree]. solar_azimuth : numeric Solar azimuth. [degree]. surface_azimuth : numeric Azimuth of the module surface, i.e., North=0, East=90, South=180, West=270. [degree] Returns ------- tan_phi : numeric Tangent of the angle between vertical and the projection of the sun direction onto the YZ plane. """ rotation = solar_azimuth - surface_azimuth tan_phi = cosd(rotation) * tand(solar_zenith) return tan_phi
def _create_shaded_side_coords(xy_center, width, shaded_length, mask_tilted_to_left, rotation_vec, side_lowest_pt): """ Create the timeseries line coordinates for the shaded portion of a PV row side, based on inputted shaded length. 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] shaded_length : np.ndarray Timeseries values of side shaded length [m] tilted_to_left : list of bool Flags indicating when the PV rows are strictly tilted to the left rotation_vec : np.ndarray Timeseries rotation vector of the PV rows in [deg] side_lowest_pt : :py:class:`~pvfactors.geometry.timeseries.TsPointCoords` Timeseries coordinates of lowest point of considered PV row side Returns ------- side_shaded_coords : :py:class:`~pvfactors.geometry.timeseries.TsPointCoords` Timeseries coordinates of the shaded portion of the PV row side """ # Get invariant values x_center, y_center = xy_center radius = width / 2. # 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) side_shaded_coords = TsLineCoords(TsPointCoords(x_sh, y_sh), side_lowest_pt) return side_shaded_coords
def isotropic(surf_tilt, DHI): r''' Determine diffuse irradiance from the sky on a tilted surface using the isotropic sky model. .. math:: I_{d} = DHI \frac{1 + \cos\beta}{2} Hottel and Woertz's model treats the sky as a uniform source of diffuse irradiance. Thus the diffuse irradiance from the sky (ground reflected irradiance is not included in this algorithm) on a tilted surface can be found from the diffuse horizontal irradiance and the tilt angle of the surface. Parameters ---------- surf_tilt : float or Series Surface tilt angle 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) DHI : float or Series Diffuse horizontal irradiance in W/m^2. DHI must be >=0. Returns ------- float or Series The diffuse component of the solar radiation on an arbitrarily tilted surface defined by the isotropic sky model as given in Loutzenhiser et. al (2007) equation 3. 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] Hottel, H.C., Woertz, B.B., 1942. Evaluation of flat-plate solar heat collector. Trans. ASME 64, 91. ''' pvl_logger.debug('diffuse_sky.isotropic()') sky_diffuse = DHI * (1 + tools.cosd(surf_tilt)) * 0.5 return sky_diffuse
def _shaded_fraction(solar_zenith, solar_azimuth, surface_tilt, surface_azimuth, gcr): """ Calculate fraction (from the bottom) of row slant height that is shaded from direct irradiance by the row in front toward the sun. See [1], Eq. 14 and also [2], Eq. 32. .. math:: F_x = \\max \\left( 0, \\min \\left(\\frac{\\text{GCR} \\cos \\theta + \\left( \\text{GCR} \\sin \\theta - \\tan \\beta_{c} \\right) \\tan Z - 1} {\\text{GCR} \\left( \\cos \\theta + \\sin \\theta \\tan Z \\right)}, 1 \\right) \\right) Parameters ---------- solar_zenith : numeric Apparent (refraction-corrected) solar zenith. [degrees] solar_azimuth : numeric Solar azimuth. [degrees] surface_tilt : numeric Row tilt from horizontal, e.g. surface facing up = 0, surface facing horizon = 90. [degrees] surface_azimuth : numeric Azimuth angle of the row surface. North=0, East=90, South=180, West=270. [degrees] gcr : numeric Ground coverage ratio, which is the ratio of row slant length to row spacing (pitch). [unitless] Returns ------- f_x : numeric Fraction of row slant height from the bottom that is shaded from direct irradiance. References ---------- .. [1] Mikofski, M., Darawali, R., Hamer, M., Neubert, A., and Newmiller, J. "Bifacial Performance Modeling in Large Arrays". 2019 IEEE 46th Photovoltaic Specialists Conference (PVSC), 2019, pp. 1282-1287. :doi:`10.1109/PVSC40753.2019.8980572`. .. [2] 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 """ tan_phi = utils._solar_projection_tangent(solar_zenith, solar_azimuth, surface_azimuth) # length of shadow behind a row as a fraction of pitch x = gcr * (sind(surface_tilt) * tan_phi + cosd(surface_tilt)) f_x = 1 - 1. / x # set f_x to be 1 when sun is behind the array ao = aoi(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth) f_x = np.where(ao < 90, f_x, 1.) # when x < 1, the shadow is not long enough to fall on the row surface f_x = np.where(x > 1., f_x, 0.) return f_x
def _unshaded_ground_fraction(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, gcr, max_zenith=87): r""" Calculate the fraction of the ground with incident direct irradiance. .. math:: F_{gnd,sky} = 1 - \min{\left(1, \text{GCR} \left|\cos \beta + \sin \beta \tan \phi \right|\right)} where :math:`\beta` is the surface tilt and :math:`\phi` is the angle from vertical of the sun vector projected to a vertical plane that contains the row azimuth `surface_azimuth`. Parameters ---------- surface_tilt : numeric Surface tilt angle. The tilt angle is defined as degrees from horizontal, e.g., surface facing up = 0, surface facing horizon = 90. [degree] surface_azimuth : numeric Azimuth of the module surface, i.e., North=0, East=90, South=180, West=270. [degree] solar_zenith : numeric Solar zenith angle. [degree]. solar_azimuth : numeric Solar azimuth. [degree]. gcr : float Ground coverage ratio, which is the ratio of row slant length to row spacing (pitch). [unitless] max_zenith : numeric, default 87 Maximum zenith angle. For solar_zenith > max_zenith, unshaded ground fraction is set to 0. [degree] Returns ------- f_gnd_beam : numeric Fraction of distance betwen rows (pitch) with direct irradiance (unshaded). [unitless] References ---------- .. [1] Mikofski, M., Darawali, R., Hamer, M., Neubert, A., and Newmiller, J. "Bifacial Performance Modeling in Large Arrays". 2019 IEEE 46th Photovoltaic Specialists Conference (PVSC), 2019, pp. 1282-1287. doi: 10.1109/PVSC40753.2019.8980572. """ tan_phi = _solar_projection_tangent(solar_zenith, solar_azimuth, surface_azimuth) f_gnd_beam = 1.0 - np.minimum( 1.0, gcr * np.abs(cosd(surface_tilt) + sind(surface_tilt) * tan_phi)) np.where(solar_zenith > max_zenith, 0., f_gnd_beam) # [1], Eq. 4 return f_gnd_beam # 1 - min(1, abs()) < 1 always
def haurwitz(ApparentZenith): ''' Determine clear sky GHI from Haurwitz model Implements the Haurwitz clear sky model for global horizontal irradiance (GHI) as presented in [1, 2]. A report on clear sky models found the Haurwitz model to have the best performance of models which require only zenith angle [3]. Extreme care should be taken in the interpretation of this result! Parameters ---------- ApparentZenith : DataFrame The apparent (refraction corrected) sun zenith angle in degrees. Returns ------- pd.Series. The modeled global horizonal irradiance in W/m^2 provided by the Haurwitz clear-sky model. Initial implementation of this algorithm by Matthew Reno. References ---------- [1] B. Haurwitz, "Insolation in Relation to Cloudiness and Cloud Density," Journal of Meteorology, vol. 2, pp. 154-166, 1945. [2] B. Haurwitz, "Insolation in Relation to Cloud Type," Journal of Meteorology, vol. 3, pp. 123-124, 1946. [3] M. Reno, C. Hansen, and J. Stein, "Global Horizontal Irradiance Clear Sky Models: Implementation and Analysis", Sandia National Laboratories, SAND2012-2389, 2012. See Also --------- maketimestruct makelocationstruct ephemeris spa ineichen ''' cos_zenith = tools.cosd(ApparentZenith) clearsky_GHI = 1098.0 * cos_zenith * np.exp(-0.059 / cos_zenith) clearsky_GHI[clearsky_GHI < 0] = 0 df_out = pd.DataFrame({'GHI': clearsky_GHI}) return df_out
def _vf(aoi_1, aoi_2): """Calculate view factor from infinitesimal surface to infinite band. See illustration: http://www.thermalradiation.net/sectionb/B-71.html Here we're using angles measured from the horizontal Parameters ---------- aoi_1 : np.ndarray Lower angles defining the infinite band aoi_2 : np.ndarray Higher angles defining the infinite band Returns ------- np.ndarray View factors from infinitesimal surface to infinite strip """ return 0.5 * np.abs(cosd(aoi_1) - cosd(aoi_2))
def haurwitz(ApparentZenith): """ Determine clear sky GHI from Haurwitz model Implements the Haurwitz clear sky model for global horizontal irradiance (GHI) as presented in [1, 2]. A report on clear sky models found the Haurwitz model to have the best performance of models which require only zenith angle [3]. Extreme care should be taken in the interpretation of this result! Parameters ---------- ApparentZenith : DataFrame The apparent (refraction corrected) sun zenith angle in degrees. Returns ------- pd.Series. The modeled global horizonal irradiance in W/m^2 provided by the Haurwitz clear-sky model. Initial implementation of this algorithm by Matthew Reno. References ---------- [1] B. Haurwitz, "Insolation in Relation to Cloudiness and Cloud Density," Journal of Meteorology, vol. 2, pp. 154-166, 1945. [2] B. Haurwitz, "Insolation in Relation to Cloud Type," Journal of Meteorology, vol. 3, pp. 123-124, 1946. [3] M. Reno, C. Hansen, and J. Stein, "Global Horizontal Irradiance Clear Sky Models: Implementation and Analysis", Sandia National Laboratories, SAND2012-2389, 2012. See Also --------- maketimestruct makelocationstruct ephemeris spa ineichen """ cos_zenith = tools.cosd(ApparentZenith) clearsky_GHI = 1098.0 * cos_zenith * np.exp(-0.059 / cos_zenith) clearsky_GHI[clearsky_GHI < 0] = 0 df_out = pd.DataFrame({"GHI": clearsky_GHI}) return df_out
def test__vf_row_ground_integ(test_system): ts, _, _ = test_system gcr = ts['gcr'] surface_tilt = ts['surface_tilt'] f_x = np.array([0., 0.5, 1.0]) shaded, noshade = infinite_sheds._vf_row_ground_integ( f_x, surface_tilt, gcr) def analytic(x, surface_tilt, gcr): c = cosd(surface_tilt) a = 1. / gcr dx = np.sqrt(a**2 + 2 * a * c * x + x**2) return c * dx - a * (c**2 - 1) * np.arctanh((a * c + x) / dx) expected_shade = 0.5 * (analytic(f_x, surface_tilt, gcr) - analytic( 0., surface_tilt, gcr) - f_x * cosd(surface_tilt)) expected_noshade = 0.5 * (analytic(1., surface_tilt, gcr) - analytic(f_x, surface_tilt, gcr) - (1. - f_x) * cosd(surface_tilt)) assert np.allclose(shaded, expected_shade) assert np.allclose(noshade, expected_noshade)
def _calc_beta_c(v, dg, ba): """ Calculate the cross-axis tilt angle. Parameters ---------- v : tuple tracker normal dg : float delta gamma, difference between axis and ground azimuths [degrees] ba : float axis tilt [degrees] Returns ------- beta_c : float cross-axis tilt angle [radians] """ vnorm = np.sqrt(np.dot(v, v)) beta_c = np.arcsin( ((v[0]*cosd(dg) - v[1]*sind(dg)) * sind(ba) + v[2]*cosd(ba)) / vnorm) return beta_c
def haurwitz(apparent_zenith): ''' Determine clear sky GHI from Haurwitz model. Implements the Haurwitz clear sky model for global horizontal irradiance (GHI) as presented in [1, 2]. A report on clear sky models found the Haurwitz model to have the best performance in terms of average monthly error among models which require only zenith angle [3]. Parameters ---------- apparent_zenith : Series The apparent (refraction corrected) sun zenith angle in degrees. Returns ------- pd.DataFrame The modeled global horizonal irradiance in W/m^2 provided by the Haurwitz clear-sky model. Initial implementation of this algorithm by Matthew Reno. References ---------- [1] B. Haurwitz, "Insolation in Relation to Cloudiness and Cloud Density," Journal of Meteorology, vol. 2, pp. 154-166, 1945. [2] B. Haurwitz, "Insolation in Relation to Cloud Type," Journal of Meteorology, vol. 3, pp. 123-124, 1946. [3] M. Reno, C. Hansen, and J. Stein, "Global Horizontal Irradiance Clear Sky Models: Implementation and Analysis", Sandia National Laboratories, SAND2012-2389, 2012. ''' cos_zenith = tools.cosd(apparent_zenith.values) clearsky_ghi = np.zeros_like(apparent_zenith.values) cos_zen_gte_0 = cos_zenith > 0 clearsky_ghi[cos_zen_gte_0] = (1098.0 * cos_zenith[cos_zen_gte_0] * np.exp(-0.059/cos_zenith[cos_zen_gte_0])) df_out = pd.DataFrame(index=apparent_zenith.index, data=clearsky_ghi, columns=['ghi']) return df_out
def masking_angle(surface_tilt, gcr, slant_height): """ The elevation angle below which diffuse irradiance is blocked. The ``height`` parameter determines how far up the module's surface to evaluate the masking angle. The lower the point, the steeper the masking angle [1]_. SAM uses a "worst-case" approach where the masking angle is calculated for the bottom of the array (i.e. ``slant_height=0``) [2]_. Parameters ---------- surface_tilt : numeric Panel tilt from horizontal [degrees]. gcr : float The ground coverage ratio of the array [unitless]. slant_height : numeric The distance up the module's slant height to evaluate the masking angle, as a fraction [0-1] of the module slant height [unitless]. Returns ------- mask_angle : numeric Angle from horizontal where diffuse light is blocked by the preceding row [degrees]. See Also -------- masking_angle_passias sky_diffuse_passias References ---------- .. [1] D. Passias and B. Källbäck, "Shading effects in rows of solar cell panels", Solar Cells, Volume 11, Pages 281-291. 1984. DOI: 10.1016/0379-6787(84)90017-6 .. [2] Gilman, P. et al., (2018). "SAM Photovoltaic Model Technical Reference Update", NREL Technical Report NREL/TP-6A20-67399. Available at https://www.nrel.gov/docs/fy18osti/67399.pdf """ # The original equation (8 in [1]) requires pitch and collector width, # but it's easy to non-dimensionalize it to make it a function of GCR # by factoring out B from the argument to arctan. numerator = (1 - slant_height) * sind(surface_tilt) denominator = 1/gcr - (1 - slant_height) * cosd(surface_tilt) phi = np.arctan(numerator / denominator) return np.degrees(phi)
def sky_diffuse_passias(masking_angle): r""" The diffuse irradiance loss caused by row-to-row sky diffuse shading. Even when the sun is high in the sky, a row's view of the sky dome will be partially blocked by the row in front. This causes a reduction in the diffuse irradiance incident on the module. The reduction depends on the masking angle, the elevation angle from a point on the shaded module to the top of the shading row. In [1]_ the masking angle is calculated as the average across the module height. SAM assumes the "worst-case" loss where the masking angle is calculated for the bottom of the array [2]_. This function, as in [1]_, makes the assumption that sky diffuse irradiance is isotropic. Parameters ---------- masking_angle : numeric The elevation angle below which diffuse irradiance is blocked [degrees]. Returns ------- derate : numeric The fraction [0-1] of blocked sky diffuse irradiance. See Also -------- masking_angle masking_angle_passias References ---------- .. [1] D. Passias and B. Källbäck, "Shading effects in rows of solar cell panels", Solar Cells, Volume 11, Pages 281-291. 1984. DOI: 10.1016/0379-6787(84)90017-6 .. [2] Gilman, P. et al., (2018). "SAM Photovoltaic Model Technical Reference Update", NREL Technical Report NREL/TP-6A20-67399. Available at https://www.nrel.gov/docs/fy18osti/67399.pdf """ return 1 - cosd(masking_angle/2)**2
def _ground_angle(x, surface_tilt, gcr): """ Angle from horizontal of the line from a point x on the row slant length to the bottom of the facing row. The angles are clockwise from horizontal, rather than the usual counterclockwise direction. Parameters ---------- x : numeric fraction of row slant length from bottom, ``x = 0`` is at the row bottom, ``x = 1`` is at the top of the row. surface_tilt : numeric Surface tilt angle in degrees from horizontal, e.g., surface facing up = 0, surface facing horizon = 90. [degree] gcr : float ground coverage ratio, ratio of row slant length to row spacing. [unitless] Returns ------- psi : numeric Angle [degree]. """ # : \\ \ # : \\ \ # : \\ \ # : \\ \ facing row # : \\.___________\ # : \ ^*-. psi \ # : \ x *-. \ # : \ v *-.\ # : \<-----P---->\ x1 = x * sind(surface_tilt) x2 = (x * cosd(surface_tilt) + 1 / gcr) psi = np.arctan2(x1, x2) # do this first because it handles 0 / 0 return np.rad2deg(psi)
def poa_horizontal_ratio(surf_tilt, surf_az, sun_zen, sun_az): """ Calculates the ratio of the beam components of the plane of array irradiance and the horizontal irradiance. 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. Ratio of the plane of array irradiance to the horizontal plane irradiance """ cos_poa_zen = aoi_projection(surf_tilt, surf_az, sun_zen, sun_az) cos_sun_zen = tools.cosd(sun_zen) # ratio of titled and horizontal beam irradiance ratio = cos_poa_zen / cos_sun_zen try: ratio.name = 'poa_ratio' except AttributeError: pass return ratio
def calc_axis_tilt(slope_azimuth, slope_tilt, axis_azimuth): """ Calculate tracker axis tilt in the global reference frame when on a sloped plane. Parameters ---------- slope_azimuth : float direction of normal to slope on horizontal [degrees] slope_tilt : float tilt of normal to slope relative to vertical [degrees] axis_azimuth : float direction of tracker axes on horizontal [degrees] Returns ------- axis_tilt : float tilt of tracker [degrees] See also -------- pvlib.tracking.singleaxis pvlib.tracking.calc_cross_axis_tilt Notes ----- See [1]_ for derivation of equations. 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 """ delta_gamma = axis_azimuth - slope_azimuth # equations 18-19 tan_axis_tilt = cosd(delta_gamma) * tand(slope_tilt) return np.degrees(np.arctan(tan_axis_tilt))
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 ineichen(apparent_zenith, airmass_absolute, linke_turbidity, altitude=0, dni_extra=1364., perez_enhancement=False): ''' Determine clear sky GHI, DNI, and DHI from Ineichen/Perez model. Implements the Ineichen and Perez clear sky model for global horizontal irradiance (GHI), direct normal irradiance (DNI), and calculates the clear-sky diffuse horizontal (DHI) component as the difference between GHI and DNI*cos(zenith) as presented in [1, 2]. A report on clear sky models found the Ineichen/Perez model to have excellent performance with a minimal input data set [3]. Default values for monthly Linke turbidity provided by SoDa [4, 5]. Parameters ----------- apparent_zenith : numeric Refraction corrected solar zenith angle in degrees. airmass_absolute : numeric Pressure corrected airmass. linke_turbidity : numeric Linke Turbidity. altitude : numeric, default 0 Altitude above sea level in meters. dni_extra : numeric, default 1364 Extraterrestrial irradiance. The units of ``dni_extra`` determine the units of the output. perez_enhancement : bool, default False Controls if the Perez enhancement factor should be applied. Setting to True may produce spurious results for times when the Sun is near the horizon and the airmass is high. See https://github.com/pvlib/pvlib-python/issues/435 Returns ------- clearsky : DataFrame (if Series input) or OrderedDict of arrays DataFrame/OrderedDict contains the columns/keys ``'dhi', 'dni', 'ghi'``. See also -------- lookup_linke_turbidity pvlib.location.Location.get_clearsky References ---------- [1] P. Ineichen and R. Perez, "A New airmass independent formulation for the Linke turbidity coefficient", Solar Energy, vol 73, pp. 151-157, 2002. [2] R. Perez et. al., "A New Operational Model for Satellite-Derived Irradiances: Description and Validation", Solar Energy, vol 73, pp. 307-317, 2002. [3] M. Reno, C. Hansen, and J. Stein, "Global Horizontal Irradiance Clear Sky Models: Implementation and Analysis", Sandia National Laboratories, SAND2012-2389, 2012. [4] http://www.soda-is.com/eng/services/climat_free_eng.php#c5 (obtained July 17, 2012). [5] J. Remund, et. al., "Worldwide Linke Turbidity Information", Proc. ISES Solar World Congress, June 2003. Goteborg, Sweden. ''' # ghi is calculated using either the equations in [1] by setting # perez_enhancement=False (default behavior) or using the model # in [2] by setting perez_enhancement=True. # The NaN handling is a little subtle. The AM input is likely to # have NaNs that we'll want to map to 0s in the output. However, we # want NaNs in other inputs to propagate through to the output. This # is accomplished by judicious use and placement of np.maximum, # np.minimum, and np.fmax # use max so that nighttime values will result in 0s instead of # negatives. propagates nans. cos_zenith = np.maximum(tools.cosd(apparent_zenith), 0) tl = linke_turbidity fh1 = np.exp(-altitude/8000.) fh2 = np.exp(-altitude/1250.) cg1 = 5.09e-05 * altitude + 0.868 cg2 = 3.92e-05 * altitude + 0.0387 ghi = np.exp(-cg2*airmass_absolute*(fh1 + fh2*(tl - 1))) # https://github.com/pvlib/pvlib-python/issues/435 if perez_enhancement: ghi *= np.exp(0.01*airmass_absolute**1.8) # use fmax to map airmass nans to 0s. multiply and divide by tl to # reinsert tl nans ghi = cg1 * dni_extra * cos_zenith * tl / tl * np.fmax(ghi, 0) # BncI = "normal beam clear sky radiation" b = 0.664 + 0.163/fh1 bnci = b * np.exp(-0.09 * airmass_absolute * (tl - 1)) bnci = dni_extra * np.fmax(bnci, 0) # "empirical correction" SE 73, 157 & SE 73, 312. bnci_2 = ((1 - (0.1 - 0.2*np.exp(-tl))/(0.1 + 0.882/fh1)) / cos_zenith) bnci_2 = ghi * np.fmin(np.fmax(bnci_2, 0), 1e20) dni = np.minimum(bnci, bnci_2) dhi = ghi - dni*cos_zenith irrads = OrderedDict() irrads['ghi'] = ghi irrads['dni'] = dni irrads['dhi'] = dhi if isinstance(dni, pd.Series): irrads = pd.DataFrame.from_dict(irrads) return irrads
'surface_tilt': 60, 'albedo': 0.2, } # own location parameter wittenberg = { 'altitude': 34, 'name': 'Wittenberg', 'latitude': my_weather.latitude, 'longitude': my_weather.longitude, } # the following has been implemented in the pvlib ModelChain in the # complete_irradiance method (pvlib version > v0.4.5) if w.get('dni') is None: w['dni'] = (w.ghi - w.dhi) / cosd( Location(**wittenberg).get_solarposition(times).zenith) # pvlib's ModelChain mc = ModelChain(PVSystem(**yingli210), Location(**wittenberg), orientation_strategy='south_at_latitude_tilt') mc.run_model(times, weather=w) if plt: mc.dc.p_mp.fillna(0).plot() plt.show() else: logging.warning("No plots shown. Install matplotlib to see the plots.") logging.info('Done!')
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 complete_irradiance(self, times=None, weather=None): """ Determine the missing irradiation columns. Only two of the following data columns (dni, ghi, dhi) are needed to calculate the missing data. This function is not safe at the moment. Results can be too high or negative. Please contribute and help to improve this function on https://github.com/pvlib/pvlib-python Parameters ---------- times : None or DatetimeIndex, default None Times at which to evaluate the model. Can be None if attribute `times` is already set. weather : None or pandas.DataFrame, default None Table with at least two columns containing one of the following data sets: dni, dhi, ghi. Can be None if attribute `weather` is already set. Returns ------- self Assigns attributes: times, weather Examples -------- This example does not work until the parameters `my_system`, `my_location`, `my_datetime` and `my_weather` are not defined properly but shows the basic idea how this method can be used. >>> from pvlib.modelchain import ModelChain >>> # my_weather containing 'dhi' and 'ghi'. >>> mc = ModelChain(my_system, my_location) # doctest: +SKIP >>> mc.complete_irradiance(my_datetime, my_weather) # doctest: +SKIP >>> mc.run_model() # doctest: +SKIP >>> # my_weather containing 'dhi', 'ghi' and 'dni'. >>> mc = ModelChain(my_system, my_location) # doctest: +SKIP >>> mc.run_model(my_datetime, my_weather) # doctest: +SKIP """ if weather is not None: self.weather = weather if times is not None: self.times = times self.solar_position = self.location.get_solarposition( self.times, method=self.solar_position_method) icolumns = set(self.weather.columns) wrn_txt = ("This function is not safe at the moment.\n" + "Results can be too high or negative.\n" + "Help to improve this function on github:\n" + "https://github.com/pvlib/pvlib-python \n") if {'ghi', 'dhi'} <= icolumns and 'dni' not in icolumns: clearsky = self.location.get_clearsky( times, solar_position=self.solar_position) self.weather.loc[:, 'dni'] = pvlib.irradiance.dni( self.weather.loc[:, 'ghi'], self.weather.loc[:, 'dhi'], self.solar_position.zenith, clearsky_dni=clearsky['dni'], clearsky_tolerance=1.1) elif {'dni', 'dhi'} <= icolumns and 'ghi' not in icolumns: warnings.warn(wrn_txt, UserWarning) self.weather.loc[:, 'ghi'] = ( self.weather.dni * tools.cosd(self.solar_position.zenith) + self.weather.dhi) elif {'dni', 'ghi'} <= icolumns and 'dhi' not in icolumns: warnings.warn(wrn_txt, UserWarning) self.weather.loc[:, 'dhi'] = ( self.weather.ghi - self.weather.dni * tools.cosd(self.solar_position.zenith)) return self
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 haydavies(surface_tilt, surface_azimuth, dhi, dni, dni_extra, solar_zenith=None, solar_azimuth=None, projection_ratio=None): r''' Determine diffuse irradiance from the sky on a tilted surface using Hay & Davies' 1980 model .. math:: I_{d} = DHI ( A R_b + (1 - A) (\frac{1 + \cos\beta}{2}) ) Hay and Davies' 1980 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, extraterrestrial irradiance, sun zenith angle, and sun azimuth angle. Parameters ---------- surface_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) surface_azimuth : 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. dni_extra : float or Series Extraterrestrial normal irradiance in W/m^2. solar_zenith : None, float or Series Solar apparent (refraction-corrected) zenith angles in decimal degrees. Must supply ``solar_zenith`` and ``solar_azimuth`` or supply ``projection_ratio``. solar_azimuth : None, float or Series Solar azimuth angles in decimal degrees. Must supply ``solar_zenith`` and ``solar_azimuth`` or supply ``projection_ratio``. projection_ratio : None, float or Series Ratio of angle of incidence projection to solar zenith angle projection. Must supply ``solar_zenith`` and ``solar_azimuth`` or supply ``projection_ratio``. Returns -------- sky_diffuse : 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]. 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] Hay, J.E., Davies, J.A., 1980. Calculations of the solar radiation incident on an inclined surface. In: Hay, J.E., Won, T.K. (Eds.), Proc. of First Canadian Solar Radiation Data Workshop, 59. Ministry of Supply and Services, Canada. ''' pvl_logger.debug('diffuse_sky.haydavies()') # if necessary, calculate ratio of titled and horizontal beam irradiance if projection_ratio is None: cos_tt = aoi_projection(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth) cos_sun_zen = tools.cosd(solar_zenith) Rb = cos_tt / cos_sun_zen else: Rb = projection_ratio # Anisotropy Index AI = dni / dni_extra # these are actually the () and [] sub-terms of the second term of eqn 7 term1 = 1 - AI term2 = 0.5 * (1 + tools.cosd(surface_tilt)) sky_diffuse = dhi * (AI * Rb + term1 * term2) sky_diffuse[sky_diffuse < 0] = 0 return sky_diffuse
def ineichen(apparent_zenith, airmass_absolute, linke_turbidity, altitude=0, dni_extra=1364.): ''' Determine clear sky GHI, DNI, and DHI from Ineichen/Perez model. Implements the Ineichen and Perez clear sky model for global horizontal irradiance (GHI), direct normal irradiance (DNI), and calculates the clear-sky diffuse horizontal (DHI) component as the difference between GHI and DNI*cos(zenith) as presented in [1, 2]. A report on clear sky models found the Ineichen/Perez model to have excellent performance with a minimal input data set [3]. Default values for monthly Linke turbidity provided by SoDa [4, 5]. Parameters ----------- apparent_zenith: numeric Refraction corrected solar zenith angle in degrees. airmass_absolute: numeric Pressure corrected airmass. linke_turbidity: numeric Linke Turbidity. altitude: numeric Altitude above sea level in meters. dni_extra: numeric Extraterrestrial irradiance. The units of ``dni_extra`` determine the units of the output. Returns ------- clearsky : DataFrame (if Series input) or OrderedDict of arrays DataFrame/OrderedDict contains the columns/keys ``'dhi', 'dni', 'ghi'``. See also -------- lookup_linke_turbidity pvlib.location.Location.get_clearsky References ---------- [1] P. Ineichen and R. Perez, "A New airmass independent formulation for the Linke turbidity coefficient", Solar Energy, vol 73, pp. 151-157, 2002. [2] R. Perez et. al., "A New Operational Model for Satellite-Derived Irradiances: Description and Validation", Solar Energy, vol 73, pp. 307-317, 2002. [3] M. Reno, C. Hansen, and J. Stein, "Global Horizontal Irradiance Clear Sky Models: Implementation and Analysis", Sandia National Laboratories, SAND2012-2389, 2012. [4] http://www.soda-is.com/eng/services/climat_free_eng.php#c5 (obtained July 17, 2012). [5] J. Remund, et. al., "Worldwide Linke Turbidity Information", Proc. ISES Solar World Congress, June 2003. Goteborg, Sweden. ''' # Dan's note on the TL correction: By my reading of the publication # on pages 151-157, Ineichen and Perez introduce (among other # things) three things. 1) Beam model in eqn. 8, 2) new turbidity # factor in eqn 9 and appendix A, and 3) Global horizontal model in # eqn. 11. They do NOT appear to use the new turbidity factor (item # 2 above) in either the beam or GHI models. The phrasing of # appendix A seems as if there are two separate corrections, the # first correction is used to correct the beam/GHI models, and the # second correction is used to correct the revised turibidity # factor. In my estimation, there is no need to correct the # turbidity factor used in the beam/GHI models. # Create the corrected TL for TL < 2 # TLcorr = TL; # TLcorr(TL < 2) = TLcorr(TL < 2) - 0.25 .* (2-TLcorr(TL < 2)) .^ (0.5); # This equation is found in Solar Energy 73, pg 311. Full ref: Perez # et. al., Vol. 73, pp. 307-317 (2002). It is slightly different # than the equation given in Solar Energy 73, pg 156. We used the # equation from pg 311 because of the existence of known typos in # the pg 156 publication (notably the fh2-(TL-1) should be fh2 * # (TL-1)). # The NaN handling is a little subtle. The AM input is likely to # have NaNs that we'll want to map to 0s in the output. However, we # want NaNs in other inputs to propagate through to the output. This # is accomplished by judicious use and placement of np.maximum, # np.minimum, and np.fmax # use max so that nighttime values will result in 0s instead of # negatives. propagates nans. cos_zenith = np.maximum(tools.cosd(apparent_zenith), 0) tl = linke_turbidity fh1 = np.exp(-altitude/8000.) fh2 = np.exp(-altitude/1250.) cg1 = 5.09e-05 * altitude + 0.868 cg2 = 3.92e-05 * altitude + 0.0387 ghi = (np.exp(-cg2*airmass_absolute*(fh1 + fh2*(tl - 1))) * np.exp(0.01*airmass_absolute**1.8)) # use fmax to map airmass nans to 0s. multiply and divide by tl to # reinsert tl nans ghi = cg1 * dni_extra * cos_zenith * tl / tl * np.fmax(ghi, 0) # BncI = "normal beam clear sky radiation" b = 0.664 + 0.163/fh1 bnci = b * np.exp(-0.09 * airmass_absolute * (tl - 1)) bnci = dni_extra * np.fmax(bnci, 0) # "empirical correction" SE 73, 157 & SE 73, 312. bnci_2 = ((1 - (0.1 - 0.2*np.exp(-tl))/(0.1 + 0.882/fh1)) / cos_zenith) bnci_2 = ghi * np.fmin(np.fmax(bnci_2, 0), 1e20) dni = np.minimum(bnci, bnci_2) dhi = ghi - dni*cos_zenith irrads = OrderedDict() irrads['ghi'] = ghi irrads['dni'] = dni irrads['dhi'] = dhi if isinstance(dni, pd.Series): irrads = pd.DataFrame.from_dict(irrads) return irrads
def ineichen( time, location, linke_turbidity=None, solarposition_method="pyephem", zenith_data=None, airmass_model="young1994", airmass_data=None, interp_turbidity=True, ): """ Determine clear sky GHI, DNI, and DHI from Ineichen/Perez model Implements the Ineichen and Perez clear sky model for global horizontal irradiance (GHI), direct normal irradiance (DNI), and calculates the clear-sky diffuse horizontal (DHI) component as the difference between GHI and DNI*cos(zenith) as presented in [1, 2]. A report on clear sky models found the Ineichen/Perez model to have excellent performance with a minimal input data set [3]. Default values for montly Linke turbidity provided by SoDa [4, 5]. Parameters ----------- time : pandas.DatetimeIndex location : pvlib.Location linke_turbidity : None or float If None, uses ``LinkeTurbidities.mat`` lookup table. solarposition_method : string Sets the solar position algorithm. See solarposition.get_solarposition() zenith_data : None or pandas.Series If None, ephemeris data will be calculated using ``solarposition_method``. airmass_model : string See pvlib.airmass.relativeairmass(). airmass_data : None or pandas.Series If None, absolute air mass data will be calculated using ``airmass_model`` and location.alitude. interp_turbidity : bool If ``True``, interpolates the monthly Linke turbidity values found in ``LinkeTurbidities.mat`` to daily values. Returns -------- DataFrame with the following columns: ``GHI, DNI, DHI``. Notes ----- If you are using this function in a loop, it may be faster to load LinkeTurbidities.mat outside of the loop and feed it in as a variable, rather than having the function open the file each time it is called. References ---------- [1] P. Ineichen and R. Perez, "A New airmass independent formulation for the Linke turbidity coefficient", Solar Energy, vol 73, pp. 151-157, 2002. [2] R. Perez et. al., "A New Operational Model for Satellite-Derived Irradiances: Description and Validation", Solar Energy, vol 73, pp. 307-317, 2002. [3] M. Reno, C. Hansen, and J. Stein, "Global Horizontal Irradiance Clear Sky Models: Implementation and Analysis", Sandia National Laboratories, SAND2012-2389, 2012. [4] http://www.soda-is.com/eng/services/climat_free_eng.php#c5 (obtained July 17, 2012). [5] J. Remund, et. al., "Worldwide Linke Turbidity Information", Proc. ISES Solar World Congress, June 2003. Goteborg, Sweden. """ # Initial implementation of this algorithm by Matthew Reno. # Ported to python by Rob Andrews # Added functionality by Will Holmgren I0 = irradiance.extraradiation(time.dayofyear) if zenith_data is None: ephem_data = solarposition.get_solarposition(time, location, method=solarposition_method) time = ephem_data.index # fixes issue with time possibly not being tz-aware try: ApparentZenith = ephem_data["apparent_zenith"] except KeyError: ApparentZenith = ephem_data["zenith"] logger.warning("could not find apparent_zenith. using zenith") else: ApparentZenith = zenith_data # ApparentZenith[ApparentZenith >= 90] = 90 # can cause problems in edge cases if linke_turbidity is None: # The .mat file 'LinkeTurbidities.mat' contains a single 2160 x 4320 x 12 # matrix of type uint8 called 'LinkeTurbidity'. The rows represent global # latitudes from 90 to -90 degrees; the columns represent global longitudes # from -180 to 180; and the depth (third dimension) represents months of # the year from January (1) to December (12). To determine the Linke # turbidity for a position on the Earth's surface for a given month do the # following: LT = LinkeTurbidity(LatitudeIndex, LongitudeIndex, month). # Note that the numbers within the matrix are 20 * Linke Turbidity, # so divide the number from the file by 20 to get the # turbidity. try: import scipy.io except ImportError: raise ImportError( "The Linke turbidity lookup table requires scipy. " + "You can still use clearsky.ineichen if you " + "supply your own turbidities." ) # consider putting this code at module level this_path = os.path.dirname(os.path.abspath(__file__)) logger.debug("this_path={}".format(this_path)) mat = scipy.io.loadmat(os.path.join(this_path, "data", "LinkeTurbidities.mat")) linke_turbidity = mat["LinkeTurbidity"] LatitudeIndex = np.round_(_linearly_scale(location.latitude, 90, -90, 1, 2160)) LongitudeIndex = np.round_(_linearly_scale(location.longitude, -180, 180, 1, 4320)) g = linke_turbidity[LatitudeIndex][LongitudeIndex] if interp_turbidity: logger.info("interpolating turbidity to the day") g2 = np.concatenate([[g[-1]], g, [g[0]]]) # wrap ends around days = np.linspace(-15, 380, num=14) # map day of year onto month (approximate) LT = pd.Series(np.interp(time.dayofyear, days, g2), index=time) else: logger.info("using monthly turbidity") ApplyMonth = lambda x: g[x[0] - 1] LT = pd.DataFrame(time.month, index=time) LT = LT.apply(ApplyMonth, axis=1) TL = LT / 20.0 logger.info("using TL=\n{}".format(TL)) else: TL = linke_turbidity # Get the absolute airmass assuming standard local pressure (per # alt2pres) using Kasten and Young's 1989 formula for airmass. if airmass_data is None: AMabsolute = atmosphere.absoluteairmass( AMrelative=atmosphere.relativeairmass(ApparentZenith, airmass_model), pressure=atmosphere.alt2pres(location.altitude), ) else: AMabsolute = airmass_data fh1 = np.exp(-location.altitude / 8000.0) fh2 = np.exp(-location.altitude / 1250.0) cg1 = 5.09e-05 * location.altitude + 0.868 cg2 = 3.92e-05 * location.altitude + 0.0387 logger.debug("fh1={}, fh2={}, cg1={}, cg2={}".format(fh1, fh2, cg1, cg2)) # Dan's note on the TL correction: By my reading of the publication on # pages 151-157, Ineichen and Perez introduce (among other things) three # things. 1) Beam model in eqn. 8, 2) new turbidity factor in eqn 9 and # appendix A, and 3) Global horizontal model in eqn. 11. They do NOT appear # to use the new turbidity factor (item 2 above) in either the beam or GHI # models. The phrasing of appendix A seems as if there are two separate # corrections, the first correction is used to correct the beam/GHI models, # and the second correction is used to correct the revised turibidity # factor. In my estimation, there is no need to correct the turbidity # factor used in the beam/GHI models. # Create the corrected TL for TL < 2 # TLcorr = TL; # TLcorr(TL < 2) = TLcorr(TL < 2) - 0.25 .* (2-TLcorr(TL < 2)) .^ (0.5); # This equation is found in Solar Energy 73, pg 311. # Full ref: Perez et. al., Vol. 73, pp. 307-317 (2002). # It is slightly different than the equation given in Solar Energy 73, pg 156. # We used the equation from pg 311 because of the existence of known typos # in the pg 156 publication (notably the fh2-(TL-1) should be fh2 * (TL-1)). cos_zenith = tools.cosd(ApparentZenith) clearsky_GHI = ( cg1 * I0 * cos_zenith * np.exp(-cg2 * AMabsolute * (fh1 + fh2 * (TL - 1))) * np.exp(0.01 * AMabsolute ** 1.8) ) clearsky_GHI[clearsky_GHI < 0] = 0 # BncI == "normal beam clear sky radiation" b = 0.664 + 0.163 / fh1 BncI = b * I0 * np.exp(-0.09 * AMabsolute * (TL - 1)) logger.debug("b={}".format(b)) # "empirical correction" SE 73, 157 & SE 73, 312. BncI_2 = clearsky_GHI * (1 - (0.1 - 0.2 * np.exp(-TL)) / (0.1 + 0.882 / fh1)) / cos_zenith # return BncI, BncI_2 clearsky_DNI = np.minimum(BncI, BncI_2) # Will H: use np.minimum explicitly clearsky_DHI = clearsky_GHI - clearsky_DNI * cos_zenith df_out = pd.DataFrame({"GHI": clearsky_GHI, "DNI": clearsky_DNI, "DHI": clearsky_DHI}) df_out.fillna(0, inplace=True) # df_out['BncI'] = BncI # df_out['BncI_2'] = BncI return df_out
def ineichen(time, latitude, longitude, altitude=0, linke_turbidity=None, solarposition_method='nrel_numpy', zenith_data=None, airmass_model='young1994', airmass_data=None, interp_turbidity=True): ''' Determine clear sky GHI, DNI, and DHI from Ineichen/Perez model Implements the Ineichen and Perez clear sky model for global horizontal irradiance (GHI), direct normal irradiance (DNI), and calculates the clear-sky diffuse horizontal (DHI) component as the difference between GHI and DNI*cos(zenith) as presented in [1, 2]. A report on clear sky models found the Ineichen/Perez model to have excellent performance with a minimal input data set [3]. Default values for montly Linke turbidity provided by SoDa [4, 5]. Parameters ----------- time : pandas.DatetimeIndex latitude : float longitude : float altitude : float linke_turbidity : None or float If None, uses ``LinkeTurbidities.mat`` lookup table. solarposition_method : string Sets the solar position algorithm. See solarposition.get_solarposition() zenith_data : None or Series If None, ephemeris data will be calculated using ``solarposition_method``. airmass_model : string See pvlib.airmass.relativeairmass(). airmass_data : None or Series If None, absolute air mass data will be calculated using ``airmass_model`` and location.alitude. interp_turbidity : bool If ``True``, interpolates the monthly Linke turbidity values found in ``LinkeTurbidities.mat`` to daily values. Returns -------- DataFrame with the following columns: ``ghi, dni, dhi``. Notes ----- If you are using this function in a loop, it may be faster to load LinkeTurbidities.mat outside of the loop and feed it in as a keyword argument, rather than having the function open and process the file each time it is called. References ---------- [1] P. Ineichen and R. Perez, "A New airmass independent formulation for the Linke turbidity coefficient", Solar Energy, vol 73, pp. 151-157, 2002. [2] R. Perez et. al., "A New Operational Model for Satellite-Derived Irradiances: Description and Validation", Solar Energy, vol 73, pp. 307-317, 2002. [3] M. Reno, C. Hansen, and J. Stein, "Global Horizontal Irradiance Clear Sky Models: Implementation and Analysis", Sandia National Laboratories, SAND2012-2389, 2012. [4] http://www.soda-is.com/eng/services/climat_free_eng.php#c5 (obtained July 17, 2012). [5] J. Remund, et. al., "Worldwide Linke Turbidity Information", Proc. ISES Solar World Congress, June 2003. Goteborg, Sweden. ''' # Initial implementation of this algorithm by Matthew Reno. # Ported to python by Rob Andrews # Added functionality by Will Holmgren (@wholmgren) I0 = irradiance.extraradiation(time.dayofyear) if zenith_data is None: ephem_data = solarposition.get_solarposition(time, latitude=latitude, longitude=longitude, altitude=altitude, method=solarposition_method) time = ephem_data.index # fixes issue with time possibly not being tz-aware try: ApparentZenith = ephem_data['apparent_zenith'] except KeyError: ApparentZenith = ephem_data['zenith'] logger.warning('could not find apparent_zenith. using zenith') else: ApparentZenith = zenith_data #ApparentZenith[ApparentZenith >= 90] = 90 # can cause problems in edge cases if linke_turbidity is None: TL = lookup_linke_turbidity(time, latitude, longitude, interp_turbidity=interp_turbidity) else: TL = linke_turbidity # Get the absolute airmass assuming standard local pressure (per # alt2pres) using Kasten and Young's 1989 formula for airmass. if airmass_data is None: AMabsolute = atmosphere.absoluteairmass(airmass_relative=atmosphere.relativeairmass(ApparentZenith, airmass_model), pressure=atmosphere.alt2pres(altitude)) else: AMabsolute = airmass_data fh1 = np.exp(-altitude/8000.) fh2 = np.exp(-altitude/1250.) cg1 = 5.09e-05 * altitude + 0.868 cg2 = 3.92e-05 * altitude + 0.0387 logger.debug('fh1=%s, fh2=%s, cg1=%s, cg2=%s', fh1, fh2, cg1, cg2) # Dan's note on the TL correction: By my reading of the publication on # pages 151-157, Ineichen and Perez introduce (among other things) three # things. 1) Beam model in eqn. 8, 2) new turbidity factor in eqn 9 and # appendix A, and 3) Global horizontal model in eqn. 11. They do NOT appear # to use the new turbidity factor (item 2 above) in either the beam or GHI # models. The phrasing of appendix A seems as if there are two separate # corrections, the first correction is used to correct the beam/GHI models, # and the second correction is used to correct the revised turibidity # factor. In my estimation, there is no need to correct the turbidity # factor used in the beam/GHI models. # Create the corrected TL for TL < 2 # TLcorr = TL; # TLcorr(TL < 2) = TLcorr(TL < 2) - 0.25 .* (2-TLcorr(TL < 2)) .^ (0.5); # This equation is found in Solar Energy 73, pg 311. # Full ref: Perez et. al., Vol. 73, pp. 307-317 (2002). # It is slightly different than the equation given in Solar Energy 73, pg 156. # We used the equation from pg 311 because of the existence of known typos # in the pg 156 publication (notably the fh2-(TL-1) should be fh2 * (TL-1)). cos_zenith = tools.cosd(ApparentZenith) clearsky_GHI = ( cg1 * I0 * cos_zenith * np.exp(-cg2*AMabsolute*(fh1 + fh2*(TL - 1))) * np.exp(0.01*AMabsolute**1.8) ) clearsky_GHI[clearsky_GHI < 0] = 0 # BncI == "normal beam clear sky radiation" b = 0.664 + 0.163/fh1 BncI = b * I0 * np.exp( -0.09 * AMabsolute * (TL - 1) ) logger.debug('b=%s', b) # "empirical correction" SE 73, 157 & SE 73, 312. BncI_2 = ( clearsky_GHI * ( 1 - (0.1 - 0.2*np.exp(-TL))/(0.1 + 0.882/fh1) ) / cos_zenith ) clearsky_DNI = np.minimum(BncI, BncI_2) clearsky_DHI = clearsky_GHI - clearsky_DNI*cos_zenith df_out = pd.DataFrame({'ghi':clearsky_GHI, 'dni':clearsky_DNI, 'dhi':clearsky_DHI}) df_out.fillna(0, inplace=True) return df_out
def dirint(ghi, zenith, times, pressure=101325, use_delta_kt_prime=True, temp_dew=None): """ Determine DNI from GHI using the DIRINT modification of the DISC model. Implements the modified DISC model known as "DIRINT" introduced in [1]. DIRINT predicts direct normal irradiance (DNI) from measured global horizontal irradiance (GHI). DIRINT improves upon the DISC model by using time-series GHI data and dew point temperature information. The effectiveness of the DIRINT model improves with each piece of information provided. Parameters ---------- ghi : pd.Series Global horizontal irradiance in W/m^2. zenith : pd.Series True (not refraction-corrected) zenith angles in decimal degrees. If Z is a vector it must be of the same size as all other vector inputs. Z must be >=0 and <=180. times : DatetimeIndex pressure : float or pd.Series The site pressure in Pascal. Pressure may be measured or an average pressure may be calculated from site altitude. use_delta_kt_prime : bool Indicates if the user would like to utilize the time-series nature of the GHI measurements. A value of ``False`` will not use the time-series improvements, any other numeric value will use time-series improvements. It is recommended that time-series data only be used if the time between measured data points is less than 1.5 hours. If none of the input arguments are vectors, then time-series improvements are not used (because it's not a time-series). temp_dew : None, float, or pd.Series Surface dew point temperatures, in degrees C. Values of temp_dew may be numeric or NaN. Any single time period point with a DewPtTemp=NaN does not have dew point improvements applied. If DewPtTemp is not provided, then dew point improvements are not applied. Returns ------- DNI : pd.Series. The modeled direct normal irradiance in W/m^2 provided by the DIRINT model. References ---------- [1] Perez, R., P. Ineichen, E. Maxwell, R. Seals and A. Zelenka, (1992). "Dynamic Global-to-Direct Irradiance Conversion Models". ASHRAE Transactions-Research Series, pp. 354-369 [2] Maxwell, E. L., "A Quasi-Physical Model for Converting Hourly Global Horizontal to Direct Normal Insolation", Technical Report No. SERI/TR-215-3087, Golden, CO: Solar Energy Research Institute, 1987. DIRINT model requires time series data (ie. one of the inputs must be a vector of length >2. """ logger.debug('clearsky.dirint') disc_out = disc(ghi, zenith, times) kt = disc_out['Kt_gen_DISC'] # Absolute Airmass, per the DISC model # Note that we calculate the AM pressure correction slightly differently # than Perez. He uses altitude, we use pressure (which we calculate # slightly differently) airmass = (1./(tools.cosd(zenith) + 0.15*((93.885-zenith)**(-1.253))) * pressure/101325) coeffs = _get_dirint_coeffs() kt_prime = kt / (1.031 * np.exp(-1.4/(0.9+9.4/airmass)) + 0.1) kt_prime[kt_prime > 0.82] = 0.82 # From SRRL code. consider np.NaN kt_prime.fillna(0, inplace=True) logger.debug('kt_prime:\n{}'.format(kt_prime)) # wholmgren: # the use_delta_kt_prime statement is a port of the MATLAB code. # I am confused by the abs() in the delta_kt_prime calculation. # It is not the absolute value of the central difference. if use_delta_kt_prime: delta_kt_prime = 0.5*( (kt_prime - kt_prime.shift(1)).abs() .add( (kt_prime - kt_prime.shift(-1)).abs(), fill_value=0)) else: delta_kt_prime = pd.Series(-1, index=times) if temp_dew is not None: w = pd.Series(np.exp(0.07 * temp_dew - 0.075), index=times) else: w = pd.Series(-1, index=times) # @wholmgren: the following bin assignments use MATLAB's 1-indexing. # Later, we'll subtract 1 to conform to Python's 0-indexing. # Create kt_prime bins kt_prime_bin = pd.Series(index=times) kt_prime_bin[(kt_prime>=0) & (kt_prime<0.24)] = 1 kt_prime_bin[(kt_prime>=0.24) & (kt_prime<0.4)] = 2 kt_prime_bin[(kt_prime>=0.4) & (kt_prime<0.56)] = 3 kt_prime_bin[(kt_prime>=0.56) & (kt_prime<0.7)] = 4 kt_prime_bin[(kt_prime>=0.7) & (kt_prime<0.8)] = 5 kt_prime_bin[(kt_prime>=0.8) & (kt_prime<=1)] = 6 logger.debug('kt_prime_bin:\n{}'.format(kt_prime_bin)) # Create zenith angle bins zenith_bin = pd.Series(index=times) zenith_bin[(zenith>=0) & (zenith<25)] = 1 zenith_bin[(zenith>=25) & (zenith<40)] = 2 zenith_bin[(zenith>=40) & (zenith<55)] = 3 zenith_bin[(zenith>=55) & (zenith<70)] = 4 zenith_bin[(zenith>=70) & (zenith<80)] = 5 zenith_bin[(zenith>=80)] = 6 logger.debug('zenith_bin:\n{}'.format(zenith_bin)) # Create the bins for w based on dew point temperature w_bin = pd.Series(index=times) w_bin[(w>=0) & (w<1)] = 1 w_bin[(w>=1) & (w<2)] = 2 w_bin[(w>=2) & (w<3)] = 3 w_bin[(w>=3)] = 4 w_bin[(w == -1)] = 5 logger.debug('w_bin:\n{}'.format(w_bin)) # Create delta_kt_prime binning. delta_kt_prime_bin = pd.Series(index=times) delta_kt_prime_bin[(delta_kt_prime>=0) & (delta_kt_prime<0.015)] = 1 delta_kt_prime_bin[(delta_kt_prime>=0.015) & (delta_kt_prime<0.035)] = 2 delta_kt_prime_bin[(delta_kt_prime>=0.035) & (delta_kt_prime<0.07)] = 3 delta_kt_prime_bin[(delta_kt_prime>=0.07) & (delta_kt_prime<0.15)] = 4 delta_kt_prime_bin[(delta_kt_prime>=0.15) & (delta_kt_prime<0.3)] = 5 delta_kt_prime_bin[(delta_kt_prime>=0.3) & (delta_kt_prime<=1)] = 6 delta_kt_prime_bin[delta_kt_prime == -1] = 7 logger.debug('delta_kt_prime_bin:\n{}'.format(delta_kt_prime_bin)) # subtract 1 to account for difference between MATLAB-style bin # assignment and Python-style array lookup. dirint_coeffs = coeffs[kt_prime_bin-1, zenith_bin-1, delta_kt_prime_bin-1, w_bin-1] dni = disc_out['DNI_gen_DISC'] * dirint_coeffs dni.name = 'DNI_DIRINT' return dni
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 haydavies(surf_tilt, surf_az, DHI, DNI, DNI_ET, sun_zen, sun_az): r''' Determine diffuse irradiance from the sky on a tilted surface using Hay & Davies' 1980 model .. math:: I_{d} = DHI ( A R_b + (1 - A) (\frac{1 + \cos\beta}{2}) ) Hay and Davies' 1980 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, 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. 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 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] Hay, J.E., Davies, J.A., 1980. Calculations of the solar radiation incident on an inclined surface. In: Hay, J.E., Won, T.K. (Eds.), Proc. of First Canadian Solar Radiation Data Workshop, 59. Ministry of Supply and Services, Canada. ''' pvl_logger.debug('diffuse_sky.haydavies()') 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 # these are actually the () and [] sub-terms of the second term of eqn 7 term1 = 1 - AI term2 = 0.5 * (1 + tools.cosd(surf_tilt)) sky_diffuse = DHI * (AI * Rb + term1 * term2) sky_diffuse[sky_diffuse < 0] = 0 return sky_diffuse