#include "fesa/io/hdf5/solid_hdf5_results_writer.h" #include #include #include #include #include #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; using B = std::vector; using D = std::vector; using S = std::vector; 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 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 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::value_type; for (std::size_t row = 0; row < table.rows; ++row) { if constexpr (std::is_same_v) { 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 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& 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(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(nodes.columns[0].values).push_back(i); std::get(nodes.columns[1].values) .push_back(node.source_id.instance_name); std::get(nodes.columns[2].values) .push_back(node.source_id.source_label_text); auto& coords = std::get(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(elements.columns[0].values).push_back(i); std::get(elements.columns[1].values) .push_back(element.SourceId().instance_name); std::get(elements.columns[2].values) .push_back(element.SourceId().source_label_text); std::get(elements.columns[3].values) .push_back(std::string(element.SourceElementType())); std::get(elements.columns[4].values).push_back("FESA-Hex8"); auto& connectivity = std::get(elements.columns[5].values); connectivity.insert(connectivity.end(), element.node_indices.begin(), element.node_indices.end()); std::get(elements.columns[6].values).push_back(element.section_index); std::get(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(&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(materials.columns[0].values).push_back(i); std::get(materials.columns[1].values).push_back(material->Name()); std::get(materials.columns[2].values) .push_back(material->YoungsModulus()); std::get(materials.columns[3].values) .push_back(material->PoissonsRatio()); std::get(materials.columns[4].values).push_back(present ? 1 : 0); std::get(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(sections.columns[0].values).push_back(i); std::get(sections.columns[1].values) .push_back(section.Location().file.generic_u8string()); std::get(sections.columns[2].values).push_back(section.Location().line); std::get(sections.columns[3].values).push_back(section.Name()); std::get(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(points.columns[0].values).push_back(i); std::get(points.columns[1].values) .push_back(static_cast(point.integration_point)); auto& natural = std::get(points.columns[2].values); natural.insert(natural.end(), point.natural_coordinates.begin(), point.natural_coordinates.end()); auto& position = std::get(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 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(table.columns[0].values) .push_back(d.severity == Severity::kWarning ? "warning" : "error"); std::get(table.columns[1].values).push_back(d.code); std::get(table.columns[2].values) .push_back(d.location.file.generic_u8string()); std::get(table.columns[3].values).push_back(d.location.line); std::get(table.columns[4].values).push_back(d.keyword); std::get(table.columns[5].values).push_back(d.entity_identity); std::get(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(column.values)) result = MakeUtf8StringType(); else { hid_t type = std::holds_alternative(column.values) ? (native ? H5T_NATIVE_UINT64 : H5T_STD_U64LE) : std::holds_alternative(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 BuildSolidPlan(const Domain& domain, SolidStateView view, const std::vector& diagnostics) { try { if (std::holds_alternative< std::reference_wrapper>(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>(view); const bool frequency = std::holds_alternative< std::reference_wrapper>(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>( &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(accepted.columns[0].values).push_back(frame.frame_index); std::get(accepted.columns[1].values) .push_back(frame.increment_index); std::get(accepted.columns[2].values).push_back(frame.load_factor); std::get(accepted.columns[3].values) .push_back(frame.iteration_count); std::get(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>( &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>{ {"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(accepted.columns[0].values).push_back(frame.frame_index); std::get(accepted.columns[1].values) .push_back(frame.increment_index); std::get(accepted.columns[2].values).push_back(frame.time); std::get(accepted.columns[3].values) .push_back(frame.iteration_count); std::get(accepted.columns[4].values) .push_back(frame.is_initial ? 1 : 0); } plan.tables.push_back(std::move(accepted)); } else { const auto& state = std::get>(view) .get(); Check(domain.Frequency()->requested_mode_count > 0, "Modal request is invalid."); const std::size_t count = static_cast(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(frequencies.columns[0].values).push_back(mode.mode_id); std::get(frequencies.columns[1].values).push_back(*mode.frequency); } std::get(modes.columns[0].values).push_back(mode.mode_id); std::get(modes.columns[1].values).push_back(mode.eigenvalue); std::get(modes.columns[2].values).push_back(mode.classification); std::get(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 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(pairs.columns[0].values).push_back(pair.sorted_rank); std::get(pairs.columns[1].values).push_back(pair.solver_column); const std::array 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(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::value_type; for (const auto& value : values) { if constexpr (std::is_same_v) Check(std::isfinite(value), "Solid numeric column is nonfinite."); if constexpr (std::is_same_v) 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::Success(std::move(plan)); } catch (const std::exception& error) { return Result::Failure( OutputFailure("invalid-solid-output", error.what())); } } Status WriteSolidFile(const std::filesystem::path& output, const Domain& domain, SolidStateView view, const std::vector& 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>(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 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>(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& 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& 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& diagnostics) { return hdf5_internal::WriteSolidFile(path, domain, std::cref(state), diagnostics); } } // namespace fesa