1 - Basics, Humidity, Design#

Module Humidity and Edge Seal Width#

Requirements:

  • weather file (psm3 preferred) demo file is provided

Objectives:

  1. Read in necessary weather data

  2. Generate solar position, POA, and module temperature

  3. Generate module humidities

  4. Calculate edge seal width

# 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.3.3
import os
import pandas as pd

import pvdeg
from pvdeg import DATA_DIR
# 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("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
pvdeg version  0.1.dev1+g4f38099

1. Reading in Weather Data and Site Meta-Data#

Most pvdeg functions have been standardized to operate from popular weather files such as TMY3, EPW, and PSM3. For high-performance computer users, NSRDB and other database fetching tools can be used. For these tutorials, we will use local weather files. Unless otherwise stated, functions require the following fields within a weather file:

  • date-time index

  • DNI

  • DHI

  • GHI

  • Temperature, Dry-Bulb

  • Temperature, Dew-Point

  • Wind Speed

  • Relative Humidity

And the following site-specific metadata. This should be contained in the weather file header.

  • Latitude

  • Longitude

  • Altitude

PSM_FILE = os.path.join(DATA_DIR,'psm3_demo.csv')

WEATHER, META = pvdeg.weather.read(PSM_FILE,'psm')

Let’s take a closer look at the dataframe (weather) and dictionary (meta) imported above. The structure will be used by most pvdeg functions.

WEATHER.head()
Year Month Day Hour Minute dni dhi ghi temp_air dew_point wind_speed relative_humidity
1999-01-01 00:30:00-07:00 1999 1 1 0 30 0.0 0.0 0.0 0.0 -5.0 1.8 79.39
1999-01-01 01:30:00-07:00 1999 1 1 1 30 0.0 0.0 0.0 0.0 -4.0 1.7 80.84
1999-01-01 02:30:00-07:00 1999 1 1 2 30 0.0 0.0 0.0 0.0 -4.0 1.5 82.98
1999-01-01 03:30:00-07:00 1999 1 1 3 30 0.0 0.0 0.0 0.0 -4.0 1.3 85.01
1999-01-01 04:30:00-07:00 1999 1 1 4 30 0.0 0.0 0.0 0.0 -4.0 1.3 85.81

Meta-Data will vary greatly between weather files. However, at a minimum they should all contain the fields required by pvdeg listed above. Let’s look at the meta-data generated by our PSM3 weather file. It provides much more information that we need, but at the very end are the three most important fields.

META
{'Source': 'NSRDB',
 'Location ID': '145809',
 'City': '-',
 'State': '-',
 'Country': '-',
 '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': '3.0.6',
 'latitude': 39.73,
 'longitude': -105.18,
 'altitude': 1820,
 'tz': -7}

We’ll be working with a lot of time series data, generally with a date-time index. If you’re processing an entire year of data, you don’t need to sort or filter the data. However, you may want to filter the data by a few different criteria. Below are some common examples.

  • Filter by date

    • month

    • range of months

  • Filter by sun-up hours

# Let's select the month of June
june_weather = WEATHER[ WEATHER.index.month == 6 ]
june_weather
Year Month Day Hour Minute dni dhi ghi temp_air dew_point wind_speed relative_humidity
1999-06-01 00:30:00-07:00 1999 6 1 0 30 0.0 0.0 0.0 6.0 0.0 1.7 79.15
1999-06-01 01:30:00-07:00 1999 6 1 1 30 0.0 0.0 0.0 6.0 0.0 1.7 75.62
1999-06-01 02:30:00-07:00 1999 6 1 2 30 0.0 0.0 0.0 6.0 0.0 1.5 78.51
1999-06-01 03:30:00-07:00 1999 6 1 3 30 0.0 0.0 0.0 5.0 0.0 1.3 76.90
1999-06-01 04:30:00-07:00 1999 6 1 4 30 0.0 0.0 0.0 6.0 0.0 1.3 75.75
... ... ... ... ... ... ... ... ... ... ... ... ...
1999-06-30 19:30:00-07:00 1999 6 30 19 30 0.0 0.0 0.0 21.0 6.0 0.6 48.54
1999-06-30 20:30:00-07:00 1999 6 30 20 30 0.0 0.0 0.0 19.0 7.0 0.4 55.36
1999-06-30 21:30:00-07:00 1999 6 30 21 30 0.0 0.0 0.0 17.0 7.0 0.5 66.32
1999-06-30 22:30:00-07:00 1999 6 30 22 30 0.0 0.0 0.0 15.0 7.0 0.8 75.74
1999-06-30 23:30:00-07:00 1999 6 30 23 30 0.0 0.0 0.0 13.0 7.0 0.8 80.16

720 rows × 12 columns

# Let's filter the summer months for the northern hemisphere
summer_months = [6,7,8,9]
summer_weather = WEATHER[ WEATHER.index.month.isin( summer_months ) ]
summer_weather
Year Month Day Hour Minute dni dhi ghi temp_air dew_point wind_speed relative_humidity
1999-06-01 00:30:00-07:00 1999 6 1 0 30 0.0 0.0 0.0 6.0 0.0 1.7 79.15
1999-06-01 01:30:00-07:00 1999 6 1 1 30 0.0 0.0 0.0 6.0 0.0 1.7 75.62
1999-06-01 02:30:00-07:00 1999 6 1 2 30 0.0 0.0 0.0 6.0 0.0 1.5 78.51
1999-06-01 03:30:00-07:00 1999 6 1 3 30 0.0 0.0 0.0 5.0 0.0 1.3 76.90
1999-06-01 04:30:00-07:00 1999 6 1 4 30 0.0 0.0 0.0 6.0 0.0 1.3 75.75
... ... ... ... ... ... ... ... ... ... ... ... ...
1999-09-30 19:30:00-07:00 1999 9 30 19 30 0.0 0.0 0.0 8.0 -1.0 2.6 58.57
1999-09-30 20:30:00-07:00 1999 9 30 20 30 0.0 0.0 0.0 7.0 -2.0 2.7 58.17
1999-09-30 21:30:00-07:00 1999 9 30 21 30 0.0 0.0 0.0 7.0 -3.0 2.9 54.27
1999-09-30 22:30:00-07:00 1999 9 30 22 30 0.0 0.0 0.0 7.0 -4.0 3.0 50.29
1999-09-30 23:30:00-07:00 1999 9 30 23 30 0.0 0.0 0.0 7.0 -5.0 3.0 46.91

2928 rows × 12 columns

# lets just select hours from the year where the sun is above the horizon
sunup_weather = WEATHER[ WEATHER['ghi'] > 0 ]
sunup_weather
Year Month Day Hour Minute dni dhi ghi temp_air dew_point wind_speed relative_humidity
1999-01-01 08:30:00-07:00 1999 1 1 8 30 65.0 65.0 76.0 1.0 -3.0 4.7 76.86
1999-01-01 09:30:00-07:00 1999 1 1 9 30 503.0 93.0 246.0 2.0 -2.0 6.3 80.37
1999-01-01 10:30:00-07:00 1999 1 1 10 30 617.0 109.0 355.0 3.0 -2.0 7.0 74.87
1999-01-01 11:30:00-07:00 1999 1 1 11 30 497.0 161.0 385.0 4.0 -3.0 6.8 70.73
1999-01-01 12:30:00-07:00 1999 1 1 12 30 0.0 128.0 128.0 4.0 -4.0 6.5 61.80
... ... ... ... ... ... ... ... ... ... ... ... ...
1999-12-31 12:30:00-07:00 1999 12 31 12 30 0.0 20.0 20.0 9.0 -8.0 1.6 31.45
1999-12-31 13:30:00-07:00 1999 12 31 13 30 562.0 125.0 354.0 8.0 -6.0 0.5 40.25
1999-12-31 14:30:00-07:00 1999 12 31 14 30 530.0 94.0 263.0 7.0 -4.0 0.4 53.19
1999-12-31 15:30:00-07:00 1999 12 31 15 30 636.0 38.0 159.0 5.0 -5.0 1.0 57.10
1999-12-31 16:30:00-07:00 1999 12 31 16 30 0.0 21.0 21.0 4.0 -6.0 1.3 56.22

4301 rows × 12 columns

2. Solar Position, POA, and Module Temperature#

Many operations with PVDEG require solar position, POA irradiance, and module temperature. Usually a pvdeg method will calculate these data sets automatically when it is required, however it will not keep the data for external use. For now, we will calculate them directly. This is helpful when you need to use the data for an external calculation. Here, we generate the solar position (azimuth, elevation, etc), plane of array irradiance (POA), and module temperature. PVDEG has wrappers for quickly using PVLIB to generate these figures with minimal input

sol_pos = pvdeg.spectral.solar_position(weather_df=WEATHER, meta=META)

poa_df = pvdeg.spectral.poa_irradiance(weather_df=WEATHER, meta=META)#, solar_position=sol_pos)

temp_mod = pvdeg.temperature.module(weather_df=WEATHER, meta=META)#, poa=poa_df)
The array tilt angle was not provided, therefore the latitude tilt of 39.7 was used.
The array azimuth was not provided, therefore an azimuth of 180.0 was used.
The array tilt angle was not provided, therefore the latitude tilt of 39.7 was used.
The array azimuth was not provided, therefore an azimuth of 180.0 was used.

For more advanced usage of these functions, refer to the documentation for pvdeg and pvlib.

Lets inspect the output from each of these functions

solar_position returns a datetime-indexed dataframe of solar position for the length and frequency given by the weather file.

sol_pos
apparent_zenith zenith apparent_elevation elevation azimuth equation_of_time
1999-01-01 00:30:00-07:00 162.416069 162.416069 -72.416069 -72.416069 20.152470 -3.300994
1999-01-01 01:30:00-07:00 155.313029 155.313029 -65.313029 -65.313029 53.821029 -3.320654
1999-01-01 02:30:00-07:00 144.992553 144.992553 -54.992553 -54.992553 72.531199 -3.340306
1999-01-01 03:30:00-07:00 133.692678 133.692678 -43.692678 -43.692678 84.789909 -3.359948
1999-01-01 04:30:00-07:00 122.172658 122.172658 -32.172658 -32.172658 94.462003 -3.379582
... ... ... ... ... ... ...
1999-12-31 19:30:00-07:00 120.682889 120.682889 -30.682889 -30.682889 264.307279 -3.093347
1999-12-31 20:30:00-07:00 132.197914 132.197914 -42.197914 -42.197914 273.785399 -3.113209
1999-12-31 21:30:00-07:00 143.553813 143.553813 -53.553813 -53.553813 285.552685 -3.133063
1999-12-31 22:30:00-07:00 154.080531 154.080531 -64.080531 -64.080531 303.037057 -3.152909
1999-12-31 23:30:00-07:00 161.842482 161.842482 -71.842482 -71.842482 334.208238 -3.172747

8760 rows × 6 columns

poa_irradiance returns a datetime-indexed dataframe as well. Each column contains either the global plane of array irradiance or a particular contribition. Lets look at a time in the index we expect to have daylight.

poa_df.loc['1999-01-01 13:30:00-07:00':'1999-01-01 17:30:00-07:00']
poa_global poa_direct poa_diffuse poa_sky_diffuse poa_ground_diffuse
1999-01-01 13:30:00-07:00 90.426538 0.0 90.426538 87.568717 2.857821
1999-01-01 14:30:00-07:00 21.921585 0.0 21.921585 21.228780 0.692805
1999-01-01 15:30:00-07:00 57.544160 0.0 57.544160 55.725547 1.818613
1999-01-01 16:30:00-07:00 7.307195 0.0 7.307195 7.076260 0.230935
1999-01-01 17:30:00-07:00 0.000000 0.0 0.000000 0.000000 0.000000

temperature.module has several optional parameters. Below is an example which explicity uses all of the default values. This function will return a datetime-indexed series matching the index of the given weather file. For more options, see the pvlib documentation for temperature models.

https://pvlib-python.readthedocs.io/en/stable/reference/pv_modeling/temperature.html

The optional arguments are:

  • poa : manually enter the POA Irradiance

  • temp_model : choose a PVLIB compatible temperature model

  • conf : choose a recognized module configuration

  • wind_speed_factor : change the empirical wind speed fit paramter

temp_mod = pvdeg.temperature.module(weather_df = WEATHER, meta = META,
                                    poa = poa_df,
                                    temp_model = 'sapm',
                                    conf= 'open_rack_glass_polymer')

temp_mod.loc['1999-01-01 13:30:00-07:00':'1999-01-01 17:30:00-07:00']
1999-01-01 13:30:00-07:00    4.639740
1999-01-01 14:30:00-07:00    2.425271
1999-01-01 15:30:00-07:00    1.249262
1999-01-01 16:30:00-07:00   -0.829008
1999-01-01 17:30:00-07:00   -2.000000
dtype: float64

3. Module Humidities#

PVDEG can be used to calculate the relative humidity of several layers within a PV module: the outside surface of the module, front ecapsulant, back encapsulant, and backsheet. This can be done with 2 techniques: Automatically and Manually.

3.a. Automatic#

Use the function humidity.module. This method does all of the calculations behind the scenes. It requires only 2 inputs (weather data and meta data). It will return a dataframe with all the layers of the module.

rh_module = pvdeg.humidity.module(weather_df=WEATHER, meta=META)
rh_module.head()
The array tilt angle was not provided, therefore the latitude tilt of 39.7 was used.
RH_surface_outside RH_front_encap RH_back_encap RH_backsheet
1999-01-01 00:30:00-07:00 79.39 44.198787 79.390000 79.390000
1999-01-01 01:30:00-07:00 80.84 44.198787 79.415220 80.127610
1999-01-01 02:30:00-07:00 82.98 44.198787 79.477223 81.228611
1999-01-01 03:30:00-07:00 85.01 44.198787 79.573455 82.291727
1999-01-01 04:30:00-07:00 85.81 44.198787 79.681927 82.745964

3.b. Manual#

Use the individual functions to calculate the RH in each layer. These are named and require more extensive input parameters. This must be done in the correct order:

  1. Surface Outside

  2. Front Encapsulant

  3. Back Encapsulant

  4. Backsheet

rh_surface_outside = pvdeg.humidity.surface_outside(rh_ambient=WEATHER['relative_humidity'],
                                                       temp_ambient=WEATHER['temp_air'],
                                                       temp_module=temp_mod)

rh_front_encap = pvdeg.humidity.front_encap(rh_ambient=rh_surface_outside,
                                               temp_ambient=WEATHER['temp_air'],
                                               temp_module=temp_mod)

rh_back_encap = pvdeg.humidity.back_encap(rh_ambient=rh_surface_outside,
                                             temp_ambient=WEATHER['temp_air'],
                                             temp_module=temp_mod)

rh_backsheet = pvdeg.humidity.backsheet_from_encap(rh_back_encap=rh_back_encap,
                                                      rh_surface_outside=WEATHER['relative_humidity'])

4. Design: Edge Seal Width#

pvdeg.design.edge_seal_width calculates the width [cm] required for several years of water ingress. If you do not specify the number of years, it will use the default value of 25. As with most pvdeg functions, we can automatically generate all necessary data if we pass the two arguments (weather_df, meta).

edge_seal_width = pvdeg.design.edge_seal_width(weather_df=WEATHER, meta=META)
edge_seal_width
0.7170984345065904

Lets look at the calculation step by step. To get the estimated edge seal width, we need the following:

  1. Humidity Saturation Point

  2. Edge seal ingress rate ( k ) or the estimated rate of water ingress through edge seal in [cm/hour^0.5]

psat, psat_avg = pvdeg.humidity.psat(WEATHER['temp_air'])

k = pvdeg.design.edge_seal_ingress_rate(avg_psat=psat_avg)

edge_seal_width = pvdeg.design.edge_seal_width(weather_df=WEATHER, meta=META,
                                               k=k, years=25)

print(edge_seal_width)
0.7170984345065904

Alternate Edge Seal Width#

If you have the dew point, or wet-bulb temperature, you can calculate the edge seal width directly.

edge_seal_width = pvdeg.design.edge_seal_width(weather_df=WEATHER, meta=META, from_dew_point=True)
edge_seal_width
0.44995358709586686