Source code for pvfactors.irradiance.models

"""Module containing irradiance models used with pv array geometries"""

from pvlib.tools import cosd
from pvlib.irradiance import aoi as aoi_function
from pvlib.irradiance import get_total_irradiance
import numpy as np
from pvfactors.irradiance.utils import \
    perez_diffuse_luminance, calculate_horizon_band_shading, \
    calculate_circumsolar_shading
from pvfactors.irradiance.base import BaseModel
from pvfactors.config import \
    DEFAULT_HORIZON_BAND_ANGLE, SKY_REFLECTIVITY_DUMMY, \
    DEFAULT_CIRCUMSOLAR_ANGLE


[docs]class IsotropicOrdered(BaseModel): """Diffuse isotropic sky model for :py:class:`~pvfactors.geometry.pvarray.OrderedPVArray`. It will calculate the appropriate values for an isotropic sky dome and apply it to the PV array.""" params = ['rho', 'inv_rho', 'direct', 'isotropic', 'reflection'] cats = ['ground_illum', 'ground_shaded', 'front_illum_pvrow', 'back_illum_pvrow', 'front_shaded_pvrow', 'back_shaded_pvrow'] irradiance_comp = ['direct'] irradiance_comp_absorbed = ['direct_absorbed']
[docs] def __init__(self, rho_front=0.01, rho_back=0.03, module_transparency=0., module_spacing_ratio=0., faoi_fn_front=None, faoi_fn_back=None): """Initialize irradiance model values that will be saved later on. Parameters ---------- rho_front : float, optional Reflectivity of the front side of the PV rows (default = 0.01) rho_back : float, optional Reflectivity of the back side of the PV rows (default = 0.03) module_transparency : float, optional Module transparency (from 0 to 1), which will let some direct light pass through the PV modules in the PV rows and reach the shaded ground (Default = 0., fully opaque) module_spacing_ratio : float, optional Module spacing ratio (from 0 to 1), which is the ratio of the area covered by the space between PV modules over the total area of the PV rows, and which determines how much direct light will reach the shaded ground through the PV rows (Default = 0., no spacing at all) faoi_fn_front : function or object, optional Function (or object containing ``faoi`` method) 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 (default = None) faoi_fn_back : function or object, optional Function (or object containing ``faoi`` method) 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 (default = None) """ self.direct = dict.fromkeys(self.cats) self.total_perez = dict.fromkeys(self.cats) self.faoi_front = dict.fromkeys(self.irradiance_comp) self.faoi_back = dict.fromkeys(self.irradiance_comp) self.faoi_ground = None self.module_transparency = module_transparency self.module_spacing_ratio = module_spacing_ratio self.rho_front = rho_front self.rho_back = rho_back # Treatment of faoi functions faoi_fn_front = (faoi_fn_front.faoi if hasattr(faoi_fn_front, 'faoi') else faoi_fn_front) faoi_fn_back = (faoi_fn_back.faoi if hasattr(faoi_fn_back, 'faoi') else faoi_fn_back) self.faoi_fn_front = faoi_fn_front self.faoi_fn_back = faoi_fn_back # The following will be updated at fitting time self.albedo = None self.GHI = None self.DHI = None self.isotropic_luminance = None
def fit(self, timestamps, DNI, DHI, solar_zenith, solar_azimuth, surface_tilt, surface_azimuth, albedo, ghi=None): """Use vectorization to calculate values used for the isotropic irradiance model. Parameters ---------- timestamps : array-like List of timestamps of the simulation. DNI : array-like Direct normal irradiance values [W/m2] DHI : array-like Diffuse horizontal irradiance values [W/m2] solar_zenith : array-like Solar zenith angles [deg] solar_azimuth : array-like Solar azimuth angles [deg] surface_tilt : array-like Surface tilt angles, from 0 to 180 [deg] surface_azimuth : array-like Surface azimuth angles [deg] albedo : array-like Albedo values (or ground reflectivity) ghi : array-like, optional Global horizontal irradiance [W/m2], if not provided, will be calculated from DNI and DHI (Default = None) """ # Make sure getting array-like values if np.isscalar(DNI): timestamps = [timestamps] DNI = np.array([DNI]) DHI = np.array([DHI]) solar_zenith = np.array([solar_zenith]) solar_azimuth = np.array([solar_azimuth]) surface_tilt = np.array([surface_tilt]) surface_azimuth = np.array([surface_azimuth]) ghi = None if ghi is None else np.array([ghi]) # Length of arrays n = len(DNI) # Make sure that albedo is a vector albedo = albedo * np.ones(n) if np.isscalar(albedo) else albedo # Save and calculate total POA values from Perez model ghi = DNI * cosd(solar_zenith) + DHI if ghi is None else ghi self.GHI = ghi self.DHI = DHI self.n_steps = n perez_front_pvrow = get_total_irradiance( surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, DNI, ghi, DHI, albedo=albedo) # Save diffuse light self.isotropic_luminance = DHI self.albedo = albedo # DNI seen by ground illuminated surfaces self.direct['ground_illum'] = DNI * cosd(solar_zenith) self.direct['ground_shaded'] = ( # Direct light through PV modules spacing self.direct['ground_illum'] * self.module_spacing_ratio # Direct light through PV modules, by transparency + self.direct['ground_illum'] * (1. - self.module_spacing_ratio) * self.module_transparency) # Calculate AOI on front pvrow using pvlib implementation aoi_front_pvrow = aoi_function( surface_tilt, surface_azimuth, solar_zenith, solar_azimuth) aoi_back_pvrow = 180. - aoi_front_pvrow # calculate faoi modifiers self._calculate_faoi_modifiers(aoi_front_pvrow, aoi_back_pvrow, surface_tilt, albedo) # DNI seen by pvrow illuminated surfaces front_is_illum = aoi_front_pvrow <= 90 # direct self.direct['front_illum_pvrow'] = np.where( front_is_illum, DNI * cosd(aoi_front_pvrow), 0.) self.direct['front_shaded_pvrow'] = ( # Direct light through PV modules spacing self.direct['front_illum_pvrow'] * self.module_spacing_ratio # Direct light through PV modules, by transparency + self.direct['front_illum_pvrow'] * (1. - self.module_spacing_ratio) * self.module_transparency) self.direct['back_illum_pvrow'] = np.where( ~front_is_illum, DNI * cosd(aoi_back_pvrow), 0.) self.direct['back_shaded_pvrow'] = ( # Direct light through PV modules spacing self.direct['back_illum_pvrow'] * self.module_spacing_ratio # Direct light through PV modules, by transparency + self.direct['back_illum_pvrow'] * (1. - self.module_spacing_ratio) * self.module_transparency) # perez self.total_perez['front_illum_pvrow'] = perez_front_pvrow['poa_global'] self.total_perez['front_shaded_pvrow'] = ( self.total_perez['front_illum_pvrow'] - self.direct['front_illum_pvrow']) self.total_perez['ground_shaded'] = (self.DHI + self.direct['ground_shaded']) def transform(self, pvarray): """Apply calculated irradiance values to PV array timeseries geometries: assign values as parameters to timeseries surfaces. Parameters ---------- pvarray : PV array object PV array on which the calculated irradiance values will be applied """ # Prepare variables n_steps = self.n_steps rho_front = self.rho_front * np.ones(n_steps) inv_rho_front = 1. / rho_front rho_back = self.rho_back * np.ones(n_steps) inv_rho_back = 1. / rho_back # Transform timeseries ground pvarray.ts_ground.update_illum_params( {'direct': self.direct['ground_illum'], 'rho': self.albedo, 'inv_rho': 1. / self.albedo, 'total_perez': self.gnd_illum, 'direct_absorbed': self.faoi_ground * self.direct['ground_illum'] }) pvarray.ts_ground.update_shaded_params( {'direct': self.direct['ground_shaded'], 'rho': self.albedo, 'inv_rho': 1. / self.albedo, 'total_perez': self.gnd_shaded, 'direct_absorbed': self.faoi_ground * self.direct['ground_shaded'] }) for ts_pvrow in pvarray.ts_pvrows: # Front for ts_seg in ts_pvrow.front.list_segments: ts_seg.illum.update_params( {'direct': self.direct['front_illum_pvrow'], 'rho': rho_front, 'inv_rho': inv_rho_front, 'total_perez': self.pvrow_illum, 'direct_absorbed': self.faoi_front['direct'] * self.direct['front_illum_pvrow']}) ts_seg.shaded.update_params( {'direct': self.direct['front_shaded_pvrow'], 'rho': rho_front, 'inv_rho': inv_rho_front, 'total_perez': self.pvrow_shaded, 'direct_absorbed': self.faoi_front['direct'] * self.direct['front_shaded_pvrow']}) # Back for ts_seg in ts_pvrow.back.list_segments: ts_seg.illum.update_params( {'direct': self.direct['back_illum_pvrow'], 'rho': rho_back, 'inv_rho': inv_rho_back, 'total_perez': np.zeros(n_steps), 'direct_absorbed': self.faoi_back['direct'] * self.direct['back_illum_pvrow']}) ts_seg.shaded.update_params( {'direct': self.direct['back_shaded_pvrow'], 'rho': rho_back, 'inv_rho': inv_rho_back, 'total_perez': np.zeros(n_steps), 'direct_absorbed': self.faoi_back['direct'] * self.direct['back_shaded_pvrow']}) def get_full_modeling_vectors(self, pvarray, idx): """Get the modeling vectors used in matrix calculations of mathematical model. Parameters ---------- pvarray : PV array object PV array on which the calculated irradiance values will be applied idx : int, optional Index of the irradiance values to apply to the PV array (in the whole timeseries values) Returns ------- irradiance_vec : numpy array List of summed up non-reflective irradiance values for all surfaces and sky rho_vec : numpy array List of reflectivity values for all surfaces and sky invrho_vec : numpy array List of inverse reflectivity for all surfaces and sky total_perez_vec : numpy array List of total perez transposed irradiance values for all surfaces and sky """ # Sum up the necessary parameters to form the irradiance vector irradiance_vec, rho_vec, inv_rho_vec, total_perez_vec = \ self.get_modeling_vectors(pvarray) # Add sky values irradiance_vec.append(self.isotropic_luminance[idx]) total_perez_vec.append(self.isotropic_luminance[idx]) rho_vec.append(SKY_REFLECTIVITY_DUMMY) inv_rho_vec.append(SKY_REFLECTIVITY_DUMMY) return np.array(irradiance_vec), np.array(rho_vec), \ np.array(inv_rho_vec), np.array(total_perez_vec) def get_full_ts_modeling_vectors(self, pvarray): """Get the modeling vectors used in matrix calculations of the mathematical model, including the sky values. Parameters ---------- pvarray : PV array object PV array on which the calculated irradiance values will be applied Returns ------- irradiance_mat : np.ndarray List of summed up non-reflective irradiance values for all surfaces and sky. Dimension = [n_surfaces + 1, n_timesteps] rho_mat : np.ndarray List of reflectivity values for all surfaces and sky. Dimension = [n_surfaces + 1, n_timesteps] invrho_mat : np.ndarray List of inverse reflectivity for all surfaces and sky. Dimension = [n_surfaces + 1, n_timesteps] total_perez_mat : np.ndarray List of total perez transposed irradiance values for all surfaces and sky. Dimension = [n_surfaces + 1, n_timesteps] """ # Sum up the necessary parameters to form the irradiance vector irradiance_mat, rho_mat, inv_rho_mat, total_perez_mat = \ self.get_ts_modeling_vectors(pvarray) # Add sky values irradiance_mat.append(self.isotropic_luminance) total_perez_mat.append(self.isotropic_luminance) rho_mat.append(SKY_REFLECTIVITY_DUMMY * np.ones(pvarray.n_states)) inv_rho_mat.append(SKY_REFLECTIVITY_DUMMY * np.ones(pvarray.n_states)) return np.array(irradiance_mat), np.array(rho_mat), \ np.array(inv_rho_mat), np.array(total_perez_mat) @property def gnd_shaded(self): """Total timeseries irradiance incident on ground shaded areas""" return self.total_perez['ground_shaded'] @property def gnd_illum(self): """Total timeseries irradiance incident on ground illuminated areas""" return self.GHI @property def pvrow_shaded(self): """Total timeseries irradiance incident on PV row's front illuminated areas and calculated by Perez transposition""" return self.total_perez['front_shaded_pvrow'] @property def pvrow_illum(self): """Total timeseries irradiance incident on PV row's front shaded areas and calculated by Perez transposition""" return self.total_perez['front_illum_pvrow'] @property def sky_luminance(self): """Total timeseries isotropic luminance of sky""" return self.isotropic_luminance def _calculate_faoi_modifiers(self, aoi_front_pvrow, aoi_back_pvrow, surface_tilt, albedo): """Calculate fAOI modifier values for all surface types: front PV row, back PV row, and ground. Parameters ---------- aoi_front_pvrow : np.ndarray Direct light angle of incidence values for PV row front side [deg] aoi_back_pvrow : np.ndarray Direct light angle of incidence values for PV row back side [deg] surface_tilt : np.ndarray Surface tilt of PV row surfaces, measured from horizontal, ranging from 0 to 180 [deg] albedo : np.ndarray Ground albedo values """ # Need to update aoi measurement: input measured from normal, # but functions require to measure it from surface horizontal aoi_front = np.abs(90. - aoi_front_pvrow) aoi_back = np.abs(90. - aoi_back_pvrow) # --- front self.faoi_front['direct'] = ( self.faoi_fn_front(aoi_front) if self.faoi_fn_front is not None else (1. - self.rho_front) * np.ones_like(surface_tilt)) # --- back self.faoi_back['direct'] = ( self.faoi_fn_back(aoi_back) if self.faoi_fn_back is not None else (1. - self.rho_back) * np.ones_like(surface_tilt)) # --- ground self.faoi_ground = (1. - albedo)
[docs]class HybridPerezOrdered(BaseModel): """Model is based off Perez diffuse light model, and applied to pvfactors :py:class:`~pvfactors.geometry.pvarray.OrderedPVArray` objects. The model applies direct, circumsolar, and horizon irradiance to the PV array surfaces. """ params = ['rho', 'inv_rho', 'direct', 'isotropic', 'circumsolar', 'horizon', 'reflection'] cats = ['ground_illum', 'ground_shaded', 'front_illum_pvrow', 'back_illum_pvrow', 'front_shaded_pvrow', 'back_shaded_pvrow'] irradiance_comp = ['direct', 'circumsolar', 'horizon'] irradiance_comp_absorbed = ['direct_absorbed', 'circumsolar_absorbed', 'horizon_absorbed']
[docs] def __init__(self, horizon_band_angle=DEFAULT_HORIZON_BAND_ANGLE, circumsolar_angle=DEFAULT_CIRCUMSOLAR_ANGLE, circumsolar_model='uniform_disk', rho_front=0.01, rho_back=0.03, module_transparency=0., module_spacing_ratio=0., faoi_fn_front=None, faoi_fn_back=None): """Initialize irradiance model values that will be saved later on. Parameters ---------- horizon_band_angle : float, optional Width of the horizon band in [deg] (Default = DEFAULT_HORIZON_BAND_ANGLE) circumsolar_angle : float, optional Diameter of the circumsolar area in [deg] (Default = DEFAULT_CIRCUMSOLAR_ANGLE) circumsolar_model : str Circumsolar shading model to use (Default = 'uniform_disk') rho_front : float, optional Reflectivity of the front side of the PV rows (default = 0.01) rho_back : float, optional Reflectivity of the back side of the PV rows (default = 0.03) module_transparency : float, optional Module transparency (from 0 to 1), which will let some direct light pass through the PV modules in the PV rows and reach the shaded ground (Default = 0., fully opaque) module_spacing_ratio : float, optional Module spacing ratio (from 0 to 1), which is the ratio of the area covered by the space between PV modules over the total area of the PV rows, and which determines how much direct light will reach the shaded ground through the PV rows (Default = 0., no spacing at all) faoi_fn_front : function or object, optional Function (or object containing ``faoi`` method) 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 (default = None) faoi_fn_back : function or object, optional Function (or object containing ``faoi`` method) 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 (default = None) """ self.direct = dict.fromkeys(self.cats) self.circumsolar = dict.fromkeys(self.cats) self.horizon = dict.fromkeys(self.cats) self.total_perez = dict.fromkeys(self.cats) self.faoi_front = dict.fromkeys(self.irradiance_comp) self.faoi_back = dict.fromkeys(self.irradiance_comp) self.faoi_ground = None self.horizon_band_angle = horizon_band_angle self.circumsolar_angle = circumsolar_angle self.circumsolar_model = circumsolar_model self.rho_front = rho_front self.rho_back = rho_back self.module_transparency = module_transparency self.module_spacing_ratio = module_spacing_ratio # Treatment of faoi functions faoi_fn_front = (faoi_fn_front.faoi if hasattr(faoi_fn_front, 'faoi') else faoi_fn_front) faoi_fn_back = (faoi_fn_back.faoi if hasattr(faoi_fn_back, 'faoi') else faoi_fn_back) self.faoi_fn_front = faoi_fn_front self.faoi_fn_back = faoi_fn_back # Attributes that will be updated at fitting time self.albedo = None self.GHI = None self.DNI = None self.n_steps = None self.isotropic_luminance = None
def fit(self, timestamps, DNI, DHI, solar_zenith, solar_azimuth, surface_tilt, surface_azimuth, albedo, ghi=None): """Use vectorization to calculate values used for the hybrid Perez irradiance model. Parameters ---------- timestamps : array-like List of timestamps of the simulation. DNI : array-like Direct normal irradiance values [W/m2] DHI : array-like Diffuse horizontal irradiance values [W/m2] solar_zenith : array-like Solar zenith angles [deg] solar_azimuth : array-like Solar azimuth angles [deg] surface_tilt : array-like Surface tilt angles, from 0 to 180 [deg] surface_azimuth : array-like Surface azimuth angles [deg] albedo : array-like Albedo values (or ground reflectivity) ghi : array-like, optional Global horizontal irradiance [W/m2], if not provided, will be calculated from DNI and DHI (Default = None) """ # Make sure getting array-like values if np.isscalar(DNI): timestamps = [timestamps] DNI = np.array([DNI]) DHI = np.array([DHI]) solar_zenith = np.array([solar_zenith]) solar_azimuth = np.array([solar_azimuth]) surface_tilt = np.array([surface_tilt]) surface_azimuth = np.array([surface_azimuth]) ghi = None if ghi is None else np.array([ghi]) # Length of arrays n = len(DNI) # Make sure that albedo is a vector albedo = albedo * np.ones(n) if np.isscalar(albedo) else albedo # Calculate terms from Perez model luminance_isotropic, luminance_circumsolar, poa_horizon, \ poa_circumsolar_front, poa_circumsolar_back, \ aoi_front_pvrow, aoi_back_pvrow = \ self._calculate_luminance_poa_components( timestamps, DNI, DHI, solar_zenith, solar_azimuth, surface_tilt, surface_azimuth) # calculate faoi modifiers self._calculate_faoi_modifiers(aoi_front_pvrow, aoi_back_pvrow, surface_tilt, albedo) # Save and calculate total POA values from Perez model ghi = DNI * cosd(solar_zenith) + DHI if ghi is None else ghi self.GHI = ghi self.DHI = DHI self.n_steps = n perez_front_pvrow = get_total_irradiance( surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, DNI, ghi, DHI, albedo=albedo) total_perez_front_pvrow = perez_front_pvrow['poa_global'] # Save isotropic luminance self.isotropic_luminance = luminance_isotropic self.albedo = albedo # Ground surfaces self.direct['ground_illum'] = DNI * cosd(solar_zenith) self.direct['ground_shaded'] = ( # Direct light through PV modules spacing self.direct['ground_illum'] * self.module_spacing_ratio # Direct light through PV modules, by transparency + self.direct['ground_illum'] * (1. - self.module_spacing_ratio) * self.module_transparency) self.circumsolar['ground_illum'] = luminance_circumsolar self.circumsolar['ground_shaded'] = ( # Circumsolar light through PV modules spacing self.circumsolar['ground_illum'] * self.module_spacing_ratio # Circumsolar light through PV modules, by transparency + self.circumsolar['ground_illum'] * (1. - self.module_spacing_ratio) * self.module_transparency) self.horizon['ground'] = np.zeros(n) # PV row surfaces front_is_illum = aoi_front_pvrow <= 90 # direct self.direct['front_illum_pvrow'] = np.where( front_is_illum, DNI * cosd(aoi_front_pvrow), 0.) self.direct['front_shaded_pvrow'] = ( # Direct light through PV modules spacing self.direct['front_illum_pvrow'] * self.module_spacing_ratio # Direct light through PV modules, by transparency + self.direct['front_illum_pvrow'] * (1. - self.module_spacing_ratio) * self.module_transparency) self.direct['back_illum_pvrow'] = np.where( ~front_is_illum, DNI * cosd(aoi_back_pvrow), 0.) self.direct['back_shaded_pvrow'] = ( # Direct light through PV modules spacing self.direct['back_illum_pvrow'] * self.module_spacing_ratio # Direct light through PV modules, by transparency + self.direct['back_illum_pvrow'] * (1. - self.module_spacing_ratio) * self.module_transparency) # circumsolar self.circumsolar['front_illum_pvrow'] = np.where( front_is_illum, poa_circumsolar_front, 0.) self.circumsolar['front_shaded_pvrow'] = ( # Direct light through PV modules spacing self.circumsolar['front_illum_pvrow'] * self.module_spacing_ratio # Direct light through PV modules, by transparency + self.circumsolar['front_illum_pvrow'] * (1. - self.module_spacing_ratio) * self.module_transparency) self.circumsolar['back_illum_pvrow'] = np.where( ~front_is_illum, poa_circumsolar_back, 0.) self.circumsolar['back_shaded_pvrow'] = ( # Direct light through PV modules spacing self.circumsolar['back_illum_pvrow'] * self.module_spacing_ratio # Direct light through PV modules, by transparency + self.circumsolar['back_illum_pvrow'] * (1. - self.module_spacing_ratio) * self.module_transparency) # horizon self.horizon['front_pvrow'] = poa_horizon self.horizon['back_pvrow'] = poa_horizon # perez self.total_perez['front_illum_pvrow'] = total_perez_front_pvrow self.total_perez['front_shaded_pvrow'] = ( total_perez_front_pvrow - self.direct['front_illum_pvrow'] - self.circumsolar['front_illum_pvrow'] + self.direct['front_shaded_pvrow'] + self.circumsolar['front_shaded_pvrow'] ) self.total_perez['ground_shaded'] = ( DHI - self.circumsolar['ground_illum'] + self.circumsolar['ground_shaded'] + self.direct['ground_shaded']) self.total_perez['ground_illum'] = ghi self.total_perez['sky'] = luminance_isotropic def transform(self, pvarray): """Apply calculated irradiance values to PV array timeseries geometries: assign values as parameters to timeseries surfaces. Parameters ---------- pvarray : PV array object PV array on which the calculated irradiance values will be applied """ # Prepare variables n_steps = self.n_steps ts_pvrows = pvarray.ts_pvrows tilted_to_left = pvarray.rotation_vec > 0 rho_front = self.rho_front * np.ones(n_steps) inv_rho_front = 1. / rho_front rho_back = self.rho_back * np.ones(n_steps) inv_rho_back = 1. / rho_back # Transform timeseries ground pvarray.ts_ground.update_illum_params({ 'direct': self.direct['ground_illum'], 'circumsolar': self.circumsolar['ground_illum'], 'horizon': np.zeros(n_steps), 'rho': self.albedo, 'inv_rho': 1. / self.albedo, 'total_perez': self.gnd_illum, 'direct_absorbed': self.faoi_ground * self.direct['ground_illum'], 'circumsolar_absorbed': self.faoi_ground * self.circumsolar['ground_illum'], 'horizon_absorbed': np.zeros(n_steps)}) pvarray.ts_ground.update_shaded_params({ 'direct': self.direct['ground_shaded'], 'circumsolar': self.circumsolar['ground_shaded'], 'horizon': np.zeros(n_steps), 'rho': self.albedo, 'inv_rho': 1. / self.albedo, 'total_perez': self.gnd_shaded, 'direct_absorbed': self.faoi_ground * self.direct['ground_shaded'], 'circumsolar_absorbed': self.faoi_ground * self.circumsolar['ground_shaded'], 'horizon_absorbed': np.zeros(n_steps)}) # Transform timeseries PV rows for idx_pvrow, ts_pvrow in enumerate(ts_pvrows): # Front for ts_seg in ts_pvrow.front.list_segments: ts_seg.illum.update_params({ 'direct': self.direct['front_illum_pvrow'], 'circumsolar': self.circumsolar['front_illum_pvrow'], 'horizon': self.horizon['front_pvrow'], 'rho': rho_front, 'inv_rho': inv_rho_front, 'total_perez': self.pvrow_illum, 'direct_absorbed': self.faoi_front['direct'] * self.direct['front_illum_pvrow'], 'circumsolar_absorbed': self.faoi_front['circumsolar'] * self.circumsolar['front_illum_pvrow'], 'horizon_absorbed': self.faoi_front['horizon'] * self.horizon['front_pvrow']}) ts_seg.shaded.update_params({ 'direct': self.direct['front_shaded_pvrow'], 'circumsolar': self.circumsolar['front_shaded_pvrow'], 'horizon': self.horizon['front_pvrow'], 'rho': rho_front, 'inv_rho': inv_rho_front, 'total_perez': self.pvrow_shaded, 'direct_absorbed': self.faoi_front['direct'] * self.direct['front_shaded_pvrow'], 'circumsolar_absorbed': self.faoi_front['circumsolar'] * self.circumsolar['front_shaded_pvrow'], 'horizon_absorbed': self.faoi_front['horizon'] * self.horizon['front_pvrow']}) # Back: apply back surface horizon shading for ts_seg in ts_pvrow.back.list_segments: # Illum # In ordered pv arrays, there should only be 1 surface -> 0 centroid_illum = ts_seg.illum.list_ts_surfaces[0].centroid hor_shd_pct_illum = self._calculate_horizon_shading_pct_ts( ts_pvrows, centroid_illum, idx_pvrow, tilted_to_left, is_back_side=True) ts_seg.illum.update_params({ 'direct': self.direct['back_illum_pvrow'], 'circumsolar': self.circumsolar['back_illum_pvrow'], 'horizon': self.horizon['back_pvrow'] * (1. - hor_shd_pct_illum / 100.), 'horizon_unshaded': self.horizon['back_pvrow'], 'horizon_shd_pct': hor_shd_pct_illum, 'rho': rho_back, 'inv_rho': inv_rho_back, 'total_perez': np.zeros(n_steps), 'direct_absorbed': self.faoi_back['direct'] * self.direct['back_illum_pvrow'], 'circumsolar_absorbed': self.faoi_back['circumsolar'] * self.circumsolar['back_illum_pvrow'], 'horizon_absorbed': self.faoi_back['horizon'] * self.horizon['back_pvrow'] * (1. - hor_shd_pct_illum / 100.)}) # Back # In ordered pv arrays, there should only be 1 surface -> 0 centroid_shaded = ts_seg.shaded.list_ts_surfaces[0].centroid hor_shd_pct_shaded = self._calculate_horizon_shading_pct_ts( ts_pvrows, centroid_shaded, idx_pvrow, tilted_to_left, is_back_side=True) ts_seg.shaded.update_params({ 'direct': self.direct['back_shaded_pvrow'], 'circumsolar': self.circumsolar['back_shaded_pvrow'], 'horizon': self.horizon['back_pvrow'] * (1. - hor_shd_pct_shaded / 100.), 'horizon_unshaded': self.horizon['back_pvrow'], 'horizon_shd_pct': hor_shd_pct_shaded, 'rho': rho_back, 'inv_rho': inv_rho_back, 'total_perez': np.zeros(n_steps), 'direct_absorbed': self.faoi_back['direct'] * self.direct['back_shaded_pvrow'], 'circumsolar_absorbed': self.faoi_back['circumsolar'] * self.circumsolar['back_shaded_pvrow'], 'horizon_absorbed': self.faoi_back['horizon'] * self.horizon['back_pvrow'] * (1. - hor_shd_pct_shaded / 100.)}) def get_full_modeling_vectors(self, pvarray, idx): """Get the modeling vectors used in matrix calculations of mathematical model. Parameters ---------- pvarray : PV array object PV array on which the calculated irradiance values will be applied idx : int, optional Index of the irradiance values to apply to the PV array (in the whole timeseries values) Returns ------- irradiance_vec : numpy array List of summed up non-reflective irradiance values for all surfaces and sky rho_vec : numpy array List of reflectivity values for all surfaces and sky invrho_vec : numpy array List of inverse reflectivity for all surfaces and sky total_perez_vec : numpy array List of total perez transposed irradiance values for all surfaces and sky """ # Sum up the necessary parameters to form the irradiance vector irradiance_vec, rho_vec, inv_rho_vec, total_perez_vec = self.get_modeling_vectors(pvarray) # Add sky values irradiance_vec.append(self.isotropic_luminance[idx]) total_perez_vec.append(self.isotropic_luminance[idx]) rho_vec.append(SKY_REFLECTIVITY_DUMMY) inv_rho_vec.append(SKY_REFLECTIVITY_DUMMY) return np.array(irradiance_vec), np.array(rho_vec), \ np.array(inv_rho_vec), np.array(total_perez_vec) def get_full_ts_modeling_vectors(self, pvarray): """Get the modeling vectors used in matrix calculations of the mathematical model, including the sky values. Parameters ---------- pvarray : PV array object PV array on which the calculated irradiance values will be applied Returns ------- irradiance_mat : np.ndarray List of summed up non-reflective irradiance values for all surfaces and sky. Dimension = [n_surfaces + 1, n_timesteps] rho_mat : np.ndarray List of reflectivity values for all surfaces and sky. Dimension = [n_surfaces + 1, n_timesteps] invrho_mat : np.ndarray List of inverse reflectivity for all surfaces and sky. Dimension = [n_surfaces + 1, n_timesteps] total_perez_mat : np.ndarray List of total perez transposed irradiance values for all surfaces and sky. Dimension = [n_surfaces + 1, n_timesteps] """ # Sum up the necessary parameters to form the irradiance vector irradiance_mat, rho_mat, inv_rho_mat, total_perez_mat = self.get_ts_modeling_vectors(pvarray) # Add sky values irradiance_mat.append(self.isotropic_luminance) total_perez_mat.append(self.isotropic_luminance) rho_mat.append(SKY_REFLECTIVITY_DUMMY * np.ones(pvarray.n_states)) inv_rho_mat.append(SKY_REFLECTIVITY_DUMMY * np.ones(pvarray.n_states)) return np.array(irradiance_mat), np.array(rho_mat), \ np.array(inv_rho_mat), np.array(total_perez_mat) @property def gnd_shaded(self): """Total timeseries irradiance incident on ground shaded areas""" return self.total_perez['ground_shaded'] @property def gnd_illum(self): """Total timeseries irradiance incident on ground illuminated areas""" return self.total_perez['ground_illum'] @property def pvrow_shaded(self): """Total timeseries irradiance incident on PV row's front illuminated areas and calculated by Perez transposition""" return self.total_perez['front_shaded_pvrow'] @property def pvrow_illum(self): """Total timeseries irradiance incident on PV row's front shaded areas and calculated by Perez transposition""" return self.total_perez['front_illum_pvrow'] @property def sky_luminance(self): """Total timeseries isotropic luminance of sky""" return self.isotropic_luminance def _calculate_horizon_shading_pct_ts(self, ts_pvrows, ts_point_coords, pvrow_idx, tilted_to_left, is_back_side=True): """Calculate horizon band shading percentage on surfaces of the ordered PV array, in a vectorized way. Parameters ---------- ts_pvrows : list of :py:class:`~pvfactors.geometry.timeseries.TsPVRow` List of timeseries PV rows in the PV array ts_point_coords : :py:class:`~pvfactors.geometry.timeseries.TsPointCoords` Timeseries coordinates of point that suffers horizon band shading pvrow_idx : int Index of PV row on which the above point is located tilted_to_left : list of bool Flags indicating when the PV rows are strictly tilted to the left is_back_side : bool Flag indicating if point is located on back side of PV row Returns ------- horizon_shading_pct : np.ndarray Percentage vector of horizon band irradiance shading (from 0 to 100) """ n_pvrows = len(ts_pvrows) if pvrow_idx == 0: shading_pct_left = np.zeros_like(tilted_to_left) else: high_pt_left = ts_pvrows[ pvrow_idx - 1].full_pvrow_coords.highest_point shading_angle_left = np.rad2deg(np.abs(np.arctan( (high_pt_left.y - ts_point_coords.y) / (high_pt_left.x - ts_point_coords.x)))) shading_pct_left = calculate_horizon_band_shading( shading_angle_left, self.horizon_band_angle) if pvrow_idx == (n_pvrows - 1): shading_pct_right = np.zeros_like(tilted_to_left) else: high_pt_right = ts_pvrows[ pvrow_idx + 1].full_pvrow_coords.highest_point shading_angle_right = np.rad2deg(np.abs(np.arctan( (high_pt_right.y - ts_point_coords.y) / (high_pt_right.x - ts_point_coords.x)))) shading_pct_right = calculate_horizon_band_shading( shading_angle_right, self.horizon_band_angle) if is_back_side: shading_pct = np.where(tilted_to_left, shading_pct_right, shading_pct_left) else: shading_pct = np.where(tilted_to_left, shading_pct_left, shading_pct_right) return shading_pct def _calculate_circumsolar_shading_pct(self, surface, idx_neighbor, pvrows, solar_2d_vector): """Model method to calculate circumsolar shading on surfaces of the ordered PV array. TODO: This needs to be merged with horizon shading for performance Parameters ---------- surface : :py:class:`~pvfactors.geometry.base.PVSurface` object PV surface for which some horizon band shading will occur idx_neighbor : int Index of the neighboring PV row (can be ``None``) pvrows : list of :py:class:`~pvfactors.geometry.pvrow.PVRow` objects List of PV rows on which ``idx_neighbor`` will be used solar_2d_vector : list Solar vector in the 2D PV array representation Returns ------- circ_shading_pct : float Percentage of circumsolar irradiance shaded (from 0 to 100) """ # TODO: should be applied to all pvrow surfaces if idx_neighbor is not None: # Calculate the solar and circumsolar elevation angles in 2D plane solar_2d_elevation = np.abs( np.arctan(solar_2d_vector[1] / solar_2d_vector[0]) ) * 180. / np.pi lower_angle_circumsolar = (solar_2d_elevation - self.circumsolar_angle / 2.) centroid = surface.centroid neighbor_point = pvrows[idx_neighbor].highest_point shading_angle = np.abs(np.arctan( (neighbor_point.y - centroid.y) / (neighbor_point.x - centroid.x))) * 180. / np.pi percentage_circ_angle_covered = (shading_angle - lower_angle_circumsolar) \ / self.circumsolar_angle * 100. circ_shading_pct = calculate_circumsolar_shading( percentage_circ_angle_covered, model=self.circumsolar_model) return circ_shading_pct @staticmethod def _calculate_luminance_poa_components( timestamps, DNI, DHI, solar_zenith, solar_azimuth, surface_tilt, surface_azimuth): """Calculate Perez-like luminance and plane-of-array irradiance values. Parameters ---------- timestamps : array-like List of timestamps of the simulation. DNI : array-like Direct normal irradiance values [W/m2] DHI : array-like Diffuse horizontal irradiance values [W/m2] solar_zenith : array-like Solar zenith angles [deg] solar_azimuth : array-like Solar azimuth angles [deg] surface_tilt : array-like Surface tilt angles, from 0 to 180 [deg] surface_azimuth : array-like Surface azimuth angles [deg] Returns ------- luminance_isotropic : numpy array Luminance values of the isotropic sky dome area luminance_circumsolar : numpy array Luminance values of the circumsolar area poa_horizon : numpy array Plane-of-array irradiance coming from horizon band and incident on the sides of the PV rows [W/m2] poa_circumsolar_front : numpy array Plane-of-array irradiance coming from the circumsolar area and incident on the front side of the PV rows [W/m2] poa_circumsolar_back : numpy array Plane-of-array irradiance coming from the circumsolar area and incident on the back side of the PV rows [W/m2] aoi_front_pvrow : numpy array Angle of incidence of direct light on front side of PV rows [deg] aoi_back_pvrow : numpy array Angle of incidence of direct light on back side of PV rows [deg] """ # Calculations from utils function df_inputs = perez_diffuse_luminance( timestamps, surface_tilt, surface_azimuth, solar_zenith, solar_azimuth, DNI, DHI) luminance_isotropic = df_inputs.luminance_isotropic.values luminance_circumsolar = df_inputs.luminance_circumsolar.values poa_horizon = df_inputs.poa_horizon.values poa_circumsolar_front = df_inputs.poa_circumsolar.values # Calculate AOI on front pvrow using pvlib implementation aoi_front_pvrow = aoi_function( surface_tilt, surface_azimuth, solar_zenith, solar_azimuth) aoi_back_pvrow = 180. - aoi_front_pvrow # Will be used for back surface adjustments: from Perez model # FIXME: pvlib clips the angle values to calculate vf -> adjust here vf_circumsolar_backsurface = cosd(aoi_back_pvrow) / cosd(solar_zenith) poa_circumsolar_back = (luminance_circumsolar * vf_circumsolar_backsurface) # Return only >0 values for poa_horizon poa_horizon = np.abs(poa_horizon) return luminance_isotropic, luminance_circumsolar, poa_horizon, \ poa_circumsolar_front, poa_circumsolar_back, \ aoi_front_pvrow, aoi_back_pvrow def _calculate_faoi_modifiers(self, aoi_front_pvrow, aoi_back_pvrow, surface_tilt, albedo): """Calculate fAOI modifier values for all surface types: front PV row, back PV row, and ground. Parameters ---------- aoi_front_pvrow : np.ndarray Direct light angle of incidence values for PV row front side [deg] aoi_back_pvrow : np.ndarray Direct light angle of incidence values for PV row back side [deg] surface_tilt : np.ndarray Surface tilt of PV row surfaces, measured from horizontal, ranging from 0 to 180 [deg] albedo : np.ndarray Ground albedo values """ # Need to update aoi measurement: input measured from normal, # but functions require to measure it from surface horizontal aoi_front = np.abs(90. - aoi_front_pvrow) aoi_back = np.abs(90. - aoi_back_pvrow) # --- front if self.faoi_fn_front is not None: faoi_direct_rays = self.faoi_fn_front(aoi_front) self.faoi_front['direct'] = faoi_direct_rays # Assume that circumsolar is just a point for this self.faoi_front['circumsolar'] = faoi_direct_rays # Assume horizon band has zero width self.faoi_front['horizon'] = self.faoi_fn_front(surface_tilt) else: faoi_diffuse_front = ((1. - self.rho_front) * np.ones_like(surface_tilt)) self.faoi_front['direct'] = faoi_diffuse_front self.faoi_front['circumsolar'] = faoi_diffuse_front self.faoi_front['horizon'] = faoi_diffuse_front # --- back if self.faoi_fn_back is not None: faoi_direct_rays = self.faoi_fn_back(aoi_back) self.faoi_back['direct'] = faoi_direct_rays # Assume that circumsolar is just a point for this self.faoi_back['circumsolar'] = faoi_direct_rays # Assume horizon band has zero width self.faoi_back['horizon'] = self.faoi_fn_back(surface_tilt) else: faoi_diffuse_back = ((1. - self.rho_back) * np.ones_like(surface_tilt)) self.faoi_back['direct'] = faoi_diffuse_back self.faoi_back['circumsolar'] = faoi_diffuse_back self.faoi_back['horizon'] = faoi_diffuse_back # --- ground self.faoi_ground = (1. - albedo)