#include #include #include #include #include #include #include #include #include #include #include #include namespace { std::array 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 reference, std::vector actual, const fesa::Tolerance tolerance) { return { quantity, std::move(position), std::move(reference), std::move(actual), tolerance, }; } bool has_failure( const std::vector& 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{&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{&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 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::quiet_NaN()}, {1.0e-5, 1.0e-6}); const auto report = fesa::compare_samples( std::span{&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 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{&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{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{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{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{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