Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/sphinx/source/reference/pv_modeling/inverter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ Functions for fitting inverter models
.. autosummary::
:toctree: ../generated/

inverter.fit_sandia
inverter.fit_sandia_lab
inverter.fit_sandia_field
171 changes: 169 additions & 2 deletions pvlib/inverter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
----------
Expand Down Expand Up @@ -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
37 changes: 29 additions & 8 deletions tests/test_inverter.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Loading