modify tolerance policy

This commit is contained in:
KOKO\Mimi
2026-08-18 02:29:26 +09:00
parent 123a1b6ccc
commit 624ea4872c
36 changed files with 1909 additions and 672 deletions
+2
View File
@@ -76,6 +76,8 @@ target_link_libraries(
add_executable(
fesa_reference_tests
reference/reference_tolerance_policy.cpp
reference/reference_tolerance_policy_test.cpp
reference/reference_comparison.cpp
reference/reference_comparison_test.cpp
reference/b33_reference_comparison_test.cpp
@@ -26,8 +26,8 @@
namespace {
constexpr const char* kStressPath = "/steps/Step-1/frames/0/element/stress_s11";
constexpr std::size_t kExpectedRowCount = 176U;
constexpr std::size_t kExpectedMetricCount = 16U;
constexpr std::size_t kExpectedRowCount = 212U;
constexpr std::size_t kExpectedMetricCount = 6U;
class Hdf5Handle {
public:
@@ -192,7 +192,10 @@ TEST(B33ReferenceComparison, GeneratesAuthoritativeHdf5AndComparisonEvidence) {
report.rows.begin(), report.rows.end(),
[](const fesa::test::RowDecision& row) {
return row.passed && std::isfinite(row.absolute_error) &&
std::isfinite(row.tolerance) && row.tolerance > 0.0 &&
std::isfinite(row.tolerance) && row.tolerance >= 0.0 &&
std::isfinite(row.reference_scale) &&
std::isfinite(row.near_zero_band) &&
std::isfinite(row.relative_error) &&
row.fesa.model_id == "cantilever-beam-b33" &&
row.reference.model_id == "cantilever-beam-b33" &&
row.fesa.step_name == "Step-1" &&
@@ -206,11 +209,11 @@ TEST(B33ReferenceComparison, GeneratesAuthoritativeHdf5AndComparisonEvidence) {
std::all_of(report.metrics.begin(), report.metrics.end(),
[](const fesa::test::ComponentMetrics& metric) {
return std::isfinite(metric.reference_scale) &&
std::isfinite(metric.near_zero_band) &&
std::isfinite(metric.maximum_absolute_error) &&
std::isfinite(metric.maximum_normalized_error) &&
std::isfinite(metric.rms_error) &&
std::isfinite(metric.norm_error) &&
metric.maximum_normalized_error <= 1.0;
std::isfinite(metric.relative_rms) &&
metric.rms_passed && metric.passed &&
metric.row_count > 0U && !metric.components.empty();
}));
EXPECT_FALSE(report.stress_comparison_applicable);
@@ -234,6 +237,9 @@ TEST(B33ReferenceComparison, GeneratesAuthoritativeHdf5AndComparisonEvidence) {
EXPECT_NE(json.find("\"stress_comparison_applicable\":false"),
std::string::npos);
EXPECT_NE(json.find("\"physics_evidence\""), std::string::npos);
EXPECT_NE(json.find("\"source_element_label\""), std::string::npos);
EXPECT_NE(json.find("\"family_identity\""), std::string::npos);
EXPECT_NE(json.find("\"row_count\""), std::string::npos);
std::vector<std::string> generated_names;
for (const auto& entry :
+30 -7
View File
@@ -122,7 +122,7 @@ void ExpectCommonMetadata(const fesa::test::Mitc4ComparisonReport& report,
void ExpectComparisonCoverage(const fesa::test::Mitc4ComparisonReport& report) {
ASSERT_EQ(report.rows.size(), kNodeCount * kComponentCount);
ASSERT_EQ(report.metrics.size(), kComponentCount);
ASSERT_EQ(report.metrics.size(), 2U);
ASSERT_EQ(report.vector_metrics.size(), kNodeCount);
EXPECT_TRUE(report.passed);
const std::size_t blocking_rows = static_cast<std::size_t>(std::count_if(
@@ -135,12 +135,35 @@ void ExpectComparisonCoverage(const fesa::test::Mitc4ComparisonReport& report) {
[](const fesa::test::Mitc4RowDecision& row) {
return !row.blocking || row.within_tolerance;
}));
EXPECT_EQ(report.warnings.size(),
static_cast<std::size_t>(
std::count_if(report.rows.begin(), report.rows.end(),
[](const fesa::test::Mitc4RowDecision& row) {
return !row.blocking && !row.within_tolerance;
})));
const auto rotation_metric =
std::find_if(report.metrics.begin(), report.metrics.end(),
[](const fesa::test::Mitc4ComponentMetrics& metric) {
return !metric.blocking;
});
ASSERT_NE(rotation_metric, report.metrics.end());
const std::size_t row_warning_count = static_cast<std::size_t>(
std::count_if(report.rows.begin(), report.rows.end(),
[](const fesa::test::Mitc4RowDecision& row) {
return !row.blocking && !row.within_tolerance;
}));
ASSERT_EQ(report.warnings.size(),
row_warning_count + (rotation_metric->rms_passed ? 0U : 1U));
std::size_t warning_index = 0U;
for (std::size_t row_index = 0U; row_index < report.rows.size();
++row_index) {
const auto& row = report.rows[row_index];
if (!row.blocking && !row.within_tolerance) {
EXPECT_EQ(report.warnings[warning_index].code,
"rotation-reference-exceedance");
EXPECT_EQ(report.warnings[warning_index].row, row_index);
++warning_index;
}
}
if (!rotation_metric->rms_passed) {
EXPECT_EQ(report.warnings[warning_index].code,
"rotation-reference-rms-exceedance");
EXPECT_EQ(report.warnings[warning_index].row, rotation_metric->worst_row);
}
}
// MITC4-E2E-S4-001
+154 -79
View File
@@ -24,6 +24,8 @@
#include <utility>
#include <vector>
#include "reference_tolerance_policy.h"
namespace fesa::test {
namespace {
@@ -32,7 +34,6 @@ constexpr const char* kDisplacementPath =
constexpr const char* kInternalFormulation = "FESA-MITC4";
constexpr const char* kIntegrationRule =
"2x2x2-gauss; mitc4-edge-midpoint-shear";
constexpr double kFixedAbsoluteTolerance = 1.0e-5;
constexpr std::array<const char*, 6> kComponents{"U1", "U2", "U3",
"UR1", "UR2", "UR3"};
const std::vector<std::string> expected_header{"Part Instance Name",
@@ -648,6 +649,18 @@ std::string FiniteText(const double value) {
return stream.str();
}
const char* BranchName(const ReferenceToleranceBranch branch) {
switch (branch) {
case ReferenceToleranceBranch::kNearZero:
return "near-zero";
case ReferenceToleranceBranch::kRelative:
return "relative";
case ReferenceToleranceBranch::kZeroScaleExact:
return "zero-scale-exact";
}
return "unknown";
}
std::string JsonEscape(const std::string& value) {
std::ostringstream stream;
for (const unsigned char character : value) {
@@ -687,6 +700,18 @@ std::string JsonEscape(const std::string& value) {
return stream.str();
}
void WriteJsonStrings(std::ostream& stream,
const std::vector<std::string>& values) {
stream << '[';
for (std::size_t index = 0U; index < values.size(); ++index) {
if (index != 0U) {
stream << ',';
}
stream << '"' << JsonEscape(values[index]) << '"';
}
stream << ']';
}
} // namespace
Result<Mitc4ComparisonReport> Mitc4ReferenceComparison::Compare(
@@ -713,14 +738,6 @@ Result<Mitc4ComparisonReport> Mitc4ReferenceComparison::Compare(
}
}
std::array<double, 6> scales{};
for (const auto& row : reference_rows) {
for (std::size_t component = 0U; component < scales.size(); ++component) {
scales[component] =
(std::max)(scales[component], std::abs(row.values[component]));
}
}
Mitc4ComparisonReport report{};
report.case_id = reference_case.case_id;
report.source_element_type = std::move(hdf5.source_element_type);
@@ -729,80 +746,126 @@ Result<Mitc4ComparisonReport> Mitc4ReferenceComparison::Compare(
report.passed = true;
report.rows.reserve(hdf5.rows.size() * kComponents.size());
report.vector_metrics.reserve(hdf5.rows.size());
std::array<double, 6> error_norms{};
std::array<double, 6> maximum_errors{};
std::array<double, 6> maximum_normalized{};
std::array<std::size_t, 6> worst_rows{};
double global_worst_normalized = -1.0;
std::array<std::vector<std::size_t>, 2> family_rows;
for (const auto& hdf5_row : hdf5.rows) {
const auto reference = reference_by_identity.find(hdf5_row.identity);
if (reference == reference_by_identity.end()) {
Fail("schema-mismatch", "A projected reference row is missing.");
}
std::array<double, 6> errors{};
for (std::size_t component = 0U; component < kComponents.size();
++component) {
const double tolerance = kFixedAbsoluteTolerance;
const double absolute_error = std::abs(
hdf5_row.values[component] - reference->second->values[component]);
const double normalized_error = absolute_error / tolerance;
if (!std::isfinite(tolerance) || !(tolerance > 0.0) ||
!std::isfinite(absolute_error) ||
!std::isfinite(normalized_error)) {
Fail("schema-mismatch",
"A finite row produced a nonfinite comparison metric.");
}
const bool blocking = component < 3U;
const bool within_tolerance = absolute_error <= tolerance;
const std::size_t row_index = report.rows.size();
report.rows.push_back(
{reference_case.case_id, hdf5_row.identity.instance_name,
hdf5_row.identity.source_node_label, kComponents[component],
hdf5_row.values[component], reference->second->values[component],
absolute_error, tolerance, normalized_error, blocking,
within_tolerance});
errors[component] = absolute_error;
error_norms[component] =
std::hypot(error_norms[component], absolute_error);
if (normalized_error > maximum_normalized[component]) {
maximum_normalized[component] = normalized_error;
maximum_errors[component] = absolute_error;
worst_rows[component] = row_index;
}
if (normalized_error > global_worst_normalized) {
global_worst_normalized = normalized_error;
report.worst_row = row_index;
}
if (blocking && !within_tolerance) {
report.passed = false;
} else if (!blocking && !within_tolerance) {
const std::string message =
"case=" + reference_case.case_id +
";instance=" + hdf5_row.identity.instance_name +
";node=" + std::to_string(hdf5_row.identity.source_node_label) +
";component=" + kComponents[component] +
";absolute_error=" + FiniteText(absolute_error) +
";tolerance=" + FiniteText(tolerance);
report.warnings.push_back(
{"rotation-reference-exceedance", row_index, message});
}
report.rows.push_back({reference_case.case_id,
hdf5_row.identity.instance_name,
hdf5_row.identity.source_node_label,
kComponents[component],
hdf5_row.values[component],
reference->second->values[component],
0.0,
0.0,
0.0,
0.0,
0.0,
false,
{},
blocking,
false});
family_rows[blocking ? 0U : 1U].push_back(row_index);
}
report.vector_metrics.push_back(
{hdf5_row.identity.instance_name, hdf5_row.identity.source_node_label,
std::hypot(errors[0U], errors[1U], errors[2U]),
std::hypot(errors[3U], errors[4U], errors[5U])});
}
report.metrics.reserve(kComponents.size());
const double row_count = static_cast<double>(hdf5.rows.size());
for (std::size_t component = 0U; component < kComponents.size();
++component) {
std::vector<ReferenceToleranceFamily> families;
families.reserve(2U);
for (std::size_t family_index = 0U; family_index < family_rows.size();
++family_index) {
ReferenceToleranceFamily family{};
family.identity = family_index == 0U ? "displacement-translation"
: "displacement-rotation";
family.components = family_index == 0U
? std::vector<std::string>{"U1", "U2", "U3"}
: std::vector<std::string>{"UR1", "UR2", "UR3"};
family.blocking = family_index == 0U;
family.rows.reserve(family_rows[family_index].size());
for (const std::size_t row_index : family_rows[family_index]) {
const auto& row = report.rows[row_index];
family.rows.push_back({row.fesa_value, row.reference_value});
}
families.push_back(std::move(family));
}
const auto evaluation = ReferenceTolerancePolicy::Evaluate(families);
if (!evaluation.valid || evaluation.families.size() != families.size()) {
Fail("schema-mismatch",
"A finite row produced a nonfinite comparison metric.");
}
report.metrics.reserve(evaluation.families.size());
double maximum_absolute_error = -1.0;
for (std::size_t family_index = 0U;
family_index < evaluation.families.size(); ++family_index) {
const auto& policy = evaluation.families[family_index];
for (std::size_t local = 0U; local < family_rows[family_index].size();
++local) {
const std::size_t row_index = family_rows[family_index][local];
auto& row = report.rows[row_index];
const auto& decision = policy.rows[local];
row.absolute_error = decision.absolute_error;
row.tolerance = decision.threshold;
row.normalized_error = decision.relative_error;
row.reference_scale = policy.reference_scale;
row.near_zero_band = policy.near_zero_band;
row.relative_error_applicable = decision.relative_error_applicable;
row.tolerance_branch = BranchName(decision.branch);
row.within_tolerance = decision.passed;
if (row.absolute_error > maximum_absolute_error) {
maximum_absolute_error = row.absolute_error;
report.worst_row = row_index;
}
if (!row.within_tolerance) {
if (row.blocking) {
report.passed = false;
} else {
const std::string message =
"case=" + reference_case.case_id +
";instance=" + row.instance_name +
";node=" + std::to_string(row.source_node_label) +
";component=" + row.component +
";absolute_error=" + FiniteText(row.absolute_error) +
";threshold=" + FiniteText(row.tolerance);
report.warnings.push_back(
{"rotation-reference-exceedance", row_index, message});
}
}
}
const std::size_t worst_row = family_rows[family_index][policy.worst_row];
report.metrics.push_back(
{kComponents[component], scales[component], kFixedAbsoluteTolerance,
maximum_errors[component], maximum_normalized[component],
error_norms[component] / std::sqrt(row_count),
error_norms[component], worst_rows[component]});
{policy.identity, policy.components, policy.blocking,
policy.reference_scale, policy.near_zero_band,
policy.near_zero_count, policy.maximum_absolute_error,
policy.relative_rms, worst_row, policy.rms_passed, policy.passed,
policy.diagnostic_code, policy.row_count});
if (policy.blocking && !policy.passed) {
report.passed = false;
}
if (!policy.blocking && !policy.rms_passed) {
report.warnings.push_back(
{"rotation-reference-rms-exceedance", worst_row,
"case=" + reference_case.case_id + ";family=" + policy.identity +
";relative_rms=" + FiniteText(policy.relative_rms)});
}
}
for (std::size_t node = 0U; node < hdf5.rows.size(); ++node) {
const std::size_t row_offset = node * kComponents.size();
report.vector_metrics.push_back(
{report.rows[row_offset].instance_name,
report.rows[row_offset].source_node_label,
std::hypot(report.rows[row_offset].absolute_error,
report.rows[row_offset + 1U].absolute_error,
report.rows[row_offset + 2U].absolute_error),
std::hypot(report.rows[row_offset + 3U].absolute_error,
report.rows[row_offset + 4U].absolute_error,
report.rows[row_offset + 5U].absolute_error)});
}
return Result<Mitc4ComparisonReport>::Success(std::move(report));
} catch (const ComparisonFailure& exception) {
@@ -845,8 +908,14 @@ Status Mitc4ReferenceComparison::WriteDeterministicJson(
<< "\",\"fesa_value\":" << row.fesa_value
<< ",\"reference_value\":" << row.reference_value
<< ",\"absolute_error\":" << row.absolute_error
<< ",\"tolerance\":" << row.tolerance
<< ",\"normalized_error\":" << row.normalized_error
<< ",\"threshold\":" << row.tolerance
<< ",\"relative_error\":" << row.normalized_error
<< ",\"reference_scale\":" << row.reference_scale
<< ",\"near_zero_band\":" << row.near_zero_band
<< ",\"relative_error_applicable\":"
<< (row.relative_error_applicable ? "true" : "false")
<< ",\"tolerance_branch\":\"" << JsonEscape(row.tolerance_branch)
<< "\""
<< ",\"blocking\":" << (row.blocking ? "true" : "false")
<< ",\"within_tolerance\":"
<< (row.within_tolerance ? "true" : "false") << '}';
@@ -857,15 +926,21 @@ Status Mitc4ReferenceComparison::WriteDeterministicJson(
stream << ',';
}
const auto& metric = report.metrics[index];
stream << "{\"component\":\"" << JsonEscape(metric.component)
<< "\",\"reference_scale\":" << metric.reference_scale
<< ",\"tolerance\":" << metric.tolerance
stream << "{\"family_identity\":\"" << JsonEscape(metric.family_identity)
<< "\",\"components\":";
WriteJsonStrings(stream, metric.components);
stream << ",\"blocking\":" << (metric.blocking ? "true" : "false")
<< ",\"reference_scale\":" << metric.reference_scale
<< ",\"near_zero_band\":" << metric.near_zero_band
<< ",\"near_zero_count\":" << metric.near_zero_count
<< ",\"row_count\":" << metric.row_count
<< ",\"maximum_absolute_error\":" << metric.maximum_absolute_error
<< ",\"maximum_normalized_error\":"
<< metric.maximum_normalized_error
<< ",\"rms_error\":" << metric.rms_error
<< ",\"vector_norm_error\":" << metric.vector_norm_error
<< ",\"worst_row\":" << metric.worst_row << '}';
<< ",\"relative_rms\":" << metric.relative_rms
<< ",\"worst_row\":" << metric.worst_row
<< ",\"rms_passed\":" << (metric.rms_passed ? "true" : "false")
<< ",\"passed\":" << (metric.passed ? "true" : "false")
<< ",\"diagnostic_code\":\"" << JsonEscape(metric.diagnostic_code)
<< "\"}";
}
stream << "],\"vector_metrics\":[";
for (std::size_t index = 0U; index < report.vector_metrics.size();
+14 -5
View File
@@ -29,19 +29,28 @@ struct Mitc4RowDecision {
double absolute_error;
double tolerance;
double normalized_error;
double reference_scale;
double near_zero_band;
bool relative_error_applicable;
std::string tolerance_branch;
bool blocking;
bool within_tolerance;
};
struct Mitc4ComponentMetrics {
std::string component;
std::string family_identity;
std::vector<std::string> components;
bool blocking;
double reference_scale;
double tolerance;
double near_zero_band;
std::size_t near_zero_count;
double maximum_absolute_error;
double maximum_normalized_error;
double rms_error;
double vector_norm_error;
double relative_rms;
std::size_t worst_row;
bool rms_passed;
bool passed;
std::string diagnostic_code;
std::size_t row_count{};
};
struct Mitc4VectorMetrics {
@@ -458,11 +458,11 @@ const fesa::test::Mitc4RowDecision* FindRow(
const fesa::test::Mitc4ComponentMetrics* FindMetric(
const fesa::test::Mitc4ComparisonReport& report,
const std::string& component) {
const std::string& family_identity) {
const auto found =
std::find_if(report.metrics.begin(), report.metrics.end(),
[&](const fesa::test::Mitc4ComponentMetrics& metric) {
return metric.component == component;
return metric.family_identity == family_identity;
});
return found == report.metrics.end() ? nullptr : &*found;
}
@@ -496,6 +496,9 @@ TEST(Mitc4ReferenceComparison,
EXPECT_EQ(report.rows[0U].component, "U1");
EXPECT_EQ(report.rows[5U].component, "UR3");
EXPECT_EQ(report.rows[6U].source_node_label, 2);
ASSERT_EQ(report.metrics.size(), 2U);
EXPECT_EQ(report.metrics[0U].row_count, 6U);
EXPECT_EQ(report.metrics[1U].row_count, 6U);
EXPECT_TRUE(std::all_of(report.rows.begin(), report.rows.end(),
[](const fesa::test::Mitc4RowDecision& row) {
return row.case_id == "shell-contract" &&
@@ -588,7 +591,7 @@ TEST(Mitc4ReferenceComparison, RejectsInvalidInventoryBeforeNumericComparison) {
// MITC4-REF-003
TEST(Mitc4ReferenceComparison,
AppliesFixedAbsoluteToleranceWithoutScaleClampOrRowDenominator) {
SharesTranslationScaleAcrossUComponentsAndUsesNearZeroBranches) {
ContractFixture fixture{"tolerance"};
auto reference = DefaultRows();
reference[0U].values[0U] = 0.0;
@@ -597,10 +600,10 @@ TEST(Mitc4ReferenceComparison,
reference[1U].values[1U] = 0.0;
WriteCsv(fixture.Csv(), reference);
auto fesa_values = reference;
fesa_values[0U].values[0U] = 0.9999e-5;
fesa_values[1U].values[0U] += 1.0001e-5;
fesa_values[0U].values[1U] = 0.9999e-5;
fesa_values[1U].values[1U] = 1.0001e-5;
fesa_values[0U].values[0U] = 0.999 * 8.0e-2;
fesa_values[1U].values[0U] += 1.001e-1;
fesa_values[0U].values[1U] = 0.999 * 8.0e-2;
fesa_values[1U].values[1U] = 1.001 * 8.0e-2;
WriteHdf5(fixture.Results(), fixture.Input(), fesa_values);
auto result =
@@ -616,25 +619,21 @@ TEST(Mitc4ReferenceComparison,
ASSERT_NE(u1_scaled, nullptr);
ASSERT_NE(u2_near, nullptr);
ASSERT_NE(u2_over, nullptr);
EXPECT_DOUBLE_EQ(u1_zero->tolerance, 1.0e-5);
EXPECT_DOUBLE_EQ(u1_zero->tolerance, 8.0e-2);
EXPECT_TRUE(u1_zero->within_tolerance);
EXPECT_NEAR(u1_zero->normalized_error, 0.9999, 1.0e-12);
EXPECT_FALSE(u1_zero->relative_error_applicable);
EXPECT_FALSE(u1_scaled->within_tolerance);
EXPECT_NEAR(u1_scaled->normalized_error, 1.0001, 2.0e-11);
EXPECT_DOUBLE_EQ(u2_near->tolerance, 1.0e-5);
EXPECT_NEAR(u1_scaled->normalized_error, 0.05005, 1.0e-12);
EXPECT_DOUBLE_EQ(u2_near->tolerance, 8.0e-2);
EXPECT_TRUE(u2_near->within_tolerance);
EXPECT_NEAR(u2_near->normalized_error, 0.9999, 1.0e-12);
EXPECT_FALSE(u2_near->relative_error_applicable);
EXPECT_FALSE(u2_over->within_tolerance);
EXPECT_NEAR(u2_over->normalized_error, 1.0001, 1.0e-12);
for (const auto& row : report.rows) {
EXPECT_DOUBLE_EQ(row.tolerance, 1.0e-5);
}
for (const auto& metric : report.metrics) {
EXPECT_DOUBLE_EQ(metric.tolerance, 1.0e-5);
}
const auto* u1_metric = FindMetric(report, "U1");
ASSERT_NE(u1_metric, nullptr);
EXPECT_DOUBLE_EQ(u1_metric->reference_scale, 2.0);
EXPECT_DOUBLE_EQ(u2_over->tolerance, 8.0e-2);
const auto* translation = FindMetric(report, "displacement-translation");
ASSERT_NE(translation, nullptr);
EXPECT_DOUBLE_EQ(translation->reference_scale, 8.0);
EXPECT_EQ(translation->components,
(std::vector<std::string>{"U1", "U2", "U3"}));
}
// MITC4-REF-004
@@ -650,8 +649,9 @@ TEST(Mitc4ReferenceComparison,
ASSERT_TRUE(result.HasValue());
const auto& report = result.Value();
EXPECT_TRUE(report.passed);
ASSERT_EQ(report.warnings.size(), 1U);
ASSERT_EQ(report.warnings.size(), 2U);
EXPECT_EQ(report.warnings[0U].code, "rotation-reference-exceedance");
EXPECT_EQ(report.warnings[1U].code, "rotation-reference-rms-exceedance");
const auto* row = FindRow(report, 1, "UR1");
ASSERT_NE(row, nullptr);
EXPECT_FALSE(row->blocking);
@@ -663,6 +663,47 @@ TEST(Mitc4ReferenceComparison,
EXPECT_NE(report.warnings[0U].message.find("UR1"), std::string::npos);
}
TEST(Mitc4ReferenceComparison,
RotationRmsExceedanceWarnsWhenEveryRotationRowPasses) {
ContractFixture fixture{"rotation-rms"};
auto fesa_values = DefaultRows();
fesa_values[1U].values[3U] = 0.1049;
fesa_values[1U].values[4U] = -0.2098;
fesa_values[1U].values[5U] = 0.3147;
WriteHdf5(fixture.Results(), fixture.Input(), fesa_values);
auto result =
fesa::test::Mitc4ReferenceComparison::Compare(fixture.ReferenceCase());
ASSERT_TRUE(result.HasValue());
const auto& report = result.Value();
EXPECT_TRUE(report.passed);
ASSERT_EQ(report.warnings.size(), 1U);
EXPECT_EQ(report.warnings[0U].code, "rotation-reference-rms-exceedance");
EXPECT_TRUE(std::all_of(report.rows.begin(), report.rows.end(),
[](const fesa::test::Mitc4RowDecision& row) {
return row.within_tolerance;
}));
}
TEST(Mitc4ReferenceComparison,
RotationRowAndRmsExceedancesProduceSeparateWarnings) {
ContractFixture fixture{"rotation-row-and-rms"};
auto fesa_values = DefaultRows();
fesa_values[0U].values[3U] = 1.0;
WriteHdf5(fixture.Results(), fixture.Input(), fesa_values);
auto result =
fesa::test::Mitc4ReferenceComparison::Compare(fixture.ReferenceCase());
ASSERT_TRUE(result.HasValue());
const auto& report = result.Value();
EXPECT_TRUE(report.passed);
ASSERT_EQ(report.warnings.size(), 2U);
EXPECT_EQ(report.warnings[0U].code, "rotation-reference-exceedance");
EXPECT_EQ(report.warnings[1U].code, "rotation-reference-rms-exceedance");
EXPECT_LT(report.warnings[0U].row, report.rows.size());
EXPECT_LT(report.warnings[1U].row, report.rows.size());
}
// MITC4-REF-005
TEST(Mitc4ReferenceComparison,
ReportsMetricsVectorsWorstRowAndJsonDeterministically) {
@@ -678,7 +719,7 @@ TEST(Mitc4ReferenceComparison,
ASSERT_TRUE(result.HasValue());
const auto& report = result.Value();
ASSERT_TRUE(report.passed);
ASSERT_EQ(report.metrics.size(), 6U);
ASSERT_EQ(report.metrics.size(), 2U);
ASSERT_EQ(report.vector_metrics.size(), 2U);
EXPECT_NEAR(report.vector_metrics[0U].displacement_norm_error,
std::sqrt(0.3125) * 1.0e-9, 1.0e-21);
@@ -687,10 +728,10 @@ TEST(Mitc4ReferenceComparison,
EXPECT_EQ(report.rows[report.worst_row].component, "UR1");
for (const auto& metric : report.metrics) {
EXPECT_TRUE(std::isfinite(metric.reference_scale));
EXPECT_TRUE(std::isfinite(metric.near_zero_band));
EXPECT_TRUE(std::isfinite(metric.maximum_absolute_error));
EXPECT_TRUE(std::isfinite(metric.maximum_normalized_error));
EXPECT_TRUE(std::isfinite(metric.rms_error));
EXPECT_TRUE(std::isfinite(metric.vector_norm_error));
EXPECT_TRUE(std::isfinite(metric.relative_rms));
EXPECT_FALSE(metric.components.empty());
EXPECT_LT(metric.worst_row, report.rows.size());
}
@@ -704,8 +745,10 @@ TEST(Mitc4ReferenceComparison,
.IsOk());
const std::string first = ReadBytes(json_a);
EXPECT_EQ(first, ReadBytes(json_b));
for (const char* key : {"\"rows\"", "\"metrics\"", "\"vector_metrics\"",
"\"warnings\"", "\"worst_row\"", "\"passed\""}) {
for (const char* key :
{"\"rows\"", "\"metrics\"", "\"vector_metrics\"", "\"warnings\"",
"\"family_identity\"", "\"near_zero_band\"", "\"relative_rms\"",
"\"row_count\"", "\"worst_row\"", "\"passed\""}) {
EXPECT_NE(first.find(key), std::string::npos) << key;
}
}
+255 -165
View File
@@ -29,7 +29,7 @@
#include "fesa/fem/dof_manager.h"
#include "fesa/io/abaqus/domain_mapper.h"
#include "fesa/io/abaqus/input_reader.h"
#include "fesa/results/result_recovery.h"
#include "reference_tolerance_policy.h"
namespace fesa::test {
namespace {
@@ -48,9 +48,6 @@ constexpr const char* kReactionPath = "/steps/Step-1/frames/0/nodal/reaction";
constexpr const char* kSectionPath =
"/steps/Step-1/frames/0/element/section_resultant";
constexpr const char* kStressPath = "/steps/Step-1/frames/0/element/stress_s11";
constexpr double kKinematicFloor = 1.0e-9;
constexpr double kForceMomentFloor = 1.0e-3;
constexpr double kRelativeCoefficient = 1.0e-6;
class ComparisonFailure final : public std::runtime_error {
public:
@@ -166,6 +163,17 @@ struct ReferenceTable {
std::vector<WideReferenceRow> rows;
};
struct SectionReferenceRow {
std::string instance_name;
std::int64_t source_element_label;
std::int64_t source_node_label;
std::vector<double> values;
};
struct SectionReferenceTable {
std::vector<SectionReferenceRow> rows;
};
ReferenceTable ReadReferenceCsv(
const std::filesystem::path& path,
const std::vector<std::string>& expected_header) {
@@ -223,6 +231,70 @@ ReferenceTable ReadReferenceCsv(
return table;
}
SectionReferenceTable ReadSectionReferenceCsv(
const std::filesystem::path& path) {
std::ifstream stream{path};
if (!stream) {
Fail("needs-reference-artifacts", "An approved reference CSV is missing.");
}
std::string line;
if (!std::getline(stream, line)) {
Fail("schema-mismatch", "An approved reference CSV is empty.");
}
if (!line.empty() && line.back() == '\r') {
line.pop_back();
}
const std::vector<std::string> expected_header = {
"Frame", "Part Instance Name",
"Element Label", "Node Label",
"SF-SF1", "SM-SM1",
"SM-SM2", "SM-SM3"};
if (SplitCsvLine(line) != expected_header) {
Fail("schema-mismatch", "An approved section CSV header is not exact.");
}
SectionReferenceTable table;
while (std::getline(stream, line)) {
if (!line.empty() && line.back() == '\r') {
line.pop_back();
}
if (line.empty()) {
Fail("schema-mismatch", "Blank reference CSV rows are not allowed.");
}
const auto fields = SplitCsvLine(line);
if (fields.size() != expected_header.size() ||
CollapseWhitespace(fields[0U]) != kFrameText || fields[1U].empty()) {
Fail("schema-mismatch",
"A reference section row has invalid schema or frame identity.");
}
SectionReferenceRow row{};
row.instance_name = fields[1U];
row.source_element_label = ParsePositiveLabel(fields[2U]);
row.source_node_label = ParsePositiveLabel(fields[3U]);
row.values.reserve(fields.size() - 4U);
for (std::size_t field = 4U; field < fields.size(); ++field) {
row.values.push_back(ParseFiniteDouble(fields[field]));
}
const auto duplicate = std::find_if(
table.rows.begin(), table.rows.end(),
[&](const SectionReferenceRow& existing) {
return AsciiLower(existing.instance_name) ==
AsciiLower(row.instance_name) &&
existing.source_element_label == row.source_element_label &&
existing.source_node_label == row.source_node_label;
});
if (duplicate != table.rows.end()) {
Fail("schema-mismatch",
"A reference section row identity is duplicated.");
}
table.rows.push_back(std::move(row));
}
if (table.rows.empty()) {
Fail("schema-mismatch", "An approved reference CSV has no data rows.");
}
return table;
}
void RequireExactArtifactInventory(
const std::filesystem::path& legacy_directory) {
std::error_code error;
@@ -894,76 +966,39 @@ std::vector<const WideReferenceRow*> OrderedRows(
return ordered;
}
double TableScale(const ReferenceTable& table, const std::size_t value_index) {
double scale = 0.0;
for (const auto& row : table.rows) {
if (value_index >= row.values.size()) {
Fail("schema-mismatch", "A reference row has the wrong component arity.");
}
scale = (std::max)(scale, std::abs(row.values[value_index]));
std::vector<const SectionReferenceRow*> OrderedSectionRows(
const SectionReferenceTable& table, const HdfProjection& hdf) {
const std::size_t endpoint_count = hdf.elements.size() * 2U;
if (table.rows.size() != endpoint_count) {
Fail("schema-mismatch", "The FESA/reference section row sets differ.");
}
return scale;
}
std::vector<NodeStationResultRow> NormalizeStations(
const Domain& domain, const HdfProjection& hdf,
const ReferenceTable& section_table) {
auto model_result = AnalysisModel::Create(domain);
if (!model_result.HasValue()) {
Fail("schema-mismatch",
"The approved input cannot create an analysis view.");
}
const AnalysisModel model = std::move(model_result.Value());
const std::array<double, 4> tolerances = {
kForceMomentFloor + kRelativeCoefficient * TableScale(section_table, 0U),
kForceMomentFloor + kRelativeCoefficient * TableScale(section_table, 3U),
kForceMomentFloor + kRelativeCoefficient * TableScale(section_table, 1U),
kForceMomentFloor + kRelativeCoefficient * TableScale(section_table, 2U)};
std::vector<EndpointResultRow> endpoints;
endpoints.reserve(hdf.elements.size() * 2U);
for (std::size_t element = 0U; element < hdf.elements.size(); ++element) {
std::vector<const SectionReferenceRow*> ordered;
ordered.reserve(endpoint_count);
for (const auto& element : hdf.elements) {
for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) {
const auto node = static_cast<std::size_t>(
hdf.elements[element].node_internal_ids[endpoint]);
std::array<double, 4> values{};
for (std::size_t component = 0U; component < values.size(); ++component) {
values[component] =
hdf.section_resultants[(element * 2U + endpoint) * 4U + component];
const std::size_t node_index =
static_cast<std::size_t>(element.node_internal_ids[endpoint]);
if (node_index >= hdf.nodes.size()) {
Fail("schema-mismatch", "An HDF5 section endpoint references no node.");
}
endpoints.push_back({static_cast<EntityIndex>(element),
static_cast<int>(endpoint),
domain.Nodes()[node].source_id,
{},
values});
const auto& node = hdf.nodes[node_index];
const auto found = std::find_if(
table.rows.begin(), table.rows.end(),
[&](const SectionReferenceRow& row) {
return AsciiLower(row.instance_name) ==
AsciiLower(element.instance_name) &&
row.source_element_label == element.source_element_label &&
row.source_node_label == node.source_node_label;
});
if (found == table.rows.end() ||
found->instance_name != element.instance_name) {
Fail("schema-mismatch",
"A reference section endpoint identity does not match HDF5.");
}
ordered.push_back(&*found);
}
}
auto normalized = ResultRecovery::NormalizeSectionResultantsToNodeStations(
model, endpoints, tolerances);
if (!normalized.HasValue()) {
const auto& diagnostics = normalized.GetStatus().Diagnostics();
const std::string code =
diagnostics.empty() ? std::string{} : diagnostics[0U].code;
if (code == "node-station-tolerance-failure") {
Fail("tolerance-failure",
"Interior endpoint section resultants disagree.");
}
Fail("schema-mismatch",
"A node station is not eligible for legacy projection.");
}
return std::move(normalized.Value());
}
const NodeStationResultRow& FindStation(
const std::vector<NodeStationResultRow>& stations, const HdfNode& node) {
const auto found = std::find_if(
stations.begin(), stations.end(), [&](const NodeStationResultRow& row) {
return row.node.instance_name == node.instance_name &&
row.node.source_label == node.source_node_label;
});
if (found == stations.end()) {
Fail("schema-mismatch", "A projected HDF5 node station is missing.");
}
return *found;
return ordered;
}
CanonicalComparisonRow CanonicalRow(const HdfNode& node,
@@ -977,6 +1012,8 @@ CanonicalComparisonRow CanonicalRow(const HdfNode& node,
kFrameIndex,
node.instance_name,
node.source_node_label,
0,
-1,
quantity,
std::move(component),
value,
@@ -1003,93 +1040,122 @@ void AppendNodalRows(ComparisonReport& report, const HdfProjection& hdf,
CanonicalRow(hdf.nodes[node], quantity, components[component],
reference[node]->values[component], units[component],
"global-cartesian", dataset_path);
report.rows.push_back(
{std::move(fesa), std::move(abaqus), 0.0, 0.0, false});
report.rows.push_back({std::move(fesa), std::move(abaqus)});
}
}
}
void AppendSectionRows(ComparisonReport& report, const HdfProjection& hdf,
const std::vector<const WideReferenceRow*>& reference,
const std::vector<NodeStationResultRow>& stations) {
void AppendSectionRows(
ComparisonReport& report, const HdfProjection& hdf,
const std::vector<const SectionReferenceRow*>& reference) {
const std::array<std::string, 4> components = {"N", "T", "My", "Mz"};
const std::array<std::string, 4> units = {"force", "force*length",
"force*length", "force*length"};
const std::array<std::size_t, 4> reference_columns = {0U, 3U, 1U, 2U};
for (std::size_t node = 0U; node < hdf.nodes.size(); ++node) {
const auto& station = FindStation(stations, hdf.nodes[node]);
for (std::size_t component = 0U; component < components.size();
++component) {
auto fesa = CanonicalRow(
hdf.nodes[node], ComparisonQuantity::kSectionResultant,
components[component], station.section_resultant[component],
units[component], "beam-local", kSectionPath);
auto abaqus =
CanonicalRow(hdf.nodes[node], ComparisonQuantity::kSectionResultant,
components[component],
reference[node]->values[reference_columns[component]],
units[component], "beam-local", kSectionPath);
report.rows.push_back(
{std::move(fesa), std::move(abaqus), 0.0, 0.0, false});
std::size_t row = 0U;
for (std::size_t element = 0U; element < hdf.elements.size(); ++element) {
for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint, ++row) {
const auto& element_row = hdf.elements[element];
const std::size_t node_index =
static_cast<std::size_t>(element_row.node_internal_ids[endpoint]);
if (node_index >= hdf.nodes.size()) {
Fail("schema-mismatch", "An HDF5 section endpoint references no node.");
}
const auto& node = hdf.nodes[node_index];
const auto canonical = [&](const double value, std::string component,
std::string unit) {
return CanonicalComparisonRow{kModelId,
kStepName,
kFrameIndex,
element_row.instance_name,
node.source_node_label,
element_row.source_element_label,
static_cast<int>(endpoint),
ComparisonQuantity::kSectionResultant,
std::move(component),
value,
std::move(unit),
"beam-local",
kSectionPath};
};
for (std::size_t component = 0U; component < components.size();
++component) {
auto fesa = canonical(
hdf.section_resultants[(element * 2U + endpoint) * 4U + component],
components[component], units[component]);
auto abaqus =
canonical(reference[row]->values[reference_columns[component]],
components[component], units[component]);
report.rows.push_back({std::move(fesa), std::move(abaqus)});
}
}
}
}
double AbsoluteFloor(const ComparisonQuantity quantity, const std::string&) {
return quantity == ComparisonQuantity::kDisplacement ? kKinematicFloor
: kForceMomentFloor;
const char* BranchName(const ReferenceToleranceBranch branch) {
switch (branch) {
case ReferenceToleranceBranch::kNearZero:
return "near-zero";
case ReferenceToleranceBranch::kRelative:
return "relative";
case ReferenceToleranceBranch::kZeroScaleExact:
return "zero-scale-exact";
}
return "unknown";
}
void EvaluateGroup(ComparisonReport& report, const ComparisonQuantity quantity,
const std::string& component) {
void EvaluateFamily(ComparisonReport& report, const ComparisonQuantity quantity,
const std::string& identity,
const std::vector<std::string>& components,
const bool blocking) {
std::vector<std::size_t> row_indices;
for (std::size_t index = 0U; index < report.rows.size(); ++index) {
if (report.rows[index].reference.quantity == quantity &&
report.rows[index].reference.component == component) {
std::find(components.begin(), components.end(),
report.rows[index].reference.component) != components.end()) {
row_indices.push_back(index);
}
}
if (row_indices.empty()) {
Fail("schema-mismatch", "A canonical comparison component has no rows.");
Fail("schema-mismatch", "A canonical comparison family has no rows.");
}
double reference_scale = 0.0;
ReferenceToleranceFamily family{};
family.identity = identity;
family.components = components;
family.blocking = blocking;
family.rows.reserve(row_indices.size());
for (const std::size_t index : row_indices) {
reference_scale = (std::max)(reference_scale,
std::abs(report.rows[index].reference.value));
family.rows.push_back(
{report.rows[index].fesa.value, report.rows[index].reference.value});
}
const double tolerance = AbsoluteFloor(quantity, component) +
kRelativeCoefficient * reference_scale;
double maximum_absolute = -1.0;
double maximum_normalized = 0.0;
std::size_t worst_row = row_indices.front();
long double squared_error = 0.0L;
for (const std::size_t index : row_indices) {
auto& row = report.rows[index];
row.absolute_error = std::abs(row.fesa.value - row.reference.value);
if (!std::isfinite(row.absolute_error)) {
Fail("schema-mismatch", "A canonical row error is nonfinite.");
}
row.tolerance = tolerance;
row.passed = row.absolute_error <= tolerance;
report.passed = report.passed && row.passed;
const double normalized = row.absolute_error / tolerance;
if (row.absolute_error > maximum_absolute) {
maximum_absolute = row.absolute_error;
worst_row = index;
}
maximum_normalized = (std::max)(maximum_normalized, normalized);
const long double error = static_cast<long double>(row.absolute_error);
squared_error += error * error;
const auto evaluation =
ReferenceTolerancePolicy::Evaluate({std::move(family)});
if (!evaluation.valid || evaluation.families.size() != 1U) {
Fail("schema-mismatch", "A canonical comparison family is nonfinite.");
}
const double norm_error = std::sqrt(static_cast<double>(squared_error));
const double rms_error = std::sqrt(static_cast<double>(
squared_error / static_cast<long double>(row_indices.size())));
if (!std::isfinite(norm_error) || !std::isfinite(rms_error)) {
Fail("schema-mismatch", "A component aggregate error is nonfinite.");
const auto& policy = evaluation.families.front();
for (std::size_t local = 0U; local < row_indices.size(); ++local) {
auto& row = report.rows[row_indices[local]];
const auto& decision = policy.rows[local];
row.absolute_error = decision.absolute_error;
row.tolerance = decision.threshold;
row.reference_scale = policy.reference_scale;
row.near_zero_band = policy.near_zero_band;
row.relative_error = decision.relative_error;
row.relative_error_applicable = decision.relative_error_applicable;
row.tolerance_branch = BranchName(decision.branch);
row.passed = decision.passed;
}
const std::size_t worst_row = row_indices[policy.worst_row];
report.metrics.push_back({quantity, policy.identity, policy.components,
policy.reference_scale, policy.near_zero_band,
policy.near_zero_count,
policy.maximum_absolute_error, policy.relative_rms,
worst_row, policy.rms_passed, policy.passed,
policy.diagnostic_code, policy.row_count});
if (blocking && !policy.passed) {
report.passed = false;
}
report.metrics.push_back({quantity, component, reference_scale,
maximum_absolute, maximum_normalized, rms_error,
norm_error, worst_row});
}
PhysicsEvidence MakePhysicsEvidence(const Domain& domain,
@@ -1214,7 +1280,8 @@ void WriteCanonicalRow(std::ostream& stream,
stream << ",\"frame_index\":" << row.frame_index << ",\"instance_name\":";
WriteJsonString(stream, row.instance_name);
stream << ",\"source_node_label\":" << row.source_node_label
<< ",\"quantity\":";
<< ",\"source_element_label\":" << row.source_element_label
<< ",\"endpoint_index\":" << row.endpoint_index << ",\"quantity\":";
WriteJsonString(stream, QuantityName(row.quantity));
stream << ",\"component\":";
WriteJsonString(stream, row.component);
@@ -1231,6 +1298,18 @@ void WriteArray(std::ostream& stream, const std::array<double, 3>& values) {
stream << '[' << values[0U] << ',' << values[1U] << ',' << values[2U] << ']';
}
void WriteStrings(std::ostream& stream,
const std::vector<std::string>& values) {
stream << '[';
for (std::size_t index = 0U; index < values.size(); ++index) {
if (index != 0U) {
stream << ',';
}
WriteJsonString(stream, values[index]);
}
stream << ']';
}
bool FiniteReport(const ComparisonReport& report) {
const auto finite_array = [](const std::array<double, 3>& values) {
return std::all_of(values.begin(), values.end(),
@@ -1245,17 +1324,19 @@ bool FiniteReport(const ComparisonReport& report) {
}
for (const auto& row : report.rows) {
if (!std::isfinite(row.fesa.value) || !std::isfinite(row.reference.value) ||
!std::isfinite(row.absolute_error) || !std::isfinite(row.tolerance)) {
!std::isfinite(row.absolute_error) || !std::isfinite(row.tolerance) ||
!std::isfinite(row.reference_scale) ||
!std::isfinite(row.near_zero_band) ||
!std::isfinite(row.relative_error)) {
return false;
}
}
return std::all_of(report.metrics.begin(), report.metrics.end(),
[](const ComponentMetrics& metric) {
return std::isfinite(metric.reference_scale) &&
std::isfinite(metric.near_zero_band) &&
std::isfinite(metric.maximum_absolute_error) &&
std::isfinite(metric.maximum_normalized_error) &&
std::isfinite(metric.rms_error) &&
std::isfinite(metric.norm_error);
std::isfinite(metric.relative_rms);
});
}
@@ -1276,18 +1357,12 @@ Result<ComparisonReport> ReferenceComparison::Compare(
ReadReferenceCsv(legacy_reference_directory / kReactionName,
{"Frame", "Part Instance Name", "Node Label", "RF-RF1",
"RF-RF2", "RF-RF3", "RM-RM1", "RM-RM2", "RM-RM3"});
const ReferenceTable section =
ReadReferenceCsv(legacy_reference_directory / kSectionName,
{"Frame", "Part Instance Name", "Node Label", "SF-SF1",
"SM-SM1", "SM-SM2", "SM-SM3"});
const SectionReferenceTable section =
ReadSectionReferenceCsv(legacy_reference_directory / kSectionName);
HdfProjection hdf = ReadHdfProjection(results_hdf5, input, domain);
const auto displacement_rows = OrderedRows(displacement, hdf.nodes);
const auto reaction_rows = OrderedRows(reaction, hdf.nodes);
const auto section_rows = OrderedRows(section, hdf.nodes);
const auto stations = NormalizeStations(domain, hdf, section);
if (stations.size() != hdf.nodes.size()) {
Fail("schema-mismatch", "The HDF5 node-station row set is incomplete.");
}
const auto section_rows = OrderedSectionRows(section, hdf);
ComparisonReport report{};
report.passed = true;
@@ -1301,19 +1376,20 @@ Result<ComparisonReport> ReferenceComparison::Compare(
{"force", "force", "force", "force*length", "force*length",
"force*length"},
hdf.reaction, kReactionPath);
AppendSectionRows(report, hdf, section_rows, stations);
AppendSectionRows(report, hdf, section_rows);
for (const std::string& component :
{"UX", "UY", "UZ", "URX", "URY", "URZ"}) {
EvaluateGroup(report, ComparisonQuantity::kDisplacement, component);
}
for (const std::string& component :
{"RF1", "RF2", "RF3", "RM1", "RM2", "RM3"}) {
EvaluateGroup(report, ComparisonQuantity::kReaction, component);
}
for (const std::string& component : {"N", "T", "My", "Mz"}) {
EvaluateGroup(report, ComparisonQuantity::kSectionResultant, component);
}
EvaluateFamily(report, ComparisonQuantity::kDisplacement,
"displacement-translation", {"UX", "UY", "UZ"}, true);
EvaluateFamily(report, ComparisonQuantity::kDisplacement,
"displacement-rotation", {"URX", "URY", "URZ"}, true);
EvaluateFamily(report, ComparisonQuantity::kReaction, "reaction-force",
{"RF1", "RF2", "RF3"}, true);
EvaluateFamily(report, ComparisonQuantity::kReaction, "reaction-moment",
{"RM1", "RM2", "RM3"}, true);
EvaluateFamily(report, ComparisonQuantity::kSectionResultant,
"section-force", {"N"}, true);
EvaluateFamily(report, ComparisonQuantity::kSectionResultant,
"section-moment", {"T", "My", "Mz"}, true);
report.physics_evidence = MakePhysicsEvidence(domain, hdf);
report.stress_comparison_applicable = false;
report.stress_comparison_reason =
@@ -1366,8 +1442,15 @@ Status ReferenceComparison::WriteDeterministicJson(
stream << ",\"reference\":";
WriteCanonicalRow(stream, row.reference);
stream << ",\"absolute_error\":" << row.absolute_error
<< ",\"tolerance\":" << row.tolerance
<< ",\"passed\":" << (row.passed ? "true" : "false") << '}';
<< ",\"threshold\":" << row.tolerance
<< ",\"reference_scale\":" << row.reference_scale
<< ",\"near_zero_band\":" << row.near_zero_band
<< ",\"relative_error\":" << row.relative_error
<< ",\"relative_error_applicable\":"
<< (row.relative_error_applicable ? "true" : "false")
<< ",\"tolerance_branch\":";
WriteJsonString(stream, row.tolerance_branch);
stream << ",\"passed\":" << (row.passed ? "true" : "false") << '}';
}
stream << "],\"metrics\":[";
for (std::size_t index = 0U; index < report.metrics.size(); ++index) {
@@ -1377,15 +1460,22 @@ Status ReferenceComparison::WriteDeterministicJson(
const auto& metric = report.metrics[index];
stream << "{\"quantity\":";
WriteJsonString(stream, QuantityName(metric.quantity));
stream << ",\"component\":";
WriteJsonString(stream, metric.component);
stream << ",\"family_identity\":";
WriteJsonString(stream, metric.family_identity);
stream << ",\"components\":";
WriteStrings(stream, metric.components);
stream << ",\"reference_scale\":" << metric.reference_scale
<< ",\"near_zero_band\":" << metric.near_zero_band
<< ",\"near_zero_count\":" << metric.near_zero_count
<< ",\"row_count\":" << metric.row_count
<< ",\"maximum_absolute_error\":" << metric.maximum_absolute_error
<< ",\"maximum_normalized_error\":"
<< metric.maximum_normalized_error
<< ",\"rms_error\":" << metric.rms_error
<< ",\"norm_error\":" << metric.norm_error
<< ",\"worst_row\":" << metric.worst_row << '}';
<< ",\"relative_rms\":" << metric.relative_rms
<< ",\"worst_row\":" << metric.worst_row
<< ",\"rms_passed\":" << (metric.rms_passed ? "true" : "false")
<< ",\"passed\":" << (metric.passed ? "true" : "false")
<< ",\"diagnostic_code\":";
WriteJsonString(stream, metric.diagnostic_code);
stream << '}';
}
stream << "],\"physics_evidence\":{\"free_residual_norm\":"
<< report.physics_evidence.free_residual_norm << ",\"applied_force\":";
+16 -4
View File
@@ -20,6 +20,8 @@ struct CanonicalComparisonRow {
std::size_t frame_index;
std::string instance_name;
std::int64_t source_node_label;
std::int64_t source_element_label;
int endpoint_index;
ComparisonQuantity quantity;
std::string component;
double value;
@@ -33,18 +35,28 @@ struct RowDecision {
CanonicalComparisonRow reference;
double absolute_error;
double tolerance;
double reference_scale;
double near_zero_band;
double relative_error;
bool relative_error_applicable;
std::string tolerance_branch;
bool passed;
};
struct ComponentMetrics {
ComparisonQuantity quantity;
std::string component;
std::string family_identity;
std::vector<std::string> components;
double reference_scale;
double near_zero_band;
std::size_t near_zero_count;
double maximum_absolute_error;
double maximum_normalized_error;
double rms_error;
double norm_error;
double relative_rms;
std::size_t worst_row;
bool rms_passed;
bool passed;
std::string diagnostic_code;
std::size_t row_count{};
};
struct PhysicsEvidence {
+140 -68
View File
@@ -41,8 +41,8 @@ constexpr const char* kSectionName = "cantilever beam elemental forces.csv";
constexpr const char* kInstanceName = "PART-1_1-1";
constexpr std::size_t kNodeCount = 11U;
constexpr std::size_t kElementCount = 10U;
constexpr std::size_t kExpectedRowCount = 176U;
constexpr std::size_t kExpectedMetricCount = 16U;
constexpr std::size_t kExpectedRowCount = 212U;
constexpr std::size_t kExpectedMetricCount = 6U;
using NodalValues = std::array<std::array<double, 6>, kNodeCount>;
using EndpointValues =
@@ -315,14 +315,29 @@ const fesa::test::RowDecision* FindRow(
const fesa::test::ComponentMetrics* FindMetric(
const fesa::test::ComparisonReport& report,
const fesa::test::ComparisonQuantity quantity,
const std::string& family_identity) {
const auto found =
std::find_if(report.metrics.begin(), report.metrics.end(),
[&](const fesa::test::ComponentMetrics& metric) {
return metric.family_identity == family_identity;
});
return found == report.metrics.end() ? nullptr : &*found;
}
const fesa::test::RowDecision* FindSectionRow(
const fesa::test::ComparisonReport& report,
const std::int64_t source_element_label, const int endpoint_index,
const std::string& component) {
const auto found = std::find_if(
report.metrics.begin(), report.metrics.end(),
[&](const fesa::test::ComponentMetrics& metric) {
return metric.quantity == quantity && metric.component == component;
report.rows.begin(), report.rows.end(),
[&](const fesa::test::RowDecision& row) {
return row.reference.quantity ==
fesa::test::ComparisonQuantity::kSectionResultant &&
row.reference.source_element_label == source_element_label &&
row.reference.endpoint_index == endpoint_index &&
row.reference.component == component;
});
return found == report.metrics.end() ? nullptr : &*found;
return found == report.rows.end() ? nullptr : &*found;
}
void ExpectExactRowInventory(const fesa::test::ComparisonReport& report) {
@@ -381,13 +396,25 @@ void ExpectExactRowInventory(const fesa::test::ComparisonReport& report) {
const std::array<std::string, 4> section_components = {"N", "T", "My", "Mz"};
const std::array<std::string, 4> section_units = {
"force", "force*length", "force*length", "force*length"};
for (std::size_t node = 0U; node < kNodeCount; ++node) {
for (std::size_t component = 0U; component < section_components.size();
++component) {
expect_row(fesa::test::ComparisonQuantity::kSectionResultant, node,
section_components[component], section_units[component],
"beam-local",
"/steps/Step-1/frames/0/element/section_resultant");
for (std::size_t element = 0U; element < kElementCount; ++element) {
for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) {
for (std::size_t component = 0U; component < section_components.size();
++component) {
ASSERT_LT(row_index, report.rows.size());
const auto& row = report.rows[row_index++];
for (const auto* side : {&row.fesa, &row.reference}) {
EXPECT_EQ(side->source_element_label,
static_cast<std::int64_t>(element + 1U));
EXPECT_EQ(side->endpoint_index, static_cast<int>(endpoint));
EXPECT_EQ(side->source_node_label,
static_cast<std::int64_t>(element + endpoint + 1U));
EXPECT_EQ(side->component, section_components[component]);
EXPECT_EQ(side->unit_dimension, section_units[component]);
EXPECT_EQ(side->coordinate_system, "beam-local");
EXPECT_EQ(side->hdf5_dataset_path,
"/steps/Step-1/frames/0/element/section_resultant");
}
}
}
}
EXPECT_EQ(row_index, report.rows.size());
@@ -475,14 +502,46 @@ TEST(ReferenceComparisonContract,
fixture.Results(), fixture.Legacy()),
"schema-mismatch");
}
{
ContractFixture fixture{"section-missing-endpoint", mismatched_values};
auto lines = ReadLines(fixture.Legacy() / kSectionName);
ASSERT_EQ(lines.size(), kElementCount * 2U + 1U);
lines.pop_back();
WriteLines(fixture.Legacy() / kSectionName, lines);
ExpectFailureCode(fesa::test::ReferenceComparison::Compare(
fixture.Results(), fixture.Legacy()),
"schema-mismatch");
}
{
ContractFixture fixture{"section-duplicate-endpoint", mismatched_values};
auto lines = ReadLines(fixture.Legacy() / kSectionName);
ASSERT_EQ(lines.size(), kElementCount * 2U + 1U);
lines.push_back(lines[1U]);
WriteLines(fixture.Legacy() / kSectionName, lines);
ExpectFailureCode(fesa::test::ReferenceComparison::Compare(
fixture.Results(), fixture.Legacy()),
"schema-mismatch");
}
{
ContractFixture fixture{"section-mismatched-endpoint", mismatched_values};
auto lines = ReadLines(fixture.Legacy() / kSectionName);
ASSERT_GT(lines.size(), 2U);
ReplaceFirst(lines[1U], ",1,1,", ",1,3,");
WriteLines(fixture.Legacy() / kSectionName, lines);
ExpectFailureCode(fesa::test::ReferenceComparison::Compare(
fixture.Results(), fixture.Legacy()),
"schema-mismatch");
}
}
TEST(ReferenceComparisonContract,
AppliesAbaqusOnlyComponentScaleWithoutClampOrDrop) {
AppliesSharedFamilyScaleAndNearZeroBranchesWithoutAnAbsoluteGate) {
auto values = ReferenceValues();
values.displacement[1U][0U] = 0.999e-9;
values.displacement[2U][0U] = 1.001e-9;
values.section_resultants[0U][0U][2U] = 1.0e7 + 9.0;
const double translation_scale = 1.90476272e-2;
const double near_zero_band = 0.01 * translation_scale;
values.displacement[1U][0U] = 0.999 * near_zero_band;
values.displacement[2U][0U] = 1.001 * near_zero_band;
values.section_resultants[0U][0U][2U] = 1.0e7 + 4.999e5;
values.section_resultants[9U][1U][2U] = 0.0;
ContractFixture fixture{"tolerance", values};
@@ -505,28 +564,28 @@ TEST(ReferenceComparisonContract,
ASSERT_NE(deliberate_failure, nullptr);
EXPECT_DOUBLE_EQ(zero->reference.value, 0.0);
EXPECT_DOUBLE_EQ(zero->fesa.value, 0.0);
EXPECT_DOUBLE_EQ(zero->tolerance, 1.0e-9);
EXPECT_DOUBLE_EQ(zero->tolerance, near_zero_band);
EXPECT_TRUE(zero->passed);
EXPECT_TRUE(near_zero->passed);
EXPECT_FALSE(deliberate_failure->passed);
EXPECT_EQ(near_zero->tolerance_branch, "near-zero");
EXPECT_FALSE(near_zero->relative_error_applicable);
const auto* my_metric = FindMetric(
report, fesa::test::ComparisonQuantity::kSectionResultant, "My");
const auto* my_metric = FindMetric(report, "section-moment");
ASSERT_NE(my_metric, nullptr);
EXPECT_DOUBLE_EQ(my_metric->reference_scale, 1.0e7);
const auto* scaled = FindRow(
report, fesa::test::ComparisonQuantity::kSectionResultant, 1, "My");
const auto* residue = FindRow(
report, fesa::test::ComparisonQuantity::kSectionResultant, 11, "My");
EXPECT_EQ(my_metric->components, (std::vector<std::string>{"T", "My", "Mz"}));
const auto* scaled = FindSectionRow(report, 1, 0, "My");
const auto* residue = FindSectionRow(report, 10, 1, "My");
ASSERT_NE(scaled, nullptr);
ASSERT_NE(residue, nullptr);
EXPECT_DOUBLE_EQ(scaled->tolerance, 10.001);
EXPECT_DOUBLE_EQ(scaled->absolute_error, 9.0);
EXPECT_DOUBLE_EQ(scaled->tolerance, 5.0e5);
EXPECT_DOUBLE_EQ(scaled->absolute_error, 4.999e5);
EXPECT_TRUE(scaled->passed);
EXPECT_DOUBLE_EQ(residue->reference.value, -1.56e-2);
EXPECT_DOUBLE_EQ(residue->reference.value, -1.5625e-2);
EXPECT_DOUBLE_EQ(residue->fesa.value, 0.0);
EXPECT_DOUBLE_EQ(residue->absolute_error, 1.56e-2);
EXPECT_DOUBLE_EQ(residue->tolerance, 10.001);
EXPECT_DOUBLE_EQ(residue->absolute_error, 1.5625e-2);
EXPECT_DOUBLE_EQ(residue->tolerance, 1.0e5);
EXPECT_TRUE(residue->passed);
}
@@ -548,19 +607,18 @@ TEST(ReferenceComparisonContract,
report.rows.begin(), report.rows.end(),
[](const fesa::test::RowDecision& row) { return row.passed; }));
const auto* metric =
FindMetric(report, fesa::test::ComparisonQuantity::kDisplacement, "UX");
const auto* metric = FindMetric(report, "displacement-translation");
const auto* worst =
FindRow(report, fesa::test::ComparisonQuantity::kDisplacement, 2, "UX");
ASSERT_NE(metric, nullptr);
ASSERT_NE(worst, nullptr);
EXPECT_DOUBLE_EQ(metric->reference_scale, 0.0);
EXPECT_EQ(metric->row_count, kNodeCount * 3U);
EXPECT_DOUBLE_EQ(metric->reference_scale, 1.90476272e-2);
EXPECT_DOUBLE_EQ(metric->maximum_absolute_error, 1.0e-9);
EXPECT_DOUBLE_EQ(metric->maximum_normalized_error, 1.0);
EXPECT_NEAR(metric->rms_error,
std::sqrt(1.25 / static_cast<double>(kNodeCount)) * 1.0e-9,
EXPECT_NEAR(metric->relative_rms,
std::sqrt(1.25 / static_cast<double>(kNodeCount * 3U)) * 1.0e-9 /
metric->reference_scale,
1.0e-21);
EXPECT_NEAR(metric->norm_error, std::sqrt(1.25) * 1.0e-9, 1.0e-21);
EXPECT_EQ(metric->worst_row,
static_cast<std::size_t>(worst - report.rows.data()));
@@ -589,7 +647,10 @@ TEST(ReferenceComparisonContract,
const std::string first = ReadBytes(json_a);
EXPECT_EQ(first, ReadBytes(json_b));
for (const char* required :
{"\"rows\"", "\"metrics\"", "\"stress_comparison_applicable\":false",
{"\"rows\"", "\"metrics\"", "\"family_identity\"", "\"components\"",
"\"near_zero_band\"", "\"relative_rms\"", "\"row_count\"",
"\"tolerance_branch\"", "\"relative_error_applicable\"",
"\"stress_comparison_applicable\":false",
"\"stress_comparison_reason\"", "\"physics_evidence\"",
"\"free_residual_norm\"", "\"applied_force\"", "\"reaction_force\"",
"\"applied_moment_about_origin\"", "\"reaction_moment_about_origin\"",
@@ -598,11 +659,10 @@ TEST(ReferenceComparisonContract,
}
}
TEST(ReferenceComparisonContract, NormalizesEligibleStationsWithoutAveraging) {
TEST(ReferenceComparisonContract,
PreservesSectionEndpointIdentityWithoutStationNormalization) {
auto values = ReferenceValues();
values.section_resultants[0U][0U] = {2.0e-4, 3.0e-4, 1.0e7, 4.0e-4};
values.section_resultants[0U][1U][2U] = 9.0e6 - 5.0;
values.section_resultants[1U][0U][2U] = 9.0e6 + 5.0;
ContractFixture fixture{"stations", values};
auto result = fesa::test::ReferenceComparison::Compare(fixture.Results(),
@@ -612,35 +672,47 @@ TEST(ReferenceComparisonContract, NormalizesEligibleStationsWithoutAveraging) {
ASSERT_TRUE(report.passed);
EXPECT_TRUE(report.physics_evidence.endpoint_consistency_passed);
const auto* n = FindRow(
report, fesa::test::ComparisonQuantity::kSectionResultant, 1, "N");
const auto* t = FindRow(
report, fesa::test::ComparisonQuantity::kSectionResultant, 1, "T");
const auto* my = FindRow(
report, fesa::test::ComparisonQuantity::kSectionResultant, 1, "My");
const auto* mz = FindRow(
report, fesa::test::ComparisonQuantity::kSectionResultant, 1, "Mz");
ASSERT_NE(n, nullptr);
ASSERT_NE(t, nullptr);
ASSERT_NE(my, nullptr);
ASSERT_NE(mz, nullptr);
EXPECT_DOUBLE_EQ(n->fesa.value, 2.0e-4);
EXPECT_DOUBLE_EQ(t->fesa.value, 3.0e-4);
EXPECT_DOUBLE_EQ(my->fesa.value, 1.0e7);
EXPECT_DOUBLE_EQ(mz->fesa.value, 4.0e-4);
const auto* interior = FindRow(
report, fesa::test::ComparisonQuantity::kSectionResultant, 2, "My");
const auto* interior = FindSectionRow(report, 1, 1, "My");
ASSERT_NE(interior, nullptr);
EXPECT_DOUBLE_EQ(interior->fesa.value, 9.0e6 - 5.0);
EXPECT_DOUBLE_EQ(interior->reference.value, 9.0e6);
EXPECT_DOUBLE_EQ(interior->absolute_error, 5.0);
auto mismatch_values = ReferenceValues();
mismatch_values.section_resultants[0U][1U][2U] = 9.0e6 - 6.0;
mismatch_values.section_resultants[1U][0U][2U] = 9.0e6 + 6.0;
ContractFixture mismatch{"station-mismatch", mismatch_values};
ExpectFailureCode(fesa::test::ReferenceComparison::Compare(mismatch.Results(),
mismatch.Legacy()),
"tolerance-failure");
const auto* adjacent = FindSectionRow(report, 2, 0, "My");
ASSERT_NE(adjacent, nullptr);
EXPECT_DOUBLE_EQ(adjacent->fesa.value, 9.0e6);
EXPECT_DOUBLE_EQ(adjacent->reference.value, 9.0e6);
}
TEST(ReferenceComparisonContract,
MapsEachSectionCsvComponentToItsDirectEndpointResultant) {
auto values = ReferenceValues();
values.section_resultants[0U][0U] = {11.0, 22.0, 33.0, 44.0};
ContractFixture fixture{"section-component-mapping", values};
auto lines = ReadLines(fixture.Legacy() / kSectionName);
ASSERT_EQ(lines.size(), kElementCount * 2U + 1U);
lines[1U] = "Increment 1: Step Time = 1.000,PART-1_1-1,1,1,11,33,44,22";
WriteLines(fixture.Legacy() / kSectionName, lines);
auto result = fesa::test::ReferenceComparison::Compare(fixture.Results(),
fixture.Legacy());
ASSERT_TRUE(result.HasValue());
const auto& report = result.Value();
ASSERT_TRUE(report.passed);
const auto* n = FindSectionRow(report, 1, 0, "N");
const auto* t = FindSectionRow(report, 1, 0, "T");
const auto* my = FindSectionRow(report, 1, 0, "My");
const auto* mz = FindSectionRow(report, 1, 0, "Mz");
ASSERT_NE(n, nullptr);
ASSERT_NE(t, nullptr);
ASSERT_NE(my, nullptr);
ASSERT_NE(mz, nullptr);
EXPECT_DOUBLE_EQ(n->reference.value, 11.0);
EXPECT_DOUBLE_EQ(t->reference.value, 22.0);
EXPECT_DOUBLE_EQ(my->reference.value, 33.0);
EXPECT_DOUBLE_EQ(mz->reference.value, 44.0);
EXPECT_DOUBLE_EQ(n->fesa.value, 11.0);
EXPECT_DOUBLE_EQ(t->fesa.value, 22.0);
EXPECT_DOUBLE_EQ(my->fesa.value, 33.0);
EXPECT_DOUBLE_EQ(mz->fesa.value, 44.0);
}
@@ -0,0 +1,115 @@
#include "reference_tolerance_policy.h"
#include <algorithm>
#include <cmath>
#include <limits>
namespace fesa::test {
namespace {
bool IsFiniteFamily(const ReferenceToleranceFamily& family) {
if (family.identity.empty() || family.components.empty() ||
family.rows.empty()) {
return false;
}
return std::all_of(family.rows.begin(), family.rows.end(),
[](const ReferenceToleranceRow& row) {
return std::isfinite(row.fesa_value) &&
std::isfinite(row.reference_value);
});
}
}
ReferenceToleranceEvaluation ReferenceTolerancePolicy::Evaluate(
const std::vector<ReferenceToleranceFamily>& families) {
ReferenceToleranceEvaluation evaluation{true, {}, true, {}};
evaluation.families.reserve(families.size());
for (const auto& family : families) {
if (!IsFiniteFamily(family)) {
return {false, "nonfinite-comparison-value", false, {}};
}
ReferenceToleranceFamilyDecision decision{};
decision.identity = family.identity;
decision.components = family.components;
decision.blocking = family.blocking;
decision.row_count = family.rows.size();
decision.rows.reserve(family.rows.size());
for (const auto& row : family.rows) {
decision.reference_scale =
(std::max)(decision.reference_scale, std::abs(row.reference_value));
}
decision.near_zero_band = kNearZeroRatio * decision.reference_scale;
double error_norm = 0.0;
for (std::size_t index = 0U; index < family.rows.size(); ++index) {
const auto& row = family.rows[index];
const double absolute_error =
std::abs(row.fesa_value - row.reference_value);
if (!std::isfinite(absolute_error)) {
return {false, "nonfinite-comparison-value", false, {}};
}
error_norm = std::hypot(error_norm, absolute_error);
if (!std::isfinite(error_norm)) {
return {false, "nonfinite-comparison-value", false, {}};
}
if (absolute_error > decision.maximum_absolute_error) {
decision.maximum_absolute_error = absolute_error;
decision.worst_row = index;
}
ReferenceToleranceRowDecision row_decision{};
row_decision.absolute_error = absolute_error;
if (decision.reference_scale == 0.0) {
++decision.near_zero_count;
row_decision.threshold = 0.0;
row_decision.branch = ReferenceToleranceBranch::kZeroScaleExact;
row_decision.passed = absolute_error == 0.0;
} else if (std::abs(row.reference_value) <= decision.near_zero_band) {
++decision.near_zero_count;
row_decision.threshold = decision.near_zero_band;
row_decision.branch = ReferenceToleranceBranch::kNearZero;
row_decision.passed = absolute_error <= row_decision.threshold;
} else {
row_decision.threshold =
kRelativeTolerance * std::abs(row.reference_value);
row_decision.relative_error =
absolute_error / std::abs(row.reference_value);
row_decision.relative_error_applicable = true;
row_decision.branch = ReferenceToleranceBranch::kRelative;
row_decision.passed = row_decision.relative_error <= kRelativeTolerance;
}
decision.rows.push_back(row_decision);
}
const bool rows_passed = std::all_of(
decision.rows.begin(), decision.rows.end(),
[](const ReferenceToleranceRowDecision& row) { return row.passed; });
if (decision.reference_scale == 0.0) {
decision.relative_rms = 0.0;
decision.rms_passed = rows_passed;
if (!rows_passed) {
decision.diagnostic_code = "zero-reference-scale-nonzero-error";
}
} else {
const double rms =
error_norm / std::sqrt(static_cast<double>(family.rows.size()));
decision.relative_rms = rms / decision.reference_scale;
if (!std::isfinite(decision.relative_rms)) {
return {false, "nonfinite-comparison-value", false, {}};
}
decision.rms_passed = decision.relative_rms <= kRelativeRmsTolerance;
}
decision.passed = rows_passed && decision.rms_passed;
if (decision.blocking && !decision.passed) {
evaluation.passed = false;
}
evaluation.families.push_back(std::move(decision));
}
return evaluation;
}
}
@@ -0,0 +1,73 @@
#ifndef FESA_TESTS_REFERENCE_REFERENCE_TOLERANCE_POLICY_H_
#define FESA_TESTS_REFERENCE_REFERENCE_TOLERANCE_POLICY_H_
#include <cstddef>
#include <string>
#include <vector>
namespace fesa::test {
enum class ReferenceToleranceBranch {
kNearZero,
kRelative,
kZeroScaleExact,
};
struct ReferenceToleranceRow {
double fesa_value;
double reference_value;
};
struct ReferenceToleranceFamily {
std::string identity;
std::vector<std::string> components;
bool blocking;
std::vector<ReferenceToleranceRow> rows;
};
struct ReferenceToleranceRowDecision {
double absolute_error;
double threshold;
double relative_error;
bool relative_error_applicable;
ReferenceToleranceBranch branch;
bool passed;
};
struct ReferenceToleranceFamilyDecision {
std::string identity;
std::vector<std::string> components;
bool blocking;
double reference_scale;
double near_zero_band;
std::size_t near_zero_count;
double relative_rms;
double maximum_absolute_error;
std::size_t worst_row;
bool rms_passed;
bool passed;
std::string diagnostic_code;
std::vector<ReferenceToleranceRowDecision> rows;
std::size_t row_count{};
};
struct ReferenceToleranceEvaluation {
bool valid;
std::string diagnostic_code;
bool passed;
std::vector<ReferenceToleranceFamilyDecision> families;
};
class ReferenceTolerancePolicy {
public:
static constexpr double kNearZeroRatio = 0.01;
static constexpr double kRelativeTolerance = 0.05;
static constexpr double kRelativeRmsTolerance = 0.01;
static ReferenceToleranceEvaluation Evaluate(
const std::vector<ReferenceToleranceFamily>& families);
};
}
#endif
@@ -0,0 +1,118 @@
#include "reference_tolerance_policy.h"
#include <gtest/gtest.h>
#include <algorithm>
#include <cmath>
#include <limits>
#include <string>
#include <utility>
#include <vector>
namespace {
using fesa::test::ReferenceToleranceBranch;
using fesa::test::ReferenceToleranceFamily;
using fesa::test::ReferenceTolerancePolicy;
using fesa::test::ReferenceToleranceRow;
ReferenceToleranceFamily MakeFamily(std::string identity,
std::vector<std::string> components,
bool blocking,
std::vector<ReferenceToleranceRow> rows) {
return {std::move(identity), std::move(components), blocking,
std::move(rows)};
}
TEST(ReferenceTolerancePolicy,
AppliesRelativeAndNearZeroThresholdsAtTheirInclusiveBoundaries) {
const auto evaluation = ReferenceTolerancePolicy::Evaluate({MakeFamily(
"displacement-translation", {"UX", "UY", "UZ"}, true,
{{105.0, 100.0}, {105.001, 100.0}, {2.0, 1.0}, {2.001, 1.0}})});
ASSERT_TRUE(evaluation.valid);
ASSERT_EQ(evaluation.families.size(), 1U);
const auto& family = evaluation.families.front();
EXPECT_EQ(family.row_count, 4U);
EXPECT_DOUBLE_EQ(family.reference_scale, 100.0);
EXPECT_DOUBLE_EQ(family.near_zero_band, 1.0);
EXPECT_EQ(family.near_zero_count, 2U);
EXPECT_FALSE(family.passed);
ASSERT_EQ(family.rows.size(), 4U);
EXPECT_EQ(family.rows[0U].branch, ReferenceToleranceBranch::kRelative);
EXPECT_TRUE(family.rows[0U].relative_error_applicable);
EXPECT_DOUBLE_EQ(family.rows[0U].relative_error, 0.05);
EXPECT_DOUBLE_EQ(family.rows[0U].threshold, 5.0);
EXPECT_TRUE(family.rows[0U].passed);
EXPECT_FALSE(family.rows[1U].passed);
EXPECT_EQ(family.rows[2U].branch, ReferenceToleranceBranch::kNearZero);
EXPECT_FALSE(family.rows[2U].relative_error_applicable);
EXPECT_DOUBLE_EQ(family.rows[2U].threshold, 1.0);
EXPECT_TRUE(family.rows[2U].passed);
EXPECT_FALSE(family.rows[3U].passed);
}
TEST(ReferenceTolerancePolicy,
RejectsFamiliesWhoseScaleRelativeRmsExceedsOnePercent) {
const auto evaluation = ReferenceTolerancePolicy::Evaluate({MakeFamily(
"reaction-force", {"RF1", "RF2", "RF3"}, true,
{{101.1, 100.0}, {101.1, 100.0}, {101.1, 100.0}, {101.1, 100.0}})});
ASSERT_TRUE(evaluation.valid);
const auto& family = evaluation.families.front();
EXPECT_EQ(family.row_count, 4U);
EXPECT_TRUE(std::all_of(family.rows.begin(), family.rows.end(),
[](const auto& row) { return row.passed; }));
EXPECT_NEAR(family.relative_rms, 0.011, 1.0e-15);
EXPECT_FALSE(family.rms_passed);
EXPECT_FALSE(family.passed);
EXPECT_FALSE(evaluation.passed);
}
TEST(ReferenceTolerancePolicy,
FailsClosedForZeroScaleNonzeroErrorWithoutNonfiniteMetrics) {
const auto evaluation = ReferenceTolerancePolicy::Evaluate(
{MakeFamily("section-force", {"N"}, true, {{0.0, 0.0}, {1.0, 0.0}})});
ASSERT_TRUE(evaluation.valid);
const auto& family = evaluation.families.front();
EXPECT_EQ(family.row_count, 2U);
EXPECT_DOUBLE_EQ(family.reference_scale, 0.0);
EXPECT_DOUBLE_EQ(family.near_zero_band, 0.0);
EXPECT_EQ(family.near_zero_count, 2U);
EXPECT_EQ(family.diagnostic_code, "zero-reference-scale-nonzero-error");
EXPECT_EQ(family.rows[0U].branch, ReferenceToleranceBranch::kZeroScaleExact);
EXPECT_TRUE(family.rows[0U].passed);
EXPECT_FALSE(family.rows[1U].passed);
EXPECT_FALSE(family.rows[1U].relative_error_applicable);
EXPECT_TRUE(std::isfinite(family.relative_rms));
EXPECT_DOUBLE_EQ(family.relative_rms, 0.0);
EXPECT_FALSE(family.rms_passed);
EXPECT_FALSE(family.passed);
}
TEST(ReferenceTolerancePolicy,
KeepsWarningOnlyFamilyExceedanceOutOfBlockingVerdict) {
const auto evaluation = ReferenceTolerancePolicy::Evaluate(
{MakeFamily("translation", {"U1", "U2", "U3"}, true, {{0.0, 0.0}}),
MakeFamily("rotation", {"UR1", "UR2", "UR3"}, false, {{1.0, 0.0}})});
ASSERT_TRUE(evaluation.valid);
ASSERT_EQ(evaluation.families.size(), 2U);
EXPECT_TRUE(evaluation.families[0U].passed);
EXPECT_FALSE(evaluation.families[1U].passed);
EXPECT_TRUE(evaluation.passed);
}
TEST(ReferenceTolerancePolicy,
RejectsNonfiniteInputsBeforeToleranceEvaluation) {
const auto evaluation = ReferenceTolerancePolicy::Evaluate(
{MakeFamily("displacement-translation", {"UX", "UY", "UZ"}, true,
{{std::numeric_limits<double>::quiet_NaN(), 0.0}})});
EXPECT_FALSE(evaluation.valid);
EXPECT_EQ(evaluation.diagnostic_code, "nonfinite-comparison-value");
EXPECT_TRUE(evaluation.families.empty());
}
}