diff --git a/docs/sphinx/source/whatsnew/v0.11.1.rst b/docs/sphinx/source/whatsnew/v0.11.1.rst index 7c8f01743e..781a68f32f 100644 --- a/docs/sphinx/source/whatsnew/v0.11.1.rst +++ b/docs/sphinx/source/whatsnew/v0.11.1.rst @@ -30,6 +30,10 @@ Enhancements * Added function for calculating wind speed at different heights, :py:func:`pvlib.atmosphere.windspeed_powerlaw`. (:issue:`2118`, :pull:`2124`) +* Implemented closed-form solution for alpha in detect_clearsky, obviating + the call to scipy.optimize that was prone to runtime errors and minimizing + computation, :py:func:`pvlib.clearsky.detect_clearsky`. Resolves :issue:`2712`. + Bug fixes ~~~~~~~~~ @@ -74,3 +78,4 @@ Contributors * Marcos R. Escudero (:ghuser:`marc-resc`) * Bernat Nicolau (:ghuser:`BernatNicolau`) * Eduardo Sarquis (:ghuser:`EduardoSarquis`) +* Andrew B Godbehere (:ghuser:`agodbehere`) \ No newline at end of file diff --git a/pvlib/clearsky.py b/pvlib/clearsky.py index 62d83bc1f4..3b36262dda 100644 --- a/pvlib/clearsky.py +++ b/pvlib/clearsky.py @@ -3,15 +3,14 @@ to calculate clear sky GHI, DNI, and DHI. """ +import calendar import os from collections import OrderedDict -import calendar +import h5py import numpy as np import pandas as pd -from scipy.optimize import minimize_scalar from scipy.linalg import hankel -import h5py from pvlib import atmosphere, tools from pvlib.tools import _degrees_to_index @@ -874,24 +873,11 @@ def detect_clearsky(measured, clearsky, times=None, infer_limits=False, clear_meas = meas[clear_samples] clear_clear = clear[clear_samples] - def rmse(alpha): - return np.sqrt(np.mean((clear_meas - alpha*clear_clear)**2)) - - optimize_result = minimize_scalar(rmse) - if not optimize_result.success: - try: - message = "Optimizer exited unsuccessfully: " \ - + optimize_result.message - except AttributeError: - message = "Optimizer exited unsuccessfully: \ - No message explaining the failure was returned. \ - If you would like to see this message, please \ - update your scipy version (try version 1.8.0 \ - or beyond)." - raise RuntimeError(message) - - else: - alpha = optimize_result.x + # Compute arg min of MSE between model and observations + C = (clear_clear**2).sum() + if not (pd.isna(C) or C == 0): # safety check + # only update alpha if C is strictly positive + alpha = (clear_meas * clear_clear).sum() / C if round(alpha*10000) == round(previous_alpha*10000): break diff --git a/pvlib/tests/test_clearsky.py b/pvlib/tests/test_clearsky.py index 4e353e997c..79502db01c 100644 --- a/pvlib/tests/test_clearsky.py +++ b/pvlib/tests/test_clearsky.py @@ -1,22 +1,17 @@ from collections import OrderedDict import numpy as np -from numpy import nan import pandas as pd -import pytz -from scipy.linalg import hankel - import pytest +import pytz +from numpy import nan from numpy.testing import assert_allclose -from .conftest import assert_frame_equal, assert_series_equal +from scipy.linalg import hankel +from pvlib import atmosphere, clearsky, irradiance, solarposition from pvlib.location import Location -from pvlib import clearsky -from pvlib import solarposition -from pvlib import atmosphere -from pvlib import irradiance -from .conftest import DATA_DIR +from .conftest import DATA_DIR, assert_frame_equal, assert_series_equal def test_ineichen_series(): @@ -677,12 +672,16 @@ def test_detect_clearsky_not_enough_data(detect_clearsky_data): with pytest.raises(ValueError, match='have at least'): clearsky.detect_clearsky(expected['GHI'], cs['ghi'], window_length=60) - -def test_detect_clearsky_optimizer_failed(detect_clearsky_data): +@pytest.mark.parametrize("window_length", [5, 10, 15, 20, 25]) +def test_detect_clearsky_optimizer_not_failed( + detect_clearsky_data, + window_length + ): expected, cs = detect_clearsky_data - with pytest.raises(RuntimeError, match='Optimizer exited unsuccessfully'): - clearsky.detect_clearsky(expected['GHI'], cs['ghi'], window_length=15) - + clear_samples = clearsky.detect_clearsky( + expected["GHI"], cs["ghi"], window_length=window_length + ) + assert isinstance(clear_samples, pd.Series) @pytest.fixture def detect_clearsky_helper_data():