Files
FESA/tests/unit/fem/beam_frame_test.cpp

279 lines
9.0 KiB
C++

#include <fesa/fem/beam_frame.hpp>
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <string_view>
#include <gtest/gtest.h>
namespace {
constexpr double kUnitTolerance =
128.0 * std::numeric_limits<double>::epsilon();
double dot(const fesa::Vec3 first, const fesa::Vec3 second) {
return first.x * second.x + first.y * second.y + first.z * second.z;
}
fesa::Vec3 cross(const fesa::Vec3 first, const fesa::Vec3 second) {
return {
first.y * second.z - first.z * second.y,
first.z * second.x - first.x * second.z,
first.x * second.y - first.y * second.x,
};
}
fesa::Vec3 rotate_about_axis(
const fesa::Vec3 value,
const fesa::Vec3 unit_axis,
const double angle) {
const double cosine = std::cos(angle);
const double sine = std::sin(angle);
const fesa::Vec3 axis_cross_value = cross(unit_axis, value);
const double axis_projection = dot(unit_axis, value);
return {
value.x * cosine + axis_cross_value.x * sine +
unit_axis.x * axis_projection * (1.0 - cosine),
value.y * cosine + axis_cross_value.y * sine +
unit_axis.y * axis_projection * (1.0 - cosine),
value.z * cosine + axis_cross_value.z * sine +
unit_axis.z * axis_projection * (1.0 - cosine),
};
}
void expect_vec_near(
const fesa::Vec3 actual,
const fesa::Vec3 expected,
const double tolerance = kUnitTolerance) {
EXPECT_NEAR(actual.x, expected.x, tolerance);
EXPECT_NEAR(actual.y, expected.y, tolerance);
EXPECT_NEAR(actual.z, expected.z, tolerance);
}
void expect_error(
const fesa::BeamFrameResult& result,
const std::string_view code) {
EXPECT_FALSE(result.frame.has_value());
ASSERT_EQ(result.diagnostics.size(), 1);
EXPECT_EQ(result.diagnostics[0].stage, fesa::DiagnosticStage::model);
EXPECT_EQ(result.diagnostics[0].severity, fesa::Severity::error);
EXPECT_EQ(result.diagnostics[0].code, code);
}
std::array<double, 12> multiply(
const fesa::Matrix12& matrix,
const std::array<double, 12>& vector) {
std::array<double, 12> product{};
for (std::size_t row = 0; row < product.size(); ++row) {
for (std::size_t column = 0; column < vector.size(); ++column) {
product[row] += matrix[row][column] * vector[column];
}
}
return product;
}
double squared_norm(const std::array<double, 12>& vector) {
double result = 0.0;
for (const double value : vector) {
result += value * value;
}
return result;
}
TEST(BeamFrame, AlignsLocalAxesForGlobalXAxisBeam) {
const auto result = fesa::make_beam_frame(
{0.0, 0.0, 0.0},
{2.0, 0.0, 0.0},
{0.0, 3.0, 0.0});
ASSERT_TRUE(result.frame.has_value());
EXPECT_TRUE(result.diagnostics.empty());
expect_vec_near(result.frame->ex, {1.0, 0.0, 0.0});
expect_vec_near(result.frame->ey, {0.0, 1.0, 0.0});
expect_vec_near(result.frame->ez, {0.0, 0.0, 1.0});
}
TEST(BeamFrame, ProducesRightHandedOrthonormalBasisForArbitraryAxis) {
const auto result = fesa::make_beam_frame(
{1.0, 2.0, 3.0},
{3.0, 4.0, 4.0},
{0.0, 1.0, 0.0});
ASSERT_TRUE(result.frame.has_value());
const fesa::BeamFrame& frame = *result.frame;
const double inverse_sqrt_five = 1.0 / std::sqrt(5.0);
expect_vec_near(frame.ex, {2.0 / 3.0, 2.0 / 3.0, 1.0 / 3.0});
expect_vec_near(
frame.ey,
{
-4.0 * inverse_sqrt_five / 3.0,
5.0 * inverse_sqrt_five / 3.0,
-2.0 * inverse_sqrt_five / 3.0,
});
expect_vec_near(
frame.ez,
{-inverse_sqrt_five, 0.0, 2.0 * inverse_sqrt_five});
EXPECT_NEAR(dot(frame.ex, frame.ex), 1.0, kUnitTolerance);
EXPECT_NEAR(dot(frame.ey, frame.ey), 1.0, kUnitTolerance);
EXPECT_NEAR(dot(frame.ez, frame.ez), 1.0, kUnitTolerance);
EXPECT_NEAR(dot(frame.ex, frame.ey), 0.0, kUnitTolerance);
EXPECT_NEAR(dot(frame.ey, frame.ez), 0.0, kUnitTolerance);
EXPECT_NEAR(dot(frame.ez, frame.ex), 0.0, kUnitTolerance);
EXPECT_NEAR(dot(frame.ex, cross(frame.ey, frame.ez)), 1.0, kUnitTolerance);
}
TEST(BeamFrame, RotatesCovariantlyWithBeamGeometry) {
constexpr fesa::Vec3 first{1.0, -2.0, 0.5};
constexpr fesa::Vec3 second{4.0, 1.0, 2.5};
constexpr fesa::Vec3 orientation{-1.0, 2.0, 3.0};
const double inverse_sqrt_fourteen = 1.0 / std::sqrt(14.0);
const fesa::Vec3 rotation_axis{
inverse_sqrt_fourteen,
2.0 * inverse_sqrt_fourteen,
3.0 * inverse_sqrt_fourteen,
};
constexpr double angle = 0.73;
const auto original =
fesa::make_beam_frame(first, second, orientation);
const auto rotated = fesa::make_beam_frame(
rotate_about_axis(first, rotation_axis, angle),
rotate_about_axis(second, rotation_axis, angle),
rotate_about_axis(orientation, rotation_axis, angle));
ASSERT_TRUE(original.frame.has_value());
ASSERT_TRUE(rotated.frame.has_value());
expect_vec_near(
rotated.frame->ex,
rotate_about_axis(original.frame->ex, rotation_axis, angle),
256.0 * std::numeric_limits<double>::epsilon());
expect_vec_near(
rotated.frame->ey,
rotate_about_axis(original.frame->ey, rotation_axis, angle),
256.0 * std::numeric_limits<double>::epsilon());
expect_vec_near(
rotated.frame->ez,
rotate_about_axis(original.frame->ez, rotation_axis, angle),
256.0 * std::numeric_limits<double>::epsilon());
}
TEST(BeamFrame, IsInvariantToFiniteOrientationScale) {
const double maximum = std::numeric_limits<double>::max();
const auto unit_scale = fesa::make_beam_frame(
{0.0, 0.0, 0.0},
{1.0, 1.0, 1.0},
{1.0, -1.0, 0.0});
const auto maximum_scale = fesa::make_beam_frame(
{0.0, 0.0, 0.0},
{1.0, 1.0, 1.0},
{maximum, -maximum, 0.0});
ASSERT_TRUE(unit_scale.frame.has_value());
ASSERT_TRUE(maximum_scale.frame.has_value());
expect_vec_near(maximum_scale.frame->ex, unit_scale.frame->ex);
expect_vec_near(maximum_scale.frame->ey, unit_scale.frame->ey);
expect_vec_near(maximum_scale.frame->ez, unit_scale.frame->ez);
}
TEST(BeamFrame, RejectsZeroLengthAxis) {
expect_error(
fesa::make_beam_frame(
{1.0, 2.0, 3.0},
{1.0, 2.0, 3.0},
{0.0, 1.0, 0.0}),
"model.zero_length_element");
}
TEST(BeamFrame, RejectsZeroOrientation) {
expect_error(
fesa::make_beam_frame(
{0.0, 0.0, 0.0},
{1.0, 0.0, 0.0},
{0.0, 0.0, 0.0}),
"model.invalid_orientation");
}
TEST(BeamFrame, RejectsOrientationParallelToElementAxis) {
expect_error(
fesa::make_beam_frame(
{0.0, 0.0, 0.0},
{2.0, 0.0, 0.0},
{5.0, 0.0, 0.0}),
"model.invalid_orientation");
}
TEST(BeamFrame, RejectsNumericallyParallelOrientationAtLargeScale) {
constexpr double transverse_ratio =
32.0 * std::numeric_limits<double>::epsilon();
expect_error(
fesa::make_beam_frame(
{0.0, 0.0, 0.0},
{2.0, 0.0, 0.0},
{1.0e200, transverse_ratio * 1.0e200, 0.0}),
"model.invalid_orientation");
}
TEST(BeamTransformation, IsIdentityForGlobalAlignedFrame) {
const fesa::Matrix12 transformation = fesa::beam_transformation({
{1.0, 0.0, 0.0},
{0.0, 1.0, 0.0},
{0.0, 0.0, 1.0},
});
for (std::size_t row = 0; row < transformation.size(); ++row) {
for (std::size_t column = 0;
column < transformation[row].size();
++column) {
EXPECT_DOUBLE_EQ(
transformation[row][column],
row == column ? 1.0 : 0.0);
}
}
}
TEST(BeamTransformation, MapsGlobalComponentsIntoLocalDofOrder) {
const fesa::Matrix12 transformation = fesa::beam_transformation({
{0.0, 1.0, 0.0},
{0.0, 0.0, 1.0},
{1.0, 0.0, 0.0},
});
const std::array<double, 12> global{
1.0, 2.0, 3.0, 4.0, 5.0, 6.0,
7.0, 8.0, 9.0, 10.0, 11.0, 12.0,
};
EXPECT_EQ(
multiply(transformation, global),
(std::array<double, 12>{
2.0, 3.0, 1.0, 5.0, 6.0, 4.0,
8.0, 9.0, 7.0, 11.0, 12.0, 10.0,
}));
}
TEST(BeamTransformation, PreservesVectorEnergyForArbitraryFrame) {
const auto frame_result = fesa::make_beam_frame(
{-2.0, 1.0, 3.0},
{1.0, 5.0, 5.0},
{2.0, -1.0, 4.0});
ASSERT_TRUE(frame_result.frame.has_value());
const fesa::Matrix12 transformation =
fesa::beam_transformation(*frame_result.frame);
const std::array<double, 12> global{
3.0, -1.0, 2.0, 0.5, 4.0, -2.0,
-3.0, 6.0, 1.5, 2.5, -0.25, 5.0,
};
const std::array<double, 12> local =
multiply(transformation, global);
const double global_energy = squared_norm(global);
const double tolerance =
256.0 * std::numeric_limits<double>::epsilon() *
std::max(1.0, global_energy);
EXPECT_NEAR(squared_norm(local), global_energy, tolerance);
}
} // namespace