diff --git a/docs/sphinx/source/reference/pv_modeling/inverter.rst b/docs/sphinx/source/reference/pv_modeling/inverter.rst index e6e623f28b..2d2a69e7a8 100644 --- a/docs/sphinx/source/reference/pv_modeling/inverter.rst +++ b/docs/sphinx/source/reference/pv_modeling/inverter.rst @@ -19,4 +19,5 @@ Functions for fitting inverter models .. autosummary:: :toctree: ../generated/ - inverter.fit_sandia + inverter.fit_sandia_lab + inverter.fit_sandia_field diff --git a/pvlib/inverter.py b/pvlib/inverter.py index 622fb9b958..1d0cad4daa 100644 --- a/pvlib/inverter.py +++ b/pvlib/inverter.py @@ -13,6 +13,8 @@ import numpy as np import pandas as pd from numpy.polynomial.polynomial import polyfit # different than np.polyfit +from scipy.optimize import minimize +from pvlib._deprecation import deprecated def _sandia_eff(v_dc, p_dc, inverter): @@ -445,9 +447,10 @@ def pvwatts_multi(pdc, pdc0, eta_inv_nom=0.96, eta_inv_ref=0.9637): return pvwatts(sum(pdc), pdc0, eta_inv_nom, eta_inv_ref) -def fit_sandia(ac_power, dc_power, dc_voltage, dc_voltage_level, p_ac_0, p_nt): +def fit_sandia_lab(ac_power, dc_power, dc_voltage, dc_voltage_level, p_ac_0, + p_nt): r''' - Determine parameters for the Sandia inverter model. + Determine parameters for the Sandia inverter model from laboratory data. Parameters ---------- @@ -551,3 +554,167 @@ def extract_c(x_d, add): # prepare dict and return return {'Paco': p_ac_0, 'Pdco': p_dc0, 'Vdco': v_nom, 'Pso': p_s0, 'C0': c0, 'C1': c1, 'C2': c2, 'C3': c3, 'Pnt': p_nt} + + +fit_sandia = deprecated(since="0.15.3", name="fit_sandia", + alternative="fit_sandia_lab")(fit_sandia_lab) + + +def _fit_sandia_field_a(pac, pdc, vdc, pac0, vdc0): + r''' Estimate Pdco, Pso and C0 for the Sandia inverter model from + time-series data. Uses robust regression on data greater than 5% of rated + Pac and DC voltage near nominal voltage vdc0. + + Parameters + ---------- + pac : numeric + Measured AC power (W) + pdc : numeric + Measured DC input power (W) + vdc + Measured DC input voltage (V) + pac0 : float + Rated AC power (W) + vdc0 : float + Nominal DC input voltage (V) + + Returns + ------- + dict + Parameters Pdco, Pso and C0 for the Sandia inverter model. + ''' + try: + import statsmodels.api as sm + except ImportError: + raise ImportError( + 'Parameter fitting requires statsmodels') + # select data. Avoid very low power, clipping and DC voltage far from + # nominal + u = (pac > 0.05*pac0) & (pac < pac0) & (np.abs(vdc - vdc0)/vdc0 < 0.05) + Y = pac[u] + X = np.array([pdc[u]**2, pdc[u]]).T + X = sm.add_constant(X) + rlm_model = sm.RLM(Y, X) + rlm_results = rlm_model.fit() + rlmp = np.array(rlm_results.params) + + p = {} + p['C0'] = rlmp[1] + p['Pso'] = (-rlmp[2] + np.sqrt(rlmp[2]**2 - 4*p['C0']*rlmp[0])) / \ + (2 * p['C0']) + C = pac0 + rlmp[2]*p['Pso'] + p['C0']*p['Pso']**2 + p['Pdco'] = (-rlmp[2] + np.sqrt(rlmp[2]**2 + 4*p['C0']*C)) / (2. * p['C0']) + return p + + +def _f2(params, pac0, vdc0, pdc0, ps0, C0, vdc, pdc, pac): + # objective function for _fit_sandia_field. Assumes Pdco, Pso and C0 + # are known. Returns the root sum of squared differences in AC power + # Input params = [C1, C2, C3] + p = {} + p['Paco'] = pac0 # AC power + p['Vdco'] = vdc0 # DC V + p['Pdco'] = pdc0 # DC power + p['Pso'] = ps0 # DC power, small + p['C0'] = C0 # unitless, tiny + p['C1'] = params[0] # 1/V, tiny + p['C2'] = params[1] # 1/V, tiny + p['C3'] = params[2] # 1/V tiny + diff = (_sandia_eff(vdc, pdc, p) - pac) + return np.sqrt(np.dot(diff, diff)) + + +def _fit_sandia_field_b(resid, pac, pdc, vdc, pac0, vdc0, pdc0, ps0, C0): + r''' Estimate C1, C2, and C3 for the Sandia inverter model from time-series + data. Estimates are conditional on parameters Pdco, Pso and C0. + + Parameters + ---------- + pac : numeric + Measured AC power (W) + pdc : numeric + Measured DC input power (W) + vdc + Measured DC input voltage (V) + pac0 : float + Rated AC power (W) + vdc0 : float + Nominal DC input voltage (V) + pdc0 : float + DC input power that produces rated AC power at nominal voltage (W) + ps0 : float + Start-up DC power (W) + C0 : float + Empirical coefficient + + Returns + ------- + dict + Parameters for the Sandia inverter model including C1, C2 and C3. + ''' + # select data. Avoid very low power and clipping + u = (pac > 0.05*pac0) & (pac < pac0) + + # initial guess + x0 = np.array([0., 0., 0.]) + + args = (pac0, vdc0, pdc0, ps0, C0, vdc[u], pdc[u], pac[u]) + options = {} + options['gtol'] = 1e-5 + result = minimize(_f2, x0, args=args, + method='BFGS', + options=options) + + params = result.x + p = {} + p['Paco'] = pac0 + p['Vdco'] = vdc0 + p['Pdco'] = pdc0 + p['Pso'] = ps0 + p['C0'] = C0 + p['C1'] = params[0] + p['C2'] = params[1] + p['C3'] = params[2] + + return p + + +def fit_sandia_field(pac, pdc, vdc, pac0, vdc0): + r''' Estimate parameters for the Sandia inverter model from time-series + data. + + Parameters + ---------- + pac : numeric + Measured AC power (W) + pdc : numeric + Measured DC input power (W) + vdc + Measured DC input voltage (V) + pac0 : float + Rated AC power (W) + vdc0 : float + Nominal DC input voltage (V) + + Returns + ------- + dict + Parameters for the Sandia inverter model. + + References + ---------- + .. [1] C. W. Hansen, K. S. Anderson, M. Theristis, "Fitting the Sandia + Inverter Model to Operational PV System Data", 54 IEEE Photovoltaic + Specialist Conference, New Orleans, USA. 2026 + .. [2] D. King, S. Gonzalez, G. Galbraith, W. Boyson, "Performance Model + for Grid-Connected Photovoltaic Inverters", Sandia National + Laboratories, Albuquerque, N.M., USA, SAND2007-5036, Sept. 2007. + :doi:`10.2172/920449` + + ''' + # get Pdco, Pso and C0 first + p = _fit_sandia_field_a(pac, pdc, vdc, pac0, vdc0) + # add C1, C2, C3 + p = _fit_sandia_field_b(_f2, pac, pdc, vdc, pac0, vdc0, + p['Pdco'], p['Pso'], p['C0']) + return p diff --git a/tests/test_inverter.py b/tests/test_inverter.py index 5794669ec9..ef28c6f3b1 100644 --- a/tests/test_inverter.py +++ b/tests/test_inverter.py @@ -1,10 +1,11 @@ import numpy as np import pandas as pd - -from .conftest import assert_series_equal from numpy.testing import assert_allclose +from .conftest import assert_series_equal from .conftest import TESTS_DATA_DIR +from .conftest import requires_statsmodels + import pytest from pvlib import inverter @@ -202,12 +203,32 @@ def test_pvwatts_multi(): 'Pso': 10., 'C0': 1e-6, 'C1': 1e-4, 'C2': 1e-2, 'C3': 1e-3, 'Pnt': 1.}), ]) -def test_fit_sandia(infilen, expected): +def test_fit_sandia_lab(infilen, expected): curves = pd.read_csv(infilen) dc_power = curves['ac_power'] / curves['efficiency'] - result = inverter.fit_sandia(ac_power=curves['ac_power'], - dc_power=dc_power, - dc_voltage=curves['dc_voltage'], - dc_voltage_level=curves['dc_voltage_level'], - p_ac_0=expected['Paco'], p_nt=expected['Pnt']) + result = inverter.fit_sandia_lab( + ac_power=curves['ac_power'], dc_power=dc_power, + dc_voltage=curves['dc_voltage'], + dc_voltage_level=curves['dc_voltage_level'], + p_ac_0=expected['Paco'], p_nt=expected['Pnt']) assert expected == pytest.approx(result, rel=1e-3) + + +@requires_statsmodels +def test_fit_sandia_field(): + pdc = np.arange(start=100., stop=1300., step=100.) + vdc = np.array([550., 600., 650, 550., 600., 650, 550., 600., 650, + 550., 600., 650]) + params = {'Paco': 1200, 'Pdco': 1300, 'Pso': 10, 'C0': 1e-6, 'C1': 1e-7, + 'C2': 1e-7, 'C3': 1e-7, 'Vdco': 600} + # pac was computed with pvlib.inverter._sandia_eff + pac = np.array([83.6134, 176.535, 269.476, 362.442, 455.422, 548.421, + 641.45, 734.489, 827.547, 920.638, 1013.74, 1106.85]) + p = inverter.fit_sandia_field(pac, pdc, vdc, params['Paco'], + params['Vdco']) + # Pdco, Pso, C0 should be within 1% + for k in ['Pdco', 'Pso', 'C0']: + assert np.isclose(p[k], params[k], rtol=1e-2) + # looser tolerance for C1, C2, C3, so test AC power + pred_ac = inverter._sandia_eff(vdc, pdc, p) + assert_allclose(pred_ac, pac, rtol=2e-5)