Skip to content

Commit

Permalink
Merge pull request #485 from cdeline/449_mad_fn
Browse files Browse the repository at this point in the history
449 mad fn
  • Loading branch information
cdeline committed Nov 9, 2023
2 parents 7dba9b2 + b92a1b4 commit 4a6e004
Show file tree
Hide file tree
Showing 5 changed files with 113 additions and 34 deletions.
5 changes: 4 additions & 1 deletion bifacial_radiance/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,10 @@ def _subhourlydatatoGencumskyformat(gencumskydata, label='right'):


#Resample to hourly. Gencumsky wants right-labeled data.
gencumskydata = gencumskydata.resample('60T', closed='right', label='right').mean()
try:
gencumskydata = gencumskydata.resample('60T', closed='right', label='right').mean()
except TypeError: # Pandas 2.0 error
gencumskydata = gencumskydata.resample('60T', closed='right', label='right').mean(numeric_only=True)

if label == 'left': #switch from left to right labeled by adding an hour
gencumskydata.index = gencumskydata.index + pd.to_timedelta('1H')
Expand Down
104 changes: 81 additions & 23 deletions bifacial_radiance/mismatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,29 +163,92 @@ def calculatePVMismatch(pvsys, stdpl, cellsx, cellsy, Gpoat):

return PowerAveraged, PowerDetailed

def mad_fn(data):
def mismatch_fit3(data):
'''
Mean average deviation calculation for mismatch purposes.
Electrical mismatch calculation following Progress in PV paper
Estimating and parameterizing mismatch power loss in bifacial photovoltaic systems
Chris Deline, Silvana Ayala Pelaez,Sara MacAlpine,Carlos Olalla
https://doi.org/10.1002/pip.3259
Parameters
----------
data : np.ndarray
Gtotal irradiance measurements.
data : np.ndarray, pd.Series, pd.DataFrame
Gtotal irradiance measurements. Each column is the irradiance for a module
at a specific time.
Returns
-------
scalar : return MAD / Average for a 1D array
fit3 : Float or pd.Series
Returns mismatch values for each module
Equation: 1/(n^2*Gavg)*Sum Sum (abs(G_i - G_j))
## Note: starting with Pandas 1.0.0 this function will not work on Series objects.
'''
import numpy as np
import pandas as pd
# Pandas returns a notimplemented error if this is a series.
if type(data) == pd.Series:

if type(data) == np.ndarray:
data = pd.DataFrame(data)

datac = data[~np.isnan(data)]
mad = mad_fn(datac) /100 # (percentage)
mad2 = mad**2

fit3 = 0.054*mad + 0.068*mad2

if fit3.__len__() == 1:
fit3 = float(fit3)

return fit3


def mad_fn(data, axis='index'):
'''
Mean average deviation calculation for mismatch purposes.
Parameters
----------
data : np.ndarray or pd.Series or pd.DataFrame
Gtotal irradiance measurements. If data is a pandas.DataFrame, one
MAD/Average is returned for each index, based on values across columns.
axis : {0 or 'index', 1 or 'columns'}, default 'index'
Calculate mean average deviation across rows (default) or columns for 2D data
* 0, or 'index' : MAD calculated across rows.
* 1, or 'columns' : MAD calculated across columns.
Returns
-------
scalar or pd.Series: return MAD / Average [%]. Scalar for a 1D array, Series for 2D.
Equation: 1/(n^2*Gavg)*Sum Sum (abs(G_i - G_j)) * 100[%]
'''
import numpy as np
import pandas as pd
def _mad_1D(data): #1D calculation of MAD
return (np.abs(np.subtract.outer(data,data)).sum()/float(data.__len__())**2 / np.mean(data))*100
if type(axis) == str:
try:
axis = {"index": 0, "rows": 0, 'columns':1}[axis]
except KeyError:
raise Exception('Incorrect index string in mad_fn. options: index, rows, columns.')

ndim = data.ndim
if ndim == 2 and axis==0:
data = data.T
# Pandas returns a notimplemented error if this is a DataFrame.
if (type(data) == pd.Series):
data = data.to_numpy()

return (np.abs(np.subtract.outer(data,data)).sum()/float(data.__len__())**2 / np.mean(data))*100
if type(data) == pd.DataFrame:
temp = data.apply(pd.Series.to_numpy, axis=1)
return(temp.apply(_mad_1D))
elif ndim ==2: #2D array
return [_mad_1D(i) for i in data]
else:
return _mad_1D(data)



Expand Down Expand Up @@ -314,24 +377,19 @@ def analysisIrradianceandPowerMismatch(testfolder, writefiletitle, portraitorlan
F.index='FrontIrradiance_cell_'+F.index.astype(str)
B.index='BackIrradiance_cell_'+B.index.astype(str)
Poat.index='POAT_Irradiance_cell_'+Poat.index.astype(str)

## Transpose
F = F.T
B = B.T
Poat = Poat.T

# Statistics Calculatoins
dfst=pd.DataFrame()
dfst['MAD/G_Total'] = mad_fn(Poat.T)
dfst['Front_MAD/G_Total'] = mad_fn(F.T)
dfst['MAD/G_Total'] = mad_fn(Poat)
dfst['Front_MAD/G_Total'] = mad_fn(F)
dfst['MAD/G_Total**2'] = dfst['MAD/G_Total']**2
dfst['Front_MAD/G_Total**2'] = dfst['Front_MAD/G_Total']**2
dfst['poat'] = Poat.mean(axis=1)
dfst['gfront'] = F.mean(axis=1)
dfst['grear'] = B.mean(axis=1)
dfst['poat'] = Poat.mean()
dfst['gfront'] = F.mean()
dfst['grear'] = B.mean()
dfst['bifi_ratio'] = dfst['grear']/dfst['gfront']
dfst['stdev'] = Poat.std(axis=1)/ dfst['poat']
dfst.index=Poat.index.astype(str)
dfst['stdev'] = Poat.std()/ dfst['poat']
dfst.index=Poat.columns.astype(str)

# Power Calculations/Saving
Pout=pd.DataFrame()
Expand All @@ -341,10 +399,10 @@ def analysisIrradianceandPowerMismatch(testfolder, writefiletitle, portraitorlan
Pout['Front_Pdet']=Pdet_front_all
Pout['Mismatch_rel'] = 100-(Pout['Pdet']*100/Pout['Pavg'])
Pout['Front_Mismatch_rel'] = 100-(Pout['Front_Pdet']*100/Pout['Front_Pavg'])
Pout.index=Poat.index.astype(str)
Pout.index=Poat.columns.astype(str)

## Save CSV
df_all = pd.concat([Pout,dfst,Poat,F,B],axis=1)
## Save CSV as one long row
df_all = pd.concat([Pout, dfst, Poat.T, F.T, B.T], axis=1)
df_all.to_csv(writefiletitle)
print("Saved Results to ", writefiletitle)

Expand Down
4 changes: 2 additions & 2 deletions docs/sphinx/source/whatsnew/pending.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ API Changes
*A new function can now be called to compile results and report out final irradiance and performance data: :py:class:`~bifacial_radiance.RadianceObj.compileResults`.
*Multiple modules and rows can now be selected in a single analysis scan. ``modWanted`` and ``rowWanted`` inputs in :py:class:`~bifacial_radiance.RadianceObj.analysis1axis` can now be a list, to select multiple rows and modules for scans. (:issue:`405`)(:pull:`408`)
*To support multiple modules and row scans for 1axis simulations, outputs like Wm2Front are now stored in ``trackerdict``.``Results`` (:issue:`405`)(:pull:`408`)
* ``mismatch.mad_fn`` has new functionality and input parameter `axis`. If a 2D matrix or dataframe is passed in as data, MAD is calculated along the row (default) or along the columns by passing 'axis=1'
Enhancements
~~~~~~~~~~~~


Bug fixes
~~~~~~~~~
* Pandas 2.0 errors have not yet been fixed. Constraining pandas < 2.0 in setup.py until (:issue:`449`) is fixed
* Fixed Pandas 2.0 errors by re-factoring ``mismatch.mad_fn`` (:issue:`449`)

Documentation
~~~~~~~~~~~~~~
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
# requirements files see:
# https://packaging.python.org/en/latest/requirements.html
install_requires=[
'pandas < 2.0 ',
'pandas ',
'pvlib >= 0.8.0',
'pvmismatch',
'configparser',
Expand Down
32 changes: 25 additions & 7 deletions tests/test_mismatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,29 @@ def test_setupforPVMismatch():


def test_MAD():
ans_T = pd.Series([ 70.892019, 69.953052, 69.014085, 68.075117,
67.136150, 66.197183, 65.258216, 64.319249,
63.380282, 62.441315, 61.502347, 60.563380])
ans = pd.Series([72.222222, 22.698413, 13.465160,
9.571620, 7.424714, 6.064461])

assert bifacial_radiance.mismatch.mad_fn(TEST_ARRAY) == \
pytest.approx(2433.333,abs = 0.001)
pytest.approx(ans.to_numpy(), abs = 0.001)

assert bifacial_radiance.mismatch.mad_fn(TEST_ARRAY, axis='columns') == \
pytest.approx(ans_T.to_numpy(), abs = 0.001)

temp = bifacial_radiance.mismatch.mad_fn(pd.DataFrame(TEST_ARRAY))
ans = pd.Series([15706.061,4936.190,2928.249,2081.526,1614.642,1318.8295])
pd.testing.assert_series_equal(temp,ans,check_less_precise=True)
temp2 = bifacial_radiance.mismatch.mad_fn(pd.DataFrame(TEST_ARRAY), axis=1)
pd.testing.assert_series_equal(temp, ans)
pd.testing.assert_series_equal(temp2, ans_T)
# test pd.Series objects are correctly handled
assert bifacial_radiance.mismatch.mad_fn(ans) == \
pytest.approx(96.491,abs = 0.001)
# assert temp == \
# pytest.approx(2433.333,abs = 0.001)
assert bifacial_radiance.mismatch.mad_fn(ans_T) == \
pytest.approx(5.674, abs = 0.001)
assert bifacial_radiance.mismatch.mad_fn(ans_T.to_numpy()) == \
pytest.approx(5.674, abs = 0.001)



def test_analysisIrradianceandPowerMismatch():
#analysisIrradianceandPowerMismatch(testfolder, writefiletitle,
Expand All @@ -77,4 +88,11 @@ def test_analysisIrradianceandPowerMismatch():
df_all = pd.read_csv(writefiletitle)
assert df_all.Mismatch_rel[0] == pytest.approx(0.376, abs = 0.001)
assert df_all["MAD/G_Total"][0] == pytest.approx(1.987, abs = 0.001)


def test_mismatch_fit3():
ans = pd.Series([0.074469, 0.015761, 0.008504, 0.005792, 0.004384, 0.003525])
pd.testing.assert_series_equal( bifacial_radiance.mismatch.mismatch_fit3(TEST_ARRAY), ans, atol=1e-6)
pd.testing.assert_series_equal( bifacial_radiance.mismatch.mismatch_fit3(pd.DataFrame(TEST_ARRAY)), ans, atol=1e-6)
assert bifacial_radiance.mismatch.mismatch_fit3(TEST_ARRAY[:,0]) == pytest.approx(ans[0], abs = 0.001)

0 comments on commit 4a6e004

Please sign in to comment.