1 - Basics, Humidity, Design#
Module Humidity and Edge Seal Width#
Requirements:
weather file (psm3 preferred) demo file is provided
Objectives:
Read in necessary weather data
Generate solar position, POA, and module temperature
Generate module humidities
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:
Surface Outside
Front Encapsulant
Back Encapsulant
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:
Humidity Saturation Point
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