Files
FESADev/src/fesa/io/hdf5/solid_hdf5_results_writer.cpp
T

759 lines
37 KiB
C++

#include "fesa/io/hdf5/solid_hdf5_results_writer.h"
#include <cmath>
#include <cstring>
#include <set>
#include <stdexcept>
#include <type_traits>
#include "fesa/analysis/analysis_model.h"
#include "fesa/build_info.h"
#include "fesa/elements/element_factory.h"
#include "fesa/elements/hex8_element.h"
#include "fesa/materials/isotropic_linear_elastic_material.h"
#include "io/hdf5/hdf5_atomic_file.h"
#include "io/hdf5/hdf5_model_writer.h"
#include "io/hdf5/hdf5_primitives.h"
#include "io/hdf5/hdf5_result_writer.h"
#include "io/hdf5/solid_hdf5_self_check.h"
#include "math/high_low_arithmetic.h"
namespace fesa::hdf5_internal {
namespace {
void Check(bool condition, const char* message) {
if (!condition) throw Hdf5Failure(message);
}
using U = std::vector<std::uint64_t>;
using B = std::vector<std::uint8_t>;
using D = std::vector<double>;
using S = std::vector<std::string>;
void Parent(hid_t file, const std::string& path) {
const auto parent = path.substr(0, path.find_last_of('/'));
if (!parent.empty() && H5Lexists(file, parent.c_str(), H5P_DEFAULT) <= 0)
(void)CreateGroup(file, parent.c_str());
}
void Table(hid_t file, const SolidTable& table) {
Parent(file, table.path);
std::vector<Hdf5Handle> native, filetypes;
std::size_t size = 0;
for (const auto& column : table.columns) {
native.push_back(SolidColumnType(column, true));
filetypes.push_back(SolidColumnType(column, false));
size += H5Tget_size(native.back().Get());
}
Hdf5Handle ft{RequireHdf5Id(H5Tcreate(H5T_COMPOUND, size),
"Solid file compound failed."),
H5Tclose};
Hdf5Handle mt{RequireHdf5Id(H5Tcreate(H5T_COMPOUND, size),
"Solid memory compound failed."),
H5Tclose};
std::vector<unsigned char> bytes(table.rows * size);
std::size_t offset = 0;
for (std::size_t c = 0; c < table.columns.size(); ++c) {
const auto& column = table.columns[c];
RequireHdf5(
H5Tinsert(ft.Get(), column.name.c_str(), offset, filetypes[c].Get()),
"Solid file member failed.");
RequireHdf5(
H5Tinsert(mt.Get(), column.name.c_str(), offset, native[c].Get()),
"Solid memory member failed.");
std::visit(
[&](const auto& values) {
using T = typename std::decay_t<decltype(values)>::value_type;
for (std::size_t row = 0; row < table.rows; ++row) {
if constexpr (std::is_same_v<T, std::string>) {
const char* pointer = values[row].c_str();
std::memcpy(bytes.data() + row * size + offset, &pointer,
sizeof(pointer));
} else
std::memcpy(bytes.data() + row * size + offset,
values.data() + row * column.width,
column.width * sizeof(T));
}
},
column.values);
offset += H5Tget_size(native[c].Get());
}
auto dataset = WriteCompoundDataset(file, table.path.c_str(), table.rows,
ft.Get(), mt.Get(), bytes.data());
for (const auto& item : table.strings)
WriteStringAttribute(dataset.Get(), item.first.c_str(), item.second);
}
SolidData Data(const std::string& path, std::vector<hsize_t> dimensions,
D values, const char* names, const char* units,
const char* location) {
return {path,
std::move(dimensions),
std::move(values),
{{"component_names", names},
{"unit_dimensions", units},
{"coordinate_system", "global-cartesian"},
{"location", location},
{"step_name", "Step-1"}},
{},
{}};
}
void FrameData(SolidPlan& plan, const Domain& domain, std::size_t frame,
std::size_t increment, double coordinate, bool dynamic,
const Vector& u, const Vector& rf, const Vector& r,
const std::vector<SolidIntegrationPointRow>& points,
double energy) {
const std::string root = "/steps/Step-1/frames/" + std::to_string(frame);
const hsize_t n = domain.Nodes().size(), e = domain.Elements().Size();
const auto add = [&](SolidData value) {
value.integers = {{"frame_index", frame}, {"increment_index", increment}};
value.doubles = {{dynamic ? "time" : "load_factor", coordinate}};
plan.datasets.push_back(std::move(value));
};
add(Data(root + "/nodal/displacement", {n, 3}, CopyVectorValues(u), "U1,U2,U3",
"length", "nodal"));
add(Data(root + "/nodal/reaction", {n, 3}, CopyVectorValues(rf), "RF1,RF2,RF3", "force",
"nodal"));
add(Data(root + "/nodal/residual", {n, 3}, CopyVectorValues(r), "R1,R2,R3", "force",
"nodal"));
D stress, strain;
Check(points.size() == 8 * e, "Solid point count is invalid.");
for (std::size_t i = 0; i < points.size(); ++i) {
Check(points[i].element == i / 8 &&
points[i].integration_point == static_cast<int>(i % 8 + 1),
"Solid point identity is invalid.");
stress.insert(stress.end(), points[i].cauchy_stress.begin(),
points[i].cauchy_stress.end());
strain.insert(strain.end(), points[i].strain.begin(),
points[i].strain.end());
}
auto s =
Data(root + "/solid/stress", {e, 8, 6}, std::move(stress),
"S11,S22,S33,S12,S13,S23", "force/length^2", "integration-point");
s.strings["stress_measure"] = "cauchy";
add(std::move(s));
auto eps =
Data(root + "/solid/strain", {e, 8, 6}, std::move(strain),
"E11,E22,E33,E12,E13,E23", "dimensionless", "integration-point");
eps.strings["strain_measure"] = domain.Steps()[0].UsesNonlinearGeometry()
? "green-lagrange"
: "infinitesimal";
eps.strings["shear_convention"] = "tensor";
add(std::move(eps));
auto en = Data(root + "/global/strain_energy", {}, D{energy}, "scalar",
"force*length", "global");
en.strings["definition"] = domain.Steps()[0].UsesNonlinearGeometry()
? "sum(W*detJ0)"
: "0.5*u^T*K0*u";
add(std::move(en));
}
void Model(SolidPlan& plan, const Domain& domain, const DofManager& dofs) {
SolidTable nodes{"/model/nodes",
domain.Nodes().size(),
{{"internal_node_id", 1, U{}},
{"instance_name", 1, S{}},
{"source_label", 1, S{}},
{"coordinates", 3, D{}}}};
for (std::size_t i = 0; i < domain.Nodes().size(); ++i) {
const auto& node = domain.Nodes()[i];
std::get<U>(nodes.columns[0].values).push_back(i);
std::get<S>(nodes.columns[1].values)
.push_back(node.source_id.instance_name);
std::get<S>(nodes.columns[2].values)
.push_back(node.source_id.source_label_text);
auto& coords = std::get<D>(nodes.columns[3].values);
coords.insert(coords.end(), node.coordinates.begin(),
node.coordinates.end());
}
plan.tables.push_back(std::move(nodes));
SolidTable elements{"/model/elements",
domain.SolidElements().Size(),
{{"internal_element_id", 1, U{}},
{"instance_name", 1, S{}},
{"source_label", 1, S{}},
{"source_element_type", 1, S{}},
{"internal_formulation", 1, S{}},
{"node_internal_ids", 8, U{}},
{"solid_section_internal_id", 1, U{}},
{"material_internal_id", 1, U{}}}};
for (std::size_t i = 0; i < domain.SolidElements().Size(); ++i) {
const auto& element = domain.SolidElements()[i];
std::get<U>(elements.columns[0].values).push_back(i);
std::get<S>(elements.columns[1].values)
.push_back(element.SourceId().instance_name);
std::get<S>(elements.columns[2].values)
.push_back(element.SourceId().source_label_text);
std::get<S>(elements.columns[3].values)
.push_back(std::string(element.SourceElementType()));
std::get<S>(elements.columns[4].values).push_back("FESA-Hex8");
auto& connectivity = std::get<U>(elements.columns[5].values);
connectivity.insert(connectivity.end(), element.node_indices.begin(),
element.node_indices.end());
std::get<U>(elements.columns[6].values).push_back(element.section_index);
std::get<U>(elements.columns[7].values).push_back(element.MaterialIndex());
}
plan.tables.push_back(std::move(elements));
SolidTable materials{"/model/solid/materials",
domain.Materials().Size(),
{{"internal_material_id", 1, U{}},
{"name", 1, S{}},
{"youngs_modulus", 1, D{}},
{"poisson_ratio", 1, D{}},
{"density_present", 1, B{}},
{"density", 1, D{}}}};
for (std::size_t i = 0; i < domain.Materials().Size(); ++i) {
const auto* material =
dynamic_cast<const LinearElasticMaterial*>(&domain.Materials()[i]);
Check(material != nullptr, "Solid material is not isotropic elastic.");
bool present = false;
double rho = 0;
for (const auto& density : domain.Densities())
if (density.material_index == i) {
present = true;
rho = density.rho;
}
std::get<U>(materials.columns[0].values).push_back(i);
std::get<S>(materials.columns[1].values).push_back(material->Name());
std::get<D>(materials.columns[2].values)
.push_back(material->YoungsModulus());
std::get<D>(materials.columns[3].values)
.push_back(material->PoissonsRatio());
std::get<B>(materials.columns[4].values).push_back(present ? 1 : 0);
std::get<D>(materials.columns[5].values).push_back(rho);
}
plan.tables.push_back(std::move(materials));
SolidTable sections{"/model/solid/sections",
domain.SolidSections().Size(),
{{"internal_section_id", 1, U{}},
{"source_file", 1, S{}},
{"source_line", 1, U{}},
{"source_elset", 1, S{}},
{"material_internal_id", 1, U{}}}};
for (std::size_t i = 0; i < domain.SolidSections().Size(); ++i) {
const auto& section = domain.SolidSections()[i];
std::get<U>(sections.columns[0].values).push_back(i);
std::get<S>(sections.columns[1].values)
.push_back(section.Location().file.generic_u8string());
std::get<U>(sections.columns[2].values).push_back(section.Location().line);
std::get<S>(sections.columns[3].values).push_back(section.Name());
std::get<U>(sections.columns[4].values).push_back(section.MaterialIndex());
}
plan.tables.push_back(std::move(sections));
SolidTable points{"/model/solid_integration_points",
8 * domain.Elements().Size(),
{{"internal_element_id", 1, U{}},
{"point_id", 1, B{}},
{"natural_coordinates", 3, D{}},
{"reference_coordinates", 3, D{}}}};
ElementFactory factory;
for (std::size_t i = 0; i < domain.Elements().Size(); ++i) {
auto element = factory.Create(domain.Elements()[i], domain);
Check(element.HasValue(), "Solid factory reference failed.");
auto hex = factory.BindSolidTrial(*element.Value());
Check(hex != nullptr, "Solid kernel identity failed.");
auto response = hex->EvaluateLinear(Vector{24});
Check(response.HasValue(), "Solid reference recovery failed.");
for (const auto& point : response.Value().points) {
std::get<U>(points.columns[0].values).push_back(i);
std::get<B>(points.columns[1].values)
.push_back(static_cast<std::uint8_t>(point.integration_point));
auto& natural = std::get<D>(points.columns[2].values);
natural.insert(natural.end(), point.natural_coordinates.begin(),
point.natural_coordinates.end());
auto& position = std::get<D>(points.columns[3].values);
position.insert(position.end(), point.reference_coordinates.begin(),
point.reference_coordinates.end());
}
}
plan.tables.push_back(std::move(points));
plan.mask.assign(dofs.FullDofCount(), 0);
for (const auto index : dofs.ConstrainedDofs()) plan.mask[index] = 1;
}
void Diagnostics(SolidPlan& plan, std::vector<Diagnostic> diagnostics) {
SortDiagnostics(diagnostics);
SolidTable table{"/diagnostics",
diagnostics.size(),
{{"severity", 1, S{}},
{"code", 1, S{}},
{"file", 1, S{}},
{"line", 1, U{}},
{"keyword", 1, S{}},
{"entity_identity", 1, S{}},
{"message", 1, S{}}}};
for (const auto& d : diagnostics) {
std::get<S>(table.columns[0].values)
.push_back(d.severity == Severity::kWarning ? "warning" : "error");
std::get<S>(table.columns[1].values).push_back(d.code);
std::get<S>(table.columns[2].values)
.push_back(d.location.file.generic_u8string());
std::get<U>(table.columns[3].values).push_back(d.location.line);
std::get<S>(table.columns[4].values).push_back(d.keyword);
std::get<S>(table.columns[5].values).push_back(d.entity_identity);
std::get<S>(table.columns[6].values).push_back(d.message);
}
plan.tables.push_back(std::move(table));
}
} // namespace
Hdf5Handle SolidColumnType(const SolidColumn& column, bool native) {
Hdf5Handle result;
if (std::holds_alternative<S>(column.values))
result = MakeUtf8StringType();
else {
hid_t type = std::holds_alternative<U>(column.values)
? (native ? H5T_NATIVE_UINT64 : H5T_STD_U64LE)
: std::holds_alternative<B>(column.values)
? (native ? H5T_NATIVE_UINT8 : H5T_STD_U8LE)
: (native ? H5T_NATIVE_DOUBLE : H5T_IEEE_F64LE);
result = Hdf5Handle{
RequireHdf5Id(H5Tcopy(type), "Solid column type failed."), H5Tclose};
}
if (column.width > 1) {
const hsize_t width = column.width;
result = Hdf5Handle{RequireHdf5Id(H5Tarray_create2(result.Get(), 1, &width),
"Solid array type failed."),
H5Tclose};
}
return result;
}
Result<SolidPlan> BuildSolidPlan(const Domain& domain, SolidStateView view,
const std::vector<Diagnostic>& diagnostics) {
try {
if (std::holds_alternative<
std::reference_wrapper<const SolidDynamicAnalysisState>>(view))
Check(math_internal::ArithmeticEnvironmentSupported(),
"Solid RN64 serialization requires supported arithmetic "
"environment.");
Check(domain.IsSolid() && domain.Steps().Size() == 1,
"Solid output requires exactly one Solid step.");
auto model = AnalysisModel::Create(domain);
Check(model.HasValue(), "Solid output model failed.");
auto dofs = DofManager::Create(model.Value());
Check(dofs.HasValue(), "Solid output DOF partition failed.");
SolidPlan plan;
const bool dynamic = std::holds_alternative<
std::reference_wrapper<const SolidDynamicAnalysisState>>(view);
const bool frequency = std::holds_alternative<
std::reference_wrapper<const SolidFrequencyState>>(view);
const bool nonlinear = domain.Steps()[0].UsesNonlinearGeometry();
Check(dynamic == (domain.Dynamic() != nullptr) &&
frequency == domain.Steps()[0].IsFrequency(),
"Solid output procedure/state mismatch.");
plan.metadata = {
{"feature_id", "hex8-structural-analysis"},
{"analysis_procedure", dynamic ? (nonlinear ? "hht-nonlinear-dynamics"
: "hht-linear-dynamics")
: frequency ? "linear-frequency"
: nonlinear ? "nonlinear-static-full-newton"
: "linear-static"},
{"element_formulation", "FESA-Hex8"},
{"integration_rule", "gauss-2x2x2"},
{"mass_formulation", "lumped-reference"},
{"source_input_identity", SourceInputIdentity(domain)},
{"source_step_name", domain.Steps()[0].Name()},
{"unit_system_label", "user-consistent-unspecified"},
{"coordinate_convention",
"global-cartesian; reference-strain; current-cauchy-stress"},
{"component_order", "node-major; UX,UY,UZ"},
{"solver_version", std::string(SolverVersion())}};
Model(plan, domain, dofs.Value());
Diagnostics(plan, diagnostics);
if (const auto* ref =
std::get_if<std::reference_wrapper<const SolidStaticState>>(
&view)) {
const auto& state = ref->get();
auto validator = SolidStaticState::Create(domain, dofs.Value());
Check(validator.HasValue(), "Static output validator creation failed.");
Check(!state.AcceptedFrames().empty(), "Static output history is empty.");
SolidTable accepted{"/steps/Step-1/accepted_frames",
state.AcceptedFrames().size(),
{{"frame_index", 1, U{}},
{"increment_index", 1, U{}},
{"load_factor", 1, D{}},
{"iteration_count", 1, U{}},
{"is_factor_one", 1, B{}}}};
for (const auto& frame : state.AcceptedFrames()) {
Check(validator.Value().CommitAcceptedFrame(frame).IsOk(),
"Static accepted frame is invalid.");
FrameData(plan, domain, frame.frame_index, frame.increment_index,
frame.load_factor, false, frame.displacement, frame.reaction,
frame.residual, frame.solid_results,
frame.physical_strain_energy);
std::get<U>(accepted.columns[0].values).push_back(frame.frame_index);
std::get<U>(accepted.columns[1].values)
.push_back(frame.increment_index);
std::get<D>(accepted.columns[2].values).push_back(frame.load_factor);
std::get<U>(accepted.columns[3].values)
.push_back(frame.iteration_count);
std::get<B>(accepted.columns[4].values)
.push_back(frame.is_factor_one ? 1 : 0);
}
Check(state.AcceptedFrames().back().is_factor_one,
"Static output is incomplete.");
if (nonlinear) plan.tables.push_back(std::move(accepted));
} else if (const auto* dynamic_ref = std::get_if<
std::reference_wrapper<const SolidDynamicAnalysisState>>(
&view)) {
Check(math_internal::ArithmeticEnvironmentSupported(),
"Solid RN64 serialization requires supported arithmetic "
"environment.");
const auto& state = dynamic_ref->get();
auto validator = SolidDynamicAnalysisState::Create(domain, dofs.Value());
Check(validator.HasValue(), "Dynamic output validator creation failed.");
Check(state.AcceptedFrames().size() ==
domain.Dynamic()->increment_count + 1,
"Dynamic output history is incomplete.");
SolidTable accepted{"/steps/Step-1/accepted_frames",
state.AcceptedFrames().size(),
{{"frame_index", 1, U{}},
{"increment_index", 1, U{}},
{"time", 1, D{}},
{"iteration_count", 1, U{}},
{"is_initial", 1, B{}}}};
for (const auto& frame : state.AcceptedFrames()) {
Check(validator.Value().CommitAcceptedFrame(frame).IsOk(),
"Full-pair dynamic accepted frame is invalid.");
FrameData(plan, domain, frame.frame_index, frame.increment_index,
frame.time, true, frame.displacement, frame.reaction,
frame.residual, frame.solid_results, frame.strain_energy);
const std::string root =
"/steps/Step-1/frames/" + std::to_string(frame.frame_index);
const auto add = [&](SolidData data) {
data.integers = {{"frame_index", frame.frame_index},
{"increment_index", frame.increment_index}};
data.doubles = {{"time", frame.time}};
plan.datasets.push_back(std::move(data));
};
add(Data(root + "/nodal/velocity", {domain.Nodes().size(), 3},
CopyVectorValues(frame.velocity), "V1,V2,V3", "length/time", "nodal"));
auto a = Data(root + "/nodal/acceleration", {domain.Nodes().size(), 3},
CopyVectorValues(frame.acceleration), "A1,A2,A3", "length/time^2",
"nodal");
a.strings["acceleration_kind"] = "physical";
add(std::move(a));
a = Data(root + "/nodal/algorithmic_acceleration",
{domain.Nodes().size(), 3},
CopyVectorValues(frame.algorithmic_acceleration), "A1,A2,A3",
"length/time^2", "nodal");
a.strings["acceleration_kind"] = "algorithmic";
add(std::move(a));
for (const auto& quantity :
std::vector<std::tuple<const char*, double, const char*>>{
{"kinetic_energy", frame.kinetic_energy, "0.5*v^T*M*v"},
{"external_work", frame.external_work,
"sum(0.5*(F_old+F_new)*delta_u)"},
{"energy_balance_error", frame.energy_balance_error,
"T+U-T0-U0-Wext"}}) {
auto data = Data(root + "/global/" + std::get<0>(quantity), {},
D{std::get<1>(quantity)}, "scalar", "force*length",
"global");
data.strings["definition"] = std::get<2>(quantity);
add(std::move(data));
}
add(Data(root + "/global/linear_momentum", {3},
D(frame.linear_momentum.begin(), frame.linear_momentum.end()),
"P1,P2,P3", "mass*length/time", "global"));
auto angular = Data(
root + "/global/angular_momentum", {3},
D(frame.angular_momentum.begin(), frame.angular_momentum.end()),
"L1,L2,L3", "mass*length^2/time", "global");
angular.strings["origin"] = "global0";
add(std::move(angular));
std::get<U>(accepted.columns[0].values).push_back(frame.frame_index);
std::get<U>(accepted.columns[1].values)
.push_back(frame.increment_index);
std::get<D>(accepted.columns[2].values).push_back(frame.time);
std::get<U>(accepted.columns[3].values)
.push_back(frame.iteration_count);
std::get<B>(accepted.columns[4].values)
.push_back(frame.is_initial ? 1 : 0);
}
plan.tables.push_back(std::move(accepted));
} else {
const auto& state =
std::get<std::reference_wrapper<const SolidFrequencyState>>(view)
.get();
Check(domain.Frequency()->requested_mode_count > 0,
"Modal request is invalid.");
const std::size_t count =
static_cast<std::size_t>(domain.Frequency()->requested_mode_count);
Check(state.full_dof_count == dofs.Value().FullDofCount() &&
state.free_dof_count == dofs.Value().FreeDofs().size() &&
state.constraint_mask == plan.mask,
"Modal DOF inventory is invalid.");
Check(state.prescribed_displacement.size() == plan.mask.size(),
"Modal prescribed shape is invalid.");
for (const auto value : state.prescribed_displacement)
Check(value == 0, "Modal prescribed value is invalid.");
Check(state.modes.size() == count && state.vectors.Rows() == count &&
state.vectors.Columns() == plan.mask.size(),
"Modal output shape is invalid.");
Check(std::isfinite(state.classification_scale) &&
state.classification_scale > 0 &&
std::isfinite(state.mass_lower_bound) &&
state.mass_lower_bound > 0,
"Modal certification scale is invalid.");
Check(std::isfinite(state.eta_gram) && state.eta_gram >= 0 &&
state.eta_gram <= 1e-10 && std::isfinite(state.max_eta_orth) &&
state.max_eta_orth >= 0 && state.max_eta_orth <= 1e-10,
"Modal Gram evidence is invalid.");
Check(state.eigenpair_checks.size() == state.backend.eigenvalues.size() &&
state.backend.column_ids.size() ==
state.eigenpair_checks.size() &&
state.eigenpair_checks.size() >= count &&
state.eigenpair_checks.size() <= state.free_dof_count,
"Modal backend verification inventory is invalid.");
std::size_t expected_cluster = 1;
SolidTable modes{"/steps/Step-1/eigen/modes",
count,
{{"mode_id", 1, U{}},
{"eigenvalue", 1, D{}},
{"classification", 1, B{}},
{"cluster_id", 1, U{}}}};
SolidTable frequencies{"/steps/Step-1/eigen/frequencies",
0,
{{"mode_id", 1, U{}}, {"frequency", 1, D{}}}};
D vectors;
for (std::size_t i = 0; i < count; ++i) {
const auto& mode = state.modes[i];
Check(mode.mode_id == i + 1 && mode.cluster_id > 0 &&
std::isfinite(mode.eigenvalue) &&
mode.eigenvalue / state.classification_scale >= -1e-9 &&
(i == 0 || mode.eigenvalue >= state.modes[i - 1].eigenvalue),
"Modal identity/order is invalid.");
if (i > 0 && (mode.eigenvalue - state.modes[i - 1].eigenvalue) /
state.classification_scale >
1e-8)
++expected_cluster;
Check(mode.cluster_id == expected_cluster &&
mode.eigenvalue == state.eigenpair_checks[i].eigenvalue,
"Modal cluster or selected-pair identity is invalid.");
const bool near =
std::abs(mode.eigenvalue) / state.classification_scale <= 1e-9;
Check(mode.classification == (near ? 0 : 1),
"Modal classification is invalid.");
Check(mode.frequency.has_value() == (mode.eigenvalue >= 0),
"Modal frequency presence is invalid.");
if (mode.frequency) {
Check(*mode.frequency ==
std::sqrt(mode.eigenvalue) / (2 * std::acos(-1.0)),
"Modal frequency is invalid.");
++frequencies.rows;
std::get<U>(frequencies.columns[0].values).push_back(mode.mode_id);
std::get<D>(frequencies.columns[1].values).push_back(*mode.frequency);
}
std::get<U>(modes.columns[0].values).push_back(mode.mode_id);
std::get<D>(modes.columns[1].values).push_back(mode.eigenvalue);
std::get<B>(modes.columns[2].values).push_back(mode.classification);
std::get<U>(modes.columns[3].values).push_back(mode.cluster_id);
for (std::size_t j = 0; j < plan.mask.size(); ++j) {
Check(std::isfinite(state.vectors(i, j)) &&
(!plan.mask[j] || state.vectors(i, j) == 0),
"Modal vector is invalid.");
vectors.push_back(state.vectors(i, j));
}
}
modes.strings = {{"component_names", "eigenvalue"},
{"unit_dimensions", "1/time^2"},
{"location", "global"},
{"coordinate_system", "global-cartesian"}};
frequencies.strings = {{"component_names", "frequency"},
{"unit_dimensions", "1/time"},
{"location", "global"},
{"coordinate_system", "global-cartesian"}};
plan.tables.push_back(std::move(modes));
plan.tables.push_back(std::move(frequencies));
auto data =
Data("/steps/Step-1/eigen/vectors", {count, domain.Nodes().size(), 3},
std::move(vectors), "UX,UY,UZ", "mass-normalized", "nodal");
data.strings["normalization"] = "phi^T*M*phi=1";
plan.datasets.push_back(std::move(data));
SolidTable pairs{"/steps/Step-1/eigen/verification/pairs",
state.eigenpair_checks.size(),
{{"sorted_rank", 1, U{}},
{"solver_column", 1, U{}},
{"eigenvalue", 1, D{}},
{"modal_mass", 1, D{}},
{"rayleigh_quotient", 1, D{}},
{"eta_r", 1, D{}},
{"eta_norm", 1, D{}},
{"eta_energy", 1, D{}}}};
std::set<std::size_t> solver_columns;
for (std::size_t i = 0; i < state.eigenpair_checks.size(); ++i) {
Check(state.backend.column_ids[i] < state.eigenpair_checks.size() &&
solver_columns.insert(state.backend.column_ids[i]).second,
"Modal backend column inventory is invalid.");
const auto& pair = state.eigenpair_checks[i];
const double normalized = pair.eigenvalue / state.classification_scale;
Check(std::isfinite(pair.eigenvalue) && std::isfinite(normalized),
"Backend eigenvalue or normalized eigenvalue is nonfinite.");
Check(normalized >= -1e-9,
"Significant negative backend eigenvalue cannot be published.");
Check(i == 0 ||
pair.eigenvalue >= state.eigenpair_checks[i - 1].eigenvalue,
"Backend eigenvalues are not sorted.");
Check(pair.sorted_rank == i + 1 &&
pair.solver_column == state.backend.column_ids[i] &&
pair.eigenvalue == state.backend.eigenvalues[i],
"Modal pair identity is invalid.");
Check(std::isfinite(pair.modal_mass) && pair.modal_mass > 0 &&
std::abs(pair.modal_mass - 1) <= 1e-10,
"Modal mass evidence is invalid.");
for (const double eta : {pair.eta_r, pair.eta_norm, pair.eta_energy})
Check(std::isfinite(eta) && eta >= 0 && eta <= 1e-10,
"Modal pair gate is invalid.");
std::get<U>(pairs.columns[0].values).push_back(pair.sorted_rank);
std::get<U>(pairs.columns[1].values).push_back(pair.solver_column);
const std::array<double, 6> values{{pair.eigenvalue, pair.modal_mass,
pair.rayleigh_quotient, pair.eta_r,
pair.eta_norm, pair.eta_energy}};
for (std::size_t j = 0; j < 6; ++j)
std::get<D>(pairs.columns[j + 2].values).push_back(values[j]);
}
plan.tables.push_back(std::move(pairs));
plan.datasets.push_back(Data("/steps/Step-1/eigen/verification/eta_gram",
{}, D{state.eta_gram}, "scalar",
"dimensionless", "global"));
plan.datasets.push_back(
Data("/steps/Step-1/eigen/verification/max_eta_orth", {},
D{state.max_eta_orth}, "scalar", "dimensionless", "global"));
}
for (const auto& table : plan.tables)
for (const auto& column : table.columns)
std::visit(
[&](const auto& values) {
Check(values.size() == table.rows * column.width,
"Solid column shape is invalid.");
using T = typename std::decay_t<decltype(values)>::value_type;
for (const auto& value : values) {
if constexpr (std::is_same_v<T, double>)
Check(std::isfinite(value),
"Solid numeric column is nonfinite.");
if constexpr (std::is_same_v<T, std::string>)
Check(IsValidUtf8(value) &&
value.find('\0') == std::string::npos,
"Solid string column is invalid UTF8.");
}
},
column.values);
for (const auto& data : plan.datasets) {
std::size_t size = 1;
for (const auto width : data.dimensions) {
Check(SizeProductFits(size, width, size),
"Solid dataset shape overflow.");
}
Check(size == data.values.size(), "Solid dataset shape is invalid.");
for (const auto value : data.values)
Check(std::isfinite(value), "Solid dataset value is nonfinite.");
}
for (const auto& item : plan.metadata)
Check(IsValidUtf8(item.second) &&
item.second.find('\0') == std::string::npos,
"Solid metadata UTF8 is invalid.");
return Result<SolidPlan>::Success(std::move(plan));
} catch (const std::exception& error) {
return Result<SolidPlan>::Failure(
OutputFailure("invalid-solid-output", error.what()));
}
}
Status WriteSolidFile(const std::filesystem::path& output, const Domain& domain,
SolidStateView view,
const std::vector<Diagnostic>& diagnostics,
const SolidFileHooks& hooks) {
const auto plan = BuildSolidPlan(domain, view, diagnostics);
if (!plan.HasValue()) return plan.GetStatus();
try {
Hdf5ErrorSilencer silence;
const auto candidate = MakeCandidatePath(output);
CandidateFileGuard guard(candidate);
Hdf5Handle file{
RequireHdf5Id(H5Fcreate(candidate.string().c_str(), H5F_ACC_EXCL,
H5P_DEFAULT, H5P_DEFAULT),
"Solid candidate create failed."),
hooks.candidate_close};
if (hooks.before_write) hooks.before_write(file.Get());
if (std::holds_alternative<
std::reference_wrapper<const SolidDynamicAnalysisState>>(view))
Check(math_internal::ArithmeticEnvironmentSupported(),
"Solid RN64 serialization arithmetic environment changed.");
{
auto group = CreateGroup(file.Get(), "/metadata");
WriteUint64Attribute(group.Get(), "schema_version", 1);
for (const auto& item : plan.Value().metadata)
WriteStringAttribute(group.Get(), item.first.c_str(), item.second);
}
(void)CreateGroup(file.Get(), "/model");
WriteNodes(file.Get(), domain);
for (const auto& table : plan.Value().tables)
if (table.path != "/model/nodes" && table.path != "/diagnostics")
Table(file.Get(), table);
WriteDiagnostics(file.Get(), diagnostics);
{
const std::vector<hsize_t> dimensions{domain.Nodes().size(), 3};
auto space = CreateDatasetSpace(dimensions);
Hdf5Handle dataset{
RequireHdf5Id(H5Dcreate2(file.Get(), "/model/nodal_constraint_mask",
H5T_STD_U8LE, space.Get(), H5P_DEFAULT,
H5P_DEFAULT, H5P_DEFAULT),
"Solid mask create failed."),
H5Dclose};
RequireHdf5(H5Dwrite(dataset.Get(), H5T_NATIVE_UINT8, H5S_ALL, H5S_ALL,
H5P_DEFAULT, plan.Value().mask.data()),
"Solid mask write failed.");
WriteStringAttribute(dataset.Get(), "component_names", "UX,UY,UZ");
}
for (const auto& data : plan.Value().datasets) {
Parent(file.Get(), data.path);
auto dataset = WriteDoubleValues(file.Get(), data.path, data.dimensions,
data.values.data(), data.values.size());
for (const auto& item : data.strings)
WriteStringAttribute(dataset.Get(), item.first.c_str(), item.second);
for (const auto& item : data.integers)
WriteUint64Attribute(dataset.Get(), item.first.c_str(), item.second);
for (const auto& item : data.doubles)
WriteDoubleAttribute(dataset.Get(), item.first.c_str(), item.second);
}
RequireHdf5(H5Fflush(file.Get(), H5F_SCOPE_GLOBAL),
"Solid candidate flush failed.");
RequireHdf5(file.CloseChecked(), "Solid candidate close failed.");
if (hooks.before_self_check) hooks.before_self_check(candidate);
const Status check =
SelfCheckSolidFile(candidate, plan.Value(), hooks.reader_close);
if (!check.IsOk()) return check;
if (hooks.before_finalize) hooks.before_finalize(candidate);
if (std::holds_alternative<
std::reference_wrapper<const SolidDynamicAnalysisState>>(view))
Check(math_internal::ArithmeticEnvironmentSupported(),
"Solid RN64 publication arithmetic environment changed.");
const Status finalized =
AtomicFinalizeValidatedCandidate(candidate, output, check);
if (finalized.IsOk()) guard.Release();
return finalized;
} catch (const std::exception& error) {
return OutputFailure("solid-output-failure", error.what());
}
}
} // namespace fesa::hdf5_internal
namespace fesa {
Status SolidHdf5ResultsWriter::Write(
const std::filesystem::path& path, const Domain& domain,
const SolidStaticState& state, const std::vector<Diagnostic>& diagnostics) {
return hdf5_internal::WriteSolidFile(path, domain, std::cref(state),
diagnostics);
}
Status SolidHdf5ResultsWriter::Write(
const std::filesystem::path& path, const Domain& domain,
const SolidFrequencyState& state,
const std::vector<Diagnostic>& diagnostics) {
return hdf5_internal::WriteSolidFile(path, domain, std::cref(state),
diagnostics);
}
Status SolidHdf5ResultsWriter::Write(
const std::filesystem::path& path, const Domain& domain,
const SolidDynamicAnalysisState& state,
const std::vector<Diagnostic>& diagnostics) {
return hdf5_internal::WriteSolidFile(path, domain, std::cref(state),
diagnostics);
}
} // namespace fesa