Files
FESA/tests/unit/validation/comparison_test.cpp
T

406 lines
13 KiB
C++

#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <fesa/validation/comparison.hpp>
namespace {
std::array<double, 6> values(
const double first,
const double second,
const double third,
const double fourth,
const double fifth,
const double sixth) {
return {first, second, third, fourth, fifth, sixth};
}
fesa::ResultFrame result_frame() {
fesa::NodalFrame nodal{
{fesa::NodeId{10}, fesa::NodeId{11}, fesa::NodeId{12}},
{
fesa::EntityOrigin{"BeamPart", "Part-1-1", 101},
fesa::EntityOrigin{"BeamPart", "Part-1-1", 102},
fesa::EntityOrigin{"BeamPart", "Part-1-1", 103},
},
{
values(1.0, 2.0, 3.0, 4.0, 5.0, 6.0),
values(7.0, 8.0, 9.0, 10.0, 11.0, 12.0),
values(0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
},
{
values(13.0, 14.0, 15.0, 16.0, 17.0, 18.0),
values(19.0, 20.0, 21.0, 22.0, 23.0, 24.0),
values(0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
},
};
fesa::BeamElementFrame beam{
fesa::ElementId{20},
fesa::EntityOrigin{"BeamPart", "Part-1-1", 501},
{
fesa::Vec3{1.0, 0.0, 0.0},
fesa::Vec3{0.0, 1.0, 0.0},
fesa::Vec3{0.0, 0.0, 1.0},
},
{{
{
-1.0,
fesa::NodeId{10},
values(0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
values(25.0, 26.0, 27.0, 28.0, 29.0, 30.0),
31.0,
{},
},
{
1.0,
fesa::NodeId{11},
values(0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
values(32.0, 33.0, 34.0, 35.0, 36.0, 37.0),
38.0,
{},
},
}},
};
return {1.0, std::move(nodal), {{std::move(beam)}}, {}};
}
fesa::ComparisonSample sample(
const fesa::ReferenceQuantity quantity,
fesa::ResultPosition position,
std::vector<double> reference,
std::vector<double> actual,
const fesa::Tolerance tolerance) {
return {
quantity,
std::move(position),
std::move(reference),
std::move(actual),
tolerance,
};
}
bool has_failure(
const std::vector<fesa::Diagnostic>& failures,
const std::string_view code) {
return std::ranges::any_of(
failures,
[code](const fesa::Diagnostic& failure) {
return failure.stage == fesa::DiagnosticStage::validation &&
failure.severity == fesa::Severity::error &&
failure.code == code;
});
}
TEST(ComparisonMetric, RejectsNearZeroErrorBeyondAbsoluteScale) {
const auto input = sample(
fesa::ReferenceQuantity::displacement,
{"Part-1-1", 101, std::nullopt},
{0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
{2.0e-9, 0.0, 0.0, 0.0, 0.0, 0.0},
{1.0e-5, 1.0e-9});
const auto report = fesa::compare_samples(
std::span<const fesa::ComparisonSample>{&input, 1});
ASSERT_FALSE(report.passed);
EXPECT_DOUBLE_EQ(report.maximum_normalized_error, 2.0);
ASSERT_EQ(report.failures.size(), 1U);
EXPECT_EQ(report.failures[0].code, "validation.tolerance_exceeded");
EXPECT_NE(
report.failures[0].message.find("quantity=displacement"),
std::string::npos);
EXPECT_NE(
report.failures[0].message.find("entity=101"),
std::string::npos);
EXPECT_NE(
report.failures[0].message.find("component=Ux"),
std::string::npos);
EXPECT_NE(
report.failures[0].message.find("reference="),
std::string::npos);
EXPECT_NE(
report.failures[0].message.find("actual="),
std::string::npos);
EXPECT_NE(
report.failures[0].message.find("normalized_error="),
std::string::npos);
EXPECT_NE(
report.failures[0].message.find("relative_tolerance="),
std::string::npos);
EXPECT_NE(
report.failures[0].message.find("absolute_scale="),
std::string::npos);
}
TEST(ComparisonMetric, RejectsRepresentativeLargeRelativeError) {
const auto input = sample(
fesa::ReferenceQuantity::reaction,
{"Part-1-1", 101, std::nullopt},
{1.0e6, 1.0e6, 1.0e6, 1.0e6, 1.0e6, 1.0e6},
{1.0e6 + 20.0, 1.0e6, 1.0e6, 1.0e6, 1.0e6, 1.0e6},
{1.0e-5, 0.0});
const auto report = fesa::compare_samples(
std::span<const fesa::ComparisonSample>{&input, 1});
EXPECT_FALSE(report.passed);
EXPECT_DOUBLE_EQ(report.maximum_normalized_error, 2.0);
EXPECT_TRUE(has_failure(
report.failures, "validation.tolerance_exceeded"));
}
TEST(ComparisonMetric, AcceptsErrorsAtTheExplicitToleranceBoundary) {
const std::vector<fesa::ComparisonSample> inputs{
sample(
fesa::ReferenceQuantity::displacement,
{"Part-1-1", 101, std::nullopt},
{0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
{1.0e-9, 0.0, 0.0, 0.0, 0.0, 0.0},
{1.0e-5, 1.0e-9}),
sample(
fesa::ReferenceQuantity::reaction,
{"Part-1-1", 101, std::nullopt},
{1.0e6, 1.0e6, 1.0e6, 1.0e6, 1.0e6, 1.0e6},
{1.0e6 + 10.0, 1.0e6, 1.0e6, 1.0e6, 1.0e6, 1.0e6},
{1.0e-5, 0.0}),
};
const auto report = fesa::compare_samples(inputs);
EXPECT_TRUE(report.passed);
EXPECT_DOUBLE_EQ(report.maximum_normalized_error, 1.0);
EXPECT_TRUE(report.failures.empty());
}
TEST(ComparisonMetric, RejectsNonfiniteValues) {
const auto input = sample(
fesa::ReferenceQuantity::centroid_stress,
{"Part-1-1", 501, 101},
{10.0},
{std::numeric_limits<double>::quiet_NaN()},
{1.0e-5, 1.0e-6});
const auto report = fesa::compare_samples(
std::span<const fesa::ComparisonSample>{&input, 1});
EXPECT_FALSE(report.passed);
EXPECT_TRUE(std::isinf(report.maximum_normalized_error));
EXPECT_TRUE(has_failure(
report.failures, "validation.nonfinite_comparison_value"));
}
TEST(ComparisonMetric, RejectsDuplicateQuantityAndPosition) {
const std::vector<fesa::ComparisonSample> inputs{
sample(
fesa::ReferenceQuantity::displacement,
{"Part-1-1", 101, std::nullopt},
{0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
{0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
{1.0e-5, 1.0e-9}),
sample(
fesa::ReferenceQuantity::displacement,
{"Part-1-1", 101, std::nullopt},
{1.0, 1.0, 1.0, 1.0, 1.0, 1.0},
{1.0, 1.0, 1.0, 1.0, 1.0, 1.0},
{1.0e-5, 1.0e-9}),
};
const auto report = fesa::compare_samples(inputs);
EXPECT_FALSE(report.passed);
EXPECT_TRUE(std::isinf(report.maximum_normalized_error));
EXPECT_TRUE(has_failure(
report.failures, "validation.duplicate_result_position"));
}
TEST(ComparisonMetric, RejectsComponentCountMismatch) {
const auto input = sample(
fesa::ReferenceQuantity::internal_force,
{"Part-1-1", 501, 101},
{1.0, 2.0, 3.0, 4.0, 5.0, 6.0},
{1.0, 2.0, 3.0, 4.0, 5.0},
{1.0e-5, 1.0e-9});
const auto report = fesa::compare_samples(
std::span<const fesa::ComparisonSample>{&input, 1});
EXPECT_FALSE(report.passed);
EXPECT_TRUE(std::isinf(report.maximum_normalized_error));
EXPECT_TRUE(has_failure(
report.failures, "validation.component_count_mismatch"));
}
TEST(CorrelationMetric, ComputesComponentWiseRmseAndRelativeL2) {
const std::vector<fesa::ComparisonSample> inputs{
sample(
fesa::ReferenceQuantity::displacement,
{"Part-1-1", 101, std::nullopt},
{3.0, 0.0, 0.0, 0.0, 0.0, 0.0},
{0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
{0.0, 1.0e-9}),
sample(
fesa::ReferenceQuantity::displacement,
{"Part-1-1", 102, std::nullopt},
{4.0, 0.0, 0.0, 0.0, 0.0, 0.0},
{0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
{0.0, 1.0e-9}),
};
const auto report = fesa::correlate_samples(inputs);
ASSERT_TRUE(report.evaluable);
ASSERT_TRUE(report.failures.empty());
ASSERT_EQ(report.metrics.size(), 6U);
EXPECT_EQ(report.metrics[0].quantity, fesa::ReferenceQuantity::displacement);
EXPECT_EQ(report.metrics[0].component_index, 0U);
EXPECT_EQ(report.metrics[0].value_count, 2U);
EXPECT_DOUBLE_EQ(
report.metrics[0].root_mean_square_error,
std::sqrt(12.5));
EXPECT_DOUBLE_EQ(report.metrics[0].relative_l2_error, 1.0);
EXPECT_DOUBLE_EQ(report.metrics[1].root_mean_square_error, 0.0);
EXPECT_DOUBLE_EQ(report.metrics[1].relative_l2_error, 0.0);
}
TEST(CorrelationMetric, UsesAbsoluteScaleNormForNearZeroReference) {
const std::vector<fesa::ComparisonSample> inputs{
sample(
fesa::ReferenceQuantity::reaction,
{"Part-1-1", 101, std::nullopt},
{0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
{3.0e-9, 0.0, 0.0, 0.0, 0.0, 0.0},
{0.0, 1.0e-9}),
sample(
fesa::ReferenceQuantity::reaction,
{"Part-1-1", 102, std::nullopt},
{0.0, 0.0, 0.0, 0.0, 0.0, 0.0},
{4.0e-9, 0.0, 0.0, 0.0, 0.0, 0.0},
{0.0, 1.0e-9}),
};
const auto report = fesa::correlate_samples(inputs);
ASSERT_TRUE(report.evaluable);
ASSERT_EQ(report.metrics.size(), 6U);
EXPECT_DOUBLE_EQ(
report.metrics[0].root_mean_square_error,
std::sqrt(12.5) * 1.0e-9);
EXPECT_DOUBLE_EQ(
report.metrics[0].relative_l2_error,
5.0 / std::sqrt(2.0));
}
TEST(EntityMatching, MatchesNodalResultByInstanceAndExternalLabel) {
const auto frame = result_frame();
const std::array reference{0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
const auto match = fesa::make_comparison_sample(
frame,
fesa::ReferenceQuantity::displacement,
{"Part-1-1", 102, std::nullopt},
reference,
{1.0e-5, 1.0e-9});
ASSERT_TRUE(match.sample.has_value());
EXPECT_TRUE(match.failures.empty());
EXPECT_EQ(
match.sample->actual,
(std::vector<double>{7.0, 8.0, 9.0, 10.0, 11.0, 12.0}));
}
TEST(EntityMatching, MatchesReactionComponentsWithoutUsingDisplacement) {
const auto frame = result_frame();
const std::array reference{0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
const auto match = fesa::make_comparison_sample(
frame,
fesa::ReferenceQuantity::reaction,
{"Part-1-1", 101, std::nullopt},
reference,
{1.0e-5, 1.0e-9});
ASSERT_TRUE(match.sample.has_value());
EXPECT_TRUE(match.failures.empty());
EXPECT_EQ(
match.sample->actual,
(std::vector<double>{13.0, 14.0, 15.0, 16.0, 17.0, 18.0}));
}
TEST(EntityMatching, MatchesElementResultByElementAndEndNodeLabels) {
const auto frame = result_frame();
const std::array reference{0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
const auto match = fesa::make_comparison_sample(
frame,
fesa::ReferenceQuantity::internal_force,
{"Part-1-1", 501, 102},
reference,
{1.0e-5, 1.0e-9});
ASSERT_TRUE(match.sample.has_value());
EXPECT_TRUE(match.failures.empty());
EXPECT_EQ(
match.sample->actual,
(std::vector<double>{32.0, 33.0, 34.0, 35.0, 36.0, 37.0}));
}
TEST(EntityMatching, MatchesCentroidStressAtTheRequestedElementEnd) {
const auto frame = result_frame();
const std::array reference{0.0};
const auto match = fesa::make_comparison_sample(
frame,
fesa::ReferenceQuantity::centroid_stress,
{"Part-1-1", 501, 101},
reference,
{1.0e-5, 1.0e-9});
ASSERT_TRUE(match.sample.has_value());
EXPECT_TRUE(match.failures.empty());
EXPECT_EQ(match.sample->actual, (std::vector<double>{31.0}));
}
TEST(EntityMatching, RejectsUnknownResultOrigin) {
const auto frame = result_frame();
const std::array reference{0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
const auto match = fesa::make_comparison_sample(
frame,
fesa::ReferenceQuantity::reaction,
{"Part-1-1", 999, std::nullopt},
reference,
{1.0e-5, 1.0e-9});
EXPECT_FALSE(match.sample.has_value());
EXPECT_TRUE(has_failure(
match.failures, "validation.unknown_result_origin"));
}
TEST(EntityMatching, RejectsNodeThatIsNotAnEndOfTheElement) {
const auto frame = result_frame();
const std::array reference{0.0};
const auto match = fesa::make_comparison_sample(
frame,
fesa::ReferenceQuantity::centroid_stress,
{"Part-1-1", 501, 103},
reference,
{1.0e-5, 1.0e-9});
EXPECT_FALSE(match.sample.has_value());
EXPECT_TRUE(has_failure(
match.failures, "validation.invalid_element_node_pair"));
}
} // namespace