Skip to content

Commit 27a3a07

Browse files
reepoikandersolar
andauthored
pvsystem.singlediode with method='newton' can be passed pd.Series of length one. (#1822)
* bishop88 works with pandas.Series of length one * fix to many cases when arguments would be converted * converting all newton numeric args if not np.isscalar * remove used imports * add back newline * update whatsnew * _prepare_newton_inputs only changes shape of x0; refactoring to remove singlediode._get_size_and_shape * remove _shape_of_max_size, np.broadcast_shapes handles this * np.broadcast_shapes not available for Python 3.7 conda -min This reverts commit 7b39673. --------- Co-authored-by: Kevin Anderson <[email protected]>
1 parent d2fbfb2 commit 27a3a07

File tree

4 files changed

+80
-95
lines changed

4 files changed

+80
-95
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ Bug fixes
2727
~~~~~~~~~
2828
* :py:func:`~pvlib.iotools.get_psm3` no longer incorrectly returns clear-sky
2929
DHI instead of clear-sky GHI when requesting ``ghi_clear``. (:pull:`1819`)
30+
* :py:func:`pvlib.singlediode.bishop88` with `method='newton'` no longer
31+
crashes when passed `pandas.Series` of length one.
32+
(:issue:`1787`, :pull:`1822`)
3033
* :py:class:`pvlib.pvsystem.PVSystem` now correctly passes ``n_ar`` module
3134
parameter to :py:func:`pvlib.iam.physical` when this IAM model is specified
3235
or inferred. (:pull:`1832`)

pvlib/pvsystem.py

Lines changed: 14 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2652,28 +2652,19 @@ def v_from_i(current, photocurrent, saturation_current, resistance_series,
26522652
parameters of real solar cells using Lambert W-function", Solar
26532653
Energy Materials and Solar Cells, 81 (2004) 269-277.
26542654
'''
2655+
args = (current, photocurrent, saturation_current,
2656+
resistance_series, resistance_shunt, nNsVth)
26552657
if method.lower() == 'lambertw':
2656-
return _singlediode._lambertw_v_from_i(
2657-
current, photocurrent, saturation_current, resistance_series,
2658-
resistance_shunt, nNsVth
2659-
)
2658+
return _singlediode._lambertw_v_from_i(*args)
26602659
else:
26612660
# Calculate points on the IV curve using either 'newton' or 'brentq'
26622661
# methods. Voltages are determined by first solving the single diode
26632662
# equation for the diode voltage V_d then backing out voltage
2664-
args = (current, photocurrent, saturation_current,
2665-
resistance_series, resistance_shunt, nNsVth)
26662663
V = _singlediode.bishop88_v_from_i(*args, method=method.lower())
2667-
# find the right size and shape for returns
2668-
size, shape = _singlediode._get_size_and_shape(args)
2669-
if size <= 1:
2670-
if shape is not None:
2671-
V = np.tile(V, shape)
2672-
if np.isnan(V).any() and size <= 1:
2673-
V = np.repeat(V, size)
2674-
if shape is not None:
2675-
V = V.reshape(shape)
2676-
return V
2664+
if all(map(np.isscalar, args)):
2665+
return V
2666+
shape = _singlediode._shape_of_max_size(*args)
2667+
return np.broadcast_to(V, shape)
26772668

26782669

26792670
def i_from_v(voltage, photocurrent, saturation_current, resistance_series,
@@ -2743,28 +2734,19 @@ def i_from_v(voltage, photocurrent, saturation_current, resistance_series,
27432734
parameters of real solar cells using Lambert W-function", Solar
27442735
Energy Materials and Solar Cells, 81 (2004) 269-277.
27452736
'''
2737+
args = (voltage, photocurrent, saturation_current,
2738+
resistance_series, resistance_shunt, nNsVth)
27462739
if method.lower() == 'lambertw':
2747-
return _singlediode._lambertw_i_from_v(
2748-
voltage, photocurrent, saturation_current, resistance_series,
2749-
resistance_shunt, nNsVth
2750-
)
2740+
return _singlediode._lambertw_i_from_v(*args)
27512741
else:
27522742
# Calculate points on the IV curve using either 'newton' or 'brentq'
27532743
# methods. Voltages are determined by first solving the single diode
27542744
# equation for the diode voltage V_d then backing out voltage
2755-
args = (voltage, photocurrent, saturation_current, resistance_series,
2756-
resistance_shunt, nNsVth)
27572745
current = _singlediode.bishop88_i_from_v(*args, method=method.lower())
2758-
# find the right size and shape for returns
2759-
size, shape = _singlediode._get_size_and_shape(args)
2760-
if size <= 1:
2761-
if shape is not None:
2762-
current = np.tile(current, shape)
2763-
if np.isnan(current).any() and size <= 1:
2764-
current = np.repeat(current, size)
2765-
if shape is not None:
2766-
current = current.reshape(shape)
2767-
return current
2746+
if all(map(np.isscalar, args)):
2747+
return current
2748+
shape = _singlediode._shape_of_max_size(*args)
2749+
return np.broadcast_to(current, shape)
27682750

27692751

27702752
def scale_voltage_current_power(data, voltage=1, current=1):

pvlib/singlediode.py

Lines changed: 52 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,8 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current,
287287
... method_kwargs={'full_output': True})
288288
"""
289289
# collect args
290-
args = (photocurrent, saturation_current, resistance_series,
291-
resistance_shunt, nNsVth, d2mutau, NsVbi,
290+
args = (photocurrent, saturation_current,
291+
resistance_series, resistance_shunt, nNsVth, d2mutau, NsVbi,
292292
breakdown_factor, breakdown_voltage, breakdown_exp)
293293
method = method.lower()
294294

@@ -319,14 +319,11 @@ def vd_from_brent(voc, v, iph, isat, rs, rsh, gamma, d2mutau, NsVbi,
319319
vd_from_brent_vectorized = np.vectorize(vd_from_brent)
320320
vd = vd_from_brent_vectorized(voc_est, voltage, *args)
321321
elif method == 'newton':
322-
# make sure all args are numpy arrays if max size > 1
323-
# if voltage is an array, then make a copy to use for initial guess, v0
324-
args, v0, method_kwargs = \
325-
_prepare_newton_inputs((voltage,), args, voltage, method_kwargs)
326-
vd = newton(func=lambda x, *a: fv(x, voltage, *a), x0=v0,
322+
x0, (voltage, *args), method_kwargs = \
323+
_prepare_newton_inputs(voltage, (voltage, *args), method_kwargs)
324+
vd = newton(func=lambda x, *a: fv(x, voltage, *a), x0=x0,
327325
fprime=lambda x, *a: bishop88(x, *a, gradients=True)[4],
328-
args=args,
329-
**method_kwargs)
326+
args=args, **method_kwargs)
330327
else:
331328
raise NotImplementedError("Method '%s' isn't implemented" % method)
332329

@@ -422,9 +419,9 @@ def bishop88_v_from_i(current, photocurrent, saturation_current,
422419
... method_kwargs={'full_output': True})
423420
"""
424421
# collect args
425-
args = (photocurrent, saturation_current, resistance_series,
426-
resistance_shunt, nNsVth, d2mutau, NsVbi, breakdown_factor,
427-
breakdown_voltage, breakdown_exp)
422+
args = (photocurrent, saturation_current,
423+
resistance_series, resistance_shunt, nNsVth, d2mutau, NsVbi,
424+
breakdown_factor, breakdown_voltage, breakdown_exp)
428425
method = method.lower()
429426

430427
# method_kwargs create dict if not provided
@@ -454,14 +451,11 @@ def vd_from_brent(voc, i, iph, isat, rs, rsh, gamma, d2mutau, NsVbi,
454451
vd_from_brent_vectorized = np.vectorize(vd_from_brent)
455452
vd = vd_from_brent_vectorized(voc_est, current, *args)
456453
elif method == 'newton':
457-
# make sure all args are numpy arrays if max size > 1
458-
# if voc_est is an array, then make a copy to use for initial guess, v0
459-
args, v0, method_kwargs = \
460-
_prepare_newton_inputs((current,), args, voc_est, method_kwargs)
461-
vd = newton(func=lambda x, *a: fi(x, current, *a), x0=v0,
454+
x0, (current, *args), method_kwargs = \
455+
_prepare_newton_inputs(voc_est, (current, *args), method_kwargs)
456+
vd = newton(func=lambda x, *a: fi(x, current, *a), x0=x0,
462457
fprime=lambda x, *a: bishop88(x, *a, gradients=True)[3],
463-
args=args,
464-
**method_kwargs)
458+
args=args, **method_kwargs)
465459
else:
466460
raise NotImplementedError("Method '%s' isn't implemented" % method)
467461

@@ -555,9 +549,9 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series,
555549
... method='newton', method_kwargs={'full_output': True})
556550
"""
557551
# collect args
558-
args = (photocurrent, saturation_current, resistance_series,
559-
resistance_shunt, nNsVth, d2mutau, NsVbi, breakdown_factor,
560-
breakdown_voltage, breakdown_exp)
552+
args = (photocurrent, saturation_current,
553+
resistance_series, resistance_shunt, nNsVth, d2mutau, NsVbi,
554+
breakdown_factor, breakdown_voltage, breakdown_exp)
561555
method = method.lower()
562556

563557
# method_kwargs create dict if not provided
@@ -584,12 +578,11 @@ def fmpp(x, *a):
584578
elif method == 'newton':
585579
# make sure all args are numpy arrays if max size > 1
586580
# if voc_est is an array, then make a copy to use for initial guess, v0
587-
args, v0, method_kwargs = \
588-
_prepare_newton_inputs((), args, voc_est, method_kwargs)
589-
vd = newton(
590-
func=fmpp, x0=v0,
591-
fprime=lambda x, *a: bishop88(x, *a, gradients=True)[7], args=args,
592-
**method_kwargs)
581+
x0, args, method_kwargs = \
582+
_prepare_newton_inputs(voc_est, args, method_kwargs)
583+
vd = newton(func=fmpp, x0=x0,
584+
fprime=lambda x, *a: bishop88(x, *a, gradients=True)[7],
585+
args=args, **method_kwargs)
593586
else:
594587
raise NotImplementedError("Method '%s' isn't implemented" % method)
595588

@@ -603,46 +596,42 @@ def fmpp(x, *a):
603596
return bishop88(vd, *args)
604597

605598

606-
def _get_size_and_shape(args):
607-
# find the right size and shape for returns
608-
size, shape = 0, None # 0 or None both mean scalar
609-
for arg in args:
610-
try:
611-
this_shape = arg.shape # try to get shape
612-
except AttributeError:
613-
this_shape = None
614-
try:
615-
this_size = len(arg) # try to get the size
616-
except TypeError:
617-
this_size = 0
618-
else:
619-
this_size = arg.size # if it has shape then it also has size
620-
if shape is None:
621-
shape = this_shape # set the shape if None
622-
# update size and shape
623-
if this_size > size:
624-
size = this_size
625-
if this_shape is not None:
626-
shape = this_shape
627-
return size, shape
628-
629-
630-
def _prepare_newton_inputs(i_or_v_tup, args, v0, method_kwargs):
631-
# broadcast arguments for newton method
632-
# the first argument should be a tuple, eg: (i,), (v,) or ()
633-
size, shape = _get_size_and_shape(i_or_v_tup + args)
634-
if size > 1:
635-
args = [np.asarray(arg) for arg in args]
636-
# newton uses initial guess for the output shape
637-
# copy v0 to a new array and broadcast it to the shape of max size
638-
if shape is not None:
639-
v0 = np.broadcast_to(v0, shape).copy()
599+
def _shape_of_max_size(*args):
600+
return max(((np.size(a), np.shape(a)) for a in args),
601+
key=lambda t: t[0])[1]
602+
603+
604+
def _prepare_newton_inputs(x0, args, method_kwargs):
605+
"""
606+
Make inputs compatible with Scipy's newton by:
607+
- converting all arugments (`x0` and `args`) into numpy.ndarrays if any
608+
argument is not a scalar.
609+
- broadcasting the initial guess `x0` to the shape of the argument with
610+
the greatest size.
611+
612+
Parameters
613+
----------
614+
x0: numeric
615+
Initial guess for newton.
616+
args: Iterable(numeric)
617+
Iterable of additional arguments to use in SciPy's newton.
618+
method_kwargs: dict
619+
Options to pass to newton.
620+
621+
Returns
622+
-------
623+
tuple
624+
The updated initial guess, arguments, and options for newton.
625+
"""
626+
if not (np.isscalar(x0) and all(map(np.isscalar, args))):
627+
args = tuple(map(np.asarray, args))
628+
x0 = np.broadcast_to(x0, _shape_of_max_size(x0, *args))
640629

641630
# set abs tolerance and maxiter from method_kwargs if not provided
642631
# apply defaults, but giving priority to user-specified values
643632
method_kwargs = {**NEWTON_DEFAULT_PARAMS, **method_kwargs}
644633

645-
return args, v0, method_kwargs
634+
return x0, args, method_kwargs
646635

647636

648637
def _lambertw_v_from_i(current, photocurrent, saturation_current,

pvlib/tests/test_singlediode.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,3 +557,14 @@ def test_bishop88_full_output_kwarg(method, bishop88_arguments):
557557
assert isinstance(ret_val[1], tuple) # second is output from optimizer
558558
# any root finder returns at least 2 elements with full_output=True
559559
assert len(ret_val[1]) >= 2
560+
561+
562+
@pytest.mark.parametrize('method', ['newton', 'brentq'])
563+
def test_bishop88_pdSeries_len_one(method, bishop88_arguments):
564+
for k, v in bishop88_arguments.items():
565+
bishop88_arguments[k] = pd.Series([v])
566+
567+
# should not raise error
568+
bishop88_i_from_v(pd.Series([0]), **bishop88_arguments, method=method)
569+
bishop88_v_from_i(pd.Series([0]), **bishop88_arguments, method=method)
570+
bishop88_mpp(**bishop88_arguments, method=method)

0 commit comments

Comments
 (0)