242 lines
8.1 KiB
C++
242 lines
8.1 KiB
C++
#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
|