Add MITC4 drilling diagnostics output and comparison contract

This commit is contained in:
KOKO\Mimi
2026-10-20 09:45:58 +09:00
parent 3d5c296651
commit a73d3fc9ca
31 changed files with 1175 additions and 45 deletions
@@ -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);
}
+158 -1
View File
@@ -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");
}
}