22Low-level functions for solving the single diode equation.
33"""
44
5- from functools import partial
65import numpy as np
76from pvlib .tools import _golden_sect_DataFrame
87
98from scipy .optimize import brentq , newton
109from 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.
1618VOLTAGE_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
293342def 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
380477def 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
461606def _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
498648def _lambertw_v_from_i (current , photocurrent , saturation_current ,
0 commit comments