Skip to content

Commit 665a076

Browse files
authored
Add function for estimating PVsyst SDM parameters from IEC 61853-1 data (#2429)
* add function * add tests * add to API reference * whatsnew * lint * add suffix to names of private functions * add irrad/temp tolerance parameters * add tolerance tests * fix a couple warnings * lint * change order of optional parameters * replace statsmodels with np.linalg.lstsq * add _2025 to function name * add note regarding array shape * lint * Update pvsyst.py * add note regarding temp coeff consistency
1 parent 7fb818b commit 665a076

File tree

5 files changed

+465
-2
lines changed

5 files changed

+465
-2
lines changed

docs/sphinx/source/reference/pv_modeling/parameters.rst

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Functions for fitting single diode models
1212
ivtools.sdm.fit_cec_sam
1313
ivtools.sdm.fit_desoto
1414
ivtools.sdm.fit_pvsyst_sandia
15+
ivtools.sdm.fit_pvsyst_iec61853_sandia_2025
1516
ivtools.sdm.fit_desoto_sandia
1617

1718
Functions for fitting the single diode equation

docs/sphinx/source/whatsnew/v0.12.1.rst

+3-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ Bug fixes
1414

1515
Enhancements
1616
~~~~~~~~~~~~
17-
* ``pvlib.ivtools.sdm`` is now a subpackage. (:issue:`2252`, :pull:`2256`)
18-
17+
* :py:mod:`pvlib.ivtools.sdm` is now a subpackage. (:issue:`2252`, :pull:`2256`)
18+
* Add a function for estimating PVsyst SDM parameters from IEC 61853-1 matrix
19+
data (:py:func:`~pvlib.ivtools.sdm.fit_pvsyst_iec61853_sandia_2025`). (:issue:`2185`, :pull:`2429`)
1920

2021
Documentation
2122
~~~~~~~~~~~~~

pvlib/ivtools/sdm/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,6 @@
1515

1616
from pvlib.ivtools.sdm.pvsyst import ( # noqa: F401
1717
fit_pvsyst_sandia,
18+
fit_pvsyst_iec61853_sandia_2025,
1819
pvsyst_temperature_coeff,
1920
)

pvlib/ivtools/sdm/pvsyst.py

+323
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
_extract_sdm_params, _initial_iv_params, _update_iv_params
1515
)
1616

17+
from pvlib.pvsystem import (
18+
_pvsyst_Rsh, _pvsyst_IL, _pvsyst_Io, _pvsyst_nNsVth, _pvsyst_gamma
19+
)
1720

1821
CONSTANTS = {'E0': 1000.0, 'T0': 25.0, 'k': constants.k, 'q': constants.e}
1922

@@ -305,3 +308,323 @@ def maxp(temp_cell, irrad_ref, alpha_sc, gamma_ref, mu_gamma, I_L_ref,
305308
gamma_pdc = _first_order_centered_difference(maxp, x0=temp_ref, args=args)
306309

307310
return gamma_pdc / pmp
311+
312+
313+
def fit_pvsyst_iec61853_sandia_2025(effective_irradiance, temp_cell,
314+
i_sc, v_oc, i_mp, v_mp,
315+
cells_in_series, EgRef=1.121,
316+
alpha_sc=None, beta_mp=None,
317+
R_s=None, r_sh_coeff=0.12,
318+
min_Rsh_irradiance=None,
319+
irradiance_tolerance=20,
320+
temperature_tolerance=1):
321+
"""
322+
Estimate parameters for the PVsyst module performance model using
323+
IEC 61853-1 matrix measurements.
324+
325+
Parameters
326+
----------
327+
effective_irradiance : array
328+
Effective irradiance for each test condition [W/m²]
329+
temp_cell : array
330+
Cell temperature for each test condition [C]
331+
i_sc : array
332+
Short circuit current for each test condition [A]
333+
v_oc : array
334+
Open circuit voltage for each test condition [V]
335+
i_mp : array
336+
Current at maximum power point for each test condition [A]
337+
v_mp : array
338+
Voltage at maximum power point for each test condition [V]
339+
cells_in_series : int
340+
The number of cells connected in series.
341+
EgRef : float, optional
342+
The energy bandgap at reference temperature in units of eV.
343+
1.121 eV for crystalline silicon. EgRef must be >0.
344+
alpha_sc : float, optional
345+
Temperature coefficient of short circuit current. If not specified,
346+
it will be estimated using the ``i_sc`` values at irradiance of
347+
1000 W/m2. [A/K]
348+
beta_mp : float, optional
349+
Temperature coefficient of maximum power voltage. If not specified,
350+
it will be estimated using the ``v_mp`` values at irradiance of
351+
1000 W/m2. [1/K]
352+
R_s : float, optional
353+
Series resistance value. If not provided, a value will be estimated
354+
from the input measurements. [ohm]
355+
r_sh_coeff : float, default 0.12
356+
Shunt resistance fitting coefficient. The default value is taken
357+
from [1]_.
358+
min_Rsh_irradiance : float, optional
359+
Irradiance threshold below which values are excluded when estimating
360+
shunt resistance parameter values. May be useful for modules
361+
with problematic low-light measurements. [W/m²]
362+
irradiance_tolerance : float, default 20
363+
Tolerance for irradiance variation around the STC value.
364+
The default value corresponds to a +/- 2% interval around the STC
365+
value of 1000 W/m². [W/m²]
366+
temperature_tolerance : float, default 1
367+
Tolerance for temperature variation around the STC value.
368+
The default value corresponds to a +/- 1 degree interval around the STC
369+
value of 25 degrees. [C]
370+
371+
Returns
372+
-------
373+
dict
374+
alpha_sc : float
375+
short circuit current temperature coefficient [A/K]
376+
gamma_ref : float
377+
diode (ideality) factor at STC [unitless]
378+
mu_gamma : float
379+
temperature coefficient for diode (ideality) factor [1/K]
380+
I_L_ref : float
381+
light current at STC [A]
382+
I_o_ref : float
383+
dark current at STC [A]
384+
R_sh_ref : float
385+
shunt resistance at STC [ohm]
386+
R_sh_0 : float
387+
shunt resistance at zero irradiance [ohm]
388+
R_sh_exp : float
389+
exponential factor defining decrease in shunt resistance with
390+
increasing effective irradiance
391+
R_s : float
392+
series resistance at STC [ohm]
393+
cells_in_series : int
394+
number of cells in series
395+
EgRef : float
396+
effective band gap at STC [eV]
397+
398+
See also
399+
--------
400+
pvlib.pvsystem.calcparams_pvsyst
401+
pvlib.ivtools.sdm.fit_pvsyst_sandia
402+
403+
Notes
404+
-----
405+
Input arrays of operating conditions and electrical measurements must be
406+
1-D with equal lengths.
407+
408+
Values supplied for ``alpha_sc``, ``beta_mp``, and ``R_s`` must be
409+
consistent with the matrix data, as these values are used when estimating
410+
other model parameters.
411+
412+
This method is non-iterative. In some cases, it may be desirable to
413+
refine the estimated parameter values using a numerical optimizer such as
414+
the default method in ``scipy.optimize.minimize``.
415+
416+
References
417+
----------
418+
.. [1] K. S. Anderson, C. W. Hansen, and M. Theristis, "A Noniterative
419+
Method of Estimating Parameter Values for the PVsyst Version 6
420+
Single-Diode Model From IEC 61853-1 Matrix Measurements," IEEE Journal
421+
of Photovoltaics, vol. 15, 3, 2025. :doi:`10.1109/JPHOTOV.2025.3554338`
422+
"""
423+
424+
is_g_stc = np.isclose(effective_irradiance, 1000, rtol=0,
425+
atol=irradiance_tolerance)
426+
is_t_stc = np.isclose(temp_cell, 25, rtol=0,
427+
atol=temperature_tolerance)
428+
429+
if alpha_sc is None:
430+
mu_i_sc = _fit_tempco_pvsyst_iec61853_sandia_2025(i_sc[is_g_stc],
431+
temp_cell[is_g_stc])
432+
i_sc_ref = float(i_sc[is_g_stc & is_t_stc].item())
433+
alpha_sc = mu_i_sc * i_sc_ref
434+
435+
if beta_mp is None:
436+
beta_mp = _fit_tempco_pvsyst_iec61853_sandia_2025(v_mp[is_g_stc],
437+
temp_cell[is_g_stc])
438+
439+
R_sh_ref, R_sh_0, R_sh_exp = \
440+
_fit_shunt_resistances_pvsyst_iec61853_sandia_2025(
441+
i_sc, i_mp, v_mp, effective_irradiance, temp_cell, beta_mp,
442+
coeff=r_sh_coeff, min_irradiance=min_Rsh_irradiance)
443+
444+
if R_s is None:
445+
R_s = _fit_series_resistance_pvsyst_iec61853_sandia_2025(v_oc, i_mp,
446+
v_mp)
447+
448+
gamma_ref, mu_gamma = \
449+
_fit_diode_ideality_factor_pvsyst_iec61853_sandia_2025(
450+
i_sc[is_t_stc], v_oc[is_t_stc], i_mp[is_t_stc], v_mp[is_t_stc],
451+
effective_irradiance[is_t_stc], temp_cell[is_t_stc],
452+
R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series)
453+
454+
I_o_ref = _fit_saturation_current_pvsyst_iec61853_sandia_2025(
455+
i_sc, v_oc, effective_irradiance, temp_cell, gamma_ref, mu_gamma,
456+
R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series, EgRef
457+
)
458+
459+
I_L_ref = _fit_photocurrent_pvsyst_iec61853_sandia_2025(
460+
i_sc, effective_irradiance, temp_cell, alpha_sc,
461+
gamma_ref, mu_gamma,
462+
I_o_ref, R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series, EgRef
463+
)
464+
465+
gamma_ref, mu_gamma = \
466+
_fit_diode_ideality_factor_post_pvsyst_iec61853_sandia_2025(
467+
i_mp, v_mp, effective_irradiance, temp_cell, alpha_sc, I_L_ref,
468+
I_o_ref, R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series, EgRef)
469+
470+
fitted_params = dict(
471+
alpha_sc=alpha_sc,
472+
gamma_ref=gamma_ref,
473+
mu_gamma=mu_gamma,
474+
I_L_ref=I_L_ref,
475+
I_o_ref=I_o_ref,
476+
R_sh_ref=R_sh_ref,
477+
R_sh_0=R_sh_0,
478+
R_sh_exp=R_sh_exp,
479+
R_s=R_s,
480+
cells_in_series=cells_in_series,
481+
EgRef=EgRef,
482+
)
483+
return fitted_params
484+
485+
486+
def _fit_tempco_pvsyst_iec61853_sandia_2025(values, temp_cell,
487+
temp_cell_ref=25):
488+
fit = np.polynomial.polynomial.Polynomial.fit(temp_cell, values, deg=1)
489+
intercept, slope = fit.convert().coef
490+
value_ref = intercept + slope*temp_cell_ref
491+
return slope / value_ref
492+
493+
494+
def _fit_shunt_resistances_pvsyst_iec61853_sandia_2025(
495+
i_sc, i_mp, v_mp, effective_irradiance, temp_cell,
496+
beta_v_mp, coeff=0.2, min_irradiance=None):
497+
if min_irradiance is None:
498+
min_irradiance = 0
499+
500+
mask = effective_irradiance >= min_irradiance
501+
i_sc = i_sc[mask]
502+
i_mp = i_mp[mask]
503+
v_mp = v_mp[mask]
504+
effective_irradiance = effective_irradiance[mask]
505+
temp_cell = temp_cell[mask]
506+
507+
# Equation 10
508+
Rsh_est = (
509+
(v_mp / (1 + beta_v_mp * (temp_cell - 25)))
510+
/ (coeff * (i_sc - i_mp))
511+
)
512+
Rshexp = 5.5
513+
514+
# Eq 11
515+
y = Rsh_est
516+
x = np.exp(-Rshexp * effective_irradiance / 1000)
517+
518+
fit = np.polynomial.polynomial.Polynomial.fit(x, y, deg=1)
519+
intercept, slope = fit.convert().coef
520+
Rshbase = intercept
521+
Rsh0 = slope + Rshbase
522+
523+
# Eq 12
524+
expRshexp = np.exp(-Rshexp)
525+
Rshref = Rshbase * (1 - expRshexp) + Rsh0 * expRshexp
526+
527+
return Rshref, Rsh0, Rshexp
528+
529+
530+
def _fit_series_resistance_pvsyst_iec61853_sandia_2025(v_oc, i_mp, v_mp):
531+
# Stein et al 2014, https://doi.org/10.1109/PVSC.2014.6925326
532+
533+
# Eq 13
534+
x = np.array([np.ones(len(i_mp)), i_mp, np.log(i_mp), v_mp]).T
535+
y = v_oc
536+
537+
coeff, _, _, _ = np.linalg.lstsq(x, y, rcond=None)
538+
R_s = coeff[1]
539+
return R_s
540+
541+
542+
def _fit_diode_ideality_factor_pvsyst_iec61853_sandia_2025(
543+
i_sc, v_oc, i_mp, v_mp, effective_irradiance, temp_cell,
544+
R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series):
545+
546+
NsVth = _pvsyst_nNsVth(temp_cell, gamma=1, cells_in_series=cells_in_series)
547+
Rsh = _pvsyst_Rsh(effective_irradiance, R_sh_ref, R_sh_0, R_sh_exp)
548+
term1 = (i_sc * (1 + R_s/Rsh) - v_oc / Rsh) # Eq 15
549+
term2 = (i_sc - i_mp) * (1 + R_s/Rsh) - v_mp / Rsh # Eq 16
550+
551+
# Eq 14
552+
x1 = NsVth * np.log(term2 / term1)
553+
554+
x = np.array([x1]).T
555+
y = v_mp + i_mp*R_s - v_oc
556+
557+
coeff, _, _, _ = np.linalg.lstsq(x, y, rcond=None)
558+
gamma_ref = coeff[0]
559+
return gamma_ref, 0
560+
561+
562+
def _fit_saturation_current_pvsyst_iec61853_sandia_2025(
563+
i_sc, v_oc, effective_irradiance, temp_cell, gamma_ref, mu_gamma,
564+
R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series, EgRef):
565+
R_sh = _pvsyst_Rsh(effective_irradiance, R_sh_ref, R_sh_0, R_sh_exp)
566+
gamma = _pvsyst_gamma(temp_cell, gamma_ref, mu_gamma)
567+
nNsVth = _pvsyst_nNsVth(temp_cell, gamma, cells_in_series)
568+
569+
# Eq 17
570+
I_o_est = (i_sc * (1 + R_s/R_sh) - v_oc/R_sh) / (np.expm1(v_oc / nNsVth))
571+
x = _pvsyst_Io(temp_cell, gamma, I_o_ref=1, EgRef=EgRef)
572+
573+
# Eq 18
574+
log_I_o_ref = np.mean(np.log(I_o_est) - np.log(x))
575+
I_o_ref = np.exp(log_I_o_ref)
576+
577+
return I_o_ref
578+
579+
580+
def _fit_photocurrent_pvsyst_iec61853_sandia_2025(
581+
i_sc, effective_irradiance, temp_cell, alpha_sc, gamma_ref,
582+
mu_gamma, I_o_ref, R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series,
583+
EgRef):
584+
R_sh = _pvsyst_Rsh(effective_irradiance, R_sh_ref, R_sh_0, R_sh_exp)
585+
gamma = _pvsyst_gamma(temp_cell, gamma_ref, mu_gamma)
586+
I_o = _pvsyst_Io(temp_cell, gamma, I_o_ref, EgRef)
587+
nNsVth = _pvsyst_nNsVth(temp_cell, gamma, cells_in_series)
588+
589+
# Eq 19
590+
I_L_est = i_sc + I_o * (np.expm1(i_sc * R_s / nNsVth)) + i_sc * R_s / R_sh
591+
592+
# Eq 20
593+
x = np.array([effective_irradiance / 1000]).T
594+
y = I_L_est - effective_irradiance / 1000 * alpha_sc * (temp_cell - 25)
595+
coeff, _, _, _ = np.linalg.lstsq(x, y, rcond=None)
596+
I_L_ref = coeff[0]
597+
return I_L_ref
598+
599+
600+
def _fit_diode_ideality_factor_post_pvsyst_iec61853_sandia_2025(
601+
i_mp, v_mp, effective_irradiance, temp_cell, alpha_sc, I_L_ref,
602+
I_o_ref, R_sh_ref, R_sh_0, R_sh_exp, R_s, cells_in_series, EgRef):
603+
604+
Rsh = _pvsyst_Rsh(effective_irradiance, R_sh_ref, R_sh_0, R_sh_exp)
605+
I_L = _pvsyst_IL(effective_irradiance, temp_cell, I_L_ref, alpha_sc)
606+
NsVth = _pvsyst_nNsVth(temp_cell, gamma=1, cells_in_series=cells_in_series)
607+
608+
Tref_K = 25 + 273.15
609+
Tcell_K = temp_cell + 273.15
610+
611+
# Eq 21
612+
k = constants.k # Boltzmann constant in J/K
613+
q = constants.e # elementary charge in coulomb
614+
numerator = (
615+
(q * EgRef / k) * (1/Tref_K - 1/Tcell_K)
616+
+ (v_mp + i_mp*R_s) / NsVth
617+
)
618+
denominator = (
619+
np.log((I_L - i_mp - (v_mp+i_mp*R_s) / Rsh) / I_o_ref)
620+
- 3 * np.log(Tcell_K / Tref_K)
621+
)
622+
gamma_est = numerator / denominator
623+
624+
# Eq 22
625+
x = np.array([np.ones(len(i_mp)), temp_cell - 25]).T
626+
y = gamma_est
627+
628+
coeff, _, _, _ = np.linalg.lstsq(x, y, rcond=None)
629+
gamma_ref, mu_gamma = coeff
630+
return gamma_ref, mu_gamma

0 commit comments

Comments
 (0)