Skip to content

Commit 0fde334

Browse files
authored
Merge branch 'main' into batzelis
2 parents 839d0b5 + 74ded02 commit 0fde334

6 files changed

Lines changed: 237 additions & 97 deletions

File tree

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ Enhancements
2525
the single-diode equation. (:pull:`2563`)
2626
* Add :py:func:`~pvlib.pvarray.batzelis`, a function to estimate maximum power
2727
open circuit, and short circuit points from datasheet values. (:pull:`2563`)
28+
* Add ``method='chandrupatla'`` (faster than ``brentq`` and slower than ``newton``,
29+
but convergence is guaranteed) as an option for
30+
:py:func:`pvlib.pvsystem.singlediode`,
31+
:py:func:`~pvlib.pvsystem.i_from_v`,
32+
:py:func:`~pvlib.pvsystem.v_from_i`,
33+
:py:func:`~pvlib.pvsystem.max_power_point`,
34+
:py:func:`~pvlib.singlediode.bishop88_mpp`,
35+
:py:func:`~pvlib.singlediode.bishop88_v_from_i`, and
36+
:py:func:`~pvlib.singlediode.bishop88_i_from_v`. (:issue:`2497`, :pull:`2498`)
2837

2938

3039

@@ -50,3 +59,4 @@ Maintenance
5059

5160
Contributors
5261
~~~~~~~~~~~~
62+

pvlib/pvsystem.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2498,7 +2498,11 @@ def singlediode(photocurrent, saturation_current, resistance_series,
24982498
24992499
method : str, default 'lambertw'
25002500
Determines the method used to calculate points on the IV curve. The
2501-
options are ``'lambertw'``, ``'newton'``, or ``'brentq'``.
2501+
options are ``'lambertw'``, ``'newton'``, ``'brentq'``, or
2502+
``'chandrupatla'``.
2503+
2504+
.. note::
2505+
``'chandrupatla'`` requires scipy 1.15 or greater.
25022506
25032507
Returns
25042508
-------
@@ -2630,7 +2634,11 @@ def max_power_point(photocurrent, saturation_current, resistance_series,
26302634
cells ``Ns`` and the builtin voltage ``Vbi`` of the intrinsic layer.
26312635
[V].
26322636
method : str
2633-
either ``'newton'`` or ``'brentq'``
2637+
either ``'newton'``, ``'brentq'``, or ``'chandrupatla'``.
2638+
2639+
.. note::
2640+
``'chandrupatla'`` requires scipy 1.15 or greater.
2641+
26342642
26352643
Returns
26362644
-------
@@ -2713,8 +2721,13 @@ def v_from_i(current, photocurrent, saturation_current, resistance_series,
27132721
0 < nNsVth
27142722
27152723
method : str
2716-
Method to use: ``'lambertw'``, ``'newton'``, or ``'brentq'``. *Note*:
2717-
``'brentq'`` is limited to 1st quadrant only.
2724+
Method to use: ``'lambertw'``, ``'newton'``, ``'brentq'``, or
2725+
``'chandrupatla'``. *Note*: ``'brentq'`` is limited to
2726+
non-negative current.
2727+
2728+
.. note::
2729+
``'chandrupatla'`` requires scipy 1.15 or greater.
2730+
27182731
27192732
Returns
27202733
-------
@@ -2795,8 +2808,13 @@ def i_from_v(voltage, photocurrent, saturation_current, resistance_series,
27952808
0 < nNsVth
27962809
27972810
method : str
2798-
Method to use: ``'lambertw'``, ``'newton'``, or ``'brentq'``. *Note*:
2799-
``'brentq'`` is limited to 1st quadrant only.
2811+
Method to use: ``'lambertw'``, ``'newton'``, ``'brentq'``, or
2812+
``'chandrupatla'``. *Note*: ``'brentq'`` is limited to
2813+
non-negative current.
2814+
2815+
.. note::
2816+
``'chandrupatla'`` requires scipy 1.15 or greater.
2817+
28002818
28012819
Returns
28022820
-------

pvlib/singlediode.py

Lines changed: 127 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -110,13 +110,13 @@ def bishop88(diode_voltage, photocurrent, saturation_current,
110110
(a-Si) modules that is the product of the PV module number of series
111111
cells :math:`N_{s}` and the builtin voltage :math:`V_{bi}` of the
112112
intrinsic layer. [V].
113-
breakdown_factor : float, default 0
113+
breakdown_factor : numeric, default 0
114114
fraction of ohmic current involved in avalanche breakdown :math:`a`.
115115
Default of 0 excludes the reverse bias term from the model. [unitless]
116-
breakdown_voltage : float, default -5.5
116+
breakdown_voltage : numeric, default -5.5
117117
reverse breakdown voltage of the photovoltaic junction :math:`V_{br}`
118118
[V]
119-
breakdown_exp : float, default 3.28
119+
breakdown_exp : numeric, default 3.28
120120
avalanche breakdown exponent :math:`m` [unitless]
121121
gradients : bool
122122
False returns only I, V, and P. True also returns gradients
@@ -163,12 +163,11 @@ def bishop88(diode_voltage, photocurrent, saturation_current,
163163
# calculate temporary values to simplify calculations
164164
v_star = diode_voltage / nNsVth # non-dimensional diode voltage
165165
g_sh = 1.0 / resistance_shunt # conductance
166-
if breakdown_factor > 0: # reverse bias is considered
167-
brk_term = 1 - diode_voltage / breakdown_voltage
168-
brk_pwr = np.power(brk_term, -breakdown_exp)
169-
i_breakdown = breakdown_factor * diode_voltage * g_sh * brk_pwr
170-
else:
171-
i_breakdown = 0.
166+
167+
brk_term = 1 - diode_voltage / breakdown_voltage
168+
brk_pwr = np.power(brk_term, -breakdown_exp)
169+
i_breakdown = breakdown_factor * diode_voltage * g_sh * brk_pwr
170+
172171
i = (photocurrent - saturation_current * np.expm1(v_star) # noqa: W503
173172
- diode_voltage * g_sh - i_recomb - i_breakdown) # noqa: W503
174173
v = diode_voltage - i * resistance_series
@@ -178,18 +177,14 @@ def bishop88(diode_voltage, photocurrent, saturation_current,
178177
grad_i_recomb = np.where(is_recomb, i_recomb / v_recomb, 0)
179178
grad_2i_recomb = np.where(is_recomb, 2 * grad_i_recomb / v_recomb, 0)
180179
g_diode = saturation_current * np.exp(v_star) / nNsVth # conductance
181-
if breakdown_factor > 0: # reverse bias is considered
182-
brk_pwr_1 = np.power(brk_term, -breakdown_exp - 1)
183-
brk_pwr_2 = np.power(brk_term, -breakdown_exp - 2)
184-
brk_fctr = breakdown_factor * g_sh
185-
grad_i_brk = brk_fctr * (brk_pwr + diode_voltage *
186-
-breakdown_exp * brk_pwr_1)
187-
grad2i_brk = (brk_fctr * -breakdown_exp # noqa: W503
188-
* (2 * brk_pwr_1 + diode_voltage # noqa: W503
189-
* (-breakdown_exp - 1) * brk_pwr_2)) # noqa: W503
190-
else:
191-
grad_i_brk = 0.
192-
grad2i_brk = 0.
180+
brk_pwr_1 = np.power(brk_term, -breakdown_exp - 1)
181+
brk_pwr_2 = np.power(brk_term, -breakdown_exp - 2)
182+
brk_fctr = breakdown_factor * g_sh
183+
grad_i_brk = brk_fctr * (brk_pwr + diode_voltage *
184+
-breakdown_exp * brk_pwr_1)
185+
grad2i_brk = (brk_fctr * -breakdown_exp # noqa: W503
186+
* (2 * brk_pwr_1 + diode_voltage # noqa: W503
187+
* (-breakdown_exp - 1) * brk_pwr_2)) # noqa: W503
193188
grad_i = -g_diode - g_sh - grad_i_recomb - grad_i_brk # di/dvd
194189
grad_v = 1.0 - grad_i * resistance_series # dv/dvd
195190
# dp/dv = d(iv)/dv = v * di/dv + i
@@ -248,12 +243,19 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current,
248243
breakdown_exp : float, default 3.28
249244
avalanche breakdown exponent :math:`m` [unitless]
250245
method : str, default 'newton'
251-
Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'``
252-
if ``breakdown_factor`` is not 0.
246+
Either ``'newton'``, ``'brentq'``, or ``'chandrupatla'``.
247+
''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0.
248+
249+
.. note::
250+
``'chandrupatla'`` requires scipy 1.15 or greater.
251+
253252
method_kwargs : dict, optional
254-
Keyword arguments passed to root finder method. See
255-
:py:func:`scipy:scipy.optimize.brentq` and
256-
:py:func:`scipy:scipy.optimize.newton` parameters.
253+
Keyword arguments passed to the root finder. For options, see:
254+
255+
* ``method='brentq'``: :py:func:`scipy:scipy.optimize.brentq`
256+
* ``method='newton'``: :py:func:`scipy:scipy.optimize.newton`
257+
* ``method='chandrupatla'``: :py:func:`scipy:scipy.optimize.elementwise.find_root`
258+
257259
``'full_output': True`` is allowed, and ``optimizer_output`` would be
258260
returned. See examples section.
259261
@@ -292,7 +294,7 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current,
292294
.. [1] "Computer simulation of the effects of electrical mismatches in
293295
photovoltaic cell interconnection circuits" JW Bishop, Solar Cell (1988)
294296
:doi:`10.1016/0379-6787(88)90059-2`
295-
"""
297+
""" # noqa: E501
296298
# collect args
297299
args = (photocurrent, saturation_current,
298300
resistance_series, resistance_shunt, nNsVth, d2mutau, NsVbi,
@@ -334,6 +336,30 @@ def vd_from_brent(voc, v, iph, isat, rs, rsh, gamma, d2mutau, NsVbi,
334336
vd = newton(func=lambda x, *a: fv(x, voltage, *a), x0=x0,
335337
fprime=lambda x, *a: bishop88(x, *a, gradients=True)[4],
336338
args=args, **method_kwargs)
339+
elif method == 'chandrupatla':
340+
try:
341+
from scipy.optimize.elementwise import find_root
342+
except ModuleNotFoundError as e:
343+
# TODO remove this when our minimum scipy version is >=1.15
344+
msg = (
345+
"method='chandrupatla' requires scipy v1.15 or greater "
346+
"(available for Python 3.10+). "
347+
"Select another method, or update your version of scipy."
348+
)
349+
raise ImportError(msg) from e
350+
351+
voc_est = estimate_voc(photocurrent, saturation_current, nNsVth)
352+
shape = _shape_of_max_size(voltage, voc_est)
353+
vlo = np.zeros(shape)
354+
vhi = np.full(shape, voc_est)
355+
bounds = (vlo, vhi)
356+
kwargs_trimmed = method_kwargs.copy()
357+
kwargs_trimmed.pop("full_output", None) # not valid for find_root
358+
359+
result = find_root(fv, bounds, args=(voltage, *args), **kwargs_trimmed)
360+
vd = result.x
361+
if method_kwargs.get('full_output'):
362+
vd = (vd, result) # mimic the other methods
337363
else:
338364
raise NotImplementedError("Method '%s' isn't implemented" % method)
339365

@@ -389,12 +415,19 @@ def bishop88_v_from_i(current, photocurrent, saturation_current,
389415
breakdown_exp : float, default 3.28
390416
avalanche breakdown exponent :math:`m` [unitless]
391417
method : str, default 'newton'
392-
Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'``
393-
if ``breakdown_factor`` is not 0.
418+
Either ``'newton'``, ``'brentq'``, or ``'chandrupatla'``.
419+
''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0.
420+
421+
.. note::
422+
``'chandrupatla'`` requires scipy 1.15 or greater.
423+
394424
method_kwargs : dict, optional
395-
Keyword arguments passed to root finder method. See
396-
:py:func:`scipy:scipy.optimize.brentq` and
397-
:py:func:`scipy:scipy.optimize.newton` parameters.
425+
Keyword arguments passed to the root finder. For options, see:
426+
427+
* ``method='brentq'``: :py:func:`scipy:scipy.optimize.brentq`
428+
* ``method='newton'``: :py:func:`scipy:scipy.optimize.newton`
429+
* ``method='chandrupatla'``: :py:func:`scipy:scipy.optimize.elementwise.find_root`
430+
398431
``'full_output': True`` is allowed, and ``optimizer_output`` would be
399432
returned. See examples section.
400433
@@ -433,7 +466,7 @@ def bishop88_v_from_i(current, photocurrent, saturation_current,
433466
.. [1] "Computer simulation of the effects of electrical mismatches in
434467
photovoltaic cell interconnection circuits" JW Bishop, Solar Cell (1988)
435468
:doi:`10.1016/0379-6787(88)90059-2`
436-
"""
469+
""" # noqa: E501
437470
# collect args
438471
args = (photocurrent, saturation_current,
439472
resistance_series, resistance_shunt, nNsVth, d2mutau, NsVbi,
@@ -475,6 +508,29 @@ def vd_from_brent(voc, i, iph, isat, rs, rsh, gamma, d2mutau, NsVbi,
475508
vd = newton(func=lambda x, *a: fi(x, current, *a), x0=x0,
476509
fprime=lambda x, *a: bishop88(x, *a, gradients=True)[3],
477510
args=args, **method_kwargs)
511+
elif method == 'chandrupatla':
512+
try:
513+
from scipy.optimize.elementwise import find_root
514+
except ModuleNotFoundError as e:
515+
# TODO remove this when our minimum scipy version is >=1.15
516+
msg = (
517+
"method='chandrupatla' requires scipy v1.15 or greater "
518+
"(available for Python 3.10+). "
519+
"Select another method, or update your version of scipy."
520+
)
521+
raise ImportError(msg) from e
522+
523+
shape = _shape_of_max_size(current, voc_est)
524+
vlo = np.zeros(shape)
525+
vhi = np.full(shape, voc_est)
526+
bounds = (vlo, vhi)
527+
kwargs_trimmed = method_kwargs.copy()
528+
kwargs_trimmed.pop("full_output", None) # not valid for find_root
529+
530+
result = find_root(fi, bounds, args=(current, *args), **kwargs_trimmed)
531+
vd = result.x
532+
if method_kwargs.get('full_output'):
533+
vd = (vd, result) # mimic the other methods
478534
else:
479535
raise NotImplementedError("Method '%s' isn't implemented" % method)
480536

@@ -527,12 +583,19 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series,
527583
breakdown_exp : numeric, default 3.28
528584
avalanche breakdown exponent :math:`m` [unitless]
529585
method : str, default 'newton'
530-
Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'``
531-
if ``breakdown_factor`` is not 0.
586+
Either ``'newton'``, ``'brentq'``, or ``'chandrupatla'``.
587+
''method'' must be ``'newton'`` if ``breakdown_factor`` is not 0.
588+
589+
.. note::
590+
``'chandrupatla'`` requires scipy 1.15 or greater.
591+
532592
method_kwargs : dict, optional
533-
Keyword arguments passed to root finder method. See
534-
:py:func:`scipy:scipy.optimize.brentq` and
535-
:py:func:`scipy:scipy.optimize.newton` parameters.
593+
Keyword arguments passed to the root finder. For options, see:
594+
595+
* ``method='brentq'``: :py:func:`scipy:scipy.optimize.brentq`
596+
* ``method='newton'``: :py:func:`scipy:scipy.optimize.newton`
597+
* ``method='chandrupatla'``: :py:func:`scipy:scipy.optimize.elementwise.find_root`
598+
536599
``'full_output': True`` is allowed, and ``optimizer_output`` would be
537600
returned. See examples section.
538601
@@ -572,7 +635,7 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series,
572635
.. [1] "Computer simulation of the effects of electrical mismatches in
573636
photovoltaic cell interconnection circuits" JW Bishop, Solar Cell (1988)
574637
:doi:`10.1016/0379-6787(88)90059-2`
575-
"""
638+
""" # noqa: E501
576639
# collect args
577640
args = (photocurrent, saturation_current,
578641
resistance_series, resistance_shunt, nNsVth, d2mutau, NsVbi,
@@ -612,6 +675,31 @@ def fmpp(x, *a):
612675
vd = newton(func=fmpp, x0=x0,
613676
fprime=lambda x, *a: bishop88(x, *a, gradients=True)[7],
614677
args=args, **method_kwargs)
678+
elif method == 'chandrupatla':
679+
try:
680+
from scipy.optimize.elementwise import find_root
681+
except ModuleNotFoundError as e:
682+
# TODO remove this when our minimum scipy version is >=1.15
683+
msg = (
684+
"method='chandrupatla' requires scipy v1.15 or greater "
685+
"(available for Python 3.10+). "
686+
"Select another method, or update your version of scipy."
687+
)
688+
raise ImportError(msg) from e
689+
690+
vlo = np.zeros_like(photocurrent)
691+
vhi = np.full_like(photocurrent, voc_est)
692+
kwargs_trimmed = method_kwargs.copy()
693+
kwargs_trimmed.pop("full_output", None) # not valid for find_root
694+
695+
result = find_root(fmpp,
696+
(vlo, vhi),
697+
args=args,
698+
**kwargs_trimmed)
699+
vd = result.x
700+
if method_kwargs.get('full_output'):
701+
vd = (vd, result) # mimic the other methods
702+
615703
else:
616704
raise NotImplementedError("Method '%s' isn't implemented" % method)
617705

tests/conftest.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import warnings
44

55
import pandas as pd
6+
import scipy
67
import os
78
from packaging.version import Version
89
import pytest
@@ -194,6 +195,17 @@ def has_spa_c():
194195
reason="requires pandas>=2.0.0")
195196

196197

198+
# single-diode equation functions have method=='chandrupatla', which relies
199+
# on scipy.optimize.elementwise.find_root, which is only available in
200+
# scipy>=1.15.
201+
# TODO remove this when our minimum scipy is >=1.15
202+
chandrupatla_available = Version(scipy.__version__) >= Version("1.15.0")
203+
chandrupatla = pytest.param(
204+
"chandrupatla", marks=pytest.mark.skipif(not chandrupatla_available,
205+
reason="needs scipy 1.15")
206+
)
207+
208+
197209
@pytest.fixture()
198210
def golden():
199211
return Location(39.742476, -105.1786, 'America/Denver', 1830.14)

0 commit comments

Comments
 (0)