From fc7f5c379fd2fcc44e9e1c7a284bccf0d26fcd48 Mon Sep 17 00:00:00 2001 From: gfs Date: Fri, 14 Aug 2026 19:07:21 +0000 Subject: [PATCH 1/4] fix(math,filters): correct scale-dependent Cholesky threshold, inverted consistency gate, and missing Deriv! in Savitzky-Golay kernel (#257 #258 #259) - CholeskyDecomposition: replace absolute 1e-10f pivot guard with eps*|a[i,i]| relative threshold so small-magnitude SPD matrices are not falsely rejected - ConsistencyMetrics: replace clamped table lookup with Wilson-Hilferty chi-squared approximation for dof > kMaxChiSquareDim; old code made the acceptance window shrink to zero for large averaging windows - SavitzkyGolayFilter: multiply kernel by Deriv! so derivative output matches the true polynomial derivative; guard optional dereference from Cholesky solve before use Co-Authored-By: Claude Sonnet 4.6 --- .../filters/passive/SavitzkyGolayFilter.hpp | 14 ++++++++++ .../passive/test/TestSavitzkyGolayFilter.cpp | 2 +- numerical/math/CholeskyDecomposition.hpp | 3 ++- numerical/math/ConsistencyMetrics.hpp | 26 ++++++++++++++----- .../math/test/TestConsistencyMetrics.cpp | 17 +++++++----- 5 files changed, 48 insertions(+), 14 deletions(-) diff --git a/numerical/filters/passive/SavitzkyGolayFilter.hpp b/numerical/filters/passive/SavitzkyGolayFilter.hpp index b3bdf459..4ce1b1fd 100644 --- a/numerical/filters/passive/SavitzkyGolayFilter.hpp +++ b/numerical/filters/passive/SavitzkyGolayFilter.hpp @@ -15,6 +15,14 @@ namespace filters::passive { namespace detail { + constexpr std::size_t Factorial(std::size_t n) + { + std::size_t result = 1; + for (std::size_t i = 2; i <= n; ++i) + result *= i; + return result; + } + template constexpr math::Matrix BuildVandermonde() { @@ -43,12 +51,18 @@ namespace filters::passive eDeriv.at(Deriv, 0) = T{ 1 }; auto z = math::CholeskyDecomposition::Solve(AtA, eDeriv); + if (!z.has_value()) + return std::array{}; + constexpr T scale = static_cast(Factorial(Deriv)); std::array coeffs{}; for (std::size_t k = 0; k < Window; ++k) for (std::size_t d = 0; d < Order + 1; ++d) coeffs[k] += A.at(k, d) * z->at(d, 0); + for (std::size_t k = 0; k < Window; ++k) + coeffs[k] *= scale; + return coeffs; } } diff --git a/numerical/filters/passive/test/TestSavitzkyGolayFilter.cpp b/numerical/filters/passive/test/TestSavitzkyGolayFilter.cpp index c93caa1a..33b92394 100644 --- a/numerical/filters/passive/test/TestSavitzkyGolayFilter.cpp +++ b/numerical/filters/passive/test/TestSavitzkyGolayFilter.cpp @@ -150,7 +150,7 @@ TEST_F(TestSavitzkyGolayFilter, second_derivative_of_quadratic_recovers_curvatur float x = static_cast(n); float out = d2.Filter(a * x * x + 5.0f * x - 2.0f); if (n >= 4) - EXPECT_NEAR(out, a, tol); + EXPECT_NEAR(out, 2.0f * a, tol); } } diff --git a/numerical/math/CholeskyDecomposition.hpp b/numerical/math/CholeskyDecomposition.hpp index f14391bb..a2660633 100644 --- a/numerical/math/CholeskyDecomposition.hpp +++ b/numerical/math/CholeskyDecomposition.hpp @@ -9,6 +9,7 @@ #include "numerical/math/Matrix.hpp" #include "numerical/math/TriangularSolve.hpp" #include +#include #include namespace math @@ -50,7 +51,7 @@ namespace math if (i != j) l.at(i, j) = T(sum / ToFloat(l.at(j, j))); - else if (sum < 1e-10f) + else if (sum <= std::numeric_limits::epsilon() * math::Abs(ToFloat(a.at(i, i)))) return std::nullopt; else l.at(i, j) = T(math::Sqrt(sum)); diff --git a/numerical/math/ConsistencyMetrics.hpp b/numerical/math/ConsistencyMetrics.hpp index 4eaf50b7..4b66e5cf 100644 --- a/numerical/math/ConsistencyMetrics.hpp +++ b/numerical/math/ConsistencyMetrics.hpp @@ -27,6 +27,16 @@ namespace math 5.023886f, 7.377759f, 9.348404f, 11.143480f, 12.832502f, 14.449376f, 16.012764f, 17.534546f, 19.022768f, 20.483177f }; + + static constexpr float kZ975 = 1.95996f; + + static float Chi2QuantileApprox(float z, float dof) + { + const float mu = 1.0f - 2.0f / (9.0f * dof); + const float sigma = math::Sqrt(2.0f / (9.0f * dof)); + const float x = mu + z * sigma; + return dof * x * x * x; + } } template @@ -77,12 +87,16 @@ namespace math if (numSamples == 0) return false; const std::size_t dof = numSamples * Dim; - const float lo = (dof <= detail::kMaxChiSquareDim) - ? detail::kChi2Lo95[dof - 1] / static_cast(numSamples) - : detail::kChi2Lo95[detail::kMaxChiSquareDim - 1] / static_cast(numSamples); - const float hi = (dof <= detail::kMaxChiSquareDim) - ? detail::kChi2Hi95[dof - 1] / static_cast(numSamples) - : detail::kChi2Hi95[detail::kMaxChiSquareDim - 1] / static_cast(numSamples); + const float fdof = static_cast(dof); + const float fsamples = static_cast(numSamples); + const float lo_raw = (dof <= detail::kMaxChiSquareDim) + ? detail::kChi2Lo95[dof - 1] + : detail::Chi2QuantileApprox(-detail::kZ975, fdof); + const float hi_raw = (dof <= detail::kMaxChiSquareDim) + ? detail::kChi2Hi95[dof - 1] + : detail::Chi2QuantileApprox(detail::kZ975, fdof); + const float lo = lo_raw / fsamples; + const float hi = hi_raw / fsamples; return static_cast(averagedValue) >= lo && static_cast(averagedValue) <= hi; } diff --git a/numerical/math/test/TestConsistencyMetrics.cpp b/numerical/math/test/TestConsistencyMetrics.cpp index 13cbb83d..1e16ccab 100644 --- a/numerical/math/test/TestConsistencyMetrics.cpp +++ b/numerical/math/test/TestConsistencyMetrics.cpp @@ -258,13 +258,11 @@ TEST_F(ConsistencyMetrics3DTest, IsTimeAveragedConsistentWithOneSample) EXPECT_TRUE(Metrics::IsTimeAveragedConsistent(midpoint, 1)); } -TEST_F(ConsistencyMetrics3DTest, IsTimeAveragedConsistentDofClampBranch) +TEST_F(ConsistencyMetrics3DTest, IsTimeAveragedConsistentLargeDofWilsonHilferty) { - const float lo = math::detail::kChi2Lo95[math::detail::kMaxChiSquareDim - 1] / 5.0f; - const float hi = math::detail::kChi2Hi95[math::detail::kMaxChiSquareDim - 1] / 5.0f; - const float midpoint = (lo + hi) / 2.0f; - - EXPECT_TRUE(Metrics::IsTimeAveragedConsistent(midpoint, 5)); + // dof=15 > kMaxChiSquareDim; WH bounds ≈ [1.248, 5.499] after dividing by 5 + EXPECT_TRUE(Metrics::IsTimeAveragedConsistent(2.373f, 5)); + EXPECT_FALSE(Metrics::IsTimeAveragedConsistent(6.0f, 5)); } TEST_F(ConsistencyMetrics3DTest, IsTimeAveragedConsistentInconsistentOutOfBounds) @@ -273,3 +271,10 @@ TEST_F(ConsistencyMetrics3DTest, IsTimeAveragedConsistentInconsistentOutOfBounds EXPECT_FALSE(Metrics::IsTimeAveragedConsistent(aboveHi, 1)); } + +TEST_F(ConsistencyMetrics1DTest, IsTimeAveragedConsistentLargeNumSamplesUsesApproximation) +{ + // dof=100 >> kMaxChiSquareDim; WH bounds ≈ [0.742, 1.296] — old code gave hi≈0.205 (wrong) + EXPECT_TRUE(Metrics::IsTimeAveragedConsistent(1.0f, 100)); + EXPECT_FALSE(Metrics::IsTimeAveragedConsistent(1.4f, 100)); +} From a47b9917e024f0e99dfc9795643983eca3dd7f06 Mon Sep 17 00:00:00 2001 From: gfs Date: Fri, 14 Aug 2026 19:19:48 +0000 Subject: [PATCH 2/4] fix(math): address reviewer findings and add Cholesky branch coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ConsistencyMetrics: Chi2QuantileApprox changed from static float to [[nodiscard]] OPTIMIZE_FOR_SPEED constexpr to avoid per-TU duplication and match the module's detail-namespace helper pattern - ConsistencyMetrics tests: remove forbidden inline comments from new WilsonHilferty and LargeNumSamples tests - CholeskyDecomposition: add NUMERICAL_TOOLBOX_COVERAGE_BUILD extern template guards for sizes 1–3 - Add CholeskyDecomposition.cpp coverage instantiation source and wire into numerical.math CMakeLists - Add TestCholeskyDecomposition.cpp covering all three branches of TryFactor (off-diagonal, nullopt on non-SPD, sqrt on diagonal) plus a regression test that small-scale SPD matrices are no longer falsely rejected by the new relative threshold Co-Authored-By: Claude Sonnet 4.6 --- numerical/math/CMakeLists.txt | 1 + numerical/math/CholeskyDecomposition.cpp | 8 ++ numerical/math/CholeskyDecomposition.hpp | 6 + numerical/math/ConsistencyMetrics.hpp | 2 +- numerical/math/test/CMakeLists.txt | 1 + .../math/test/TestCholeskyDecomposition.cpp | 110 ++++++++++++++++++ .../math/test/TestConsistencyMetrics.cpp | 2 - 7 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 numerical/math/CholeskyDecomposition.cpp create mode 100644 numerical/math/test/TestCholeskyDecomposition.cpp diff --git a/numerical/math/CMakeLists.txt b/numerical/math/CMakeLists.txt index b341959f..54df9573 100644 --- a/numerical/math/CMakeLists.txt +++ b/numerical/math/CMakeLists.txt @@ -36,6 +36,7 @@ target_sources(numerical.math PRIVATE numerical_add_coverage_sources(numerical.math Math.cpp + CholeskyDecomposition.cpp ComplexNumber.cpp ConsistencyMetrics.cpp Cordic.cpp diff --git a/numerical/math/CholeskyDecomposition.cpp b/numerical/math/CholeskyDecomposition.cpp new file mode 100644 index 00000000..6341c1a7 --- /dev/null +++ b/numerical/math/CholeskyDecomposition.cpp @@ -0,0 +1,8 @@ +#include "numerical/math/CholeskyDecomposition.hpp" + +namespace math +{ + template class CholeskyDecomposition; + template class CholeskyDecomposition; + template class CholeskyDecomposition; +} diff --git a/numerical/math/CholeskyDecomposition.hpp b/numerical/math/CholeskyDecomposition.hpp index a2660633..4cabaf90 100644 --- a/numerical/math/CholeskyDecomposition.hpp +++ b/numerical/math/CholeskyDecomposition.hpp @@ -77,4 +77,10 @@ namespace math const Vector y = SolveLowerTriangular(l.value(), b); return SolveUpperTriangular(l.value().Transpose(), y); } + +#ifdef NUMERICAL_TOOLBOX_COVERAGE_BUILD + extern template class CholeskyDecomposition; + extern template class CholeskyDecomposition; + extern template class CholeskyDecomposition; +#endif } diff --git a/numerical/math/ConsistencyMetrics.hpp b/numerical/math/ConsistencyMetrics.hpp index 4b66e5cf..bb2a720d 100644 --- a/numerical/math/ConsistencyMetrics.hpp +++ b/numerical/math/ConsistencyMetrics.hpp @@ -30,7 +30,7 @@ namespace math static constexpr float kZ975 = 1.95996f; - static float Chi2QuantileApprox(float z, float dof) + [[nodiscard]] OPTIMIZE_FOR_SPEED constexpr float Chi2QuantileApprox(float z, float dof) { const float mu = 1.0f - 2.0f / (9.0f * dof); const float sigma = math::Sqrt(2.0f / (9.0f * dof)); diff --git a/numerical/math/test/CMakeLists.txt b/numerical/math/test/CMakeLists.txt index 5bbcb224..bb07325a 100644 --- a/numerical/math/test/CMakeLists.txt +++ b/numerical/math/test/CMakeLists.txt @@ -11,6 +11,7 @@ target_link_libraries(numerical.math_test PUBLIC numerical_add_qemu_test(numerical.math_test) target_sources(numerical.math_test PRIVATE + TestCholeskyDecomposition.cpp TestComplexNumber.cpp TestMath.cpp TestConsistencyMetrics.cpp diff --git a/numerical/math/test/TestCholeskyDecomposition.cpp b/numerical/math/test/TestCholeskyDecomposition.cpp new file mode 100644 index 00000000..988cd5dd --- /dev/null +++ b/numerical/math/test/TestCholeskyDecomposition.cpp @@ -0,0 +1,110 @@ +#include "numerical/math/CholeskyDecomposition.hpp" +#include "numerical/math/Tolerance.hpp" +#include +#include + +namespace +{ + class CholeskyDecompositionTest : public ::testing::Test + { + protected: + using Chol1 = math::CholeskyDecomposition; + using Chol2 = math::CholeskyDecomposition; + using Mat1 = math::SquareMatrix; + using Mat2 = math::SquareMatrix; + using Vec2 = math::Vector; + }; +} + +TEST_F(CholeskyDecompositionTest, TryFactorScalarSpdMatrixSucceeds) +{ + Mat1 a; + a.at(0, 0) = 4.0f; + + auto l = Chol1::TryFactor(a); + + ASSERT_TRUE(l.has_value()); + EXPECT_NEAR(l->at(0, 0), 2.0f, math::Tolerance()); +} + +TEST_F(CholeskyDecompositionTest, TryFactorSingularScalarReturnsNullopt) +{ + Mat1 a; + a.at(0, 0) = 0.0f; + + EXPECT_FALSE(Chol1::TryFactor(a).has_value()); +} + +TEST_F(CholeskyDecompositionTest, TryFactor2x2SpdMatrixCoversOffDiagonalBranch) +{ + Mat2 a; + a.at(0, 0) = 4.0f; a.at(0, 1) = 2.0f; + a.at(1, 0) = 2.0f; a.at(1, 1) = 3.0f; + + auto l = Chol2::TryFactor(a); + + ASSERT_TRUE(l.has_value()); + EXPECT_NEAR(l->at(0, 0), 2.0f, math::Tolerance()); + EXPECT_NEAR(l->at(1, 0), 1.0f, math::Tolerance()); + EXPECT_NEAR(l->at(1, 1), std::sqrt(2.0f), math::Tolerance()); +} + +TEST_F(CholeskyDecompositionTest, TryFactorNonSpdMatrixReturnsNullopt) +{ + Mat2 a; + a.at(0, 0) = 1.0f; a.at(0, 1) = 2.0f; + a.at(1, 0) = 2.0f; a.at(1, 1) = 1.0f; + + EXPECT_FALSE(Chol2::TryFactor(a).has_value()); +} + +TEST_F(CholeskyDecompositionTest, TryFactorSmallScaleSpdMatrixAccepted) +{ + Mat1 a; + a.at(0, 0) = 5e-11f; + + auto l = Chol1::TryFactor(a); + + ASSERT_TRUE(l.has_value()); + EXPECT_GT(l->at(0, 0), 0.0f); +} + +TEST_F(CholeskyDecompositionTest, FactorNonSpdReturnsZeroMatrix) +{ + Mat2 a; + a.at(0, 0) = 1.0f; a.at(0, 1) = 2.0f; + a.at(1, 0) = 2.0f; a.at(1, 1) = 1.0f; + + auto l = Chol2::Factor(a); + + EXPECT_NEAR(l.at(0, 0), 0.0f, math::Tolerance()); + EXPECT_NEAR(l.at(1, 1), 0.0f, math::Tolerance()); +} + +TEST_F(CholeskyDecompositionTest, SolveDiagonalSystemFindsExactSolution) +{ + Mat2 a; + a.at(0, 0) = 4.0f; a.at(0, 1) = 0.0f; + a.at(1, 0) = 0.0f; a.at(1, 1) = 9.0f; + Vec2 b; + b.at(0, 0) = 8.0f; + b.at(1, 0) = 27.0f; + + auto x = Chol2::Solve(a, b); + + ASSERT_TRUE(x.has_value()); + EXPECT_NEAR(x->at(0, 0), 2.0f, math::Tolerance()); + EXPECT_NEAR(x->at(1, 0), 3.0f, math::Tolerance()); +} + +TEST_F(CholeskyDecompositionTest, SolveNonSpdSystemReturnsNullopt) +{ + Mat2 a; + a.at(0, 0) = 1.0f; a.at(0, 1) = 2.0f; + a.at(1, 0) = 2.0f; a.at(1, 1) = 1.0f; + Vec2 b; + b.at(0, 0) = 1.0f; + b.at(1, 0) = 1.0f; + + EXPECT_FALSE(Chol2::Solve(a, b).has_value()); +} diff --git a/numerical/math/test/TestConsistencyMetrics.cpp b/numerical/math/test/TestConsistencyMetrics.cpp index 1e16ccab..0d1edc4d 100644 --- a/numerical/math/test/TestConsistencyMetrics.cpp +++ b/numerical/math/test/TestConsistencyMetrics.cpp @@ -260,7 +260,6 @@ TEST_F(ConsistencyMetrics3DTest, IsTimeAveragedConsistentWithOneSample) TEST_F(ConsistencyMetrics3DTest, IsTimeAveragedConsistentLargeDofWilsonHilferty) { - // dof=15 > kMaxChiSquareDim; WH bounds ≈ [1.248, 5.499] after dividing by 5 EXPECT_TRUE(Metrics::IsTimeAveragedConsistent(2.373f, 5)); EXPECT_FALSE(Metrics::IsTimeAveragedConsistent(6.0f, 5)); } @@ -274,7 +273,6 @@ TEST_F(ConsistencyMetrics3DTest, IsTimeAveragedConsistentInconsistentOutOfBounds TEST_F(ConsistencyMetrics1DTest, IsTimeAveragedConsistentLargeNumSamplesUsesApproximation) { - // dof=100 >> kMaxChiSquareDim; WH bounds ≈ [0.742, 1.296] — old code gave hi≈0.205 (wrong) EXPECT_TRUE(Metrics::IsTimeAveragedConsistent(1.0f, 100)); EXPECT_FALSE(Metrics::IsTimeAveragedConsistent(1.4f, 100)); } From a272ee722b4937e8c25f85f0536867171eb1a8fc Mon Sep 17 00:00:00 2001 From: gfs Date: Fri, 14 Aug 2026 21:22:37 +0200 Subject: [PATCH 3/4] Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../math/test/TestCholeskyDecomposition.cpp | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/numerical/math/test/TestCholeskyDecomposition.cpp b/numerical/math/test/TestCholeskyDecomposition.cpp index 988cd5dd..7f8ec98c 100644 --- a/numerical/math/test/TestCholeskyDecomposition.cpp +++ b/numerical/math/test/TestCholeskyDecomposition.cpp @@ -38,8 +38,10 @@ TEST_F(CholeskyDecompositionTest, TryFactorSingularScalarReturnsNullopt) TEST_F(CholeskyDecompositionTest, TryFactor2x2SpdMatrixCoversOffDiagonalBranch) { Mat2 a; - a.at(0, 0) = 4.0f; a.at(0, 1) = 2.0f; - a.at(1, 0) = 2.0f; a.at(1, 1) = 3.0f; + a.at(0, 0) = 4.0f; + a.at(0, 1) = 2.0f; + a.at(1, 0) = 2.0f; + a.at(1, 1) = 3.0f; auto l = Chol2::TryFactor(a); @@ -52,8 +54,10 @@ TEST_F(CholeskyDecompositionTest, TryFactor2x2SpdMatrixCoversOffDiagonalBranch) TEST_F(CholeskyDecompositionTest, TryFactorNonSpdMatrixReturnsNullopt) { Mat2 a; - a.at(0, 0) = 1.0f; a.at(0, 1) = 2.0f; - a.at(1, 0) = 2.0f; a.at(1, 1) = 1.0f; + a.at(0, 0) = 1.0f; + a.at(0, 1) = 2.0f; + a.at(1, 0) = 2.0f; + a.at(1, 1) = 1.0f; EXPECT_FALSE(Chol2::TryFactor(a).has_value()); } @@ -72,8 +76,10 @@ TEST_F(CholeskyDecompositionTest, TryFactorSmallScaleSpdMatrixAccepted) TEST_F(CholeskyDecompositionTest, FactorNonSpdReturnsZeroMatrix) { Mat2 a; - a.at(0, 0) = 1.0f; a.at(0, 1) = 2.0f; - a.at(1, 0) = 2.0f; a.at(1, 1) = 1.0f; + a.at(0, 0) = 1.0f; + a.at(0, 1) = 2.0f; + a.at(1, 0) = 2.0f; + a.at(1, 1) = 1.0f; auto l = Chol2::Factor(a); @@ -84,8 +90,10 @@ TEST_F(CholeskyDecompositionTest, FactorNonSpdReturnsZeroMatrix) TEST_F(CholeskyDecompositionTest, SolveDiagonalSystemFindsExactSolution) { Mat2 a; - a.at(0, 0) = 4.0f; a.at(0, 1) = 0.0f; - a.at(1, 0) = 0.0f; a.at(1, 1) = 9.0f; + a.at(0, 0) = 4.0f; + a.at(0, 1) = 0.0f; + a.at(1, 0) = 0.0f; + a.at(1, 1) = 9.0f; Vec2 b; b.at(0, 0) = 8.0f; b.at(1, 0) = 27.0f; @@ -100,8 +108,10 @@ TEST_F(CholeskyDecompositionTest, SolveDiagonalSystemFindsExactSolution) TEST_F(CholeskyDecompositionTest, SolveNonSpdSystemReturnsNullopt) { Mat2 a; - a.at(0, 0) = 1.0f; a.at(0, 1) = 2.0f; - a.at(1, 0) = 2.0f; a.at(1, 1) = 1.0f; + a.at(0, 0) = 1.0f; + a.at(0, 1) = 2.0f; + a.at(1, 0) = 2.0f; + a.at(1, 1) = 1.0f; Vec2 b; b.at(0, 0) = 1.0f; b.at(1, 0) = 1.0f; From fb5655e037ca238b87aa0de9b7f38f030cd6887a Mon Sep 17 00:00:00 2001 From: gfs Date: Fri, 14 Aug 2026 21:27:07 +0200 Subject: [PATCH 4/4] Apply suggestions from code review --- numerical/filters/passive/SavitzkyGolayFilter.hpp | 2 +- numerical/math/ConsistencyMetrics.hpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/numerical/filters/passive/SavitzkyGolayFilter.hpp b/numerical/filters/passive/SavitzkyGolayFilter.hpp index 4ce1b1fd..16c5b05e 100644 --- a/numerical/filters/passive/SavitzkyGolayFilter.hpp +++ b/numerical/filters/passive/SavitzkyGolayFilter.hpp @@ -54,7 +54,7 @@ namespace filters::passive if (!z.has_value()) return std::array{}; - constexpr T scale = static_cast(Factorial(Deriv)); + constexpr auto scale = static_cast(Factorial(Deriv)); std::array coeffs{}; for (std::size_t k = 0; k < Window; ++k) for (std::size_t d = 0; d < Order + 1; ++d) diff --git a/numerical/math/ConsistencyMetrics.hpp b/numerical/math/ConsistencyMetrics.hpp index bb2a720d..5f598127 100644 --- a/numerical/math/ConsistencyMetrics.hpp +++ b/numerical/math/ConsistencyMetrics.hpp @@ -87,8 +87,8 @@ namespace math if (numSamples == 0) return false; const std::size_t dof = numSamples * Dim; - const float fdof = static_cast(dof); - const float fsamples = static_cast(numSamples); + const auto fdof = static_cast(dof); + const auto fsamples = static_cast(numSamples); const float lo_raw = (dof <= detail::kMaxChiSquareDim) ? detail::kChi2Lo95[dof - 1] : detail::Chi2QuantileApprox(-detail::kZ975, fdof);