Account for AOI reflection losses (in full mode only)

In this section, we will learn:

  • how pvfactors accounts for AOI losses by default

  • how to account for AOI-dependent reflection losses for direct, circumsolar, and horizon irradiance components

  • how to account for AOI-dependent reflection losses for isotropic and reflection irradiance components

  • how to run all of this using the pvfactors run functions

Imports and settings

[1]:
# Import external libraries
import os
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime
import pandas as pd
import warnings

# Settings
%matplotlib inline
np.set_printoptions(precision=3, linewidth=300)
warnings.filterwarnings('ignore')
plt.style.use('seaborn-whitegrid')
plt.rcParams.update({'font.size': 12})
# Paths
LOCAL_DIR = os.getcwd()
DATA_DIR = os.path.join(LOCAL_DIR, 'data')
filepath = os.path.join(DATA_DIR, 'test_df_inputs_MET_clearsky_tucson.csv')

RUN_FIXED_TILT = True

Let’s define a few helper functions that will help clarify the notebook

[2]:
# Helper functions for plotting and simulation
def plot_irradiance(df_report):
    # Plot irradiance
    f, ax = plt.subplots(nrows=1, ncols=2, figsize=(12, 4))
    # Plot back surface irradiance
    df_report[['qinc_back', 'qabs_back']].plot(ax=ax[0])
    ax[0].set_title('Back surface irradiance')
    ax[0].set_ylabel('W/m2')
    # Plot front surface irradiance
    df_report[['qinc_front', 'qabs_front']].plot(ax=ax[1])
    ax[1].set_title('Front surface irradiance')
    ax[1].set_ylabel('W/m2')
    plt.show()

def plot_aoi_losses(df_report):
    # plotting AOI losses
    f, ax = plt.subplots(figsize=(5.5, 4))
    df_report[['aoi_losses_back_%']].plot(ax=ax)
    df_report[['aoi_losses_front_%']].plot(ax=ax)
    # Adjust axes
    ax.set_ylabel('%')
    ax.legend(['AOI losses back PV row', 'AOI losses front PV row'])
    ax.set_title('AOI losses')
    plt.show()

# Create a function that will build a simulation report
def fn_report(pvarray):
    # Get irradiance values
    report = {'qinc_back': pvarray.ts_pvrows[1].back.get_param_weighted('qinc'),
              'qabs_back': pvarray.ts_pvrows[1].back.get_param_weighted('qabs'),
              'qinc_front': pvarray.ts_pvrows[1].front.get_param_weighted('qinc'),
              'qabs_front': pvarray.ts_pvrows[1].front.get_param_weighted('qabs')}
    # Calculate AOI losses
    report['aoi_losses_back_%'] = (report['qinc_back'] - report['qabs_back']) / report['qinc_back'] * 100.
    report['aoi_losses_front_%'] = (report['qinc_front'] - report['qabs_front']) / report['qinc_front'] * 100.
    # Return report
    return report

Get timeseries inputs

[3]:
def export_data(fp):
    tz = 'US/Arizona'
    df = pd.read_csv(fp, index_col=0)
    df.index = pd.DatetimeIndex(df.index).tz_convert(tz)
    return df

df = export_data(filepath)
df_inputs = df.iloc[:48, :]
[4]:
# Plot the data
f, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(12, 3))
df_inputs[['dni', 'dhi']].plot(ax=ax1)
df_inputs[['solar_zenith', 'solar_azimuth']].plot(ax=ax2)
df_inputs[['surface_tilt', 'surface_azimuth']].plot(ax=ax3)
plt.show()
../_images/tutorials_Account_for_AOI_losses_8_0.png
[5]:
# Use a fixed albedo
albedo = 0.2

Prepare PV array parameters

[6]:
pvarray_parameters = {
    'n_pvrows': 3,            # number of pv rows
    'pvrow_height': 1,        # height of pvrows (measured at center / torque tube)
    'pvrow_width': 1,         # width of pvrows
    'axis_azimuth': 0.,       # azimuth angle of rotation axis
    'gcr': 0.4,               # ground coverage ratio
}

Default AOI loss behavior

In pvfactors:

  • qinc is the total incident irradiance on a surface, and it does not account for reflection losses

  • but qabs, which is the total absorbed irradiance by a surface, does accounts for it.

By default, pvfactors assumes that all reflection losses (or AOI losses) are diffuse; i.e. they do not depend on angle of incidence (AOI). Here is an example.

Let’s run a full mode simulation (reflection equilibrium) and compare the calculated incident and absorbed irradiance on both sides of a PV row in a modeled PV array. We’ll use 3% reflection for PV row front surfaces, and 5% for the back surfaces.

[7]:
from pvfactors.geometry import OrderedPVArray
# Create PV array
pvarray = OrderedPVArray.init_from_dict(pvarray_parameters)
[8]:
from pvfactors.engine import PVEngine
from pvfactors.irradiance import HybridPerezOrdered
# Create irradiance model
irradiance_model = HybridPerezOrdered(rho_front=0.03, rho_back=0.05)
# Create engine
engine = PVEngine(pvarray, irradiance_model=irradiance_model)
# Fit engine to data
engine.fit(df_inputs.index, df_inputs.dni, df_inputs.dhi,
           df_inputs.solar_zenith, df_inputs.solar_azimuth,
           df_inputs.surface_tilt, df_inputs.surface_azimuth,
           albedo)
[9]:
# Plot pvarray shapely geometries
f, ax = plt.subplots(figsize=(8, 4))
pvarray.plot_at_idx(12, ax)
plt.title('Modeled PV array at {}'.format(df_inputs.index[12]))
plt.show()
../_images/tutorials_Account_for_AOI_losses_16_0.png
[10]:
# Run full mode simulation
report = engine.run_full_mode(fn_build_report=fn_report)
# Turn report into dataframe
df_report = pd.DataFrame(report, index=df_inputs.index)
[11]:
plot_irradiance(df_report)
../_images/tutorials_Account_for_AOI_losses_18_0.png

Let’s plot the back AOI losses

[12]:
plot_aoi_losses(df_report)
../_images/tutorials_Account_for_AOI_losses_20_0.png

As shown above, by default pvfactors apply constant values of AOI losses for all the surfaces in the system, and for all the incident irradiance components:

  • 3% loss for the irradiance incident on front of PV rows, which corresponds to the chosen rho_front in the irradiance model

  • 5% loss for the irradiance incident on back of PV rows, which corresponds to the chosen rho_back in the irradiance model

Use an fAOI function in the irradiance model

The next step that can improve the AOI loss calculation, especially for the PV row front surface that receives a lot of direct light, would be to use reflection losses that would be dependent on the AOI, and that would be applied to all the irradiance model components: direct, circumsolar, and horizon light components.

What is an fAOI function?

The fAOI function that the users need to provide takes an angle of incidence as input (AOI measured in degrees and against the surface horizontal - from 0 to 180 deg, not against the surface normal vector - which would have been from 0 to 90 deg), and it returns a transmission value for the incident light. So it’s effectively a factor that removes reflection losses.

Let’s see what this looks like. First, let’s create such a function using a pvfactors utility function, and then we’ll plot it.

Given a pvlib module database name, you can create an fAOI function as follows using pvfactors.

[13]:
# import utility function
from pvfactors.viewfactors.aoimethods import faoi_fn_from_pvlib_sandia
# Choose a module name
module_name = 'SunPower_128_Cell_Module___2009_'
# Create an faoi function
faoi_function = faoi_fn_from_pvlib_sandia(module_name)
[14]:
# Plot faoi function values
aoi_values = np.linspace(0, 180, 100)
faoi_values = faoi_function(aoi_values)

f, ax = plt.subplots()
ax.plot(aoi_values, faoi_values)
ax.set_title('fAOI values for pvlib\'s {}'.format(module_name))
ax.set_ylabel('fAOI values')
ax.set_xlabel('AOI angles measured from "horizontal" [deg]')
plt.show()
../_images/tutorials_Account_for_AOI_losses_28_0.png

As expected, there are less reflection losses for incident light rays normal to the surface than everywhere else.

Use the fAOI function

It’s then easy to use the created fAOI function in the irradiance models. It just has to be passed to the model at initialization.

For this example, we will use the same fAOI function for the front and back surfaces of the PV rows.

[15]:
# Create irradiance model with fAOI function
irradiance_model = HybridPerezOrdered(faoi_fn_front=faoi_function, faoi_fn_back=faoi_function)

Then pass the model to the PVEngine and run the simulation as usual.

[16]:
# Create engine
engine = PVEngine(pvarray, irradiance_model=irradiance_model)
# Fit engine to data
engine.fit(df_inputs.index, df_inputs.dni, df_inputs.dhi,
           df_inputs.solar_zenith, df_inputs.solar_azimuth,
           df_inputs.surface_tilt, df_inputs.surface_azimuth,
           albedo)
# Run full mode simulation
report = engine.run_full_mode(fn_build_report=fn_report)
# Turn report into dataframe
df_report = pd.DataFrame(report, index=df_inputs.index)

Let’s now see what the irradiance and AOI losses look like.

[17]:
plot_irradiance(df_report)
../_images/tutorials_Account_for_AOI_losses_36_0.png
[18]:
plot_aoi_losses(df_report)
../_images/tutorials_Account_for_AOI_losses_37_0.png

We can now see the changes in AOI losses, which now use the fAOI function for the direct, circumsolar, and horizon light components. But it still uses the constant rho_front and rho_back values for the reflection and isotropic components of the incident light on the surfaces.

Advanced: use an fAOI function for the (ground and array) reflection and isotropic components

The more advanced use is to apply the fAOI losses to the reflection and isotropic component of the light incident on the PV row surfaces.

In order to do so you simply need to pass the fAOI function to the view factor calculator before initializing the PVEngine.

In this case, the simulation workflow will be as follows:

  • the PVEngine will still calculate the equilibrium of reflections assuming diffuse surfaces and constant reflection losses

  • it will then use the calculated radiosity values and apply the fAOI using an integral combining the AOI losses and the view factor integrands, as described in the theory section, and similarly to Marion, B., et al (2017)

A word of caution

The users should be careful when using fAOI losses with the view factor calculator for the following reasons:

  • in order to be fully consistent in the PVEngine calculations, it is wiser to re-calculate a global hemispherical reflectivity value using the fAOI function, which will be used in the reflection equilibrium calculation

  • the method used for accounting fAOI losses in reflections is physically valid only if the surfaces are “infinitesimal” because it uses view factor formulas only valid in this case (see http://www.thermalradiation.net/sectionb/B-71.html). So in order to make it work in pvfactors, you’ll need to discretize the PV row sides into smaller segments

  • the method relies on the numerical calculation of an integral, and that calculation will converge only given a sufficient number of integral points (which can be provided to the pvfactors view factor calculator). Marion, B., et al (2017) seems to be using 180 points, but in pvfactors’ implementation it doesn’t look like it’s enough for the integral to converge, so we’ll use 1000 integral points in this example

  • the two points above slow down the computation time by an order of magnitude. 8760 simulations that normally take a couple of seconds to run with pvfactors’s full mode can then take up to a minute

Apply fAOI losses to reflection terms

Discretize the PV row sides of the PV array:

[19]:
# first let's discretize the PV row sides
pvarray_parameters.update({
    'cut': {1: {'front': 5, 'back': 5}}
})
# Create a new pv array
pvarray = OrderedPVArray.init_from_dict(pvarray_parameters)

Add fAOI losses to the view factor calculator, and use 1000 integration points

[20]:
from pvfactors.viewfactors import VFCalculator

vf_calculator = VFCalculator(faoi_fn_front=faoi_function, faoi_fn_back=faoi_function,
                             n_aoi_integral_sections=1000)

Re-calculate global hemispherical reflectivity values based on fAOI function

[21]:
# For back PV row surface
is_back = True
rho_back = vf_calculator.vf_aoi_methods.rho_from_faoi_fn(is_back)
# For front PV row surface
is_back = False
rho_front = vf_calculator.vf_aoi_methods.rho_from_faoi_fn(is_back)

# Print results
print('Reflectivity values for front side: {}, and back side: {}'.format(rho_front, rho_back))
Reflectivity values for front side: 0.029002539185428944, and back side: 0.029002539185428944

Since we’re using the same fAOI function for front and back sides, we now get the same global hemispherical reflectivity values.

We can now create the irradiance model.

[22]:
irradiance_model = HybridPerezOrdered(rho_front=rho_front, rho_back=rho_back,
                                      faoi_fn_front=faoi_function, faoi_fn_back=faoi_function)

Simulations can then be run the usual way:

[23]:
# Create engine
engine = PVEngine(pvarray, vf_calculator=vf_calculator,
                  irradiance_model=irradiance_model)
# Fit engine to data
engine.fit(df_inputs.index, df_inputs.dni, df_inputs.dhi,
           df_inputs.solar_zenith, df_inputs.solar_azimuth,
           df_inputs.surface_tilt, df_inputs.surface_azimuth,
           albedo)
[24]:
# Plot pvarray shapely geometries
f, ax = plt.subplots(figsize=(8, 4))
ax = pvarray.plot_at_idx(12, ax, with_surface_index=True)
plt.title('Modeled PV array at {}'.format(df_inputs.index[14]))
plt.show()
../_images/tutorials_Account_for_AOI_losses_54_0.png

Run the simulation:

[25]:
# Run full mode simulation
report = engine.run_full_mode(fn_build_report=fn_report)
# Turn report into dataframe
df_report = pd.DataFrame(report, index=df_inputs.index)

Let’s now see what the irradiance and AOI losses look like.

[26]:
plot_irradiance(df_report)
../_images/tutorials_Account_for_AOI_losses_58_0.png
[27]:
plot_aoi_losses(df_report)
../_images/tutorials_Account_for_AOI_losses_59_0.png

This is the way to apply fAOI losses to all the irradiance components in a pvfactors simulation.

Doing all of the above using the “run functions”

When using the “run functions”, you’ll just need to define the parameters in advance and then pass it to the functions.

[28]:
# Define the parameters for the irradiance model and the view factor calculator
irradiance_params = {'rho_front': rho_front, 'rho_back': rho_back,
                     'faoi_fn_front': faoi_function, 'faoi_fn_back': faoi_function}

vf_calculator_params = {'faoi_fn_front': faoi_function, 'faoi_fn_back': faoi_function,
                        'n_aoi_integral_sections': 1000}

Using run_timeseries_engine()

[29]:
from pvfactors.run import run_timeseries_engine

# run simulations in parallel mode
report_from_fn = run_timeseries_engine(fn_report, pvarray_parameters, df_inputs.index,
                                       df_inputs.dni, df_inputs.dhi,
                                       df_inputs.solar_zenith, df_inputs.solar_azimuth,
                                       df_inputs.surface_tilt, df_inputs.surface_azimuth,
                                       albedo,
                                       irradiance_model_params=irradiance_params,
                                       vf_calculator_params=vf_calculator_params)

# Turn report into dataframe
df_report_from_fn = pd.DataFrame(report_from_fn, index=df_inputs.index)
[30]:
plot_irradiance(df_report_from_fn)
../_images/tutorials_Account_for_AOI_losses_66_0.png
[31]:
plot_aoi_losses(df_report_from_fn)
../_images/tutorials_Account_for_AOI_losses_67_0.png

Using run_parallel_engine()

Because of Python’s multiprocessing, and because functions cannot be pickled in Python, the functions need to be wrapped up into classes.

[32]:
class ReportBuilder(object):
    """Class for building the reports with multiprocessing"""
    @staticmethod
    def build(pvarray):
        pvrow = pvarray.ts_pvrows[1]
        report = {'qinc_front': pvrow.front.get_param_weighted('qinc'),
                  'qabs_front': pvrow.front.get_param_weighted('qabs'),
                  'qinc_back': pvrow.back.get_param_weighted('qinc'),
                  'qabs_back': pvrow.back.get_param_weighted('qabs')}

        # Calculate AOI losses
        report['aoi_losses_back_%'] = (report['qinc_back'] - report['qabs_back']) / report['qinc_back'] * 100.
        report['aoi_losses_front_%'] = (report['qinc_front'] - report['qabs_front']) / report['qinc_front'] * 100.
        # Return report
        return report

    @staticmethod
    def merge(reports):
        report = reports[0]
        keys = report.keys()
        for other_report in reports[1:]:
            for key in keys:
                report[key] = list(report[key])
                report[key] += list(other_report[key])
        return report


class FaoiClass(object):
    """Class for passing the faoi function to engine"""
    @staticmethod
    def faoi(*args, **kwargs):
        fn = faoi_fn_from_pvlib_sandia(module_name)
        return fn(*args, **kwargs)

Pass the objects through the dictionaries and run the simulation

[33]:
# Define the parameters for the irradiance model and the view factor calculator
irradiance_params = {'rho_front': rho_front, 'rho_back': rho_back,
                     'faoi_fn_front': FaoiClass, 'faoi_fn_back': FaoiClass}

vf_calculator_params = {'faoi_fn_front': FaoiClass, 'faoi_fn_back': FaoiClass,
                        'n_aoi_integral_sections': 1000}
[34]:
from pvfactors.run import run_parallel_engine

# run simulations in parallel mode
report_from_fn = run_parallel_engine(ReportBuilder, pvarray_parameters, df_inputs.index,
                                     df_inputs.dni, df_inputs.dhi,
                                     df_inputs.solar_zenith, df_inputs.solar_azimuth,
                                     df_inputs.surface_tilt, df_inputs.surface_azimuth,
                                     albedo,
                                     irradiance_model_params=irradiance_params,
                                     vf_calculator_params=vf_calculator_params)

# Turn report into dataframe
df_report_from_fn = pd.DataFrame(report_from_fn, index=df_inputs.index)
INFO:pvfactors.run:Parallel calculation elapsed time: 0.731104850769043 sec
[35]:
plot_irradiance(df_report_from_fn)
../_images/tutorials_Account_for_AOI_losses_74_0.png
[36]:
plot_aoi_losses(df_report_from_fn)
../_images/tutorials_Account_for_AOI_losses_75_0.png

It’s that easy!