feat(beam-reference-qualification): correlate Abaqus beam results
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
add_executable(fesa_cantilever_reference_tests
|
||||
cantilever_reference_test.cpp
|
||||
)
|
||||
|
||||
target_compile_features(
|
||||
fesa_cantilever_reference_tests PRIVATE cxx_std_20
|
||||
)
|
||||
target_compile_options(
|
||||
fesa_cantilever_reference_tests PRIVATE /W4 /permissive- /EHsc
|
||||
)
|
||||
target_compile_definitions(
|
||||
fesa_cantilever_reference_tests
|
||||
PRIVATE
|
||||
FESA_REFERENCE_COMPARE_PATH="$<TARGET_FILE:fesa-reference-compare>"
|
||||
FESA_REPOSITORY_ROOT="${CMAKE_SOURCE_DIR}"
|
||||
FESA_TEST_BINARY_DIR="${CMAKE_BINARY_DIR}"
|
||||
)
|
||||
target_link_libraries(
|
||||
fesa_cantilever_reference_tests
|
||||
PRIVATE
|
||||
fesa_core
|
||||
GTest::gtest_main
|
||||
)
|
||||
add_dependencies(
|
||||
fesa_cantilever_reference_tests
|
||||
fesa-reference-compare
|
||||
)
|
||||
|
||||
add_test(
|
||||
NAME CantileverReference
|
||||
COMMAND "$<TARGET_FILE:fesa_cantilever_reference_tests>"
|
||||
)
|
||||
|
||||
set_property(
|
||||
TEST CantileverReference
|
||||
PROPERTY ENVIRONMENT_MODIFICATION
|
||||
${FESA_DEPENDENCY_RUNTIME_MODIFICATIONS}
|
||||
)
|
||||
@@ -0,0 +1,241 @@
|
||||
#include <fesa/analysis/run_solver.hpp>
|
||||
#include <fesa/io/hdf5/writer.hpp>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
|
||||
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<char>{input},
|
||||
std::istreambuf_iterator<char>{},
|
||||
};
|
||||
}
|
||||
|
||||
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<double, 3>& force,
|
||||
std::array<double, 3>& moment,
|
||||
const fesa::Vec3& position,
|
||||
const std::array<double, 6>& 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<double, 3> total_force{};
|
||||
std::array<double, 3> total_moment{};
|
||||
std::array<double, 3> applied_force{};
|
||||
std::array<double, 3> 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
|
||||
Reference in New Issue
Block a user