feat: add eigen2 frequency pipeline and fail-closed validation
Implement MITC4 mass, constrained whitening, MKL FEAST, modal HDF5, and deterministic Value comparison. MSVC Debug builds; 394 of 395 tests pass. Original-model all-c residual and the documented S3 static contract conflict remain unresolved; this is an implementation checkpoint, not feature acceptance.
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
#include "nonlinear_static_reference_comparison.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -13,6 +11,8 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "nonlinear_static_reference_comparison.h"
|
||||
|
||||
#ifndef FESA_TEST_SOURCE_DIR
|
||||
#error FESA_TEST_SOURCE_DIR must identify the repository root.
|
||||
#endif
|
||||
@@ -34,13 +34,10 @@ std::string ReadBytes(const std::filesystem::path& path) {
|
||||
|
||||
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 bool nonlinear = case_id == "shellnl-s4-final";
|
||||
const std::filesystem::path directory =
|
||||
root / "reference" / (fine ? "shellNL-meshfine"
|
||||
: (nonlinear ? "shellNL" : "shell"));
|
||||
const std::string stem = fine ? "shellNL-meshfine"
|
||||
: (nonlinear ? "shellNL" : "shell");
|
||||
root / "reference" / (nonlinear ? "shellNL" : "shell");
|
||||
const std::string stem = nonlinear ? "shellNL" : "shell";
|
||||
return {case_id,
|
||||
directory / (stem + ".inp"),
|
||||
directory / (stem + " displacements.csv"),
|
||||
@@ -105,9 +102,8 @@ void ExpectInternalFamiliesPassed(
|
||||
<< family.family_identity << " rows=" << family.row_count
|
||||
<< " numerator=" << family.numerator << " scale=" << family.scale
|
||||
<< " metric="
|
||||
<< (family.normalized_metric.has_value()
|
||||
? *family.normalized_metric
|
||||
: -1.0)
|
||||
<< (family.normalized_metric.has_value() ? *family.normalized_metric
|
||||
: -1.0)
|
||||
<< " diagnostic=" << family.diagnostic_code;
|
||||
}
|
||||
}
|
||||
@@ -159,14 +155,14 @@ TEST(NonlinearStaticReference,
|
||||
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_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");
|
||||
: "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);
|
||||
@@ -182,13 +178,12 @@ TEST(NonlinearStaticReference,
|
||||
|
||||
ExpectFamily(comparison.Value(),
|
||||
case_id == "shellnl-s4-final" ? "coarse.translation"
|
||||
: "linear.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);
|
||||
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);
|
||||
@@ -219,9 +214,9 @@ TEST(NonlinearStaticReference,
|
||||
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());
|
||||
EXPECT_FALSE(
|
||||
fesa::test::CompareHdf5ToReferenceCsv(reference_case, invalid_hdf5)
|
||||
.HasValue());
|
||||
|
||||
fesa::test::NonlinearReferenceComparisonReport report{};
|
||||
report.blocking_passed = true;
|
||||
@@ -230,17 +225,29 @@ TEST(NonlinearStaticReference,
|
||||
{"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);
|
||||
{"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"};
|
||||
TEST(NonlinearStaticReference, AuditsRetainedStaticArtifacts) {
|
||||
const std::array<std::string, 2> case_ids{"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);
|
||||
@@ -281,16 +288,14 @@ TEST(NonlinearStaticReference,
|
||||
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);
|
||||
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"}) {
|
||||
TEST(NonlinearStaticReference, BlocksOnGlobalBalanceAndAcceptedFreeResidual) {
|
||||
for (const std::string case_id : {"shellnl-s4-final"}) {
|
||||
const auto reference_case = MakeCase(case_id);
|
||||
const auto copied =
|
||||
fesa::test::CopyInputToExecutionDirectory(reference_case);
|
||||
@@ -302,9 +307,7 @@ TEST(NonlinearStaticReference,
|
||||
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::string prefix = "coarse.";
|
||||
const std::array<std::string, 4> identities{
|
||||
prefix + "global_force_balance", prefix + "global_moment_balance",
|
||||
prefix + "accepted_free_force_residual",
|
||||
@@ -313,7 +316,8 @@ TEST(NonlinearStaticReference,
|
||||
const auto found = std::find_if(
|
||||
comparison.Value().internal_equilibrium.begin(),
|
||||
comparison.Value().internal_equilibrium.end(),
|
||||
[&identity](const fesa::test::NonlinearInternalEquilibriumReport& row) {
|
||||
[&identity](
|
||||
const fesa::test::NonlinearInternalEquilibriumReport& row) {
|
||||
return row.family_identity == identity;
|
||||
});
|
||||
ASSERT_NE(found, comparison.Value().internal_equilibrium.end())
|
||||
@@ -321,9 +325,8 @@ TEST(NonlinearStaticReference,
|
||||
EXPECT_TRUE(found->passed)
|
||||
<< identity << " numerator=" << found->numerator
|
||||
<< " scale=" << found->scale << " metric="
|
||||
<< (found->normalized_metric.has_value()
|
||||
? *found->normalized_metric
|
||||
: -1.0)
|
||||
<< (found->normalized_metric.has_value() ? *found->normalized_metric
|
||||
: -1.0)
|
||||
<< " diagnostic=" << found->diagnostic_code;
|
||||
}
|
||||
EXPECT_TRUE(comparison.Value().internal_blocking_passed);
|
||||
@@ -334,19 +337,26 @@ TEST(NonlinearStaticReference,
|
||||
}
|
||||
|
||||
// NSNR-REF-023.
|
||||
TEST(NonlinearStaticReference,
|
||||
RejectsMissingExtraDuplicateAndNonfiniteRowsBeforeToleranceAndRecordsReviewOnlyFindings) {
|
||||
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", {}}};
|
||||
{"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);
|
||||
@@ -359,22 +369,22 @@ 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);
|
||||
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);
|
||||
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);
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user