feat(shell-surface-pressure-load): step 6 - compare-pressure-reference

This commit is contained in:
KOKO\Mimi
2026-09-01 17:00:55 +09:00
parent d099894e87
commit 6817942894
4 changed files with 382 additions and 8 deletions
+1
View File
@@ -102,6 +102,7 @@ add_executable(
reference/mitc4_reference_cases_test.cpp reference/mitc4_reference_cases_test.cpp
reference/nonlinear_static_reference_comparison.cpp reference/nonlinear_static_reference_comparison.cpp
reference/nonlinear_static_reference_cases_test.cpp reference/nonlinear_static_reference_cases_test.cpp
reference/shell_surface_pressure_reference_test.cpp
) )
target_link_libraries( target_link_libraries(
@@ -62,6 +62,7 @@ struct CaseContract {
const CaseContract& ContractFor(const NonlinearReferenceCase& reference_case) { const CaseContract& ContractFor(const NonlinearReferenceCase& reference_case) {
static const CaseContract kCoarse{49U, true, "coarse."}; static const CaseContract kCoarse{49U, true, "coarse."};
static const CaseContract kFine{729U, true, "fine."}; static const CaseContract kFine{729U, true, "fine."};
static const CaseContract kPressure{729U, true, "pressure."};
static const CaseContract kLinear{49U, false, "linear."}; static const CaseContract kLinear{49U, false, "linear."};
if (reference_case.case_id == "shellnl-s4-final") { if (reference_case.case_id == "shellnl-s4-final") {
return kCoarse; return kCoarse;
@@ -69,6 +70,9 @@ const CaseContract& ContractFor(const NonlinearReferenceCase& reference_case) {
if (reference_case.case_id == "shellnl-meshfine-s4-final") { if (reference_case.case_id == "shellnl-meshfine-s4-final") {
return kFine; return kFine;
} }
if (reference_case.case_id == "shellnl-meshfine-pressure-s4-follower") {
return kPressure;
}
if (reference_case.case_id == "shell-s4-linear-limit") { if (reference_case.case_id == "shell-s4-linear-limit") {
return kLinear; return kLinear;
} }
@@ -276,11 +280,49 @@ std::string Fnv1a64(const std::string& input) {
struct DeclaredArtifact { struct DeclaredArtifact {
std::filesystem::path path; std::filesystem::path path;
const char* sha256; const char* sha256;
std::uintmax_t expected_byte_size{0U};
NonlinearReferenceArtifactDisposition disposition{
NonlinearReferenceArtifactDisposition::kRequired};
}; };
std::vector<DeclaredArtifact> DeclaredArtifacts( std::vector<DeclaredArtifact> DeclaredArtifacts(
const NonlinearReferenceCase& reference_case) { const NonlinearReferenceCase& reference_case) {
const std::filesystem::path root{FESA_TEST_SOURCE_DIR}; 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<DeclaredArtifact> 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") { if (reference_case.case_id == "shellnl-meshfine-s4-final") {
const auto directory = root / "reference" / "shellNL-meshfine"; const auto directory = root / "reference" / "shellNL-meshfine";
const std::vector<DeclaredArtifact> artifacts{ const std::vector<DeclaredArtifact> artifacts{
@@ -364,7 +406,7 @@ ArtifactSnapshot Snapshot(const DeclaredArtifact& artifact) {
} }
const std::string bytes = ReadBytes(artifact.path); const std::string bytes = ReadBytes(artifact.path);
const std::string hash = Sha256(bytes); const std::string hash = Sha256(bytes);
if (hash != artifact.sha256) { if (artifact.sha256 == nullptr || hash != artifact.sha256) {
Fail("reference-artifact-hash-mismatch", Fail("reference-artifact-hash-mismatch",
"A declared reference artifact does not match its approved hash."); "A declared reference artifact does not match its approved hash.");
} }
@@ -373,6 +415,11 @@ ArtifactSnapshot Snapshot(const DeclaredArtifact& artifact) {
Fail("needs-reference-artifacts", Fail("needs-reference-artifacts",
"A declared reference artifact size cannot be read."); "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); const auto write_time = std::filesystem::last_write_time(artifact.path, error);
if (error) { if (error) {
Fail("needs-reference-artifacts", Fail("needs-reference-artifacts",
@@ -983,6 +1030,17 @@ void RequireExactNodeLabels(const std::vector<Hdf5Node>& nodes,
} }
} }
template <typename Row>
void RequirePressureInstance(const std::vector<Row>& 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, std::string Key(const std::string& instance_name,
const std::int64_t source_node_label, const std::int64_t source_node_label,
const std::string& component) { const std::string& component) {
@@ -1411,6 +1469,38 @@ void CompareNonlinearCase(NonlinearReferenceComparisonReport& report,
AppendInternalEquilibrium(&report, reference_case, hdf5); 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<std::size_t> translations{0U, 1U, 2U};
const std::vector<std::size_t> rotations{3U, 4U, 5U};
const std::vector<std::int64_t> 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 } // namespace
NonlinearInternalEquilibriumReport EvaluateGlobalBalance( NonlinearInternalEquilibriumReport EvaluateGlobalBalance(
@@ -1434,7 +1524,10 @@ Result<std::vector<ArtifactSnapshot>> ArtifactCheck(
std::vector<ArtifactSnapshot> snapshots; std::vector<ArtifactSnapshot> snapshots;
snapshots.reserve(artifacts.size()); snapshots.reserve(artifacts.size());
for (const auto& artifact : artifacts) { for (const auto& artifact : artifacts) {
snapshots.push_back(Snapshot(artifact)); if (artifact.disposition ==
NonlinearReferenceArtifactDisposition::kRequired) {
snapshots.push_back(Snapshot(artifact));
}
} }
return Result<std::vector<ArtifactSnapshot>>::Success(std::move(snapshots)); return Result<std::vector<ArtifactSnapshot>>::Success(std::move(snapshots));
} catch (const ComparisonFailure& exception) { } catch (const ComparisonFailure& exception) {
@@ -1542,8 +1635,11 @@ Result<NonlinearReferenceComparisonReport> CompareHdf5ToReferenceCsv(
if (!std::filesystem::is_regular_file(hdf5_results_path)) { if (!std::filesystem::is_regular_file(hdf5_results_path)) {
Fail("schema-mismatch", "The generated HDF5 result is missing."); Fail("schema-mismatch", "The generated HDF5 result is missing.");
} }
auto report = MakeReport(reference_case, hdf5_results_path, before.Value(), {}); auto report =
if (ContractFor(reference_case).nonlinear) { 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); CompareNonlinearCase(report, reference_case);
} else if (reference_case.case_id == "shell-s4-linear-limit") { } else if (reference_case.case_id == "shell-s4-linear-limit") {
CompareLinearCase(report, reference_case); CompareLinearCase(report, reference_case);
@@ -1552,12 +1648,14 @@ Result<NonlinearReferenceComparisonReport> CompareHdf5ToReferenceCsv(
} }
const auto after = ArtifactCheck(reference_case); const auto after = ArtifactCheck(reference_case);
if (!after.HasValue()) { if (!after.HasValue()) {
return Result<NonlinearReferenceComparisonReport>::Failure(after.GetStatus()); return Result<NonlinearReferenceComparisonReport>::Failure(
after.GetStatus());
} }
report.post_run_artifacts = after.Value(); report.post_run_artifacts = after.Value();
report.blocking_passed = report.external_blocking_passed && report.blocking_passed =
report.internal_blocking_passed; report.external_blocking_passed && report.internal_blocking_passed;
return Result<NonlinearReferenceComparisonReport>::Success(std::move(report)); return Result<NonlinearReferenceComparisonReport>::Success(
std::move(report));
} catch (const ComparisonFailure& exception) { } catch (const ComparisonFailure& exception) {
return Result<NonlinearReferenceComparisonReport>::Failure( return Result<NonlinearReferenceComparisonReport>::Failure(
FailureStatus(reference_case, exception.Code(), exception.what())); FailureStatus(reference_case, exception.Code(), exception.what()));
@@ -14,6 +14,12 @@
namespace fesa::test { namespace fesa::test {
enum class NonlinearReferenceArtifactDisposition {
kRequired,
kReviewOnly,
kExcluded,
};
struct NonlinearReferenceCase { struct NonlinearReferenceCase {
std::string case_id; std::string case_id;
std::filesystem::path original_input_path; std::filesystem::path original_input_path;
@@ -21,6 +27,14 @@ struct NonlinearReferenceCase {
std::filesystem::path reaction_csv_path; std::filesystem::path reaction_csv_path;
std::filesystem::path stress_inventory_path; std::filesystem::path stress_inventory_path;
std::filesystem::path execution_directory; 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 { struct ArtifactSnapshot {
@@ -0,0 +1,261 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <optional>
#include <stdexcept>
#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
#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<std::filesystem::path> 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<std::filesystem::path> 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<std::filesystem::path>{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<std::string>{"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);
}
}