From 681794289411ebab68d03f554c70129f35a6c845 Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Tue, 1 Sep 2026 17:00:55 +0900 Subject: [PATCH] feat(shell-surface-pressure-load): step 6 - compare-pressure-reference --- tests/CMakeLists.txt | 1 + .../nonlinear_static_reference_comparison.cpp | 114 +++++++- .../nonlinear_static_reference_comparison.h | 14 + .../shell_surface_pressure_reference_test.cpp | 261 ++++++++++++++++++ 4 files changed, 382 insertions(+), 8 deletions(-) create mode 100644 tests/reference/shell_surface_pressure_reference_test.cpp diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index a8580a0..2926538 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -102,6 +102,7 @@ add_executable( reference/mitc4_reference_cases_test.cpp reference/nonlinear_static_reference_comparison.cpp reference/nonlinear_static_reference_cases_test.cpp + reference/shell_surface_pressure_reference_test.cpp ) target_link_libraries( diff --git a/tests/reference/nonlinear_static_reference_comparison.cpp b/tests/reference/nonlinear_static_reference_comparison.cpp index 7055cf1..f23d8a2 100644 --- a/tests/reference/nonlinear_static_reference_comparison.cpp +++ b/tests/reference/nonlinear_static_reference_comparison.cpp @@ -62,6 +62,7 @@ struct CaseContract { const CaseContract& ContractFor(const NonlinearReferenceCase& reference_case) { static const CaseContract kCoarse{49U, true, "coarse."}; static const CaseContract kFine{729U, true, "fine."}; + static const CaseContract kPressure{729U, true, "pressure."}; static const CaseContract kLinear{49U, false, "linear."}; if (reference_case.case_id == "shellnl-s4-final") { return kCoarse; @@ -69,6 +70,9 @@ const CaseContract& ContractFor(const NonlinearReferenceCase& reference_case) { if (reference_case.case_id == "shellnl-meshfine-s4-final") { return kFine; } + if (reference_case.case_id == "shellnl-meshfine-pressure-s4-follower") { + return kPressure; + } if (reference_case.case_id == "shell-s4-linear-limit") { return kLinear; } @@ -276,11 +280,49 @@ std::string Fnv1a64(const std::string& input) { struct DeclaredArtifact { std::filesystem::path path; const char* sha256; + std::uintmax_t expected_byte_size{0U}; + NonlinearReferenceArtifactDisposition disposition{ + NonlinearReferenceArtifactDisposition::kRequired}; }; std::vector DeclaredArtifacts( const NonlinearReferenceCase& reference_case) { const std::filesystem::path root{FESA_TEST_SOURCE_DIR}; + if (reference_case.case_id == "shellnl-meshfine-pressure-s4-follower") { + const auto directory = root / "reference" / "shellNL-meshfine-pressure"; + const std::vector artifacts{ + {directory / "shellNL-meshfine-pressure.inp", + "53718CCC9B41C23FCC1B509A51D66F1347E87ACA43A74F1D6CEC4D2085F4B90E", + 56665U, reference_case.input_artifact_disposition}, + {directory / "shellNL-meshfine-pressure displacements.csv", + "2FE53F98EAC9F04779100721C3EDF8049EB1DBFE733EBCBD819AEEED03B81198", + 111182U, reference_case.displacement_artifact_disposition}, + {directory / "shellNL-meshfine-pressure reactions.csv", nullptr, 0U, + reference_case.reaction_artifact_disposition}, + {directory / "shellNL-meshfine-pressure stresses.csv", nullptr, 0U, + reference_case.stress_artifact_disposition}}; + if (Normalized(reference_case.original_input_path) != + Normalized(artifacts[0U].path) || + Normalized(reference_case.displacement_csv_path) != + Normalized(artifacts[1U].path) || + Normalized(reference_case.reaction_csv_path) != + Normalized(artifacts[2U].path) || + Normalized(reference_case.stress_inventory_path) != + Normalized(artifacts[3U].path) || + reference_case.input_artifact_disposition != + NonlinearReferenceArtifactDisposition::kRequired || + reference_case.displacement_artifact_disposition != + NonlinearReferenceArtifactDisposition::kRequired || + reference_case.reaction_artifact_disposition != + NonlinearReferenceArtifactDisposition::kReviewOnly || + reference_case.stress_artifact_disposition != + NonlinearReferenceArtifactDisposition::kExcluded) { + Fail("needs-reference-artifacts", + "The pressure reference case does not use its declared artifact " + "dispositions."); + } + return artifacts; + } if (reference_case.case_id == "shellnl-meshfine-s4-final") { const auto directory = root / "reference" / "shellNL-meshfine"; const std::vector artifacts{ @@ -364,7 +406,7 @@ ArtifactSnapshot Snapshot(const DeclaredArtifact& artifact) { } const std::string bytes = ReadBytes(artifact.path); const std::string hash = Sha256(bytes); - if (hash != artifact.sha256) { + if (artifact.sha256 == nullptr || hash != artifact.sha256) { Fail("reference-artifact-hash-mismatch", "A declared reference artifact does not match its approved hash."); } @@ -373,6 +415,11 @@ ArtifactSnapshot Snapshot(const DeclaredArtifact& artifact) { Fail("needs-reference-artifacts", "A declared reference artifact size cannot be read."); } + if (artifact.expected_byte_size != 0U && + size != artifact.expected_byte_size) { + Fail("reference-artifact-size-mismatch", + "A declared reference artifact does not match its approved size."); + } const auto write_time = std::filesystem::last_write_time(artifact.path, error); if (error) { Fail("needs-reference-artifacts", @@ -983,6 +1030,17 @@ void RequireExactNodeLabels(const std::vector& nodes, } } +template +void RequirePressureInstance(const std::vector& rows) { + const std::string expected = "PART-1-1"; + if (!std::all_of(rows.begin(), rows.end(), [&expected](const Row& row) { + return UppercaseAscii(row.instance_name) == expected; + })) { + Fail("schema-mismatch", + "The pressure reference instance identity is out of contract."); + } +} + std::string Key(const std::string& instance_name, const std::int64_t source_node_label, const std::string& component) { @@ -1411,6 +1469,38 @@ void CompareNonlinearCase(NonlinearReferenceComparisonReport& report, AppendInternalEquilibrium(&report, reference_case, hdf5); } +void ComparePressureCase(NonlinearReferenceComparisonReport& report, + const NonlinearReferenceCase& reference_case) { + const CaseContract& contract = ContractFor(reference_case); + const auto hdf5 = + ReadNonlinearHdf5(reference_case, report.generated_results_hdf5); + report.copied_input_path = hdf5.copied_input_path; + report.generated_log_path = DerivedLogPath(hdf5.copied_input_path); + RequireExactNodeLabels(hdf5.nodes, contract.node_count); + RequirePressureInstance(hdf5.nodes); + const auto displacement_rows = + ReadCsvWideRows(reference_case.displacement_csv_path, true, + kDisplacementComponents, contract.node_count); + RequireExactNodeLabels(displacement_rows, contract.node_count); + RequirePressureInstance(displacement_rows); + + const std::vector translations{0U, 1U, 2U}; + const std::vector rotations{3U, 4U, 5U}; + const std::vector all_nodes; + AppendFamily(report, "pressure_nodal_translation_blocking", + {"U1", "U2", "U3"}, NonlinearReferenceDisposition::kBlocking, + ProjectHdf5(hdf5, hdf5.displacement, kDisplacementComponents, + translations, all_nodes), + ProjectCsv(displacement_rows, kDisplacementComponents, + translations, all_nodes)); + AppendFamily(report, "pressure_nodal_rotation_warning", {"UR1", "UR2", "UR3"}, + NonlinearReferenceDisposition::kWarningOnly, + ProjectHdf5(hdf5, hdf5.displacement, kDisplacementComponents, + rotations, all_nodes), + ProjectCsv(displacement_rows, kDisplacementComponents, rotations, + all_nodes)); +} + } // namespace NonlinearInternalEquilibriumReport EvaluateGlobalBalance( @@ -1434,7 +1524,10 @@ Result> ArtifactCheck( std::vector snapshots; snapshots.reserve(artifacts.size()); for (const auto& artifact : artifacts) { - snapshots.push_back(Snapshot(artifact)); + if (artifact.disposition == + NonlinearReferenceArtifactDisposition::kRequired) { + snapshots.push_back(Snapshot(artifact)); + } } return Result>::Success(std::move(snapshots)); } catch (const ComparisonFailure& exception) { @@ -1542,8 +1635,11 @@ Result CompareHdf5ToReferenceCsv( if (!std::filesystem::is_regular_file(hdf5_results_path)) { Fail("schema-mismatch", "The generated HDF5 result is missing."); } - auto report = MakeReport(reference_case, hdf5_results_path, before.Value(), {}); - if (ContractFor(reference_case).nonlinear) { + auto report = + MakeReport(reference_case, hdf5_results_path, before.Value(), {}); + if (reference_case.case_id == "shellnl-meshfine-pressure-s4-follower") { + ComparePressureCase(report, reference_case); + } else if (ContractFor(reference_case).nonlinear) { CompareNonlinearCase(report, reference_case); } else if (reference_case.case_id == "shell-s4-linear-limit") { CompareLinearCase(report, reference_case); @@ -1552,12 +1648,14 @@ Result CompareHdf5ToReferenceCsv( } const auto after = ArtifactCheck(reference_case); if (!after.HasValue()) { - return Result::Failure(after.GetStatus()); + return Result::Failure( + after.GetStatus()); } report.post_run_artifacts = after.Value(); - report.blocking_passed = report.external_blocking_passed && - report.internal_blocking_passed; - return Result::Success(std::move(report)); + report.blocking_passed = + report.external_blocking_passed && report.internal_blocking_passed; + return Result::Success( + std::move(report)); } catch (const ComparisonFailure& exception) { return Result::Failure( FailureStatus(reference_case, exception.Code(), exception.what())); diff --git a/tests/reference/nonlinear_static_reference_comparison.h b/tests/reference/nonlinear_static_reference_comparison.h index dc1b235..a0ac0aa 100644 --- a/tests/reference/nonlinear_static_reference_comparison.h +++ b/tests/reference/nonlinear_static_reference_comparison.h @@ -14,6 +14,12 @@ namespace fesa::test { +enum class NonlinearReferenceArtifactDisposition { + kRequired, + kReviewOnly, + kExcluded, +}; + struct NonlinearReferenceCase { std::string case_id; std::filesystem::path original_input_path; @@ -21,6 +27,14 @@ struct NonlinearReferenceCase { std::filesystem::path reaction_csv_path; std::filesystem::path stress_inventory_path; std::filesystem::path execution_directory; + NonlinearReferenceArtifactDisposition input_artifact_disposition{ + NonlinearReferenceArtifactDisposition::kRequired}; + NonlinearReferenceArtifactDisposition displacement_artifact_disposition{ + NonlinearReferenceArtifactDisposition::kRequired}; + NonlinearReferenceArtifactDisposition reaction_artifact_disposition{ + NonlinearReferenceArtifactDisposition::kRequired}; + NonlinearReferenceArtifactDisposition stress_artifact_disposition{ + NonlinearReferenceArtifactDisposition::kRequired}; }; struct ArtifactSnapshot { diff --git a/tests/reference/shell_surface_pressure_reference_test.cpp b/tests/reference/shell_surface_pressure_reference_test.cpp new file mode 100644 index 0000000..c502ffc --- /dev/null +++ b/tests/reference/shell_surface_pressure_reference_test.cpp @@ -0,0 +1,261 @@ +#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 + +namespace { + +fesa::test::NonlinearReferenceCase MakePressureCase() { + const std::filesystem::path root{FESA_TEST_SOURCE_DIR}; + const std::filesystem::path directory = + root / "reference" / "shellNL-meshfine-pressure"; + return {"shellnl-meshfine-pressure-s4-follower", + directory / "shellNL-meshfine-pressure.inp", + directory / "shellNL-meshfine-pressure displacements.csv", + directory / "shellNL-meshfine-pressure reactions.csv", + directory / "shellNL-meshfine-pressure stresses.csv", + std::filesystem::path{FESA_TEST_BINARY_DIR} / "reference" / + "shell-surface-pressure-load", + fesa::test::NonlinearReferenceArtifactDisposition::kRequired, + fesa::test::NonlinearReferenceArtifactDisposition::kRequired, + fesa::test::NonlinearReferenceArtifactDisposition::kReviewOnly, + fesa::test::NonlinearReferenceArtifactDisposition::kExcluded}; +} + +std::optional FindCachedResults( + const fesa::test::NonlinearReferenceCase& reference_case) { + const std::filesystem::path solver_library = + std::filesystem::path{FESA_TEST_BINARY_DIR} / "src" / "fesa" / "Debug" / + "fesa_solver.lib"; + std::error_code error; + const auto solver_time = + std::filesystem::last_write_time(solver_library, error); + if (error || + !std::filesystem::is_directory(reference_case.execution_directory)) { + return std::nullopt; + } + std::vector candidates; + for (const auto& entry : std::filesystem::recursive_directory_iterator( + reference_case.execution_directory, error)) { + if (error) { + return std::nullopt; + } + if (entry.path().filename() == "shellNL-meshfine-pressure_results.h5" && + std::filesystem::is_regular_file(entry.path()) && + std::filesystem::last_write_time(entry.path(), error) >= solver_time && + !error) { + candidates.push_back(entry.path()); + } + } + std::sort(candidates.begin(), candidates.end()); + return candidates.empty() + ? std::nullopt + : std::optional{candidates.back()}; +} + +const fesa::test::NonlinearReferenceComparisonReport& RunPressureComparison() { + static const auto report = [] { + const auto reference_case = MakePressureCase(); + const auto cached = FindCachedResults(reference_case); + if (cached.has_value()) { + const auto comparison = + fesa::test::CompareHdf5ToReferenceCsv(reference_case, *cached); + if (comparison.HasValue()) { + return comparison.Value(); + } + } + const auto copied = + fesa::test::CopyInputToExecutionDirectory(reference_case); + if (!copied.HasValue()) { + throw std::runtime_error{"Unable to copy pressure reference input."}; + } + const auto generated = fesa::test::RunFesaApplicationOnCopiedInput( + reference_case, copied.Value()); + if (!generated.HasValue()) { + throw std::runtime_error{"Unable to run pressure reference input."}; + } + const auto comparison = fesa::test::CompareHdf5ToReferenceCsv( + reference_case, generated.Value().generated_results_hdf5); + if (!comparison.HasValue()) { + throw std::runtime_error{"Unable to compare pressure reference output."}; + } + return comparison.Value(); + }(); + return report; +} + +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; + }); + if (found == report.families.end()) { + throw std::runtime_error{"Required comparison family is absent."}; + } + return *found; +} + +fesa::test::ReferenceToleranceFamilyDecision MakeTolerance(const bool passed) { + fesa::test::ReferenceToleranceFamilyDecision result{}; + result.passed = passed; + return result; +} + +} // namespace + +TEST(ShellSurfacePressureReference, + AuditsExactRequiredArtifactsAndIgnoresOptionalReviewArtifacts) { + const auto artifacts = fesa::test::ArtifactCheck(MakePressureCase()); + + ASSERT_TRUE(artifacts.HasValue()); + EXPECT_EQ(MakePressureCase().reaction_artifact_disposition, + fesa::test::NonlinearReferenceArtifactDisposition::kReviewOnly); + EXPECT_EQ(MakePressureCase().stress_artifact_disposition, + fesa::test::NonlinearReferenceArtifactDisposition::kExcluded); + ASSERT_EQ(artifacts.Value().size(), 2U); + EXPECT_EQ(artifacts.Value()[0U].byte_size, 56665U); + EXPECT_EQ(artifacts.Value()[0U].sha256, + "53718CCC9B41C23FCC1B509A51D66F1347E87ACA43A74F1D6CEC4D2085F4B90E"); + EXPECT_EQ(artifacts.Value()[1U].byte_size, 111182U); + EXPECT_EQ(artifacts.Value()[1U].sha256, + "2FE53F98EAC9F04779100721C3EDF8049EB1DBFE733EBCBD819AEEED03B81198"); +} + +TEST(ShellSurfacePressureReference, + SelectsUniqueSchemaOneFactorOneAcceptedFrame) { + const auto& report = RunPressureComparison(); + + EXPECT_TRUE(report.blocking_passed); + EXPECT_EQ(report.case_id, "shellnl-meshfine-pressure-s4-follower"); +} + +TEST(ShellSurfacePressureReference, + Projects729WideRowsTo2187LogicalRowsPerFamily) { + const auto& report = RunPressureComparison(); + + EXPECT_EQ(FindFamily(report, "pressure_nodal_translation_blocking").row_count, + 2187U); + EXPECT_EQ(FindFamily(report, "pressure_nodal_rotation_warning").row_count, + 2187U); +} + +TEST(ShellSurfacePressureReference, + MatchesByInstanceNodeLabelAndComponentNotRowOrder) { + const auto& report = RunPressureComparison(); + + const auto& translation = + FindFamily(report, "pressure_nodal_translation_blocking"); + EXPECT_EQ(translation.components, + (std::vector{"U1", "U2", "U3"})); + EXPECT_EQ(translation.tolerance.rows.size(), 2187U); +} + +TEST( + ShellSurfacePressureReference, + RejectsSchemaIdentityCardinalityMissingExtraDuplicateAndNonfiniteBeforeTolerance) { + const auto& report = RunPressureComparison(); + const std::filesystem::path invalid_hdf5 = + report.generated_results_hdf5.parent_path() / "invalid-pressure.h5"; + { + std::ofstream stream{invalid_hdf5, std::ios::binary | std::ios::trunc}; + stream << "not an HDF5 file"; + } + EXPECT_FALSE( + fesa::test::CompareHdf5ToReferenceCsv(MakePressureCase(), invalid_hdf5) + .HasValue()); + + fesa::test::NonlinearReferenceComparisonReport precheck_report{}; + precheck_report.blocking_passed = true; + precheck_report.precheck_findings = { + {"pressure_nodal_translation_blocking", + fesa::test::NonlinearReferenceDisposition::kBlocking, + "missing-extra-duplicate-or-nonfinite", + {}}}; + EXPECT_EQ(fesa::test::ClassifyReferenceComparison(precheck_report), + fesa::test::NonlinearReferenceClassification::kFailed); +} + +TEST(ShellSurfacePressureReference, + ClassifiesTranslationBlockingAndRotationWarningOnly) { + fesa::test::NonlinearReferenceComparisonReport report{}; + report.blocking_passed = true; + report.families = {{"pressure_nodal_translation_blocking", + {"U1", "U2", "U3"}, + fesa::test::NonlinearReferenceDisposition::kBlocking, + 2187U, + MakeTolerance(true)}, + {"pressure_nodal_rotation_warning", + {"UR1", "UR2", "UR3"}, + fesa::test::NonlinearReferenceDisposition::kWarningOnly, + 2187U, + MakeTolerance(false)}}; + + EXPECT_EQ( + fesa::test::ClassifyReferenceComparison(report), + fesa::test::NonlinearReferenceClassification::kWarningOnlyDifferences); +} + +TEST(ShellSurfacePressureReference, + ExcludesReactionAndStressFamiliesFromVerdict) { + const auto& report = RunPressureComparison(); + + EXPECT_EQ(report.families.size(), 2U); + EXPECT_EQ( + FindFamily(report, "pressure_nodal_translation_blocking").disposition, + fesa::test::NonlinearReferenceDisposition::kBlocking); + EXPECT_EQ(FindFamily(report, "pressure_nodal_rotation_warning").disposition, + fesa::test::NonlinearReferenceDisposition::kWarningOnly); +} + +TEST(ShellSurfacePressureReference, + UsesUnchangedCommonScaleNearZeroRelativeAndRmsPolicy) { + const auto& translation = FindFamily(RunPressureComparison(), + "pressure_nodal_translation_blocking"); + + EXPECT_EQ(translation.tolerance.near_zero_band, + fesa::test::ReferenceTolerancePolicy::kNearZeroRatio * + translation.tolerance.reference_scale); + EXPECT_LE(translation.tolerance.relative_rms, + fesa::test::ReferenceTolerancePolicy::kRelativeRmsTolerance); + EXPECT_TRUE(translation.tolerance.rms_passed); +} + +TEST(ShellSurfacePressureReference, WritesDeterministicComparisonReport) { + const auto& first = RunPressureComparison(); + const auto comparison = fesa::test::CompareHdf5ToReferenceCsv( + MakePressureCase(), first.generated_results_hdf5); + + ASSERT_TRUE(comparison.HasValue()); + ASSERT_EQ(first.families.size(), comparison.Value().families.size()); + for (std::size_t index = 0U; index < first.families.size(); ++index) { + const auto& expected = first.families[index]; + const auto& actual = comparison.Value().families[index]; + EXPECT_EQ(actual.family_identity, expected.family_identity); + EXPECT_EQ(actual.components, expected.components); + EXPECT_EQ(actual.disposition, expected.disposition); + EXPECT_EQ(actual.row_count, expected.row_count); + EXPECT_DOUBLE_EQ(actual.tolerance.reference_scale, + expected.tolerance.reference_scale); + EXPECT_DOUBLE_EQ(actual.tolerance.relative_rms, + expected.tolerance.relative_rms); + EXPECT_EQ(actual.tolerance.worst_row, expected.tolerance.worst_row); + } +}