feat(beam-reference-qualification): step 0 — comparison-metric-and-entity-matching
This commit is contained in:
@@ -47,6 +47,7 @@ add_library(fesa_core STATIC
|
||||
src/fesa/model/domain_builder.cpp
|
||||
src/fesa/results/result_database.cpp
|
||||
src/fesa/solvers/linear/pardiso_linear_solver.cpp
|
||||
src/fesa/validation/comparison.cpp
|
||||
)
|
||||
|
||||
target_include_directories(fesa_core
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <fesa/core/diagnostic.hpp>
|
||||
#include <fesa/results/result_database.hpp>
|
||||
|
||||
namespace fesa {
|
||||
|
||||
enum class ReferenceQuantity {
|
||||
displacement,
|
||||
reaction,
|
||||
internal_force,
|
||||
centroid_stress
|
||||
};
|
||||
|
||||
struct Tolerance final {
|
||||
double relative;
|
||||
double absolute_scale;
|
||||
};
|
||||
|
||||
struct ResultPosition final {
|
||||
std::string instance_name;
|
||||
std::int64_t entity_label;
|
||||
std::optional<std::int64_t> end_node_label;
|
||||
};
|
||||
|
||||
struct ComparisonSample final {
|
||||
ReferenceQuantity quantity;
|
||||
ResultPosition position;
|
||||
std::vector<double> reference;
|
||||
std::vector<double> actual;
|
||||
Tolerance tolerance;
|
||||
};
|
||||
|
||||
struct ComparisonReport final {
|
||||
bool passed;
|
||||
double maximum_normalized_error;
|
||||
std::vector<Diagnostic> failures;
|
||||
};
|
||||
|
||||
struct ComparisonSampleMatch final {
|
||||
std::optional<ComparisonSample> sample;
|
||||
std::vector<Diagnostic> failures;
|
||||
};
|
||||
|
||||
[[nodiscard]] ComparisonSampleMatch make_comparison_sample(
|
||||
const ResultFrame& frame,
|
||||
ReferenceQuantity quantity,
|
||||
const ResultPosition& position,
|
||||
std::span<const double> reference,
|
||||
Tolerance tolerance);
|
||||
|
||||
[[nodiscard]] ComparisonReport compare_samples(
|
||||
std::span<const ComparisonSample> samples);
|
||||
|
||||
} // namespace fesa
|
||||
@@ -0,0 +1,442 @@
|
||||
#include <fesa/validation/comparison.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <iomanip>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
|
||||
using PositionKey = std::tuple<
|
||||
ReferenceQuantity,
|
||||
std::string,
|
||||
std::int64_t,
|
||||
std::optional<std::int64_t>>;
|
||||
|
||||
void add_failure(
|
||||
std::vector<Diagnostic>& failures,
|
||||
std::string code,
|
||||
std::string message) {
|
||||
failures.push_back({
|
||||
DiagnosticStage::validation,
|
||||
Severity::error,
|
||||
std::move(code),
|
||||
std::move(message),
|
||||
std::nullopt,
|
||||
});
|
||||
}
|
||||
|
||||
std::string_view quantity_name(const ReferenceQuantity quantity) {
|
||||
switch (quantity) {
|
||||
case ReferenceQuantity::displacement:
|
||||
return "displacement";
|
||||
case ReferenceQuantity::reaction:
|
||||
return "reaction";
|
||||
case ReferenceQuantity::internal_force:
|
||||
return "internal_force";
|
||||
case ReferenceQuantity::centroid_stress:
|
||||
return "centroid_stress";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
std::size_t expected_component_count(
|
||||
const ReferenceQuantity quantity) {
|
||||
switch (quantity) {
|
||||
case ReferenceQuantity::displacement:
|
||||
case ReferenceQuantity::reaction:
|
||||
case ReferenceQuantity::internal_force:
|
||||
return 6;
|
||||
case ReferenceQuantity::centroid_stress:
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string component_name(
|
||||
const ReferenceQuantity quantity,
|
||||
const std::size_t index) {
|
||||
switch (quantity) {
|
||||
case ReferenceQuantity::displacement:
|
||||
return std::string{NodalFrame::displacement_components[index]};
|
||||
case ReferenceQuantity::reaction:
|
||||
return std::string{NodalFrame::reaction_components[index]};
|
||||
case ReferenceQuantity::internal_force:
|
||||
return std::string{
|
||||
BeamElementFrame::section_force_components[index]};
|
||||
case ReferenceQuantity::centroid_stress:
|
||||
return std::string{BeamElementFrame::axial_stress_component};
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
std::string number_text(const double value) {
|
||||
std::ostringstream stream;
|
||||
stream << std::setprecision(std::numeric_limits<double>::max_digits10)
|
||||
<< value;
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
std::string tolerance_text(const Tolerance tolerance) {
|
||||
return " relative_tolerance=" + number_text(tolerance.relative) +
|
||||
" absolute_scale=" + number_text(tolerance.absolute_scale);
|
||||
}
|
||||
|
||||
std::string position_text(
|
||||
const ReferenceQuantity quantity,
|
||||
const ResultPosition& position) {
|
||||
std::string text = "quantity=" + std::string{quantity_name(quantity)} +
|
||||
" instance=" + position.instance_name +
|
||||
" entity=" + std::to_string(position.entity_label);
|
||||
if (position.end_node_label.has_value()) {
|
||||
text += " end_node=" +
|
||||
std::to_string(*position.end_node_label);
|
||||
} else {
|
||||
text += " end_node=n/a";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
std::string scalar_failure_text(
|
||||
const ComparisonSample& sample,
|
||||
const std::size_t component,
|
||||
const double normalized_error) {
|
||||
return position_text(sample.quantity, sample.position) +
|
||||
" component=" + component_name(sample.quantity, component) +
|
||||
" reference=" + number_text(sample.reference[component]) +
|
||||
" actual=" + number_text(sample.actual[component]) +
|
||||
" normalized_error=" + number_text(normalized_error) +
|
||||
tolerance_text(sample.tolerance);
|
||||
}
|
||||
|
||||
std::string unevaluable_failure_text(
|
||||
const ReferenceQuantity quantity,
|
||||
const ResultPosition& position,
|
||||
const Tolerance tolerance,
|
||||
const std::string_view reason) {
|
||||
return position_text(quantity, position) +
|
||||
" component=n/a reference=n/a actual=n/a "
|
||||
"normalized_error=inf" + tolerance_text(tolerance) +
|
||||
" reason=" + std::string{reason};
|
||||
}
|
||||
|
||||
bool origin_matches(
|
||||
const EntityOrigin& origin,
|
||||
const std::string& instance_name,
|
||||
const std::int64_t local_label) {
|
||||
return origin.instance_name == instance_name &&
|
||||
origin.local_label == local_label;
|
||||
}
|
||||
|
||||
ComparisonSampleMatch matching_failure(
|
||||
const ReferenceQuantity quantity,
|
||||
const ResultPosition& position,
|
||||
const Tolerance tolerance,
|
||||
std::string code,
|
||||
const std::string_view reason) {
|
||||
std::vector<Diagnostic> failures;
|
||||
add_failure(
|
||||
failures,
|
||||
std::move(code),
|
||||
unevaluable_failure_text(
|
||||
quantity, position, tolerance, reason));
|
||||
return {std::nullopt, std::move(failures)};
|
||||
}
|
||||
|
||||
std::vector<double> as_vector(const std::array<double, 6>& values) {
|
||||
return {values.begin(), values.end()};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ComparisonSampleMatch make_comparison_sample(
|
||||
const ResultFrame& frame,
|
||||
const ReferenceQuantity quantity,
|
||||
const ResultPosition& position,
|
||||
const std::span<const double> reference,
|
||||
const Tolerance tolerance) {
|
||||
std::vector<double> actual;
|
||||
|
||||
if (
|
||||
quantity == ReferenceQuantity::displacement ||
|
||||
quantity == ReferenceQuantity::reaction) {
|
||||
if (position.end_node_label.has_value()) {
|
||||
return matching_failure(
|
||||
quantity,
|
||||
position,
|
||||
tolerance,
|
||||
"validation.invalid_result_position",
|
||||
"nodal_position_has_end_node");
|
||||
}
|
||||
|
||||
std::optional<std::size_t> matched_index;
|
||||
for (
|
||||
std::size_t index = 0;
|
||||
index < frame.nodal.origins.size();
|
||||
++index) {
|
||||
if (origin_matches(
|
||||
frame.nodal.origins[index],
|
||||
position.instance_name,
|
||||
position.entity_label)) {
|
||||
if (matched_index.has_value()) {
|
||||
return matching_failure(
|
||||
quantity,
|
||||
position,
|
||||
tolerance,
|
||||
"validation.unknown_result_origin",
|
||||
"ambiguous_result_origin");
|
||||
}
|
||||
matched_index = index;
|
||||
}
|
||||
}
|
||||
if (!matched_index.has_value()) {
|
||||
return matching_failure(
|
||||
quantity,
|
||||
position,
|
||||
tolerance,
|
||||
"validation.unknown_result_origin",
|
||||
"unknown_result_origin");
|
||||
}
|
||||
|
||||
const auto& field =
|
||||
quantity == ReferenceQuantity::displacement
|
||||
? frame.nodal.displacement
|
||||
: frame.nodal.reaction;
|
||||
if (*matched_index >= field.size()) {
|
||||
return matching_failure(
|
||||
quantity,
|
||||
position,
|
||||
tolerance,
|
||||
"validation.component_count_mismatch",
|
||||
"missing_actual_components");
|
||||
}
|
||||
actual = as_vector(field[*matched_index]);
|
||||
} else {
|
||||
if (!position.end_node_label.has_value()) {
|
||||
return matching_failure(
|
||||
quantity,
|
||||
position,
|
||||
tolerance,
|
||||
"validation.invalid_element_node_pair",
|
||||
"missing_end_node");
|
||||
}
|
||||
|
||||
const BeamElementFrame* matched_beam = nullptr;
|
||||
for (const BeamElementFrame& beam : frame.element.beams) {
|
||||
if (origin_matches(
|
||||
beam.origin,
|
||||
position.instance_name,
|
||||
position.entity_label)) {
|
||||
if (matched_beam != nullptr) {
|
||||
return matching_failure(
|
||||
quantity,
|
||||
position,
|
||||
tolerance,
|
||||
"validation.unknown_result_origin",
|
||||
"ambiguous_result_origin");
|
||||
}
|
||||
matched_beam = &beam;
|
||||
}
|
||||
}
|
||||
if (matched_beam == nullptr) {
|
||||
return matching_failure(
|
||||
quantity,
|
||||
position,
|
||||
tolerance,
|
||||
"validation.unknown_result_origin",
|
||||
"unknown_result_origin");
|
||||
}
|
||||
|
||||
std::optional<NodeId> end_node;
|
||||
for (
|
||||
std::size_t index = 0;
|
||||
index < frame.nodal.origins.size() &&
|
||||
index < frame.nodal.node_ids.size();
|
||||
++index) {
|
||||
if (origin_matches(
|
||||
frame.nodal.origins[index],
|
||||
position.instance_name,
|
||||
*position.end_node_label)) {
|
||||
if (end_node.has_value()) {
|
||||
return matching_failure(
|
||||
quantity,
|
||||
position,
|
||||
tolerance,
|
||||
"validation.unknown_result_origin",
|
||||
"ambiguous_end_node_origin");
|
||||
}
|
||||
end_node = frame.nodal.node_ids[index];
|
||||
}
|
||||
}
|
||||
if (!end_node.has_value()) {
|
||||
return matching_failure(
|
||||
quantity,
|
||||
position,
|
||||
tolerance,
|
||||
"validation.unknown_result_origin",
|
||||
"unknown_end_node_origin");
|
||||
}
|
||||
|
||||
const BeamSectionResult* matched_end = nullptr;
|
||||
for (const BeamSectionResult& end : matched_beam->end_results) {
|
||||
if (end.end_node == *end_node) {
|
||||
matched_end = &end;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (matched_end == nullptr) {
|
||||
return matching_failure(
|
||||
quantity,
|
||||
position,
|
||||
tolerance,
|
||||
"validation.invalid_element_node_pair",
|
||||
"node_is_not_element_end");
|
||||
}
|
||||
|
||||
if (quantity == ReferenceQuantity::internal_force) {
|
||||
actual = as_vector(matched_end->section_force);
|
||||
} else {
|
||||
actual = {matched_end->centroid_sigma_xx};
|
||||
}
|
||||
}
|
||||
|
||||
ComparisonSample matched{
|
||||
quantity,
|
||||
position,
|
||||
{reference.begin(), reference.end()},
|
||||
std::move(actual),
|
||||
tolerance,
|
||||
};
|
||||
return {std::move(matched), {}};
|
||||
}
|
||||
|
||||
ComparisonReport compare_samples(
|
||||
const std::span<const ComparisonSample> samples) {
|
||||
ComparisonReport report{true, 0.0, {}};
|
||||
std::set<PositionKey> positions;
|
||||
|
||||
for (const ComparisonSample& sample : samples) {
|
||||
const PositionKey key{
|
||||
sample.quantity,
|
||||
sample.position.instance_name,
|
||||
sample.position.entity_label,
|
||||
sample.position.end_node_label,
|
||||
};
|
||||
if (!positions.insert(key).second) {
|
||||
add_failure(
|
||||
report.failures,
|
||||
"validation.duplicate_result_position",
|
||||
unevaluable_failure_text(
|
||||
sample.quantity,
|
||||
sample.position,
|
||||
sample.tolerance,
|
||||
"duplicate_result_position"));
|
||||
report.maximum_normalized_error =
|
||||
std::numeric_limits<double>::infinity();
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::size_t expected =
|
||||
expected_component_count(sample.quantity);
|
||||
if (
|
||||
sample.reference.size() != expected ||
|
||||
sample.actual.size() != expected) {
|
||||
add_failure(
|
||||
report.failures,
|
||||
"validation.component_count_mismatch",
|
||||
unevaluable_failure_text(
|
||||
sample.quantity,
|
||||
sample.position,
|
||||
sample.tolerance,
|
||||
"component_count_mismatch") +
|
||||
" expected=" + std::to_string(expected) +
|
||||
" reference_count=" +
|
||||
std::to_string(sample.reference.size()) +
|
||||
" actual_count=" +
|
||||
std::to_string(sample.actual.size()));
|
||||
report.maximum_normalized_error =
|
||||
std::numeric_limits<double>::infinity();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
!std::isfinite(sample.tolerance.relative) ||
|
||||
!std::isfinite(sample.tolerance.absolute_scale) ||
|
||||
sample.tolerance.relative < 0.0 ||
|
||||
sample.tolerance.absolute_scale < 0.0) {
|
||||
add_failure(
|
||||
report.failures,
|
||||
"validation.invalid_tolerance",
|
||||
unevaluable_failure_text(
|
||||
sample.quantity,
|
||||
sample.position,
|
||||
sample.tolerance,
|
||||
"invalid_tolerance"));
|
||||
report.maximum_normalized_error =
|
||||
std::numeric_limits<double>::infinity();
|
||||
continue;
|
||||
}
|
||||
|
||||
for (std::size_t component = 0; component < expected; ++component) {
|
||||
const double reference = sample.reference[component];
|
||||
const double actual = sample.actual[component];
|
||||
if (!std::isfinite(reference) || !std::isfinite(actual)) {
|
||||
add_failure(
|
||||
report.failures,
|
||||
"validation.nonfinite_comparison_value",
|
||||
scalar_failure_text(
|
||||
sample,
|
||||
component,
|
||||
std::numeric_limits<double>::infinity()));
|
||||
report.maximum_normalized_error =
|
||||
std::numeric_limits<double>::infinity();
|
||||
continue;
|
||||
}
|
||||
|
||||
const double denominator =
|
||||
sample.tolerance.absolute_scale +
|
||||
sample.tolerance.relative * std::abs(reference);
|
||||
if (!(denominator > 0.0) || !std::isfinite(denominator)) {
|
||||
add_failure(
|
||||
report.failures,
|
||||
"validation.invalid_tolerance",
|
||||
scalar_failure_text(
|
||||
sample,
|
||||
component,
|
||||
std::numeric_limits<double>::infinity()));
|
||||
report.maximum_normalized_error =
|
||||
std::numeric_limits<double>::infinity();
|
||||
continue;
|
||||
}
|
||||
|
||||
const double normalized_error =
|
||||
std::abs(actual - reference) / denominator;
|
||||
report.maximum_normalized_error = std::max(
|
||||
report.maximum_normalized_error,
|
||||
normalized_error);
|
||||
if (!(normalized_error <= 1.0)) {
|
||||
add_failure(
|
||||
report.failures,
|
||||
"validation.tolerance_exceeded",
|
||||
scalar_failure_text(
|
||||
sample, component, normalized_error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
report.passed = report.failures.empty();
|
||||
return report;
|
||||
}
|
||||
|
||||
} // namespace fesa
|
||||
@@ -743,3 +743,32 @@ set_property(
|
||||
PROPERTY ENVIRONMENT_MODIFICATION
|
||||
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
|
||||
)
|
||||
|
||||
add_executable(fesa_validation_comparison_tests
|
||||
unit/validation/comparison_test.cpp
|
||||
)
|
||||
|
||||
target_compile_features(
|
||||
fesa_validation_comparison_tests PRIVATE cxx_std_20
|
||||
)
|
||||
target_compile_options(
|
||||
fesa_validation_comparison_tests PRIVATE /W4 /permissive- /EHsc
|
||||
)
|
||||
target_link_libraries(
|
||||
fesa_validation_comparison_tests
|
||||
PRIVATE
|
||||
fesa_core
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME ComparisonMetric
|
||||
COMMAND "$<TARGET_FILE:fesa_validation_comparison_tests>"
|
||||
--gtest_filter=ComparisonMetric.*
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME EntityMatching
|
||||
COMMAND "$<TARGET_FILE:fesa_validation_comparison_tests>"
|
||||
--gtest_filter=EntityMatching.*
|
||||
)
|
||||
|
||||
@@ -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