tutorial banner

Tutorial A - Single Diode Model#

Let’s put together what we know of modeling POA irradiance and cell temperature and calculate a module’s performance.

# if running on google colab, uncomment the next line and execute this cell to install the dependencies and prevent "ModuleNotFoundError" in later cells:
# !pip install -r https://raw.githubusercontent.com/PVSC-Python-Tutorials/PVPMC_2022/main/requirements.txt
# import pvlib and other useful python packages
import pvlib
import pandas as pd
from matplotlib import pyplot as plt
# a "typical meteorological year" or TMY is made of individual months
# cherry picked from several years, but to run a simulation we need
# to calculate solar positions for an actual year. EG: PVsyst defaults
# to 1990
YEAR = 1990
STARTDATE = '%d-01-01T00:00:00' % YEAR
ENDDATE = '%d-12-31T23:59:59' % YEAR
TIMES = pd.date_range(start=STARTDATE, end=ENDDATE, freq='H')
# pvlib python can retrieve CEC module and inverter parameters from the
# SAM libraries
CECMODS = pvlib.pvsystem.retrieve_sam('CECMod')
INVERTERS = pvlib.pvsystem.retrieve_sam('CECInverter')

# It can be tricky to find the modules you want so you can visit their
# GitHub page to search the CSV files manually, or use pvfree
# https://pvfree.herokuapp.com/cec_modules/ and ditto for inverters
# https://pvfree.herokuapp.com/pvinverters/
# NOTE: whitespace, hyphens, dashes, etc. are replaced by underscores
# These are some basic 300-W Canadian Solar poly and mono Si modules
CECMOD_POLY = CECMODS['Canadian_Solar_Inc__CS6X_300P']
CECMOD_MONO = CECMODS['Canadian_Solar_Inc__CS6X_300M']
# here's a trick, transpose the database, and search the index using
# strings
INVERTERS.T[INVERTERS.T.index.str.startswith('SMA_America__STP')]
#                                       Vac         Pso     Paco  ... Mppt_high    CEC_Date             CEC_Type
# SMA_America__STP_33_US_41__480V_      480  126.152641  33300.0  ...     800.0         NaN  Utility Interactive
# SMA_America__STP_50_US_41__480V_      480  111.328354  50010.0  ...     800.0    1/2/2019  Utility Interactive
# SMA_America__STP_60_US_10__400V_      400   97.213982  59860.0  ...     800.0         NaN  Utility Interactive
# SMA_America__STP_60_US_10__480V_      480  116.969749  60000.0  ...     800.0         NaN  Utility Interactive
# SMA_America__STP_62_US_41__480V_      480  133.166687  62500.0  ...     800.0         NaN  Utility Interactive
# SMA_America__STP12000TL_US_10__480V_  480   56.013401  12000.0  ...     800.0  10/15/2018  Utility Interactive
# SMA_America__STP15000TL_US_10__480V_  480   52.128044  15000.0  ...     800.0  10/15/2018  Utility Interactive
# SMA_America__STP20000TL_US_10__480V_  480   46.517708  20000.0  ...     800.0  10/15/2018  Utility Interactive
# SMA_America__STP24000TL_US_10__480V_  480   46.893803  24060.0  ...     800.0  10/15/2018  Utility Interactive
# SMA_America__STP30000TL_US_10__480V_  480    62.93433  30010.0  ...     800.0  10/15/2018  Utility Interactive
# SMA_America__STP50_US_40__480V_       480  125.080681  50072.0  ...     800.0  10/15/2018  Utility Interactive

# that was almost too easy, let's use the 60-kW Sunny TriPower, it's a good inverter.
INVERTER_60K = INVERTERS['SMA_America__STP_60_US_10__480V_']
# I know we already did this in the other tutorials, but now we have
# to do it again, sorry. Think of it as a review or pop quiz
# we need to get:
# * weather
# * solar position
# * PV surface orientation, aoi, etc.
# * plane of array irradiance
# * module temperatures
# but first we need to know where in the world are we?
LATITUDE, LONGITUDE = 40.5137, -108.5449
# now we can get some weather, before we used TMY,
# now we'll get some data from PVGIS
data, months, inputs, meta = pvlib.iotools.get_pvgis_tmy(latitude=LATITUDE, longitude=LONGITUDE)
/opt/hostedtoolcache/Python/3.7.15/x64/lib/python3.7/site-packages/pvlib/iotools/pvgis.py:491: pvlibDeprecationWarning: PVGIS variable names will be renamed to pvlib conventions by default starting in pvlib 0.10.0. Specify map_variables=True to enable that behavior now, or specify map_variables=False to hide this warning.
  'to hide this warning.', pvlibDeprecationWarning
# get solar position
data.index = TIMES
sp = pvlib.solarposition.get_solarposition(
        TIMES, LATITUDE, LONGITUDE)
solar_zenith = sp.apparent_zenith.values
solar_azimuth = sp.azimuth.values
# get tracker positions
tracker = pvlib.tracking.singleaxis(solar_zenith, solar_azimuth)
surface_tilt = tracker['surface_tilt']
surface_azimuth = tracker['surface_azimuth']
aoi = tracker['aoi']
# get irradiance
dni = data['Gb(n)'].values
ghi = data['G(h)'].values
dhi = data['Gd(h)'].values
surface_albedo = 0.25
temp_air = data['T2m'].values
dni_extra = pvlib.irradiance.get_extra_radiation(TIMES).values

# we use the Hay Davies transposition model
poa_sky_diffuse = pvlib.irradiance.get_sky_diffuse(
        surface_tilt, surface_azimuth, solar_zenith, solar_azimuth,
        dni, ghi, dhi, dni_extra=dni_extra, model='haydavies')
poa_ground_diffuse = pvlib.irradiance.get_ground_diffuse(
        surface_tilt, ghi, albedo=surface_albedo)
poa = pvlib.irradiance.poa_components(
        aoi, dni, poa_sky_diffuse, poa_ground_diffuse)
poa_direct = poa['poa_direct']
poa_diffuse = poa['poa_diffuse']
poa_global = poa['poa_global']
iam = pvlib.iam.ashrae(aoi)
effective_irradiance = poa_direct*iam + poa_diffuse
# module temperature
temp_cell = pvlib.temperature.pvsyst_cell(poa_global, temp_air)
# finally this is the magic
cecparams = pvlib.pvsystem.calcparams_cec(
        effective_irradiance, temp_cell,
        CECMOD_MONO.alpha_sc, CECMOD_MONO.a_ref,
        CECMOD_MONO.I_L_ref, CECMOD_MONO.I_o_ref,
        CECMOD_MONO.R_sh_ref, CECMOD_MONO.R_s, CECMOD_MONO.Adjust)
mpp = pvlib.pvsystem.max_power_point(*cecparams, method='newton')
mpp = pd.DataFrame(mpp, index=TIMES)
# the goods
mpp.p_mp.resample('D').sum().plot(title='Daily Energy')
plt.ylabel('Production [Wh]');
_images/Tutorial A - Single Diode Model_13_0.png
# now your turn, do the same thing for the poly-Si module.
# how does it compare?

String Length#

Before we can do the AC side we need to build up our array. The first thing is the string length, which is determined by the open circuit voltage, the lowest expected temperature at the site, and the open circuit temperature coefficient.

temp_ref = 25.0  # degC
dc_ac = 1.3
# maximum open circuit voltage
MAX_VOC = CECMOD_MONO.V_oc_ref + CECMOD_MONO.beta_oc * (temp_air.min() - temp_ref)
STRING_LENGTH = int(INVERTER_60K['Vdcmax'] // MAX_VOC)
STRING_VOLTAGE = STRING_LENGTH * MAX_VOC
STRING_OUTPUT = CECMOD_MONO.STC * STRING_LENGTH
STRING_COUNT = int(dc_ac * INVERTER_60K['Paco'] // STRING_OUTPUT)
DC_CAPACITY = STRING_COUNT * STRING_OUTPUT
DCAC = DC_CAPACITY / INVERTER_60K['Paco']
MAX_VOC, STRING_LENGTH, STRING_VOLTAGE, STRING_OUTPUT, STRING_COUNT, DC_CAPACITY, DCAC
(52.2110052, 15, 783.165078, 4500.45, 17, 76507.65, 1.2751275)

AC Output#

The Sandia grid inverter is a convenient model. The coefficients in the NREL SAM library are derived from California Energy Commision (CEC) testing (see CEC Solar Equipment List). Use pvlib.inverter.sandia to calculate AC output given DC voltage and power and the inverter parameters from the NREL SAM library which we downloaded earlier into INVERTER_60K for the SMA STP 60kW inverter.

EDAILY = mpp.p_mp * STRING_LENGTH * STRING_COUNT
AC_OUTPUT = pvlib.inverter.sandia(
    mpp.v_mp * STRING_LENGTH,
    mpp.p_mp * STRING_LENGTH * STRING_COUNT,
    INVERTER_60K)
AC_OUTPUT.max()
60000.0
# the goods
plt.rcParams['font.size'] = 14
ax = EDAILY.resample('D').sum().plot(figsize=(15, 10), label='DC', title='Daily Energy')
AC_OUTPUT.resample('D').sum().plot(ax=ax, label='AC')
plt.ylabel('Energy [Wh/day]')
plt.legend()
plt.grid()
_images/Tutorial A - Single Diode Model_19_0.png

Creative Commons License

This work is licensed under a Creative Commons Attribution 4.0 International License.