Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ its family**, not golden output. This is the reference for the `unit-tester` age
writing **unit tests** for `numerical/`. It answers one question per algorithm: *which mathematical
properties must a correct implementation satisfy, and how do we assert them?*

## Gradient-check test helper

`numerical/math/test_doubles/GradientCheck.hpp` (namespace `math::test`, `numerical.math_test_helper`):

- `CentralDifferenceGradient(f, x[, h])` — central differences; `h` is a scalar, a per-component
vector, or omitted for the default `cbrt(eps) · max(1, |x_i|)` (`DefaultFiniteDifferenceSteps`).
- `ExpectGradientNear(analytic, f, x[, h], tol)` — `ADD_FAILURE` per component whose absolute **and**
relative errors both exceed `tol`.

Use it for the **M1 gradient check** of every `Gradient`/`Backward` implementation (§7, §10).

## Rationale

A numerical algorithm is not validated by "it compiles and doesn't crash." Every family has a small
Expand Down
10 changes: 10 additions & 0 deletions doc/math/MatrixOperations.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ The skew-symmetric part $\tfrac{1}{2}(M - M^\top)$ is `Symmetrize`'s companion.

Operate on `math::Matrix` / `math::SquareMatrix`. Consumed by the `filters::active` Kalman family and `estimators::ExpectationMaximization`; `CongruenceTransform` pairs naturally with `Symmetrize` for covariance-positivity hygiene.

## Zero-dimension Vectors

Matrix and vector dimensions must be strictly positive; a zero-length parameter vector is not
representable. Element access on an empty fixed-size array has no valid index, so allowing it would
trade a compile-time error for silent undefined behaviour.

A component without trainable parameters (for example a pooling or pure activation stage) should
report the absence of parameters explicitly — an optional parameter vector that is empty, or an
interface that has no parameter accessor at all — rather than returning a zero-length vector.

## References & Further Reading

- Golub, G. H. & Van Loan, C. F., "Matrix Computations", 4th ed., §2 (symmetric/skew decomposition)
Expand Down
1 change: 1 addition & 0 deletions numerical/math/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ numerical_link_qemu_runtime(numerical.math_test)
target_sources(numerical.math_test PRIVATE
TestCholeskyDecomposition.cpp
TestComplexNumber.cpp
TestGradientCheck.cpp
TestMath.cpp
TestConsistencyMetrics.cpp
TestCordic.cpp
Expand Down
61 changes: 61 additions & 0 deletions numerical/math/test/TestGradientCheck.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#include "numerical/math/test_doubles/GradientCheck.hpp"
#include <cmath>
#include <gtest/gtest-spi.h>
#include <gtest/gtest.h>
#include <limits>

namespace
{
using Vector3 = math::Vector<float, 3>;

float Cubic(const Vector3& x)
{
return x.at(0, 0) * x.at(0, 0) * x.at(0, 0) + 2.0f * x.at(1, 0) * x.at(1, 0) + 3.0f * x.at(2, 0);
}

Vector3 CubicGradient(const Vector3& x)
{
return Vector3{ { 3.0f * x.at(0, 0) * x.at(0, 0) }, { 4.0f * x.at(1, 0) }, { 3.0f } };
}

class TestGradientCheck
: public ::testing::Test
{
public:
const Vector3 x{ { 2.0f }, { -1.0f }, { 0.5f } };
};
}

TEST_F(TestGradientCheck, central_difference_matches_analytic_gradient)
{
const auto numeric = math::test::CentralDifferenceGradient(Cubic, x, 1e-2f);
const auto analytic = CubicGradient(x);

EXPECT_NEAR(numeric.at(0, 0), analytic.at(0, 0), 1e-3f);
EXPECT_NEAR(numeric.at(1, 0), analytic.at(1, 0), 1e-3f);
EXPECT_NEAR(numeric.at(2, 0), analytic.at(2, 0), 1e-3f);
}

TEST_F(TestGradientCheck, default_step_scales_with_component_magnitude)
{
const float cbrtEps = std::cbrt(std::numeric_limits<float>::epsilon());

const auto h = math::test::DefaultFiniteDifferenceSteps(Vector3{ { 0.1f }, { -100.0f }, { 1.0f } });

EXPECT_FLOAT_EQ(h.at(0, 0), cbrtEps);
EXPECT_FLOAT_EQ(h.at(1, 0), 100.0f * cbrtEps);
EXPECT_FLOAT_EQ(h.at(2, 0), cbrtEps);
}

TEST_F(TestGradientCheck, expect_gradient_near_accepts_correct_gradient)
{
math::test::ExpectGradientNear(CubicGradient(x), Cubic, x, 1e-3f);
}

TEST_F(TestGradientCheck, expect_gradient_near_reports_wrong_component)
{
auto wrong = CubicGradient(x);
wrong.at(1, 0) = 99.0f;

EXPECT_NONFATAL_FAILURE(math::test::ExpectGradientNear(wrong, Cubic, x, 1e-2f, 1e-3f), "component[1]");
}
1 change: 1 addition & 0 deletions numerical/math/test_doubles/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ target_link_libraries(numerical.math_test_helper INTERFACE
)

target_sources(numerical.math_test_helper PRIVATE
GradientCheck.hpp
MatrixTestSupport.hpp
)
83 changes: 83 additions & 0 deletions numerical/math/test_doubles/GradientCheck.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#pragma once

#include "numerical/math/Matrix.hpp"
#include <algorithm>
#include <cmath>
#include <gtest/gtest.h>
#include <limits>

namespace math::test
{
template<typename T, std::size_t N>
math::Vector<T, N> DefaultFiniteDifferenceSteps(const math::Vector<T, N>& x)
{
static_assert(std::is_floating_point_v<T>, "DefaultFiniteDifferenceSteps requires a floating-point type");
const T cbrtEps = std::cbrt(std::numeric_limits<T>::epsilon());
math::Vector<T, N> h;
for (std::size_t i = 0; i < N; ++i)
h.at(i, 0) = cbrtEps * std::max(T{ 1 }, std::abs(x.at(i, 0)));
return h;
}

template<typename T, std::size_t N, typename Func>
math::Vector<T, N> CentralDifferenceGradient(Func f, const math::Vector<T, N>& x, const math::Vector<T, N>& h)
{
static_assert(std::is_floating_point_v<T>, "CentralDifferenceGradient requires a floating-point type");
math::Vector<T, N> gradient;
for (std::size_t i = 0; i < N; ++i)
{
math::Vector<T, N> xPlus = x;
math::Vector<T, N> xMinus = x;
xPlus.at(i, 0) += h.at(i, 0);
xMinus.at(i, 0) -= h.at(i, 0);
gradient.at(i, 0) = (f(xPlus) - f(xMinus)) / (xPlus.at(i, 0) - xMinus.at(i, 0));
}
return gradient;
}

template<typename T, std::size_t N, typename Func>
math::Vector<T, N> CentralDifferenceGradient(Func f, const math::Vector<T, N>& x, T h)
{
math::Vector<T, N> steps;
for (std::size_t i = 0; i < N; ++i)
steps.at(i, 0) = h;
return CentralDifferenceGradient(f, x, steps);
}

template<typename T, std::size_t N, typename Func>
math::Vector<T, N> CentralDifferenceGradient(Func f, const math::Vector<T, N>& x)
{
return CentralDifferenceGradient(f, x, DefaultFiniteDifferenceSteps(x));
}

template<typename T, std::size_t N, typename Func>
void ExpectGradientNear(const math::Vector<T, N>& analytic, Func f, const math::Vector<T, N>& x, const math::Vector<T, N>& h, T tol)
{
const auto numeric = CentralDifferenceGradient(f, x, h);
for (std::size_t i = 0; i < N; ++i)
{
const T a = analytic.at(i, 0);
const T n = numeric.at(i, 0);
const T absErr = std::abs(a - n);
const T relErr = absErr / (std::max(std::abs(a), std::abs(n)) + std::numeric_limits<T>::min());
if (absErr > tol && relErr > tol)
ADD_FAILURE() << "component[" << i << "] analytic=" << a << " numeric=" << n
<< " absErr=" << absErr << " relErr=" << relErr << " tol=" << tol;
}
}

template<typename T, std::size_t N, typename Func>
void ExpectGradientNear(const math::Vector<T, N>& analytic, Func f, const math::Vector<T, N>& x, T h, T tol)
{
math::Vector<T, N> steps;
for (std::size_t i = 0; i < N; ++i)
steps.at(i, 0) = h;
ExpectGradientNear(analytic, f, x, steps, tol);
}

template<typename T, std::size_t N, typename Func>
void ExpectGradientNear(const math::Vector<T, N>& analytic, Func f, const math::Vector<T, N>& x, T tol)
{
ExpectGradientNear(analytic, f, x, DefaultFiniteDifferenceSteps(x), tol);
}
}
Loading