#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr double kEquilibriumRelativeTolerance = 2.0e-13; constexpr double kEquilibriumAbsoluteTolerance = 1.0e-12; constexpr std::string_view kInstanceName = "PART-1_1-1"; class TemporaryPath final { public: explicit TemporaryPath(std::filesystem::path path) : path_{std::move(path)} { std::error_code error; std::filesystem::create_directories(path_.parent_path(), error); if (error) { throw std::runtime_error{ "Failed to create reference test directory."}; } std::filesystem::remove(path_, error); } ~TemporaryPath() { std::error_code error; std::filesystem::remove(path_, error); } TemporaryPath(const TemporaryPath&) = delete; TemporaryPath& operator=(const TemporaryPath&) = delete; [[nodiscard]] const std::filesystem::path& path() const noexcept { return path_; } private: std::filesystem::path path_; }; std::filesystem::path reference_path(const std::string_view name) { return std::filesystem::path{FESA_REPOSITORY_ROOT} / "reference" / "cantilever beam" / name; } std::filesystem::path test_output_path(const std::string_view name) { return std::filesystem::path{FESA_TEST_BINARY_DIR} / "testing" / name; } std::string quote(const std::filesystem::path& path) { return '"' + path.string() + '"'; } std::string read_text(const std::filesystem::path& path) { std::ifstream input{path, std::ios::binary}; return { std::istreambuf_iterator{input}, std::istreambuf_iterator{}, }; } const fesa::Vec3& coordinates_for( const fesa::Hdf5ModelSnapshot& model, const fesa::NodeId node_id) { const auto found = std::ranges::find_if( model.nodes, [node_id](const fesa::Hdf5NodeSnapshot& node) { return node.id == node_id; }); if (found == model.nodes.end()) { throw std::runtime_error{ "Result or load references an unknown node ID."}; } return found->coordinates; } void add_nodal_resultant( std::array& force, std::array& moment, const fesa::Vec3& position, const std::array& values) { force[0] += values[0]; force[1] += values[1]; force[2] += values[2]; moment[0] += values[3] + position.y * values[2] - position.z * values[1]; moment[1] += values[4] + position.z * values[0] - position.x * values[2]; moment[2] += values[5] + position.x * values[1] - position.y * values[0]; } void expect_finite(const fesa::ResultFrame& frame) { for (const auto& displacement : frame.nodal.displacement) { for (const double value : displacement) { EXPECT_TRUE(std::isfinite(value)); } } for (const auto& reaction : frame.nodal.reaction) { for (const double value : reaction) { EXPECT_TRUE(std::isfinite(value)); } } for (const fesa::BeamElementFrame& beam : frame.element.beams) { EXPECT_TRUE(fesa::is_finite(beam.local_frame.ex)); EXPECT_TRUE(fesa::is_finite(beam.local_frame.ey)); EXPECT_TRUE(fesa::is_finite(beam.local_frame.ez)); for (const fesa::BeamSectionResult& end : beam.end_results) { EXPECT_TRUE(std::isfinite(end.xi)); EXPECT_TRUE(std::isfinite(end.centroid_sigma_xx)); for (const double value : end.section_strain) { EXPECT_TRUE(std::isfinite(value)); } for (const double value : end.section_force) { EXPECT_TRUE(std::isfinite(value)); } for (const double value : end.sigma_xx) { EXPECT_TRUE(std::isfinite(value)); } } } } TEST(CantileverReference, CorrelatesAllAvailableAbaqusResults) { const TemporaryPath results{ test_output_path("cantilever-reference.h5")}; const TemporaryPath standard_output{ test_output_path("cantilever-reference-compare.stdout.txt")}; const TemporaryPath error_output{ test_output_path("cantilever-reference-compare.stderr.txt")}; const fesa::AnalysisRunResult run = fesa::run_solver({ reference_path("cantilever beam fesa.inp"), results.path(), }); ASSERT_TRUE(run.succeeded); ASSERT_TRUE(run.diagnostics.empty()); const fesa::Hdf5ReadResult read = fesa::read_hdf5_results(results.path()); ASSERT_TRUE(read.diagnostics.empty()); ASSERT_TRUE(read.database.has_value()); ASSERT_TRUE(read.model.has_value()); ASSERT_TRUE(read.analysis.has_value()); ASSERT_EQ(read.database->steps.size(), 1U); ASSERT_EQ(read.database->steps[0].frames.size(), 1U); const fesa::ResultFrame& frame = read.database->steps[0].frames[0]; expect_finite(frame); std::array total_force{}; std::array total_moment{}; std::array applied_force{}; std::array applied_moment{}; ASSERT_EQ(frame.nodal.node_ids.size(), frame.nodal.reaction.size()); for (std::size_t index = 0; index < frame.nodal.node_ids.size(); ++index) { add_nodal_resultant( total_force, total_moment, coordinates_for(*read.model, frame.nodal.node_ids[index]), frame.nodal.reaction[index]); } for (const fesa::NodalLoad& load : read.analysis->step.nodal_loads) { const fesa::Vec3& position = coordinates_for(*read.model, load.node); add_nodal_resultant( total_force, total_moment, position, load.values); add_nodal_resultant( applied_force, applied_moment, position, load.values); } for (std::size_t component = 0; component < total_force.size(); ++component) { const double tolerance = std::max( kEquilibriumAbsoluteTolerance, kEquilibriumRelativeTolerance * std::abs(applied_force[component])); EXPECT_NEAR(total_force[component], 0.0, tolerance); } for (std::size_t component = 0; component < total_moment.size(); ++component) { const double tolerance = std::max( kEquilibriumAbsoluteTolerance, kEquilibriumRelativeTolerance * std::abs(applied_moment[component])); EXPECT_NEAR(total_moment[component], 0.0, tolerance); } const std::string command = '"' + quote(std::filesystem::path{FESA_REFERENCE_COMPARE_PATH}) + " --results " + quote(results.path()) + " --instance " + std::string{kInstanceName} + " --displacements " + quote(reference_path("cantilever beam displacements.csv")) + " --reactions " + quote(reference_path("cantilever beam reactions.csv")) + " --internal-forces " + quote(reference_path("cantilever beam elemental forces.csv")) + std::string{" --displacement-absolute-scale 1e-10"} + " --reaction-absolute-scale 1e-8" + " --internal-force-absolute-scale 1e-8" + " 1>" + quote(standard_output.path()) + " 2>" + quote(error_output.path()) + '"'; const int exit_code = std::system(command.c_str()); const std::string standard_text = read_text(standard_output.path()); const std::string error_text = read_text(error_output.path()); EXPECT_EQ(exit_code, 0) << standard_text << error_text; EXPECT_NE( standard_text.find("quantity=displacement"), std::string::npos); EXPECT_NE( standard_text.find("quantity=reaction"), std::string::npos); EXPECT_NE( standard_text.find("quantity=internal_force"), std::string::npos); EXPECT_NE(standard_text.find("rmse="), std::string::npos); EXPECT_NE(standard_text.find("relative_l2="), std::string::npos); } } // namespace