feat: implement consistent-mass HHT shell dynamics
This commit is contained in:
@@ -0,0 +1,636 @@
|
||||
#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, 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"),
|
||||
"consistent-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
|
||||
Reference in New Issue
Block a user