ASTM Demonstration#

PVDeg Logo

Steps:

  1. Import weather data

  2. Calculate installation standoff

  3. Calculate installation standoff - with more detail

Background:

This example demonstrates the calculation of a minimum standoff distance necessary for roof-mounted PV modules to ensure that the \(T_{98}\) operational temperature remains under 70°C, in which case the more rigorous thermal stability testing requirements of IEC TS 63126 would not needed to be considered. We use data from [Fuentes, 1987] to model the approximate exponential decay in temperature, \(T(X)\), with increasing standoff distance, \(X\), as,

\[ X = -X_0 \ln\left(1-\frac{T_0-T}{\Delta T}\right)\]

where \(T_0\) is the temperature for \(X=0\) (insulated back) and \(\Delta T\) is the temperature difference between an insulated back (\(X=0\)) and open rack mounting configuration (\(X=\infty)\).

The following figure showcases this calulation for the entire United States. We used pvlib and data from the National Solar Radiation Database (NSRDB) to calculate the module temperatures for different mounting configuration and applied our model to obtain the standoff distance for roof-mounted PV systems.

# 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 pvdeg==0.1.0
import os
import pvlib
import pvdeg
import pandas as pd
from pvdeg import DATA_DIR
import matplotlib.pyplot as plt
# This information helps with debugging and getting support :)
import sys, platform
print("Working on a ", platform.system(), platform.release())
print("Python version ", sys.version)
print("Pandas version ", pd.__version__)
print("pvlib version ", pvlib.__version__)
print("pvdeg version ", pvdeg.__version__)
Working on a  Linux 6.5.0-1025-azure
Python version  3.11.9 (main, Jul 15 2024, 21:50:21) [GCC 11.4.0]
Pandas version  2.2.2
pvlib version  0.10.3
pvdeg version  0.1.dev1+g4f38099

1. Import Weather Data#

The function has two minimum requirements:

  • Weather data containing (at least) DNI, DHI, GHI, Temperature, RH, Wind-Speed

  • Site meta-data containing (at least) Latitude, Longitude, Time Zone

Where to get Free Solar Irradiance Data?#

There are many different sources of solar irradiance data. For your projects, these are some of the most common:

  • NSRDB - National Solar Radiation Database. You can access data through the website for many locations accross the world, or you can use their web API to download data programmatically. An “API” is an “application programming interface”, and a “web API” is a programming interface that allows you to write code to interact with web services like the NSRDB.

  • EPW - Energy Plus Weather data is available for many locations accross the world. It’s in its own format file (‘EPW’) so you can’t open it easily in a spreadsheet program like Excel, but you can use pvlib.iotools.read_epw() to get it into a dataframe and use it.

  • PVGIS - Free global weather data provided by the European Union and derived from many govermental agencies including the NSRDB. PVGIS also provides a web API. You can get PVGIS TMY data using pvlib.iotools.get_pvgis_tmy().

  • Perhaps another useful link: https://sam.nrel.gov/weather-data.html

Where else can you get historical irradiance data?#

There are several commercial providers of solar irradiance data. Data is available at different spatial and time resolutions. Each provider offers data under subscription that will provide access to irradiance (and other weather variables) via API to leverage in python.

NSRDB Example

NREL API Key#

At the NREL Developer Network, there are APIs to a lot of valuable solar resources like weather data from the NSRDB, operational data from PVDAQ, or indicative calculations using PVWatts. In order to use these resources from NREL, you need to register for a free API key. You can test out the APIs using the DEMO_KEY but it has limited bandwidth compared to the usage limit for registered users. NREL has some API usage instructions, but pvlib has a few builtin functions, like pvlib.iotools.get_psm3(), that wrap the NREL API, and call them for you to make it much easier to use. Skip ahead to the next section to learn more. But before you do…

Please pause now to visit https://developer.nrel.gov/signup/ and get an API key.

Application Programming Interface (API)#

What exactly is an API? Nowadays, the phrase is used interchangeably with a “web API” but in general an API is just a recipe for how to interface with a application programmatically, IE: in code. An API could be as simple as a function signature or its published documentation, EG: the API for the solarposition function is you give it an ISO8601 formatted date with a timezone, the latitude, longitude, and elevation as numbers, and it returns the zenith and azimuth as numbers.

A web API is the same, except the application is a web service, that you access at its URL using web methods. We won’t go into too much more detail here, but the most common web method is GET which is pretty self explanatory. Look over the NREL web usage instructions for some examples, but interacting with a web API can be as easy as entering a URL into a browser. Try the URL below to get the PVWatts energy output for a fixed tilt site in Broomfield, CO.

https://developer.nrel.gov/api/pvwatts/v6.json?api_key=DEMO_KEY&lat=40&lon=-105&system_capacity=4&azimuth=180&tilt=40&array_type=1&module_type=1&losses=10

In addition to just using your browser, you can also access web APIs programmatically. The most popular Python package to interact with web APIs is requests. There’s also free open source command-line tools like cURL and HTTPie, and a popular nagware/freemium GUI application called Postman.

If you have an NREL API key please enter it in the next cell.

NREL_API_KEY = None  # <-- please set your NREL API key here

# note you must use "quotes" around your key, for example:
# NREL_API_KEY = 'DEMO_KEY'  # single or double both work fine

# during the live tutorial, we've stored a dedicated key on our server
if NREL_API_KEY is None:
    try:
        NREL_API_KEY = os.environ['NREL_API_KEY']  # get dedicated key for tutorial from servier
    except KeyError:
        NREL_API_KEY = 'DEMO_KEY'  # OK for this demo, but better to get your own key

Fetching TMYs from the NSRDB#

The NSRDB, one of many sources of weather data intended for PV modeling, is free and easy to access using pvlib. As an example, we’ll fetch a TMY dataset for Phoenix, AZ at coordinates (33.4484, -112.0740).

This function uses pvdeg.weather.get(), which returns a Python dictionary of metadata and a Pandas dataframe of the timeseries weather data.

This function internally leverages pvlib.iotools.get_psm3(). However, for some of the NSRDB data relative humidity is not a given parameter, and pvdeg calculates the values from the downloaded data as an internal processing step.

# This cell does not run on a Collab instal. WE are troubleshooting it, but the next cell performs the same request directly with PVLib.
# If TMY is requested though, relative_humidity will not be included as NSRDB TMY dataset does not have it.
'''
weather_db = 'PSM3'
weather_id = (33.4484, -112.0740)
weather_arg = {'api_key': NREL_API_KEY,
               'email': 'user@mail.com',
               'names': '2021',   # tmy is another common option; right now requesting only 2021 data
               'attributes': [],  # grabs all. to select, could be 'air_temperature', 'dew_point', 'dhi',
               # 'dni', 'ghi', 'surface_albedo', 'surface_pressure', 'wind_direction', 'wind_speed'
               'map_variables': True,
               'leap_day': False}

weather_df, meta = pvdeg.weather.get(weather_db, weather_id, **weather_arg)
''';
weather_df, meta = pvlib.iotools.get_psm3(
    latitude=33.4484, longitude=-112.0740,
    api_key=NREL_API_KEY,
    email='silvana.ovaitt@nrel.gov',  # <-- any email works here fine
    names='2021',
    map_variables=True,
    attributes=[],
    leap_day=False)
weather_df
Year Month Day Hour Minute temp_air dhi_clear dni_clear ghi_clear Cloud Type ... ghi relative_humidity solar_zenith albedo pressure precipitable_water wind_direction wind_speed Global Horizontal UV Irradiance (280-400nm) Global Horizontal UV Irradiance (295-385nm)
2021-01-01 00:30:00-07:00 2021 1 1 0 30 6.5 0.0 0.0 0.0 0 ... 0.0 35.09 169.51 0.16 968.0 1.0 38.0 1.8 0.0 0.0
2021-01-01 01:30:00-07:00 2021 1 1 1 30 6.0 0.0 0.0 0.0 4 ... 0.0 36.34 163.48 0.16 968.0 1.0 42.0 1.8 0.0 0.0
2021-01-01 02:30:00-07:00 2021 1 1 2 30 5.5 0.0 0.0 0.0 4 ... 0.0 37.37 152.07 0.16 968.0 1.0 45.0 1.8 0.0 0.0
2021-01-01 03:30:00-07:00 2021 1 1 3 30 5.1 0.0 0.0 0.0 4 ... 0.0 38.47 139.71 0.16 968.0 1.0 46.0 1.7 0.0 0.0
2021-01-01 04:30:00-07:00 2021 1 1 4 30 4.7 0.0 0.0 0.0 0 ... 0.0 39.97 127.21 0.16 969.0 1.0 46.0 1.8 0.0 0.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
2021-12-31 19:30:00-07:00 2021 12 31 19 30 9.1 0.0 0.0 0.0 7 ... 0.0 27.14 114.11 0.16 967.0 0.9 28.0 1.2 0.0 0.0
2021-12-31 20:30:00-07:00 2021 12 31 20 30 8.5 0.0 0.0 0.0 7 ... 0.0 28.57 126.45 0.16 967.0 0.9 31.0 1.3 0.0 0.0
2021-12-31 21:30:00-07:00 2021 12 31 21 30 7.8 0.0 0.0 0.0 7 ... 0.0 29.85 138.95 0.16 967.0 0.9 33.0 1.4 0.0 0.0
2021-12-31 22:30:00-07:00 2021 12 31 22 30 7.4 0.0 0.0 0.0 7 ... 0.0 31.44 151.32 0.16 968.0 1.0 33.0 1.5 0.0 0.0
2021-12-31 23:30:00-07:00 2021 12 31 23 30 7.0 0.0 0.0 0.0 0 ... 0.0 33.22 162.85 0.16 968.0 1.0 34.0 1.6 0.0 0.0

8760 rows × 24 columns

meta
{'Source': 'NSRDB',
 'Location ID': '323705',
 'City': '-',
 'State': '-',
 'Country': '-',
 'Time Zone': -7,
 'Local Time Zone': -7,
 'Clearsky DHI Units': 'w/m2',
 'Clearsky DNI Units': 'w/m2',
 'Clearsky GHI Units': 'w/m2',
 'Dew Point Units': 'c',
 'DHI Units': 'w/m2',
 'DNI Units': 'w/m2',
 'GHI Units': 'w/m2',
 'Solar Zenith Angle Units': 'Degree',
 'Temperature Units': 'c',
 'Pressure Units': 'mbar',
 'Relative Humidity Units': '%',
 'Precipitable Water Units': 'cm',
 'Wind Direction Units': 'Degrees',
 'Wind Speed Units': 'm/s',
 'Cloud Type -15': 'N/A',
 'Cloud Type 0': 'Clear',
 'Cloud Type 1': 'Probably Clear',
 'Cloud Type 2': 'Fog',
 'Cloud Type 3': 'Water',
 'Cloud Type 4': 'Super-Cooled Water',
 'Cloud Type 5': 'Mixed',
 'Cloud Type 6': 'Opaque Ice',
 'Cloud Type 7': 'Cirrus',
 'Cloud Type 8': 'Overlapping',
 'Cloud Type 9': 'Overshooting',
 'Cloud Type 10': 'Unknown',
 'Cloud Type 11': 'Dust',
 'Cloud Type 12': 'Smoke',
 'Fill Flag 0': 'N/A',
 'Fill Flag 1': 'Missing Image',
 'Fill Flag 2': 'Low Irradiance',
 'Fill Flag 3': 'Exceeds Clearsky',
 'Fill Flag 4': 'Missing CLoud Properties',
 'Fill Flag 5': 'Rayleigh Violation',
 'Surface Albedo Units': 'N/A',
 'Version': 'v3.2.2',
 'latitude': 33.45,
 'longitude': -112.06,
 'altitude': 334}
weather_df.head()
Year Month Day Hour Minute temp_air dhi_clear dni_clear ghi_clear Cloud Type ... ghi relative_humidity solar_zenith albedo pressure precipitable_water wind_direction wind_speed Global Horizontal UV Irradiance (280-400nm) Global Horizontal UV Irradiance (295-385nm)
2021-01-01 00:30:00-07:00 2021 1 1 0 30 6.5 0.0 0.0 0.0 0 ... 0.0 35.09 169.51 0.16 968.0 1.0 38.0 1.8 0.0 0.0
2021-01-01 01:30:00-07:00 2021 1 1 1 30 6.0 0.0 0.0 0.0 4 ... 0.0 36.34 163.48 0.16 968.0 1.0 42.0 1.8 0.0 0.0
2021-01-01 02:30:00-07:00 2021 1 1 2 30 5.5 0.0 0.0 0.0 4 ... 0.0 37.37 152.07 0.16 968.0 1.0 45.0 1.8 0.0 0.0
2021-01-01 03:30:00-07:00 2021 1 1 3 30 5.1 0.0 0.0 0.0 4 ... 0.0 38.47 139.71 0.16 968.0 1.0 46.0 1.7 0.0 0.0
2021-01-01 04:30:00-07:00 2021 1 1 4 30 4.7 0.0 0.0 0.0 0 ... 0.0 39.97 127.21 0.16 969.0 1.0 46.0 1.8 0.0 0.0

5 rows × 24 columns

fig, ax1 = plt.subplots(figsize=(9, 6))
# Instantiate a second axes that shares the same x-axis
ax1.plot(weather_df.loc['2021-06-28 05:00:00-07:00':'2021-06-28 20:00:00-07:00']['dni_clear'], label='DNI')
ax2 = ax1.twinx()
ax2.plot(weather_df.loc['2021-06-28 05:00:00-07:00':'2021-06-28 20:00:00-07:00']['temp_air'], 'r', label='Temperature')
ax1.set_ylim([0,1000])
ax2.set_ylim([0,50])
ax1.set_ylabel('DNI')
ax2.set_ylabel('Temperature $\degree$C');
../_images/61d358d4944d28856e0206b4e382075cec522ee28dfb620bf8c475aec2377c3a.png

2. Calculate Installation Standoff - Level 1#

We use pvlib.standards.calc_standoff() which takes at minimum the weather data and metadata, and returns the minimum installation distance in centimeters.

standoff = pvdeg.standards.standoff(weather_df=weather_df, meta=meta)
The array tilt angle was not provided, therefore the latitude tilt of 33.5 was used.
The array azimuth was not provided, therefore an azimuth of 180.0 was used.
print("Minimum installation distance:", standoff['x'])
Minimum installation distance: 0    11.12151
Name: x, dtype: float64

3. Calculate Installation Standoff - Level 2#

Let’s take a closer look at the function and some optional parameters.

  • level : 1 or 2 (see IEC TS 63216)

  • tilt and azimuth : tilt from horizontal of PV module and azimuth in degrees from North

  • sky_model : pvlib compatible model for generating sky characteristics (Options: ‘isotropic’, ‘klucher’, ‘haydavies’, ‘reindl’, ‘king’, ‘perez’)

  • temp_model : pvlib compatible module temperature model (Options: ‘sapm’, ‘pvsyst’, ‘faiman’, ‘sandia’)

  • module_type : basic module construction (Options: ‘glass_polymer’, ‘glass_glass’)

  • x_0 : thermal decay constant [cm] (see documentation)

  • wind_speed_factor : Wind speed correction factor to account for different wind speed measurement heights between weather database (e.g. NSRDB) and the tempeature model (e.g. SAPM)

standoff = pvdeg.standards.standoff(weather_df=weather_df, meta=meta, T98=70)
The array tilt angle was not provided, therefore the latitude tilt of 33.5 was used.
The array azimuth was not provided, therefore an azimuth of 180.0 was used.
print("Minimum installation distance:", standoff['x'])
Minimum installation distance: 0    11.12151
Name: x, dtype: float64