feat(nonlinear-static-newton-raphson): step 11 - equilibrium-projection-and-reference-gate
This commit is contained in:
@@ -0,0 +1,401 @@
|
||||
#include "nonlinear_static_reference_comparison.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#ifndef FESA_TEST_SOURCE_DIR
|
||||
#error FESA_TEST_SOURCE_DIR must identify the repository root.
|
||||
#endif
|
||||
|
||||
#ifndef FESA_TEST_BINARY_DIR
|
||||
#error FESA_TEST_BINARY_DIR must identify the CMake binary root.
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
std::string ReadBytes(const std::filesystem::path& path) {
|
||||
std::ifstream stream{path, std::ios::binary};
|
||||
if (!stream) {
|
||||
throw std::runtime_error{"Unable to read reference artifact."};
|
||||
}
|
||||
return {std::istreambuf_iterator<char>{stream},
|
||||
std::istreambuf_iterator<char>{}};
|
||||
}
|
||||
|
||||
fesa::test::NonlinearReferenceCase MakeCase(const std::string& case_id) {
|
||||
const std::filesystem::path root{FESA_TEST_SOURCE_DIR};
|
||||
const bool fine = case_id == "shellnl-meshfine-s4-final";
|
||||
const bool nonlinear = case_id == "shellnl-s4-final" || fine;
|
||||
const std::filesystem::path directory =
|
||||
root / "reference" / (fine ? "shellNL-meshfine"
|
||||
: (nonlinear ? "shellNL" : "shell"));
|
||||
const std::string stem = fine ? "shellNL-meshfine"
|
||||
: (nonlinear ? "shellNL" : "shell");
|
||||
return {case_id,
|
||||
directory / (stem + ".inp"),
|
||||
directory / (stem + " displacements.csv"),
|
||||
directory / (stem + " reactions.csv"),
|
||||
directory / (stem + " stresses.csv"),
|
||||
std::filesystem::path{FESA_TEST_BINARY_DIR} / "reference" /
|
||||
"nonlinear-static-reference" / stem};
|
||||
}
|
||||
|
||||
void ExpectSnapshotsEqual(
|
||||
const std::vector<fesa::test::ArtifactSnapshot>& expected,
|
||||
const std::vector<fesa::test::ArtifactSnapshot>& actual) {
|
||||
ASSERT_EQ(actual.size(), expected.size());
|
||||
for (std::size_t index = 0U; index < expected.size(); ++index) {
|
||||
EXPECT_EQ(actual[index].path, expected[index].path);
|
||||
EXPECT_EQ(actual[index].sha256, expected[index].sha256);
|
||||
EXPECT_EQ(actual[index].byte_size, expected[index].byte_size);
|
||||
EXPECT_EQ(actual[index].last_write_time, expected[index].last_write_time);
|
||||
}
|
||||
}
|
||||
|
||||
const fesa::test::NonlinearReferenceFamilyReport* FindFamily(
|
||||
const fesa::test::NonlinearReferenceComparisonReport& report,
|
||||
const std::string& identity) {
|
||||
const auto found = std::find_if(
|
||||
report.families.begin(), report.families.end(),
|
||||
[&identity](const fesa::test::NonlinearReferenceFamilyReport& family) {
|
||||
return family.family_identity == identity;
|
||||
});
|
||||
return found == report.families.end() ? nullptr : &*found;
|
||||
}
|
||||
|
||||
void ExpectFamily(const fesa::test::NonlinearReferenceComparisonReport& report,
|
||||
const std::string& identity, const std::size_t row_count,
|
||||
const fesa::test::NonlinearReferenceDisposition disposition) {
|
||||
const auto* family = FindFamily(report, identity);
|
||||
ASSERT_NE(family, nullptr) << identity;
|
||||
EXPECT_EQ(family->row_count, row_count);
|
||||
EXPECT_EQ(family->disposition, disposition);
|
||||
}
|
||||
|
||||
void ExpectBlockingFamiliesPassed(
|
||||
const fesa::test::NonlinearReferenceComparisonReport& report) {
|
||||
for (const auto& family : report.families) {
|
||||
if (family.disposition !=
|
||||
fesa::test::NonlinearReferenceDisposition::kBlocking) {
|
||||
continue;
|
||||
}
|
||||
EXPECT_TRUE(family.tolerance.passed)
|
||||
<< family.family_identity << " rows=" << family.row_count
|
||||
<< " scale=" << family.tolerance.reference_scale
|
||||
<< " max_abs=" << family.tolerance.maximum_absolute_error
|
||||
<< " rms=" << family.tolerance.relative_rms
|
||||
<< " diagnostic=" << family.tolerance.diagnostic_code;
|
||||
}
|
||||
}
|
||||
|
||||
void ExpectInternalFamiliesPassed(
|
||||
const fesa::test::NonlinearReferenceComparisonReport& report) {
|
||||
for (const auto& family : report.internal_equilibrium) {
|
||||
EXPECT_TRUE(family.passed)
|
||||
<< family.family_identity << " rows=" << family.row_count
|
||||
<< " numerator=" << family.numerator << " scale=" << family.scale
|
||||
<< " metric="
|
||||
<< (family.normalized_metric.has_value()
|
||||
? *family.normalized_metric
|
||||
: -1.0)
|
||||
<< " diagnostic=" << family.diagnostic_code;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// NSNR-REF-001 and NSNR-REF-018.
|
||||
TEST(NonlinearStaticReference, AuditsEveryDeclaredArtifactBeforeExecution) {
|
||||
const auto nonlinear = MakeCase("shellnl-s4-final");
|
||||
const auto linear = MakeCase("shell-s4-linear-limit");
|
||||
|
||||
const auto nonlinear_artifacts = fesa::test::ArtifactCheck(nonlinear);
|
||||
ASSERT_TRUE(nonlinear_artifacts.HasValue());
|
||||
ASSERT_EQ(nonlinear_artifacts.Value().size(), 4U);
|
||||
EXPECT_EQ(nonlinear_artifacts.Value()[0U].sha256,
|
||||
"4BF3ECB0F7F1E8462C573E993DBC97AC5D920C91CF6F23AD4C6891D77FFBADA2");
|
||||
EXPECT_EQ(nonlinear_artifacts.Value()[3U].sha256,
|
||||
"5500E5114B1C44887528E30CEC054A1EB0F68BE3F4B8CE81FA59FC74A881483F");
|
||||
|
||||
const auto linear_artifacts = fesa::test::ArtifactCheck(linear);
|
||||
ASSERT_TRUE(linear_artifacts.HasValue());
|
||||
ASSERT_EQ(linear_artifacts.Value().size(), 4U);
|
||||
EXPECT_EQ(linear_artifacts.Value()[0U].sha256,
|
||||
"4005851E1AB22FD3A16AC17A8D5DA3E051233F69F37419079F3553AD134ECFCF");
|
||||
EXPECT_EQ(linear_artifacts.Value()[3U].sha256,
|
||||
"2B75A5B0DEB045A283C9BB7E9B1153B79F7537FF487214BF4A23316B59B660A7");
|
||||
}
|
||||
|
||||
// NSNR-REF-002 through NSNR-REF-018.
|
||||
TEST(NonlinearStaticReference,
|
||||
CopiesRunsComparesAndPreservesTheReadOnlyReferenceInventory) {
|
||||
for (const std::string case_id :
|
||||
{"shellnl-s4-final", "shell-s4-linear-limit"}) {
|
||||
const auto reference_case = MakeCase(case_id);
|
||||
const auto before = fesa::test::ArtifactCheck(reference_case);
|
||||
ASSERT_TRUE(before.HasValue());
|
||||
|
||||
const auto copied =
|
||||
fesa::test::CopyInputToExecutionDirectory(reference_case);
|
||||
ASSERT_TRUE(copied.HasValue());
|
||||
EXPECT_NE(copied.Value().parent_path(),
|
||||
reference_case.original_input_path.parent_path());
|
||||
EXPECT_EQ(ReadBytes(copied.Value()),
|
||||
ReadBytes(reference_case.original_input_path));
|
||||
|
||||
const auto generated = fesa::test::RunFesaApplicationOnCopiedInput(
|
||||
reference_case, copied.Value());
|
||||
ASSERT_TRUE(generated.HasValue());
|
||||
EXPECT_EQ(generated.Value().copied_input_path, copied.Value());
|
||||
EXPECT_TRUE(std::filesystem::is_regular_file(
|
||||
generated.Value().generated_results_hdf5));
|
||||
EXPECT_TRUE(std::filesystem::is_regular_file(
|
||||
generated.Value().generated_log_path));
|
||||
EXPECT_EQ(generated.Value().generated_results_hdf5.filename(),
|
||||
case_id == "shellnl-s4-final" ? "shellNL_results.h5"
|
||||
: "shell_results.h5");
|
||||
EXPECT_EQ(generated.Value().generated_log_path.filename(),
|
||||
case_id == "shellnl-s4-final" ? "shellNL_log.txt"
|
||||
: "shell_log.txt");
|
||||
|
||||
const auto comparison = fesa::test::CompareHdf5ToReferenceCsv(
|
||||
reference_case, generated.Value().generated_results_hdf5);
|
||||
ASSERT_TRUE(comparison.HasValue());
|
||||
ExpectBlockingFamiliesPassed(comparison.Value());
|
||||
if (case_id == "shellnl-s4-final") {
|
||||
ExpectInternalFamiliesPassed(comparison.Value());
|
||||
}
|
||||
EXPECT_TRUE(comparison.Value().blocking_passed);
|
||||
EXPECT_EQ(comparison.Value().copied_input_path, copied.Value());
|
||||
EXPECT_EQ(comparison.Value().generated_log_path,
|
||||
generated.Value().generated_log_path);
|
||||
|
||||
ExpectFamily(comparison.Value(),
|
||||
case_id == "shellnl-s4-final" ? "coarse.translation"
|
||||
: "linear.translation",
|
||||
147U, fesa::test::NonlinearReferenceDisposition::kBlocking);
|
||||
ExpectFamily(comparison.Value(),
|
||||
case_id == "shellnl-s4-final" ? "coarse.rotation"
|
||||
: "linear.rotation",
|
||||
147U,
|
||||
fesa::test::NonlinearReferenceDisposition::kWarningOnly);
|
||||
if (case_id == "shellnl-s4-final") {
|
||||
ExpectFamily(comparison.Value(), "coarse.local_reaction_inplane_force",
|
||||
48U, fesa::test::NonlinearReferenceDisposition::kReviewOnly);
|
||||
ExpectFamily(comparison.Value(), "coarse.local_reaction_transverse_force",
|
||||
24U, fesa::test::NonlinearReferenceDisposition::kReviewOnly);
|
||||
ExpectFamily(comparison.Value(), "coarse.local_reaction_physical_moment",
|
||||
48U, fesa::test::NonlinearReferenceDisposition::kReviewOnly);
|
||||
EXPECT_EQ(FindFamily(comparison.Value(), "RM3"), nullptr);
|
||||
}
|
||||
|
||||
const auto after = fesa::test::ArtifactCheck(reference_case);
|
||||
ASSERT_TRUE(after.HasValue());
|
||||
ExpectSnapshotsEqual(before.Value(), after.Value());
|
||||
}
|
||||
}
|
||||
|
||||
// NSNR-REF-009 and NSNR-REF-017.
|
||||
TEST(NonlinearStaticReference,
|
||||
RejectsInvalidHdf5BeforeToleranceAndKeepsWarningOnlyClassification) {
|
||||
const auto reference_case = MakeCase("shellnl-s4-final");
|
||||
const auto copied = fesa::test::CopyInputToExecutionDirectory(reference_case);
|
||||
ASSERT_TRUE(copied.HasValue());
|
||||
const auto generated = fesa::test::RunFesaApplicationOnCopiedInput(
|
||||
reference_case, copied.Value());
|
||||
ASSERT_TRUE(generated.HasValue());
|
||||
|
||||
const auto invalid_hdf5 = copied.Value().parent_path() / "invalid.h5";
|
||||
std::ofstream stream{invalid_hdf5, std::ios::binary | std::ios::trunc};
|
||||
stream << "not an HDF5 file";
|
||||
stream.close();
|
||||
EXPECT_FALSE(fesa::test::CompareHdf5ToReferenceCsv(reference_case,
|
||||
invalid_hdf5)
|
||||
.HasValue());
|
||||
|
||||
fesa::test::NonlinearReferenceComparisonReport report{};
|
||||
report.blocking_passed = true;
|
||||
report.families.push_back(
|
||||
{"nonlinear.rotation",
|
||||
{"UR1", "UR2", "UR3"},
|
||||
fesa::test::NonlinearReferenceDisposition::kWarningOnly,
|
||||
147U,
|
||||
{"nonlinear.rotation", {"UR1", "UR2", "UR3"}, false, 1.0, 0.01,
|
||||
0U, 0.02, 0.02, 0U, false, false, "relative-rms-exceeded", {}, 147U}});
|
||||
EXPECT_EQ(fesa::test::ClassifyReferenceComparison(report),
|
||||
fesa::test::NonlinearReferenceClassification::kWarningOnlyDifferences);
|
||||
}
|
||||
|
||||
// NSNR-REF-020.
|
||||
TEST(NonlinearStaticReference, AuditsFinePrimaryAndCoarseCompanionArtifacts) {
|
||||
const std::array<std::string, 3> case_ids{
|
||||
"shellnl-meshfine-s4-final", "shellnl-s4-final",
|
||||
"shell-s4-linear-limit"};
|
||||
for (const auto& case_id : case_ids) {
|
||||
const auto reference_case = MakeCase(case_id);
|
||||
const auto before = fesa::test::ArtifactCheck(reference_case);
|
||||
ASSERT_TRUE(before.HasValue()) << case_id;
|
||||
ASSERT_EQ(before.Value().size(), 4U) << case_id;
|
||||
|
||||
const auto copied =
|
||||
fesa::test::CopyInputToExecutionDirectory(reference_case);
|
||||
ASSERT_TRUE(copied.HasValue()) << case_id;
|
||||
EXPECT_NE(copied.Value().parent_path(),
|
||||
reference_case.original_input_path.parent_path());
|
||||
const auto generated = fesa::test::RunFesaApplicationOnCopiedInput(
|
||||
reference_case, copied.Value());
|
||||
ASSERT_TRUE(generated.HasValue()) << case_id;
|
||||
EXPECT_TRUE(std::filesystem::is_regular_file(
|
||||
generated.Value().generated_results_hdf5));
|
||||
|
||||
const auto after = fesa::test::ArtifactCheck(reference_case);
|
||||
ASSERT_TRUE(after.HasValue()) << case_id;
|
||||
ExpectSnapshotsEqual(before.Value(), after.Value());
|
||||
}
|
||||
}
|
||||
|
||||
// NSNR-REF-021.
|
||||
TEST(NonlinearStaticReference,
|
||||
ReviewOnlyLocalReactionsCannotChangeBlockingVerdict) {
|
||||
const auto reference_case = MakeCase("shellnl-s4-final");
|
||||
const auto copied = fesa::test::CopyInputToExecutionDirectory(reference_case);
|
||||
ASSERT_TRUE(copied.HasValue());
|
||||
const auto generated = fesa::test::RunFesaApplicationOnCopiedInput(
|
||||
reference_case, copied.Value());
|
||||
ASSERT_TRUE(generated.HasValue());
|
||||
const auto comparison = fesa::test::CompareHdf5ToReferenceCsv(
|
||||
reference_case, generated.Value().generated_results_hdf5);
|
||||
ASSERT_TRUE(comparison.HasValue());
|
||||
|
||||
ExpectFamily(comparison.Value(), "coarse.local_reaction_inplane_force", 48U,
|
||||
fesa::test::NonlinearReferenceDisposition::kReviewOnly);
|
||||
ExpectFamily(comparison.Value(), "coarse.local_reaction_transverse_force",
|
||||
24U, fesa::test::NonlinearReferenceDisposition::kReviewOnly);
|
||||
ExpectFamily(comparison.Value(), "coarse.local_reaction_physical_moment",
|
||||
48U, fesa::test::NonlinearReferenceDisposition::kReviewOnly);
|
||||
EXPECT_TRUE(comparison.Value().external_blocking_passed);
|
||||
}
|
||||
|
||||
// NSNR-REF-022.
|
||||
TEST(NonlinearStaticReference,
|
||||
BlocksOnGlobalBalanceAndAcceptedFreeResidual) {
|
||||
for (const std::string case_id :
|
||||
{"shellnl-meshfine-s4-final", "shellnl-s4-final"}) {
|
||||
const auto reference_case = MakeCase(case_id);
|
||||
const auto copied =
|
||||
fesa::test::CopyInputToExecutionDirectory(reference_case);
|
||||
ASSERT_TRUE(copied.HasValue()) << case_id;
|
||||
const auto generated = fesa::test::RunFesaApplicationOnCopiedInput(
|
||||
reference_case, copied.Value());
|
||||
ASSERT_TRUE(generated.HasValue()) << case_id;
|
||||
const auto comparison = fesa::test::CompareHdf5ToReferenceCsv(
|
||||
reference_case, generated.Value().generated_results_hdf5);
|
||||
ASSERT_TRUE(comparison.HasValue()) << case_id;
|
||||
|
||||
const std::string prefix = case_id == "shellnl-meshfine-s4-final"
|
||||
? "fine."
|
||||
: "coarse.";
|
||||
const std::array<std::string, 4> identities{
|
||||
prefix + "global_force_balance", prefix + "global_moment_balance",
|
||||
prefix + "accepted_free_force_residual",
|
||||
prefix + "accepted_free_moment_residual"};
|
||||
for (const auto& identity : identities) {
|
||||
const auto found = std::find_if(
|
||||
comparison.Value().internal_equilibrium.begin(),
|
||||
comparison.Value().internal_equilibrium.end(),
|
||||
[&identity](const fesa::test::NonlinearInternalEquilibriumReport& row) {
|
||||
return row.family_identity == identity;
|
||||
});
|
||||
ASSERT_NE(found, comparison.Value().internal_equilibrium.end())
|
||||
<< identity;
|
||||
EXPECT_TRUE(found->passed)
|
||||
<< identity << " numerator=" << found->numerator
|
||||
<< " scale=" << found->scale << " metric="
|
||||
<< (found->normalized_metric.has_value()
|
||||
? *found->normalized_metric
|
||||
: -1.0)
|
||||
<< " diagnostic=" << found->diagnostic_code;
|
||||
}
|
||||
EXPECT_TRUE(comparison.Value().internal_blocking_passed);
|
||||
const std::array<double, 3> zero_resultant{};
|
||||
EXPECT_NE(comparison.Value().constrained_reaction_resultant.force,
|
||||
zero_resultant);
|
||||
}
|
||||
}
|
||||
|
||||
// NSNR-REF-023.
|
||||
TEST(NonlinearStaticReference,
|
||||
RejectsMissingExtraDuplicateAndNonfiniteRowsBeforeToleranceAndRecordsReviewOnlyFindings) {
|
||||
fesa::test::NonlinearReferenceComparisonReport report{};
|
||||
report.blocking_passed = true;
|
||||
report.external_blocking_passed = true;
|
||||
report.internal_blocking_passed = true;
|
||||
report.precheck_findings = {
|
||||
{"blocking", fesa::test::NonlinearReferenceDisposition::kBlocking,
|
||||
"missing-row", {}},
|
||||
{"warning", fesa::test::NonlinearReferenceDisposition::kWarningOnly,
|
||||
"duplicate-row", {}},
|
||||
{"review", fesa::test::NonlinearReferenceDisposition::kReviewOnly,
|
||||
"nonfinite-row", {}}};
|
||||
|
||||
EXPECT_EQ(fesa::test::ClassifyReferenceComparison(report),
|
||||
fesa::test::NonlinearReferenceClassification::kFailed);
|
||||
EXPECT_EQ(report.precheck_findings[1U].diagnostic_code, "duplicate-row");
|
||||
EXPECT_EQ(report.precheck_findings[2U].diagnostic_code, "nonfinite-row");
|
||||
}
|
||||
|
||||
// NSNR-REF-024.
|
||||
TEST(NonlinearStaticReference,
|
||||
GlobalForceAndMomentBalanceUseApprovedInclusiveOneEighthGate) {
|
||||
for (const std::string& identity :
|
||||
{"fine.global_force_balance", "fine.global_moment_balance"}) {
|
||||
const auto below = fesa::test::EvaluateGlobalBalance(
|
||||
identity, 5.0e-9, 1.0, 0.0);
|
||||
EXPECT_TRUE(below.passed) << identity;
|
||||
ASSERT_TRUE(below.normalized_metric.has_value()) << identity;
|
||||
EXPECT_EQ(*below.normalized_metric, 5.0e-9) << identity;
|
||||
|
||||
const auto boundary = fesa::test::EvaluateGlobalBalance(
|
||||
identity, 1.0e-8, 1.0, 0.0);
|
||||
EXPECT_TRUE(boundary.passed) << identity;
|
||||
ASSERT_TRUE(boundary.normalized_metric.has_value()) << identity;
|
||||
EXPECT_EQ(*boundary.normalized_metric, 1.0e-8) << identity;
|
||||
|
||||
const auto above = fesa::test::EvaluateGlobalBalance(
|
||||
identity, std::nextafter(1.0e-8,
|
||||
std::numeric_limits<double>::infinity()),
|
||||
1.0, 0.0);
|
||||
EXPECT_FALSE(above.passed) << identity;
|
||||
ASSERT_TRUE(above.normalized_metric.has_value()) << identity;
|
||||
EXPECT_GT(*above.normalized_metric, 1.0e-8) << identity;
|
||||
}
|
||||
}
|
||||
|
||||
// NSNR-REF-025.
|
||||
TEST(NonlinearStaticReference,
|
||||
GlobalForceAndMomentExactZeroScaleRequiresExactZeroBalance) {
|
||||
for (const std::string& identity :
|
||||
{"coarse.global_force_balance", "coarse.global_moment_balance"}) {
|
||||
const auto exact_zero =
|
||||
fesa::test::EvaluateGlobalBalance(identity, 0.0, 0.0, 0.0);
|
||||
EXPECT_TRUE(exact_zero.passed) << identity;
|
||||
EXPECT_EQ(exact_zero.scale, 0.0) << identity;
|
||||
EXPECT_FALSE(exact_zero.normalized_metric.has_value()) << identity;
|
||||
|
||||
const auto nonzero =
|
||||
fesa::test::EvaluateGlobalBalance(identity, 1.0e-300, 0.0, 0.0);
|
||||
EXPECT_FALSE(nonzero.passed) << identity;
|
||||
EXPECT_EQ(nonzero.scale, 0.0) << identity;
|
||||
EXPECT_FALSE(nonzero.normalized_metric.has_value()) << identity;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user