feat(thermal): publish fully validated HDF5 histories atomically
This commit is contained in:
@@ -0,0 +1,19 @@
|
|||||||
|
#ifndef FESA_IO_HDF5_THERMAL_HDF5_RESULTS_WRITER_H_
|
||||||
|
#define FESA_IO_HDF5_THERMAL_HDF5_RESULTS_WRITER_H_
|
||||||
|
|
||||||
|
#include "fesa/results/thermal_results_writer.h"
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
/// @brief Writes schema-v1 shell transient-heat results after closed-file
|
||||||
|
/// validation.
|
||||||
|
class ThermalHdf5ResultsWriter final : public ThermalResultsWriter {
|
||||||
|
public:
|
||||||
|
/// @brief Writes the complete schema-v1 thermal artifact without replacing a
|
||||||
|
/// prior final file until closed-file validation succeeds.
|
||||||
|
Status Write(const std::filesystem::path& output_path, const Domain& domain,
|
||||||
|
const ThermalAnalysisState& state,
|
||||||
|
const std::vector<Diagnostic>& diagnostics) override;
|
||||||
|
};
|
||||||
|
} // namespace fesa
|
||||||
|
|
||||||
|
#endif // FESA_IO_HDF5_THERMAL_HDF5_RESULTS_WRITER_H_
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#ifndef FESA_RESULTS_THERMAL_RESULTS_WRITER_H_
|
||||||
|
#define FESA_RESULTS_THERMAL_RESULTS_WRITER_H_
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/analysis/thermal_analysis_state.h"
|
||||||
|
#include "fesa/core/diagnostic.h"
|
||||||
|
#include "fesa/core/status.h"
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
class Domain;
|
||||||
|
|
||||||
|
/// @brief Persists a complete accepted thermal history atomically.
|
||||||
|
class ThermalResultsWriter {
|
||||||
|
public:
|
||||||
|
virtual ~ThermalResultsWriter() = default;
|
||||||
|
/// @brief Writes every accepted thermal frame and diagnostics atomically.
|
||||||
|
/// @note The Domain and immutable state must outlive this call.
|
||||||
|
virtual Status Write(const std::filesystem::path& output_path,
|
||||||
|
const Domain& domain, const ThermalAnalysisState& state,
|
||||||
|
const std::vector<Diagnostic>& diagnostics) = 0;
|
||||||
|
};
|
||||||
|
} // namespace fesa
|
||||||
|
|
||||||
|
#endif // FESA_RESULTS_THERMAL_RESULTS_WRITER_H_
|
||||||
@@ -8,6 +8,9 @@
|
|||||||
|
|
||||||
namespace fesa::hdf5_internal {
|
namespace fesa::hdf5_internal {
|
||||||
|
|
||||||
|
/// @brief Writes every Domain node preserving its source identity.
|
||||||
|
void WriteNodes(hid_t file, const Domain& domain);
|
||||||
|
|
||||||
/// @brief Writes metadata and exact model/source identity datasets.
|
/// @brief Writes metadata and exact model/source identity datasets.
|
||||||
void WriteModel(hid_t file, const Domain& domain,
|
void WriteModel(hid_t file, const Domain& domain,
|
||||||
const WriterModelData& model_data);
|
const WriterModelData& model_data);
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
|
|
||||||
namespace fesa::hdf5_internal {
|
namespace fesa::hdf5_internal {
|
||||||
|
|
||||||
|
/// @brief Writes deterministically ordered diagnostics using the common schema.
|
||||||
|
void WriteDiagnostics(hid_t file, const std::vector<Diagnostic>& diagnostics);
|
||||||
|
|
||||||
/// @brief Builds the complete backend-neutral result dataset inventory.
|
/// @brief Builds the complete backend-neutral result dataset inventory.
|
||||||
Result<WriterPlan> BuildWriterPlan(const std::filesystem::path& output_path,
|
Result<WriterPlan> BuildWriterPlan(const std::filesystem::path& output_path,
|
||||||
const Domain& domain,
|
const Domain& domain,
|
||||||
|
|||||||
@@ -0,0 +1,445 @@
|
|||||||
|
#include "fesa/io/hdf5/thermal_hdf5_results_writer.h"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <exception>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/build_info.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"
|
||||||
|
#include "io/hdf5/hdf5_result_writer.h"
|
||||||
|
#include "io/hdf5/thermal_hdf5_self_check.h"
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
namespace {
|
||||||
|
using hdf5_internal::Hdf5Handle;
|
||||||
|
|
||||||
|
/// @brief Carries node/source-DOF reaction identities in stable row order.
|
||||||
|
struct ReactionRow {
|
||||||
|
std::uint64_t internal_node_id;
|
||||||
|
std::uint64_t source_dof;
|
||||||
|
};
|
||||||
|
/// @brief Carries accepted schedule values independently of stored-time
|
||||||
|
/// differences.
|
||||||
|
struct FrameRow {
|
||||||
|
std::uint64_t frame_index;
|
||||||
|
std::uint64_t increment_index;
|
||||||
|
double time;
|
||||||
|
double time_increment;
|
||||||
|
std::uint8_t is_initial;
|
||||||
|
};
|
||||||
|
/// @brief Carries thermal properties with global Domain material identities.
|
||||||
|
struct MaterialRow {
|
||||||
|
std::uint64_t internal_material_id;
|
||||||
|
const char* name;
|
||||||
|
double conductivity;
|
||||||
|
double density;
|
||||||
|
double specific_heat;
|
||||||
|
};
|
||||||
|
/// @brief Carries section provenance and global material linkage.
|
||||||
|
struct SectionRow {
|
||||||
|
std::uint64_t internal_section_id;
|
||||||
|
const char* source_file;
|
||||||
|
std::uint64_t source_line;
|
||||||
|
const char* source_elset;
|
||||||
|
std::uint64_t material_internal_id;
|
||||||
|
double thickness;
|
||||||
|
};
|
||||||
|
/// @brief Carries DS4 source identities and fixed four-node connectivity.
|
||||||
|
struct ElementRow {
|
||||||
|
std::uint64_t internal_element_id;
|
||||||
|
const char* instance_name;
|
||||||
|
const char* source_label;
|
||||||
|
const char* source_element_type;
|
||||||
|
const char* internal_formulation;
|
||||||
|
std::uint64_t node_internal_ids[4];
|
||||||
|
std::uint64_t shell_section_internal_id;
|
||||||
|
std::uint64_t material_internal_id;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// @brief Writes fixed named fields, preserving zero-length reaction tables.
|
||||||
|
template <class Row>
|
||||||
|
Hdf5Handle Compound(hid_t file, const char* path, const std::vector<Row>& rows,
|
||||||
|
hid_t file_type, hid_t memory_type) {
|
||||||
|
if (rows.empty()) {
|
||||||
|
const hsize_t zero[] = {0U};
|
||||||
|
const hsize_t unlimited[] = {H5S_UNLIMITED};
|
||||||
|
Hdf5Handle space{
|
||||||
|
hdf5_internal::RequireHdf5Id(H5Screate_simple(1, zero, unlimited),
|
||||||
|
"Unable to create empty compound space."),
|
||||||
|
H5Sclose};
|
||||||
|
Hdf5Handle properties{hdf5_internal::RequireHdf5Id(
|
||||||
|
H5Pcreate(H5P_DATASET_CREATE),
|
||||||
|
"Unable to create empty compound properties."),
|
||||||
|
H5Pclose};
|
||||||
|
const hsize_t chunk[] = {1U};
|
||||||
|
hdf5_internal::RequireHdf5(H5Pset_chunk(properties.Get(), 1, chunk),
|
||||||
|
"Unable to configure empty compound chunk.");
|
||||||
|
return Hdf5Handle{
|
||||||
|
hdf5_internal::RequireHdf5Id(
|
||||||
|
H5Dcreate2(file, path, file_type, space.Get(), H5P_DEFAULT,
|
||||||
|
properties.Get(), H5P_DEFAULT),
|
||||||
|
"Unable to create empty compound HDF5 dataset."),
|
||||||
|
H5Dclose};
|
||||||
|
}
|
||||||
|
return hdf5_internal::WriteCompoundDataset(file, path, rows.size(), file_type,
|
||||||
|
memory_type, rows.data());
|
||||||
|
}
|
||||||
|
/// @brief Defines an explicit named field without making padding a read
|
||||||
|
/// contract.
|
||||||
|
void Member(hid_t t, const char* n, size_t off, hid_t f) {
|
||||||
|
hdf5_internal::RequireHdf5(H5Tinsert(t, n, off, f),
|
||||||
|
"Unable to define compound field.");
|
||||||
|
}
|
||||||
|
/// @brief Builds fixed-width connectivity array datatypes.
|
||||||
|
Hdf5Handle Array(hid_t type, int rank, const hsize_t* dims) {
|
||||||
|
return Hdf5Handle{
|
||||||
|
hdf5_internal::RequireHdf5Id(H5Tarray_create2(type, rank, dims),
|
||||||
|
"Unable to create array type."),
|
||||||
|
H5Tclose};
|
||||||
|
}
|
||||||
|
/// @brief Separates fixed little-endian file fields from native memory fields.
|
||||||
|
template <class Row, class Init>
|
||||||
|
void Types(Hdf5Handle& ft, Hdf5Handle& mt, Init init) {
|
||||||
|
ft = Hdf5Handle{
|
||||||
|
hdf5_internal::RequireHdf5Id(H5Tcreate(H5T_COMPOUND, sizeof(Row)),
|
||||||
|
"Unable to create file compound."),
|
||||||
|
H5Tclose};
|
||||||
|
mt = Hdf5Handle{
|
||||||
|
hdf5_internal::RequireHdf5Id(H5Tcreate(H5T_COMPOUND, sizeof(Row)),
|
||||||
|
"Unable to create memory compound."),
|
||||||
|
H5Tclose};
|
||||||
|
init(ft.Get(), mt.Get());
|
||||||
|
}
|
||||||
|
/// @brief Writes all twelve thermal discriminator, source and schedule
|
||||||
|
/// attributes.
|
||||||
|
void Metadata(hid_t file, const Domain& domain,
|
||||||
|
const ThermalAnalysisState& state) {
|
||||||
|
auto g = hdf5_internal::CreateGroup(file, "/metadata");
|
||||||
|
auto a = [&](const char* n, const std::string& v) {
|
||||||
|
hdf5_internal::WriteStringAttribute(g.Get(), n, v);
|
||||||
|
};
|
||||||
|
hdf5_internal::WriteUint64Attribute(g.Get(), "schema_version", 1);
|
||||||
|
a("feature_id", "shell-transient-heat-transfer");
|
||||||
|
a("analysis_procedure", "uncoupled-transient-heat-transfer");
|
||||||
|
a("source_step_name",
|
||||||
|
domain.ThermalStep()->name.empty() ? "Step-1" : domain.ThermalStep()->name);
|
||||||
|
a("solver_version", std::string{SolverVersion()});
|
||||||
|
a("source_input_identity", hdf5_internal::SourceInputIdentity(domain));
|
||||||
|
a("unit_system_label", "user-consistent-unspecified");
|
||||||
|
a("internal_formulation", "FESA-Q4-P2x2-thermal-shell");
|
||||||
|
a("integration_rule", "surface-3x3-gauss; thickness-exact-P2x2");
|
||||||
|
a("time_integration", "backward-euler");
|
||||||
|
hdf5_internal::WriteDoubleAttribute(g.Get(), "nominal_time_increment",
|
||||||
|
state.NominalTimeIncrement());
|
||||||
|
hdf5_internal::WriteDoubleAttribute(g.Get(), "time_period",
|
||||||
|
state.TimePeriod());
|
||||||
|
}
|
||||||
|
/// @brief Writes all geometry with separate active thermal row maps and
|
||||||
|
/// recovery axes.
|
||||||
|
void WriteThermalModel(hid_t file, const Domain& d,
|
||||||
|
const ThermalAnalysisState& s) {
|
||||||
|
auto model = hdf5_internal::CreateGroup(file, "/model");
|
||||||
|
(void)model;
|
||||||
|
// Nodes retain all Domain rows; active result mappings remain separate.
|
||||||
|
Hdf5Handle ft, mt;
|
||||||
|
auto st = hdf5_internal::MakeUtf8StringType();
|
||||||
|
hdf5_internal::WriteNodes(file, d);
|
||||||
|
std::vector<ElementRow> elements;
|
||||||
|
for (size_t i = 0; i < d.ThermalElements().Size(); ++i) {
|
||||||
|
const auto& e = d.ThermalElements()[i];
|
||||||
|
elements.push_back({i,
|
||||||
|
e.SourceId().instance_name.c_str(),
|
||||||
|
e.SourceId().source_label_text.c_str(),
|
||||||
|
"DS4",
|
||||||
|
"FESA-Q4-P2x2-thermal-shell",
|
||||||
|
{e.NodeIndices()[0], e.NodeIndices()[1],
|
||||||
|
e.NodeIndices()[2], e.NodeIndices()[3]},
|
||||||
|
e.ThermalSectionIndex(),
|
||||||
|
e.MaterialIndex()});
|
||||||
|
}
|
||||||
|
hsize_t four[] = {4};
|
||||||
|
auto fu = Array(H5T_STD_U64LE, 1, four),
|
||||||
|
mu = Array(H5T_NATIVE_UINT64, 1, four);
|
||||||
|
Types<ElementRow>(ft, mt, [&](hid_t f, hid_t m) {
|
||||||
|
auto add = [&](hid_t t, hid_t u, hid_t ar) {
|
||||||
|
Member(t, "internal_element_id", HOFFSET(ElementRow, internal_element_id),
|
||||||
|
u);
|
||||||
|
Member(t, "instance_name", HOFFSET(ElementRow, instance_name), st.Get());
|
||||||
|
Member(t, "source_label", HOFFSET(ElementRow, source_label), st.Get());
|
||||||
|
Member(t, "source_element_type", HOFFSET(ElementRow, source_element_type),
|
||||||
|
st.Get());
|
||||||
|
Member(t, "internal_formulation",
|
||||||
|
HOFFSET(ElementRow, internal_formulation), st.Get());
|
||||||
|
Member(t, "node_internal_ids", HOFFSET(ElementRow, node_internal_ids),
|
||||||
|
ar);
|
||||||
|
Member(t, "shell_section_internal_id",
|
||||||
|
HOFFSET(ElementRow, shell_section_internal_id), u);
|
||||||
|
Member(t, "material_internal_id",
|
||||||
|
HOFFSET(ElementRow, material_internal_id), u);
|
||||||
|
};
|
||||||
|
add(f, H5T_STD_U64LE, fu.Get());
|
||||||
|
add(m, H5T_NATIVE_UINT64, mu.Get());
|
||||||
|
});
|
||||||
|
(void)Compound(file, "/model/elements", elements, ft.Get(), mt.Get());
|
||||||
|
std::vector<MaterialRow> mats;
|
||||||
|
for (size_t i = 0; i < d.ThermalMaterials().Size(); ++i) {
|
||||||
|
const auto& m = d.ThermalMaterials()[i];
|
||||||
|
size_t id = 0;
|
||||||
|
for (; id < d.Materials().Size(); ++id)
|
||||||
|
if (&d.Materials()[id] == &m) break;
|
||||||
|
mats.push_back({id, m.Name().c_str(), m.Conductivity(), m.Density(),
|
||||||
|
m.SpecificHeat()});
|
||||||
|
}
|
||||||
|
Types<MaterialRow>(ft, mt, [&](hid_t f, hid_t m) {
|
||||||
|
auto add = [&](hid_t t, hid_t u, hid_t q) {
|
||||||
|
Member(t, "internal_material_id",
|
||||||
|
HOFFSET(MaterialRow, internal_material_id), u);
|
||||||
|
Member(t, "name", HOFFSET(MaterialRow, name), st.Get());
|
||||||
|
Member(t, "conductivity", HOFFSET(MaterialRow, conductivity), q);
|
||||||
|
Member(t, "density", HOFFSET(MaterialRow, density), q);
|
||||||
|
Member(t, "specific_heat", HOFFSET(MaterialRow, specific_heat), q);
|
||||||
|
};
|
||||||
|
add(f, H5T_STD_U64LE, H5T_IEEE_F64LE);
|
||||||
|
add(m, H5T_NATIVE_UINT64, H5T_NATIVE_DOUBLE);
|
||||||
|
});
|
||||||
|
auto md =
|
||||||
|
Compound(file, "/model/thermal_materials", mats, ft.Get(), mt.Get());
|
||||||
|
hdf5_internal::WriteStringAttribute(md.Get(), "conductivity_units_label",
|
||||||
|
"power/(length*temperature)");
|
||||||
|
hdf5_internal::WriteStringAttribute(md.Get(), "density_units_label",
|
||||||
|
"mass/volume");
|
||||||
|
hdf5_internal::WriteStringAttribute(md.Get(), "specific_heat_units_label",
|
||||||
|
"energy/(mass*temperature)");
|
||||||
|
std::vector<SectionRow> secs;
|
||||||
|
std::vector<std::string> files;
|
||||||
|
files.reserve(d.ThermalSections().size());
|
||||||
|
|
||||||
|
for (size_t i = 0; i < d.ThermalSections().size(); ++i) {
|
||||||
|
const auto& q = d.ThermalSections()[i];
|
||||||
|
files.push_back(hdf5_internal::NormalizedPathString(q.location.file));
|
||||||
|
secs.push_back({i, files.back().c_str(), q.location.line, q.name.c_str(),
|
||||||
|
hdf5_internal::GlobalThermalMaterialId(d, q.material_index),
|
||||||
|
q.thickness});
|
||||||
|
}
|
||||||
|
Types<SectionRow>(ft, mt, [&](hid_t f, hid_t m) {
|
||||||
|
auto add = [&](hid_t t, hid_t u, hid_t q) {
|
||||||
|
Member(t, "internal_section_id", HOFFSET(SectionRow, internal_section_id),
|
||||||
|
u);
|
||||||
|
Member(t, "source_file", HOFFSET(SectionRow, source_file), st.Get());
|
||||||
|
Member(t, "source_line", HOFFSET(SectionRow, source_line), u);
|
||||||
|
Member(t, "source_elset", HOFFSET(SectionRow, source_elset), st.Get());
|
||||||
|
Member(t, "material_internal_id",
|
||||||
|
HOFFSET(SectionRow, material_internal_id), u);
|
||||||
|
Member(t, "thickness", HOFFSET(SectionRow, thickness), q);
|
||||||
|
};
|
||||||
|
add(f, H5T_STD_U64LE, H5T_IEEE_F64LE);
|
||||||
|
add(m, H5T_NATIVE_UINT64, H5T_NATIVE_DOUBLE);
|
||||||
|
});
|
||||||
|
auto sd = Compound(file, "/model/shell_sections", secs, ft.Get(), mt.Get());
|
||||||
|
hdf5_internal::WriteStringAttribute(sd.Get(), "thickness_units_label",
|
||||||
|
"length");
|
||||||
|
std::vector<std::uint64_t> nids(s.NodeOrder().begin(), s.NodeOrder().end()),
|
||||||
|
eids(s.ElementOrder().begin(), s.ElementOrder().end());
|
||||||
|
auto U = [&](const char* p, const std::vector<std::uint64_t>& v) {
|
||||||
|
auto sp = hdf5_internal::CreateDatasetSpace({v.size()});
|
||||||
|
Hdf5Handle ds{hdf5_internal::RequireHdf5Id(
|
||||||
|
H5Dcreate2(file, p, H5T_STD_U64LE, sp.Get(), H5P_DEFAULT,
|
||||||
|
H5P_DEFAULT, H5P_DEFAULT),
|
||||||
|
"Unable to create IDs."),
|
||||||
|
H5Dclose};
|
||||||
|
if (!v.empty())
|
||||||
|
hdf5_internal::RequireHdf5(H5Dwrite(ds.Get(), H5T_NATIVE_UINT64, H5S_ALL,
|
||||||
|
H5S_ALL, H5P_DEFAULT, v.data()),
|
||||||
|
"Unable to write IDs.");
|
||||||
|
};
|
||||||
|
U("/model/thermal_node_ids", nids);
|
||||||
|
U("/model/thermal_element_ids", eids);
|
||||||
|
std::vector<std::uint64_t> dofs = {11, 12, 13, 14, 15};
|
||||||
|
U("/model/temperature_dofs", dofs);
|
||||||
|
const std::vector<double> zeta = {-1, -.5, 0, .5, 1},
|
||||||
|
coords = {-1 / std::sqrt(3.), -1 / std::sqrt(3.),
|
||||||
|
1 / std::sqrt(3.), -1 / std::sqrt(3.),
|
||||||
|
1 / std::sqrt(3.), 1 / std::sqrt(3.),
|
||||||
|
-1 / std::sqrt(3.), 1 / std::sqrt(3.)},
|
||||||
|
fz = {-1, -.5, 0, 0, .5, 1};
|
||||||
|
auto D = [&](const char* p, std::vector<hsize_t> sh,
|
||||||
|
const std::vector<double>& v) {
|
||||||
|
(void)hdf5_internal::WriteDoubleValues(file, p, sh, v.data(), v.size());
|
||||||
|
};
|
||||||
|
D("/model/temperature_zeta", {5}, zeta);
|
||||||
|
D("/model/thermal_flux_inplane_coordinates", {4, 2}, coords);
|
||||||
|
D("/model/thermal_flux_zeta", {6}, fz);
|
||||||
|
std::vector<std::int8_t> side = {1, 0, -1, 1, 0, -1};
|
||||||
|
auto sp = hdf5_internal::CreateDatasetSpace({6});
|
||||||
|
Hdf5Handle sid{
|
||||||
|
hdf5_internal::RequireHdf5Id(
|
||||||
|
H5Dcreate2(file, "/model/thermal_flux_trace_side", H5T_STD_I8LE,
|
||||||
|
sp.Get(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT),
|
||||||
|
"Unable to create trace side."),
|
||||||
|
H5Dclose};
|
||||||
|
hdf5_internal::RequireHdf5(H5Dwrite(sid.Get(), H5T_NATIVE_INT8, H5S_ALL,
|
||||||
|
H5S_ALL, H5P_DEFAULT, side.data()),
|
||||||
|
"Unable to write trace side.");
|
||||||
|
std::vector<std::uint8_t> mask;
|
||||||
|
for (const auto& r : s.ConstraintMask())
|
||||||
|
mask.insert(mask.end(), r.begin(), r.end());
|
||||||
|
hdf5_internal::WriteUint8Dataset(file, "/model/thermal_constraint_mask",
|
||||||
|
{s.NodeOrder().size(), 5}, mask.data(),
|
||||||
|
mask.size());
|
||||||
|
std::vector<ReactionRow> reactions;
|
||||||
|
for (const auto& r : s.ReactionDofs())
|
||||||
|
reactions.push_back({r.node, (std::uint64_t)r.source_dof});
|
||||||
|
Types<ReactionRow>(ft, mt, [&](hid_t f, hid_t m) {
|
||||||
|
Member(f, "internal_node_id", HOFFSET(ReactionRow, internal_node_id),
|
||||||
|
H5T_STD_U64LE);
|
||||||
|
Member(f, "source_dof", HOFFSET(ReactionRow, source_dof), H5T_STD_U64LE);
|
||||||
|
Member(m, "internal_node_id", HOFFSET(ReactionRow, internal_node_id),
|
||||||
|
H5T_NATIVE_UINT64);
|
||||||
|
Member(m, "source_dof", HOFFSET(ReactionRow, source_dof),
|
||||||
|
H5T_NATIVE_UINT64);
|
||||||
|
});
|
||||||
|
(void)Compound(file, "/model/thermal_reaction_dofs", reactions, ft.Get(),
|
||||||
|
mt.Get());
|
||||||
|
std::vector<double> frames;
|
||||||
|
for (const auto& e : s.FluxLocalFrames())
|
||||||
|
for (const auto& p : e)
|
||||||
|
for (size_t g = 0; g < 3; ++g)
|
||||||
|
for (size_t a = 0; a < 3; ++a) frames.push_back(p[a][g]);
|
||||||
|
D("/model/thermal_flux_local_frames", {s.ElementOrder().size(), 4, 3, 3},
|
||||||
|
frames);
|
||||||
|
}
|
||||||
|
/// @brief Writes raw initial temperature and completed-only reactions and
|
||||||
|
/// fluxes.
|
||||||
|
void WriteFrames(hid_t file, const ThermalAnalysisState& s) {
|
||||||
|
(void)hdf5_internal::CreateGroup(file, "/steps/Step-1");
|
||||||
|
std::vector<FrameRow> rows;
|
||||||
|
for (const auto& f : s.AcceptedFrames())
|
||||||
|
rows.push_back({f.frame_index, f.increment_index, f.time, f.time_increment,
|
||||||
|
(std::uint8_t)f.is_initial});
|
||||||
|
Hdf5Handle ft, mt;
|
||||||
|
Types<FrameRow>(ft, mt, [&](hid_t x, hid_t y) {
|
||||||
|
auto a = [&](hid_t t, hid_t u, hid_t q) {
|
||||||
|
Member(t, "frame_index", HOFFSET(FrameRow, frame_index), u);
|
||||||
|
Member(t, "increment_index", HOFFSET(FrameRow, increment_index), u);
|
||||||
|
Member(t, "time", HOFFSET(FrameRow, time), q);
|
||||||
|
Member(t, "time_increment", HOFFSET(FrameRow, time_increment), q);
|
||||||
|
Member(t, "is_initial", HOFFSET(FrameRow, is_initial), H5T_STD_U8LE);
|
||||||
|
};
|
||||||
|
a(x, H5T_STD_U64LE, H5T_IEEE_F64LE);
|
||||||
|
a(y, H5T_NATIVE_UINT64, H5T_NATIVE_DOUBLE);
|
||||||
|
});
|
||||||
|
auto af =
|
||||||
|
Compound(file, "/steps/Step-1/accepted_frames", rows, ft.Get(), mt.Get());
|
||||||
|
hdf5_internal::WriteStringAttribute(af.Get(), "time_units_label", "time");
|
||||||
|
for (const auto& f : s.AcceptedFrames()) {
|
||||||
|
std::string root = "/steps/Step-1/frames/" + std::to_string(f.frame_index);
|
||||||
|
auto g = hdf5_internal::CreateGroup(file, root.c_str());
|
||||||
|
(void)hdf5_internal::CreateGroup(file, (root + "/nodal").c_str());
|
||||||
|
hdf5_internal::WriteUint64Attribute(g.Get(), "frame_index", f.frame_index);
|
||||||
|
hdf5_internal::WriteUint64Attribute(g.Get(), "increment_index",
|
||||||
|
f.increment_index);
|
||||||
|
hdf5_internal::WriteDoubleAttribute(g.Get(), "time", f.time);
|
||||||
|
hdf5_internal::WriteDoubleAttribute(g.Get(), "time_increment",
|
||||||
|
f.time_increment);
|
||||||
|
hdf5_internal::WriteUint8Attribute(g.Get(), "is_initial", f.is_initial);
|
||||||
|
auto td = hdf5_internal::WriteDoubleValues(
|
||||||
|
file, root + "/nodal/temperature", {s.NodeOrder().size(), 5},
|
||||||
|
f.temperature.Data(), f.temperature.Size());
|
||||||
|
hdf5_internal::WriteStringAttribute(td.Get(), "component_order",
|
||||||
|
"NT11,NT12,NT13,NT14,NT15");
|
||||||
|
hdf5_internal::WriteStringAttribute(td.Get(), "units_label", "temperature");
|
||||||
|
hdf5_internal::WriteStringAttribute(td.Get(), "coordinate_system",
|
||||||
|
"scalar");
|
||||||
|
hdf5_internal::WriteStringAttribute(td.Get(), "location",
|
||||||
|
"nodal-thickness-stations");
|
||||||
|
if (f.completed) {
|
||||||
|
(void)hdf5_internal::CreateGroup(file, (root + "/element").c_str());
|
||||||
|
auto rd = hdf5_internal::WriteDoubleValues(
|
||||||
|
file, root + "/nodal/reaction_heat_rate", {s.ReactionDofs().size()},
|
||||||
|
f.completed->reaction_heat_rate.Data(),
|
||||||
|
f.completed->reaction_heat_rate.Size());
|
||||||
|
hdf5_internal::WriteStringAttribute(rd.Get(), "component_order", "RFL");
|
||||||
|
hdf5_internal::WriteStringAttribute(rd.Get(), "units_label", "power");
|
||||||
|
hdf5_internal::WriteStringAttribute(rd.Get(), "coordinate_system",
|
||||||
|
"scalar");
|
||||||
|
hdf5_internal::WriteStringAttribute(rd.Get(), "location",
|
||||||
|
"prescribed-temperature-dofs");
|
||||||
|
hdf5_internal::WriteStringAttribute(rd.Get(), "positive_direction",
|
||||||
|
"into-model");
|
||||||
|
std::vector<double> q;
|
||||||
|
for (const auto& e : f.completed->global_heat_flux)
|
||||||
|
for (size_t p = 0; p < 4; ++p)
|
||||||
|
for (size_t z = 0; z < 6; ++z)
|
||||||
|
for (size_t c = 0; c < 3; ++c) q.push_back(e[6 * p + z][c]);
|
||||||
|
auto fd = hdf5_internal::WriteDoubleValues(
|
||||||
|
file, root + "/element/heat_flux", {s.ElementOrder().size(), 4, 6, 3},
|
||||||
|
q.data(), q.size());
|
||||||
|
hdf5_internal::WriteStringAttribute(fd.Get(), "component_order",
|
||||||
|
"QX,QY,QZ");
|
||||||
|
hdf5_internal::WriteStringAttribute(fd.Get(), "units_label",
|
||||||
|
"power/area");
|
||||||
|
hdf5_internal::WriteStringAttribute(fd.Get(), "coordinate_system",
|
||||||
|
"global-cartesian");
|
||||||
|
hdf5_internal::WriteStringAttribute(fd.Get(), "location",
|
||||||
|
"element-inplane-thickness-traces");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// @brief Converts adapter failures into the existing output-failure category.
|
||||||
|
Status Fail(const char* m) {
|
||||||
|
return hdf5_internal::OutputFailure("thermal-output-schema", m);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Status hdf5_internal::WriteThermalFile(
|
||||||
|
const std::filesystem::path& output_path, const Domain& domain,
|
||||||
|
const ThermalAnalysisState& state,
|
||||||
|
const std::vector<Diagnostic>& diagnostics, const ThermalFileHooks& hooks) {
|
||||||
|
const auto inventory = ValidateThermalOutput(domain, state);
|
||||||
|
if (!inventory.IsOk()) return inventory;
|
||||||
|
if (!hooks.candidate_close || !hooks.reader_close)
|
||||||
|
return Fail("Missing thermal file closer.");
|
||||||
|
try {
|
||||||
|
hdf5_internal::Hdf5ErrorSilencer silence;
|
||||||
|
auto candidate = hdf5_internal::MakeCandidatePath(output_path);
|
||||||
|
hdf5_internal::CandidateFileGuard guard{candidate};
|
||||||
|
Hdf5Handle file{hdf5_internal::RequireHdf5Id(
|
||||||
|
H5Fcreate(candidate.string().c_str(), H5F_ACC_EXCL,
|
||||||
|
H5P_DEFAULT, H5P_DEFAULT),
|
||||||
|
"Unable to create thermal candidate."),
|
||||||
|
hooks.candidate_close};
|
||||||
|
if (hooks.before_write) hooks.before_write(file.Get());
|
||||||
|
Metadata(file.Get(), domain, state);
|
||||||
|
WriteThermalModel(file.Get(), domain, state);
|
||||||
|
WriteFrames(file.Get(), state);
|
||||||
|
hdf5_internal::WriteDiagnostics(file.Get(), diagnostics);
|
||||||
|
hdf5_internal::RequireHdf5(H5Fflush(file.Get(), H5F_SCOPE_GLOBAL),
|
||||||
|
"Unable to flush thermal candidate.");
|
||||||
|
hdf5_internal::RequireHdf5(file.CloseChecked(),
|
||||||
|
"Unable to close thermal candidate.");
|
||||||
|
if (hooks.before_self_check) hooks.before_self_check(candidate);
|
||||||
|
const Status ok = hdf5_internal::SelfCheckThermalFile(
|
||||||
|
candidate, domain, state, diagnostics, hooks.reader_close);
|
||||||
|
if (ok.IsOk() && hooks.before_finalize) hooks.before_finalize(candidate);
|
||||||
|
const Status out = hdf5_internal::AtomicFinalizeValidatedCandidate(
|
||||||
|
candidate, output_path, ok);
|
||||||
|
if (out.IsOk()) guard.Release();
|
||||||
|
return out;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
return Fail(e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Status ThermalHdf5ResultsWriter::Write(
|
||||||
|
const std::filesystem::path& output_path, const Domain& domain,
|
||||||
|
const ThermalAnalysisState& state,
|
||||||
|
const std::vector<Diagnostic>& diagnostics) {
|
||||||
|
return hdf5_internal::WriteThermalFile(output_path, domain, state,
|
||||||
|
diagnostics);
|
||||||
|
}
|
||||||
|
} // namespace fesa
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#ifndef FESA_IO_HDF5_THERMAL_HDF5_SELF_CHECK_H_
|
||||||
|
#define FESA_IO_HDF5_THERMAL_HDF5_SELF_CHECK_H_
|
||||||
|
|
||||||
|
#include <hdf5.h>
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <functional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/analysis/thermal_analysis_state.h"
|
||||||
|
#include "fesa/core/status.h"
|
||||||
|
#include "fesa/model/domain.h"
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
struct Diagnostic;
|
||||||
|
}
|
||||||
|
namespace fesa::hdf5_internal {
|
||||||
|
/// @brief Supplies private lifecycle fault seams around the real HDF5
|
||||||
|
/// operations.
|
||||||
|
/// @note Production uses default closers and no callbacks. Test closers must
|
||||||
|
/// release their handle even when reporting an injected close failure.
|
||||||
|
struct ThermalFileHooks {
|
||||||
|
herr_t (*candidate_close)(hid_t) = H5Fclose;
|
||||||
|
herr_t (*reader_close)(hid_t) = H5Fclose;
|
||||||
|
std::function<void(hid_t)> before_write;
|
||||||
|
std::function<void(const std::filesystem::path&)> before_self_check;
|
||||||
|
std::function<void(const std::filesystem::path&)> before_finalize;
|
||||||
|
};
|
||||||
|
/// @brief Executes the actual candidate/write/check/close/finalize lifecycle.
|
||||||
|
Status WriteThermalFile(const std::filesystem::path& output_path,
|
||||||
|
const Domain& domain, const ThermalAnalysisState& state,
|
||||||
|
const std::vector<Diagnostic>& diagnostics,
|
||||||
|
const ThermalFileHooks& hooks = {});
|
||||||
|
/// @brief Maps section-local thermal material indices into the global Domain
|
||||||
|
/// table.
|
||||||
|
std::uint64_t GlobalThermalMaterialId(const Domain& domain, std::size_t local);
|
||||||
|
/// @brief Checks supplied immutable source/initial/schedule inventories before
|
||||||
|
/// writing.
|
||||||
|
Status ValidateThermalOutput(const Domain& domain,
|
||||||
|
const ThermalAnalysisState& state);
|
||||||
|
/// @brief Checks every mandatory thermal field, value and attribute before
|
||||||
|
/// publication.
|
||||||
|
Status SelfCheckThermalFile(const std::filesystem::path& path,
|
||||||
|
const Domain& domain,
|
||||||
|
const ThermalAnalysisState& state,
|
||||||
|
const std::vector<Diagnostic>& diagnostics,
|
||||||
|
herr_t (*close_reader)(hid_t) = H5Fclose);
|
||||||
|
} // namespace fesa::hdf5_internal
|
||||||
|
#endif // FESA_IO_HDF5_THERMAL_HDF5_SELF_CHECK_H_
|
||||||
@@ -0,0 +1,993 @@
|
|||||||
|
#include "fesa/io/hdf5/thermal_hdf5_results_writer.h"
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <hdf5.h>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <functional>
|
||||||
|
#include <iterator>
|
||||||
|
#include <limits>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "fesa/analysis/analysis_model.h"
|
||||||
|
#include "fesa/assembly/thermal_assembler.h"
|
||||||
|
#include "fesa/fem/dof_manager.h"
|
||||||
|
#include "io/hdf5/hdf5_raii.h"
|
||||||
|
#include "io/hdf5/thermal_hdf5_self_check.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
fesa::ModelDefinition Definition() {
|
||||||
|
fesa::ModelDefinition m{};
|
||||||
|
const std::filesystem::path file{"thermal-writer.inp"};
|
||||||
|
m.source_path = file;
|
||||||
|
m.source_content_identity = "thermal-writer-source";
|
||||||
|
m.nodes = {{{"unused", 90, "0090"}, {9, 9, 9}, {file, 1}},
|
||||||
|
{{"part", 7, "0007"}, {0, 0, 0}, {file, 2}},
|
||||||
|
{{"part", 2, "0002"}, {1, 0, 0}, {file, 3}},
|
||||||
|
{{"part", 9, "0009"}, {1, 1, 0}, {file, 4}},
|
||||||
|
{{"part", 1, "0001"}, {0, 1, 0}, {file, 5}}};
|
||||||
|
// The structural row makes the thermal material's global ID nonzero.
|
||||||
|
m.materials = {{"structural", 1, .25, {file, 6}}};
|
||||||
|
m.thermal_materials = {{{"part", 1, "1"}, "thermal", 2, 3, 4, {file, 7}}};
|
||||||
|
m.thermal_sections = {{{"part", 1, "1"}, "ELSET", .2, 0, {file, 8}}};
|
||||||
|
m.thermal_elements = {{{"part", 13, "0013"}, {1, 2, 3, 4}, 0, {file, 9}}};
|
||||||
|
m.thermal_initial_conditions = {{1, -2, {file, 10}}};
|
||||||
|
m.thermal_step = fesa::ThermalStepDefinition{"raw Step", .3, .7, 3};
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Fixture {
|
||||||
|
std::unique_ptr<fesa::Domain> domain;
|
||||||
|
fesa::ThermalAnalysisState state;
|
||||||
|
};
|
||||||
|
Fixture Make(fesa::ModelDefinition definition = Definition(),
|
||||||
|
bool complete = true) {
|
||||||
|
auto d = fesa::Domain::Create(std::move(definition));
|
||||||
|
if (!d.HasValue()) throw std::runtime_error{"domain"};
|
||||||
|
auto domain = std::make_unique<fesa::Domain>(std::move(d.Value()));
|
||||||
|
auto m = fesa::AnalysisModel::Create(*domain);
|
||||||
|
auto dofs = fesa::DofManager::CreateThermal(m.Value());
|
||||||
|
auto a =
|
||||||
|
fesa::AssembleThermal(m.Value(), dofs.Value(), fesa::SerialParallelFor{});
|
||||||
|
auto state =
|
||||||
|
fesa::ThermalAnalysisState::Create(m.Value(), dofs.Value(), a.Value());
|
||||||
|
if (!state.HasValue()) throw std::runtime_error{"state"};
|
||||||
|
for (std::uint32_t i = 1; complete && i <= state.Value().TimeGrid().Count();
|
||||||
|
++i) {
|
||||||
|
fesa::Vector t{20U};
|
||||||
|
fesa::Vector r{state.Value().ReactionDofs().size()};
|
||||||
|
for (std::size_t row = 0; row < t.Size(); ++row)
|
||||||
|
t[row] = static_cast<double>(100 * i + row) - 200;
|
||||||
|
for (std::size_t row = 0; row < dofs.Value().ConstrainedDofs().size();
|
||||||
|
++row)
|
||||||
|
t[dofs.Value().ConstrainedDofs()[row]] =
|
||||||
|
dofs.Value().PrescribedValues()[row];
|
||||||
|
for (std::size_t row = 0; row < r.Size(); ++row)
|
||||||
|
r[row] = -10. * i + static_cast<double>(row);
|
||||||
|
std::vector<fesa::ThermalElementFlux> q(1);
|
||||||
|
for (auto& e : q)
|
||||||
|
for (std::size_t x = 0; x < 24; ++x)
|
||||||
|
e[x] = fesa::Vector3{double(x), -double(x) - i, 3. * i};
|
||||||
|
auto frame = state.Value().MakeCompletedFrame(i, std::move(t), std::move(r),
|
||||||
|
std::move(q));
|
||||||
|
if (!frame.HasValue() ||
|
||||||
|
!state.Value().CommitAcceptedFrame(frame.Value()).IsOk())
|
||||||
|
throw std::runtime_error{"frame"};
|
||||||
|
}
|
||||||
|
return {std::move(domain), std::move(state.Value())};
|
||||||
|
}
|
||||||
|
class Temp {
|
||||||
|
public:
|
||||||
|
Temp() {
|
||||||
|
static std::atomic<unsigned> next{0};
|
||||||
|
do {
|
||||||
|
directory_ = std::filesystem::temp_directory_path() /
|
||||||
|
("fesa-thermal-writer-" + std::to_string(next.fetch_add(1)));
|
||||||
|
} while (!std::filesystem::create_directory(directory_));
|
||||||
|
p_ = directory_ / "result.h5";
|
||||||
|
}
|
||||||
|
~Temp() {
|
||||||
|
std::error_code e;
|
||||||
|
std::filesystem::remove_all(directory_, e);
|
||||||
|
}
|
||||||
|
const std::filesystem::path& Path() const { return p_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::filesystem::path directory_;
|
||||||
|
std::filesystem::path p_;
|
||||||
|
};
|
||||||
|
using fesa::hdf5_internal::Hdf5Handle;
|
||||||
|
std::string Message(const fesa::Status& status) {
|
||||||
|
return status.Diagnostics().empty() ? "" : status.Diagnostics()[0].message;
|
||||||
|
}
|
||||||
|
std::vector<char> Bytes(const std::filesystem::path& path) {
|
||||||
|
std::ifstream stream(path, std::ios::binary);
|
||||||
|
return {std::istreambuf_iterator<char>(stream),
|
||||||
|
std::istreambuf_iterator<char>()};
|
||||||
|
}
|
||||||
|
std::vector<fesa::Diagnostic> Diagnostics() {
|
||||||
|
return {{fesa::Severity::kError,
|
||||||
|
"later",
|
||||||
|
{"b.inp", 9},
|
||||||
|
"HEAT TRANSFER",
|
||||||
|
"part.0013",
|
||||||
|
"late message"},
|
||||||
|
{fesa::Severity::kWarning,
|
||||||
|
"early",
|
||||||
|
{"a.inp", 2},
|
||||||
|
"NODE",
|
||||||
|
"part.0007",
|
||||||
|
u8"\uD55C\uAE00 warning"}};
|
||||||
|
}
|
||||||
|
herr_t CollectAttribute(hid_t, const char* name, const H5A_info_t*,
|
||||||
|
void* data) {
|
||||||
|
static_cast<std::vector<std::string>*>(data)->push_back(name);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
void CopyAttributes(hid_t from, hid_t to) {
|
||||||
|
std::vector<std::string> names;
|
||||||
|
hsize_t at = 0;
|
||||||
|
ASSERT_GE(H5Aiterate2(from, H5_INDEX_NAME, H5_ITER_INC, &at, CollectAttribute,
|
||||||
|
&names),
|
||||||
|
0);
|
||||||
|
for (const auto& name : names) {
|
||||||
|
Hdf5Handle a{H5Aopen(from, name.c_str(), H5P_DEFAULT), H5Aclose};
|
||||||
|
Hdf5Handle t{H5Aget_type(a.Get()), H5Tclose},
|
||||||
|
s{H5Aget_space(a.Get()), H5Sclose};
|
||||||
|
std::vector<char> value(
|
||||||
|
static_cast<std::size_t>(H5Sget_simple_extent_npoints(s.Get())) *
|
||||||
|
H5Tget_size(t.Get()));
|
||||||
|
ASSERT_GE(H5Aread(a.Get(), t.Get(), value.data()), 0);
|
||||||
|
Hdf5Handle b{H5Acreate2(to, name.c_str(), t.Get(), s.Get(), H5P_DEFAULT,
|
||||||
|
H5P_DEFAULT),
|
||||||
|
H5Aclose};
|
||||||
|
ASSERT_GE(H5Awrite(b.Get(), t.Get(), value.data()), 0);
|
||||||
|
if (H5Tis_variable_str(t.Get()) > 0 ||
|
||||||
|
H5Tdetect_class(t.Get(), H5T_VLEN) > 0)
|
||||||
|
ASSERT_GE(H5Dvlen_reclaim(t.Get(), s.Get(), H5P_DEFAULT, value.data()),
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void RewriteCompound(hid_t file, const char* path, const std::string& omit = "",
|
||||||
|
const std::string& change = "", hid_t changed_type = -1,
|
||||||
|
bool additive = false) {
|
||||||
|
Hdf5Handle old{H5Dopen2(file, path, H5P_DEFAULT), H5Dclose};
|
||||||
|
Hdf5Handle type{H5Dget_type(old.Get()), H5Tclose},
|
||||||
|
space{H5Dget_space(old.Get()), H5Sclose};
|
||||||
|
const int count = H5Tget_nmembers(type.Get());
|
||||||
|
ASSERT_GE(count, 0);
|
||||||
|
std::vector<std::string> names;
|
||||||
|
std::vector<Hdf5Handle> fields;
|
||||||
|
std::size_t width = 0;
|
||||||
|
// Reverse field order and repack offsets; never preserve writer C++ padding.
|
||||||
|
for (int i = count - 1; i >= 0; --i) {
|
||||||
|
char* raw = H5Tget_member_name(type.Get(), static_cast<unsigned>(i));
|
||||||
|
ASSERT_NE(raw, nullptr);
|
||||||
|
const std::string name{raw};
|
||||||
|
ASSERT_GE(H5free_memory(raw), 0);
|
||||||
|
if (name == omit) continue;
|
||||||
|
names.push_back(name);
|
||||||
|
fields.emplace_back(
|
||||||
|
name == change
|
||||||
|
? H5Tcopy(changed_type)
|
||||||
|
: H5Tget_member_type(type.Get(), static_cast<unsigned>(i)),
|
||||||
|
H5Tclose);
|
||||||
|
width += H5Tget_size(fields.back().Get());
|
||||||
|
}
|
||||||
|
if (additive) {
|
||||||
|
names.push_back("optional_future_value");
|
||||||
|
fields.emplace_back(H5Tcopy(H5T_STD_U64LE), H5Tclose);
|
||||||
|
width += 8;
|
||||||
|
}
|
||||||
|
Hdf5Handle replacement{H5Tcreate(H5T_COMPOUND, width), H5Tclose};
|
||||||
|
std::size_t offset = 0;
|
||||||
|
for (std::size_t i = 0; i < names.size(); ++i) {
|
||||||
|
ASSERT_GE(
|
||||||
|
H5Tinsert(replacement.Get(), names[i].c_str(), offset, fields[i].Get()),
|
||||||
|
0);
|
||||||
|
offset += H5Tget_size(fields[i].Get());
|
||||||
|
}
|
||||||
|
const auto rows =
|
||||||
|
static_cast<std::size_t>(H5Sget_simple_extent_npoints(space.Get()));
|
||||||
|
std::vector<char> data(rows * width, 0);
|
||||||
|
if (rows) {
|
||||||
|
std::size_t field_offset = 0;
|
||||||
|
for (std::size_t i = 0; i < names.size(); ++i) {
|
||||||
|
if (names[i] != "optional_future_value") {
|
||||||
|
Hdf5Handle projection{H5Tcreate(H5T_COMPOUND, width), H5Tclose};
|
||||||
|
ASSERT_GE(H5Tinsert(projection.Get(), names[i].c_str(), field_offset,
|
||||||
|
fields[i].Get()),
|
||||||
|
0);
|
||||||
|
const herr_t read = H5Dread(old.Get(), projection.Get(), H5S_ALL,
|
||||||
|
H5S_ALL, H5P_DEFAULT, data.data());
|
||||||
|
if (names[i] != change) ASSERT_GE(read, 0);
|
||||||
|
}
|
||||||
|
field_offset += H5Tget_size(fields[i].Get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ASSERT_GE(H5Ldelete(file, path, H5P_DEFAULT), 0);
|
||||||
|
Hdf5Handle properties{H5Pcreate(H5P_DATASET_CREATE), H5Pclose};
|
||||||
|
const hsize_t chunk = 1;
|
||||||
|
ASSERT_GE(H5Pset_chunk(properties.Get(), 1, &chunk), 0);
|
||||||
|
Hdf5Handle rewritten{H5Dcreate2(file, path, replacement.Get(), space.Get(),
|
||||||
|
H5P_DEFAULT, properties.Get(), H5P_DEFAULT),
|
||||||
|
H5Dclose};
|
||||||
|
ASSERT_GE(rewritten.Get(), 0);
|
||||||
|
if (rows)
|
||||||
|
ASSERT_GE(H5Dwrite(rewritten.Get(), replacement.Get(), H5S_ALL, H5S_ALL,
|
||||||
|
H5P_DEFAULT, data.data()),
|
||||||
|
0);
|
||||||
|
CopyAttributes(old.Get(), rewritten.Get());
|
||||||
|
if (rows && H5Tdetect_class(replacement.Get(), H5T_VLEN) > 0)
|
||||||
|
ASSERT_GE(H5Dvlen_reclaim(replacement.Get(), space.Get(), H5P_DEFAULT,
|
||||||
|
data.data()),
|
||||||
|
0);
|
||||||
|
else if (rows) {
|
||||||
|
// H5Tdetect_class does not classify variable strings as H5T_VLEN.
|
||||||
|
bool strings = false;
|
||||||
|
for (const auto& field : fields)
|
||||||
|
strings = strings || H5Tis_variable_str(field.Get()) > 0;
|
||||||
|
if (strings)
|
||||||
|
ASSERT_GE(H5Dvlen_reclaim(replacement.Get(), space.Get(), H5P_DEFAULT,
|
||||||
|
data.data()),
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
template <class T>
|
||||||
|
void WriteColumn(hid_t file, const char* path, const char* field, hid_t native,
|
||||||
|
const std::vector<T>& values) {
|
||||||
|
Hdf5Handle d{H5Dopen2(file, path, H5P_DEFAULT), H5Dclose};
|
||||||
|
Hdf5Handle t{H5Tcreate(H5T_COMPOUND, sizeof(T)), H5Tclose};
|
||||||
|
ASSERT_GE(H5Tinsert(t.Get(), field, 0, native), 0);
|
||||||
|
ASSERT_GE(
|
||||||
|
H5Dwrite(d.Get(), t.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data()),
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
void StringAttribute(hid_t file, const char* path, const char* name,
|
||||||
|
const std::string& value,
|
||||||
|
H5T_cset_t charset = H5T_CSET_UTF8, bool scalar = true) {
|
||||||
|
Hdf5Handle object{H5Oopen(file, path, H5P_DEFAULT), H5Oclose};
|
||||||
|
ASSERT_GE(H5Adelete(object.Get(), name), 0);
|
||||||
|
Hdf5Handle t{H5Tcopy(H5T_C_S1), H5Tclose};
|
||||||
|
ASSERT_GE(H5Tset_size(t.Get(), value.size() + 1), 0);
|
||||||
|
ASSERT_GE(H5Tset_cset(t.Get(), charset), 0);
|
||||||
|
const hsize_t two = 2;
|
||||||
|
Hdf5Handle s{
|
||||||
|
scalar ? H5Screate(H5S_SCALAR) : H5Screate_simple(1, &two, nullptr),
|
||||||
|
H5Sclose};
|
||||||
|
Hdf5Handle a{H5Acreate2(object.Get(), name, t.Get(), s.Get(), H5P_DEFAULT,
|
||||||
|
H5P_DEFAULT),
|
||||||
|
H5Aclose};
|
||||||
|
std::vector<char> data((value.size() + 1) * (scalar ? 1 : 2), 0);
|
||||||
|
std::copy(value.begin(), value.end(), data.begin());
|
||||||
|
ASSERT_GE(H5Awrite(a.Get(), t.Get(), data.data()), 0);
|
||||||
|
}
|
||||||
|
void ReplacePrimitive(hid_t file, const std::string& path, bool wrong_shape) {
|
||||||
|
Hdf5Handle old{H5Dopen2(file, path.c_str(), H5P_DEFAULT), H5Dclose};
|
||||||
|
Hdf5Handle old_type{H5Dget_type(old.Get()), H5Tclose},
|
||||||
|
old_space{H5Dget_space(old.Get()), H5Sclose};
|
||||||
|
const int rank = H5Sget_simple_extent_ndims(old_space.Get());
|
||||||
|
ASSERT_GT(rank, 0);
|
||||||
|
std::vector<hsize_t> shape(static_cast<std::size_t>(rank));
|
||||||
|
ASSERT_GE(H5Sget_simple_extent_dims(old_space.Get(), shape.data(), nullptr),
|
||||||
|
0);
|
||||||
|
if (wrong_shape) ++shape[0];
|
||||||
|
Hdf5Handle space{H5Screate_simple(rank, shape.data(), nullptr), H5Sclose};
|
||||||
|
ASSERT_GE(H5Ldelete(file, path.c_str(), H5P_DEFAULT), 0);
|
||||||
|
Hdf5Handle replaced{
|
||||||
|
H5Dcreate2(file, path.c_str(),
|
||||||
|
wrong_shape ? old_type.Get() : H5T_STD_I32LE, space.Get(),
|
||||||
|
H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT),
|
||||||
|
H5Dclose};
|
||||||
|
ASSERT_GE(replaced.Get(), 0);
|
||||||
|
CopyAttributes(old.Get(), replaced.Get());
|
||||||
|
}
|
||||||
|
void ChangePrimitive(hid_t file, const std::string& path) {
|
||||||
|
Hdf5Handle d{H5Dopen2(file, path.c_str(), H5P_DEFAULT), H5Dclose};
|
||||||
|
Hdf5Handle t{H5Dget_type(d.Get()), H5Tclose},
|
||||||
|
s{H5Dget_space(d.Get()), H5Sclose};
|
||||||
|
const auto count = H5Sget_simple_extent_npoints(s.Get());
|
||||||
|
ASSERT_GT(count, 0);
|
||||||
|
if (H5Tget_class(t.Get()) == H5T_FLOAT) {
|
||||||
|
std::vector<double> values(static_cast<std::size_t>(count));
|
||||||
|
ASSERT_GE(H5Dread(d.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT,
|
||||||
|
values.data()),
|
||||||
|
0);
|
||||||
|
values[0] += 1;
|
||||||
|
ASSERT_GE(H5Dwrite(d.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL,
|
||||||
|
H5P_DEFAULT, values.data()),
|
||||||
|
0);
|
||||||
|
} else {
|
||||||
|
std::vector<std::int64_t> values(static_cast<std::size_t>(count));
|
||||||
|
ASSERT_GE(H5Dread(d.Get(), H5T_NATIVE_INT64, H5S_ALL, H5S_ALL, H5P_DEFAULT,
|
||||||
|
values.data()),
|
||||||
|
0);
|
||||||
|
values[0] += 1;
|
||||||
|
ASSERT_GE(H5Dwrite(d.Get(), H5T_NATIVE_INT64, H5S_ALL, H5S_ALL, H5P_DEFAULT,
|
||||||
|
values.data()),
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TEST(ThermalHdf5ResultsWriter,
|
||||||
|
WritesRawInitialAndCompletedZeroReactionHistory) {
|
||||||
|
auto x = Make();
|
||||||
|
Temp out;
|
||||||
|
fesa::ThermalHdf5ResultsWriter writer;
|
||||||
|
const auto result = writer.Write(
|
||||||
|
out.Path(), *x.domain, x.state,
|
||||||
|
{{fesa::Severity::kWarning, "fixture", {}, "", "", "fixture"}});
|
||||||
|
ASSERT_TRUE(result.IsOk())
|
||||||
|
<< (result.Diagnostics().empty() ? "no diagnostic"
|
||||||
|
: result.Diagnostics()[0].message);
|
||||||
|
hid_t file =
|
||||||
|
H5Fopen(out.Path().string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT);
|
||||||
|
ASSERT_GE(file, 0);
|
||||||
|
EXPECT_GT(
|
||||||
|
H5Lexists(file, "/steps/Step-1/frames/0/nodal/temperature", H5P_DEFAULT),
|
||||||
|
0);
|
||||||
|
EXPECT_LE(H5Lexists(file, "/steps/Step-1/frames/0/nodal/reaction_heat_rate",
|
||||||
|
H5P_DEFAULT),
|
||||||
|
0);
|
||||||
|
EXPECT_LE(
|
||||||
|
H5Lexists(file, "/steps/Step-1/frames/0/element/heat_flux", H5P_DEFAULT),
|
||||||
|
0);
|
||||||
|
EXPECT_GT(H5Lexists(file, "/steps/Step-1/frames/3/nodal/reaction_heat_rate",
|
||||||
|
H5P_DEFAULT),
|
||||||
|
0);
|
||||||
|
EXPECT_GT(
|
||||||
|
H5Lexists(file, "/steps/Step-1/frames/3/element/heat_flux", H5P_DEFAULT),
|
||||||
|
0);
|
||||||
|
EXPECT_GT(H5Fclose(file), -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ThermalHdf5ResultsWriter, SectionMaterialUsesGlobalMaterialIdentity) {
|
||||||
|
auto x = Make();
|
||||||
|
Temp out;
|
||||||
|
ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{}
|
||||||
|
.Write(out.Path(), *x.domain, x.state, {})
|
||||||
|
.IsOk());
|
||||||
|
using fesa::hdf5_internal::Hdf5Handle;
|
||||||
|
Hdf5Handle f{
|
||||||
|
H5Fopen(out.Path().string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT),
|
||||||
|
H5Fclose};
|
||||||
|
Hdf5Handle d{H5Dopen2(f.Get(), "/model/shell_sections", H5P_DEFAULT),
|
||||||
|
H5Dclose};
|
||||||
|
Hdf5Handle t{H5Tcreate(H5T_COMPOUND, sizeof(std::uint64_t)), H5Tclose};
|
||||||
|
ASSERT_GE(H5Tinsert(t.Get(), "material_internal_id", 0, H5T_NATIVE_UINT64),
|
||||||
|
0);
|
||||||
|
std::uint64_t id = 99;
|
||||||
|
ASSERT_GE(H5Dread(d.Get(), t.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, &id), 0);
|
||||||
|
EXPECT_EQ(id, 1U);
|
||||||
|
Hdf5Handle utf8{H5Tcopy(H5T_C_S1), H5Tclose};
|
||||||
|
ASSERT_GE(H5Tset_size(utf8.Get(), H5T_VARIABLE), 0);
|
||||||
|
ASSERT_GE(H5Tset_cset(utf8.Get(), H5T_CSET_UTF8), 0);
|
||||||
|
Hdf5Handle source_type{H5Tcreate(H5T_COMPOUND, sizeof(char*)), H5Tclose};
|
||||||
|
ASSERT_GE(H5Tinsert(source_type.Get(), "source_file", 0, utf8.Get()), 0);
|
||||||
|
char* source = nullptr;
|
||||||
|
ASSERT_GE(H5Dread(d.Get(), source_type.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT,
|
||||||
|
&source),
|
||||||
|
0);
|
||||||
|
ASSERT_NE(source, nullptr);
|
||||||
|
EXPECT_EQ(std::string{source}, std::filesystem::absolute("thermal-writer.inp")
|
||||||
|
.lexically_normal()
|
||||||
|
.generic_u8string());
|
||||||
|
EXPECT_GE(H5free_memory(source), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ThermalHdf5ResultsWriter,
|
||||||
|
RejectsMissingMandatoryAttributesAndChangedValues) {
|
||||||
|
const auto x = Make();
|
||||||
|
for (const auto& mutation : std::vector<std::function<void(hid_t)>>{
|
||||||
|
[](hid_t f) {
|
||||||
|
hid_t m = H5Gopen2(f, "/metadata", H5P_DEFAULT);
|
||||||
|
EXPECT_GE(H5Adelete(m, "source_input_identity"), 0);
|
||||||
|
H5Gclose(m);
|
||||||
|
},
|
||||||
|
[](hid_t f) {
|
||||||
|
hid_t d = H5Dopen2(f, "/steps/Step-1/frames/1/nodal/temperature",
|
||||||
|
H5P_DEFAULT);
|
||||||
|
EXPECT_GE(H5Adelete(d, "units_label"), 0);
|
||||||
|
H5Dclose(d);
|
||||||
|
},
|
||||||
|
[](hid_t f) {
|
||||||
|
hid_t d = H5Dopen2(f, "/model/thermal_node_ids", H5P_DEFAULT);
|
||||||
|
std::uint64_t ids[4] = {1, 1, 3, 4};
|
||||||
|
EXPECT_GE(H5Dwrite(d, H5T_NATIVE_UINT64, H5S_ALL, H5S_ALL,
|
||||||
|
H5P_DEFAULT, ids),
|
||||||
|
0);
|
||||||
|
H5Dclose(d);
|
||||||
|
},
|
||||||
|
[](hid_t f) {
|
||||||
|
hid_t d = H5Dopen2(f, "/steps/Step-1/frames/1/nodal/temperature",
|
||||||
|
H5P_DEFAULT);
|
||||||
|
double v[20] = {4};
|
||||||
|
EXPECT_GE(H5Dwrite(d, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL,
|
||||||
|
H5P_DEFAULT, v),
|
||||||
|
0);
|
||||||
|
H5Dclose(d);
|
||||||
|
}}) {
|
||||||
|
Temp out;
|
||||||
|
ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{}
|
||||||
|
.Write(out.Path(), *x.domain, x.state, {})
|
||||||
|
.IsOk());
|
||||||
|
hid_t f = H5Fopen(out.Path().string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT);
|
||||||
|
ASSERT_GE(f, 0);
|
||||||
|
mutation(f);
|
||||||
|
ASSERT_GE(H5Fclose(f), 0);
|
||||||
|
EXPECT_FALSE(fesa::hdf5_internal::SelfCheckThermalFile(
|
||||||
|
out.Path(), *x.domain, x.state, {})
|
||||||
|
.IsOk());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ThermalHdf5ResultsWriter,
|
||||||
|
EveryRequiredAttributeIsCheckedBeforePublication) {
|
||||||
|
const auto x = Make();
|
||||||
|
std::vector<std::pair<std::string, std::string>> required;
|
||||||
|
const auto add = [&](const std::string& path,
|
||||||
|
std::initializer_list<const char*> names) {
|
||||||
|
for (const auto* name : names) required.emplace_back(path, name);
|
||||||
|
};
|
||||||
|
add("/metadata",
|
||||||
|
{"schema_version", "feature_id", "analysis_procedure", "source_step_name",
|
||||||
|
"solver_version", "source_input_identity", "unit_system_label",
|
||||||
|
"internal_formulation", "integration_rule", "time_integration",
|
||||||
|
"nominal_time_increment", "time_period"});
|
||||||
|
add("/model/nodes", {"units_label"});
|
||||||
|
add("/model/shell_sections", {"thickness_units_label"});
|
||||||
|
add("/model/thermal_materials",
|
||||||
|
{"conductivity_units_label", "density_units_label",
|
||||||
|
"specific_heat_units_label"});
|
||||||
|
add("/steps/Step-1/accepted_frames", {"time_units_label"});
|
||||||
|
for (int frame = 0; frame < 4; ++frame) {
|
||||||
|
const auto path = "/steps/Step-1/frames/" + std::to_string(frame);
|
||||||
|
add(path, {"frame_index", "increment_index", "time", "time_increment",
|
||||||
|
"is_initial"});
|
||||||
|
add(path + "/nodal/temperature",
|
||||||
|
{"component_order", "units_label", "coordinate_system", "location"});
|
||||||
|
if (frame != 0) {
|
||||||
|
add(path + "/nodal/reaction_heat_rate",
|
||||||
|
{"component_order", "units_label", "coordinate_system", "location",
|
||||||
|
"positive_direction"});
|
||||||
|
add(path + "/element/heat_flux",
|
||||||
|
{"component_order", "units_label", "coordinate_system", "location"});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& attribute : required) {
|
||||||
|
SCOPED_TRACE(attribute.first + ":" + attribute.second);
|
||||||
|
Temp out;
|
||||||
|
ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{}
|
||||||
|
.Write(out.Path(), *x.domain, x.state, {})
|
||||||
|
.IsOk());
|
||||||
|
const hid_t file =
|
||||||
|
H5Fopen(out.Path().string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT);
|
||||||
|
ASSERT_GE(file, 0);
|
||||||
|
const hid_t object = H5Oopen(file, attribute.first.c_str(), H5P_DEFAULT);
|
||||||
|
ASSERT_GE(object, 0);
|
||||||
|
ASSERT_GE(H5Adelete(object, attribute.second.c_str()), 0);
|
||||||
|
ASSERT_GE(H5Oclose(object), 0);
|
||||||
|
ASSERT_GE(H5Fclose(file), 0);
|
||||||
|
EXPECT_FALSE(fesa::hdf5_internal::SelfCheckThermalFile(
|
||||||
|
out.Path(), *x.domain, x.state, {})
|
||||||
|
.IsOk());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ThermalHdf5ResultsWriter, EveryCompoundFieldAndEveryDatasetIsMandatory) {
|
||||||
|
const auto x = Make();
|
||||||
|
const auto diagnostics = Diagnostics();
|
||||||
|
const std::vector<std::pair<const char*, std::vector<const char*>>> tables = {
|
||||||
|
{"/model/nodes",
|
||||||
|
{"internal_node_id", "instance_name", "source_label", "coordinates"}},
|
||||||
|
{"/model/elements",
|
||||||
|
{"internal_element_id", "instance_name", "source_label",
|
||||||
|
"source_element_type", "internal_formulation", "node_internal_ids",
|
||||||
|
"shell_section_internal_id", "material_internal_id"}},
|
||||||
|
{"/model/thermal_materials",
|
||||||
|
{"internal_material_id", "name", "conductivity", "density",
|
||||||
|
"specific_heat"}},
|
||||||
|
{"/model/shell_sections",
|
||||||
|
{"internal_section_id", "source_file", "source_line", "source_elset",
|
||||||
|
"material_internal_id", "thickness"}},
|
||||||
|
{"/model/thermal_reaction_dofs", {"internal_node_id", "source_dof"}},
|
||||||
|
{"/steps/Step-1/accepted_frames",
|
||||||
|
{"frame_index", "increment_index", "time", "time_increment",
|
||||||
|
"is_initial"}},
|
||||||
|
{"/diagnostics",
|
||||||
|
{"severity", "code", "file", "line", "keyword", "entity_identity",
|
||||||
|
"message"}}};
|
||||||
|
for (const auto& table : tables)
|
||||||
|
for (const auto* field : table.second) {
|
||||||
|
SCOPED_TRACE(std::string{table.first} + ":" + field);
|
||||||
|
Temp out;
|
||||||
|
ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{}
|
||||||
|
.Write(out.Path(), *x.domain, x.state, diagnostics)
|
||||||
|
.IsOk());
|
||||||
|
{
|
||||||
|
Hdf5Handle file{
|
||||||
|
H5Fopen(out.Path().string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||||
|
H5Fclose};
|
||||||
|
RewriteCompound(file.Get(), table.first, field);
|
||||||
|
}
|
||||||
|
const auto checked = fesa::hdf5_internal::SelfCheckThermalFile(
|
||||||
|
out.Path(), *x.domain, x.state, diagnostics);
|
||||||
|
EXPECT_FALSE(checked.IsOk());
|
||||||
|
EXPECT_NE(Message(checked).find("Missing compound field"),
|
||||||
|
std::string::npos);
|
||||||
|
}
|
||||||
|
std::vector<std::string> datasets;
|
||||||
|
for (const auto& table : tables) datasets.push_back(table.first);
|
||||||
|
for (const auto* name :
|
||||||
|
{"thermal_node_ids", "thermal_element_ids", "temperature_dofs",
|
||||||
|
"temperature_zeta", "thermal_constraint_mask",
|
||||||
|
"thermal_flux_inplane_coordinates", "thermal_flux_zeta",
|
||||||
|
"thermal_flux_trace_side", "thermal_flux_local_frames"})
|
||||||
|
datasets.push_back(std::string{"/model/"} + name);
|
||||||
|
for (std::size_t i = 0; i < x.state.AcceptedFrames().size(); ++i) {
|
||||||
|
const auto root = "/steps/Step-1/frames/" + std::to_string(i);
|
||||||
|
datasets.push_back(root + "/nodal/temperature");
|
||||||
|
if (i) {
|
||||||
|
datasets.push_back(root + "/nodal/reaction_heat_rate");
|
||||||
|
datasets.push_back(root + "/element/heat_flux");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& path : datasets) {
|
||||||
|
SCOPED_TRACE(path);
|
||||||
|
Temp out;
|
||||||
|
ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{}
|
||||||
|
.Write(out.Path(), *x.domain, x.state, diagnostics)
|
||||||
|
.IsOk());
|
||||||
|
{
|
||||||
|
Hdf5Handle file{
|
||||||
|
H5Fopen(out.Path().string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||||
|
H5Fclose};
|
||||||
|
ASSERT_GE(H5Ldelete(file.Get(), path.c_str(), H5P_DEFAULT), 0);
|
||||||
|
}
|
||||||
|
EXPECT_FALSE(fesa::hdf5_internal::SelfCheckThermalFile(
|
||||||
|
out.Path(), *x.domain, x.state, diagnostics)
|
||||||
|
.IsOk());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ThermalHdf5ResultsWriter,
|
||||||
|
ReorderedPackedAdditiveCompoundsAndFixedUtf8AreAccepted) {
|
||||||
|
auto x = Make();
|
||||||
|
const auto diagnostics = Diagnostics();
|
||||||
|
Temp out;
|
||||||
|
ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{}
|
||||||
|
.Write(out.Path(), *x.domain, x.state, diagnostics)
|
||||||
|
.IsOk());
|
||||||
|
{
|
||||||
|
Hdf5Handle file{
|
||||||
|
H5Fopen(out.Path().string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||||
|
H5Fclose};
|
||||||
|
for (const auto* path :
|
||||||
|
{"/model/nodes", "/model/elements", "/model/thermal_materials",
|
||||||
|
"/model/shell_sections", "/model/thermal_reaction_dofs",
|
||||||
|
"/steps/Step-1/accepted_frames", "/diagnostics"})
|
||||||
|
RewriteCompound(file.Get(), path, "", "", -1, true);
|
||||||
|
StringAttribute(file.Get(), "/metadata", "source_step_name", "raw Step");
|
||||||
|
StringAttribute(file.Get(), "/steps/Step-1/frames/0/nodal/temperature",
|
||||||
|
"units_label", "temperature");
|
||||||
|
Hdf5Handle fixed{H5Tcopy(H5T_C_S1), H5Tclose};
|
||||||
|
ASSERT_GE(H5Tset_size(fixed.Get(), 16), 0);
|
||||||
|
ASSERT_GE(H5Tset_cset(fixed.Get(), H5T_CSET_UTF8), 0);
|
||||||
|
{
|
||||||
|
fesa::hdf5_internal::Hdf5ErrorSilencer silent;
|
||||||
|
RewriteCompound(file.Get(), "/model/thermal_materials", "", "name",
|
||||||
|
fixed.Get());
|
||||||
|
}
|
||||||
|
std::array<char, 16> name{};
|
||||||
|
std::copy_n("thermal", 7, name.data());
|
||||||
|
WriteColumn(file.Get(), "/model/thermal_materials", "name", fixed.Get(),
|
||||||
|
std::vector<std::array<char, 16>>{name});
|
||||||
|
}
|
||||||
|
const auto checked = fesa::hdf5_internal::SelfCheckThermalFile(
|
||||||
|
out.Path(), *x.domain, x.state, diagnostics);
|
||||||
|
EXPECT_TRUE(checked.IsOk()) << Message(checked);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ThermalHdf5ResultsWriter,
|
||||||
|
RejectsWrongTypesArrayExtentsNonscalarAndNonUtf8Attributes) {
|
||||||
|
auto x = Make();
|
||||||
|
const hsize_t wrong_extent = 3;
|
||||||
|
Hdf5Handle array{H5Tarray_create2(H5T_STD_U64LE, 1, &wrong_extent), H5Tclose};
|
||||||
|
const std::vector<std::function<void(hid_t)>> mutations = {
|
||||||
|
[](hid_t f) {
|
||||||
|
RewriteCompound(f, "/model/thermal_materials", "", "conductivity",
|
||||||
|
H5T_IEEE_F32LE);
|
||||||
|
},
|
||||||
|
[](hid_t f) {
|
||||||
|
RewriteCompound(f, "/model/elements", "", "internal_element_id",
|
||||||
|
H5T_STD_U32LE);
|
||||||
|
},
|
||||||
|
[&](hid_t f) {
|
||||||
|
RewriteCompound(f, "/model/elements", "", "node_internal_ids",
|
||||||
|
array.Get());
|
||||||
|
},
|
||||||
|
[](hid_t f) {
|
||||||
|
StringAttribute(f, "/metadata", "feature_id",
|
||||||
|
"shell-transient-heat-transfer", H5T_CSET_ASCII);
|
||||||
|
},
|
||||||
|
[](hid_t f) {
|
||||||
|
StringAttribute(f, "/metadata", "source_step_name", "raw Step",
|
||||||
|
H5T_CSET_UTF8, false);
|
||||||
|
},
|
||||||
|
[](hid_t f) {
|
||||||
|
Hdf5Handle g{H5Gopen2(f, "/steps/Step-1/frames/1", H5P_DEFAULT),
|
||||||
|
H5Gclose};
|
||||||
|
ASSERT_GE(H5Adelete(g.Get(), "is_initial"), 0);
|
||||||
|
const hsize_t count = 2;
|
||||||
|
Hdf5Handle s{H5Screate_simple(1, &count, nullptr), H5Sclose};
|
||||||
|
Hdf5Handle a{H5Acreate2(g.Get(), "is_initial", H5T_STD_U8LE, s.Get(),
|
||||||
|
H5P_DEFAULT, H5P_DEFAULT),
|
||||||
|
H5Aclose};
|
||||||
|
const std::uint8_t data[2] = {0, 0};
|
||||||
|
ASSERT_GE(H5Awrite(a.Get(), H5T_NATIVE_UINT8, data), 0);
|
||||||
|
},
|
||||||
|
[](hid_t f) {
|
||||||
|
Hdf5Handle g{H5Gopen2(f, "/metadata", H5P_DEFAULT), H5Gclose};
|
||||||
|
ASSERT_GE(H5Adelete(g.Get(), "time_period"), 0);
|
||||||
|
const hsize_t count = 2;
|
||||||
|
Hdf5Handle s{H5Screate_simple(1, &count, nullptr), H5Sclose};
|
||||||
|
Hdf5Handle a{H5Acreate2(g.Get(), "time_period", H5T_IEEE_F64LE, s.Get(),
|
||||||
|
H5P_DEFAULT, H5P_DEFAULT),
|
||||||
|
H5Aclose};
|
||||||
|
const double data[2] = {.7, .7};
|
||||||
|
ASSERT_GE(H5Awrite(a.Get(), H5T_NATIVE_DOUBLE, data), 0);
|
||||||
|
}};
|
||||||
|
for (std::size_t i = 0; i < mutations.size(); ++i) {
|
||||||
|
SCOPED_TRACE(i);
|
||||||
|
Temp out;
|
||||||
|
ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{}
|
||||||
|
.Write(out.Path(), *x.domain, x.state, {})
|
||||||
|
.IsOk());
|
||||||
|
{
|
||||||
|
fesa::hdf5_internal::Hdf5ErrorSilencer silent;
|
||||||
|
Hdf5Handle file{
|
||||||
|
H5Fopen(out.Path().string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||||
|
H5Fclose};
|
||||||
|
mutations[i](file.Get());
|
||||||
|
}
|
||||||
|
const auto checked = fesa::hdf5_internal::SelfCheckThermalFile(
|
||||||
|
out.Path(), *x.domain, x.state, {});
|
||||||
|
EXPECT_FALSE(checked.IsOk());
|
||||||
|
if (i < 3)
|
||||||
|
EXPECT_NE(Message(checked).find("type/extent"), std::string::npos)
|
||||||
|
<< Message(checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ThermalHdf5ResultsWriter,
|
||||||
|
RejectsWrongValuesFrameInventoriesNonfiniteAndDiagnostics) {
|
||||||
|
auto definition = Definition();
|
||||||
|
definition.thermal_model_boundaries = {{1, 11, 5, {}}, {4, 15, -4, {}}};
|
||||||
|
const auto x = Make(std::move(definition));
|
||||||
|
const auto diagnostics = Diagnostics();
|
||||||
|
std::vector<std::function<void(hid_t)>> mutations = {
|
||||||
|
[](hid_t f) {
|
||||||
|
WriteColumn(f, "/model/nodes", "internal_node_id", H5T_NATIVE_UINT64,
|
||||||
|
std::vector<std::uint64_t>{0, 1, 1, 3, 4});
|
||||||
|
},
|
||||||
|
[](hid_t f) {
|
||||||
|
WriteColumn(f, "/model/elements", "shell_section_internal_id",
|
||||||
|
H5T_NATIVE_UINT64, std::vector<std::uint64_t>{77});
|
||||||
|
},
|
||||||
|
[](hid_t f) {
|
||||||
|
WriteColumn(f, "/model/thermal_materials", "conductivity",
|
||||||
|
H5T_NATIVE_DOUBLE, std::vector<double>{3});
|
||||||
|
},
|
||||||
|
[](hid_t f) {
|
||||||
|
WriteColumn(f, "/model/shell_sections", "source_line",
|
||||||
|
H5T_NATIVE_UINT64, std::vector<std::uint64_t>{99});
|
||||||
|
},
|
||||||
|
[](hid_t f) {
|
||||||
|
WriteColumn(f, "/model/thermal_reaction_dofs", "source_dof",
|
||||||
|
H5T_NATIVE_UINT64, std::vector<std::uint64_t>{12, 15});
|
||||||
|
},
|
||||||
|
[](hid_t f) {
|
||||||
|
WriteColumn(f, "/steps/Step-1/accepted_frames", "frame_index",
|
||||||
|
H5T_NATIVE_UINT64, std::vector<std::uint64_t>{0, 1, 1, 3});
|
||||||
|
},
|
||||||
|
[](hid_t f) {
|
||||||
|
WriteColumn(f, "/steps/Step-1/accepted_frames", "time_increment",
|
||||||
|
H5T_NATIVE_DOUBLE, std::vector<double>{0, .3, .3, .3});
|
||||||
|
},
|
||||||
|
[](hid_t f) {
|
||||||
|
WriteColumn(f, "/diagnostics", "line", H5T_NATIVE_UINT64,
|
||||||
|
std::vector<std::uint64_t>{9, 2});
|
||||||
|
},
|
||||||
|
[](hid_t f) {
|
||||||
|
Hdf5Handle g{H5Gcreate2(f, "/steps/Step-1/frames/99", H5P_DEFAULT,
|
||||||
|
H5P_DEFAULT, H5P_DEFAULT),
|
||||||
|
H5Gclose};
|
||||||
|
ASSERT_GE(g.Get(), 0);
|
||||||
|
},
|
||||||
|
[](hid_t f) {
|
||||||
|
ASSERT_GE(H5Ldelete(f, "/steps/Step-1/frames/2", H5P_DEFAULT), 0);
|
||||||
|
},
|
||||||
|
[](hid_t f) {
|
||||||
|
ASSERT_GE(
|
||||||
|
H5Lcreate_soft("/missing", f, "/steps/Step-1/frames/0/element",
|
||||||
|
H5P_DEFAULT, H5P_DEFAULT),
|
||||||
|
0);
|
||||||
|
},
|
||||||
|
[](hid_t f) {
|
||||||
|
Hdf5Handle d{
|
||||||
|
H5Dopen2(f, "/steps/Step-1/frames/1/nodal/reaction_heat_rate",
|
||||||
|
H5P_DEFAULT),
|
||||||
|
H5Dclose};
|
||||||
|
const double values[] = {std::numeric_limits<double>::quiet_NaN(), -9};
|
||||||
|
ASSERT_GE(H5Dwrite(d.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL,
|
||||||
|
H5P_DEFAULT, values),
|
||||||
|
0);
|
||||||
|
},
|
||||||
|
[](hid_t f) {
|
||||||
|
StringAttribute(f, "/metadata", "source_input_identity",
|
||||||
|
"wrong-input-bytes");
|
||||||
|
}};
|
||||||
|
for (std::size_t i = 0; i < mutations.size(); ++i) {
|
||||||
|
SCOPED_TRACE(i);
|
||||||
|
Temp out;
|
||||||
|
ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{}
|
||||||
|
.Write(out.Path(), *x.domain, x.state, diagnostics)
|
||||||
|
.IsOk());
|
||||||
|
{
|
||||||
|
Hdf5Handle file{
|
||||||
|
H5Fopen(out.Path().string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||||
|
H5Fclose};
|
||||||
|
mutations[i](file.Get());
|
||||||
|
}
|
||||||
|
EXPECT_FALSE(fesa::hdf5_internal::SelfCheckThermalFile(
|
||||||
|
out.Path(), *x.domain, x.state, diagnostics)
|
||||||
|
.IsOk());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ThermalHdf5ResultsWriter,
|
||||||
|
RotatedFramesAndEndpointCloseScheduleRoundTripIndependently) {
|
||||||
|
auto definition = Definition();
|
||||||
|
definition.thermal_step->time_period = .9;
|
||||||
|
// Positive 90-degree rotation about X: columns
|
||||||
|
// e1=(1,0,0),e2=(0,0,1),n=(0,-1,0).
|
||||||
|
for (std::size_t i = 1; i < 5; ++i) {
|
||||||
|
const double y = definition.nodes[i].coordinates[1];
|
||||||
|
definition.nodes[i].coordinates[1] = 0;
|
||||||
|
definition.nodes[i].coordinates[2] = y;
|
||||||
|
}
|
||||||
|
const auto x = Make(std::move(definition));
|
||||||
|
Temp out;
|
||||||
|
ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{}
|
||||||
|
.Write(out.Path(), *x.domain, x.state, {})
|
||||||
|
.IsOk());
|
||||||
|
Hdf5Handle f{
|
||||||
|
H5Fopen(out.Path().string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT),
|
||||||
|
H5Fclose};
|
||||||
|
Hdf5Handle d{
|
||||||
|
H5Dopen2(f.Get(), "/model/thermal_flux_local_frames", H5P_DEFAULT),
|
||||||
|
H5Dclose};
|
||||||
|
std::vector<double> frames(36);
|
||||||
|
ASSERT_GE(H5Dread(d.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT,
|
||||||
|
frames.data()),
|
||||||
|
0);
|
||||||
|
const double expected[] = {1, 0, 0, 0, 0, -1, 0, 1, 0};
|
||||||
|
for (std::size_t i = 0; i < frames.size(); ++i)
|
||||||
|
EXPECT_DOUBLE_EQ(frames[i], expected[i % 9]);
|
||||||
|
Hdf5Handle g{H5Gopen2(f.Get(), "/steps/Step-1/frames/3", H5P_DEFAULT),
|
||||||
|
H5Gclose};
|
||||||
|
Hdf5Handle a{H5Aopen(g.Get(), "time_increment", H5P_DEFAULT), H5Aclose};
|
||||||
|
double actual = 0;
|
||||||
|
ASSERT_GE(H5Aread(a.Get(), H5T_NATIVE_DOUBLE, &actual), 0);
|
||||||
|
EXPECT_EQ(actual, .3);
|
||||||
|
EXPECT_NE(actual, .9 - .6);
|
||||||
|
Hdf5Handle initial{
|
||||||
|
H5Dopen2(f.Get(), "/steps/Step-1/frames/0/nodal/temperature",
|
||||||
|
H5P_DEFAULT),
|
||||||
|
H5Dclose};
|
||||||
|
double t[20];
|
||||||
|
ASSERT_GE(H5Dread(initial.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL,
|
||||||
|
H5P_DEFAULT, t),
|
||||||
|
0);
|
||||||
|
for (std::size_t i = 0; i < 20; ++i) EXPECT_EQ(t[i], i < 5 ? -2 : 0);
|
||||||
|
Hdf5Handle flux{H5Dopen2(f.Get(), "/steps/Step-1/frames/3/element/heat_flux",
|
||||||
|
H5P_DEFAULT),
|
||||||
|
H5Dclose};
|
||||||
|
double q[72];
|
||||||
|
ASSERT_GE(
|
||||||
|
H5Dread(flux.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, q),
|
||||||
|
0);
|
||||||
|
for (std::size_t point = 0; point < 4; ++point)
|
||||||
|
for (std::size_t trace = 0; trace < 6; ++trace) {
|
||||||
|
const auto sample = point * 6 + trace;
|
||||||
|
EXPECT_EQ(q[3 * sample], static_cast<double>(sample));
|
||||||
|
EXPECT_EQ(q[3 * sample + 1], -static_cast<double>(sample) - 3);
|
||||||
|
EXPECT_EQ(q[3 * sample + 2], 9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
herr_t FailedClose(hid_t file) {
|
||||||
|
const auto closed = H5Fclose(file);
|
||||||
|
EXPECT_GE(closed, 0);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
TEST(ThermalHdf5ResultsWriter,
|
||||||
|
EveryFailedLifecycleBoundaryPreservesOldFinalAndCleansCandidate) {
|
||||||
|
const auto x = Make();
|
||||||
|
Temp out;
|
||||||
|
ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{}
|
||||||
|
.Write(out.Path(), *x.domain, x.state, {})
|
||||||
|
.IsOk());
|
||||||
|
const auto previous = Bytes(out.Path());
|
||||||
|
std::vector<fesa::hdf5_internal::ThermalFileHooks> failures(5);
|
||||||
|
failures[0].before_write = [](hid_t file) {
|
||||||
|
const hsize_t size = 1;
|
||||||
|
Hdf5Handle s{H5Screate_simple(1, &size, nullptr), H5Sclose};
|
||||||
|
Hdf5Handle d{H5Dcreate2(file, "/metadata", H5T_STD_U8LE, s.Get(),
|
||||||
|
H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT),
|
||||||
|
H5Dclose};
|
||||||
|
ASSERT_GE(d.Get(), 0);
|
||||||
|
};
|
||||||
|
failures[1].candidate_close = FailedClose;
|
||||||
|
failures[2].before_self_check = [](const auto& candidate) {
|
||||||
|
Hdf5Handle f{H5Fopen(candidate.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||||
|
H5Fclose};
|
||||||
|
Hdf5Handle m{H5Gopen2(f.Get(), "/metadata", H5P_DEFAULT), H5Gclose};
|
||||||
|
ASSERT_GE(H5Adelete(m.Get(), "schema_version"), 0);
|
||||||
|
};
|
||||||
|
failures[3].reader_close = FailedClose;
|
||||||
|
failures[4].before_finalize = [](const auto& candidate) {
|
||||||
|
EXPECT_TRUE(std::filesystem::remove(candidate));
|
||||||
|
};
|
||||||
|
for (std::size_t i = 0; i < failures.size(); ++i) {
|
||||||
|
SCOPED_TRACE(i);
|
||||||
|
EXPECT_FALSE(fesa::hdf5_internal::WriteThermalFile(out.Path(), *x.domain,
|
||||||
|
x.state, {}, failures[i])
|
||||||
|
.IsOk());
|
||||||
|
EXPECT_EQ(Bytes(out.Path()), previous);
|
||||||
|
std::size_t files = 0;
|
||||||
|
for (const auto& entry :
|
||||||
|
std::filesystem::directory_iterator(out.Path().parent_path())) {
|
||||||
|
++files;
|
||||||
|
EXPECT_EQ(entry.path(), out.Path());
|
||||||
|
}
|
||||||
|
EXPECT_EQ(files, 1U);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ThermalHdf5ResultsWriter,
|
||||||
|
DomainMismatchAndIncompleteHistoryCannotReplaceFinal) {
|
||||||
|
const auto x = Make();
|
||||||
|
Temp out;
|
||||||
|
ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{}
|
||||||
|
.Write(out.Path(), *x.domain, x.state, {})
|
||||||
|
.IsOk());
|
||||||
|
const auto previous = Bytes(out.Path());
|
||||||
|
auto definition = Definition();
|
||||||
|
definition.nodes[1].source_id = {"part", 8, "0008"};
|
||||||
|
auto different = fesa::Domain::Create(std::move(definition));
|
||||||
|
ASSERT_TRUE(different.HasValue());
|
||||||
|
EXPECT_FALSE(fesa::ThermalHdf5ResultsWriter{}
|
||||||
|
.Write(out.Path(), different.Value(), x.state, {})
|
||||||
|
.IsOk());
|
||||||
|
const auto partial = Make(Definition(), false);
|
||||||
|
EXPECT_FALSE(fesa::ThermalHdf5ResultsWriter{}
|
||||||
|
.Write(out.Path(), *partial.domain, partial.state, {})
|
||||||
|
.IsOk());
|
||||||
|
EXPECT_EQ(Bytes(out.Path()), previous);
|
||||||
|
auto constrained = Definition();
|
||||||
|
constrained.thermal_model_boundaries = {{1, 11, 5, {}}};
|
||||||
|
const auto prescribed = Make(constrained);
|
||||||
|
constrained.thermal_model_boundaries[0].value = 6;
|
||||||
|
auto changed_g = fesa::Domain::Create(std::move(constrained));
|
||||||
|
ASSERT_TRUE(changed_g.HasValue());
|
||||||
|
EXPECT_FALSE(fesa::ThermalHdf5ResultsWriter{}
|
||||||
|
.Write(out.Path(), changed_g.Value(), prescribed.state, {})
|
||||||
|
.IsOk());
|
||||||
|
EXPECT_EQ(Bytes(out.Path()), previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ThermalHdf5ResultsWriter, EveryPrimitiveDatasetChecksShapeTypeAndValue) {
|
||||||
|
auto definition = Definition();
|
||||||
|
definition.thermal_model_boundaries = {{1, 11, 5, {}}};
|
||||||
|
const auto x = Make(std::move(definition));
|
||||||
|
std::vector<std::string> datasets;
|
||||||
|
for (const auto* name :
|
||||||
|
{"thermal_node_ids", "thermal_element_ids", "temperature_dofs",
|
||||||
|
"temperature_zeta", "thermal_constraint_mask",
|
||||||
|
"thermal_flux_inplane_coordinates", "thermal_flux_zeta",
|
||||||
|
"thermal_flux_trace_side", "thermal_flux_local_frames"})
|
||||||
|
datasets.push_back(std::string{"/model/"} + name);
|
||||||
|
for (std::size_t i = 0; i < x.state.AcceptedFrames().size(); ++i) {
|
||||||
|
const auto root = "/steps/Step-1/frames/" + std::to_string(i);
|
||||||
|
datasets.push_back(root + "/nodal/temperature");
|
||||||
|
if (i) {
|
||||||
|
datasets.push_back(root + "/nodal/reaction_heat_rate");
|
||||||
|
datasets.push_back(root + "/element/heat_flux");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const auto& path : datasets)
|
||||||
|
for (int mutation = 0; mutation < 3; ++mutation) {
|
||||||
|
SCOPED_TRACE(path + ":" + std::to_string(mutation));
|
||||||
|
Temp out;
|
||||||
|
ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{}
|
||||||
|
.Write(out.Path(), *x.domain, x.state, {})
|
||||||
|
.IsOk());
|
||||||
|
{
|
||||||
|
Hdf5Handle f{
|
||||||
|
H5Fopen(out.Path().string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||||
|
H5Fclose};
|
||||||
|
if (mutation == 2)
|
||||||
|
ChangePrimitive(f.Get(), path);
|
||||||
|
else
|
||||||
|
ReplacePrimitive(f.Get(), path, mutation == 0);
|
||||||
|
}
|
||||||
|
const auto checked = fesa::hdf5_internal::SelfCheckThermalFile(
|
||||||
|
out.Path(), *x.domain, x.state, {});
|
||||||
|
EXPECT_FALSE(checked.IsOk());
|
||||||
|
if (mutation == 0)
|
||||||
|
EXPECT_NE(Message(checked).find("extent"), std::string::npos)
|
||||||
|
<< Message(checked);
|
||||||
|
if (mutation == 1)
|
||||||
|
EXPECT_NE(Message(checked).find("primitive type"), std::string::npos)
|
||||||
|
<< Message(checked);
|
||||||
|
}
|
||||||
|
for (const auto* path :
|
||||||
|
{"/model/nodes", "/model/elements", "/model/thermal_materials",
|
||||||
|
"/model/shell_sections", "/model/thermal_reaction_dofs",
|
||||||
|
"/steps/Step-1/accepted_frames", "/diagnostics"}) {
|
||||||
|
SCOPED_TRACE(path);
|
||||||
|
Temp out;
|
||||||
|
ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{}
|
||||||
|
.Write(out.Path(), *x.domain, x.state, {})
|
||||||
|
.IsOk());
|
||||||
|
{
|
||||||
|
Hdf5Handle f{
|
||||||
|
H5Fopen(out.Path().string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||||
|
H5Fclose};
|
||||||
|
ReplacePrimitive(f.Get(), path, true);
|
||||||
|
}
|
||||||
|
const auto checked = fesa::hdf5_internal::SelfCheckThermalFile(
|
||||||
|
out.Path(), *x.domain, x.state, {});
|
||||||
|
EXPECT_FALSE(checked.IsOk());
|
||||||
|
EXPECT_NE(Message(checked).find("extent"), std::string::npos)
|
||||||
|
<< Message(checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ThermalHdf5ResultsWriter,
|
||||||
|
InitialRecoveryCannotBeFilledWithCompletedZeros) {
|
||||||
|
const auto x = Make();
|
||||||
|
for (const bool flux : {false, true}) {
|
||||||
|
Temp out;
|
||||||
|
ASSERT_TRUE(fesa::ThermalHdf5ResultsWriter{}
|
||||||
|
.Write(out.Path(), *x.domain, x.state, {})
|
||||||
|
.IsOk());
|
||||||
|
{
|
||||||
|
Hdf5Handle f{
|
||||||
|
H5Fopen(out.Path().string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
|
||||||
|
H5Fclose};
|
||||||
|
if (flux) {
|
||||||
|
Hdf5Handle g{H5Gcreate2(f.Get(), "/steps/Step-1/frames/0/element",
|
||||||
|
H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT),
|
||||||
|
H5Gclose};
|
||||||
|
ASSERT_GE(g.Get(), 0);
|
||||||
|
ASSERT_GE(H5Ocopy(f.Get(), "/steps/Step-1/frames/1/element/heat_flux",
|
||||||
|
f.Get(), "/steps/Step-1/frames/0/element/heat_flux",
|
||||||
|
H5P_DEFAULT, H5P_DEFAULT),
|
||||||
|
0);
|
||||||
|
} else
|
||||||
|
ASSERT_GE(
|
||||||
|
H5Ocopy(f.Get(), "/steps/Step-1/frames/1/nodal/reaction_heat_rate",
|
||||||
|
f.Get(), "/steps/Step-1/frames/0/nodal/reaction_heat_rate",
|
||||||
|
H5P_DEFAULT, H5P_DEFAULT),
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
EXPECT_FALSE(fesa::hdf5_internal::SelfCheckThermalFile(
|
||||||
|
out.Path(), *x.domain, x.state, {})
|
||||||
|
.IsOk());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
Reference in New Issue
Block a user