#include "fesa/io/hdf5/dynamic_hdf5_results_writer.h" #include #include #include #include #include #include #include #include #include "fesa/analysis/analysis_model.h" #include "fesa/build_info.h" #include "fesa/core/diagnostic.h" #include "fesa/fem/dof_manager.h" #include "io/hdf5/hdf5_atomic_file.h" #include "io/hdf5/hdf5_model_writer.h" #include "io/hdf5/hdf5_primitives.h" #include "io/hdf5/hdf5_raii.h" namespace fesa { namespace { using hdf5_internal::Hdf5Failure; using hdf5_internal::Hdf5Handle; using hdf5_internal::CopyVectorValues; using hdf5_internal::FlattenNodalTriads; using hdf5_internal::FlattenNodalDirectors; using hdf5_internal::SpatialReactionValues; struct AcceptedFrameRow { std::uint64_t frame_index; std::uint64_t increment_index; double time; std::uint64_t iteration_count; std::uint8_t is_initial; }; struct DiagnosticRow { const char* severity; const char* code; const char* file; std::uint64_t line; const char* keyword; const char* entity_identity; const char* message; }; struct DensityRow { std::uint64_t internal_material_id; double rho; }; /// @brief Rejects a candidate before its final path can be replaced. void Check(const bool condition, const char* message) { if (!condition) throw Hdf5Failure{message}; } /// @brief Checks scalar type, rank and value of one numeric attribute. template void CheckAttribute(const hid_t object, const char* name, const hid_t file_type, const hid_t memory_type, const Value expected) { Hdf5Handle attribute{ hdf5_internal::RequireHdf5Id(H5Aopen(object, name, H5P_DEFAULT), "Missing numeric attribute."), H5Aclose}; Hdf5Handle type{H5Aget_type(attribute.Get()), H5Tclose}; Hdf5Handle space{H5Aget_space(attribute.Get()), H5Sclose}; Check(H5Tequal(type.Get(), file_type) > 0 && H5Sget_simple_extent_type(space.Get()) == H5S_SCALAR, "Invalid numeric attribute schema."); Value actual{}; hdf5_internal::RequireHdf5(H5Aread(attribute.Get(), memory_type, &actual), "Unable to read numeric attribute."); Check(actual == expected, "Numeric attribute value mismatch."); } /// @brief Checks UTF-8 variable-length scalar attribute storage and value. void CheckString(const hid_t object, const char* name, const std::string& expected) { Hdf5Handle attribute{ hdf5_internal::RequireHdf5Id(H5Aopen(object, name, H5P_DEFAULT), "Missing string attribute."), H5Aclose}; Hdf5Handle type{H5Aget_type(attribute.Get()), H5Tclose}; Hdf5Handle space{H5Aget_space(attribute.Get()), H5Sclose}; Check(H5Tget_class(type.Get()) == H5T_STRING && H5Tis_variable_str(type.Get()) > 0 && H5Tget_cset(type.Get()) == H5T_CSET_UTF8 && H5Sget_simple_extent_type(space.Get()) == H5S_SCALAR, "Invalid string attribute schema."); char* actual = nullptr; hdf5_internal::RequireHdf5(H5Aread(attribute.Get(), type.Get(), &actual), "Unable to read string attribute."); const bool equal = actual != nullptr && expected == actual; (void)H5free_memory(actual); Check(equal, "String attribute value mismatch."); } /// @brief Opens one mandatory contiguous, uncompressed dataset of exact shape. Hdf5Handle OpenDataset(const hid_t file, const std::string& path, const std::vector& dimensions) { Hdf5Handle dataset{ hdf5_internal::RequireHdf5Id(H5Dopen2(file, path.c_str(), H5P_DEFAULT), "Missing dynamic dataset."), H5Dclose}; Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose}; Check(H5Sget_simple_extent_ndims(space.Get()) == static_cast(dimensions.size()), "Dataset rank mismatch."); std::vector actual(dimensions.size()); hdf5_internal::RequireHdf5( H5Sget_simple_extent_dims(space.Get(), actual.data(), nullptr), "Unable to read dataset dimensions."); Check(actual == dimensions, "Dataset shape mismatch."); Hdf5Handle properties{H5Dget_create_plist(dataset.Get()), H5Pclose}; Check(H5Pget_layout(properties.Get()) == H5D_CONTIGUOUS && H5Pget_nfilters(properties.Get()) == 0, "Dataset must be contiguous and uncompressed."); return dataset; } /// @brief Compares binary64 readback to the complete finite accepted values. Hdf5Handle CheckDoubles(const hid_t file, const std::string& path, const std::vector& dimensions, const std::vector& expected) { auto dataset = OpenDataset(file, path, dimensions); Hdf5Handle type{H5Dget_type(dataset.Get()), H5Tclose}; Check(H5Tequal(type.Get(), H5T_IEEE_F64LE) > 0, "Dataset must use little-endian binary64."); std::vector actual(expected.size()); hdf5_internal::RequireHdf5(H5Dread(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, actual.data()), "Unable to read dynamic values."); for (std::size_t index = 0U; index < actual.size(); ++index) { Check(std::isfinite(actual[index]) && actual[index] == expected[index], "Dataset value mismatch or nonfinite value."); } return dataset; } /// @brief Validates ordered compound members without accepting extra fields. void CheckMembers(const hid_t dataset, const std::vector& names) { Hdf5Handle type{H5Dget_type(dataset), H5Tclose}; Check(H5Tget_class(type.Get()) == H5T_COMPOUND && H5Tget_nmembers(type.Get()) == static_cast(names.size()), "Compound field inventory mismatch."); for (std::size_t index = 0U; index < names.size(); ++index) { char* name = H5Tget_member_name(type.Get(), static_cast(index)); const bool equal = name != nullptr && names[index] == name; (void)H5free_memory(name); Check(equal, "Compound field order mismatch."); } } /// @brief Compares one fixed-width compound column with its source inventory. template void CheckColumn(const hid_t dataset, const char* name, const hid_t file_type, const hid_t memory_type, const std::vector& expected) { Hdf5Handle type{H5Dget_type(dataset), H5Tclose}; const int index = H5Tget_member_index(type.Get(), name); Check(index >= 0, "Missing compound column."); Hdf5Handle member{ H5Tget_member_type(type.Get(), static_cast(index)), H5Tclose}; Check(H5Tequal(member.Get(), file_type) > 0, "Compound column dtype mismatch."); Hdf5Handle memory{H5Tcreate(H5T_COMPOUND, sizeof(Value)), H5Tclose}; hdf5_internal::RequireHdf5(H5Tinsert(memory.Get(), name, 0U, memory_type), "Unable to create readback column type."); std::vector actual(expected.size()); if (!actual.empty()) { hdf5_internal::RequireHdf5(H5Dread(dataset, memory.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, actual.data()), "Unable to read compound column."); } Check(actual == expected, "Compound column value mismatch."); } /// @brief Compares and reclaims a UTF-8 variable-length compound column. void CheckStringColumn(const hid_t dataset, const char* name, const std::vector& expected) { Hdf5Handle type{H5Dget_type(dataset), H5Tclose}; const int index = H5Tget_member_index(type.Get(), name); Check(index >= 0, "Missing compound string column."); Hdf5Handle member{ H5Tget_member_type(type.Get(), static_cast(index)), H5Tclose}; Check(H5Tget_class(member.Get()) == H5T_STRING && H5Tis_variable_str(member.Get()) > 0 && H5Tget_cset(member.Get()) == H5T_CSET_UTF8, "Invalid compound string dtype."); Hdf5Handle memory{H5Tcreate(H5T_COMPOUND, sizeof(char*)), H5Tclose}; hdf5_internal::RequireHdf5(H5Tinsert(memory.Get(), name, 0U, member.Get()), "Unable to create readback string type."); std::vector actual(expected.size(), nullptr); if (actual.empty()) return; hdf5_internal::RequireHdf5(H5Dread(dataset, memory.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, actual.data()), "Unable to read compound strings."); bool equal = true; for (std::size_t row = 0U; row < actual.size(); ++row) { equal = equal && actual[row] != nullptr && expected[row] == actual[row]; } Hdf5Handle space{H5Dget_space(dataset), H5Sclose}; hdf5_internal::RequireHdf5( H5Dvlen_reclaim(memory.Get(), space.Get(), H5P_DEFAULT, actual.data()), "Unable to reclaim compound strings."); Check(equal, "Compound string value mismatch."); } /// @brief Writes the stable material-index density table. void WriteDensity(const hid_t file, const Domain& domain) { std::vector rows; for (const auto& density : domain.Densities()) { rows.push_back({density.material_index, density.rho}); } Hdf5Handle type{H5Tcreate(H5T_COMPOUND, sizeof(DensityRow)), H5Tclose}; hdf5_internal::RequireHdf5( H5Tinsert(type.Get(), "internal_material_id", HOFFSET(DensityRow, internal_material_id), H5T_STD_U64LE), "Unable to define density identity."); hdf5_internal::RequireHdf5( H5Tinsert(type.Get(), "rho", HOFFSET(DensityRow, rho), H5T_IEEE_F64LE), "Unable to define density value."); (void)hdf5_internal::WriteCompoundDataset(file, "/model/material_density", rows.size(), type.Get(), type.Get(), rows.data()); } /// @brief Reports an output failure without exposing backend exceptions. Status Failure(const char* code, const std::string& message) { return hdf5_internal::OutputFailure(code, message); } /// @brief Removes inherited metadata before applying the dynamic schema. void DeleteAttribute(const hid_t object, const char* name) { if (H5Aexists(object, name) > 0) { hdf5_internal::RequireHdf5(H5Adelete(object, name), "Unable to replace metadata attribute."); } } /// @brief Maps a validated frame's integration semantics to its schema. std::uint64_t SchemaVersion(const DynamicAcceptedFrameState& frame) { return frame.time_integration_formulation == DynamicTimeIntegrationFormulation::kReducedCoordinateHht ? 2U : 1U; } /// @brief Returns the explicit integration discriminator without aliases. const char* IntegrationFormulation(const DynamicAcceptedFrameState& frame) { return SchemaVersion(frame) == 2U ? "reduced-coordinate-HHT-with-physical-rate-reconstruction" : "full-coordinate-HHT"; } /// @brief Writes the dynamic procedure identity and fixed time schedule. void WriteMetadata(const hid_t file, const Domain& domain, const DynamicAcceptedFrameState& frame) { Hdf5Handle metadata{ hdf5_internal::RequireHdf5Id(H5Gopen2(file, "/metadata", H5P_DEFAULT), "The model metadata group is missing."), H5Gclose}; for (const char* name : {"feature_id", "analysis_procedure", "schema_version", "mass_formulation", "internal_formulation", "initial_state", "acceleration_convention", "step_name", "source_step_name", "unit_system_label", "coordinate_convention", "integration_rule", "frame_index", "time_integration_formulation"}) { DeleteAttribute(metadata.Get(), name); } const DynamicStepDefinition* const step = domain.Dynamic(); hdf5_internal::WriteStringAttribute(metadata.Get(), "feature_id", "hht-implicit-dynamics"); hdf5_internal::WriteStringAttribute(metadata.Get(), "analysis_procedure", "hht-implicit-dynamics"); hdf5_internal::WriteUint64Attribute(metadata.Get(), "schema_version", SchemaVersion(frame)); hdf5_internal::WriteStringAttribute(metadata.Get(), "time_integration_formulation", IntegrationFormulation(frame)); hdf5_internal::WriteStringAttribute(metadata.Get(), "mass_formulation", "nodal-lumped-kinetic"); hdf5_internal::WriteStringAttribute(metadata.Get(), "internal_formulation", "FESA-MITC4-total-lagrangian"); hdf5_internal::WriteStringAttribute(metadata.Get(), "initial_state", "zero-q-v-step-load-0+"); hdf5_internal::WriteStringAttribute( metadata.Get(), "acceleration_convention", "physical-endpoint-and-auxiliary-separated"); hdf5_internal::WriteStringAttribute(metadata.Get(), "source_step_name", step->source_step_name_raw); hdf5_internal::WriteStringAttribute(metadata.Get(), "step_name", "Step-1"); hdf5_internal::WriteStringAttribute(metadata.Get(), "unit_system_label", "consistent-input-units"); hdf5_internal::WriteDoubleAttribute(metadata.Get(), "alpha", step->alpha); hdf5_internal::WriteDoubleAttribute(metadata.Get(), "beta", step->beta); hdf5_internal::WriteDoubleAttribute(metadata.Get(), "gamma", step->gamma); hdf5_internal::WriteDoubleAttribute(metadata.Get(), "initial_increment", step->initial_increment); hdf5_internal::WriteDoubleAttribute(metadata.Get(), "time_period", step->time_period); hdf5_internal::WriteUint64Attribute(metadata.Get(), "increment_count", step->increment_count); hdf5_internal::WriteUint64Attribute(metadata.Get(), "maximum_increments", step->maximum_increments); (void)domain; } /// @brief Records exact accepted endpoint identity on a result dataset. void WriteFrameAttributes(const hid_t dataset, const DynamicAcceptedFrameState& frame) { hdf5_internal::WriteUint64Attribute(dataset, "schema_version", SchemaVersion(frame)); hdf5_internal::WriteStringAttribute(dataset, "step_name", "Step-1"); hdf5_internal::WriteUint64Attribute(dataset, "frame_index", frame.frame_index); hdf5_internal::WriteUint64Attribute(dataset, "increment_index", frame.increment_index); hdf5_internal::WriteDoubleAttribute(dataset, "time", frame.time); hdf5_internal::WriteUint8Attribute(dataset, "is_initial", frame.is_initial ? 1U : 0U); } /// @brief Writes or validates a result against the explicit schema plan. void ProcessDataset(const hid_t file, const std::string& path, const std::vector& dimensions, const std::vector& values, const char* names, const char* units, const char* coordinates, const char* location, const DynamicAcceptedFrameState& frame, const std::string& source_step, const bool readback) { Hdf5Handle dataset = readback ? CheckDoubles(file, path, dimensions, values) : hdf5_internal::WriteDoubleValues(file, path, dimensions, values.data(), values.size()); const auto string_attribute = [&](const char* name, const std::string& value) { if (readback) CheckString(dataset.Get(), name, value); else hdf5_internal::WriteStringAttribute(dataset.Get(), name, value); }; string_attribute("component_names", names); string_attribute("component_unit_dimensions", units); string_attribute("coordinate_system", coordinates); string_attribute("location", location); string_attribute("source_step_name", source_step); if (readback) { CheckAttribute(dataset.Get(), "schema_version", H5T_STD_U64LE, H5T_NATIVE_UINT64, SchemaVersion(frame)); CheckString(dataset.Get(), "step_name", "Step-1"); CheckAttribute(dataset.Get(), "frame_index", H5T_STD_U64LE, H5T_NATIVE_UINT64, std::uint64_t{frame.frame_index}); CheckAttribute(dataset.Get(), "increment_index", H5T_STD_U64LE, H5T_NATIVE_UINT64, std::uint64_t{frame.increment_index}); CheckAttribute(dataset.Get(), "time", H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE, frame.time); CheckAttribute(dataset.Get(), "is_initial", H5T_STD_U8LE, H5T_NATIVE_UINT8, static_cast(frame.is_initial)); } else { WriteFrameAttributes(dataset.Get(), frame); } if (path.find("/nodal/") != std::string::npos) { string_attribute("node_identity_dataset", "/model/nodes"); } const std::string leaf = path.substr(path.find_last_of('/') + 1U); if (leaf == "displacement" || leaf == "velocity" || leaf == "acceleration" || leaf == "algorithmic_acceleration") { string_attribute("rotation_representation", "total-rotation-vector-chart"); string_attribute( "quantity_semantics", leaf == "displacement" ? "global-translation-and-total-rotation-vector" : leaf == "velocity" ? "global-translation-rate-and-chart-rate" : leaf == "acceleration" ? "physical-endpoint-chart-acceleration" : SchemaVersion(frame) == 2U ? "full-chart-HHT-filter-combination-not-predictor-driver" : "auxiliary-HHT-chart-acceleration"); } if (leaf == "triad") string_attribute("axis_names", "A,B,D"); if (path.find("/element/shell/") != std::string::npos) { string_attribute("element_identity_dataset", "/model/elements"); string_attribute("source_element_type_dataset", "/model/elements.source_element_type"); string_attribute("internal_formulation", "FESA-MITC4-total-lagrangian"); string_attribute("midsurface_location_dataset", "/model/shell/midsurface_locations"); if (leaf == "local_frame") { string_attribute("axis_names", "E1,E2,E3"); } else { string_attribute( "local_frame_dataset", path.substr(0U, path.find_last_of('/') + 1U) + "local_frame"); } if (leaf == "stress") { string_attribute("section_position_dataset", "/model/shell/section_positions"); } } } /// @brief Flattens physical recovery in element and Gauss order. std::vector FlattenShell(const DynamicAcceptedFrameState& frame, const int quantity) { std::vector values; for (const ShellResultRow& row : frame.shell_results) { if (quantity == 0) for (const auto& a : row.local_frame) values.insert(values.end(), a.begin(), a.end()); if (quantity == 1) values.insert(values.end(), row.generalized_strain.begin(), row.generalized_strain.end()); if (quantity == 2) values.insert(values.end(), row.section_resultant.begin(), row.section_resultant.end()); if (quantity == 3) for (const auto& s : row.stress) values.insert(values.end(), s.components.begin(), s.components.end()); } return values; } /// @brief Writes or reads back all mandatory accepted-endpoint quantities. void WriteFrame(const hid_t file, const Domain& domain, const DynamicAcceptedFrameState& frame, const bool readback = false) { const hsize_t nodes = static_cast(domain.Nodes().size()); const hsize_t elements = static_cast(domain.ShellElements().Size()); const std::string root = "/steps/Step-1/frames/" + std::to_string(frame.frame_index); if (!readback) { (void)hdf5_internal::CreateGroup(file, (root + "/nodal").c_str()); (void)hdf5_internal::CreateGroup(file, (root + "/element/shell").c_str()); (void)hdf5_internal::CreateGroup(file, (root + "/global").c_str()); } const auto WriteDataset = [&](const hid_t target, const std::string& path, const std::vector& dimensions, const std::vector& values, const char* names, const char* units, const char* coordinates, const char* location, const DynamicAcceptedFrameState& endpoint) { ProcessDataset(target, path, dimensions, values, names, units, coordinates, location, endpoint, domain.Dynamic()->source_step_name_raw, readback); }; constexpr const char* kGeneralized = "F1,F2,F3,MPHI1,MPHI2,MPHI3"; constexpr const char* kGeneralizedUnits = "force,force,force,force*length,force*length,force*length"; const std::vector nodal{nodes, 6U}; WriteDataset(file, root + "/nodal/displacement", nodal, CopyVectorValues(frame.generalized_coordinates), "UX,UY,UZ,URX,URY,URZ", "length,length,length,1,1,1", "global-cartesian", "nodal", frame); WriteDataset(file, root + "/nodal/velocity", nodal, CopyVectorValues(frame.velocity), "V1,V2,V3,PHIDOT1,PHIDOT2,PHIDOT3", "length/time,length/time,length/time,1/time,1/time,1/time", "fixed-initial-global-chart", "nodal", frame); WriteDataset( file, root + "/nodal/acceleration", nodal, CopyVectorValues(frame.acceleration), "A1,A2,A3,PHIDDOT1,PHIDDOT2,PHIDDOT3", "length/time^2,length/time^2,length/time^2,1/time^2,1/time^2,1/time^2", "fixed-initial-global-chart", "nodal", frame); WriteDataset( file, root + "/nodal/algorithmic_acceleration", nodal, CopyVectorValues(frame.algorithmic_acceleration), "AUX1,AUX2,AUX3,AUX4,AUX5,AUX6", "length/time^2,length/time^2,length/time^2,1/time^2,1/time^2,1/time^2", "fixed-initial-global-chart", "nodal", frame); WriteDataset(file, root + "/nodal/angular_velocity", {nodes, 3U}, CopyVectorValues(frame.angular_velocity), "OMEGA1,OMEGA2,OMEGA3", "1/time,1/time,1/time", "global-cartesian", "nodal", frame); WriteDataset(file, root + "/nodal/angular_acceleration", {nodes, 3U}, CopyVectorValues(frame.angular_acceleration), "OMEGADOT1,OMEGADOT2,OMEGADOT3", "1/time^2,1/time^2,1/time^2", "global-cartesian", "nodal", frame); WriteDataset(file, root + "/nodal/triad", {nodes, 3U, 3U}, FlattenNodalTriads(frame.nodal_triad), "X,Y,Z", "1,1,1", "global-cartesian", "nodal", frame); WriteDataset(file, root + "/nodal/director", {nodes, 3U}, FlattenNodalDirectors(frame.nodal_director), "D1,D2,D3", "1,1,1", "global-cartesian", "nodal", frame); WriteDataset(file, root + "/nodal/generalized_inertia_force", nodal, CopyVectorValues(frame.inertia_force), kGeneralized, kGeneralizedUnits, "fixed-initial-global-chart", "nodal", frame); WriteDataset(file, root + "/nodal/generalized_internal_force", nodal, CopyVectorValues(frame.internal_force), kGeneralized, kGeneralizedUnits, "fixed-initial-global-chart", "nodal", frame); WriteDataset(file, root + "/nodal/generalized_external_force", nodal, CopyVectorValues(frame.external_force), kGeneralized, kGeneralizedUnits, "fixed-initial-global-chart", "nodal", frame); WriteDataset(file, root + "/nodal/generalized_residual", nodal, CopyVectorValues(frame.residual), kGeneralized, kGeneralizedUnits, "fixed-initial-global-chart", "nodal", frame); WriteDataset(file, root + "/nodal/generalized_reaction", nodal, CopyVectorValues(frame.reaction), kGeneralized, kGeneralizedUnits, "fixed-initial-global-chart", "nodal", frame); WriteDataset(file, root + "/nodal/reaction", nodal, SpatialReactionValues(frame.generalized_coordinates, frame.reaction, frame.nodal_internal_order.size()), "RF1,RF2,RF3,RM1,RM2,RM3", kGeneralizedUnits, "global-cartesian", "nodal", frame); const std::string shell = root + "/element/shell/"; WriteDataset(file, shell + "local_frame", {elements, 4U, 3U, 3U}, FlattenShell(frame, 0), "X,Y,Z", "1,1,1", "global-cartesian", "shell-local-frame", frame); WriteDataset(file, shell + "generalized_strain", {elements, 4U, 8U}, FlattenShell(frame, 1), "E11,E22,G12,K11,K22,K12,G13,G23", "1,1,1,1/length,1/length,1/length,1,1", "shell-local", "midsurface", frame); WriteDataset(file, shell + "section_resultant", {elements, 4U, 8U}, FlattenShell(frame, 2), "N11,N22,N12,M11,M22,M12,Q13,Q23", "force/length,force/length,force/length,force,force,force,force/" "length,force/length", "shell-local", "midsurface", frame); WriteDataset(file, shell + "stress", {elements, 4U, 3U, 3U}, FlattenShell(frame, 3), "S11,S22,S12", "force/length^2,force/length^2,force/length^2", "shell-local", "section-position", frame); WriteDataset( file, root + "/global/energy", {5U}, {frame.kinetic_energy, frame.physical_strain_energy, frame.drilling_energy, frame.external_work, frame.energy_defect}, "KINETIC_ENERGY,PHYSICAL_STRAIN_ENERGY,DRILLING_ENERGY,EXTERNAL_WORK," "ENERGY_DEFECT", "force*length,force*length,force*length,force*length,force*length", "global", "global", frame); WriteDataset(file, root + "/global/equilibrium", {6U}, std::vector{frame.global_equilibrium.begin(), frame.global_equilibrium.end()}, "FORCE_1,FORCE_2,FORCE_3,MOMENT_1,MOMENT_2,MOMENT_3", kGeneralizedUnits, "global-cartesian", "global-balance-error", frame); } /// @brief Writes ordinal and exact-time accepted frame identities. void WriteAcceptedFrames(const hid_t file, const std::vector& frames) { std::vector rows; rows.reserve(frames.size()); for (const auto& frame : frames) rows.push_back({frame.frame_index, frame.increment_index, frame.time, frame.iteration_count, static_cast(frame.is_initial)}); Hdf5Handle type{hdf5_internal::RequireHdf5Id( H5Tcreate(H5T_COMPOUND, sizeof(AcceptedFrameRow)), "Unable to create accepted-frame type."), H5Tclose}; hdf5_internal::RequireHdf5( H5Tinsert(type.Get(), "frame_index", HOFFSET(AcceptedFrameRow, frame_index), H5T_STD_U64LE), "Unable to define accepted-frame field."); hdf5_internal::RequireHdf5( H5Tinsert(type.Get(), "increment_index", HOFFSET(AcceptedFrameRow, increment_index), H5T_STD_U64LE), "Unable to define accepted-frame field."); hdf5_internal::RequireHdf5( H5Tinsert(type.Get(), "time", HOFFSET(AcceptedFrameRow, time), H5T_IEEE_F64LE), "Unable to define accepted-frame field."); hdf5_internal::RequireHdf5( H5Tinsert(type.Get(), "iteration_count", HOFFSET(AcceptedFrameRow, iteration_count), H5T_STD_U64LE), "Unable to define accepted-frame field."); hdf5_internal::RequireHdf5( H5Tinsert(type.Get(), "is_initial", HOFFSET(AcceptedFrameRow, is_initial), H5T_STD_U8LE), "Unable to define accepted-frame field."); (void)hdf5_internal::WriteCompoundDataset( file, "/steps/Step-1/accepted_frames", rows.size(), type.Get(), type.Get(), rows.data()); } /// @brief Writes sorted diagnostics with dynamic schema field names. void WriteDiagnostics(const hid_t file, const std::vector& input_diagnostics) { std::vector diagnostics = input_diagnostics; SortDiagnostics(diagnostics); std::vector files; std::vector rows; files.reserve(diagnostics.size()); rows.reserve(diagnostics.size()); for (const Diagnostic& diagnostic : diagnostics) { files.push_back( hdf5_internal::NormalizedPathString(diagnostic.location.file)); } for (std::size_t index = 0U; index < diagnostics.size(); ++index) { const Diagnostic& diagnostic = diagnostics[index]; rows.push_back( {diagnostic.severity == Severity::kWarning ? "warning" : "error", diagnostic.code.c_str(), files[index].c_str(), static_cast(diagnostic.location.line), diagnostic.keyword.c_str(), diagnostic.entity_identity.c_str(), diagnostic.message.c_str()}); } auto string_type = hdf5_internal::MakeUtf8StringType(); Hdf5Handle type{hdf5_internal::RequireHdf5Id( H5Tcreate(H5T_COMPOUND, sizeof(DiagnosticRow)), "Unable to create dynamic diagnostic type."), H5Tclose}; const auto add = [&](const char* name, const std::size_t offset, const hid_t value_type) { hdf5_internal::RequireHdf5(H5Tinsert(type.Get(), name, offset, value_type), "Unable to define dynamic diagnostic field."); }; add("severity", HOFFSET(DiagnosticRow, severity), string_type.Get()); add("code", HOFFSET(DiagnosticRow, code), string_type.Get()); add("source_file", HOFFSET(DiagnosticRow, file), string_type.Get()); add("source_line", HOFFSET(DiagnosticRow, line), H5T_STD_U64LE); add("keyword", HOFFSET(DiagnosticRow, keyword), string_type.Get()); add("entity_identity", HOFFSET(DiagnosticRow, entity_identity), string_type.Get()); add("message", HOFFSET(DiagnosticRow, message), string_type.Get()); (void)hdf5_internal::WriteCompoundDataset( file, "/diagnostics", rows.size(), type.Get(), type.Get(), rows.data()); } /// @brief Checks every source-model field written by the shared model writer. void CheckModel(const hid_t file, const Domain& domain, const hdf5_internal::WriterModelData& model) { const hsize_t count = static_cast(domain.Nodes().size()); auto nodes = OpenDataset(file, "/model/nodes", {count}); CheckMembers(nodes.Get(), {"internal_node_id", "instance_name", "source_label", "coordinates"}); std::vector ids; std::vector instances; std::vector labels; std::vector> coordinates; for (std::size_t index = 0U; index < domain.Nodes().size(); ++index) { ids.push_back(index); instances.push_back(domain.Nodes()[index].source_id.instance_name); labels.push_back(domain.Nodes()[index].source_id.source_label_text); coordinates.push_back(domain.Nodes()[index].coordinates); } const hsize_t three = 3U; Hdf5Handle triple_file{H5Tarray_create2(H5T_IEEE_F64LE, 1, &three), H5Tclose}; Hdf5Handle triple_memory{H5Tarray_create2(H5T_NATIVE_DOUBLE, 1, &three), H5Tclose}; CheckColumn(nodes.Get(), "internal_node_id", H5T_STD_U64LE, H5T_NATIVE_UINT64, ids); CheckStringColumn(nodes.Get(), "instance_name", instances); CheckStringColumn(nodes.Get(), "source_label", labels); CheckColumn(nodes.Get(), "coordinates", triple_file.Get(), triple_memory.Get(), coordinates); CheckString(nodes.Get(), "coordinate_system", "global-cartesian"); CheckString(nodes.Get(), "units_label", "length"); auto elements = OpenDataset(file, "/model/elements", {static_cast(domain.Elements().Size())}); CheckMembers( elements.Get(), {"internal_element_id", "instance_name", "source_label", "source_element_type", "internal_formulation", "node_internal_ids", "shell_section_internal_id", "material_internal_id"}); ids.clear(); instances.clear(); labels.clear(); std::vector source_types; std::vector formulations; std::vector sections; std::vector materials; std::vector> connectivity; for (std::size_t index = 0U; index < domain.ShellElements().Size(); ++index) { const auto& element = domain.ShellElements()[index]; ids.push_back(index); instances.push_back(element.source_id.instance_name); labels.push_back(element.source_id.source_label_text); source_types.push_back( hdf5_internal::ShellSourceTypeName(element.source_type)); formulations.push_back("FESA-MITC4"); sections.push_back(element.section_index); materials.push_back(element.material_index); connectivity.push_back({element.node_indices[0U], element.node_indices[1U], element.node_indices[2U], element.node_indices[3U]}); } const hsize_t four = 4U; Hdf5Handle nodes_file{H5Tarray_create2(H5T_STD_U64LE, 1, &four), H5Tclose}; Hdf5Handle nodes_memory{H5Tarray_create2(H5T_NATIVE_UINT64, 1, &four), H5Tclose}; CheckColumn(elements.Get(), "internal_element_id", H5T_STD_U64LE, H5T_NATIVE_UINT64, ids); CheckStringColumn(elements.Get(), "instance_name", instances); CheckStringColumn(elements.Get(), "source_label", labels); CheckStringColumn(elements.Get(), "source_element_type", source_types); CheckStringColumn(elements.Get(), "internal_formulation", formulations); CheckColumn(elements.Get(), "node_internal_ids", nodes_file.Get(), nodes_memory.Get(), connectivity); CheckColumn(elements.Get(), "shell_section_internal_id", H5T_STD_U64LE, H5T_NATIVE_UINT64, sections); CheckColumn(elements.Get(), "material_internal_id", H5T_STD_U64LE, H5T_NATIVE_UINT64, materials); auto material_table = OpenDataset(file, "/model/shell/materials", {static_cast(domain.Materials().Size())}); CheckMembers(material_table.Get(), {"internal_material_id", "name", "E", "nu"}); ids.clear(); labels.clear(); std::vector youngs; std::vector poisson; for (std::size_t index = 0U; index < domain.Materials().Size(); ++index) { const auto& material = domain.LinearElasticMaterials()[index]; ids.push_back(index); labels.push_back(material.name); youngs.push_back(material.youngs_modulus); poisson.push_back(material.poisson_ratio); } CheckColumn(material_table.Get(), "internal_material_id", H5T_STD_U64LE, H5T_NATIVE_UINT64, ids); CheckStringColumn(material_table.Get(), "name", labels); CheckColumn(material_table.Get(), "E", H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE, youngs); CheckColumn(material_table.Get(), "nu", H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE, poisson); auto density_table = OpenDataset(file, "/model/material_density", {static_cast(domain.Materials().Size())}); CheckMembers(density_table.Get(), {"internal_material_id", "rho"}); std::vector densities; for (const auto& density : domain.Densities()) densities.push_back(density.rho); CheckColumn(density_table.Get(), "internal_material_id", H5T_STD_U64LE, H5T_NATIVE_UINT64, ids); CheckColumn(density_table.Get(), "rho", H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE, densities); auto section_table = OpenDataset(file, "/model/shell/sections", {static_cast(domain.ShellSections().Size())}); CheckMembers(section_table.Get(), {"internal_section_id", "source_file", "source_line", "source_elset", "material_internal_id", "thickness"}); ids.clear(); labels.clear(); materials.clear(); std::vector files; std::vector lines; std::vector thickness; for (std::size_t index = 0U; index < domain.ShellSections().Size(); ++index) { const auto& section = domain.ShellSections()[index]; ids.push_back(index); files.push_back(hdf5_internal::NormalizedPathString(section.location.file)); lines.push_back(section.location.line); labels.push_back(section.name); materials.push_back(section.material_index); thickness.push_back(section.thickness); } CheckColumn(section_table.Get(), "internal_section_id", H5T_STD_U64LE, H5T_NATIVE_UINT64, ids); CheckStringColumn(section_table.Get(), "source_file", files); CheckColumn(section_table.Get(), "source_line", H5T_STD_U64LE, H5T_NATIVE_UINT64, lines); CheckStringColumn(section_table.Get(), "source_elset", labels); CheckColumn(section_table.Get(), "material_internal_id", H5T_STD_U64LE, H5T_NATIVE_UINT64, materials); CheckColumn(section_table.Get(), "thickness", H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE, thickness); auto mask = OpenDataset(file, "/model/nodal_constraint_mask", {count, 6U}); Hdf5Handle mask_type{H5Dget_type(mask.Get()), H5Tclose}; Check(H5Tequal(mask_type.Get(), H5T_STD_U8LE) > 0, "Constraint dtype mismatch."); std::vector actual_mask(model.constraint_mask.size()); hdf5_internal::RequireHdf5(H5Dread(mask.Get(), H5T_NATIVE_UINT8, H5S_ALL, H5S_ALL, H5P_DEFAULT, actual_mask.data()), "Unable to read constraint mask."); Check(actual_mask == model.constraint_mask, "Constraint mask mismatch."); (void)CheckDoubles(file, "/model/prescribed_displacement", {count, 6U}, model.prescribed_displacement); std::vector directors; std::vector frames; hdf5_internal::AppendShellReferenceFrames(domain, directors, frames); (void)CheckDoubles(file, "/model/shell/nodal_director", {count, 3U}, directors); (void)CheckDoubles(file, "/model/shell/nodal_frame", {count, 3U, 3U}, frames); const double gauss = 1.0 / std::sqrt(3.0); (void)CheckDoubles( file, "/model/shell/midsurface_locations", {4U, 2U}, {-gauss, -gauss, gauss, -gauss, gauss, gauss, -gauss, gauss}); (void)CheckDoubles(file, "/model/shell/section_positions", {3U}, {-1.0, 0.0, 1.0}); } /// @brief Checks complete accepted-frame table and diagnostic identities. void CheckTables(const hid_t file, const DynamicAnalysisState& state, std::vector diagnostics) { auto table = OpenDataset(file, "/steps/Step-1/accepted_frames", {static_cast(state.AcceptedFrames().size())}); CheckMembers(table.Get(), {"frame_index", "increment_index", "time", "iteration_count", "is_initial"}); std::vector frames; std::vector increments; std::vector iterations; std::vector times; std::vector initial; for (const auto& frame : state.AcceptedFrames()) { frames.push_back(frame.frame_index); increments.push_back(frame.increment_index); iterations.push_back(frame.iteration_count); times.push_back(frame.time); initial.push_back(static_cast(frame.is_initial)); } CheckColumn(table.Get(), "frame_index", H5T_STD_U64LE, H5T_NATIVE_UINT64, frames); CheckColumn(table.Get(), "increment_index", H5T_STD_U64LE, H5T_NATIVE_UINT64, increments); CheckColumn(table.Get(), "iteration_count", H5T_STD_U64LE, H5T_NATIVE_UINT64, iterations); CheckColumn(table.Get(), "time", H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE, times); CheckColumn(table.Get(), "is_initial", H5T_STD_U8LE, H5T_NATIVE_UINT8, initial); SortDiagnostics(diagnostics); auto diagnostic_table = OpenDataset( file, "/diagnostics", {static_cast(diagnostics.size())}); CheckMembers(diagnostic_table.Get(), {"severity", "code", "source_file", "source_line", "keyword", "entity_identity", "message"}); std::vector severity; std::vector codes; std::vector files; std::vector lines; std::vector keywords; std::vector identities; std::vector messages; for (const auto& diagnostic : diagnostics) { severity.push_back(diagnostic.severity == Severity::kWarning ? "warning" : "error"); codes.push_back(diagnostic.code); files.push_back( hdf5_internal::NormalizedPathString(diagnostic.location.file)); lines.push_back(diagnostic.location.line); keywords.push_back(diagnostic.keyword); identities.push_back(diagnostic.entity_identity); messages.push_back(diagnostic.message); } CheckStringColumn(diagnostic_table.Get(), "severity", severity); CheckStringColumn(diagnostic_table.Get(), "code", codes); CheckStringColumn(diagnostic_table.Get(), "source_file", files); CheckColumn(diagnostic_table.Get(), "source_line", H5T_STD_U64LE, H5T_NATIVE_UINT64, lines); CheckStringColumn(diagnostic_table.Get(), "keyword", keywords); CheckStringColumn(diagnostic_table.Get(), "entity_identity", identities); CheckStringColumn(diagnostic_table.Get(), "message", messages); } /// @brief Reopens and verifies the complete dynamic publication candidate. Status SelfCheck(const std::filesystem::path& path, const Domain& domain, const DynamicAnalysisState& state, const hdf5_internal::WriterModelData& model, const std::vector& diagnostics) { try { Hdf5Handle file{ hdf5_internal::RequireHdf5Id( H5Fopen(path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT), "Unable to reopen dynamic candidate."), H5Fclose}; Hdf5Handle metadata{hdf5_internal::RequireHdf5Id( H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), "The dynamic metadata group is missing."), H5Gclose}; const auto& step = *domain.Dynamic(); CheckString(metadata.Get(), "feature_id", "hht-implicit-dynamics"); CheckString(metadata.Get(), "analysis_procedure", "hht-implicit-dynamics"); CheckString(metadata.Get(), "mass_formulation", "nodal-lumped-kinetic"); CheckString(metadata.Get(), "internal_formulation", "FESA-MITC4-total-lagrangian"); CheckString(metadata.Get(), "initial_state", "zero-q-v-step-load-0+"); CheckString(metadata.Get(), "acceleration_convention", "physical-endpoint-and-auxiliary-separated"); CheckString(metadata.Get(), "source_step_name", step.source_step_name_raw); CheckString(metadata.Get(), "step_name", "Step-1"); CheckString(metadata.Get(), "unit_system_label", "consistent-input-units"); CheckString(metadata.Get(), "solver_version", std::string{SolverVersion()}); CheckString(metadata.Get(), "source_input_identity", hdf5_internal::SourceInputIdentity(domain)); CheckAttribute(metadata.Get(), "schema_version", H5T_STD_U64LE, H5T_NATIVE_UINT64, SchemaVersion(state.AcceptedFrames().front())); CheckString(metadata.Get(), "time_integration_formulation", IntegrationFormulation(state.AcceptedFrames().front())); CheckAttribute(metadata.Get(), "increment_count", H5T_STD_U64LE, H5T_NATIVE_UINT64, std::uint64_t{step.increment_count}); CheckAttribute(metadata.Get(), "maximum_increments", H5T_STD_U64LE, H5T_NATIVE_UINT64, std::uint64_t{step.maximum_increments}); for (const auto& entry : std::vector>{ {"alpha", step.alpha}, {"beta", step.beta}, {"gamma", step.gamma}, {"initial_increment", step.initial_increment}, {"time_period", step.time_period}}) { CheckAttribute(metadata.Get(), entry.first, H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE, entry.second); } CheckModel(file.Get(), domain, model); CheckTables(file.Get(), state, diagnostics); Hdf5Handle frames{H5Gopen2(file.Get(), "/steps/Step-1/frames", H5P_DEFAULT), H5Gclose}; H5G_info_t information{}; hdf5_internal::RequireHdf5(H5Gget_info(frames.Get(), &information), "Unable to inspect accepted frame group."); Check(information.nlinks == state.AcceptedFrames().size(), "Unexpected frame group inventory."); for (const auto& frame : state.AcceptedFrames()) { WriteFrame(file.Get(), domain, frame, true); } return Status::Ok(); } catch (const std::exception& error) { return Failure("dynamic-output-schema", error.what()); } } } // namespace Status DynamicHdf5ResultsWriter::Write( const std::filesystem::path& output_path, const Domain& domain, const DynamicAnalysisState& state, const std::vector& diagnostics) { if (output_path.empty() || output_path.filename().empty() || !state.IsInitialized() || state.AcceptedFrames().empty() || domain.Dynamic() == nullptr) return Failure("dynamic-output-schema", "Dynamic output requires a named path, initialized state, " "and accepted frames."); try { hdf5_internal::Hdf5ErrorSilencer silence; const auto& step = *domain.Dynamic(); Check(state.AcceptedFrames().size() == static_cast(step.increment_count) + 1U && state.AcceptedFrames().back().time == step.time_period, "Dynamic output requires the complete accepted history."); auto analysis_model = AnalysisModel::Create(domain); Check(analysis_model.HasValue(), "Dynamic model inventory is invalid."); auto dofs_result = DofManager::Create(analysis_model.Value()); Check(dofs_result.HasValue(), "Dynamic constraint inventory is invalid."); const auto& dofs = dofs_result.Value(); auto validated_state = DynamicAnalysisState::Create(domain, dofs); Check(validated_state.HasValue(), "Dynamic schedule or inventory is invalid."); for (const auto& frame : state.AcceptedFrames()) { Check(validated_state.Value().CommitAcceptedFrame(frame).IsOk(), "Dynamic frame does not match the output Domain."); } Check( domain.Densities().size() == domain.Materials().Size() && domain.LinearElasticMaterials().Size() == domain.Materials().Size(), "Material and density inventories differ."); for (std::size_t index = 0U; index < domain.Densities().size(); ++index) { const auto& density = domain.Densities()[index]; Check(density.material_index == index && std::isfinite(density.rho) && density.rho > 0.0, "Material density identity or value is invalid."); } hdf5_internal::WriterModelData model{}; model.constraint_mask.assign(dofs.FullDofCount(), 0U); model.prescribed_displacement.assign(dofs.FullDofCount(), 0.0); for (const std::size_t dof : dofs.ConstrainedDofs()) { model.constraint_mask[dof] = 1U; } const std::filesystem::path candidate_path = hdf5_internal::MakeCandidatePath(output_path); hdf5_internal::CandidateFileGuard cleanup{candidate_path}; Hdf5Handle file{hdf5_internal::RequireHdf5Id( H5Fcreate(candidate_path.string().c_str(), H5F_ACC_EXCL, H5P_DEFAULT, H5P_DEFAULT), "Unable to create dynamic candidate."), H5Fclose}; (void)hdf5_internal::CreateGroup(file.Get(), "/model/shell"); hdf5_internal::WriteModel(file.Get(), domain, model); WriteMetadata(file.Get(), domain, state.AcceptedFrames().front()); WriteDensity(file.Get(), domain); (void)hdf5_internal::CreateGroup(file.Get(), "/steps"); (void)hdf5_internal::CreateGroup(file.Get(), "/steps/Step-1"); WriteAcceptedFrames(file.Get(), state.AcceptedFrames()); for (const auto& frame : state.AcceptedFrames()) { WriteFrame(file.Get(), domain, frame); } WriteDiagnostics(file.Get(), diagnostics); hdf5_internal::RequireHdf5(H5Fflush(file.Get(), H5F_SCOPE_GLOBAL), "Unable to flush dynamic candidate."); hdf5_internal::RequireHdf5(file.CloseChecked(), "Unable to close dynamic candidate."); const Status check = SelfCheck(candidate_path, domain, state, model, diagnostics); const Status finalization = hdf5_internal::AtomicFinalizeValidatedCandidate( candidate_path, output_path, check); if (finalization.IsOk()) cleanup.Release(); return finalization; } catch (const Hdf5Failure& error) { return Failure("dynamic-output-schema", error.what()); } catch (const std::exception& error) { return Failure("hdf5-write-failure", error.what()); } } } // namespace fesa