From 2902baee299e387e944f490d4b4bfeef6a9dd376 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Fri, 1 Dec 2023 09:57:06 -0700 Subject: [PATCH 01/14] Minimal functionality for early feedback --- pvlib/pvsystem.py | 57 ++++++++++++++++++++++++++++++++++++ pvlib/tests/test_pvsystem.py | 45 ++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index f0c1cd2cda..3aeaf7d241 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -7,11 +7,13 @@ import functools import io import itertools +import numbers import os import inspect from urllib.request import urlopen import numpy as np from scipy import constants +import scipy.optimize import pandas as pd from dataclasses import dataclass from abc import ABC, abstractmethod @@ -3022,3 +3024,58 @@ def combine_loss_factors(index, *losses, fill_method='ffill'): combined_factor *= (1 - loss) return 1 - combined_factor + + +def _negative_total_power(current, *args): + """ + Compute negative of total power generated by devices in series at specified current. + """ + return -np.sum(current * v_from_i(current, *args)) + + +def max_power_point_mismatched( + photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, +): + """Compute maximum power info for (possibly) mismatched set of devices in series.""" + if all([isinstance(input, numbers.Number) for input in ( + photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth + )]): + # There cannot be mismatch, so fall back to non-mismatch function. + return { + **max_power_point( + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth + ) + } + + i_mp_0 = max_power_point( + np.mean(photocurrent), + np.mean(saturation_current), + np.mean(resistance_series), + np.mean(resistance_shunt), + np.mean(nNsVth), + )["i_mp"] + args = photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth + sol = scipy.optimize.minimize(_negative_total_power, i_mp_0, args) + + if sol.success: + i_mp = sol.x[0] + v_mp = v_from_i( + i_mp, + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + ) + + return { + "i_mp": i_mp, + "v_mp": v_mp, + "p_mp": i_mp * v_mp, + } + + raise RuntimeError(f"unsuccessful solution: {sol}") diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 6557d77e10..d60abe161f 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -4,6 +4,7 @@ import numpy as np from numpy import nan, array import pandas as pd +import scipy.constants import pytest from .conftest import ( @@ -2549,3 +2550,47 @@ def test_Array_temperature_missing_parameters(model, keys): array.temperature_model_parameters = params with pytest.raises(KeyError, match=match): array.get_cell_temperature(irrads, temps, winds, model) + + +@pytest.mark.parametrize( + 'inputs', + [ + { + "photocurrent": 6.2, + "saturation_current": 1.0e-8, + "n": 1.1, + "resistance_series": 0.0001, + "resistance_shunt": 5000.0, + "Ns": 60, + "T": 25.0, + }, + { + "photocurrent": np.array([5.8, 6.2]), + "saturation_current": 1.0e-8, + "n": 1.1, + "resistance_series": 0.0001, + "resistance_shunt": 5000.0, + "Ns": 60, + "T": 25.0, + } + ] +) +def test_max_power_point_mismatched(inputs): + """Test max power point computaiton for mismatched devices in series.""" + + photocurrent = inputs["photocurrent"] + saturation_current = inputs["saturation_current"] + resistance_series = inputs["resistance_series"] + resistance_shunt = inputs["resistance_shunt"] + q_C = scipy.constants.value("elementary charge") + k_B_J_per_K = scipy.constants.value("Boltzmann constant") + T_K = scipy.constants.convert_temperature(inputs["T"], "Celsius", "Kelvin") + nNsVth = inputs["n"] * inputs["Ns"] * k_B_J_per_K * T_K / q_C + + pvsystem.max_power_point_mismatched( + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + ) From 1c2404a8a84a9cc3e6683ba454c4cf42ed083507 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Fri, 1 Dec 2023 10:15:38 -0700 Subject: [PATCH 02/14] Appease flake8 --- pvlib/pvsystem.py | 36 +++++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 3aeaf7d241..920e9beb60 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -3028,18 +3028,34 @@ def combine_loss_factors(index, *losses, fill_method='ffill'): def _negative_total_power(current, *args): """ - Compute negative of total power generated by devices in series at specified current. + Compute negative of total power generated by devices in series at + specified current. """ return -np.sum(current * v_from_i(current, *args)) def max_power_point_mismatched( - photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth, + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, ): - """Compute maximum power info for (possibly) mismatched set of devices in series.""" - if all([isinstance(input, numbers.Number) for input in ( - photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth - )]): + """ + Compute maximum power info for (possibly) mismatched set of devices in + series. + """ + if all( + [ + isinstance(input, numbers.Number) for input in ( + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + ) + ] + ): # There cannot be mismatch, so fall back to non-mismatch function. return { **max_power_point( @@ -3058,7 +3074,13 @@ def max_power_point_mismatched( np.mean(resistance_shunt), np.mean(nNsVth), )["i_mp"] - args = photocurrent, saturation_current, resistance_series, resistance_shunt, nNsVth + args = ( + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + ) sol = scipy.optimize.minimize(_negative_total_power, i_mp_0, args) if sol.success: From 4859d64fcba7763afaa84e62981ce2399619f4e2 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Fri, 1 Dec 2023 10:28:10 -0700 Subject: [PATCH 03/14] Appease flake8 more --- pvlib/tests/test_pvsystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index d60abe161f..4f007f3e4b 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -2553,7 +2553,7 @@ def test_Array_temperature_missing_parameters(model, keys): @pytest.mark.parametrize( - 'inputs', + 'inputs', [ { "photocurrent": 6.2, From e881ce7f7934eb2ca2d606be93dba5dc27bee837 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Fri, 1 Dec 2023 11:33:22 -0700 Subject: [PATCH 04/14] Improve solver, streamline, and add tests --- pvlib/pvsystem.py | 67 ++++++++++++++++++---------------- pvlib/tests/test_pvsystem.py | 70 +++++++++++++++++++++++++++++++----- 2 files changed, 98 insertions(+), 39 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 920e9beb60..f7f3d889e0 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -3040,40 +3040,29 @@ def max_power_point_mismatched( resistance_series, resistance_shunt, nNsVth, + *, + i_mp_ic=None, ): """ Compute maximum power info for (possibly) mismatched set of devices in - series. + series. When using this serially on time-series data, passing i_mp_ic from + previous step may speed up computation. Algorithm falls back to automated + computation of i_mp_ic if solution fails with provided i_mp_ic. The value + of i_mp_ic used is returned along with i_mp (same value for all devices), + v_mp, and p_mp. """ - if all( - [ - isinstance(input, numbers.Number) for input in ( - photocurrent, - saturation_current, - resistance_series, - resistance_shunt, - nNsVth, - ) - ] - ): - # There cannot be mismatch, so fall back to non-mismatch function. - return { - **max_power_point( - photocurrent, - saturation_current, - resistance_series, - resistance_shunt, - nNsVth - ) - } + if i_mp_ic is None: + i_mp_ic = max_power_point( + np.mean(photocurrent), + np.mean(saturation_current), + np.mean(resistance_series), + np.mean(resistance_shunt), + np.mean(nNsVth), + )["i_mp"] + retry_ic = False + else: + retry_ic = True - i_mp_0 = max_power_point( - np.mean(photocurrent), - np.mean(saturation_current), - np.mean(resistance_series), - np.mean(resistance_shunt), - np.mean(nNsVth), - )["i_mp"] args = ( photocurrent, saturation_current, @@ -3081,7 +3070,9 @@ def max_power_point_mismatched( resistance_shunt, nNsVth, ) - sol = scipy.optimize.minimize(_negative_total_power, i_mp_0, args) + sol = scipy.optimize.minimize( + _negative_total_power, i_mp_ic, args=args, jac='3-point' + ) if sol.success: i_mp = sol.x[0] @@ -3095,9 +3086,25 @@ def max_power_point_mismatched( ) return { + "i_mp_ic": i_mp_ic, "i_mp": i_mp, "v_mp": v_mp, "p_mp": i_mp * v_mp, + "i_mp_string": i_mp, + "v_mp_string": np.sum(v_mp), + "p_mp_string": -sol.fun, } + if retry_ic: + # Try solution one more time using automated inital condition. + # Caller can detect this occurance by seeing change in i_mp_ic in + # return value. + return max_power_point_mismatched( + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + ) + raise RuntimeError(f"unsuccessful solution: {sol}") diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 4f007f3e4b..3de4dd6aee 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -2564,6 +2564,35 @@ def test_Array_temperature_missing_parameters(model, keys): "Ns": 60, "T": 25.0, }, + { + "photocurrent": 6.2, + "saturation_current": 1.0e-8, + "n": 1.1, + "resistance_series": 0.0001, + "resistance_shunt": 5000.0, + "Ns": 60, + "T": 25.0, + "i_mp_ic": 5.8, + }, + { + "photocurrent": np.array([5.8, 6.2]), + "saturation_current": 1.0e-8, + "n": 1.1, + "resistance_series": 0.0001, + "resistance_shunt": 5000.0, + "Ns": 60, + "T": 25.0, + }, + { + "photocurrent": np.array([5.8, 6.2]), + "saturation_current": 1.0e-8, + "n": 1.1, + "resistance_series": 0.0001, + "resistance_shunt": 5000.0, + "Ns": 60, + "T": 25.0, + "i_mp_ic": None, + }, { "photocurrent": np.array([5.8, 6.2]), "saturation_current": 1.0e-8, @@ -2572,11 +2601,22 @@ def test_Array_temperature_missing_parameters(model, keys): "resistance_shunt": 5000.0, "Ns": 60, "T": 25.0, - } + "i_mp_ic": -1.0e14, + }, + { + "photocurrent": np.array([5.8, 6.2]), + "saturation_current": 1.0e-8, + "n": 1.1, + "resistance_series": 0.0001, + "resistance_shunt": 5000.0, + "Ns": 60, + "T": 25.0, + "i_mp_ic": 5.6, + }, ] ) def test_max_power_point_mismatched(inputs): - """Test max power point computaiton for mismatched devices in series.""" + """Test max power point computation for mismatched devices in series.""" photocurrent = inputs["photocurrent"] saturation_current = inputs["saturation_current"] @@ -2587,10 +2627,22 @@ def test_max_power_point_mismatched(inputs): T_K = scipy.constants.convert_temperature(inputs["T"], "Celsius", "Kelvin") nNsVth = inputs["n"] * inputs["Ns"] * k_B_J_per_K * T_K / q_C - pvsystem.max_power_point_mismatched( - photocurrent, - saturation_current, - resistance_series, - resistance_shunt, - nNsVth, - ) + if "i_mp_ic" in inputs: + result = pvsystem.max_power_point_mismatched( + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + i_mp_ic=inputs["i_mp_ic"], + ) + else: + result = pvsystem.max_power_point_mismatched( + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + ) + + print(result) From 61753205671b95c323d58f6668f45ef5eef7d0c4 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Fri, 1 Dec 2023 11:34:18 -0700 Subject: [PATCH 05/14] Remove unused import --- pvlib/pvsystem.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index f7f3d889e0..18d01931d7 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -7,7 +7,6 @@ import functools import io import itertools -import numbers import os import inspect from urllib.request import urlopen From cbe1768c1e5ebb25af253fa0d4e999d9a6ba82b2 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Fri, 1 Dec 2023 11:43:45 -0700 Subject: [PATCH 06/14] Add test for exceptional case --- pvlib/tests/test_pvsystem.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 3de4dd6aee..087542863c 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -2646,3 +2646,27 @@ def test_max_power_point_mismatched(inputs): ) print(result) + +def test_max_power_point_mismatched_error(): + """ + Test errored max power point computation for mismatched devices in series. + """ + photocurrent = -6.2 + saturation_current = 1.0e-8 + resistance_series = 0.0001 + resistance_shunt = 5000.0 + q_C = scipy.constants.value("elementary charge") + k_B_J_per_K = scipy.constants.value("Boltzmann constant") + T_K = scipy.constants.convert_temperature(25.0, "Celsius", "Kelvin") + nNsVth = 1.1 * 60 * k_B_J_per_K * T_K / q_C + + with pytest.raises(ValueError) as e_info: + pvsystem.max_power_point_mismatched( + photocurrent, + saturation_current, + resistance_series, + resistance_shunt, + nNsVth, + ) + + print(e_info) From 999df203311f92c5aa15ddb8e23827016cdf21b7 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Fri, 1 Dec 2023 11:44:59 -0700 Subject: [PATCH 07/14] Linting --- pvlib/tests/test_pvsystem.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 087542863c..f91d9726f9 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -2647,6 +2647,7 @@ def test_max_power_point_mismatched(inputs): print(result) + def test_max_power_point_mismatched_error(): """ Test errored max power point computation for mismatched devices in series. From 3ca7556b07bd8f9ed3df7fc144bf2708c0cf2cf3 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Fri, 1 Dec 2023 12:01:38 -0700 Subject: [PATCH 08/14] Catch exceptions in i_mp_ic computation and update docstring --- pvlib/pvsystem.py | 21 +++++++++++++-------- pvlib/tests/test_pvsystem.py | 6 +++--- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 18d01931d7..827f62e44f 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -3048,17 +3048,22 @@ def max_power_point_mismatched( previous step may speed up computation. Algorithm falls back to automated computation of i_mp_ic if solution fails with provided i_mp_ic. The value of i_mp_ic used is returned along with i_mp (same value for all devices), - v_mp, and p_mp. + v_mp, p_mp, i_mp_string, v_mp_string, and p_mp_string. """ if i_mp_ic is None: - i_mp_ic = max_power_point( - np.mean(photocurrent), - np.mean(saturation_current), - np.mean(resistance_series), - np.mean(resistance_shunt), - np.mean(nNsVth), - )["i_mp"] retry_ic = False + try: + i_mp_ic = max_power_point( + np.mean(photocurrent), + np.mean(saturation_current), + np.mean(resistance_series), + np.mean(resistance_shunt), + np.mean(nNsVth), + )["i_mp"] + except Exception as exc: + raise RuntimeError( + f"unsuccessful determination of i_mp_ic" + ) from exc else: retry_ic = True diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index f91d9726f9..0ab31b1d42 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -2648,11 +2648,11 @@ def test_max_power_point_mismatched(inputs): print(result) -def test_max_power_point_mismatched_error(): +def test_max_power_point_mismatched_exception(): """ Test errored max power point computation for mismatched devices in series. """ - photocurrent = -6.2 + photocurrent = -6.2 # This is bad. saturation_current = 1.0e-8 resistance_series = 0.0001 resistance_shunt = 5000.0 @@ -2661,7 +2661,7 @@ def test_max_power_point_mismatched_error(): T_K = scipy.constants.convert_temperature(25.0, "Celsius", "Kelvin") nNsVth = 1.1 * 60 * k_B_J_per_K * T_K / q_C - with pytest.raises(ValueError) as e_info: + with pytest.raises(RuntimeError) as e_info: pvsystem.max_power_point_mismatched( photocurrent, saturation_current, From 1cca561f1a0af4bf32aa33c8dd16c9a67a1a2978 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Fri, 1 Dec 2023 12:09:30 -0700 Subject: [PATCH 09/14] Appease flake8 even more --- pvlib/pvsystem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 827f62e44f..de68435abe 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -3062,7 +3062,7 @@ def max_power_point_mismatched( )["i_mp"] except Exception as exc: raise RuntimeError( - f"unsuccessful determination of i_mp_ic" + "unsuccessful determination of i_mp_ic" ) from exc else: retry_ic = True From 58e44e07057b49fa8b9ebad919eb5b56b1f5d5ec Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Fri, 1 Dec 2023 12:15:08 -0700 Subject: [PATCH 10/14] Test both exceptional cases --- pvlib/tests/test_pvsystem.py | 43 ++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 0ab31b1d42..6ebff287ae 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -2648,18 +2648,47 @@ def test_max_power_point_mismatched(inputs): print(result) -def test_max_power_point_mismatched_exception(): +@pytest.mark.parametrize( + 'inputs', + [ + { + "photocurrent": -6.2, # This is bad. + "saturation_current": 1.0e-8, + "n": 1.1, + "resistance_series": 0.0001, + "resistance_shunt": 5000.0, + "Ns": 60, + "T": 25.0, + }, + { + "photocurrent": -6.2, # This is bad. + "saturation_current": 1.0e-8, + "n": 1.1, + "resistance_series": 0.0001, + "resistance_shunt": 5000.0, + "Ns": 60, + "T": 25.0, + "i_mp_ic": 5.6, + }, + ] +) +def test_max_power_point_mismatched_exception(inputs, monkeypatch): """ Test errored max power point computation for mismatched devices in series. """ - photocurrent = -6.2 # This is bad. - saturation_current = 1.0e-8 - resistance_series = 0.0001 - resistance_shunt = 5000.0 + # Monkey patch the objective function to force thrown exception. + monkeypatch.setattr( + "pvlib.pvsystem._negative_total_power", ZeroDivisionError + ) + + photocurrent = inputs["photocurrent"] + saturation_current = inputs["saturation_current"] + resistance_series = inputs["resistance_series"] + resistance_shunt = inputs["resistance_shunt"] q_C = scipy.constants.value("elementary charge") k_B_J_per_K = scipy.constants.value("Boltzmann constant") - T_K = scipy.constants.convert_temperature(25.0, "Celsius", "Kelvin") - nNsVth = 1.1 * 60 * k_B_J_per_K * T_K / q_C + T_K = scipy.constants.convert_temperature(inputs["T"], "Celsius", "Kelvin") + nNsVth = inputs["n"] * inputs["Ns"] * k_B_J_per_K * T_K / q_C with pytest.raises(RuntimeError) as e_info: pvsystem.max_power_point_mismatched( From e40af1e5803b99927bc392bf790e3a77d68fe84c Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Fri, 1 Dec 2023 14:47:50 -0700 Subject: [PATCH 11/14] Monkey patch solver to test exception --- pvlib/pvsystem.py | 21 ++++++------ pvlib/tests/test_pvsystem.py | 63 +++++++++++++----------------------- 2 files changed, 32 insertions(+), 52 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index de68435abe..59e8ece177 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -3052,18 +3052,14 @@ def max_power_point_mismatched( """ if i_mp_ic is None: retry_ic = False - try: - i_mp_ic = max_power_point( - np.mean(photocurrent), - np.mean(saturation_current), - np.mean(resistance_series), - np.mean(resistance_shunt), - np.mean(nNsVth), - )["i_mp"] - except Exception as exc: - raise RuntimeError( - "unsuccessful determination of i_mp_ic" - ) from exc + + i_mp_ic = max_power_point( + np.mean(photocurrent), + np.mean(saturation_current), + np.mean(resistance_series), + np.mean(resistance_shunt), + np.mean(nNsVth), + )["i_mp"] else: retry_ic = True @@ -3074,6 +3070,7 @@ def max_power_point_mismatched( resistance_shunt, nNsVth, ) + sol = scipy.optimize.minimize( _negative_total_power, i_mp_ic, args=args, jac='3-point' ) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 6ebff287ae..bf35e9a299 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -5,6 +5,7 @@ from numpy import nan, array import pandas as pd import scipy.constants +import scipy.optimize import pytest from .conftest import ( @@ -13,7 +14,6 @@ import unittest.mock as mock from pvlib import inverter, pvsystem -from pvlib import atmosphere from pvlib import iam as _iam from pvlib import irradiance from pvlib import spectrum @@ -2648,48 +2648,31 @@ def test_max_power_point_mismatched(inputs): print(result) -@pytest.mark.parametrize( - 'inputs', - [ - { - "photocurrent": -6.2, # This is bad. - "saturation_current": 1.0e-8, - "n": 1.1, - "resistance_series": 0.0001, - "resistance_shunt": 5000.0, - "Ns": 60, - "T": 25.0, - }, - { - "photocurrent": -6.2, # This is bad. - "saturation_current": 1.0e-8, - "n": 1.1, - "resistance_series": 0.0001, - "resistance_shunt": 5000.0, - "Ns": 60, - "T": 25.0, - "i_mp_ic": 5.6, - }, - ] -) -def test_max_power_point_mismatched_exception(inputs, monkeypatch): - """ - Test errored max power point computation for mismatched devices in series. - """ - # Monkey patch the objective function to force thrown exception. +def test_max_power_point_mismatched_unsuccessful_solver(monkeypatch): + """Test errored max power point computation where solver is unsuccessful.""" + photocurrent = 6.2 + saturation_current = 1.0e-8 + resistance_series = 0.0001 + resistance_shunt = 5000.0 + n = 1.1 + Ns = 60 + T = 25.0 + T_K = scipy.constants.convert_temperature(T, "Celsius", "Kelvin") + k_B_J_per_K = scipy.constants.value("Boltzmann constant") + q_C = scipy.constants.value("elementary charge") + nNsVth = n * Ns * k_B_J_per_K * T_K / q_C + + def minimize_monkeypatched(*_, **__): + """Return an unsuccessful solution from solver.""" + return scipy.optimize.OptimizeResult(success=False) + + # Monkey patch solver to return unsuccessfully. monkeypatch.setattr( - "pvlib.pvsystem._negative_total_power", ZeroDivisionError + scipy.optimize, + "minimize", + minimize_monkeypatched, ) - photocurrent = inputs["photocurrent"] - saturation_current = inputs["saturation_current"] - resistance_series = inputs["resistance_series"] - resistance_shunt = inputs["resistance_shunt"] - q_C = scipy.constants.value("elementary charge") - k_B_J_per_K = scipy.constants.value("Boltzmann constant") - T_K = scipy.constants.convert_temperature(inputs["T"], "Celsius", "Kelvin") - nNsVth = inputs["n"] * inputs["Ns"] * k_B_J_per_K * T_K / q_C - with pytest.raises(RuntimeError) as e_info: pvsystem.max_power_point_mismatched( photocurrent, From 123fd99a9b296e432ad9886c9aafbe4334257c5f Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Fri, 1 Dec 2023 14:49:37 -0700 Subject: [PATCH 12/14] Appease the flake8 monster --- pvlib/tests/test_pvsystem.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index bf35e9a299..6343d16b19 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -2649,10 +2649,12 @@ def test_max_power_point_mismatched(inputs): def test_max_power_point_mismatched_unsuccessful_solver(monkeypatch): - """Test errored max power point computation where solver is unsuccessful.""" + """ + Test errored max power point computation where solver is unsuccessful. + """ photocurrent = 6.2 saturation_current = 1.0e-8 - resistance_series = 0.0001 + resistance_series = 0.0001 resistance_shunt = 5000.0 n = 1.1 Ns = 60 From fe7ab2a4763401573e828ed3df7d050ae812c334 Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Sat, 2 Dec 2023 09:04:41 -0700 Subject: [PATCH 13/14] Add fixmes for remaining items to do --- pvlib/pvsystem.py | 4 +++- pvlib/tests/test_pvsystem.py | 7 ++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index 59e8ece177..2594f01572 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -3028,7 +3028,7 @@ def combine_loss_factors(index, *losses, fill_method='ffill'): def _negative_total_power(current, *args): """ Compute negative of total power generated by devices in series at - specified current. + specified current. Designed for use by scipy.optimize.minimize. """ return -np.sum(current * v_from_i(current, *args)) @@ -3043,6 +3043,8 @@ def max_power_point_mismatched( i_mp_ic=None, ): """ + FIXME Replace this with proper docstring. + Compute maximum power info for (possibly) mismatched set of devices in series. When using this serially on time-series data, passing i_mp_ic from previous step may speed up computation. Algorithm falls back to automated diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 6343d16b19..cdf7d8ff8c 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -2555,6 +2555,7 @@ def test_Array_temperature_missing_parameters(model, keys): @pytest.mark.parametrize( 'inputs', [ + # FIXME Need many more argument combinations in additional test cases. { "photocurrent": 6.2, "saturation_current": 1.0e-8, @@ -2645,13 +2646,12 @@ def test_max_power_point_mismatched(inputs): nNsVth, ) + # FIXME Replace this with test assertions. print(result) def test_max_power_point_mismatched_unsuccessful_solver(monkeypatch): - """ - Test errored max power point computation where solver is unsuccessful. - """ + """Test mismatched max power point computation where solver is unsuccessful.""" photocurrent = 6.2 saturation_current = 1.0e-8 resistance_series = 0.0001 @@ -2684,4 +2684,5 @@ def minimize_monkeypatched(*_, **__): nNsVth, ) + # FIXME Replace this with test assertions. print(e_info) From 51439e9e568bdcbbf9784f41c341d85905c1a12e Mon Sep 17 00:00:00 2001 From: Mark Campanelli Date: Sat, 2 Dec 2023 09:08:01 -0700 Subject: [PATCH 14/14] Oh flake8 how I love you! --- pvlib/tests/test_pvsystem.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index cdf7d8ff8c..6c2143c5ed 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -2651,7 +2651,9 @@ def test_max_power_point_mismatched(inputs): def test_max_power_point_mismatched_unsuccessful_solver(monkeypatch): - """Test mismatched max power point computation where solver is unsuccessful.""" + """ + Test mismatched max power point computation where solver is unsuccessful. + """ photocurrent = 6.2 saturation_current = 1.0e-8 resistance_series = 0.0001