diff --git a/docs/sphinx/source/changelog/v2.0.0.rst b/docs/sphinx/source/changelog/v2.0.0.rst index b1a6ed17..ff422804 100644 --- a/docs/sphinx/source/changelog/v2.0.0.rst +++ b/docs/sphinx/source/changelog/v2.0.0.rst @@ -13,9 +13,14 @@ Enhancements * Add new functions :py:func:`~rdtools.normalization.energy_from_power` and :py:func:`~rdtools.normalization.interpolate`. * Add new :py:mod:`~rdtools.plotting` module for generating standard plots. +* Add parameter ``convergence_threshold`` to + :py:func:`~rdtools.normalization.irradiance_rescale` (:pull:`152`). Bug fixes --------- +* Allow ``max_iterations=0`` in + :py:func:`~rdtools.normalization.irradiance_rescale` (:pull:`152`). + Testing ------- diff --git a/rdtools/normalization.py b/rdtools/normalization.py index 72bda304..1a0eba14 100644 --- a/rdtools/normalization.py +++ b/rdtools/normalization.py @@ -305,7 +305,8 @@ def delta_index(series): return deltas, np.mean(deltas.dropna()) -def irradiance_rescale(irrad, modeled_irrad, max_iterations=100, method=None): +def irradiance_rescale(irrad, modeled_irrad, max_iterations=100, + method='iterative', convergence_threshold=1e-6): ''' Attempt to rescale modeled irradiance to match measured irradiance on clear days. @@ -319,12 +320,17 @@ def irradiance_rescale(irrad, modeled_irrad, max_iterations=100, method=None): max_iterations : int, default 100 The maximum number of times to attempt rescale optimization. Ignored if `method` = 'single_opt' - method: str, default None + method : str, default 'iterative' The calculation method to use. 'single_opt' implements the irradiance_rescale of rdtools v1.1.3 and earlier. 'iterative' implements a more stable calculation that may yield different results from the single_opt method. - If omitted, issues a warning and uses the iterative calculation. + convergence_threshold : float, default 1e-6 + The acceptable iteration-to-iteration scaling factor difference to + determine convergence. If the threshold is not reached after + `max_iterations`, raise + :py:exc:`rdtools.normalization.ConvergenceError`. + Must be greater than zero. Only used if `method=='iterative'`. Returns ------- @@ -332,13 +338,6 @@ def irradiance_rescale(irrad, modeled_irrad, max_iterations=100, method=None): Rescaled modeled irradiance time series ''' - if method is None: - warnings.warn("The underlying calculations for irradiance_rescale " - "have changed which may affect results. To revert to " - "the version of irradiance_rescale from rdtools v1.1.3 " - "or earlier, use method = 'single_opt'.") - method = 'iterative' - if method == 'iterative': def _rmse(fact): """ @@ -361,24 +360,22 @@ def _single_rescale(irrad, modeled_irrad, guess): return factor # Calculate an initial guess for the rescale factor - factor = np.percentile(irrad.dropna(), 90) / \ - np.percentile(modeled_irrad.dropna(), 90) + factor = (np.percentile(irrad.dropna(), 90) / + np.percentile(modeled_irrad.dropna(), 90)) + prev_factor = 1.0 # Iteratively run the optimization, # recalculating the clear sky filter each time - convergence_threshold = 10**-6 - for i in range(max_iterations): + iteration = 0 + while abs(factor - prev_factor) > convergence_threshold: + iteration += 1 + if iteration > max_iterations: + msg = 'Rescale did not converge within max_iterations' + raise ConvergenceError(msg) prev_factor = factor factor = _single_rescale(irrad, modeled_irrad, factor) - delta = abs(factor - prev_factor) - if delta < convergence_threshold: - break - if delta >= convergence_threshold: - msg = 'Rescale did not converge within max_iterations' - raise ConvergenceError(msg) - else: - return factor * modeled_irrad + return factor * modeled_irrad elif method == 'single_opt': def _rmse(fact): diff --git a/rdtools/test/irradiance_rescale_test.py b/rdtools/test/irradiance_rescale_test.py new file mode 100644 index 00000000..b065dde8 --- /dev/null +++ b/rdtools/test/irradiance_rescale_test.py @@ -0,0 +1,68 @@ +import pandas as pd +from pandas.testing import assert_series_equal +from rdtools import irradiance_rescale +from rdtools.normalization import ConvergenceError +import pytest + + +@pytest.fixture +def simple_irradiance(): + times = pd.date_range('2019-06-01 12:00', freq='15T', periods=5) + time_series = pd.Series([1, 2, 3, 4, 5], index=times, dtype=float) + return time_series + + +@pytest.mark.parametrize("method", ['iterative', 'single_opt']) +def test_rescale(method, simple_irradiance): + # test basic functionality + modeled = simple_irradiance + measured = 1.05 * simple_irradiance + rescaled = irradiance_rescale(measured, modeled, method=method) + expected = measured + assert_series_equal(rescaled, expected, check_exact=False) + + +def test_max_iterations(simple_irradiance): + # use iterative method without enough iterations to converge + measured = simple_irradiance * 100 # method expects irrad > 200 + modeled = measured.copy() + modeled.iloc[2] *= 1.1 + modeled.iloc[3] *= 1.3 + modeled.iloc[4] *= 0.8 + + with pytest.raises(ConvergenceError): + _ = irradiance_rescale(measured, modeled, method='iterative', + max_iterations=2) + + _ = irradiance_rescale(measured, modeled, method='iterative', + max_iterations=10) + + +def test_max_iterations_zero(simple_irradiance): + # zero is sort of a special case, test it separately + + # test series already close enough + true_factor = 1.0 + 1e-8 + rescaled = irradiance_rescale(simple_irradiance, + simple_irradiance * true_factor, + max_iterations=0, + method='iterative') + assert_series_equal(rescaled, simple_irradiance, check_exact=False) + + # tighten threshold so that it isn't already close enough + with pytest.raises(ConvergenceError): + _ = irradiance_rescale(simple_irradiance, + simple_irradiance * true_factor, + max_iterations=0, + convergence_threshold=1e-9, + method='iterative') + + +def test_convergence_threshold(simple_irradiance): + # can't converge if threshold is negative + with pytest.raises(ConvergenceError): + _ = irradiance_rescale(simple_irradiance, + simple_irradiance * 1.05, + max_iterations=5, # reduced count for speed + convergence_threshold=-1, + method='iterative')