text: # -*- coding: utf-8 -*-
  """Tests for pybaselines.whittaker.
  
  @author: Donald Erb
  Created on March 20, 2021
  
  """
  
  from unittest import mock
  
  import numpy as np
  from numpy.testing import assert_allclose
  import pytest
  
  from pybaselines import whittaker
  from pybaselines.utils import ParameterWarning
  
  from .conftest import AlgorithmTester, get_data, has_pentapy
  
  
  class TestAsLS(AlgorithmTester):
      """Class for testing asls baseline."""
  
      func = whittaker.asls
  
      def test_unchanged_data(self, data_fixture):
          """Ensures that input data is unchanged by the function."""
          x, y = get_data()
          self._test_unchanged_data(data_fixture, y, None, y)
  
      def test_output(self):
          """Ensures that the output has the desired format."""
          self._test_output(self.y, self.y, checked_keys=('weights', 'tol_history'))
  
      def test_list_input(self):
          """Ensures that function works the same for both array and list inputs."""
          y_list = self.y.tolist()
          self._test_algorithm_list(array_args=(self.y,), list_args=(y_list,))
  
      @pytest.mark.parametrize('p', (-1, 2))
      def test_outside_p_fails(self, p):
          """Ensures p values outside of [0, 1] raise an exception."""
          with pytest.raises(ValueError):
              self._call_func(self.y, p=p)
  
      @pytest.mark.parametrize('diff_order', (1, 3))
      def test_diff_orders(self, diff_order):
          """Ensure that other difference orders work."""
          lam = {1: 1e2, 3: 1e10}[diff_order]
          self._call_func(self.y, lam=lam, diff_order=diff_order)
  
      @has_pentapy
      def test_pentapy_solver(self):
          """Ensure pentapy solver gives similar result to SciPy's solver."""
          with mock.patch.object(whittaker, '_HAS_PENTAPY', False):
              scipy_output = self._call_func(self.y)[0]
          pentapy_output = self._call_func(self.y)[0]
  
          assert_allclose(pentapy_output, scipy_output, 1e-4)
  
      def test_tol_history(self):
          """Ensures the 'tol_history' item in the parameter output is correct."""
          max_iter = 5
          _, params = self._call_func(self.y, max_iter=max_iter, tol=-1)
  
          assert params['tol_history'].size == max_iter + 1
  
  
  class TestIAsLS(AlgorithmTester):
      """Class for testing iasls baseline."""
  
      func = whittaker.iasls
  
      def test_unchanged_data(self, data_fixture):
          """Ensures that input data is unchanged by the function."""
          x, y = get_data()
          self._test_unchanged_data(data_fixture, y, x, y, x)
  
      def test_no_x(self):
          """Ensures that function output is the same when no x is input."""
          self._test_algorithm_no_x(with_args=(self.y, self.x), without_args=(self.y,))
  
      def test_output(self):
          """Ensures that the output has the desired format."""
          self._test_output(self.y, self.y, checked_keys=('weights', 'tol_history'))
  
      def test_list_input(self):
          """Ensures that function works the same for both array and list inputs."""
          y_list = self.y.tolist()
          self._test_algorithm_list(array_args=(self.y,), list_args=(y_list,))
  
      @pytest.mark.parametrize('p', (-1, 2))
      def test_outside_p_fails(self, p):
          """Ensures p values outside of [0, 1] raise an exception."""
          with pytest.raises(ValueError):
              self._call_func(self.y, p=p)
  
      @has_pentapy
      def test_pentapy_solver(self):
          """Ensure pentapy solver gives similar result to SciPy's solver."""
          with mock.patch.object(whittaker, '_HAS_PENTAPY', False):
              scipy_output = self._call_func(self.y)[0]
          pentapy_output = self._call_func(self.y)[0]
  
          assert_allclose(pentapy_output, scipy_output, 1e-4)
  
      def test_tol_history(self):
          """Ensures the 'tol_history' item in the parameter output is correct."""
          max_iter = 5
          _, params = self._call_func(self.y, max_iter=max_iter, tol=-1)
  
          assert params['tol_history'].size == max_iter + 1
  
  
  class TestAirPLS(AlgorithmTester):
      """Class for testing airpls baseline."""
  
      func = whittaker.airpls
  
      def test_unchanged_data(self, data_fixture):
          """Ensures that input data is unchanged by the function."""
          x, y = get_data()
          self._test_unchanged_data(data_fixture, y, None, y)
  
      def test_output(self):
          """Ensures that the output has the desired format."""
          self._test_output(self.y, self.y, checked_keys=('weights', 'tol_history'))
  
      def test_list_input(self):
          """Ensures that function works the same for both array and list inputs."""
          y_list = self.y.tolist()
          self._test_algorithm_list(array_args=(self.y,), list_args=(y_list,))
  
      @pytest.mark.parametrize('diff_order', (1, 3))
      def test_diff_orders(self, diff_order):
          """Ensure that other difference orders work."""
          lam = {1: 1e3, 3: 1e10}[diff_order]
          self._call_func(self.y, lam=lam, diff_order=diff_order)
  
      @has_pentapy
      def test_pentapy_solver(self):
          """Ensure pentapy solver gives similar result to SciPy's solver."""
          with mock.patch.object(whittaker, '_HAS_PENTAPY', False):
              scipy_output = self._call_func(self.y)[0]
          pentapy_output = self._call_func(self.y)[0]
  
          assert_allclose(pentapy_output, scipy_output, 1e-4)
  
      # ignore the RuntimeWarning that occurs from using +/- inf or nan
      @pytest.mark.filterwarnings('ignore::RuntimeWarning')
      def test_avoid_nonfinite_weights(self, no_noise_data_fixture):
          """
          Ensures that the function gracefully exits when errors occur.
  
          When there are no negative residuals, which occurs when a low tol value is used with
          a high max_iter value, the weighting function would produce values all ~0, which
          can fail the solvers. The returned baseline should be the last iteration that was
          successful, and thus should not contain nan or +/- inf.
  
          Use data without noise since the lack of noise makes it easier to induce failure.
          Set tol to -1 so that it is never reached, and set max_iter to a high value.
          Uses np.isfinite on the dot product of the baseline since the dot product is fast,
          would propogate the nan or inf, and will create only a single value to check
          for finite-ness.
  
          """
          y, x = no_noise_data_fixture
          with pytest.warns(ParameterWarning):
              baseline = self._call_func(y, tol=-1, max_iter=3000)[0]
  
          assert np.isfinite(baseline.dot(baseline))
  
      def test_tol_history(self):
          """Ensures the 'tol_history' item in the parameter output is correct."""
          max_iter = 5
          _, params = self._call_func(self.y, max_iter=max_iter, tol=-1)
  
          assert params['tol_history'].size == max_iter + 1
  
  
  class TestArPLS(AlgorithmTester):
      """Class for testing arpls baseline."""
  
      func = whittaker.arpls
  
      def test_unchanged_data(self, data_fixture):
          """Ensures that input data is unchanged by the function."""
          x, y = get_data()
          self._test_unchanged_data(data_fixture, y, None, y)
  
      def test_output(self):
          """Ensures that the output has the desired format."""
          self._test_output(self.y, self.y, checked_keys=('weights', 'tol_history'))
  
      def test_list_input(self):
          """Ensures that function works the same for both array and list inputs."""
          y_list = self.y.tolist()
          self._test_algorithm_list(array_args=(self.y,), list_args=(y_list,))
  
      @pytest.mark.parametrize('diff_order', (1, 3))
      def test_diff_orders(self, diff_order):
          """Ensure that other difference orders work."""
          lam = {1: 1e2, 3: 1e10}[diff_order]
          self._call_func(self.y, lam=lam, diff_order=diff_order)
  
      @has_pentapy
      def test_pentapy_solver(self):
          """Ensure pentapy solver gives similar result to SciPy's solver."""
          with mock.patch.object(whittaker, '_HAS_PENTAPY', False):
              scipy_output = self._call_func(self.y)[0]
          pentapy_output = self._call_func(self.y)[0]
  
          assert_allclose(pentapy_output, scipy_output, 1e-4)
  
      def test_tol_history(self):
          """Ensures the 'tol_history' item in the parameter output is correct."""
          max_iter = 5
          _, params = self._call_func(self.y, max_iter=max_iter, tol=-1)
  
          assert params['tol_history'].size == max_iter + 1
  
      def test_avoid_overflow_warning(self, no_noise_data_fixture):
          """
          Ensures no warning is emitted for exponential overflow.
  
          The weighting is 1 / (1 + exp(values)), so if values is too high,
          exp(values) is inf, which should usually emit an overflow warning.
          However, the resulting weight is 0, which is fine, so the warning is
          not needed and should be avoided. This test ensures the overflow warning
          is not emitted, and also ensures that the output is all finite, just in
          case the weighting was not actually stable.
  
          """
          y, x = no_noise_data_fixture
          with np.errstate(over='raise'):
              baseline = self._call_func(y, tol=-1, max_iter=1000)[0]
  
          assert np.isfinite(baseline.dot(baseline))
  
  
  class TestDrPLS(AlgorithmTester):
      """Class for testing drpls baseline."""
  
      func = whittaker.drpls
  
      def test_unchanged_data(self, data_fixture):
          """Ensures that input data is unchanged by the function."""
          x, y = get_data()
          self._test_unchanged_data(data_fixture, y, None, y)
  
      def test_output(self):
          """Ensures that the output has the desired format."""
          self._test_output(self.y, self.y, checked_keys=('weights', 'tol_history'))
  
      def test_list_input(self):
          """Ensures that function works the same for both array and list inputs."""
          y_list = self.y.tolist()
          self._test_algorithm_list(array_args=(self.y,), list_args=(y_list,))
  
      @has_pentapy
      def test_pentapy_solver(self):
          """Ensure pentapy solver gives similar result to SciPy's solver."""
          with mock.patch.object(whittaker, '_HAS_PENTAPY', False):
              scipy_output = self._call_func(self.y)[0]
          pentapy_output = self._call_func(self.y)[0]
  
          assert_allclose(pentapy_output, scipy_output, 1e-4)
  
      # ignore the RuntimeWarning that occurs from using +/- inf or nan
      @pytest.mark.filterwarnings('ignore::RuntimeWarning')
      def test_avoid_nonfinite_weights(self, no_noise_data_fixture):
          """
          Ensures that the function gracefully exits when non-finite weights are created.
  
          When there are no negative residuals or exp(iterations) / std is very high, both
          of which occur when a low tol value is used with a high max_iter value, the
          weighting function would produce non-finite values. The returned baseline should
          be the last iteration that was successful, and thus should not contain nan or +/- inf.
  
          Use data without noise since the lack of noise makes it easier to induce failure.
          Set tol to -1 so that it is never reached, and set max_iter to a high value.
          Uses np.isfinite on the dot product of the baseline since the dot product is fast,
          would propogate the nan or inf, and will create only a single value to check
          for finite-ness.
  
          """
          y, x = no_noise_data_fixture
          with pytest.warns(ParameterWarning):
              baseline, params = self._call_func(y, tol=-1, max_iter=1000)
  
          assert np.isfinite(baseline.dot(baseline))
          # ensure last tolerence calculation was non-finite as a double-check that
          # this test is actually doing what it should be doing
          assert not np.isfinite(params['tol_history'][-1])
  
      def test_tol_history(self):
          """Ensures the 'tol_history' item in the parameter output is correct."""
          max_iter = 5
          _, params = self._call_func(self.y, max_iter=max_iter, tol=-1)
  
          assert params['tol_history'].size == max_iter + 1
  
  
  class TestIArPLS(AlgorithmTester):
      """Class for testing iarpls baseline."""
  
      func = whittaker.iarpls
  
      def test_unchanged_data(self, data_fixture):
          """Ensures that input data is unchanged by the function."""
          x, y = get_data()
          self._test_unchanged_data(data_fixture, y, None, y)
  
      def test_output(self):
          """Ensures that the output has the desired format."""
          self._test_output(self.y, self.y, checked_keys=('weights', 'tol_history'))
  
      def test_list_input(self):
          """Ensures that function works the same for both array and list inputs."""
          y_list = self.y.tolist()
          self._test_algorithm_list(array_args=(self.y,), list_args=(y_list,))
  
      @pytest.mark.parametrize('diff_order', (1, 3))
      def test_diff_orders(self, diff_order):
          """Ensure that other difference orders work."""
          lam = {1: 1e2, 3: 1e10}[diff_order]
          self._call_func(self.y, lam=lam, diff_order=diff_order)
  
      @has_pentapy
      def test_pentapy_solver(self):
          """Ensure pentapy solver gives similar result to SciPy's solver."""
          with mock.patch.object(whittaker, '_HAS_PENTAPY', False):
              scipy_output = self._call_func(self.y)[0]
          pentapy_output = self._call_func(self.y)[0]
  
          assert_allclose(pentapy_output, scipy_output, 1e-4)
  
      # ignore the RuntimeWarning that occurs from using +/- inf or nan
      @pytest.mark.filterwarnings('ignore::RuntimeWarning')
      def test_avoid_nonfinite_weights(self, no_noise_data_fixture):
          """
          Ensures that the function gracefully exits when non-finite weights are created.
  
          When there are no negative residuals or exp(iterations) / std is very high, both
          of which occur when a low tol value is used with a high max_iter value, the
          weighting function would produce non-finite values. The returned baseline should
          be the last iteration that was successful, and thus should not contain nan or +/- inf.
  
          Use data without noise since the lack of noise makes it easier to induce failure.
          Set tol to -1 so that it is never reached, and set max_iter to a high value.
          Uses np.isfinite on the dot product of the baseline since the dot product is fast,
          would propogate the nan or inf, and will create only a single value to check
          for finite-ness.
  
          """
          y, x = no_noise_data_fixture
          with pytest.warns(ParameterWarning):
              baseline, params = self._call_func(y, tol=-1, max_iter=1000)
  
          assert np.isfinite(baseline.dot(baseline))
          # ensure last tolerence calculation was non-finite as a double-check that
          # this test is actually doing what it should be doing
          assert not np.isfinite(params['tol_history'][-1])
  
      def test_tol_history(self):
          """Ensures the 'tol_history' item in the parameter output is correct."""
          max_iter = 5
          _, params = self._call_func(self.y, max_iter=max_iter, tol=-1)
  
          assert params['tol_history'].size == max_iter + 1
  
  
  class TestAsPLS(AlgorithmTester):
      """Class for testing aspls baseline."""
  
      func = whittaker.aspls
  
      def test_unchanged_data(self, data_fixture):
          """Ensures that input data is unchanged by the function."""
          x, y = get_data()
          self._test_unchanged_data(data_fixture, y, None, y)
  
      def test_output(self):
          """Ensures that the output has the desired format."""
          self._test_output(
              self.y, self.y, checked_keys=('weights', 'alpha', 'tol_history')
          )
  
      def test_list_input(self):
          """Ensures that function works the same for both array and list inputs."""
          y_list = self.y.tolist()
          self._test_algorithm_list(array_args=(self.y,), list_args=(y_list,))
  
      def test_wrong_alpha_shape(self):
          """Ensures that an exception is raised if input alpha and data are different shapes."""
          alpha = np.ones(self.y.shape[0] + 1)
          with pytest.raises(ValueError):
              self._call_func(self.y, alpha=alpha)
  
      @pytest.mark.parametrize('diff_order', (1, 3))
      def test_diff_orders(self, diff_order):
          """Ensure that other difference orders work."""
          lam = {1: 1e4, 3: 1e10}[diff_order]
          self._call_func(self.y, lam=lam, diff_order=diff_order)
  
      @has_pentapy
      def test_pentapy_solver(self):
          """Ensure pentapy solver gives similar result to SciPy's solver."""
          with mock.patch.object(whittaker, '_HAS_PENTAPY', False):
              scipy_output = self._call_func(self.y)[0]
          pentapy_output = self._call_func(self.y)[0]
  
          assert_allclose(pentapy_output, scipy_output, 1e-4)
  
      def test_tol_history(self):
          """Ensures the 'tol_history' item in the parameter output is correct."""
          max_iter = 5
          _, params = self._call_func(self.y, max_iter=max_iter, tol=-1)
  
          assert params['tol_history'].size == max_iter + 1
  
      def test_avoid_overflow_warning(self, no_noise_data_fixture):
          """
          Ensures no warning is emitted for exponential overflow.
  
          The weighting is 1 / (1 + exp(values)), so if values is too high,
          exp(values) is inf, which should usually emit an overflow warning.
          However, the resulting weight is 0, which is fine, so the warning is
          not needed and should be avoided. This test ensures the overflow warning
          is not emitted, and also ensures that the output is all finite, just in
          case the weighting was not actually stable.
  
          """
          y, x = no_noise_data_fixture
          with np.errstate(over='raise'):
              baseline = self._call_func(y, tol=-1, max_iter=1000)[0]
  
          assert np.isfinite(baseline.dot(baseline))
  
  
  class TestPsalsa(AlgorithmTester):
      """Class for testing psalsa baseline."""
  
      func = whittaker.psalsa
  
      def test_unchanged_data(self, data_fixture):
          """Ensures that input data is unchanged by the function."""
          x, y = get_data()
          self._test_unchanged_data(data_fixture, y, None, y)
  
      def test_output(self):
          """Ensures that the output has the desired format."""
          self._test_output(self.y, self.y, checked_keys=('weights', 'tol_history'))
  
      def test_list_input(self):
          """Ensures that function works the same for both array and list inputs."""
          y_list = self.y.tolist()
          self._test_algorithm_list(array_args=(self.y,), list_args=(y_list,))
  
      @pytest.mark.parametrize('p', (-1, 2))
      def test_outside_p_fails(self, p):
          """Ensures p values outside of [0, 1] raise an exception."""
          with pytest.raises(ValueError):
              self._call_func(self.y, p=p)
  
      @pytest.mark.parametrize('diff_order', (1, 3))
      def test_diff_orders(self, diff_order):
          """Ensure that other difference orders work."""
          lam = {1: 1e2, 3: 1e10}[diff_order]
          self._call_func(self.y, lam=lam, diff_order=diff_order)
  
      @has_pentapy
      def test_pentapy_solver(self):
          """Ensure pentapy solver gives similar result to SciPy's solver."""
          with mock.patch.object(whittaker, '_HAS_PENTAPY', False):
              scipy_output = self._call_func(self.y)[0]
          pentapy_output = self._call_func(self.y)[0]
  
          assert_allclose(pentapy_output, scipy_output, 1e-4)
  
      def test_tol_history(self):
          """Ensures the 'tol_history' item in the parameter output is correct."""
          max_iter = 5
          _, params = self._call_func(self.y, max_iter=max_iter, tol=-1)
  
          assert params['tol_history'].size == max_iter + 1
  
  
  class TestDerpsalsa(AlgorithmTester):
      """Class for testing derpsalsa baseline."""
  
      func = whittaker.derpsalsa
  
      def test_unchanged_data(self, data_fixture):
          """Ensures that input data is unchanged by the function."""
          x, y = get_data()
          self._test_unchanged_data(data_fixture, y, None, y)
  
      def test_output(self):
          """Ensures that the output has the desired format."""
          self._test_output(self.y, self.y, checked_keys=('weights', 'tol_history'))
  
      def test_list_input(self):
          """Ensures that function works the same for both array and list inputs."""
          y_list = self.y.tolist()
          self._test_algorithm_list(array_args=(self.y,), list_args=(y_list,))
  
      @pytest.mark.parametrize('p', (-1, 2))
      def test_outside_p_fails(self, p):
          """Ensures p values outside of [0, 1] raise an exception."""
          with pytest.raises(ValueError):
              self._call_func(self.y, p=p)
  
      @pytest.mark.parametrize('diff_order', (1, 3))
      def test_diff_orders(self, diff_order):
          """Ensure that other difference orders work."""
          lam = {1: 1e2, 3: 1e10}[diff_order]
          self._call_func(self.y, lam=lam, diff_order=diff_order)
  
      @has_pentapy
      def test_pentapy_solver(self):
          """Ensure pentapy solver gives similar result to SciPy's solver."""
          with mock.patch.object(whittaker, '_HAS_PENTAPY', False):
              scipy_output = self._call_func(self.y)[0]
          pentapy_output = self._call_func(self.y)[0]
  
          assert_allclose(pentapy_output, scipy_output, 1e-4)
  
      def test_tol_history(self):
          """Ensures the 'tol_history' item in the parameter output is correct."""
          max_iter = 5
          _, params = self._call_func(self.y, max_iter=max_iter, tol=-1)
  
          assert params['tol_history'].size == max_iter + 1
  