diff --git a/docs/examples/adr-pv-module-efficiency/README.rst b/docs/examples/adr-pv-module-efficiency/README.rst new file mode 100644 index 0000000000..0db64aa046 --- /dev/null +++ b/docs/examples/adr-pv-module-efficiency/README.rst @@ -0,0 +1,3 @@ +ADR Model for PV Module Efficiency +---------------------------------- + diff --git a/docs/examples/adr-pv-module-efficiency/plot_fit_to_matrix.py b/docs/examples/adr-pv-module-efficiency/plot_fit_to_matrix.py new file mode 100644 index 0000000000..e692d2300d --- /dev/null +++ b/docs/examples/adr-pv-module-efficiency/plot_fit_to_matrix.py @@ -0,0 +1,110 @@ +""" +Obtaining ADR model parameters from IEC 61853 matrix measurements +================================================================= + +There's a fitting function provided in pvlib to do exactly that. + +(WORK IN PROGRESS) + +Since PV module efficiency varies with irradiance and temperature +what better way to train a model than using efficiency measurement +over a broad range of temperature and irradiance levels? +The standard IEC 61853-1 defines a standard matrix of conditions +for such measurements and this example shows how the ADR model +parameters can be determined with just a few lines of code using +functions in pvlib-python. + +""" + +from io import StringIO +import pandas as pd +import matplotlib.pyplot as plt + +from pvlib.pvefficiency import adr, fit_pvefficiency_adr + +# %% The text on this line is not displayed +# +# Here are some matrix measurements: +# + +iec61853data = ''' + irradiance temperature p_mp +0 100 15.0 30.159 +1 200 15.0 63.057 +2 400 15.0 129.849 +3 600 15.0 197.744 +4 800 15.0 264.825 +5 1000 15.0 330.862 +6 100 25.0 29.250 +7 200 25.0 61.137 +8 400 25.0 126.445 +9 600 25.0 192.278 +10 800 25.0 257.561 +11 1000 25.0 322.305 +12 1100 25.0 354.174 +13 100 50.0 26.854 +14 200 50.0 56.698 +15 400 50.0 117.062 +16 600 50.0 177.959 +17 800 50.0 238.626 +18 1000 50.0 298.954 +19 1100 50.0 328.413 +20 100 75.0 24.074 +21 200 75.0 51.103 +22 400 75.0 106.546 +23 600 75.0 162.966 +24 800 75.0 218.585 +25 1000 75.0 273.651 +26 1100 75.0 301.013 +''' +df = pd.read_csv(StringIO(iec61853data), delim_whitespace=True) + +# %% +# +# Now calculate the normalized or relative efficiency values +# and use the fitting function to determine the parameters. +# The parameters (shown below) can now be used to +# simulate the module operating in a PV system. +# + +P_REF = 322.305 # (W) STC value from the table above +G_REF = 1000. # (W/m2) + +df['eta_rel'] = (df['p_mp'] / P_REF) / (df['irradiance'] / G_REF) + +adr_params = fit_pvefficiency_adr(df['irradiance'], df['temperature'], + df['eta_rel']) + +for k, v in adr_params.items(): + print('%-5s = %7.4f' % (k, v)) + +# %% +# +# Compare the model output to the original measurements. +# The chart below shows minor differences but due to their random nature +# they are most likely evidence of the limitations of measurement accuracy. +# + +eta_rel_adr = adr(df['irradiance'], df['temperature'], **adr_params) + +plt.figure() +plt.plot(df['irradiance'], df['eta_rel'], 'oc', ms=8) +plt.plot(df['irradiance'], eta_rel_adr, '.k') +plt.legend(['Lab measurements', 'ADR model fit']) +plt.xlabel('Irradiance [W/m²]') +plt.ylabel('Relative efficiency [-]') +plt.grid(alpha=0.5) +plt.show() + +# %% +# +# References +# ---------- +# .. [1] A. Driesse and J. S. Stein, "From IEC 61853 power measurements +# to PV system simulations", Sandia Report No. SAND2020-3877, 2020. +# +# .. [2] A. Driesse, M. Theristis and J. S. Stein, "A New Photovoltaic Module +# Efficiency Model for Energy Prediction and Rating," in IEEE Journal +# of Photovoltaics, vol. 11, no. 2, pp. 527-534, March 2021, +# doi: 10.1109/JPHOTOV.2020.3045677. +# diff --git a/docs/examples/adr-pv-module-efficiency/plot_simulate_system.py b/docs/examples/adr-pv-module-efficiency/plot_simulate_system.py new file mode 100644 index 0000000000..4273e6159f --- /dev/null +++ b/docs/examples/adr-pv-module-efficiency/plot_simulate_system.py @@ -0,0 +1,152 @@ +""" +Simulating PV systems using the ADR module efficiency model +=========================================================== + +Time series processing with the ADR model is fast and ... efficient! + +This example reads a TMY3 weather file, and runs a basic simulation +on a fixed latitude-tilt system. +Efficiency is independent of system size, so adjusting the system +capacity is just a matter of setting the desired value, e.g. P_STC = 5000. + +""" + +import os +import pandas as pd +import matplotlib.pyplot as plt + +import pvlib +from pvlib import iotools, location, pvefficiency +from pvlib.irradiance import aoi, get_total_irradiance + +# %% +# +# Read a TMY3 file containing weather data and select needed columns +# + +PVLIB_DIR = pvlib.__path__[0] +DATA_FILE = os.path.join(PVLIB_DIR, 'data', '723170TYA.CSV') + +tmy, metadata = iotools.read_tmy3(DATA_FILE, coerce_year=1990) + +df = pd.DataFrame({'ghi': tmy['GHI'], 'dhi': tmy['DHI'], + 'dni': tmy['DNI'], 'dni_extra': tmy['ETRN'], + 'temp_air': tmy['DryBulb'], 'wind_speed': tmy['Wspd'], + }) + +# %% +# +# Shift timestamps to middle of hour and then calculate sun positions +# + +df.index = df.index - pd.Timedelta(minutes=30) + +loc = location.Location.from_tmy(metadata) +solpos = loc.get_solarposition(df.index) + +# %% +# +# Determine total irradiance on a fixed-tilt array +# + +TILT = metadata['latitude'] +ORIENT = 180 + +df['aoi'] = aoi(TILT, ORIENT, solpos.apparent_zenith, solpos.azimuth) + +total_irrad = get_total_irradiance(TILT, ORIENT, + solpos.apparent_zenith, solpos.azimuth, + df.dni, df.ghi, df.dhi, df.dni_extra) + +df['poa_global'] = total_irrad.poa_global + +# %% +# +# Estimate the expected operating temperature of the PV modules +# + +df['temp_pv'] = pvlib.temperature.faiman(df.poa_global, df.temp_air, + df.wind_speed) + +# %% +# +# Now we're ready to calculate PV array DC output power based +# on POA irradiance and PV module operating temperature. +# Among the models available in pvlib-python to do this are: +# - PVWatts +# - SAPM +# - single-diode model variations +# - and now also the ADR PV efficiency model +# +# Simulation is done in two steps: +# - first calculate efficiency using the ADR model, +# - then convert (scale up) efficiency to power. +# + +# Borrow the ADR model parameters from the other example: + +adr_params = {'k_a': 0.99879, + 'k_d': -5.85188, + 'tc_d': 0.01939, + 'k_rs': 0.06962, + 'k_rsh': 0.21036 + } + +df['eta_rel'] = pvefficiency.adr(df['poa_global'], df['temp_pv'], **adr_params) + +# Set the desired array size: +P_STC = 5000. # (W) +# and the irradiance level needed to achieve this output ( +G_STC = 1000. # (W/m2) + +df['p_mp'] = P_STC * df['eta_rel'] * (df['poa_global'] / G_STC) + +# %% +# +# Show how power and efficiency vary with both irradiance and temperature +# + +plt.figure() +pc = plt.scatter(df['poa_global'], df['eta_rel'], c=df['temp_pv'], cmap='jet') +plt.colorbar(label='Temperature [C]', ax=plt.gca()) +pc.set_alpha(0.25) +plt.grid(alpha=0.5) +plt.ylim(0.48) +plt.xlabel('Irradiance [W/m²]') +plt.ylabel('Relative efficiency [-]') +plt.show() + +plt.figure() +pc = plt.scatter(df['poa_global'], df['p_mp'], c=df['temp_pv'], cmap='jet') +plt.colorbar(label='Temperature [C]', ax=plt.gca()) +pc.set_alpha(0.25) +plt.grid(alpha=0.5) +plt.xlabel('Irradiance [W/m²]') +plt.ylabel('Array power [W]') +plt.show() + +# %% +# +# One day: +# + +DEMO_DAY = '1990-08-05' + +plt.figure() +plt.plot(df['p_mp'][DEMO_DAY]) +plt.xticks(rotation=30) +plt.ylabel('Power [W]') +plt.show() + +# %% +# +# References +# ---------- +# .. [1] A. Driesse and J. S. Stein, "From IEC 61853 power measurements +# to PV system simulations", Sandia Report No. SAND2020-3877, 2020. +# +# .. [2] A. Driesse, M. Theristis and J. S. Stein, "A New Photovoltaic Module +# Efficiency Model for Energy Prediction and Rating," in IEEE Journal +# of Photovoltaics, vol. 11, no. 2, pp. 527-534, March 2021, +# doi: 10.1109/JPHOTOV.2020.3045677. +# diff --git a/docs/sphinx/source/reference/pv_modeling.rst b/docs/sphinx/source/reference/pv_modeling.rst index 31c380c1bb..918d237ff5 100644 --- a/docs/sphinx/source/reference/pv_modeling.rst +++ b/docs/sphinx/source/reference/pv_modeling.rst @@ -187,3 +187,5 @@ Other pvsystem.retrieve_sam pvsystem.scale_voltage_current_power + pvefficiency.adr + pvefficiency.fit_pvefficiency_adr diff --git a/pvlib/pvefficiency.py b/pvlib/pvefficiency.py new file mode 100644 index 0000000000..50f7c6e56c --- /dev/null +++ b/pvlib/pvefficiency.py @@ -0,0 +1,214 @@ +""" +This module contains implementations of PV module efficiency models. + +These models have a common purpose, which is to predict the efficiency at +maximum power point as a function of the main operating conditions: +effective irradiance and module temperature. +""" + +import numpy as np +from scipy.optimize import curve_fit +from scipy.special import exp10 + + +def adr(irradiance, temperature, k_a, k_d, tc_d, k_rs, k_rsh): + ''' + Calculate PV module efficiency using the ADR model. + + The efficiency varies with irradiance and operating temperature + and is determined by 5 model parameters as described in [1]_. + + Parameters + ---------- + irradiance : numeric, non-negative + The effective irradiance incident on the PV module. [W/m²] + + temperature : numeric + The PV module operating temperature. [°C] + + k_a : numeric + Absolute scaling factor, which is equal to the efficiency at + reference conditions. This factor allows the model to be used + with relative or absolute efficiencies, and to accommodate data sets + which are not perfectly normalized but have a slight bias at + the reference conditions. [unitless|%] + + k_d : numeric, negative + “Dark irradiance” or diode coefficient which influences the voltage + increase with irradiance. [unitless] + + tc_d : numeric + Temperature coefficient of the diode coefficient, which indirectly + influences voltage. Because it is the only temperature coefficient + in the model, its value will also reflect secondary temperature + dependencies that are present in the PV module. [unitless] + + k_rs and k_rsh : numeric + Series and shunt resistance loss factors. Because of the normalization + they can be read as power loss fractions at reference conditions. + For example, if k_rs is 0.05, the internal loss assigned to the + series resistance has a magnitude equal to 5% of the module output. + [unitless] + + Returns + ------- + eta : numeric + The efficiency of the module at the specified irradiance and + temperature. + + Notes + ----- + The efficiency values may be absolute or relative, and may be expressed + as percent or per unit. This is determined by the efficiency data + used to derive values for the 5 model parameters. The first model + parameter k_a is equal to the efficiency at STC and therefore + indicates the efficiency scale being used. k_a can also be changed + freely to adjust the scale, or to change the module class to a slightly + higher or lower efficiency. + + All arguments may be scalars or array-like. If multiple arguments + are array-like they must be the same shape or broadcastable to the + same shape. + + See also + -------- + pvlib.pvefficiency.fit_pvefficiency_adr + + References + ---------- + .. [1] A. Driesse and J. S. Stein, "From IEC 61853 power measurements + to PV system simulations", Sandia Report No. SAND2020-3877, 2020. + + .. [2] A. Driesse, M. Theristis and J. S. Stein, "A New Photovoltaic Module + Efficiency Model for Energy Prediction and Rating," in IEEE Journal + of Photovoltaics, vol. 11, no. 2, pp. 527-534, March 2021, + doi: 10.1109/JPHOTOV.2020.3045677. + + Examples + -------- + >>> adr([1000, 200], 25, + k_a=100, k_d=-6.0, tc_d=0.02, k_rs=0.05, k_rsh=0.10) + array([100. , 92.79729308]) + + >>> adr([1000, 200], 25, + k_a=1.0, k_d=-6.0, tc_d=0.02, k_rs=0.05, k_rsh=0.10) + array([1. , 0.92797293]) + + + Adapted from https://github.com/adriesse/pvpltools-python + Copyright (c) 2022, Anton Driesse, PV Performance Labs + All rights reserved. + ''' + k_a = np.array(k_a) + k_d = np.array(k_d) + tc_d = np.array(tc_d) + k_rs = np.array(k_rs) + k_rsh = np.array(k_rsh) + + # normalize the irradiance + G_REF = np.array(1000.) + s = irradiance / G_REF + + # obtain the difference from reference temperature + T_REF = np.array(25.) + dt = temperature - T_REF + + # equation 29 in JPV + s_o = exp10(k_d + (dt * tc_d)) # noQA: E221 + s_o_ref = exp10(k_d) + + # equation 28 and 30 in JPV + # the constant k_v does not appear here because it cancels out + v = np.log(s / s_o + 1) # noQA: E221 + v /= np.log(1 / s_o_ref + 1) + + # equation 25 in JPV + eta = k_a * ((1 + k_rs + k_rsh) * v - k_rs * s - k_rsh * v**2) + + return eta + + +def fit_pvefficiency_adr(irradiance, temperature, eta, dict_output=True, + **kwargs): + """ + Determine the parameters of the adr module efficiency model by non-linear + least-squares fit to lab or field measurements. + + Parameters + ---------- + irradiance : numeric, non-negative + Effective irradiance incident on the PV module. [W/m²] + + temperature : numeric + PV module operating temperature. [°C] + + eta : numeric + Efficiency of the PV module at the specified irradiance and + temperature(s). [unitless] or [%] + + dict_output : boolean, optional + When True (default), return the result as a dictionary; when False, + return the result as a numpy array. + + kwargs : + Optional keyword arguments passed to `scip.optimize.curve_fit`. + These kwargs can over-ride some options set within this function, + which could be interesting for very advanced users. + + Returns + ------- + popt : array or dict + Optimal values for the parameters. + + Notes + ----- + The best fits are obtained when the lab or field data include a wide range + of both irradiance and temperature values. A minimal data set + would consist of 6 operating points covering low, medium and high + irradiance levels at two operating temperatures. + + See also + -------- + pvlib.pvefficiency.adr + scipy.optimize.curve_fit + + Adapted from https://github.com/adriesse/pvpltools-python + Copyright (c) 2022, Anton Driesse, PV Performance Labs + All rights reserved. + """ + irradiance = np.asarray(irradiance, dtype=float).reshape(-1) + temperature = np.asarray(temperature, dtype=float).reshape(-1) + eta = np.asarray(eta, dtype=float).reshape(-1) + + eta_max = np.max(eta) + + P_NAMES = ['k_a', 'k_d', 'tc_d', 'k_rs', 'k_rsh'] + P_MAX = [+np.inf, 0, +0.1, 1, 1] # noQA: E221 + P_MIN = [0, -12, -0.1, 0, 0] # noQA: E221 + P0 = [eta_max, -6, 0.0, 0, 0] # noQA: E221 + P_SCALE = [eta_max, 10, 0.1, 1, 1] + + fit_options = dict(p0=P0, + bounds=[P_MIN, P_MAX], + method='trf', + x_scale=P_SCALE, + loss='soft_l1', + f_scale=eta_max * 0.05, + ) + + fit_options.update(kwargs) + + def adr_wrapper(xdata, *params): + return adr(*xdata, *params) + + result = curve_fit(adr_wrapper, + xdata=[irradiance, temperature], + ydata=eta, + **fit_options, + ) + popt = result[0] + + if dict_output: + return dict(zip(P_NAMES, popt)) + else: + return popt diff --git a/pvlib/tests/test_pvefficiency.py b/pvlib/tests/test_pvefficiency.py new file mode 100644 index 0000000000..804ce092fa --- /dev/null +++ b/pvlib/tests/test_pvefficiency.py @@ -0,0 +1,41 @@ +from numpy.testing import assert_allclose + +from pvlib import pvefficiency + + +def test_adr(): + g = [1000, 200, 1000, 200, 1000, 200] + t = [25, 25, 50, 50, 75, 75] + p = [1.0, -6.684898, 0.018855, 0.069917, 0.054369] + + e = [1.0, 0.949154, 0.92812, 0.876472, 0.855699, 0.80325] + + result = pvefficiency.adr(g, t, *p) + assert_allclose(result, e, rtol=1e-5) + + +def test_fit_pvefficiency_adr(): + g = [1000, 200, 1000, 200, 1000, 200] + t = [25, 25, 50, 50, 75, 75] + e = [1.0, 0.949154, 0.92812, 0.876472, 0.855699, 0.80325] + + p = [1.0, -6.684898, 0.018855, 0.069917, 0.054369] + + result = pvefficiency.fit_pvefficiency_adr(g, t, e, dict_output=False) + # the fitted parameters vary somewhat by platform during the testing + # so the tolerance is higher on the parameters than on the efficiencies + # in the other tests + assert_allclose(result, p, rtol=1e-3) + + result = pvefficiency.fit_pvefficiency_adr(g, t, e, dict_output=True) + assert 'k_a' in result + + +def test_adr_round_trip(): + g = [1000, 200, 1000, 200, 1000, 200] + t = [25, 25, 50, 50, 75, 75] + e = [1.0, 0.949154, 0.92812, 0.876472, 0.855699, 0.80325] + + p = pvefficiency.fit_pvefficiency_adr(g, t, e, dict_output=False) + result = pvefficiency.adr(g, t, *p) + assert_allclose(result, e, rtol=1e-5)