Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/sphinx/source/changelog/v2.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------
Expand Down
41 changes: 19 additions & 22 deletions rdtools/normalization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -319,26 +320,24 @@ 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
-------
pd.Series
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):
"""
Expand All @@ -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):
Expand Down
68 changes: 68 additions & 0 deletions rdtools/test/irradiance_rescale_test.py
Original file line number Diff line number Diff line change
@@ -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')