mirror of
https://gitlab.com/libeigen/eigen.git
synced 2025-04-24 02:29:33 +08:00
473 lines
15 KiB
C++
473 lines
15 KiB
C++
// This file is part of Eigen, a lightweight C++ template library
|
|
// for linear algebra.
|
|
//
|
|
// Copyright (C) Essex Edwards <essex.edwards@gmail.com>
|
|
//
|
|
// This Source Code Form is subject to the terms of the Mozilla
|
|
// Public License v. 2.0. If a copy of the MPL was not distributed
|
|
// with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
#define EIGEN_RUNTIME_NO_MALLOC
|
|
|
|
#include "main.h"
|
|
#include <unsupported/Eigen/NNLS>
|
|
|
|
/// Check that 'x' solves the NNLS optimization problem `min ||A*x-b|| s.t. 0 <= x`.
|
|
/// The \p tolerance parameter is the absolute tolerance on the gradient, A'*(A*x-b).
|
|
template <typename MatrixType, typename VectorB, typename VectorX, typename Scalar>
|
|
static void verify_nnls_optimality(const MatrixType &A, const VectorB &b, const VectorX &x, const Scalar tolerance) {
|
|
// The NNLS optimality conditions are:
|
|
//
|
|
// * 0 = A'*A*x - A'*b - lambda
|
|
// * 0 <= x[i] \forall i
|
|
// * 0 <= lambda[i] \forall i
|
|
// * 0 = x[i]*lambda[i] \forall i
|
|
//
|
|
// we don't know lambda, but by assuming the first optimality condition is true,
|
|
// we can derive it and then check the others conditions.
|
|
const VectorX lambda = A.transpose() * (A * x - b);
|
|
|
|
// NNLS solutions are EXACTLY not negative.
|
|
VERIFY_LE(0, x.minCoeff());
|
|
|
|
// Exact lambda would be non-negative, but computed lambda might leak a little
|
|
VERIFY_LE(-tolerance, lambda.minCoeff());
|
|
|
|
// x[i]*lambda[i] == 0 <~~> (x[i]==0) || (lambda[i] is small)
|
|
VERIFY(((x.array() == Scalar(0)) || (lambda.array() <= tolerance)).all());
|
|
}
|
|
|
|
template <typename MatrixType, typename VectorB, typename VectorX>
|
|
static void test_nnls_known_solution(const MatrixType &A, const VectorB &b, const VectorX &x_expected) {
|
|
using Scalar = typename MatrixType::Scalar;
|
|
|
|
using std::sqrt;
|
|
const Scalar tolerance = sqrt(Eigen::GenericNumTraits<Scalar>::epsilon());
|
|
Index max_iter = 5 * A.cols(); // A heuristic guess.
|
|
NNLS<MatrixType> nnls(A, max_iter, tolerance);
|
|
const VectorX x = nnls.solve(b);
|
|
|
|
VERIFY_IS_EQUAL(nnls.info(), ComputationInfo::Success);
|
|
VERIFY_IS_APPROX(x, x_expected);
|
|
verify_nnls_optimality(A, b, x, tolerance);
|
|
}
|
|
|
|
template <typename MatrixType>
|
|
static void test_nnls_random_problem() {
|
|
//
|
|
// SETUP
|
|
//
|
|
|
|
Index cols = MatrixType::ColsAtCompileTime;
|
|
if (cols == Dynamic) cols = internal::random<Index>(1, EIGEN_TEST_MAX_SIZE);
|
|
Index rows = MatrixType::RowsAtCompileTime;
|
|
if (rows == Dynamic) rows = internal::random<Index>(cols, EIGEN_TEST_MAX_SIZE);
|
|
VERIFY_LE(cols, rows); // To have a unique LS solution: cols <= rows.
|
|
|
|
// Make some sort of random test problem from a wide range of scales and condition numbers.
|
|
using std::pow;
|
|
using Scalar = typename MatrixType::Scalar;
|
|
const Scalar sqrtConditionNumber = pow(Scalar(10), internal::random<Scalar>(Scalar(0), Scalar(2)));
|
|
const Scalar scaleA = pow(Scalar(10), internal::random<Scalar>(Scalar(-3), Scalar(3)));
|
|
const Scalar minSingularValue = scaleA / sqrtConditionNumber;
|
|
const Scalar maxSingularValue = scaleA * sqrtConditionNumber;
|
|
MatrixType A(rows, cols);
|
|
generateRandomMatrixSvs(setupRangeSvs<Matrix<Scalar, Dynamic, 1>>(cols, minSingularValue, maxSingularValue), rows,
|
|
cols, A);
|
|
|
|
// Make a random RHS also with a random scaling.
|
|
using VectorB = decltype(A.col(0).eval());
|
|
const Scalar scaleB = pow(Scalar(10), internal::random<Scalar>(Scalar(-3), Scalar(3)));
|
|
const VectorB b = scaleB * VectorB::Random(A.rows());
|
|
|
|
//
|
|
// ACT
|
|
//
|
|
|
|
using Scalar = typename MatrixType::Scalar;
|
|
using std::sqrt;
|
|
const Scalar tolerance =
|
|
sqrt(Eigen::GenericNumTraits<Scalar>::epsilon()) * b.cwiseAbs().maxCoeff() * A.cwiseAbs().maxCoeff();
|
|
Index max_iter = 5 * A.cols(); // A heuristic guess.
|
|
NNLS<MatrixType> nnls(A, max_iter, tolerance);
|
|
const typename NNLS<MatrixType>::SolutionVectorType &x = nnls.solve(b);
|
|
|
|
//
|
|
// VERIFY
|
|
//
|
|
|
|
// In fact, NNLS can fail on some problems, but they are rare in practice.
|
|
VERIFY_IS_EQUAL(nnls.info(), ComputationInfo::Success);
|
|
verify_nnls_optimality(A, b, x, tolerance);
|
|
}
|
|
|
|
static void test_nnls_handles_zero_rhs() {
|
|
//
|
|
// SETUP
|
|
//
|
|
const Index cols = internal::random<Index>(1, EIGEN_TEST_MAX_SIZE);
|
|
const Index rows = internal::random<Index>(cols, EIGEN_TEST_MAX_SIZE);
|
|
const MatrixXd A = MatrixXd::Random(rows, cols);
|
|
const VectorXd b = VectorXd::Zero(rows);
|
|
|
|
//
|
|
// ACT
|
|
//
|
|
NNLS<MatrixXd> nnls(A);
|
|
const VectorXd x = nnls.solve(b);
|
|
|
|
//
|
|
// VERIFY
|
|
//
|
|
VERIFY_IS_EQUAL(nnls.info(), ComputationInfo::Success);
|
|
VERIFY_LE(nnls.iterations(), 1); // 0 or 1 would be be fine for an edge case like this.
|
|
VERIFY_IS_EQUAL(x, VectorXd::Zero(cols));
|
|
}
|
|
|
|
static void test_nnls_handles_Mx0_matrix() {
|
|
//
|
|
// SETUP
|
|
//
|
|
const Index rows = internal::random<Index>(1, EIGEN_TEST_MAX_SIZE);
|
|
const MatrixXd A(rows, 0);
|
|
const VectorXd b = VectorXd::Random(rows);
|
|
|
|
//
|
|
// ACT
|
|
//
|
|
NNLS<MatrixXd> nnls(A);
|
|
const VectorXd x = nnls.solve(b);
|
|
|
|
//
|
|
// VERIFY
|
|
//
|
|
VERIFY_IS_EQUAL(nnls.info(), ComputationInfo::Success);
|
|
VERIFY_LE(nnls.iterations(), 0);
|
|
VERIFY_IS_EQUAL(x.size(), 0);
|
|
}
|
|
|
|
static void test_nnls_handles_0x0_matrix() {
|
|
//
|
|
// SETUP
|
|
//
|
|
const MatrixXd A(0, 0);
|
|
const VectorXd b(0);
|
|
|
|
//
|
|
// ACT
|
|
//
|
|
NNLS<MatrixXd> nnls(A);
|
|
const VectorXd x = nnls.solve(b);
|
|
|
|
//
|
|
// VERIFY
|
|
//
|
|
VERIFY_IS_EQUAL(nnls.info(), ComputationInfo::Success);
|
|
VERIFY_LE(nnls.iterations(), 0);
|
|
VERIFY_IS_EQUAL(x.size(), 0);
|
|
}
|
|
|
|
static void test_nnls_handles_dependent_columns() {
|
|
//
|
|
// SETUP
|
|
//
|
|
const Index rank = internal::random<Index>(1, EIGEN_TEST_MAX_SIZE / 2);
|
|
const Index cols = 2 * rank;
|
|
const Index rows = internal::random<Index>(cols, EIGEN_TEST_MAX_SIZE);
|
|
const MatrixXd A = MatrixXd::Random(rows, rank) * MatrixXd::Random(rank, cols);
|
|
const VectorXd b = VectorXd::Random(rows);
|
|
|
|
//
|
|
// ACT
|
|
//
|
|
const double tolerance = 1e-8;
|
|
NNLS<MatrixXd> nnls(A);
|
|
const VectorXd &x = nnls.solve(b);
|
|
|
|
//
|
|
// VERIFY
|
|
//
|
|
// What should happen when the input 'A' has dependent columns?
|
|
// We might still succeed. Or we might not converge.
|
|
// Either outcome is fine. If Success is indicated,
|
|
// then 'x' must actually be a solution vector.
|
|
|
|
if (nnls.info() == ComputationInfo::Success) {
|
|
verify_nnls_optimality(A, b, x, tolerance);
|
|
}
|
|
}
|
|
|
|
static void test_nnls_handles_wide_matrix() {
|
|
//
|
|
// SETUP
|
|
//
|
|
const Index cols = internal::random<Index>(2, EIGEN_TEST_MAX_SIZE);
|
|
const Index rows = internal::random<Index>(2, cols - 1);
|
|
const MatrixXd A = MatrixXd::Random(rows, cols);
|
|
const VectorXd b = VectorXd::Random(rows);
|
|
|
|
//
|
|
// ACT
|
|
//
|
|
const double tolerance = 1e-8;
|
|
NNLS<MatrixXd> nnls(A);
|
|
const VectorXd &x = nnls.solve(b);
|
|
|
|
//
|
|
// VERIFY
|
|
//
|
|
// What should happen when the input 'A' is wide?
|
|
// The unconstrained least-squares problem has infinitely many solutions.
|
|
// Subject the the non-negativity constraints,
|
|
// the solution might actually be unique (e.g. it is [0,0,..,0]).
|
|
// So, NNLS might succeed or it might fail.
|
|
// Either outcome is fine. If Success is indicated,
|
|
// then 'x' must actually be a solution vector.
|
|
|
|
if (nnls.info() == ComputationInfo::Success) {
|
|
verify_nnls_optimality(A, b, x, tolerance);
|
|
}
|
|
}
|
|
|
|
// 4x2 problem, unconstrained solution positive
|
|
static void test_nnls_known_1() {
|
|
Matrix<double, 4, 2> A(4, 2);
|
|
Matrix<double, 4, 1> b(4);
|
|
Matrix<double, 2, 1> x(2);
|
|
A << 1, 1, 2, 4, 3, 9, 4, 16;
|
|
b << 0.6, 2.2, 4.8, 8.4;
|
|
x << 0.1, 0.5;
|
|
|
|
return test_nnls_known_solution(A, b, x);
|
|
}
|
|
|
|
// 4x3 problem, unconstrained solution positive
|
|
static void test_nnls_known_2() {
|
|
Matrix<double, 4, 3> A(4, 3);
|
|
Matrix<double, 4, 1> b(4);
|
|
Matrix<double, 3, 1> x(3);
|
|
|
|
A << 1, 1, 1, 2, 4, 8, 3, 9, 27, 4, 16, 64;
|
|
b << 0.73, 3.24, 8.31, 16.72;
|
|
x << 0.1, 0.5, 0.13;
|
|
|
|
test_nnls_known_solution(A, b, x);
|
|
}
|
|
|
|
// Simple 4x4 problem, unconstrained solution non-negative
|
|
static void test_nnls_known_3() {
|
|
Matrix<double, 4, 4> A(4, 4);
|
|
Matrix<double, 4, 1> b(4);
|
|
Matrix<double, 4, 1> x(4);
|
|
|
|
A << 1, 1, 1, 1, 2, 4, 8, 16, 3, 9, 27, 81, 4, 16, 64, 256;
|
|
b << 0.73, 3.24, 8.31, 16.72;
|
|
x << 0.1, 0.5, 0.13, 0;
|
|
|
|
test_nnls_known_solution(A, b, x);
|
|
}
|
|
|
|
// Simple 4x3 problem, unconstrained solution non-negative
|
|
static void test_nnls_known_4() {
|
|
Matrix<double, 4, 3> A(4, 3);
|
|
Matrix<double, 4, 1> b(4);
|
|
Matrix<double, 3, 1> x(3);
|
|
|
|
A << 1, 1, 1, 2, 4, 8, 3, 9, 27, 4, 16, 64;
|
|
b << 0.23, 1.24, 3.81, 8.72;
|
|
x << 0.1, 0, 0.13;
|
|
|
|
test_nnls_known_solution(A, b, x);
|
|
}
|
|
|
|
// Simple 4x3 problem, unconstrained solution indefinite
|
|
static void test_nnls_known_5() {
|
|
Matrix<double, 4, 3> A(4, 3);
|
|
Matrix<double, 4, 1> b(4);
|
|
Matrix<double, 3, 1> x(3);
|
|
|
|
A << 1, 1, 1, 2, 4, 8, 3, 9, 27, 4, 16, 64;
|
|
b << 0.13, 0.84, 2.91, 7.12;
|
|
// Solution obtained by original nnls() implementation in Fortran
|
|
x << 0.0, 0.0, 0.1106544;
|
|
|
|
test_nnls_known_solution(A, b, x);
|
|
}
|
|
|
|
static void test_nnls_small_reference_problems() {
|
|
test_nnls_known_1();
|
|
test_nnls_known_2();
|
|
test_nnls_known_3();
|
|
test_nnls_known_4();
|
|
test_nnls_known_5();
|
|
}
|
|
|
|
static void test_nnls_with_half_precision() {
|
|
// The random matrix generation tools don't work with `half`,
|
|
// so here's a simpler setup mostly just to check that NNLS compiles & runs with custom scalar types.
|
|
|
|
using Mat = Matrix<half, 8, 2>;
|
|
using VecB = Matrix<half, 8, 1>;
|
|
using VecX = Matrix<half, 2, 1>;
|
|
Mat A = Mat::Random(); // full-column rank with high probability.
|
|
VecB b = VecB::Random();
|
|
|
|
NNLS<Mat> nnls(A, 20, half(1e-2f));
|
|
const VecX x = nnls.solve(b);
|
|
|
|
VERIFY_IS_EQUAL(nnls.info(), ComputationInfo::Success);
|
|
verify_nnls_optimality(A, b, x, half(1e-1));
|
|
}
|
|
|
|
static void test_nnls_special_case_solves_in_zero_iterations() {
|
|
// The particular NNLS algorithm that is implemented starts with all variables
|
|
// in the active set.
|
|
// This test builds a system where all constraints are active at the solution,
|
|
// so that initial guess is already correct.
|
|
//
|
|
// If the implementation changes to another algorithm that does not have this property,
|
|
// then this test will need to change (e.g. starting from all constraints inactive,
|
|
// or using ADMM, or an interior point solver).
|
|
|
|
const Index n = 10;
|
|
const Index m = 3 * n;
|
|
const VectorXd b = VectorXd::Random(m);
|
|
// With high probability, this is full column rank, which we need for uniqueness.
|
|
MatrixXd A = MatrixXd::Random(m, n);
|
|
// Make every column of `A` such that adding it to the active set only /increases/ the objective,
|
|
// this ensuring the NNLS solution is all zeros.
|
|
const VectorXd alignment = -(A.transpose() * b).cwiseSign();
|
|
A = A * alignment.asDiagonal();
|
|
|
|
NNLS<MatrixXd> nnls(A);
|
|
nnls.solve(b);
|
|
|
|
VERIFY_IS_EQUAL(nnls.info(), ComputationInfo::Success);
|
|
VERIFY(nnls.iterations() == 0);
|
|
}
|
|
|
|
static void test_nnls_special_case_solves_in_n_iterations() {
|
|
// The particular NNLS algorithm that is implemented starts with all variables
|
|
// in the active set and then adds one variable to the inactive set each iteration.
|
|
// This test builds a system where all variables are inactive at the solution,
|
|
// so it should take 'n' iterations to get there.
|
|
//
|
|
// If the implementation changes to another algorithm that does not have this property,
|
|
// then this test will need to change (e.g. starting from all constraints inactive,
|
|
// or using ADMM, or an interior point solver).
|
|
|
|
const Index n = 10;
|
|
const Index m = 3 * n;
|
|
// With high probability, this is full column rank, which we need for uniqueness.
|
|
const MatrixXd A = MatrixXd::Random(m, n);
|
|
const VectorXd x = VectorXd::Random(n).cwiseAbs().array() + 1; // all positive.
|
|
const VectorXd b = A * x;
|
|
|
|
NNLS<MatrixXd> nnls(A);
|
|
nnls.solve(b);
|
|
|
|
VERIFY_IS_EQUAL(nnls.info(), ComputationInfo::Success);
|
|
VERIFY(nnls.iterations() == n);
|
|
}
|
|
|
|
static void test_nnls_returns_NoConvergence_when_maxIterations_is_too_low() {
|
|
// Using the special case that takes `n` iterations,
|
|
// from `test_nnls_special_case_solves_in_n_iterations`,
|
|
// we can set max iterations too low and that should cause the solve to fail.
|
|
|
|
const Index n = 10;
|
|
const Index m = 3 * n;
|
|
// With high probability, this is full column rank, which we need for uniqueness.
|
|
const MatrixXd A = MatrixXd::Random(m, n);
|
|
const VectorXd x = VectorXd::Random(n).cwiseAbs().array() + 1; // all positive.
|
|
const VectorXd b = A * x;
|
|
|
|
NNLS<MatrixXd> nnls(A);
|
|
const Index max_iters = n - 1;
|
|
nnls.setMaxIterations(max_iters);
|
|
nnls.solve(b);
|
|
|
|
VERIFY_IS_EQUAL(nnls.info(), ComputationInfo::NoConvergence);
|
|
VERIFY(nnls.iterations() == max_iters);
|
|
}
|
|
|
|
static void test_nnls_default_maxIterations_is_twice_column_count() {
|
|
const Index cols = internal::random<Index>(1, EIGEN_TEST_MAX_SIZE);
|
|
const Index rows = internal::random<Index>(cols, EIGEN_TEST_MAX_SIZE);
|
|
const MatrixXd A = MatrixXd::Random(rows, cols);
|
|
|
|
NNLS<MatrixXd> nnls(A);
|
|
|
|
VERIFY_IS_EQUAL(nnls.maxIterations(), 2 * cols);
|
|
}
|
|
|
|
static void test_nnls_does_not_allocate_during_solve() {
|
|
const Index cols = internal::random<Index>(1, EIGEN_TEST_MAX_SIZE);
|
|
const Index rows = internal::random<Index>(cols, EIGEN_TEST_MAX_SIZE);
|
|
const MatrixXd A = MatrixXd::Random(rows, cols);
|
|
const VectorXd b = VectorXd::Random(rows);
|
|
|
|
NNLS<MatrixXd> nnls(A);
|
|
|
|
internal::set_is_malloc_allowed(false);
|
|
nnls.solve(b);
|
|
internal::set_is_malloc_allowed(true);
|
|
}
|
|
|
|
static void test_nnls_repeated_calls_to_compute_and_solve() {
|
|
const Index cols2 = internal::random<Index>(1, EIGEN_TEST_MAX_SIZE);
|
|
const Index rows2 = internal::random<Index>(cols2, EIGEN_TEST_MAX_SIZE);
|
|
const MatrixXd A2 = MatrixXd::Random(rows2, cols2);
|
|
const VectorXd b2 = VectorXd::Random(rows2);
|
|
|
|
NNLS<MatrixXd> nnls;
|
|
|
|
for (int i = 0; i < 4; ++i) {
|
|
const Index cols = internal::random<Index>(1, EIGEN_TEST_MAX_SIZE);
|
|
const Index rows = internal::random<Index>(cols, EIGEN_TEST_MAX_SIZE);
|
|
const MatrixXd A = MatrixXd::Random(rows, cols);
|
|
|
|
nnls.compute(A);
|
|
VERIFY_IS_EQUAL(nnls.info(), ComputationInfo::Success);
|
|
|
|
for (int j = 0; j < 3; ++j) {
|
|
const VectorXd b = VectorXd::Random(rows);
|
|
const VectorXd x = nnls.solve(b);
|
|
VERIFY_IS_EQUAL(nnls.info(), ComputationInfo::Success);
|
|
verify_nnls_optimality(A, b, x, 1e-4);
|
|
}
|
|
}
|
|
}
|
|
|
|
EIGEN_DECLARE_TEST(NNLS) {
|
|
// Small matrices with known solutions:
|
|
CALL_SUBTEST_1(test_nnls_small_reference_problems());
|
|
CALL_SUBTEST_1(test_nnls_handles_Mx0_matrix());
|
|
CALL_SUBTEST_1(test_nnls_handles_0x0_matrix());
|
|
|
|
for (int i = 0; i < g_repeat; i++) {
|
|
// Essential NNLS properties, across different types.
|
|
CALL_SUBTEST_2(test_nnls_random_problem<MatrixXf>());
|
|
CALL_SUBTEST_3(test_nnls_random_problem<MatrixXd>());
|
|
using MatFixed = Matrix<double, 12, 5>;
|
|
CALL_SUBTEST_4(test_nnls_random_problem<MatFixed>());
|
|
CALL_SUBTEST_5(test_nnls_with_half_precision());
|
|
|
|
// Robustness tests:
|
|
CALL_SUBTEST_6(test_nnls_handles_zero_rhs());
|
|
CALL_SUBTEST_6(test_nnls_handles_dependent_columns());
|
|
CALL_SUBTEST_6(test_nnls_handles_wide_matrix());
|
|
|
|
// Properties specific to the implementation,
|
|
// not NNLS in general.
|
|
CALL_SUBTEST_7(test_nnls_special_case_solves_in_zero_iterations());
|
|
CALL_SUBTEST_7(test_nnls_special_case_solves_in_n_iterations());
|
|
CALL_SUBTEST_7(test_nnls_returns_NoConvergence_when_maxIterations_is_too_low());
|
|
CALL_SUBTEST_7(test_nnls_default_maxIterations_is_twice_column_count());
|
|
CALL_SUBTEST_8(test_nnls_repeated_calls_to_compute_and_solve());
|
|
|
|
// This test fails. It hits allocations in HouseholderSequence.h
|
|
// test_nnls_does_not_allocate_during_solve();
|
|
}
|
|
}
|