Skip to content

Commit ea25214

Browse files
cwhanseechedey-lskandersolar
authored
Add lambertw_pvlib (#2723)
* add lambertw_pvlib * whatsnew * formatting * Apply suggestions from code review Co-authored-by: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> * from review * move to ivtools, make private * Apply suggestions from code review Co-authored-by: Kevin Anderson <kevin.anderso@gmail.com> * accept float, restrict to x>=0, simplify test * formatting * small < 100 * Apply suggestions from code review Co-authored-by: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> * fix test loop --------- Co-authored-by: Echedey Luis <80125792+echedey-ls@users.noreply.github.com> Co-authored-by: Kevin Anderson <kevin.anderso@gmail.com>
1 parent 92bb1e5 commit ea25214

File tree

3 files changed

+83
-1
lines changed

3 files changed

+83
-1
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ Bug fixes
3030

3131
Enhancements
3232
~~~~~~~~~~~~
33-
* Use ``k`` and ``cap_adjustment`` from :py:func:`pvlib.pvsystem.Array.module_parameters` in :py:func:`pvlib.pvsystem.PVSystem.pvwatts_dc`
33+
* Use ``k`` and ``cap_adjustment`` from :py:func:`pvlib.pvsystem.Array.module_parameters`
34+
in :py:func:`pvlib.pvsystem.PVSystem.pvwatts_dc`
3435
(:issue:`2714`, :pull:`2715`)
3536
* Include `ross` and `faiman_rad` in the allowed models within
3637
:py:meth:`pvlib.pvsystem.PVSystem.get_cell_temperature` (:issue:`2625`, :pull:`2631`)

pvlib/ivtools/utils.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,3 +544,63 @@ def astm_e1036(v, i, imax_limits=(0.75, 1.15), vmax_limits=(0.75, 1.15),
544544
result['mp_fit'] = mp_fit
545545

546546
return result
547+
548+
549+
def _log_lambertw(logx):
550+
r'''Computes W(x) starting from log(x).
551+
552+
Parameters
553+
----------
554+
logx : numeric
555+
556+
Returns
557+
-------
558+
numeric
559+
Lambert's W(x)
560+
561+
'''
562+
# handles overflow cases, but results in nan for x <= 1
563+
w = logx - np.log(logx) # initial guess, w = log(x) - log(log(x))
564+
565+
for _ in range(0, 3):
566+
# Newton's. Halley's is not substantially faster or more accurate
567+
# because f''(w) = -1 / (w**2) is small for large w
568+
w = w * (1. - np.log(w) + logx) / (1. + w)
569+
return w
570+
571+
572+
def _lambertw_pvlib(x):
573+
r'''Lambert's W function principal branch, :math:`W_0(x)`, for
574+
:math:`x>=0`.
575+
576+
Parameters
577+
----------
578+
x : float or np.array
579+
Must be real numbers.
580+
581+
Returns
582+
-------
583+
float or np.array
584+
585+
'''
586+
localx = np.asarray(x, float)
587+
w = np.full_like(localx, np.nan)
588+
small = localx <= 100
589+
# for large x, solve 0 = f(w) = w + log(w) - log(x) using Newton's
590+
# w will contain nan for these numbers due to log(w) = log(log(x))
591+
w[~small] = _log_lambertw(np.log(localx[~small]))
592+
593+
# for small x, solve 0 = g(w) = w * exp(w) - x using Halley's method
594+
if np.any(small):
595+
z = localx[small]
596+
temp = np.log1p(localx[small])
597+
g = temp - np.log1p(temp)
598+
for _ in range(0, 3):
599+
expg = np.exp(g)
600+
g_expg_z = g*expg - z
601+
g_p1 = g + 1
602+
g = g - g_expg_z * g_p1 / \
603+
(expg * g_p1**2 - 0.5*(g + 2)*g_expg_z)
604+
w[small] = g
605+
606+
return w[0] if w.shape == 1 else w

tests/ivtools/test_utils.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44
from pvlib.ivtools.utils import _numdiff, rectify_iv_curve, astm_e1036
55
from pvlib.ivtools.utils import _schumaker_qspline
6+
from pvlib.ivtools.utils import _lambertw_pvlib
67

78
from tests.conftest import TESTS_DATA_DIR
89

@@ -171,3 +172,23 @@ def test_astm_e1036_fit_points(v_array, i_array):
171172
'ff': 0.7520255886236707}
172173
result.pop('mp_fit')
173174
assert result == pytest.approx(expected)
175+
176+
177+
def test_lambertw_pvlib():
178+
test_x = np.array([0., 1.e-10, 1., 10., 100., 1.e+10, 1.e+100, 1.e+300])
179+
# known solution from scipy.special.lambertw
180+
# scipy 1.7.1, python 3.13.1, numpy 2.3.5
181+
expected = np.array([
182+
0.0000000000000000e+00, 9.9999999989999997e-11, 5.6714329040978384e-01,
183+
1.7455280027406994e+00, 3.3856301402900502e+00, 2.0028685413304952e+01,
184+
2.2484310644511851e+02, 6.8424720862976085e+02])
185+
result = _lambertw_pvlib(test_x)
186+
assert np.allclose(result, expected, rtol=1e-14)
187+
# with float input
188+
for x, known in zip(test_x[[1, 5]], expected[[1, 5]]):
189+
result = _lambertw_pvlib(x)
190+
assert np.isclose(result, known)
191+
# with 1d array
192+
for x, known in zip(test_x[[1, 5]], expected[[1, 5]]):
193+
result = _lambertw_pvlib(np.array([x]))
194+
assert np.isclose(result, known)

0 commit comments

Comments
 (0)