#include "fesa/elements/euler_beam_3d.hpp" #include #include #include #include #include #include #include #include #include #include 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 coordinates, std::size_t line) { return {{"Beam-1", static_cast(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 firstAxis = {0.0, 1.0, 0.0}, std::vector> 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& indices, const std::vector& 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{ 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 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 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::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 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& freeEndLoad) { std::array, 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::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, 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, 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 v = {1.2, -0.4, 0.3, -0.07}; const std::array w = {-0.8, 0.6, -0.2, 0.05}; const auto value = [](const std::array& coefficients, double x) { return coefficients[0] + coefficients[1] * x + coefficients[2] * x * x + coefficients[3] * x * x * x; }; const auto slope = [](const std::array& coefficients, double x) { return coefficients[1] + 2.0 * coefficients[2] * x + 3.0 * coefficients[3] * x * x; }; const auto curvature = [](const std::array& 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 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 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 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, 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(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(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 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& 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(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(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 expectedStrain = {epsilon, twist, kappaY, kappaZ}; const std::array 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 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