#include "fesa/io/hdf5/solid_hdf5_results_writer.h" #include #include #include #include #include #include #include #include #include "fesa/analysis/linear_frequency_procedure.h" #include "fesa/analysis/linear_static_procedure.h" #include "fesa/analysis/newton_iteration_log.h" #include "fesa/analysis/nonlinear_static_procedure.h" #include "fesa/analysis/prepared_analysis.h" #include "fesa/analysis/solid_frequency_procedure.h" #include "fesa/app/output_paths.h" #include "fesa/assembly/parallel_for.h" #include "fesa/io/hdf5/eigen_hdf5_results_writer.h" #include "fesa/io/hdf5/hdf5_results_writer.h" #include "fesa/io/hdf5/nonlinear_hdf5_results_writer.h" #include "fesa/solvers/linear/mkl_pardiso_solver.h" #include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h" #include "io/hdf5/hdf5_primitives.h" #include "io/hdf5/solid_hdf5_self_check.h" #include "solid_output_test_fixture.h" namespace { using namespace fesa; using namespace fesa::hdf5_internal; struct Actual { PreparedAnalysisInput input; PreparedAnalysisOutcome outcome; }; Actual Execute(const solid_output_test::Files& files) { auto input = PrepareAnalysisInput(files.input); if (!input.HasValue()) throw std::runtime_error("actual input"); SerialParallelFor parallel; MklPardisoSolver linear; MklPardisoTangentSolver tangent; LinearStaticProcedureExecutor oldlinear(parallel, linear); NonlinearStaticProcedureExecutor oldnonlinear(parallel, tangent); LinearFrequencyProcedureExecutor oldfrequency; SolidStaticProcedureExecutor stat(parallel, linear, tangent); SolidFrequencyProcedureExecutor frequency; SolidHhtDynamicProcedureExecutor dynamic(parallel, linear, tangent); NewtonIterationLogWriter log; if (!log.Open(files.log).IsOk()) throw std::runtime_error("log"); auto result = ExecutePreparedAnalysis( input.Value().domain, oldlinear, oldnonlinear, oldfrequency, log, nullptr, nullptr, &stat, &frequency, &dynamic); if (!log.Close().IsOk() || !result.HasValue()) throw std::runtime_error("actual execute"); return {std::move(input.Value()), std::move(result.Value())}; } SolidStateView View(const Actual& actual) { if (const auto* value = std::get_if(&actual.outcome)) return std::cref(value->state); if (const auto* value = std::get_if(&actual.outcome)) return std::cref(*value); return std::cref(std::get(actual.outcome).state); } std::vector Bytes(const std::filesystem::path& path) { std::ifstream stream(path, std::ios::binary); return {std::istreambuf_iterator(stream), {}}; } void Existing(const std::filesystem::path& path) { std::ofstream out(path, std::ios::binary); out << "previous final preserved"; } std::string Attr(hid_t object, const char* name) { Hdf5Handle a{H5Aopen(object, name, H5P_DEFAULT), H5Aclose}; Hdf5Handle type{H5Aget_type(a.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* pointer = nullptr; EXPECT_GE(H5Aread(a.Get(), type.Get(), &pointer), 0); std::string value = pointer ? pointer : ""; H5free_memory(pointer); return value; } void Compound(hid_t file, const char* path, const std::vector& names, const std::vector& kinds, const std::vector& widths = {}) { Hdf5Handle dataset{H5Dopen2(file, path, H5P_DEFAULT), H5Dclose}; ASSERT_GE(dataset.Get(), 0); Hdf5Handle type{H5Dget_type(dataset.Get()), H5Tclose}; ASSERT_EQ(H5Tget_class(type.Get()), H5T_COMPOUND); ASSERT_EQ(H5Tget_nmembers(type.Get()), static_cast(names.size())); for (std::size_t i = 0; i < names.size(); ++i) { char* name = H5Tget_member_name(type.Get(), static_cast(i)); ASSERT_NE(name, nullptr); EXPECT_EQ(name, names[i]); H5free_memory(name); Hdf5Handle field{H5Tget_member_type(type.Get(), static_cast(i)), H5Tclose}; if (!widths.empty() && widths[i] > 1) { ASSERT_EQ(H5Tget_class(field.Get()), H5T_ARRAY); hsize_t width = 0; ASSERT_EQ(H5Tget_array_dims2(field.Get(), &width), 1); EXPECT_EQ(width, widths[i]); field = Hdf5Handle{H5Tget_super(field.Get()), H5Tclose}; } if (kinds[i] == 's') { EXPECT_EQ(H5Tget_class(field.Get()), H5T_STRING); EXPECT_GT(H5Tis_variable_str(field.Get()), 0); EXPECT_EQ(H5Tget_cset(field.Get()), H5T_CSET_UTF8); } else EXPECT_GT(H5Tequal(field.Get(), kinds[i] == 'u' ? H5T_STD_U64LE : kinds[i] == 'b' ? H5T_STD_U8LE : H5T_IEEE_F64LE), 0); } } template T NumberAttr(hid_t object, const char* name, hid_t file_type, hid_t native_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); T value{}; EXPECT_GE(H5Aread(attribute.Get(), native_type, &value), 0); return value; } void DatasetContract(hid_t file, const std::string& path, const std::vector& expected, const char* components, const char* units, const char* location) { Hdf5Handle dataset{H5Dopen2(file, path.c_str(), H5P_DEFAULT), H5Dclose}; ASSERT_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}; ASSERT_EQ(H5Sget_simple_extent_ndims(space.Get()), static_cast(expected.size())); std::vector shape(expected.size()); H5Sget_simple_extent_dims(space.Get(), shape.data(), nullptr); EXPECT_EQ(shape, expected); EXPECT_EQ(Attr(dataset.Get(), "component_names"), components); EXPECT_EQ(Attr(dataset.Get(), "unit_dimensions"), units); EXPECT_EQ(Attr(dataset.Get(), "location"), location); EXPECT_EQ(Attr(dataset.Get(), "coordinate_system"), "global-cartesian"); const auto marker = path.find("/frames/"); if (marker != std::string::npos) { const auto index = static_cast(std::stoull(path.substr(marker + 8))); Hdf5Handle metadata{H5Gopen2(file, "/metadata", H5P_DEFAULT), H5Gclose}; const auto procedure = Attr(metadata.Get(), "analysis_procedure"); const bool dynamic = procedure.find("hht-") == 0; const bool nonlinear = procedure == "nonlinear-static-full-newton" || procedure == "hht-nonlinear-dynamics"; EXPECT_EQ(Attr(dataset.Get(), "step_name"), "Step-1"); EXPECT_EQ(NumberAttr(dataset.Get(), "frame_index", H5T_STD_U64LE, H5T_NATIVE_UINT64), index); EXPECT_EQ(NumberAttr(dataset.Get(), "increment_index", H5T_STD_U64LE, H5T_NATIVE_UINT64), dynamic ? index : procedure == "linear-static" ? 1 : index + 1); EXPECT_EQ( NumberAttr(dataset.Get(), dynamic ? "time" : "load_factor", H5T_IEEE_F64LE, H5T_NATIVE_DOUBLE), dynamic ? static_cast(index) * .1 : procedure == "linear-static" ? 1 : static_cast(index + 1) * .5); if (path.find("/solid/stress") != std::string::npos) EXPECT_EQ(Attr(dataset.Get(), "stress_measure"), "cauchy"); if (path.find("/solid/strain") != std::string::npos) { EXPECT_EQ(Attr(dataset.Get(), "strain_measure"), nonlinear ? "green-lagrange" : "infinitesimal"); EXPECT_EQ(Attr(dataset.Get(), "shear_convention"), "tensor"); } if (path.find("/nodal/acceleration") != std::string::npos) EXPECT_EQ(Attr(dataset.Get(), "acceleration_kind"), "physical"); if (path.find("/nodal/algorithmic_acceleration") != std::string::npos) EXPECT_EQ(Attr(dataset.Get(), "acceleration_kind"), "algorithmic"); if (path.find("/global/strain_energy") != std::string::npos) EXPECT_EQ(Attr(dataset.Get(), "definition"), nonlinear ? "sum(W*detJ0)" : "0.5*u^T*K0*u"); if (path.find("/global/kinetic_energy") != std::string::npos) EXPECT_EQ(Attr(dataset.Get(), "definition"), "0.5*v^T*M*v"); if (path.find("/global/external_work") != std::string::npos) EXPECT_EQ(Attr(dataset.Get(), "definition"), "sum(0.5*(F_old+F_new)*delta_u)"); if (path.find("/global/energy_balance_error") != std::string::npos) EXPECT_EQ(Attr(dataset.Get(), "definition"), "T+U-T0-U0-Wext"); if (path.find("/global/angular_momentum") != std::string::npos) EXPECT_EQ(Attr(dataset.Get(), "origin"), "global0"); } } TEST(SolidHdf5ResultsWriter, AllFiveActualApiOutcomesHaveIndependentOfficialSchema) { for (unsigned p = 0; p < 5; ++p) { SCOPED_TRACE(p); solid_output_test::Files files(p); auto actual = Execute(files); ASSERT_TRUE(WriteSolidFile(files.output, actual.input.domain, View(actual), actual.input.diagnostics) .IsOk()); Hdf5Handle file{ H5Fopen(files.output.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(Attr(metadata.Get(), "analysis_procedure"), solid_output_test::Procedure(p)); EXPECT_EQ(Attr(metadata.Get(), "feature_id"), "hex8-structural-analysis"); EXPECT_EQ(Attr(metadata.Get(), "source_step_name"), "RawSolid"); EXPECT_EQ(Attr(metadata.Get(), "element_formulation"), "FESA-Hex8"); EXPECT_EQ(Attr(metadata.Get(), "integration_rule"), "gauss-2x2x2"); EXPECT_EQ(Attr(metadata.Get(), "mass_formulation"), "consistent-reference"); EXPECT_EQ(Attr(metadata.Get(), "component_order"), "node-major; UX,UY,UZ"); Compound( file.Get(), "/model/nodes", {"internal_node_id", "instance_name", "source_label", "coordinates"}, {'u', 's', 's', 'd'}, {1, 1, 1, 3}); Compound( file.Get(), "/model/elements", {"internal_element_id", "instance_name", "source_label", "source_element_type", "internal_formulation", "node_internal_ids", "solid_section_internal_id", "material_internal_id"}, {'u', 's', 's', 's', 's', 'u', 'u', 'u'}, {1, 1, 1, 1, 1, 8, 1, 1}); Compound(file.Get(), "/model/solid/materials", {"internal_material_id", "name", "youngs_modulus", "poisson_ratio", "density_present", "density"}, {'u', 's', 'd', 'd', 'b', 'd'}); Compound(file.Get(), "/model/solid/sections", {"internal_section_id", "source_file", "source_line", "source_elset", "material_internal_id"}, {'u', 's', 'u', 's', 'u'}); Compound(file.Get(), "/model/solid_integration_points", {"internal_element_id", "point_id", "natural_coordinates", "reference_coordinates"}, {'u', 'b', 'd', 'd'}, {1, 1, 3, 3}); Compound(file.Get(), "/diagnostics", {"severity", "code", "file", "line", "keyword", "entity_identity", "message"}, {'s', 's', 's', 'u', 's', 's', 's'}); Hdf5Handle mask{ H5Dopen2(file.Get(), "/model/nodal_constraint_mask", H5P_DEFAULT), H5Dclose}; Hdf5Handle masktype{H5Dget_type(mask.Get()), H5Tclose}; EXPECT_GT(H5Tequal(masktype.Get(), H5T_STD_U8LE), 0); EXPECT_EQ(Attr(mask.Get(), "component_names"), "UX,UY,UZ"); if (p == 2) { EXPECT_EQ(H5Lexists(file.Get(), "/steps/Step-1/frames", H5P_DEFAULT), 0); Compound(file.Get(), "/steps/Step-1/eigen/modes", {"mode_id", "eigenvalue", "classification", "cluster_id"}, {'u', 'd', 'b', 'u'}); Compound(file.Get(), "/steps/Step-1/eigen/frequencies", {"mode_id", "frequency"}, {'u', 'd'}); Compound(file.Get(), "/steps/Step-1/eigen/verification/pairs", {"sorted_rank", "solver_column", "eigenvalue", "modal_mass", "rayleigh_quotient", "eta_r", "eta_norm", "eta_energy"}, {'u', 'u', 'd', 'd', 'd', 'd', 'd', 'd'}); DatasetContract(file.Get(), "/steps/Step-1/eigen/vectors", {1, 8, 3}, "UX,UY,UZ", "mass-normalized", "nodal"); for (const auto& quantity : std::vector>{ {"modes", "eigenvalue", "1/time^2"}, {"frequencies", "frequency", "1/time"}}) { const auto path = std::string("/steps/Step-1/eigen/") + std::get<0>(quantity); Hdf5Handle dataset{H5Dopen2(file.Get(), path.c_str(), H5P_DEFAULT), H5Dclose}; EXPECT_EQ(Attr(dataset.Get(), "component_names"), std::get<1>(quantity)); EXPECT_EQ(Attr(dataset.Get(), "unit_dimensions"), std::get<2>(quantity)); EXPECT_EQ(Attr(dataset.Get(), "location"), "global"); EXPECT_EQ(Attr(dataset.Get(), "coordinate_system"), "global-cartesian"); EXPECT_EQ(H5Aexists(dataset.Get(), "frame_index"), 0); EXPECT_EQ(H5Aexists(dataset.Get(), "step_name"), 0); } const auto& state = std::get(actual.outcome); ASSERT_EQ(state.eigenpair_checks[0].solver_column, 0u); } else { const unsigned frames = p == 0 ? 1 : p == 1 ? 2 : 3; if (p == 1) Compound(file.Get(), "/steps/Step-1/accepted_frames", {"frame_index", "increment_index", "load_factor", "iteration_count", "is_factor_one"}, {'u', 'u', 'd', 'u', 'b'}); if (p >= 3) Compound(file.Get(), "/steps/Step-1/accepted_frames", {"frame_index", "increment_index", "time", "iteration_count", "is_initial"}, {'u', 'u', 'd', 'u', 'b'}); if (p == 0) EXPECT_EQ( H5Lexists(file.Get(), "/steps/Step-1/accepted_frames", H5P_DEFAULT), 0); for (unsigned i = 0; i < frames; ++i) { const auto root = "/steps/Step-1/frames/" + std::to_string(i); DatasetContract(file.Get(), root + "/nodal/displacement", {8, 3}, "U1,U2,U3", "length", "nodal"); DatasetContract(file.Get(), root + "/nodal/reaction", {8, 3}, "RF1,RF2,RF3", "force", "nodal"); DatasetContract(file.Get(), root + "/nodal/residual", {8, 3}, "R1,R2,R3", "force", "nodal"); DatasetContract(file.Get(), root + "/solid/stress", {1, 8, 6}, "S11,S22,S33,S12,S13,S23", "force/length^2", "integration-point"); DatasetContract(file.Get(), root + "/solid/strain", {1, 8, 6}, "E11,E22,E33,E12,E13,E23", "dimensionless", "integration-point"); DatasetContract(file.Get(), root + "/global/strain_energy", {}, "scalar", "force*length", "global"); if (p >= 3) { DatasetContract(file.Get(), root + "/nodal/velocity", {8, 3}, "V1,V2,V3", "length/time", "nodal"); DatasetContract(file.Get(), root + "/nodal/acceleration", {8, 3}, "A1,A2,A3", "length/time^2", "nodal"); DatasetContract(file.Get(), root + "/nodal/algorithmic_acceleration", {8, 3}, "A1,A2,A3", "length/time^2", "nodal"); for (const auto* energy : {"kinetic_energy", "external_work", "energy_balance_error"}) DatasetContract(file.Get(), root + "/global/" + energy, {}, "scalar", "force*length", "global"); DatasetContract(file.Get(), root + "/global/linear_momentum", {3}, "P1,P2,P3", "mass*length/time", "global"); DatasetContract(file.Get(), root + "/global/angular_momentum", {3}, "L1,L2,L3", "mass*length^2/time", "global"); EXPECT_LE( H5Lexists(file.Get(), (root + "/nodal/displacement_low").c_str(), H5P_DEFAULT), 0); } } } } } TEST(SolidHdf5ResultsWriter, EveryProcedurePreservesPreviousFinalAcrossLifecycleFailures) { for (unsigned p = 0; p < 5; ++p) { solid_output_test::Files files(p); auto actual = Execute(files); Existing(files.output); const auto previous = Bytes(files.output); for (unsigned failure = 0; failure < 5; ++failure) { SCOPED_TRACE(p); SCOPED_TRACE(failure); SolidFileHooks hooks; if (failure == 0) hooks.before_write = [](hid_t) { throw std::runtime_error("injected write"); }; if (failure == 1) hooks.candidate_close = [](hid_t id) -> herr_t { H5Fclose(id); return -1; }; if (failure == 2) hooks.before_self_check = [](const auto& path) { Hdf5Handle file{ H5Fopen(path.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose}; H5Ldelete(file.Get(), "/model/elements", H5P_DEFAULT); }; if (failure == 3) hooks.reader_close = [](hid_t id) -> herr_t { H5Fclose(id); return -1; }; if (failure == 4) hooks.before_finalize = [](const auto& path) { std::filesystem::remove(path); }; const auto status = WriteSolidFile(files.output, actual.input.domain, View(actual), actual.input.diagnostics, hooks); EXPECT_FALSE(status.IsOk()); EXPECT_EQ(Bytes(files.output), previous); for (const auto& entry : std::filesystem::directory_iterator(files.folder)) EXPECT_EQ(entry.path().filename().string().find("candidate"), std::string::npos); } } } TEST(SolidHdf5ResultsWriter, MalformedWholePairsAndIdentityFailBeforeCandidateCreation) { solid_output_test::Files files(3); auto actual = Execute(files); Existing(files.output); const auto previous = Bytes(files.output); auto& state = std::get(actual.outcome).state; auto& frame = const_cast( state.AcceptedFrames().back()); const auto original = frame; for (unsigned malformed = 0; malformed < 5; ++malformed) { SCOPED_TRACE(malformed); frame = original; if (malformed == 0) frame.low.displacement = Vector{1}; if (malformed == 1) frame.low.displacement[21] = std::numeric_limits::quiet_NaN(); if (malformed == 2) frame.low.displacement[21] = frame.displacement[21]; if (malformed == 3) frame.nodal_source_ids[0].source_label_text = "wrong"; if (malformed == 4) frame.low.residual[21] = 1; EXPECT_FALSE(WriteSolidFile(files.output, actual.input.domain, View(actual), actual.input.diagnostics) .IsOk()); EXPECT_EQ(Bytes(files.output), previous); } frame = original; const int rounding = std::fegetround(); ASSERT_EQ(std::fesetround(FE_DOWNWARD), 0); const auto status = WriteSolidFile(files.output, actual.input.domain, View(actual), actual.input.diagnostics); std::fesetround(rounding); EXPECT_FALSE(status.IsOk()); EXPECT_EQ(Bytes(files.output), previous); } TEST(SolidHdf5ResultsWriter, ModalColumnsRemainZeroBasedAndRejectInvalidInventory) { solid_output_test::Files files(2); auto actual = Execute(files); auto& state = std::get(actual.outcome); ASSERT_EQ(state.eigenpair_checks[0].solver_column, 0u); Existing(files.output); const auto previous = Bytes(files.output); const auto original = state; state.eigenpair_checks[0].solver_column = state.eigenpair_checks.size(); EXPECT_FALSE(WriteSolidFile(files.output, actual.input.domain, View(actual), actual.input.diagnostics) .IsOk()); EXPECT_EQ(Bytes(files.output), previous); state = original; state.backend.column_ids[0] = state.eigenpair_checks.size(); state.eigenpair_checks[0].solver_column = state.backend.column_ids[0]; EXPECT_FALSE(WriteSolidFile(files.output, actual.input.domain, View(actual), actual.input.diagnostics) .IsOk()); } TEST(SolidHdf5ResultsWriter, ReadbackRejectsMalformedTypesShapesValuesAndSourceIds) { solid_output_test::Files files(3); auto actual = Execute(files); Existing(files.output); const auto previous = Bytes(files.output); for (unsigned malformed = 0; malformed < 9; ++malformed) { SCOPED_TRACE(malformed); SolidFileHooks hooks; hooks.before_self_check = [&](const auto& path) { Hdf5Handle file{H5Fopen(path.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose}; const char* result = "/steps/Step-1/frames/2/nodal/displacement"; if (malformed == 0) H5Ldelete(file.Get(), result, H5P_DEFAULT); if (malformed == 1) { Hdf5Handle dataset{H5Dopen2(file.Get(), result, H5P_DEFAULT), H5Dclose}; std::vector values(24, 0); values[0] = std::numeric_limits::quiet_NaN(); H5Dwrite(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data()); } if (malformed == 2 || malformed == 3) { H5Ldelete(file.Get(), result, H5P_DEFAULT); hsize_t shape[]{malformed == 2 ? 1u : 8u, 3}; Hdf5Handle space{H5Screate_simple(2, shape, nullptr), H5Sclose}; Hdf5Handle data{ H5Dcreate2(file.Get(), result, malformed == 2 ? H5T_IEEE_F64LE : H5T_IEEE_F32LE, space.Get(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT), H5Dclose}; } if (malformed == 4) { Hdf5Handle dataset{ H5Dopen2(file.Get(), "/model/solid_integration_points", H5P_DEFAULT), H5Dclose}; Hdf5Handle memory{H5Tcreate(H5T_COMPOUND, 1), H5Tclose}; H5Tinsert(memory.Get(), "point_id", 0, H5T_NATIVE_UINT8); std::vector values(8, 99); H5Dwrite(dataset.Get(), memory.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data()); } if (malformed == 5) { Hdf5Handle dataset{H5Dopen2(file.Get(), "/model/elements", H5P_DEFAULT), H5Dclose}; Hdf5Handle memory{H5Tcreate(H5T_COMPOUND, sizeof(std::uint64_t)), H5Tclose}; H5Tinsert(memory.Get(), "internal_element_id", 0, H5T_NATIVE_UINT64); const std::uint64_t value = 99; H5Dwrite(dataset.Get(), memory.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, &value); } if (malformed == 6) { Hdf5Handle dataset{H5Dopen2(file.Get(), result, H5P_DEFAULT), H5Dclose}; H5Adelete(dataset.Get(), "unit_dimensions"); WriteStringAttribute(dataset.Get(), "unit_dimensions", "wrong"); } if (malformed == 7) { Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose}; H5Adelete(metadata.Get(), "analysis_procedure"); Hdf5Handle type{H5Tcopy(H5T_C_S1), H5Tclose}; H5Tset_size(type.Get(), 16); Hdf5Handle space{H5Screate(H5S_SCALAR), H5Sclose}; Hdf5Handle attr{ H5Acreate2(metadata.Get(), "analysis_procedure", type.Get(), space.Get(), H5P_DEFAULT, H5P_DEFAULT), H5Aclose}; } if (malformed == 8) { H5Ldelete(file.Get(), "/model/elements", H5P_DEFAULT); Hdf5Handle type{H5Tcreate(H5T_COMPOUND, 16), H5Tclose}; H5Tinsert(type.Get(), "material_internal_id", 0, H5T_STD_U64LE); H5Tinsert(type.Get(), "internal_element_id", 8, H5T_STD_U64LE); hsize_t one = 1; Hdf5Handle space{H5Screate_simple(1, &one, nullptr), H5Sclose}; Hdf5Handle dataset{ H5Dcreate2(file.Get(), "/model/elements", type.Get(), space.Get(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT), H5Dclose}; } }; EXPECT_FALSE(WriteSolidFile(files.output, actual.input.domain, View(actual), actual.input.diagnostics, hooks) .IsOk()); EXPECT_EQ(Bytes(files.output), previous); } } TEST(SolidHdf5ResultsWriter, ReadbackMatchesActualAcceptedValuesAndFullPairObservables) { for (unsigned p = 0; p < 5; ++p) { SCOPED_TRACE(p); solid_output_test::Files files(p); auto actual = Execute(files); ASSERT_TRUE(WriteSolidFile(files.output, actual.input.domain, View(actual), actual.input.diagnostics) .IsOk()); Hdf5Handle file{ H5Fopen(files.output.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT), H5Fclose}; const auto compare = [&](const std::string& path, const std::vector& expected) { Hdf5Handle dataset{H5Dopen2(file.Get(), path.c_str(), H5P_DEFAULT), H5Dclose}; ASSERT_GE(dataset.Get(), 0); std::vector values(expected.size()); ASSERT_GE(H5Dread(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data()), 0); EXPECT_EQ(values, expected); }; const auto vector = [](const Vector& input) { std::vector values; for (std::size_t i = 0; i < input.Size(); ++i) values.push_back(input[i]); return values; }; Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose}; EXPECT_EQ(Attr(metadata.Get(), "source_input_identity"), SourceInputIdentity(actual.input.domain)); if (p == 2) { const auto& state = std::get(actual.outcome); std::vector values; for (std::size_t i = 0; i < state.vectors.Rows(); ++i) for (std::size_t j = 0; j < state.vectors.Columns(); ++j) values.push_back(state.vectors(i, j)); compare("/steps/Step-1/eigen/vectors", values); } else { const auto frame = [&](const auto& accepted) { const auto root = "/steps/Step-1/frames/" + std::to_string(accepted.frame_index); compare(root + "/nodal/displacement", vector(accepted.displacement)); compare(root + "/nodal/reaction", vector(accepted.reaction)); compare(root + "/nodal/residual", vector(accepted.residual)); std::vector stress, strain; for (const auto& point : accepted.solid_results) { stress.insert(stress.end(), point.cauchy_stress.begin(), point.cauchy_stress.end()); strain.insert(strain.end(), point.strain.begin(), point.strain.end()); } compare(root + "/solid/stress", stress); compare(root + "/solid/strain", strain); }; if (p < 2) for (const auto& accepted : std::get(actual.outcome) .state.AcceptedFrames()) frame(accepted); else { bool nonzero_low = false; for (const auto& accepted : std::get(actual.outcome) .state.AcceptedFrames()) { frame(accepted); const auto root = "/steps/Step-1/frames/" + std::to_string(accepted.frame_index); compare(root + "/nodal/velocity", vector(accepted.velocity)); compare(root + "/nodal/acceleration", vector(accepted.acceleration)); compare(root + "/nodal/algorithmic_acceleration", vector(accepted.algorithmic_acceleration)); compare(root + "/global/strain_energy", {accepted.strain_energy}); compare(root + "/global/kinetic_energy", {accepted.kinetic_energy}); compare(root + "/global/external_work", {accepted.external_work}); compare(root + "/global/energy_balance_error", {accepted.energy_balance_error}); for (std::size_t i = 0; i < accepted.low.displacement.Size(); ++i) nonzero_low = nonzero_low || accepted.low.displacement[i] != 0; } EXPECT_TRUE(nonzero_low); } } } } class FailedTangent final : public TangentSolver { public: Status FactorizeFresh(const SparseMatrix&) override { return Status::Failure(FailureCategory::kSolver, {{Severity::kError, "injected-tangent-failure", {}, "", {}, "test backend failed"}}); } Status SolveCorrection(const Vector&, Vector&) const override { return Status::Failure(FailureCategory::kSolver, {}); } }; TEST(SolidPreparedAnalysis, FailureRecordsAreLoggedAndLogFailureTakesPrecedence) { for (unsigned p : {1u, 3u, 4u}) { solid_output_test::Files files(p); auto input = PrepareAnalysisInput(files.input); ASSERT_TRUE(input.HasValue()); SerialParallelFor parallel; MklPardisoSolver linear; FailedTangent tangent; LinearStaticProcedureExecutor oldlinear(parallel, linear); NonlinearStaticProcedureExecutor oldnonlinear(parallel, tangent); LinearFrequencyProcedureExecutor oldfrequency; SolidStaticProcedureExecutor stat(parallel, linear, tangent); SolidFrequencyProcedureExecutor frequency; SolidHhtDynamicProcedureExecutor dynamic(parallel, linear, tangent); for (bool closed : {false, true}) { SCOPED_TRACE(p); SCOPED_TRACE(closed); NewtonIterationLogWriter log; ASSERT_TRUE(log.Open(files.log).IsOk()); if (closed) ASSERT_TRUE(log.Close().IsOk()); const auto result = ExecutePreparedAnalysis( input.Value().domain, oldlinear, oldnonlinear, oldfrequency, log, nullptr, nullptr, &stat, &frequency, &dynamic); ASSERT_FALSE(result.HasValue()); EXPECT_EQ(result.GetStatus().Category(), closed ? FailureCategory::kOutput : FailureCategory::kSolver); if (!closed) { ASSERT_TRUE(log.Close().IsOk()); const auto bytes = Bytes(files.log); const std::string text(bytes.begin(), bytes.end()); EXPECT_NE(text.find("injected-tangent-failure"), std::string::npos); EXPECT_EQ(text.find("rotation"), std::string::npos); EXPECT_NE(text.find("stage="), std::string::npos); } } } } TEST(SolidPreparedAnalysis, MissingSolidAdapterFailsWithoutLegacyDispatch) { solid_output_test::Files files(0); auto input = PrepareAnalysisInput(files.input); ASSERT_TRUE(input.HasValue()); SerialParallelFor parallel; MklPardisoSolver linear; MklPardisoTangentSolver tangent; LinearStaticProcedureExecutor oldlinear(parallel, linear); NonlinearStaticProcedureExecutor oldnonlinear(parallel, tangent); LinearFrequencyProcedureExecutor oldfrequency; NewtonIterationLogWriter log; ASSERT_TRUE(log.Open(files.log).IsOk()); const auto result = ExecutePreparedAnalysis(input.Value().domain, oldlinear, oldnonlinear, oldfrequency, log); ASSERT_FALSE(result.HasValue()); ASSERT_FALSE(result.GetStatus().Diagnostics().empty()); EXPECT_EQ(result.GetStatus().Diagnostics()[0].code, "missing-solid-executor"); EXPECT_TRUE(log.Close().IsOk()); } TEST(SolidHdf5ResultsWriter, UnsupportedEnvironmentAfterCallbackCannotPublish) { solid_output_test::Files files(3); auto actual = Execute(files); Existing(files.output); const auto previous = Bytes(files.output); for (unsigned callback = 0; callback < 2; ++callback) { for (unsigned control : {0x8000u, 0x40u}) { SCOPED_TRACE(callback); SCOPED_TRACE(control); struct Restore { unsigned original = _mm_getcsr(); ~Restore() { _mm_setcsr(original); } } restore; SolidFileHooks hooks; if (callback == 0) hooks.before_write = [&](hid_t) { _mm_setcsr(restore.original | control); }; else hooks.before_finalize = [&](const auto&) { _mm_setcsr(restore.original | control); }; EXPECT_FALSE(WriteSolidFile(files.output, actual.input.domain, View(actual), actual.input.diagnostics, hooks) .IsOk()); EXPECT_EQ(Bytes(files.output), previous); } } } TEST(SolidPreparedAnalysis, MissingWriterPreservesFinalForEveryTypedOutcome) { for (unsigned p = 0; p < 5; ++p) { solid_output_test::Files files(p); auto actual = Execute(files); Existing(files.output); const auto previous = Bytes(files.output); const auto paths = DeriveSingleInputOutputPaths({files.input.string()}, files.folder); ASSERT_TRUE(paths.HasValue()); Hdf5ResultsWriter linear; NonlinearHdf5ResultsWriter nonlinear; EigenHdf5ResultsWriter eigen; const auto status = FinalizePreparedAnalysisResult( paths.Value(), actual.input.domain, actual.outcome, actual.input.diagnostics, linear, nonlinear, eigen); ASSERT_FALSE(status.IsOk()); ASSERT_FALSE(status.Diagnostics().empty()); EXPECT_EQ(status.Diagnostics()[0].code, "missing-solid-writer"); EXPECT_EQ(Bytes(files.output), previous); } } TEST(SolidNewtonIterationLog, InvalidScalarEvidencePermanentlyFailsLogLifecycle) { for (unsigned failure = 0; failure < 3; ++failure) { solid_output_test::Files files(3); NewtonIterationLogWriter log; ASSERT_TRUE(log.Open(files.log).IsOk()); SolidDynamicIterationRecord record; record.stage = "failed"; record.outcome = "failed"; if (failure == 0) record.evidence.correction_metrics[2] = std::numeric_limits::infinity(); if (failure == 1) record.evidence.residual_scale = -1; if (failure == 2) record.evidence.worst_weighted_terms[0] = std::numeric_limits::quiet_NaN(); EXPECT_FALSE(log.Write(record).IsOk()); EXPECT_FALSE(log.Close().IsOk()); } } TEST(SolidHdf5ResultsWriter, AbsentStaticDensityIsAnExplicitPlaceholder) { solid_output_test::Files files(0); auto deck = solid_output_test::Deck(0); const auto position = deck.find("*Density\n2\n"); ASSERT_NE(position, std::string::npos); deck.erase(position, std::string("*Density\n2\n").size()); { std::ofstream out(files.input); out << deck; } auto actual = Execute(files); ASSERT_TRUE(WriteSolidFile(files.output, actual.input.domain, View(actual), actual.input.diagnostics) .IsOk()); Hdf5Handle file{ H5Fopen(files.output.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT), H5Fclose}; Hdf5Handle dataset{ H5Dopen2(file.Get(), "/model/solid/materials", H5P_DEFAULT), H5Dclose}; struct Density { std::uint8_t present; double rho; } row{}; Hdf5Handle type{H5Tcreate(H5T_COMPOUND, sizeof(row)), H5Tclose}; H5Tinsert(type.Get(), "density_present", HOFFSET(Density, present), H5T_NATIVE_UINT8); H5Tinsert(type.Get(), "density", HOFFSET(Density, rho), H5T_NATIVE_DOUBLE); ASSERT_GE( H5Dread(dataset.Get(), type.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, &row), 0); EXPECT_EQ(row.present, 0); EXPECT_EQ(row.rho, 0); } TEST(SolidHdf5ResultsWriter, ModalPhysicalTableAttributeCorruptionPreservesFinal) { solid_output_test::Files files(2); auto actual = Execute(files); Existing(files.output); const auto previous = Bytes(files.output); for (const auto* quantity : {"modes", "frequencies"}) { SolidFileHooks hooks; hooks.before_self_check = [&](const auto& path) { Hdf5Handle file{H5Fopen(path.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose}; const auto dataset_path = std::string("/steps/Step-1/eigen/") + quantity; Hdf5Handle dataset{ H5Dopen2(file.Get(), dataset_path.c_str(), H5P_DEFAULT), H5Dclose}; H5Adelete(dataset.Get(), "unit_dimensions"); WriteStringAttribute(dataset.Get(), "unit_dimensions", "wrong"); }; EXPECT_FALSE(WriteSolidFile(files.output, actual.input.domain, View(actual), actual.input.diagnostics, hooks) .IsOk()); EXPECT_EQ(Bytes(files.output), previous); } } TEST(SolidHdf5ResultsWriter, UnselectedBackendPairsCannotPublishInvalidSpectrumOrDimension) { solid_output_test::Files files(2); auto deck = solid_output_test::Deck(2); const auto position = deck.find("Tip,2,3\n"); ASSERT_NE(position, std::string::npos); deck.erase(position, std::string("Tip,2,3\n").size()); { std::ofstream out(files.input); out << deck; } auto actual = Execute(files); auto& state = std::get(actual.outcome); ASSERT_EQ(state.modes.size(), 1u); ASSERT_EQ(state.free_dof_count, 3u); ASSERT_GT(state.eigenpair_checks.size(), state.modes.size()); ASSERT_LE(state.eigenpair_checks.size(), state.free_dof_count); const auto original = state; Existing(files.output); const auto previous = Bytes(files.output); for (unsigned failure = 0; failure < 3; ++failure) { SCOPED_TRACE(failure); state = original; if (failure == 0) state.eigenpair_checks[1].eigenvalue = -1e-6 * state.classification_scale; if (failure == 1) state.eigenpair_checks[1].eigenvalue = state.eigenpair_checks[0].eigenvalue * .5; if (failure < 2) state.backend.eigenvalues[1] = state.eigenpair_checks[1].eigenvalue; if (failure == 2) { while (state.eigenpair_checks.size() <= state.free_dof_count) { auto pair = state.eigenpair_checks.back(); pair.sorted_rank = state.eigenpair_checks.size() + 1; pair.solver_column = state.eigenpair_checks.size(); state.eigenpair_checks.push_back(pair); state.backend.eigenvalues.push_back(pair.eigenvalue); state.backend.column_ids.push_back(pair.solver_column); } } const auto status = WriteSolidFile(files.output, actual.input.domain, View(actual), actual.input.diagnostics); ASSERT_FALSE(status.IsOk()); ASSERT_FALSE(status.Diagnostics().empty()); const auto& message = status.Diagnostics()[0].message; EXPECT_NE(message.find(failure == 0 ? "Significant negative" : failure == 1 ? "not sorted" : "inventory"), std::string::npos); EXPECT_EQ(Bytes(files.output), previous); } } } // namespace