blob: 9d78e4ef6a3582e03f7ea97bdf07c813b145b307 [file] [log] [blame]
/*
* Copyright 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//#define LOG_NDEBUG 0
#define LOG_TAG "audio_utils_statistics_tests"
#include <audio_utils/Statistics.h>
#include <random>
#include <stdio.h>
#include <gtest/gtest.h>
// create uniform distribution
template <typename T, typename V>
static void initUniform(V& data, T rangeMin, T rangeMax) {
const size_t count = data.capacity();
std::minstd_rand gen(count);
std::uniform_real_distribution<T> dis(rangeMin, rangeMax);
// for_each works for scalars
for (auto& datum : data) {
android::audio_utils::for_each(datum, [&](T &value) { return value = dis(gen);});
}
}
// create gaussian distribution
template <typename T, typename V>
static void initNormal(V& data, T mean, T stddev) {
const size_t count = data.capacity();
std::minstd_rand gen(count);
// values near the mean are the most likely
// standard deviation affects the dispersion of generated values from the mean
std::normal_distribution<> dis{mean, stddev};
// for_each works for scalars
for (auto& datum : data) {
android::audio_utils::for_each(datum, [&](T &value) { return value = dis(gen);});
}
}
// Used to create compile-time reference constants for variance testing.
template <typename T>
class ConstexprStatistics {
public:
template <size_t N>
explicit constexpr ConstexprStatistics(const T (&a)[N])
: mN{N}
, mMax{android::audio_utils::max(a)}
, mMin{android::audio_utils::min(a)}
, mMean{android::audio_utils::sum(a) / mN}
, mM2{android::audio_utils::sumSqDiff(a, mMean)}
, mPopVariance{mM2 / mN}
, mPopStdDev{android::audio_utils::sqrt_constexpr(mPopVariance)}
, mVariance{mM2 / (mN - 1)}
, mStdDev{android::audio_utils::sqrt_constexpr(mVariance)}
{ }
constexpr int64_t getN() const { return mN; }
constexpr T getMin() const { return mMin; }
constexpr T getMax() const { return mMax; }
constexpr double getWeight() const { return (double)mN; }
constexpr double getMean() const { return mMean; }
constexpr double getVariance() const { return mVariance; }
constexpr double getStdDev() const { return mStdDev; }
constexpr double getPopVariance() const { return mPopVariance; }
constexpr double getPopStdDev() const { return mPopStdDev; }
private:
const size_t mN;
const T mMax;
const T mMin;
const double mMean;
const double mM2;
const double mPopVariance;
const double mPopStdDev;
const double mVariance;
const double mStdDev;
};
class StatisticsTest : public testing::TestWithParam<const char *>
{
};
// find power of 2 that is small enough that it doesn't add to 1. due to finite mantissa.
template <typename T>
constexpr T smallp2() {
T smallOne{};
for (smallOne = T{1.}; smallOne + T{1.} > T{1.}; smallOne *= T(0.5));
return smallOne;
}
// Our near expectation is 16x the bit that doesn't fit the mantissa.
// this works so long as we add values close in exponent with each other
// realizing that errors accumulate as the sqrt of N (random walk, lln, etc).
#define TEST_EXPECT_NEAR(e, v) \
EXPECT_NEAR((e), (v), abs((e) * std::numeric_limits<decltype(e)>::epsilon() * 8))
#define PRINT_AND_EXPECT_EQ(expected, expr) { \
auto value = (expr); \
printf("(%s): %s\n", #expr, std::to_string(value).c_str()); \
if ((expected) == (expected)) { EXPECT_EQ((expected), (value)); } \
EXPECT_EQ((expected) != (expected), (value) != (value)); /* nan check */\
}
#define PRINT_AND_EXPECT_NEAR(expected, expr) { \
auto ref = (expected); \
auto value = (expr); \
printf("(%s): %s\n", #expr, std::to_string(value).c_str()); \
TEST_EXPECT_NEAR(ref, value); \
}
template <typename T, typename S>
static void verify(const T &stat, const S &refstat) {
EXPECT_EQ(refstat.getN(), stat.getN());
EXPECT_EQ(refstat.getMin(), stat.getMin());
EXPECT_EQ(refstat.getMax(), stat.getMax());
TEST_EXPECT_NEAR(refstat.getWeight(), stat.getWeight());
TEST_EXPECT_NEAR(refstat.getMean(), stat.getMean());
TEST_EXPECT_NEAR(refstat.getVariance(), stat.getVariance());
TEST_EXPECT_NEAR(refstat.getStdDev(), stat.getStdDev());
TEST_EXPECT_NEAR(refstat.getPopVariance(), stat.getPopVariance());
TEST_EXPECT_NEAR(refstat.getPopStdDev(), stat.getPopStdDev());
}
// Test against fixed reference
TEST(StatisticsTest, high_precision_sums)
{
static const double simple[] = { 1., 2., 3. };
double rssum = android::audio_utils::sum<double, double>(simple);
PRINT_AND_EXPECT_EQ(6., rssum);
double kssum =
android::audio_utils::sum<double, android::audio_utils::KahanSum<double>>(simple);
PRINT_AND_EXPECT_EQ(6., kssum);
double nmsum =
android::audio_utils::sum<double, android::audio_utils::NeumaierSum<double>>(simple);
PRINT_AND_EXPECT_EQ(6., nmsum);
double rs{};
android::audio_utils::KahanSum<double> ks{};
android::audio_utils::NeumaierSum<double> ns{};
// add 1.
rs += 1.;
ks += 1.;
ns += 1.;
static constexpr double smallOne = std::numeric_limits<double>::epsilon() * 0.5;
// add lots of small values
static const int loop = 1000;
for (int i = 0; i < loop; ++i) {
rs += smallOne;
ks += smallOne;
ns += smallOne;
}
// remove 1.
rs += -1.;
ks += -1.;
ns += -1.;
const double totalAdded = smallOne * loop;
printf("totalAdded: %lg\n", totalAdded);
PRINT_AND_EXPECT_EQ(0., rs); // normal count fails
PRINT_AND_EXPECT_EQ(totalAdded, ks); // kahan succeeds
PRINT_AND_EXPECT_EQ(totalAdded, ns); // neumaier succeeds
// test case where kahan fails and neumaier method succeeds.
static const double tricky[] = { 1e100, 1., -1e100 };
rssum = android::audio_utils::sum<double, double>(tricky);
PRINT_AND_EXPECT_EQ(0., rssum);
kssum = android::audio_utils::sum<double, android::audio_utils::KahanSum<double>>(tricky);
PRINT_AND_EXPECT_EQ(0., kssum);
nmsum = android::audio_utils::sum<double, android::audio_utils::NeumaierSum<double>>(tricky);
PRINT_AND_EXPECT_EQ(1., nmsum);
}
TEST(StatisticsTest, minmax_bounds)
{
// range based min and max use iterator forms of min and max.
static constexpr double one[] = { 1. };
PRINT_AND_EXPECT_EQ(std::numeric_limits<double>::infinity(),
android::audio_utils::min(&one[0], &one[0]));
PRINT_AND_EXPECT_EQ(-std::numeric_limits<double>::infinity(),
android::audio_utils::max(&one[0], &one[0]));
static constexpr int un[] = { 1 };
PRINT_AND_EXPECT_EQ(std::numeric_limits<int>::max(),
android::audio_utils::min(&un[0], &un[0]));
PRINT_AND_EXPECT_EQ(std::numeric_limits<int>::min(),
android::audio_utils::max(&un[0], &un[0]));
double nanarray[] = { nan(""), nan(""), nan("") };
PRINT_AND_EXPECT_EQ(std::numeric_limits<double>::infinity(),
android::audio_utils::min(nanarray));
PRINT_AND_EXPECT_EQ(-std::numeric_limits<double>::infinity(),
android::audio_utils::max(nanarray));
android::audio_utils::Statistics<double> s(nanarray);
PRINT_AND_EXPECT_EQ(std::numeric_limits<double>::infinity(),
s.getMin());
PRINT_AND_EXPECT_EQ(-std::numeric_limits<double>::infinity(),
s.getMax());
}
/*
TEST(StatisticsTest, sqrt_convergence)
{
union {
int i;
float f;
} u;
for (int i = 0; i < INT_MAX; ++i) {
u.i = i;
const float f = u.f;
if (!android::audio_utils::isnan(f)) {
const float sf = android::audio_utils::sqrt(f);
if ((i & (1 << 16) - 1) == 0) {
printf("i: %d f:%f sf:%f\n", i, f, sf);
}
}
}
}
*/
TEST(StatisticsTest, minmax_simple_array)
{
static constexpr double ary[] = { -1.5, 1.5, -2.5, 2.5 };
PRINT_AND_EXPECT_EQ(-2.5, android::audio_utils::min(ary));
PRINT_AND_EXPECT_EQ(2.5, android::audio_utils::max(ary));
static constexpr int ray[] = { -1, 1, -2, 2 };
PRINT_AND_EXPECT_EQ(-2, android::audio_utils::min(ray));
PRINT_AND_EXPECT_EQ(2, android::audio_utils::max(ray));
}
TEST(StatisticsTest, sqrt)
{
// check doubles
PRINT_AND_EXPECT_EQ(std::numeric_limits<double>::infinity(),
android::audio_utils::sqrt(std::numeric_limits<double>::infinity()));
PRINT_AND_EXPECT_EQ(std::nan(""),
android::audio_utils::sqrt(-std::numeric_limits<double>::infinity()));
PRINT_AND_EXPECT_NEAR(sqrt(std::numeric_limits<double>::epsilon()),
android::audio_utils::sqrt(std::numeric_limits<double>::epsilon()));
PRINT_AND_EXPECT_EQ(3.,
android::audio_utils::sqrt(9.));
PRINT_AND_EXPECT_EQ(0.,
android::audio_utils::sqrt(0.));
PRINT_AND_EXPECT_EQ(std::nan(""),
android::audio_utils::sqrt(-1.));
PRINT_AND_EXPECT_EQ(std::nan(""),
android::audio_utils::sqrt(std::nan("")));
// check floats
PRINT_AND_EXPECT_EQ(std::numeric_limits<float>::infinity(),
android::audio_utils::sqrt(std::numeric_limits<float>::infinity()));
PRINT_AND_EXPECT_EQ(std::nanf(""),
android::audio_utils::sqrt(-std::numeric_limits<float>::infinity()));
PRINT_AND_EXPECT_NEAR(sqrtf(std::numeric_limits<float>::epsilon()),
android::audio_utils::sqrt(std::numeric_limits<float>::epsilon()));
PRINT_AND_EXPECT_EQ(2.f,
android::audio_utils::sqrt(4.f));
PRINT_AND_EXPECT_EQ(0.f,
android::audio_utils::sqrt(0.f));
PRINT_AND_EXPECT_EQ(std::nanf(""),
android::audio_utils::sqrt(-1.f));
PRINT_AND_EXPECT_EQ(std::nanf(""),
android::audio_utils::sqrt(std::nanf("")));
}
TEST(StatisticsTest, stat_reference)
{
// fixed reference compile time constants.
static constexpr double data[] = {0.1, -0.1, 0.2, -0.3};
static constexpr ConstexprStatistics<double> rstat(data); // use alpha = 1.
static constexpr android::audio_utils::Statistics<double> stat{data};
verify(stat, rstat);
}
TEST(StatisticsTest, stat_variable_alpha)
{
constexpr size_t TEST_SIZE = 1 << 20;
std::vector<double> data(TEST_SIZE);
std::vector<double> alpha(TEST_SIZE);
initUniform(data, -1., 1.);
initUniform(alpha, .95, .99);
android::audio_utils::ReferenceStatistics<double> rstat;
android::audio_utils::Statistics<double> stat;
static_assert(std::is_trivially_copyable<decltype(stat)>::value,
"basic statistics must be trivially copyable");
for (size_t i = 0; i < TEST_SIZE; ++i) {
rstat.setAlpha(alpha[i]);
rstat.add(data[i]);
stat.setAlpha(alpha[i]);
stat.add(data[i]);
}
printf("statistics: %s\n", stat.toString().c_str());
printf("ref statistics: %s\n", rstat.toString().c_str());
verify(stat, rstat);
}
TEST(StatisticsTest, stat_vector)
{
// for operator overloading...
using namespace android::audio_utils;
using data_t = std::tuple<double, double>;
using covariance_t = std::tuple<double, double, double, double>;
using covariance_ut_t = std::tuple<double, double, double>;
constexpr size_t TEST_SIZE = 1 << 20;
std::vector<data_t> data(TEST_SIZE);
// std::vector<double> alpha(TEST_SIZE);
initUniform(data, -1., 1.);
std::cout << "sample data[0]: " << data[0] << "\n";
Statistics<data_t, data_t, data_t, double, double, innerProduct_scalar<data_t>> stat;
Statistics<data_t, data_t, data_t, double,
covariance_t, outerProduct_tuple<data_t>> stat_outer;
Statistics<data_t, data_t, data_t, double,
covariance_ut_t, outerProduct_UT_tuple<data_t>> stat_outer_ut;
using pair_t = std::pair<double, double>;
std::vector<pair_t> pairs(TEST_SIZE);
initUniform(pairs, -1., 1.);
Statistics<pair_t, pair_t, pair_t, double, double, innerProduct_scalar<pair_t>> stat_pair;
using array_t = std::array<double, 2>;
using array_covariance_ut_t = std::array<double, 3>;
std::vector<array_t> arrays(TEST_SIZE);
initUniform(arrays, -1., 1.);
Statistics<array_t, array_t, array_t, double,
double, innerProduct_scalar<array_t>> stat_array;
Statistics<array_t, array_t, array_t, double,
array_covariance_ut_t, outerProduct_UT_array<array_t>> stat_array_ut;
for (size_t i = 0; i < TEST_SIZE; ++i) {
stat.add(data[i]);
stat_outer.add(data[i]);
stat_outer_ut.add(data[i]);
stat_pair.add(pairs[i]);
stat_array.add(arrays[i]);
stat_array_ut.add(arrays[i]);
}
#if 0
// these aren't trivially copyable
static_assert(std::is_trivially_copyable<decltype(stat)>::value,
"tuple based inner product not trivially copyable");
static_assert(std::is_trivially_copyable<decltype(stat_outer)>::value,
"tuple based outer product not trivially copyable");
static_assert(std::is_trivially_copyable<decltype(stat_outer_ut)>::value,
"tuple based outer product not trivially copyable");
#endif
static_assert(std::is_trivially_copyable<decltype(stat_array)>::value,
"array based inner product not trivially copyable");
static_assert(std::is_trivially_copyable<decltype(stat_array_ut)>::value,
"array based inner product not trivially copyable");
// inner product variance should be same as outer product diagonal sum
const double variance = stat.getPopVariance();
EXPECT_NEAR(variance,
std::get<0>(stat_outer.getPopVariance()) +
std::get<3>(stat_outer.getPopVariance()),
variance * std::numeric_limits<double>::epsilon() * 128);
// outer product covariance should be identical
PRINT_AND_EXPECT_NEAR(std::get<1>(stat_outer.getPopVariance()),
std::get<2>(stat_outer.getPopVariance()));
// upper triangular computation should be identical to outer product
PRINT_AND_EXPECT_NEAR(std::get<0>(stat_outer.getPopVariance()),
std::get<0>(stat_outer_ut.getPopVariance()));
PRINT_AND_EXPECT_NEAR(std::get<1>(stat_outer.getPopVariance()),
std::get<1>(stat_outer_ut.getPopVariance()));
PRINT_AND_EXPECT_NEAR(std::get<3>(stat_outer.getPopVariance()),
std::get<2>(stat_outer_ut.getPopVariance()));
PRINT_AND_EXPECT_EQ(variance, stat_pair.getPopVariance());
EXPECT_TRUE(equivalent(stat_array_ut.getPopVariance(), stat_outer_ut.getPopVariance()));
printf("statistics_inner: %s\n", stat.toString().c_str());
printf("statistics_outer: %s\n", stat_outer.toString().c_str());
printf("statistics_outer_ut: %s\n", stat_outer_ut.toString().c_str());
}
TEST(StatisticsTest, stat_linearfit)
{
using namespace android::audio_utils; // for operator overload
LinearLeastSquaresFit<double> fit;
static_assert(std::is_trivially_copyable<decltype(fit)>::value,
"LinearLeastSquaresFit must be trivially copyable");
using array_t = std::array<double, 2>;
array_t data{0.0, 1.5};
for (size_t i = 0; i < 10; ++i) {
fit.add(data);
data = data + array_t{0.1, 0.2};
}
// check the y line equation
{
double a, b, r2;
fit.computeYLine(a, b, r2);
printf("y line - a:%lf b:%lf r2:%lf\n", a, b, r2);
PRINT_AND_EXPECT_NEAR(1.5, a); // y intercept
PRINT_AND_EXPECT_NEAR(2.0, b); // y slope
PRINT_AND_EXPECT_NEAR(1.0, r2); // correlation coefficient.
// check same as static variant
double ac, bc, r2c;
computeYLineFromStatistics(ac, bc, r2c,
std::get<0>(fit.getMean()), /* mean_x */
std::get<1>(fit.getMean()), /* mean_y */
std::get<0>(fit.getPopVariance()), /* var_x */
std::get<1>(fit.getPopVariance()), /* cov_xy */
std::get<2>(fit.getPopVariance())); /* var_y */
EXPECT_EQ(a, ac);
EXPECT_EQ(b, bc);
EXPECT_EQ(r2, r2c);
TEST_EXPECT_NEAR(1.9, fit.getYFromX(0.2));
TEST_EXPECT_NEAR(0.2, fit.getXFromY(1.9));
TEST_EXPECT_NEAR(1.0, fit.getR2());
}
// check the x line equation
{
double a, b, r2;
fit.computeXLine(a, b, r2);
printf("x line - a:%lf b:%lf r2:%lf\n", a, b, r2);
PRINT_AND_EXPECT_NEAR(-0.75, a); // x intercept
PRINT_AND_EXPECT_NEAR(0.5, b); // x slope
PRINT_AND_EXPECT_NEAR(1.0, r2); // correlation coefficient.
}
}
TEST(StatisticsTest, stat_linearfit_noise)
{
using namespace android::audio_utils; // for operator overload
using array_t = std::array<double, 2>;
LinearLeastSquaresFit<double> fit;
// We use 1000 steps for a linear line going from (0, 0) to (1, 1) as true data for
// our linear fit.
constexpr size_t ELEMENTS = 1000;
array_t incr{1. / ELEMENTS, 1. / ELEMENTS};
// To simulate additive noise, we use a Gaussian with stddev of 1, and then scale
// achieve the desired stddev. We precompute our noise here (1000 of them).
std::vector<array_t> noise(ELEMENTS);
initNormal(noise, 0. /* mean */, 1. /* stddev */);
for (int i = 0; i < 30; ++i) {
// We run through 30 trials, with noise stddev ranging from 0 to 1.
// The steps increment linearly from 0.001 to 0.01, linearly from 0.01 to 0.1, and
// linearly again from 0.1 to 1.0.
// 0.001, 0.002, ... 0.009, 0.01, 0.02, ....0.09, 0.1, 0.2, .... 1.0
const double stddev = (i <= 10) ? i / 1000. : (i <= 20) ? (i - 9) / 100. : (i - 19) / 10.;
fit.reset();
for (size_t j = 0; j < ELEMENTS; ++j) {
array_t data = j * incr + noise[j] * stddev;
fit.add(data);
}
double a, b, r2;
fit.computeYLine(a, b, r2);
printf("stddev: %lf y line - N:%lld a:%lf b:%lf r2:%lf\n",
stddev, (long long) fit.getN(), a, b, r2);
}
}
TEST_P(StatisticsTest, stat_simple_char)
{
const char *param = GetParam();
android::audio_utils::Statistics<char> stat(0.9);
android::audio_utils::ReferenceStatistics<char> rstat(0.9);
// feed the string character by character to the statistics collectors.
for (size_t i = 0; param[i] != '\0'; ++i) {
stat.add(param[i]);
rstat.add(param[i]);
}
printf("statistics for %s: %s\n", param, stat.toString().c_str());
printf("ref statistics for %s: %s\n", param, rstat.toString().c_str());
// verify that the statistics are the same
verify(stat, rstat);
}
// find the variance of pet names as signed characters.
const char *pets[] = {"cat", "dog", "elephant", "mountain lion"};
INSTANTIATE_TEST_CASE_P(PetNameStatistics, StatisticsTest,
::testing::ValuesIn(pets));
TEST(StatisticsTest, simple_stats)
{
simple_stats_t ss{};
for (const double value : { -1., 1., 3.}) {
simple_stats_log(&ss, value);
}
PRINT_AND_EXPECT_EQ(3., ss.last);
PRINT_AND_EXPECT_EQ(1., ss.mean);
PRINT_AND_EXPECT_EQ(-1., ss.min);
PRINT_AND_EXPECT_EQ(3., ss.max);
PRINT_AND_EXPECT_EQ(3, ss.n);
char buffer[256];
simple_stats_to_string(&ss, buffer, sizeof(buffer));
printf("simple_stats: %s", buffer);
}