feat(result-contract-completion): step 2 — self-contained-hdf5
This commit is contained in:
@@ -146,11 +146,36 @@ void write_double_dataset(
|
||||
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";
|
||||
}
|
||||
|
||||
fesa::Domain make_domain() {
|
||||
fesa::DomainBuilder builder;
|
||||
builder.add_node({
|
||||
@@ -180,7 +205,7 @@ fesa::Domain make_domain() {
|
||||
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},
|
||||
@@ -209,7 +234,22 @@ fesa::Domain make_domain() {
|
||||
fesa::MaterialId{6},
|
||||
fesa::SectionId{4},
|
||||
});
|
||||
builder.set_step({"Load/Case", {}, {}});
|
||||
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());
|
||||
@@ -219,7 +259,7 @@ fesa::Domain make_domain() {
|
||||
|
||||
fesa::ResultDatabase make_database() {
|
||||
return {
|
||||
"1.0.0",
|
||||
"2.0.0",
|
||||
{{
|
||||
"Load/Case",
|
||||
{{
|
||||
@@ -248,6 +288,84 @@ fesa::ResultDatabase make_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.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),
|
||||
},
|
||||
},
|
||||
};
|
||||
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) {
|
||||
@@ -276,7 +394,7 @@ TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
|
||||
ASSERT_TRUE(read.database.has_value());
|
||||
ASSERT_TRUE(read.model.has_value());
|
||||
|
||||
EXPECT_EQ(read.database->schema_version, "1.0.0");
|
||||
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");
|
||||
@@ -354,6 +472,134 @@ TEST(ResultRoundTrip, PreservesMinimalSchemaModelAndNodalResults) {
|
||||
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())
|
||||
.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");
|
||||
|
||||
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(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);
|
||||
|
||||
EXPECT_TRUE(has_results_error(diagnostics, "hdf5.unsupported_schema"));
|
||||
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()).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";
|
||||
@@ -369,7 +615,7 @@ TEST(Hdf5, RejectsInvalidResultDatabaseBeforeWriting) {
|
||||
EXPECT_FALSE(std::filesystem::exists(path));
|
||||
}
|
||||
|
||||
TEST(Hdf5, RejectsFrameDiagnosticsThatSchemaCannotRepresent) {
|
||||
TEST(Hdf5, PreservesFrameDiagnosticsRepresentedBySchema) {
|
||||
const auto path = std::filesystem::path{FESA_TEST_BINARY_DIR} /
|
||||
"Testing" / "Temporary" /
|
||||
"fesa-unsupported-diagnostics.h5";
|
||||
@@ -386,9 +632,18 @@ TEST(Hdf5, RejectsFrameDiagnosticsThatSchemaCannotRepresent) {
|
||||
|
||||
const auto diagnostics = fesa::write_hdf5(path, domain, database);
|
||||
|
||||
EXPECT_TRUE(has_results_error(
|
||||
diagnostics, "hdf5.unsupported_result_diagnostics"));
|
||||
EXPECT_FALSE(std::filesystem::exists(path));
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user