feat(beam-reference-qualification): step 0 — comparison-metric-and-entity-matching
This commit is contained in:
@@ -0,0 +1,345 @@
|
||||
#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(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
|
||||
Reference in New Issue
Block a user