test: preserve Dev verification contracts through Refactor APIs
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
@@ -426,3 +427,266 @@ TEST(JetLinearSolve, FailedChannelSolvePreservesCandidateAndInputs) {
|
||||
}
|
||||
} // namespace
|
||||
} // namespace fesa::math_internal
|
||||
|
||||
namespace fesa::dense_blas_internal {
|
||||
TEST(DenseBlasInternal, DiagonalProductKeepsEntrywiseCancellationAndChecks) {
|
||||
Vector diagonal{3U};
|
||||
diagonal[0U] = 4;
|
||||
diagonal[1U] = 2;
|
||||
diagonal[2U] = -1;
|
||||
Vector input{3U};
|
||||
const double large = std::ldexp(1.0, 56);
|
||||
input[0U] = 1;
|
||||
input[1U] = -large;
|
||||
input[2U] = 2 * large;
|
||||
const auto product = MultiplyDiagonal(diagonal, input);
|
||||
EXPECT_DOUBLE_EQ(product[0U] + (product[1U] - product[2U]), 4);
|
||||
EXPECT_EQ(MultiplyDiagonal(Vector{0U}, Vector{0U}).Size(), 0U);
|
||||
EXPECT_THROW(static_cast<void>(MultiplyDiagonal(diagonal, Vector{2U})),
|
||||
std::invalid_argument);
|
||||
input[0U] = std::numeric_limits<double>::infinity();
|
||||
EXPECT_THROW(static_cast<void>(MultiplyDiagonal(diagonal, input)),
|
||||
std::invalid_argument);
|
||||
input[0U] = 1;
|
||||
diagonal[0U] = std::numeric_limits<double>::quiet_NaN();
|
||||
EXPECT_THROW(static_cast<void>(MultiplyDiagonal(diagonal, input)),
|
||||
std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST(DenseBlasInternal, LocalDenseScopeRestoresTlsAfterProductsAndUnwind) {
|
||||
const int previous = mkl_set_num_threads_local(0);
|
||||
struct Restore {
|
||||
int previous;
|
||||
~Restore() { static_cast<void>(mkl_set_num_threads_local(previous)); }
|
||||
} restore{previous};
|
||||
const int default_threads = mkl_get_max_threads();
|
||||
const std::array<double, 2U> left{0.0, 2.0};
|
||||
const std::array<double, 2U> right{3.0, 4.0};
|
||||
std::array<double, 4U> product{};
|
||||
product.fill(std::numeric_limits<double>::quiet_NaN());
|
||||
{
|
||||
const ScopedLocalDenseThreads scope{};
|
||||
EXPECT_EQ(mkl_get_max_threads(), 1);
|
||||
EXPECT_TRUE(
|
||||
OuterProduct(left.data(), 2U, right.data(), 2U, product.data()));
|
||||
EXPECT_DOUBLE_EQ(product[0U], 0.0);
|
||||
EXPECT_DOUBLE_EQ(product[3U], 8.0);
|
||||
}
|
||||
EXPECT_EQ(mkl_get_max_threads(), default_threads);
|
||||
EXPECT_EQ(mkl_set_num_threads_local(0), 0);
|
||||
try {
|
||||
const ScopedLocalDenseThreads scope{};
|
||||
OuterProduct(nullptr, 2U, right.data(), 2U, product.data());
|
||||
} catch (const std::invalid_argument&) {
|
||||
}
|
||||
EXPECT_EQ(mkl_set_num_threads_local(0), 0);
|
||||
static_cast<void>(mkl_set_num_threads_local(3));
|
||||
{
|
||||
const ScopedLocalDenseThreads scope{};
|
||||
EXPECT_EQ(mkl_get_max_threads(), 3);
|
||||
}
|
||||
EXPECT_EQ(mkl_get_max_threads(), 3);
|
||||
}
|
||||
|
||||
TEST(DenseBlasInternal, OuterProductLeavesFiniteInputOverflowForOwnerGate) {
|
||||
const std::array<double, 1U> left{std::numeric_limits<double>::max()};
|
||||
const std::array<double, 1U> right{2.0};
|
||||
std::array<double, 1U> product{};
|
||||
EXPECT_TRUE(OuterProduct(left.data(), 1U, right.data(), 1U, product.data()));
|
||||
EXPECT_TRUE(std::isinf(product[0U]));
|
||||
}
|
||||
|
||||
TEST(DenseBlasInternal,
|
||||
OuterProductUsesRectangularStorageAndPreservesRejection) {
|
||||
const std::array<double, 2U> left{2.0, -3.0};
|
||||
const std::array<double, 3U> right{4.0, -5.0, 0.25};
|
||||
std::array<double, 6U> product{};
|
||||
ASSERT_TRUE(OuterProduct(left.data(), left.size(), right.data(), right.size(),
|
||||
product.data()));
|
||||
const std::array<double, 6U> expected{8.0, -10.0, 0.5, -12.0, 15.0, -0.75};
|
||||
EXPECT_EQ(product, expected);
|
||||
const auto accepted = product;
|
||||
const std::array<double, 2U> invalid{
|
||||
1.0, std::numeric_limits<double>::quiet_NaN()};
|
||||
EXPECT_FALSE(OuterProduct(invalid.data(), invalid.size(), right.data(),
|
||||
right.size(), product.data()));
|
||||
EXPECT_EQ(product, accepted);
|
||||
EXPECT_TRUE(OuterProduct(nullptr, 0U, nullptr, 3U, nullptr));
|
||||
EXPECT_TRUE(OuterProduct(nullptr, 2U, nullptr, 0U, nullptr));
|
||||
EXPECT_THROW(OuterProduct(nullptr, 2U, right.data(), 3U, product.data()),
|
||||
std::invalid_argument);
|
||||
const auto too_large =
|
||||
static_cast<std::size_t>((std::numeric_limits<MKL_INT>::max)()) + 1U;
|
||||
EXPECT_THROW(OuterProduct(nullptr, too_large, nullptr, 1U, nullptr),
|
||||
std::length_error);
|
||||
const auto too_many_elements =
|
||||
std::numeric_limits<std::size_t>::max() / sizeof(double) + 1U;
|
||||
EXPECT_THROW(OuterProduct(nullptr, too_many_elements, nullptr, 1U, nullptr),
|
||||
std::length_error);
|
||||
}
|
||||
|
||||
TEST(DenseBlasInternal, PolicyQueryPreservesDefaultPositiveAndNestedTls) {
|
||||
const int previous = mkl_set_num_threads_local(0);
|
||||
struct Restore {
|
||||
int previous;
|
||||
~Restore() { static_cast<void>(mkl_set_num_threads_local(previous)); }
|
||||
} restore{previous};
|
||||
const int global = mkl_get_max_threads();
|
||||
const int domain = mkl_domain_get_max_threads(MKL_DOMAIN_BLAS);
|
||||
const int dynamic = mkl_get_dynamic();
|
||||
for (int repeat = 0; repeat < 2; ++repeat) {
|
||||
const auto policy = ReadScopedLocalDensePolicy();
|
||||
EXPECT_EQ(policy.blas_max_threads, 1);
|
||||
EXPECT_EQ(policy.dynamic, dynamic);
|
||||
EXPECT_EQ(mkl_set_num_threads_local(0), 0);
|
||||
EXPECT_EQ(mkl_get_max_threads(), global);
|
||||
EXPECT_EQ(mkl_domain_get_max_threads(MKL_DOMAIN_BLAS), domain);
|
||||
EXPECT_EQ(mkl_get_dynamic(), dynamic);
|
||||
}
|
||||
static_cast<void>(mkl_set_num_threads_local(2));
|
||||
EXPECT_EQ(ReadScopedLocalDensePolicy().blas_max_threads, 2);
|
||||
EXPECT_EQ(mkl_set_num_threads_local(2), 2);
|
||||
static_cast<void>(mkl_set_num_threads_local(0));
|
||||
try {
|
||||
const ScopedLocalDenseThreads outer{};
|
||||
EXPECT_EQ(ReadScopedLocalDensePolicy().blas_max_threads, 1);
|
||||
EXPECT_EQ(mkl_set_num_threads_local(1), 1);
|
||||
throw std::runtime_error{"policy-query-unwind"};
|
||||
} catch (const std::runtime_error&) {
|
||||
}
|
||||
EXPECT_EQ(mkl_set_num_threads_local(0), 0);
|
||||
EXPECT_EQ(mkl_get_dynamic(), dynamic);
|
||||
}
|
||||
|
||||
TEST(DenseBlasInternal, SingleEquationScopeRestoresTlsAndRespectsOverrides) {
|
||||
const int previous = mkl_set_num_threads_local(0);
|
||||
struct Restore {
|
||||
int previous;
|
||||
~Restore() { static_cast<void>(mkl_set_num_threads_local(previous)); }
|
||||
} restore{previous};
|
||||
const int default_threads = mkl_get_max_threads();
|
||||
{
|
||||
const ScopedSingleEquationThreads scope{1U};
|
||||
EXPECT_EQ(mkl_get_max_threads(), 1);
|
||||
}
|
||||
EXPECT_EQ(mkl_get_max_threads(), default_threads);
|
||||
EXPECT_EQ(mkl_set_num_threads_local(0), 0);
|
||||
{
|
||||
const ScopedSingleEquationThreads scope{2U};
|
||||
EXPECT_EQ(mkl_get_max_threads(), default_threads);
|
||||
}
|
||||
try {
|
||||
const ScopedSingleEquationThreads scope{1U};
|
||||
throw std::runtime_error{"scope failure"};
|
||||
} catch (const std::runtime_error&) {
|
||||
}
|
||||
EXPECT_EQ(mkl_set_num_threads_local(0), 0);
|
||||
static_cast<void>(mkl_set_num_threads_local(3));
|
||||
{
|
||||
const ScopedSingleEquationThreads scope{1U};
|
||||
EXPECT_EQ(mkl_get_max_threads(), 3);
|
||||
}
|
||||
EXPECT_EQ(mkl_get_max_threads(), 3);
|
||||
}
|
||||
|
||||
TEST(DenseBlasInternal, TransposeProductChecksShapeAndKnownRectangularAction) {
|
||||
Matrix left{3U, 2U};
|
||||
Matrix right{3U, 1U};
|
||||
for (std::size_t row = 0; row < 3U; ++row) {
|
||||
left(row, 0U) = static_cast<double>(row + 1U);
|
||||
left(row, 1U) = 2.0;
|
||||
right(row, 0U) = static_cast<double>(row);
|
||||
}
|
||||
const auto product = MultiplyTransposeLeft(left, right);
|
||||
ASSERT_EQ(product.Rows(), 2U);
|
||||
ASSERT_EQ(product.Columns(), 1U);
|
||||
EXPECT_DOUBLE_EQ(product(0U, 0U), 8.0);
|
||||
EXPECT_DOUBLE_EQ(product(1U, 0U), 6.0);
|
||||
EXPECT_THROW(static_cast<void>(MultiplyTransposeLeft(left, Matrix{2U, 1U})),
|
||||
std::invalid_argument);
|
||||
const auto empty = MultiplyTransposeLeft(Matrix{0U, 2U}, Matrix{0U, 3U});
|
||||
EXPECT_EQ(empty.Rows(), 2U);
|
||||
EXPECT_EQ(empty.Columns(), 3U);
|
||||
EXPECT_DOUBLE_EQ(empty(1U, 2U), 0.0);
|
||||
left(0U, 0U) = std::numeric_limits<double>::quiet_NaN();
|
||||
EXPECT_THROW(static_cast<void>(MultiplyTransposeLeft(left, right)),
|
||||
std::invalid_argument);
|
||||
left(0U, 0U) = 1.0;
|
||||
right(0U, 0U) = std::numeric_limits<double>::infinity();
|
||||
EXPECT_THROW(static_cast<void>(MultiplyTransposeLeft(left, right)),
|
||||
std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST(DenseBlasInternal, TransposeRightProductUsesInnerAndRectangularStrides) {
|
||||
const std::array<double, 6U> left{1, 2, 3, 4, 5, 6};
|
||||
const std::array<double, 12U> right{7, 8, 9, 10, 11, 12,
|
||||
13, 14, 15, 16, 17, 18};
|
||||
std::array<double, 8U> product;
|
||||
ASSERT_TRUE(MultiplyTransposeRight(left.data(), 2U, right.data(), 4U, 3U,
|
||||
product.data()));
|
||||
const std::array<double, 8U> expected{50, 68, 86, 104, 122, 167, 212, 257};
|
||||
EXPECT_EQ(product, expected);
|
||||
EXPECT_TRUE(
|
||||
MultiplyTransposeRight(nullptr, 2U, nullptr, 4U, 0U, product.data()));
|
||||
for (const auto value : product) EXPECT_DOUBLE_EQ(value, 0.0);
|
||||
auto nonfinite = right;
|
||||
nonfinite[8U] = std::numeric_limits<double>::infinity();
|
||||
product.fill(31.0);
|
||||
EXPECT_FALSE(MultiplyTransposeRight(left.data(), 2U, nonfinite.data(), 4U, 3U,
|
||||
product.data()));
|
||||
for (const auto value : product) EXPECT_DOUBLE_EQ(value, 31.0);
|
||||
}
|
||||
|
||||
// The former lower/inverse output parameters are now owned factor results.
|
||||
TEST(DenseBlasInternal, CholeskyAndInverseMatchKnownFactorsAndKeepFailedOutput) {
|
||||
Matrix matrix{2U, 2U};
|
||||
matrix(0, 0) = 4;
|
||||
matrix(0, 1) = matrix(1, 0) = 2;
|
||||
matrix(1, 1) = 5;
|
||||
auto factor = DenseFactor::Cholesky(matrix);
|
||||
ASSERT_TRUE(factor.HasValue());
|
||||
const auto lower = factor.Value().Lower();
|
||||
Matrix identity{2U, 2U};
|
||||
identity(0, 0) = identity(1, 1) = 1;
|
||||
const auto inverse = factor.Value().SolveLower(identity);
|
||||
ASSERT_TRUE(lower.HasValue());
|
||||
ASSERT_TRUE(inverse.HasValue());
|
||||
EXPECT_DOUBLE_EQ(lower.Value()(0, 0), 2);
|
||||
EXPECT_DOUBLE_EQ(lower.Value()(1, 0), 1);
|
||||
EXPECT_DOUBLE_EQ(lower.Value()(1, 1), 2);
|
||||
EXPECT_DOUBLE_EQ(lower.Value()(0, 1), 0);
|
||||
EXPECT_DOUBLE_EQ(inverse.Value()(0, 0), .5);
|
||||
EXPECT_DOUBLE_EQ(inverse.Value()(1, 0), -.25);
|
||||
EXPECT_DOUBLE_EQ(inverse.Value()(1, 1), .5);
|
||||
matrix(1, 1) = 0;
|
||||
EXPECT_FALSE(DenseFactor::Cholesky(matrix).HasValue());
|
||||
EXPECT_DOUBLE_EQ(lower.Value()(1, 1), 2);
|
||||
EXPECT_DOUBLE_EQ(inverse.Value()(1, 1), .5);
|
||||
EXPECT_FALSE(DenseFactor::Cholesky(Matrix{2U, 1U}).HasValue());
|
||||
matrix(0, 0) = std::numeric_limits<double>::quiet_NaN();
|
||||
EXPECT_FALSE(DenseFactor::Cholesky(matrix).HasValue());
|
||||
EXPECT_DOUBLE_EQ(factor.Value().SolveLower(identity).Value()(1, 1), .5);
|
||||
}
|
||||
|
||||
TEST(DenseBlasInternal,
|
||||
CholeskyFailureReasonPreservesCandidatesAndResetsOnReuse) {
|
||||
Matrix matrix{2U, 2U};
|
||||
matrix(0, 0) = 4;
|
||||
matrix(1, 1) = 9;
|
||||
const auto accepted = DenseFactor::Cholesky(matrix);
|
||||
ASSERT_TRUE(accepted.HasValue());
|
||||
Matrix identity{2U, 2U};
|
||||
identity(0, 0) = identity(1, 1) = 1;
|
||||
matrix(1, 1) = -1;
|
||||
const auto failed = DenseFactor::Cholesky(matrix);
|
||||
ASSERT_FALSE(failed.HasValue());
|
||||
ASSERT_FALSE(failed.GetStatus().Diagnostics().empty());
|
||||
EXPECT_EQ(failed.GetStatus().Diagnostics()[0].code, "dense-factor-pivot");
|
||||
EXPECT_DOUBLE_EQ(accepted.Value().Lower().Value()(1, 1), 3);
|
||||
EXPECT_DOUBLE_EQ(accepted.Value().SolveLower(identity).Value()(1, 1), 1.0 / 3.0);
|
||||
matrix(1, 1) = 9;
|
||||
EXPECT_TRUE(DenseFactor::Cholesky(matrix).HasValue());
|
||||
EXPECT_FALSE(DenseFactor::Cholesky(Matrix{2U, 1U}).HasValue());
|
||||
EXPECT_TRUE(DenseFactor::Cholesky(matrix).HasValue());
|
||||
}
|
||||
} // namespace fesa::dense_blas_internal
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
#include "../../../src/fesa/math/high_low_arithmetic.h"
|
||||
#include "elements/hex8_pair_simd.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include "elements/hex8_pair_simd.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
#include "high_low_arithmetic_original.h"
|
||||
|
||||
namespace {
|
||||
using namespace fesa::math_internal;
|
||||
@@ -126,9 +132,111 @@ TEST(HighLowArithmetic, RejectsChangedRoundingAndFlushToZeroEnvironment) {
|
||||
EXPECT_FALSE(ArithmeticEnvironmentSupported());
|
||||
#endif
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace {
|
||||
std::uint64_t Bits(const double value) {
|
||||
std::uint64_t result = 0;
|
||||
std::memcpy(&result, &value, sizeof(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
double FromBits(const std::uint64_t bits) {
|
||||
double result = 0;
|
||||
std::memcpy(&result, &bits, sizeof(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
TEST(HighLowArithmetic, FiniteClassificationMatchesBinary64Boundaries) {
|
||||
const auto check = [](const double value) {
|
||||
const bool expected = std::isfinite(value);
|
||||
EXPECT_EQ(IsFinite(HighLow{value, 0.0}), expected);
|
||||
EXPECT_EQ(IsFinite(HighLow{1.0, value}), expected);
|
||||
};
|
||||
for (std::uint64_t exponent = 0; exponent < 2048U; ++exponent) {
|
||||
for (const auto fraction : {0ULL, 1ULL, 1ULL << 51U,
|
||||
(1ULL << 52U) - 1ULL}) {
|
||||
for (const auto sign : {0ULL, 1ULL << 63U})
|
||||
check(FromBits(sign | (exponent << 52U) | fraction));
|
||||
}
|
||||
}
|
||||
std::mt19937_64 random{0x5800f17U};
|
||||
for (int sample = 0; sample < 10000; ++sample) check(FromBits(random()));
|
||||
}
|
||||
|
||||
void ExpectSamePair(const HighLow actual,
|
||||
const fesa::high_low_original::HighLow expected) {
|
||||
EXPECT_EQ(Bits(actual.high), Bits(expected.high));
|
||||
EXPECT_EQ(Bits(actual.low), Bits(expected.low));
|
||||
}
|
||||
|
||||
TEST(HighLowArithmetic, BitExponentMatchesFrozenOracleAcrossEveryExponent) {
|
||||
ASSERT_TRUE(ArithmeticEnvironmentSupported());
|
||||
// Every normal exponent, every possible trailing significand-bit count,
|
||||
// both signs; then every subnormal single-bit and mixed-bit boundary.
|
||||
for (std::uint64_t exponent = 1; exponent < 2047; ++exponent) {
|
||||
for (unsigned trailing = 0; trailing <= 52U; ++trailing) {
|
||||
const auto fraction = trailing == 52U ? 0ULL : 1ULL << trailing;
|
||||
for (const auto sign : {0ULL, 1ULL << 63U}) {
|
||||
const double value = FromBits(sign | (exponent << 52U) | fraction);
|
||||
ASSERT_EQ(LowestBitExponent(value),
|
||||
fesa::high_low_original::LowestBitExponent(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (unsigned bit = 0; bit < 52U; ++bit) {
|
||||
for (const auto significand : {1ULL << bit, (1ULL << 52U) - (1ULL << bit)}) {
|
||||
const double value = FromBits(significand);
|
||||
EXPECT_EQ(LowestBitExponent(value),
|
||||
fesa::high_low_original::LowestBitExponent(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(HighLowArithmetic, OptimizedArithmeticMatchesFrozenOriginalBits) {
|
||||
ASSERT_TRUE(ArithmeticEnvironmentSupported());
|
||||
std::vector<double> values{0.0, -0.0,
|
||||
std::numeric_limits<double>::denorm_min(),
|
||||
-std::numeric_limits<double>::denorm_min(),
|
||||
std::numeric_limits<double>::min(),
|
||||
std::nextafter(std::numeric_limits<double>::min(), 0.0),
|
||||
std::numeric_limits<double>::max(),
|
||||
-std::numeric_limits<double>::max(), 1.0, -1.0,
|
||||
std::nextafter(1.0, 2.0), std::ldexp(1.0, -53),
|
||||
std::ldexp(1.0, -54), std::numeric_limits<double>::infinity(),
|
||||
-std::numeric_limits<double>::infinity(),
|
||||
std::numeric_limits<double>::quiet_NaN()};
|
||||
std::mt19937_64 random{0x58a0fU};
|
||||
for (int sample = 0; sample < 4000; ++sample) {
|
||||
values.push_back(FromBits(random()));
|
||||
}
|
||||
for (std::size_t index = 0; index < values.size(); ++index) {
|
||||
const double high = values[index];
|
||||
const double low = values[(index * 13U + 7U) % values.size()];
|
||||
const fesa::high_low_original::HighLow original{high, low};
|
||||
EXPECT_EQ(IsNormalized({high, low}),
|
||||
fesa::high_low_original::IsNormalized(original));
|
||||
// Normalize a raw pair with the frozen operation, never the new predicate.
|
||||
const auto a = fesa::high_low_original::TwoSum(high, low);
|
||||
const auto b = fesa::high_low_original::TwoSum(
|
||||
values[(index + 1U) % values.size()],
|
||||
values[(index * 3U + 2U) % values.size()]);
|
||||
ExpectSamePair(Add({a.high, a.low}, {b.high, b.low}),
|
||||
fesa::high_low_original::Add(a, b));
|
||||
ExpectSamePair(Multiply({a.high, a.low}, {b.high, b.low}),
|
||||
fesa::high_low_original::Multiply(a, b));
|
||||
ExpectSamePair(Divide({a.high, a.low}, {b.high, b.low}),
|
||||
fesa::high_low_original::Divide(a, b));
|
||||
ExpectSamePair(TwoProduct(high, low),
|
||||
fesa::high_low_original::TwoProduct(high, low));
|
||||
}
|
||||
for (double high : {1.0, std::nextafter(1.0, 2.0), -1.0,
|
||||
std::numeric_limits<double>::max()}) {
|
||||
for (double low : {0.0, -0.0, std::ldexp(1.0, -53),
|
||||
-std::ldexp(1.0, -53)}) {
|
||||
EXPECT_EQ(IsNormalized({high, low}),
|
||||
fesa::high_low_original::IsNormalized({high, low}));
|
||||
}
|
||||
}
|
||||
}
|
||||
TEST(HighLowArithmetic, PacketDispatchRetainsComponentsAndInstructionGates) {
|
||||
using namespace fesa::hex8_internal;
|
||||
const std::uint32_t leaf1 = (1U << 12) | (1U << 26) | (1U << 27) | (1U << 28);
|
||||
|
||||
@@ -167,4 +167,21 @@ TEST(DenseMath, MatrixSelfAssignmentPreservesShapeAndStorage) {
|
||||
}
|
||||
|
||||
} // namespace
|
||||
TEST(DenseMath, MatrixProductsRejectNonfiniteOperandsAndPreserveValidReuse) {
|
||||
Matrix left{2U, 2U, 1.0};
|
||||
Matrix right{2U, 1U, 2.0};
|
||||
Vector rhs{2U, 2.0};
|
||||
rhs[1U] = std::numeric_limits<double>::infinity();
|
||||
EXPECT_THROW(static_cast<void>(left.Multiply(rhs)), std::invalid_argument);
|
||||
rhs[1U] = 2.0;
|
||||
left(0U, 1U) = std::numeric_limits<double>::quiet_NaN();
|
||||
EXPECT_THROW(static_cast<void>(left.Multiply(rhs)), std::invalid_argument);
|
||||
EXPECT_THROW(static_cast<void>(left.Multiply(right)), std::invalid_argument);
|
||||
left(0U, 1U) = 1.0;
|
||||
right(1U, 0U) = std::numeric_limits<double>::infinity();
|
||||
EXPECT_THROW(static_cast<void>(left.Multiply(right)), std::invalid_argument);
|
||||
right(1U, 0U) = 2.0;
|
||||
EXPECT_DOUBLE_EQ(left.Multiply(rhs)[0U], 4.0);
|
||||
EXPECT_DOUBLE_EQ(left.Multiply(right)(1U, 0U), 4.0);
|
||||
}
|
||||
} // namespace fesa
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <future>
|
||||
#include <limits>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
@@ -131,4 +133,104 @@ TEST(SparseAssembly, PreservesExpectedStructuralZeros) {
|
||||
4);
|
||||
}
|
||||
|
||||
TEST(SparseAssembly, MultipliesRectangularCopyMoveAndConcurrentConstCalls) {
|
||||
auto assembled = SparseMatrix::FromCoo(
|
||||
3U, 5U, {{0U, 4U, 3.0, 0U, 0U}, {2U, 1U, -2.0, 1U, 0U}},
|
||||
{{0U, 1U, 1U, 2U}, {4U, 1U}});
|
||||
ASSERT_TRUE(assembled.HasValue());
|
||||
const auto original = assembled.Value();
|
||||
auto copied = original;
|
||||
auto moved = std::move(copied);
|
||||
Vector rhs{5U, 2.0};
|
||||
const auto evaluate = [&rhs](const SparseMatrix& matrix) {
|
||||
for (int repeat = 0; repeat < 20; ++repeat) {
|
||||
const auto product = matrix.Multiply(rhs);
|
||||
if (product.Size() != 3U || product[0U] != 6.0 ||
|
||||
product[1U] != 0.0 || product[2U] != -4.0) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
auto first = std::async(std::launch::async, [&] { return evaluate(moved); });
|
||||
auto second = std::async(std::launch::async, [&] { return evaluate(moved); });
|
||||
EXPECT_TRUE(first.get());
|
||||
EXPECT_TRUE(second.get());
|
||||
EXPECT_TRUE(evaluate(original));
|
||||
auto replacement = SparseMatrix::FromCoo(
|
||||
1U, 1U, {{0U, 0U, 7.0, 0U, 0U}}, {{0U, 1U}, {0U}});
|
||||
ASSERT_TRUE(replacement.HasValue());
|
||||
moved = replacement.Value();
|
||||
EXPECT_DOUBLE_EQ(moved.Multiply(Vector{1U, 3.0})[0U], 21.0);
|
||||
EXPECT_TRUE(evaluate(original));
|
||||
}
|
||||
|
||||
TEST(SparseAssembly, PreservesZeroShapesAndEmptyRows) {
|
||||
auto no_rows = SparseMatrix::FromCoo(0U, 3U, {}, {{0U}, {}});
|
||||
auto no_columns = SparseMatrix::FromCoo(2U, 0U, {}, {{0U, 0U, 0U}, {}});
|
||||
auto no_entries = SparseMatrix::FromCoo(2U, 3U, {}, {{0U, 0U, 0U}, {}});
|
||||
ASSERT_TRUE(no_rows.HasValue());
|
||||
ASSERT_TRUE(no_columns.HasValue());
|
||||
ASSERT_TRUE(no_entries.HasValue());
|
||||
EXPECT_EQ(no_rows.Value().Multiply(Vector{3U}).Size(), 0U);
|
||||
const auto empty_columns = no_columns.Value().Multiply(Vector{0U});
|
||||
const auto empty_entries = no_entries.Value().Multiply(Vector{3U});
|
||||
ASSERT_EQ(empty_columns.Size(), 2U);
|
||||
ASSERT_EQ(empty_entries.Size(), 2U);
|
||||
for (std::size_t row = 0; row < 2U; ++row) {
|
||||
EXPECT_DOUBLE_EQ(empty_columns[row], 0.0);
|
||||
EXPECT_DOUBLE_EQ(empty_entries[row], 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(SparseAssembly, RejectsNonfiniteRhsAndRecoversAfterFiniteOverflow) {
|
||||
auto matrix = SparseMatrix::FromCoo(
|
||||
2U, 2U, {{0U, 0U, 2.0, 0U, 0U}}, {{0U, 1U, 2U}, {0U, 1U}});
|
||||
ASSERT_TRUE(matrix.HasValue());
|
||||
Vector rhs{2U, (std::numeric_limits<double>::infinity)()};
|
||||
EXPECT_THROW(static_cast<void>(matrix.Value().Multiply(rhs)),
|
||||
std::invalid_argument);
|
||||
rhs[1U] = 4.0;
|
||||
rhs[0U] = (std::numeric_limits<double>::max)();
|
||||
EXPECT_TRUE(std::isinf(matrix.Value().Multiply(rhs)[0U]));
|
||||
rhs[0U] = (std::numeric_limits<double>::quiet_NaN)();
|
||||
EXPECT_THROW(static_cast<void>(matrix.Value().Multiply(rhs)),
|
||||
std::invalid_argument);
|
||||
EXPECT_THROW(static_cast<void>(matrix.Value().Multiply(Vector{1U})),
|
||||
std::invalid_argument);
|
||||
rhs[0U] = 3.0;
|
||||
rhs[1U] = 4.0;
|
||||
const auto finite = matrix.Value().Multiply(rhs);
|
||||
EXPECT_DOUBLE_EQ(finite[0U], 6.0);
|
||||
EXPECT_DOUBLE_EQ(finite[1U], 0.0);
|
||||
}
|
||||
|
||||
TEST(SparseAssembly, MatchesIndependentLongDoubleResidualOnUnevenRows) {
|
||||
const std::vector<double> entries{1.25, -0.375, 4.5, -3.0, 0.125, 2.0};
|
||||
const SparsePattern pattern{{0U, 3U, 3U, 5U, 6U}, {0U, 2U, 4U, 1U, 3U, 2U}};
|
||||
std::vector<CooContribution> contributions;
|
||||
for (std::size_t row = 0; row < 4U; ++row) {
|
||||
for (auto position = pattern.row_offsets[row];
|
||||
position < pattern.row_offsets[row + 1U]; ++position) {
|
||||
contributions.push_back(
|
||||
{row, pattern.column_indices[position], entries[position], row,
|
||||
position});
|
||||
}
|
||||
}
|
||||
auto matrix = SparseMatrix::FromCoo(4U, 5U, contributions, pattern);
|
||||
ASSERT_TRUE(matrix.HasValue());
|
||||
Vector rhs{5U};
|
||||
for (std::size_t column = 0; column < 5U; ++column) {
|
||||
rhs[column] = 0.3 + static_cast<double>(column) * 0.7;
|
||||
}
|
||||
const auto product = matrix.Value().Multiply(rhs);
|
||||
for (std::size_t row = 0; row < 4U; ++row) {
|
||||
long double expected = 0;
|
||||
for (auto position = pattern.row_offsets[row];
|
||||
position < pattern.row_offsets[row + 1U]; ++position) {
|
||||
expected += static_cast<long double>(entries[position]) *
|
||||
rhs[pattern.column_indices[position]];
|
||||
}
|
||||
EXPECT_NEAR(product[row], static_cast<double>(expected), 2.0e-14);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Reference in New Issue
Block a user