#define NOMINMAX #include #include "fesa/io/hdf5/hdf5_results_writer.hpp" #include "fesa/analysis/analysis_model.hpp" #include "fesa/analysis/analysis_state.hpp" #include "fesa/build_info.h" #include "fesa/fem/dof_manager.hpp" #include "fesa/model/domain.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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 useDefaultCentroid) { fesa::ModelDefinition definition{}; definition.sourcePath = source; definition.sourceContentIdentity = "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}, useDefaultCentroid ? 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 useDefaultCentroid = false) { auto domainResult = fesa::Domain::create( makeDefinition(source, useDefaultCentroid)); if (!domainResult.HasValue()) { throw std::runtime_error{"Writer fixture Domain construction failed."}; } auto domain = std::make_unique( std::move(domainResult.Value())); auto modelResult = fesa::AnalysisModel::create(*domain); if (!modelResult.HasValue()) { throw std::runtime_error{"Writer fixture AnalysisModel construction failed."}; } const fesa::AnalysisModel model = std::move(modelResult.Value()); auto dofsResult = fesa::DofManager::create(model); if (!dofsResult.HasValue()) { throw std::runtime_error{"Writer fixture DofManager construction failed."}; } auto dofs = std::make_unique( std::move(dofsResult.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].sourceId, {1.0, 2.0, 3.0, 4.0, 5.0, 6.0}, {11.0, 12.0, 13.0, 14.0}}, {0U, 1, nodes[1U].sourceId, {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 (useDefaultCentroid) { 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.sourcePath = source; definition.sourceContentIdentity = "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.shellSections = { {"PlateSet", 0.02, 0U, {source, 30U}}}; definition.shellElements = {{ {"Shell-1", 401, "401"}, fesa::ShellSourceElementType::s4r, {0U, 1U, 2U, 3U}, 0U, 0U, {source, 40U}}}; for (std::size_t node = 0U; node < definition.nodes.size(); ++node) { definition.shellNodeInitialFrames.push_back({ static_cast(node), {0.0, 0.0, 1.0}, {1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}}); } definition.nodeSets = { {"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 domainResult = fesa::Domain::create(makeShellDefinition(source)); if (!domainResult.HasValue()) { throw std::runtime_error{"Shell writer fixture Domain construction failed."}; } auto domain = std::make_unique( std::move(domainResult.Value())); auto modelResult = fesa::AnalysisModel::create(*domain); if (!modelResult.HasValue()) { throw std::runtime_error{"Shell writer fixture AnalysisModel construction failed."}; } const fesa::AnalysisModel model = std::move(modelResult.Value()); auto dofsResult = fesa::DofManager::create(model); if (!dofsResult.HasValue()) { throw std::runtime_error{"Shell writer fixture DofManager construction failed."}; } auto dofs = std::make_unique( std::move(dofsResult.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::gp1, fesa::ShellMidsurfaceLocation::gp2, fesa::ShellMidsurfaceLocation::gp3, fesa::ShellMidsurfaceLocation::gp4}; 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::bottom, -1.0, {base + 21.0, base + 22.0, base + 23.0}}, {fesa::ShellSectionPosition::middle, 0.0, {base + 24.0, base + 25.0, base + 26.0}}, {fesa::ShellSectionPosition::top, 1.0, {base + 27.0, base + 28.0, base + 29.0}}}}}); } candidate.physicalStrainEnergy = 123.5; candidate.equilibrium = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0}; candidate.verificationMetrics = {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 valueCount = 1U; for (const hsize_t dimension : dimensions) { valueCount *= static_cast(dimension); } const auto dataset = openDataset(file, path); std::vector values(valueCount); 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 valueCount = 1U; for (const hsize_t dimension : dimensions) { valueCount *= 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(valueCount); 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 compoundType, const unsigned index, const std::string& name) { Hdf5Handle memberType{ H5Tget_member_type(compoundType, index), H5Tclose}; ASSERT_GE(memberType.get(), 0); const bool isUint64 = name == "internal_node_id" || name == "internal_element_id" || name == "gauss_point_index" || name == "section_point_index" || name == "line"; const bool isFloat64 = name == "x1" || name == "x2" || name == "S11"; const bool isString = name == "instance_name" || name == "source_label" || name == "source" || name == "severity" || name == "code" || name == "file" || name == "keyword" || name == "entity_identity" || name == "message"; if (isUint64) { EXPECT_EQ(H5Tget_class(memberType.get()), H5T_INTEGER); EXPECT_EQ(H5Tget_size(memberType.get()), sizeof(std::uint64_t)); EXPECT_EQ(H5Tget_sign(memberType.get()), H5T_SGN_NONE); EXPECT_GT(H5Tequal(memberType.get(), H5T_STD_U64LE), 0); return; } if (isFloat64) { EXPECT_EQ(H5Tget_class(memberType.get()), H5T_FLOAT); EXPECT_EQ(H5Tget_size(memberType.get()), sizeof(double)); EXPECT_GT(H5Tequal(memberType.get(), H5T_IEEE_F64LE), 0); return; } if (isString) { EXPECT_EQ(H5Tget_class(memberType.get()), H5T_STRING); EXPECT_GT(H5Tis_variable_str(memberType.get()), 0); EXPECT_EQ(H5Tget_cset(memberType.get()), H5T_CSET_UTF8); return; } ASSERT_EQ(H5Tget_class(memberType.get()), H5T_ARRAY); const int rank = H5Tget_array_ndims(memberType.get()); ASSERT_GT(rank, 0); std::vector dimensions(static_cast(rank)); ASSERT_GE(H5Tget_array_dims2(memberType.get(), dimensions.data()), 0); Hdf5Handle baseType{H5Tget_super(memberType.get()), H5Tclose}; ASSERT_GE(baseType.get(), 0); if (name == "node_internal_ids") { EXPECT_EQ(dimensions, std::vector({2U})); EXPECT_GT(H5Tequal(baseType.get(), H5T_STD_U64LE), 0); } else if (name == "coordinates") { EXPECT_EQ(dimensions, std::vector({3U})); EXPECT_GT(H5Tequal(baseType.get(), H5T_IEEE_F64LE), 0); } else { EXPECT_EQ(name, "local_axes"); EXPECT_EQ(dimensions, std::vector({3U, 3U})); EXPECT_GT(H5Tequal(baseType.get(), H5T_IEEE_F64LE), 0); } } void expectCompoundMembers( const hid_t file, const std::string& path, const std::vector& expectedNames) { 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(expectedNames.size())); for (std::size_t index = 0U; index < expectedNames.size(); ++index) { char* rawName = H5Tget_member_name(type.get(), static_cast(index)); ASSERT_NE(rawName, nullptr); const std::string actualName{rawName}; (void)H5free_memory(rawName); EXPECT_EQ(actualName, expectedNames[index]); expectPortableCompoundMember( type.get(), static_cast(index), expectedNames[index]); } } void expectCompoundMemberNames( const hid_t file, const std::string& path, const std::vector& expectedNames) { 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(expectedNames.size())); for (std::size_t index = 0U; index < expectedNames.size(); ++index) { char* rawName = H5Tget_member_name( type.get(), static_cast(index)); ASSERT_NE(rawName, nullptr); const std::string actualName{rawName}; (void)H5free_memory(rawName); EXPECT_EQ(actualName, expectedNames[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& coordinateSystem, 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"), coordinateSystem); 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 internalNodeId; char* instanceName; char* sourceLabel; 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 stringType = makeUtf8StringType(); const hsize_t coordinateDimensions[] = {3U}; Hdf5Handle coordinatesType{ H5Tarray_create2(H5T_NATIVE_DOUBLE, 1, coordinateDimensions), H5Tclose}; Hdf5Handle memoryType{H5Tcreate(H5T_COMPOUND, sizeof(NodeReadRow)), H5Tclose}; if (H5Tinsert(memoryType.get(), "internal_node_id", HOFFSET(NodeReadRow, internalNodeId), H5T_NATIVE_UINT64) < 0 || H5Tinsert(memoryType.get(), "instance_name", HOFFSET(NodeReadRow, instanceName), stringType.get()) < 0 || H5Tinsert(memoryType.get(), "source_label", HOFFSET(NodeReadRow, sourceLabel), stringType.get()) < 0 || H5Tinsert(memoryType.get(), "coordinates", HOFFSET(NodeReadRow, coordinates), coordinatesType.get()) < 0) { throw std::runtime_error{"Unable to create the node memory type."}; } std::vector rows(2U); if (H5Dread(dataset.get(), memoryType.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 stringType = makeUtf8StringType(); const hsize_t coordinateDimensions[] = {3U}; Hdf5Handle coordinatesType{ H5Tarray_create2(H5T_NATIVE_DOUBLE, 1, coordinateDimensions), H5Tclose}; Hdf5Handle memoryType{H5Tcreate(H5T_COMPOUND, sizeof(NodeReadRow)), H5Tclose}; (void)H5Tinsert(memoryType.get(), "internal_node_id", HOFFSET(NodeReadRow, internalNodeId), H5T_NATIVE_UINT64); (void)H5Tinsert(memoryType.get(), "instance_name", HOFFSET(NodeReadRow, instanceName), stringType.get()); (void)H5Tinsert(memoryType.get(), "source_label", HOFFSET(NodeReadRow, sourceLabel), stringType.get()); (void)H5Tinsert(memoryType.get(), "coordinates", HOFFSET(NodeReadRow, coordinates), coordinatesType.get()); (void)H5Dvlen_reclaim( memoryType.get(), space.get(), H5P_DEFAULT, rows.data()); } struct ElementReadRow { std::uint64_t internalElementId; char* instanceName; char* sourceLabel; std::uint64_t nodeInternalIds[2]; double localAxes[9]; }; std::vector readElementRows(const hid_t file) { const auto dataset = openDataset(file, "/model/elements"); auto stringType = makeUtf8StringType(); const hsize_t nodeDimensions[] = {2U}; const hsize_t axesDimensions[] = {3U, 3U}; Hdf5Handle nodeType{ H5Tarray_create2(H5T_NATIVE_UINT64, 1, nodeDimensions), H5Tclose}; Hdf5Handle axesType{ H5Tarray_create2(H5T_NATIVE_DOUBLE, 2, axesDimensions), H5Tclose}; Hdf5Handle memoryType{ H5Tcreate(H5T_COMPOUND, sizeof(ElementReadRow)), H5Tclose}; (void)H5Tinsert(memoryType.get(), "internal_element_id", HOFFSET(ElementReadRow, internalElementId), H5T_NATIVE_UINT64); (void)H5Tinsert(memoryType.get(), "instance_name", HOFFSET(ElementReadRow, instanceName), stringType.get()); (void)H5Tinsert(memoryType.get(), "source_label", HOFFSET(ElementReadRow, sourceLabel), stringType.get()); (void)H5Tinsert(memoryType.get(), "node_internal_ids", HOFFSET(ElementReadRow, nodeInternalIds), nodeType.get()); (void)H5Tinsert(memoryType.get(), "local_axes", HOFFSET(ElementReadRow, localAxes), axesType.get()); std::vector rows(1U); if (H5Dread(dataset.get(), memoryType.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].internalElementId, 0U); EXPECT_STREQ(rows[0U].instanceName, u8"Beam-\u03b1"); EXPECT_STREQ(rows[0U].sourceLabel, "303"); EXPECT_EQ(rows[0U].nodeInternalIds[0U], 0U); EXPECT_EQ(rows[0U].nodeInternalIds[1U], 1U); const std::array expectedAxes = { 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 < expectedAxes.size(); ++index) { EXPECT_NEAR(rows[0U].localAxes[index], expectedAxes[index], 1.0e-15); } (void)H5Dvlen_reclaim( memoryType.get(), space.get(), H5P_DEFAULT, rows.data()); return rows; } struct StressReadRow { std::uint64_t internalElementId; std::uint64_t gaussPointIndex; std::uint64_t sectionPointIndex; 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 stringType = makeUtf8StringType(); Hdf5Handle memoryType{ H5Tcreate(H5T_COMPOUND, sizeof(StressReadRow)), H5Tclose}; (void)H5Tinsert(memoryType.get(), "internal_element_id", HOFFSET(StressReadRow, internalElementId), H5T_NATIVE_UINT64); (void)H5Tinsert(memoryType.get(), "gauss_point_index", HOFFSET(StressReadRow, gaussPointIndex), H5T_NATIVE_UINT64); (void)H5Tinsert(memoryType.get(), "section_point_index", HOFFSET(StressReadRow, sectionPointIndex), H5T_NATIVE_UINT64); (void)H5Tinsert(memoryType.get(), "x1", HOFFSET(StressReadRow, x1), H5T_NATIVE_DOUBLE); (void)H5Tinsert(memoryType.get(), "x2", HOFFSET(StressReadRow, x2), H5T_NATIVE_DOUBLE); (void)H5Tinsert(memoryType.get(), "source", HOFFSET(StressReadRow, source), stringType.get()); (void)H5Tinsert(memoryType.get(), "S11", HOFFSET(StressReadRow, s11), H5T_NATIVE_DOUBLE); std::vector rows( dimensions.empty() ? 0U : static_cast(dimensions[0U])); if (!rows.empty() && H5Dread(dataset.get(), memoryType.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 stringType = makeUtf8StringType(); Hdf5Handle memoryType{ H5Tcreate(H5T_COMPOUND, sizeof(StressReadRow)), H5Tclose}; (void)H5Tinsert(memoryType.get(), "internal_element_id", HOFFSET(StressReadRow, internalElementId), H5T_NATIVE_UINT64); (void)H5Tinsert(memoryType.get(), "gauss_point_index", HOFFSET(StressReadRow, gaussPointIndex), H5T_NATIVE_UINT64); (void)H5Tinsert(memoryType.get(), "section_point_index", HOFFSET(StressReadRow, sectionPointIndex), H5T_NATIVE_UINT64); (void)H5Tinsert(memoryType.get(), "x1", HOFFSET(StressReadRow, x1), H5T_NATIVE_DOUBLE); (void)H5Tinsert(memoryType.get(), "x2", HOFFSET(StressReadRow, x2), H5T_NATIVE_DOUBLE); (void)H5Tinsert(memoryType.get(), "source", HOFFSET(StressReadRow, source), stringType.get()); (void)H5Tinsert(memoryType.get(), "S11", HOFFSET(StressReadRow, s11), H5T_NATIVE_DOUBLE); if (!rows.empty()) { (void)H5Dvlen_reclaim( memoryType.get(), space.get(), H5P_DEFAULT, rows.data()); } } struct DiagnosticReadRow { char* severity; char* code; char* file; std::uint64_t line; char* keyword; char* entityIdentity; char* message; }; std::vector readDiagnosticRows(const hid_t file) { const auto dataset = openDataset(file, "/diagnostics"); const auto dimensions = datasetDimensions(file, "/diagnostics"); auto stringType = makeUtf8StringType(); Hdf5Handle memoryType{ H5Tcreate(H5T_COMPOUND, sizeof(DiagnosticReadRow)), H5Tclose}; (void)H5Tinsert(memoryType.get(), "severity", HOFFSET(DiagnosticReadRow, severity), stringType.get()); (void)H5Tinsert(memoryType.get(), "code", HOFFSET(DiagnosticReadRow, code), stringType.get()); (void)H5Tinsert(memoryType.get(), "file", HOFFSET(DiagnosticReadRow, file), stringType.get()); (void)H5Tinsert(memoryType.get(), "line", HOFFSET(DiagnosticReadRow, line), H5T_NATIVE_UINT64); (void)H5Tinsert(memoryType.get(), "keyword", HOFFSET(DiagnosticReadRow, keyword), stringType.get()); (void)H5Tinsert(memoryType.get(), "entity_identity", HOFFSET(DiagnosticReadRow, entityIdentity), stringType.get()); (void)H5Tinsert(memoryType.get(), "message", HOFFSET(DiagnosticReadRow, message), stringType.get()); std::vector rows( dimensions.empty() ? 0U : static_cast(dimensions[0U])); if (!rows.empty() && H5Dread(dataset.get(), memoryType.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 stringType = makeUtf8StringType(); Hdf5Handle memoryType{ H5Tcreate(H5T_COMPOUND, sizeof(DiagnosticReadRow)), H5Tclose}; (void)H5Tinsert(memoryType.get(), "severity", HOFFSET(DiagnosticReadRow, severity), stringType.get()); (void)H5Tinsert(memoryType.get(), "code", HOFFSET(DiagnosticReadRow, code), stringType.get()); (void)H5Tinsert(memoryType.get(), "file", HOFFSET(DiagnosticReadRow, file), stringType.get()); (void)H5Tinsert(memoryType.get(), "line", HOFFSET(DiagnosticReadRow, line), H5T_NATIVE_UINT64); (void)H5Tinsert(memoryType.get(), "keyword", HOFFSET(DiagnosticReadRow, keyword), stringType.get()); (void)H5Tinsert(memoryType.get(), "entity_identity", HOFFSET(DiagnosticReadRow, entityIdentity), stringType.get()); (void)H5Tinsert(memoryType.get(), "message", HOFFSET(DiagnosticReadRow, message), stringType.get()); if (!rows.empty()) { (void)H5Dvlen_reclaim( memoryType.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& expectedCode) { 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, expectedCode); } } // 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 normalizedSource = std::filesystem::absolute(source).lexically_normal().generic_u8string(); EXPECT_EQ( readStringAttribute(metadata.get(), "source_input_identity"), "path=" + normalizedSource + ";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 nodesDataset = openDataset(file.get(), "/model/nodes"); EXPECT_EQ( readStringAttribute(nodesDataset.get(), "coordinate_system"), "global-cartesian"); EXPECT_EQ(readStringAttribute(nodesDataset.get(), "units_label"), "length"); auto nodes = readNodeRows(file.get()); ASSERT_EQ(nodes.size(), 2U); EXPECT_EQ(nodes[0U].internalNodeId, 0U); EXPECT_STREQ(nodes[0U].instanceName, u8"Beam-\u03b1"); EXPECT_STREQ(nodes[0U].sourceLabel, "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 elementsDataset = openDataset(file.get(), "/model/elements"); EXPECT_EQ( readStringAttribute(elementsDataset.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 endForce = readDoubleDataset( file.get(), std::string{kStepRoot} + "/element/end_force_local"); ASSERT_EQ(endForce.size(), 12U); EXPECT_DOUBLE_EQ(endForce.front(), 1.0); EXPECT_DOUBLE_EQ(endForce.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 sectionResultant = readDoubleDataset( file.get(), std::string{kStepRoot} + "/element/section_resultant"); ASSERT_EQ(sectionResultant.size(), 8U); EXPECT_DOUBLE_EQ(sectionResultant.front(), 11.0); EXPECT_DOUBLE_EQ(sectionResultant.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 generalizedStrain = readDoubleDataset( file.get(), std::string{kStepRoot} + "/element/generalized_strain"); ASSERT_EQ(generalizedStrain.size(), 8U); EXPECT_DOUBLE_EQ(generalizedStrain.front(), 0.01); EXPECT_DOUBLE_EQ(generalizedStrain.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 generalizedResultant = readDoubleDataset( file.get(), std::string{kStepRoot} + "/element/generalized_resultant"); ASSERT_EQ(generalizedResultant.size(), 8U); EXPECT_DOUBLE_EQ(generalizedResultant.front(), 21.0); EXPECT_DOUBLE_EQ(generalizedResultant.back(), 28.0); const std::string stressPath = std::string{kStepRoot} + "/element/stress_s11"; EXPECT_EQ(datasetDimensions(file.get(), stressPath), std::vector({4U})); expectCompoundMembers( file.get(), stressPath, {"internal_element_id", "gauss_point_index", "section_point_index", "x1", "x2", "source", "S11"}); const auto stressDataset = openDataset(file.get(), stressPath); EXPECT_EQ(readStringAttribute(stressDataset.get(), "component_names"), "S11"); EXPECT_EQ( readStringAttribute(stressDataset.get(), "component_unit_dimensions"), "force/length^2"); EXPECT_EQ( readStringAttribute(stressDataset.get(), "coordinate_system"), "beam-local"); EXPECT_EQ(readStringAttribute(stressDataset.get(), "location"), "section-point"); EXPECT_EQ(readStringAttribute(stressDataset.get(), "step_name"), "Step-1"); EXPECT_EQ(readUint64Attribute(stressDataset.get(), "frame_index"), 0U); auto stressRows = readStressRows(file.get()); ASSERT_EQ(stressRows.size(), 4U); EXPECT_EQ(stressRows[0U].internalElementId, 0U); EXPECT_EQ(stressRows[0U].gaussPointIndex, 1U); EXPECT_EQ(stressRows[0U].sectionPointIndex, 1U); EXPECT_DOUBLE_EQ(stressRows[0U].x1, -0.1); EXPECT_DOUBLE_EQ(stressRows[0U].x2, 0.2); EXPECT_STREQ(stressRows[0U].source, "input"); EXPECT_DOUBLE_EQ(stressRows[3U].s11, 34.0); reclaimStressRows(file.get(), stressRows); 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 ignoredRequest{ 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, {ignoredRequest}) .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 stressRows = readStressRows(file.get()); ASSERT_EQ(stressRows.size(), 2U); for (std::size_t index = 0U; index < stressRows.size(); ++index) { EXPECT_EQ(stressRows[index].internalElementId, 0U); EXPECT_EQ(stressRows[index].gaussPointIndex, index + 1U); EXPECT_EQ(stressRows[index].sectionPointIndex, 0U); EXPECT_DOUBLE_EQ(stressRows[index].x1, 0.0); EXPECT_DOUBLE_EQ(stressRows[index].x2, 0.0); EXPECT_STREQ(stressRows[index].source, "fesa-default"); } reclaimStressRows(file.get(), stressRows); 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].entityIdentity, ""); 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 invalidOutput = directory.path() / "invalid-results.h5"; expectOutputFailure( writer.write(invalidOutput, *fixture.domain, *fixture.state, {}), "invalid-result-state"); EXPECT_FALSE(std::filesystem::exists(invalidOutput)); 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 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})); EXPECT_EQ( readDoubleDataset(file.get(), "/model/shell/nodal_director"), std::vector({0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0})); 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 shellRoot = std::string{kStepRoot} + "/element/shell"; expectNumericDataset( file.get(), shellRoot + "/local_frame", {1U, 4U, 3U, 3U}, "X,Y,Z", "1,1,1", "global-cartesian", "shell-local-frame"); expectNumericDataset( file.get(), shellRoot + "/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(), shellRoot + "/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(), shellRoot + "/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(), shellRoot + "/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(), shellRoot + "/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 ignoredRequest{ 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, {ignoredRequest}) .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 invalidState = 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, invalidState, {}), "invalid-result-rows"); EXPECT_EQ(readBytes(final), sentinel); EXPECT_EQ(entryCount(directory.path()), 1U); }