feat(thermal): publish fully validated HDF5 histories atomically
This commit is contained in:
@@ -0,0 +1,581 @@
|
||||
#include "io/hdf5/thermal_hdf5_self_check.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/build_info.h"
|
||||
#include "io/hdf5/hdf5_primitives.h"
|
||||
#include "io/hdf5/hdf5_raii.h"
|
||||
|
||||
namespace fesa::hdf5_internal {
|
||||
namespace {
|
||||
using U64 = std::uint64_t;
|
||||
/// @brief Rejects malformed mandatory schema without exposing HDF5 diagnostics.
|
||||
void Require(bool condition, const std::string& message) {
|
||||
if (!condition) throw Hdf5Failure{message};
|
||||
}
|
||||
/// @brief Opens one mandatory dataset and checks its entire declared extent.
|
||||
Hdf5Handle Dataset(hid_t file, const std::string& path,
|
||||
const std::vector<hsize_t>& shape) {
|
||||
Hdf5Handle d{
|
||||
RequireHdf5Id(H5Dopen2(file, path.c_str(), H5P_DEFAULT), path.c_str()),
|
||||
H5Dclose};
|
||||
Hdf5Handle space{RequireHdf5Id(H5Dget_space(d.Get()), "Missing dataspace."),
|
||||
H5Sclose};
|
||||
Require(
|
||||
H5Sget_simple_extent_ndims(space.Get()) == static_cast<int>(shape.size()),
|
||||
path + ": wrong rank");
|
||||
std::vector<hsize_t> actual(shape.size());
|
||||
Require(H5Sget_simple_extent_dims(space.Get(), actual.data(), nullptr) >= 0 &&
|
||||
actual == shape,
|
||||
path + ": wrong extent");
|
||||
return d;
|
||||
}
|
||||
/// @brief Compares every typed scalar against an independent expected
|
||||
/// inventory.
|
||||
template <class T>
|
||||
void Values(hid_t d, hid_t file_type, hid_t memory_type,
|
||||
const std::vector<T>& expected) {
|
||||
Hdf5Handle type{RequireHdf5Id(H5Dget_type(d), "Missing dataset type."),
|
||||
H5Tclose};
|
||||
Require(H5Tequal(type.Get(), file_type) > 0,
|
||||
"Wrong mandatory dataset primitive type.");
|
||||
std::vector<T> values(expected.size());
|
||||
if (!values.empty())
|
||||
RequireHdf5(
|
||||
H5Dread(d, memory_type, H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data()),
|
||||
"Cannot read dataset.");
|
||||
Require(
|
||||
values == expected,
|
||||
"Dataset values or ordered identities differ from the supplied history.");
|
||||
}
|
||||
/// @brief Checks a scalar numeric attribute before reading its one-value
|
||||
/// buffer.
|
||||
template <class T>
|
||||
void Number(hid_t object, const char* name, hid_t file_type, hid_t memory_type,
|
||||
T expected) {
|
||||
Hdf5Handle a{RequireHdf5Id(H5Aopen(object, name, H5P_DEFAULT), name),
|
||||
H5Aclose};
|
||||
Hdf5Handle t{RequireHdf5Id(H5Aget_type(a.Get()), "Missing attribute type."),
|
||||
H5Tclose};
|
||||
Hdf5Handle s{RequireHdf5Id(H5Aget_space(a.Get()), "Missing attribute space."),
|
||||
H5Sclose};
|
||||
Require(H5Tequal(t.Get(), file_type) > 0 &&
|
||||
H5Sget_simple_extent_type(s.Get()) == H5S_SCALAR,
|
||||
std::string{name} + ": wrong scalar attribute type/space");
|
||||
T value{};
|
||||
RequireHdf5(H5Aread(a.Get(), memory_type, &value), "Cannot read attribute.");
|
||||
Require(value == expected, std::string{name} + ": wrong attribute value");
|
||||
}
|
||||
/// @brief Reads either fixed or variable UTF-8 strings, preserving schema
|
||||
/// freedom.
|
||||
void Strings(hid_t string_type, const std::vector<std::string>& expected,
|
||||
const std::function<herr_t(hid_t, void*)>& read, bool compound,
|
||||
const char* field) {
|
||||
Require(H5Tget_class(string_type) == H5T_STRING &&
|
||||
H5Tget_cset(string_type) == H5T_CSET_UTF8,
|
||||
"Mandatory string is not UTF-8.");
|
||||
const auto variable = H5Tis_variable_str(string_type);
|
||||
Require(variable >= 0, "Cannot inspect string storage.");
|
||||
const std::size_t width = variable ? sizeof(char*) : H5Tget_size(string_type);
|
||||
Require(width > 0, "Invalid fixed string width.");
|
||||
Hdf5Handle memory;
|
||||
hid_t read_type = string_type;
|
||||
if (compound) {
|
||||
memory = Hdf5Handle{RequireHdf5Id(H5Tcreate(H5T_COMPOUND, width),
|
||||
"Cannot create string projection."),
|
||||
H5Tclose};
|
||||
RequireHdf5(H5Tinsert(memory.Get(), field, 0, string_type),
|
||||
"Cannot project string field.");
|
||||
read_type = memory.Get();
|
||||
}
|
||||
bool equal = true;
|
||||
if (variable) {
|
||||
std::vector<char*> values(expected.size(), nullptr);
|
||||
const herr_t status = values.empty() ? 0 : read(read_type, values.data());
|
||||
for (std::size_t i = 0; i < values.size(); ++i) {
|
||||
if (!values[i] || !IsValidUtf8(values[i]) || expected[i] != values[i])
|
||||
equal = false;
|
||||
if (values[i] && H5free_memory(values[i]) < 0) equal = false;
|
||||
}
|
||||
RequireHdf5(status, "Cannot read variable strings.");
|
||||
} else {
|
||||
std::size_t bytes = 0;
|
||||
Require(SizeProductFits(width, expected.size(), bytes),
|
||||
"String extent overflows memory.");
|
||||
std::vector<char> values(bytes);
|
||||
if (!values.empty())
|
||||
RequireHdf5(read(read_type, values.data()), "Cannot read fixed strings.");
|
||||
for (std::size_t i = 0; i < expected.size(); ++i) {
|
||||
std::string value(values.data() + i * width, width);
|
||||
const auto end = value.find('\0');
|
||||
if (end != std::string::npos)
|
||||
value.resize(end);
|
||||
else if (H5Tget_strpad(string_type) == H5T_STR_SPACEPAD)
|
||||
while (!value.empty() && value.back() == ' ') value.pop_back();
|
||||
if (!IsValidUtf8(value) || value != expected[i]) equal = false;
|
||||
}
|
||||
}
|
||||
Require(equal,
|
||||
"String field/attribute differs from the expected source identity or "
|
||||
"value.");
|
||||
}
|
||||
/// @brief Checks a mandatory scalar UTF-8 attribute, including its exact value.
|
||||
void Text(hid_t object, const char* name, const std::string& expected) {
|
||||
Hdf5Handle a{RequireHdf5Id(H5Aopen(object, name, H5P_DEFAULT), name),
|
||||
H5Aclose};
|
||||
Hdf5Handle t{RequireHdf5Id(H5Aget_type(a.Get()), "Missing text type."),
|
||||
H5Tclose};
|
||||
Hdf5Handle s{RequireHdf5Id(H5Aget_space(a.Get()), "Missing text space."),
|
||||
H5Sclose};
|
||||
Require(H5Sget_simple_extent_type(s.Get()) == H5S_SCALAR,
|
||||
"Text attribute is not scalar.");
|
||||
Strings(
|
||||
t.Get(), {expected},
|
||||
[&](hid_t type, void* data) { return H5Aread(a.Get(), type, data); },
|
||||
false, name);
|
||||
}
|
||||
/// @brief Locates a named field independently of compound order and padding.
|
||||
Hdf5Handle Field(hid_t dataset, const char* name) {
|
||||
Hdf5Handle t{RequireHdf5Id(H5Dget_type(dataset), "Missing compound type."),
|
||||
H5Tclose};
|
||||
Require(H5Tget_class(t.Get()) == H5T_COMPOUND,
|
||||
"Mandatory table is not compound.");
|
||||
const int index = H5Tget_member_index(t.Get(), name);
|
||||
Require(index >= 0, std::string{"Missing compound field: "} + name);
|
||||
return {
|
||||
RequireHdf5Id(H5Tget_member_type(t.Get(), static_cast<unsigned>(index)),
|
||||
"Cannot read compound member."),
|
||||
H5Tclose};
|
||||
}
|
||||
/// @brief Checks typed compound columns, including fixed-array element extents.
|
||||
template <class T>
|
||||
void Column(hid_t d, const char* name, const std::vector<T>& expected,
|
||||
hid_t primitive, hid_t native, hsize_t extent = 1) {
|
||||
auto disk = Field(d, name);
|
||||
Hdf5Handle fa, ma;
|
||||
hid_t ft = primitive, mt = native;
|
||||
if (extent != 1) {
|
||||
fa = {RequireHdf5Id(H5Tarray_create2(primitive, 1, &extent),
|
||||
"Cannot create file array projection."),
|
||||
H5Tclose};
|
||||
ma = {RequireHdf5Id(H5Tarray_create2(native, 1, &extent),
|
||||
"Cannot create memory array projection."),
|
||||
H5Tclose};
|
||||
ft = fa.Get();
|
||||
mt = ma.Get();
|
||||
}
|
||||
Require(H5Tequal(disk.Get(), ft) > 0,
|
||||
std::string{name} + ": wrong compound member type/extent");
|
||||
Hdf5Handle memory{
|
||||
RequireHdf5Id(
|
||||
H5Tcreate(H5T_COMPOUND, sizeof(T) * static_cast<std::size_t>(extent)),
|
||||
"Cannot create numeric projection."),
|
||||
H5Tclose};
|
||||
RequireHdf5(H5Tinsert(memory.Get(), name, 0, mt),
|
||||
"Cannot project numeric field.");
|
||||
std::vector<T> values(expected.size());
|
||||
if (!values.empty())
|
||||
RequireHdf5(
|
||||
H5Dread(d, memory.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data()),
|
||||
"Cannot read compound column.");
|
||||
Require(values == expected,
|
||||
std::string{name} + ": compound values or identities differ");
|
||||
}
|
||||
/// @brief Checks a named string column with independent expected source
|
||||
/// strings.
|
||||
void ColumnText(hid_t d, const char* name,
|
||||
const std::vector<std::string>& expected) {
|
||||
auto type = Field(d, name);
|
||||
Strings(
|
||||
type.Get(), expected,
|
||||
[&](hid_t t, void* p) {
|
||||
return H5Dread(d, t, H5S_ALL, H5S_ALL, H5P_DEFAULT, p);
|
||||
},
|
||||
true, name);
|
||||
}
|
||||
/// @brief Checks unsigned identity columns without depending on C++ row layout.
|
||||
void Id(hid_t d, const char* name, const std::vector<U64>& expected) {
|
||||
Column(d, name, expected, H5T_STD_U64LE, H5T_NATIVE_UINT64);
|
||||
}
|
||||
/// @brief Checks finite double columns against immutable Domain/history values.
|
||||
void Real(hid_t d, const char* name, const std::vector<double>& expected) {
|
||||
Column(d, name, expected, H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE);
|
||||
}
|
||||
/// @brief Requires exact optional-child absence without treating API errors as
|
||||
/// absence.
|
||||
void Absent(hid_t file, const std::string& parent, const char* child) {
|
||||
const htri_t exists = H5Lexists(file, parent.c_str(), H5P_DEFAULT);
|
||||
Require(exists >= 0, "Cannot inspect optional recovery parent.");
|
||||
if (exists == 0) return;
|
||||
Require(H5Lexists(file, (parent + "/" + child).c_str(), H5P_DEFAULT) == 0,
|
||||
"Initial recovery is present or cannot be inspected.");
|
||||
}
|
||||
/// @brief Checks source tables against Domain rows, including global material
|
||||
/// IDs.
|
||||
void ModelTables(hid_t f, const Domain& domain) {
|
||||
std::vector<U64> ids, connectivity, sections, materials, lines;
|
||||
std::vector<std::string> instances, labels, types, formulations, names, files,
|
||||
elsets;
|
||||
std::vector<double> xyz, k, rho, c, h;
|
||||
for (std::size_t i = 0; i < domain.Nodes().size(); ++i) {
|
||||
const auto& node = domain.Nodes()[i];
|
||||
ids.push_back(i);
|
||||
instances.push_back(node.source_id.instance_name);
|
||||
labels.push_back(node.source_id.source_label_text);
|
||||
xyz.insert(xyz.end(), node.coordinates.begin(), node.coordinates.end());
|
||||
}
|
||||
auto nodes = Dataset(f, "/model/nodes", {ids.size()});
|
||||
Id(nodes.Get(), "internal_node_id", ids);
|
||||
ColumnText(nodes.Get(), "instance_name", instances);
|
||||
ColumnText(nodes.Get(), "source_label", labels);
|
||||
Column(nodes.Get(), "coordinates", xyz, H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE, 3);
|
||||
Text(nodes.Get(), "units_label", "length");
|
||||
ids.clear();
|
||||
instances.clear();
|
||||
labels.clear();
|
||||
for (std::size_t i = 0; i < domain.ThermalElements().Size(); ++i) {
|
||||
const auto& e = domain.ThermalElements()[i];
|
||||
ids.push_back(i);
|
||||
instances.push_back(e.SourceId().instance_name);
|
||||
labels.push_back(e.SourceId().source_label_text);
|
||||
types.push_back("DS4");
|
||||
formulations.push_back("FESA-Q4-P2x2-thermal-shell");
|
||||
for (const auto node : e.NodeIndices()) connectivity.push_back(node);
|
||||
sections.push_back(e.ThermalSectionIndex());
|
||||
materials.push_back(e.MaterialIndex());
|
||||
}
|
||||
auto elements = Dataset(f, "/model/elements", {ids.size()});
|
||||
Id(elements.Get(), "internal_element_id", ids);
|
||||
ColumnText(elements.Get(), "instance_name", instances);
|
||||
ColumnText(elements.Get(), "source_label", labels);
|
||||
ColumnText(elements.Get(), "source_element_type", types);
|
||||
ColumnText(elements.Get(), "internal_formulation", formulations);
|
||||
Column(elements.Get(), "node_internal_ids", connectivity, H5T_STD_U64LE,
|
||||
H5T_NATIVE_UINT64, 4);
|
||||
Id(elements.Get(), "shell_section_internal_id", sections);
|
||||
Id(elements.Get(), "material_internal_id", materials);
|
||||
ids.clear();
|
||||
for (std::size_t i = 0; i < domain.ThermalMaterials().Size(); ++i) {
|
||||
const auto& material = domain.ThermalMaterials()[i];
|
||||
ids.push_back(GlobalThermalMaterialId(domain, i));
|
||||
names.push_back(material.Name());
|
||||
k.push_back(material.Conductivity());
|
||||
rho.push_back(material.Density());
|
||||
c.push_back(material.SpecificHeat());
|
||||
}
|
||||
auto mat = Dataset(f, "/model/thermal_materials", {ids.size()});
|
||||
Id(mat.Get(), "internal_material_id", ids);
|
||||
ColumnText(mat.Get(), "name", names);
|
||||
Real(mat.Get(), "conductivity", k);
|
||||
Real(mat.Get(), "density", rho);
|
||||
Real(mat.Get(), "specific_heat", c);
|
||||
Text(mat.Get(), "conductivity_units_label", "power/(length*temperature)");
|
||||
Text(mat.Get(), "density_units_label", "mass/volume");
|
||||
Text(mat.Get(), "specific_heat_units_label", "energy/(mass*temperature)");
|
||||
ids.clear();
|
||||
materials.clear();
|
||||
for (std::size_t i = 0; i < domain.ThermalSections().size(); ++i) {
|
||||
const auto& section = domain.ThermalSections()[i];
|
||||
ids.push_back(i);
|
||||
files.push_back(NormalizedPathString(section.location.file));
|
||||
lines.push_back(section.location.line);
|
||||
elsets.push_back(section.name);
|
||||
materials.push_back(
|
||||
GlobalThermalMaterialId(domain, section.material_index));
|
||||
h.push_back(section.thickness);
|
||||
}
|
||||
auto sec = Dataset(f, "/model/shell_sections", {ids.size()});
|
||||
Id(sec.Get(), "internal_section_id", ids);
|
||||
ColumnText(sec.Get(), "source_file", files);
|
||||
Id(sec.Get(), "source_line", lines);
|
||||
ColumnText(sec.Get(), "source_elset", elsets);
|
||||
Id(sec.Get(), "material_internal_id", materials);
|
||||
Real(sec.Get(), "thickness", h);
|
||||
Text(sec.Get(), "thickness_units_label", "length");
|
||||
}
|
||||
/// @brief Checks all scalar metadata values, including actual input byte
|
||||
/// identity.
|
||||
void Metadata(hid_t file, const Domain& domain,
|
||||
const ThermalAnalysisState& state) {
|
||||
Hdf5Handle m{RequireHdf5Id(H5Gopen2(file, "/metadata", H5P_DEFAULT),
|
||||
"Missing metadata."),
|
||||
H5Gclose};
|
||||
Number(m.Get(), "schema_version", H5T_STD_U64LE, H5T_NATIVE_UINT64, U64{1});
|
||||
Text(m.Get(), "feature_id", "shell-transient-heat-transfer");
|
||||
Text(m.Get(), "analysis_procedure", "uncoupled-transient-heat-transfer");
|
||||
Text(m.Get(), "source_step_name",
|
||||
domain.ThermalStep()->name.empty() ? "Step-1"
|
||||
: domain.ThermalStep()->name);
|
||||
Text(m.Get(), "solver_version", std::string{SolverVersion()});
|
||||
Text(m.Get(), "source_input_identity", SourceInputIdentity(domain));
|
||||
Text(m.Get(), "unit_system_label", "user-consistent-unspecified");
|
||||
Text(m.Get(), "internal_formulation", "FESA-Q4-P2x2-thermal-shell");
|
||||
Text(m.Get(), "integration_rule", "surface-3x3-gauss; thickness-exact-P2x2");
|
||||
Text(m.Get(), "time_integration", "backward-euler");
|
||||
Number(m.Get(), "nominal_time_increment", H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE,
|
||||
state.NominalTimeIncrement());
|
||||
Number(m.Get(), "time_period", H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE,
|
||||
state.TimePeriod());
|
||||
}
|
||||
/// @brief Checks active maps, station/trace conventions and the global-axis R
|
||||
/// layout.
|
||||
void Mappings(hid_t f, const ThermalAnalysisState& s) {
|
||||
const auto u = [&](const char* p, std::vector<U64> v) {
|
||||
auto d = Dataset(f, p, {v.size()});
|
||||
Values(d.Get(), H5T_STD_U64LE, H5T_NATIVE_UINT64, v);
|
||||
};
|
||||
const auto real = [&](const char* p, std::vector<hsize_t> sh,
|
||||
const std::vector<double>& v) {
|
||||
auto d = Dataset(f, p, sh);
|
||||
Values(d.Get(), H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE, v);
|
||||
};
|
||||
u("/model/thermal_node_ids", {s.NodeOrder().begin(), s.NodeOrder().end()});
|
||||
u("/model/thermal_element_ids",
|
||||
{s.ElementOrder().begin(), s.ElementOrder().end()});
|
||||
u("/model/temperature_dofs", {11, 12, 13, 14, 15});
|
||||
real("/model/temperature_zeta", {5}, {-1, -.5, 0, .5, 1});
|
||||
std::vector<std::uint8_t> mask;
|
||||
for (const auto& row : s.ConstraintMask())
|
||||
mask.insert(mask.end(), row.begin(), row.end());
|
||||
auto md =
|
||||
Dataset(f, "/model/thermal_constraint_mask", {s.NodeOrder().size(), 5});
|
||||
Values(md.Get(), H5T_STD_U8LE, H5T_NATIVE_UINT8, mask);
|
||||
std::vector<U64> nodes, dofs;
|
||||
for (const auto& r : s.ReactionDofs()) {
|
||||
nodes.push_back(r.node);
|
||||
dofs.push_back(r.source_dof);
|
||||
}
|
||||
auto reaction = Dataset(f, "/model/thermal_reaction_dofs", {nodes.size()});
|
||||
Id(reaction.Get(), "internal_node_id", nodes);
|
||||
Id(reaction.Get(), "source_dof", dofs);
|
||||
const double g = 1 / std::sqrt(3.);
|
||||
real("/model/thermal_flux_inplane_coordinates", {4, 2},
|
||||
{-g, -g, g, -g, g, g, -g, g});
|
||||
real("/model/thermal_flux_zeta", {6}, {-1, -.5, 0, 0, .5, 1});
|
||||
auto side = Dataset(f, "/model/thermal_flux_trace_side", {6});
|
||||
Values(side.Get(), H5T_STD_I8LE, H5T_NATIVE_INT8,
|
||||
std::vector<std::int8_t>{1, 0, -1, 1, 0, -1});
|
||||
std::vector<double> frames;
|
||||
for (const auto& element : s.FluxLocalFrames())
|
||||
for (const auto& point : element)
|
||||
for (std::size_t component = 0; component < 3; ++component)
|
||||
for (std::size_t axis = 0; axis < 3; ++axis)
|
||||
frames.push_back(point[axis][component]);
|
||||
real("/model/thermal_flux_local_frames", {s.ElementOrder().size(), 4, 3, 3},
|
||||
frames);
|
||||
}
|
||||
/// @brief Checks result values and exact component/unit/location attributes.
|
||||
void ResultValues(hid_t file, const std::string& path,
|
||||
std::vector<hsize_t> shape, const std::vector<double>& values,
|
||||
const char* components, const char* units,
|
||||
const char* coordinates, const char* location) {
|
||||
auto dataset = Dataset(file, path, shape);
|
||||
Values(dataset.Get(), H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE, values);
|
||||
Text(dataset.Get(), "component_order", components);
|
||||
Text(dataset.Get(), "units_label", units);
|
||||
Text(dataset.Get(), "coordinate_system", coordinates);
|
||||
Text(dataset.Get(), "location", location);
|
||||
}
|
||||
/// @brief Checks frame inventory and actual equation deltas without recomputing
|
||||
/// time differences.
|
||||
void Frames(hid_t f, const ThermalAnalysisState& s) {
|
||||
std::vector<U64> index, increment;
|
||||
std::vector<double> time, delta;
|
||||
std::vector<std::uint8_t> initial;
|
||||
for (const auto& row : s.AcceptedFrames()) {
|
||||
index.push_back(row.frame_index);
|
||||
increment.push_back(row.increment_index);
|
||||
time.push_back(row.time);
|
||||
delta.push_back(row.time_increment);
|
||||
initial.push_back(row.is_initial);
|
||||
}
|
||||
auto table = Dataset(f, "/steps/Step-1/accepted_frames", {index.size()});
|
||||
Id(table.Get(), "frame_index", index);
|
||||
Id(table.Get(), "increment_index", increment);
|
||||
Real(table.Get(), "time", time);
|
||||
Real(table.Get(), "time_increment", delta);
|
||||
Column(table.Get(), "is_initial", initial, H5T_STD_U8LE, H5T_NATIVE_UINT8);
|
||||
Text(table.Get(), "time_units_label", "time");
|
||||
Hdf5Handle group{
|
||||
RequireHdf5Id(H5Gopen2(f, "/steps/Step-1/frames", H5P_DEFAULT),
|
||||
"Missing frames group."),
|
||||
H5Gclose};
|
||||
H5G_info_t info{};
|
||||
RequireHdf5(H5Gget_info(group.Get(), &info), "Cannot enumerate frames.");
|
||||
Require(info.nlinks == index.size(), "Missing or extra frame groups.");
|
||||
for (const auto& frame : s.AcceptedFrames()) {
|
||||
const auto root =
|
||||
"/steps/Step-1/frames/" + std::to_string(frame.frame_index);
|
||||
Hdf5Handle g{
|
||||
RequireHdf5Id(H5Gopen2(f, root.c_str(), H5P_DEFAULT), "Missing frame."),
|
||||
H5Gclose};
|
||||
Number(g.Get(), "frame_index", H5T_STD_U64LE, H5T_NATIVE_UINT64,
|
||||
frame.frame_index);
|
||||
Number(g.Get(), "increment_index", H5T_STD_U64LE, H5T_NATIVE_UINT64,
|
||||
frame.increment_index);
|
||||
Number(g.Get(), "time", H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE, frame.time);
|
||||
Number(g.Get(), "time_increment", H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE,
|
||||
frame.time_increment);
|
||||
Number(g.Get(), "is_initial", H5T_STD_U8LE, H5T_NATIVE_UINT8,
|
||||
static_cast<std::uint8_t>(frame.is_initial));
|
||||
ResultValues(f, root + "/nodal/temperature", {s.NodeOrder().size(), 5},
|
||||
{frame.temperature.Data(),
|
||||
frame.temperature.Data() + frame.temperature.Size()},
|
||||
"NT11,NT12,NT13,NT14,NT15", "temperature", "scalar",
|
||||
"nodal-thickness-stations");
|
||||
if (frame.is_initial) {
|
||||
Absent(f, root + "/nodal", "reaction_heat_rate");
|
||||
Absent(f, root + "/element", "heat_flux");
|
||||
} else {
|
||||
const auto& payload = *frame.completed;
|
||||
std::vector<double> reaction(payload.reaction_heat_rate.Size());
|
||||
for (std::size_t i = 0; i < reaction.size(); ++i)
|
||||
reaction[i] = payload.reaction_heat_rate[i];
|
||||
ResultValues(f, root + "/nodal/reaction_heat_rate", {reaction.size()},
|
||||
reaction, "RFL", "power", "scalar",
|
||||
"prescribed-temperature-dofs");
|
||||
auto rd =
|
||||
Dataset(f, root + "/nodal/reaction_heat_rate", {reaction.size()});
|
||||
Text(rd.Get(), "positive_direction", "into-model");
|
||||
std::vector<double> flux;
|
||||
for (const auto& element : payload.global_heat_flux)
|
||||
for (const auto& q : element)
|
||||
for (const double value : q.Components()) flux.push_back(value);
|
||||
ResultValues(f, root + "/element/heat_flux",
|
||||
{s.ElementOrder().size(), 4, 6, 3}, flux, "QX,QY,QZ",
|
||||
"power/area", "global-cartesian",
|
||||
"element-inplane-thickness-traces");
|
||||
}
|
||||
}
|
||||
}
|
||||
/// @brief Checks sorted diagnostics with all established provenance fields.
|
||||
void Diagnostics(hid_t f, std::vector<Diagnostic> diagnostics) {
|
||||
SortDiagnostics(diagnostics);
|
||||
std::vector<std::string> severity, code, file, keyword, entity, message;
|
||||
std::vector<U64> line;
|
||||
for (const auto& d : diagnostics) {
|
||||
severity.push_back(d.severity == Severity::kWarning ? "warning" : "error");
|
||||
code.push_back(d.code);
|
||||
file.push_back(NormalizedPathString(d.location.file));
|
||||
line.push_back(d.location.line);
|
||||
keyword.push_back(d.keyword);
|
||||
entity.push_back(d.entity_identity);
|
||||
message.push_back(d.message);
|
||||
}
|
||||
auto dataset = Dataset(f, "/diagnostics", {diagnostics.size()});
|
||||
ColumnText(dataset.Get(), "severity", severity);
|
||||
ColumnText(dataset.Get(), "code", code);
|
||||
ColumnText(dataset.Get(), "file", file);
|
||||
Id(dataset.Get(), "line", line);
|
||||
ColumnText(dataset.Get(), "keyword", keyword);
|
||||
ColumnText(dataset.Get(), "entity_identity", entity);
|
||||
ColumnText(dataset.Get(), "message", message);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
U64 GlobalThermalMaterialId(const Domain& domain, std::size_t local) {
|
||||
Require(local < domain.ThermalMaterials().Size(),
|
||||
"Invalid local thermal material index.");
|
||||
const auto* thermal = &domain.ThermalMaterials()[local];
|
||||
for (std::size_t i = 0; i < domain.Materials().Size(); ++i)
|
||||
if (&domain.Materials()[i] == thermal) return i;
|
||||
throw Hdf5Failure{"Thermal material has no global Domain identity."};
|
||||
}
|
||||
|
||||
Status ValidateThermalOutput(const Domain& domain,
|
||||
const ThermalAnalysisState& state) {
|
||||
try {
|
||||
Require(state.IsComplete() && domain.ThermalStep() != nullptr &&
|
||||
domain.Steps().Size() == 0,
|
||||
"Thermal output requires one complete thermal history.");
|
||||
auto model = AnalysisModel::Create(domain);
|
||||
Require(model.HasValue() && model.Value().IsThermal(),
|
||||
"Invalid thermal Domain.");
|
||||
auto dofs = DofManager::CreateThermal(model.Value());
|
||||
Require(dofs.HasValue(), "Invalid thermal equation inventory.");
|
||||
Require(state.NodeOrder() == model.Value().ActiveThermalNodes() &&
|
||||
state.ElementOrder() == model.Value().ActiveThermalElements(),
|
||||
"Active Domain/state inventories differ.");
|
||||
Require(state.NominalTimeIncrement() ==
|
||||
domain.ThermalStep()->nominal_increment &&
|
||||
state.TimePeriod() == domain.ThermalStep()->time_period,
|
||||
"Domain/state schedules differ.");
|
||||
const auto grid =
|
||||
ThermalTimeGrid::Create(domain.ThermalStep()->time_period,
|
||||
domain.ThermalStep()->nominal_increment,
|
||||
domain.ThermalStep()->maximum_increments);
|
||||
Require(grid.HasValue() && grid.Value().Count() == state.TimeGrid().Count(),
|
||||
"Domain/state increment inventory differs.");
|
||||
for (std::size_t i = 0; i < state.NodeOrder().size(); ++i)
|
||||
Require(SameIdentity(state.NodeSourceIds()[i],
|
||||
domain.Nodes()[state.NodeOrder()[i]].source_id),
|
||||
"Node source identity differs.");
|
||||
for (std::size_t i = 0; i < state.ElementOrder().size(); ++i)
|
||||
Require(SameIdentity(
|
||||
state.ElementSourceIds()[i],
|
||||
domain.ThermalElements()[state.ElementOrder()[i]].SourceId()),
|
||||
"Element source identity differs.");
|
||||
std::vector<std::array<std::uint8_t, 5>> mask(state.NodeOrder().size());
|
||||
for (const auto row : dofs.Value().ConstrainedDofs())
|
||||
mask[row / 5][row % 5] = 1;
|
||||
Require(mask == state.ConstraintMask(),
|
||||
"Prescribed temperature inventory differs.");
|
||||
for (const auto& frame : state.AcceptedFrames())
|
||||
if (!frame.is_initial)
|
||||
for (std::size_t i = 0; i < dofs.Value().ConstrainedDofs().size(); ++i)
|
||||
Require(frame.temperature[dofs.Value().ConstrainedDofs()[i]] ==
|
||||
dofs.Value().PrescribedValues()[i],
|
||||
"Completed prescribed temperature differs from Domain.");
|
||||
Vector raw{dofs.Value().FullDofCount()};
|
||||
for (const auto& initial : domain.ThermalInitialConditions()) {
|
||||
const auto at = std::find(state.NodeOrder().begin(),
|
||||
state.NodeOrder().end(), initial.node_index);
|
||||
if (at != state.NodeOrder().end()) {
|
||||
const auto row =
|
||||
static_cast<std::size_t>(at - state.NodeOrder().begin());
|
||||
for (std::size_t station = 0; station < 5; ++station)
|
||||
raw[5 * row + station] = initial.station_values[station];
|
||||
}
|
||||
}
|
||||
const auto& actual = state.AcceptedFrames().front().temperature;
|
||||
Require(actual.Size() == raw.Size(), "Raw initial shape differs.");
|
||||
for (std::size_t i = 0; i < raw.Size(); ++i)
|
||||
Require(actual[i] == raw[i],
|
||||
"Raw initial temperatures differ from Domain.");
|
||||
return Status::Ok();
|
||||
} catch (const std::exception& e) {
|
||||
return OutputFailure("invalid-thermal-output-inventory", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
Status SelfCheckThermalFile(const std::filesystem::path& path,
|
||||
const Domain& domain,
|
||||
const ThermalAnalysisState& state,
|
||||
const std::vector<Diagnostic>& diagnostics,
|
||||
herr_t (*close_reader)(hid_t)) {
|
||||
const auto inventory = ValidateThermalOutput(domain, state);
|
||||
if (!inventory.IsOk()) return inventory;
|
||||
try {
|
||||
Hdf5ErrorSilencer silence;
|
||||
Require(close_reader != nullptr, "Missing thermal reader closer.");
|
||||
Hdf5Handle file{RequireHdf5Id(H5Fopen(path.string().c_str(), H5F_ACC_RDONLY,
|
||||
H5P_DEFAULT),
|
||||
"Cannot reopen thermal candidate."),
|
||||
close_reader};
|
||||
Metadata(file.Get(), domain, state);
|
||||
ModelTables(file.Get(), domain);
|
||||
Mappings(file.Get(), state);
|
||||
Frames(file.Get(), state);
|
||||
Diagnostics(file.Get(), diagnostics);
|
||||
RequireHdf5(file.CloseChecked(), "Cannot close checked thermal candidate.");
|
||||
return Status::Ok();
|
||||
} catch (const std::exception& e) {
|
||||
return OutputFailure("invalid-thermal-hdf5-candidate", e.what());
|
||||
}
|
||||
}
|
||||
} // namespace fesa::hdf5_internal
|
||||
Reference in New Issue
Block a user