Files
FESA/tests/integration/io/hdf5_results_test.cpp

1222 lines
42 KiB
C++

#include <fesa/io/hdf5/writer.hpp>
#include <fesa/model/domain_builder.hpp>
#include <hdf5.h>
#include <algorithm>
#include <array>
#include <cstdint>
#include <filesystem>
#include <limits>
#include <optional>
#include <span>
#include <stdexcept>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include <gtest/gtest.h>
namespace {
class TestHdf5Handle final {
public:
using CloseFunction = herr_t (*)(hid_t);
TestHdf5Handle(hid_t id, CloseFunction close)
: id_{id}, close_{close} {
if (id_ < 0) {
throw std::runtime_error{"Failed to open test HDF5 resource."};
}
}
~TestHdf5Handle() {
if (id_ >= 0) {
close_(id_);
}
}
TestHdf5Handle(const TestHdf5Handle&) = delete;
TestHdf5Handle& operator=(const TestHdf5Handle&) = delete;
TestHdf5Handle(TestHdf5Handle&& other) noexcept
: id_{std::exchange(other.id_, H5I_INVALID_HID)},
close_{other.close_} {}
TestHdf5Handle& operator=(TestHdf5Handle&&) = delete;
[[nodiscard]] hid_t get() const noexcept {
return id_;
}
private:
hid_t id_;
CloseFunction close_;
};
std::string hdf5_path(const std::filesystem::path& path) {
const std::u8string value = path.u8string();
return {reinterpret_cast<const char*>(value.data()), value.size()};
}
void require_hdf5_status(const herr_t status) {
if (status < 0) {
throw std::runtime_error{"Test HDF5 mutation failed."};
}
}
void replace_step_time_with_vector(const std::filesystem::path& path) {
const std::string encoded_path = hdf5_path(path);
TestHdf5Handle file{
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
&H5Fclose,
};
TestHdf5Handle frame{
H5Gopen2(
file.get(), "/results/steps/0/frames/0", H5P_DEFAULT),
&H5Gclose,
};
require_hdf5_status(H5Adelete(frame.get(), "step_time"));
const std::array<hsize_t, 1> dimensions{2U};
TestHdf5Handle space{
H5Screate_simple(1, dimensions.data(), nullptr),
&H5Sclose,
};
TestHdf5Handle attribute{
H5Acreate2(
frame.get(),
"step_time",
H5T_IEEE_F64LE,
space.get(),
H5P_DEFAULT,
H5P_DEFAULT),
&H5Aclose,
};
const std::array<double, 2> values{1.25, 2.5};
require_hdf5_status(
H5Awrite(attribute.get(), H5T_NATIVE_DOUBLE, values.data()));
}
void delete_link(
const std::filesystem::path& path,
const std::string_view link_path) {
const std::string encoded_path = hdf5_path(path);
const std::string owned_link_path{link_path};
TestHdf5Handle file{
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
&H5Fclose,
};
require_hdf5_status(
H5Ldelete(file.get(), owned_link_path.c_str(), H5P_DEFAULT));
}
void copy_object(
const std::filesystem::path& path,
const std::string_view source_path,
const std::string_view target_path) {
const std::string encoded_path = hdf5_path(path);
const std::string owned_source_path{source_path};
const std::string owned_target_path{target_path};
TestHdf5Handle file{
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
&H5Fclose,
};
require_hdf5_status(H5Ocopy(
file.get(),
owned_source_path.c_str(),
file.get(),
owned_target_path.c_str(),
H5P_DEFAULT,
H5P_DEFAULT));
}
void write_double_attribute(
const std::filesystem::path& path,
const std::string_view object_path,
const std::string_view attribute_name,
const double value) {
const std::string encoded_path = hdf5_path(path);
const std::string owned_object_path{object_path};
const std::string owned_attribute_name{attribute_name};
TestHdf5Handle file{
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
&H5Fclose,
};
TestHdf5Handle object{
H5Oopen(file.get(), owned_object_path.c_str(), H5P_DEFAULT),
&H5Oclose,
};
TestHdf5Handle attribute{
H5Aopen(object.get(), owned_attribute_name.c_str(), H5P_DEFAULT),
&H5Aclose,
};
require_hdf5_status(
H5Awrite(attribute.get(), H5T_NATIVE_DOUBLE, &value));
}
void write_int64_dataset(
const std::filesystem::path& path,
const std::string_view dataset_path,
const std::span<const std::int64_t> values) {
const std::string encoded_path = hdf5_path(path);
const std::string owned_dataset_path{dataset_path};
TestHdf5Handle file{
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
&H5Fclose,
};
TestHdf5Handle dataset{
H5Dopen2(
file.get(), owned_dataset_path.c_str(), H5P_DEFAULT),
&H5Dclose,
};
require_hdf5_status(H5Dwrite(
dataset.get(),
H5T_NATIVE_INT64,
H5S_ALL,
H5S_ALL,
H5P_DEFAULT,
values.data()));
}
void write_double_dataset(
const std::filesystem::path& path,
const std::string_view dataset_path,
const std::span<const double> values) {
const std::string encoded_path = hdf5_path(path);
const std::string owned_dataset_path{dataset_path};
TestHdf5Handle file{
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
&H5Fclose,
};
TestHdf5Handle dataset{
H5Dopen2(
file.get(), owned_dataset_path.c_str(), H5P_DEFAULT),
&H5Dclose,
};
require_hdf5_status(H5Dwrite(
dataset.get(),
H5T_NATIVE_DOUBLE,
H5S_ALL,
H5S_ALL,
H5P_DEFAULT,
values.data()));
}
void write_uint8_dataset(
const std::filesystem::path& path,
const std::string_view dataset_path,
const std::span<const std::uint8_t> values) {
const std::string encoded_path = hdf5_path(path);
const std::string owned_dataset_path{dataset_path};
TestHdf5Handle file{
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
&H5Fclose,
};
TestHdf5Handle dataset{
H5Dopen2(
file.get(), owned_dataset_path.c_str(), H5P_DEFAULT),
&H5Dclose,
};
require_hdf5_status(H5Dwrite(
dataset.get(),
H5T_NATIVE_UINT8,
H5S_ALL,
H5S_ALL,
H5P_DEFAULT,
values.data()));
}
void write_root_string_attribute(
const std::filesystem::path& path,
const std::string_view name,
const std::string_view value) {
const std::string encoded_path = hdf5_path(path);
const std::string owned_name{name};
const std::string owned_value{value};
TestHdf5Handle file{
H5Fopen(encoded_path.c_str(), H5F_ACC_RDWR, H5P_DEFAULT),
&H5Fclose,
};
TestHdf5Handle attribute{
H5Aopen(file.get(), owned_name.c_str(), H5P_DEFAULT),
&H5Aclose,
};
TestHdf5Handle type{H5Aget_type(attribute.get()), &H5Tclose};
const char* pointer = owned_value.c_str();
require_hdf5_status(H5Awrite(attribute.get(), type.get(), &pointer));
}
std::filesystem::path round_trip_path() {
return std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" /
"Temporary" / "fesa-round-trip.h5";
}
std::filesystem::path self_contained_path() {
return std::filesystem::path{FESA_TEST_BINARY_DIR} / "Testing" /
"Temporary" / "fesa-self-contained.h5";
}
const fesa::Hdf5InputIdentity& test_input_identity() {
static const fesa::Hdf5InputIdentity identity{
"beam model.inp",
"fnv1a64:0123456789abcdef",
};
return identity;
}
fesa::Domain make_domain(const bool add_unreported_node = false) {
fesa::DomainBuilder builder;
builder.add_node({
fesa::NodeId{42},
fesa::EntityOrigin{"BeamPart", "Beam-1", 1001},
fesa::Vec3{1.25, -2.5, 3.75},
});
builder.add_node({
fesa::NodeId{7},
fesa::EntityOrigin{"BeamPart", "Beam-1", 1002},
fesa::Vec3{2.25, -2.5, 3.75},
});
if (add_unreported_node) {
builder.add_node({
fesa::NodeId{99},
fesa::EntityOrigin{"BeamPart", "Beam-1", 1003},
fesa::Vec3{8.0, 0.0, 0.0},
});
}
builder.add_material({
fesa::MaterialId{6},
"Steel",
210.0e9,
0.3,
});
builder.add_section({
fesa::SectionId{4},
"InputShear",
0.04,
1.2e-4,
1.4e-4,
2.0e-4,
0.031,
0.032,
fesa::ShearPropertySource::input,
fesa::Vec3{0.0, 1.0, 0.0},
{{0.25, -0.5}, {-0.75, 0.125}},
});
builder.add_section({
fesa::SectionId{12},
"DefaultShear",
0.06,
1.5e-4,
1.7e-4,
2.2e-4,
0.05,
0.05,
fesa::ShearPropertySource::phase1_default,
fesa::Vec3{0.0, 1.0, 0.0},
{},
});
builder.add_beam_element({
fesa::ElementId{9},
fesa::EntityOrigin{"BeamPart", "Beam-1", 2001},
{fesa::NodeId{7}, fesa::NodeId{42}},
fesa::MaterialId{6},
fesa::SectionId{4},
});
builder.add_beam_element({
fesa::ElementId{17},
fesa::EntityOrigin{"BeamPart", "Beam-1", 2002},
{fesa::NodeId{42}, fesa::NodeId{7}},
fesa::MaterialId{6},
fesa::SectionId{4},
});
builder.add_node_set({"Fixed", {fesa::NodeId{42}}});
builder.add_node_set(
{"Loaded", {fesa::NodeId{7}, fesa::NodeId{42}}});
builder.add_element_set(
{"AllBeams", {fesa::ElementId{9}, fesa::ElementId{17}}});
builder.set_step({
"Load/Case",
{
{fesa::NodeId{42}, 1, 0.0},
{fesa::NodeId{7}, 6, 0.125},
},
{{
fesa::NodeId{7},
{100.0, -200.0, 300.0, -400.0, 500.0, -600.0},
}},
});
auto built = std::move(builder).build();
EXPECT_TRUE(built.domain.has_value());
EXPECT_TRUE(built.diagnostics.empty());
return std::move(*built.domain);
}
fesa::BeamSectionResult make_end_result(
double xi,
fesa::NodeId node,
double offset);
fesa::ResultDatabase make_database() {
fesa::ResultDatabase database{
"2.0.0",
{{
"Load/Case",
{{
1.25,
{
{fesa::NodeId{7}, fesa::NodeId{42}},
{
fesa::EntityOrigin{
"BeamPart", "Beam-1", 1002},
fesa::EntityOrigin{
"BeamPart", "Beam-1", 1001},
},
{
{1.0, 2.0, 3.0, 4.0, 5.0, 6.0},
{-1.0, -2.0, -3.0, -4.0, -5.0, -6.0},
},
{
{10.0, 20.0, 30.0, 40.0, 50.0, 60.0},
{-10.0, -20.0, -30.0, -40.0, -50.0, -60.0},
},
},
{},
{},
}},
}},
};
database.steps[0].frames[0].element.beams = {
{
fesa::ElementId{9},
{"BeamPart", "Beam-1", 2001},
{
{-1.0, 0.0, 0.0},
{0.0, 1.0, 0.0},
{0.0, 0.0, -1.0},
},
{
make_end_result(-1.0, fesa::NodeId{7}, 0.0),
make_end_result(1.0, fesa::NodeId{42}, 100.0),
},
},
{
fesa::ElementId{17},
{"BeamPart", "Beam-1", 2002},
{
{1.0, 0.0, 0.0},
{0.0, 1.0, 0.0},
{0.0, 0.0, 1.0},
},
{
make_end_result(-1.0, fesa::NodeId{42}, 200.0),
make_end_result(1.0, fesa::NodeId{7}, 300.0),
},
},
};
return database;
}
fesa::BeamSectionResult make_end_result(
const double xi,
const fesa::NodeId node,
const double offset) {
return {
xi,
node,
{
offset + 1.0,
offset + 2.0,
offset + 3.0,
offset + 4.0,
offset + 5.0,
offset + 6.0,
},
{
offset + 10.0,
offset + 20.0,
offset + 30.0,
offset + 40.0,
offset + 50.0,
offset + 60.0,
},
offset + 70.0,
{offset + 80.0, offset + 90.0},
};
}
fesa::ResultDatabase make_complete_database() {
fesa::ResultDatabase database = make_database();
auto& frame = database.steps[0].frames[0];
frame.diagnostics = {
{
fesa::DiagnosticStage::solver,
fesa::Severity::warning,
"solver.residual",
"Residual diagnostic",
fesa::SourceLocation{"beam model.inp", 41, 7},
},
{
fesa::DiagnosticStage::results,
fesa::Severity::error,
"results.equilibrium",
"Equilibrium diagnostic",
std::nullopt,
},
};
return database;
}
bool has_results_error(
const std::vector<fesa::Diagnostic>& diagnostics,
const std::string_view code) {
return std::ranges::any_of(
diagnostics,
[code](const fesa::Diagnostic& diagnostic) {
return diagnostic.stage == fesa::DiagnosticStage::results &&
diagnostic.severity == fesa::Severity::error &&
diagnostic.code == code;
});
}
TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
const auto path = round_trip_path();
std::filesystem::create_directories(path.parent_path());
std::filesystem::remove(path);
const auto domain = make_domain();
const auto database = make_database();
const auto write_diagnostics = fesa::write_hdf5(
path, domain, database, test_input_identity());
ASSERT_TRUE(write_diagnostics.empty());
const auto read = fesa::read_hdf5_results(path);
ASSERT_TRUE(read.diagnostics.empty());
ASSERT_TRUE(read.database.has_value());
ASSERT_TRUE(read.model.has_value());
EXPECT_EQ(read.database->schema_version, "2.0.0");
ASSERT_EQ(read.database->steps.size(), 1U);
const auto& step = read.database->steps[0];
EXPECT_EQ(step.name, "Load/Case");
ASSERT_EQ(step.frames.size(), 1U);
const auto& frame = step.frames[0];
EXPECT_DOUBLE_EQ(frame.step_time, 1.25);
EXPECT_EQ(
frame.nodal.node_ids,
(std::vector<fesa::NodeId>{fesa::NodeId{7}, fesa::NodeId{42}}));
EXPECT_EQ(
frame.nodal.origins,
(std::vector<fesa::EntityOrigin>{
{"BeamPart", "Beam-1", 1002},
{"BeamPart", "Beam-1", 1001},
}));
EXPECT_EQ(
frame.nodal.displacement[0],
(std::array<double, 6>{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}));
EXPECT_EQ(
frame.nodal.displacement[1],
(std::array<double, 6>{-1.0, -2.0, -3.0, -4.0, -5.0, -6.0}));
EXPECT_EQ(
frame.nodal.reaction[0],
(std::array<double, 6>{10.0, 20.0, 30.0, 40.0, 50.0, 60.0}));
EXPECT_EQ(
frame.nodal.reaction[1],
(std::array<double, 6>{
-10.0, -20.0, -30.0, -40.0, -50.0, -60.0}));
EXPECT_TRUE(frame.diagnostics.empty());
ASSERT_EQ(read.model->nodes.size(), 2U);
EXPECT_EQ(read.model->nodes[0].dense_index, 0U);
EXPECT_EQ(read.model->nodes[0].id, fesa::NodeId{42});
EXPECT_EQ(
read.model->nodes[0].origin,
(fesa::EntityOrigin{"BeamPart", "Beam-1", 1001}));
EXPECT_DOUBLE_EQ(read.model->nodes[0].coordinates.x, 1.25);
EXPECT_DOUBLE_EQ(read.model->nodes[0].coordinates.y, -2.5);
EXPECT_DOUBLE_EQ(read.model->nodes[0].coordinates.z, 3.75);
EXPECT_EQ(read.model->nodes[1].dense_index, 1U);
EXPECT_EQ(read.model->nodes[1].id, fesa::NodeId{7});
EXPECT_EQ(
read.model->nodes[1].origin,
(fesa::EntityOrigin{"BeamPart", "Beam-1", 1002}));
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.x, 2.25);
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.y, -2.5);
EXPECT_DOUBLE_EQ(read.model->nodes[1].coordinates.z, 3.75);
ASSERT_EQ(read.model->elements.size(), 2U);
EXPECT_EQ(read.model->elements[0].dense_index, 0U);
EXPECT_EQ(read.model->elements[0].id, fesa::ElementId{9});
EXPECT_EQ(
read.model->elements[0].connectivity,
(std::array<std::uint64_t, 2>{1U, 0U}));
EXPECT_EQ(read.model->elements[0].section, fesa::SectionId{4});
EXPECT_EQ(read.model->elements[1].dense_index, 1U);
EXPECT_EQ(read.model->elements[1].id, fesa::ElementId{17});
EXPECT_EQ(
read.model->elements[1].connectivity,
(std::array<std::uint64_t, 2>{0U, 1U}));
EXPECT_EQ(read.model->elements[1].section, fesa::SectionId{4});
ASSERT_EQ(read.model->sections.size(), 2U);
EXPECT_EQ(read.model->sections[0].id, fesa::SectionId{4});
EXPECT_DOUBLE_EQ(read.model->sections[0].shear_area_y, 0.031);
EXPECT_DOUBLE_EQ(read.model->sections[0].shear_area_z, 0.032);
EXPECT_EQ(
read.model->sections[0].shear_source,
fesa::ShearPropertySource::input);
EXPECT_EQ(read.model->sections[1].id, fesa::SectionId{12});
EXPECT_DOUBLE_EQ(read.model->sections[1].shear_area_y, 0.05);
EXPECT_DOUBLE_EQ(read.model->sections[1].shear_area_z, 0.05);
EXPECT_EQ(
read.model->sections[1].shear_source,
fesa::ShearPropertySource::phase1_default);
}
TEST(SelfContainedHdf5, PublicReaderReconstructsCompletePhase1Contract) {
const auto path = self_contained_path();
std::filesystem::create_directories(path.parent_path());
std::filesystem::remove(path);
ASSERT_TRUE(fesa::write_hdf5(
path,
make_domain(),
make_complete_database(),
test_input_identity())
.empty());
const fesa::Hdf5ReadResult read = fesa::read_hdf5_results(path);
ASSERT_TRUE(read.diagnostics.empty());
ASSERT_TRUE(read.metadata.has_value());
ASSERT_TRUE(read.model.has_value());
ASSERT_TRUE(read.analysis.has_value());
ASSERT_TRUE(read.database.has_value());
EXPECT_EQ(read.metadata->schema_version, "2.0.0");
EXPECT_EQ(read.metadata->fesa_version, "0.1.0");
EXPECT_EQ(
read.metadata->unit_policy,
"consistent_input_units_no_conversion");
EXPECT_EQ(read.metadata->input_source, "beam model.inp");
EXPECT_EQ(
read.metadata->input_fingerprint,
"fnv1a64:0123456789abcdef");
ASSERT_EQ(read.model->nodes.size(), 2U);
ASSERT_EQ(read.model->elements.size(), 2U);
EXPECT_EQ(
read.model->elements[0].origin,
(fesa::EntityOrigin{"BeamPart", "Beam-1", 2001}));
EXPECT_EQ(read.model->elements[0].material, fesa::MaterialId{6});
ASSERT_EQ(read.model->materials.size(), 1U);
EXPECT_EQ(read.model->materials[0].name, "Steel");
EXPECT_DOUBLE_EQ(read.model->materials[0].young, 210.0e9);
EXPECT_DOUBLE_EQ(read.model->materials[0].poisson, 0.3);
ASSERT_EQ(read.model->sections.size(), 2U);
EXPECT_EQ(read.model->sections[0].name, "InputShear");
EXPECT_DOUBLE_EQ(read.model->sections[0].area, 0.04);
EXPECT_DOUBLE_EQ(read.model->sections[0].iy, 1.2e-4);
EXPECT_DOUBLE_EQ(read.model->sections[0].iz, 1.4e-4);
EXPECT_DOUBLE_EQ(read.model->sections[0].torsion_j, 2.0e-4);
EXPECT_DOUBLE_EQ(read.model->sections[0].orientation.y, 1.0);
EXPECT_EQ(
read.model->sections[0].recovery_points,
(std::vector<std::array<double, 2>>{
{0.25, -0.5}, {-0.75, 0.125}}));
ASSERT_EQ(read.model->node_sets.size(), 2U);
EXPECT_EQ(read.model->node_sets[0].name, "Fixed");
EXPECT_EQ(
read.model->node_sets[1].members,
(std::vector<fesa::NodeId>{
fesa::NodeId{7}, fesa::NodeId{42}}));
ASSERT_EQ(read.model->element_sets.size(), 1U);
EXPECT_EQ(
read.model->element_sets[0].members,
(std::vector<fesa::ElementId>{
fesa::ElementId{9}, fesa::ElementId{17}}));
EXPECT_EQ(read.analysis->step.name, "Load/Case");
ASSERT_EQ(read.analysis->step.prescribed_dofs.size(), 2U);
EXPECT_EQ(
read.analysis->step.prescribed_dofs[1].node,
fesa::NodeId{7});
EXPECT_EQ(read.analysis->step.prescribed_dofs[1].dof, 6U);
EXPECT_DOUBLE_EQ(
read.analysis->step.prescribed_dofs[1].value, 0.125);
ASSERT_EQ(read.analysis->step.nodal_loads.size(), 1U);
EXPECT_DOUBLE_EQ(
read.analysis->step.nodal_loads[0].values[5], -600.0);
EXPECT_EQ(read.analysis->solver.backend, "mkl_pardiso");
EXPECT_EQ(
read.analysis->solver.constraint_method,
"essential_dof_elimination");
EXPECT_EQ(read.analysis->solver.assembly, "deterministic_serial");
const auto& frame = read.database->steps[0].frames[0];
ASSERT_EQ(frame.element.beams.size(), 2U);
EXPECT_EQ(frame.element.beams[0].element, fesa::ElementId{9});
EXPECT_DOUBLE_EQ(frame.element.beams[0].local_frame.ex.x, -1.0);
EXPECT_EQ(
frame.element.beams[0].end_results[0].end_node,
fesa::NodeId{7});
EXPECT_EQ(
frame.element.beams[0].end_results[1].section_force,
(std::array<double, 6>{
110.0, 120.0, 130.0, 140.0, 150.0, 160.0}));
EXPECT_EQ(
frame.element.beams[1].end_results[0].sigma_xx,
(std::vector<double>{280.0, 290.0}));
ASSERT_EQ(frame.diagnostics.size(), 2U);
EXPECT_EQ(frame.diagnostics[0].stage, fesa::DiagnosticStage::solver);
EXPECT_EQ(frame.diagnostics[0].severity, fesa::Severity::warning);
ASSERT_TRUE(frame.diagnostics[0].source.has_value());
EXPECT_EQ(frame.diagnostics[0].source->file, "beam model.inp");
EXPECT_EQ(frame.diagnostics[0].source->line, 41U);
EXPECT_EQ(frame.diagnostics[0].source->column, 7U);
EXPECT_FALSE(frame.diagnostics[1].source.has_value());
}
TEST(SelfContainedHdf5, RejectsMissingResultFrameBeforeWriting) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-missing-result-frame.h5";
std::filesystem::remove(path);
auto database = make_complete_database();
database.steps[0].frames.clear();
const auto diagnostics = fesa::write_hdf5(
path, make_domain(), database, test_input_identity());
EXPECT_TRUE(has_results_error(
diagnostics, "hdf5.incomplete_result_frame"));
EXPECT_FALSE(std::filesystem::exists(path));
}
TEST(SelfContainedHdf5, RejectsIncompleteNodalCoverageBeforeWriting) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-incomplete-nodal-results.h5";
std::filesystem::remove(path);
const auto database = make_complete_database();
const auto diagnostics = fesa::write_hdf5(
path, make_domain(true), database, test_input_identity());
EXPECT_TRUE(has_results_error(
diagnostics, "hdf5.incomplete_result_frame"));
EXPECT_FALSE(std::filesystem::exists(path));
}
TEST(SelfContainedHdf5, RejectsIncompleteBeamCoverageBeforeWriting) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-incomplete-beam-results.h5";
std::filesystem::remove(path);
auto database = make_complete_database();
database.steps[0].frames[0].element.beams.pop_back();
const auto diagnostics = fesa::write_hdf5(
path, make_domain(), database, test_input_identity());
EXPECT_TRUE(has_results_error(
diagnostics, "hdf5.incomplete_result_frame"));
EXPECT_FALSE(std::filesystem::exists(path));
}
TEST(SelfContainedHdf5, RejectsFiniteLocalFrameThatDisagreesWithModel) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-wrong-local-frame.h5";
std::filesystem::remove(path);
auto database = make_complete_database();
database.steps[0].frames[0].element.beams[0].local_frame = {
{1.0, 0.0, 0.0},
{0.0, 1.0, 0.0},
{0.0, 0.0, 1.0},
};
const auto diagnostics = fesa::write_hdf5(
path, make_domain(), database, test_input_identity());
EXPECT_TRUE(has_results_error(
diagnostics, "hdf5.result_element_mismatch"));
EXPECT_FALSE(std::filesystem::exists(path));
}
TEST(Hdf5, RejectsVersion1AfterMajorSchemaChange) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" / "fesa-schema-1.h5";
std::filesystem::remove(path);
auto database = make_database();
database.schema_version = "1.0.0";
const auto diagnostics = fesa::write_hdf5(
path, make_domain(), database, test_input_identity());
EXPECT_TRUE(has_results_error(diagnostics, "hdf5.unsupported_schema"));
EXPECT_FALSE(std::filesystem::exists(path));
}
TEST(Hdf5, RejectsMissingInputIdentityBeforeWriting) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-missing-input-identity.h5";
std::filesystem::remove(path);
const auto diagnostics = fesa::write_hdf5(
path, make_domain(), make_database(), {"", ""});
EXPECT_TRUE(has_results_error(
diagnostics, "hdf5.invalid_input_identity"));
EXPECT_FALSE(std::filesystem::exists(path));
}
TEST(Hdf5, RejectsUnlistedMinorSchemaVersion) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" / "fesa-schema-2-1.h5";
std::filesystem::remove(path);
ASSERT_TRUE(fesa::write_hdf5(
path, make_domain(), make_database(), test_input_identity())
.empty());
write_root_string_attribute(path, "schema_version", "2.1.0");
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_FALSE(read.metadata.has_value());
EXPECT_FALSE(read.analysis.has_value());
EXPECT_TRUE(has_results_error(
read.diagnostics, "hdf5.unsupported_schema"));
}
TEST(Hdf5, RejectsInvalidResultDatabaseBeforeWriting) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" / "fesa-invalid.h5";
std::filesystem::remove(path);
const auto domain = make_domain();
auto database = make_database();
database.steps[0].frames[0].nodal.reaction.pop_back();
const auto diagnostics =
fesa::write_hdf5(path, domain, database, test_input_identity());
EXPECT_TRUE(
has_results_error(diagnostics, "results.nodal_size_mismatch"));
EXPECT_FALSE(std::filesystem::exists(path));
}
TEST(Hdf5, PreservesFrameDiagnosticsRepresentedBySchema) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-unsupported-diagnostics.h5";
std::filesystem::remove(path);
const auto domain = make_domain();
auto database = make_database();
database.steps[0].frames[0].diagnostics.push_back({
fesa::DiagnosticStage::solver,
fesa::Severity::warning,
"solver.residual",
"Residual diagnostic",
std::nullopt,
});
const auto diagnostics =
fesa::write_hdf5(path, domain, database, test_input_identity());
ASSERT_TRUE(diagnostics.empty());
const auto read = fesa::read_hdf5_results(path);
ASSERT_TRUE(read.diagnostics.empty());
ASSERT_TRUE(read.database.has_value());
const auto& stored = read.database->steps[0].frames[0].diagnostics;
ASSERT_EQ(stored.size(), 1U);
EXPECT_EQ(stored[0].stage, fesa::DiagnosticStage::solver);
EXPECT_EQ(stored[0].severity, fesa::Severity::warning);
EXPECT_EQ(stored[0].code, "solver.residual");
EXPECT_EQ(stored[0].message, "Residual diagnostic");
EXPECT_FALSE(stored[0].source.has_value());
}
TEST(Hdf5, RejectsResultNodeMissingFromDomainBeforeWriting) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-missing-result-node.h5";
std::filesystem::remove(path);
const auto domain = make_domain();
auto database = make_database();
auto& nodal = database.steps[0].frames[0].nodal;
nodal.node_ids.push_back(fesa::NodeId{999});
nodal.origins.push_back({"BeamPart", "Beam-1", 1999});
nodal.displacement.push_back({});
nodal.reaction.push_back({});
const auto diagnostics =
fesa::write_hdf5(path, domain, database, test_input_identity());
EXPECT_TRUE(has_results_error(
diagnostics, "hdf5.result_node_not_in_model"));
EXPECT_FALSE(std::filesystem::exists(path));
}
TEST(Hdf5, ReportsMissingFileAtResultsStage) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" / "fesa-missing.h5";
std::filesystem::remove(path);
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_TRUE(has_results_error(read.diagnostics, "hdf5.open_failed"));
}
TEST(Hdf5, RejectsNonScalarStepTimeAttribute) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-nonscalar-step-time.h5";
std::filesystem::remove(path);
const auto write_diagnostics = fesa::write_hdf5(
path, make_domain(), make_database(), test_input_identity());
ASSERT_TRUE(write_diagnostics.empty());
replace_step_time_with_vector(path);
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_TRUE(has_results_error(read.diagnostics, "hdf5.read_failed"));
}
TEST(Hdf5, RejectsSerializedResultWithoutRequiredFrame) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-missing-serialized-frame.h5";
std::filesystem::remove(path);
ASSERT_TRUE(fesa::write_hdf5(
path, make_domain(), make_database(), test_input_identity())
.empty());
delete_link(path, "/results/steps/0/frames/0");
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_FALSE(read.metadata.has_value());
EXPECT_FALSE(read.analysis.has_value());
EXPECT_TRUE(has_results_error(
read.diagnostics, "hdf5.incomplete_result_frame"));
}
TEST(Hdf5, RejectsSerializedResultWithExtraFrame) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-extra-serialized-frame.h5";
std::filesystem::remove(path);
ASSERT_TRUE(fesa::write_hdf5(
path, make_domain(), make_database(), test_input_identity())
.empty());
copy_object(
path,
"/results/steps/0/frames/0",
"/results/steps/0/frames/1");
write_double_attribute(
path, "/results/steps/0/frames/1", "step_time", 2.5);
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_FALSE(read.metadata.has_value());
EXPECT_FALSE(read.analysis.has_value());
EXPECT_TRUE(has_results_error(
read.diagnostics, "hdf5.incomplete_result_frame"));
}
TEST(Hdf5, RejectsInvalidSerializedInputFingerprint) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-invalid-input-fingerprint.h5";
std::filesystem::remove(path);
ASSERT_TRUE(fesa::write_hdf5(
path, make_domain(), make_database(), test_input_identity())
.empty());
write_root_string_attribute(
path, "input_fingerprint", "sha256:not-the-contract");
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_FALSE(read.metadata.has_value());
EXPECT_FALSE(read.analysis.has_value());
EXPECT_TRUE(has_results_error(
read.diagnostics, "hdf5.invalid_input_identity"));
}
TEST(Hdf5, RejectsResultNodeMissingFromSerializedModel) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-invalid-result-node.h5";
std::filesystem::remove(path);
auto database = make_database();
auto& nodal = database.steps[0].frames[0].nodal;
nodal.node_ids.push_back(fesa::NodeId{99});
nodal.origins.push_back({"BeamPart", "Beam-1", 1003});
nodal.displacement.push_back({});
nodal.reaction.push_back({});
const auto write_diagnostics = fesa::write_hdf5(
path, make_domain(true), database, test_input_identity());
ASSERT_TRUE(write_diagnostics.empty());
const std::array<std::int64_t, 3> node_ids{42, 7, 100};
write_int64_dataset(path, "/model/nodes/internal_id", node_ids);
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_TRUE(has_results_error(
read.diagnostics, "hdf5.result_node_not_in_model"));
}
TEST(Hdf5, RejectsDuplicateSerializedNodeIds) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-duplicate-node-ids.h5";
std::filesystem::remove(path);
ASSERT_TRUE(fesa::write_hdf5(
path, make_domain(), make_database(), test_input_identity())
.empty());
const std::array<std::int64_t, 2> ids{42, 42};
write_int64_dataset(path, "/model/nodes/internal_id", ids);
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_TRUE(
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
}
TEST(Hdf5, RejectsDuplicateSerializedElementIds) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-duplicate-element-ids.h5";
std::filesystem::remove(path);
ASSERT_TRUE(fesa::write_hdf5(
path, make_domain(), make_database(), test_input_identity())
.empty());
const std::array<std::int64_t, 2> ids{9, 9};
write_int64_dataset(path, "/model/elements/internal_id", ids);
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_TRUE(
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
}
TEST(Hdf5, RejectsDuplicateSerializedSectionIds) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-duplicate-section-ids.h5";
std::filesystem::remove(path);
ASSERT_TRUE(fesa::write_hdf5(
path, make_domain(), make_database(), test_input_identity())
.empty());
const std::array<std::int64_t, 2> ids{4, 4};
write_int64_dataset(path, "/model/sections/internal_id", ids);
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_TRUE(
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
}
TEST(Hdf5, RejectsNonfiniteSerializedCoordinates) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-nonfinite-coordinates.h5";
std::filesystem::remove(path);
ASSERT_TRUE(fesa::write_hdf5(
path, make_domain(), make_database(), test_input_identity())
.empty());
const std::array<double, 6> coordinates{
std::numeric_limits<double>::quiet_NaN(),
-2.5,
3.75,
2.25,
-2.5,
3.75,
};
write_double_dataset(path, "/model/nodes/coordinates", coordinates);
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_TRUE(
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
}
TEST(Hdf5, RejectsSerializedZeroLengthElement) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-zero-length-element.h5";
std::filesystem::remove(path);
ASSERT_TRUE(fesa::write_hdf5(
path, make_domain(), make_database(), test_input_identity())
.empty());
const std::array<double, 6> coordinates{
1.25, -2.5, 3.75, 1.25, -2.5, 3.75};
write_double_dataset(path, "/model/nodes/coordinates", coordinates);
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_TRUE(
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
}
TEST(Hdf5, RejectsSerializedOrientationParallelToElement) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-parallel-orientation.h5";
std::filesystem::remove(path);
ASSERT_TRUE(fesa::write_hdf5(
path, make_domain(), make_database(), test_input_identity())
.empty());
const std::array<double, 6> orientations{
1.0, 0.0, 0.0, 0.0, 1.0, 0.0};
write_double_dataset(
path, "/model/sections/orientation", orientations);
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_TRUE(
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
}
TEST(Hdf5, RejectsDuplicateSerializedNodeOrigins) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-duplicate-node-origins.h5";
std::filesystem::remove(path);
ASSERT_TRUE(fesa::write_hdf5(
path, make_domain(), make_database(), test_input_identity())
.empty());
const std::array<std::int64_t, 2> labels{1001, 1001};
write_int64_dataset(path, "/model/nodes/local_label", labels);
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_TRUE(
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
}
TEST(Hdf5, RejectsDuplicateSerializedBoundaryConditions) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-duplicate-boundary-conditions.h5";
std::filesystem::remove(path);
ASSERT_TRUE(fesa::write_hdf5(
path, make_domain(), make_database(), test_input_identity())
.empty());
const std::array<std::int64_t, 2> node_ids{42, 42};
const std::array<std::uint8_t, 2> dofs{1, 1};
write_int64_dataset(
path,
"/analysis/steps/0/boundary_conditions/node_ids",
node_ids);
write_uint8_dataset(
path, "/analysis/steps/0/boundary_conditions/dofs", dofs);
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_TRUE(
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
}
TEST(Hdf5, RejectsSerializedLocalFrameThatDisagreesWithModel) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-serialized-wrong-local-frame.h5";
std::filesystem::remove(path);
ASSERT_TRUE(fesa::write_hdf5(
path, make_domain(), make_database(), test_input_identity())
.empty());
const std::array<double, 18> local_frames{
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, 1.0,
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, 1.0,
};
write_double_dataset(
path,
"/results/steps/0/frames/0/element/beam/local_frame",
local_frames);
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_TRUE(has_results_error(
read.diagnostics, "hdf5.result_element_mismatch"));
}
TEST(Hdf5, RejectsNonfiniteSerializedShearArea) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-nonfinite-shear-area.h5";
std::filesystem::remove(path);
ASSERT_TRUE(fesa::write_hdf5(
path, make_domain(), make_database(), test_input_identity())
.empty());
const std::array<double, 2> shear_areas{
std::numeric_limits<double>::infinity(),
0.05,
};
write_double_dataset(path, "/model/sections/shear_area_y", shear_areas);
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_TRUE(
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
}
TEST(Hdf5, RejectsNonpositiveSerializedShearArea) {
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
"Testing" / "Temporary" /
"fesa-nonpositive-shear-area.h5";
std::filesystem::remove(path);
ASSERT_TRUE(fesa::write_hdf5(
path, make_domain(), make_database(), test_input_identity())
.empty());
const std::array<double, 2> shear_areas{-0.031, 0.05};
write_double_dataset(path, "/model/sections/shear_area_y", shear_areas);
const auto read = fesa::read_hdf5_results(path);
EXPECT_FALSE(read.database.has_value());
EXPECT_FALSE(read.model.has_value());
EXPECT_TRUE(
has_results_error(read.diagnostics, "hdf5.invalid_model_data"));
}
} // namespace