feat(linear-static-3d-euler-beam): step 16 - euler-beam-element-review-fix
This commit is contained in:
@@ -76,6 +76,17 @@ double maximumAbsoluteEntry(const Matrix& matrix) {
|
||||
return maximum;
|
||||
}
|
||||
|
||||
bool matrixIsFinite(const Matrix& matrix) {
|
||||
for (std::size_t row = 0; row < matrix.rows(); ++row) {
|
||||
for (std::size_t column = 0; column < matrix.columns(); ++column) {
|
||||
if (!std::isfinite(matrix(row, column))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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."};
|
||||
@@ -118,6 +129,11 @@ void expectScaledNear(double actual, double expected, double relativeTolerance)
|
||||
EXPECT_LE(std::abs(actual - expected), relativeTolerance * scale);
|
||||
}
|
||||
|
||||
void expectRelativeNear(double actual, double expected, double relativeTolerance) {
|
||||
ASSERT_NE(expected, 0.0);
|
||||
EXPECT_LE(std::abs(actual - expected) / std::abs(expected), relativeTolerance);
|
||||
}
|
||||
|
||||
Matrix expectedClosedStiffness(double length,
|
||||
const GeneralBeamSection& section,
|
||||
const LinearElasticMaterial& material) {
|
||||
@@ -311,6 +327,153 @@ Vector solveFixedFirstNode(const Matrix& stiffness,
|
||||
return displacement;
|
||||
}
|
||||
|
||||
Vector solveDenseSystem(Matrix matrix, Vector rightHandSide) {
|
||||
if (matrix.rows() != matrix.columns() ||
|
||||
matrix.rows() != rightHandSide.size()) {
|
||||
throw std::invalid_argument{"Dense test solve requires a square system."};
|
||||
}
|
||||
|
||||
for (std::size_t pivot = 0; pivot < matrix.rows(); ++pivot) {
|
||||
std::size_t pivotRow = pivot;
|
||||
for (std::size_t row = pivot + 1U; row < matrix.rows(); ++row) {
|
||||
if (std::abs(matrix(row, pivot)) >
|
||||
std::abs(matrix(pivotRow, pivot))) {
|
||||
pivotRow = row;
|
||||
}
|
||||
}
|
||||
if (!(std::abs(matrix(pivotRow, pivot)) > 0.0) ||
|
||||
!std::isfinite(matrix(pivotRow, pivot))) {
|
||||
throw std::runtime_error{"Uniform-load test fixture is singular."};
|
||||
}
|
||||
for (std::size_t column = pivot; column < matrix.columns(); ++column) {
|
||||
std::swap(matrix(pivot, column), matrix(pivotRow, column));
|
||||
}
|
||||
std::swap(rightHandSide[pivot], rightHandSide[pivotRow]);
|
||||
|
||||
const double pivotValue = matrix(pivot, pivot);
|
||||
for (std::size_t column = pivot; column < matrix.columns(); ++column) {
|
||||
matrix(pivot, column) /= pivotValue;
|
||||
}
|
||||
rightHandSide[pivot] /= pivotValue;
|
||||
for (std::size_t row = 0; row < matrix.rows(); ++row) {
|
||||
if (row == pivot) {
|
||||
continue;
|
||||
}
|
||||
const double factor = matrix(row, pivot);
|
||||
for (std::size_t column = pivot; column < matrix.columns(); ++column) {
|
||||
matrix(row, column) -= factor * matrix(pivot, column);
|
||||
}
|
||||
rightHandSide[row] -= factor * rightHandSide[pivot];
|
||||
}
|
||||
}
|
||||
return rightHandSide;
|
||||
}
|
||||
|
||||
Vector solveUniformTransverseCantilever(std::size_t elementCount,
|
||||
double length,
|
||||
double lineLoad,
|
||||
const GeneralBeamSection& section,
|
||||
const LinearElasticMaterial& material) {
|
||||
const double elementLength = length / static_cast<double>(elementCount);
|
||||
const std::size_t systemSize = 2U * (elementCount + 1U);
|
||||
Matrix assembledStiffness{systemSize, systemSize};
|
||||
Vector assembledLoad{systemSize};
|
||||
const std::array<std::size_t, 4> bendingDofs = {1U, 5U, 7U, 11U};
|
||||
|
||||
// Test-only direct assembly keeps this evidence at the formulation boundary:
|
||||
// two [v,rz] DOFs per node, with no Domain, parser, or DLOAD path.
|
||||
for (std::size_t element = 0; element < elementCount; ++element) {
|
||||
const EulerBeam3D beam = alignedBeam(elementLength, section, material);
|
||||
const Matrix elementStiffness = beam.localStiffness();
|
||||
const Vector elementLoad = beam.localEquivalentLoad(
|
||||
{0.0, lineLoad, 0.0, 0.0});
|
||||
const std::array<std::size_t, 4> assembledDofs = {
|
||||
2U * element,
|
||||
2U * element + 1U,
|
||||
2U * (element + 1U),
|
||||
2U * (element + 1U) + 1U};
|
||||
for (std::size_t row = 0; row < bendingDofs.size(); ++row) {
|
||||
assembledLoad[assembledDofs[row]] += elementLoad[bendingDofs[row]];
|
||||
for (std::size_t column = 0; column < bendingDofs.size(); ++column) {
|
||||
assembledStiffness(assembledDofs[row], assembledDofs[column]) +=
|
||||
elementStiffness(bendingDofs[row], bendingDofs[column]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const std::size_t freeSize = systemSize - 2U;
|
||||
Matrix freeStiffness{freeSize, freeSize};
|
||||
Vector freeLoad{freeSize};
|
||||
for (std::size_t row = 0; row < freeSize; ++row) {
|
||||
freeLoad[row] = assembledLoad[row + 2U];
|
||||
for (std::size_t column = 0; column < freeSize; ++column) {
|
||||
freeStiffness(row, column) =
|
||||
assembledStiffness(row + 2U, column + 2U);
|
||||
}
|
||||
}
|
||||
|
||||
const Vector freeDisplacement =
|
||||
solveDenseSystem(std::move(freeStiffness), std::move(freeLoad));
|
||||
Vector nodalDisplacement{systemSize};
|
||||
for (std::size_t dof = 0; dof < freeSize; ++dof) {
|
||||
nodalDisplacement[dof + 2U] = freeDisplacement[dof];
|
||||
}
|
||||
return nodalDisplacement;
|
||||
}
|
||||
|
||||
double uniformLoadInteriorDisplacementError(
|
||||
const Vector& nodalDisplacement,
|
||||
std::size_t elementCount,
|
||||
double length,
|
||||
double lineLoad,
|
||||
double flexuralRigidity) {
|
||||
const double elementLength = length / static_cast<double>(elementCount);
|
||||
const std::array<double, 5> gaussPoints = {
|
||||
-0.9061798459386640,
|
||||
-0.5384693101056831,
|
||||
0.0,
|
||||
0.5384693101056831,
|
||||
0.9061798459386640};
|
||||
const std::array<double, 5> gaussWeights = {
|
||||
0.2369268850561891,
|
||||
0.4786286704993665,
|
||||
0.5688888888888889,
|
||||
0.4786286704993665,
|
||||
0.2369268850561891};
|
||||
double squaredError = 0.0;
|
||||
double squaredReference = 0.0;
|
||||
|
||||
// Five-point integration is independent of production and exactly integrates
|
||||
// the squared error between cubic Hermite interpolation and the quartic beam solution.
|
||||
for (std::size_t element = 0; element < elementCount; ++element) {
|
||||
for (std::size_t point = 0; point < gaussPoints.size(); ++point) {
|
||||
const double r = 0.5 * (1.0 + gaussPoints[point]);
|
||||
const double rSquared = r * r;
|
||||
const double rCubed = rSquared * r;
|
||||
const double h1 = 1.0 - 3.0 * rSquared + 2.0 * rCubed;
|
||||
const double h2 = elementLength * (r - 2.0 * rSquared + rCubed);
|
||||
const double h3 = 3.0 * rSquared - 2.0 * rCubed;
|
||||
const double h4 = elementLength * (-rSquared + rCubed);
|
||||
const double interpolated =
|
||||
h1 * nodalDisplacement[2U * element] +
|
||||
h2 * nodalDisplacement[2U * element + 1U] +
|
||||
h3 * nodalDisplacement[2U * (element + 1U)] +
|
||||
h4 * nodalDisplacement[2U * (element + 1U) + 1U];
|
||||
const double x = elementLength *
|
||||
(static_cast<double>(element) + r);
|
||||
const double analytical =
|
||||
lineLoad * x * x *
|
||||
(6.0 * length * length - 4.0 * length * x + x * x) /
|
||||
(24.0 * flexuralRigidity);
|
||||
const double weight = 0.5 * elementLength * gaussWeights[point];
|
||||
const double difference = interpolated - analytical;
|
||||
squaredError += weight * difference * difference;
|
||||
squaredReference += weight * analytical * analytical;
|
||||
}
|
||||
}
|
||||
return std::sqrt(squaredError / squaredReference);
|
||||
}
|
||||
|
||||
Matrix transformationFromKnownRows(
|
||||
const std::array<std::array<double, 3>, 3>& rotation) {
|
||||
Matrix transformation{kElementDofCount, kElementDofCount};
|
||||
@@ -587,6 +750,61 @@ TEST(EulerBeam3D, ConstantLineLoadMatchesAllSignedComponents) {
|
||||
for (std::size_t index = 0; index < expected.size(); ++index) {
|
||||
expectScaledNear(equivalent[index], expected[index], kMatrixTolerance);
|
||||
}
|
||||
|
||||
auto convergenceSection = makeSection();
|
||||
convergenceSection.area = 1.0;
|
||||
convergenceSection.i11 = 1.0;
|
||||
convergenceSection.i22 = 1.0;
|
||||
convergenceSection.torsionalConstant = 1.0;
|
||||
const auto convergenceMaterial = makeMaterial(5.0, 0.25);
|
||||
const double transverseLoad = -3.0;
|
||||
const std::array<std::size_t, 3> elementCounts = {1U, 2U, 4U};
|
||||
std::array<double, 3> relativeErrors{};
|
||||
for (std::size_t mesh = 0; mesh < elementCounts.size(); ++mesh) {
|
||||
const double elementLength =
|
||||
length / static_cast<double>(elementCounts[mesh]);
|
||||
const Vector elementLoad = alignedBeam(
|
||||
elementLength,
|
||||
convergenceSection,
|
||||
convergenceMaterial)
|
||||
.localEquivalentLoad(
|
||||
{0.0, transverseLoad, 0.0, 0.0});
|
||||
expectRelativeNear(
|
||||
elementLoad[1U], transverseLoad * elementLength / 2.0, kMatrixTolerance);
|
||||
expectRelativeNear(
|
||||
elementLoad[5U],
|
||||
transverseLoad * elementLength * elementLength / 12.0,
|
||||
kMatrixTolerance);
|
||||
expectRelativeNear(
|
||||
elementLoad[7U], transverseLoad * elementLength / 2.0, kMatrixTolerance);
|
||||
expectRelativeNear(
|
||||
elementLoad[11U],
|
||||
-transverseLoad * elementLength * elementLength / 12.0,
|
||||
kMatrixTolerance);
|
||||
|
||||
const Vector nodalDisplacement = solveUniformTransverseCantilever(
|
||||
elementCounts[mesh],
|
||||
length,
|
||||
transverseLoad,
|
||||
convergenceSection,
|
||||
convergenceMaterial);
|
||||
relativeErrors[mesh] = uniformLoadInteriorDisplacementError(
|
||||
nodalDisplacement,
|
||||
elementCounts[mesh],
|
||||
length,
|
||||
transverseLoad,
|
||||
convergenceMaterial.youngsModulus * convergenceSection.i22);
|
||||
}
|
||||
EXPECT_GT(relativeErrors[0U], relativeErrors[1U]);
|
||||
EXPECT_GT(relativeErrors[1U], relativeErrors[2U]);
|
||||
EXPECT_NEAR(
|
||||
std::log(relativeErrors[0U] / relativeErrors[1U]) / std::log(2.0),
|
||||
4.0,
|
||||
1.0e-8);
|
||||
EXPECT_NEAR(
|
||||
std::log(relativeErrors[1U] / relativeErrors[2U]) / std::log(2.0),
|
||||
4.0,
|
||||
1.0e-8);
|
||||
}
|
||||
|
||||
TEST(EulerBeam3D, AnalyticalAxialTorsionAndTwoPlaneBendingRecover) {
|
||||
@@ -600,75 +818,108 @@ TEST(EulerBeam3D, AnalyticalAxialTorsionAndTwoPlaneBendingRecover) {
|
||||
|
||||
const double axialForce = 1250.0;
|
||||
const Vector axial = solveFixedFirstNode(stiffness, {axialForce, 0.0, 0.0, 0.0, 0.0, 0.0});
|
||||
expectScaledNear(
|
||||
expectRelativeNear(
|
||||
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);
|
||||
expectRelativeNear(
|
||||
axialRecovery.equilibriumEndActions[0U][0U],
|
||||
-axialForce,
|
||||
kAnalyticalTolerance);
|
||||
expectRelativeNear(
|
||||
axialRecovery.equilibriumEndActions[1U][0U],
|
||||
axialForce,
|
||||
kAnalyticalTolerance);
|
||||
expectRelativeNear(
|
||||
axialRecovery.endpointSectionResultants[0U][0U],
|
||||
axialForce,
|
||||
kAnalyticalTolerance);
|
||||
expectRelativeNear(
|
||||
axialRecovery.endpointSectionResultants[1U][0U],
|
||||
axialForce,
|
||||
kAnalyticalTolerance);
|
||||
|
||||
const double torque = -870.0;
|
||||
const Vector torsion = solveFixedFirstNode(stiffness, {0.0, 0.0, 0.0, torque, 0.0, 0.0});
|
||||
expectScaledNear(
|
||||
expectRelativeNear(
|
||||
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);
|
||||
expectRelativeNear(
|
||||
torsionRecovery.equilibriumEndActions[0U][3U],
|
||||
-torque,
|
||||
kAnalyticalTolerance);
|
||||
expectRelativeNear(
|
||||
torsionRecovery.equilibriumEndActions[1U][3U],
|
||||
torque,
|
||||
kAnalyticalTolerance);
|
||||
expectRelativeNear(
|
||||
torsionRecovery.endpointSectionResultants[0U][1U],
|
||||
torque,
|
||||
kAnalyticalTolerance);
|
||||
|
||||
const double localYForce = 640.0;
|
||||
const Vector localY = solveFixedFirstNode(stiffness, {0.0, localYForce, 0.0, 0.0, 0.0, 0.0});
|
||||
expectScaledNear(
|
||||
expectRelativeNear(
|
||||
localY[7U],
|
||||
localYForce * length * length * length /
|
||||
(3.0 * material.youngsModulus * section.i22),
|
||||
kAnalyticalTolerance);
|
||||
expectScaledNear(
|
||||
expectRelativeNear(
|
||||
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(
|
||||
expectRelativeNear(
|
||||
localYRecovery.equilibriumEndActions[0U][1U],
|
||||
-localYForce,
|
||||
kAnalyticalTolerance);
|
||||
expectRelativeNear(
|
||||
localYRecovery.equilibriumEndActions[1U][1U],
|
||||
localYForce,
|
||||
kAnalyticalTolerance);
|
||||
expectRelativeNear(
|
||||
localYRecovery.equilibriumEndActions[0U][5U],
|
||||
-localYForce * length,
|
||||
kMatrixTolerance);
|
||||
expectScaledNear(
|
||||
kAnalyticalTolerance);
|
||||
expectRelativeNear(
|
||||
localYRecovery.endpointSectionResultants[0U][3U],
|
||||
localYForce * length,
|
||||
kMatrixTolerance);
|
||||
kAnalyticalTolerance);
|
||||
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(
|
||||
expectRelativeNear(
|
||||
localZ[8U],
|
||||
localZForce * length * length * length /
|
||||
(3.0 * material.youngsModulus * section.i11),
|
||||
kAnalyticalTolerance);
|
||||
expectScaledNear(
|
||||
expectRelativeNear(
|
||||
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(
|
||||
expectRelativeNear(
|
||||
localZRecovery.equilibriumEndActions[0U][2U],
|
||||
-localZForce,
|
||||
kAnalyticalTolerance);
|
||||
expectRelativeNear(
|
||||
localZRecovery.equilibriumEndActions[1U][2U],
|
||||
localZForce,
|
||||
kAnalyticalTolerance);
|
||||
expectRelativeNear(
|
||||
localZRecovery.equilibriumEndActions[0U][4U],
|
||||
localZForce * length,
|
||||
kMatrixTolerance);
|
||||
expectScaledNear(
|
||||
kAnalyticalTolerance);
|
||||
expectRelativeNear(
|
||||
localZRecovery.endpointSectionResultants[0U][2U],
|
||||
-localZForce * length,
|
||||
kMatrixTolerance);
|
||||
kAnalyticalTolerance);
|
||||
EXPECT_NEAR(localZRecovery.endpointSectionResultants[1U][2U], 0.0, 1.0e-8);
|
||||
}
|
||||
|
||||
@@ -680,7 +931,14 @@ TEST(EulerBeam3D, RejectsInvalidGeometryAndProperties) {
|
||||
|
||||
const auto expectFailure = [](const Result<EulerBeam3D>& result,
|
||||
const std::string& code) {
|
||||
ASSERT_FALSE(result.hasValue());
|
||||
if (result.hasValue()) {
|
||||
const Matrix stiffness = result.value().localStiffness();
|
||||
ADD_FAILURE()
|
||||
<< "Invalid fixture was accepted; local stiffness finite="
|
||||
<< matrixIsFinite(stiffness)
|
||||
<< ", maximum absolute entry=" << maximumAbsoluteEntry(stiffness);
|
||||
return;
|
||||
}
|
||||
EXPECT_EQ(result.status().failureCategory(), FailureCategory::model);
|
||||
ASSERT_EQ(result.status().diagnostics().size(), 1U);
|
||||
EXPECT_EQ(result.status().diagnostics()[0U].code, code);
|
||||
@@ -746,6 +1004,42 @@ TEST(EulerBeam3D, RejectsInvalidGeometryAndProperties) {
|
||||
"invalid-beam-property");
|
||||
}
|
||||
|
||||
auto overflowMaterial = makeMaterial(
|
||||
std::numeric_limits<double>::max() / 4.0,
|
||||
0.25);
|
||||
auto overflowSection = validSection;
|
||||
overflowSection.area = 8.0;
|
||||
expectFailure(
|
||||
EulerBeam3D::create(origin, unitX, overflowSection, overflowMaterial),
|
||||
"invalid-beam-property");
|
||||
|
||||
auto underflowSection = validSection;
|
||||
underflowSection.area = std::numeric_limits<double>::denorm_min();
|
||||
underflowSection.i11 = std::numeric_limits<double>::denorm_min();
|
||||
underflowSection.i22 = std::numeric_limits<double>::denorm_min();
|
||||
underflowSection.torsionalConstant =
|
||||
std::numeric_limits<double>::denorm_min();
|
||||
expectFailure(
|
||||
EulerBeam3D::create(
|
||||
origin,
|
||||
unitX,
|
||||
underflowSection,
|
||||
makeMaterial(0.5, 0.25)),
|
||||
"invalid-beam-property");
|
||||
|
||||
auto lengthScaledSection = validSection;
|
||||
lengthScaledSection.area = 1.0;
|
||||
lengthScaledSection.i11 = 1.0;
|
||||
lengthScaledSection.i22 = 1.0;
|
||||
lengthScaledSection.torsionalConstant = 1.0;
|
||||
expectFailure(
|
||||
EulerBeam3D::create(
|
||||
origin,
|
||||
makeNode({1.0e103, 0.0, 0.0}, 2U),
|
||||
lengthScaledSection,
|
||||
makeMaterial(1.0, 0.25)),
|
||||
"invalid-beam-property");
|
||||
|
||||
auto coupledSection = validSection;
|
||||
coupledSection.i12 = 1.0e-9;
|
||||
expectFailure(
|
||||
|
||||
Reference in New Issue
Block a user