Skip to content

Commit 19467f3

Browse files
echedey-lsadriessecwhansekandersolar
authored
Allow users to interact with root finders parameters (#1764)
* Change of use in bishop88_i_from_v * What the warnings test should look like * Apply Adriesse's suggestion Co-Authored-By: Anton Driesse <[email protected]> * Update test_singlediode.py * Minimun runnable test for pass conditions * Cleanup import * Add functional test for success and failing calls with kwargs * Add documentation (minimal, only bishop88_i_from_v) * Try to fix scipy links (I) * It worked 🎉🎉🎉 * Move default newton values to _prepare_newton_inputs * Modify bishop88_v_from_i, bishop88_mpp; update tests and docs `maxiter` had to be changed so solutions converge near target value correctly. * Typo in kwargs type error Co-Authored-By: Cliff Hansen <[email protected]> * Add what's new entry * Delete fprime2 default newton param Co-Authored-By: Kevin Anderson <[email protected]> * Rename kwargs to method_kwargs Co-Authored-By: Anton Driesse <[email protected]> * Adriesse's suggestion to make full output available Missing tests Co-Authored-By: Anton Driesse <[email protected]> * Update tests from Kevin suggestion Co-Authored-By: Kevin Anderson <[email protected]> * I won't miss Py3.7 * Uhm, yeah, don't like this solution but anyways... * Tests should be fixed now * full_output=True tests * Update docs * Now these are tests * Bruh should have run locally * Docs: add use cases and document as `**method_kwargs :` * Tests: also test expected length of the first value In case some unpacking went wrong * Docs: doctest examples - make functional * Behaviour & docs: Change how method_kwargs are passed, enhance docs * omg who would have expected the mutable default parameter bug * Apply suggestions from code review Co-authored by @kandersolar Co-authored-by: Kevin Anderson <[email protected]> --------- Co-authored-by: Anton Driesse <[email protected]> Co-authored-by: Cliff Hansen <[email protected]> Co-authored-by: Kevin Anderson <[email protected]> Co-authored-by: Kevin Anderson <[email protected]>
1 parent 964dc43 commit 19467f3

File tree

3 files changed

+317
-28
lines changed

3 files changed

+317
-28
lines changed

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ Enhancements
3131
~~~~~~~~~~~~
3232
* Added `map_variables` parameter to :py:func:`pvlib.iotools.read_srml`
3333
and :py:func:`pvlib.iotools.read_srml_month_from_solardat` (:pull:`1773`)
34+
* Allow passing keyword arguments to :py:func:`scipy:scipy.optimize.brentq` and
35+
:py:func:`scipy:scipy.optimize.newton` solvers in
36+
:py:func:`~pvlib.singlediode.bishop88_mpp`,
37+
:py:func:`~pvlib.singlediode.bishop88_i_from_v` and
38+
:py:func:`~pvlib.singlediode.bishop88_v_from_i`. Among others,
39+
tolerance and number of iterations can be set.
40+
(:issue:`1249`, :pull:`1764`)
3441
* Improved `ModelChainResult.__repr__` (:pull:`1236`)
3542

3643

@@ -57,5 +64,5 @@ Contributors
5764
~~~~~~~~~~~~
5865
* Taos Transue (:ghuser:`reepoi`)
5966
* Adam R. Jensen (:ghuser:`AdamRJensen`)
67+
* Echedey Luis (:ghuser:`echedey-ls`)
6068
* Cliff Hansen (:ghuser:`cwhanse`)
61-

pvlib/singlediode.py

Lines changed: 177 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22
Low-level functions for solving the single diode equation.
33
"""
44

5-
from functools import partial
65
import numpy as np
76
from pvlib.tools import _golden_sect_DataFrame
87

98
from scipy.optimize import brentq, newton
109
from scipy.special import lambertw
1110

12-
# set keyword arguments for all uses of newton in this module
13-
newton = partial(newton, tol=1e-6, maxiter=100, fprime2=None)
11+
# newton method default parameters for this module
12+
NEWTON_DEFAULT_PARAMS = {
13+
'tol': 1e-6,
14+
'maxiter': 100
15+
}
1416

1517
# intrinsic voltage per cell junction for a:Si, CdTe, Mertens et al.
1618
VOLTAGE_BUILTIN = 0.9 # [V]
@@ -206,7 +208,7 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current,
206208
resistance_series, resistance_shunt, nNsVth,
207209
d2mutau=0, NsVbi=np.Inf, breakdown_factor=0.,
208210
breakdown_voltage=-5.5, breakdown_exp=3.28,
209-
method='newton'):
211+
method='newton', method_kwargs=None):
210212
"""
211213
Find current given any voltage.
212214
@@ -247,22 +249,59 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current,
247249
method : str, default 'newton'
248250
Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'``
249251
if ``breakdown_factor`` is not 0.
252+
method_kwargs : dict, optional
253+
Keyword arguments passed to root finder method. See
254+
:py:func:`scipy:scipy.optimize.brentq` and
255+
:py:func:`scipy:scipy.optimize.newton` parameters.
256+
``'full_output': True`` is allowed, and ``optimizer_output`` would be
257+
returned. See examples section.
250258
251259
Returns
252260
-------
253261
current : numeric
254262
current (I) at the specified voltage (V). [A]
263+
optimizer_output : tuple, optional, if specified in ``method_kwargs``
264+
see root finder documentation for selected method.
265+
Found root is diode voltage in [1]_.
266+
267+
Examples
268+
--------
269+
Using the following arguments that may come from any
270+
`calcparams_.*` function in :py:mod:`pvlib.pvsystem`:
271+
272+
>>> args = {'photocurrent': 1., 'saturation_current': 9e-10, 'nNsVth': 4.,
273+
... 'resistance_series': 4., 'resistance_shunt': 5000.0}
274+
275+
Use default values:
276+
277+
>>> i = bishop88_i_from_v(0.0, **args)
278+
279+
Specify tolerances and maximum number of iterations:
280+
281+
>>> i = bishop88_i_from_v(0.0, **args, method='newton',
282+
... method_kwargs={'tol': 1e-3, 'rtol': 1e-3, 'maxiter': 20})
283+
284+
Retrieve full output from the root finder:
285+
286+
>>> i, method_output = bishop88_i_from_v(0.0, **args, method='newton',
287+
... method_kwargs={'full_output': True})
255288
"""
256289
# collect args
257290
args = (photocurrent, saturation_current, resistance_series,
258291
resistance_shunt, nNsVth, d2mutau, NsVbi,
259292
breakdown_factor, breakdown_voltage, breakdown_exp)
293+
method = method.lower()
294+
295+
# method_kwargs create dict if not provided
296+
# this pattern avoids bugs with Mutable Default Parameters
297+
if not method_kwargs:
298+
method_kwargs = {}
260299

261300
def fv(x, v, *a):
262301
# calculate voltage residual given diode voltage "x"
263302
return bishop88(x, *a)[1] - v
264303

265-
if method.lower() == 'brentq':
304+
if method == 'brentq':
266305
# first bound the search using voc
267306
voc_est = estimate_voc(photocurrent, saturation_current, nNsVth)
268307

@@ -274,27 +313,37 @@ def vd_from_brent(voc, v, iph, isat, rs, rsh, gamma, d2mutau, NsVbi,
274313
return brentq(fv, 0.0, voc,
275314
args=(v, iph, isat, rs, rsh, gamma, d2mutau, NsVbi,
276315
breakdown_factor, breakdown_voltage,
277-
breakdown_exp))
316+
breakdown_exp),
317+
**method_kwargs)
278318

279319
vd_from_brent_vectorized = np.vectorize(vd_from_brent)
280320
vd = vd_from_brent_vectorized(voc_est, voltage, *args)
281-
elif method.lower() == 'newton':
321+
elif method == 'newton':
282322
# make sure all args are numpy arrays if max size > 1
283323
# if voltage is an array, then make a copy to use for initial guess, v0
284-
args, v0 = _prepare_newton_inputs((voltage,), args, voltage)
324+
args, v0, method_kwargs = \
325+
_prepare_newton_inputs((voltage,), args, voltage, method_kwargs)
285326
vd = newton(func=lambda x, *a: fv(x, voltage, *a), x0=v0,
286327
fprime=lambda x, *a: bishop88(x, *a, gradients=True)[4],
287-
args=args)
328+
args=args,
329+
**method_kwargs)
288330
else:
289331
raise NotImplementedError("Method '%s' isn't implemented" % method)
290-
return bishop88(vd, *args)[0]
332+
333+
# When 'full_output' parameter is specified, returned 'vd' is a tuple with
334+
# many elements, where the root is the first one. So we use it to output
335+
# the bishop88 result and return tuple(scalar, tuple with method results)
336+
if method_kwargs.get('full_output') is True:
337+
return (bishop88(vd[0], *args)[0], vd)
338+
else:
339+
return bishop88(vd, *args)[0]
291340

292341

293342
def bishop88_v_from_i(current, photocurrent, saturation_current,
294343
resistance_series, resistance_shunt, nNsVth,
295344
d2mutau=0, NsVbi=np.Inf, breakdown_factor=0.,
296345
breakdown_voltage=-5.5, breakdown_exp=3.28,
297-
method='newton'):
346+
method='newton', method_kwargs=None):
298347
"""
299348
Find voltage given any current.
300349
@@ -335,24 +384,62 @@ def bishop88_v_from_i(current, photocurrent, saturation_current,
335384
method : str, default 'newton'
336385
Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'``
337386
if ``breakdown_factor`` is not 0.
387+
method_kwargs : dict, optional
388+
Keyword arguments passed to root finder method. See
389+
:py:func:`scipy:scipy.optimize.brentq` and
390+
:py:func:`scipy:scipy.optimize.newton` parameters.
391+
``'full_output': True`` is allowed, and ``optimizer_output`` would be
392+
returned. See examples section.
338393
339394
Returns
340395
-------
341396
voltage : numeric
342397
voltage (V) at the specified current (I) in volts [V]
398+
optimizer_output : tuple, optional, if specified in ``method_kwargs``
399+
see root finder documentation for selected method.
400+
Found root is diode voltage in [1]_.
401+
402+
Examples
403+
--------
404+
Using the following arguments that may come from any
405+
`calcparams_.*` function in :py:mod:`pvlib.pvsystem`:
406+
407+
>>> args = {'photocurrent': 1., 'saturation_current': 9e-10, 'nNsVth': 4.,
408+
... 'resistance_series': 4., 'resistance_shunt': 5000.0}
409+
410+
Use default values:
411+
412+
>>> v = bishop88_v_from_i(0.0, **args)
413+
414+
Specify tolerances and maximum number of iterations:
415+
416+
>>> v = bishop88_v_from_i(0.0, **args, method='newton',
417+
... method_kwargs={'tol': 1e-3, 'rtol': 1e-3, 'maxiter': 20})
418+
419+
Retrieve full output from the root finder:
420+
421+
>>> v, method_output = bishop88_v_from_i(0.0, **args, method='newton',
422+
... method_kwargs={'full_output': True})
343423
"""
344424
# collect args
345425
args = (photocurrent, saturation_current, resistance_series,
346426
resistance_shunt, nNsVth, d2mutau, NsVbi, breakdown_factor,
347427
breakdown_voltage, breakdown_exp)
428+
method = method.lower()
429+
430+
# method_kwargs create dict if not provided
431+
# this pattern avoids bugs with Mutable Default Parameters
432+
if not method_kwargs:
433+
method_kwargs = {}
434+
348435
# first bound the search using voc
349436
voc_est = estimate_voc(photocurrent, saturation_current, nNsVth)
350437

351438
def fi(x, i, *a):
352439
# calculate current residual given diode voltage "x"
353440
return bishop88(x, *a)[0] - i
354441

355-
if method.lower() == 'brentq':
442+
if method == 'brentq':
356443
# brentq only works with scalar inputs, so we need a set up function
357444
# and np.vectorize to repeatedly call the optimizer with the right
358445
# arguments for possible array input
@@ -361,26 +448,36 @@ def vd_from_brent(voc, i, iph, isat, rs, rsh, gamma, d2mutau, NsVbi,
361448
return brentq(fi, 0.0, voc,
362449
args=(i, iph, isat, rs, rsh, gamma, d2mutau, NsVbi,
363450
breakdown_factor, breakdown_voltage,
364-
breakdown_exp))
451+
breakdown_exp),
452+
**method_kwargs)
365453

366454
vd_from_brent_vectorized = np.vectorize(vd_from_brent)
367455
vd = vd_from_brent_vectorized(voc_est, current, *args)
368-
elif method.lower() == 'newton':
456+
elif method == 'newton':
369457
# make sure all args are numpy arrays if max size > 1
370458
# if voc_est is an array, then make a copy to use for initial guess, v0
371-
args, v0 = _prepare_newton_inputs((current,), args, voc_est)
459+
args, v0, method_kwargs = \
460+
_prepare_newton_inputs((current,), args, voc_est, method_kwargs)
372461
vd = newton(func=lambda x, *a: fi(x, current, *a), x0=v0,
373462
fprime=lambda x, *a: bishop88(x, *a, gradients=True)[3],
374-
args=args)
463+
args=args,
464+
**method_kwargs)
375465
else:
376466
raise NotImplementedError("Method '%s' isn't implemented" % method)
377-
return bishop88(vd, *args)[1]
467+
468+
# When 'full_output' parameter is specified, returned 'vd' is a tuple with
469+
# many elements, where the root is the first one. So we use it to output
470+
# the bishop88 result and return tuple(scalar, tuple with method results)
471+
if method_kwargs.get('full_output') is True:
472+
return (bishop88(vd[0], *args)[1], vd)
473+
else:
474+
return bishop88(vd, *args)[1]
378475

379476

380477
def bishop88_mpp(photocurrent, saturation_current, resistance_series,
381478
resistance_shunt, nNsVth, d2mutau=0, NsVbi=np.Inf,
382479
breakdown_factor=0., breakdown_voltage=-5.5,
383-
breakdown_exp=3.28, method='newton'):
480+
breakdown_exp=3.28, method='newton', method_kwargs=None):
384481
"""
385482
Find max power point.
386483
@@ -419,43 +516,91 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series,
419516
method : str, default 'newton'
420517
Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'``
421518
if ``breakdown_factor`` is not 0.
519+
method_kwargs : dict, optional
520+
Keyword arguments passed to root finder method. See
521+
:py:func:`scipy:scipy.optimize.brentq` and
522+
:py:func:`scipy:scipy.optimize.newton` parameters.
523+
``'full_output': True`` is allowed, and ``optimizer_output`` would be
524+
returned. See examples section.
422525
423526
Returns
424527
-------
425528
tuple
426529
max power current ``i_mp`` [A], max power voltage ``v_mp`` [V], and
427530
max power ``p_mp`` [W]
531+
optimizer_output : tuple, optional, if specified in ``method_kwargs``
532+
see root finder documentation for selected method.
533+
Found root is diode voltage in [1]_.
534+
535+
Examples
536+
--------
537+
Using the following arguments that may come from any
538+
`calcparams_.*` function in :py:mod:`pvlib.pvsystem`:
539+
540+
>>> args = {'photocurrent': 1., 'saturation_current': 9e-10, 'nNsVth': 4.,
541+
... 'resistance_series': 4., 'resistance_shunt': 5000.0}
542+
543+
Use default values:
544+
545+
>>> i_mp, v_mp, p_mp = bishop88_mpp(**args)
546+
547+
Specify tolerances and maximum number of iterations:
548+
549+
>>> i_mp, v_mp, p_mp = bishop88_mpp(**args, method='newton',
550+
... method_kwargs={'tol': 1e-3, 'rtol': 1e-3, 'maxiter': 20})
551+
552+
Retrieve full output from the root finder:
553+
554+
>>> (i_mp, v_mp, p_mp), method_output = bishop88_mpp(**args,
555+
... method='newton', method_kwargs={'full_output': True})
428556
"""
429557
# collect args
430558
args = (photocurrent, saturation_current, resistance_series,
431559
resistance_shunt, nNsVth, d2mutau, NsVbi, breakdown_factor,
432560
breakdown_voltage, breakdown_exp)
561+
method = method.lower()
562+
563+
# method_kwargs create dict if not provided
564+
# this pattern avoids bugs with Mutable Default Parameters
565+
if not method_kwargs:
566+
method_kwargs = {}
567+
433568
# first bound the search using voc
434569
voc_est = estimate_voc(photocurrent, saturation_current, nNsVth)
435570

436571
def fmpp(x, *a):
437572
return bishop88(x, *a, gradients=True)[6]
438573

439-
if method.lower() == 'brentq':
574+
if method == 'brentq':
440575
# break out arguments for numpy.vectorize to handle broadcasting
441576
vec_fun = np.vectorize(
442577
lambda voc, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, vbr_a, vbr,
443578
vbr_exp: brentq(fmpp, 0.0, voc,
444579
args=(iph, isat, rs, rsh, gamma, d2mutau, NsVbi,
445-
vbr_a, vbr, vbr_exp))
580+
vbr_a, vbr, vbr_exp),
581+
**method_kwargs)
446582
)
447583
vd = vec_fun(voc_est, *args)
448-
elif method.lower() == 'newton':
584+
elif method == 'newton':
449585
# make sure all args are numpy arrays if max size > 1
450586
# if voc_est is an array, then make a copy to use for initial guess, v0
451-
args, v0 = _prepare_newton_inputs((), args, voc_est)
587+
args, v0, method_kwargs = \
588+
_prepare_newton_inputs((), args, voc_est, method_kwargs)
452589
vd = newton(
453590
func=fmpp, x0=v0,
454-
fprime=lambda x, *a: bishop88(x, *a, gradients=True)[7], args=args
455-
)
591+
fprime=lambda x, *a: bishop88(x, *a, gradients=True)[7], args=args,
592+
**method_kwargs)
456593
else:
457594
raise NotImplementedError("Method '%s' isn't implemented" % method)
458-
return bishop88(vd, *args)
595+
596+
# When 'full_output' parameter is specified, returned 'vd' is a tuple with
597+
# many elements, where the root is the first one. So we use it to output
598+
# the bishop88 result and return
599+
# tuple(tuple with bishop88 solution, tuple with method results)
600+
if method_kwargs.get('full_output') is True:
601+
return (bishop88(vd[0], *args), vd)
602+
else:
603+
return bishop88(vd, *args)
459604

460605

461606
def _get_size_and_shape(args):
@@ -482,7 +627,7 @@ def _get_size_and_shape(args):
482627
return size, shape
483628

484629

485-
def _prepare_newton_inputs(i_or_v_tup, args, v0):
630+
def _prepare_newton_inputs(i_or_v_tup, args, v0, method_kwargs):
486631
# broadcast arguments for newton method
487632
# the first argument should be a tuple, eg: (i,), (v,) or ()
488633
size, shape = _get_size_and_shape(i_or_v_tup + args)
@@ -492,7 +637,12 @@ def _prepare_newton_inputs(i_or_v_tup, args, v0):
492637
# copy v0 to a new array and broadcast it to the shape of max size
493638
if shape is not None:
494639
v0 = np.broadcast_to(v0, shape).copy()
495-
return args, v0
640+
641+
# set abs tolerance and maxiter from method_kwargs if not provided
642+
# apply defaults, but giving priority to user-specified values
643+
method_kwargs = {**NEWTON_DEFAULT_PARAMS, **method_kwargs}
644+
645+
return args, v0, method_kwargs
496646

497647

498648
def _lambertw_v_from_i(current, photocurrent, saturation_current,

0 commit comments

Comments
 (0)