Restored recommendations based on acoustic similarities (using musicnn), fixes #301

This commit is contained in:
emeric
2026-06-02 08:32:43 +02:00
parent 1524106124
commit eb7f65878f
227 changed files with 10324 additions and 4673 deletions
+39
View File
@@ -0,0 +1,39 @@
include(GoogleTest)
add_executable(test-math
ChamferDistance.cpp
CentroidCalculator.cpp
CosineDistance.cpp
CovarianceCalculator.cpp
DotProduct.cpp
Entropy.cpp
EuclideanDistance.cpp
FFT.cpp
MedoidCalculator.cpp
NormalizedCosineDistance.cpp
PrincipalComponents.cpp
SquareMatrix.cpp
StatsAccumulator.cpp
Vector.cpp
Window.cpp
)
target_include_directories(test-math PRIVATE
../include
)
target_link_libraries(test-math PRIVATE
lmscore
lmsmath
Threads::Threads
GTest::GTest
GTest::gtest_main
)
target_compile_options(test-math PRIVATE
$<$<NOT:$<CONFIG:Debug>>:-ffast-math>
)
if (NOT CMAKE_CROSSCOMPILING)
gtest_discover_tests(test-math)
endif()
+72
View File
@@ -0,0 +1,72 @@
/*
* Copyright (C) 2026 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <gtest/gtest.h>
#include "math/CentroidCalculator.hpp"
#include "math/Vector.hpp"
namespace lms::math::centroidCalculatorTests
{
TEST(CentroidCalculator, initialState)
{
CentroidCalculator<Vector<3, float>> calculator;
EXPECT_TRUE(calculator.empty());
EXPECT_EQ(calculator.count(), 0U);
}
TEST(CentroidCalculator, addAndFinalize)
{
CentroidCalculator<Vector<3, float>> calculator;
calculator.add(Vector<3, float>{ 1.0F, 2.0F, 3.0F });
calculator.add(Vector<3, float>{ 4.0F, 5.0F, 6.0F });
const Vector<3, float> result = calculator.finalize();
EXPECT_FLOAT_EQ(result[0], 2.5F);
EXPECT_FLOAT_EQ(result[1], 3.5F);
EXPECT_FLOAT_EQ(result[2], 4.5F);
}
TEST(CentroidCalculator, finalizeNormalized)
{
CentroidCalculator<Vector<2, float>> calculator;
calculator.add(Vector<2, float>{ 3.0F, 4.0F });
const Vector<2, float> result = calculator.finalizeNormalized();
EXPECT_NEAR(result.computeNorm(), 1.0F, 1e-6F);
EXPECT_NEAR(result[0], 0.6F, 1e-6F);
EXPECT_NEAR(result[1], 0.8F, 1e-6F);
}
TEST(CentroidCalculator, computeCentroidSpan)
{
const std::array<Vector<2, float>, 2> values{
Vector<2, float>{ 0.0F, 2.0F },
Vector<2, float>{ 2.0F, 0.0F }
};
const Vector<2, float> result = computeCentroid(std::span<const Vector<2, float>>(values));
EXPECT_FLOAT_EQ(result[0], 1.0F);
EXPECT_FLOAT_EQ(result[1], 1.0F);
}
} // namespace lms::math::centroidCalculatorTests
+137
View File
@@ -0,0 +1,137 @@
/*
* Copyright (C) 2026 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <cmath>
#include <gtest/gtest.h>
#include "math/ChamferDistance.hpp"
#include "math/Vector.hpp"
namespace lms::math::chamferDistanceTests
{
constexpr float epsilon{ 1e-4F };
template<std::size_t Size>
struct SimpleDistance
{
SimpleDistance(const Vector<Size, float>& ref)
: _ref{ ref } {}
float operator()(const Vector<Size, float>& b) const
{
float sum{};
for (std::size_t i{}; i < Size; ++i)
{
const float diff{ _ref[i] - b[i] };
sum += diff * diff;
}
return std::sqrt(sum);
}
const Vector<Size, float>& _ref;
};
TEST(ChamferDistance, singleElementSets)
{
const Vector<2, float> A[]{ { 0.F, 0.F } };
const Vector<2, float> B[]{ { 3.F, 4.F } };
const float result{ chamferDistanceAtoB<SimpleDistance<2>>(A, B) };
const float expected{ 5.F }; // sqrt(3^2 + 4^2) = 5
EXPECT_NEAR(result, expected, epsilon);
}
TEST(ChamferDistance, identicalSets)
{
const Vector<2, float> A[]{ { 1.F, 2.F }, { 3.F, 4.F } };
const float result{ chamferDistanceAtoB<SimpleDistance<2>>(A, A) };
EXPECT_NEAR(result, 0.F, epsilon);
}
TEST(ChamferDistance, asymmetricDistance)
{
// A = {(0,0), (1,0)}, B = {(0,0), (2,0)}
// For a=(0,0): min(dist to (0,0), dist to (2,0)) = 0
// For a=(1,0): min(dist to (0,0), dist to (2,0)) = min(1, 1) = 1
// Average = (0 + 1) / 2 = 0.5
const Vector<2, float> A[]{ { 0.F, 0.F }, { 1.F, 0.F } };
const Vector<2, float> B[]{ { 0.F, 0.F }, { 2.F, 0.F } };
const float result{ chamferDistanceAtoB<SimpleDistance<2>>(A, B) };
EXPECT_NEAR(result, 0.5F, epsilon);
}
TEST(ChamferDistance, symmetricalDistance)
{
const Vector<2, float> A[]{ { 0.F, 0.F }, { 2.F, 0.F } };
const Vector<2, float> B[]{ { 0.F, 0.F }, { 1.F, 0.F } };
const float symDist{ symmetricalChamferDistance<SimpleDistance<2>>(A, B) };
const float aToB{ chamferDistanceAtoB<SimpleDistance<2>>(A, B) };
const float bToA{ chamferDistanceAtoB<SimpleDistance<2>>(B, A) };
const float expected{ (aToB + bToA) / 2.F };
EXPECT_NEAR(symDist, expected, epsilon);
}
TEST(ChamferDistance, largerSets)
{
// A has 3 elements, B has 2 elements
const Vector<2, float> A[]{ { 0.F, 0.F }, { 1.F, 1.F }, { 2.F, 2.F } };
const Vector<2, float> B[]{ { 0.F, 0.F }, { 3.F, 3.F } };
const float result{ chamferDistanceAtoB<SimpleDistance<2>>(A, B) };
// a1: min(0, sqrt(27)) = 0
// a2: min(sqrt(2), sqrt(8)) = sqrt(2)
// a3: min(sqrt(8), sqrt(2)) = sqrt(2)
// Average = (0 + sqrt(2) + sqrt(2)) / 3 = 2*sqrt(2) / 3
const float expected{ 2.F * std::sqrt(2.F) / 3.F };
EXPECT_NEAR(result, expected, epsilon);
}
TEST(ChamferDistance, negativeCoordinates)
{
const Vector<2, float> A[]{ { -1.F, -1.F } };
const Vector<2, float> B[]{ { 1.F, 1.F } };
const float result{ chamferDistanceAtoB<SimpleDistance<2>>(A, B) };
const float expected{ std::sqrt(8.F) }; // sqrt(2^2 + 2^2)
EXPECT_NEAR(result, expected, epsilon);
}
TEST(ChamferDistance, higherDimensions)
{
const Vector<5, float> A[]{ { 1.F, 2.F, 3.F, 4.F, 5.F } };
const Vector<5, float> B[]{ { 1.F, 2.F, 3.F, 4.F, 5.F } };
const float result{ chamferDistanceAtoB<SimpleDistance<5>>(A, B) };
EXPECT_NEAR(result, 0.F, epsilon);
}
} // namespace lms::math::chamferDistanceTests
+76
View File
@@ -0,0 +1,76 @@
/*
* Copyright (C) 2026 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <gtest/gtest.h>
#include "math/CosineDistance.hpp"
namespace lms::math::cosineDistanceTests
{
constexpr float epsilon{ 1e-6F };
TEST(CosineDistance, equalVectors)
{
const Vector<3, float> a{ 1.F, 2.F, 3.F };
const Vector<3, float> b{ 1.F, 2.F, 3.F };
EXPECT_NEAR(computeCosineDistance(a, b), 0.F, epsilon);
}
TEST(CosineDistance, orthogonalVectors)
{
const Vector<3, float> a{ 1.F, 0.F, 0.F };
const Vector<3, float> b{ 0.F, 1.F, 0.F };
EXPECT_NEAR(computeCosineDistance(a, b), 1.F, epsilon);
}
TEST(CosineDistance, oppositeVectors)
{
const Vector<3, float> a{ 1.F, 2.F, 3.F };
const Vector<3, float> b{ -1.F, -2.F, -3.F };
EXPECT_NEAR(computeCosineDistance(a, b), 2.F, epsilon);
}
TEST(CosineDistance, zeroNormVector)
{
const Vector<3, float> a{ 0.F, 0.F, 0.F };
const Vector<3, float> b{ 1.F, 2.F, 3.F };
EXPECT_FLOAT_EQ(computeCosineDistance(a, b), 1.F);
}
TEST(CosineDistance, vectorMethod)
{
const Vector<3, float> a{ 1.F, 2.F, 3.F };
const Vector<3, float> b{ 1.F, 2.F, 3.F };
EXPECT_NEAR(computeCosineDistance(a, b), 0.F, epsilon);
}
TEST(CosineDistance, functor)
{
const Vector<3, float> reference{ 1.F, 0.F, 0.F };
const Vector<3, float> candidate{ 0.F, 1.F, 0.F };
const CosineDistance<3, float> distance{ reference };
EXPECT_NEAR(distance(candidate), 1.F, epsilon);
}
} // namespace lms::math::cosineDistanceTests
@@ -0,0 +1,81 @@
/*
* Copyright (C) 2026 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <gtest/gtest.h>
#include "math/CovarianceCalculator.hpp"
#include "math/SquareMatrix.hpp"
#include "math/Vector.hpp"
namespace lms::math::covarianceCalculatorTests
{
constexpr float epsilon{ 1e-6F };
TEST(CovarianceCalculator, empty)
{
CovarianceMatrixCalculator<2, float> calculator;
EXPECT_TRUE(calculator.empty());
EXPECT_EQ(calculator.count(), 0U);
}
TEST(CovarianceCalculator, sampleCovariance)
{
CovarianceMatrixCalculator<2, float> calculator;
calculator.add({ 1.0F, 0.0F });
calculator.add({ -1.0F, 0.0F });
SquareMatrix<float, 2> covariance;
calculator.finalizeSample(covariance);
EXPECT_NEAR(covariance[0][0], 2.0F, epsilon);
EXPECT_NEAR(covariance[0][1], 0.0F, epsilon);
EXPECT_NEAR(covariance[1][0], 0.0F, epsilon);
EXPECT_NEAR(covariance[1][1], 0.0F, epsilon);
}
TEST(CovarianceCalculator, populationCovariance)
{
CovarianceMatrixCalculator<2, float> calculator;
calculator.add(Vector<2, float>{ 1.0F, 0.0F });
calculator.add(Vector<2, float>{ -1.0F, 0.0F });
SquareMatrix<float, 2> covariance;
calculator.finalizePopulation(covariance);
EXPECT_NEAR(covariance[0][0], 1.0F, epsilon);
EXPECT_NEAR(covariance[0][1], 0.0F, epsilon);
EXPECT_NEAR(covariance[1][0], 0.0F, epsilon);
EXPECT_NEAR(covariance[1][1], 0.0F, epsilon);
}
TEST(CovarianceCalculator, singleValueReturnsZero)
{
CovarianceMatrixCalculator<2, float> calculator;
calculator.add({ 1.0F, 2.0F });
SquareMatrix<float, 2> covariance;
calculator.finalizeSample(covariance);
EXPECT_FLOAT_EQ(covariance[0][0], 0.0F);
EXPECT_FLOAT_EQ(covariance[0][1], 0.0F);
EXPECT_FLOAT_EQ(covariance[1][0], 0.0F);
EXPECT_FLOAT_EQ(covariance[1][1], 0.0F);
}
} // namespace lms::math::covarianceCalculatorTests
+74
View File
@@ -0,0 +1,74 @@
/*
* Copyright (C) 2026 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <gtest/gtest.h>
#include "math/DotProduct.hpp"
namespace lms::math::dotProductTests
{
TEST(DotProduct, zeroLength)
{
const Vector<0, float> a{};
const Vector<0, float> b{};
EXPECT_FLOAT_EQ(computeDotProduct(a, b), 0.F);
}
TEST(DotProduct, simpleValues)
{
const Vector<3, float> a{ 1.F, 2.F, 3.F };
const Vector<3, float> b{ 4.F, 5.F, 6.F };
EXPECT_FLOAT_EQ(computeDotProduct(a, b), 32.F);
}
TEST(DotProduct, orthogonalVectors)
{
const Vector<3, float> a{ 1.F, 0.F, 0.F };
const Vector<3, float> b{ 0.F, 1.F, 0.F };
EXPECT_FLOAT_EQ(computeDotProduct(a, b), 0.F);
}
TEST(DotProduct, negativeValues)
{
const Vector<3, float> a{ -1.F, 2.F, -3.F };
const Vector<3, float> b{ 4.F, -5.F, 6.F };
EXPECT_FLOAT_EQ(computeDotProduct(a, b), -32.F);
}
TEST(DotProduct, vectorMethod)
{
const Vector<3, float> a{ 1.F, 2.F, 3.F };
const Vector<3, float> b{ 4.F, 5.F, 6.F };
EXPECT_FLOAT_EQ(computeDotProduct(a, b), 32.F);
}
TEST(DotProduct, functor)
{
const Vector<3, float> reference{ 1.F, 2.F, 3.F };
const Vector<3, float> candidate{ 4.F, 5.F, 6.F };
const DotProduct<3, float> dotProduct{ reference };
EXPECT_FLOAT_EQ(dotProduct(candidate), 32.F);
}
} // namespace lms::math::dotProductTests
+85
View File
@@ -0,0 +1,85 @@
/*
* Copyright (C) 2026 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <array>
#include <gtest/gtest.h>
#include "math/Entropy.hpp"
namespace lms::math
{
TEST(EntropyTest, ZeroInput)
{
std::array<float, 12> c{};
const float e{ entropy<float>(c) };
EXPECT_EQ(e, 0.f);
}
TEST(EntropyTest, SingleBinIsZeroEntropy)
{
std::array<float, 12> c{};
c[3] = 1.F;
const float e{ entropy<float>(c) };
EXPECT_FLOAT_EQ(e, 0.F);
}
TEST(EntropyTest, UniformDistributionMaxEntropy)
{
std::array<float, 12> c;
for (auto& v : c)
v = 1.F;
const float e{ entropy<float>(c) };
const float expected{ std::log(12.f) };
EXPECT_FLOAT_EQ(e, expected);
}
TEST(EntropyTest, ScaleInvariance)
{
std::array<float, 12> c{ 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 0.6f, 0.7f, 0.8f, 0.9f, 1.0f, 1.1f, 1.2f };
const float a{ entropy<float>(c) };
for (auto& v : c)
v *= 1000.f;
const float b{ entropy<float>(c) };
EXPECT_FLOAT_EQ(a, b);
}
TEST(EntropyTest, MoreSpreadMeansHigherEntropy)
{
std::array<float, 12> tight{};
std::array<float, 12> spread{};
tight[5] = 0.5F;
tight[6] = 0.5F;
spread[2] = 0.3F;
spread[6] = 0.4F;
spread[9] = 0.3F;
EXPECT_GT(entropy<float>(spread), entropy<float>(tight));
}
} // namespace lms::math
+118
View File
@@ -0,0 +1,118 @@
/*
* Copyright (C) 2026 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <limits>
#include <gtest/gtest.h>
#include "math/EuclideanDistance.hpp"
namespace lms::math::euclideanDistanceTests
{
constexpr float epsilon{ 1e-4F };
TEST(EuclideanDistance, zeroLength)
{
const Vector<0, float> a{};
const Vector<0, float> b{};
const Vector<0, float> weights{};
EXPECT_FLOAT_EQ(computeEuclideanSquaredDistance(a, b), 0.F);
EXPECT_FLOAT_EQ(computeEuclideanSquaredDistanceWithWeights(a, b, weights), 0.F);
}
TEST(EuclideanDistance, equalVectors)
{
const Vector<3, float> a{ 1.F, 2.F, 3.F };
const Vector<3, float> b{ 1.F, 2.F, 3.F };
EXPECT_FLOAT_EQ(computeEuclideanSquaredDistance(a, b), 0.F);
}
TEST(EuclideanDistance, unweightedDistance)
{
const Vector<3, float> a{ 1.F, 2.F, 3.F };
const Vector<3, float> b{ 4.F, 6.F, 8.F };
const float expected{ 50.F }; // 3^2 + 4^2 + 5^2
EXPECT_NEAR(computeEuclideanSquaredDistance(a, b), expected, epsilon);
}
TEST(EuclideanDistance, weightedDistance)
{
const Vector<3, float> a{ 1.F, 3.F, 5.F };
const Vector<3, float> b{ 2.F, 1.F, 6.F };
const Vector<3, float> weights{ 1.F, 0.5F, 2.F };
const float expected{ 5.F }; // 1*1 + 4*0.5 + 1*2
EXPECT_NEAR(computeEuclideanSquaredDistanceWithWeights(a, b, weights), expected, epsilon);
}
TEST(EuclideanDistance, largeMagnitudeValues)
{
// 1e15^2 * 2 = 2e30, well within the float max (~3.4e38), so no overflow
const float big{ 1e15F };
const Vector<2, float> a{ big, big };
const Vector<2, float> b{ 0.F, 0.F };
const float result{ computeEuclideanSquaredDistance(a, b) };
EXPECT_GT(result, 0.F);
}
TEST(EuclideanDistance, smallMagnitudeValues)
{
// Subnormal inputs; result must stay non-negative
const float tiny{ std::numeric_limits<float>::min() };
const Vector<3, float> a{ tiny, tiny, tiny };
const Vector<3, float> b{ 0.F, 0.F, 0.F };
const float result{ computeEuclideanSquaredDistance(a, b) };
EXPECT_GE(result, 0.F);
}
TEST(EuclideanDistance, negativeValues)
{
// Negative components must produce the same result as their positive mirror
const Vector<3, float> a{ -1.F, -2.F, -3.F };
const Vector<3, float> b{ 1.F, 2.F, 3.F };
const Vector<3, float> aMirror{ 1.F, 2.F, 3.F };
const Vector<3, float> bMirror{ -1.F, -2.F, -3.F };
EXPECT_FLOAT_EQ(
computeEuclideanSquaredDistance(a, b),
computeEuclideanSquaredDistance(aMirror, bMirror));
}
TEST(EuclideanDistance, zeroWeights)
{
const Vector<3, float> a{ 1.F, 2.F, 3.F };
const Vector<3, float> b{ 4.F, 5.F, 6.F };
const Vector<3, float> weights{ 0.F, 0.F, 0.F };
EXPECT_FLOAT_EQ(computeEuclideanSquaredDistanceWithWeights(a, b, weights), 0.F);
}
TEST(EuclideanDistance, singleElement)
{
const Vector<1, float> a{ 3.F };
const Vector<1, float> b{ 7.F };
EXPECT_FLOAT_EQ(computeEuclideanSquaredDistance(a, b), 16.F);
}
} // namespace lms::math::euclideanDistanceTests
+218
View File
@@ -0,0 +1,218 @@
/*
* Copyright (C) 2026 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <algorithm>
#include <cmath>
#include <complex>
#include <numbers>
#include <vector>
#include <gtest/gtest.h>
#include "core/AlignedHeapArray.hpp"
#include "math/FFT.hpp"
#include "math/Window.hpp"
namespace lms::math::fftTests
{
constexpr float epsilon{ 1e-3F };
namespace
{
std::size_t getRealFFTOutputSize(std::size_t inputSize)
{
return inputSize / 2 + 1;
}
std::vector<std::complex<float>> computeRealDFT(const std::vector<float>& input)
{
const std::size_t N{ input.size() };
std::vector<std::complex<float>> output(getRealFFTOutputSize(N));
for (std::size_t k{}; k <= N / 2; ++k)
{
std::complex<double> sum{ 0.0, 0.0 };
for (std::size_t n{}; n < N; ++n)
{
const double angle{ -2.0 * std::numbers::pi_v<double> * static_cast<double>(k) * static_cast<double>(n) / static_cast<double>(N) };
std::complex<double> w{ std::cos(angle), std::sin(angle) };
sum += static_cast<double>(input[n]) * w;
}
output[k] = { static_cast<float>(sum.real()), static_cast<float>(sum.imag()) };
}
return output;
}
} // namespace
TEST(FFT, impulse)
{
constexpr std::size_t N{ 8 };
const std::initializer_list<float> inputSignal{ 1.F, 0.F, 0.F, 0.F, 0.F, 0.F, 0.F, 0.F };
const auto expected{ computeRealDFT(inputSignal) };
FixedRealFFTPlan<N> plan;
core::AlignedHeapArray<float, FixedRealFFTPlan<N>::minBufferAlignment> input{ N };
core::AlignedHeapArray<std::complex<float>, FixedRealFFTPlan<N>::minBufferAlignment> output{ getRealFFTOutputSize(N) };
std::copy(inputSignal.begin(), inputSignal.end(), input.begin());
plan.apply(input, output);
for (std::size_t i{}; i < output.size(); ++i)
{
EXPECT_NEAR(output[i].real(), expected[i].real(), epsilon);
EXPECT_NEAR(output[i].imag(), expected[i].imag(), epsilon);
}
}
TEST(FFT, realForwardMatchesReference)
{
constexpr std::size_t N{ 64 };
std::vector<float> inputSignal(N);
for (std::size_t i{}; i < N; ++i)
{
inputSignal[i] = std::sin(2.F * std::numbers::pi_v<float> * static_cast<float>(i) / static_cast<float>(N))
+ 0.25F * std::sin(6.F * std::numbers::pi_v<float> * static_cast<float>(i) / static_cast<float>(N));
}
const auto expected{ computeRealDFT(inputSignal) };
FixedRealFFTPlan<N> plan;
core::AlignedHeapArray<float, FixedRealFFTPlan<N>::minBufferAlignment> input{ N };
core::AlignedHeapArray<std::complex<float>, FixedRealFFTPlan<N>::minBufferAlignment> output{ getRealFFTOutputSize(N) };
std::copy(inputSignal.begin(), inputSignal.end(), input.begin());
plan.apply({ input.data(), input.size() }, { output.data(), output.size() });
for (std::size_t i{}; i < output.size(); ++i)
{
EXPECT_NEAR(output[i].real(), expected[i].real(), epsilon);
EXPECT_NEAR(output[i].imag(), expected[i].imag(), epsilon);
}
}
TEST(FFT, singleFrequencyBin)
{
constexpr std::size_t N{ 64 };
for (std::size_t k{ 1 }; k < N / 2; ++k)
{
std::vector<float> inputSignal(N);
for (std::size_t n{}; n < N; ++n)
inputSignal[n] = std::sin(2.F * std::numbers::pi_v<float> * static_cast<float>(k) * static_cast<float>(n) / static_cast<float>(N));
const auto expected{ computeRealDFT(inputSignal) };
FixedRealFFTPlan<N> plan;
core::AlignedHeapArray<float, FixedRealFFTPlan<N>::minBufferAlignment> input{ N };
core::AlignedHeapArray<std::complex<float>, FixedRealFFTPlan<N>::minBufferAlignment> output{ getRealFFTOutputSize(N) };
std::copy(inputSignal.begin(), inputSignal.end(), input.begin());
plan.apply({ input.data(), input.size() }, { output.data(), output.size() });
for (std::size_t i{}; i < output.size(); ++i)
{
if (i == k)
EXPECT_GT(std::abs(output[i]), 10.F);
else
EXPECT_NEAR(std::abs(output[i]), std::abs(expected[i]), epsilon);
}
}
}
TEST(FFT, forwardIsUnnormalized)
{
constexpr std::size_t N{ 64 };
FixedRealFFTPlan<N> plan;
core::AlignedHeapArray<float, FixedRealFFTPlan<N>::minBufferAlignment> input{ N };
core::AlignedHeapArray<std::complex<float>, FixedRealFFTPlan<N>::minBufferAlignment> output{ getRealFFTOutputSize(N) };
std::fill(input.begin(), input.end(), 1.F);
plan.apply({ input.data(), input.size() }, { output.data(), output.size() });
EXPECT_NEAR(output[0].real(), static_cast<float>(N), epsilon);
}
TEST(FFT, parseval)
{
constexpr std::size_t N{ 64 };
std::vector<float> inputSignal(N);
for (std::size_t i{}; i < N; ++i)
inputSignal[i] = std::sin(static_cast<float>(i));
float timeEnergy{};
for (const auto value : inputSignal)
timeEnergy += value * value;
FixedRealFFTPlan<N> plan;
core::AlignedHeapArray<float, FixedRealFFTPlan<N>::minBufferAlignment> input{ N };
core::AlignedHeapArray<std::complex<float>, FixedRealFFTPlan<N>::minBufferAlignment> output{ getRealFFTOutputSize(N) };
std::copy(inputSignal.begin(), inputSignal.end(), input.begin());
plan.apply({ input.data(), input.size() }, { output.data(), output.size() });
float freqEnergy{};
freqEnergy += std::norm(output[0]);
freqEnergy += std::norm(output[N / 2]);
for (std::size_t k{ 1 }; k < N / 2; ++k)
freqEnergy += 2.F * std::norm(output[k]);
EXPECT_NEAR(timeEnergy, freqEnergy / static_cast<float>(N), epsilon);
}
TEST(FFT, parsevalWithWindow)
{
constexpr std::size_t N{ 64 };
std::vector<float> inputSignal(N);
for (std::size_t n{}; n < N; ++n)
inputSignal[n] = std::sin(2.F * std::numbers::pi_v<float> * static_cast<float>(n) / static_cast<float>(N));
const math::HannWindow<N, float> window;
const float windowEnergy{ window.energy() };
std::vector<float> windowedInput(N);
window.apply(std::span<const float, N>{ inputSignal.data(), inputSignal.size() },
std::span<float, N>{ windowedInput.data(), windowedInput.size() });
float E_time{};
for (float x : windowedInput)
E_time += x * x;
E_time /= windowEnergy;
FixedRealFFTPlan<N> plan;
core::AlignedHeapArray<float, FixedRealFFTPlan<N>::minBufferAlignment> input{ N };
core::AlignedHeapArray<std::complex<float>, FixedRealFFTPlan<N>::minBufferAlignment> output{ getRealFFTOutputSize(N) };
std::copy(windowedInput.begin(), windowedInput.end(), input.begin());
plan.apply({ input.data(), input.size() }, { output.data(), output.size() });
float E_freq{};
E_freq += std::norm(output[0]);
E_freq += std::norm(output[N / 2]);
for (std::size_t k{ 1 }; k < N / 2; ++k)
E_freq += 2.F * std::norm(output[k]);
E_freq /= (windowEnergy * static_cast<float>(N));
EXPECT_NEAR(E_time, E_freq, epsilon * E_time) << "Time-domain and frequency-domain energy mismatch after windowing";
}
} // namespace lms::math::fftTests
+120
View File
@@ -0,0 +1,120 @@
/*
* Copyright (C) 2026 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <gtest/gtest.h>
#include "math/MedoidCalculator.hpp"
#include "math/Vector.hpp"
namespace lms::math::medoidCalculatorTests
{
TEST(MedoidCalculator, initialState)
{
MedoidCalculator<Vector<3, float>> calculator;
EXPECT_TRUE(calculator.empty());
EXPECT_EQ(calculator.count(), 0U);
}
TEST(MedoidCalculator, singleVector)
{
MedoidCalculator<Vector<3, float>> calculator;
const Vector<3, float> vec{ 1.0F, 2.0F, 3.0F };
calculator.add(vec);
EXPECT_FALSE(calculator.empty());
EXPECT_EQ(calculator.count(), 1U);
EXPECT_EQ(calculator.findMedoidIndex(), 0U);
const Vector<3, float> result = calculator.finalize();
EXPECT_EQ(result[0], 1.0F);
EXPECT_EQ(result[1], 2.0F);
EXPECT_EQ(result[2], 3.0F);
}
TEST(MedoidCalculator, twoVectors)
{
MedoidCalculator<Vector<2, float>> calculator;
const Vector<2, float> v1{ 0.0F, 0.0F };
const Vector<2, float> v2{ 4.0F, 0.0F };
calculator.add(v1);
calculator.add(v2);
EXPECT_EQ(calculator.count(), 2U);
// Both have equal distance to the other, but first one is returned
const std::size_t medoidIndex = calculator.findMedoidIndex();
EXPECT_TRUE(medoidIndex == 0 || medoidIndex == 1);
}
TEST(MedoidCalculator, threeDifferentVectors)
{
MedoidCalculator<Vector<2, float>> calculator;
// Three points: (0,0), (1,0), (10,0)
// Medoid should be (1,0) as it's closest to the others
calculator.add(Vector<2, float>{ 0.0F, 0.0F });
calculator.add(Vector<2, float>{ 1.0F, 0.0F });
calculator.add(Vector<2, float>{ 10.0F, 0.0F });
const std::size_t medoidIndex = calculator.findMedoidIndex();
EXPECT_EQ(medoidIndex, 1U); // The middle point (1,0) is the medoid
const Vector<2, float> result = calculator.finalize();
EXPECT_FLOAT_EQ(result[0], 1.0F);
EXPECT_FLOAT_EQ(result[1], 0.0F);
}
TEST(MedoidCalculator, computeMedoidSpan)
{
const std::array<Vector<2, float>, 3> values{
Vector<2, float>{ 0.0F, 0.0F },
Vector<2, float>{ 1.0F, 0.0F },
Vector<2, float>{ 10.0F, 0.0F }
};
const Vector<2, float> result = computeMedoid(std::span<const Vector<2, float>>(values));
EXPECT_FLOAT_EQ(result[0], 1.0F);
EXPECT_FLOAT_EQ(result[1], 0.0F);
}
TEST(MedoidCalculator, getVector)
{
MedoidCalculator<Vector<2, float>> calculator;
calculator.add(Vector<2, float>{ 1.0F, 2.0F });
calculator.add(Vector<2, float>{ 3.0F, 4.0F });
const Vector<2, float>& v0 = calculator.getVector(0U);
const Vector<2, float>& v1 = calculator.getVector(1U);
EXPECT_FLOAT_EQ(v0[0], 1.0F);
EXPECT_FLOAT_EQ(v0[1], 2.0F);
EXPECT_FLOAT_EQ(v1[0], 3.0F);
EXPECT_FLOAT_EQ(v1[1], 4.0F);
}
TEST(MedoidCalculator, clear)
{
MedoidCalculator<Vector<2, float>> calculator;
calculator.add(Vector<2, float>{ 1.0F, 2.0F });
EXPECT_EQ(calculator.count(), 1);
calculator.clear();
EXPECT_EQ(calculator.count(), 0);
}
} // namespace lms::math::medoidCalculatorTests
@@ -0,0 +1,73 @@
/*
* Copyright (C) 2026 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <gtest/gtest.h>
#include "math/NormalizedCosineDistance.hpp"
namespace lms::math::normalizedCosineDistanceTests
{
constexpr float epsilon{ 1e-6F };
TEST(NormalizedCosineDistance, equalNormalizedVectors)
{
Vector<3, float> a{ 1.F, 2.F, 3.F };
Vector<3, float> b{ 1.F, 2.F, 3.F };
a.normalizeL2();
b.normalizeL2();
EXPECT_NEAR(computeNormalizedCosineDistance(a, b), 0.F, epsilon);
}
TEST(NormalizedCosineDistance, orthogonalNormalizedVectors)
{
Vector<3, float> a{ 1.F, 0.F, 0.F };
Vector<3, float> b{ 0.F, 1.F, 0.F };
a.normalizeL2();
b.normalizeL2();
EXPECT_NEAR(computeNormalizedCosineDistance(a, b), 0.5F, epsilon);
}
TEST(NormalizedCosineDistance, oppositeNormalizedVectors)
{
Vector<3, float> a{ 1.F, 1.F, 0.F };
Vector<3, float> b{ -1.F, -1.F, 0.F };
a.normalizeL2();
b.normalizeL2();
EXPECT_NEAR(computeNormalizedCosineDistance(a, b), 1.F, epsilon);
}
TEST(NormalizedCosineDistance, functor)
{
Vector<3, float> reference{ 1.F, 0.F, 0.F };
Vector<3, float> candidate{ 0.F, 1.F, 0.F };
reference.normalizeL2();
candidate.normalizeL2();
const NormalizedCosineDistance<3, float> distance{ reference };
EXPECT_NEAR(distance(candidate), 0.5F, epsilon);
}
} // namespace lms::math::normalizedCosineDistanceTests
+282
View File
@@ -0,0 +1,282 @@
/*
* Copyright (C) 2026 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <gtest/gtest.h>
#include "math/PrincipalComponents.hpp"
namespace lms::math::principalComponentsTests
{
constexpr float epsilon{ 1e-4F };
constexpr float doubleEpsilon{ 1e-8 };
TEST(PrincipalComponents, dotProductZeroVectors)
{
Vector<3, float> a{ 0.0F, 0.0F, 0.0F };
Vector<3, float> b{ 1.0F, 2.0F, 3.0F };
EXPECT_FLOAT_EQ(dotProduct(a, b), 0.0F);
}
TEST(PrincipalComponents, dotProductOrthogonal)
{
Vector<3, float> a{ 1.0F, 0.0F, 0.0F };
Vector<3, float> b{ 0.0F, 1.0F, 0.0F };
EXPECT_FLOAT_EQ(dotProduct(a, b), 0.0F);
}
TEST(PrincipalComponents, dotProductParallel)
{
Vector<3, float> a{ 1.0F, 2.0F, 3.0F };
Vector<3, float> b{ 2.0F, 4.0F, 6.0F };
EXPECT_FLOAT_EQ(dotProduct(a, b), 28.0F); // 2 + 8 + 18
}
TEST(PrincipalComponents, dotProductAntiparallel)
{
Vector<3, float> a{ 1.0F, 2.0F, 3.0F };
Vector<3, float> b{ -1.0F, -2.0F, -3.0F };
EXPECT_FLOAT_EQ(dotProduct(a, b), -14.0F);
}
TEST(PrincipalComponents, dotProductDouble)
{
Vector<3, double> a{ 0.5, 0.5, 0.5 };
Vector<3, double> b{ 2.0, 2.0, 2.0 };
EXPECT_DOUBLE_EQ(dotProduct(a, b), 3.0);
}
TEST(PrincipalComponents, pearsonCorrelationIdentical)
{
Vector<5, float> a{ 1.0F, 2.0F, 3.0F, 4.0F, 5.0F };
Vector<5, float> b{ 1.0F, 2.0F, 3.0F, 4.0F, 5.0F };
EXPECT_NEAR(pearsonCorrelation(a, b), 1.0F, epsilon);
}
TEST(PrincipalComponents, pearsonCorrelationNegative)
{
Vector<5, float> a{ 1.0F, 2.0F, 3.0F, 4.0F, 5.0F };
Vector<5, float> b{ 5.0F, 4.0F, 3.0F, 2.0F, 1.0F };
EXPECT_NEAR(pearsonCorrelation(a, b), -1.0F, epsilon);
}
TEST(PrincipalComponents, pearsonCorrelationIndependent)
{
Vector<4, float> a{ 1.0F, 2.0F, 3.0F, 4.0F };
Vector<4, float> b{ 4.0F, 3.0F, 2.0F, 1.0F };
EXPECT_NEAR(std::abs(pearsonCorrelation(a, b)), 1.0F, epsilon);
}
TEST(PrincipalComponents, pearsonCorrelationConstantVector)
{
Vector<5, float> a{ 1.0F, 2.0F, 3.0F, 4.0F, 5.0F };
Vector<5, float> b{ 2.0F, 2.0F, 2.0F, 2.0F, 2.0F };
// Constant vector has zero variance
EXPECT_FLOAT_EQ(pearsonCorrelation(a, b), 0.0F);
}
TEST(PrincipalComponents, pearsonCorrelationBothConstant)
{
Vector<5, float> a{ 1.0F, 1.0F, 1.0F, 1.0F, 1.0F };
Vector<5, float> b{ 2.0F, 2.0F, 2.0F, 2.0F, 2.0F };
EXPECT_FLOAT_EQ(pearsonCorrelation(a, b), 0.0F);
}
TEST(PrincipalComponents, pearsonCorrelationWeakPositive)
{
Vector<4, float> a{ 1.0F, 2.0F, 3.0F, 4.0F };
Vector<4, float> b{ 1.1F, 2.1F, 2.9F, 3.9F };
float corr = pearsonCorrelation(a, b);
EXPECT_GT(corr, 0.9F);
EXPECT_LE(corr, 1.0F);
}
TEST(PrincipalComponents, powerIterationReturnsEigenpairs)
{
SquareMatrix<double, 2> covariance;
covariance[0][0] = 2.0;
covariance[0][1] = 0.0;
covariance[1][0] = 0.0;
covariance[1][1] = 1.0;
Vector<2, double> eigenvalues{};
std::array<Vector<2, double>, 2> eigenvectors;
computeEigenpairsViaPowerIteration(covariance, eigenvectors, eigenvalues);
EXPECT_NEAR(eigenvalues[0], 2.0, epsilon);
EXPECT_NEAR(eigenvalues[1], 1.0, epsilon);
for (std::size_t k{}; k < 2; ++k)
{
Vector<2, double> Av{};
for (std::size_t i{}; i < 2; ++i)
{
for (std::size_t j{}; j < 2; ++j)
Av[i] += covariance[i][j] * eigenvectors[k][j];
}
EXPECT_NEAR(Av[0], eigenvalues[k] * eigenvectors[k][0], epsilon);
EXPECT_NEAR(Av[1], eigenvalues[k] * eigenvectors[k][1], epsilon);
}
}
TEST(PrincipalComponents, powerIterationIdentity)
{
SquareMatrix<double, 3> covariance;
covariance.fill(0.0);
covariance[0][0] = 1.0;
covariance[1][1] = 1.0;
covariance[2][2] = 1.0;
Vector<3, double> eigenvalues{};
std::array<Vector<3, double>, 3> eigenvectors;
computeEigenpairsViaPowerIteration(covariance, eigenvectors, eigenvalues);
// All eigenvalues should be 1
EXPECT_NEAR(eigenvalues[0], 1.0, doubleEpsilon);
EXPECT_NEAR(eigenvalues[1], 1.0, doubleEpsilon);
EXPECT_NEAR(eigenvalues[2], 1.0, doubleEpsilon);
}
TEST(PrincipalComponents, powerIterationSymmetric)
{
SquareMatrix<double, 2> covariance;
covariance[0][0] = 4.0;
covariance[0][1] = 2.0;
covariance[1][0] = 2.0;
covariance[1][1] = 3.0;
Vector<2, double> eigenvalues{};
std::array<Vector<2, double>, 2> eigenvectors;
computeEigenpairsViaPowerIteration(covariance, eigenvectors, eigenvalues);
// Verify A*v = lambda*v for each eigenpair
for (std::size_t k{}; k < 2; ++k)
{
Vector<2, double> Av{};
for (std::size_t i{}; i < 2; ++i)
{
for (std::size_t j{}; j < 2; ++j)
Av[i] += covariance[i][j] * eigenvectors[k][j];
}
EXPECT_NEAR(Av[0], eigenvalues[k] * eigenvectors[k][0], epsilon);
EXPECT_NEAR(Av[1], eigenvalues[k] * eigenvectors[k][1], epsilon);
}
}
TEST(PrincipalComponents, powerIterationWithCustomIterations)
{
SquareMatrix<double, 2> covariance;
covariance[0][0] = 2.0;
covariance[0][1] = 0.0;
covariance[1][0] = 0.0;
covariance[1][1] = 1.0;
Vector<2, double> eigenvalues{};
std::array<Vector<2, double>, 2> eigenvectors;
// Use only 50 iterations
computeEigenpairsViaPowerIteration(covariance, eigenvectors, eigenvalues, 50, 1e-10);
EXPECT_NEAR(eigenvalues[0], 2.0, 1e-2);
EXPECT_NEAR(eigenvalues[1], 1.0, 1e-2);
}
TEST(PrincipalComponents, powerIterationWithCustomEpsilon)
{
SquareMatrix<double, 2> covariance;
covariance[0][0] = 2.0;
covariance[0][1] = 0.0;
covariance[1][0] = 0.0;
covariance[1][1] = 1.0;
Vector<2, double> eigenvalues{};
std::array<Vector<2, double>, 2> eigenvectors;
// Use looser epsilon
computeEigenpairsViaPowerIteration(covariance, eigenvectors, eigenvalues, 200, 1e-6);
EXPECT_NEAR(eigenvalues[0], 2.0, epsilon);
EXPECT_NEAR(eigenvalues[1], 1.0, epsilon);
}
TEST(PrincipalComponents, powerIterationLargerMatrix)
{
// Create a 4x4 diagonal matrix
SquareMatrix<double, 4> covariance;
covariance.fill(0.0);
covariance[0][0] = 4.0;
covariance[1][1] = 3.0;
covariance[2][2] = 2.0;
covariance[3][3] = 1.0;
Vector<4, double> eigenvalues{};
std::array<Vector<4, double>, 4> eigenvectors;
computeEigenpairsViaPowerIteration(covariance, eigenvectors, eigenvalues);
// Eigenvalues should be 4, 3, 2, 1 (in descending order after deflation)
EXPECT_NEAR(eigenvalues[0], 4.0, doubleEpsilon);
EXPECT_NEAR(eigenvalues[1], 3.0, doubleEpsilon);
EXPECT_NEAR(eigenvalues[2], 2.0, doubleEpsilon);
EXPECT_NEAR(eigenvalues[3], 1.0, doubleEpsilon);
}
TEST(PrincipalComponents, projectOntoBasis)
{
std::array<std::array<float, 2>, 2> basis{};
basis[0][0] = 1.0F;
basis[0][1] = 0.0F;
basis[1][0] = 0.0F;
basis[1][1] = 1.0F;
Vector<2, float> centered{ 1.0F, 2.0F };
Vector<2, float> output;
std::array<float, 2> scales{ 2.0F, 3.0F };
projectOntoBasis(basis, centered, output, scales);
EXPECT_FLOAT_EQ(output[0], 2.0F);
EXPECT_FLOAT_EQ(output[1], 6.0F);
}
TEST(PrincipalComponents, pearsonCorrelation)
{
Vector<3, float> a{ 1.0F, 2.0F, 3.0F };
Vector<3, float> b{ 1.0F, 2.0F, 3.0F };
Vector<3, float> c{ -1.0F, -2.0F, -3.0F };
EXPECT_NEAR(pearsonCorrelation(a, b), 1.0F, epsilon);
EXPECT_NEAR(pearsonCorrelation(a, c), -1.0F, epsilon);
}
} // namespace lms::math::principalComponentsTests
+161
View File
@@ -0,0 +1,161 @@
/*
* Copyright (C) 2026 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <gtest/gtest.h>
#include "math/SquareMatrix.hpp"
namespace lms::math::squareMatrixTests
{
constexpr float epsilon{ 1e-4F };
TEST(SquareMatrix, choleskyDecomposePositiveDefinite)
{
SquareMatrix<float, 3> matrix;
matrix.fill(0.F);
matrix[0][0] = 4.F;
matrix[0][1] = 12.F;
matrix[0][2] = -16.F;
matrix[1][0] = 12.F;
matrix[1][1] = 37.F;
matrix[1][2] = -43.F;
matrix[2][0] = -16.F;
matrix[2][1] = -43.F;
matrix[2][2] = 98.F;
SquareMatrix<float, 3> lower;
EXPECT_TRUE(choleskyDecompose(matrix, lower));
EXPECT_FLOAT_EQ(lower[0][0], 2.F);
EXPECT_FLOAT_EQ(lower[1][0], 6.F);
EXPECT_FLOAT_EQ(lower[1][1], 1.F);
EXPECT_FLOAT_EQ(lower[2][0], -8.F);
EXPECT_FLOAT_EQ(lower[2][1], 5.F);
EXPECT_FLOAT_EQ(lower[2][2], 3.F);
}
TEST(SquareMatrix, choleskyDecomposeIdentity)
{
SquareMatrix<float, 3> matrix;
matrix.fill(0.F);
matrix[0][0] = 1.F;
matrix[1][1] = 1.F;
matrix[2][2] = 1.F;
SquareMatrix<float, 3> lower;
EXPECT_TRUE(choleskyDecompose(matrix, lower));
for (std::size_t i{}; i < 3; ++i)
{
for (std::size_t j{}; j < 3; ++j)
{
if (i == j)
EXPECT_FLOAT_EQ(lower[i][j], 1.F);
else
EXPECT_FLOAT_EQ(lower[i][j], 0.F);
}
}
}
TEST(SquareMatrix, choleskyDecomposeSize1)
{
SquareMatrix<float, 1> matrix;
matrix[0][0] = 9.F;
SquareMatrix<float, 1> lower;
EXPECT_TRUE(choleskyDecompose(matrix, lower));
EXPECT_FLOAT_EQ(lower[0][0], 3.F);
}
TEST(SquareMatrix, choleskyDecomposeNonPositiveDefinite)
{
SquareMatrix<float, 2> matrix;
matrix.fill(0.F);
SquareMatrix<float, 2> lower;
EXPECT_FALSE(choleskyDecompose(matrix, lower));
}
TEST(SquareMatrix, invertLowerTriangular)
{
SquareMatrix<float, 3> lower;
lower.fill(0.F);
lower[0][0] = 2.F;
lower[1][0] = 6.F;
lower[1][1] = 1.F;
lower[2][0] = -8.F;
lower[2][1] = 5.F;
lower[2][2] = 3.F;
SquareMatrix<float, 3> inverse;
invertLowerTriangular(lower, inverse);
SquareMatrix<float, 3> identity;
identity.fill(0.F);
for (std::size_t i{}; i < 3; ++i)
{
for (std::size_t j{}; j < 3; ++j)
{
float sum = 0.F;
for (std::size_t k{}; k < 3; ++k)
{
sum += lower[i][k] * inverse[k][j];
}
identity[i][j] = sum;
}
}
for (std::size_t i{}; i < 3; ++i)
{
for (std::size_t j{}; j < 3; ++j)
{
if (i == j)
EXPECT_NEAR(identity[i][j], 1.F, epsilon);
else
EXPECT_NEAR(identity[i][j], 0.F, epsilon);
}
}
}
TEST(SquareMatrix, invertLowerTriangularSize1)
{
SquareMatrix<float, 1> lower;
lower[0][0] = 5.F;
SquareMatrix<float, 1> inverse;
invertLowerTriangular(lower, inverse);
EXPECT_FLOAT_EQ(inverse[0][0], 0.2F);
}
TEST(SquareMatrix, computeSymmetryMaxDiff)
{
SquareMatrix<float, 2> matrix;
matrix.fill(0.F);
matrix[0][1] = 1.F;
matrix[1][0] = 1.2F;
EXPECT_NEAR(computeSymmetryMaxDiff(matrix), 0.2F, epsilon);
}
} // namespace lms::math::squareMatrixTests
+172
View File
@@ -0,0 +1,172 @@
/*
* Copyright (C) 2026 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <gtest/gtest.h>
#include "math/StatsAccumulator.hpp"
namespace lms::math::statsAccumulatorTests
{
constexpr float epsilon{ 1e-4F };
TEST(StatsAccumulator, initialState)
{
StatsAccumulator stats;
EXPECT_EQ(stats.getCount(), 0);
EXPECT_FLOAT_EQ(stats.getMean(), 0.F);
EXPECT_FLOAT_EQ(stats.getPopulationVariance(), 0.F);
EXPECT_FLOAT_EQ(stats.getSampleVariance(), 0.F);
EXPECT_FLOAT_EQ(stats.getPopulationStdDev(), 0.F);
}
TEST(StatsAccumulator, singleValue)
{
constexpr float value{ 5.F };
StatsAccumulator stats;
stats.add(value);
EXPECT_EQ(stats.getCount(), 1);
EXPECT_FLOAT_EQ(stats.getMean(), value);
// Variance should be 0 for a single value
EXPECT_FLOAT_EQ(stats.getPopulationVariance(), 0.F);
EXPECT_FLOAT_EQ(stats.getSampleVariance(), 0.F);
}
TEST(StatsAccumulator, multipleValuesMean)
{
constexpr float a{ 2.F };
constexpr float b{ 4.F };
constexpr float c{ 6.F };
StatsAccumulator stats;
stats.add(a);
stats.add(b);
stats.add(c);
EXPECT_EQ(stats.getCount(), 3);
EXPECT_FLOAT_EQ(stats.getMean(), b);
}
TEST(StatsAccumulator, populationVariance)
{
constexpr float a{ 2.F };
constexpr float b{ 4.F };
constexpr float c{ 6.F };
constexpr float expectedVariance{ 8.F / 3.F };
StatsAccumulator stats;
stats.add(a);
stats.add(b);
stats.add(c);
// Population variance = 8 / 3 ≈ 2.6667
EXPECT_NEAR(stats.getPopulationVariance(), expectedVariance, epsilon);
}
TEST(StatsAccumulator, sampleVariance)
{
constexpr float a{ 2.F };
constexpr float b{ 4.F };
constexpr float c{ 6.F };
constexpr float expectedVariance{ 4.F };
StatsAccumulator stats;
stats.add(a);
stats.add(b);
stats.add(c);
// Sample variance = 8 / 2 = 4
EXPECT_NEAR(stats.getSampleVariance(), expectedVariance, epsilon);
}
TEST(StatsAccumulator, standardDeviation)
{
constexpr float a{ 2.F };
constexpr float b{ 4.F };
constexpr float c{ 6.F };
constexpr float expectedStdDev{ 2.F };
StatsAccumulator stats;
stats.add(a);
stats.add(b);
stats.add(c);
// sqrt(4) = 2 (sample stddev)
EXPECT_NEAR(stats.getSampleStdDev(), expectedStdDev, epsilon);
}
TEST(StatsAccumulator, largeMagnitudeValues)
{
// Welford's algorithm must stay numerically stable with large inputs
// 1e6 is within float's ~7 significant-digit range
constexpr float big{ 1e6F };
constexpr float offset{ 2.F };
StatsAccumulator stats;
stats.add(big);
stats.add(big + 1.F);
stats.add(big + offset);
EXPECT_NEAR(stats.getMean(), big + 1.F, 1e-1F);
EXPECT_NEAR(stats.getSampleVariance(), 1.F, 1e-1F);
EXPECT_NEAR(stats.getSampleStdDev(), 1.F, 1e-1F);
}
TEST(StatsAccumulator, negativeValues)
{
constexpr float a{ -6.F };
constexpr float b{ -4.F };
constexpr float c{ -2.F };
constexpr float expectedVariance{ 4.F };
StatsAccumulator stats;
stats.add(a);
stats.add(b);
stats.add(c);
EXPECT_NEAR(stats.getMean(), b, epsilon);
EXPECT_NEAR(stats.getSampleVariance(), expectedVariance, epsilon);
}
TEST(StatsAccumulator, mixedSignValues)
{
constexpr float a{ -1.F };
constexpr float b{ 0.F };
constexpr float c{ 1.F };
constexpr float expectedVariance{ 1.F };
StatsAccumulator stats;
stats.add(a);
stats.add(b);
stats.add(c);
EXPECT_NEAR(stats.getMean(), 0.F, epsilon);
EXPECT_NEAR(stats.getSampleVariance(), expectedVariance, epsilon);
}
TEST(StatsAccumulator, smallMagnitudeValues)
{
// Values well within float's normal range; variance must stay non-negative
constexpr float tiny{ 1e-30F };
constexpr float multiplier2{ 2.F };
constexpr float multiplier3{ 3.F };
StatsAccumulator stats;
stats.add(tiny);
stats.add(tiny * multiplier2);
stats.add(tiny * multiplier3);
EXPECT_GE(stats.getSampleVariance(), 0.F);
EXPECT_GE(stats.getSampleStdDev(), 0.F);
}
} // namespace lms::math::statsAccumulatorTests
+278
View File
@@ -0,0 +1,278 @@
/*
* Copyright (C) 2026 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <gtest/gtest.h>
#include "math/Vector.hpp"
namespace lms::math::vectorTests
{
constexpr float epsilon{ 1e-5F };
TEST(Vector, constructionDefault)
{
Vector<3, float> v;
EXPECT_FLOAT_EQ(v[0], 0.F);
EXPECT_FLOAT_EQ(v[1], 0.F);
EXPECT_FLOAT_EQ(v[2], 0.F);
}
TEST(Vector, constructionWithInitValue)
{
Vector<3, float> v{ 5.0F };
EXPECT_FLOAT_EQ(v[0], 5.0F);
EXPECT_FLOAT_EQ(v[1], 5.0F);
EXPECT_FLOAT_EQ(v[2], 5.0F);
}
TEST(Vector, constructionWithArgs)
{
Vector<3, float> v{ 1.0F, 2.0F, 3.0F };
EXPECT_FLOAT_EQ(v[0], 1.0F);
EXPECT_FLOAT_EQ(v[1], 2.0F);
EXPECT_FLOAT_EQ(v[2], 3.0F);
}
TEST(Vector, size)
{
Vector<3, float> v;
EXPECT_EQ(v.getSize(), 3U);
}
TEST(Vector, dataAccess)
{
Vector<3, float> v{ 1.0F, 2.0F, 3.0F };
const float* data = v.data();
EXPECT_FLOAT_EQ(data[0], 1.0F);
EXPECT_FLOAT_EQ(data[1], 2.0F);
EXPECT_FLOAT_EQ(data[2], 3.0F);
}
TEST(Vector, operatorAddAssign)
{
Vector<3, float> a{ 1.0F, 2.0F, 3.0F };
Vector<3, float> b{ 4.0F, 5.0F, 6.0F };
a += b;
EXPECT_FLOAT_EQ(a[0], 5.0F);
EXPECT_FLOAT_EQ(a[1], 7.0F);
EXPECT_FLOAT_EQ(a[2], 9.0F);
}
TEST(Vector, operatorSubAssign)
{
Vector<3, float> a{ 4.0F, 5.0F, 6.0F };
Vector<3, float> b{ 1.0F, 2.0F, 3.0F };
a -= b;
EXPECT_FLOAT_EQ(a[0], 3.0F);
EXPECT_FLOAT_EQ(a[1], 3.0F);
EXPECT_FLOAT_EQ(a[2], 3.0F);
}
TEST(Vector, operatorMulAssign)
{
Vector<3, float> v{ 1.0F, 2.0F, 3.0F };
v *= 2.0F;
EXPECT_FLOAT_EQ(v[0], 2.0F);
EXPECT_FLOAT_EQ(v[1], 4.0F);
EXPECT_FLOAT_EQ(v[2], 6.0F);
}
TEST(Vector, operatorAdd)
{
Vector<3, float> a{ 1.0F, 2.0F, 3.0F };
Vector<3, float> b{ 4.0F, 5.0F, 6.0F };
Vector<3, float> result = a + b;
EXPECT_FLOAT_EQ(result[0], 5.0F);
EXPECT_FLOAT_EQ(result[1], 7.0F);
EXPECT_FLOAT_EQ(result[2], 9.0F);
// Ensure originals unchanged
EXPECT_FLOAT_EQ(a[0], 1.0F);
EXPECT_FLOAT_EQ(b[0], 4.0F);
}
TEST(Vector, operatorSub)
{
Vector<3, float> a{ 4.0F, 5.0F, 6.0F };
Vector<3, float> b{ 1.0F, 2.0F, 3.0F };
Vector<3, float> result = a - b;
EXPECT_FLOAT_EQ(result[0], 3.0F);
EXPECT_FLOAT_EQ(result[1], 3.0F);
EXPECT_FLOAT_EQ(result[2], 3.0F);
// Ensure originals unchanged
EXPECT_FLOAT_EQ(a[0], 4.0F);
}
TEST(Vector, operatorMulScalarRight)
{
Vector<3, float> v{ 1.0F, 2.0F, 3.0F };
Vector<3, float> result = v * 2.0F;
EXPECT_FLOAT_EQ(result[0], 2.0F);
EXPECT_FLOAT_EQ(result[1], 4.0F);
EXPECT_FLOAT_EQ(result[2], 6.0F);
// Ensure original unchanged
EXPECT_FLOAT_EQ(v[0], 1.0F);
}
TEST(Vector, operatorMulScalarLeft)
{
Vector<3, float> v{ 1.0F, 2.0F, 3.0F };
Vector<3, float> result = 3.0F * v;
EXPECT_FLOAT_EQ(result[0], 3.0F);
EXPECT_FLOAT_EQ(result[1], 6.0F);
EXPECT_FLOAT_EQ(result[2], 9.0F);
}
TEST(Vector, computeNorm)
{
Vector<3, float> v{ 3.0F, 4.0F, 0.0F };
EXPECT_FLOAT_EQ(v.computeNorm(), 5.0F);
}
TEST(Vector, computeNormZero)
{
Vector<3, float> v{ 0.0F, 0.0F, 0.0F };
EXPECT_FLOAT_EQ(v.computeNorm(), 0.0F);
}
TEST(Vector, normalizeL2)
{
Vector<3, float> v{ 3.0F, 4.0F, 0.0F };
v.normalizeL2();
EXPECT_NEAR(v.computeNorm(), 1.0F, epsilon);
EXPECT_NEAR(v[0], 0.6F, epsilon);
EXPECT_NEAR(v[1], 0.8F, epsilon);
EXPECT_NEAR(v[2], 0.0F, epsilon);
}
TEST(Vector, normalizeL2ZeroVector)
{
Vector<3, float> v{ 0.0F, 0.0F, 0.0F };
v.normalizeL2();
// Zero vector remains unchanged
EXPECT_FLOAT_EQ(v[0], 0.0F);
EXPECT_FLOAT_EQ(v[1], 0.0F);
EXPECT_FLOAT_EQ(v[2], 0.0F);
}
TEST(Vector, normalizeL2SmallVector)
{
constexpr float tiny{ 1e-15F };
Vector<3, float> v{ tiny, tiny, tiny };
v.normalizeL2();
// Small vector remains unchanged due to epsilon check
EXPECT_FLOAT_EQ(v[0], tiny);
EXPECT_FLOAT_EQ(v[1], tiny);
EXPECT_FLOAT_EQ(v[2], tiny);
}
TEST(Vector, iterators)
{
Vector<3, float> v{ 1.0F, 2.0F, 3.0F };
std::size_t index{};
for (float val : v)
{
EXPECT_FLOAT_EQ(val, static_cast<float>(index + 1));
++index;
}
}
TEST(Vector, constIterators)
{
const Vector<3, float> v{ 1.0F, 2.0F, 3.0F };
std::size_t index{};
for (auto it = v.cbegin(); it != v.cend(); ++it)
{
EXPECT_FLOAT_EQ(*it, static_cast<float>(index + 1));
++index;
}
}
TEST(Vector, size1)
{
Vector<1, float> v{ 5.0F };
EXPECT_FLOAT_EQ(v[0], 5.0F);
EXPECT_FLOAT_EQ(v.computeNorm(), 5.0F);
}
TEST(Vector, largeSize)
{
constexpr std::size_t size{ 1000 };
Vector<size, float> v{ 1.0F };
EXPECT_NEAR(v.computeNorm(), std::sqrt(static_cast<float>(size)), 1e-4F);
}
TEST(Vector, negativeValues)
{
Vector<3, float> v{ -1.0F, -2.0F, -3.0F };
EXPECT_FLOAT_EQ(v.computeNorm(), std::sqrt(14.0F));
}
TEST(Vector, mixedSignValues)
{
Vector<3, float> a{ -1.0F, 2.0F, -3.0F };
Vector<3, float> b{ 1.0F, -2.0F, 3.0F };
const Vector<3, float> sum{ a + b };
EXPECT_FLOAT_EQ(sum[0], 0.0F);
EXPECT_FLOAT_EQ(sum[1], 0.0F);
EXPECT_FLOAT_EQ(sum[2], 0.0F);
}
TEST(Vector, doubleType)
{
Vector<3, double> v{ 1.0, 2.0, 3.0 };
EXPECT_DOUBLE_EQ(v[0], 1.0);
EXPECT_NEAR(v.computeNorm(), std::sqrt(14.0), 1e-15);
}
} // namespace lms::math::vectorTests
+90
View File
@@ -0,0 +1,90 @@
/*
* Copyright (C) 2026 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include <cmath>
#include <vector>
#include <gtest/gtest.h>
#include "math/Window.hpp"
namespace lms::math::tests
{
TEST(Window, oneSampleWindowIsFinite)
{
const HannWindow<1, float> window;
const auto values{ window.values() };
EXPECT_TRUE(std::isfinite(values[0]));
EXPECT_GE(values[0], 0.F);
EXPECT_LE(values[0], 1.F);
EXPECT_FLOAT_EQ(window.energy(), 1.F);
}
TEST(Window, twoSamplesWindow)
{
const HannWindow<2, float> window;
const auto values{ window.values() };
EXPECT_FLOAT_EQ(values[0], 0.F);
EXPECT_FLOAT_EQ(values[1], 0.F);
EXPECT_FLOAT_EQ(window.energy(), 0.F);
}
TEST(Window, coefficientsAreFiniteAndInRange)
{
const HannWindow<17, float> window;
for (float v : window.values())
{
EXPECT_TRUE(std::isfinite(v));
EXPECT_GE(v, 0.F);
EXPECT_LE(v, 1.F);
}
EXPECT_GT(window.energy(), 0.F);
}
TEST(Window, symmetric)
{
const HannWindow<31, float> window;
const auto values{ window.values() };
for (std::size_t i{}; i < values.size() / 2; ++i)
EXPECT_NEAR(values[i], values[values.size() - 1 - i], 1e-6F);
}
TEST(Window, applyUsesPrecomputedCoefficients)
{
constexpr std::size_t size{ 8 };
const HannWindow<size, float> window;
std::vector<float> input(size);
for (std::size_t i{}; i < size; ++i)
input[i] = static_cast<float>(i + 1);
std::vector<float> output(size);
window.apply(std::span<const float, size>{ input.data(), input.size() },
std::span<float, size>{ output.data(), output.size() });
const auto coefficients{ window.values() };
for (std::size_t i{}; i < size; ++i)
EXPECT_FLOAT_EQ(output[i], input[i] * coefficients[i]);
}
} // namespace lms::math::tests