feat(nonlinear-static-newton-raphson): step 4 - nonlinear-mitc4-residual-and-tangent

This commit is contained in:
KOKO\Mimi
2026-08-24 14:00:06 +09:00
parent 9a829614d8
commit ab44ada4cf
4 changed files with 1190 additions and 3 deletions
+28 -2
View File
@@ -7,6 +7,7 @@
#include "fesa/core/status.h" #include "fesa/core/status.h"
#include "fesa/elements/mitc4_shell.h" #include "fesa/elements/mitc4_shell.h"
#include "fesa/math/matrix.h" #include "fesa/math/matrix.h"
#include "fesa/math/vector.h"
#include "fesa/math/vector3.h" #include "fesa/math/vector3.h"
#include "fesa/model/model_types.h" #include "fesa/model/model_types.h"
#include "fesa/results/result_records.h" #include "fesa/results/result_records.h"
@@ -35,6 +36,26 @@ struct NonlinearMitc4Recovery {
double physical_strain_energy; double physical_strain_energy;
}; };
/// @brief Stores complete internal equilibrium data for one nonlinear trial.
struct NonlinearMitc4InternalEquilibrium {
Vector internal_residual;
Matrix internal_tangent;
Vector internal_force;
NonlinearMitc4Recovery recovery;
double physical_strain_energy;
};
class NonlinearMitc4Shell;
/// @brief Evaluates the internal residual and tangent at one complete trial.
/// @param element Immutable nonlinear MITC4 reference evaluator.
/// @param trial Complete global translation and rotation trial coordinates.
/// @return Internal force, consistent tangent and physical recovery, or
/// failure.
/// @note External load vectors and their tangents are intentionally excluded.
Result<NonlinearMitc4InternalEquilibrium> EvaluateInternalResidualAndTangent(
const NonlinearMitc4Shell& element, const NonlinearMitc4TrialDofs& trial);
/// @brief Evaluates fixed-reference Total Lagrangian MITC4 kinematics. /// @brief Evaluates fixed-reference Total Lagrangian MITC4 kinematics.
/// @note This class owns no mutable trial or accepted analysis state. /// @note This class owns no mutable trial or accepted analysis state.
class NonlinearMitc4Shell { class NonlinearMitc4Shell {
@@ -68,8 +89,12 @@ class NonlinearMitc4Shell {
std::array<Matrix, 4> initial_frames, std::array<Matrix, 4> initial_frames,
std::array<Vector3, 4> initial_directors, std::array<Vector3, 4> initial_directors,
double thickness, double youngs_modulus, double thickness, double youngs_modulus,
double poisson_ratio, SourceLocation source_location, double poisson_ratio, double drilling_stiffness,
std::string identity); SourceLocation source_location, std::string identity);
friend Result<NonlinearMitc4InternalEquilibrium>
EvaluateInternalResidualAndTangent(const NonlinearMitc4Shell& element,
const NonlinearMitc4TrialDofs& trial);
std::array<Vector3, 4> coordinates_; std::array<Vector3, 4> coordinates_;
std::array<Matrix, 4> initial_frames_; std::array<Matrix, 4> initial_frames_;
@@ -77,6 +102,7 @@ class NonlinearMitc4Shell {
double thickness_; double thickness_;
double youngs_modulus_; double youngs_modulus_;
double poisson_ratio_; double poisson_ratio_;
double drilling_stiffness_;
SourceLocation source_location_; SourceLocation source_location_;
std::string identity_; std::string identity_;
}; };
+741 -1
View File
@@ -4,6 +4,8 @@
#include <array> #include <array>
#include <cmath> #include <cmath>
#include <cstddef> #include <cstddef>
#include <limits>
#include <memory>
#include <string> #include <string>
#include <utility> #include <utility>
@@ -343,19 +345,675 @@ bool IsFinite(const NonlinearMitc4Recovery& recovery) {
return true; return true;
} }
constexpr std::size_t kGlobalDofCount = 24U;
constexpr std::size_t kHessianSize = kGlobalDofCount * kGlobalDofCount;
/// @brief Carries exact first and second trial-coordinate variations.
class Hessian {
public:
Hessian() : values_{std::make_shared<std::array<double, kHessianSize>>()} {}
double& operator[](const std::size_t index) { return (*values_)[index]; }
const double& operator[](const std::size_t index) const {
return (*values_)[index];
}
auto begin() const { return values_->begin(); }
auto end() const { return values_->end(); }
private:
std::shared_ptr<std::array<double, kHessianSize>> values_;
};
/// @brief Carries exact first and second trial-coordinate variations.
struct Jet {
double value{};
std::array<double, kGlobalDofCount> first{};
Hessian second;
};
using JetVector3 = std::array<Jet, 3>;
using JetMatrix3 = std::array<std::array<Jet, 3>, 3>;
using JetTensor = std::array<std::array<Jet, 3>, 3>;
/// @brief Returns a constant second-order trial-coordinate quantity.
Jet Constant(const double value) {
Jet result{};
result.value = value;
return result;
}
/// @brief Returns one independent global trial coordinate.
Jet Variable(const double value, const std::size_t index) {
Jet result = Constant(value);
result.first[index] = 1.0;
return result;
}
/// @brief Adds two second-order trial-coordinate quantities.
Jet operator+(const Jet& left, const Jet& right) {
Jet result{};
result.value = left.value + right.value;
for (std::size_t index = 0U; index < kGlobalDofCount; ++index) {
result.first[index] = left.first[index] + right.first[index];
}
for (std::size_t index = 0U; index < kHessianSize; ++index) {
result.second[index] = left.second[index] + right.second[index];
}
return result;
}
/// @brief Subtracts two second-order trial-coordinate quantities.
Jet operator-(const Jet& left, const Jet& right) {
Jet result{};
result.value = left.value - right.value;
for (std::size_t index = 0U; index < kGlobalDofCount; ++index) {
result.first[index] = left.first[index] - right.first[index];
}
for (std::size_t index = 0U; index < kHessianSize; ++index) {
result.second[index] = left.second[index] - right.second[index];
}
return result;
}
/// @brief Multiplies two second-order trial-coordinate quantities.
Jet operator*(const Jet& left, const Jet& right) {
Jet result{};
result.value = left.value * right.value;
for (std::size_t index = 0U; index < kGlobalDofCount; ++index) {
result.first[index] =
left.first[index] * right.value + left.value * right.first[index];
}
for (std::size_t row = 0U; row < kGlobalDofCount; ++row) {
for (std::size_t column = 0U; column < kGlobalDofCount; ++column) {
const std::size_t index = row * kGlobalDofCount + column;
result.second[index] = left.second[index] * right.value +
left.first[row] * right.first[column] +
left.first[column] * right.first[row] +
left.value * right.second[index];
}
}
return result;
}
/// @brief Scales a second-order trial-coordinate quantity.
Jet operator*(const double scalar, const Jet& value) {
Jet result{};
result.value = scalar * value.value;
for (std::size_t index = 0U; index < kGlobalDofCount; ++index) {
result.first[index] = scalar * value.first[index];
}
for (std::size_t index = 0U; index < kHessianSize; ++index) {
result.second[index] = scalar * value.second[index];
}
return result;
}
/// @brief Scales a second-order trial-coordinate quantity.
Jet operator*(const Jet& value, const double scalar) { return scalar * value; }
/// @brief Applies a scalar unary function through its first two derivatives.
Jet Compose(const Jet& input, const double value, const double first,
const double second) {
Jet result{};
result.value = value;
for (std::size_t index = 0U; index < kGlobalDofCount; ++index) {
result.first[index] = first * input.first[index];
}
for (std::size_t row = 0U; row < kGlobalDofCount; ++row) {
for (std::size_t column = 0U; column < kGlobalDofCount; ++column) {
const std::size_t index = row * kGlobalDofCount + column;
result.second[index] = first * input.second[index] +
second * input.first[row] * input.first[column];
}
}
return result;
}
/// @brief Forms an analytic reciprocal without a secant approximation.
Jet Reciprocal(const Jet& value) {
const double inverse = 1.0 / value.value;
return Compose(value, inverse, -inverse * inverse,
2.0 * inverse * inverse * inverse);
}
/// @brief Divides two second-order trial-coordinate quantities.
Jet operator/(const Jet& left, const Jet& right) {
return left * Reciprocal(right);
}
/// @brief Computes the analytic square root variation.
Jet Sqrt(const Jet& value) {
const double root = std::sqrt(value.value);
return Compose(value, root, 0.5 / root, -0.25 / (root * root * root));
}
/// @brief Computes the analytic sine variation.
Jet Sin(const Jet& value) {
return Compose(value, std::sin(value.value), std::cos(value.value),
-std::sin(value.value));
}
/// @brief Computes the analytic cosine variation.
Jet Cos(const Jet& value) {
return Compose(value, std::cos(value.value), -std::sin(value.value),
-std::cos(value.value));
}
/// @brief Computes the two-argument angle and its analytic first/second map.
Jet Atan2(const Jet& y, const Jet& x) {
const double radius_squared = x.value * x.value + y.value * y.value;
Jet result{};
result.value = std::atan2(y.value, x.value);
const double derivative_y = x.value / radius_squared;
const double derivative_x = -y.value / radius_squared;
const double radius_fourth = radius_squared * radius_squared;
const double derivative_yy = -2.0 * x.value * y.value / radius_fourth;
const double derivative_yx =
(y.value * y.value - x.value * x.value) / radius_fourth;
const double derivative_xx = 2.0 * x.value * y.value / radius_fourth;
for (std::size_t index = 0U; index < kGlobalDofCount; ++index) {
result.first[index] =
derivative_y * y.first[index] + derivative_x * x.first[index];
}
for (std::size_t row = 0U; row < kGlobalDofCount; ++row) {
for (std::size_t column = 0U; column < kGlobalDofCount; ++column) {
const std::size_t index = row * kGlobalDofCount + column;
result.second[index] = derivative_y * y.second[index] +
derivative_x * x.second[index] +
derivative_yy * y.first[row] * y.first[column] +
derivative_yx * (y.first[row] * x.first[column] +
x.first[row] * y.first[column]) +
derivative_xx * x.first[row] * x.first[column];
}
}
return result;
}
/// @brief Checks every coefficient in an automatic-differentiation quantity.
bool IsFinite(const Jet& value) {
if (!std::isfinite(value.value)) {
return false;
}
return std::all_of(value.first.begin(), value.first.end(),
[](const double coefficient) {
return std::isfinite(coefficient);
}) &&
std::all_of(value.second.begin(), value.second.end(),
[](const double coefficient) {
return std::isfinite(coefficient);
});
}
/// @brief Forms a Jet vector from a fixed reference vector.
JetVector3 ConstantVector(const Vector3& value) {
return {Constant(value.X()), Constant(value.Y()), Constant(value.Z())};
}
/// @brief Adds two Jet vectors.
JetVector3 Add(const JetVector3& left, const JetVector3& right) {
return {left[0U] + right[0U], left[1U] + right[1U], left[2U] + right[2U]};
}
/// @brief Subtracts two Jet vectors.
JetVector3 Subtract(const JetVector3& left, const JetVector3& right) {
return {left[0U] - right[0U], left[1U] - right[1U], left[2U] - right[2U]};
}
/// @brief Scales a Jet vector.
JetVector3 Scale(const Jet& scalar, const JetVector3& vector) {
return {scalar * vector[0U], scalar * vector[1U], scalar * vector[2U]};
}
/// @brief Scales a Jet vector by a fixed value.
JetVector3 Scale(const double scalar, const JetVector3& vector) {
return {scalar * vector[0U], scalar * vector[1U], scalar * vector[2U]};
}
/// @brief Computes the Jet dot product.
Jet Dot(const JetVector3& left, const JetVector3& right) {
return left[0U] * right[0U] + left[1U] * right[1U] + left[2U] * right[2U];
}
/// @brief Computes the Jet right-handed cross product.
JetVector3 Cross(const JetVector3& left, const JetVector3& right) {
return {left[1U] * right[2U] - left[2U] * right[1U],
left[2U] * right[0U] - left[0U] * right[2U],
left[0U] * right[1U] - left[1U] * right[0U]};
}
/// @brief Builds a Jet identity matrix.
JetMatrix3 Identity() {
JetMatrix3 result{};
for (std::size_t index = 0U; index < 3U; ++index) {
result[index][index] = Constant(1.0);
}
return result;
}
/// @brief Forms a skew matrix from a Jet vector.
JetMatrix3 Skew(const JetVector3& value) {
JetMatrix3 result{};
result[0U][1U] = -1.0 * value[2U];
result[0U][2U] = value[1U];
result[1U][0U] = value[2U];
result[1U][2U] = -1.0 * value[0U];
result[2U][0U] = -1.0 * value[1U];
result[2U][1U] = value[0U];
return result;
}
/// @brief Multiplies two three-by-three Jet matrices.
JetMatrix3 Multiply(const JetMatrix3& left, const JetMatrix3& right) {
JetMatrix3 result{};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
for (std::size_t index = 0U; index < 3U; ++index) {
result[row][column] =
result[row][column] + left[row][index] * right[index][column];
}
}
}
return result;
}
/// @brief Multiplies a Jet matrix by a Jet vector.
JetVector3 Multiply(const JetMatrix3& matrix, const JetVector3& vector) {
JetVector3 result{};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
result[row] = result[row] + matrix[row][column] * vector[column];
}
}
return result;
}
/// @brief Transposes a three-by-three Jet matrix.
JetMatrix3 Transpose(const JetMatrix3& matrix) {
JetMatrix3 result{};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
result[row][column] = matrix[column][row];
}
}
return result;
}
/// @brief Returns a scalar multiple of a Jet matrix.
JetMatrix3 Scale(const double scalar, const JetMatrix3& matrix) {
JetMatrix3 result{};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
result[row][column] = scalar * matrix[row][column];
}
}
return result;
}
/// @brief Returns a trial-coordinate scalar multiple of a Jet matrix.
JetMatrix3 Scale(const Jet& scalar, const JetMatrix3& matrix) {
JetMatrix3 result{};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
result[row][column] = scalar * matrix[row][column];
}
}
return result;
}
/// @brief Adds two three-by-three Jet matrices.
JetMatrix3 Add(const JetMatrix3& left, const JetMatrix3& right) {
JetMatrix3 result{};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
result[row][column] = left[row][column] + right[row][column];
}
}
return result;
}
/// @brief Computes the analytic three-by-three inverse through its adjugate.
JetMatrix3 Inverse(const JetMatrix3& matrix) {
JetMatrix3 cofactor{};
cofactor[0U][0U] =
matrix[1U][1U] * matrix[2U][2U] - matrix[1U][2U] * matrix[2U][1U];
cofactor[0U][1U] =
matrix[1U][2U] * matrix[2U][0U] - matrix[1U][0U] * matrix[2U][2U];
cofactor[0U][2U] =
matrix[1U][0U] * matrix[2U][1U] - matrix[1U][1U] * matrix[2U][0U];
cofactor[1U][0U] =
matrix[0U][2U] * matrix[2U][1U] - matrix[0U][1U] * matrix[2U][2U];
cofactor[1U][1U] =
matrix[0U][0U] * matrix[2U][2U] - matrix[0U][2U] * matrix[2U][0U];
cofactor[1U][2U] =
matrix[0U][1U] * matrix[2U][0U] - matrix[0U][0U] * matrix[2U][1U];
cofactor[2U][0U] =
matrix[0U][1U] * matrix[1U][2U] - matrix[0U][2U] * matrix[1U][1U];
cofactor[2U][1U] =
matrix[0U][2U] * matrix[1U][0U] - matrix[0U][0U] * matrix[1U][2U];
cofactor[2U][2U] =
matrix[0U][0U] * matrix[1U][1U] - matrix[0U][1U] * matrix[1U][0U];
const Jet determinant = matrix[0U][0U] * cofactor[0U][0U] +
matrix[0U][1U] * cofactor[0U][1U] +
matrix[0U][2U] * cofactor[0U][2U];
JetMatrix3 result{};
const Jet inverse_determinant = Reciprocal(determinant);
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
result[row][column] = cofactor[column][row] * inverse_determinant;
}
}
return result;
}
/// @brief Evaluates Rodrigues' map with an analytic near-zero series branch.
JetMatrix3 ExpSkew(const JetVector3& rotation) {
const Jet squared_norm = Dot(rotation, rotation);
Jet sine_over_norm{};
Jet one_minus_cosine_over_norm_squared{};
if (squared_norm.value < 1.0e-8) {
const Jet squared = squared_norm * squared_norm;
const Jet cubed = squared * squared_norm;
sine_over_norm = Constant(1.0) - squared_norm / Constant(6.0) +
squared / Constant(120.0) - cubed / Constant(5040.0);
one_minus_cosine_over_norm_squared =
Constant(0.5) - squared_norm / Constant(24.0) +
squared / Constant(720.0) - cubed / Constant(40320.0);
} else {
const Jet norm = Sqrt(squared_norm);
sine_over_norm = Sin(norm) / norm;
one_minus_cosine_over_norm_squared =
(Constant(1.0) - Cos(norm)) / squared_norm;
}
const JetMatrix3 skew = Skew(rotation);
return Add(Add(Identity(), Scale(sine_over_norm, skew)),
Scale(one_minus_cosine_over_norm_squared, Multiply(skew, skew)));
}
/// @brief Stores the complete Jet-valued global element trial coordinates.
struct JetTrial {
std::array<JetVector3, kNodeCount> displacement;
std::array<JetVector3, kNodeCount> rotation;
std::array<JetVector3, kNodeCount> director;
};
/// @brief Lifts one validated nonlinear trial into independent Jet variables.
JetTrial MakeJetTrial(const NonlinearMitc4TrialDofs& trial,
const std::array<Matrix, kNodeCount>& initial_frames) {
JetTrial result{};
for (std::size_t node = 0U; node < kNodeCount; ++node) {
for (std::size_t component = 0U; component < 3U; ++component) {
result.displacement[node][component] =
Variable(trial.displacement[node][component], 6U * node + component);
result.rotation[node][component] = Variable(
trial.rotation_vector[node][component], 6U * node + 3U + component);
}
JetMatrix3 frame{};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
frame[row][column] = Constant(initial_frames[node](row, column));
}
}
const JetVector3 initial_director{frame[0U][2U], frame[1U][2U],
frame[2U][2U]};
result.director[node] =
Multiply(ExpSkew(result.rotation[node]), initial_director);
}
return result;
}
/// @brief Forms one current covariant basis from the exact trial variables.
std::array<JetVector3, 3> CurrentCovariant(
const std::array<Vector3, kNodeCount>& coordinates, const JetTrial& trial,
const double thickness, const double xi, const double eta,
const double zeta) {
const Mitc4ShapeFunctions shape = Mitc4Shell::ShapeFunctions(xi, eta);
JetVector3 displacement_xi{};
JetVector3 displacement_eta{};
JetVector3 director{};
JetVector3 director_xi{};
JetVector3 director_eta{};
Vector3 reference_xi{};
Vector3 reference_eta{};
for (std::size_t node = 0U; node < kNodeCount; ++node) {
reference_xi =
reference_xi + shape.xi_derivatives[node] * coordinates[node];
reference_eta =
reference_eta + shape.eta_derivatives[node] * coordinates[node];
displacement_xi = Add(displacement_xi, Scale(shape.xi_derivatives[node],
trial.displacement[node]));
displacement_eta = Add(displacement_eta, Scale(shape.eta_derivatives[node],
trial.displacement[node]));
director = Add(director, Scale(shape.values[node], trial.director[node]));
director_xi = Add(director_xi,
Scale(shape.xi_derivatives[node], trial.director[node]));
director_eta = Add(
director_eta, Scale(shape.eta_derivatives[node], trial.director[node]));
}
const double half_thickness = 0.5 * thickness;
return {Add(Add(ConstantVector(reference_xi), displacement_xi),
Scale(half_thickness * zeta, director_xi)),
Add(Add(ConstantVector(reference_eta), displacement_eta),
Scale(half_thickness * zeta, director_eta)),
Scale(half_thickness, director)};
}
/// @brief Forms the direct covariant Green--Lagrange strain tensor.
JetTensor DirectStrain(const std::array<JetVector3, 3>& current,
const Geometry& reference) {
JetTensor result{};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
result[row][column] =
0.5 * (Dot(current[row], current[column]) -
Constant(reference.reference_covariant[row].Dot(
reference.reference_covariant[column])));
}
}
result[2U][2U] = Constant(0.0);
return result;
}
/// @brief Reconstructs a fixed-reference Cartesian Jet strain tensor.
JetTensor CartesianStrain(const JetTensor& covariant,
const Geometry& reference) {
JetTensor result{};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
for (std::size_t first = 0U; first < 3U; ++first) {
for (std::size_t second = 0U; second < 3U; ++second) {
result[row][column] =
result[row][column] + reference.reciprocal[first][row] *
reference.reciprocal[second][column] *
covariant[first][second];
}
}
}
}
return result;
}
/// @brief Forms one fixed-frame engineering strain component.
Jet FrameComponent(const JetVector3& left, const JetTensor& tensor,
const JetVector3& right) {
Jet result{};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
result = result + left[row] * tensor[row][column] * right[column];
}
}
return result;
}
/// @brief Evaluates the physical strain energy through exact Jet variations.
Jet PhysicalEnergy(const std::array<Vector3, kNodeCount>& coordinates,
const std::array<Vector3, kNodeCount>& initial_directors,
const JetTrial& trial, const double thickness,
const double youngs, const double poisson, bool& valid) {
const NonlinearMitc4TrialDofs reference_trial{};
constexpr std::array<std::array<double, 2>, 4> tying_locations{
std::array<double, 2>{0.0, -1.0}, std::array<double, 2>{0.0, 1.0},
std::array<double, 2>{-1.0, 0.0}, std::array<double, 2>{1.0, 0.0}};
std::array<JetTensor, 4> tying{};
for (std::size_t point = 0U; point < tying_locations.size(); ++point) {
Geometry reference{};
if (!EvaluateGeometry(coordinates, initial_directors, initial_directors,
reference_trial, thickness,
tying_locations[point][0U],
tying_locations[point][1U], 0.0, reference)) {
valid = false;
return {};
}
tying[point] =
DirectStrain(CurrentCovariant(coordinates, trial, thickness,
tying_locations[point][0U],
tying_locations[point][1U], 0.0),
reference);
}
const double gauss = 1.0 / std::sqrt(3.0);
const std::array<std::array<double, 2>, 4> surface_points{
std::array<double, 2>{-gauss, -gauss},
std::array<double, 2>{gauss, -gauss}, std::array<double, 2>{gauss, gauss},
std::array<double, 2>{-gauss, gauss}};
const std::array<double, 2> thickness_points{-gauss, gauss};
const double plane_stress = youngs / (1.0 - poisson * poisson);
const double shear = kShearCorrection * youngs / (2.0 * (1.0 + poisson));
Jet energy{};
for (const auto& surface : surface_points) {
for (const double zeta : thickness_points) {
Geometry reference{};
if (!EvaluateGeometry(coordinates, initial_directors, initial_directors,
reference_trial, thickness, surface[0U],
surface[1U], zeta, reference)) {
valid = false;
return {};
}
JetTensor covariant =
DirectStrain(CurrentCovariant(coordinates, trial, thickness,
surface[0U], surface[1U], zeta),
reference);
const Mitc4TyingWeights weights =
Mitc4Shell::TyingWeights(surface[0U], surface[1U]);
covariant[0U][2U] = weights.xi_zeta[0U] * tying[0U][0U][2U] +
weights.xi_zeta[1U] * tying[1U][0U][2U];
covariant[2U][0U] = covariant[0U][2U];
covariant[1U][2U] = weights.eta_zeta[0U] * tying[2U][1U][2U] +
weights.eta_zeta[1U] * tying[3U][1U][2U];
covariant[2U][1U] = covariant[1U][2U];
const JetTensor cartesian = CartesianStrain(covariant, reference);
const JetVector3 e1 = ConstantVector(Vector3{reference.local_frame.e1});
const JetVector3 e2 = ConstantVector(Vector3{reference.local_frame.e2});
const JetVector3 e3 = ConstantVector(Vector3{reference.local_frame.e3});
const std::array<Jet, 5> strain{FrameComponent(e1, cartesian, e1),
FrameComponent(e2, cartesian, e2),
2.0 * FrameComponent(e1, cartesian, e2),
2.0 * FrameComponent(e1, cartesian, e3),
2.0 * FrameComponent(e2, cartesian, e3)};
const std::array<Jet, 5> stress{
plane_stress * (strain[0U] + poisson * strain[1U]),
plane_stress * (poisson * strain[0U] + strain[1U]),
plane_stress * (1.0 - poisson) * 0.5 * strain[2U], shear * strain[3U],
shear * strain[4U]};
Jet density{};
for (std::size_t component = 0U; component < strain.size(); ++component) {
density = density + strain[component] * stress[component];
}
energy = energy + 0.5 * reference.reference_jacobian * density;
}
}
if (!IsFinite(energy)) {
valid = false;
}
return energy;
}
/// @brief Computes a fixed-iteration analytic polar factor for drill
/// corotation.
JetMatrix3 PolarRotation(const JetMatrix3& deformation) {
JetMatrix3 result = deformation;
for (std::size_t iteration = 0U; iteration < 12U; ++iteration) {
result = Scale(0.5, Add(result, Transpose(Inverse(result))));
}
return result;
}
/// @brief Evaluates the objective numerical drilling potential and variations.
Jet DrillingEnergy(const std::array<Vector3, kNodeCount>& coordinates,
const std::array<Vector3, kNodeCount>& initial_directors,
const JetTrial& trial, const double thickness,
const double drilling_stiffness, bool& valid) {
const NonlinearMitc4TrialDofs reference_trial{};
Geometry reference{};
if (!EvaluateGeometry(coordinates, initial_directors, initial_directors,
reference_trial, thickness, 0.0, 0.0, 0.0, reference)) {
valid = false;
return {};
}
const auto current =
CurrentCovariant(coordinates, trial, thickness, 0.0, 0.0, 0.0);
JetMatrix3 deformation{};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
for (std::size_t alpha = 0U; alpha < 3U; ++alpha) {
deformation[row][column] =
deformation[row][column] +
current[alpha][row] * reference.reciprocal[alpha][column];
}
}
}
const JetMatrix3 element_rotation = PolarRotation(deformation);
Jet energy{};
for (std::size_t node = 0U; node < kNodeCount; ++node) {
const JetMatrix3 nodal_rotation = ExpSkew(trial.rotation[node]);
const JetMatrix3 corotated =
Multiply(Transpose(element_rotation), nodal_rotation);
const JetVector3 initial_director = ConstantVector(initial_directors[node]);
const JetVector3 swung_director = Multiply(corotated, initial_director);
const Jet alignment = Dot(initial_director, swung_director);
if (!std::isfinite(alignment.value) ||
!(alignment.value > -1.0 + kDirectorOppositionTolerance)) {
valid = false;
return {};
}
const JetVector3 swing_axis = Cross(initial_director, swung_director);
const JetMatrix3 swing_skew = Skew(swing_axis);
const JetMatrix3 swing = Add(Add(Identity(), swing_skew),
Scale(Reciprocal(Constant(1.0) + alignment),
Multiply(swing_skew, swing_skew)));
const JetMatrix3 twist = Multiply(Transpose(swing), corotated);
const JetVector3 twist_vex{twist[2U][1U] - twist[1U][2U],
twist[0U][2U] - twist[2U][0U],
twist[1U][0U] - twist[0U][1U]};
const Jet sine = 0.5 * Dot(initial_director, twist_vex);
const Jet cosine =
0.5 * (twist[0U][0U] + twist[1U][1U] + twist[2U][2U] - Constant(1.0));
const Jet gamma = Atan2(sine, cosine);
energy = energy + 0.5 * drilling_stiffness * gamma * gamma;
}
if (!IsFinite(energy)) {
valid = false;
}
return energy;
}
} // namespace } // namespace
NonlinearMitc4Shell::NonlinearMitc4Shell( NonlinearMitc4Shell::NonlinearMitc4Shell(
std::array<Vector3, 4> coordinates, std::array<Matrix, 4> initial_frames, std::array<Vector3, 4> coordinates, std::array<Matrix, 4> initial_frames,
std::array<Vector3, 4> initial_directors, const double thickness, std::array<Vector3, 4> initial_directors, const double thickness,
const double youngs_modulus, const double poisson_ratio, const double youngs_modulus, const double poisson_ratio,
SourceLocation source_location, std::string identity) const double drilling_stiffness, SourceLocation source_location,
std::string identity)
: coordinates_{std::move(coordinates)}, : coordinates_{std::move(coordinates)},
initial_frames_{std::move(initial_frames)}, initial_frames_{std::move(initial_frames)},
initial_directors_{std::move(initial_directors)}, initial_directors_{std::move(initial_directors)},
thickness_{thickness}, thickness_{thickness},
youngs_modulus_{youngs_modulus}, youngs_modulus_{youngs_modulus},
poisson_ratio_{poisson_ratio}, poisson_ratio_{poisson_ratio},
drilling_stiffness_{drilling_stiffness},
source_location_{std::move(source_location)}, source_location_{std::move(source_location)},
identity_{std::move(identity)} {} identity_{std::move(identity)} {}
@@ -448,12 +1106,31 @@ Result<NonlinearMitc4Shell> NonlinearMitc4Shell::Create(
"Nonlinear MITC4 requires finite E>0 and -1<nu<0.5."); "Nonlinear MITC4 requires finite E>0 and -1<nu<0.5.");
} }
std::array<std::array<double, 3>, kNodeCount> director_components{};
for (std::size_t node = 0U; node < kNodeCount; ++node) {
director_components[node] = directors[node].Components();
}
const auto linear_shell =
Mitc4Shell::Create(nodes, director_components, section, material);
if (!linear_shell.HasValue()) {
return Result<NonlinearMitc4Shell>::Failure(linear_shell.GetStatus());
}
const auto linear_stiffness = linear_shell.Value().Stiffness();
if (!linear_stiffness.HasValue() ||
!std::isfinite(linear_stiffness.Value().drilling_stiffness) ||
!(linear_stiffness.Value().drilling_stiffness > 0.0)) {
return Failure<NonlinearMitc4Shell>(
"invalid-nonlinear-shell-drilling", section.location, identity,
"Nonlinear MITC4 requires a finite positive fixed drilling stiffness.");
}
NonlinearMitc4Shell shell{coordinates, NonlinearMitc4Shell shell{coordinates,
std::move(frames), std::move(frames),
directors, directors,
section.thickness, section.thickness,
material.youngs_modulus, material.youngs_modulus,
material.poisson_ratio, material.poisson_ratio,
linear_stiffness.Value().drilling_stiffness,
nodes[0U]->location, nodes[0U]->location,
identity}; identity};
const auto reference = shell.EvaluateKinematics(NonlinearMitc4TrialDofs{}); const auto reference = shell.EvaluateKinematics(NonlinearMitc4TrialDofs{});
@@ -611,4 +1288,67 @@ Result<NonlinearMitc4Recovery> NonlinearMitc4Shell::RecoverState(
return EvaluateKinematics(trial); return EvaluateKinematics(trial);
} }
Result<NonlinearMitc4InternalEquilibrium> EvaluateInternalResidualAndTangent(
const NonlinearMitc4Shell& element, const NonlinearMitc4TrialDofs& trial) {
const auto recovery = element.EvaluateKinematics(trial);
if (!recovery.HasValue()) {
return Result<NonlinearMitc4InternalEquilibrium>::Failure(
recovery.GetStatus());
}
bool valid = true;
const JetTrial jet_trial = MakeJetTrial(trial, element.initial_frames_);
const Jet physical =
PhysicalEnergy(element.coordinates_, element.initial_directors_,
jet_trial, element.thickness_, element.youngs_modulus_,
element.poisson_ratio_, valid);
const Jet drilling = DrillingEnergy(
element.coordinates_, element.initial_directors_, jet_trial,
element.thickness_, element.drilling_stiffness_, valid);
const Jet internal_energy = physical + drilling;
if (!valid || !IsFinite(internal_energy)) {
return Failure<NonlinearMitc4InternalEquilibrium>(
"invalid-nonlinear-shell-equilibrium", element.source_location_,
element.identity_,
"Nonlinear MITC4 internal residual and tangent must remain finite.");
}
Vector internal_residual{kGlobalDofCount};
Matrix internal_tangent{kGlobalDofCount, kGlobalDofCount};
for (std::size_t row = 0U; row < kGlobalDofCount; ++row) {
internal_residual[row] = internal_energy.first[row];
for (std::size_t column = 0U; column < kGlobalDofCount; ++column) {
internal_tangent(row, column) =
internal_energy.second[row * kGlobalDofCount + column];
}
}
for (std::size_t row = 0U; row < kGlobalDofCount; ++row) {
if (!std::isfinite(internal_residual[row])) {
return Failure<NonlinearMitc4InternalEquilibrium>(
"invalid-nonlinear-shell-equilibrium", element.source_location_,
element.identity_,
"Nonlinear MITC4 internal residual and tangent must remain finite.");
}
for (std::size_t column = 0U; column < kGlobalDofCount; ++column) {
if (!std::isfinite(internal_tangent(row, column))) {
return Failure<NonlinearMitc4InternalEquilibrium>(
"invalid-nonlinear-shell-equilibrium", element.source_location_,
element.identity_,
"Nonlinear MITC4 internal residual and tangent must remain "
"finite.");
}
}
}
NonlinearMitc4InternalEquilibrium equilibrium{
std::move(internal_residual), std::move(internal_tangent),
Vector{kGlobalDofCount}, recovery.Value(),
recovery.Value().physical_strain_energy};
for (std::size_t index = 0U; index < kGlobalDofCount; ++index) {
equilibrium.internal_force[index] = equilibrium.internal_residual[index];
}
return Result<NonlinearMitc4InternalEquilibrium>::Success(
std::move(equilibrium));
}
} // namespace fesa } // namespace fesa
+1
View File
@@ -21,6 +21,7 @@ add_executable(
unit/elements/finite_rotation_test.cpp unit/elements/finite_rotation_test.cpp
unit/elements/mitc4_shell_test.cpp unit/elements/mitc4_shell_test.cpp
unit/elements/nonlinear_mitc4_shell_test.cpp unit/elements/nonlinear_mitc4_shell_test.cpp
unit/elements/nonlinear_mitc4_shell_tangent_test.cpp
unit/fem/dof_manager_test.cpp unit/fem/dof_manager_test.cpp
unit/math/dense_blas_internal_test.cpp unit/math/dense_blas_internal_test.cpp
unit/math/matrix_test.cpp unit/math/matrix_test.cpp
@@ -0,0 +1,420 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <stdexcept>
#include <string>
#include "fesa/elements/finite_rotation.h"
#include "fesa/elements/nonlinear_mitc4_shell.h"
#include "fesa/model/model_types.h"
namespace {
using Coordinates = std::array<double, 3>;
fesa::Node Node(const std::int64_t label, const Coordinates coordinates) {
return {{"Shell-Instance", label, std::to_string(label)},
coordinates,
{"nonlinear-mitc4-tangent.inp", static_cast<std::size_t>(label)}};
}
std::array<fesa::Node, 4> Nodes() {
return {Node(1, {-1.0, -1.0, 0.0}), Node(2, {1.0, -1.0, 0.0}),
Node(3, {1.0, 1.0, 0.0}), Node(4, {-1.0, 1.0, 0.0})};
}
std::array<const fesa::Node*, 4> NodePointers(
const std::array<fesa::Node, 4>& nodes) {
return {&nodes[0], &nodes[1], &nodes[2], &nodes[3]};
}
std::array<fesa::ShellNodeInitialFrame, 4> Frames() {
return {{{0U, {0.0, 0.0, 1.0}, {1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}},
{1U, {0.0, 0.0, 1.0}, {1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}},
{2U, {0.0, 0.0, 1.0}, {1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}},
{3U, {0.0, 0.0, 1.0}, {1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}}}};
}
fesa::NonlinearMitc4Shell Shell(const std::array<fesa::Node, 4>& nodes) {
const fesa::ShellSection section{
"Section", 0.2, 0U, {"nonlinear-mitc4-tangent.inp", 20U}};
const fesa::LinearElasticMaterial material{
"Material", 120.0, 0.25, {"nonlinear-mitc4-tangent.inp", 21U}};
const auto shell = fesa::NonlinearMitc4Shell::Create(
NodePointers(nodes), Frames(), section, material);
if (!shell.HasValue()) {
throw std::runtime_error{shell.GetStatus().Diagnostics()[0U].code};
}
return shell.Value();
}
fesa::NonlinearMitc4TrialDofs Trial() {
fesa::NonlinearMitc4TrialDofs trial{};
trial.displacement[0U] = {-0.02, 0.01, 0.03};
trial.displacement[1U] = {0.01, -0.03, -0.01};
trial.displacement[2U] = {0.03, 0.02, 0.04};
trial.displacement[3U] = {-0.01, 0.04, -0.02};
trial.rotation_vector[0U] = {0.03, -0.02, 0.01};
trial.rotation_vector[1U] = {-0.01, 0.04, -0.02};
trial.rotation_vector[2U] = {0.02, 0.01, 0.03};
trial.rotation_vector[3U] = {-0.03, 0.02, -0.01};
return trial;
}
fesa::NonlinearMitc4TrialDofs AddScaled(
const fesa::NonlinearMitc4TrialDofs& trial,
const fesa::NonlinearMitc4TrialDofs& direction, const double scale) {
fesa::NonlinearMitc4TrialDofs result = trial;
for (std::size_t node = 0U; node < 4U; ++node) {
result.displacement[node] =
result.displacement[node] + scale * direction.displacement[node];
result.rotation_vector[node] =
result.rotation_vector[node] + scale * direction.rotation_vector[node];
}
return result;
}
fesa::NonlinearMitc4TrialDofs Direction() {
fesa::NonlinearMitc4TrialDofs direction{};
direction.displacement[0U] = {0.5, -0.2, 0.1};
direction.displacement[1U] = {-0.1, 0.3, -0.4};
direction.displacement[2U] = {0.2, 0.1, 0.5};
direction.displacement[3U] = {-0.3, -0.4, -0.2};
direction.rotation_vector[0U] = {0.2, -0.1, 0.3};
direction.rotation_vector[1U] = {-0.3, 0.2, -0.1};
direction.rotation_vector[2U] = {0.1, 0.3, -0.2};
direction.rotation_vector[3U] = {-0.2, -0.3, 0.1};
return direction;
}
fesa::Vector ToVector(const fesa::NonlinearMitc4TrialDofs& trial) {
fesa::Vector result{24U};
for (std::size_t node = 0U; node < 4U; ++node) {
for (std::size_t component = 0U; component < 3U; ++component) {
result[6U * node + component] = trial.displacement[node][component];
result[6U * node + 3U + component] =
trial.rotation_vector[node][component];
}
}
return result;
}
double ReferenceLength(const std::array<fesa::Node, 4>& nodes) {
double length = 0.0;
for (std::size_t first = 0U; first < nodes.size(); ++first) {
for (std::size_t second = first + 1U; second < nodes.size(); ++second) {
length = (std::max)(length, (fesa::Vector3{nodes[first].coordinates} -
fesa::Vector3{nodes[second].coordinates})
.Norm());
}
}
return length;
}
fesa::NonlinearMitc4TrialDofs WorkConjugateDirection(
const std::array<fesa::Node, 4>& nodes) {
auto direction = Direction();
double maximum_component = 0.0;
for (std::size_t node = 0U; node < 4U; ++node) {
for (std::size_t component = 0U; component < 3U; ++component) {
maximum_component =
(std::max)(maximum_component,
std::abs(direction.displacement[node][component]));
maximum_component =
(std::max)(maximum_component,
std::abs(direction.rotation_vector[node][component]));
}
}
const double length = ReferenceLength(nodes);
for (std::size_t node = 0U; node < 4U; ++node) {
direction.displacement[node] =
length * direction.displacement[node] / maximum_component;
direction.rotation_vector[node] =
direction.rotation_vector[node] / maximum_component;
}
return direction;
}
fesa::Vector WorkConjugate(const fesa::Vector& vector, const double length) {
fesa::Vector result = vector;
for (std::size_t node = 0U; node < 4U; ++node) {
for (std::size_t component = 0U; component < 3U; ++component) {
result[6U * node + component] *= length;
}
}
return result;
}
double ChannelNorm(const fesa::Vector& vector, const bool translation) {
double sum = 0.0;
const std::size_t offset = translation ? 0U : 3U;
for (std::size_t node = 0U; node < 4U; ++node) {
for (std::size_t component = 0U; component < 3U; ++component) {
const double value = vector[6U * node + offset + component];
sum += value * value;
}
}
return std::sqrt(sum);
}
double FrobeniusNorm(const fesa::Matrix& matrix) {
double sum = 0.0;
for (std::size_t row = 0U; row < matrix.Rows(); ++row) {
for (std::size_t column = 0U; column < matrix.Columns(); ++column) {
sum += matrix(row, column) * matrix(row, column);
}
}
return std::sqrt(sum);
}
fesa::Matrix WorkConjugateTangent(const fesa::Matrix& tangent,
const double length) {
fesa::Matrix result = tangent;
for (std::size_t row = 0U; row < tangent.Rows(); ++row) {
const double row_scale = row % 6U < 3U ? length : 1.0;
for (std::size_t column = 0U; column < tangent.Columns(); ++column) {
const double column_scale = column % 6U < 3U ? length : 1.0;
result(row, column) *= row_scale * column_scale;
}
}
return result;
}
void ExpectRelative(const double actual, const double expected) {
if (expected == 0.0) {
EXPECT_LE(std::abs(actual), 1.0e-12);
return;
}
EXPECT_LE(std::abs(actual - expected) / std::abs(expected), 1.0e-9);
}
// NSNR-ELEM-TAN-001
TEST(NonlinearMitc4Residual, EvaluatesResidualTangentAndRecoveryAtOneTrial) {
const auto nodes = Nodes();
const auto shell = Shell(nodes);
const auto trial = Trial();
const auto recovery = shell.RecoverState(trial);
const auto result = fesa::EvaluateInternalResidualAndTangent(shell, trial);
ASSERT_TRUE(recovery.HasValue());
ASSERT_TRUE(result.HasValue());
EXPECT_EQ(result.Value().internal_residual.Size(), 24U);
EXPECT_EQ(result.Value().internal_tangent.Rows(), 24U);
EXPECT_EQ(result.Value().internal_tangent.Columns(), 24U);
EXPECT_DOUBLE_EQ(result.Value().physical_strain_energy,
result.Value().recovery.physical_strain_energy);
EXPECT_DOUBLE_EQ(result.Value().recovery.physical_strain_energy,
recovery.Value().physical_strain_energy);
for (std::size_t index = 0U; index < 24U; ++index) {
EXPECT_DOUBLE_EQ(result.Value().internal_residual[index],
result.Value().internal_force[index]);
}
for (std::size_t point = 0U;
point < result.Value().recovery.gauss_points.size(); ++point) {
EXPECT_EQ(result.Value().recovery.gauss_points[point].generalized_strain,
recovery.Value().gauss_points[point].generalized_strain);
EXPECT_EQ(result.Value().recovery.gauss_points[point].section_resultant,
recovery.Value().gauss_points[point].section_resultant);
}
}
// NSNR-ELEM-TAN-002 and NSNR-ELEM-TAN-003
TEST(NonlinearMitc4Tangent, MatchesTheApprovedDirectionalSchedule) {
const auto nodes = Nodes();
const auto shell = Shell(nodes);
const auto trial = Trial();
const auto direction = WorkConjugateDirection(nodes);
const double length = ReferenceLength(nodes);
const auto equilibrium =
fesa::EvaluateInternalResidualAndTangent(shell, trial);
ASSERT_TRUE(equilibrium.HasValue());
const fesa::Vector direction_vector = ToVector(direction);
const fesa::Vector analytical = WorkConjugate(
equilibrium.Value().internal_tangent.Multiply(direction_vector), length);
const std::array<double, 8> steps{1.0e-3, 3.0e-4, 1.0e-4, 3.0e-5,
1.0e-5, 3.0e-6, 1.0e-6, 3.0e-7};
std::array<double, 8> force_errors{};
std::array<double, 8> moment_errors{};
ASSERT_GT(ChannelNorm(analytical, true), 0.0);
ASSERT_GT(ChannelNorm(analytical, false), 0.0);
for (std::size_t index = 0U; index < steps.size(); ++index) {
const auto plus = fesa::EvaluateInternalResidualAndTangent(
shell, AddScaled(trial, direction, steps[index]));
const auto minus = fesa::EvaluateInternalResidualAndTangent(
shell, AddScaled(trial, direction, -steps[index]));
ASSERT_TRUE(plus.HasValue());
ASSERT_TRUE(minus.HasValue());
fesa::Vector derivative = plus.Value().internal_residual;
derivative.Axpy(-1.0, minus.Value().internal_residual);
derivative.Scale(0.5 / steps[index]);
derivative = WorkConjugate(derivative, length);
derivative.Axpy(-1.0, analytical);
force_errors[index] =
ChannelNorm(derivative, true) / ChannelNorm(analytical, true);
moment_errors[index] =
ChannelNorm(derivative, false) / ChannelNorm(analytical, false);
}
std::size_t force_order_count = 0U;
std::size_t moment_order_count = 0U;
for (std::size_t index = 1U; index < force_errors.size(); ++index) {
if (force_errors[index - 1U] > 0.0 && force_errors[index] > 0.0) {
const double order =
std::log(force_errors[index - 1U] / force_errors[index]) /
std::log(steps[index - 1U] / steps[index]);
if (order >= 1.5) {
++force_order_count;
}
}
if (moment_errors[index - 1U] > 0.0 && moment_errors[index] > 0.0) {
const double order =
std::log(moment_errors[index - 1U] / moment_errors[index]) /
std::log(steps[index - 1U] / steps[index]);
if (order >= 1.5) {
++moment_order_count;
}
}
}
EXPECT_GE(force_order_count, 2U);
EXPECT_GE(moment_order_count, 2U);
EXPECT_LE(force_errors[3U], 1.0e-6);
EXPECT_LE(moment_errors[3U], 1.0e-6);
}
// NSNR-ELEM-TAN-004 and NSNR-ELEM-TAN-005
TEST(NonlinearMitc4Tangent, PreservesRigidTranslationNullspaceAndSymmetry) {
const auto nodes = Nodes();
const auto equilibrium =
fesa::EvaluateInternalResidualAndTangent(Shell(nodes), {});
ASSERT_TRUE(equilibrium.HasValue());
const double length = ReferenceLength(nodes);
fesa::NonlinearMitc4TrialDofs rigid_translation{};
for (auto& displacement : rigid_translation.displacement) {
displacement = {0.3, -0.2, 0.4};
}
const fesa::Vector null_action =
WorkConjugate(equilibrium.Value().internal_tangent.Multiply(
ToVector(rigid_translation)),
length);
EXPECT_LE(null_action.Norm(), 1.0e-10);
const fesa::Matrix scaled_tangent =
WorkConjugateTangent(equilibrium.Value().internal_tangent, length);
fesa::Matrix antisymmetric = scaled_tangent;
for (std::size_t row = 0U; row < antisymmetric.Rows(); ++row) {
for (std::size_t column = 0U; column < antisymmetric.Columns(); ++column) {
antisymmetric(row, column) -= scaled_tangent(column, row);
}
}
ASSERT_GT(FrobeniusNorm(scaled_tangent), 0.0);
EXPECT_LE(FrobeniusNorm(antisymmetric) / FrobeniusNorm(scaled_tangent),
1.0e-12);
}
// NSNR-ELEM-TAN-006
TEST(NonlinearMitc4Residual, DoesNotAcceptExternalLoadShares) {
const auto nodes = Nodes();
const auto first =
fesa::EvaluateInternalResidualAndTangent(Shell(nodes), Trial());
const auto second =
fesa::EvaluateInternalResidualAndTangent(Shell(nodes), Trial());
ASSERT_TRUE(first.HasValue());
ASSERT_TRUE(second.HasValue());
for (std::size_t index = 0U; index < 24U; ++index) {
EXPECT_DOUBLE_EQ(first.Value().internal_residual[index],
second.Value().internal_residual[index]);
}
}
// NSNR-ELEM-TAN-007
TEST(NonlinearMitc4Objectivity, FiniteRigidMotionHasZeroInternalForce) {
const auto nodes = Nodes();
const fesa::Vector3 rotation{0.3, -0.2, 0.1};
const fesa::Matrix tensor = fesa::ExpSkew(rotation);
fesa::NonlinearMitc4TrialDofs trial{};
for (std::size_t node = 0U; node < nodes.size(); ++node) {
const fesa::Vector3 initial{nodes[node].coordinates};
const fesa::Vector3 rotated{
tensor(0U, 0U) * initial.X() + tensor(0U, 1U) * initial.Y() +
tensor(0U, 2U) * initial.Z(),
tensor(1U, 0U) * initial.X() + tensor(1U, 1U) * initial.Y() +
tensor(1U, 2U) * initial.Z(),
tensor(2U, 0U) * initial.X() + tensor(2U, 1U) * initial.Y() +
tensor(2U, 2U) * initial.Z()};
trial.displacement[node] =
rotated + fesa::Vector3{0.7, -0.4, 1.2} - initial;
trial.rotation_vector[node] = rotation;
}
const auto result =
fesa::EvaluateInternalResidualAndTangent(Shell(nodes), trial);
ASSERT_TRUE(result.HasValue());
EXPECT_LE(result.Value().internal_residual.Norm(), 1.0e-10);
EXPECT_LE(result.Value().physical_strain_energy, 1.0e-12);
}
// NSNR-ELEM-TAN-008
TEST(NonlinearMitc4Patch, RetainsPhysicalRecoveryIdentities) {
const auto nodes = Nodes();
const auto shell = Shell(nodes);
constexpr double kMembraneStrain = 1.0e-2;
const double green_strain =
kMembraneStrain + 0.5 * kMembraneStrain * kMembraneStrain;
constexpr double kYoungsModulus = 120.0;
constexpr double kPoissonRatio = 0.25;
constexpr double kThickness = 0.2;
const double plane_stress =
kYoungsModulus / (1.0 - kPoissonRatio * kPoissonRatio);
const double s11 = plane_stress * green_strain;
const double s22 = plane_stress * kPoissonRatio * green_strain;
fesa::NonlinearMitc4TrialDofs trial{};
for (std::size_t node = 0U; node < nodes.size(); ++node) {
trial.displacement[node] = {kMembraneStrain * nodes[node].coordinates[0U],
0.0, 0.0};
}
const auto result = fesa::EvaluateInternalResidualAndTangent(shell, trial);
ASSERT_TRUE(result.HasValue());
for (const auto& point : result.Value().recovery.gauss_points) {
ExpectRelative(point.generalized_strain[0U], green_strain);
for (std::size_t component = 1U; component < 8U; ++component) {
ExpectRelative(point.generalized_strain[component], 0.0);
}
ExpectRelative(point.section_resultant[0U], kThickness * s11);
ExpectRelative(point.section_resultant[1U], kThickness * s22);
for (std::size_t component = 2U; component < 8U; ++component) {
ExpectRelative(point.section_resultant[component], 0.0);
}
for (const auto& stress : point.stress) {
ExpectRelative(stress.components[0U], s11);
ExpectRelative(stress.components[1U], s22);
ExpectRelative(stress.components[2U], 0.0);
}
}
}
// NSNR-ELEM-TAN-009
TEST(NonlinearMitc4Residual, RejectsNonfiniteTrialData) {
const auto nodes = Nodes();
auto trial = Trial();
trial.rotation_vector[2U] = {std::numeric_limits<double>::quiet_NaN(), 0.0,
0.0};
const auto result =
fesa::EvaluateInternalResidualAndTangent(Shell(nodes), trial);
ASSERT_FALSE(result.HasValue());
EXPECT_EQ(result.GetStatus().Diagnostics()[0U].code,
"invalid-nonlinear-shell-trial");
auto overflow = Trial();
overflow.displacement[1U] = {1.0e300, 0.0, 0.0};
const auto overflow_result =
fesa::EvaluateInternalResidualAndTangent(Shell(nodes), overflow);
EXPECT_FALSE(overflow_result.HasValue());
}
} // namespace