Files
FESADev/tests/unit/io/hdf5/hdf5_results_writer_test.cpp

1343 lines
57 KiB
C++

#define NOMINMAX
#include "fesa/io/hdf5/hdf5_results_writer.h"
#include <Windows.h>
#include <gtest/gtest.h>
#include <hdf5.h>
#include <array>
#include <atomic>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <limits>
#include <memory>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
#include "fesa/analysis/analysis_model.h"
#include "fesa/analysis/analysis_state.h"
#include "fesa/build_info.h"
#include "fesa/fem/dof_manager.h"
#include "fesa/math/vector3.h"
#include "fesa/model/domain.h"
namespace {
constexpr const char* kStepRoot = "/steps/Step-1/frames/0";
class Hdf5Handle {
public:
using Closer = herr_t (*)(hid_t);
Hdf5Handle() = default;
Hdf5Handle(const hid_t value, Closer closer)
: value_{value}, closer_{closer} {}
Hdf5Handle(const Hdf5Handle&) = delete;
Hdf5Handle& operator=(const Hdf5Handle&) = delete;
Hdf5Handle(Hdf5Handle&& other) noexcept
: value_{other.value_}, closer_{other.closer_} {
other.value_ = -1;
other.closer_ = nullptr;
}
Hdf5Handle& operator=(Hdf5Handle&& other) noexcept {
if (this != &other) {
Reset();
value_ = other.value_;
closer_ = other.closer_;
other.value_ = -1;
other.closer_ = nullptr;
}
return *this;
}
~Hdf5Handle() { Reset(); }
hid_t Get() const noexcept { return value_; }
private:
void Reset() noexcept {
if (value_ >= 0 && closer_ != nullptr) {
(void)closer_(value_);
}
value_ = -1;
closer_ = nullptr;
}
hid_t value_{-1};
Closer closer_{nullptr};
};
class WinHandle {
public:
explicit WinHandle(HANDLE value) : value_{value} {}
WinHandle(const WinHandle&) = delete;
WinHandle& operator=(const WinHandle&) = delete;
~WinHandle() {
if (value_ != INVALID_HANDLE_VALUE) {
(void)CloseHandle(value_);
}
}
HANDLE Get() const noexcept { return value_; }
private:
HANDLE value_{INVALID_HANDLE_VALUE};
};
class TempDirectory {
public:
explicit TempDirectory(const std::string& label) {
static std::atomic<std::uint64_t> sequence{0U};
path_ =
std::filesystem::temp_directory_path() /
("fesa-step23-" + label + "-" + std::to_string(GetCurrentProcessId()) +
"-" + std::to_string(sequence.fetch_add(1U)));
std::error_code error;
if (!std::filesystem::create_directory(path_, error) || error) {
throw std::runtime_error{"Unable to create the Step 23 test directory."};
}
}
TempDirectory(const TempDirectory&) = delete;
TempDirectory& operator=(const TempDirectory&) = delete;
~TempDirectory() {
std::error_code ignored;
std::filesystem::remove_all(path_, ignored);
}
const std::filesystem::path& Path() const noexcept { return path_; }
private:
std::filesystem::path path_;
};
struct WriterFixture {
std::unique_ptr<fesa::Domain> domain;
std::unique_ptr<fesa::DofManager> dofs;
std::unique_ptr<fesa::AnalysisState> state;
};
fesa::ModelDefinition MakeDefinition(const std::filesystem::path& source,
const bool use_default_centroid) {
fesa::ModelDefinition definition{};
definition.source_path = source;
definition.source_content_identity = "fnv1a64:0123456789abcdef";
definition.nodes = {
{{u8"Beam-\u03b1", 101, "101"}, {0.0, 0.0, 0.0}, {source, 10U}},
{{u8"Beam-\u03b1", 202, "202"}, {3.0, 4.0, 0.0}, {source, 11U}}};
definition.materials = {{"Steel", 210.0e9, 0.3, {source, 20U}}};
definition.sections = {
{"General",
0.02,
3.0e-5,
0.0,
4.0e-5,
5.0e-5,
{0.0, 0.0, 1.0},
use_default_centroid
? std::vector<std::array<double, 2>>{}
: std::vector<std::array<double, 2>>{{{-0.1, 0.2}, {0.3, -0.4}}},
{source, 30U}}};
definition.elements = {
{{u8"Beam-\u03b1", 303, "303"}, {0U, 1U}, 0U, 0U, {source, 40U}}};
definition.steps = {{"Step-1", {}, {}, 0.1, 1.0, 0.01, 1.0, {source, 50U}}};
return definition;
}
WriterFixture MakeFixture(const std::filesystem::path& source,
const bool use_default_centroid = false) {
auto domain_result =
fesa::Domain::Create(MakeDefinition(source, use_default_centroid));
if (!domain_result.HasValue()) {
throw std::runtime_error{"Writer fixture Domain construction failed."};
}
auto domain =
std::make_unique<fesa::Domain>(std::move(domain_result.Value()));
auto model_result = fesa::AnalysisModel::Create(*domain);
if (!model_result.HasValue()) {
throw std::runtime_error{
"Writer fixture AnalysisModel construction failed."};
}
const fesa::AnalysisModel model = std::move(model_result.Value());
auto dofs_result = fesa::DofManager::Create(model);
if (!dofs_result.HasValue()) {
throw std::runtime_error{"Writer fixture DofManager construction failed."};
}
auto dofs =
std::make_unique<fesa::DofManager>(std::move(dofs_result.Value()));
auto state = std::make_unique<fesa::AnalysisState>(
fesa::AnalysisState::Create(*dofs, {"Step-1", 0U}));
for (std::size_t index = 0U; index < state->Displacement().Size(); ++index) {
state->Displacement()[index] = 0.25 + static_cast<double>(index);
state->ExternalForce()[index] = 100.0 + static_cast<double>(index);
state->InternalForce()[index] = 200.0 + 2.0 * static_cast<double>(index);
state->Residual()[index] = 100.0 + static_cast<double>(index);
state->Reaction()[index] = 100.0 + static_cast<double>(index);
}
const auto& nodes = domain->Nodes();
state->EndpointResults() = {{0U,
0,
nodes[0U].source_id,
{1.0, 2.0, 3.0, 4.0, 5.0, 6.0},
{11.0, 12.0, 13.0, 14.0}},
{0U,
1,
nodes[1U].source_id,
{7.0, 8.0, 9.0, 10.0, 11.0, 12.0},
{15.0, 16.0, 17.0, 18.0}}};
state->GaussResults() = {
{0U, 1, {0.01, 0.02, 0.03, 0.04}, {21.0, 22.0, 23.0, 24.0}},
{0U, 2, {0.05, 0.06, 0.07, 0.08}, {25.0, 26.0, 27.0, 28.0}}};
if (use_default_centroid) {
state->StressResults() = {{0U, 1, 0U, 0.0, 0.0, 31.0, "fesa-default"},
{0U, 2, 0U, 0.0, 0.0, 32.0, "fesa-default"}};
} else {
state->StressResults() = {{0U, 1, 1U, -0.1, 0.2, 31.0, "input"},
{0U, 1, 2U, 0.3, -0.4, 32.0, "input"},
{0U, 2, 1U, -0.1, 0.2, 33.0, "input"},
{0U, 2, 2U, 0.3, -0.4, 34.0, "input"}};
}
return {std::move(domain), std::move(dofs), std::move(state)};
}
fesa::ModelDefinition MakeShellDefinition(const std::filesystem::path& source) {
fesa::ModelDefinition definition{};
definition.source_path = source;
definition.source_content_identity = "fnv1a64:fedcba9876543210";
definition.nodes = {{{"Shell-1", 11, "11"}, {-1.0, -1.0, 0.0}, {source, 10U}},
{{"Shell-1", 12, "12"}, {1.0, -1.0, 0.0}, {source, 11U}},
{{"Shell-1", 13, "13"}, {1.0, 1.0, 0.0}, {source, 12U}},
{{"Shell-1", 14, "14"}, {-1.0, 1.0, 0.0}, {source, 13U}}};
definition.materials = {{"ShellSteel", 210.0e9, 0.3, {source, 20U}}};
definition.shell_sections = {{"PlateSet", 0.02, 0U, {source, 30U}}};
definition.shell_elements = {{{"Shell-1", 401, "401"},
fesa::ShellSourceElementType::kS4r,
{0U, 1U, 2U, 3U},
0U,
0U,
{source, 40U}}};
for (std::size_t node = 0U; node < definition.nodes.size(); ++node) {
definition.shell_node_initial_frames.push_back(
{static_cast<fesa::EntityIndex>(node),
{0.0, 0.0, 1.0},
{1.0, 0.0, 0.0},
{0.0, 1.0, 0.0}});
}
definition.node_sets = {{"Fixed", {}, {0U}, {source, 50U}}};
definition.steps = {{"Step-1",
{{"Fixed", 1, 3, 0.0, {source, 60U}},
{"Fixed", 4, 4, 0.125, {source, 61U}}},
{},
0.1,
1.0,
0.01,
1.0,
{source, 59U}}};
return definition;
}
WriterFixture MakeShellFixture(const std::filesystem::path& source) {
auto domain_result = fesa::Domain::Create(MakeShellDefinition(source));
if (!domain_result.HasValue()) {
throw std::runtime_error{
"Shell writer fixture Domain construction failed."};
}
auto domain =
std::make_unique<fesa::Domain>(std::move(domain_result.Value()));
auto model_result = fesa::AnalysisModel::Create(*domain);
if (!model_result.HasValue()) {
throw std::runtime_error{
"Shell writer fixture AnalysisModel construction failed."};
}
const fesa::AnalysisModel model = std::move(model_result.Value());
auto dofs_result = fesa::DofManager::Create(model);
if (!dofs_result.HasValue()) {
throw std::runtime_error{
"Shell writer fixture DofManager construction failed."};
}
auto dofs =
std::make_unique<fesa::DofManager>(std::move(dofs_result.Value()));
auto state = std::make_unique<fesa::AnalysisState>(
fesa::AnalysisState::Create(*dofs, {"Step-1", 0U}));
for (std::size_t index = 0U; index < state->Displacement().Size(); ++index) {
state->Displacement()[index] = 0.01 * static_cast<double>(index + 1U);
state->ExternalForce()[index] = 10.0 + static_cast<double>(index);
state->InternalForce()[index] = 20.0 + static_cast<double>(index);
state->Residual()[index] = 30.0 + static_cast<double>(index);
state->Reaction()[index] = 40.0 + static_cast<double>(index);
}
const double gauss = 1.0 / std::sqrt(3.0);
const std::array<std::array<double, 2>, 4> coordinates{
{{-gauss, -gauss}, {gauss, -gauss}, {gauss, gauss}, {-gauss, gauss}}};
const std::array<fesa::ShellMidsurfaceLocation, 4> locations{
fesa::ShellMidsurfaceLocation::kGp1, fesa::ShellMidsurfaceLocation::kGp2,
fesa::ShellMidsurfaceLocation::kGp3, fesa::ShellMidsurfaceLocation::kGp4};
fesa::ShellStateCandidate candidate{};
for (std::size_t point = 0U; point < locations.size(); ++point) {
const double base = 100.0 * static_cast<double>(point + 1U);
candidate.rows.push_back(
{0U,
locations[point],
coordinates[point],
{{{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}},
{base + 1.0, base + 2.0, base + 3.0, base + 4.0, base + 5.0,
base + 6.0, base + 7.0, base + 8.0},
{base + 11.0, base + 12.0, base + 13.0, base + 14.0, base + 15.0,
base + 16.0, base + 17.0, base + 18.0},
{{{fesa::ShellSectionPosition::kBottom,
-1.0,
{base + 21.0, base + 22.0, base + 23.0}},
{fesa::ShellSectionPosition::kMiddle,
0.0,
{base + 24.0, base + 25.0, base + 26.0}},
{fesa::ShellSectionPosition::kTop,
1.0,
{base + 27.0, base + 28.0, base + 29.0}}}}});
}
candidate.physical_strain_energy = 123.5;
candidate.equilibrium = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0};
candidate.verification_metrics = {1.0e-13, 2.0e-13, 3.0e-13};
const fesa::Status commit =
state->CommitShellResults({0U}, std::move(candidate));
if (!commit.IsOk()) {
throw std::runtime_error{"Shell writer fixture state commit failed."};
}
return {std::move(domain), std::move(dofs), std::move(state)};
}
Hdf5Handle OpenFile(const std::filesystem::path& path) {
const hid_t file =
H5Fopen(path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT);
if (file < 0) {
throw std::runtime_error{"Unable to open test HDF5 output."};
}
return Hdf5Handle{file, H5Fclose};
}
Hdf5Handle OpenDataset(const hid_t file, const std::string& path) {
const hid_t dataset = H5Dopen2(file, path.c_str(), H5P_DEFAULT);
if (dataset < 0) {
throw std::runtime_error{"Unable to open expected HDF5 dataset: " + path};
}
return Hdf5Handle{dataset, H5Dclose};
}
std::vector<hsize_t> DatasetDimensions(const hid_t file,
const std::string& path) {
const auto dataset = OpenDataset(file, path);
Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose};
if (space.Get() < 0) {
throw std::runtime_error{"Unable to inspect HDF5 dataspace."};
}
const int rank = H5Sget_simple_extent_ndims(space.Get());
if (rank < 0) {
throw std::runtime_error{"Unable to inspect HDF5 rank."};
}
std::vector<hsize_t> dimensions(static_cast<std::size_t>(rank));
if (rank > 0 &&
H5Sget_simple_extent_dims(space.Get(), dimensions.data(), nullptr) < 0) {
throw std::runtime_error{"Unable to inspect HDF5 dimensions."};
}
return dimensions;
}
std::vector<double> ReadDoubleDataset(const hid_t file,
const std::string& path) {
const auto dimensions = DatasetDimensions(file, path);
std::size_t value_count = 1U;
for (const hsize_t dimension : dimensions) {
value_count *= static_cast<std::size_t>(dimension);
}
const auto dataset = OpenDataset(file, path);
std::vector<double> values(value_count);
if (!values.empty() && H5Dread(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL,
H5S_ALL, H5P_DEFAULT, values.data()) < 0) {
throw std::runtime_error{"Unable to read numeric HDF5 dataset."};
}
return values;
}
std::vector<std::uint8_t> ReadUint8Dataset(const hid_t file,
const std::string& path) {
const auto dimensions = DatasetDimensions(file, path);
std::size_t value_count = 1U;
for (const hsize_t dimension : dimensions) {
value_count *= static_cast<std::size_t>(dimension);
}
const auto dataset = OpenDataset(file, path);
Hdf5Handle type{H5Dget_type(dataset.Get()), H5Tclose};
if (type.Get() < 0 || H5Tget_class(type.Get()) != H5T_INTEGER ||
H5Tget_size(type.Get()) != sizeof(std::uint8_t) ||
H5Tget_sign(type.Get()) != H5T_SGN_NONE ||
H5Tequal(type.Get(), H5T_STD_U8LE) <= 0) {
throw std::runtime_error{"Expected a portable uint8 HDF5 dataset."};
}
std::vector<std::uint8_t> values(value_count);
if (!values.empty() && H5Dread(dataset.Get(), H5T_NATIVE_UINT8, H5S_ALL,
H5S_ALL, H5P_DEFAULT, values.data()) < 0) {
throw std::runtime_error{"Unable to read uint8 HDF5 dataset."};
}
return values;
}
std::string ReadStringAttribute(const hid_t object, const char* name) {
Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose};
Hdf5Handle type{H5Aget_type(attribute.Get()), H5Tclose};
if (attribute.Get() < 0 || type.Get() < 0 ||
H5Tget_class(type.Get()) != H5T_STRING ||
H5Tis_variable_str(type.Get()) <= 0 ||
H5Tget_cset(type.Get()) != H5T_CSET_UTF8) {
throw std::runtime_error{"Expected a variable-length UTF-8 attribute."};
}
char* raw = nullptr;
if (H5Aread(attribute.Get(), type.Get(), &raw) < 0 || raw == nullptr) {
throw std::runtime_error{"Unable to read UTF-8 HDF5 attribute."};
}
const std::string value{raw};
(void)H5free_memory(raw);
return value;
}
std::uint64_t ReadUint64Attribute(const hid_t object, const char* name) {
Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose};
Hdf5Handle type{H5Aget_type(attribute.Get()), H5Tclose};
if (attribute.Get() < 0 || type.Get() < 0 ||
H5Tget_class(type.Get()) != H5T_INTEGER ||
H5Tget_size(type.Get()) != sizeof(std::uint64_t) ||
H5Tget_sign(type.Get()) != H5T_SGN_NONE ||
H5Tequal(type.Get(), H5T_STD_U64LE) <= 0) {
throw std::runtime_error{"Expected a portable uint64 HDF5 attribute."};
}
std::uint64_t value = 0U;
if (H5Aread(attribute.Get(), H5T_NATIVE_UINT64, &value) < 0) {
throw std::runtime_error{"Unable to read uint64 HDF5 attribute."};
}
return value;
}
void ExpectPortableCompoundMember(const hid_t compound_type,
const unsigned index,
const std::string& name) {
Hdf5Handle member_type{H5Tget_member_type(compound_type, index), H5Tclose};
ASSERT_GE(member_type.Get(), 0);
const bool is_uint64 = name == "internal_node_id" ||
name == "internal_element_id" ||
name == "gauss_point_index" ||
name == "section_point_index" || name == "line";
const bool is_float64 = name == "x1" || name == "x2" || name == "S11";
const bool is_string =
name == "instance_name" || name == "source_label" || name == "source" ||
name == "severity" || name == "code" || name == "file" ||
name == "keyword" || name == "entity_identity" || name == "message";
if (is_uint64) {
EXPECT_EQ(H5Tget_class(member_type.Get()), H5T_INTEGER);
EXPECT_EQ(H5Tget_size(member_type.Get()), sizeof(std::uint64_t));
EXPECT_EQ(H5Tget_sign(member_type.Get()), H5T_SGN_NONE);
EXPECT_GT(H5Tequal(member_type.Get(), H5T_STD_U64LE), 0);
return;
}
if (is_float64) {
EXPECT_EQ(H5Tget_class(member_type.Get()), H5T_FLOAT);
EXPECT_EQ(H5Tget_size(member_type.Get()), sizeof(double));
EXPECT_GT(H5Tequal(member_type.Get(), H5T_IEEE_F64LE), 0);
return;
}
if (is_string) {
EXPECT_EQ(H5Tget_class(member_type.Get()), H5T_STRING);
EXPECT_GT(H5Tis_variable_str(member_type.Get()), 0);
EXPECT_EQ(H5Tget_cset(member_type.Get()), H5T_CSET_UTF8);
return;
}
ASSERT_EQ(H5Tget_class(member_type.Get()), H5T_ARRAY);
const int rank = H5Tget_array_ndims(member_type.Get());
ASSERT_GT(rank, 0);
std::vector<hsize_t> dimensions(static_cast<std::size_t>(rank));
ASSERT_GE(H5Tget_array_dims2(member_type.Get(), dimensions.data()), 0);
Hdf5Handle base_type{H5Tget_super(member_type.Get()), H5Tclose};
ASSERT_GE(base_type.Get(), 0);
if (name == "node_internal_ids") {
EXPECT_EQ(dimensions, std::vector<hsize_t>({2U}));
EXPECT_GT(H5Tequal(base_type.Get(), H5T_STD_U64LE), 0);
} else if (name == "coordinates") {
EXPECT_EQ(dimensions, std::vector<hsize_t>({3U}));
EXPECT_GT(H5Tequal(base_type.Get(), H5T_IEEE_F64LE), 0);
} else {
EXPECT_EQ(name, "local_axes");
EXPECT_EQ(dimensions, std::vector<hsize_t>({3U, 3U}));
EXPECT_GT(H5Tequal(base_type.Get(), H5T_IEEE_F64LE), 0);
}
}
void ExpectCompoundMembers(const hid_t file, const std::string& path,
const std::vector<std::string>& expected_names) {
const auto dataset = OpenDataset(file, path);
Hdf5Handle type{H5Dget_type(dataset.Get()), H5Tclose};
ASSERT_EQ(H5Tget_class(type.Get()), H5T_COMPOUND);
ASSERT_EQ(H5Tget_nmembers(type.Get()),
static_cast<int>(expected_names.size()));
for (std::size_t index = 0U; index < expected_names.size(); ++index) {
char* raw_name =
H5Tget_member_name(type.Get(), static_cast<unsigned>(index));
ASSERT_NE(raw_name, nullptr);
const std::string actual_name{raw_name};
(void)H5free_memory(raw_name);
EXPECT_EQ(actual_name, expected_names[index]);
ExpectPortableCompoundMember(type.Get(), static_cast<unsigned>(index),
expected_names[index]);
}
}
void ExpectCompoundMemberNames(const hid_t file, const std::string& path,
const std::vector<std::string>& expected_names) {
const auto dataset = OpenDataset(file, path);
Hdf5Handle type{H5Dget_type(dataset.Get()), H5Tclose};
ASSERT_EQ(H5Tget_class(type.Get()), H5T_COMPOUND);
ASSERT_EQ(H5Tget_nmembers(type.Get()),
static_cast<int>(expected_names.size()));
for (std::size_t index = 0U; index < expected_names.size(); ++index) {
char* raw_name =
H5Tget_member_name(type.Get(), static_cast<unsigned>(index));
ASSERT_NE(raw_name, nullptr);
const std::string actual_name{raw_name};
(void)H5free_memory(raw_name);
EXPECT_EQ(actual_name, expected_names[index]);
}
}
void ExpectNumericDataset(const hid_t file, const std::string& path,
const std::vector<hsize_t>& dimensions,
const std::string& components,
const std::string& units,
const std::string& coordinate_system,
const std::string& location) {
EXPECT_EQ(DatasetDimensions(file, path), dimensions);
const auto dataset = OpenDataset(file, path);
Hdf5Handle type{H5Dget_type(dataset.Get()), H5Tclose};
ASSERT_EQ(H5Tget_class(type.Get()), H5T_FLOAT);
EXPECT_EQ(H5Tget_size(type.Get()), 8U);
EXPECT_GT(H5Tequal(type.Get(), H5T_IEEE_F64LE), 0);
EXPECT_EQ(ReadStringAttribute(dataset.Get(), "component_names"), components);
EXPECT_EQ(ReadStringAttribute(dataset.Get(), "component_unit_dimensions"),
units);
EXPECT_EQ(ReadStringAttribute(dataset.Get(), "coordinate_system"),
coordinate_system);
EXPECT_EQ(ReadStringAttribute(dataset.Get(), "location"), location);
EXPECT_EQ(ReadStringAttribute(dataset.Get(), "step_name"), "Step-1");
EXPECT_EQ(ReadUint64Attribute(dataset.Get(), "frame_index"), 0U);
}
Hdf5Handle MakeUtf8StringType() {
Hdf5Handle type{H5Tcopy(H5T_C_S1), H5Tclose};
if (type.Get() < 0 || H5Tset_size(type.Get(), H5T_VARIABLE) < 0 ||
H5Tset_cset(type.Get(), H5T_CSET_UTF8) < 0) {
throw std::runtime_error{"Unable to create a test UTF-8 memory type."};
}
return type;
}
struct NodeReadRow {
std::uint64_t internal_node_id;
char* instance_name;
char* source_label;
double coordinates[3];
};
std::vector<NodeReadRow> ReadNodeRows(const hid_t file) {
const auto dataset = OpenDataset(file, "/model/nodes");
Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose};
auto string_type = MakeUtf8StringType();
const hsize_t coordinate_dimensions[] = {3U};
Hdf5Handle coordinates_type{
H5Tarray_create2(H5T_NATIVE_DOUBLE, 1, coordinate_dimensions), H5Tclose};
Hdf5Handle memory_type{H5Tcreate(H5T_COMPOUND, sizeof(NodeReadRow)),
H5Tclose};
if (H5Tinsert(memory_type.Get(), "internal_node_id",
HOFFSET(NodeReadRow, internal_node_id),
H5T_NATIVE_UINT64) < 0 ||
H5Tinsert(memory_type.Get(), "instance_name",
HOFFSET(NodeReadRow, instance_name), string_type.Get()) < 0 ||
H5Tinsert(memory_type.Get(), "source_label",
HOFFSET(NodeReadRow, source_label), string_type.Get()) < 0 ||
H5Tinsert(memory_type.Get(), "coordinates",
HOFFSET(NodeReadRow, coordinates),
coordinates_type.Get()) < 0) {
throw std::runtime_error{"Unable to create the node memory type."};
}
std::vector<NodeReadRow> rows(2U);
if (H5Dread(dataset.Get(), memory_type.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT,
rows.data()) < 0) {
throw std::runtime_error{"Unable to read node rows."};
}
return rows;
}
void ReclaimNodeRows(const hid_t file, std::vector<NodeReadRow>& rows) {
const auto dataset = OpenDataset(file, "/model/nodes");
Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose};
auto string_type = MakeUtf8StringType();
const hsize_t coordinate_dimensions[] = {3U};
Hdf5Handle coordinates_type{
H5Tarray_create2(H5T_NATIVE_DOUBLE, 1, coordinate_dimensions), H5Tclose};
Hdf5Handle memory_type{H5Tcreate(H5T_COMPOUND, sizeof(NodeReadRow)),
H5Tclose};
(void)H5Tinsert(memory_type.Get(), "internal_node_id",
HOFFSET(NodeReadRow, internal_node_id), H5T_NATIVE_UINT64);
(void)H5Tinsert(memory_type.Get(), "instance_name",
HOFFSET(NodeReadRow, instance_name), string_type.Get());
(void)H5Tinsert(memory_type.Get(), "source_label",
HOFFSET(NodeReadRow, source_label), string_type.Get());
(void)H5Tinsert(memory_type.Get(), "coordinates",
HOFFSET(NodeReadRow, coordinates), coordinates_type.Get());
(void)H5Dvlen_reclaim(memory_type.Get(), space.Get(), H5P_DEFAULT,
rows.data());
}
struct ElementReadRow {
std::uint64_t internal_element_id;
char* instance_name;
char* source_label;
std::uint64_t node_internal_ids[2];
double local_axes[9];
};
std::vector<ElementReadRow> ReadElementRows(const hid_t file) {
const auto dataset = OpenDataset(file, "/model/elements");
auto string_type = MakeUtf8StringType();
const hsize_t node_dimensions[] = {2U};
const hsize_t axes_dimensions[] = {3U, 3U};
Hdf5Handle node_type{H5Tarray_create2(H5T_NATIVE_UINT64, 1, node_dimensions),
H5Tclose};
Hdf5Handle axes_type{H5Tarray_create2(H5T_NATIVE_DOUBLE, 2, axes_dimensions),
H5Tclose};
Hdf5Handle memory_type{H5Tcreate(H5T_COMPOUND, sizeof(ElementReadRow)),
H5Tclose};
(void)H5Tinsert(memory_type.Get(), "internal_element_id",
HOFFSET(ElementReadRow, internal_element_id),
H5T_NATIVE_UINT64);
(void)H5Tinsert(memory_type.Get(), "instance_name",
HOFFSET(ElementReadRow, instance_name), string_type.Get());
(void)H5Tinsert(memory_type.Get(), "source_label",
HOFFSET(ElementReadRow, source_label), string_type.Get());
(void)H5Tinsert(memory_type.Get(), "node_internal_ids",
HOFFSET(ElementReadRow, node_internal_ids), node_type.Get());
(void)H5Tinsert(memory_type.Get(), "local_axes",
HOFFSET(ElementReadRow, local_axes), axes_type.Get());
std::vector<ElementReadRow> rows(1U);
if (H5Dread(dataset.Get(), memory_type.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT,
rows.data()) < 0) {
throw std::runtime_error{"Unable to read element rows."};
}
Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose};
EXPECT_EQ(rows[0U].internal_element_id, 0U);
EXPECT_STREQ(rows[0U].instance_name, u8"Beam-\u03b1");
EXPECT_STREQ(rows[0U].source_label, "303");
EXPECT_EQ(rows[0U].node_internal_ids[0U], 0U);
EXPECT_EQ(rows[0U].node_internal_ids[1U], 1U);
const std::array<double, 9> expected_axes = {0.6, 0.8, 0.0, 0.0, 0.0,
1.0, 0.8, -0.6, 0.0};
for (std::size_t index = 0U; index < expected_axes.size(); ++index) {
EXPECT_NEAR(rows[0U].local_axes[index], expected_axes[index], 1.0e-15);
}
(void)H5Dvlen_reclaim(memory_type.Get(), space.Get(), H5P_DEFAULT,
rows.data());
return rows;
}
struct StressReadRow {
std::uint64_t internal_element_id;
std::uint64_t gauss_point_index;
std::uint64_t section_point_index;
double x1;
double x2;
char* source;
double s11;
};
std::vector<StressReadRow> ReadStressRows(const hid_t file) {
const std::string path = std::string{kStepRoot} + "/element/stress_s11";
const auto dataset = OpenDataset(file, path);
const auto dimensions = DatasetDimensions(file, path);
auto string_type = MakeUtf8StringType();
Hdf5Handle memory_type{H5Tcreate(H5T_COMPOUND, sizeof(StressReadRow)),
H5Tclose};
(void)H5Tinsert(memory_type.Get(), "internal_element_id",
HOFFSET(StressReadRow, internal_element_id),
H5T_NATIVE_UINT64);
(void)H5Tinsert(memory_type.Get(), "gauss_point_index",
HOFFSET(StressReadRow, gauss_point_index), H5T_NATIVE_UINT64);
(void)H5Tinsert(memory_type.Get(), "section_point_index",
HOFFSET(StressReadRow, section_point_index),
H5T_NATIVE_UINT64);
(void)H5Tinsert(memory_type.Get(), "x1", HOFFSET(StressReadRow, x1),
H5T_NATIVE_DOUBLE);
(void)H5Tinsert(memory_type.Get(), "x2", HOFFSET(StressReadRow, x2),
H5T_NATIVE_DOUBLE);
(void)H5Tinsert(memory_type.Get(), "source", HOFFSET(StressReadRow, source),
string_type.Get());
(void)H5Tinsert(memory_type.Get(), "S11", HOFFSET(StressReadRow, s11),
H5T_NATIVE_DOUBLE);
std::vector<StressReadRow> rows(
dimensions.empty() ? 0U : static_cast<std::size_t>(dimensions[0U]));
if (!rows.empty() && H5Dread(dataset.Get(), memory_type.Get(), H5S_ALL,
H5S_ALL, H5P_DEFAULT, rows.data()) < 0) {
throw std::runtime_error{"Unable to read stress rows."};
}
return rows;
}
void ReclaimStressRows(const hid_t file, std::vector<StressReadRow>& rows) {
const std::string path = std::string{kStepRoot} + "/element/stress_s11";
const auto dataset = OpenDataset(file, path);
Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose};
auto string_type = MakeUtf8StringType();
Hdf5Handle memory_type{H5Tcreate(H5T_COMPOUND, sizeof(StressReadRow)),
H5Tclose};
(void)H5Tinsert(memory_type.Get(), "internal_element_id",
HOFFSET(StressReadRow, internal_element_id),
H5T_NATIVE_UINT64);
(void)H5Tinsert(memory_type.Get(), "gauss_point_index",
HOFFSET(StressReadRow, gauss_point_index), H5T_NATIVE_UINT64);
(void)H5Tinsert(memory_type.Get(), "section_point_index",
HOFFSET(StressReadRow, section_point_index),
H5T_NATIVE_UINT64);
(void)H5Tinsert(memory_type.Get(), "x1", HOFFSET(StressReadRow, x1),
H5T_NATIVE_DOUBLE);
(void)H5Tinsert(memory_type.Get(), "x2", HOFFSET(StressReadRow, x2),
H5T_NATIVE_DOUBLE);
(void)H5Tinsert(memory_type.Get(), "source", HOFFSET(StressReadRow, source),
string_type.Get());
(void)H5Tinsert(memory_type.Get(), "S11", HOFFSET(StressReadRow, s11),
H5T_NATIVE_DOUBLE);
if (!rows.empty()) {
(void)H5Dvlen_reclaim(memory_type.Get(), space.Get(), H5P_DEFAULT,
rows.data());
}
}
struct DiagnosticReadRow {
char* severity;
char* code;
char* file;
std::uint64_t line;
char* keyword;
char* entity_identity;
char* message;
};
std::vector<DiagnosticReadRow> ReadDiagnosticRows(const hid_t file) {
const auto dataset = OpenDataset(file, "/diagnostics");
const auto dimensions = DatasetDimensions(file, "/diagnostics");
auto string_type = MakeUtf8StringType();
Hdf5Handle memory_type{H5Tcreate(H5T_COMPOUND, sizeof(DiagnosticReadRow)),
H5Tclose};
(void)H5Tinsert(memory_type.Get(), "severity",
HOFFSET(DiagnosticReadRow, severity), string_type.Get());
(void)H5Tinsert(memory_type.Get(), "code", HOFFSET(DiagnosticReadRow, code),
string_type.Get());
(void)H5Tinsert(memory_type.Get(), "file", HOFFSET(DiagnosticReadRow, file),
string_type.Get());
(void)H5Tinsert(memory_type.Get(), "line", HOFFSET(DiagnosticReadRow, line),
H5T_NATIVE_UINT64);
(void)H5Tinsert(memory_type.Get(), "keyword",
HOFFSET(DiagnosticReadRow, keyword), string_type.Get());
(void)H5Tinsert(memory_type.Get(), "entity_identity",
HOFFSET(DiagnosticReadRow, entity_identity),
string_type.Get());
(void)H5Tinsert(memory_type.Get(), "message",
HOFFSET(DiagnosticReadRow, message), string_type.Get());
std::vector<DiagnosticReadRow> rows(
dimensions.empty() ? 0U : static_cast<std::size_t>(dimensions[0U]));
if (!rows.empty() && H5Dread(dataset.Get(), memory_type.Get(), H5S_ALL,
H5S_ALL, H5P_DEFAULT, rows.data()) < 0) {
throw std::runtime_error{"Unable to read diagnostic rows."};
}
return rows;
}
void ReclaimDiagnosticRows(const hid_t file,
std::vector<DiagnosticReadRow>& rows) {
const auto dataset = OpenDataset(file, "/diagnostics");
Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose};
auto string_type = MakeUtf8StringType();
Hdf5Handle memory_type{H5Tcreate(H5T_COMPOUND, sizeof(DiagnosticReadRow)),
H5Tclose};
(void)H5Tinsert(memory_type.Get(), "severity",
HOFFSET(DiagnosticReadRow, severity), string_type.Get());
(void)H5Tinsert(memory_type.Get(), "code", HOFFSET(DiagnosticReadRow, code),
string_type.Get());
(void)H5Tinsert(memory_type.Get(), "file", HOFFSET(DiagnosticReadRow, file),
string_type.Get());
(void)H5Tinsert(memory_type.Get(), "line", HOFFSET(DiagnosticReadRow, line),
H5T_NATIVE_UINT64);
(void)H5Tinsert(memory_type.Get(), "keyword",
HOFFSET(DiagnosticReadRow, keyword), string_type.Get());
(void)H5Tinsert(memory_type.Get(), "entity_identity",
HOFFSET(DiagnosticReadRow, entity_identity),
string_type.Get());
(void)H5Tinsert(memory_type.Get(), "message",
HOFFSET(DiagnosticReadRow, message), string_type.Get());
if (!rows.empty()) {
(void)H5Dvlen_reclaim(memory_type.Get(), space.Get(), H5P_DEFAULT,
rows.data());
}
}
std::vector<char> ReadBytes(const std::filesystem::path& path) {
std::ifstream input{path, std::ios::binary};
return {std::istreambuf_iterator<char>{input},
std::istreambuf_iterator<char>{}};
}
void WriteBytes(const std::filesystem::path& path,
const std::vector<char>& bytes) {
std::ofstream output{path, std::ios::binary | std::ios::trunc};
output.write(bytes.data(), static_cast<std::streamsize>(bytes.size()));
if (!output) {
throw std::runtime_error{"Unable to write atomicity sentinel bytes."};
}
}
std::size_t EntryCount(const std::filesystem::path& directory) {
return static_cast<std::size_t>(
std::distance(std::filesystem::directory_iterator{directory},
std::filesystem::directory_iterator{}));
}
void ExpectOutputFailure(const fesa::Status& status,
const std::string& expected_code) {
ASSERT_FALSE(status.IsOk());
EXPECT_EQ(status.Category(), fesa::FailureCategory::kOutput);
ASSERT_EQ(status.Diagnostics().size(), 1U);
EXPECT_EQ(status.Diagnostics()[0U].severity, fesa::Severity::kError);
EXPECT_EQ(status.Diagnostics()[0U].code, expected_code);
}
} // namespace
TEST(Hdf5ResultsWriter, WritesExactSchemaShapesAttributesAndIdentity) {
TempDirectory directory{"schema"};
const auto source = directory.Path() / "model.inp";
auto fixture = MakeFixture(source);
const auto output = directory.Path() / "results.h5";
fesa::Hdf5ResultsWriter writer;
ASSERT_TRUE(writer.Write(output, *fixture.domain, *fixture.state, {}).IsOk());
ASSERT_GT(H5Fis_hdf5(output.string().c_str()), 0);
const auto file = OpenFile(output);
for (const char* path :
{"/metadata", "/model/nodes", "/model/elements",
"/steps/Step-1/frames/0/nodal/displacement",
"/steps/Step-1/frames/0/nodal/reaction",
"/steps/Step-1/frames/0/element/end_force_local",
"/steps/Step-1/frames/0/element/section_resultant",
"/steps/Step-1/frames/0/element/generalized_strain",
"/steps/Step-1/frames/0/element/generalized_resultant",
"/steps/Step-1/frames/0/element/stress_s11", "/diagnostics"}) {
EXPECT_GT(H5Lexists(file.Get(), path, H5P_DEFAULT), 0) << path;
}
Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose};
ASSERT_GE(metadata.Get(), 0);
EXPECT_EQ(ReadUint64Attribute(metadata.Get(), "schema_version"), 0U);
EXPECT_EQ(ReadStringAttribute(metadata.Get(), "feature_id"),
"linear-static-3d-euler-beam");
EXPECT_EQ(ReadStringAttribute(metadata.Get(), "solver_version"),
std::string{fesa::SolverVersion()});
const std::string normalized_source =
std::filesystem::absolute(source).lexically_normal().generic_u8string();
EXPECT_EQ(ReadStringAttribute(metadata.Get(), "source_input_identity"),
"path=" + normalized_source +
";content_identity=fnv1a64:0123456789abcdef");
EXPECT_EQ(ReadStringAttribute(metadata.Get(), "unit_system_label"),
"user-consistent-unspecified");
EXPECT_EQ(ReadStringAttribute(metadata.Get(), "coordinate_convention"),
"global-cartesian; beam-local=(t,n1,t-cross-n1)");
EXPECT_EQ(ReadStringAttribute(metadata.Get(), "element_formulation"),
"B33-3D-Euler-Bernoulli");
EXPECT_EQ(ReadStringAttribute(metadata.Get(), "step_name"), "Step-1");
EXPECT_EQ(ReadUint64Attribute(metadata.Get(), "frame_index"), 0U);
EXPECT_EQ(DatasetDimensions(file.Get(), "/model/nodes"),
std::vector<hsize_t>({2U}));
ExpectCompoundMembers(
file.Get(), "/model/nodes",
{"internal_node_id", "instance_name", "source_label", "coordinates"});
const auto nodes_dataset = OpenDataset(file.Get(), "/model/nodes");
EXPECT_EQ(ReadStringAttribute(nodes_dataset.Get(), "coordinate_system"),
"global-cartesian");
EXPECT_EQ(ReadStringAttribute(nodes_dataset.Get(), "units_label"), "length");
auto nodes = ReadNodeRows(file.Get());
ASSERT_EQ(nodes.size(), 2U);
EXPECT_EQ(nodes[0U].internal_node_id, 0U);
EXPECT_STREQ(nodes[0U].instance_name, u8"Beam-\u03b1");
EXPECT_STREQ(nodes[0U].source_label, "101");
EXPECT_DOUBLE_EQ(nodes[1U].coordinates[0U], 3.0);
EXPECT_DOUBLE_EQ(nodes[1U].coordinates[1U], 4.0);
ReclaimNodeRows(file.Get(), nodes);
EXPECT_EQ(DatasetDimensions(file.Get(), "/model/elements"),
std::vector<hsize_t>({1U}));
ExpectCompoundMembers(file.Get(), "/model/elements",
{"internal_element_id", "instance_name", "source_label",
"node_internal_ids", "local_axes"});
const auto elements_dataset = OpenDataset(file.Get(), "/model/elements");
EXPECT_EQ(ReadStringAttribute(elements_dataset.Get(), "formulation"),
"B33-3D-Euler-Bernoulli");
(void)ReadElementRows(file.Get());
ExpectNumericDataset(
file.Get(), std::string{kStepRoot} + "/nodal/displacement", {2U, 6U},
"UX,UY,UZ,URX,URY,URZ", "length,length,length,radian,radian,radian",
"global-cartesian", "nodal");
const auto displacement = ReadDoubleDataset(
file.Get(), std::string{kStepRoot} + "/nodal/displacement");
ASSERT_EQ(displacement.size(), 12U);
EXPECT_DOUBLE_EQ(displacement.front(), 0.25);
EXPECT_DOUBLE_EQ(displacement.back(), 11.25);
ExpectNumericDataset(
file.Get(), std::string{kStepRoot} + "/nodal/reaction", {2U, 6U},
"RF1,RF2,RF3,RM1,RM2,RM3",
"force,force,force,force*length,force*length,force*length",
"global-cartesian", "nodal");
const auto reaction =
ReadDoubleDataset(file.Get(), std::string{kStepRoot} + "/nodal/reaction");
ASSERT_EQ(reaction.size(), 12U);
EXPECT_DOUBLE_EQ(reaction.front(), 100.0);
EXPECT_DOUBLE_EQ(reaction.back(), 111.0);
ExpectNumericDataset(
file.Get(), std::string{kStepRoot} + "/element/end_force_local",
{1U, 2U, 6U}, "FX,FY,FZ,MX,MY,MZ",
"force,force,force,force*length,force*length,force*length", "beam-local",
"endpoint-outward-action");
const auto end_force = ReadDoubleDataset(
file.Get(), std::string{kStepRoot} + "/element/end_force_local");
ASSERT_EQ(end_force.size(), 12U);
EXPECT_DOUBLE_EQ(end_force.front(), 1.0);
EXPECT_DOUBLE_EQ(end_force.back(), 12.0);
ExpectNumericDataset(
file.Get(), std::string{kStepRoot} + "/element/section_resultant",
{1U, 2U, 4U}, "N,T,My,Mz", "force,force*length,force*length,force*length",
"beam-local", "endpoint-positive-local-x-section-cut");
const auto section_resultant = ReadDoubleDataset(
file.Get(), std::string{kStepRoot} + "/element/section_resultant");
ASSERT_EQ(section_resultant.size(), 8U);
EXPECT_DOUBLE_EQ(section_resultant.front(), 11.0);
EXPECT_DOUBLE_EQ(section_resultant.back(), 18.0);
ExpectNumericDataset(
file.Get(), std::string{kStepRoot} + "/element/generalized_strain",
{1U, 2U, 4U}, "epsilon0,kappa_x,kappa_y,kappa_z",
"1,1/length,1/length,1/length", "beam-local", "integration-point");
const auto generalized_strain = ReadDoubleDataset(
file.Get(), std::string{kStepRoot} + "/element/generalized_strain");
ASSERT_EQ(generalized_strain.size(), 8U);
EXPECT_DOUBLE_EQ(generalized_strain.front(), 0.01);
EXPECT_DOUBLE_EQ(generalized_strain.back(), 0.08);
ExpectNumericDataset(
file.Get(), std::string{kStepRoot} + "/element/generalized_resultant",
{1U, 2U, 4U}, "N,T,My,Mz", "force,force*length,force*length,force*length",
"beam-local", "integration-point");
const auto generalized_resultant = ReadDoubleDataset(
file.Get(), std::string{kStepRoot} + "/element/generalized_resultant");
ASSERT_EQ(generalized_resultant.size(), 8U);
EXPECT_DOUBLE_EQ(generalized_resultant.front(), 21.0);
EXPECT_DOUBLE_EQ(generalized_resultant.back(), 28.0);
const std::string stress_path =
std::string{kStepRoot} + "/element/stress_s11";
EXPECT_EQ(DatasetDimensions(file.Get(), stress_path),
std::vector<hsize_t>({4U}));
ExpectCompoundMembers(file.Get(), stress_path,
{"internal_element_id", "gauss_point_index",
"section_point_index", "x1", "x2", "source", "S11"});
const auto stress_dataset = OpenDataset(file.Get(), stress_path);
EXPECT_EQ(ReadStringAttribute(stress_dataset.Get(), "component_names"),
"S11");
EXPECT_EQ(
ReadStringAttribute(stress_dataset.Get(), "component_unit_dimensions"),
"force/length^2");
EXPECT_EQ(ReadStringAttribute(stress_dataset.Get(), "coordinate_system"),
"beam-local");
EXPECT_EQ(ReadStringAttribute(stress_dataset.Get(), "location"),
"section-point");
EXPECT_EQ(ReadStringAttribute(stress_dataset.Get(), "step_name"), "Step-1");
EXPECT_EQ(ReadUint64Attribute(stress_dataset.Get(), "frame_index"), 0U);
auto stress_rows = ReadStressRows(file.Get());
ASSERT_EQ(stress_rows.size(), 4U);
EXPECT_EQ(stress_rows[0U].internal_element_id, 0U);
EXPECT_EQ(stress_rows[0U].gauss_point_index, 1U);
EXPECT_EQ(stress_rows[0U].section_point_index, 1U);
EXPECT_DOUBLE_EQ(stress_rows[0U].x1, -0.1);
EXPECT_DOUBLE_EQ(stress_rows[0U].x2, 0.2);
EXPECT_STREQ(stress_rows[0U].source, "input");
EXPECT_DOUBLE_EQ(stress_rows[3U].s11, 34.0);
ReclaimStressRows(file.Get(), stress_rows);
EXPECT_EQ(DatasetDimensions(file.Get(), "/diagnostics"),
std::vector<hsize_t>({0U}));
ExpectCompoundMembers(file.Get(), "/diagnostics",
{"severity", "code", "file", "line", "keyword",
"entity_identity", "message"});
EXPECT_EQ(H5Lexists(file.Get(),
"/steps/Step-1/frames/0/element/transverse_shear_stress",
H5P_DEFAULT),
0);
}
TEST(Hdf5ResultsWriter, WritesMandatoryOutputsDespiteOutputRequests) {
TempDirectory directory{"mandatory"};
auto fixture = MakeFixture(directory.Path() / "request-model.inp");
const fesa::Diagnostic ignored_request{
fesa::Severity::kWarning,
"ignored-output-request",
{fixture.domain->SourcePath(), 70U},
"*OUTPUT",
"FIELD",
"Abaqus output requests do not filter FESA mandatory results."};
const auto output = directory.Path() / "results.h5";
fesa::Hdf5ResultsWriter writer;
ASSERT_TRUE(
writer.Write(output, *fixture.domain, *fixture.state, {ignored_request})
.IsOk());
const auto file = OpenFile(output);
for (const char* suffix :
{"/nodal/displacement", "/nodal/reaction", "/element/end_force_local",
"/element/section_resultant", "/element/generalized_strain",
"/element/generalized_resultant", "/element/stress_s11"}) {
const std::string path = std::string{kStepRoot} + suffix;
EXPECT_GT(H5Lexists(file.Get(), path.c_str(), H5P_DEFAULT), 0) << path;
}
EXPECT_EQ(DatasetDimensions(file.Get(), "/diagnostics"),
std::vector<hsize_t>({1U}));
}
TEST(Hdf5ResultsWriter, WritesWarningsAndDefaultCentroid) {
TempDirectory directory{"warnings"};
auto fixture = MakeFixture(directory.Path() / "centroid.inp", true);
std::vector<fesa::Diagnostic> diagnostics = {
{fesa::Severity::kWarning,
"ignored-output-request",
{fixture.domain->SourcePath(), 80U},
"*OUTPUT",
"FIELD",
"Ignored output request."},
{fesa::Severity::kWarning,
"ignored-keyword",
{fixture.domain->SourcePath(), 20U},
"*PREPRINT",
"",
"Ignored generator control."}};
const auto output = directory.Path() / "results.h5";
fesa::Hdf5ResultsWriter writer;
ASSERT_TRUE(writer.Write(output, *fixture.domain, *fixture.state, diagnostics)
.IsOk());
const auto file = OpenFile(output);
auto stress_rows = ReadStressRows(file.Get());
ASSERT_EQ(stress_rows.size(), 2U);
for (std::size_t index = 0U; index < stress_rows.size(); ++index) {
EXPECT_EQ(stress_rows[index].internal_element_id, 0U);
EXPECT_EQ(stress_rows[index].gauss_point_index, index + 1U);
EXPECT_EQ(stress_rows[index].section_point_index, 0U);
EXPECT_DOUBLE_EQ(stress_rows[index].x1, 0.0);
EXPECT_DOUBLE_EQ(stress_rows[index].x2, 0.0);
EXPECT_STREQ(stress_rows[index].source, "fesa-default");
}
ReclaimStressRows(file.Get(), stress_rows);
auto rows = ReadDiagnosticRows(file.Get());
ASSERT_EQ(rows.size(), 2U);
EXPECT_STREQ(rows[0U].severity, "warning");
EXPECT_STREQ(rows[0U].code, "ignored-keyword");
EXPECT_STREQ(rows[0U].file,
std::filesystem::absolute(fixture.domain->SourcePath())
.lexically_normal()
.generic_u8string()
.c_str());
EXPECT_EQ(rows[0U].line, 20U);
EXPECT_STREQ(rows[0U].keyword, "*PREPRINT");
EXPECT_STREQ(rows[0U].entity_identity, "");
EXPECT_STREQ(rows[0U].message, "Ignored generator control.");
EXPECT_STREQ(rows[1U].code, "ignored-output-request");
EXPECT_EQ(rows[1U].line, 80U);
ReclaimDiagnosticRows(file.Get(), rows);
}
TEST(Hdf5ResultsWriter, FailureLeavesNoPartialAndPreservesExistingFinal) {
TempDirectory directory{"failure"};
auto fixture = MakeFixture(directory.Path() / "failure.inp");
fesa::Hdf5ResultsWriter writer;
fixture.state->Displacement()[0U] = std::numeric_limits<double>::quiet_NaN();
const auto invalid_output = directory.Path() / "invalid-results.h5";
ExpectOutputFailure(
writer.Write(invalid_output, *fixture.domain, *fixture.state, {}),
"invalid-result-state");
EXPECT_FALSE(std::filesystem::exists(invalid_output));
EXPECT_EQ(EntryCount(directory.Path()), 0U);
fixture.state->Displacement()[0U] = 0.25;
const auto final = directory.Path() / "results.h5";
const std::vector<char> sentinel = {'p', 'r', 'e', 'v', 'i', 'o', 'u', 's'};
WriteBytes(final, sentinel);
WinHandle lock{CreateFileW(final.c_str(), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr)};
ASSERT_NE(lock.Get(), INVALID_HANDLE_VALUE);
ExpectOutputFailure(writer.Write(final, *fixture.domain, *fixture.state, {}),
"hdf5-finalization-failure");
EXPECT_EQ(ReadBytes(final), sentinel);
EXPECT_EQ(EntryCount(directory.Path()), 1U);
}
TEST(Hdf5ResultsWriter, SuccessfullyReplacesExistingFinal) {
TempDirectory directory{"replace"};
auto fixture = MakeFixture(directory.Path() / "replace.inp");
const auto final = directory.Path() / "results.h5";
WriteBytes(final, {'o', 'l', 'd'});
fesa::Hdf5ResultsWriter writer;
ASSERT_TRUE(writer.Write(final, *fixture.domain, *fixture.state, {}).IsOk());
EXPECT_GT(H5Fis_hdf5(final.string().c_str()), 0);
EXPECT_EQ(EntryCount(directory.Path()), 1U);
const auto file = OpenFile(final);
Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose};
ASSERT_GE(metadata.Get(), 0);
EXPECT_EQ(ReadUint64Attribute(metadata.Get(), "schema_version"), 0U);
}
// MITC4-H5-001, C-DUP-002
TEST(Hdf5ResultsWriter, WritesExactShellMetadataAndModelIdentity) {
TempDirectory directory{"shell-model"};
const auto source = directory.Path() / "shell.inp";
auto fixture = MakeShellFixture(source);
const auto output = directory.Path() / "results.h5";
fesa::Hdf5ResultsWriter writer;
ASSERT_TRUE(writer.Write(output, *fixture.domain, *fixture.state, {}).IsOk());
const auto file = OpenFile(output);
Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose};
ASSERT_GE(metadata.Get(), 0);
EXPECT_EQ(ReadUint64Attribute(metadata.Get(), "schema_version"), 0U);
EXPECT_EQ(ReadStringAttribute(metadata.Get(), "feature_id"),
"linear-static-mitc4-shell");
EXPECT_EQ(
ReadStringAttribute(metadata.Get(), "coordinate_convention"),
"global-cartesian; shell-local=(e1,e2,e3); positive-thickness=+zeta");
EXPECT_EQ(ReadStringAttribute(metadata.Get(), "internal_formulation"),
"FESA-MITC4");
EXPECT_EQ(ReadStringAttribute(metadata.Get(), "integration_rule"),
"2x2x2-gauss; mitc4-edge-midpoint-shear");
EXPECT_EQ(DatasetDimensions(file.Get(), "/model/elements"),
std::vector<hsize_t>({1U}));
ExpectCompoundMemberNames(
file.Get(), "/model/elements",
{"internal_element_id", "instance_name", "source_label",
"source_element_type", "internal_formulation", "node_internal_ids",
"shell_section_internal_id", "material_internal_id"});
const auto elements = OpenDataset(file.Get(), "/model/elements");
EXPECT_EQ(ReadStringAttribute(elements.Get(), "formulation"), "FESA-MITC4");
EXPECT_EQ(DatasetDimensions(file.Get(), "/model/shell/nodal_director"),
std::vector<hsize_t>({4U, 3U}));
constexpr fesa::Vector3 director{0.0, 0.0, 1.0};
std::vector<double> expected_directors;
for (std::size_t node = 0U; node < 4U; ++node) {
expected_directors.insert(expected_directors.end(),
director.Components().begin(),
director.Components().end());
}
EXPECT_EQ(ReadDoubleDataset(file.Get(), "/model/shell/nodal_director"),
expected_directors);
EXPECT_EQ(DatasetDimensions(file.Get(), "/model/shell/nodal_frame"),
std::vector<hsize_t>({4U, 3U, 3U}));
ExpectCompoundMemberNames(file.Get(), "/model/shell/materials",
{"internal_material_id", "name", "E", "nu"});
ExpectCompoundMemberNames(
file.Get(), "/model/shell/sections",
{"internal_section_id", "source_file", "source_line", "source_elset",
"material_internal_id", "thickness"});
EXPECT_EQ(DatasetDimensions(file.Get(), "/model/nodal_constraint_mask"),
std::vector<hsize_t>({4U, 6U}));
const auto mask =
ReadUint8Dataset(file.Get(), "/model/nodal_constraint_mask");
ASSERT_EQ(mask.size(), 24U);
EXPECT_EQ(mask[0U], 1U);
EXPECT_EQ(mask[1U], 1U);
EXPECT_EQ(mask[2U], 1U);
EXPECT_EQ(mask[3U], 1U);
EXPECT_EQ(mask[4U], 0U);
EXPECT_EQ(mask[5U], 0U);
const auto prescribed =
ReadDoubleDataset(file.Get(), "/model/prescribed_displacement");
ASSERT_EQ(prescribed.size(), 24U);
EXPECT_DOUBLE_EQ(prescribed[0U], 0.0);
EXPECT_DOUBLE_EQ(prescribed[3U], 0.125);
EXPECT_DOUBLE_EQ(prescribed[4U], 0.0);
EXPECT_EQ(ReadDoubleDataset(file.Get(), "/model/shell/section_positions"),
std::vector<double>({-1.0, 0.0, 1.0}));
const auto locations =
ReadDoubleDataset(file.Get(), "/model/shell/midsurface_locations");
ASSERT_EQ(locations.size(), 8U);
const double gauss = 1.0 / std::sqrt(3.0);
EXPECT_DOUBLE_EQ(locations[0U], -gauss);
EXPECT_DOUBLE_EQ(locations[1U], -gauss);
EXPECT_DOUBLE_EQ(locations[6U], -gauss);
EXPECT_DOUBLE_EQ(locations[7U], gauss);
}
// MITC4-H5-002
TEST(Hdf5ResultsWriter, WritesExactMandatoryShellResultInventory) {
TempDirectory directory{"shell-results"};
auto fixture = MakeShellFixture(directory.Path() / "shell.inp");
const auto output = directory.Path() / "results.h5";
fesa::Hdf5ResultsWriter writer;
ASSERT_TRUE(writer.Write(output, *fixture.domain, *fixture.state, {}).IsOk());
const auto file = OpenFile(output);
const std::string shell_root = std::string{kStepRoot} + "/element/shell";
ExpectNumericDataset(file.Get(), shell_root + "/local_frame",
{1U, 4U, 3U, 3U}, "X,Y,Z", "1,1,1", "global-cartesian",
"shell-local-frame");
ExpectNumericDataset(file.Get(), shell_root + "/generalized_strain",
{1U, 4U, 8U}, "E11,E22,G12,K11,K22,K12,G13,G23",
"1,1,1,1/length,1/length,1/length,1,1", "shell-local",
"midsurface");
ExpectNumericDataset(file.Get(), shell_root + "/section_resultant",
{1U, 4U, 8U}, "N11,N22,N12,M11,M22,M12,Q13,Q23",
"force/length,force/length,force/"
"length,force,force,force,force/length,force/length",
"shell-local", "midsurface");
ExpectNumericDataset(file.Get(), shell_root + "/stress", {1U, 4U, 3U, 3U},
"S11,S22,S12",
"force/length^2,force/length^2,force/length^2",
"shell-local", "section-position");
const auto strain =
ReadDoubleDataset(file.Get(), shell_root + "/generalized_strain");
ASSERT_EQ(strain.size(), 32U);
EXPECT_DOUBLE_EQ(strain.front(), 101.0);
EXPECT_DOUBLE_EQ(strain.back(), 408.0);
const auto stress = ReadDoubleDataset(file.Get(), shell_root + "/stress");
ASSERT_EQ(stress.size(), 36U);
EXPECT_DOUBLE_EQ(stress.front(), 121.0);
EXPECT_DOUBLE_EQ(stress.back(), 429.0);
ExpectNumericDataset(file.Get(), std::string{kStepRoot} + "/global/energy",
{1U}, "PHYSICAL_STRAIN_ENERGY", "force*length", "global",
"global");
ExpectNumericDataset(
file.Get(), std::string{kStepRoot} + "/global/equilibrium", {6U},
"FORCE_1,FORCE_2,FORCE_3,MOMENT_1,MOMENT_2,MOMENT_3",
"force,force,force,force*length,force*length,force*length",
"global-cartesian", "global-origin");
ExpectNumericDataset(
file.Get(), std::string{kStepRoot} + "/global/verification_metrics", {3U},
"FREE_RESIDUAL_NORMALIZED,FORCE_BALANCE_NORMALIZED,MOMENT_BALANCE_"
"NORMALIZED",
"1,1,1", "global", "verification");
const auto metrics = OpenDataset(
file.Get(), std::string{kStepRoot} + "/global/verification_metrics");
EXPECT_EQ(ReadStringAttribute(metrics.Get(), "metric_definition_ids"),
"free-residual-l2-over-max-free-force-l2,force-balance-l2-over-"
"max-force-sum,moment-balance-l2-over-max-moment-sum");
EXPECT_EQ(ReadStringAttribute(metrics.Get(), "acceptance_thresholds"),
"1e-10,1e-10,1e-10");
}
// MITC4-H5-003
TEST(Hdf5ResultsWriter,
WritesShellInventoryDespiteRequestsAndOmitsForbiddenPaths) {
TempDirectory directory{"shell-mandatory"};
auto fixture = MakeShellFixture(directory.Path() / "shell.inp");
const fesa::Diagnostic ignored_request{
fesa::Severity::kWarning,
"ignored-output-request",
{fixture.domain->SourcePath(), 80U},
"*ELEMENT OUTPUT",
"S",
"Output requests cannot filter mandatory shell results."};
const auto output = directory.Path() / "results.h5";
fesa::Hdf5ResultsWriter writer;
ASSERT_TRUE(
writer.Write(output, *fixture.domain, *fixture.state, {ignored_request})
.IsOk());
const auto file = OpenFile(output);
for (const char* suffix :
{"/element/shell/local_frame", "/element/shell/generalized_strain",
"/element/shell/section_resultant", "/element/shell/stress",
"/global/energy", "/global/equilibrium",
"/global/verification_metrics"}) {
const std::string path = std::string{kStepRoot} + suffix;
EXPECT_GT(H5Lexists(file.Get(), path.c_str(), H5P_DEFAULT), 0) << path;
}
for (const char* forbidden :
{"/steps/Step-1/frames/0/element/shell/drilling",
"/steps/Step-1/frames/0/element/shell/drilling_energy",
"/steps/Step-1/frames/0/element/shell/S33",
"/steps/Step-1/frames/0/element/shell/S13",
"/steps/Step-1/frames/0/element/shell/S23"}) {
EXPECT_EQ(H5Lexists(file.Get(), forbidden, H5P_DEFAULT), 0) << forbidden;
}
EXPECT_EQ(DatasetDimensions(file.Get(), "/diagnostics"),
std::vector<hsize_t>({1U}));
}
// MITC4-H5-004
TEST(Hdf5ResultsWriter, InvalidShellInventoryPreservesExistingFinal) {
TempDirectory directory{"shell-atomic"};
auto fixture = MakeShellFixture(directory.Path() / "shell.inp");
auto invalid_state =
fesa::AnalysisState::Create(*fixture.dofs, {"Step-1", 0U});
const auto final = directory.Path() / "results.h5";
const std::vector<char> sentinel = {'s', 'h', 'e', 'l', 'l'};
WriteBytes(final, sentinel);
fesa::Hdf5ResultsWriter writer;
ExpectOutputFailure(writer.Write(final, *fixture.domain, invalid_state, {}),
"invalid-result-rows");
EXPECT_EQ(ReadBytes(final), sentinel);
EXPECT_EQ(EntryCount(directory.Path()), 1U);
}
// C-DUP-002
TEST(Hdf5ResultsWriter,
Vector3ShellSerializationRejectsNonfiniteWithoutReplacingFinal) {
TempDirectory directory{"shell-vector3-atomic"};
auto fixture = MakeShellFixture(directory.Path() / "shell.inp");
const auto final = directory.Path() / "results.h5";
fesa::Hdf5ResultsWriter writer;
ASSERT_TRUE(writer.Write(final, *fixture.domain, *fixture.state, {}).IsOk());
const std::vector<char> valid_bytes = ReadBytes(final);
const auto file = OpenFile(final);
const auto directors =
ReadDoubleDataset(file.Get(), "/model/shell/nodal_director");
ASSERT_EQ(directors.size(), 12U);
for (std::size_t offset = 0U; offset < directors.size(); offset += 3U) {
const fesa::Vector3 director{directors[offset], directors[offset + 1U],
directors[offset + 2U]};
EXPECT_TRUE(director.IsFinite());
EXPECT_EQ(director, (fesa::Vector3{0.0, 0.0, 1.0}));
}
fixture.state->Displacement()[0U] = std::numeric_limits<double>::quiet_NaN();
ExpectOutputFailure(writer.Write(final, *fixture.domain, *fixture.state, {}),
"invalid-result-state");
EXPECT_EQ(ReadBytes(final), valid_bytes);
EXPECT_EQ(EntryCount(directory.Path()), 1U);
}