Files
FESADev/tests/unit/io/hdf5/dynamic_hdf5_results_writer_test.cpp
T
KOKO\Mimi eaa5cbeb21 feat: adopt lumped mass and reduced-coordinate shell HHT
Integrate Hex8 mass verification and fixed-null-basis shell dynamics with numerical and physics handoff evidence. Preserve interrupted reference and full-validation status; full integration validation is deferred until after performance optimization.
2026-10-13 16:57:19 +09:00

673 lines
29 KiB
C++

#include "fesa/io/hdf5/dynamic_hdf5_results_writer.h"
#include <Windows.h>
#include <gtest/gtest.h>
#include <hdf5.h>
#include <array>
#include <cmath>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
#include "fesa/analysis/analysis_model.h"
#include "fesa/analysis/dynamic_analysis_state.h"
#include "fesa/elements/finite_rotation.h"
#include "fesa/fem/dof_manager.h"
#include "io/hdf5/hdf5_raii.h"
namespace {
using fesa::hdf5_internal::Hdf5Handle;
constexpr std::array<std::array<double, 3>, 3> kIdentityTriad{
std::array<double, 3>{1.0, 0.0, 0.0}, std::array<double, 3>{0.0, 1.0, 0.0},
std::array<double, 3>{0.0, 0.0, 1.0}};
fesa::ModelDefinition MakeDefinition(const bool fixed = true) {
const std::filesystem::path source{"dynamic-state.inp"};
fesa::ModelDefinition definition{};
definition.source_path = source;
definition.source_content_identity = "hht-output-unit-fixture";
definition.nodes = {{{"Shell-A", 10, "0010"}, {0.0, 0.0, 0.0}, {source, 1U}},
{{"Shell-A", 20, "20"}, {1.0, 0.0, 0.0}, {source, 2U}},
{{"Shell-A", 30, "30"}, {1.0, 1.0, 0.0}, {source, 3U}},
{{"Shell-A", 40, "40"}, {0.0, 1.0, 0.0}, {source, 4U}}};
definition.materials = {{"Material", 1000.0, 0.25, {source, 5U}}};
definition.densities = {{0U, 2.0, {source, 6U}}};
definition.shell_sections = {{"Shell", 0.1, 0U, {source, 7U}}};
definition.shell_elements = {{{"Shell-A", 9, "0009"},
fesa::ShellSourceElementType::kS4,
{0U, 1U, 2U, 3U},
0U,
0U,
{source, 8U}}};
for (std::size_t node = 0U; node < 4U; ++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 = {{"All", std::nullopt, {0U, 1U, 2U, 3U}, {source, 9U}},
{"Loaded", std::nullopt, {0U}, {source, 10U}}};
fesa::DynamicStepDefinition step{};
step.source_step_name_raw = "Transient Load";
step.source_step_name_canonical = "TRANSIENT LOAD";
step.initial_increment = 0.25;
step.time_period = 0.5;
step.increment_count = 2U;
step.maximum_increments = 3U;
if (fixed) {
step.boundaries = {{"All", 1, 6, 0.0, {source, 11U}}};
step.loads = {{"Loaded", 3, 4.0, {source, 12U}}};
}
definition.dynamic_step = step;
return definition;
}
struct Fixture {
std::unique_ptr<fesa::Domain> domain;
fesa::DynamicAnalysisState state;
};
Fixture MakeFixture(fesa::ModelDefinition definition = MakeDefinition()) {
auto domain_result = fesa::Domain::Create(std::move(definition));
if (!domain_result.HasValue())
throw std::runtime_error{"Invalid fixture Domain."};
auto domain =
std::make_unique<fesa::Domain>(std::move(domain_result.Value()));
auto model = fesa::AnalysisModel::Create(*domain);
if (!model.HasValue())
throw std::runtime_error{"Invalid fixture AnalysisModel."};
auto dofs = fesa::DofManager::Create(model.Value());
if (!dofs.HasValue()) throw std::runtime_error{"Invalid fixture DofManager."};
auto state = fesa::DynamicAnalysisState::Create(*domain, dofs.Value());
if (!state.HasValue()) throw std::runtime_error{"Invalid fixture state."};
return {std::move(domain), std::move(state.Value())};
}
// A fully restrained, initially undeformed shell under a dead nodal force has
// zero motion and strain, and nonzero support force at every accepted endpoint.
fesa::DynamicAcceptedFrameState MakeFrame(const fesa::Domain& domain,
const std::uint32_t index) {
fesa::DynamicAcceptedFrameState frame{};
frame.frame_index = index;
frame.increment_index = index;
frame.time = static_cast<double>(index) * domain.Dynamic()->initial_increment;
frame.is_initial = index == 0U;
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
frame.nodal_internal_order.push_back(static_cast<fesa::EntityIndex>(node));
frame.nodal_source_ids.push_back(domain.Nodes()[node].source_id);
frame.nodal_triad.push_back(kIdentityTriad);
frame.nodal_director.push_back({0.0, 0.0, 1.0});
}
for (auto* vector :
{&frame.generalized_coordinates, &frame.velocity, &frame.acceleration,
&frame.algorithmic_acceleration, &frame.inertia_force,
&frame.internal_force, &frame.external_force, &frame.residual,
&frame.reaction, &frame.residual_scale}) {
*vector = fesa::Vector{24U};
}
frame.angular_velocity = fesa::Vector{12U};
frame.angular_acceleration = fesa::Vector{12U};
if (!domain.Dynamic()->loads.empty()) {
frame.external_force[2U] = 4.0;
frame.residual[2U] = -4.0;
frame.reaction[2U] = -4.0;
frame.residual_scale[2U] = 4.0;
}
const double gauss = 1.0 / std::sqrt(3.0);
const std::array<std::array<double, 2>, 4> natural{
std::array<double, 2>{-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};
for (std::size_t point = 0U; point < 4U; ++point) {
frame.shell_results.push_back(
{0U,
locations[point],
natural[point],
kIdentityTriad,
{},
{},
{{{fesa::ShellSectionPosition::kBottom, -1.0, {}},
{fesa::ShellSectionPosition::kMiddle, 0.0, {}},
{fesa::ShellSectionPosition::kTop, 1.0, {}}}}});
}
return frame;
}
class TempFile {
public:
explicit TempFile(const std::string& name)
: path_{std::filesystem::temp_directory_path() /
("fesa-dynamic-writer-" + name + ".h5")} {
std::error_code error;
std::filesystem::remove(path_, error);
}
~TempFile() {
std::error_code error;
std::filesystem::remove(path_, error);
}
const std::filesystem::path& Path() const { return path_; }
private:
std::filesystem::path path_;
};
void CommitHistory(Fixture& fixture) {
for (std::uint32_t index = 0U; index <= 2U; ++index) {
ASSERT_TRUE(
fixture.state.CommitAcceptedFrame(MakeFrame(*fixture.domain, index))
.IsOk());
}
}
std::string ReadBytes(const std::filesystem::path& path) {
std::ifstream input{path, std::ios::binary};
return std::string{std::istreambuf_iterator<char>{input}, {}};
}
std::string ReadString(const hid_t object, const char* name) {
Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose};
Hdf5Handle type{H5Aget_type(attribute.Get()), H5Tclose};
EXPECT_EQ(H5Tget_class(type.Get()), H5T_STRING);
EXPECT_GT(H5Tis_variable_str(type.Get()), 0);
EXPECT_EQ(H5Tget_cset(type.Get()), H5T_CSET_UTF8);
char* raw = nullptr;
EXPECT_GE(H5Aread(attribute.Get(), type.Get(), &raw), 0);
const std::string value = raw == nullptr ? "" : raw;
(void)H5free_memory(raw);
return value;
}
template <typename Value>
Value ReadAttribute(const hid_t object, const char* name, const hid_t file_type,
const hid_t memory_type) {
Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose};
Hdf5Handle type{H5Aget_type(attribute.Get()), H5Tclose};
Hdf5Handle space{H5Aget_space(attribute.Get()), H5Sclose};
EXPECT_GT(H5Tequal(type.Get(), file_type), 0);
EXPECT_EQ(H5Sget_simple_extent_type(space.Get()), H5S_SCALAR);
Value result{};
EXPECT_GE(H5Aread(attribute.Get(), memory_type, &result), 0);
return result;
}
std::vector<double> ReadDoubles(const hid_t file, const std::string& path,
const std::vector<hsize_t>& dimensions) {
Hdf5Handle dataset{H5Dopen2(file, path.c_str(), H5P_DEFAULT), H5Dclose};
EXPECT_GE(dataset.Get(), 0);
Hdf5Handle type{H5Dget_type(dataset.Get()), H5Tclose};
EXPECT_GT(H5Tequal(type.Get(), H5T_IEEE_F64LE), 0);
Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose};
EXPECT_EQ(H5Sget_simple_extent_ndims(space.Get()), dimensions.size());
std::vector<hsize_t> actual(dimensions.size());
EXPECT_GE(H5Sget_simple_extent_dims(space.Get(), actual.data(), nullptr), 0);
EXPECT_EQ(actual, dimensions);
Hdf5Handle properties{H5Dget_create_plist(dataset.Get()), H5Pclose};
EXPECT_EQ(H5Pget_layout(properties.Get()), H5D_CONTIGUOUS);
EXPECT_EQ(H5Pget_nfilters(properties.Get()), 0);
std::size_t count = 1U;
for (const auto size : dimensions) count *= static_cast<std::size_t>(size);
std::vector<double> values(count);
EXPECT_GE(H5Dread(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL,
H5P_DEFAULT, values.data()),
0);
for (const double value : values) EXPECT_TRUE(std::isfinite(value));
return values;
}
void ExpectCompound(const hid_t file, const char* path,
const std::vector<std::pair<const char*, hid_t>>& fields,
const hsize_t rows) {
Hdf5Handle dataset{H5Dopen2(file, path, H5P_DEFAULT), H5Dclose};
ASSERT_GE(dataset.Get(), 0);
Hdf5Handle type{H5Dget_type(dataset.Get()), H5Tclose};
EXPECT_EQ(H5Tget_nmembers(type.Get()), fields.size());
for (std::size_t index = 0U; index < fields.size(); ++index) {
char* name = H5Tget_member_name(type.Get(), static_cast<unsigned>(index));
ASSERT_NE(name, nullptr);
EXPECT_STREQ(name, fields[index].first);
(void)H5free_memory(name);
Hdf5Handle member{
H5Tget_member_type(type.Get(), static_cast<unsigned>(index)), H5Tclose};
EXPECT_GT(H5Tequal(member.Get(), fields[index].second), 0);
}
Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose};
EXPECT_EQ(H5Sget_simple_extent_ndims(space.Get()), 1);
hsize_t actual = 0U;
EXPECT_GE(H5Sget_simple_extent_dims(space.Get(), &actual, nullptr), 0);
EXPECT_EQ(actual, rows);
}
TEST(DynamicHdf5ResultsWriter, RejectsDefaultStateBeforeCreatingOutput) {
auto domain = fesa::Domain::Create(fesa::ModelDefinition{});
ASSERT_TRUE(domain.HasValue());
TempFile output{"default"};
fesa::DynamicHdf5ResultsWriter writer;
const auto status = writer.Write(output.Path(), domain.Value(), {}, {});
EXPECT_FALSE(status.IsOk());
ASSERT_EQ(status.Diagnostics().size(), 1U);
EXPECT_EQ(status.Diagnostics()[0U].code, "dynamic-output-schema");
EXPECT_FALSE(std::filesystem::exists(output.Path()));
}
TEST(DynamicHdf5ResultsWriter, SeparatesReducedFilterCombinationFromLegacyDriver) {
auto fixture = MakeFixture();
for (std::uint32_t index = 0U; index <= 2U; ++index) {
auto frame = MakeFrame(*fixture.domain, index);
frame.time_integration_formulation =
fesa::DynamicTimeIntegrationFormulation::kReducedCoordinateHht;
ASSERT_TRUE(fixture.state.CommitAcceptedFrame(frame).IsOk());
}
TempFile output{"reduced-schema"};
fesa::DynamicHdf5ResultsWriter writer;
ASSERT_TRUE(writer.Write(output.Path(), *fixture.domain, fixture.state, {}).IsOk());
Hdf5Handle file{
H5Fopen(output.Path().string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT),
H5Fclose};
ASSERT_GE(file.Get(), 0);
Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose};
EXPECT_EQ(ReadAttribute<std::uint64_t>(metadata.Get(), "schema_version",
H5T_STD_U64LE, H5T_NATIVE_UINT64), 2U);
EXPECT_EQ(ReadString(metadata.Get(), "time_integration_formulation"),
"reduced-coordinate-HHT-with-physical-rate-reconstruction");
for (std::uint32_t index = 0U; index <= 2U; ++index) {
const std::string root = "/steps/Step-1/frames/" + std::to_string(index);
Hdf5Handle auxiliary{H5Dopen2(file.Get(),
(root + "/nodal/algorithmic_acceleration").c_str(),
H5P_DEFAULT), H5Dclose};
EXPECT_EQ(ReadAttribute<std::uint64_t>(auxiliary.Get(), "schema_version",
H5T_STD_U64LE, H5T_NATIVE_UINT64), 2U);
EXPECT_EQ(ReadString(auxiliary.Get(), "quantity_semantics"),
"full-chart-HHT-filter-combination-not-predictor-driver");
Hdf5Handle physical{H5Dopen2(file.Get(), (root + "/nodal/acceleration").c_str(),
H5P_DEFAULT), H5Dclose};
EXPECT_EQ(ReadString(physical.Get(), "quantity_semantics"),
"physical-endpoint-chart-acceleration");
}
}
TEST(DynamicHdf5ResultsWriter, WritesCompletePhysicalClampedHistoryAndSchema) {
auto fixture = MakeFixture();
CommitHistory(fixture);
TempFile output{"complete"};
fesa::DynamicHdf5ResultsWriter writer;
const std::vector<fesa::Diagnostic> diagnostics{{fesa::Severity::kWarning,
"ignored-output-request",
{"dynamic-state.inp", 22U},
"OUTPUT",
"",
"Field request ignored."},
{fesa::Severity::kWarning,
"ignored-output-request",
{"dynamic-state.inp", 21U},
"RESTART",
"",
"Restart request ignored."}};
const auto status =
writer.Write(output.Path(), *fixture.domain, fixture.state, diagnostics);
ASSERT_TRUE(status.IsOk())
<< (status.Diagnostics().empty() ? "" : status.Diagnostics()[0U].message);
Hdf5Handle file{
H5Fopen(output.Path().string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT),
H5Fclose};
ASSERT_GE(file.Get(), 0);
Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose};
EXPECT_EQ(ReadString(metadata.Get(), "feature_id"), "hht-implicit-dynamics");
EXPECT_EQ(ReadString(metadata.Get(), "analysis_procedure"),
"hht-implicit-dynamics");
EXPECT_EQ(ReadString(metadata.Get(), "source_step_name"), "Transient Load");
EXPECT_EQ(ReadString(metadata.Get(), "mass_formulation"),
"nodal-lumped-kinetic");
EXPECT_EQ(ReadString(metadata.Get(), "acceleration_convention"),
"physical-endpoint-and-auxiliary-separated");
EXPECT_EQ(ReadString(metadata.Get(), "initial_state"),
"zero-q-v-step-load-0+");
EXPECT_EQ(ReadString(metadata.Get(), "unit_system_label"),
"consistent-input-units");
EXPECT_EQ(ReadAttribute<std::uint64_t>(metadata.Get(), "schema_version",
H5T_STD_U64LE, H5T_NATIVE_UINT64),
1U);
EXPECT_DOUBLE_EQ(ReadAttribute<double>(metadata.Get(), "alpha",
H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE),
-0.05);
EXPECT_DOUBLE_EQ(ReadAttribute<double>(metadata.Get(), "beta", H5T_IEEE_F64LE,
H5T_NATIVE_DOUBLE),
0.275625);
EXPECT_DOUBLE_EQ(ReadAttribute<double>(metadata.Get(), "gamma",
H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE),
0.55);
ExpectCompound(file.Get(), "/steps/Step-1/accepted_frames",
{{"frame_index", H5T_STD_U64LE},
{"increment_index", H5T_STD_U64LE},
{"time", H5T_IEEE_F64LE},
{"iteration_count", H5T_STD_U64LE},
{"is_initial", H5T_STD_U8LE}},
3U);
ExpectCompound(
file.Get(), "/model/material_density",
{{"internal_material_id", H5T_STD_U64LE}, {"rho", H5T_IEEE_F64LE}}, 1U);
struct Density {
std::uint64_t id;
double rho;
};
Hdf5Handle density{
H5Dopen2(file.Get(), "/model/material_density", H5P_DEFAULT), H5Dclose};
Hdf5Handle density_type{H5Dget_type(density.Get()), H5Tclose};
Density density_row{};
ASSERT_GE(H5Dread(density.Get(), density_type.Get(), H5S_ALL, H5S_ALL,
H5P_DEFAULT, &density_row),
0);
EXPECT_EQ(density_row.id, 0U);
EXPECT_DOUBLE_EQ(density_row.rho, 2.0);
Hdf5Handle mask{
H5Dopen2(file.Get(), "/model/nodal_constraint_mask", H5P_DEFAULT),
H5Dclose};
Hdf5Handle mask_type{H5Dget_type(mask.Get()), H5Tclose};
EXPECT_GT(H5Tequal(mask_type.Get(), H5T_STD_U8LE), 0);
std::array<std::uint8_t, 24U> constraints{};
ASSERT_GE(H5Dread(mask.Get(), H5T_NATIVE_UINT8, H5S_ALL, H5S_ALL, H5P_DEFAULT,
constraints.data()),
0);
for (const auto value : constraints) EXPECT_EQ(value, 1U);
EXPECT_EQ(ReadDoubles(file.Get(), "/model/prescribed_displacement", {4U, 6U}),
std::vector<double>(24U, 0.0));
EXPECT_EQ(H5Lexists(file.Get(), "/model/prescribed_displacement_start",
H5P_DEFAULT),
0);
EXPECT_EQ(H5Lexists(file.Get(), "/model/prescribed_displacement_target",
H5P_DEFAULT),
0);
struct Dataset {
const char* path;
std::vector<hsize_t> dimensions;
const char* components;
};
const std::vector<Dataset> datasets{
{"nodal/displacement", {4U, 6U}, "UX,UY,UZ,URX,URY,URZ"},
{"nodal/velocity", {4U, 6U}, "V1,V2,V3,PHIDOT1,PHIDOT2,PHIDOT3"},
{"nodal/acceleration", {4U, 6U}, "A1,A2,A3,PHIDDOT1,PHIDDOT2,PHIDDOT3"},
{"nodal/algorithmic_acceleration",
{4U, 6U},
"AUX1,AUX2,AUX3,AUX4,AUX5,AUX6"},
{"nodal/angular_velocity", {4U, 3U}, "OMEGA1,OMEGA2,OMEGA3"},
{"nodal/angular_acceleration", {4U, 3U}, "OMEGADOT1,OMEGADOT2,OMEGADOT3"},
{"nodal/triad", {4U, 3U, 3U}, "X,Y,Z"},
{"nodal/director", {4U, 3U}, "D1,D2,D3"},
{"nodal/generalized_inertia_force",
{4U, 6U},
"F1,F2,F3,MPHI1,MPHI2,MPHI3"},
{"nodal/generalized_internal_force",
{4U, 6U},
"F1,F2,F3,MPHI1,MPHI2,MPHI3"},
{"nodal/generalized_external_force",
{4U, 6U},
"F1,F2,F3,MPHI1,MPHI2,MPHI3"},
{"nodal/generalized_residual", {4U, 6U}, "F1,F2,F3,MPHI1,MPHI2,MPHI3"},
{"nodal/generalized_reaction", {4U, 6U}, "F1,F2,F3,MPHI1,MPHI2,MPHI3"},
{"nodal/reaction", {4U, 6U}, "RF1,RF2,RF3,RM1,RM2,RM3"},
{"element/shell/local_frame", {1U, 4U, 3U, 3U}, "X,Y,Z"},
{"element/shell/generalized_strain",
{1U, 4U, 8U},
"E11,E22,G12,K11,K22,K12,G13,G23"},
{"element/shell/section_resultant",
{1U, 4U, 8U},
"N11,N22,N12,M11,M22,M12,Q13,Q23"},
{"element/shell/stress", {1U, 4U, 3U, 3U}, "S11,S22,S12"},
{"global/energy",
{5U},
"KINETIC_ENERGY,PHYSICAL_STRAIN_ENERGY,DRILLING_ENERGY,EXTERNAL_WORK,"
"ENERGY_DEFECT"},
{"global/equilibrium",
{6U},
"FORCE_1,FORCE_2,FORCE_3,MOMENT_1,MOMENT_2,MOMENT_3"}};
for (std::uint64_t index = 0U; index <= 2U; ++index) {
const std::string root =
"/steps/Step-1/frames/" + std::to_string(index) + "/";
for (const auto& expected : datasets) {
const std::string path = root + expected.path;
SCOPED_TRACE(path);
const auto values = ReadDoubles(file.Get(), path, expected.dimensions);
Hdf5Handle dataset{H5Dopen2(file.Get(), path.c_str(), H5P_DEFAULT),
H5Dclose};
EXPECT_EQ(ReadString(dataset.Get(), "component_names"),
expected.components);
EXPECT_FALSE(
ReadString(dataset.Get(), "component_unit_dimensions").empty());
EXPECT_FALSE(ReadString(dataset.Get(), "coordinate_system").empty());
EXPECT_FALSE(ReadString(dataset.Get(), "location").empty());
EXPECT_EQ(ReadString(dataset.Get(), "step_name"), "Step-1");
EXPECT_EQ(ReadString(dataset.Get(), "source_step_name"),
"Transient Load");
EXPECT_EQ(ReadAttribute<std::uint64_t>(dataset.Get(), "frame_index",
H5T_STD_U64LE, H5T_NATIVE_UINT64),
index);
EXPECT_EQ(ReadAttribute<std::uint64_t>(dataset.Get(), "increment_index",
H5T_STD_U64LE, H5T_NATIVE_UINT64),
index);
EXPECT_EQ(ReadAttribute<std::uint8_t>(dataset.Get(), "is_initial",
H5T_STD_U8LE, H5T_NATIVE_UINT8),
index == 0U ? 1U : 0U);
EXPECT_DOUBLE_EQ(ReadAttribute<double>(dataset.Get(), "time",
H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE),
static_cast<double>(index) * 0.25);
EXPECT_EQ(H5Aexists(dataset.Get(), "load_factor"), 0);
const std::string leaf{expected.path};
if (leaf == "nodal/triad" || leaf == "element/shell/local_frame") {
for (std::size_t entry = 0U; entry < values.size(); ++entry) {
EXPECT_DOUBLE_EQ(
values[entry],
entry % 9U == 0U || entry % 9U == 4U || entry % 9U == 8U ? 1.0
: 0.0);
}
} else if (leaf == "nodal/director") {
for (std::size_t entry = 0U; entry < values.size(); ++entry)
EXPECT_DOUBLE_EQ(values[entry], entry % 3U == 2U ? 1.0 : 0.0);
} else {
for (std::size_t entry = 0U; entry < values.size(); ++entry) {
const bool force =
entry == 2U && (leaf == "nodal/generalized_external_force" ||
leaf == "nodal/generalized_residual" ||
leaf == "nodal/generalized_reaction" ||
leaf == "nodal/reaction");
EXPECT_DOUBLE_EQ(
values[entry],
force ? (leaf == "nodal/generalized_external_force" ? 4.0 : -4.0)
: 0.0);
}
}
}
}
Hdf5Handle stress{
H5Dopen2(file.Get(), "/steps/Step-1/frames/2/element/shell/stress",
H5P_DEFAULT),
H5Dclose};
EXPECT_EQ(ReadString(stress.Get(), "source_element_type_dataset"),
"/model/elements.source_element_type");
EXPECT_EQ(ReadString(stress.Get(), "midsurface_location_dataset"),
"/model/shell/midsurface_locations");
EXPECT_EQ(ReadString(stress.Get(), "section_position_dataset"),
"/model/shell/section_positions");
EXPECT_EQ(ReadString(stress.Get(), "local_frame_dataset"),
"/steps/Step-1/frames/2/element/shell/local_frame");
Hdf5Handle diagnostic_table{H5Dopen2(file.Get(), "/diagnostics", H5P_DEFAULT),
H5Dclose};
Hdf5Handle diagnostic_type{H5Dget_type(diagnostic_table.Get()), H5Tclose};
EXPECT_GE(H5Tget_member_index(diagnostic_type.Get(), "source_file"), 0);
EXPECT_GE(H5Tget_member_index(diagnostic_type.Get(), "source_line"), 0);
}
TEST(DynamicHdf5ResultsWriter,
PreservesSeparateSuppliedEndpointAndAuxiliaryEvidence) {
auto fixture = MakeFixture(MakeDefinition(false));
ASSERT_TRUE(
fixture.state.CommitAcceptedFrame(MakeFrame(*fixture.domain, 0U)).IsOk());
// This checks serialization of supplied endpoint evidence; it does not assert
// that these records are a converged HHT trajectory or independent physics
// proof.
for (std::uint32_t index = 1U; index <= 2U; ++index) {
auto frame = MakeFrame(*fixture.domain, index);
frame.acceleration[0U] = 2.0;
frame.algorithmic_acceleration[0U] = 3.0;
frame.velocity[0U] = 4.0;
frame.inertia_force[0U] = 5.0;
frame.residual[0U] = 5.0;
frame.residual_scale[0U] = 5.0;
frame.kinetic_energy = 6.0;
frame.physical_strain_energy = 7.0;
frame.drilling_energy = 8.0;
frame.external_work = 9.0;
frame.energy_defect = 12.0;
ASSERT_TRUE(fixture.state.CommitAcceptedFrame(frame).IsOk());
}
TempFile output{"separate-accelerations"};
fesa::DynamicHdf5ResultsWriter writer;
ASSERT_TRUE(
writer.Write(output.Path(), *fixture.domain, fixture.state, {}).IsOk());
Hdf5Handle file{
H5Fopen(output.Path().string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT),
H5Fclose};
const std::string root = "/steps/Step-1/frames/2/";
EXPECT_DOUBLE_EQ(
ReadDoubles(file.Get(), root + "nodal/acceleration", {4U, 6U})[0U], 2.0);
EXPECT_DOUBLE_EQ(
ReadDoubles(file.Get(), root + "nodal/algorithmic_acceleration",
{4U, 6U})[0U],
3.0);
EXPECT_DOUBLE_EQ(
ReadDoubles(file.Get(), root + "nodal/generalized_inertia_force",
{4U, 6U})[0U],
5.0);
EXPECT_EQ(ReadDoubles(file.Get(), root + "global/energy", {5U}),
(std::vector<double>{6.0, 7.0, 8.0, 9.0, 12.0}));
Hdf5Handle acceleration{
H5Dopen2(file.Get(), (root + "nodal/acceleration").c_str(), H5P_DEFAULT),
H5Dclose};
EXPECT_EQ(ReadString(acceleration.Get(), "rotation_representation"),
"total-rotation-vector-chart");
EXPECT_EQ(ReadString(acceleration.Get(), "quantity_semantics"),
"physical-endpoint-chart-acceleration");
}
TEST(DynamicHdf5ResultsWriter,
ConvertsPartialConstraintMomentToSpatialReaction) {
auto definition = MakeDefinition(false);
definition.dynamic_step->boundaries = {{"Loaded", 4, 4, 0.0, {}}};
auto fixture = MakeFixture(std::move(definition));
ASSERT_TRUE(
fixture.state.CommitAcceptedFrame(MakeFrame(*fixture.domain, 0U)).IsOk());
// A supplied finite-chart endpoint tests the output coordinate transform,
// independently of time-integration acceptance.
for (std::uint32_t index = 1U; index <= 2U; ++index) {
auto frame = MakeFrame(*fixture.domain, index);
frame.generalized_coordinates[4U] = 0.2;
frame.generalized_coordinates[5U] = 0.3;
const auto rotation = fesa::ExpSkew(fesa::Vector3{0.0, 0.2, 0.3});
for (std::size_t axis = 0U; axis < 3U; ++axis) {
for (std::size_t component = 0U; component < 3U; ++component) {
frame.nodal_triad[0U][axis][component] = rotation(component, axis);
}
frame.nodal_director[0U][axis] = rotation(axis, 2U);
}
frame.internal_force[3U] = 5.0;
frame.residual[3U] = 5.0;
frame.reaction[3U] = 5.0;
frame.residual_scale[3U] = 5.0;
ASSERT_TRUE(fixture.state.CommitAcceptedFrame(frame).IsOk());
}
TempFile output{"spatial-reaction"};
fesa::DynamicHdf5ResultsWriter writer;
ASSERT_TRUE(
writer.Write(output.Path(), *fixture.domain, fixture.state, {}).IsOk());
Hdf5Handle file{
H5Fopen(output.Path().string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT),
H5Fclose};
const std::string root = "/steps/Step-1/frames/2/nodal/";
const auto generalized =
ReadDoubles(file.Get(), root + "generalized_reaction", {4U, 6U});
const auto spatial = ReadDoubles(file.Get(), root + "reaction", {4U, 6U});
EXPECT_DOUBLE_EQ(generalized[3U], 5.0);
EXPECT_DOUBLE_EQ(generalized[4U], 0.0);
EXPECT_DOUBLE_EQ(generalized[5U], 0.0);
const double angle = std::sqrt(0.13);
const double coefficient =
1.0 / 0.13 - (1.0 + std::cos(angle)) / (2.0 * angle * std::sin(angle));
EXPECT_NEAR(spatial[3U], 5.0 * (1.0 - coefficient * 0.13), 1.0e-14);
EXPECT_NEAR(spatial[4U], 0.75, 1.0e-14);
EXPECT_NEAR(spatial[5U], -0.5, 1.0e-14);
}
TEST(DynamicHdf5ResultsWriter,
IncompleteOrMismatchedHistoryPreservesExistingFile) {
auto fixture = MakeFixture();
ASSERT_TRUE(
fixture.state.CommitAcceptedFrame(MakeFrame(*fixture.domain, 0U)).IsOk());
TempFile output{"preservation"};
{
std::ofstream existing{output.Path(), std::ios::binary};
existing << "previous-result";
}
fesa::DynamicHdf5ResultsWriter writer;
EXPECT_FALSE(
writer.Write(output.Path(), *fixture.domain, fixture.state, {}).IsOk());
EXPECT_EQ(ReadBytes(output.Path()), "previous-result");
for (std::uint32_t index = 1U; index <= 2U; ++index)
ASSERT_TRUE(
fixture.state.CommitAcceptedFrame(MakeFrame(*fixture.domain, index))
.IsOk());
auto different_definition = MakeDefinition();
different_definition.nodes[0U].source_id.source_label_text = "10";
auto other = MakeFixture(std::move(different_definition));
EXPECT_FALSE(
writer.Write(output.Path(), *other.domain, fixture.state, {}).IsOk());
EXPECT_EQ(ReadBytes(output.Path()), "previous-result");
ASSERT_TRUE(
writer.Write(output.Path(), *fixture.domain, fixture.state, {}).IsOk());
const auto valid_bytes = ReadBytes(output.Path());
// Deliberate post-commit corruption proves writer revalidation is independent
// of the state API that normally prevents these mutations.
auto& corrupt = const_cast<fesa::DynamicAcceptedFrameState&>(
fixture.state.CommittedState());
corrupt.reaction[2U] = 0.0;
EXPECT_FALSE(
writer.Write(output.Path(), *fixture.domain, fixture.state, {}).IsOk());
EXPECT_EQ(ReadBytes(output.Path()), valid_bytes);
}
TEST(DynamicHdf5ResultsWriter, FailedAtomicReplacementPreservesPreviousBytes) {
auto fixture = MakeFixture();
CommitHistory(fixture);
TempFile output{"locked"};
{
std::ofstream existing{output.Path(), std::ios::binary};
existing << "locked-previous-result";
}
const HANDLE locked =
CreateFileW(output.Path().c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
ASSERT_NE(locked, INVALID_HANDLE_VALUE);
fesa::DynamicHdf5ResultsWriter writer;
const auto status =
writer.Write(output.Path(), *fixture.domain, fixture.state, {});
EXPECT_FALSE(status.IsOk());
EXPECT_TRUE(CloseHandle(locked));
ASSERT_EQ(status.Diagnostics().size(), 1U);
EXPECT_EQ(status.Diagnostics()[0U].code, "hdf5-finalization-failure");
EXPECT_EQ(ReadBytes(output.Path()), "locked-previous-result");
}
} // namespace