feat(linear-static-3d-euler-beam): step 16 - euler-beam-element

This commit is contained in:
KOKO\Mimi
2026-08-09 18:04:52 +09:00
parent 6fa01de5f9
commit 987f276ef1
6 changed files with 1486 additions and 0 deletions
+885
View File
@@ -0,0 +1,885 @@
#include "fesa/elements/euler_beam_3d.hpp"
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <limits>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
namespace fesa {
namespace {
constexpr std::size_t kElementDofCount = 12U;
constexpr double kMatrixTolerance = 1.0e-12;
constexpr double kRigidTolerance = 1.0e-10;
constexpr double kAnalyticalTolerance = 1.0e-9;
Node makeNode(std::array<double, 3> coordinates, std::size_t line) {
return {{"Beam-1", static_cast<std::int64_t>(line), std::to_string(line)},
coordinates,
{"beam-test.inp", line}};
}
LinearElasticMaterial makeMaterial(double youngsModulus = 210.0e9,
double poissonRatio = 0.3) {
return {"Steel", youngsModulus, poissonRatio, {"beam-test.inp", 20U}};
}
GeneralBeamSection makeSection(
std::array<double, 3> firstAxis = {0.0, 1.0, 0.0},
std::vector<std::array<double, 2>> sectionPoints = {}) {
return {"Section-1",
0.012,
2.5e-5,
0.0,
4.0e-5,
1.5e-5,
firstAxis,
std::move(sectionPoints),
{"beam-test.inp", 30U}};
}
EulerBeam3D requireBeam(const Node& firstNode,
const Node& secondNode,
const GeneralBeamSection& section,
const LinearElasticMaterial& material) {
auto result = EulerBeam3D::create(firstNode, secondNode, section, material);
if (!result.hasValue()) {
throw std::runtime_error{"Expected a valid EulerBeam3D fixture."};
}
return std::move(result.value());
}
EulerBeam3D alignedBeam(double length,
const GeneralBeamSection& section = makeSection(),
const LinearElasticMaterial& material = makeMaterial()) {
return requireBeam(
makeNode({0.0, 0.0, 0.0}, 1U),
makeNode({length, 0.0, 0.0}, 2U),
section,
material);
}
double maximumAbsoluteEntry(const Matrix& matrix) {
double maximum = 0.0;
for (std::size_t row = 0; row < matrix.rows(); ++row) {
for (std::size_t column = 0; column < matrix.columns(); ++column) {
maximum = (std::max)(maximum, std::abs(matrix(row, column)));
}
}
return maximum;
}
double normalizedMatrixError(const Matrix& actual, const Matrix& expected) {
if (actual.rows() != expected.rows() || actual.columns() != expected.columns()) {
throw std::invalid_argument{"Matrix comparison requires equal shapes."};
}
double maximumDifference = 0.0;
for (std::size_t row = 0; row < actual.rows(); ++row) {
for (std::size_t column = 0; column < actual.columns(); ++column) {
maximumDifference = (std::max)(
maximumDifference,
std::abs(actual(row, column) - expected(row, column)));
}
}
const double scale = (std::max)(
1.0,
(std::max)(maximumAbsoluteEntry(actual), maximumAbsoluteEntry(expected)));
return maximumDifference / scale;
}
double vectorNorm(const Vector& vector) {
double sum = 0.0;
for (std::size_t index = 0; index < vector.size(); ++index) {
sum += vector[index] * vector[index];
}
return std::sqrt(sum);
}
double quadraticEnergy(const Matrix& matrix, const Vector& vector) {
const Vector product = matrix.multiply(vector);
double value = 0.0;
for (std::size_t index = 0; index < vector.size(); ++index) {
value += vector[index] * product[index];
}
return value;
}
void expectScaledNear(double actual, double expected, double relativeTolerance) {
const double scale = (std::max)(1.0, std::abs(expected));
EXPECT_LE(std::abs(actual - expected), relativeTolerance * scale);
}
Matrix expectedClosedStiffness(double length,
const GeneralBeamSection& section,
const LinearElasticMaterial& material) {
Matrix expected{kElementDofCount, kElementDofCount};
const double shearModulus =
material.youngsModulus / (2.0 * (1.0 + material.poissonRatio));
const auto addBlock = [&expected](const std::vector<std::size_t>& indices,
const std::vector<double>& values) {
const std::size_t width = indices.size();
for (std::size_t row = 0; row < width; ++row) {
for (std::size_t column = 0; column < width; ++column) {
expected(indices[row], indices[column]) = values[row * width + column];
}
}
};
const double axial = material.youngsModulus * section.area / length;
addBlock({0U, 6U}, {axial, -axial, -axial, axial});
const double torsion = shearModulus * section.torsionalConstant / length;
addBlock({3U, 9U}, {torsion, -torsion, -torsion, torsion});
const auto bendingBlock = [length](double flexuralRigidity, double rotationSign) {
const double v = 12.0 * flexuralRigidity / (length * length * length);
const double c = rotationSign * 6.0 * flexuralRigidity / (length * length);
const double d = 4.0 * flexuralRigidity / length;
const double e = 2.0 * flexuralRigidity / length;
return std::vector<double>{
v, c, -v, c,
c, d, -c, e,
-v, -c, v, -c,
c, e, -c, d};
};
addBlock(
{1U, 5U, 7U, 11U},
bendingBlock(material.youngsModulus * section.i22, 1.0));
addBlock(
{2U, 4U, 8U, 10U},
bendingBlock(material.youngsModulus * section.i11, -1.0));
return expected;
}
std::array<double, kElementDofCount> symmetricEigenvalues(Matrix matrix) {
for (std::size_t iteration = 0; iteration < 100U * kElementDofCount; ++iteration) {
std::size_t p = 0U;
std::size_t q = 1U;
double maximumOffDiagonal = 0.0;
for (std::size_t row = 0; row < kElementDofCount; ++row) {
for (std::size_t column = row + 1U; column < kElementDofCount; ++column) {
const double candidate = std::abs(matrix(row, column));
if (candidate > maximumOffDiagonal) {
maximumOffDiagonal = candidate;
p = row;
q = column;
}
}
}
if (maximumOffDiagonal <=
1.0e-14 * (std::max)(1.0, maximumAbsoluteEntry(matrix))) {
break;
}
const double app = matrix(p, p);
const double aqq = matrix(q, q);
const double apq = matrix(p, q);
const double angle = 0.5 * std::atan2(2.0 * apq, aqq - app);
const double cosine = std::cos(angle);
const double sine = std::sin(angle);
for (std::size_t index = 0; index < kElementDofCount; ++index) {
if (index == p || index == q) {
continue;
}
const double aip = matrix(index, p);
const double aiq = matrix(index, q);
matrix(index, p) = cosine * aip - sine * aiq;
matrix(p, index) = matrix(index, p);
matrix(index, q) = sine * aip + cosine * aiq;
matrix(q, index) = matrix(index, q);
}
matrix(p, p) = cosine * cosine * app - 2.0 * sine * cosine * apq +
sine * sine * aqq;
matrix(q, q) = sine * sine * app + 2.0 * sine * cosine * apq +
cosine * cosine * aqq;
matrix(p, q) = 0.0;
matrix(q, p) = 0.0;
}
std::array<double, kElementDofCount> eigenvalues{};
for (std::size_t index = 0; index < kElementDofCount; ++index) {
eigenvalues[index] = matrix(index, index);
}
return eigenvalues;
}
std::size_t symmetricRank(const Matrix& matrix, double relativeTolerance) {
const auto eigenvalues = symmetricEigenvalues(matrix);
double maximum = 0.0;
for (const double value : eigenvalues) {
maximum = (std::max)(maximum, std::abs(value));
}
return static_cast<std::size_t>(std::count_if(
eigenvalues.begin(),
eigenvalues.end(),
[maximum, relativeTolerance](double value) {
return std::abs(value) > relativeTolerance * maximum;
}));
}
Matrix testOnlyOnePointStiffness(double length,
const GeneralBeamSection& section,
const LinearElasticMaterial& material) {
// At xi=0 the two bending curvature rows retain only the nodal rotations.
// This deliberately under-integrated negative control is independent of production.
Matrix b{4U, kElementDofCount};
b(0U, 0U) = -1.0 / length;
b(0U, 6U) = 1.0 / length;
b(1U, 3U) = -1.0 / length;
b(1U, 9U) = 1.0 / length;
b(2U, 4U) = -1.0 / length;
b(2U, 10U) = 1.0 / length;
b(3U, 5U) = -1.0 / length;
b(3U, 11U) = 1.0 / length;
const double shearModulus =
material.youngsModulus / (2.0 * (1.0 + material.poissonRatio));
const std::array<double, 4> diagonal = {
material.youngsModulus * section.area,
shearModulus * section.torsionalConstant,
material.youngsModulus * section.i11,
material.youngsModulus * section.i22};
Matrix stiffness{kElementDofCount, kElementDofCount};
for (std::size_t row = 0; row < kElementDofCount; ++row) {
for (std::size_t column = 0; column < kElementDofCount; ++column) {
for (std::size_t component = 0; component < diagonal.size(); ++component) {
stiffness(row, column) +=
b(component, row) * diagonal[component] * b(component, column) * length;
}
}
}
return stiffness;
}
Vector solveFixedFirstNode(const Matrix& stiffness,
const std::array<double, 6>& freeEndLoad) {
std::array<std::array<double, 7>, 6> augmented{};
for (std::size_t row = 0; row < 6U; ++row) {
for (std::size_t column = 0; column < 6U; ++column) {
augmented[row][column] = stiffness(row + 6U, column + 6U);
}
augmented[row][6U] = freeEndLoad[row];
}
for (std::size_t pivot = 0; pivot < 6U; ++pivot) {
std::size_t pivotRow = pivot;
for (std::size_t row = pivot + 1U; row < 6U; ++row) {
if (std::abs(augmented[row][pivot]) >
std::abs(augmented[pivotRow][pivot])) {
pivotRow = row;
}
}
if (std::abs(augmented[pivotRow][pivot]) <=
std::numeric_limits<double>::min()) {
throw std::runtime_error{"Cantilever fixture is singular."};
}
std::swap(augmented[pivot], augmented[pivotRow]);
const double pivotValue = augmented[pivot][pivot];
for (std::size_t column = pivot; column < 7U; ++column) {
augmented[pivot][column] /= pivotValue;
}
for (std::size_t row = 0; row < 6U; ++row) {
if (row == pivot) {
continue;
}
const double factor = augmented[row][pivot];
for (std::size_t column = pivot; column < 7U; ++column) {
augmented[row][column] -= factor * augmented[pivot][column];
}
}
}
Vector displacement{kElementDofCount};
for (std::size_t component = 0; component < 6U; ++component) {
displacement[component + 6U] = augmented[component][6U];
}
return displacement;
}
Matrix transformationFromKnownRows(
const std::array<std::array<double, 3>, 3>& rotation) {
Matrix transformation{kElementDofCount, kElementDofCount};
for (std::size_t block = 0; block < 4U; ++block) {
for (std::size_t row = 0; row < 3U; ++row) {
for (std::size_t column = 0; column < 3U; ++column) {
transformation(block * 3U + row, block * 3U + column) =
rotation[row][column];
}
}
}
return transformation;
}
Vector transposeMultiply(const Matrix& matrix, const Vector& vector) {
if (matrix.rows() != vector.size()) {
throw std::invalid_argument{"Transpose multiply dimension mismatch."};
}
Vector result{matrix.columns()};
for (std::size_t column = 0; column < matrix.columns(); ++column) {
for (std::size_t row = 0; row < matrix.rows(); ++row) {
result[column] += matrix(row, column) * vector[row];
}
}
return result;
}
double determinant(const std::array<std::array<double, 3>, 3>& matrix) {
return matrix[0][0] *
(matrix[1][1] * matrix[2][2] - matrix[1][2] * matrix[2][1]) -
matrix[0][1] *
(matrix[1][0] * matrix[2][2] - matrix[1][2] * matrix[2][0]) +
matrix[0][2] *
(matrix[1][0] * matrix[2][1] - matrix[1][1] * matrix[2][0]);
}
TEST(EulerBeam3D, HermiteAndBMatrixMatchReviewedSigns) {
const double length = 2.5;
const auto section = makeSection();
const auto material = makeMaterial();
const auto beam = alignedBeam(length, section, material);
const double axialStrain = 0.012;
const double twist = -0.021;
const std::array<double, 4> v = {1.2, -0.4, 0.3, -0.07};
const std::array<double, 4> w = {-0.8, 0.6, -0.2, 0.05};
const auto value = [](const std::array<double, 4>& coefficients, double x) {
return coefficients[0] + coefficients[1] * x + coefficients[2] * x * x +
coefficients[3] * x * x * x;
};
const auto slope = [](const std::array<double, 4>& coefficients, double x) {
return coefficients[1] + 2.0 * coefficients[2] * x +
3.0 * coefficients[3] * x * x;
};
const auto curvature = [](const std::array<double, 4>& coefficients, double x) {
return 2.0 * coefficients[2] + 6.0 * coefficients[3] * x;
};
Vector displacement{kElementDofCount};
displacement[0U] = 0.2;
displacement[1U] = value(v, 0.0);
displacement[2U] = value(w, 0.0);
displacement[3U] = -0.1;
displacement[4U] = -slope(w, 0.0);
displacement[5U] = slope(v, 0.0);
displacement[6U] = displacement[0U] + axialStrain * length;
displacement[7U] = value(v, length);
displacement[8U] = value(w, length);
displacement[9U] = displacement[3U] + twist * length;
displacement[10U] = -slope(w, length);
displacement[11U] = slope(v, length);
const BeamRecovery recovery = beam.recover(displacement);
const double inverseSqrtThree = 1.0 / std::sqrt(3.0);
const std::array<double, 2> gaussXi = {-inverseSqrtThree, inverseSqrtThree};
for (std::size_t point = 0; point < gaussXi.size(); ++point) {
const double x = 0.5 * length * (1.0 + gaussXi[point]);
EXPECT_NEAR(recovery.gaussGeneralizedStrains[point][0U], axialStrain, 1.0e-14);
EXPECT_NEAR(recovery.gaussGeneralizedStrains[point][1U], twist, 1.0e-14);
EXPECT_NEAR(
recovery.gaussGeneralizedStrains[point][2U],
-curvature(w, x),
1.0e-13);
EXPECT_NEAR(
recovery.gaussGeneralizedStrains[point][3U],
curvature(v, x),
1.0e-13);
}
EXPECT_NEAR(
recovery.endpointSectionResultants[0U][2U],
material.youngsModulus * section.i11 * -curvature(w, 0.0),
1.0e-5);
EXPECT_NEAR(
recovery.endpointSectionResultants[1U][2U],
material.youngsModulus * section.i11 * -curvature(w, length),
1.0e-5);
EXPECT_NEAR(
recovery.endpointSectionResultants[0U][3U],
material.youngsModulus * section.i22 * curvature(v, 0.0),
1.0e-5);
EXPECT_NEAR(
recovery.endpointSectionResultants[1U][3U],
material.youngsModulus * section.i22 * curvature(v, length),
1.0e-5);
}
TEST(EulerBeam3D, TwoPointGaussMatchesClosedStiffness) {
const double length = 3.7;
auto section = makeSection();
section.area = 0.019;
section.i11 = 3.1e-5;
section.i22 = 7.4e-5;
section.torsionalConstant = 2.2e-5;
const auto material = makeMaterial(73.0e9, 0.27);
const auto beam = alignedBeam(length, section, material);
const Matrix actual = beam.localStiffness();
const Matrix closed = expectedClosedStiffness(length, section, material);
EXPECT_LE(normalizedMatrixError(actual, closed), kMatrixTolerance);
Matrix transpose{actual.rows(), actual.columns()};
for (std::size_t row = 0; row < actual.rows(); ++row) {
for (std::size_t column = 0; column < actual.columns(); ++column) {
transpose(row, column) = actual(column, row);
}
}
EXPECT_LE(normalizedMatrixError(actual, transpose), kMatrixTolerance);
}
TEST(EulerBeam3D, HasSixRigidModesRankSixAndPositiveDeformationEnergy) {
const double length = 2.0;
auto section = makeSection();
section.area = 1.4;
section.i11 = 0.8;
section.i22 = 1.1;
section.torsionalConstant = 0.6;
const auto material = makeMaterial(5.0, 0.25);
const Matrix stiffness = alignedBeam(length, section, material).localStiffness();
std::array<Vector, 6> rigidModes = {
Vector{kElementDofCount}, Vector{kElementDofCount}, Vector{kElementDofCount},
Vector{kElementDofCount}, Vector{kElementDofCount}, Vector{kElementDofCount}};
rigidModes[0U][0U] = rigidModes[0U][6U] = 1.0;
rigidModes[1U][1U] = rigidModes[1U][7U] = 1.0;
rigidModes[2U][2U] = rigidModes[2U][8U] = 1.0;
rigidModes[3U][3U] = rigidModes[3U][9U] = 1.0;
rigidModes[4U][4U] = rigidModes[4U][10U] = 1.0;
rigidModes[4U][8U] = -length;
rigidModes[5U][5U] = rigidModes[5U][11U] = 1.0;
rigidModes[5U][7U] = length;
const double stiffnessScale = (std::max)(1.0, maximumAbsoluteEntry(stiffness));
for (const Vector& mode : rigidModes) {
const double normalizedResidual =
vectorNorm(stiffness.multiply(mode)) /
(stiffnessScale * (std::max)(1.0, vectorNorm(mode)));
EXPECT_LE(normalizedResidual, kRigidTolerance);
}
const std::array<double, kElementDofCount> q = {
1.0, 1.0, 1.0, length, length, length,
1.0, 1.0, 1.0, length, length, length};
Matrix scaled{kElementDofCount, kElementDofCount};
for (std::size_t row = 0; row < kElementDofCount; ++row) {
for (std::size_t column = 0; column < kElementDofCount; ++column) {
scaled(row, column) = stiffness(row, column) / (q[row] * q[column]);
}
}
const auto eigenvalues = symmetricEigenvalues(scaled);
double maximumSingularValue = 0.0;
for (const double value : eigenvalues) {
maximumSingularValue = (std::max)(maximumSingularValue, std::abs(value));
}
const auto positiveCount = std::count_if(
eigenvalues.begin(), eigenvalues.end(), [maximumSingularValue](double value) {
return std::abs(value) > kRigidTolerance * maximumSingularValue;
});
EXPECT_EQ(positiveCount, 6);
for (const double value : eigenvalues) {
EXPECT_GE(value, -kRigidTolerance * maximumSingularValue);
}
for (std::size_t component = 0; component < 6U; ++component) {
Vector deformation{kElementDofCount};
deformation[6U + component] = 1.0;
EXPECT_GT(quadraticEnergy(stiffness, deformation), 0.0);
}
}
TEST(EulerBeam3D, RotatedTransformPreservesWorkAndEnergy) {
const double inverseSqrtTwo = 1.0 / std::sqrt(2.0);
const std::array<std::array<double, 3>, 3> rotation = {{
{{2.0 / 3.0, 2.0 / 3.0, 1.0 / 3.0}},
{{-inverseSqrtTwo, inverseSqrtTwo, 0.0}},
{{-inverseSqrtTwo / 3.0, -inverseSqrtTwo / 3.0,
4.0 * inverseSqrtTwo / 3.0}}}};
const Matrix transformation = transformationFromKnownRows(rotation);
for (std::size_t row = 0; row < 3U; ++row) {
for (std::size_t column = 0; column < 3U; ++column) {
double dot = 0.0;
for (std::size_t component = 0; component < 3U; ++component) {
dot += rotation[row][component] * rotation[column][component];
}
EXPECT_NEAR(dot, row == column ? 1.0 : 0.0, 1.0e-14);
}
}
EXPECT_NEAR(determinant(rotation), 1.0, 1.0e-14);
const auto section = makeSection({-2.0, 2.0, 0.0});
const auto material = makeMaterial();
const auto beam = requireBeam(
makeNode({1.0, -2.0, 0.5}, 1U),
makeNode({3.0, 0.0, 1.5}, 2U),
section,
material);
const Matrix local = beam.localStiffness();
const Matrix global = beam.globalStiffness();
Matrix expectedGlobal{kElementDofCount, kElementDofCount};
const Matrix localTimesTransform = local.multiply(transformation);
for (std::size_t row = 0; row < kElementDofCount; ++row) {
for (std::size_t column = 0; column < kElementDofCount; ++column) {
for (std::size_t inner = 0; inner < kElementDofCount; ++inner) {
expectedGlobal(row, column) +=
transformation(inner, row) * localTimesTransform(inner, column);
}
}
}
EXPECT_LE(normalizedMatrixError(global, expectedGlobal), kMatrixTolerance);
Vector localDisplacement{kElementDofCount};
for (std::size_t index = 0; index < localDisplacement.size(); ++index) {
localDisplacement[index] = 0.01 * static_cast<double>(index + 1U) - 0.04;
}
const Vector globalDisplacement = transposeMultiply(transformation, localDisplacement);
const Vector localForce = local.multiply(localDisplacement);
const Vector globalForce = global.multiply(globalDisplacement);
const Vector expectedGlobalForce = transposeMultiply(transformation, localForce);
for (std::size_t index = 0; index < kElementDofCount; ++index) {
expectScaledNear(globalForce[index], expectedGlobalForce[index], kMatrixTolerance);
}
expectScaledNear(
quadraticEnergy(global, globalDisplacement),
quadraticEnergy(local, localDisplacement),
kMatrixTolerance);
Vector globalVariation{kElementDofCount};
for (std::size_t index = 0; index < globalVariation.size(); ++index) {
globalVariation[index] = 0.03 - 0.002 * static_cast<double>(index);
}
const Vector localVariation = transformation.multiply(globalVariation);
expectScaledNear(
globalVariation.dot(globalForce),
localVariation.dot(localForce),
kMatrixTolerance);
const BeamRecovery recovery = beam.recover(globalDisplacement);
EXPECT_NEAR(
recovery.gaussGeneralizedStrains[0U][0U],
(localDisplacement[6U] - localDisplacement[0U]) / 3.0,
1.0e-14);
}
TEST(EulerBeam3D, ConstantLineLoadMatchesAllSignedComponents) {
const double length = 4.0;
const ConstantLocalLineLoad load{2.5, -3.0, 5.5, -7.0};
const Vector equivalent = alignedBeam(length).localEquivalentLoad(load);
const std::array<double, kElementDofCount> expected = {
5.0, -6.0, 11.0, -14.0, -22.0 / 3.0, -4.0,
5.0, -6.0, 11.0, -14.0, 22.0 / 3.0, 4.0};
ASSERT_EQ(equivalent.size(), expected.size());
for (std::size_t index = 0; index < expected.size(); ++index) {
expectScaledNear(equivalent[index], expected[index], kMatrixTolerance);
}
}
TEST(EulerBeam3D, AnalyticalAxialTorsionAndTwoPlaneBendingRecover) {
const double length = 3.0;
const auto section = makeSection();
const auto material = makeMaterial();
const auto beam = alignedBeam(length, section, material);
const Matrix stiffness = beam.localStiffness();
const double shearModulus =
material.youngsModulus / (2.0 * (1.0 + material.poissonRatio));
const double axialForce = 1250.0;
const Vector axial = solveFixedFirstNode(stiffness, {axialForce, 0.0, 0.0, 0.0, 0.0, 0.0});
expectScaledNear(
axial[6U],
axialForce * length / (material.youngsModulus * section.area),
kAnalyticalTolerance);
const BeamRecovery axialRecovery = beam.recover(axial);
expectScaledNear(axialRecovery.equilibriumEndActions[0U][0U], -axialForce, kMatrixTolerance);
expectScaledNear(axialRecovery.equilibriumEndActions[1U][0U], axialForce, kMatrixTolerance);
expectScaledNear(axialRecovery.endpointSectionResultants[0U][0U], axialForce, kMatrixTolerance);
expectScaledNear(axialRecovery.endpointSectionResultants[1U][0U], axialForce, kMatrixTolerance);
const double torque = -870.0;
const Vector torsion = solveFixedFirstNode(stiffness, {0.0, 0.0, 0.0, torque, 0.0, 0.0});
expectScaledNear(
torsion[9U],
torque * length / (shearModulus * section.torsionalConstant),
kAnalyticalTolerance);
const BeamRecovery torsionRecovery = beam.recover(torsion);
expectScaledNear(torsionRecovery.equilibriumEndActions[0U][3U], -torque, kMatrixTolerance);
expectScaledNear(torsionRecovery.equilibriumEndActions[1U][3U], torque, kMatrixTolerance);
expectScaledNear(torsionRecovery.endpointSectionResultants[0U][1U], torque, kMatrixTolerance);
const double localYForce = 640.0;
const Vector localY = solveFixedFirstNode(stiffness, {0.0, localYForce, 0.0, 0.0, 0.0, 0.0});
expectScaledNear(
localY[7U],
localYForce * length * length * length /
(3.0 * material.youngsModulus * section.i22),
kAnalyticalTolerance);
expectScaledNear(
localY[11U],
localYForce * length * length /
(2.0 * material.youngsModulus * section.i22),
kAnalyticalTolerance);
const BeamRecovery localYRecovery = beam.recover(localY);
expectScaledNear(localYRecovery.equilibriumEndActions[0U][1U], -localYForce, kMatrixTolerance);
expectScaledNear(localYRecovery.equilibriumEndActions[1U][1U], localYForce, kMatrixTolerance);
expectScaledNear(
localYRecovery.equilibriumEndActions[0U][5U],
-localYForce * length,
kMatrixTolerance);
expectScaledNear(
localYRecovery.endpointSectionResultants[0U][3U],
localYForce * length,
kMatrixTolerance);
EXPECT_NEAR(localYRecovery.endpointSectionResultants[1U][3U], 0.0, 1.0e-8);
const double localZForce = -510.0;
const Vector localZ = solveFixedFirstNode(stiffness, {0.0, 0.0, localZForce, 0.0, 0.0, 0.0});
expectScaledNear(
localZ[8U],
localZForce * length * length * length /
(3.0 * material.youngsModulus * section.i11),
kAnalyticalTolerance);
expectScaledNear(
localZ[10U],
-localZForce * length * length /
(2.0 * material.youngsModulus * section.i11),
kAnalyticalTolerance);
const BeamRecovery localZRecovery = beam.recover(localZ);
expectScaledNear(localZRecovery.equilibriumEndActions[0U][2U], -localZForce, kMatrixTolerance);
expectScaledNear(localZRecovery.equilibriumEndActions[1U][2U], localZForce, kMatrixTolerance);
expectScaledNear(
localZRecovery.equilibriumEndActions[0U][4U],
localZForce * length,
kMatrixTolerance);
expectScaledNear(
localZRecovery.endpointSectionResultants[0U][2U],
-localZForce * length,
kMatrixTolerance);
EXPECT_NEAR(localZRecovery.endpointSectionResultants[1U][2U], 0.0, 1.0e-8);
}
TEST(EulerBeam3D, RejectsInvalidGeometryAndProperties) {
const Node origin = makeNode({0.0, 0.0, 0.0}, 1U);
const Node unitX = makeNode({1.0, 0.0, 0.0}, 2U);
const auto validSection = makeSection();
const auto validMaterial = makeMaterial();
const auto expectFailure = [](const Result<EulerBeam3D>& result,
const std::string& code) {
ASSERT_FALSE(result.hasValue());
EXPECT_EQ(result.status().failureCategory(), FailureCategory::model);
ASSERT_EQ(result.status().diagnostics().size(), 1U);
EXPECT_EQ(result.status().diagnostics()[0U].code, code);
};
expectFailure(
EulerBeam3D::create(origin, origin, validSection, validMaterial),
"invalid-beam-length");
expectFailure(
EulerBeam3D::create(
origin,
makeNode({1.0e-12, 0.0, 0.0}, 2U),
validSection,
validMaterial),
"invalid-beam-length");
const double coordinate = 1048576.0;
const Node scaledFirst = makeNode({coordinate, 0.0, 0.0}, 1U);
const Node belowThreshold = makeNode({coordinate + coordinate * 0.5e-12, 0.0, 0.0}, 2U);
const Node aboveThreshold = makeNode({coordinate + coordinate * 2.0e-12, 0.0, 0.0}, 2U);
expectFailure(
EulerBeam3D::create(scaledFirst, belowThreshold, validSection, validMaterial),
"invalid-beam-length");
EXPECT_TRUE(EulerBeam3D::create(
scaledFirst, aboveThreshold, validSection, validMaterial)
.hasValue());
auto parallelGuide = validSection;
parallelGuide.firstAxis = {1.0, 0.0, 0.0};
expectFailure(
EulerBeam3D::create(origin, unitX, parallelGuide, validMaterial),
"invalid-beam-guide-vector");
auto guideAtThreshold = validSection;
guideAtThreshold.firstAxis = {1.0, 1.0e-12, 0.0};
expectFailure(
EulerBeam3D::create(origin, unitX, guideAtThreshold, validMaterial),
"invalid-beam-guide-vector");
auto guideAboveThreshold = validSection;
guideAboveThreshold.firstAxis = {1.0, 2.0e-12, 0.0};
EXPECT_TRUE(EulerBeam3D::create(origin, unitX, guideAboveThreshold, validMaterial).hasValue());
auto invalidMaterial = validMaterial;
invalidMaterial.youngsModulus = 0.0;
expectFailure(
EulerBeam3D::create(origin, unitX, validSection, invalidMaterial),
"invalid-beam-property");
invalidMaterial = validMaterial;
invalidMaterial.poissonRatio = -2.0;
expectFailure(
EulerBeam3D::create(origin, unitX, validSection, invalidMaterial),
"invalid-beam-property");
for (std::size_t property = 0; property < 4U; ++property) {
auto invalidSection = validSection;
double* properties[] = {
&invalidSection.area,
&invalidSection.i11,
&invalidSection.i22,
&invalidSection.torsionalConstant};
*properties[property] = 0.0;
expectFailure(
EulerBeam3D::create(origin, unitX, invalidSection, validMaterial),
"invalid-beam-property");
}
auto coupledSection = validSection;
coupledSection.i12 = 1.0e-9;
expectFailure(
EulerBeam3D::create(origin, unitX, coupledSection, validMaterial),
"unsupported-coupled-section");
}
TEST(EulerBeam3D, RecoversSectionPointAndDefaultCentroidS11) {
const double length = 2.0;
const double epsilon = 0.01;
const double kappaY = 0.02;
const double kappaZ = -0.03;
const auto material = makeMaterial();
auto section = makeSection({0.0, 1.0, 0.0}, {{0.25, -0.5}, {-0.4, 0.3}});
const auto beam = alignedBeam(length, section, material);
Vector displacement{kElementDofCount};
displacement[6U] = epsilon * length;
displacement[7U] = 0.5 * kappaZ * length * length;
displacement[8U] = -0.5 * kappaY * length * length;
displacement[10U] = kappaY * length;
displacement[11U] = kappaZ * length;
const BeamRecovery recovery = beam.recover(displacement);
ASSERT_EQ(recovery.stressPoints.size(), 4U);
for (std::size_t gaussPoint = 0; gaussPoint < 2U; ++gaussPoint) {
for (std::size_t point = 0; point < section.sectionPoints.size(); ++point) {
const BeamStressPoint& stress =
recovery.stressPoints[gaussPoint * section.sectionPoints.size() + point];
const double x1 = section.sectionPoints[point][0U];
const double x2 = section.sectionPoints[point][1U];
EXPECT_EQ(stress.gaussPoint, static_cast<int>(gaussPoint + 1U));
EXPECT_EQ(stress.sectionPoint, point + 1U);
EXPECT_DOUBLE_EQ(stress.x1, x1);
EXPECT_DOUBLE_EQ(stress.x2, x2);
expectScaledNear(
stress.s11,
material.youngsModulus * (epsilon + x2 * kappaY - x1 * kappaZ),
kMatrixTolerance);
}
}
const auto defaultBeam = alignedBeam(length, makeSection(), material);
const BeamRecovery defaultRecovery = defaultBeam.recover(displacement);
ASSERT_EQ(defaultRecovery.stressPoints.size(), 2U);
for (std::size_t gaussPoint = 0; gaussPoint < 2U; ++gaussPoint) {
const BeamStressPoint& stress = defaultRecovery.stressPoints[gaussPoint];
EXPECT_EQ(stress.gaussPoint, static_cast<int>(gaussPoint + 1U));
EXPECT_EQ(stress.sectionPoint, 0U);
EXPECT_DOUBLE_EQ(stress.x1, 0.0);
EXPECT_DOUBLE_EQ(stress.x2, 0.0);
EXPECT_EQ(stress.source, "fesa-default");
expectScaledNear(
stress.s11,
material.youngsModulus * epsilon,
kMatrixTolerance);
}
}
TEST(EulerBeam3D, ReproducesConstantStrainTwistAndCurvaturePatches) {
const double length = 2.8;
const double epsilon = -0.014;
const double twist = 0.023;
const double kappaY = -0.031;
const double kappaZ = 0.047;
const auto section = makeSection();
const auto material = makeMaterial();
const double shearModulus =
material.youngsModulus / (2.0 * (1.0 + material.poissonRatio));
const auto beam = alignedBeam(length, section, material);
Vector displacement{kElementDofCount};
displacement[6U] = epsilon * length;
displacement[7U] = 0.5 * kappaZ * length * length;
displacement[8U] = -0.5 * kappaY * length * length;
displacement[9U] = twist * length;
displacement[10U] = kappaY * length;
displacement[11U] = kappaZ * length;
const std::array<double, 4> expectedStrain = {epsilon, twist, kappaY, kappaZ};
const std::array<double, 4> expectedResultant = {
material.youngsModulus * section.area * epsilon,
shearModulus * section.torsionalConstant * twist,
material.youngsModulus * section.i11 * kappaY,
material.youngsModulus * section.i22 * kappaZ};
const BeamRecovery recovery = beam.recover(displacement);
for (std::size_t point = 0; point < 2U; ++point) {
for (std::size_t component = 0; component < 4U; ++component) {
expectScaledNear(
recovery.gaussGeneralizedStrains[point][component],
expectedStrain[component],
kMatrixTolerance);
expectScaledNear(
recovery.gaussGeneralizedResultants[point][component],
expectedResultant[component],
kMatrixTolerance);
expectScaledNear(
recovery.endpointSectionResultants[point][component],
expectedResultant[component],
kMatrixTolerance);
}
}
const std::array<std::size_t, 4> endActionComponents = {0U, 3U, 4U, 5U};
for (std::size_t component = 0; component < expectedResultant.size(); ++component) {
expectScaledNear(
recovery.equilibriumEndActions[0U][endActionComponents[component]],
-expectedResultant[component],
kMatrixTolerance);
expectScaledNear(
recovery.equilibriumEndActions[1U][endActionComponents[component]],
expectedResultant[component],
kMatrixTolerance);
}
EXPECT_NEAR(recovery.equilibriumEndActions[0U][1U], 0.0, 1.0e-8);
EXPECT_NEAR(recovery.equilibriumEndActions[0U][2U], 0.0, 1.0e-8);
EXPECT_NEAR(recovery.equilibriumEndActions[1U][1U], 0.0, 1.0e-8);
EXPECT_NEAR(recovery.equilibriumEndActions[1U][2U], 0.0, 1.0e-8);
}
TEST(EulerBeam3D, OnePointNegativeControlHasRankFour) {
const double length = 3.7;
auto section = makeSection();
section.area = 1.0;
section.i11 = 0.7;
section.i22 = 1.2;
section.torsionalConstant = 0.9;
const auto material = makeMaterial(4.0, 0.25);
const Matrix onePoint = testOnlyOnePointStiffness(length, section, material);
const Matrix production = alignedBeam(length, section, material).localStiffness();
EXPECT_EQ(symmetricRank(onePoint, kRigidTolerance), 4U);
EXPECT_EQ(symmetricRank(production, kRigidTolerance), 6U);
}
} // namespace
} // namespace fesa