Skip to content

Commit

Permalink
sam iotools pvlib#1371
Browse files Browse the repository at this point in the history
adds a sam.py, and pytest
  • Loading branch information
shirubana committed Sep 22, 2022
1 parent 73965c2 commit 100d1f4
Show file tree
Hide file tree
Showing 3 changed files with 194 additions and 0 deletions.
1 change: 1 addition & 0 deletions pvlib/iotools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,4 @@
from pvlib.iotools.sodapro import get_cams # noqa: F401
from pvlib.iotools.sodapro import read_cams # noqa: F401
from pvlib.iotools.sodapro import parse_cams # noqa: F401
from pvlib.iotools.sam import saveSAM_WeatherFile, tz_convert # noqa: F401
164 changes: 164 additions & 0 deletions pvlib/iotools/sam.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
"""Functions for reading and writing SAM data files."""

import pandas as pd

def saveSAM_WeatherFile(data, metadata, savefile='SAM_WeatherFile.csv', standardSAM = True, includeminute=False):
"""
Saves a dataframe with weather data from pvlib format on SAM-friendly format.
Parameters
-----------
data : pandas.DataFrame
timeseries data in PVLib format. Should be TZ converted (not UTC). Ideally it is one sequential year data; if not suggested to use standardSAM = False.
metdata : dictionary
Dictionary with at least 'latitude', 'longitude', 'elevation', 'source', and 'TZ' for timezone.
savefile : str
Name of file to save output as.
standardSAM : boolean
This checks the dataframe to avoid having a leap day, then averages it to SAM style (closed to the right),
and fills the years so it starst on YEAR/1/1 0:0 and ends on YEAR/12/31 23:00.
includeminute ; Bool
For hourly data, if SAM input does not have Minutes, it calculates the sun position 30 minutes
prior to the hour (i.e. 12 timestamp means sun position at 11:30)
If minutes are included, it will calculate the sun position at the time of the timestamp (12:00 at 12:00)
Set to true if resolution of data is sub-hourly.
Returns
-------
Nothing, it just writes the file.
"""

def _is_leap_and_29Feb(s):
''' Creates a mask to help remove Leap Years. Obtained from:
https://stackoverflow.com/questions/34966422/remove-leap-year-day-from-pandas-dataframe/34966636
'''
return (s.index.year % 4 == 0) & \
((s.index.year % 100 != 0) | (s.index.year % 400 == 0)) & \
(s.index.month == 2) & (s.index.day == 29)

def _averageSAMStyle(df, interval='60T', closed='right', label='right'):
''' Averages subhourly data into hourly data in SAM's expected format.
'''
try:
df = df.resample(interval, closed=closed, label=label).mean() #
except:
print('Warning - unable to average')
return df

def _fillYearSAMStyle(df, freq='60T'):
''' Fills year
'''
# add zeros for the rest of the year
if freq is None:
try:
freq = pd.infer_freq(df.index)
except:
freq = '60T' # 15 minute data by default
# add a timepoint at the end of the year
# idx = df.index
# apply correct TZ info (if applicable)
tzinfo = df.index.tzinfo
starttime = pd.to_datetime('%s-%s-%s %s:%s' % (df.index.year[0],1,1,0,0 ) ).tz_localize(tzinfo)
endtime = pd.to_datetime('%s-%s-%s %s:%s' % (df.index.year[-1],12,31,23,60-int(freq[:-1])) ).tz_localize(tzinfo)

df2 = _averageSAMStyle(df, freq)
df2.iloc[0] = 0 # set first datapt to zero to forward fill w zeros
df2.iloc[-1] = 0 # set last datapt to zero to forward fill w zeros
df2.loc[starttime] = 0
df2.loc[endtime] = 0
df2 = df2.resample(freq).ffill()
return df2


# Modify this to cut into different years. Right now handles partial year and sub-hourly interval.
if standardSAM:
filterdatesLeapYear = ~(_is_leap_and_29Feb(data))
data = data[filterdatesLeapYear]
data = _fillYearSAMStyle(data)


# metadata
latitude=metadata['latitude']
longitude=metadata['longitude']
elevation=metadata['elevation']
timezone_offset = metadata['TZ']
source = metadata['source']

# make a header
header = '\n'.join(
[ 'Source,Latitude,Longitude,Time Zone,Elevation',
source + ',' + str(latitude) + ',' + str(longitude) + ',' + str(timezone_offset) + ',' + str(elevation)]) + '\n'

savedata = pd.DataFrame({'Year':data.index.year, 'Month':data.index.month, 'Day':data.index.day,
'Hour':data.index.hour})

if includeminute:
savedata['Minute'] = data.index.minute

windspeed = list(data.wind_speed)
temp_amb = list(data.temp_air)
savedata['Wspd'] = windspeed
savedata['Tdry'] = temp_amb

if 'dni' in data:
dni = list(data.dni)
savedata['DHI'] = dni

if 'dhi' in data:
dhi = list(data.dhi)
savedata['DNI'] = dhi

if 'ghi' in data:
ghi = list(data.ghi)
savedata['GHI'] = ghi

if 'poa' in data: # This is a nifty function of SAM for using field measured POA irradiance!
poa = list(data.poa)
savedata['POA'] = poa

if 'albedo' in data:
albedo = list(data.albedo)
savedata['Albedo'] = albedo

with open(savefile, 'w', newline='') as ict:
# Write the header lines, including the index variable for
# the last one if you're letting Pandas produce that for you.
# (see above).
for line in header:
ict.write(line)

savedata.to_csv(ict, index=False)


def tz_convert(df, tz_convert_val, metadata=None):
"""
Support function to convert metdata to a different local timezone. Particularly for
GIS weather files which are returned in UTC by default.
Parameters
----------
df : DataFrame
A dataframe in UTC timezone
tz_convert_val : int
Convert timezone to this fixed value, following ISO standard
(negative values indicating West of UTC.)
Returns: metdata, metadata
Returns
-------
df : DataFrame
Dataframe in the converted local timezone.
metadata : dict
Adds (or updates) the existing Timezone in the metadata dictionary
"""
import pytz
if (type(tz_convert_val) == int) | (type(tz_convert_val) == float):
df = df.tz_convert(pytz.FixedOffset(tz_convert_val*60))

if metadata is not None:
metadata['TZ'] = tz_convert_val
return df, metadata
return df
29 changes: 29 additions & 0 deletions pvlib/tests/iotools/test_sam.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"""
test the SAM IO tools
"""
import pandas as pd
from pvlib.iotools import get_pvgis_tmy, read_pvgis_hourly
from pvlib.iotools import saveSAM_WeatherFile, tz_convert
from ..conftest import (DATA_DIR, RERUNS, RERUNS_DELAY, assert_frame_equal,
fail_on_pvlib_version)

# PVGIS Hourly tests
# The test files are actual files from PVGIS where the data section have been
# reduced to only a few lines
testfile_radiation_csv = DATA_DIR / \
'pvgis_hourly_Timeseries_45.000_8.000_SA_30deg_0deg_2016_2016.csv'

# REMOVE
testfile_radiation_csv = r'C:\Users\sayala\Documents\GitHub\pvlib-python\pvlib\data\pvgis_hourly_Timeseries_45.000_8.000_SA_30deg_0deg_2016_2016.csv'

def test_saveSAM_WeatherFile():
data, inputs, metadata = read_pvgis_hourly(testfile_radiation_csv, map_variables=True)#, pvgis_format=pvgis_format)
metadata = {'latitude': inputs['latitude'],
'longitude': inputs['longitude'],
'elevation': inputs['elevation'],
'source': 'User-generated'}
metadata['TZ'] = -7
data = tz_convert(data, tz_convert_val=metadata['TZ'])
coerce_year=2021 # read_pvgis_hourly does not coerce_year, so doing it here.
data.index = data.index.map(lambda dt: dt.replace(year=coerce_year))
saveSAM_WeatherFile(data, metadata, savefile='test_SAMWeatherFile.csv', standardSAM=True)

0 comments on commit 100d1f4

Please sign in to comment.