From 3534864bb3e3e5973caf8ee836c40b19f80e46f2 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Thu, 12 Mar 2026 12:43:57 -0700 Subject: [PATCH 01/10] start at mismatch.py --- pvlib/mismatch.py | 97 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 pvlib/mismatch.py diff --git a/pvlib/mismatch.py b/pvlib/mismatch.py new file mode 100644 index 0000000000..1083ceacd0 --- /dev/null +++ b/pvlib/mismatch.py @@ -0,0 +1,97 @@ +""" +Contains functions for solving for DC power in arrays with mismatched conditions. + +""" +import numpy as np +import singlediode as _singlediode + + +def _iv_series_lambertw(photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth, neg_v_limit=None, + delta_i=0.001): + r'''Solve the IV curve for series-connected devices using a single diode + equivalent circuit model. + + Uses a simplified model for reverse bias behavior, where current is + unbounded at a constant reverse bias voltage ``neg_v_limit``. + + Input parameters photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth may be arrays. If arrays, all must be + broadcastable to a common shape. The first dimension of each array + is time. The 2nd dimension is devices in series. + + Parameters + ---------- + photocurrent : numeric + photocurrent (A). + saturation_current : numeric + saturation current (A). + resistance_series : numeric + series resistance (ohm). + resistance_shunt : numeric + shunt resistance (ohm). + nNsVth : numeric + product of diode factor n, number of series cells Ns, and + thermal voltage (Vth), (V). + neg_v_limit : float, optional + Limit on reverse bias voltage, from cell breakdown voltage or reverse + bias diode activation voltage (V). Should be negative. For example, + if neg_v_limit=-5, then at V=-5 current is unbounded in the positive + direction. + delta_i : float, optional + Width of interval used to discretize current (A). + + Returns + ------- + None. + + ''' + # solve for Isc + isc = _singlediode._lambertw_i_from_v( + 0., photocurrent, saturation_current, resistance_series, + resistance_shunt, nNsVth) + + # discretize current from max(Isc) down to 0. + currents = np.arange(isc.max(), 0., step=-delta_i) + + # shape all the arrays + # target shape is ntimes x ndevices x ncurrents + # use a dict so we can add axes using a loop + params = {'photocurrent': photocurrent, + 'saturation_current': saturation_current, + 'resistance_series': resistance_series, + 'resistance_shunt': resistance_shunt, + 'nNsVth': nNsVth} + + for p in params: + if not isinstance(params[p], np.ndarray): + pass # float, int + if len(params[p].shape) == 1: + params[p] = params[p][:, np.newaxis, np.newaxis] + elif len(params[p].shape) == 2: + params[p] = params[p][:, :, np.newaxis] + else: + pass # already 3d + + il, io, rs, rsh, a = (params[p] for p in params) + + currents = currents[np.newaxis, np.newaxis, :] + + il, io, rs, rsh, a, currents = np.broadcast_arrays( + il, io, rs, rsh, a, currents) + + # solve voltages at each current for each IV curve + voltages = _singlediode._lambertw_v_from_i( + currents, il, io, rs, rsh, a) + + # apply negative voltage limit + if neg_v_limit is not None: + voltages[voltages < neg_v_limit] = neg_v_limit + + # add voltage at common current to get series voltage + voltage_sum = voltages.sum(axis=1) + + # drop currents dimension for devices + currents = currents[:, 0, :] + + return voltage_sum, currents From 1c04fffe1cc4093a4c5a9efaf06b1d7e72ce6080 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Sun, 22 Mar 2026 16:51:56 -0700 Subject: [PATCH 02/10] work --- pvlib/mismatch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvlib/mismatch.py b/pvlib/mismatch.py index 1083ceacd0..c8f33c7369 100644 --- a/pvlib/mismatch.py +++ b/pvlib/mismatch.py @@ -65,13 +65,13 @@ def _iv_series_lambertw(photocurrent, saturation_current, resistance_series, for p in params: if not isinstance(params[p], np.ndarray): - pass # float, int + continue # float, int if len(params[p].shape) == 1: params[p] = params[p][:, np.newaxis, np.newaxis] elif len(params[p].shape) == 2: params[p] = params[p][:, :, np.newaxis] else: - pass # already 3d + continue # already 3d il, io, rs, rsh, a = (params[p] for p in params) From 824c74660eb6d155d8d02bb1c55b842c9c2bde22 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Mon, 17 Aug 2026 14:44:26 -0600 Subject: [PATCH 03/10] update series calculation --- pvlib/mismatch.py | 230 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 171 insertions(+), 59 deletions(-) diff --git a/pvlib/mismatch.py b/pvlib/mismatch.py index c8f33c7369..4d20141e5d 100644 --- a/pvlib/mismatch.py +++ b/pvlib/mismatch.py @@ -1,97 +1,209 @@ """ -Contains functions for solving for DC power in arrays with mismatched conditions. +Contains functions for solving for DC power in arrays with mismatched +conditions. """ import numpy as np +from scipy.optimize.elementwise import find_root import singlediode as _singlediode +def _iv_series_lambert_v_from_i(I, il, io, rs, rsh, a, neg_v_limit, + ndevices=None, idx=None): + # solve voltages at each current for each IV curve + if ndevices is not None: + # broadcast I to ndevices to apply same current each device + I = np.broadcast_to(I[np.newaxis, :], (ndevices, len(I))) + # slice each parameter on its ntimes dimension with idx + if idx is not None: + il, io, rs, rsh, a = (il[:, idx], io[:, idx], rs[:, idx], rsh[:, idx], + a[:, idx]) + + voltages = _singlediode._lambertw_v_from_i( + I.flatten(), il.flatten(), io.flatten(), rs.flatten(), rsh.flatten(), + a.flatten()) + + # apply negative voltage limit + voltages[voltages < neg_v_limit] = neg_v_limit + + # reshape + voltages = voltages.reshape(I.shape) + return voltages + + +def _setup_currents(device_isc, string_isc, npts): + r''' Form array of currents. Array of currents will contain all values + from device_isc which are less than string_isc. + + Parameters + ---------- + device_isc : ndarray + Shape (ndevices, ntimes) + string_isc : ndarray + Shape (ntimes,) + npts : int + number of current points in the returned array + + Returns + ------- + ndarray + shape (ntimes, npts) + + ''' + ntimes = len(string_isc) + A = np.zeros((ntimes, npts)) + + u = device_isc < string_isc[np.newaxis, :] + + # have to loop on ntimes since count of device_isc < string_isc may + # differ for each time + for i in range(ntimes): + vals = np.unique(device_isc[u[:, i], i]) + # ensure string_isc and 0. are added + vals = np.append(vals, [string_isc[i], 0.]) + k_i = len(vals) + + if k_i == 0: + continue + + # Copy original values + A[i, :k_i] = vals + + n_fill = npts - k_i + if n_fill <= 0: + continue + + # Build grid + grid = np.linspace(string_isc[i], 0., npts) + + # Compute distance to nearest point in arr + # shape: (grid_size, k_i) + dists = np.abs(grid[:, None] - vals[None, :]) + + # nearest distance per grid point + nearest_dist = np.min(dists, axis=1) + + # inverse-distance weights (higher near arr values) + weights = 1.0 / (nearest_dist + 1e-12) # 1e-12 to avoid div by 0 + + # Avoid re-selecting original values exactly + mask_existing = np.isclose(nearest_dist, 0.0, atol=1e-12) + weights[mask_existing] = 0.0 + + # Select top-weighted grid points + idx = np.argpartition(weights, -n_fill)[-n_fill:] + selected = grid[idx] + + # Combine and add to A + A[i, :] = np.concatenate([vals, selected]) + + # return sorted in descending order for each time + A = -np.sort(-A, axis=1) + + return A + + def _iv_series_lambertw(photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth, neg_v_limit=None, - delta_i=0.001): - r'''Solve the IV curve for series-connected devices using a single diode - equivalent circuit model. + resistance_shunt, nNsVth, neg_v_limit=0., + npts=100): + r'''Solve the IV curve for series-connected devices where each device + is described by the single diode equation. Uses a simplified model for reverse bias behavior, where current is unbounded at a constant reverse bias voltage ``neg_v_limit``. - Input parameters photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth may be arrays. If arrays, all must be - broadcastable to a common shape. The first dimension of each array - is time. The 2nd dimension is devices in series. + Input parameter ``photocurrent`` must have shape (devices, times). + Input parameters ``saturation_current``, ``resistance_series``, + ``resistance_shunt``, ``nNsVth`` may be arrays. If arrays, must be + broadcastable to the shape of ``photocurrent``. Parameters ---------- photocurrent : numeric - photocurrent (A). + photocurrent (A). Must have shape (devices, times). saturation_current : numeric - saturation current (A). + saturation current (A). Must be broadcastable with photocurrent. resistance_series : numeric - series resistance (ohm). + series resistance (ohm). Must be broadcastable with photocurrent. resistance_shunt : numeric - shunt resistance (ohm). + shunt resistance (ohm). Must be broadcastable with photocurrent. nNsVth : numeric product of diode factor n, number of series cells Ns, and - thermal voltage (Vth), (V). + thermal voltage (Vth), (V). Must be broadcastable with photocurrent. neg_v_limit : float, optional Limit on reverse bias voltage, from cell breakdown voltage or reverse bias diode activation voltage (V). Should be negative. For example, if neg_v_limit=-5, then at V=-5 current is unbounded in the positive direction. - delta_i : float, optional - Width of interval used to discretize current (A). + npts : int, optional + Number of points used to discretize the returned IV curves. Returns ------- - None. + voltages : numeric + Voltage points for the series IV curves (V), shape + (times, npts). + currents : numeric + Current points for the series IV curves (A), shape + (times, npts). ''' + # target shape is ndevices x ntimes + IL, I0, Rs, Rsh, a = \ + np.broadcast_arrays(photocurrent, saturation_current, + resistance_series, resistance_shunt, nNsVth) + + ndevices, ntimes = IL.shape + # solve for Isc - isc = _singlediode._lambertw_i_from_v( - 0., photocurrent, saturation_current, resistance_series, - resistance_shunt, nNsVth) - - # discretize current from max(Isc) down to 0. - currents = np.arange(isc.max(), 0., step=-delta_i) - - # shape all the arrays - # target shape is ntimes x ndevices x ncurrents - # use a dict so we can add axes using a loop - params = {'photocurrent': photocurrent, - 'saturation_current': saturation_current, - 'resistance_series': resistance_series, - 'resistance_shunt': resistance_shunt, - 'nNsVth': nNsVth} - - for p in params: - if not isinstance(params[p], np.ndarray): - continue # float, int - if len(params[p].shape) == 1: - params[p] = params[p][:, np.newaxis, np.newaxis] - elif len(params[p].shape) == 2: - params[p] = params[p][:, :, np.newaxis] - else: - continue # already 3d - - il, io, rs, rsh, a = (params[p] for p in params) - - currents = currents[np.newaxis, np.newaxis, :] - - il, io, rs, rsh, a, currents = np.broadcast_arrays( - il, io, rs, rsh, a, currents) + # Isc for each device + device_isc = _singlediode._lambertw_i_from_v( + neg_v_limit, IL, I0, Rs, Rsh, a) + + # find Isc for series of devices + # 1d array contains bounds for each time + # add/substract eps in case max==min + max_isc = device_isc.max(axis=0) * 1.01 + min_isc = device_isc.min(axis=0) * 0.99 + + # use index idx so that find_root can slice arguments + # internally find_root will slice current as each element converges + # as an argument, idx lets find_root also slice the other parameters + # remove idx and use preserve_shape once available in find_root + # https://github.com/scipy/scipy/issues/24869 + idx = np.arange(ntimes) + def optfn(current, idx): + # current is ntimes only since it is common for all devices. + # other parameters are ntimes x ndevices + v = _iv_series_lambert_v_from_i( + current, IL, I0, Rs, + Rsh, a, neg_v_limit, ndevices, idx) + # return string voltage + return v.sum(axis=0) + + isc_result = find_root( + optfn, + (min_isc, max_isc), args=(idx,)) + string_isc = isc_result.x # 1d in ntimes + + # discretize current from max(Isc) down to 0. at each time step + # Include each device Isc (except the highest) so that the series IV curve + current_pts = _setup_currents(device_isc, string_isc, npts) + + # shape all arrays to be ndevices x ntimes x ncurrents + I = np.repeat(current_pts[np.newaxis, :, :], ndevices, axis=0) + I, IL, I0, Rs, Rsh, a = np.broadcast_arrays( + I, IL[:, :, np.newaxis], I0[:, :, np.newaxis], Rs[:, :, np.newaxis], + Rsh[:, :, np.newaxis], a[:, :, np.newaxis]) # solve voltages at each current for each IV curve - voltages = _singlediode._lambertw_v_from_i( - currents, il, io, rs, rsh, a) - - # apply negative voltage limit - if neg_v_limit is not None: - voltages[voltages < neg_v_limit] = neg_v_limit + voltages = _iv_series_lambert_v_from_i( + I, IL, I0, Rs, Rsh, a, neg_v_limit) - # add voltage at common current to get series voltage - voltage_sum = voltages.sum(axis=1) + # add voltage across devices to get series voltage + voltage_sum = voltages.sum(axis=0) # drop currents dimension for devices - currents = currents[:, 0, :] + I = I[0, :, :] - return voltage_sum, currents + return voltage_sum, I From 80b952050017e2302926b54dfd37bd2abe6644ed Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Mon, 17 Aug 2026 16:05:00 -0600 Subject: [PATCH 04/10] rename some 1 letter variables --- pvlib/mismatch.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/pvlib/mismatch.py b/pvlib/mismatch.py index 4d20141e5d..d70751b071 100644 --- a/pvlib/mismatch.py +++ b/pvlib/mismatch.py @@ -8,26 +8,27 @@ import singlediode as _singlediode -def _iv_series_lambert_v_from_i(I, il, io, rs, rsh, a, neg_v_limit, +def _iv_series_lambert_v_from_i(current, il, io, rs, rsh, a, neg_v_limit, ndevices=None, idx=None): # solve voltages at each current for each IV curve if ndevices is not None: # broadcast I to ndevices to apply same current each device - I = np.broadcast_to(I[np.newaxis, :], (ndevices, len(I))) + current = np.broadcast_to(current[np.newaxis, :], + (ndevices, len(current))) # slice each parameter on its ntimes dimension with idx if idx is not None: il, io, rs, rsh, a = (il[:, idx], io[:, idx], rs[:, idx], rsh[:, idx], a[:, idx]) voltages = _singlediode._lambertw_v_from_i( - I.flatten(), il.flatten(), io.flatten(), rs.flatten(), rsh.flatten(), - a.flatten()) + current.flatten(), il.flatten(), io.flatten(), rs.flatten(), + rsh.flatten(), a.flatten()) # apply negative voltage limit voltages[voltages < neg_v_limit] = neg_v_limit # reshape - voltages = voltages.reshape(I.shape) + voltages = voltages.reshape(current.shape) return voltages @@ -51,7 +52,7 @@ def _setup_currents(device_isc, string_isc, npts): ''' ntimes = len(string_isc) - A = np.zeros((ntimes, npts)) + currents = np.zeros((ntimes, npts)) u = device_isc < string_isc[np.newaxis, :] @@ -67,7 +68,7 @@ def _setup_currents(device_isc, string_isc, npts): continue # Copy original values - A[i, :k_i] = vals + currents[i, :k_i] = vals n_fill = npts - k_i if n_fill <= 0: @@ -95,12 +96,12 @@ def _setup_currents(device_isc, string_isc, npts): selected = grid[idx] # Combine and add to A - A[i, :] = np.concatenate([vals, selected]) + currents[i, :] = np.concatenate([vals, selected]) # return sorted in descending order for each time - A = -np.sort(-A, axis=1) + currents = -np.sort(-currents, axis=1) - return A + return currents def _iv_series_lambertw(photocurrent, saturation_current, resistance_series, From 0918ad16ce27f51e26f32d2951d45c96481b6baa Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Tue, 18 Aug 2026 13:46:03 -0600 Subject: [PATCH 05/10] tests for series combinations --- tests/test_mismatch.py | 77 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 tests/test_mismatch.py diff --git a/tests/test_mismatch.py b/tests/test_mismatch.py new file mode 100644 index 0000000000..8914678c35 --- /dev/null +++ b/tests/test_mismatch.py @@ -0,0 +1,77 @@ +import numpy as np +from pvlib import singlediode as _singlediode +from pvlib.mismatch import _setup_currents, _iv_series_lambertw + + +def test__setup_currents(): + + # 2 devices, 1 time + cur_bkpts = np.array([[4.1, 5.2]]).T + string_isc = np.array([6.]) + curs = _setup_currents(cur_bkpts, string_isc, 10) + assert np.isin(cur_bkpts, curs).all() + assert (np.diff(curs) < 0.).all() # strictly decreasing + assert curs[:, -1] == 0. + assert curs[:, 0] == string_isc + + +def test__iv_series_lambertw_isc(): + + # 2 devices, 1 time + # Isc should be equal to the current on the higher curve where voltage is + # +neg_v_limit + IL = np.array([[1.0], [6.01]]) + Io = 1e-9 + nNsVth = 2.5 + Rs = 0.5 + Rsh = 1000. + npts = 5 + neg_v_limit = -5. + expected_isc = _singlediode._lambertw_i_from_v(-neg_v_limit, IL[1], Io, Rs, + Rsh, nNsVth) + vs, cs = _iv_series_lambertw(IL, Io, Rs, Rsh, nNsVth, neg_v_limit=neg_v_limit, + npts=npts) + assert vs.shape == cs.shape + assert vs.shape == (1, npts) # ntimes x npts + assert np.isclose(cs[0, 0], expected_isc) + assert np.isclose(vs[0, 0], 0.) + + +def test__iv_series_lambertw_voc(): + + # 2 devices, 1 time + # Voc should be equal to the sum of Voc for each device + IL = np.array([[1.0], [6.01]]) + Io = 1e-9 + nNsVth = 2.5 + Rs = 0.5 + Rsh = 1000. + npts = 5 + neg_v_limit = -5. + expected_voc = _singlediode._lambertw_v_from_i(0, IL, Io, Rs, + Rsh, nNsVth) + vs, cs = _iv_series_lambertw(IL, Io, Rs, Rsh, nNsVth, neg_v_limit=neg_v_limit, + npts=npts) + + assert np.isclose(vs[0, -1], expected_voc.sum()) + assert np.isclose(cs[0, -1], 0.) + + +def test__iv_series_lambertw_breakpoints(): + + # 2 devices, 1 time + # Voc should be equal to the sum of Voc for each device + IL = np.array([[1.0], [6.01]]) + Io = 1e-9 + nNsVth = 2.5 + Rs = 0.5 + Rsh = 1000. + npts = 10 + neg_v_limit = -5. + + cur_breakpoints = _singlediode._lambertw_i_from_v(neg_v_limit, IL, Io, Rs, + Rsh, nNsVth) + vs, cs = _iv_series_lambertw(IL, Io, Rs, Rsh, nNsVth, neg_v_limit=neg_v_limit, + npts=npts) + # current cs should contain all breakpoints less than string_isc + assert np.isin(cur_breakpoints[cur_breakpoints < cs[0, 0]], cs).all() From d51a6a3483373a640cd87704b0b039eb39e951d9 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Tue, 18 Aug 2026 13:54:05 -0600 Subject: [PATCH 06/10] fix import --- pvlib/mismatch.py | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/pvlib/mismatch.py b/pvlib/mismatch.py index d70751b071..a9d2cb06e9 100644 --- a/pvlib/mismatch.py +++ b/pvlib/mismatch.py @@ -5,11 +5,13 @@ """ import numpy as np from scipy.optimize.elementwise import find_root -import singlediode as _singlediode +from pvlib import singlediode as _singlediode def _iv_series_lambert_v_from_i(current, il, io, rs, rsh, a, neg_v_limit, ndevices=None, idx=None): + # wrapper for pvlib._singlediode._lambertw_v_from_i, handles + # dimensions expected in series calculation # solve voltages at each current for each IV curve if ndevices is not None: # broadcast I to ndevices to apply same current each device @@ -32,9 +34,10 @@ def _iv_series_lambert_v_from_i(current, il, io, rs, rsh, a, neg_v_limit, return voltages -def _setup_currents(device_isc, string_isc, npts): - r''' Form array of currents. Array of currents will contain all values - from device_isc which are less than string_isc. +def _setup_currents(current_bkpts, string_isc, npts): + r''' Form array of currents from string_isc down to 0. + The array of currents will contain all values + from current_bkpts which are less than string_isc. Parameters ---------- @@ -54,12 +57,12 @@ def _setup_currents(device_isc, string_isc, npts): ntimes = len(string_isc) currents = np.zeros((ntimes, npts)) - u = device_isc < string_isc[np.newaxis, :] + u = current_bkpts < string_isc[np.newaxis, :] # have to loop on ntimes since count of device_isc < string_isc may # differ for each time for i in range(ntimes): - vals = np.unique(device_isc[u[:, i], i]) + vals = np.unique(current_bkpts[u[:, i], i]) # ensure string_isc and 0. are added vals = np.append(vals, [string_isc[i], 0.]) k_i = len(vals) @@ -156,21 +159,20 @@ def _iv_series_lambertw(photocurrent, saturation_current, resistance_series, ndevices, ntimes = IL.shape - # solve for Isc - # Isc for each device - device_isc = _singlediode._lambertw_i_from_v( + # solve for current at negative voltage limit for each device. + # these currents create breakpoints in the series IV curve + current_bkpts = _singlediode._lambertw_i_from_v( neg_v_limit, IL, I0, Rs, Rsh, a) - # find Isc for series of devices - # 1d array contains bounds for each time - # add/substract eps in case max==min - max_isc = device_isc.max(axis=0) * 1.01 - min_isc = device_isc.min(axis=0) * 0.99 + # find Isc for string IV curve + # bounds, 1d array for each time + max_isc = current_bkpts.max(axis=0) * 1.01 + min_isc = current_bkpts.min(axis=0) * 0.99 - # use index idx so that find_root can slice arguments - # internally find_root will slice current as each element converges - # as an argument, idx lets find_root also slice the other parameters - # remove idx and use preserve_shape once available in find_root + # Use an index idx so that find_root can slice arguments + # Internally find_root will slice current as each element converges + # As an argument, idx lets find_root also slice the other parameters + # Remove idx and use preserve_shape once available in find_root # https://github.com/scipy/scipy/issues/24869 idx = np.arange(ntimes) def optfn(current, idx): @@ -187,9 +189,10 @@ def optfn(current, idx): (min_isc, max_isc), args=(idx,)) string_isc = isc_result.x # 1d in ntimes - # discretize current from max(Isc) down to 0. at each time step + # discretize current from string_isc down to 0 at each time step # Include each device Isc (except the highest) so that the series IV curve - current_pts = _setup_currents(device_isc, string_isc, npts) + # includes the breakpoints + current_pts = _setup_currents(current_bkpts, string_isc, npts) # shape all arrays to be ndevices x ntimes x ncurrents I = np.repeat(current_pts[np.newaxis, :, :], ndevices, axis=0) From 8998ef591f0fe1f292799d6b12d84eb07456c643 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Tue, 18 Aug 2026 13:59:18 -0600 Subject: [PATCH 07/10] flake8 --- pvlib/mismatch.py | 13 +++++++------ tests/test_mismatch.py | 15 +++++++++------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/pvlib/mismatch.py b/pvlib/mismatch.py index a9d2cb06e9..2af7f0148a 100644 --- a/pvlib/mismatch.py +++ b/pvlib/mismatch.py @@ -175,6 +175,7 @@ def _iv_series_lambertw(photocurrent, saturation_current, resistance_series, # Remove idx and use preserve_shape once available in find_root # https://github.com/scipy/scipy/issues/24869 idx = np.arange(ntimes) + def optfn(current, idx): # current is ntimes only since it is common for all devices. # other parameters are ntimes x ndevices @@ -195,19 +196,19 @@ def optfn(current, idx): current_pts = _setup_currents(current_bkpts, string_isc, npts) # shape all arrays to be ndevices x ntimes x ncurrents - I = np.repeat(current_pts[np.newaxis, :, :], ndevices, axis=0) - I, IL, I0, Rs, Rsh, a = np.broadcast_arrays( - I, IL[:, :, np.newaxis], I0[:, :, np.newaxis], Rs[:, :, np.newaxis], + curs = np.repeat(current_pts[np.newaxis, :, :], ndevices, axis=0) + curs, IL, I0, Rs, Rsh, a = np.broadcast_arrays( + curs, IL[:, :, np.newaxis], I0[:, :, np.newaxis], Rs[:, :, np.newaxis], Rsh[:, :, np.newaxis], a[:, :, np.newaxis]) # solve voltages at each current for each IV curve voltages = _iv_series_lambert_v_from_i( - I, IL, I0, Rs, Rsh, a, neg_v_limit) + curs, IL, I0, Rs, Rsh, a, neg_v_limit) # add voltage across devices to get series voltage voltage_sum = voltages.sum(axis=0) # drop currents dimension for devices - I = I[0, :, :] + curs = curs[0, :, :] - return voltage_sum, I + return voltage_sum, curs diff --git a/tests/test_mismatch.py b/tests/test_mismatch.py index 8914678c35..4c097df070 100644 --- a/tests/test_mismatch.py +++ b/tests/test_mismatch.py @@ -29,7 +29,8 @@ def test__iv_series_lambertw_isc(): neg_v_limit = -5. expected_isc = _singlediode._lambertw_i_from_v(-neg_v_limit, IL[1], Io, Rs, Rsh, nNsVth) - vs, cs = _iv_series_lambertw(IL, Io, Rs, Rsh, nNsVth, neg_v_limit=neg_v_limit, + vs, cs = _iv_series_lambertw(IL, Io, Rs, Rsh, nNsVth, + neg_v_limit=neg_v_limit, npts=npts) assert vs.shape == cs.shape assert vs.shape == (1, npts) # ntimes x npts @@ -50,15 +51,16 @@ def test__iv_series_lambertw_voc(): neg_v_limit = -5. expected_voc = _singlediode._lambertw_v_from_i(0, IL, Io, Rs, Rsh, nNsVth) - vs, cs = _iv_series_lambertw(IL, Io, Rs, Rsh, nNsVth, neg_v_limit=neg_v_limit, + vs, cs = _iv_series_lambertw(IL, Io, Rs, Rsh, nNsVth, + neg_v_limit=neg_v_limit, npts=npts) - + assert np.isclose(vs[0, -1], expected_voc.sum()) assert np.isclose(cs[0, -1], 0.) def test__iv_series_lambertw_breakpoints(): - + # 2 devices, 1 time # Voc should be equal to the sum of Voc for each device IL = np.array([[1.0], [6.01]]) @@ -68,10 +70,11 @@ def test__iv_series_lambertw_breakpoints(): Rsh = 1000. npts = 10 neg_v_limit = -5. - + cur_breakpoints = _singlediode._lambertw_i_from_v(neg_v_limit, IL, Io, Rs, Rsh, nNsVth) - vs, cs = _iv_series_lambertw(IL, Io, Rs, Rsh, nNsVth, neg_v_limit=neg_v_limit, + vs, cs = _iv_series_lambertw(IL, Io, Rs, Rsh, nNsVth, + neg_v_limit=neg_v_limit, npts=npts) # current cs should contain all breakpoints less than string_isc assert np.isin(cur_breakpoints[cur_breakpoints < cs[0, 0]], cs).all() From b0c9e71070d473c2ef4dac45d166650dfe65c06b Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 19 Aug 2026 06:32:32 -0600 Subject: [PATCH 08/10] add to docs --- .../effects_on_pv_system_output/electrical-mismatch.rst | 9 +++++++++ .../reference/effects_on_pv_system_output/index.rst | 1 + docs/sphinx/source/whatsnew/v0.15.3.rst | 3 +++ 3 files changed, 13 insertions(+) create mode 100644 docs/sphinx/source/reference/effects_on_pv_system_output/electrical-mismatch.rst diff --git a/docs/sphinx/source/reference/effects_on_pv_system_output/electrical-mismatch.rst b/docs/sphinx/source/reference/effects_on_pv_system_output/electrical-mismatch.rst new file mode 100644 index 0000000000..7723287917 --- /dev/null +++ b/docs/sphinx/source/reference/effects_on_pv_system_output/electrical-mismatch.rst @@ -0,0 +1,9 @@ +.. currentmodule:: pvlib + +Electrical mismatch +------------------- + +.. autosummary:: + :toctree: ../generated/ + + mismatch._iv_series_lambertw diff --git a/docs/sphinx/source/reference/effects_on_pv_system_output/index.rst b/docs/sphinx/source/reference/effects_on_pv_system_output/index.rst index cb046de7db..a2e0dc2de3 100644 --- a/docs/sphinx/source/reference/effects_on_pv_system_output/index.rst +++ b/docs/sphinx/source/reference/effects_on_pv_system_output/index.rst @@ -11,3 +11,4 @@ Effects on PV System Output soiling shading spectrum + electrical-mismatch diff --git a/docs/sphinx/source/whatsnew/v0.15.3.rst b/docs/sphinx/source/whatsnew/v0.15.3.rst index 8cf8515577..a5b4339406 100644 --- a/docs/sphinx/source/whatsnew/v0.15.3.rst +++ b/docs/sphinx/source/whatsnew/v0.15.3.rst @@ -68,6 +68,8 @@ Enhancements * Accelerate :py:func:`~pvlib.bifacial.utils.vf_ground_sky_2d_integ` by one or two orders of magnitude. This also makes :py:mod:`pvlib.bifacial.infinite_sheds` faster. (:pull:`2740`) +* Add `:py:mod:`~pvlib.mismatch` to contain functions for computing DC power + and IV curves accounting for electrical mismatch (:pull:`2718`) Documentation @@ -120,3 +122,4 @@ Contributors * Leonardo Scappatura (:ghuser:`Leonard013`) * Carolina Crespo (:ghuser:`cbcrespo`) * Ioannis Sifnaios (:ghuser:`IoannisSifnaios`) +* Cliff Hansen (:ghuser:`cwhanse`) From 9fcf6a9d2a02a081af74db833da15b95ac5a8d15 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 19 Aug 2026 06:36:02 -0600 Subject: [PATCH 09/10] oops to v0.16.0 --- docs/sphinx/source/whatsnew/v0.16.0.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/sphinx/source/whatsnew/v0.16.0.rst b/docs/sphinx/source/whatsnew/v0.16.0.rst index 3ea266e6cb..8b7ea2b9ec 100644 --- a/docs/sphinx/source/whatsnew/v0.16.0.rst +++ b/docs/sphinx/source/whatsnew/v0.16.0.rst @@ -66,7 +66,8 @@ Enhancements (:issue:`2828`, :pull:`2832`) * Allow variables from multiple datasets to be requested at once in :py:func:`~pvlib.iotools.get_merra2`. (:pull:`2839`) - +* Add `:py:mod:`~pvlib.mismatch` to contain functions for computing DC power + and IV curves accounting for electrical mismatch (:pull:`2718`) Documentation ~~~~~~~~~~~~~ @@ -97,3 +98,4 @@ Contributors * Sai Asish Y (:ghuser:`SAY-5`) * Kevin Anderson (:ghuser:`kandersolar`) * Johann Loux (:ghuser:`JoLo90`) +* Cliff Hansen (:ghuser:`cwhanse`) From 64dcfbf93c99865828ed6098a6121274a765fe58 Mon Sep 17 00:00:00 2001 From: Cliff Hansen Date: Wed, 19 Aug 2026 06:38:13 -0600 Subject: [PATCH 10/10] not to v0.15.3 --- docs/sphinx/source/whatsnew/v0.15.3.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/sphinx/source/whatsnew/v0.15.3.rst b/docs/sphinx/source/whatsnew/v0.15.3.rst index a5b4339406..8cf8515577 100644 --- a/docs/sphinx/source/whatsnew/v0.15.3.rst +++ b/docs/sphinx/source/whatsnew/v0.15.3.rst @@ -68,8 +68,6 @@ Enhancements * Accelerate :py:func:`~pvlib.bifacial.utils.vf_ground_sky_2d_integ` by one or two orders of magnitude. This also makes :py:mod:`pvlib.bifacial.infinite_sheds` faster. (:pull:`2740`) -* Add `:py:mod:`~pvlib.mismatch` to contain functions for computing DC power - and IV curves accounting for electrical mismatch (:pull:`2718`) Documentation @@ -122,4 +120,3 @@ Contributors * Leonardo Scappatura (:ghuser:`Leonard013`) * Carolina Crespo (:ghuser:`cbcrespo`) * Ioannis Sifnaios (:ghuser:`IoannisSifnaios`) -* Cliff Hansen (:ghuser:`cwhanse`)