2
2
Low-level functions for solving the single diode equation.
3
3
"""
4
4
5
- from functools import partial
6
5
import numpy as np
7
6
from pvlib .tools import _golden_sect_DataFrame
8
7
9
8
from scipy .optimize import brentq , newton
10
9
from scipy .special import lambertw
11
10
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
+ }
14
16
15
17
# intrinsic voltage per cell junction for a:Si, CdTe, Mertens et al.
16
18
VOLTAGE_BUILTIN = 0.9 # [V]
@@ -206,7 +208,7 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current,
206
208
resistance_series , resistance_shunt , nNsVth ,
207
209
d2mutau = 0 , NsVbi = np .Inf , breakdown_factor = 0. ,
208
210
breakdown_voltage = - 5.5 , breakdown_exp = 3.28 ,
209
- method = 'newton' ):
211
+ method = 'newton' , method_kwargs = None ):
210
212
"""
211
213
Find current given any voltage.
212
214
@@ -247,22 +249,59 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current,
247
249
method : str, default 'newton'
248
250
Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'``
249
251
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.
250
258
251
259
Returns
252
260
-------
253
261
current : numeric
254
262
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})
255
288
"""
256
289
# collect args
257
290
args = (photocurrent , saturation_current , resistance_series ,
258
291
resistance_shunt , nNsVth , d2mutau , NsVbi ,
259
292
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 = {}
260
299
261
300
def fv (x , v , * a ):
262
301
# calculate voltage residual given diode voltage "x"
263
302
return bishop88 (x , * a )[1 ] - v
264
303
265
- if method . lower () == 'brentq' :
304
+ if method == 'brentq' :
266
305
# first bound the search using voc
267
306
voc_est = estimate_voc (photocurrent , saturation_current , nNsVth )
268
307
@@ -274,27 +313,37 @@ def vd_from_brent(voc, v, iph, isat, rs, rsh, gamma, d2mutau, NsVbi,
274
313
return brentq (fv , 0.0 , voc ,
275
314
args = (v , iph , isat , rs , rsh , gamma , d2mutau , NsVbi ,
276
315
breakdown_factor , breakdown_voltage ,
277
- breakdown_exp ))
316
+ breakdown_exp ),
317
+ ** method_kwargs )
278
318
279
319
vd_from_brent_vectorized = np .vectorize (vd_from_brent )
280
320
vd = vd_from_brent_vectorized (voc_est , voltage , * args )
281
- elif method . lower () == 'newton' :
321
+ elif method == 'newton' :
282
322
# make sure all args are numpy arrays if max size > 1
283
323
# 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 )
285
326
vd = newton (func = lambda x , * a : fv (x , voltage , * a ), x0 = v0 ,
286
327
fprime = lambda x , * a : bishop88 (x , * a , gradients = True )[4 ],
287
- args = args )
328
+ args = args ,
329
+ ** method_kwargs )
288
330
else :
289
331
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 ]
291
340
292
341
293
342
def bishop88_v_from_i (current , photocurrent , saturation_current ,
294
343
resistance_series , resistance_shunt , nNsVth ,
295
344
d2mutau = 0 , NsVbi = np .Inf , breakdown_factor = 0. ,
296
345
breakdown_voltage = - 5.5 , breakdown_exp = 3.28 ,
297
- method = 'newton' ):
346
+ method = 'newton' , method_kwargs = None ):
298
347
"""
299
348
Find voltage given any current.
300
349
@@ -335,24 +384,62 @@ def bishop88_v_from_i(current, photocurrent, saturation_current,
335
384
method : str, default 'newton'
336
385
Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'``
337
386
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.
338
393
339
394
Returns
340
395
-------
341
396
voltage : numeric
342
397
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})
343
423
"""
344
424
# collect args
345
425
args = (photocurrent , saturation_current , resistance_series ,
346
426
resistance_shunt , nNsVth , d2mutau , NsVbi , breakdown_factor ,
347
427
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
+
348
435
# first bound the search using voc
349
436
voc_est = estimate_voc (photocurrent , saturation_current , nNsVth )
350
437
351
438
def fi (x , i , * a ):
352
439
# calculate current residual given diode voltage "x"
353
440
return bishop88 (x , * a )[0 ] - i
354
441
355
- if method . lower () == 'brentq' :
442
+ if method == 'brentq' :
356
443
# brentq only works with scalar inputs, so we need a set up function
357
444
# and np.vectorize to repeatedly call the optimizer with the right
358
445
# arguments for possible array input
@@ -361,26 +448,36 @@ def vd_from_brent(voc, i, iph, isat, rs, rsh, gamma, d2mutau, NsVbi,
361
448
return brentq (fi , 0.0 , voc ,
362
449
args = (i , iph , isat , rs , rsh , gamma , d2mutau , NsVbi ,
363
450
breakdown_factor , breakdown_voltage ,
364
- breakdown_exp ))
451
+ breakdown_exp ),
452
+ ** method_kwargs )
365
453
366
454
vd_from_brent_vectorized = np .vectorize (vd_from_brent )
367
455
vd = vd_from_brent_vectorized (voc_est , current , * args )
368
- elif method . lower () == 'newton' :
456
+ elif method == 'newton' :
369
457
# make sure all args are numpy arrays if max size > 1
370
458
# 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 )
372
461
vd = newton (func = lambda x , * a : fi (x , current , * a ), x0 = v0 ,
373
462
fprime = lambda x , * a : bishop88 (x , * a , gradients = True )[3 ],
374
- args = args )
463
+ args = args ,
464
+ ** method_kwargs )
375
465
else :
376
466
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 ]
378
475
379
476
380
477
def bishop88_mpp (photocurrent , saturation_current , resistance_series ,
381
478
resistance_shunt , nNsVth , d2mutau = 0 , NsVbi = np .Inf ,
382
479
breakdown_factor = 0. , breakdown_voltage = - 5.5 ,
383
- breakdown_exp = 3.28 , method = 'newton' ):
480
+ breakdown_exp = 3.28 , method = 'newton' , method_kwargs = None ):
384
481
"""
385
482
Find max power point.
386
483
@@ -419,43 +516,91 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series,
419
516
method : str, default 'newton'
420
517
Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'``
421
518
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.
422
525
423
526
Returns
424
527
-------
425
528
tuple
426
529
max power current ``i_mp`` [A], max power voltage ``v_mp`` [V], and
427
530
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})
428
556
"""
429
557
# collect args
430
558
args = (photocurrent , saturation_current , resistance_series ,
431
559
resistance_shunt , nNsVth , d2mutau , NsVbi , breakdown_factor ,
432
560
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
+
433
568
# first bound the search using voc
434
569
voc_est = estimate_voc (photocurrent , saturation_current , nNsVth )
435
570
436
571
def fmpp (x , * a ):
437
572
return bishop88 (x , * a , gradients = True )[6 ]
438
573
439
- if method . lower () == 'brentq' :
574
+ if method == 'brentq' :
440
575
# break out arguments for numpy.vectorize to handle broadcasting
441
576
vec_fun = np .vectorize (
442
577
lambda voc , iph , isat , rs , rsh , gamma , d2mutau , NsVbi , vbr_a , vbr ,
443
578
vbr_exp : brentq (fmpp , 0.0 , voc ,
444
579
args = (iph , isat , rs , rsh , gamma , d2mutau , NsVbi ,
445
- vbr_a , vbr , vbr_exp ))
580
+ vbr_a , vbr , vbr_exp ),
581
+ ** method_kwargs )
446
582
)
447
583
vd = vec_fun (voc_est , * args )
448
- elif method . lower () == 'newton' :
584
+ elif method == 'newton' :
449
585
# make sure all args are numpy arrays if max size > 1
450
586
# 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 )
452
589
vd = newton (
453
590
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 )
456
593
else :
457
594
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 )
459
604
460
605
461
606
def _get_size_and_shape (args ):
@@ -482,7 +627,7 @@ def _get_size_and_shape(args):
482
627
return size , shape
483
628
484
629
485
- def _prepare_newton_inputs (i_or_v_tup , args , v0 ):
630
+ def _prepare_newton_inputs (i_or_v_tup , args , v0 , method_kwargs ):
486
631
# broadcast arguments for newton method
487
632
# the first argument should be a tuple, eg: (i,), (v,) or ()
488
633
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):
492
637
# copy v0 to a new array and broadcast it to the shape of max size
493
638
if shape is not None :
494
639
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
496
646
497
647
498
648
def _lambertw_v_from_i (current , photocurrent , saturation_current ,
0 commit comments