#include "fesa/io/hdf5/nonlinear_hdf5_results_writer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "fesa/analysis/analysis_model.h" #include "fesa/analysis/nonlinear_analysis_state.h" #include "fesa/fem/dof_manager.h" #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 { using fesa::hdf5_internal::Hdf5Handle; constexpr std::array, 3> kIdentityTriad{ std::array{1.0, 0.0, 0.0}, std::array{0.0, 1.0, 0.0}, std::array{0.0, 0.0, 1.0}}; class TempDirectory { public: explicit TempDirectory(const std::string& leaf) : path_{std::filesystem::temp_directory_path() / ("fesa-nonlinear-hdf5-" + leaf)} { std::error_code error; std::filesystem::remove_all(path_, error); if (!std::filesystem::create_directory(path_)) { throw std::runtime_error{"Unable to create nonlinear HDF5 test directory."}; } } ~TempDirectory() { std::error_code error; std::filesystem::remove_all(path_, error); } const std::filesystem::path& Path() const noexcept { return path_; } private: std::filesystem::path path_; }; class WinHandle { public: explicit WinHandle(const HANDLE handle) : handle_{handle} {} WinHandle(const WinHandle&) = delete; WinHandle& operator=(const WinHandle&) = delete; ~WinHandle() { if (handle_ != INVALID_HANDLE_VALUE) { (void)CloseHandle(handle_); } } HANDLE Get() const noexcept { return handle_; } private: HANDLE handle_{INVALID_HANDLE_VALUE}; }; struct Fixture { std::unique_ptr domain; std::unique_ptr dofs; fesa::NonlinearAnalysisState state; }; struct AcceptedFrameRow { std::uint64_t frame_index; std::uint64_t increment_index; double load_factor; std::uint64_t iteration_count; std::uint8_t is_factor_one; }; struct DiagnosticRow { char* severity; char* code; char* file; std::uint64_t line; char* keyword; char* entity_identity; char* message; }; fesa::ModelDefinition MakeDefinition(const std::filesystem::path& source) { fesa::ModelDefinition definition{}; definition.source_path = source; definition.source_content_identity = "nonlinear-hdf5-fixture"; definition.nodes = { {{"Shell-1", 10, "10"}, {0.0, 0.0, 0.0}, {source, 1U}}, {{"Shell-1", 20, "20"}, {1.0, 0.0, 0.0}, {source, 2U}}, {{"Shell-1", 30, "30"}, {1.0, 1.0, 0.0}, {source, 3U}}, {{"Shell-1", 40, "40"}, {0.0, 1.0, 0.0}, {source, 4U}}}; definition.materials = {{"Steel", 210.0e9, 0.3, {source, 5U}}}; definition.shell_sections = {{"Shell", 0.1, 0U, {source, 6U}}}; definition.shell_elements = {{{"Shell-1", 1, "1"}, fesa::ShellSourceElementType::kS4, {0U, 1U, 2U, 3U}, 0U, 0U, {source, 7U}}}; for (std::size_t node = 0U; node < definition.nodes.size(); ++node) { definition.shell_node_initial_frames.push_back( {static_cast(node), {0.0, 0.0, 1.0}, {1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}}); } definition.steps = {{"Step-1", {}, {}, fesa::StaticProcedureKind::kNonlinearFullNewton, true, 0.5, 1.0, 0.001, 1.0, {source, 8U}}}; return definition; } fesa::NonlinearStateBaselineInput MakeBaseline(const fesa::Domain& domain) { fesa::NonlinearStateBaselineInput baseline{}; for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) { baseline.node_baselines.push_back( {static_cast(node), domain.Nodes()[node].source_id, kIdentityTriad, {0.0, 0.0, 1.0}}); } return baseline; } fesa::ShellResultRow MakeShellRow(const fesa::ShellMidsurfaceLocation location, const std::array coordinates, const double seed) { return {0U, location, coordinates, kIdentityTriad, {seed, seed + 1.0, seed + 2.0, seed + 3.0, seed + 4.0, seed + 5.0, seed + 6.0, seed + 7.0}, {seed + 10.0, seed + 11.0, seed + 12.0, seed + 13.0, seed + 14.0, seed + 15.0, seed + 16.0, seed + 17.0}, {{{fesa::ShellSectionPosition::kBottom, -1.0, {seed + 20.0, seed + 21.0, seed + 22.0}}, {fesa::ShellSectionPosition::kMiddle, 0.0, {seed + 23.0, seed + 24.0, seed + 25.0}}, {fesa::ShellSectionPosition::kTop, 1.0, {seed + 26.0, seed + 27.0, seed + 28.0}}}}}; } std::vector MakeShellRows(const double seed) { const double gauss = 1.0 / std::sqrt(3.0); return { MakeShellRow(fesa::ShellMidsurfaceLocation::kGp1, {-gauss, -gauss}, seed), MakeShellRow(fesa::ShellMidsurfaceLocation::kGp2, {gauss, -gauss}, seed + 30.0), MakeShellRow(fesa::ShellMidsurfaceLocation::kGp3, {gauss, gauss}, seed + 60.0), MakeShellRow(fesa::ShellMidsurfaceLocation::kGp4, {-gauss, gauss}, seed + 90.0)}; } fesa::AcceptedFrameState MakeFrame(const fesa::NonlinearTrialState& committed, const std::uint32_t frame_index, const std::uint32_t increment_index, const double load_factor, std::optional mode = fesa::Mitc4DrillingMode::kDirectSpring) { fesa::AcceptedFrameState frame{ frame_index, increment_index, load_factor, 2U, load_factor == 1.0, committed.nodal_internal_order, committed.nodal_source_ids, committed.generalized_coordinates, committed.displacement, committed.rotation_vector, committed.nodal_triad, committed.nodal_director, committed.internal_force, committed.external_force, committed.residual, committed.reaction, MakeShellRows(100.0 * load_factor), 20.0 * load_factor, {load_factor, 2.0 * load_factor, 3.0 * load_factor, 4.0 * load_factor, 5.0 * load_factor, 6.0 * load_factor}}; for (std::size_t node = 0U; node < frame.nodal_internal_order.size(); ++node) { const std::size_t offset = 6U * node; frame.displacement[offset] = load_factor * static_cast(node + 1U); frame.displacement[offset + 1U] = -frame.displacement[offset]; frame.displacement[offset + 2U] = 0.5 * frame.displacement[offset]; frame.rotation_vector[offset + 5U] = 0.1 * load_factor; for (std::size_t component = 0U; component < 6U; ++component) { frame.generalized_coordinates[offset + component] = component < 3U ? frame.displacement[offset + component] : frame.rotation_vector[offset + component]; frame.internal_force[offset + component] = 10.0 * static_cast(component + 1U); frame.external_force[offset + component] = 0.0; frame.residual[offset + component] = frame.internal_force[offset + component]; 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, std::optional 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."}; } auto domain = std::make_unique(std::move(domain_result.Value())); auto model_result = fesa::AnalysisModel::Create(*domain); if (!model_result.HasValue()) { throw std::runtime_error{"Unable to construct nonlinear HDF5 model."}; } auto dofs_result = fesa::DofManager::Create(model_result.Value()); if (!dofs_result.HasValue()) { throw std::runtime_error{"Unable to construct nonlinear HDF5 DOFs."}; } auto dofs = std::make_unique(std::move(dofs_result.Value())); auto state_result = fesa::NonlinearAnalysisState::Create(*domain, *dofs, MakeBaseline(*domain)); if (!state_result.HasValue()) { throw std::runtime_error{"Unable to construct nonlinear HDF5 state."}; } fesa::NonlinearAnalysisState state = std::move(state_result.Value()); if (!state.CommitAcceptedFrame( MakeFrame(state.CommittedState(), 0U, 1U, 0.5, mode)) .IsOk() || !state.CommitAcceptedFrame( MakeFrame(state.CommittedState(), 1U, 2U, 1.0, mode)) .IsOk()) { throw std::runtime_error{"Unable to commit nonlinear HDF5 test frames."}; } return {std::move(domain), std::move(dofs), std::move(state)}; } Hdf5Handle OpenFile(const std::filesystem::path& path) { return {H5Fopen(path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT), H5Fclose}; } Hdf5Handle OpenDataset(const hid_t file, const std::string& path) { return {H5Dopen2(file, path.c_str(), H5P_DEFAULT), H5Dclose}; } std::vector DatasetDimensions(const hid_t file, const std::string& path) { const auto dataset = OpenDataset(file, path); Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose}; const int rank = H5Sget_simple_extent_ndims(space.Get()); std::vector dimensions(static_cast(rank)); if (rank > 0) { EXPECT_GE(H5Sget_simple_extent_dims(space.Get(), dimensions.data(), nullptr), 0); } return dimensions; } std::string ReadStringAttribute(const hid_t object, const char* name) { Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose}; Hdf5Handle type{H5Aget_type(attribute.Get()), H5Tclose}; char* raw = nullptr; EXPECT_GE(H5Aread(attribute.Get(), type.Get(), &raw), 0); const std::string value = raw == nullptr ? std::string{} : std::string{raw}; if (raw != nullptr) { EXPECT_GE(H5free_memory(raw), 0); } return value; } std::uint64_t ReadUint64Attribute(const hid_t object, const char* name) { Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose}; std::uint64_t value = 0U; EXPECT_GE(H5Aread(attribute.Get(), H5T_NATIVE_UINT64, &value), 0); return value; } double ReadDoubleAttribute(const hid_t object, const char* name) { Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose}; double value = 0.0; EXPECT_GE(H5Aread(attribute.Get(), H5T_NATIVE_DOUBLE, &value), 0); return value; } std::vector ReadDoubleDataset(const hid_t file, const std::string& path) { const auto dimensions = DatasetDimensions(file, path); std::size_t count = 1U; for (const hsize_t dimension : dimensions) { count *= static_cast(dimension); } std::vector values(count); const auto dataset = OpenDataset(file, path); if (!values.empty()) { EXPECT_GE(H5Dread(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data()), 0); } return values; } Hdf5Handle AcceptedFrameMemoryType() { Hdf5Handle type{H5Tcreate(H5T_COMPOUND, sizeof(AcceptedFrameRow)), H5Tclose}; EXPECT_GE(H5Tinsert(type.Get(), "frame_index", HOFFSET(AcceptedFrameRow, frame_index), H5T_NATIVE_UINT64), 0); EXPECT_GE(H5Tinsert(type.Get(), "increment_index", HOFFSET(AcceptedFrameRow, increment_index), H5T_NATIVE_UINT64), 0); EXPECT_GE(H5Tinsert(type.Get(), "load_factor", HOFFSET(AcceptedFrameRow, load_factor), H5T_NATIVE_DOUBLE), 0); EXPECT_GE(H5Tinsert(type.Get(), "iteration_count", HOFFSET(AcceptedFrameRow, iteration_count), H5T_NATIVE_UINT64), 0); EXPECT_GE(H5Tinsert(type.Get(), "is_factor_one", HOFFSET(AcceptedFrameRow, is_factor_one), H5T_NATIVE_UINT8), 0); return type; } std::vector ReadAcceptedFrames(const hid_t file) { const auto dataset = OpenDataset(file, "/steps/Step-1/accepted_frames"); const auto dimensions = DatasetDimensions(file, "/steps/Step-1/accepted_frames"); std::vector rows(static_cast(dimensions[0U])); auto type = AcceptedFrameMemoryType(); EXPECT_GE(H5Dread(dataset.Get(), type.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, rows.data()), 0); return rows; } Hdf5Handle DiagnosticMemoryType() { Hdf5Handle string_type{H5Tcopy(H5T_C_S1), H5Tclose}; EXPECT_GE(H5Tset_size(string_type.Get(), H5T_VARIABLE), 0); EXPECT_GE(H5Tset_cset(string_type.Get(), H5T_CSET_UTF8), 0); Hdf5Handle type{H5Tcreate(H5T_COMPOUND, sizeof(DiagnosticRow)), H5Tclose}; EXPECT_GE(H5Tinsert(type.Get(), "severity", HOFFSET(DiagnosticRow, severity), string_type.Get()), 0); EXPECT_GE(H5Tinsert(type.Get(), "code", HOFFSET(DiagnosticRow, code), string_type.Get()), 0); EXPECT_GE(H5Tinsert(type.Get(), "file", HOFFSET(DiagnosticRow, file), string_type.Get()), 0); EXPECT_GE(H5Tinsert(type.Get(), "line", HOFFSET(DiagnosticRow, line), H5T_NATIVE_UINT64), 0); EXPECT_GE(H5Tinsert(type.Get(), "keyword", HOFFSET(DiagnosticRow, keyword), string_type.Get()), 0); EXPECT_GE(H5Tinsert(type.Get(), "entity_identity", HOFFSET(DiagnosticRow, entity_identity), string_type.Get()), 0); EXPECT_GE(H5Tinsert(type.Get(), "message", HOFFSET(DiagnosticRow, message), string_type.Get()), 0); return type; } std::vector ReadDiagnostics(const hid_t file) { const auto dataset = OpenDataset(file, "/diagnostics"); const auto dimensions = DatasetDimensions(file, "/diagnostics"); std::vector rows(static_cast(dimensions[0U])); auto type = DiagnosticMemoryType(); EXPECT_GE(H5Dread(dataset.Get(), type.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, rows.data()), 0); return rows; } void ReclaimDiagnostics(const std::vector& rows) { for (const auto& row : rows) { for (char* value : {row.severity, row.code, row.file, row.keyword, row.entity_identity, row.message}) { if (value != nullptr) { EXPECT_GE(H5free_memory(value), 0); } } } } std::vector ReadBytes(const std::filesystem::path& path) { std::ifstream input{path, std::ios::binary}; return {std::istreambuf_iterator{input}, std::istreambuf_iterator{}}; } void WriteBytes(const std::filesystem::path& path, const std::vector& bytes) { std::ofstream output{path, std::ios::binary | std::ios::trunc}; output.write(bytes.data(), static_cast(bytes.size())); if (!output) { throw std::runtime_error{"Unable to write nonlinear HDF5 test bytes."}; } } } // namespace // NSNR-HDF5-001 through NSNR-HDF5-006, NSNR-HDF5-008 through NSNR-HDF5-010. TEST(NonlinearHdf5ResultsWriter, WritesSchemaV1ModelAndCommittedAcceptedFramesOnly) { TempDirectory directory{"schema"}; auto fixture = MakeFixture(directory.Path() / "model.inp"); const auto output = directory.Path() / "model_results.h5"; fesa::NonlinearHdf5ResultsWriter writer; const auto status = writer.Write(output, *fixture.domain, fixture.state, {}); ASSERT_TRUE(status.IsOk()) << status.Diagnostics().at(0U).message; const auto file = OpenFile(output); ASSERT_GE(file.Get(), 0); Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose}; ASSERT_GE(metadata.Get(), 0); EXPECT_EQ(ReadUint64Attribute(metadata.Get(), "schema_version"), 1U); EXPECT_EQ(ReadStringAttribute(metadata.Get(), "feature_id"), "nonlinear-static-newton-raphson"); EXPECT_EQ(ReadStringAttribute(metadata.Get(), "analysis_procedure"), "nonlinear-static-full-newton"); EXPECT_EQ(ReadStringAttribute(metadata.Get(), "internal_formulation"), "FESA-MITC4-total-lagrangian"); EXPECT_EQ(ReadStringAttribute(metadata.Get(), "integration_rule"), "2x2x2-gauss; mitc4-edge-midpoint-shear"); EXPECT_EQ(ReadUint64Attribute(metadata.Get(), "factor_zero_is_frame"), 0U); EXPECT_DOUBLE_EQ(ReadDoubleAttribute(metadata.Get(), "factor_zero_load_factor"), 0.0); for (const char* path : {"/model/nodes", "/model/elements", "/model/shell/materials", "/model/shell/sections", "/model/shell/nodal_director", "/model/shell/nodal_frame", "/model/nodal_constraint_mask", "/model/prescribed_displacement_start", "/model/prescribed_displacement_target", "/model/shell/midsurface_locations", "/model/shell/section_positions"}) { EXPECT_GT(H5Lexists(file.Get(), path, H5P_DEFAULT), 0) << path; } EXPECT_EQ(DatasetDimensions(file.Get(), "/steps/Step-1/accepted_frames"), std::vector({2U})); const auto frames = ReadAcceptedFrames(file.Get()); ASSERT_EQ(frames.size(), 2U); EXPECT_EQ(frames[0U].frame_index, 0U); EXPECT_EQ(frames[0U].increment_index, 1U); EXPECT_DOUBLE_EQ(frames[0U].load_factor, 0.5); EXPECT_EQ(frames[0U].is_factor_one, 0U); EXPECT_EQ(frames[1U].frame_index, 1U); EXPECT_EQ(frames[1U].increment_index, 2U); EXPECT_DOUBLE_EQ(frames[1U].load_factor, 1.0); EXPECT_EQ(frames[1U].is_factor_one, 1U); EXPECT_EQ(H5Lexists(file.Get(), "/steps/Step-1/frames/2", H5P_DEFAULT), 0); EXPECT_EQ(H5Lexists(file.Get(), "/steps/Step-1/trial_history", H5P_DEFAULT), 0); EXPECT_EQ(H5Lexists(file.Get(), "/surface_pressure", H5P_DEFAULT), 0); const std::string root = "/steps/Step-1/frames/1"; for (const std::string& suffix : {"/nodal/displacement", "/nodal/triad", "/nodal/director", "/nodal/generalized_internal_force", "/nodal/generalized_external_force", "/nodal/generalized_residual", "/nodal/reaction", "/element/shell/local_frame", "/element/shell/generalized_strain", "/element/shell/section_resultant", "/element/shell/stress", "/global/energy", "/global/equilibrium"}) { EXPECT_GT(H5Lexists(file.Get(), (root + suffix).c_str(), H5P_DEFAULT), 0) << suffix; } EXPECT_EQ(DatasetDimensions(file.Get(), root + "/nodal/displacement"), std::vector({4U, 6U})); EXPECT_EQ(DatasetDimensions(file.Get(), root + "/nodal/triad"), std::vector({4U, 3U, 3U})); EXPECT_EQ(DatasetDimensions(file.Get(), root + "/element/shell/local_frame"), std::vector({1U, 4U, 3U, 3U})); EXPECT_EQ(DatasetDimensions(file.Get(), root + "/element/shell/stress"), std::vector({1U, 4U, 3U, 3U})); const auto displacement = ReadDoubleDataset(file.Get(), root + "/nodal/displacement"); ASSERT_EQ(displacement.size(), 24U); EXPECT_DOUBLE_EQ(displacement[0U], 1.0); EXPECT_DOUBLE_EQ(displacement[1U], -1.0); EXPECT_DOUBLE_EQ(displacement[2U], 0.5); EXPECT_DOUBLE_EQ(displacement[5U], 0.1); } // NSNR-HDF5-007. TEST(NonlinearHdf5ResultsWriter, ProjectsRotationalReactionToSpatialMoment) { TempDirectory directory{"reaction"}; auto fixture = MakeFixture(directory.Path() / "reaction.inp"); const auto output = directory.Path() / "reaction_results.h5"; fesa::NonlinearHdf5ResultsWriter writer; ASSERT_TRUE(writer.Write(output, *fixture.domain, fixture.state, {}).IsOk()); const auto file = OpenFile(output); const auto reaction = ReadDoubleDataset( file.Get(), "/steps/Step-1/frames/1/nodal/reaction"); ASSERT_EQ(reaction.size(), 24U); const double theta = 0.1; const double quadratic = (1.0 - 0.5 * theta / std::tan(0.5 * theta)) / (theta * theta); EXPECT_NEAR(reaction[3U], (1.0 - quadratic * theta * theta) * 40.0 - 0.5 * theta * 50.0, 1.0e-12); EXPECT_NEAR(reaction[4U], 0.5 * theta * 40.0 + (1.0 - quadratic * theta * theta) * 50.0, 1.0e-12); EXPECT_NEAR(reaction[5U], 60.0, 1.0e-12); EXPECT_GT(quadratic, 0.0); } // NSNR-HDF5-014. TEST(NonlinearHdf5ResultsWriter, WritesSchemaV1GlobalBalanceErrorLocation) { TempDirectory directory{"global-balance-location"}; auto fixture = MakeFixture(directory.Path() / "global-balance.inp"); const auto output = directory.Path() / "global-balance_results.h5"; fesa::NonlinearHdf5ResultsWriter writer; ASSERT_TRUE(writer.Write(output, *fixture.domain, fixture.state, {}).IsOk()); const auto file = OpenFile(output); const auto equilibrium = OpenDataset( file.Get(), "/steps/Step-1/frames/1/global/equilibrium"); EXPECT_EQ(DatasetDimensions(file.Get(), "/steps/Step-1/frames/1/global/equilibrium"), std::vector({6U})); EXPECT_EQ(ReadStringAttribute(equilibrium.Get(), "component_names"), "FORCE_1,FORCE_2,FORCE_3,MOMENT_1,MOMENT_2,MOMENT_3"); EXPECT_EQ(ReadStringAttribute(equilibrium.Get(), "location"), "global-balance-error"); Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose}; ASSERT_GE(metadata.Get(), 0); EXPECT_EQ(ReadUint64Attribute(metadata.Get(), "schema_version"), 1U); } // NSNR-HDF5-012 and NSNR-HDF5-013. TEST(NonlinearHdf5ResultsWriter, StoresSortedDiagnosticsThroughBackendNeutralBoundary) { static_assert(std::is_base_of_v); TempDirectory directory{"diagnostics"}; auto fixture = MakeFixture(directory.Path() / "diagnostics.inp"); const std::vector diagnostics{ {fesa::Severity::kWarning, "later", {fixture.domain->SourcePath(), 20U}, "*OUTPUT", "", "Later warning."}, {fesa::Severity::kWarning, "earlier", {fixture.domain->SourcePath(), 10U}, "*PREPRINT", "", "Earlier warning."}}; const auto output = directory.Path() / "diagnostics_results.h5"; fesa::NonlinearHdf5ResultsWriter writer; ASSERT_TRUE(writer.Write(output, *fixture.domain, fixture.state, diagnostics) .IsOk()); const auto file = OpenFile(output); const auto rows = ReadDiagnostics(file.Get()); ASSERT_EQ(rows.size(), 2U); EXPECT_STREQ(rows[0U].code, "earlier"); EXPECT_EQ(rows[0U].line, 10U); EXPECT_STREQ(rows[1U].code, "later"); EXPECT_EQ(rows[1U].line, 20U); ReclaimDiagnostics(rows); } // NSNR-HDF5-011. TEST(NonlinearHdf5ResultsWriter, CandidateFinalizationFailurePreservesPriorFinal) { TempDirectory directory{"atomic"}; auto fixture = MakeFixture(directory.Path() / "atomic.inp"); const auto output = directory.Path() / "atomic_results.h5"; const std::vector sentinel{'p', 'r', 'i', 'o', 'r'}; WriteBytes(output, sentinel); WinHandle lock{CreateFileW(output.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)}; ASSERT_NE(lock.Get(), INVALID_HANDLE_VALUE); fesa::NonlinearHdf5ResultsWriter writer; const auto status = writer.Write(output, *fixture.domain, fixture.state, {}); ASSERT_FALSE(status.IsOk()); EXPECT_EQ(status.Category(), fesa::FailureCategory::kOutput); ASSERT_EQ(status.Diagnostics().size(), 1U); 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"); } }