Source code for pvfactors.viewfactors.aoimethods

"""Module containing AOI loss calculation methods"""

from pvfactors.config import DISTANCE_TOLERANCE
from pvfactors.geometry.timeseries import (
    TsPointCoords, TsSurface, TsLineCoords)
from pvfactors import PVFactorsError
import pvlib
from pvlib.tools import cosd
import numpy as np


[docs]class AOIMethods: """Class containing methods related to calculating AOI losses for :py:class:`~pvfactors.geometry.pvarray.OrderedPVArray` objects."""
[docs] def __init__(self, faoi_fn_front, faoi_fn_back, n_integral_sections=300): """Instantiate class with faoi function and number of sections to use to calculate integrals of view factors with faoi losses Parameters ---------- faoi_fn_front : function Function which takes a list (or numpy array) of incidence angles measured from the surface horizontal (with values from 0 to 180 deg) and returns the fAOI values for the front side of PV rows faoi_fn_back : function Function which takes a list (or numpy array) of incidence angles measured from the surface horizontal (with values from 0 to 180 deg) and returns the fAOI values for the back side of PV rows n_integral_sections : int, optional Number of integral divisions of the 0 to 180 deg interval to use for the fAOI loss integral (default = 300) """ # Check that faoi fn where passed faoi_fns_ok = callable(faoi_fn_front) and callable(faoi_fn_back) if not faoi_fns_ok: raise PVFactorsError("The faoi_fn passed to the AOI methods are " "not callable. Please check the fAOI " "functions again") self.faoi_fn_front = faoi_fn_front self.faoi_fn_back = faoi_fn_back self.n_integral_sections = n_integral_sections # The following will be updated at fitting time self.interval = None self.aoi_angles_low = None self.aoi_angles_high = None self.integrand_front = None self.integrand_back = None
def fit(self, n_timestamps): """Fit the AOI methods to timeseries inputs: create all the necessary integration attributes. Parameters ---------- n_timestamps : int Number of simulation timestamps """ # Will use x values at the middle of the integral sections aoi_angles = np.linspace(0., 180., num=self.n_integral_sections + 1) # Assumes that at least 2 aoi angle values, otherwise what's the point self.interval = aoi_angles[1] - aoi_angles[0] # Get integral intervals' low, high, and middle points aoi_angles_low = aoi_angles[:-1] aoi_angles_high = aoi_angles_low + self.interval aoi_angles_middle = aoi_angles_low + self.interval / 2. # Calculate faoi values using middle points of integral intervals faoi_front = self.faoi_fn_front(aoi_angles_middle) faoi_back = self.faoi_fn_back(aoi_angles_middle) # Calculate small view factor values for each section vf_values = self._vf(aoi_angles_low, aoi_angles_high) # Multiply to get integrand integrand_front = faoi_front * vf_values integrand_back = faoi_back * vf_values # Replicate these values for all timestamps such that shapes # becomes: [n_timestamps, n_integral_sections]map self.aoi_angles_low = np.tile(aoi_angles_low, (n_timestamps, 1)) self.aoi_angles_high = np.tile(aoi_angles_high, (n_timestamps, 1)) self.integrand_front = np.tile(integrand_front, (n_timestamps, 1)) self.integrand_back = np.tile(integrand_back, (n_timestamps, 1)) def vf_aoi_pvrow_to_sky(self, ts_pvrows, ts_ground, tilted_to_left, vf_matrix): """Calculate the view factors between timeseries PV row surface and sky while accounting for AOI losses, and assign values to the passed view factor matrix using the surface indices. Parameters ---------- ts_pvrows : list of :py:class:`~pvfactors.geometry.timeseries.TsPVRow` List of timeseries PV rows in the PV array ts_ground : :py:class:`~pvfactors.geometry.timeseries.TsGround` Timeseries ground of the PV array tilted_to_left : list of bool Flags indicating when the PV rows are strictly tilted to the left vf_matrix : np.ndarray View factor matrix to update during calculation. Should have 3 dimensions as follows: [n_surfaces, n_surfaces, n_timesteps] """ sky_index = vf_matrix.shape[0] - 1 # --- Build list of dummy sky surfaces # create sky left open area pt_1 = TsPointCoords(ts_ground.x_min * np.ones_like(tilted_to_left), ts_ground.y_ground * np.ones_like(tilted_to_left)) pt_2 = ts_pvrows[0].highest_point sky_left = TsSurface(TsLineCoords(pt_1, pt_2)) # create sky right open area pt_1 = TsPointCoords(ts_ground.x_max * np.ones_like(tilted_to_left), ts_ground.y_ground * np.ones_like(tilted_to_left)) pt_2 = ts_pvrows[-1].highest_point sky_right = TsSurface(TsLineCoords(pt_2, pt_1)) # Add sky surfaces in-between PV rows dummy_sky_surfaces = [sky_left] for idx_pvrow, ts_pvrow in enumerate(ts_pvrows[:-1]): right_ts_pvrow = ts_pvrows[idx_pvrow + 1] pt_1 = ts_pvrow.highest_point pt_2 = right_ts_pvrow.highest_point sky_surface = TsSurface(TsLineCoords(pt_1, pt_2)) dummy_sky_surfaces.append(sky_surface) # Add sky right open area dummy_sky_surfaces.append(sky_right) # Now calculate vf_aoi for all PV row surfaces to sky for idx_pvrow, ts_pvrow in enumerate(ts_pvrows): # Get dummy sky surfaces sky_left = dummy_sky_surfaces[idx_pvrow] sky_right = dummy_sky_surfaces[idx_pvrow + 1] # Calculate vf_aoi for surfaces in PV row # front side front = ts_pvrow.front for front_surf in front.all_ts_surfaces: vf_aoi_left = self._vf_aoi_surface_to_surface( front_surf, sky_left, is_back=False) vf_aoi_right = self._vf_aoi_surface_to_surface( front_surf, sky_right, is_back=False) vf_aoi = np.where(tilted_to_left, vf_aoi_left, vf_aoi_right) vf_matrix[front_surf.index, sky_index, :] = vf_aoi # back side back = ts_pvrow.back for back_surf in back.all_ts_surfaces: vf_aoi_left = self._vf_aoi_surface_to_surface( back_surf, sky_left, is_back=True) vf_aoi_right = self._vf_aoi_surface_to_surface( back_surf, sky_right, is_back=True) vf_aoi = np.where(tilted_to_left, vf_aoi_right, vf_aoi_left) vf_matrix[back_surf.index, sky_index, :] = vf_aoi def vf_aoi_pvrow_to_pvrow(self, ts_pvrows, tilted_to_left, vf_matrix): """Calculate the view factors between timeseries PV row surfaces while accounting for AOI losses, and assign values to the passed view factor matrix using the surface indices. Parameters ---------- ts_pvrows : list of :py:class:`~pvfactors.geometry.timeseries.TsPVRow` List of timeseries PV rows in the PV array tilted_to_left : list of bool Flags indicating when the PV rows are strictly tilted to the left vf_matrix : np.ndarray View factor matrix to update during calculation. Should have 3 dimensions as follows: [n_surfaces, n_surfaces, n_timesteps] """ for idx_pvrow, ts_pvrow in enumerate(ts_pvrows[:-1]): # Get the next pv row right_ts_pvrow = ts_pvrows[idx_pvrow + 1] # front side front = ts_pvrow.front for surf_i in front.all_ts_surfaces: i = surf_i.index for surf_j in right_ts_pvrow.back.all_ts_surfaces: j = surf_j.index # vf aoi from i to j vf_i_to_j = self._vf_aoi_surface_to_surface( surf_i, surf_j, is_back=False) vf_i_to_j = np.where(tilted_to_left, 0., vf_i_to_j) # vf aoi from j to i vf_j_to_i = self._vf_aoi_surface_to_surface( surf_j, surf_i, is_back=True) vf_j_to_i = np.where(tilted_to_left, 0., vf_j_to_i) # save results vf_matrix[i, j, :] = vf_i_to_j vf_matrix[j, i, :] = vf_j_to_i # back side back = ts_pvrow.back for surf_i in back.all_ts_surfaces: i = surf_i.index for surf_j in right_ts_pvrow.front.all_ts_surfaces: j = surf_j.index # vf aoi from i to j vf_i_to_j = self._vf_aoi_surface_to_surface( surf_i, surf_j, is_back=True) vf_i_to_j = np.where(tilted_to_left, vf_i_to_j, 0.) # vf aoi from j to i vf_j_to_i = self._vf_aoi_surface_to_surface( surf_j, surf_i, is_back=False) vf_j_to_i = np.where(tilted_to_left, vf_j_to_i, 0.) # save results vf_matrix[i, j, :] = vf_i_to_j vf_matrix[j, i, :] = vf_j_to_i def vf_aoi_pvrow_to_gnd(self, ts_pvrows, ts_ground, tilted_to_left, vf_aoi_matrix): """Calculate the view factors between timeseries PV row and ground surfaces while accounting for non-diffuse AOI losses, and assign it to the passed view factor aoi matrix using the surface indices. Notes ----- This assumes that the PV row surfaces are infinitesimal (very small) Parameters ---------- ts_pvrows : list of :py:class:`~pvfactors.geometry.timeseries.TsPVRow` List of timeseries PV rows in the PV array ts_ground : :py:class:`~pvfactors.geometry.timeseries.TsGround` Timeseries ground of the PV array tilted_to_left : list of bool Flags indicating when the PV rows are strictly tilted to the left vf_aoi_matrix : np.ndarray View factor aoi matrix to update during calculation. Should have 3 dimensions as follows: [n_surfaces, n_surfaces, n_timesteps] """ n_pvrows = len(ts_pvrows) for idx_pvrow, ts_pvrow in enumerate(ts_pvrows): # Separate gnd surfaces depending on side left_gnd_surfaces = ts_ground.ts_surfaces_side_of_cut_point( 'left', idx_pvrow) right_gnd_surfaces = ts_ground.ts_surfaces_side_of_cut_point( 'right', idx_pvrow) # Front side front = ts_pvrow.front for pvrow_surf in front.all_ts_surfaces: ts_length = pvrow_surf.length i = pvrow_surf.index for gnd_surf in left_gnd_surfaces: j = gnd_surf.index vf_pvrow_to_gnd = ( self._vf_aoi_pvrow_surf_to_gnd_surf_obstruction( pvrow_surf, idx_pvrow, n_pvrows, tilted_to_left, ts_pvrows, gnd_surf, ts_length, is_back=False, is_left=True)) vf_aoi_matrix[i, j, :] = vf_pvrow_to_gnd for gnd_surf in right_gnd_surfaces: j = gnd_surf.index vf_pvrow_to_gnd = ( self._vf_aoi_pvrow_surf_to_gnd_surf_obstruction( pvrow_surf, idx_pvrow, n_pvrows, tilted_to_left, ts_pvrows, gnd_surf, ts_length, is_back=False, is_left=False)) vf_aoi_matrix[i, j, :] = vf_pvrow_to_gnd # Back side back = ts_pvrow.back for pvrow_surf in back.all_ts_surfaces: ts_length = pvrow_surf.length i = pvrow_surf.index for gnd_surf in left_gnd_surfaces: j = gnd_surf.index vf_pvrow_to_gnd = ( self._vf_aoi_pvrow_surf_to_gnd_surf_obstruction( pvrow_surf, idx_pvrow, n_pvrows, tilted_to_left, ts_pvrows, gnd_surf, ts_length, is_back=True, is_left=True)) vf_aoi_matrix[i, j, :] = vf_pvrow_to_gnd for gnd_surf in right_gnd_surfaces: j = gnd_surf.index vf_pvrow_to_gnd = ( self._vf_aoi_pvrow_surf_to_gnd_surf_obstruction( pvrow_surf, idx_pvrow, n_pvrows, tilted_to_left, ts_pvrows, gnd_surf, ts_length, is_back=True, is_left=False)) vf_aoi_matrix[i, j, :] = vf_pvrow_to_gnd def _vf_aoi_surface_to_surface(self, surf_1, surf_2, is_back=True): """Calculate view factor, while accounting from AOI losses, from surface 1 to surface 2. Notes ----- This assumes that surf_1 is infinitesimal (very small) Parameters ---------- surf_1 : :py:class:`~pvfactors.geometry.timeseries.TsSurface` Infinitesimal surface from which to calculate view factor with AOI losses surf_2 : :py:class:`~pvfactors.geometry.timeseries.TsSurface` Surface to which the view factor with AOI losses should be calculated is_back : bool Flag specifying whether pv row surface is on back or front side of PV row (Default = True) Returns ------- vf_aoi : np.ndarray View factors with aoi losses from surface 1 to surface 2, dimension is [n_timesteps] """ # skip calculation if either surface is empty (always zero length) skip = surf_1.is_empty or surf_2.is_empty if skip: vf_aoi = np.zeros_like(surf_2.length) else: # Get surface 1 params u_vector = surf_1.u_vector centroid = surf_1.centroid # Calculate AOI angles aoi_angles_1 = self._calculate_aoi_angles(u_vector, centroid, surf_2.b1) aoi_angles_2 = self._calculate_aoi_angles(u_vector, centroid, surf_2.b2) low_aoi_angles = np.where(aoi_angles_1 < aoi_angles_2, aoi_angles_1, aoi_angles_2) high_aoi_angles = np.where(aoi_angles_1 < aoi_angles_2, aoi_angles_2, aoi_angles_1) # Calculate vf_aoi vf_aoi_raw = self._calculate_vf_aoi_wedge_level( low_aoi_angles, high_aoi_angles, is_back=is_back) # Should be zero where either of the surfaces have zero length vf_aoi = np.where((surf_1.length < DISTANCE_TOLERANCE) | (surf_2.length < DISTANCE_TOLERANCE), 0., vf_aoi_raw) return vf_aoi def _vf_aoi_pvrow_surf_to_gnd_surf_obstruction( self, pvrow_surf, pvrow_idx, n_pvrows, tilted_to_left, ts_pvrows, gnd_surf, ts_length, is_back=True, is_left=True): """Calculate view factors from timeseries PV row surface to a timeseries ground surface, accounting for AOI losses. This will return the calculated view factors from the PV row surface to the ground surface. Notes ----- This assumes that the PV row surfaces are infinitesimal (very small) Parameters ---------- pvrow_surf : :py:class:`~pvfactors.geometry.timeseries.TsSurface` Timeseries PV row surface to use for calculation pvrow_idx : int Index of the timeseries PV row on the which the pvrow_surf is n_pvrows : int Number of timeseries PV rows in the PV array, and therefore number of shadows they cast on the ground tilted_to_left : list of bool Flags indicating when the PV rows are strictly tilted to the left ts_pvrows : list of :py:class:`~pvfactors.geometry.timeseries.TsPVRow` List of timeseries PV rows in the PV array gnd_surf : :py:class:`~pvfactors.geometry.timeseries.TsSurface` Timeseries ground surface to use for calculation pvrow_surf_length : np.ndarray Length (width) of the timeseries PV row surface [m] is_back : bool Flag specifying whether pv row surface is on back or front side of PV row (Default = True) is_left : bool Flag specifying whether gnd surface is left of pv row cut point or not (Default = True) Returns ------- vf_aoi_pvrow_to_gnd_surf : np.ndarray View factors aoi from timeseries PV row surface to timeseries ground surface, dimension is [n_timesteps] """ # skip calculation if either surface is empty (always zero length) skip = pvrow_surf.is_empty or gnd_surf.is_empty if skip: vf_aoi = np.zeros_like(gnd_surf.length) else: centroid = pvrow_surf.centroid u_vector = pvrow_surf.u_vector no_obstruction = (is_left & (pvrow_idx == 0)) \ or ((not is_left) & (pvrow_idx == n_pvrows - 1)) if no_obstruction: # There is no obstruction to the ground surface aoi_angles_1 = self._calculate_aoi_angles(u_vector, centroid, gnd_surf.b1) aoi_angles_2 = self._calculate_aoi_angles(u_vector, centroid, gnd_surf.b2) else: # Get lowest point of obstructing point idx_obstructing_pvrow = (pvrow_idx - 1 if is_left else pvrow_idx + 1) pt_obstr = ts_pvrows[idx_obstructing_pvrow ].full_pvrow_coords.lowest_point # Adjust angle seen when there is obstruction aoi_angles_1 = self._calculate_aoi_angles_w_obstruction( u_vector, centroid, gnd_surf.b1, pt_obstr, is_left) aoi_angles_2 = self._calculate_aoi_angles_w_obstruction( u_vector, centroid, gnd_surf.b2, pt_obstr, is_left) low_aoi_angles = np.where(aoi_angles_1 < aoi_angles_2, aoi_angles_1, aoi_angles_2) high_aoi_angles = np.where(aoi_angles_1 < aoi_angles_2, aoi_angles_2, aoi_angles_1) vf_aoi_raw = self._calculate_vf_aoi_wedge_level( low_aoi_angles, high_aoi_angles, is_back=is_back) # Should be zero where either of the surfaces have zero length vf_aoi_raw = np.where((ts_length < DISTANCE_TOLERANCE) | (gnd_surf.length < DISTANCE_TOLERANCE), 0., vf_aoi_raw) # Final result depends on whether front or back surface if is_left: vf_aoi = (np.where(tilted_to_left, 0., vf_aoi_raw) if is_back else np.where(tilted_to_left, vf_aoi_raw, 0.)) else: vf_aoi = (np.where(tilted_to_left, vf_aoi_raw, 0.) if is_back else np.where(tilted_to_left, 0., vf_aoi_raw)) return vf_aoi def _calculate_aoi_angles_w_obstruction( self, u_vector, centroid, point_gnd, point_obstr, gnd_surf_is_left): """Calculate AOI angles for a PV row surface of the :py:class:`~pvfactors.geometry.pvarray.OrderedPVArray` that sees a ground surface, while being potentially obstructed by another PV row Parameters ---------- u_vector : np.ndarray Direction vector of the surface for which to calculate AOI angles centroid : :py:class:`~pvfactors.geometry.timeseries.TsPointCoords` Centroid point of PV row surface for which to calculate AOI angles point : :py:class:`~pvfactors.geometry.timeseries.TsPointCoords` Point of ground surface that will determine AOI angle point_obstr: :py:class:`~pvfactors.geometry.timeseries.TsPointCoords` Potentially obstructing point for the view aoi angle calculation gnd_surf_is_left : bool Flag specifying whether ground surface is left of PV row's cut point or not Returns ------- np.ndarray AOI angles formed by remote point and centroid on surface, measured against surface direction vector, accounting for potential obstruction [degrees] """ if point_obstr is None: # There is no obstruction point = point_gnd else: # Determine if there is obstruction by using the angles made by # specific strings with the x-axis alpha_pv = self._angle_with_x_axis(point_gnd, centroid) alpha_ob = self._angle_with_x_axis(point_gnd, point_obstr) if gnd_surf_is_left: is_obstructing = alpha_pv > alpha_ob else: is_obstructing = alpha_pv < alpha_ob x = np.where(is_obstructing, point_obstr.x, point_gnd.x) y = np.where(is_obstructing, point_obstr.y, point_gnd.y) point = TsPointCoords(x, y) aoi_angles = self._calculate_aoi_angles(u_vector, centroid, point) return aoi_angles def _calculate_vf_aoi_wedge_level(self, low_angles, high_angles, is_back=True): """Calculate faoi modified view factors for a wedge defined by low and high angles. Parameters ---------- low_angles : np.ndarray Low AOI angles (between 0 and 180 deg), length = n_timestamps high_angles : np.ndarray High AOI angles (between 0 and 180 deg), length = n_timestamps. Should be bigger than ``low_angles`` is_back : bool Flag specifying whether pv row surface is on back or front side of PV row (Default = True) Returns ------- np.ndarray faoi modified view factors for wedge shape = (n_timestamps, ) """ # Calculate integrand: all d_vf_aoi values faoi_integrand = self._calculate_vfaoi_integrand( low_angles, high_angles, is_back=is_back) # Total vf_aoi will be sum of all smaller d_vf_aoi values total_vf_aoi = faoi_integrand.sum(axis=1) # Make sure vf is counted as zero if the wedge is super small total_vf_aoi = np.where( np.abs(high_angles - low_angles) < DISTANCE_TOLERANCE, 0., total_vf_aoi) return total_vf_aoi def _calculate_vfaoi_integrand(self, low_angles, high_angles, is_back=True): """ Calculate the timeseries view factors with aoi loss integrand given the low and high angles that define the surface. Parameters ---------- low_angles : np.ndarray Low AOI angles (between 0 and 180 deg), length = n_timestamps high_angles : np.ndarray High AOI angles (between 0 and 180 deg), length = n_timestamps. Should be bigger than ``low_angles`` is_back : bool Flag specifying whether pv row surface is on back or front side of PV row (Default = True) Returns ------- np.ndarray vf_aoi integrand values for all timestamps shape = (n_timestamps, n_integral_sections) """ # Turn into dimension: [n_timestamps, n_integral_sections] low_angles_mat = np.tile(low_angles, (self.n_integral_sections, 1)).T high_angles_mat = np.tile(high_angles, (self.n_integral_sections, 1)).T # Filter out integrand values outside of range include_integral_section = ((low_angles_mat <= self.aoi_angles_high) & (high_angles_mat > self.aoi_angles_low)) # The integrand values are different for front and back sides if is_back: faoi_integrand = np.where(include_integral_section, self.integrand_back, 0.) else: faoi_integrand = np.where(include_integral_section, self.integrand_front, 0.) return faoi_integrand @staticmethod def _calculate_aoi_angles(u_vector, centroid, point): """Calculate AOI angles from direction vector of surface, centroid point of that surface, and point from another surface Parameters ---------- u_vector : np.ndarray Direction vector of the surface for which to calculate AOI angles centroid : :py:class:`~pvfactors.geometry.timeseries.TsPointCoords` Centroid point of surface for which to calculate AOI angles point : :py:class:`~pvfactors.geometry.timeseries.TsPointCoords` Point of remote surface that will determine AOI angle Returns ------- np.ndarray AOI angles formed by remote point and centroid on surface, measured against surface direction vector [degrees] """ v_vector = np.array([point.x - centroid.x, point.y - centroid.y]) dot_product = u_vector[0, :] * v_vector[0, :] \ + u_vector[1, :] * v_vector[1, :] u_norm = np.linalg.norm(u_vector, axis=0) v_norm = np.linalg.norm(v_vector, axis=0) cos_theta = dot_product / (u_norm * v_norm) # because of round off errors, cos_theta can be slightly > 1, # or slightly < -1, so clip it cos_theta = np.clip(cos_theta, -1., 1.) aoi_angles = np.rad2deg(np.arccos(cos_theta)) return aoi_angles @staticmethod 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)) @staticmethod def _angle_with_x_axis(pt_1, pt_2): """Angle with x-axis of vector going from pt_1 to pt_2 Parameters ---------- pt_1 : :py:class:`~pvfactors.geometry.timeseries.TsPointCoords` Timeseries point coordinates of point 1 pt_2 : :py:class:`~pvfactors.geometry.timeseries.TsPointCoords` Timeseries point coordinates of point 2 Returns ------- np.ndarray Angle between vector pt_1->pt_2 and x-axis """ return np.arctan2(pt_2.y - pt_1.y, pt_2.x - pt_1.x) def rho_from_faoi_fn(self, is_back): """Calculate global average reflectivity from faoi function for either side of the PV row (requires calculating view factors) Parameters ---------- is_back : bool Flag specifying whether to use front or back faoi function Returns ------- rho_average : float Global average reflectivity value of surface """ # Will use x values at the middle of the integral sections aoi_angles = np.linspace(0., 180., num=self.n_integral_sections + 1) # Assumes that at least 2 aoi angle values, otherwise what's the point self.interval = aoi_angles[1] - aoi_angles[0] # Get integral intervals' low, high, and middle points aoi_angles_low = aoi_angles[:-1] aoi_angles_high = aoi_angles_low + self.interval aoi_angles_middle = aoi_angles_low + self.interval / 2. # Calculate faoi values using middle points of integral intervals if is_back: faoi_values = self.faoi_fn_back(aoi_angles_middle) else: faoi_values = self.faoi_fn_front(aoi_angles_middle) # Calculate small view factor values for each section vf_values = self._vf(aoi_angles_low, aoi_angles_high) # Multiply to get integrand integrand_values = faoi_values * vf_values return (1. - integrand_values.sum())
def faoi_fn_from_pvlib_sandia(pvmodule_name): """Generate a faoi function from a pvlib sandia PV module name Parameters ---------- pvmodule_name : str Name of PV module in pvlib Sandia module database Returns ------- faoi_function Function that returns positive loss values for numeric inputs between 0 and 180 degrees. """ # Get Sandia module database from pvlib sandia_modules = pvlib.pvsystem.retrieve_sam('SandiaMod') # Grab pv module sandia coeffs from database pvmodule = sandia_modules[pvmodule_name] def fn(angles): """fAOI loss funtion: calculate how much light is absorbed at given incidence angles Parameters ---------- angles : np.ndarray or list Angles measured from surface horizontal, from 0 de 180 deg Returns ------- np.ndarray fAOI values """ angles = np.array(angles) if isinstance(angles, list) else angles # Transform the inputs for the SAPM function angles = np.where(angles >= 90, angles - 90, 90. - angles) # Use pvlib sapm aoi loss method return pvlib.iam.sapm(angles, pvmodule, upper=1.) return fn