-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Allow users to interact with root finders parameters #1764
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 25 commits
08bcf80
0fcc51e
c7917a3
fe43074
2276e91
09c8ed0
8565131
6a94af4
0f5c17d
3423314
3578397
e36fecf
f1165bc
8b070be
9e5b634
4ec2c3c
3445d27
6208a89
c04eade
9dc1d34
b043f67
0b89f56
c626a49
542dd30
440c9fe
8be402d
42c547b
13dbb57
6d83c81
114b003
06749ee
9406e2a
3e4f6a1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,15 +2,17 @@ | |
Low-level functions for solving the single diode equation. | ||
""" | ||
|
||
from functools import partial | ||
import numpy as np | ||
from pvlib.tools import _golden_sect_DataFrame | ||
|
||
from scipy.optimize import brentq, newton | ||
from scipy.special import lambertw | ||
|
||
# set keyword arguments for all uses of newton in this module | ||
newton = partial(newton, tol=1e-6, maxiter=100, fprime2=None) | ||
# newton method default parameters for this module | ||
NEWTON_DEFAULT_PARAMS = { | ||
'tol': 1e-6, | ||
'maxiter': 100 | ||
} | ||
|
||
# intrinsic voltage per cell junction for a:Si, CdTe, Mertens et al. | ||
VOLTAGE_BUILTIN = 0.9 # [V] | ||
|
@@ -206,7 +208,7 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, | |
resistance_series, resistance_shunt, nNsVth, | ||
d2mutau=0, NsVbi=np.Inf, breakdown_factor=0., | ||
breakdown_voltage=-5.5, breakdown_exp=3.28, | ||
method='newton'): | ||
method='newton', **method_kwargs): | ||
""" | ||
Find current given any voltage. | ||
|
||
|
@@ -247,22 +249,29 @@ def bishop88_i_from_v(voltage, photocurrent, saturation_current, | |
method : str, default 'newton' | ||
Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'`` | ||
if ``breakdown_factor`` is not 0. | ||
method_kwargs : dict | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right now this docstring entry is inconsistent with the actual function behavior. The difference is whether the function is used as: I think I somewhat prefer the first one. @adriesse? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. It looks a bit weird how
However, in PVLIB (taken from pvlib.location.Location.get_clearsky) it is usually
This stackoverflow post does give some more insight that could be useful. I like the function signature way of passing parameters, given they are well documented, and document kwargs with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I didn't justify my opinion. Right now, Something that puzzles me a bit is using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought there might be others with opinions about this... There's a similar situation in scipy There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Okay, I've changed the behaviour although that still feels like the pattern for a user-provided function. tests (with few unrelated exceptions I believe), doctest examples are passing and all seems good to me. Do you mind having a look specially at the formatting of docs? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess choosing the method name is an indirect way of giving a user-provided function. :) |
||
Passed to root finder method. See | ||
:py:func:`scipy:scipy.optimize.brentq` and | ||
:py:func:`scipy:scipy.optimize.newton` parameters. | ||
|
||
Returns | ||
------- | ||
current : numeric | ||
current (I) at the specified voltage (V). [A] | ||
optimizer_return : optional, present if ``full_output = True`` | ||
see root finder documentation for selected method | ||
""" | ||
# collect args | ||
args = (photocurrent, saturation_current, resistance_series, | ||
resistance_shunt, nNsVth, d2mutau, NsVbi, | ||
breakdown_factor, breakdown_voltage, breakdown_exp) | ||
method = method.lower() | ||
|
||
def fv(x, v, *a): | ||
# calculate voltage residual given diode voltage "x" | ||
return bishop88(x, *a)[1] - v | ||
|
||
if method.lower() == 'brentq': | ||
if method == 'brentq': | ||
# first bound the search using voc | ||
voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) | ||
|
||
|
@@ -274,27 +283,37 @@ def vd_from_brent(voc, v, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, | |
return brentq(fv, 0.0, voc, | ||
args=(v, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, | ||
breakdown_factor, breakdown_voltage, | ||
breakdown_exp)) | ||
breakdown_exp), | ||
**method_kwargs) | ||
|
||
vd_from_brent_vectorized = np.vectorize(vd_from_brent) | ||
vd = vd_from_brent_vectorized(voc_est, voltage, *args) | ||
elif method.lower() == 'newton': | ||
elif method == 'newton': | ||
# make sure all args are numpy arrays if max size > 1 | ||
# if voltage is an array, then make a copy to use for initial guess, v0 | ||
args, v0 = _prepare_newton_inputs((voltage,), args, voltage) | ||
args, v0, method_kwargs = \ | ||
_prepare_newton_inputs((voltage,), args, voltage, method_kwargs) | ||
vd = newton(func=lambda x, *a: fv(x, voltage, *a), x0=v0, | ||
fprime=lambda x, *a: bishop88(x, *a, gradients=True)[4], | ||
args=args) | ||
args=args, | ||
**method_kwargs) | ||
else: | ||
raise NotImplementedError("Method '%s' isn't implemented" % method) | ||
return bishop88(vd, *args)[0] | ||
|
||
# When 'full_output' parameter is specified, returned 'vd' is a tuple with | ||
# many elements, where the root is the first one. So we use it to output | ||
# the bishop88 result and return tuple(scalar, tuple with method results) | ||
if method_kwargs.get('full_output') is True: | ||
return (bishop88(vd[0], *args)[0], vd) | ||
else: | ||
return bishop88(vd, *args)[0] | ||
|
||
|
||
def bishop88_v_from_i(current, photocurrent, saturation_current, | ||
resistance_series, resistance_shunt, nNsVth, | ||
d2mutau=0, NsVbi=np.Inf, breakdown_factor=0., | ||
breakdown_voltage=-5.5, breakdown_exp=3.28, | ||
method='newton'): | ||
method='newton', **method_kwargs): | ||
""" | ||
Find voltage given any current. | ||
|
||
|
@@ -335,24 +354,31 @@ def bishop88_v_from_i(current, photocurrent, saturation_current, | |
method : str, default 'newton' | ||
Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'`` | ||
if ``breakdown_factor`` is not 0. | ||
method_kwargs : dict | ||
Passed to root finder method. See | ||
:py:func:`scipy:scipy.optimize.brentq` and | ||
:py:func:`scipy:scipy.optimize.newton` parameters. | ||
|
||
Returns | ||
------- | ||
voltage : numeric | ||
voltage (V) at the specified current (I) in volts [V] | ||
optimizer_return : optional, present if ``full_output = True`` | ||
see root finder documentation for selected method | ||
""" | ||
# collect args | ||
args = (photocurrent, saturation_current, resistance_series, | ||
resistance_shunt, nNsVth, d2mutau, NsVbi, breakdown_factor, | ||
breakdown_voltage, breakdown_exp) | ||
method = method.lower() | ||
# first bound the search using voc | ||
voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) | ||
|
||
def fi(x, i, *a): | ||
# calculate current residual given diode voltage "x" | ||
return bishop88(x, *a)[0] - i | ||
|
||
if method.lower() == 'brentq': | ||
if method == 'brentq': | ||
# brentq only works with scalar inputs, so we need a set up function | ||
# and np.vectorize to repeatedly call the optimizer with the right | ||
# arguments for possible array input | ||
|
@@ -361,26 +387,36 @@ def vd_from_brent(voc, i, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, | |
return brentq(fi, 0.0, voc, | ||
args=(i, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, | ||
breakdown_factor, breakdown_voltage, | ||
breakdown_exp)) | ||
breakdown_exp), | ||
**method_kwargs) | ||
|
||
vd_from_brent_vectorized = np.vectorize(vd_from_brent) | ||
vd = vd_from_brent_vectorized(voc_est, current, *args) | ||
elif method.lower() == 'newton': | ||
elif method == 'newton': | ||
# make sure all args are numpy arrays if max size > 1 | ||
# if voc_est is an array, then make a copy to use for initial guess, v0 | ||
args, v0 = _prepare_newton_inputs((current,), args, voc_est) | ||
args, v0, method_kwargs = \ | ||
_prepare_newton_inputs((current,), args, voc_est, method_kwargs) | ||
vd = newton(func=lambda x, *a: fi(x, current, *a), x0=v0, | ||
fprime=lambda x, *a: bishop88(x, *a, gradients=True)[3], | ||
args=args) | ||
args=args, | ||
**method_kwargs) | ||
else: | ||
raise NotImplementedError("Method '%s' isn't implemented" % method) | ||
return bishop88(vd, *args)[1] | ||
|
||
# When 'full_output' parameter is specified, returned 'vd' is a tuple with | ||
# many elements, where the root is the first one. So we use it to output | ||
# the bishop88 result and return tuple(scalar, tuple with method results) | ||
if method_kwargs.get('full_output') is True: | ||
return (bishop88(vd[0], *args)[1], vd) | ||
else: | ||
return bishop88(vd, *args)[1] | ||
|
||
|
||
def bishop88_mpp(photocurrent, saturation_current, resistance_series, | ||
resistance_shunt, nNsVth, d2mutau=0, NsVbi=np.Inf, | ||
breakdown_factor=0., breakdown_voltage=-5.5, | ||
breakdown_exp=3.28, method='newton'): | ||
breakdown_exp=3.28, method='newton', **method_kwargs): | ||
""" | ||
Find max power point. | ||
|
||
|
@@ -419,43 +455,60 @@ def bishop88_mpp(photocurrent, saturation_current, resistance_series, | |
method : str, default 'newton' | ||
Either ``'newton'`` or ``'brentq'``. ''method'' must be ``'newton'`` | ||
if ``breakdown_factor`` is not 0. | ||
method_kwargs : dict | ||
Passed to root finder method. See | ||
:py:func:`scipy:scipy.optimize.brentq` and | ||
:py:func:`scipy:scipy.optimize.newton` parameters. | ||
|
||
Returns | ||
------- | ||
tuple | ||
max power current ``i_mp`` [A], max power voltage ``v_mp`` [V], and | ||
max power ``p_mp`` [W] | ||
optimizer_return : optional, present if ``full_output = True`` | ||
see root finder documentation for selected method | ||
""" | ||
# collect args | ||
args = (photocurrent, saturation_current, resistance_series, | ||
resistance_shunt, nNsVth, d2mutau, NsVbi, breakdown_factor, | ||
breakdown_voltage, breakdown_exp) | ||
method = method.lower() | ||
# first bound the search using voc | ||
voc_est = estimate_voc(photocurrent, saturation_current, nNsVth) | ||
|
||
def fmpp(x, *a): | ||
return bishop88(x, *a, gradients=True)[6] | ||
|
||
if method.lower() == 'brentq': | ||
if method == 'brentq': | ||
# break out arguments for numpy.vectorize to handle broadcasting | ||
vec_fun = np.vectorize( | ||
lambda voc, iph, isat, rs, rsh, gamma, d2mutau, NsVbi, vbr_a, vbr, | ||
vbr_exp: brentq(fmpp, 0.0, voc, | ||
args=(iph, isat, rs, rsh, gamma, d2mutau, NsVbi, | ||
vbr_a, vbr, vbr_exp)) | ||
vbr_a, vbr, vbr_exp), | ||
**method_kwargs) | ||
) | ||
vd = vec_fun(voc_est, *args) | ||
elif method.lower() == 'newton': | ||
elif method == 'newton': | ||
# make sure all args are numpy arrays if max size > 1 | ||
# if voc_est is an array, then make a copy to use for initial guess, v0 | ||
args, v0 = _prepare_newton_inputs((), args, voc_est) | ||
args, v0, method_kwargs = \ | ||
_prepare_newton_inputs((), args, voc_est, method_kwargs) | ||
vd = newton( | ||
func=fmpp, x0=v0, | ||
fprime=lambda x, *a: bishop88(x, *a, gradients=True)[7], args=args | ||
) | ||
fprime=lambda x, *a: bishop88(x, *a, gradients=True)[7], args=args, | ||
**method_kwargs) | ||
else: | ||
raise NotImplementedError("Method '%s' isn't implemented" % method) | ||
return bishop88(vd, *args) | ||
|
||
# When 'full_output' parameter is specified, returned 'vd' is a tuple with | ||
# many elements, where the root is the first one. So we use it to output | ||
# the bishop88 result and return | ||
# tuple(tuple with bishop88 solution, tuple with method results) | ||
if method_kwargs.get('full_output') is True: | ||
return (bishop88(vd[0], *args), vd) | ||
else: | ||
return bishop88(vd, *args) | ||
|
||
|
||
def _get_size_and_shape(args): | ||
|
@@ -482,7 +535,7 @@ def _get_size_and_shape(args): | |
return size, shape | ||
|
||
|
||
def _prepare_newton_inputs(i_or_v_tup, args, v0): | ||
def _prepare_newton_inputs(i_or_v_tup, args, v0, method_kwargs): | ||
# broadcast arguments for newton method | ||
# the first argument should be a tuple, eg: (i,), (v,) or () | ||
size, shape = _get_size_and_shape(i_or_v_tup + args) | ||
|
@@ -492,7 +545,14 @@ def _prepare_newton_inputs(i_or_v_tup, args, v0): | |
# copy v0 to a new array and broadcast it to the shape of max size | ||
if shape is not None: | ||
v0 = np.broadcast_to(v0, shape).copy() | ||
return args, v0 | ||
|
||
# set abs tolerance and maxiter from method_kwargs if not provided | ||
method_kwargs['tol'] = \ | ||
method_kwargs.pop('tol', NEWTON_DEFAULT_PARAMS['tol']) | ||
method_kwargs['maxiter'] = \ | ||
method_kwargs.pop('maxiter', NEWTON_DEFAULT_PARAMS['maxiter']) | ||
echedey-ls marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
return args, v0, method_kwargs | ||
|
||
|
||
def _lambertw_v_from_i(current, photocurrent, saturation_current, | ||
|
Uh oh!
There was an error while loading. Please reload this page.