#include "fesa/io/hdf5/dynamic_hdf5_results_writer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "fesa/analysis/analysis_model.h" #include "fesa/analysis/dynamic_analysis_state.h" #include "fesa/elements/finite_rotation.h" #include "fesa/fem/dof_manager.h" #include "io/hdf5/hdf5_raii.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}}; fesa::ModelDefinition MakeDefinition(const bool fixed = true) { const std::filesystem::path source{"dynamic-state.inp"}; fesa::ModelDefinition definition{}; definition.source_path = source; definition.source_content_identity = "hht-output-unit-fixture"; definition.nodes = {{{"Shell-A", 10, "0010"}, {0.0, 0.0, 0.0}, {source, 1U}}, {{"Shell-A", 20, "20"}, {1.0, 0.0, 0.0}, {source, 2U}}, {{"Shell-A", 30, "30"}, {1.0, 1.0, 0.0}, {source, 3U}}, {{"Shell-A", 40, "40"}, {0.0, 1.0, 0.0}, {source, 4U}}}; definition.materials = {{"Material", 1000.0, 0.25, {source, 5U}}}; definition.densities = {{0U, 2.0, {source, 6U}}}; definition.shell_sections = {{"Shell", 0.1, 0U, {source, 7U}}}; definition.shell_elements = {{{"Shell-A", 9, "0009"}, fesa::ShellSourceElementType::kS4, {0U, 1U, 2U, 3U}, 0U, 0U, {source, 8U}}}; for (std::size_t node = 0U; node < 4U; ++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.node_sets = {{"All", std::nullopt, {0U, 1U, 2U, 3U}, {source, 9U}}, {"Loaded", std::nullopt, {0U}, {source, 10U}}}; fesa::DynamicStepDefinition step{}; step.source_step_name_raw = "Transient Load"; step.source_step_name_canonical = "TRANSIENT LOAD"; step.initial_increment = 0.25; step.time_period = 0.5; step.increment_count = 2U; step.maximum_increments = 3U; if (fixed) { step.boundaries = {{"All", 1, 6, 0.0, {source, 11U}}}; step.loads = {{"Loaded", 3, 4.0, {source, 12U}}}; } definition.dynamic_step = step; return definition; } struct Fixture { std::unique_ptr domain; fesa::DynamicAnalysisState state; }; Fixture MakeFixture(fesa::ModelDefinition definition = MakeDefinition()) { auto domain_result = fesa::Domain::Create(std::move(definition)); if (!domain_result.HasValue()) throw std::runtime_error{"Invalid fixture Domain."}; auto domain = std::make_unique(std::move(domain_result.Value())); auto model = fesa::AnalysisModel::Create(*domain); if (!model.HasValue()) throw std::runtime_error{"Invalid fixture AnalysisModel."}; auto dofs = fesa::DofManager::Create(model.Value()); if (!dofs.HasValue()) throw std::runtime_error{"Invalid fixture DofManager."}; auto state = fesa::DynamicAnalysisState::Create(*domain, dofs.Value()); if (!state.HasValue()) throw std::runtime_error{"Invalid fixture state."}; return {std::move(domain), std::move(state.Value())}; } // A fully restrained, initially undeformed shell under a dead nodal force has // zero motion and strain, and nonzero support force at every accepted endpoint. fesa::DynamicAcceptedFrameState MakeFrame(const fesa::Domain& domain, const std::uint32_t index) { fesa::DynamicAcceptedFrameState frame{}; frame.frame_index = index; frame.increment_index = index; frame.time = static_cast(index) * domain.Dynamic()->initial_increment; frame.is_initial = index == 0U; for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) { frame.nodal_internal_order.push_back(static_cast(node)); frame.nodal_source_ids.push_back(domain.Nodes()[node].source_id); frame.nodal_triad.push_back(kIdentityTriad); frame.nodal_director.push_back({0.0, 0.0, 1.0}); } for (auto* vector : {&frame.generalized_coordinates, &frame.velocity, &frame.acceleration, &frame.algorithmic_acceleration, &frame.inertia_force, &frame.internal_force, &frame.external_force, &frame.residual, &frame.reaction, &frame.residual_scale}) { *vector = fesa::Vector{24U}; } frame.angular_velocity = fesa::Vector{12U}; frame.angular_acceleration = fesa::Vector{12U}; if (!domain.Dynamic()->loads.empty()) { frame.external_force[2U] = 4.0; frame.residual[2U] = -4.0; frame.reaction[2U] = -4.0; frame.residual_scale[2U] = 4.0; } const double gauss = 1.0 / std::sqrt(3.0); const std::array, 4> natural{ std::array{-gauss, -gauss}, {gauss, -gauss}, {gauss, gauss}, {-gauss, gauss}}; const std::array locations{ fesa::ShellMidsurfaceLocation::kGp1, fesa::ShellMidsurfaceLocation::kGp2, fesa::ShellMidsurfaceLocation::kGp3, fesa::ShellMidsurfaceLocation::kGp4}; for (std::size_t point = 0U; point < 4U; ++point) { frame.shell_results.push_back( {0U, locations[point], natural[point], kIdentityTriad, {}, {}, {{{fesa::ShellSectionPosition::kBottom, -1.0, {}}, {fesa::ShellSectionPosition::kMiddle, 0.0, {}}, {fesa::ShellSectionPosition::kTop, 1.0, {}}}}}); } return frame; } class TempFile { public: explicit TempFile(const std::string& name) : path_{std::filesystem::temp_directory_path() / ("fesa-dynamic-writer-" + name + ".h5")} { std::error_code error; std::filesystem::remove(path_, error); } ~TempFile() { std::error_code error; std::filesystem::remove(path_, error); } const std::filesystem::path& Path() const { return path_; } private: std::filesystem::path path_; }; void CommitHistory(Fixture& fixture) { for (std::uint32_t index = 0U; index <= 2U; ++index) { ASSERT_TRUE( fixture.state.CommitAcceptedFrame(MakeFrame(*fixture.domain, index)) .IsOk()); } } std::string ReadBytes(const std::filesystem::path& path) { std::ifstream input{path, std::ios::binary}; return std::string{std::istreambuf_iterator{input}, {}}; } std::string ReadString(const hid_t object, const char* name) { Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose}; Hdf5Handle type{H5Aget_type(attribute.Get()), H5Tclose}; EXPECT_EQ(H5Tget_class(type.Get()), H5T_STRING); EXPECT_GT(H5Tis_variable_str(type.Get()), 0); EXPECT_EQ(H5Tget_cset(type.Get()), H5T_CSET_UTF8); char* raw = nullptr; EXPECT_GE(H5Aread(attribute.Get(), type.Get(), &raw), 0); const std::string value = raw == nullptr ? "" : raw; (void)H5free_memory(raw); return value; } template Value ReadAttribute(const hid_t object, const char* name, const hid_t file_type, const hid_t memory_type) { Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose}; Hdf5Handle type{H5Aget_type(attribute.Get()), H5Tclose}; Hdf5Handle space{H5Aget_space(attribute.Get()), H5Sclose}; EXPECT_GT(H5Tequal(type.Get(), file_type), 0); EXPECT_EQ(H5Sget_simple_extent_type(space.Get()), H5S_SCALAR); Value result{}; EXPECT_GE(H5Aread(attribute.Get(), memory_type, &result), 0); return result; } std::vector ReadDoubles(const hid_t file, const std::string& path, const std::vector& dimensions) { Hdf5Handle dataset{H5Dopen2(file, path.c_str(), H5P_DEFAULT), H5Dclose}; EXPECT_GE(dataset.Get(), 0); Hdf5Handle type{H5Dget_type(dataset.Get()), H5Tclose}; EXPECT_GT(H5Tequal(type.Get(), H5T_IEEE_F64LE), 0); Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose}; EXPECT_EQ(H5Sget_simple_extent_ndims(space.Get()), dimensions.size()); std::vector actual(dimensions.size()); EXPECT_GE(H5Sget_simple_extent_dims(space.Get(), actual.data(), nullptr), 0); EXPECT_EQ(actual, dimensions); Hdf5Handle properties{H5Dget_create_plist(dataset.Get()), H5Pclose}; EXPECT_EQ(H5Pget_layout(properties.Get()), H5D_CONTIGUOUS); EXPECT_EQ(H5Pget_nfilters(properties.Get()), 0); std::size_t count = 1U; for (const auto size : dimensions) count *= static_cast(size); std::vector values(count); EXPECT_GE(H5Dread(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data()), 0); for (const double value : values) EXPECT_TRUE(std::isfinite(value)); return values; } void ExpectCompound(const hid_t file, const char* path, const std::vector>& fields, const hsize_t rows) { Hdf5Handle dataset{H5Dopen2(file, path, H5P_DEFAULT), H5Dclose}; ASSERT_GE(dataset.Get(), 0); Hdf5Handle type{H5Dget_type(dataset.Get()), H5Tclose}; EXPECT_EQ(H5Tget_nmembers(type.Get()), fields.size()); for (std::size_t index = 0U; index < fields.size(); ++index) { char* name = H5Tget_member_name(type.Get(), static_cast(index)); ASSERT_NE(name, nullptr); EXPECT_STREQ(name, fields[index].first); (void)H5free_memory(name); Hdf5Handle member{ H5Tget_member_type(type.Get(), static_cast(index)), H5Tclose}; EXPECT_GT(H5Tequal(member.Get(), fields[index].second), 0); } Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose}; EXPECT_EQ(H5Sget_simple_extent_ndims(space.Get()), 1); hsize_t actual = 0U; EXPECT_GE(H5Sget_simple_extent_dims(space.Get(), &actual, nullptr), 0); EXPECT_EQ(actual, rows); } TEST(DynamicHdf5ResultsWriter, RejectsDefaultStateBeforeCreatingOutput) { auto domain = fesa::Domain::Create(fesa::ModelDefinition{}); ASSERT_TRUE(domain.HasValue()); TempFile output{"default"}; fesa::DynamicHdf5ResultsWriter writer; const auto status = writer.Write(output.Path(), domain.Value(), {}, {}); EXPECT_FALSE(status.IsOk()); ASSERT_EQ(status.Diagnostics().size(), 1U); EXPECT_EQ(status.Diagnostics()[0U].code, "dynamic-output-schema"); EXPECT_FALSE(std::filesystem::exists(output.Path())); } TEST(DynamicHdf5ResultsWriter, WritesCompletePhysicalClampedHistoryAndSchema) { auto fixture = MakeFixture(); CommitHistory(fixture); TempFile output{"complete"}; fesa::DynamicHdf5ResultsWriter writer; const std::vector diagnostics{{fesa::Severity::kWarning, "ignored-output-request", {"dynamic-state.inp", 22U}, "OUTPUT", "", "Field request ignored."}, {fesa::Severity::kWarning, "ignored-output-request", {"dynamic-state.inp", 21U}, "RESTART", "", "Restart request ignored."}}; const auto status = writer.Write(output.Path(), *fixture.domain, fixture.state, diagnostics); ASSERT_TRUE(status.IsOk()) << (status.Diagnostics().empty() ? "" : status.Diagnostics()[0U].message); Hdf5Handle file{ H5Fopen(output.Path().string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT), H5Fclose}; ASSERT_GE(file.Get(), 0); Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose}; EXPECT_EQ(ReadString(metadata.Get(), "feature_id"), "hht-implicit-dynamics"); EXPECT_EQ(ReadString(metadata.Get(), "analysis_procedure"), "hht-implicit-dynamics"); EXPECT_EQ(ReadString(metadata.Get(), "source_step_name"), "Transient Load"); EXPECT_EQ(ReadString(metadata.Get(), "mass_formulation"), "consistent-kinetic"); EXPECT_EQ(ReadString(metadata.Get(), "acceleration_convention"), "physical-endpoint-and-auxiliary-separated"); EXPECT_EQ(ReadString(metadata.Get(), "initial_state"), "zero-q-v-step-load-0+"); EXPECT_EQ(ReadString(metadata.Get(), "unit_system_label"), "consistent-input-units"); EXPECT_EQ(ReadAttribute(metadata.Get(), "schema_version", H5T_STD_U64LE, H5T_NATIVE_UINT64), 1U); EXPECT_DOUBLE_EQ(ReadAttribute(metadata.Get(), "alpha", H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE), -0.05); EXPECT_DOUBLE_EQ(ReadAttribute(metadata.Get(), "beta", H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE), 0.275625); EXPECT_DOUBLE_EQ(ReadAttribute(metadata.Get(), "gamma", H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE), 0.55); ExpectCompound(file.Get(), "/steps/Step-1/accepted_frames", {{"frame_index", H5T_STD_U64LE}, {"increment_index", H5T_STD_U64LE}, {"time", H5T_IEEE_F64LE}, {"iteration_count", H5T_STD_U64LE}, {"is_initial", H5T_STD_U8LE}}, 3U); ExpectCompound( file.Get(), "/model/material_density", {{"internal_material_id", H5T_STD_U64LE}, {"rho", H5T_IEEE_F64LE}}, 1U); struct Density { std::uint64_t id; double rho; }; Hdf5Handle density{ H5Dopen2(file.Get(), "/model/material_density", H5P_DEFAULT), H5Dclose}; Hdf5Handle density_type{H5Dget_type(density.Get()), H5Tclose}; Density density_row{}; ASSERT_GE(H5Dread(density.Get(), density_type.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, &density_row), 0); EXPECT_EQ(density_row.id, 0U); EXPECT_DOUBLE_EQ(density_row.rho, 2.0); Hdf5Handle mask{ H5Dopen2(file.Get(), "/model/nodal_constraint_mask", H5P_DEFAULT), H5Dclose}; Hdf5Handle mask_type{H5Dget_type(mask.Get()), H5Tclose}; EXPECT_GT(H5Tequal(mask_type.Get(), H5T_STD_U8LE), 0); std::array constraints{}; ASSERT_GE(H5Dread(mask.Get(), H5T_NATIVE_UINT8, H5S_ALL, H5S_ALL, H5P_DEFAULT, constraints.data()), 0); for (const auto value : constraints) EXPECT_EQ(value, 1U); EXPECT_EQ(ReadDoubles(file.Get(), "/model/prescribed_displacement", {4U, 6U}), std::vector(24U, 0.0)); EXPECT_EQ(H5Lexists(file.Get(), "/model/prescribed_displacement_start", H5P_DEFAULT), 0); EXPECT_EQ(H5Lexists(file.Get(), "/model/prescribed_displacement_target", H5P_DEFAULT), 0); struct Dataset { const char* path; std::vector dimensions; const char* components; }; const std::vector datasets{ {"nodal/displacement", {4U, 6U}, "UX,UY,UZ,URX,URY,URZ"}, {"nodal/velocity", {4U, 6U}, "V1,V2,V3,PHIDOT1,PHIDOT2,PHIDOT3"}, {"nodal/acceleration", {4U, 6U}, "A1,A2,A3,PHIDDOT1,PHIDDOT2,PHIDDOT3"}, {"nodal/algorithmic_acceleration", {4U, 6U}, "AUX1,AUX2,AUX3,AUX4,AUX5,AUX6"}, {"nodal/angular_velocity", {4U, 3U}, "OMEGA1,OMEGA2,OMEGA3"}, {"nodal/angular_acceleration", {4U, 3U}, "OMEGADOT1,OMEGADOT2,OMEGADOT3"}, {"nodal/triad", {4U, 3U, 3U}, "X,Y,Z"}, {"nodal/director", {4U, 3U}, "D1,D2,D3"}, {"nodal/generalized_inertia_force", {4U, 6U}, "F1,F2,F3,MPHI1,MPHI2,MPHI3"}, {"nodal/generalized_internal_force", {4U, 6U}, "F1,F2,F3,MPHI1,MPHI2,MPHI3"}, {"nodal/generalized_external_force", {4U, 6U}, "F1,F2,F3,MPHI1,MPHI2,MPHI3"}, {"nodal/generalized_residual", {4U, 6U}, "F1,F2,F3,MPHI1,MPHI2,MPHI3"}, {"nodal/generalized_reaction", {4U, 6U}, "F1,F2,F3,MPHI1,MPHI2,MPHI3"}, {"nodal/reaction", {4U, 6U}, "RF1,RF2,RF3,RM1,RM2,RM3"}, {"element/shell/local_frame", {1U, 4U, 3U, 3U}, "X,Y,Z"}, {"element/shell/generalized_strain", {1U, 4U, 8U}, "E11,E22,G12,K11,K22,K12,G13,G23"}, {"element/shell/section_resultant", {1U, 4U, 8U}, "N11,N22,N12,M11,M22,M12,Q13,Q23"}, {"element/shell/stress", {1U, 4U, 3U, 3U}, "S11,S22,S12"}, {"global/energy", {5U}, "KINETIC_ENERGY,PHYSICAL_STRAIN_ENERGY,DRILLING_ENERGY,EXTERNAL_WORK," "ENERGY_DEFECT"}, {"global/equilibrium", {6U}, "FORCE_1,FORCE_2,FORCE_3,MOMENT_1,MOMENT_2,MOMENT_3"}}; for (std::uint64_t index = 0U; index <= 2U; ++index) { const std::string root = "/steps/Step-1/frames/" + std::to_string(index) + "/"; for (const auto& expected : datasets) { const std::string path = root + expected.path; SCOPED_TRACE(path); const auto values = ReadDoubles(file.Get(), path, expected.dimensions); Hdf5Handle dataset{H5Dopen2(file.Get(), path.c_str(), H5P_DEFAULT), H5Dclose}; EXPECT_EQ(ReadString(dataset.Get(), "component_names"), expected.components); EXPECT_FALSE( ReadString(dataset.Get(), "component_unit_dimensions").empty()); EXPECT_FALSE(ReadString(dataset.Get(), "coordinate_system").empty()); EXPECT_FALSE(ReadString(dataset.Get(), "location").empty()); EXPECT_EQ(ReadString(dataset.Get(), "step_name"), "Step-1"); EXPECT_EQ(ReadString(dataset.Get(), "source_step_name"), "Transient Load"); EXPECT_EQ(ReadAttribute(dataset.Get(), "frame_index", H5T_STD_U64LE, H5T_NATIVE_UINT64), index); EXPECT_EQ(ReadAttribute(dataset.Get(), "increment_index", H5T_STD_U64LE, H5T_NATIVE_UINT64), index); EXPECT_EQ(ReadAttribute(dataset.Get(), "is_initial", H5T_STD_U8LE, H5T_NATIVE_UINT8), index == 0U ? 1U : 0U); EXPECT_DOUBLE_EQ(ReadAttribute(dataset.Get(), "time", H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE), static_cast(index) * 0.25); EXPECT_EQ(H5Aexists(dataset.Get(), "load_factor"), 0); const std::string leaf{expected.path}; if (leaf == "nodal/triad" || leaf == "element/shell/local_frame") { for (std::size_t entry = 0U; entry < values.size(); ++entry) { EXPECT_DOUBLE_EQ( values[entry], entry % 9U == 0U || entry % 9U == 4U || entry % 9U == 8U ? 1.0 : 0.0); } } else if (leaf == "nodal/director") { for (std::size_t entry = 0U; entry < values.size(); ++entry) EXPECT_DOUBLE_EQ(values[entry], entry % 3U == 2U ? 1.0 : 0.0); } else { for (std::size_t entry = 0U; entry < values.size(); ++entry) { const bool force = entry == 2U && (leaf == "nodal/generalized_external_force" || leaf == "nodal/generalized_residual" || leaf == "nodal/generalized_reaction" || leaf == "nodal/reaction"); EXPECT_DOUBLE_EQ( values[entry], force ? (leaf == "nodal/generalized_external_force" ? 4.0 : -4.0) : 0.0); } } } } Hdf5Handle stress{ H5Dopen2(file.Get(), "/steps/Step-1/frames/2/element/shell/stress", H5P_DEFAULT), H5Dclose}; EXPECT_EQ(ReadString(stress.Get(), "source_element_type_dataset"), "/model/elements.source_element_type"); EXPECT_EQ(ReadString(stress.Get(), "midsurface_location_dataset"), "/model/shell/midsurface_locations"); EXPECT_EQ(ReadString(stress.Get(), "section_position_dataset"), "/model/shell/section_positions"); EXPECT_EQ(ReadString(stress.Get(), "local_frame_dataset"), "/steps/Step-1/frames/2/element/shell/local_frame"); Hdf5Handle diagnostic_table{H5Dopen2(file.Get(), "/diagnostics", H5P_DEFAULT), H5Dclose}; Hdf5Handle diagnostic_type{H5Dget_type(diagnostic_table.Get()), H5Tclose}; EXPECT_GE(H5Tget_member_index(diagnostic_type.Get(), "source_file"), 0); EXPECT_GE(H5Tget_member_index(diagnostic_type.Get(), "source_line"), 0); } TEST(DynamicHdf5ResultsWriter, PreservesSeparateSuppliedEndpointAndAuxiliaryEvidence) { auto fixture = MakeFixture(MakeDefinition(false)); ASSERT_TRUE( fixture.state.CommitAcceptedFrame(MakeFrame(*fixture.domain, 0U)).IsOk()); // This checks serialization of supplied endpoint evidence; it does not assert // that these records are a converged HHT trajectory or independent physics // proof. for (std::uint32_t index = 1U; index <= 2U; ++index) { auto frame = MakeFrame(*fixture.domain, index); frame.acceleration[0U] = 2.0; frame.algorithmic_acceleration[0U] = 3.0; frame.velocity[0U] = 4.0; frame.inertia_force[0U] = 5.0; frame.residual[0U] = 5.0; frame.residual_scale[0U] = 5.0; frame.kinetic_energy = 6.0; frame.physical_strain_energy = 7.0; frame.drilling_energy = 8.0; frame.external_work = 9.0; frame.energy_defect = 12.0; ASSERT_TRUE(fixture.state.CommitAcceptedFrame(frame).IsOk()); } TempFile output{"separate-accelerations"}; fesa::DynamicHdf5ResultsWriter writer; ASSERT_TRUE( writer.Write(output.Path(), *fixture.domain, fixture.state, {}).IsOk()); Hdf5Handle file{ H5Fopen(output.Path().string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT), H5Fclose}; const std::string root = "/steps/Step-1/frames/2/"; EXPECT_DOUBLE_EQ( ReadDoubles(file.Get(), root + "nodal/acceleration", {4U, 6U})[0U], 2.0); EXPECT_DOUBLE_EQ( ReadDoubles(file.Get(), root + "nodal/algorithmic_acceleration", {4U, 6U})[0U], 3.0); EXPECT_DOUBLE_EQ( ReadDoubles(file.Get(), root + "nodal/generalized_inertia_force", {4U, 6U})[0U], 5.0); EXPECT_EQ(ReadDoubles(file.Get(), root + "global/energy", {5U}), (std::vector{6.0, 7.0, 8.0, 9.0, 12.0})); Hdf5Handle acceleration{ H5Dopen2(file.Get(), (root + "nodal/acceleration").c_str(), H5P_DEFAULT), H5Dclose}; EXPECT_EQ(ReadString(acceleration.Get(), "rotation_representation"), "total-rotation-vector-chart"); EXPECT_EQ(ReadString(acceleration.Get(), "quantity_semantics"), "physical-endpoint-chart-acceleration"); } TEST(DynamicHdf5ResultsWriter, ConvertsPartialConstraintMomentToSpatialReaction) { auto definition = MakeDefinition(false); definition.dynamic_step->boundaries = {{"Loaded", 4, 4, 0.0, {}}}; auto fixture = MakeFixture(std::move(definition)); ASSERT_TRUE( fixture.state.CommitAcceptedFrame(MakeFrame(*fixture.domain, 0U)).IsOk()); // A supplied finite-chart endpoint tests the output coordinate transform, // independently of time-integration acceptance. for (std::uint32_t index = 1U; index <= 2U; ++index) { auto frame = MakeFrame(*fixture.domain, index); frame.generalized_coordinates[4U] = 0.2; frame.generalized_coordinates[5U] = 0.3; const auto rotation = fesa::ExpSkew(fesa::Vector3{0.0, 0.2, 0.3}); for (std::size_t axis = 0U; axis < 3U; ++axis) { for (std::size_t component = 0U; component < 3U; ++component) { frame.nodal_triad[0U][axis][component] = rotation(component, axis); } frame.nodal_director[0U][axis] = rotation(axis, 2U); } frame.internal_force[3U] = 5.0; frame.residual[3U] = 5.0; frame.reaction[3U] = 5.0; frame.residual_scale[3U] = 5.0; ASSERT_TRUE(fixture.state.CommitAcceptedFrame(frame).IsOk()); } TempFile output{"spatial-reaction"}; fesa::DynamicHdf5ResultsWriter writer; ASSERT_TRUE( writer.Write(output.Path(), *fixture.domain, fixture.state, {}).IsOk()); Hdf5Handle file{ H5Fopen(output.Path().string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT), H5Fclose}; const std::string root = "/steps/Step-1/frames/2/nodal/"; const auto generalized = ReadDoubles(file.Get(), root + "generalized_reaction", {4U, 6U}); const auto spatial = ReadDoubles(file.Get(), root + "reaction", {4U, 6U}); EXPECT_DOUBLE_EQ(generalized[3U], 5.0); EXPECT_DOUBLE_EQ(generalized[4U], 0.0); EXPECT_DOUBLE_EQ(generalized[5U], 0.0); const double angle = std::sqrt(0.13); const double coefficient = 1.0 / 0.13 - (1.0 + std::cos(angle)) / (2.0 * angle * std::sin(angle)); EXPECT_NEAR(spatial[3U], 5.0 * (1.0 - coefficient * 0.13), 1.0e-14); EXPECT_NEAR(spatial[4U], 0.75, 1.0e-14); EXPECT_NEAR(spatial[5U], -0.5, 1.0e-14); } TEST(DynamicHdf5ResultsWriter, IncompleteOrMismatchedHistoryPreservesExistingFile) { auto fixture = MakeFixture(); ASSERT_TRUE( fixture.state.CommitAcceptedFrame(MakeFrame(*fixture.domain, 0U)).IsOk()); TempFile output{"preservation"}; { std::ofstream existing{output.Path(), std::ios::binary}; existing << "previous-result"; } fesa::DynamicHdf5ResultsWriter writer; EXPECT_FALSE( writer.Write(output.Path(), *fixture.domain, fixture.state, {}).IsOk()); EXPECT_EQ(ReadBytes(output.Path()), "previous-result"); for (std::uint32_t index = 1U; index <= 2U; ++index) ASSERT_TRUE( fixture.state.CommitAcceptedFrame(MakeFrame(*fixture.domain, index)) .IsOk()); auto different_definition = MakeDefinition(); different_definition.nodes[0U].source_id.source_label_text = "10"; auto other = MakeFixture(std::move(different_definition)); EXPECT_FALSE( writer.Write(output.Path(), *other.domain, fixture.state, {}).IsOk()); EXPECT_EQ(ReadBytes(output.Path()), "previous-result"); ASSERT_TRUE( writer.Write(output.Path(), *fixture.domain, fixture.state, {}).IsOk()); const auto valid_bytes = ReadBytes(output.Path()); // Deliberate post-commit corruption proves writer revalidation is independent // of the state API that normally prevents these mutations. auto& corrupt = const_cast( fixture.state.CommittedState()); corrupt.reaction[2U] = 0.0; EXPECT_FALSE( writer.Write(output.Path(), *fixture.domain, fixture.state, {}).IsOk()); EXPECT_EQ(ReadBytes(output.Path()), valid_bytes); } TEST(DynamicHdf5ResultsWriter, FailedAtomicReplacementPreservesPreviousBytes) { auto fixture = MakeFixture(); CommitHistory(fixture); TempFile output{"locked"}; { std::ofstream existing{output.Path(), std::ios::binary}; existing << "locked-previous-result"; } const HANDLE locked = CreateFileW(output.Path().c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); ASSERT_NE(locked, INVALID_HANDLE_VALUE); fesa::DynamicHdf5ResultsWriter writer; const auto status = writer.Write(output.Path(), *fixture.domain, fixture.state, {}); EXPECT_FALSE(status.IsOk()); EXPECT_TRUE(CloseHandle(locked)); ASSERT_EQ(status.Diagnostics().size(), 1U); EXPECT_EQ(status.Diagnostics()[0U].code, "hdf5-finalization-failure"); EXPECT_EQ(ReadBytes(output.Path()), "locked-previous-result"); } } // namespace