diff --git a/include/fesa/elements/mitc4_shell.hpp b/include/fesa/elements/mitc4_shell.hpp index 8ba0db9..4d970ed 100644 --- a/include/fesa/elements/mitc4_shell.hpp +++ b/include/fesa/elements/mitc4_shell.hpp @@ -2,6 +2,7 @@ #include "fesa/core/status.hpp" #include "fesa/math/matrix.hpp" +#include "fesa/math/vector.hpp" #include "fesa/model/model_types.hpp" #include @@ -39,8 +40,21 @@ struct Mitc4Stiffness { double drillingStiffness; }; -// Concrete small-rotation MITC4 kinematics, constitutive, and stiffness kernel. -// Global equation ownership, physical recovery, and assembly remain outside. +struct Mitc4PhysicalRecoveryPoint { + std::array naturalCoordinates; + Mitc4LocalFrame localFrame; + std::array generalizedStrain; + std::array sectionResultant; + std::array, 3> inPlaneStress; +}; + +struct Mitc4PhysicalRecovery { + std::array points; + double strainEnergy; +}; + +// Concrete small-rotation MITC4 kinematics, constitutive, stiffness, and +// physical-only recovery kernel. Global equation/result ownership remains outside. class Mitc4Shell { public: static Result create( @@ -73,6 +87,8 @@ public: [[nodiscard]] Matrix bendingSectionMatrix() const; [[nodiscard]] Matrix transverseShearSectionMatrix() const; [[nodiscard]] Result stiffness() const; + [[nodiscard]] Result recoverPhysical( + const Vector& globalElementDisplacement24) const; private: using Vector3 = std::array; diff --git a/src/fesa/elements/mitc4_shell.cpp b/src/fesa/elements/mitc4_shell.cpp index 45e92f1..da2f5ed 100644 --- a/src/fesa/elements/mitc4_shell.cpp +++ b/src/fesa/elements/mitc4_shell.cpp @@ -249,6 +249,15 @@ bool isFinite(const Matrix& matrix) { return true; } +bool isFinite(const Vector& vector) { + for (std::size_t index = 0U; index < vector.size(); ++index) { + if (!std::isfinite(vector[index])) { + return false; + } + } + return true; +} + Result stiffnessFailure( const SourceLocation& location, const std::string& identity, @@ -263,6 +272,20 @@ Result stiffnessFailure( std::move(message)}})); } +Result recoveryFailure( + const SourceLocation& location, + const std::string& identity, + std::string message) { + return Result::failure(Status::failure( + FailureCategory::model, + {{Severity::error, + "invalid-shell-recovery", + location, + "*ELEMENT", + identity, + std::move(message)}})); +} + } // namespace Result Mitc4Shell::create( @@ -673,6 +696,132 @@ Result Mitc4Shell::stiffness() const { drillingStiffness}); } +Result Mitc4Shell::recoverPhysical( + const Vector& globalElementDisplacement24) const { + if (globalElementDisplacement24.size() != kGlobalDofCount) { + return recoveryFailure( + sourceLocation_, identity_, + "MITC4 physical recovery requires exactly 24 global element DOFs."); + } + if (!isFinite(globalElementDisplacement24)) { + return recoveryFailure( + sourceLocation_, identity_, + "MITC4 physical recovery displacement must be finite."); + } + + const Vector physicalDisplacement = + physicalTransformation20().multiply(globalElementDisplacement24); + const Matrix tyingSamples = covariantTyingShearSamples20(); + const Matrix constitutive = materialConstitutive5(); + const Matrix planeStress = planeStressConstitutive(); + const double gauss = 1.0 / std::sqrt(3.0); + const std::array, 4> surfacePoints{ + std::array{-gauss, -gauss}, + std::array{gauss, -gauss}, + std::array{gauss, gauss}, + std::array{-gauss, gauss}}; + constexpr std::array thicknessPoints{-1.0, 1.0}; + constexpr std::array sectionPositions{-1.0, 0.0, 1.0}; + + Mitc4PhysicalRecovery recovery{}; + for (std::size_t surface = 0U; surface < surfacePoints.size(); ++surface) { + auto& point = recovery.points[surface]; + point.naturalCoordinates = surfacePoints[surface]; + GeometryData midsurfaceGeometry{}; + if (!evaluateGeometry( + point.naturalCoordinates[0], point.naturalCoordinates[1], 0.0, + midsurfaceGeometry)) { + return recoveryFailure( + sourceLocation_, identity_, + "MITC4 midsurface recovery geometry is invalid."); + } + point.localFrame = midsurfaceGeometry.frame; + + for (double thicknessSign : thicknessPoints) { + const double zeta = thicknessSign * gauss; + const Matrix strainMatrix = strainDisplacement( + point.naturalCoordinates[0], point.naturalCoordinates[1], zeta, + &tyingSamples); + const Vector strain = strainMatrix.multiply(physicalDisplacement); + const Vector stress = constitutive.multiply(strain); + GeometryData geometry{}; + if (!evaluateGeometry( + point.naturalCoordinates[0], point.naturalCoordinates[1], + zeta, geometry)) { + return recoveryFailure( + sourceLocation_, identity_, + "Validated MITC4 recovery geometry became invalid."); + } + + for (std::size_t component = 0U; component < 3U; ++component) { + point.generalizedStrain[component] += 0.5 * strain[component]; + point.generalizedStrain[3U + component] += + 3.0 * zeta * strain[component] / thickness_; + point.sectionResultant[component] += + 0.5 * thickness_ * stress[component]; + point.sectionResultant[3U + component] += + 0.25 * thickness_ * thickness_ * zeta * stress[component]; + } + for (std::size_t component = 0U; component < 2U; ++component) { + point.generalizedStrain[6U + component] += + 0.5 * strain[3U + component]; + point.sectionResultant[6U + component] += + 0.5 * thickness_ * stress[3U + component]; + } + recovery.strainEnergy += + 0.5 * strain.dot(stress) * geometry.jacobian; + } + + for (std::size_t position = 0U; + position < sectionPositions.size(); ++position) { + GeometryData sectionGeometry{}; + if (!evaluateGeometry( + point.naturalCoordinates[0], point.naturalCoordinates[1], + sectionPositions[position], sectionGeometry)) { + return recoveryFailure( + sourceLocation_, identity_, + "MITC4 section-position recovery geometry is invalid."); + } + const Vector strain = strainDisplacement( + point.naturalCoordinates[0], point.naturalCoordinates[1], + sectionPositions[position], &tyingSamples) + .multiply(physicalDisplacement); + Vector inPlaneStrain{3U}; + for (std::size_t component = 0U; component < 3U; ++component) { + inPlaneStrain[component] = strain[component]; + } + const Vector stress = planeStress.multiply(inPlaneStrain); + for (std::size_t component = 0U; component < 3U; ++component) { + point.inPlaneStress[position][component] = stress[component]; + } + } + } + + if (!std::isfinite(recovery.strainEnergy)) { + return recoveryFailure( + sourceLocation_, identity_, + "MITC4 physical strain energy must be finite."); + } + for (const auto& point : recovery.points) { + const auto finite = [](const auto& values) { + return std::all_of(values.begin(), values.end(), [](double value) { + return std::isfinite(value); + }); + }; + if (!finite(point.generalizedStrain) || + !finite(point.sectionResultant) || + std::any_of( + point.inPlaneStress.begin(), point.inPlaneStress.end(), + [&finite](const auto& stress) { return !finite(stress); })) { + return recoveryFailure( + sourceLocation_, identity_, + "MITC4 physical recovery values must be finite."); + } + } + + return Result::success(std::move(recovery)); +} + Mitc4Shell::Mitc4Shell( std::array coordinates, std::array directors, diff --git a/tests/unit/elements/mitc4_shell_test.cpp b/tests/unit/elements/mitc4_shell_test.cpp index e622023..229af97 100644 --- a/tests/unit/elements/mitc4_shell_test.cpp +++ b/tests/unit/elements/mitc4_shell_test.cpp @@ -823,3 +823,114 @@ TEST(Mitc4ShellDrilling, FailsNonfiniteReferenceAndStabilizesEachPureDrillCoordi repeatedFailure.status().diagnostics()[0].message, failure.status().diagnostics()[0].message); } + +// MITC4-KERNEL-007 +TEST(Mitc4ShellDrilling, ExcludesPureDrillFromPhysicalRecoveryAndEnergy) { + const auto nodes = planarNodes(); + const auto shellCandidate = fesa::Mitc4Shell::create( + nodePointers(nodes), directors(), section(), material()); + ASSERT_TRUE(shellCandidate.hasValue()); + const auto& shell = shellCandidate.value(); + const auto stiffnessCandidate = shell.stiffness(); + ASSERT_TRUE(stiffnessCandidate.hasValue()); + + for (std::size_t nodeIndex = 0U; nodeIndex < nodes.size(); ++nodeIndex) { + fesa::Vector pureDrill{24U}; + pureDrill[6U * nodeIndex + 5U] = 1.0; + EXPECT_GT( + stiffnessCandidate.value().stabilizedGlobal24.multiply(pureDrill).norm(), + 0.0); + + const auto recoveryCandidate = shell.recoverPhysical(pureDrill); + ASSERT_TRUE(recoveryCandidate.hasValue()); + const auto& recovery = recoveryCandidate.value(); + EXPECT_DOUBLE_EQ(recovery.strainEnergy, 0.0); + for (const auto& point : recovery.points) { + for (double value : point.generalizedStrain) { + EXPECT_DOUBLE_EQ(value, 0.0); + } + for (double value : point.sectionResultant) { + EXPECT_DOUBLE_EQ(value, 0.0); + } + for (const auto& stress : point.inPlaneStress) { + for (double value : stress) { + EXPECT_DOUBLE_EQ(value, 0.0); + } + } + } + } +} + +// MITC4-PHYSREC-001 +TEST(Mitc4ShellPhysicalRecovery, RecoversHandFieldAtFixedLocationsAndSectionPositions) { + const auto nodes = planarNodes(); + const auto shellCandidate = fesa::Mitc4Shell::create( + nodePointers(nodes), directors(), section(), material()); + ASSERT_TRUE(shellCandidate.hasValue()); + const auto& shell = shellCandidate.value(); + + constexpr std::array generalized{ + 0.1, -0.05, 0.2, 0.3, -0.15, 0.25, 0.4, -0.3}; + fesa::Vector globalField{24U}; + for (std::size_t nodeIndex = 0U; nodeIndex < nodes.size(); ++nodeIndex) { + const double x = nodes[nodeIndex].coordinates[0]; + const double y = nodes[nodeIndex].coordinates[1]; + const std::size_t offset = 6U * nodeIndex; + globalField[offset] = generalized[0] * x + 0.5 * generalized[2] * y; + globalField[offset + 1U] = + generalized[1] * y + 0.5 * generalized[2] * x; + globalField[offset + 2U] = + generalized[6] * x + generalized[7] * y - + 0.5 * generalized[5] * x * y; + globalField[offset + 3U] = + -generalized[4] * y - 0.5 * generalized[5] * x; + globalField[offset + 4U] = + generalized[3] * x + 0.5 * generalized[5] * y; + } + + const auto recoveryCandidate = shell.recoverPhysical(globalField); + ASSERT_TRUE(recoveryCandidate.hasValue()); + const auto& recovery = recoveryCandidate.value(); + const double gauss = 1.0 / std::sqrt(3.0); + const std::array, 4> expectedCoordinates{ + std::array{-gauss, -gauss}, + std::array{gauss, -gauss}, + std::array{gauss, gauss}, + std::array{-gauss, gauss}}; + constexpr std::array expectedResultant{ + 22.4, -6.4, 19.2, 22.4, -6.4, 8.0, 32.0, -24.0}; + constexpr std::array, 3> expectedStress{ + std::array{-22.4, 6.4, -2.4}, + std::array{11.2, -3.2, 9.6}, + std::array{44.8, -12.8, 21.6}}; + + ASSERT_EQ(recovery.points.size(), expectedCoordinates.size()); + for (std::size_t pointIndex = 0U; + pointIndex < recovery.points.size(); ++pointIndex) { + const auto& point = recovery.points[pointIndex]; + EXPECT_EQ(point.naturalCoordinates, expectedCoordinates[pointIndex]); + expectOrthonormalRightHanded(point.localFrame); + expectVectorNear(point.localFrame.e1, {1.0, 0.0, 0.0}); + expectVectorNear(point.localFrame.e2, {0.0, 1.0, 0.0}); + expectVectorNear(point.localFrame.e3, {0.0, 0.0, 1.0}); + for (std::size_t component = 0U; + component < generalized.size(); ++component) { + EXPECT_NEAR( + point.generalizedStrain[component], generalized[component], + 1.0e-12); + EXPECT_NEAR( + point.sectionResultant[component], expectedResultant[component], + 1.0e-12); + } + for (std::size_t position = 0U; + position < expectedStress.size(); ++position) { + for (std::size_t component = 0U; + component < expectedStress[position].size(); ++component) { + EXPECT_NEAR( + point.inPlaneStress[position][component], + expectedStress[position][component], 1.0e-12); + } + } + } + EXPECT_NEAR(recovery.strainEnergy, 72.16, 1.0e-12); +}