#include #include #include #include #include #include #include #include #include #include #include #include "nonlinear_static_reference_comparison.h" #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 #include "io/hdf5/mitc4_drilling_diagnostics.h" 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{stream}, std::istreambuf_iterator{}}; } fesa::test::NonlinearReferenceCase MakeCase(const std::string& case_id) { const std::filesystem::path root{FESA_TEST_SOURCE_DIR}; const bool nonlinear = case_id == "shellnl-s4-final"; const std::filesystem::path directory = root / "reference" / (nonlinear ? "shellNL" : "shell"); const std::string stem = 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& expected, const std::vector& 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, AuditsRetainedStaticArtifacts) { const std::array 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); 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-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 = "coarse."; const std::array 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 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::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; } } TEST(NonlinearStaticReference, DrillingSyntheticMomentSelectsOnlyExistingInternalFamily) { 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& path = generated.Value().generated_results_hdf5; const auto baseline = fesa::test::CompareHdf5ToReferenceCsv(reference_case, path); ASSERT_TRUE(baseline.HasValue()); ASSERT_TRUE(baseline.Value().drilling_diagnostics); const auto final_index = baseline.Value().drilling_diagnostics->frames.back().frame_index; const std::string root = "/steps/Step-1/frames/" + std::to_string(final_index) + "/global/"; const auto moment_gate = [](const auto& report) -> const auto& { const auto found = std::find_if(report.internal_equilibrium.begin(), report.internal_equilibrium.end(), [](const auto& family) { return family.family_identity == "coarse.global_moment_balance"; }); if (found == report.internal_equilibrium.end()) throw std::runtime_error{"Missing existing moment family."}; return *found; }; // This mutates only a generated working output into an algebra fixture. // Its comparator verdict is not physics reference evidence. for (const std::string mode : {"direct-initial-director-spring", "legacy-corotated-twist", "markerless"}) { { fesa::hdf5_internal::Hdf5Handle file{H5Fopen(path.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose}; fesa::hdf5_internal::Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose}; ASSERT_GE(H5Adelete(metadata.Get(), "mitc4_drilling_formulation"), 0); ASSERT_GE(H5Adelete(metadata.Get(), "drilling_diagnostics_version"), 0); if (mode != "markerless") { fesa::hdf5_internal::WriteStringAttribute(metadata.Get(), "mitc4_drilling_formulation", mode); fesa::hdf5_internal::WriteUint64Attribute(metadata.Get(), "drilling_diagnostics_version", 1U); } for (const auto& entry : std::vector>>{ {"equilibrium", {0.,0.,0.,1e6,0.,0.}}, {"drilling_moment", {1e6,0.,0.}}, {"equilibrium_with_drilling", {0.,0.,0.,0.,0.,0.}}}) { fesa::hdf5_internal::Hdf5Handle dataset{H5Dopen2(file.Get(), (root + entry.first).c_str(), H5P_DEFAULT), H5Dclose}; ASSERT_GE(H5Dwrite(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, entry.second.data()), 0); } } const auto compared = fesa::test::CompareHdf5ToReferenceCsv(reference_case, path); ASSERT_TRUE(compared.HasValue()); EXPECT_TRUE(compared.Value().external_blocking_passed); EXPECT_EQ(compared.Value().families.size(), baseline.Value().families.size()); EXPECT_EQ(compared.Value().internal_equilibrium.size(), baseline.Value().internal_equilibrium.size()); const auto& gate = moment_gate(compared.Value()); EXPECT_EQ(gate.scale, moment_gate(baseline.Value()).scale); EXPECT_EQ(gate.numerator, mode == "direct-initial-director-spring" ? 0.0 : 1e6); EXPECT_EQ(gate.passed, mode == "direct-initial-director-spring"); EXPECT_EQ(compared.Value().drilling_diagnostics.has_value(), mode != "markerless"); } // Restore new metadata, retain exact raw-minus-M algebra, and expose a real imbalance. { fesa::hdf5_internal::Hdf5Handle file{H5Fopen(path.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose}; fesa::hdf5_internal::WriteDrillingMetadata(file.Get(), fesa::Mitc4DrillingMode::kDirectSpring); for (const auto& entry : std::vector>>{ {"equilibrium", {0.,0.,0.,2e6,0.,0.}}, {"equilibrium_with_drilling", {0.,0.,0.,1e6,0.,0.}}}) { fesa::hdf5_internal::Hdf5Handle dataset{H5Dopen2(file.Get(), (root + entry.first).c_str(), H5P_DEFAULT), H5Dclose}; ASSERT_GE(H5Dwrite(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, entry.second.data()), 0); } } const auto imbalanced = fesa::test::CompareHdf5ToReferenceCsv(reference_case, path); ASSERT_TRUE(imbalanced.HasValue()); EXPECT_FALSE(moment_gate(imbalanced.Value()).passed); EXPECT_EQ(moment_gate(imbalanced.Value()).scale, moment_gate(baseline.Value()).scale); { fesa::hdf5_internal::Hdf5Handle file{H5Fopen(path.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose}; ASSERT_GE(H5Ldelete(file.Get(), "/steps/Step-1/frames/0/global/drilling_moment", H5P_DEFAULT), 0); } EXPECT_FALSE(fesa::test::CompareHdf5ToReferenceCsv(reference_case, path).HasValue()); }