def test_analytic_gradient_matches_numerical_gradient_with_zero_delta(
            self):
        """Test analytical gradient matches finite difference approximation."""

        random_seed = 0
        random_state = check_random_state(random_seed)

        n_features = 3
        n_components = 5
        n_samples = 10

        X = random_state.uniform(size=(n_samples, n_features))
        K = X.dot(X.T)

        C = right_stochastic_matrix((n_components, n_samples),
                                    random_state=random_state)
        S = right_stochastic_matrix((n_samples, n_components),
                                    random_state=random_state)

        self.assertTrue(np.allclose(C.sum(axis=1), 1, 1e-12))
        self.assertTrue(np.allclose(S.sum(axis=1), 1, 1e-12))

        delta = 0
        aa = KernelAA(n_components=n_components, delta=delta)

        aa.K = K
        aa.C = C
        aa.S = S

        aa._initialize_workspace()
        aa._update_weights_gradient()

        analytic_grad_S = aa.grad_S

        def central_difference_deriv(i, j, h=1e-4):
            old_x = S[i, j]

            xmh = old_x - h
            aa.S[i, j] = xmh
            aa._initialize_workspace()
            fmh = aa._evaluate_cost()

            xph = old_x + h
            aa.S[i, j] = xph
            aa._initialize_workspace()
            fph = aa._evaluate_cost()

            aa.S[i, j] = old_x

            return (fph - fmh) / (2 * h)

        numeric_grad_S = np.zeros((n_samples, n_components))
        for i in range(n_samples):
            for j in range(n_components):
                numeric_grad_S[i, j] = central_difference_deriv(i, j)

        self.assertTrue(np.allclose(analytic_grad_S, numeric_grad_S, 1e-4))
    def test_repeated_updates_converge_with_nonzero_delta(self):
        """Test repeated updates converge to a fixed point with non-zero delta."""

        random_seed = 0
        random_state = check_random_state(random_seed)

        n_features = 30
        n_components = 11
        n_samples = 320
        max_iter = 1000
        tolerance = 1e-4

        X = random_state.uniform(size=(n_samples, n_features))
        K = X.dot(X.T)

        C = right_stochastic_matrix((n_components, n_samples),
                                    random_state=random_state)
        S = right_stochastic_matrix((n_samples, n_components),
                                    random_state=random_state)

        self.assertTrue(np.allclose(C.sum(axis=1), 1, tolerance))
        self.assertTrue(np.allclose(S.sum(axis=1), 1, tolerance))

        delta = 3.2
        aa = KernelAA(n_components=n_components, delta=delta)

        aa.K = K
        aa.C = C
        aa.S = S

        aa._initialize_workspace()

        cost_delta = 1 + tolerance
        old_cost = aa._evaluate_cost()
        new_cost = old_cost
        n_iter = 0
        while abs(cost_delta) > tolerance and n_iter < max_iter:
            old_cost = new_cost
            error = aa._update_dictionary()
            self.assertEqual(error, 0)
            new_cost = aa._evaluate_cost()

            cost_delta = new_cost - old_cost

            self.assertTrue(cost_delta <= 0)

            n_iter += 1

        self.assertTrue(n_iter < max_iter)

        updated_C = aa.C
        self.assertTrue(np.allclose(updated_C.sum(axis=1), 1, 1e-12))

        updated_alpha = aa.alpha
        for i in range(n_components):
            self.assertTrue(1 - delta <= updated_alpha[i] <= 1 + delta)
    def test_repeated_updates_converge_with_zero_delta(self):
        """Test repeated updates converge to a fixed point with delta = 0."""

        random_seed = 0
        random_state = check_random_state(random_seed)

        n_features = 10
        n_components = 3
        n_samples = 600
        max_iter = 100
        tolerance = 1e-6

        X = random_state.uniform(size=(n_samples, n_features))
        K = X.dot(X.T)

        C = right_stochastic_matrix((n_components, n_samples),
                                    random_state=random_state)
        S = right_stochastic_matrix((n_samples, n_components),
                                    random_state=random_state)

        self.assertTrue(np.allclose(C.sum(axis=1), 1, tolerance))
        self.assertTrue(np.allclose(S.sum(axis=1), 1, tolerance))

        delta = 0
        aa = KernelAA(n_components=n_components, delta=delta)

        aa.K = K
        aa.C = C
        aa.S = S

        aa._initialize_workspace()

        cost_delta = 1 + tolerance
        old_cost = aa._evaluate_cost()
        new_cost = old_cost
        n_iter = 0
        while abs(cost_delta) > tolerance and n_iter < max_iter:
            old_cost = new_cost
            error = aa._update_weights()
            self.assertEqual(error, 0)
            new_cost = aa._evaluate_cost()

            cost_delta = new_cost - old_cost

            self.assertTrue(cost_delta <= 0)

            n_iter += 1

        self.assertTrue(n_iter < max_iter)

        updated_S = aa.S
        self.assertTrue(np.allclose(updated_S.sum(axis=1), 1, 1e-12))
    def test_single_dictionary_update_reduces_cost_with_nonzero_delta(self):
        """Test single update step reduces cost function."""

        random_seed = 0
        random_state = check_random_state(random_seed)

        n_features = 10
        n_components = 5
        n_samples = 400

        X = random_state.uniform(size=(n_samples, n_features))
        K = X.dot(X.T)

        C = right_stochastic_matrix((n_components, n_samples),
                                    random_state=random_state)
        S = right_stochastic_matrix((n_samples, n_components),
                                    random_state=random_state)

        self.assertTrue(np.allclose(C.sum(axis=1), 1, 1e-12))
        self.assertTrue(np.allclose(S.sum(axis=1), 1, 1e-12))

        delta = 1.2
        aa = KernelAA(n_components=n_components, delta=delta)

        aa.K = K
        aa.C = C
        aa.S = S

        aa._initialize_workspace()

        initial_cost = aa._evaluate_cost()

        error = aa._update_dictionary()

        self.assertEqual(error, 0)

        final_cost = aa._evaluate_cost()

        self.assertTrue(final_cost <= initial_cost)

        updated_C = aa.C

        self.assertTrue(np.allclose(updated_C.sum(axis=1), 1, 1e-12))
    def test_exact_solution_with_zero_delta_is_fixed_point(self):
        """Test exact solution for weights is fixed point of update step."""

        random_seed = 0
        random_state = check_random_state(random_seed)

        n_features = 30
        n_components = 10
        n_samples = 130
        tolerance = 1e-12

        basis = random_state.uniform(size=(n_components, n_features))

        S = right_stochastic_matrix((n_samples, n_components),
                                    random_state=random_state)

        archetype_indices = np.zeros(n_components, dtype='i8')
        for i in range(n_components):
            new_index = False
            current_index = 0

            while not new_index:
                new_index = True

                current_index = random_state.randint(low=0, high=n_samples)

                for index in archetype_indices:
                    if current_index == index:
                        new_index = False

            archetype_indices[i] = current_index

        C = np.zeros((n_components, n_samples))
        component = 0
        for index in archetype_indices:
            C[component, index] = 1.0
            for i in range(n_components):
                if i == component:
                    S[index, i] = 1.0
                else:
                    S[index, i] = 0.0
            component += 1

        X = S.dot(basis)
        basis_projection = C.dot(X)

        self.assertTrue(np.allclose(basis_projection, basis, tolerance))
        self.assertTrue(np.linalg.norm(X - S.dot(C.dot(X))) < tolerance)

        K = X.dot(X.T)

        delta = 0
        aa = KernelAA(n_components=n_components, delta=delta)

        aa.K = K
        aa.C = C
        aa.S = S

        aa._initialize_workspace()

        initial_cost = aa._evaluate_cost()

        error = aa._update_weights()

        self.assertEqual(error, 0)

        final_cost = aa._evaluate_cost()

        self.assertTrue(abs(final_cost - initial_cost) < tolerance)

        updated_S = aa.S

        self.assertTrue(np.allclose(updated_S.sum(axis=1), 1, 1e-12))
        self.assertTrue(np.allclose(updated_S, S, tolerance))