#define NOMINMAX #include "fesa/io/hdf5/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/analysis_state.h" #include "fesa/build_info.h" #include "fesa/fem/dof_manager.h" #include "fesa/math/vector3.h" #include "fesa/model/domain.h" namespace { constexpr const char* kStepRoot = "/steps/Step-1/frames/0"; class Hdf5Handle { public: using Closer = herr_t (*)(hid_t); Hdf5Handle() = default; Hdf5Handle(const hid_t value, Closer closer) : value_{value}, closer_{closer} {} Hdf5Handle(const Hdf5Handle&) = delete; Hdf5Handle& operator=(const Hdf5Handle&) = delete; Hdf5Handle(Hdf5Handle&& other) noexcept : value_{other.value_}, closer_{other.closer_} { other.value_ = -1; other.closer_ = nullptr; } Hdf5Handle& operator=(Hdf5Handle&& other) noexcept { if (this != &other) { Reset(); value_ = other.value_; closer_ = other.closer_; other.value_ = -1; other.closer_ = nullptr; } return *this; } ~Hdf5Handle() { Reset(); } hid_t Get() const noexcept { return value_; } private: void Reset() noexcept { if (value_ >= 0 && closer_ != nullptr) { (void)closer_(value_); } value_ = -1; closer_ = nullptr; } hid_t value_{-1}; Closer closer_{nullptr}; }; class WinHandle { public: explicit WinHandle(HANDLE value) : value_{value} {} WinHandle(const WinHandle&) = delete; WinHandle& operator=(const WinHandle&) = delete; ~WinHandle() { if (value_ != INVALID_HANDLE_VALUE) { (void)CloseHandle(value_); } } HANDLE Get() const noexcept { return value_; } private: HANDLE value_{INVALID_HANDLE_VALUE}; }; class TempDirectory { public: explicit TempDirectory(const std::string& label) { static std::atomic sequence{0U}; path_ = std::filesystem::temp_directory_path() / ("fesa-step23-" + label + "-" + std::to_string(GetCurrentProcessId()) + "-" + std::to_string(sequence.fetch_add(1U))); std::error_code error; if (!std::filesystem::create_directory(path_, error) || error) { throw std::runtime_error{"Unable to create the Step 23 test directory."}; } } TempDirectory(const TempDirectory&) = delete; TempDirectory& operator=(const TempDirectory&) = delete; ~TempDirectory() { std::error_code ignored; std::filesystem::remove_all(path_, ignored); } const std::filesystem::path& Path() const noexcept { return path_; } private: std::filesystem::path path_; }; struct WriterFixture { std::unique_ptr domain; std::unique_ptr dofs; std::unique_ptr state; }; fesa::ModelDefinition MakeDefinition(const std::filesystem::path& source, const bool use_default_centroid) { fesa::ModelDefinition definition{}; definition.source_path = source; definition.source_content_identity = "fnv1a64:0123456789abcdef"; definition.nodes = { {{u8"Beam-\u03b1", 101, "101"}, {0.0, 0.0, 0.0}, {source, 10U}}, {{u8"Beam-\u03b1", 202, "202"}, {3.0, 4.0, 0.0}, {source, 11U}}}; definition.materials = {{"Steel", 210.0e9, 0.3, {source, 20U}}}; definition.sections = { {"General", 0.02, 3.0e-5, 0.0, 4.0e-5, 5.0e-5, {0.0, 0.0, 1.0}, use_default_centroid ? std::vector>{} : std::vector>{{{-0.1, 0.2}, {0.3, -0.4}}}, {source, 30U}}}; definition.elements = { {{u8"Beam-\u03b1", 303, "303"}, {0U, 1U}, 0U, 0U, {source, 40U}}}; definition.steps = {{"Step-1", {}, {}, 0.1, 1.0, 0.01, 1.0, {source, 50U}}}; return definition; } WriterFixture MakeFixture(const std::filesystem::path& source, const bool use_default_centroid = false) { auto domain_result = fesa::Domain::Create(MakeDefinition(source, use_default_centroid)); if (!domain_result.HasValue()) { throw std::runtime_error{"Writer fixture Domain construction failed."}; } 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{ "Writer fixture AnalysisModel construction failed."}; } const fesa::AnalysisModel model = std::move(model_result.Value()); auto dofs_result = fesa::DofManager::Create(model); if (!dofs_result.HasValue()) { throw std::runtime_error{"Writer fixture DofManager construction failed."}; } auto dofs = std::make_unique(std::move(dofs_result.Value())); auto state = std::make_unique( fesa::AnalysisState::Create(*dofs, {"Step-1", 0U})); for (std::size_t index = 0U; index < state->Displacement().Size(); ++index) { state->Displacement()[index] = 0.25 + static_cast(index); state->ExternalForce()[index] = 100.0 + static_cast(index); state->InternalForce()[index] = 200.0 + 2.0 * static_cast(index); state->Residual()[index] = 100.0 + static_cast(index); state->Reaction()[index] = 100.0 + static_cast(index); } const auto& nodes = domain->Nodes(); state->EndpointResults() = {{0U, 0, nodes[0U].source_id, {1.0, 2.0, 3.0, 4.0, 5.0, 6.0}, {11.0, 12.0, 13.0, 14.0}}, {0U, 1, nodes[1U].source_id, {7.0, 8.0, 9.0, 10.0, 11.0, 12.0}, {15.0, 16.0, 17.0, 18.0}}}; state->GaussResults() = { {0U, 1, {0.01, 0.02, 0.03, 0.04}, {21.0, 22.0, 23.0, 24.0}}, {0U, 2, {0.05, 0.06, 0.07, 0.08}, {25.0, 26.0, 27.0, 28.0}}}; if (use_default_centroid) { state->StressResults() = {{0U, 1, 0U, 0.0, 0.0, 31.0, "fesa-default"}, {0U, 2, 0U, 0.0, 0.0, 32.0, "fesa-default"}}; } else { state->StressResults() = {{0U, 1, 1U, -0.1, 0.2, 31.0, "input"}, {0U, 1, 2U, 0.3, -0.4, 32.0, "input"}, {0U, 2, 1U, -0.1, 0.2, 33.0, "input"}, {0U, 2, 2U, 0.3, -0.4, 34.0, "input"}}; } return {std::move(domain), std::move(dofs), std::move(state)}; } fesa::ModelDefinition MakeShellDefinition(const std::filesystem::path& source) { fesa::ModelDefinition definition{}; definition.source_path = source; definition.source_content_identity = "fnv1a64:fedcba9876543210"; definition.nodes = {{{"Shell-1", 11, "11"}, {-1.0, -1.0, 0.0}, {source, 10U}}, {{"Shell-1", 12, "12"}, {1.0, -1.0, 0.0}, {source, 11U}}, {{"Shell-1", 13, "13"}, {1.0, 1.0, 0.0}, {source, 12U}}, {{"Shell-1", 14, "14"}, {-1.0, 1.0, 0.0}, {source, 13U}}}; definition.materials = {{"ShellSteel", 210.0e9, 0.3, {source, 20U}}}; definition.shell_sections = {{"PlateSet", 0.02, 0U, {source, 30U}}}; definition.shell_elements = {{{"Shell-1", 401, "401"}, fesa::ShellSourceElementType::kS4r, {0U, 1U, 2U, 3U}, 0U, 0U, {source, 40U}}}; 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.node_sets = {{"Fixed", {}, {0U}, {source, 50U}}}; definition.steps = {{"Step-1", {{"Fixed", 1, 3, 0.0, {source, 60U}}, {"Fixed", 4, 4, 0.125, {source, 61U}}}, {}, 0.1, 1.0, 0.01, 1.0, {source, 59U}}}; return definition; } WriterFixture MakeShellFixture(const std::filesystem::path& source) { auto domain_result = fesa::Domain::Create(MakeShellDefinition(source)); if (!domain_result.HasValue()) { throw std::runtime_error{ "Shell writer fixture Domain construction failed."}; } 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{ "Shell writer fixture AnalysisModel construction failed."}; } const fesa::AnalysisModel model = std::move(model_result.Value()); auto dofs_result = fesa::DofManager::Create(model); if (!dofs_result.HasValue()) { throw std::runtime_error{ "Shell writer fixture DofManager construction failed."}; } auto dofs = std::make_unique(std::move(dofs_result.Value())); auto state = std::make_unique( fesa::AnalysisState::Create(*dofs, {"Step-1", 0U})); for (std::size_t index = 0U; index < state->Displacement().Size(); ++index) { state->Displacement()[index] = 0.01 * static_cast(index + 1U); state->ExternalForce()[index] = 10.0 + static_cast(index); state->InternalForce()[index] = 20.0 + static_cast(index); state->Residual()[index] = 30.0 + static_cast(index); state->Reaction()[index] = 40.0 + static_cast(index); } const double gauss = 1.0 / std::sqrt(3.0); const std::array, 4> coordinates{ {{-gauss, -gauss}, {gauss, -gauss}, {gauss, gauss}, {-gauss, gauss}}}; const std::array locations{ fesa::ShellMidsurfaceLocation::kGp1, fesa::ShellMidsurfaceLocation::kGp2, fesa::ShellMidsurfaceLocation::kGp3, fesa::ShellMidsurfaceLocation::kGp4}; fesa::ShellStateCandidate candidate{}; for (std::size_t point = 0U; point < locations.size(); ++point) { const double base = 100.0 * static_cast(point + 1U); candidate.rows.push_back( {0U, locations[point], coordinates[point], {{{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}}, {base + 1.0, base + 2.0, base + 3.0, base + 4.0, base + 5.0, base + 6.0, base + 7.0, base + 8.0}, {base + 11.0, base + 12.0, base + 13.0, base + 14.0, base + 15.0, base + 16.0, base + 17.0, base + 18.0}, {{{fesa::ShellSectionPosition::kBottom, -1.0, {base + 21.0, base + 22.0, base + 23.0}}, {fesa::ShellSectionPosition::kMiddle, 0.0, {base + 24.0, base + 25.0, base + 26.0}}, {fesa::ShellSectionPosition::kTop, 1.0, {base + 27.0, base + 28.0, base + 29.0}}}}}); } 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}; const fesa::Status commit = state->CommitShellResults({0U}, std::move(candidate)); if (!commit.IsOk()) { throw std::runtime_error{"Shell writer fixture state commit failed."}; } return {std::move(domain), std::move(dofs), std::move(state)}; } Hdf5Handle OpenFile(const std::filesystem::path& path) { const hid_t file = H5Fopen(path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT); if (file < 0) { throw std::runtime_error{"Unable to open test HDF5 output."}; } return Hdf5Handle{file, H5Fclose}; } Hdf5Handle OpenDataset(const hid_t file, const std::string& path) { const hid_t dataset = H5Dopen2(file, path.c_str(), H5P_DEFAULT); if (dataset < 0) { throw std::runtime_error{"Unable to open expected HDF5 dataset: " + path}; } return Hdf5Handle{dataset, 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}; if (space.Get() < 0) { throw std::runtime_error{"Unable to inspect HDF5 dataspace."}; } const int rank = H5Sget_simple_extent_ndims(space.Get()); if (rank < 0) { throw std::runtime_error{"Unable to inspect HDF5 rank."}; } std::vector dimensions(static_cast(rank)); if (rank > 0 && H5Sget_simple_extent_dims(space.Get(), dimensions.data(), nullptr) < 0) { throw std::runtime_error{"Unable to inspect HDF5 dimensions."}; } return dimensions; } std::vector ReadDoubleDataset(const hid_t file, const std::string& path) { const auto dimensions = DatasetDimensions(file, path); std::size_t value_count = 1U; for (const hsize_t dimension : dimensions) { value_count *= static_cast(dimension); } const auto dataset = OpenDataset(file, path); std::vector values(value_count); if (!values.empty() && H5Dread(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data()) < 0) { throw std::runtime_error{"Unable to read numeric HDF5 dataset."}; } return values; } std::vector ReadUint8Dataset(const hid_t file, const std::string& path) { const auto dimensions = DatasetDimensions(file, path); std::size_t value_count = 1U; for (const hsize_t dimension : dimensions) { value_count *= static_cast(dimension); } const auto dataset = OpenDataset(file, path); Hdf5Handle type{H5Dget_type(dataset.Get()), H5Tclose}; if (type.Get() < 0 || H5Tget_class(type.Get()) != H5T_INTEGER || H5Tget_size(type.Get()) != sizeof(std::uint8_t) || H5Tget_sign(type.Get()) != H5T_SGN_NONE || H5Tequal(type.Get(), H5T_STD_U8LE) <= 0) { throw std::runtime_error{"Expected a portable uint8 HDF5 dataset."}; } std::vector values(value_count); if (!values.empty() && H5Dread(dataset.Get(), H5T_NATIVE_UINT8, H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data()) < 0) { throw std::runtime_error{"Unable to read uint8 HDF5 dataset."}; } return values; } 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}; if (attribute.Get() < 0 || type.Get() < 0 || H5Tget_class(type.Get()) != H5T_STRING || H5Tis_variable_str(type.Get()) <= 0 || H5Tget_cset(type.Get()) != H5T_CSET_UTF8) { throw std::runtime_error{"Expected a variable-length UTF-8 attribute."}; } char* raw = nullptr; if (H5Aread(attribute.Get(), type.Get(), &raw) < 0 || raw == nullptr) { throw std::runtime_error{"Unable to read UTF-8 HDF5 attribute."}; } const std::string value{raw}; (void)H5free_memory(raw); return value; } std::uint64_t ReadUint64Attribute(const hid_t object, const char* name) { Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose}; Hdf5Handle type{H5Aget_type(attribute.Get()), H5Tclose}; if (attribute.Get() < 0 || type.Get() < 0 || H5Tget_class(type.Get()) != H5T_INTEGER || H5Tget_size(type.Get()) != sizeof(std::uint64_t) || H5Tget_sign(type.Get()) != H5T_SGN_NONE || H5Tequal(type.Get(), H5T_STD_U64LE) <= 0) { throw std::runtime_error{"Expected a portable uint64 HDF5 attribute."}; } std::uint64_t value = 0U; if (H5Aread(attribute.Get(), H5T_NATIVE_UINT64, &value) < 0) { throw std::runtime_error{"Unable to read uint64 HDF5 attribute."}; } return value; } void ExpectPortableCompoundMember(const hid_t compound_type, const unsigned index, const std::string& name) { Hdf5Handle member_type{H5Tget_member_type(compound_type, index), H5Tclose}; ASSERT_GE(member_type.Get(), 0); const bool is_uint64 = name == "internal_node_id" || name == "internal_element_id" || name == "gauss_point_index" || name == "section_point_index" || name == "line"; const bool is_float64 = name == "x1" || name == "x2" || name == "S11"; const bool is_string = name == "instance_name" || name == "source_label" || name == "source" || name == "severity" || name == "code" || name == "file" || name == "keyword" || name == "entity_identity" || name == "message"; if (is_uint64) { EXPECT_EQ(H5Tget_class(member_type.Get()), H5T_INTEGER); EXPECT_EQ(H5Tget_size(member_type.Get()), sizeof(std::uint64_t)); EXPECT_EQ(H5Tget_sign(member_type.Get()), H5T_SGN_NONE); EXPECT_GT(H5Tequal(member_type.Get(), H5T_STD_U64LE), 0); return; } if (is_float64) { EXPECT_EQ(H5Tget_class(member_type.Get()), H5T_FLOAT); EXPECT_EQ(H5Tget_size(member_type.Get()), sizeof(double)); EXPECT_GT(H5Tequal(member_type.Get(), H5T_IEEE_F64LE), 0); return; } if (is_string) { EXPECT_EQ(H5Tget_class(member_type.Get()), H5T_STRING); EXPECT_GT(H5Tis_variable_str(member_type.Get()), 0); EXPECT_EQ(H5Tget_cset(member_type.Get()), H5T_CSET_UTF8); return; } ASSERT_EQ(H5Tget_class(member_type.Get()), H5T_ARRAY); const int rank = H5Tget_array_ndims(member_type.Get()); ASSERT_GT(rank, 0); std::vector dimensions(static_cast(rank)); ASSERT_GE(H5Tget_array_dims2(member_type.Get(), dimensions.data()), 0); Hdf5Handle base_type{H5Tget_super(member_type.Get()), H5Tclose}; ASSERT_GE(base_type.Get(), 0); if (name == "node_internal_ids") { EXPECT_EQ(dimensions, std::vector({2U})); EXPECT_GT(H5Tequal(base_type.Get(), H5T_STD_U64LE), 0); } else if (name == "coordinates") { EXPECT_EQ(dimensions, std::vector({3U})); EXPECT_GT(H5Tequal(base_type.Get(), H5T_IEEE_F64LE), 0); } else { EXPECT_EQ(name, "local_axes"); EXPECT_EQ(dimensions, std::vector({3U, 3U})); EXPECT_GT(H5Tequal(base_type.Get(), H5T_IEEE_F64LE), 0); } } void ExpectCompoundMembers(const hid_t file, const std::string& path, const std::vector& expected_names) { const auto dataset = OpenDataset(file, path); Hdf5Handle type{H5Dget_type(dataset.Get()), H5Tclose}; ASSERT_EQ(H5Tget_class(type.Get()), H5T_COMPOUND); ASSERT_EQ(H5Tget_nmembers(type.Get()), static_cast(expected_names.size())); for (std::size_t index = 0U; index < expected_names.size(); ++index) { char* raw_name = H5Tget_member_name(type.Get(), static_cast(index)); ASSERT_NE(raw_name, nullptr); const std::string actual_name{raw_name}; (void)H5free_memory(raw_name); EXPECT_EQ(actual_name, expected_names[index]); ExpectPortableCompoundMember(type.Get(), static_cast(index), expected_names[index]); } } void ExpectCompoundMemberNames(const hid_t file, const std::string& path, const std::vector& expected_names) { const auto dataset = OpenDataset(file, path); Hdf5Handle type{H5Dget_type(dataset.Get()), H5Tclose}; ASSERT_EQ(H5Tget_class(type.Get()), H5T_COMPOUND); ASSERT_EQ(H5Tget_nmembers(type.Get()), static_cast(expected_names.size())); for (std::size_t index = 0U; index < expected_names.size(); ++index) { char* raw_name = H5Tget_member_name(type.Get(), static_cast(index)); ASSERT_NE(raw_name, nullptr); const std::string actual_name{raw_name}; (void)H5free_memory(raw_name); EXPECT_EQ(actual_name, expected_names[index]); } } void ExpectNumericDataset(const hid_t file, const std::string& path, const std::vector& dimensions, const std::string& components, const std::string& units, const std::string& coordinate_system, const std::string& location) { EXPECT_EQ(DatasetDimensions(file, path), dimensions); const auto dataset = OpenDataset(file, path); Hdf5Handle type{H5Dget_type(dataset.Get()), H5Tclose}; ASSERT_EQ(H5Tget_class(type.Get()), H5T_FLOAT); EXPECT_EQ(H5Tget_size(type.Get()), 8U); EXPECT_GT(H5Tequal(type.Get(), H5T_IEEE_F64LE), 0); EXPECT_EQ(ReadStringAttribute(dataset.Get(), "component_names"), components); EXPECT_EQ(ReadStringAttribute(dataset.Get(), "component_unit_dimensions"), units); EXPECT_EQ(ReadStringAttribute(dataset.Get(), "coordinate_system"), coordinate_system); EXPECT_EQ(ReadStringAttribute(dataset.Get(), "location"), location); EXPECT_EQ(ReadStringAttribute(dataset.Get(), "step_name"), "Step-1"); EXPECT_EQ(ReadUint64Attribute(dataset.Get(), "frame_index"), 0U); } Hdf5Handle MakeUtf8StringType() { Hdf5Handle type{H5Tcopy(H5T_C_S1), H5Tclose}; if (type.Get() < 0 || H5Tset_size(type.Get(), H5T_VARIABLE) < 0 || H5Tset_cset(type.Get(), H5T_CSET_UTF8) < 0) { throw std::runtime_error{"Unable to create a test UTF-8 memory type."}; } return type; } struct NodeReadRow { std::uint64_t internal_node_id; char* instance_name; char* source_label; double coordinates[3]; }; std::vector ReadNodeRows(const hid_t file) { const auto dataset = OpenDataset(file, "/model/nodes"); Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose}; auto string_type = MakeUtf8StringType(); const hsize_t coordinate_dimensions[] = {3U}; Hdf5Handle coordinates_type{ H5Tarray_create2(H5T_NATIVE_DOUBLE, 1, coordinate_dimensions), H5Tclose}; Hdf5Handle memory_type{H5Tcreate(H5T_COMPOUND, sizeof(NodeReadRow)), H5Tclose}; if (H5Tinsert(memory_type.Get(), "internal_node_id", HOFFSET(NodeReadRow, internal_node_id), H5T_NATIVE_UINT64) < 0 || H5Tinsert(memory_type.Get(), "instance_name", HOFFSET(NodeReadRow, instance_name), string_type.Get()) < 0 || H5Tinsert(memory_type.Get(), "source_label", HOFFSET(NodeReadRow, source_label), string_type.Get()) < 0 || H5Tinsert(memory_type.Get(), "coordinates", HOFFSET(NodeReadRow, coordinates), coordinates_type.Get()) < 0) { throw std::runtime_error{"Unable to create the node memory type."}; } std::vector rows(2U); if (H5Dread(dataset.Get(), memory_type.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, rows.data()) < 0) { throw std::runtime_error{"Unable to read node rows."}; } return rows; } void ReclaimNodeRows(const hid_t file, std::vector& rows) { const auto dataset = OpenDataset(file, "/model/nodes"); Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose}; auto string_type = MakeUtf8StringType(); const hsize_t coordinate_dimensions[] = {3U}; Hdf5Handle coordinates_type{ H5Tarray_create2(H5T_NATIVE_DOUBLE, 1, coordinate_dimensions), H5Tclose}; Hdf5Handle memory_type{H5Tcreate(H5T_COMPOUND, sizeof(NodeReadRow)), H5Tclose}; (void)H5Tinsert(memory_type.Get(), "internal_node_id", HOFFSET(NodeReadRow, internal_node_id), H5T_NATIVE_UINT64); (void)H5Tinsert(memory_type.Get(), "instance_name", HOFFSET(NodeReadRow, instance_name), string_type.Get()); (void)H5Tinsert(memory_type.Get(), "source_label", HOFFSET(NodeReadRow, source_label), string_type.Get()); (void)H5Tinsert(memory_type.Get(), "coordinates", HOFFSET(NodeReadRow, coordinates), coordinates_type.Get()); (void)H5Dvlen_reclaim(memory_type.Get(), space.Get(), H5P_DEFAULT, rows.data()); } struct ElementReadRow { std::uint64_t internal_element_id; char* instance_name; char* source_label; std::uint64_t node_internal_ids[2]; double local_axes[9]; }; std::vector ReadElementRows(const hid_t file) { const auto dataset = OpenDataset(file, "/model/elements"); auto string_type = MakeUtf8StringType(); const hsize_t node_dimensions[] = {2U}; const hsize_t axes_dimensions[] = {3U, 3U}; Hdf5Handle node_type{H5Tarray_create2(H5T_NATIVE_UINT64, 1, node_dimensions), H5Tclose}; Hdf5Handle axes_type{H5Tarray_create2(H5T_NATIVE_DOUBLE, 2, axes_dimensions), H5Tclose}; Hdf5Handle memory_type{H5Tcreate(H5T_COMPOUND, sizeof(ElementReadRow)), H5Tclose}; (void)H5Tinsert(memory_type.Get(), "internal_element_id", HOFFSET(ElementReadRow, internal_element_id), H5T_NATIVE_UINT64); (void)H5Tinsert(memory_type.Get(), "instance_name", HOFFSET(ElementReadRow, instance_name), string_type.Get()); (void)H5Tinsert(memory_type.Get(), "source_label", HOFFSET(ElementReadRow, source_label), string_type.Get()); (void)H5Tinsert(memory_type.Get(), "node_internal_ids", HOFFSET(ElementReadRow, node_internal_ids), node_type.Get()); (void)H5Tinsert(memory_type.Get(), "local_axes", HOFFSET(ElementReadRow, local_axes), axes_type.Get()); std::vector rows(1U); if (H5Dread(dataset.Get(), memory_type.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, rows.data()) < 0) { throw std::runtime_error{"Unable to read element rows."}; } Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose}; EXPECT_EQ(rows[0U].internal_element_id, 0U); EXPECT_STREQ(rows[0U].instance_name, u8"Beam-\u03b1"); EXPECT_STREQ(rows[0U].source_label, "303"); EXPECT_EQ(rows[0U].node_internal_ids[0U], 0U); EXPECT_EQ(rows[0U].node_internal_ids[1U], 1U); const std::array expected_axes = {0.6, 0.8, 0.0, 0.0, 0.0, 1.0, 0.8, -0.6, 0.0}; for (std::size_t index = 0U; index < expected_axes.size(); ++index) { EXPECT_NEAR(rows[0U].local_axes[index], expected_axes[index], 1.0e-15); } (void)H5Dvlen_reclaim(memory_type.Get(), space.Get(), H5P_DEFAULT, rows.data()); return rows; } struct StressReadRow { std::uint64_t internal_element_id; std::uint64_t gauss_point_index; std::uint64_t section_point_index; double x1; double x2; char* source; double s11; }; std::vector ReadStressRows(const hid_t file) { const std::string path = std::string{kStepRoot} + "/element/stress_s11"; const auto dataset = OpenDataset(file, path); const auto dimensions = DatasetDimensions(file, path); auto string_type = MakeUtf8StringType(); Hdf5Handle memory_type{H5Tcreate(H5T_COMPOUND, sizeof(StressReadRow)), H5Tclose}; (void)H5Tinsert(memory_type.Get(), "internal_element_id", HOFFSET(StressReadRow, internal_element_id), H5T_NATIVE_UINT64); (void)H5Tinsert(memory_type.Get(), "gauss_point_index", HOFFSET(StressReadRow, gauss_point_index), H5T_NATIVE_UINT64); (void)H5Tinsert(memory_type.Get(), "section_point_index", HOFFSET(StressReadRow, section_point_index), H5T_NATIVE_UINT64); (void)H5Tinsert(memory_type.Get(), "x1", HOFFSET(StressReadRow, x1), H5T_NATIVE_DOUBLE); (void)H5Tinsert(memory_type.Get(), "x2", HOFFSET(StressReadRow, x2), H5T_NATIVE_DOUBLE); (void)H5Tinsert(memory_type.Get(), "source", HOFFSET(StressReadRow, source), string_type.Get()); (void)H5Tinsert(memory_type.Get(), "S11", HOFFSET(StressReadRow, s11), H5T_NATIVE_DOUBLE); std::vector rows( dimensions.empty() ? 0U : static_cast(dimensions[0U])); if (!rows.empty() && H5Dread(dataset.Get(), memory_type.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, rows.data()) < 0) { throw std::runtime_error{"Unable to read stress rows."}; } return rows; } void ReclaimStressRows(const hid_t file, std::vector& rows) { const std::string path = std::string{kStepRoot} + "/element/stress_s11"; const auto dataset = OpenDataset(file, path); Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose}; auto string_type = MakeUtf8StringType(); Hdf5Handle memory_type{H5Tcreate(H5T_COMPOUND, sizeof(StressReadRow)), H5Tclose}; (void)H5Tinsert(memory_type.Get(), "internal_element_id", HOFFSET(StressReadRow, internal_element_id), H5T_NATIVE_UINT64); (void)H5Tinsert(memory_type.Get(), "gauss_point_index", HOFFSET(StressReadRow, gauss_point_index), H5T_NATIVE_UINT64); (void)H5Tinsert(memory_type.Get(), "section_point_index", HOFFSET(StressReadRow, section_point_index), H5T_NATIVE_UINT64); (void)H5Tinsert(memory_type.Get(), "x1", HOFFSET(StressReadRow, x1), H5T_NATIVE_DOUBLE); (void)H5Tinsert(memory_type.Get(), "x2", HOFFSET(StressReadRow, x2), H5T_NATIVE_DOUBLE); (void)H5Tinsert(memory_type.Get(), "source", HOFFSET(StressReadRow, source), string_type.Get()); (void)H5Tinsert(memory_type.Get(), "S11", HOFFSET(StressReadRow, s11), H5T_NATIVE_DOUBLE); if (!rows.empty()) { (void)H5Dvlen_reclaim(memory_type.Get(), space.Get(), H5P_DEFAULT, rows.data()); } } struct DiagnosticReadRow { char* severity; char* code; char* file; std::uint64_t line; char* keyword; char* entity_identity; char* message; }; std::vector ReadDiagnosticRows(const hid_t file) { const auto dataset = OpenDataset(file, "/diagnostics"); const auto dimensions = DatasetDimensions(file, "/diagnostics"); auto string_type = MakeUtf8StringType(); Hdf5Handle memory_type{H5Tcreate(H5T_COMPOUND, sizeof(DiagnosticReadRow)), H5Tclose}; (void)H5Tinsert(memory_type.Get(), "severity", HOFFSET(DiagnosticReadRow, severity), string_type.Get()); (void)H5Tinsert(memory_type.Get(), "code", HOFFSET(DiagnosticReadRow, code), string_type.Get()); (void)H5Tinsert(memory_type.Get(), "file", HOFFSET(DiagnosticReadRow, file), string_type.Get()); (void)H5Tinsert(memory_type.Get(), "line", HOFFSET(DiagnosticReadRow, line), H5T_NATIVE_UINT64); (void)H5Tinsert(memory_type.Get(), "keyword", HOFFSET(DiagnosticReadRow, keyword), string_type.Get()); (void)H5Tinsert(memory_type.Get(), "entity_identity", HOFFSET(DiagnosticReadRow, entity_identity), string_type.Get()); (void)H5Tinsert(memory_type.Get(), "message", HOFFSET(DiagnosticReadRow, message), string_type.Get()); std::vector rows( dimensions.empty() ? 0U : static_cast(dimensions[0U])); if (!rows.empty() && H5Dread(dataset.Get(), memory_type.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, rows.data()) < 0) { throw std::runtime_error{"Unable to read diagnostic rows."}; } return rows; } void ReclaimDiagnosticRows(const hid_t file, std::vector& rows) { const auto dataset = OpenDataset(file, "/diagnostics"); Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose}; auto string_type = MakeUtf8StringType(); Hdf5Handle memory_type{H5Tcreate(H5T_COMPOUND, sizeof(DiagnosticReadRow)), H5Tclose}; (void)H5Tinsert(memory_type.Get(), "severity", HOFFSET(DiagnosticReadRow, severity), string_type.Get()); (void)H5Tinsert(memory_type.Get(), "code", HOFFSET(DiagnosticReadRow, code), string_type.Get()); (void)H5Tinsert(memory_type.Get(), "file", HOFFSET(DiagnosticReadRow, file), string_type.Get()); (void)H5Tinsert(memory_type.Get(), "line", HOFFSET(DiagnosticReadRow, line), H5T_NATIVE_UINT64); (void)H5Tinsert(memory_type.Get(), "keyword", HOFFSET(DiagnosticReadRow, keyword), string_type.Get()); (void)H5Tinsert(memory_type.Get(), "entity_identity", HOFFSET(DiagnosticReadRow, entity_identity), string_type.Get()); (void)H5Tinsert(memory_type.Get(), "message", HOFFSET(DiagnosticReadRow, message), string_type.Get()); if (!rows.empty()) { (void)H5Dvlen_reclaim(memory_type.Get(), space.Get(), H5P_DEFAULT, rows.data()); } } 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 atomicity sentinel bytes."}; } } std::size_t EntryCount(const std::filesystem::path& directory) { return static_cast( std::distance(std::filesystem::directory_iterator{directory}, std::filesystem::directory_iterator{})); } void ExpectOutputFailure(const fesa::Status& status, const std::string& expected_code) { ASSERT_FALSE(status.IsOk()); EXPECT_EQ(status.Category(), fesa::FailureCategory::kOutput); ASSERT_EQ(status.Diagnostics().size(), 1U); EXPECT_EQ(status.Diagnostics()[0U].severity, fesa::Severity::kError); EXPECT_EQ(status.Diagnostics()[0U].code, expected_code); } } // namespace TEST(Hdf5ResultsWriter, WritesExactSchemaShapesAttributesAndIdentity) { TempDirectory directory{"schema"}; const auto source = directory.Path() / "model.inp"; auto fixture = MakeFixture(source); const auto output = directory.Path() / "results.h5"; fesa::Hdf5ResultsWriter writer; ASSERT_TRUE(writer.Write(output, *fixture.domain, *fixture.state, {}).IsOk()); ASSERT_GT(H5Fis_hdf5(output.string().c_str()), 0); const auto file = OpenFile(output); for (const char* path : {"/metadata", "/model/nodes", "/model/elements", "/steps/Step-1/frames/0/nodal/displacement", "/steps/Step-1/frames/0/nodal/reaction", "/steps/Step-1/frames/0/element/end_force_local", "/steps/Step-1/frames/0/element/section_resultant", "/steps/Step-1/frames/0/element/generalized_strain", "/steps/Step-1/frames/0/element/generalized_resultant", "/steps/Step-1/frames/0/element/stress_s11", "/diagnostics"}) { EXPECT_GT(H5Lexists(file.Get(), path, H5P_DEFAULT), 0) << path; } Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose}; ASSERT_GE(metadata.Get(), 0); EXPECT_EQ(ReadUint64Attribute(metadata.Get(), "schema_version"), 0U); EXPECT_EQ(ReadStringAttribute(metadata.Get(), "feature_id"), "linear-static-3d-euler-beam"); EXPECT_EQ(ReadStringAttribute(metadata.Get(), "solver_version"), std::string{fesa::SolverVersion()}); const std::string normalized_source = std::filesystem::absolute(source).lexically_normal().generic_u8string(); EXPECT_EQ(ReadStringAttribute(metadata.Get(), "source_input_identity"), "path=" + normalized_source + ";content_identity=fnv1a64:0123456789abcdef"); EXPECT_EQ(ReadStringAttribute(metadata.Get(), "unit_system_label"), "user-consistent-unspecified"); EXPECT_EQ(ReadStringAttribute(metadata.Get(), "coordinate_convention"), "global-cartesian; beam-local=(t,n1,t-cross-n1)"); EXPECT_EQ(ReadStringAttribute(metadata.Get(), "element_formulation"), "B33-3D-Euler-Bernoulli"); EXPECT_EQ(ReadStringAttribute(metadata.Get(), "step_name"), "Step-1"); EXPECT_EQ(ReadUint64Attribute(metadata.Get(), "frame_index"), 0U); EXPECT_EQ(DatasetDimensions(file.Get(), "/model/nodes"), std::vector({2U})); ExpectCompoundMembers( file.Get(), "/model/nodes", {"internal_node_id", "instance_name", "source_label", "coordinates"}); const auto nodes_dataset = OpenDataset(file.Get(), "/model/nodes"); EXPECT_EQ(ReadStringAttribute(nodes_dataset.Get(), "coordinate_system"), "global-cartesian"); EXPECT_EQ(ReadStringAttribute(nodes_dataset.Get(), "units_label"), "length"); auto nodes = ReadNodeRows(file.Get()); ASSERT_EQ(nodes.size(), 2U); EXPECT_EQ(nodes[0U].internal_node_id, 0U); EXPECT_STREQ(nodes[0U].instance_name, u8"Beam-\u03b1"); EXPECT_STREQ(nodes[0U].source_label, "101"); EXPECT_DOUBLE_EQ(nodes[1U].coordinates[0U], 3.0); EXPECT_DOUBLE_EQ(nodes[1U].coordinates[1U], 4.0); ReclaimNodeRows(file.Get(), nodes); EXPECT_EQ(DatasetDimensions(file.Get(), "/model/elements"), std::vector({1U})); ExpectCompoundMembers(file.Get(), "/model/elements", {"internal_element_id", "instance_name", "source_label", "node_internal_ids", "local_axes"}); const auto elements_dataset = OpenDataset(file.Get(), "/model/elements"); EXPECT_EQ(ReadStringAttribute(elements_dataset.Get(), "formulation"), "B33-3D-Euler-Bernoulli"); (void)ReadElementRows(file.Get()); ExpectNumericDataset( file.Get(), std::string{kStepRoot} + "/nodal/displacement", {2U, 6U}, "UX,UY,UZ,URX,URY,URZ", "length,length,length,radian,radian,radian", "global-cartesian", "nodal"); const auto displacement = ReadDoubleDataset( file.Get(), std::string{kStepRoot} + "/nodal/displacement"); ASSERT_EQ(displacement.size(), 12U); EXPECT_DOUBLE_EQ(displacement.front(), 0.25); EXPECT_DOUBLE_EQ(displacement.back(), 11.25); ExpectNumericDataset( file.Get(), std::string{kStepRoot} + "/nodal/reaction", {2U, 6U}, "RF1,RF2,RF3,RM1,RM2,RM3", "force,force,force,force*length,force*length,force*length", "global-cartesian", "nodal"); const auto reaction = ReadDoubleDataset(file.Get(), std::string{kStepRoot} + "/nodal/reaction"); ASSERT_EQ(reaction.size(), 12U); EXPECT_DOUBLE_EQ(reaction.front(), 100.0); EXPECT_DOUBLE_EQ(reaction.back(), 111.0); ExpectNumericDataset( file.Get(), std::string{kStepRoot} + "/element/end_force_local", {1U, 2U, 6U}, "FX,FY,FZ,MX,MY,MZ", "force,force,force,force*length,force*length,force*length", "beam-local", "endpoint-outward-action"); const auto end_force = ReadDoubleDataset( file.Get(), std::string{kStepRoot} + "/element/end_force_local"); ASSERT_EQ(end_force.size(), 12U); EXPECT_DOUBLE_EQ(end_force.front(), 1.0); EXPECT_DOUBLE_EQ(end_force.back(), 12.0); ExpectNumericDataset( file.Get(), std::string{kStepRoot} + "/element/section_resultant", {1U, 2U, 4U}, "N,T,My,Mz", "force,force*length,force*length,force*length", "beam-local", "endpoint-positive-local-x-section-cut"); const auto section_resultant = ReadDoubleDataset( file.Get(), std::string{kStepRoot} + "/element/section_resultant"); ASSERT_EQ(section_resultant.size(), 8U); EXPECT_DOUBLE_EQ(section_resultant.front(), 11.0); EXPECT_DOUBLE_EQ(section_resultant.back(), 18.0); ExpectNumericDataset( file.Get(), std::string{kStepRoot} + "/element/generalized_strain", {1U, 2U, 4U}, "epsilon0,kappa_x,kappa_y,kappa_z", "1,1/length,1/length,1/length", "beam-local", "integration-point"); const auto generalized_strain = ReadDoubleDataset( file.Get(), std::string{kStepRoot} + "/element/generalized_strain"); ASSERT_EQ(generalized_strain.size(), 8U); EXPECT_DOUBLE_EQ(generalized_strain.front(), 0.01); EXPECT_DOUBLE_EQ(generalized_strain.back(), 0.08); ExpectNumericDataset( file.Get(), std::string{kStepRoot} + "/element/generalized_resultant", {1U, 2U, 4U}, "N,T,My,Mz", "force,force*length,force*length,force*length", "beam-local", "integration-point"); const auto generalized_resultant = ReadDoubleDataset( file.Get(), std::string{kStepRoot} + "/element/generalized_resultant"); ASSERT_EQ(generalized_resultant.size(), 8U); EXPECT_DOUBLE_EQ(generalized_resultant.front(), 21.0); EXPECT_DOUBLE_EQ(generalized_resultant.back(), 28.0); const std::string stress_path = std::string{kStepRoot} + "/element/stress_s11"; EXPECT_EQ(DatasetDimensions(file.Get(), stress_path), std::vector({4U})); ExpectCompoundMembers(file.Get(), stress_path, {"internal_element_id", "gauss_point_index", "section_point_index", "x1", "x2", "source", "S11"}); const auto stress_dataset = OpenDataset(file.Get(), stress_path); EXPECT_EQ(ReadStringAttribute(stress_dataset.Get(), "component_names"), "S11"); EXPECT_EQ( ReadStringAttribute(stress_dataset.Get(), "component_unit_dimensions"), "force/length^2"); EXPECT_EQ(ReadStringAttribute(stress_dataset.Get(), "coordinate_system"), "beam-local"); EXPECT_EQ(ReadStringAttribute(stress_dataset.Get(), "location"), "section-point"); EXPECT_EQ(ReadStringAttribute(stress_dataset.Get(), "step_name"), "Step-1"); EXPECT_EQ(ReadUint64Attribute(stress_dataset.Get(), "frame_index"), 0U); auto stress_rows = ReadStressRows(file.Get()); ASSERT_EQ(stress_rows.size(), 4U); EXPECT_EQ(stress_rows[0U].internal_element_id, 0U); EXPECT_EQ(stress_rows[0U].gauss_point_index, 1U); EXPECT_EQ(stress_rows[0U].section_point_index, 1U); EXPECT_DOUBLE_EQ(stress_rows[0U].x1, -0.1); EXPECT_DOUBLE_EQ(stress_rows[0U].x2, 0.2); EXPECT_STREQ(stress_rows[0U].source, "input"); EXPECT_DOUBLE_EQ(stress_rows[3U].s11, 34.0); ReclaimStressRows(file.Get(), stress_rows); EXPECT_EQ(DatasetDimensions(file.Get(), "/diagnostics"), std::vector({0U})); ExpectCompoundMembers(file.Get(), "/diagnostics", {"severity", "code", "file", "line", "keyword", "entity_identity", "message"}); EXPECT_EQ(H5Lexists(file.Get(), "/steps/Step-1/frames/0/element/transverse_shear_stress", H5P_DEFAULT), 0); } TEST(Hdf5ResultsWriter, WritesMandatoryOutputsDespiteOutputRequests) { TempDirectory directory{"mandatory"}; auto fixture = MakeFixture(directory.Path() / "request-model.inp"); const fesa::Diagnostic ignored_request{ fesa::Severity::kWarning, "ignored-output-request", {fixture.domain->SourcePath(), 70U}, "*OUTPUT", "FIELD", "Abaqus output requests do not filter FESA mandatory results."}; const auto output = directory.Path() / "results.h5"; fesa::Hdf5ResultsWriter writer; ASSERT_TRUE( writer.Write(output, *fixture.domain, *fixture.state, {ignored_request}) .IsOk()); const auto file = OpenFile(output); for (const char* suffix : {"/nodal/displacement", "/nodal/reaction", "/element/end_force_local", "/element/section_resultant", "/element/generalized_strain", "/element/generalized_resultant", "/element/stress_s11"}) { const std::string path = std::string{kStepRoot} + suffix; EXPECT_GT(H5Lexists(file.Get(), path.c_str(), H5P_DEFAULT), 0) << path; } EXPECT_EQ(DatasetDimensions(file.Get(), "/diagnostics"), std::vector({1U})); } TEST(Hdf5ResultsWriter, WritesWarningsAndDefaultCentroid) { TempDirectory directory{"warnings"}; auto fixture = MakeFixture(directory.Path() / "centroid.inp", true); std::vector diagnostics = { {fesa::Severity::kWarning, "ignored-output-request", {fixture.domain->SourcePath(), 80U}, "*OUTPUT", "FIELD", "Ignored output request."}, {fesa::Severity::kWarning, "ignored-keyword", {fixture.domain->SourcePath(), 20U}, "*PREPRINT", "", "Ignored generator control."}}; const auto output = directory.Path() / "results.h5"; fesa::Hdf5ResultsWriter writer; ASSERT_TRUE(writer.Write(output, *fixture.domain, *fixture.state, diagnostics) .IsOk()); const auto file = OpenFile(output); auto stress_rows = ReadStressRows(file.Get()); ASSERT_EQ(stress_rows.size(), 2U); for (std::size_t index = 0U; index < stress_rows.size(); ++index) { EXPECT_EQ(stress_rows[index].internal_element_id, 0U); EXPECT_EQ(stress_rows[index].gauss_point_index, index + 1U); EXPECT_EQ(stress_rows[index].section_point_index, 0U); EXPECT_DOUBLE_EQ(stress_rows[index].x1, 0.0); EXPECT_DOUBLE_EQ(stress_rows[index].x2, 0.0); EXPECT_STREQ(stress_rows[index].source, "fesa-default"); } ReclaimStressRows(file.Get(), stress_rows); auto rows = ReadDiagnosticRows(file.Get()); ASSERT_EQ(rows.size(), 2U); EXPECT_STREQ(rows[0U].severity, "warning"); EXPECT_STREQ(rows[0U].code, "ignored-keyword"); EXPECT_STREQ(rows[0U].file, std::filesystem::absolute(fixture.domain->SourcePath()) .lexically_normal() .generic_u8string() .c_str()); EXPECT_EQ(rows[0U].line, 20U); EXPECT_STREQ(rows[0U].keyword, "*PREPRINT"); EXPECT_STREQ(rows[0U].entity_identity, ""); EXPECT_STREQ(rows[0U].message, "Ignored generator control."); EXPECT_STREQ(rows[1U].code, "ignored-output-request"); EXPECT_EQ(rows[1U].line, 80U); ReclaimDiagnosticRows(file.Get(), rows); } TEST(Hdf5ResultsWriter, FailureLeavesNoPartialAndPreservesExistingFinal) { TempDirectory directory{"failure"}; auto fixture = MakeFixture(directory.Path() / "failure.inp"); fesa::Hdf5ResultsWriter writer; fixture.state->Displacement()[0U] = std::numeric_limits::quiet_NaN(); const auto invalid_output = directory.Path() / "invalid-results.h5"; ExpectOutputFailure( writer.Write(invalid_output, *fixture.domain, *fixture.state, {}), "invalid-result-state"); EXPECT_FALSE(std::filesystem::exists(invalid_output)); EXPECT_EQ(EntryCount(directory.Path()), 0U); fixture.state->Displacement()[0U] = 0.25; const auto final = directory.Path() / "results.h5"; const std::vector sentinel = {'p', 'r', 'e', 'v', 'i', 'o', 'u', 's'}; WriteBytes(final, sentinel); WinHandle lock{CreateFileW(final.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)}; ASSERT_NE(lock.Get(), INVALID_HANDLE_VALUE); ExpectOutputFailure(writer.Write(final, *fixture.domain, *fixture.state, {}), "hdf5-finalization-failure"); EXPECT_EQ(ReadBytes(final), sentinel); EXPECT_EQ(EntryCount(directory.Path()), 1U); } TEST(Hdf5ResultsWriter, SuccessfullyReplacesExistingFinal) { TempDirectory directory{"replace"}; auto fixture = MakeFixture(directory.Path() / "replace.inp"); const auto final = directory.Path() / "results.h5"; WriteBytes(final, {'o', 'l', 'd'}); fesa::Hdf5ResultsWriter writer; ASSERT_TRUE(writer.Write(final, *fixture.domain, *fixture.state, {}).IsOk()); EXPECT_GT(H5Fis_hdf5(final.string().c_str()), 0); EXPECT_EQ(EntryCount(directory.Path()), 1U); const auto file = OpenFile(final); Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose}; ASSERT_GE(metadata.Get(), 0); EXPECT_EQ(ReadUint64Attribute(metadata.Get(), "schema_version"), 0U); } // MITC4-H5-001, C-DUP-002 TEST(Hdf5ResultsWriter, WritesExactShellMetadataAndModelIdentity) { TempDirectory directory{"shell-model"}; const auto source = directory.Path() / "shell.inp"; auto fixture = MakeShellFixture(source); const auto output = directory.Path() / "results.h5"; fesa::Hdf5ResultsWriter writer; ASSERT_TRUE(writer.Write(output, *fixture.domain, *fixture.state, {}).IsOk()); const auto file = OpenFile(output); Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose}; ASSERT_GE(metadata.Get(), 0); EXPECT_EQ(ReadUint64Attribute(metadata.Get(), "schema_version"), 0U); EXPECT_EQ(ReadStringAttribute(metadata.Get(), "feature_id"), "linear-static-mitc4-shell"); EXPECT_EQ( ReadStringAttribute(metadata.Get(), "coordinate_convention"), "global-cartesian; shell-local=(e1,e2,e3); positive-thickness=+zeta"); EXPECT_EQ(ReadStringAttribute(metadata.Get(), "internal_formulation"), "FESA-MITC4"); EXPECT_EQ(ReadStringAttribute(metadata.Get(), "integration_rule"), "2x2x2-gauss; mitc4-edge-midpoint-shear"); EXPECT_EQ(DatasetDimensions(file.Get(), "/model/elements"), std::vector({1U})); ExpectCompoundMemberNames( file.Get(), "/model/elements", {"internal_element_id", "instance_name", "source_label", "source_element_type", "internal_formulation", "node_internal_ids", "shell_section_internal_id", "material_internal_id"}); const auto elements = OpenDataset(file.Get(), "/model/elements"); EXPECT_EQ(ReadStringAttribute(elements.Get(), "formulation"), "FESA-MITC4"); EXPECT_EQ(DatasetDimensions(file.Get(), "/model/shell/nodal_director"), std::vector({4U, 3U})); constexpr fesa::Vector3 director{0.0, 0.0, 1.0}; std::vector expected_directors; for (std::size_t node = 0U; node < 4U; ++node) { expected_directors.insert(expected_directors.end(), director.Components().begin(), director.Components().end()); } EXPECT_EQ(ReadDoubleDataset(file.Get(), "/model/shell/nodal_director"), expected_directors); EXPECT_EQ(DatasetDimensions(file.Get(), "/model/shell/nodal_frame"), std::vector({4U, 3U, 3U})); ExpectCompoundMemberNames(file.Get(), "/model/shell/materials", {"internal_material_id", "name", "E", "nu"}); ExpectCompoundMemberNames( file.Get(), "/model/shell/sections", {"internal_section_id", "source_file", "source_line", "source_elset", "material_internal_id", "thickness"}); EXPECT_EQ(DatasetDimensions(file.Get(), "/model/nodal_constraint_mask"), std::vector({4U, 6U})); const auto mask = ReadUint8Dataset(file.Get(), "/model/nodal_constraint_mask"); ASSERT_EQ(mask.size(), 24U); EXPECT_EQ(mask[0U], 1U); EXPECT_EQ(mask[1U], 1U); EXPECT_EQ(mask[2U], 1U); EXPECT_EQ(mask[3U], 1U); EXPECT_EQ(mask[4U], 0U); EXPECT_EQ(mask[5U], 0U); const auto prescribed = ReadDoubleDataset(file.Get(), "/model/prescribed_displacement"); ASSERT_EQ(prescribed.size(), 24U); EXPECT_DOUBLE_EQ(prescribed[0U], 0.0); EXPECT_DOUBLE_EQ(prescribed[3U], 0.125); EXPECT_DOUBLE_EQ(prescribed[4U], 0.0); EXPECT_EQ(ReadDoubleDataset(file.Get(), "/model/shell/section_positions"), std::vector({-1.0, 0.0, 1.0})); const auto locations = ReadDoubleDataset(file.Get(), "/model/shell/midsurface_locations"); ASSERT_EQ(locations.size(), 8U); const double gauss = 1.0 / std::sqrt(3.0); EXPECT_DOUBLE_EQ(locations[0U], -gauss); EXPECT_DOUBLE_EQ(locations[1U], -gauss); EXPECT_DOUBLE_EQ(locations[6U], -gauss); EXPECT_DOUBLE_EQ(locations[7U], gauss); } // MITC4-H5-002 TEST(Hdf5ResultsWriter, WritesExactMandatoryShellResultInventory) { TempDirectory directory{"shell-results"}; auto fixture = MakeShellFixture(directory.Path() / "shell.inp"); const auto output = directory.Path() / "results.h5"; fesa::Hdf5ResultsWriter writer; ASSERT_TRUE(writer.Write(output, *fixture.domain, *fixture.state, {}).IsOk()); const auto file = OpenFile(output); const std::string shell_root = std::string{kStepRoot} + "/element/shell"; ExpectNumericDataset(file.Get(), shell_root + "/local_frame", {1U, 4U, 3U, 3U}, "X,Y,Z", "1,1,1", "global-cartesian", "shell-local-frame"); ExpectNumericDataset(file.Get(), shell_root + "/generalized_strain", {1U, 4U, 8U}, "E11,E22,G12,K11,K22,K12,G13,G23", "1,1,1,1/length,1/length,1/length,1,1", "shell-local", "midsurface"); ExpectNumericDataset(file.Get(), shell_root + "/section_resultant", {1U, 4U, 8U}, "N11,N22,N12,M11,M22,M12,Q13,Q23", "force/length,force/length,force/" "length,force,force,force,force/length,force/length", "shell-local", "midsurface"); ExpectNumericDataset(file.Get(), shell_root + "/stress", {1U, 4U, 3U, 3U}, "S11,S22,S12", "force/length^2,force/length^2,force/length^2", "shell-local", "section-position"); const auto strain = ReadDoubleDataset(file.Get(), shell_root + "/generalized_strain"); ASSERT_EQ(strain.size(), 32U); EXPECT_DOUBLE_EQ(strain.front(), 101.0); EXPECT_DOUBLE_EQ(strain.back(), 408.0); const auto stress = ReadDoubleDataset(file.Get(), shell_root + "/stress"); ASSERT_EQ(stress.size(), 36U); EXPECT_DOUBLE_EQ(stress.front(), 121.0); EXPECT_DOUBLE_EQ(stress.back(), 429.0); ExpectNumericDataset(file.Get(), std::string{kStepRoot} + "/global/energy", {1U}, "PHYSICAL_STRAIN_ENERGY", "force*length", "global", "global"); ExpectNumericDataset( file.Get(), std::string{kStepRoot} + "/global/equilibrium", {6U}, "FORCE_1,FORCE_2,FORCE_3,MOMENT_1,MOMENT_2,MOMENT_3", "force,force,force,force*length,force*length,force*length", "global-cartesian", "global-origin"); ExpectNumericDataset( file.Get(), std::string{kStepRoot} + "/global/verification_metrics", {3U}, "FREE_RESIDUAL_NORMALIZED,FORCE_BALANCE_NORMALIZED,MOMENT_BALANCE_" "NORMALIZED", "1,1,1", "global", "verification"); const auto metrics = OpenDataset( file.Get(), std::string{kStepRoot} + "/global/verification_metrics"); EXPECT_EQ(ReadStringAttribute(metrics.Get(), "metric_definition_ids"), "free-residual-l2-over-max-free-force-l2,force-balance-l2-over-" "max-force-sum,moment-balance-l2-over-max-moment-sum"); EXPECT_EQ(ReadStringAttribute(metrics.Get(), "acceptance_thresholds"), "1e-10,1e-10,1e-10"); } // MITC4-H5-003 TEST(Hdf5ResultsWriter, WritesShellInventoryDespiteRequestsAndOmitsForbiddenPaths) { TempDirectory directory{"shell-mandatory"}; auto fixture = MakeShellFixture(directory.Path() / "shell.inp"); const fesa::Diagnostic ignored_request{ fesa::Severity::kWarning, "ignored-output-request", {fixture.domain->SourcePath(), 80U}, "*ELEMENT OUTPUT", "S", "Output requests cannot filter mandatory shell results."}; const auto output = directory.Path() / "results.h5"; fesa::Hdf5ResultsWriter writer; ASSERT_TRUE( writer.Write(output, *fixture.domain, *fixture.state, {ignored_request}) .IsOk()); const auto file = OpenFile(output); for (const char* suffix : {"/element/shell/local_frame", "/element/shell/generalized_strain", "/element/shell/section_resultant", "/element/shell/stress", "/global/energy", "/global/equilibrium", "/global/verification_metrics"}) { const std::string path = std::string{kStepRoot} + suffix; EXPECT_GT(H5Lexists(file.Get(), path.c_str(), H5P_DEFAULT), 0) << path; } for (const char* forbidden : {"/steps/Step-1/frames/0/element/shell/drilling", "/steps/Step-1/frames/0/element/shell/drilling_energy", "/steps/Step-1/frames/0/element/shell/S33", "/steps/Step-1/frames/0/element/shell/S13", "/steps/Step-1/frames/0/element/shell/S23"}) { EXPECT_EQ(H5Lexists(file.Get(), forbidden, H5P_DEFAULT), 0) << forbidden; } EXPECT_EQ(DatasetDimensions(file.Get(), "/diagnostics"), std::vector({1U})); } // MITC4-H5-004 TEST(Hdf5ResultsWriter, InvalidShellInventoryPreservesExistingFinal) { TempDirectory directory{"shell-atomic"}; auto fixture = MakeShellFixture(directory.Path() / "shell.inp"); auto invalid_state = fesa::AnalysisState::Create(*fixture.dofs, {"Step-1", 0U}); const auto final = directory.Path() / "results.h5"; const std::vector sentinel = {'s', 'h', 'e', 'l', 'l'}; WriteBytes(final, sentinel); fesa::Hdf5ResultsWriter writer; ExpectOutputFailure(writer.Write(final, *fixture.domain, invalid_state, {}), "invalid-result-rows"); EXPECT_EQ(ReadBytes(final), sentinel); EXPECT_EQ(EntryCount(directory.Path()), 1U); } // C-DUP-002 TEST(Hdf5ResultsWriter, Vector3ShellSerializationRejectsNonfiniteWithoutReplacingFinal) { TempDirectory directory{"shell-vector3-atomic"}; auto fixture = MakeShellFixture(directory.Path() / "shell.inp"); const auto final = directory.Path() / "results.h5"; fesa::Hdf5ResultsWriter writer; ASSERT_TRUE(writer.Write(final, *fixture.domain, *fixture.state, {}).IsOk()); const std::vector valid_bytes = ReadBytes(final); const auto file = OpenFile(final); const auto directors = ReadDoubleDataset(file.Get(), "/model/shell/nodal_director"); ASSERT_EQ(directors.size(), 12U); for (std::size_t offset = 0U; offset < directors.size(); offset += 3U) { const fesa::Vector3 director{directors[offset], directors[offset + 1U], directors[offset + 2U]}; EXPECT_TRUE(director.IsFinite()); EXPECT_EQ(director, (fesa::Vector3{0.0, 0.0, 1.0})); } fixture.state->Displacement()[0U] = std::numeric_limits::quiet_NaN(); ExpectOutputFailure(writer.Write(final, *fixture.domain, *fixture.state, {}), "invalid-result-state"); EXPECT_EQ(ReadBytes(final), valid_bytes); EXPECT_EQ(EntryCount(directory.Path()), 1U); }