2. 2019-06 MetPVNet¶
We calibrate the Pyrnet (30 stations with two pyranometer setup) for MetPVNet 2018/2019 measurement campaigns. Cross-calibration is done versus MORDOR3 global pyranometer.
Pyranometer data is processed with Pyrnet processing software: https://gitea.tropos.de/walther/Pyrnet_processing.git (20.06.2019)
#[notes]
#Python cells begin with a tag and dependency line:
#[tag] =>[tag_of_cell_mandatory_to_run_before]
2.1. Imports¶
#[imports]
# python modules
import os
import xarray as xr
import numpy as np
import pandas as pd
from matplotlib import pyplot
2.2. Definitions¶
Set data paths and threshholds
#[definitions] =>[imports]
# database path
# mordor path suffix - with wildcards to get all files for that period
mpf="mordor/201906_tropos/ncdata/2019/06/*_Rad*.nc"
# pyrnet path suffix - lvl1 means uncalibrated raw data in nc standard format
ppf="/pyrnet/201906_troposcalib/lvl1/"
#get involved station IDs of pyranometer net
stations=[]
for fname in os.listdir(pf+ppf):
if fname[:3]=='Pyr':
stations.append(int(fname.split('_')[1]))
stations=np.sort(stations)
#stations=[1]
# Factor for ADC gain (300) and unit transformation [V -> uV]
gain_units_factor=1e6/300.0
#calibration periode
start_date=np.datetime64("2019-06-08")
end_date=np.datetime64("2019-06-17")
2.3. MORDOR3 dataset¶
Currently (07.2019) MORDOR netcdf data is still not fully developed in terms of variable name / dimension connection for optimal xarray use. This will be change in future. Check the code carefully if you run for later calibration
#[mordor_data] =>[definitions]
#load mordor radiation data
mds=xr.open_mfdataset(pf+mpf)
#adjust handling of dimension and time variable in mordor nc file
mds=mds.rename({'t':'time'})
mds=mds.set_coords('time')
## Preselect data to reduce ram usage
#drop data after pyrnet was removed from roof
mds=mds.where(mds['time']<end_date,drop=True)
#drop data of first two days because of time synchronous issues of measure laptop
mds=mds.where(mds['time']>start_date,drop=True)
#print mds header
print(mds)
<xarray.Dataset>
Dimensions: (time: 7749295)
Coordinates:
* time (time) datetime64[ns] 2019-06-08T00:00:00.100000 ... 2019-06-16T23:59:59.900000
Data variables:
GHI (time) float64 dask.array<shape=(7749295,), chunksize=(861142,)>
UPI_lw (time) float64 dask.array<shape=(7749295,), chunksize=(861142,)>
UPI (time) float64 dask.array<shape=(7749295,), chunksize=(861142,)>
DNI (time) float64 dask.array<shape=(7749295,), chunksize=(861142,)>
szen (time) float32 dask.array<shape=(7749295,), chunksize=(861142,)>
sazi (time) float32 dask.array<shape=(7749295,), chunksize=(861142,)>
DHI (time) float64 dask.array<shape=(7749295,), chunksize=(861142,)>
GHI_lw (time) float64 dask.array<shape=(7749295,), chunksize=(861142,)>
Attributes:
earth_sun_distance: 1.01490394190788
earth_sun_distance_units: AU
Title: Radiaton data observed with MORDOR3
Institution: Leibniz Institute for Tropospheric Research (T...
Contact_person: Hartwig Deneke plus satellite group, sat@tropo...
Source: Product derived from MObile RaDiation ObseRvat...
History: Data file generated at TROPOS_calibration at 2...
Conventions: MESOR
Author: Jonas Witthuhn (witthuhn@tropos.de)
License: For non-commercial use only.
latitude: 51.35
longitude: 12.44
latitude_units: deg N
lontitude_units: deg E
2.4. Calibration¶
Calibration follows ISO 9847 in [Indian Standard - Calibration Field Pyranometers by comparison to a reference pyranometer] [1].
Cloudy sky threatment is applied due to variing conditions during the calibration periode.
2.4.1. Step 1¶
Drop Night measures and low signal measures from pyranometer data. Since calibration without incoming radiation doesnt work.
These data is kept for calibration:
solar zenith angle < 80° ( as recommended in [1])
Measured Voltage > 0.033 V (drop the lowest 1%)
2.4.2. Step 2¶
Fit MORDOR reference data to selected Pyranometer data.
2.4.3. Calculating the calibration factor¶
We define here the equations used before moving to step 3.
Calculate series of calibration factor for this dataset following [1] 5.4.1.1 equation (1).
Our final calibration equation looks like this: $$ Irradiance = Volts * 10^{-6} * gain^{-1} * calibration^{-1} $$ With:
Irradiance: Calibrated irradiance Value [Wm-2]
Volts: Measured voltage [V] -> to [uV] with $10^{-6}$
gain: ADC gain = 300
calibration: calibration factor with units [uV W-1 m2] Therefore we calculate: $$ C(ij) = \frac{V(ij) * 10^{-6}}{300 * I(ij)} $$ , for each timestep (j, hourly series ; i instant sample).
I - Irradiance from MORDOR
V - Voltage from Pyranometer to calibrate
C - resulting calibration factor
To get rid of outliers/ arbitrary shading etc. we calculate the integratet series of calibration following [1] 5.4.1.1 equation (2): $$ C(j) = \frac{10^{-6}}{300}*\frac{[V(j)]{int}}{[I(j){int}]} $$ , with $ []_{int} $ beeing integrated values over intervals of one hour.
2.4.4. Step 3¶
Remove outliers from series using xarray grouping and apply function. The following functions removes outliers (deviation more than 2% according to [1]) from a selected group. This step involves calculating calibration series and the integration of one hour intervals to smooth out high variable situation, which would break the calibration even when time synchronization is slightly off. Also this gets rid of some random shading events like birds / chimney / rods in line of sigth, which would affect calibration otherwise. We following [1] 5.4.1.1 equation (2) here.
2.4.5. Step 4¶
The series of measured voltage and irradiance is now without outliers. So we use equation 1 again to calculate from this reduced series the calibration factor for the instant samples.
2.4.6. Step 5¶
We just found the Calibration factor to be the mean of the reduced calibration factor series and the uncertainty to be the standard deviation of this reduced series. Steo 3, 4 and 5 are done for every pyranometer seperate
[1] https://archive.org/details/gov.in.is.iso.9847.1992
#[remove_function] => [imports]
##function for outlier removal
def remove_outliers(x):
"""
x is an xarray dataset containing these variables:
coords: 'time' - datetime64
'gain_units_factor' - float - to scale the calibration
'volts' - array - voltage measures of pyranometer
'irradiance' - array - measured irradiance of mordor
"""
#calculate calibration series for single samples
C=x['volts']*x['gain_units_factor']/x['irradiance']
#integrated series
ix=x.integrate('time')
M=ix['volts']*x['gain_units_factor']/ix['irradiance']
while np.any(np.abs(C-M)>0.02*M):
#calculate as long there are outliers deviating more than 2 percent
x=x.where(np.abs(C-M)<0.02*M)
C=x['volts']*x['gain_units_factor']/x['irradiance']
#integrated series
ix=x.integrate('time')
M=ix['volts']*x['gain_units_factor']/ix['irradiance']
#return the reduced dataset x
return x
#[calibrations] =>[mordor_data][remove_function]
#preparing output table
print('BOX , C(pyr1), stdC(pyr1) , C(pyr_tilt) , stdC(pyr_tilt)')
with open('new_calibration.csv','w') as out:
out.write('#BOX , C(pyr1), stdC(pyr1) , C(pyr_tilt) , stdC(pyr_tilt)\n')
# do calibration for all stations
for st in stations:
#load uncalibrated pyrnet data
pds=xr.open_mfdataset(str(pf+ppf+
"Pyr_%s/"%(str(st).zfill(3))+
"metpvnet_preliminary_PYRNET_*_%s_lvl1.nc"%(str(st).zfill(3))))
#drop some nan values along time dimension
pds=pds.dropna('time')
## Step 1 #############################################
#drop data after pyrnet was removed from roof
pds=pds.where(pds['time']<end_date,drop=True)
#drop data of first two days because of time synchronous issues of measure laptop
pds=pds.where(pds['time']>start_date,drop=True)
#drop night measurements
pds=pds.where(pds['sza']<80,drop=True)
#drop low and nan measures
pds=pds.where(pds['ghi']>0.033,drop=True)
## Step 2 #############################################
#fit mordor measures to pyranometer
tds=mds.reindex_like(pds)
## Step 3 #############################################
##Calibration pyr1
#preparing selection dataset for calibration
Cds=xr.Dataset({'time':pds['time'],
'gain_units_factor':gain_units_factor,
'irradiance':('time',tds['GHI']),
'volts':('time',pds['ghi'])})
#do hourly samples as reccomended in ISO 9842
Cds=Cds.groupby('time.hour').apply(remove_outliers)
## Step 4 #############################################
# calculate the calbration series without outliers
C=Cds['volts']*gain_units_factor/Cds['irradiance']
## Step 5 #############################################
# we found the calibration for pyranometer 1
Cpyr1=np.nanmean(C.values)
uCpyr1=np.nanstd(C.values)
## Step 3 #############################################
##Calibration pyr2
#preparing selection dataset for calibration
Cds=xr.Dataset({'time':pds['time'],
'gain_units_factor':gain_units_factor,
'irradiance':('time',tds['GHI']),
'volts':('time',pds['ghi_tilt'])})
#do hourly samples as reccomended in ISO 9842
Cds=Cds.groupby('time.hour').apply(remove_outliers)
## Step 4 #############################################
# calculate the calbration series without outliers
C=Cds['volts']*gain_units_factor/Cds['irradiance']
# Step 5 #############################################
# we found the calibration for pyranometer 2
Cpyr2=np.nanmean(C.values)
uCpyr2=np.nanstd(C.values)
print("%d , %.2f , %.3f , %.2f , %.3f"%(st,Cpyr1,uCpyr1,Cpyr2,uCpyr2))
with open('new_calibration.csv','a') as out:
out.write("%d , %.2f , %.3f , %.2f , %.3f\n"%(st,Cpyr1,uCpyr1,Cpyr2,uCpyr2))
BOX , C(pyr1), stdC(pyr1) , C(pyr_tilt) , stdC(pyr_tilt)
1 , 7.73 , 0.244 , 6.98 , 0.211
4 , 7.62 , 0.462 , 8.23 , 0.247
5 , 7.48 , 0.259 , 6.37 , 0.224
7 , 7.52 , 0.242 , 7.20 , 0.277
9 , 7.59 , 0.244 , 6.85 , 0.204
10 , 7.60 , 0.254 , 6.74 , 0.230
12 , 7.60 , 0.311 , 6.87 , 0.216
14 , 7.71 , 0.250 , 7.30 , 0.192
15 , 7.59 , 0.235 , 6.77 , 0.433
18 , 7.57 , 0.291 , 6.90 , 0.227
24 , 7.73 , 0.335 , 6.59 , 0.208
25 , 7.46 , 0.275 , 7.14 , 0.222
26 , 7.64 , 0.243 , 7.05 , 0.146
32 , 7.70 , 0.303 , 6.94 , 0.282
33 , 7.74 , 0.258 , 6.89 , 0.265
35 , 7.62 , 0.258 , 7.10 , 0.222
37 , 7.72 , 0.250 , 6.73 , 0.241
38 , 7.62 , 0.230 , 6.93 , 0.215
43 , 7.41 , 0.253 , 7.20 , 0.246
44 , 7.26 , 0.230 , 7.05 , 0.251
46 , 7.70 , 0.322 , 7.13 , 0.212
47 , 7.47 , 0.316 , 7.40 , 0.263
49 , 7.74 , 0.379 , 7.32 , 0.271
53 , 7.63 , 0.211 , 6.48 , 0.171
54 , 7.58 , 0.239 , 7.22 , 0.262
55 , 7.41 , 0.249 , 6.51 , 0.221
58 , 7.62 , 0.276 , 6.90 , 0.231
60 , 7.64 , 0.226 , 6.62 , 0.196
86 , 7.45 , 0.232 , 6.69 , 0.188
87 , 7.42 , 0.246 , 7.11 , 0.227