Add MITC4 drilling diagnostics output and comparison contract
This commit is contained in:
@@ -162,6 +162,8 @@ add_executable(
|
||||
reference/thermal_reference_cases_test.cpp
|
||||
)
|
||||
|
||||
target_include_directories(fesa_reference_tests PRIVATE ${PROJECT_SOURCE_DIR}/src/fesa)
|
||||
|
||||
target_link_libraries(
|
||||
fesa_reference_tests
|
||||
PRIVATE
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "io/hdf5/mitc4_drilling_diagnostics.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kStepRoot = "/steps/Step-1/frames/0";
|
||||
@@ -525,4 +527,40 @@ TEST(Mitc4ShellCli, WritesShellHdf5ThroughTheExistingApplicationRoute) {
|
||||
const auto result = RunApplication({input.string()});
|
||||
ASSERT_EQ(result.exit_code, 0) << result.standard_error;
|
||||
ExpectShellHdf5Identity(output, input);
|
||||
const auto file = OpenFile(output);
|
||||
const auto drilling = fesa::hdf5_internal::ReadDrillingDiagnostics(file.Get());
|
||||
ASSERT_TRUE(drilling);
|
||||
EXPECT_EQ(drilling->formulation, "direct-initial-director-spring");
|
||||
EXPECT_EQ(drilling->version, 1U);
|
||||
ASSERT_EQ(drilling->frames.size(), 1U);
|
||||
EXPECT_EQ(drilling->frames.front().frame_index, 0U);
|
||||
}
|
||||
|
||||
|
||||
TEST(Mitc4ShellCli, DrillingActualHhtPipelineWritesInitialAndAcceptedDiagnostics) {
|
||||
TempDirectory directory{"shell-drilling-hht"};
|
||||
const auto input = directory.Path() / "shell.inp";
|
||||
const auto output = directory.Path() / "shell_results.h5";
|
||||
std::string deck = AllConstrainedShellDeck();
|
||||
const auto material = deck.find("*Material, name=Steel\n");
|
||||
ASSERT_NE(material, std::string::npos);
|
||||
deck.insert(material + std::string{"*Material, name=Steel\n"}.size(), "*Density\n2.\n");
|
||||
const auto step = deck.find("*Step,");
|
||||
ASSERT_NE(step, std::string::npos);
|
||||
deck.replace(step, std::string::npos,
|
||||
"*Step, name=Load, nlgeom=YES\n*Dynamic, Direct\n0.25, 0.5\n*Cload\nAll, 3, 1.\n*End Step\n");
|
||||
WriteText(input, deck);
|
||||
const auto result = RunApplication({input.string()});
|
||||
ASSERT_EQ(result.exit_code, 0) << result.standard_error;
|
||||
const auto file = OpenFile(output);
|
||||
const auto drilling = fesa::hdf5_internal::ReadDrillingDiagnostics(file.Get());
|
||||
ASSERT_TRUE(drilling);
|
||||
EXPECT_EQ(drilling->formulation, "direct-initial-director-spring");
|
||||
ASSERT_EQ(drilling->frames.size(), 3U);
|
||||
for (std::size_t i = 0; i < 3U; ++i) {
|
||||
EXPECT_EQ(drilling->frames[i].frame_index, i);
|
||||
EXPECT_EQ(drilling->frames[i].raw_equilibrium, (std::array<double, 6>{}));
|
||||
EXPECT_EQ(drilling->frames[i].drilling_moment, (std::array<double, 3>{}));
|
||||
EXPECT_EQ(drilling->frames[i].equilibrium_with_drilling, (std::array<double, 6>{}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
#include "fesa/io/abaqus/domain_mapper.h"
|
||||
#include "fesa/io/abaqus/input_reader.h"
|
||||
|
||||
#include "mitc4_drilling_reader.h"
|
||||
|
||||
namespace fesa::test {
|
||||
namespace {
|
||||
|
||||
@@ -385,6 +387,9 @@ Result<EigenReferenceComparisonReport> EigenReferenceComparison::Compare(
|
||||
}
|
||||
|
||||
EigenReferenceComparisonReport report{};
|
||||
report.drilling_diagnostics = ReadMitc4DrillingReport(file.Get());
|
||||
if (report.drilling_diagnostics && !report.drilling_diagnostics->frames.empty())
|
||||
Fail("schema-mismatch", "Eigen drilling diagnostics must have no equilibrium frames.");
|
||||
report.case_id = reference_case.case_id;
|
||||
report.source_step_name = frequency.source_step_name_raw;
|
||||
report.requested_mode_count = requested;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/core/status.h"
|
||||
#include "mitc4_drilling_report.h"
|
||||
#include "reference_tolerance_policy.h"
|
||||
|
||||
namespace fesa::test {
|
||||
@@ -36,6 +37,7 @@ struct EigenReferenceComparisonReport {
|
||||
bool passed;
|
||||
std::string policy_id;
|
||||
double relative_rms_tolerance;
|
||||
std::optional<Mitc4DrillingDiagnosticsReport> drilling_diagnostics;
|
||||
};
|
||||
|
||||
/// @brief Compares modal eigenvalues without changing the approved input or
|
||||
|
||||
@@ -123,6 +123,25 @@ TEST(EigenReferenceComparison,
|
||||
EXPECT_EQ(ok.Value().policy_id, "mitc4-eigen-first-implementation");
|
||||
EXPECT_DOUBLE_EQ(ok.Value().relative_rms_tolerance, 0.05);
|
||||
EXPECT_EQ(ok.Value().families.size(), 2U);
|
||||
EXPECT_FALSE(ok.Value().drilling_diagnostics);
|
||||
for (const std::string mode : {"direct-initial-director-spring", "legacy-corotated-twist"}) {
|
||||
f = H5Fopen(h5.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT);
|
||||
m = H5Gopen2(f, "/metadata", H5P_DEFAULT);
|
||||
if (H5Aexists(m, "mitc4_drilling_formulation") > 0) H5Adelete(m, "mitc4_drilling_formulation");
|
||||
if (H5Aexists(m, "drilling_diagnostics_version") > 0) H5Adelete(m, "drilling_diagnostics_version");
|
||||
Str(m, "mitc4_drilling_formulation", mode);
|
||||
U(m, "drilling_diagnostics_version", 1U);
|
||||
H5Gclose(m);
|
||||
H5Fclose(f);
|
||||
const auto compared = fesa::test::EigenReferenceComparison::Compare(reference_case);
|
||||
ASSERT_TRUE(compared.HasValue());
|
||||
ASSERT_TRUE(compared.Value().drilling_diagnostics);
|
||||
EXPECT_EQ(compared.Value().drilling_diagnostics->formulation, mode);
|
||||
EXPECT_EQ(compared.Value().drilling_diagnostics->version, 1U);
|
||||
EXPECT_TRUE(compared.Value().drilling_diagnostics->frames.empty());
|
||||
EXPECT_EQ(compared.Value().families.size(), 2U);
|
||||
}
|
||||
|
||||
// Only synthetic FESA output is changed; each TXT mode remains an independent
|
||||
// scalar family, including at both inclusive five-percent boundaries.
|
||||
for (const auto& sample :
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
#ifndef FESA_TESTS_REFERENCE_MITC4_DRILLING_READER_H_
|
||||
#define FESA_TESTS_REFERENCE_MITC4_DRILLING_READER_H_
|
||||
|
||||
#include "mitc4_drilling_report.h"
|
||||
#include "io/hdf5/mitc4_drilling_diagnostics.h"
|
||||
|
||||
namespace fesa::test {
|
||||
|
||||
inline std::optional<Mitc4DrillingDiagnosticsReport> ReadMitc4DrillingReport(hid_t file) {
|
||||
const auto stored = hdf5_internal::ReadDrillingDiagnostics(file);
|
||||
if (!stored) return std::nullopt;
|
||||
Mitc4DrillingDiagnosticsReport result{stored->formulation, stored->version, {}};
|
||||
for (const auto& frame : stored->frames)
|
||||
result.frames.push_back({frame.storage_step, frame.frame_index, frame.raw_equilibrium,
|
||||
frame.drilling_moment, frame.equilibrium_with_drilling});
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace fesa::test
|
||||
|
||||
#endif // FESA_TESTS_REFERENCE_MITC4_DRILLING_READER_H_
|
||||
@@ -0,0 +1,58 @@
|
||||
#ifndef FESA_TESTS_REFERENCE_MITC4_DRILLING_REPORT_H_
|
||||
#define FESA_TESTS_REFERENCE_MITC4_DRILLING_REPORT_H_
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace fesa::test {
|
||||
|
||||
struct Mitc4DrillingFrameReport {
|
||||
std::string storage_step;
|
||||
std::uint64_t frame_index;
|
||||
std::array<double, 6> raw_equilibrium;
|
||||
std::array<double, 3> drilling_moment;
|
||||
std::array<double, 6> equilibrium_with_drilling;
|
||||
};
|
||||
|
||||
struct Mitc4DrillingDiagnosticsReport {
|
||||
std::string formulation;
|
||||
std::uint64_t version;
|
||||
std::vector<Mitc4DrillingFrameReport> frames;
|
||||
};
|
||||
|
||||
inline void WriteDrillingDiagnosticsJson(
|
||||
std::ostream& stream, const std::optional<Mitc4DrillingDiagnosticsReport>& report) {
|
||||
if (!report) { stream << "null"; return; }
|
||||
stream << "{\"formulation\":\"" << report->formulation
|
||||
<< "\",\"version\":" << report->version << ",\"frames\":[";
|
||||
const auto numbers = [&](const auto& values) {
|
||||
stream << '[';
|
||||
for (std::size_t i = 0; i < values.size(); ++i) {
|
||||
if (i != 0U) stream << ',';
|
||||
stream << values[i];
|
||||
}
|
||||
stream << ']';
|
||||
};
|
||||
for (std::size_t i = 0; i < report->frames.size(); ++i) {
|
||||
if (i != 0U) stream << ',';
|
||||
const auto& frame = report->frames[i];
|
||||
stream << "{\"storage_step\":\"Step-1\",\"frame_index\":" << frame.frame_index
|
||||
<< ",\"raw_equilibrium\":";
|
||||
numbers(frame.raw_equilibrium);
|
||||
stream << ",\"drilling_moment\":";
|
||||
numbers(frame.drilling_moment);
|
||||
stream << ",\"equilibrium_with_drilling\":";
|
||||
numbers(frame.equilibrium_with_drilling);
|
||||
stream << '}';
|
||||
}
|
||||
stream << "]}";
|
||||
}
|
||||
|
||||
} // namespace fesa::test
|
||||
|
||||
#endif // FESA_TESTS_REFERENCE_MITC4_DRILLING_REPORT_H_
|
||||
@@ -26,6 +26,8 @@
|
||||
|
||||
#include "reference_tolerance_policy.h"
|
||||
|
||||
#include "mitc4_drilling_reader.h"
|
||||
|
||||
namespace fesa::test {
|
||||
namespace {
|
||||
|
||||
@@ -573,6 +575,7 @@ std::vector<double> ReadDisplacement(const hid_t file,
|
||||
}
|
||||
|
||||
struct Hdf5Projection {
|
||||
std::optional<Mitc4DrillingDiagnosticsReport> drilling_diagnostics;
|
||||
std::vector<WideRow> rows;
|
||||
std::string source_element_type;
|
||||
std::string internal_formulation;
|
||||
@@ -619,7 +622,7 @@ Hdf5Projection ReadHdf5(const Mitc4ReferenceCase& reference_case) {
|
||||
values.begin() + static_cast<std::ptrdiff_t>(row * kComponents.size()),
|
||||
kComponents.size(), rows[row].values.begin());
|
||||
}
|
||||
return {std::move(rows), reference_case.expected_source_element_type,
|
||||
return {ReadMitc4DrillingReport(file.Get()), std::move(rows), reference_case.expected_source_element_type,
|
||||
kInternalFormulation, kIntegrationRule};
|
||||
}
|
||||
|
||||
@@ -739,6 +742,7 @@ Result<Mitc4ComparisonReport> Mitc4ReferenceComparison::Compare(
|
||||
}
|
||||
|
||||
Mitc4ComparisonReport report{};
|
||||
report.drilling_diagnostics = std::move(hdf5.drilling_diagnostics);
|
||||
report.case_id = reference_case.case_id;
|
||||
report.source_element_type = std::move(hdf5.source_element_type);
|
||||
report.internal_formulation = std::move(hdf5.internal_formulation);
|
||||
@@ -889,7 +893,9 @@ Status Mitc4ReferenceComparison::WriteDeterministicJson(
|
||||
}
|
||||
stream.imbue(std::locale::classic());
|
||||
stream << std::setprecision(std::numeric_limits<double>::max_digits10);
|
||||
stream << "{\"case_id\":\"" << JsonEscape(report.case_id)
|
||||
stream << "{\"drilling_diagnostics\":";
|
||||
WriteDrillingDiagnosticsJson(stream, report.drilling_diagnostics);
|
||||
stream << ",\"case_id\":\"" << JsonEscape(report.case_id)
|
||||
<< "\",\"source_element_type\":\""
|
||||
<< JsonEscape(report.source_element_type)
|
||||
<< "\",\"internal_formulation\":\""
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/core/status.h"
|
||||
#include "mitc4_drilling_report.h"
|
||||
|
||||
namespace fesa::test {
|
||||
|
||||
@@ -77,6 +78,7 @@ struct Mitc4ComparisonReport {
|
||||
std::vector<Mitc4Warning> warnings;
|
||||
std::size_t worst_row;
|
||||
bool passed;
|
||||
std::optional<Mitc4DrillingDiagnosticsReport> drilling_diagnostics;
|
||||
};
|
||||
|
||||
class Mitc4ReferenceComparison {
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#error FESA_TEST_BINARY_DIR must identify the CMake binary root.
|
||||
#endif
|
||||
|
||||
#include "io/hdf5/mitc4_drilling_diagnostics.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kInstanceName = "PART-1-1";
|
||||
@@ -771,3 +773,56 @@ TEST(Mitc4ReferenceComparison, RequiresOnlyDeclaredInputCsvAndHdf5) {
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
TEST(Mitc4ReferenceComparison, DrillingOldMarkerlessReportIsNull) {
|
||||
ContractFixture fixture{"drilling-old"};
|
||||
const auto compared = fesa::test::Mitc4ReferenceComparison::Compare(fixture.ReferenceCase());
|
||||
ASSERT_TRUE(compared.HasValue());
|
||||
EXPECT_FALSE(compared.Value().drilling_diagnostics);
|
||||
const auto json = fixture.Root() / "drilling-old.json";
|
||||
ASSERT_TRUE(fesa::test::Mitc4ReferenceComparison::WriteDeterministicJson(compared.Value(), json).IsOk());
|
||||
EXPECT_NE(ReadBytes(json).find("\"drilling_diagnostics\":null"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(Mitc4ReferenceComparison, DrillingNewMarkersPreserveAllFramesWithoutNewVerdict) {
|
||||
for (const auto mode : {fesa::Mitc4DrillingMode::kDirectSpring,
|
||||
fesa::Mitc4DrillingMode::kLegacyCorotatedTwist}) {
|
||||
ContractFixture fixture{"drilling-new"};
|
||||
{
|
||||
Hdf5Handle file{H5Fopen(fixture.Results().string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose};
|
||||
fesa::hdf5_internal::WriteDrillingMetadata(file.Get(), mode);
|
||||
// Nonzero raw balance is diagnostic only for the linear displacement comparator.
|
||||
for (const int index : {0, 2, 10}) {
|
||||
const std::string root = "/steps/Step-1/frames/" + std::to_string(index);
|
||||
(void)fesa::hdf5_internal::CreateGroup(file.Get(), (root + "/global").c_str());
|
||||
const std::array<double, 6> raw{1., 2., 3., 4., 5., 6.};
|
||||
const fesa::Mitc4DrillingEvidence evidence{mode, {4., 5., 6.}, {1., 2., 3., 0., 0., 0.}};
|
||||
fesa::hdf5_internal::WriteModelDoubleDataset(file.Get(), root + "/global/equilibrium", {6U},
|
||||
raw.data(), 6U, "", "", "", "");
|
||||
fesa::hdf5_internal::WriteDrillingFrame(file.Get(), root, evidence);
|
||||
}
|
||||
}
|
||||
const auto compared = fesa::test::Mitc4ReferenceComparison::Compare(fixture.ReferenceCase());
|
||||
ASSERT_TRUE(compared.HasValue());
|
||||
EXPECT_TRUE(compared.Value().passed);
|
||||
EXPECT_EQ(compared.Value().metrics.size(), 2U);
|
||||
const auto& diagnostics = compared.Value().drilling_diagnostics;
|
||||
ASSERT_TRUE(diagnostics);
|
||||
EXPECT_EQ(diagnostics->formulation, fesa::Mitc4DrillingModeName(mode));
|
||||
ASSERT_EQ(diagnostics->frames.size(), 3U);
|
||||
EXPECT_EQ(diagnostics->frames[0].frame_index, 0U);
|
||||
EXPECT_EQ(diagnostics->frames[1].frame_index, 2U);
|
||||
EXPECT_EQ(diagnostics->frames[2].frame_index, 10U);
|
||||
const auto json = fixture.Root() / "drilling-new.json";
|
||||
ASSERT_TRUE(fesa::test::Mitc4ReferenceComparison::WriteDeterministicJson(compared.Value(), json).IsOk());
|
||||
const auto bytes = ReadBytes(json);
|
||||
EXPECT_NE(bytes.find("\"drilling_diagnostics\":{\"formulation\":\"" +
|
||||
diagnostics->formulation + "\",\"version\":1,\"frames\":["), std::string::npos);
|
||||
{
|
||||
Hdf5Handle file{H5Fopen(fixture.Results().string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose};
|
||||
ASSERT_GE(H5Ldelete(file.Get(), "/steps/Step-1/frames/2/global/drilling_moment", H5P_DEFAULT), 0);
|
||||
}
|
||||
EXPECT_FALSE(fesa::test::Mitc4ReferenceComparison::Compare(fixture.ReferenceCase()).HasValue());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#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) {
|
||||
@@ -409,3 +411,75 @@ TEST(NonlinearStaticReference,
|
||||
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<std::pair<std::string, std::vector<double>>>{
|
||||
{"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<std::pair<std::string, std::vector<double>>>{
|
||||
{"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());
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
#error FESA_TEST_BINARY_DIR must identify the CMake binary root.
|
||||
#endif
|
||||
|
||||
#include "mitc4_drilling_reader.h"
|
||||
|
||||
namespace fesa::test {
|
||||
namespace {
|
||||
|
||||
@@ -896,6 +898,8 @@ std::vector<std::uint8_t> ReadConstraintMask(const hid_t file,
|
||||
}
|
||||
|
||||
struct Hdf5Projection {
|
||||
std::uint64_t selected_frame_index;
|
||||
std::optional<Mitc4DrillingDiagnosticsReport> drilling_diagnostics;
|
||||
std::filesystem::path copied_input_path;
|
||||
std::vector<Hdf5Node> nodes;
|
||||
std::vector<double> displacement;
|
||||
@@ -972,7 +976,7 @@ Hdf5Projection ReadNonlinearHdf5(
|
||||
const std::uint64_t frame_index = ReadFactorOneFrame(file.Get());
|
||||
const std::string root = "/steps/Step-1/frames/" +
|
||||
std::to_string(frame_index) + "/nodal/";
|
||||
return {copied_input,
|
||||
return {frame_index, ReadMitc4DrillingReport(file.Get()), copied_input,
|
||||
nodes,
|
||||
ReadNodalDataset(file.Get(), root + "displacement",
|
||||
"UX,UY,UZ,URX,URY,URZ", contract.node_count),
|
||||
@@ -1320,6 +1324,16 @@ void AppendInternalEquilibrium(NonlinearReferenceComparisonReport* report,
|
||||
applied_moment_scale += applied_resultant.Norm();
|
||||
reaction_moment_scale += constrained_resultant.Norm();
|
||||
}
|
||||
const std::array<double, 6>* moment_balance = &projection.global_equilibrium;
|
||||
if (projection.drilling_diagnostics &&
|
||||
projection.drilling_diagnostics->formulation == "direct-initial-director-spring") {
|
||||
const auto& frames = projection.drilling_diagnostics->frames;
|
||||
const auto selected = std::find_if(frames.begin(), frames.end(), [&](const auto& frame) {
|
||||
return frame.frame_index == projection.selected_frame_index;
|
||||
});
|
||||
if (selected == frames.end()) Fail("schema-mismatch", "Missing factor-one drilling diagnostics.");
|
||||
moment_balance = &selected->equilibrium_with_drilling;
|
||||
}
|
||||
report->constrained_reaction_resultant = {
|
||||
reaction_force.Components(), reaction_moment.Components()};
|
||||
const std::string prefix = ContractFor(reference_case).family_prefix;
|
||||
@@ -1329,9 +1343,9 @@ void AppendInternalEquilibrium(NonlinearReferenceComparisonReport* report,
|
||||
projection.global_equilibrium[2U]},
|
||||
applied_force_scale, reaction_force_scale);
|
||||
AppendGlobalBalance(report, prefix + "global_moment_balance",
|
||||
{projection.global_equilibrium[3U],
|
||||
projection.global_equilibrium[4U],
|
||||
projection.global_equilibrium[5U]},
|
||||
{(*moment_balance)[3U],
|
||||
(*moment_balance)[4U],
|
||||
(*moment_balance)[5U]},
|
||||
applied_moment_scale, reaction_moment_scale);
|
||||
AppendFreeResidual(report, prefix + "accepted_free_force_residual",
|
||||
projection, false);
|
||||
@@ -1385,6 +1399,7 @@ void CompareLinearCase(NonlinearReferenceComparisonReport& report,
|
||||
: diagnostics.front().message;
|
||||
Fail("schema-mismatch", message);
|
||||
}
|
||||
report.drilling_diagnostics = legacy.Value().drilling_diagnostics;
|
||||
for (const auto& metric : legacy.Value().metrics) {
|
||||
ReferenceToleranceFamilyDecision tolerance{};
|
||||
tolerance.identity = metric.blocking ? "linear.translation" : "linear.rotation";
|
||||
@@ -1418,6 +1433,7 @@ void CompareNonlinearCase(NonlinearReferenceComparisonReport& report,
|
||||
report.generated_results_hdf5);
|
||||
report.copied_input_path = hdf5.copied_input_path;
|
||||
report.generated_log_path = DerivedLogPath(hdf5.copied_input_path);
|
||||
report.drilling_diagnostics = hdf5.drilling_diagnostics;
|
||||
RequireExactNodeLabels(hdf5.nodes, contract.node_count);
|
||||
const auto displacement_rows = ReadCsvWideRows(
|
||||
reference_case.displacement_csv_path, true, kDisplacementComponents,
|
||||
@@ -1475,6 +1491,7 @@ void ComparePressureCase(NonlinearReferenceComparisonReport& report,
|
||||
ReadNonlinearHdf5(reference_case, report.generated_results_hdf5);
|
||||
report.copied_input_path = hdf5.copied_input_path;
|
||||
report.generated_log_path = DerivedLogPath(hdf5.copied_input_path);
|
||||
report.drilling_diagnostics = hdf5.drilling_diagnostics;
|
||||
RequireExactNodeLabels(hdf5.nodes, contract.node_count);
|
||||
RequirePressureInstance(hdf5.nodes);
|
||||
const auto displacement_rows =
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/core/status.h"
|
||||
#include "mitc4_drilling_report.h"
|
||||
#include "reference_tolerance_policy.h"
|
||||
|
||||
namespace fesa::test {
|
||||
@@ -114,6 +115,7 @@ struct NonlinearReferenceComparisonReport {
|
||||
ConstrainedReactionResultantReport constrained_reaction_resultant;
|
||||
bool external_blocking_passed;
|
||||
bool internal_blocking_passed;
|
||||
std::optional<Mitc4DrillingDiagnosticsReport> drilling_diagnostics;
|
||||
};
|
||||
|
||||
enum class NonlinearReferenceClassification {
|
||||
|
||||
@@ -349,3 +349,88 @@ def test_cli_writes_json_and_exit_two_for_missing_results(tmp_path, artifacts):
|
||||
evidence = json.loads(report.read_text())
|
||||
assert not evidence["precheck"]["passed"]
|
||||
assert evidence["families"] == []
|
||||
|
||||
|
||||
DRILLING_MODES = ("direct-initial-director-spring", "legacy-corotated-twist")
|
||||
|
||||
|
||||
def drilling_result(tmp_path, artifacts, mode=DRILLING_MODES[0]):
|
||||
path = changed_result(tmp_path, artifacts)
|
||||
with h5py.File(path, "r+") as file:
|
||||
file["metadata"].attrs["mitc4_drilling_formulation"] = mode
|
||||
file["metadata"].attrs["drilling_diagnostics_version"] = np.uint64(1)
|
||||
for frame in range(101):
|
||||
root = file.require_group(f"steps/Step-1/frames/{frame}/global")
|
||||
raw = np.array([1., 2., 3., 4., 5., 6.], dtype="<f8")
|
||||
moment = np.array([4., 5., 6.], dtype="<f8")
|
||||
corrected = raw.copy()
|
||||
corrected[3:] -= moment
|
||||
root.create_dataset("equilibrium", data=raw)
|
||||
root.create_dataset("drilling_moment", data=moment)
|
||||
root.create_dataset("equilibrium_with_drilling", data=corrected)
|
||||
return path
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mode", DRILLING_MODES)
|
||||
def test_drilling_diagnostics_are_precheck_and_report_all_frames(tmp_path, artifacts, mode):
|
||||
result = comparator.compare(artifacts[0], artifacts[1], drilling_result(tmp_path, artifacts, mode))
|
||||
assert result["precheck"]["passed"], result["precheck"]
|
||||
assert result["verdict"] == "passed"
|
||||
assert len(result["families"]) == 202
|
||||
diagnostics = result["drilling_diagnostics"]
|
||||
assert diagnostics["formulation"] == mode
|
||||
assert diagnostics["version"] == 1
|
||||
assert [frame["frame_index"] for frame in diagnostics["frames"]] == list(range(101))
|
||||
assert diagnostics["frames"][0] == {
|
||||
"storage_step": "Step-1", "frame_index": 0,
|
||||
"raw_equilibrium": [1., 2., 3., 4., 5., 6.],
|
||||
"drilling_moment": [4., 5., 6.],
|
||||
"equilibrium_with_drilling": [1., 2., 3., 0., 0., 0.]}
|
||||
|
||||
|
||||
def test_markerless_drilling_report_is_null(artifacts):
|
||||
assert comparator.compare(*artifacts)["drilling_diagnostics"] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mutation", ["mode_missing", "version_missing", "unknown_mode", "unknown_version",
|
||||
"version_type", "mode_type", "initial_missing", "intermediate_missing", "both_missing",
|
||||
"wrong_shape", "wrong_type", "big_endian", "nan", "inf", "wrong_algebra", "wrong_force"])
|
||||
def test_drilling_diagnostics_rejected_before_displacement_tolerance(tmp_path, artifacts, mutation):
|
||||
path = drilling_result(tmp_path, artifacts)
|
||||
with h5py.File(path, "r+") as file:
|
||||
metadata = file["metadata"]
|
||||
root = file["steps/Step-1/frames/50/global"]
|
||||
if mutation == "mode_missing":
|
||||
del metadata.attrs["mitc4_drilling_formulation"]
|
||||
elif mutation == "version_missing":
|
||||
del metadata.attrs["drilling_diagnostics_version"]
|
||||
elif mutation == "unknown_mode":
|
||||
metadata.attrs["mitc4_drilling_formulation"] = "unknown"
|
||||
elif mutation == "unknown_version":
|
||||
metadata.attrs["drilling_diagnostics_version"] = np.uint64(2)
|
||||
elif mutation == "version_type":
|
||||
del metadata.attrs["drilling_diagnostics_version"]
|
||||
metadata.attrs["drilling_diagnostics_version"] = np.uint32(1)
|
||||
elif mutation == "mode_type":
|
||||
del metadata.attrs["mitc4_drilling_formulation"]
|
||||
metadata.attrs["mitc4_drilling_formulation"] = np.bytes_(DRILLING_MODES[0])
|
||||
elif mutation == "initial_missing":
|
||||
del file["steps/Step-1/frames/0/global/drilling_moment"]
|
||||
elif mutation == "intermediate_missing":
|
||||
del root["equilibrium_with_drilling"]
|
||||
elif mutation == "both_missing":
|
||||
del root["drilling_moment"]
|
||||
del root["equilibrium_with_drilling"]
|
||||
elif mutation in {"wrong_shape", "wrong_type", "big_endian"}:
|
||||
del root["drilling_moment"]
|
||||
root.create_dataset("drilling_moment", data=np.zeros(2 if mutation == "wrong_shape" else 3,
|
||||
dtype=">f8" if mutation == "big_endian" else ("<f4" if mutation == "wrong_type" else "<f8")))
|
||||
elif mutation == "nan":
|
||||
root["drilling_moment"][0] = np.nan
|
||||
elif mutation == "inf":
|
||||
root["equilibrium"][0] = np.inf
|
||||
else:
|
||||
root["equilibrium_with_drilling"][0 if mutation == "wrong_force" else 3] = 1e-100
|
||||
result = comparator.compare(artifacts[0], artifacts[1], path)
|
||||
assert not result["precheck"]["passed"]
|
||||
assert result["families"] == []
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
#include "fesa/fem/dof_manager.h"
|
||||
#include "io/hdf5/hdf5_raii.h"
|
||||
|
||||
#include "io/hdf5/mitc4_drilling_diagnostics.h"
|
||||
|
||||
#include "mitc4_fault_test_helpers.h"
|
||||
|
||||
namespace {
|
||||
|
||||
using fesa::hdf5_internal::Hdf5Handle;
|
||||
@@ -145,6 +149,8 @@ fesa::DynamicAcceptedFrameState MakeFrame(const fesa::Domain& domain,
|
||||
{fesa::ShellSectionPosition::kMiddle, 0.0, {}},
|
||||
{fesa::ShellSectionPosition::kTop, 1.0, {}}}}});
|
||||
}
|
||||
frame.mitc4_drilling = fesa::Mitc4DrillingEvidence{
|
||||
fesa::Mitc4DrillingMode::kDirectSpring, {}, frame.global_equilibrium};
|
||||
return frame;
|
||||
}
|
||||
|
||||
@@ -670,3 +676,58 @@ TEST(DynamicHdf5ResultsWriter, FailedAtomicReplacementPreservesPreviousBytes) {
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
TEST(DynamicHdf5ResultsWriter, DrillingModesIncludeInitialAndEveryAcceptedFrame) {
|
||||
for (const auto mode : {fesa::Mitc4DrillingMode::kDirectSpring,
|
||||
fesa::Mitc4DrillingMode::kLegacyCorotatedTwist}) {
|
||||
auto fixture = MakeFixture();
|
||||
for (std::uint32_t i = 0; i < 3U; ++i) {
|
||||
auto frame = MakeFrame(*fixture.domain, i);
|
||||
frame.mitc4_drilling->mode = mode;
|
||||
ASSERT_TRUE(fixture.state.CommitAcceptedFrame(frame).IsOk());
|
||||
}
|
||||
TempFile path{"drilling-roundtrip"};
|
||||
ASSERT_TRUE(fesa::DynamicHdf5ResultsWriter{}.Write(path.Path(), *fixture.domain, fixture.state, {}).IsOk());
|
||||
Hdf5Handle file{H5Fopen(path.Path().string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT), H5Fclose};
|
||||
const auto stored = fesa::hdf5_internal::ReadDrillingDiagnostics(file.Get());
|
||||
ASSERT_TRUE(stored);
|
||||
EXPECT_EQ(stored->formulation, fesa::Mitc4DrillingModeName(mode));
|
||||
ASSERT_EQ(stored->frames.size(), 3U);
|
||||
for (std::size_t i = 0; i < 3U; ++i) {
|
||||
EXPECT_EQ(stored->frames[i].frame_index, i);
|
||||
EXPECT_EQ(stored->frames[i].raw_equilibrium, fixture.state.AcceptedFrames()[i].global_equilibrium);
|
||||
EXPECT_EQ(stored->frames[i].equilibrium_with_drilling,
|
||||
fixture.state.AcceptedFrames()[i].mitc4_drilling->equilibrium_with_drilling);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(DynamicHdf5ResultsWriter, DrillingMissingActualStatePreservesPriorFinal) {
|
||||
auto fixture = MakeFixture();
|
||||
for (std::uint32_t i = 0; i < 3U; ++i) {
|
||||
auto frame = MakeFrame(*fixture.domain, i);
|
||||
frame.mitc4_drilling.reset();
|
||||
ASSERT_TRUE(fixture.state.CommitAcceptedFrame(frame).IsOk());
|
||||
}
|
||||
TempFile path{"drilling-missing"};
|
||||
std::ofstream(path.Path(), std::ios::binary) << "prior";
|
||||
EXPECT_FALSE(fesa::DynamicHdf5ResultsWriter{}.Write(path.Path(), *fixture.domain, fixture.state, {}).IsOk());
|
||||
std::ifstream stream{path.Path(), std::ios::binary};
|
||||
EXPECT_EQ((std::string{std::istreambuf_iterator<char>{stream}, {}}), "prior");
|
||||
}
|
||||
|
||||
|
||||
TEST(DynamicHdf5ResultsWriter, DrillingLifecycleFaultsPreserveFinalBytes) {
|
||||
auto fixture = MakeFixture();
|
||||
CommitHistory(fixture);
|
||||
TempFile path{"drilling-faults"};
|
||||
for (const std::string fault : {"candidate-close", "reader-close", "markers", "datasets"}) {
|
||||
SCOPED_TRACE(fault);
|
||||
std::ofstream(path.Path(), std::ios::binary | std::ios::trunc) << "prior";
|
||||
EXPECT_FALSE(fesa::hdf5_internal::WriteDynamicFile(path.Path(), *fixture.domain, fixture.state, {},
|
||||
fesa::test::Mitc4LifecycleFault(fault)).IsOk());
|
||||
std::ifstream stream{path.Path(), std::ios::binary};
|
||||
EXPECT_EQ((std::string{std::istreambuf_iterator<char>{stream}, {}}), "prior");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,10 @@
|
||||
#include "fesa/model/domain.h"
|
||||
#include "io/hdf5/eigen_hdf5_self_check.h"
|
||||
|
||||
#include "io/hdf5/mitc4_drilling_diagnostics.h"
|
||||
|
||||
#include "mitc4_fault_test_helpers.h"
|
||||
|
||||
namespace {
|
||||
|
||||
fesa::ModelDefinition Definition(const std::filesystem::path& source) {
|
||||
@@ -81,6 +85,10 @@ TEST(EigenHdf5ResultsWriter, WritesModalSchemaAndRejectsTamperedState) {
|
||||
const hid_t file =
|
||||
H5Fopen(path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT);
|
||||
ASSERT_GE(file, 0);
|
||||
const auto drilling = fesa::hdf5_internal::ReadDrillingDiagnostics(file);
|
||||
ASSERT_TRUE(drilling);
|
||||
EXPECT_EQ(drilling->formulation, fesa::Mitc4DrillingModeName(*state.drilling_mode));
|
||||
EXPECT_TRUE(drilling->frames.empty());
|
||||
EXPECT_GT(H5Lexists(file, "/steps/Step-1/eigen/modes", H5P_DEFAULT), 0);
|
||||
const hid_t controls =
|
||||
H5Dopen2(file, "/steps/Step-1/eigen/backend/base_fpm", H5P_DEFAULT);
|
||||
@@ -105,7 +113,8 @@ TEST(EigenHdf5ResultsWriter, WritesModalSchemaAndRejectsTamperedState) {
|
||||
[](auto& candidate) { candidate.constraint_mask[0] = 1U; },
|
||||
[](auto& candidate) { candidate.nodal_reduction[0].rcond_inf = 1e-15; },
|
||||
[](auto& candidate) { candidate.backend.base_fpm[5U] = 1; },
|
||||
[](auto& candidate) { candidate.eta_gram = 1e-5; }};
|
||||
[](auto& candidate) { candidate.eta_gram = 1e-5; },
|
||||
[](auto& candidate) { candidate.drilling_mode.reset(); }};
|
||||
for (const auto& mutate : mutations) {
|
||||
auto candidate = state;
|
||||
mutate(candidate);
|
||||
@@ -133,6 +142,27 @@ TEST(EigenHdf5ResultsWriter, WritesModalSchemaAndRejectsTamperedState) {
|
||||
locked_final.close();
|
||||
ASSERT_TRUE(
|
||||
fesa::hdf5_internal::SelfCheckEigenFile(path, domain, state, {}).IsOk());
|
||||
for (const auto mode : {fesa::Mitc4DrillingMode::kDirectSpring,
|
||||
fesa::Mitc4DrillingMode::kLegacyCorotatedTwist}) {
|
||||
state.drilling_mode = mode;
|
||||
ASSERT_TRUE(writer.Write(path, domain, state, {}).IsOk());
|
||||
fesa::hdf5_internal::Hdf5Handle stored_file{
|
||||
H5Fopen(path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT), H5Fclose};
|
||||
const auto stored = fesa::hdf5_internal::ReadDrillingDiagnostics(stored_file.Get());
|
||||
ASSERT_TRUE(stored);
|
||||
EXPECT_EQ(stored->formulation, fesa::Mitc4DrillingModeName(mode));
|
||||
EXPECT_TRUE(stored->frames.empty());
|
||||
}
|
||||
{
|
||||
fesa::hdf5_internal::Hdf5Handle stored_file{
|
||||
H5Fopen(path.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose};
|
||||
fesa::hdf5_internal::Hdf5Handle metadata{
|
||||
H5Gopen2(stored_file.Get(), "/metadata", H5P_DEFAULT), H5Gclose};
|
||||
ASSERT_GE(H5Adelete(metadata.Get(), "mitc4_drilling_formulation"), 0);
|
||||
ASSERT_GE(H5Adelete(metadata.Get(), "drilling_diagnostics_version"), 0);
|
||||
}
|
||||
EXPECT_FALSE(fesa::hdf5_internal::SelfCheckEigenFile(path, domain, state, {}).IsOk());
|
||||
ASSERT_TRUE(writer.Write(path, domain, state, {}).IsOk());
|
||||
const hid_t corrupt_file =
|
||||
H5Fopen(path.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT);
|
||||
ASSERT_GE(corrupt_file, 0);
|
||||
@@ -153,3 +183,22 @@ TEST(EigenHdf5ResultsWriter, WritesModalSchemaAndRejectsTamperedState) {
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
TEST(EigenHdf5ResultsWriter, DrillingLifecycleFaultsPreserveFinalBytes) {
|
||||
const auto path = std::filesystem::temp_directory_path() / "fesa-eigen-drilling-fault.h5";
|
||||
auto domain = fesa::Domain::Create(Definition(path.parent_path() / "shell.inp"));
|
||||
ASSERT_TRUE(domain.HasValue());
|
||||
auto state = fesa::LinearFrequencyProcedureExecutor{}.Execute(domain.Value());
|
||||
ASSERT_TRUE(state.HasValue());
|
||||
for (const std::string fault : {"candidate-close", "reader-close", "markers"}) {
|
||||
SCOPED_TRACE(fault);
|
||||
std::ofstream(path, std::ios::binary | std::ios::trunc) << "prior";
|
||||
EXPECT_FALSE(fesa::hdf5_internal::WriteEigenFile(path, domain.Value(), state.Value(), {},
|
||||
fesa::test::Mitc4LifecycleFault(fault)).IsOk());
|
||||
std::ifstream stream{path, std::ios::binary};
|
||||
EXPECT_EQ((std::string{std::istreambuf_iterator<char>{stream}, {}}), "prior");
|
||||
}
|
||||
std::error_code ignored;
|
||||
std::filesystem::remove(path, ignored);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <iterator>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
@@ -27,6 +28,13 @@
|
||||
#include "fesa/math/vector3.h"
|
||||
#include "fesa/model/domain.h"
|
||||
|
||||
#include "io/hdf5/mitc4_drilling_diagnostics.h"
|
||||
|
||||
#include "mitc4_fault_test_helpers.h"
|
||||
|
||||
#include "io/hdf5/hdf5_result_writer.h"
|
||||
#include "io/hdf5/hdf5_self_check.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char* kStepRoot = "/steps/Step-1/frames/0";
|
||||
@@ -241,7 +249,8 @@ fesa::ModelDefinition MakeShellDefinition(const std::filesystem::path& source) {
|
||||
return definition;
|
||||
}
|
||||
|
||||
WriterFixture MakeShellFixture(const std::filesystem::path& source) {
|
||||
WriterFixture MakeShellFixture(const std::filesystem::path& source,
|
||||
std::optional<fesa::Mitc4DrillingMode> mode = fesa::Mitc4DrillingMode::kDirectSpring) {
|
||||
auto domain_result = fesa::Domain::Create(MakeShellDefinition(source));
|
||||
if (!domain_result.HasValue()) {
|
||||
throw std::runtime_error{
|
||||
@@ -303,6 +312,8 @@ WriterFixture MakeShellFixture(const std::filesystem::path& source) {
|
||||
candidate.physical_strain_energy = 123.5;
|
||||
candidate.equilibrium = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0};
|
||||
candidate.verification_metrics = {1.0e-13, 2.0e-13, 3.0e-13};
|
||||
if (mode) candidate.mitc4_drilling = fesa::Mitc4DrillingEvidence{
|
||||
*mode, {0.5, 1.0, 1.5}, {1.0, 2.0, 3.0, 3.5, 4.0, 4.5}};
|
||||
const fesa::Status commit =
|
||||
state->CommitShellResults({0U}, std::move(candidate));
|
||||
if (!commit.IsOk()) {
|
||||
@@ -1343,3 +1354,149 @@ TEST(Hdf5ResultsWriter,
|
||||
EXPECT_EQ(ReadBytes(final), valid_bytes);
|
||||
EXPECT_EQ(EntryCount(directory.Path()), 1U);
|
||||
}
|
||||
|
||||
|
||||
TEST(Hdf5ResultsWriter, DrillingActualModesRoundTripWithoutDefaultInference) {
|
||||
for (const auto mode : {fesa::Mitc4DrillingMode::kDirectSpring,
|
||||
fesa::Mitc4DrillingMode::kLegacyCorotatedTwist}) {
|
||||
TempDirectory directory{"drilling-roundtrip"};
|
||||
auto fixture = MakeShellFixture(directory.Path() / "shell.inp", mode);
|
||||
const auto output = directory.Path() / "results.h5";
|
||||
ASSERT_TRUE(fesa::Hdf5ResultsWriter{}.Write(output, *fixture.domain, *fixture.state, {}).IsOk());
|
||||
const auto file = OpenFile(output);
|
||||
const auto stored = fesa::hdf5_internal::ReadDrillingDiagnostics(file.Get());
|
||||
ASSERT_TRUE(stored);
|
||||
EXPECT_EQ(stored->formulation, fesa::Mitc4DrillingModeName(mode));
|
||||
ASSERT_EQ(stored->frames.size(), 1U);
|
||||
EXPECT_EQ(stored->frames[0].raw_equilibrium, fixture.state->Equilibrium());
|
||||
EXPECT_EQ(stored->frames[0].drilling_moment, fixture.state->Mitc4Drilling()->drilling_moment);
|
||||
EXPECT_EQ(stored->frames[0].equilibrium_with_drilling,
|
||||
fixture.state->Mitc4Drilling()->equilibrium_with_drilling);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(Hdf5ResultsWriter, DrillingMissingActualStatePreservesFinal) {
|
||||
TempDirectory directory{"drilling-missing"};
|
||||
auto fixture = MakeShellFixture(directory.Path() / "shell.inp", std::nullopt);
|
||||
const auto output = directory.Path() / "results.h5";
|
||||
const std::vector<char> sentinel{'p','r','i','o','r'};
|
||||
WriteBytes(output, sentinel);
|
||||
EXPECT_FALSE(fesa::Hdf5ResultsWriter{}.Write(output, *fixture.domain, *fixture.state, {}).IsOk());
|
||||
EXPECT_EQ(ReadBytes(output), sentinel);
|
||||
EXPECT_EQ(EntryCount(directory.Path()), 1U);
|
||||
}
|
||||
|
||||
TEST(Hdf5ResultsWriter, DrillingReadbackCannotTreatRemovedMarkersAsHistorical) {
|
||||
for (const bool remove_markers : {true, false}) {
|
||||
TempDirectory directory{"drilling-readback"};
|
||||
auto fixture = MakeShellFixture(directory.Path() / "shell.inp");
|
||||
const auto output = directory.Path() / "results.h5";
|
||||
ASSERT_TRUE(fesa::Hdf5ResultsWriter{}.Write(output, *fixture.domain, *fixture.state, {}).IsOk());
|
||||
{
|
||||
fesa::hdf5_internal::Hdf5Handle file{
|
||||
H5Fopen(output.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose};
|
||||
if (remove_markers) {
|
||||
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);
|
||||
EXPECT_FALSE(fesa::hdf5_internal::ReadDrillingDiagnostics(file.Get()));
|
||||
} else {
|
||||
ASSERT_GE(H5Ldelete(file.Get(), "/steps/Step-1/frames/0/global/drilling_moment", H5P_DEFAULT), 0);
|
||||
ASSERT_GE(H5Ldelete(file.Get(), "/steps/Step-1/frames/0/global/equilibrium_with_drilling", H5P_DEFAULT), 0);
|
||||
}
|
||||
}
|
||||
const auto plan = fesa::hdf5_internal::BuildWriterPlan(output, *fixture.domain, *fixture.state, {});
|
||||
ASSERT_TRUE(plan.HasValue());
|
||||
EXPECT_FALSE(fesa::hdf5_internal::SelfCheck(output, *fixture.domain, *fixture.state, 0U,
|
||||
plan.Value().model_data).IsOk());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TEST(Hdf5ResultsWriter, DrillingReaderRejectsMalformedAndInconsistentEvidence) {
|
||||
for (const std::string mutation : {"partial-mode", "partial-version", "mode", "version", "version-type",
|
||||
"mode-type", "shape", "type", "endian", "nan", "inf", "algebra", "force"}) {
|
||||
SCOPED_TRACE(mutation);
|
||||
TempDirectory directory{"drilling-invalid"};
|
||||
auto fixture = MakeShellFixture(directory.Path() / "shell.inp");
|
||||
const auto output = directory.Path() / "results.h5";
|
||||
ASSERT_TRUE(fesa::Hdf5ResultsWriter{}.Write(output, *fixture.domain, *fixture.state, {}).IsOk());
|
||||
fesa::hdf5_internal::Hdf5ErrorSilencer silence;
|
||||
fesa::hdf5_internal::Hdf5Handle file{
|
||||
H5Fopen(output.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose};
|
||||
fesa::hdf5_internal::Hdf5Handle metadata{
|
||||
H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose};
|
||||
const std::string path = "/steps/Step-1/frames/0/global/drilling_moment";
|
||||
if (mutation == "partial-mode" || mutation == "partial-version") {
|
||||
ASSERT_GE(H5Adelete(metadata.Get(), mutation == "partial-mode" ?
|
||||
"mitc4_drilling_formulation" : "drilling_diagnostics_version"), 0);
|
||||
} else if (mutation == "mode" || mutation == "mode-type") {
|
||||
ASSERT_GE(H5Adelete(metadata.Get(), "mitc4_drilling_formulation"), 0);
|
||||
if (mutation == "mode") fesa::hdf5_internal::WriteStringAttribute(metadata.Get(), "mitc4_drilling_formulation", "unknown");
|
||||
else fesa::hdf5_internal::WriteUint64Attribute(metadata.Get(), "mitc4_drilling_formulation", 1U);
|
||||
} else if (mutation == "version" || mutation == "version-type") {
|
||||
ASSERT_GE(H5Adelete(metadata.Get(), "drilling_diagnostics_version"), 0);
|
||||
if (mutation == "version") fesa::hdf5_internal::WriteUint64Attribute(metadata.Get(), "drilling_diagnostics_version", 2U);
|
||||
else fesa::hdf5_internal::WriteDoubleAttribute(metadata.Get(), "drilling_diagnostics_version", 1.0);
|
||||
} else if (mutation == "shape" || mutation == "type" || mutation == "endian") {
|
||||
ASSERT_GE(H5Ldelete(file.Get(), path.c_str(), H5P_DEFAULT), 0);
|
||||
const hsize_t size = mutation == "shape" ? 2U : 3U;
|
||||
fesa::hdf5_internal::Hdf5Handle space{H5Screate_simple(1, &size, nullptr), H5Sclose};
|
||||
fesa::hdf5_internal::Hdf5Handle dataset{H5Dcreate2(file.Get(), path.c_str(),
|
||||
mutation == "type" ? H5T_IEEE_F32LE : (mutation == "endian" ? H5T_IEEE_F64BE : H5T_IEEE_F64LE),
|
||||
space.Get(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT), H5Dclose};
|
||||
ASSERT_GE(dataset.Get(), 0);
|
||||
} else {
|
||||
const std::string target = mutation == "inf" ? "/steps/Step-1/frames/0/global/equilibrium" :
|
||||
(mutation == "nan" ? path : "/steps/Step-1/frames/0/global/equilibrium_with_drilling");
|
||||
fesa::hdf5_internal::Hdf5Handle dataset{H5Dopen2(file.Get(), target.c_str(), H5P_DEFAULT), H5Dclose};
|
||||
std::array<double, 6> values{1., 2., 3., 3.5, 4., 4.5};
|
||||
if (mutation == "nan") values[0] = std::numeric_limits<double>::quiet_NaN();
|
||||
else if (mutation == "inf") values[0] = std::numeric_limits<double>::infinity();
|
||||
else values[mutation == "force" ? 0U : 3U] = 1e-100;
|
||||
ASSERT_GE(H5Dwrite(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data()), 0);
|
||||
}
|
||||
EXPECT_THROW((void)fesa::hdf5_internal::ReadDrillingDiagnostics(file.Get()),
|
||||
fesa::hdf5_internal::Hdf5Failure);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TEST(Hdf5ResultsWriter, DrillingHistoricalSelfCheckDoesNotInferMode) {
|
||||
TempDirectory directory{"drilling-historical"};
|
||||
auto fixture = MakeShellFixture(directory.Path() / "shell.inp");
|
||||
const auto output = directory.Path() / "results.h5";
|
||||
ASSERT_TRUE(fesa::Hdf5ResultsWriter{}.Write(output, *fixture.domain, *fixture.state, {}).IsOk());
|
||||
{
|
||||
fesa::hdf5_internal::Hdf5Handle file{
|
||||
H5Fopen(output.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);
|
||||
ASSERT_GE(H5Ldelete(file.Get(), "/steps/Step-1/frames/0/global/drilling_moment", H5P_DEFAULT), 0);
|
||||
ASSERT_GE(H5Ldelete(file.Get(), "/steps/Step-1/frames/0/global/equilibrium_with_drilling", H5P_DEFAULT), 0);
|
||||
}
|
||||
auto historical = MakeShellFixture(directory.Path() / "shell.inp", std::nullopt);
|
||||
const auto plan = fesa::hdf5_internal::BuildWriterPlan(output, *historical.domain, *historical.state, {});
|
||||
ASSERT_TRUE(plan.HasValue());
|
||||
EXPECT_TRUE(fesa::hdf5_internal::SelfCheck(output, *historical.domain, *historical.state, 0U,
|
||||
plan.Value().model_data).IsOk());
|
||||
EXPECT_FALSE(historical.state->Mitc4Drilling());
|
||||
}
|
||||
|
||||
|
||||
TEST(Hdf5ResultsWriter, DrillingLifecycleFaultsPreserveFinalBytes) {
|
||||
TempDirectory directory{"drilling-faults"};
|
||||
auto fixture = MakeShellFixture(directory.Path() / "shell.inp");
|
||||
const auto path = directory.Path() / "results.h5";
|
||||
const std::vector<char> prior{'p','r','i','o','r'};
|
||||
for (const std::string fault : {"candidate-close", "reader-close", "markers", "datasets"}) {
|
||||
SCOPED_TRACE(fault);
|
||||
WriteBytes(path, prior);
|
||||
EXPECT_FALSE(fesa::hdf5_internal::WriteStaticFile(path, *fixture.domain, *fixture.state, {},
|
||||
fesa::test::Mitc4LifecycleFault(fault)).IsOk());
|
||||
EXPECT_EQ(ReadBytes(path), prior);
|
||||
EXPECT_EQ(EntryCount(directory.Path()), 1U);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
#ifndef FESA_TESTS_UNIT_IO_HDF5_MITC4_FAULT_TEST_HELPERS_H_
|
||||
#define FESA_TESTS_UNIT_IO_HDF5_MITC4_FAULT_TEST_HELPERS_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "io/hdf5/mitc4_file_hooks.h"
|
||||
#include "io/hdf5/hdf5_raii.h"
|
||||
|
||||
namespace fesa::test {
|
||||
|
||||
inline herr_t CloseMitc4FileAndReportFailure(hid_t file) {
|
||||
(void)H5Fclose(file);
|
||||
return -1;
|
||||
}
|
||||
|
||||
inline hdf5_internal::Mitc4FileHooks Mitc4LifecycleFault(const std::string& fault) {
|
||||
hdf5_internal::Mitc4FileHooks hooks;
|
||||
if (fault == "candidate-close") hooks.candidate_close = CloseMitc4FileAndReportFailure;
|
||||
if (fault == "reader-close") hooks.reader_close = CloseMitc4FileAndReportFailure;
|
||||
if (fault == "markers" || fault == "datasets") {
|
||||
hooks.before_self_check = [fault](const std::filesystem::path& path) {
|
||||
hdf5_internal::Hdf5Handle file{hdf5_internal::RequireHdf5Id(
|
||||
H5Fopen(path.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), "Cannot corrupt test candidate."), H5Fclose};
|
||||
if (fault == "markers") {
|
||||
hdf5_internal::Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose};
|
||||
hdf5_internal::RequireHdf5(H5Adelete(metadata.Get(), "mitc4_drilling_formulation"), "Cannot remove mode.");
|
||||
hdf5_internal::RequireHdf5(H5Adelete(metadata.Get(), "drilling_diagnostics_version"), "Cannot remove version.");
|
||||
} else {
|
||||
hdf5_internal::RequireHdf5(H5Ldelete(file.Get(),
|
||||
"/steps/Step-1/frames/0/global/drilling_moment", H5P_DEFAULT), "Cannot remove moment.");
|
||||
hdf5_internal::RequireHdf5(H5Ldelete(file.Get(),
|
||||
"/steps/Step-1/frames/0/global/equilibrium_with_drilling", H5P_DEFAULT), "Cannot remove corrected balance.");
|
||||
}
|
||||
hdf5_internal::RequireHdf5(file.CloseChecked(), "Cannot close corrupted candidate.");
|
||||
};
|
||||
}
|
||||
return hooks;
|
||||
}
|
||||
|
||||
} // namespace fesa::test
|
||||
|
||||
#endif // FESA_TESTS_UNIT_IO_HDF5_MITC4_FAULT_TEST_HELPERS_H_
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
@@ -25,6 +26,11 @@
|
||||
#include "fesa/model/domain.h"
|
||||
#include "fesa/results/nonlinear_results_writer.h"
|
||||
#include "io/hdf5/hdf5_raii.h"
|
||||
#include "io/hdf5/hdf5_self_check.h"
|
||||
|
||||
#include "io/hdf5/mitc4_drilling_diagnostics.h"
|
||||
|
||||
#include "mitc4_fault_test_helpers.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -184,7 +190,8 @@ std::vector<fesa::ShellResultRow> MakeShellRows(const double seed) {
|
||||
fesa::AcceptedFrameState MakeFrame(const fesa::NonlinearTrialState& committed,
|
||||
const std::uint32_t frame_index,
|
||||
const std::uint32_t increment_index,
|
||||
const double load_factor) {
|
||||
const double load_factor,
|
||||
std::optional<fesa::Mitc4DrillingMode> mode = fesa::Mitc4DrillingMode::kDirectSpring) {
|
||||
fesa::AcceptedFrameState frame{
|
||||
frame_index,
|
||||
increment_index,
|
||||
@@ -223,10 +230,13 @@ fesa::AcceptedFrameState MakeFrame(const fesa::NonlinearTrialState& committed,
|
||||
frame.reaction[offset + component] = frame.residual[offset + component];
|
||||
}
|
||||
}
|
||||
if (mode) frame.mitc4_drilling = fesa::Mitc4DrillingEvidence{
|
||||
*mode, {}, frame.global_equilibrium};
|
||||
return frame;
|
||||
}
|
||||
|
||||
Fixture MakeFixture(const std::filesystem::path& source) {
|
||||
Fixture MakeFixture(const std::filesystem::path& source,
|
||||
std::optional<fesa::Mitc4DrillingMode> mode = fesa::Mitc4DrillingMode::kDirectSpring) {
|
||||
auto domain_result = fesa::Domain::Create(MakeDefinition(source));
|
||||
if (!domain_result.HasValue()) {
|
||||
throw std::runtime_error{"Unable to construct nonlinear HDF5 Domain."};
|
||||
@@ -248,10 +258,10 @@ Fixture MakeFixture(const std::filesystem::path& source) {
|
||||
}
|
||||
fesa::NonlinearAnalysisState state = std::move(state_result.Value());
|
||||
if (!state.CommitAcceptedFrame(
|
||||
MakeFrame(state.CommittedState(), 0U, 1U, 0.5))
|
||||
MakeFrame(state.CommittedState(), 0U, 1U, 0.5, mode))
|
||||
.IsOk() ||
|
||||
!state.CommitAcceptedFrame(
|
||||
MakeFrame(state.CommittedState(), 1U, 2U, 1.0))
|
||||
MakeFrame(state.CommittedState(), 1U, 2U, 1.0, mode))
|
||||
.IsOk()) {
|
||||
throw std::runtime_error{"Unable to commit nonlinear HDF5 test frames."};
|
||||
}
|
||||
@@ -615,3 +625,55 @@ TEST(NonlinearHdf5ResultsWriter, CandidateFinalizationFailurePreservesPriorFinal
|
||||
EXPECT_EQ(status.Diagnostics()[0U].code, "hdf5-finalization-failure");
|
||||
EXPECT_EQ(ReadBytes(output), sentinel);
|
||||
}
|
||||
|
||||
|
||||
TEST(NonlinearHdf5ResultsWriter, DrillingModesAndEveryAcceptedFrameRoundTrip) {
|
||||
for (const auto mode : {fesa::Mitc4DrillingMode::kDirectSpring,
|
||||
fesa::Mitc4DrillingMode::kLegacyCorotatedTwist}) {
|
||||
TempDirectory directory{"drilling"};
|
||||
auto fixture = MakeFixture(directory.Path() / "shell.inp", mode);
|
||||
const auto path = directory.Path() / "results.h5";
|
||||
ASSERT_TRUE(fesa::NonlinearHdf5ResultsWriter{}.Write(path, *fixture.domain, fixture.state, {}).IsOk());
|
||||
{
|
||||
const auto file = OpenFile(path);
|
||||
const auto stored = fesa::hdf5_internal::ReadDrillingDiagnostics(file.Get());
|
||||
ASSERT_TRUE(stored);
|
||||
EXPECT_EQ(stored->formulation, fesa::Mitc4DrillingModeName(mode));
|
||||
ASSERT_EQ(stored->frames.size(), fixture.state.AcceptedFrames().size());
|
||||
EXPECT_EQ(stored->frames.front().frame_index, 0U);
|
||||
EXPECT_EQ(stored->frames.back().raw_equilibrium, fixture.state.AcceptedFrames().back().global_equilibrium);
|
||||
}
|
||||
{
|
||||
Hdf5Handle file{H5Fopen(path.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose};
|
||||
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);
|
||||
}
|
||||
EXPECT_FALSE(fesa::hdf5_internal::NonlinearSelfCheck(path, *fixture.domain, fixture.state, 0U).IsOk());
|
||||
}
|
||||
}
|
||||
|
||||
TEST(NonlinearHdf5ResultsWriter, DrillingMissingActualStatePreservesPriorFinal) {
|
||||
TempDirectory directory{"drilling-missing"};
|
||||
auto fixture = MakeFixture(directory.Path() / "shell.inp", std::nullopt);
|
||||
const auto path = directory.Path() / "results.h5";
|
||||
std::ofstream(path, std::ios::binary) << "prior";
|
||||
EXPECT_FALSE(fesa::NonlinearHdf5ResultsWriter{}.Write(path, *fixture.domain, fixture.state, {}).IsOk());
|
||||
const auto bytes = ReadBytes(path);
|
||||
EXPECT_EQ(std::string(bytes.begin(), bytes.end()), "prior");
|
||||
}
|
||||
|
||||
|
||||
TEST(NonlinearHdf5ResultsWriter, DrillingLifecycleFaultsPreserveFinalBytes) {
|
||||
TempDirectory directory{"drilling-faults"};
|
||||
auto fixture = MakeFixture(directory.Path() / "shell.inp");
|
||||
const auto path = directory.Path() / "results.h5";
|
||||
for (const std::string fault : {"candidate-close", "reader-close", "markers", "datasets"}) {
|
||||
SCOPED_TRACE(fault);
|
||||
std::ofstream(path, std::ios::binary | std::ios::trunc) << "prior";
|
||||
EXPECT_FALSE(fesa::hdf5_internal::WriteNonlinearFile(path, *fixture.domain, fixture.state, {},
|
||||
fesa::test::Mitc4LifecycleFault(fault)).IsOk());
|
||||
const auto bytes = ReadBytes(path);
|
||||
EXPECT_EQ(std::string(bytes.begin(), bytes.end()), "prior");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user