Files
FESADev/src/fesa/io/hdf5/dynamic_hdf5_results_writer.cpp
T

998 lines
48 KiB
C++

#include "fesa/io/hdf5/dynamic_hdf5_results_writer.h"
#include <array>
#include <cmath>
#include <cstdint>
#include <exception>
#include <filesystem>
#include <string>
#include <utility>
#include <vector>
#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 <typename Value>
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<hsize_t>& 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<int>(dimensions.size()),
"Dataset rank mismatch.");
std::vector<hsize_t> 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<hsize_t>& dimensions,
const std::vector<double>& 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<double> 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<std::string>& names) {
Hdf5Handle type{H5Dget_type(dataset), H5Tclose};
Check(H5Tget_class(type.Get()) == H5T_COMPOUND &&
H5Tget_nmembers(type.Get()) == static_cast<int>(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<unsigned>(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 <typename Value>
void CheckColumn(const hid_t dataset, const char* name, const hid_t file_type,
const hid_t memory_type, const std::vector<Value>& 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<unsigned>(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<Value> 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<std::string>& 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<unsigned>(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<char*> 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<DensityRow> 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<hsize_t>& dimensions,
const std::vector<double>& 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<std::uint8_t>(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<double> FlattenShell(const DynamicAcceptedFrameState& frame,
const int quantity) {
std::vector<double> 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<hsize_t>(domain.Nodes().size());
const hsize_t elements = static_cast<hsize_t>(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<hsize_t>& dimensions,
const std::vector<double>& 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<hsize_t> 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<double>{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<DynamicAcceptedFrameState>& frames) {
std::vector<AcceptedFrameRow> 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<std::uint8_t>(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<Diagnostic>& input_diagnostics) {
std::vector<Diagnostic> diagnostics = input_diagnostics;
SortDiagnostics(diagnostics);
std::vector<std::string> files;
std::vector<DiagnosticRow> 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<std::uint64_t>(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<hsize_t>(domain.Nodes().size());
auto nodes = OpenDataset(file, "/model/nodes", {count});
CheckMembers(nodes.Get(), {"internal_node_id", "instance_name",
"source_label", "coordinates"});
std::vector<std::uint64_t> ids;
std::vector<std::string> instances;
std::vector<std::string> labels;
std::vector<std::array<double, 3>> 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<hsize_t>(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<std::string> source_types;
std::vector<std::string> formulations;
std::vector<std::uint64_t> sections;
std::vector<std::uint64_t> materials;
std::vector<std::array<std::uint64_t, 4>> 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<hsize_t>(domain.Materials().Size())});
CheckMembers(material_table.Get(),
{"internal_material_id", "name", "E", "nu"});
ids.clear();
labels.clear();
std::vector<double> youngs;
std::vector<double> 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<hsize_t>(domain.Materials().Size())});
CheckMembers(density_table.Get(), {"internal_material_id", "rho"});
std::vector<double> 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<hsize_t>(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<std::string> files;
std::vector<std::uint64_t> lines;
std::vector<double> 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<std::uint8_t> 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<double> directors;
std::vector<double> 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<Diagnostic> diagnostics) {
auto table =
OpenDataset(file, "/steps/Step-1/accepted_frames",
{static_cast<hsize_t>(state.AcceptedFrames().size())});
CheckMembers(table.Get(), {"frame_index", "increment_index", "time",
"iteration_count", "is_initial"});
std::vector<std::uint64_t> frames;
std::vector<std::uint64_t> increments;
std::vector<std::uint64_t> iterations;
std::vector<double> times;
std::vector<std::uint8_t> 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<std::uint8_t>(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<hsize_t>(diagnostics.size())});
CheckMembers(diagnostic_table.Get(),
{"severity", "code", "source_file", "source_line", "keyword",
"entity_identity", "message"});
std::vector<std::string> severity;
std::vector<std::string> codes;
std::vector<std::string> files;
std::vector<std::uint64_t> lines;
std::vector<std::string> keywords;
std::vector<std::string> identities;
std::vector<std::string> 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<Diagnostic>& 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<std::pair<const char*, double>>{
{"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<Diagnostic>& 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<std::size_t>(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