Files
FESADev/src/fesa/elements/mitc4_shell.cpp
T
KOKO\Mimi 22c9d4435c feat: add eigen2 frequency pipeline and fail-closed validation
Implement MITC4 mass, constrained whitening, MKL FEAST, modal HDF5, and deterministic Value comparison. MSVC Debug builds; 394 of 395 tests pass. Original-model all-c residual and the documented S3 static contract conflict remain unresolved; this is an implementation checkpoint, not feature acceptance.
2026-09-09 03:14:30 +09:00

1205 lines
48 KiB
C++

#include "fesa/elements/mitc4_shell.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include <stdexcept>
#include <string>
#include <utility>
#include "fesa/model/model_types.h"
namespace fesa {
Mitc4ShellDefinition::Mitc4ShellDefinition(
SourceEntityId source_id_value,
const ShellSourceElementType source_type_value,
std::array<EntityIndex, 4> node_indices_value,
const EntityIndex material_index_value,
const EntityIndex section_index_value, SourceLocation location_value)
: source_id{std::move(source_id_value)},
source_type{source_type_value},
node_indices{std::move(node_indices_value)},
material_index{material_index_value},
section_index{section_index_value},
location{std::move(location_value)},
property_index_{section_index_value},
node_indices_view_{node_indices.begin(), node_indices.end()} {}
ElementDefinitionKind Mitc4ShellDefinition::Kind() const noexcept {
return ElementDefinitionKind::kMitc4Shell;
}
const SourceEntityId& Mitc4ShellDefinition::SourceId() const noexcept {
return source_id;
}
std::string_view Mitc4ShellDefinition::SourceElementType() const noexcept {
return source_type == ShellSourceElementType::kS4 ? "S4" : "S4R";
}
const std::vector<EntityIndex>& Mitc4ShellDefinition::NodeIndices()
const noexcept {
return node_indices_view_;
}
EntityIndex Mitc4ShellDefinition::MaterialIndex() const noexcept {
return material_index;
}
EntityIndex Mitc4ShellDefinition::PropertyIndex() const noexcept {
return property_index_;
}
void Mitc4ShellDefinition::SetPropertyIndex(
const EntityIndex property_index) noexcept {
property_index_ = property_index;
}
void Mitc4ShellDefinition::SynchronizeNodeIndices() {
node_indices_view_.assign(node_indices.begin(), node_indices.end());
}
namespace {
constexpr std::size_t kNodeCount = 4U;
constexpr std::size_t kPhysicalDofsPerNode = 5U;
constexpr std::size_t kGlobalDofsPerNode = 6U;
constexpr std::size_t kPhysicalDofCount = 20U;
constexpr std::size_t kGlobalDofCount = 24U;
constexpr double kShearCorrection = 5.0 / 6.0;
constexpr double kFrameTolerance = 1.0e-12;
Vector3 WeightedSum(const std::array<double, kNodeCount>& weights,
const std::array<Vector3, kNodeCount>& values) {
Vector3 result{};
for (std::size_t node = 0U; node < kNodeCount; ++node) {
result = result + weights[node] * values[node];
}
return result;
}
Vector3 DerivativeSum(const std::array<double, kNodeCount>& derivatives,
const std::array<Vector3, kNodeCount>& values) {
std::array<Vector3, kNodeCount> relative{};
for (std::size_t node = 0U; node < kNodeCount; ++node) {
relative[node] = values[node] - values[0];
}
return WeightedSum(derivatives, relative);
}
bool SameCoordinates(const Vector3& left, const Vector3& right) {
return left == right;
}
/// @brief Selects each director's least-aligned global axis deterministically
/// and projects it into the tangent plane.
std::array<Vector3, kNodeCount> NodalTangentsA(
const std::array<Vector3, kNodeCount>& directors) {
const std::array<Vector3, 3> global_axes{
Vector3{1.0, 0.0, 0.0}, Vector3{0.0, 1.0, 0.0}, Vector3{0.0, 0.0, 1.0}};
std::array<Vector3, kNodeCount> tangents{};
for (std::size_t node = 0U; node < kNodeCount; ++node) {
std::size_t selected = 0U;
double alignment = std::abs(global_axes[0].Dot(directors[node]));
for (std::size_t axis = 1U; axis < global_axes.size(); ++axis) {
const double candidate = std::abs(global_axes[axis].Dot(directors[node]));
if (candidate < alignment) {
selected = axis;
alignment = candidate;
}
}
const Vector3 tangent_candidate =
global_axes[selected] -
global_axes[selected].Dot(directors[node]) * directors[node];
tangents[node] = (1.0 / tangent_candidate.Norm()) * tangent_candidate;
}
return tangents;
}
/// @brief Completes each right-handed nodal director frame.
std::array<Vector3, kNodeCount> NodalTangentsB(
const std::array<Vector3, kNodeCount>& directors,
const std::array<Vector3, kNodeCount>& tangent_a) {
std::array<Vector3, kNodeCount> tangents{};
for (std::size_t node = 0U; node < kNodeCount; ++node) {
tangents[node] = directors[node].Cross(tangent_a[node]);
}
return tangents;
}
std::string ElementIdentity(const std::array<const Node*, kNodeCount>& nodes) {
std::string identity;
for (const Node* node : nodes) {
if (node == nullptr) {
continue;
}
if (!identity.empty()) {
identity += "-";
}
identity += node->source_id.source_label_text;
}
return identity;
}
Result<Mitc4Shell> ModelFailure(std::string code,
const SourceLocation& location,
const std::string& identity,
std::string message) {
return Result<Mitc4Shell>::Failure(Status::Failure(
FailureCategory::kModel, {{Severity::kError, std::move(code), location,
"*ELEMENT", identity, std::move(message)}}));
}
/// @brief Forms one physical covariant strain column with thickness stretch
/// excluded.
std::array<std::array<double, 3>, 3> CovariantStrainColumn(
const std::array<Vector3, 3>& covariant,
const std::array<Vector3, 3>& derivatives) {
std::array<std::array<double, 3>, 3> strain{};
for (std::size_t first = 0U; first < 3U; ++first) {
for (std::size_t second = 0U; second < 3U; ++second) {
strain[first][second] = 0.5 * (covariant[first].Dot(derivatives[second]) +
covariant[second].Dot(derivatives[first]));
}
}
// Thickness stretch is excluded from the five-component shell law.
strain[2U][2U] = 0.0;
return strain;
}
/// @brief Reconstructs Cartesian strain in the approved reciprocal-basis
/// reduction order.
std::array<std::array<double, 3>, 3> ReconstructCartesianStrain(
const std::array<std::array<double, 3>, 3>& covariant_strain,
const std::array<Vector3, 3>& reciprocal) {
std::array<std::array<double, 3>, 3> tensor{};
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) {
tensor[row][column] += covariant_strain[first][second] *
reciprocal[first][row] *
reciprocal[second][column];
}
}
}
}
return tensor;
}
double FrameComponent(const Vector3& left,
const std::array<std::array<double, 3>, 3>& tensor,
const Vector3& right) {
double value = 0.0;
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
value += left[row] * tensor[row][column] * right[column];
}
}
return value;
}
/// @brief Projects tensor strain into signed local engineering components.
std::array<double, 5> LocalEngineeringComponents(
const std::array<std::array<double, 3>, 3>& tensor,
const Mitc4LocalFrame& frame) {
const Vector3 e1{frame.e1};
const Vector3 e2{frame.e2};
const Vector3 e3{frame.e3};
return {FrameComponent(e1, tensor, e1), FrameComponent(e2, tensor, e2),
2.0 * FrameComponent(e1, tensor, e2),
2.0 * FrameComponent(e1, tensor, e3),
2.0 * FrameComponent(e2, tensor, e3)};
}
Matrix ScaledMatrix(const Matrix& source, double factor) {
Matrix result{source.Rows(), source.Columns()};
for (std::size_t row = 0U; row < source.Rows(); ++row) {
for (std::size_t column = 0U; column < source.Columns(); ++column) {
result(row, column) = factor * source(row, column);
}
}
return result;
}
/// @brief Lifts a local matrix by a deterministic serial congruence product.
/// @note The loop and accumulation order are part of reproducible stiffness.
Matrix Congruence(const Matrix& local, const Matrix& transformation) {
if (local.Rows() != local.Columns() ||
local.Rows() != transformation.Rows()) {
throw std::invalid_argument{
"MITC4 congruence dimensions are incompatible."};
}
Matrix result{transformation.Columns(), transformation.Columns()};
for (std::size_t row = 0U; row < result.Rows(); ++row) {
for (std::size_t column = row; column < result.Columns(); ++column) {
double value = 0.0;
for (std::size_t local_row = 0U; local_row < local.Rows(); ++local_row) {
for (std::size_t local_column = 0U; local_column < local.Columns();
++local_column) {
value += transformation(local_row, row) *
local(local_row, local_column) *
transformation(local_column, column);
}
}
result(row, column) = value;
result(column, row) = value;
}
}
return result;
}
bool IsFinite(const Matrix& matrix) {
for (std::size_t row = 0U; row < matrix.Rows(); ++row) {
for (std::size_t column = 0U; column < matrix.Columns(); ++column) {
if (!std::isfinite(matrix(row, column))) {
return false;
}
}
}
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<Mitc4Stiffness> StiffnessFailure(const SourceLocation& location,
const std::string& identity,
std::string message) {
return Result<Mitc4Stiffness>::Failure(
Status::Failure(FailureCategory::kModel,
{{Severity::kError, "invalid-shell-stiffness", location,
"*ELEMENT", identity, std::move(message)}}));
}
Result<Mitc4Mass> MassFailure(const SourceLocation& location,
const std::string& identity,
std::string message) {
return Result<Mitc4Mass>::Failure(
Status::Failure(FailureCategory::kModel,
{{Severity::kError, "invalid-shell-mass", location,
"*ELEMENT", identity, std::move(message)}}));
}
/// @brief Returns S=-[director]_cross for theta cross director.
Matrix DirectorSkew(const Vector3& director) {
Matrix result{3U, 3U};
result(0U, 1U) = director.Z();
result(0U, 2U) = -director.Y();
result(1U, 0U) = -director.Z();
result(1U, 2U) = director.X();
result(2U, 0U) = director.Y();
result(2U, 1U) = -director.X();
return result;
}
Result<Mitc4PhysicalRecovery> RecoveryFailure(const SourceLocation& location,
const std::string& identity,
std::string message) {
return Result<Mitc4PhysicalRecovery>::Failure(
Status::Failure(FailureCategory::kModel,
{{Severity::kError, "invalid-shell-recovery", location,
"*ELEMENT", identity, std::move(message)}}));
}
/// @brief Creates a structured failure at the runtime element boundary.
template <class T>
Result<T> RuntimeFailure(const SourceLocation& location,
const SourceEntityId& source_id, std::string message) {
return Result<T>::Failure(Status::Failure(
FailureCategory::kModel,
{{Severity::kError, "invalid-runtime-element", location, "*ELEMENT",
source_id.source_label_text, std::move(message)}}));
}
} // namespace
Result<Mitc4Shell> Mitc4Shell::Create(
std::array<const Node*, 4> nodes,
std::array<std::array<double, 3>, 4> initial_directors,
const ShellSection& section, const LinearElasticMaterial& material) {
const std::string identity = ElementIdentity(nodes);
if (std::any_of(nodes.begin(), nodes.end(),
[](const Node* value) { return value == nullptr; })) {
return ModelFailure("invalid-shell-geometry", section.location, identity,
"MITC4 creation requires four valid node references.");
}
std::array<Vector3, kNodeCount> coordinates{};
for (std::size_t node = 0U; node < kNodeCount; ++node) {
coordinates[node] = Vector3{nodes[node]->coordinates};
if (!coordinates[node].IsFinite()) {
return ModelFailure("invalid-shell-geometry", nodes[node]->location,
identity, "MITC4 node coordinates must be finite.");
}
for (std::size_t previous = 0U; previous < node; ++previous) {
if (SameCoordinates(coordinates[node], coordinates[previous])) {
return ModelFailure("invalid-shell-geometry", nodes[node]->location,
identity,
"MITC4 nodes must have distinct coordinates.");
}
}
}
const std::array<Vector3, kNodeCount> directors{
Vector3{initial_directors[0U]}, Vector3{initial_directors[1U]},
Vector3{initial_directors[2U]}, Vector3{initial_directors[3U]}};
for (std::size_t node = 0U; node < kNodeCount; ++node) {
const double director_norm = directors[node].Norm();
if (!directors[node].IsFinite() || !std::isfinite(director_norm) ||
std::abs(director_norm - 1.0) > kFrameTolerance) {
return ModelFailure(
"invalid-shell-director", nodes[node]->location, identity,
"MITC4 initial directors must be finite unit vectors.");
}
}
if (!std::isfinite(section.thickness) || !(section.thickness > 0.0)) {
return ModelFailure("invalid-shell-section", section.location, identity,
"MITC4 shell thickness must be finite and positive.");
}
if (!std::isfinite(material.youngs_modulus) ||
!(material.youngs_modulus > 0.0) ||
!std::isfinite(material.poisson_ratio) ||
!(material.poisson_ratio > -1.0) || !(material.poisson_ratio < 0.5)) {
return ModelFailure(
"invalid-shell-material", material.location, identity,
"MITC4 isotropic material requires finite E>0 and -1<nu<0.5.");
}
const auto center_shape = ShapeFunctions(0.0, 0.0);
const Vector3 center_xi =
DerivativeSum(center_shape.xi_derivatives, coordinates);
const Vector3 center_eta =
DerivativeSum(center_shape.eta_derivatives, coordinates);
const Vector3 center_area = center_xi.Cross(center_eta);
const double center_measure = center_area.Norm();
if (!center_area.IsFinite() || !std::isfinite(center_measure) ||
!(center_measure > 0.0)) {
return ModelFailure(
"invalid-shell-geometry", nodes[0]->location, identity,
"MITC4 center surface basis must be finite and nonzero.");
}
const Vector3 normal_candidate = (1.0 / center_measure) * center_area;
if (std::any_of(directors.begin(), directors.end(),
[&normal_candidate](const Vector3& director) {
return !(normal_candidate.Dot(director) > 0.0);
})) {
return ModelFailure(
"invalid-shell-director", nodes[0]->location, identity,
"MITC4 directors must follow the source-order positive face.");
}
const auto tangent_a = NodalTangentsA(directors);
const auto tangent_b = NodalTangentsB(directors, tangent_a);
Mitc4Shell shell{coordinates,
directors,
tangent_a,
tangent_b,
normal_candidate,
section.thickness,
material.youngs_modulus,
material.poisson_ratio,
nodes[0]->location,
identity};
const double shear_modulus =
material.youngs_modulus / (2.0 * (1.0 + material.poisson_ratio));
const double plane_stress_factor =
material.youngs_modulus /
(1.0 - material.poisson_ratio * material.poisson_ratio);
const double thickness_cubed =
section.thickness * section.thickness * section.thickness;
const std::array<double, 6> derived{
shear_modulus,
plane_stress_factor,
kShearCorrection * shear_modulus,
plane_stress_factor * section.thickness,
plane_stress_factor * thickness_cubed / 12.0,
kShearCorrection * shear_modulus * section.thickness};
if (std::any_of(derived.begin(), derived.end(), [](double value) {
return !std::isfinite(value) || !(value > 0.0);
})) {
return ModelFailure(
"invalid-shell-material", material.location, identity,
"Derived MITC4 constitutive coefficients must be finite and positive.");
}
GeometryData geometry{};
if (!shell.EvaluateGeometry(0.0, 0.0, 0.0, geometry)) {
return ModelFailure("invalid-shell-jacobian", nodes[0]->location, identity,
"MITC4 center frame or Jacobian is invalid.");
}
for (const auto& point : VolumeQuadrature()) {
if (!shell.EvaluateGeometry(point.natural_coordinates[0],
point.natural_coordinates[1],
point.natural_coordinates[2], geometry)) {
return ModelFailure("invalid-shell-jacobian", nodes[0]->location,
identity,
"MITC4 quadrature frame or Jacobian is invalid.");
}
}
constexpr std::array<Vector3, 4> tying_points{
Vector3{0.0, -1.0, 0.0}, Vector3{0.0, 1.0, 0.0}, Vector3{-1.0, 0.0, 0.0},
Vector3{1.0, 0.0, 0.0}};
for (const auto& point : tying_points) {
if (!shell.EvaluateGeometry(point[0], point[1], point[2], geometry)) {
return ModelFailure("invalid-shell-jacobian", nodes[0]->location,
identity,
"MITC4 tying-point frame or Jacobian is invalid.");
}
}
return Result<Mitc4Shell>::Success(std::move(shell));
}
Mitc4ShapeFunctions Mitc4Shell::ShapeFunctions(double xi, double eta) noexcept {
constexpr std::array<double, kNodeCount> xi_signs{-1.0, 1.0, 1.0, -1.0};
constexpr std::array<double, kNodeCount> eta_signs{-1.0, -1.0, 1.0, 1.0};
Mitc4ShapeFunctions shape{};
for (std::size_t node = 0U; node < kNodeCount; ++node) {
shape.values[node] =
0.25 * (1.0 + xi_signs[node] * xi) * (1.0 + eta_signs[node] * eta);
shape.xi_derivatives[node] =
0.25 * xi_signs[node] * (1.0 + eta_signs[node] * eta);
shape.eta_derivatives[node] =
0.25 * eta_signs[node] * (1.0 + xi_signs[node] * xi);
}
return shape;
}
Mitc4TyingWeights Mitc4Shell::TyingWeights(double xi, double eta) noexcept {
return {{(1.0 - eta) * 0.5, (1.0 + eta) * 0.5},
{(1.0 - xi) * 0.5, (1.0 + xi) * 0.5}};
}
const std::array<Mitc4QuadraturePoint, 8>&
Mitc4Shell::VolumeQuadrature() noexcept {
static const std::array<Mitc4QuadraturePoint, 8> points = [] {
const double gauss = 1.0 / std::sqrt(3.0);
return std::array<Mitc4QuadraturePoint, 8>{
Mitc4QuadraturePoint{{-gauss, -gauss, -gauss}, 1.0},
{{-gauss, -gauss, gauss}, 1.0},
{{gauss, -gauss, -gauss}, 1.0},
{{gauss, -gauss, gauss}, 1.0},
{{gauss, gauss, -gauss}, 1.0},
{{gauss, gauss, gauss}, 1.0},
{{-gauss, gauss, -gauss}, 1.0},
{{-gauss, gauss, gauss}, 1.0}};
}();
return points;
}
Mitc4LocalFrame Mitc4Shell::LocalFrame(double xi, double eta) const {
GeometryData geometry{};
if (!EvaluateGeometry(xi, eta, 0.0, geometry)) {
throw std::invalid_argument{
"MITC4 local frame is invalid at the requested point."};
}
return geometry.frame;
}
Matrix Mitc4Shell::PhysicalTransformation20() const {
Matrix transformation{kPhysicalDofCount, kGlobalDofCount};
for (std::size_t node = 0U; node < kNodeCount; ++node) {
const std::size_t physical_offset = node * kPhysicalDofsPerNode;
const std::size_t global_offset = node * kGlobalDofsPerNode;
for (std::size_t component = 0U; component < 3U; ++component) {
transformation(physical_offset + component, global_offset + component) =
1.0;
transformation(physical_offset + 3U, global_offset + 3U + component) =
tangent_a_[node][component];
transformation(physical_offset + 4U, global_offset + 3U + component) =
tangent_b_[node][component];
}
}
return transformation;
}
Matrix Mitc4Shell::DrillingTransformation4() const {
Matrix transformation{kNodeCount, kGlobalDofCount};
for (std::size_t node = 0U; node < kNodeCount; ++node) {
const std::size_t global_offset = node * kGlobalDofsPerNode;
for (std::size_t component = 0U; component < 3U; ++component) {
transformation(node, global_offset + 3U + component) =
directors_[node][component];
}
}
return transformation;
}
Matrix Mitc4Shell::DirectStrainDisplacement20(double xi, double eta,
double zeta) const {
return StrainDisplacement(xi, eta, zeta, nullptr);
}
Matrix Mitc4Shell::CovariantTyingShearSamples20() const {
constexpr std::array<Vector3, 4> points{
Vector3{0.0, -1.0, 0.0}, Vector3{0.0, 1.0, 0.0}, Vector3{-1.0, 0.0, 0.0},
Vector3{1.0, 0.0, 0.0}};
Matrix samples{4U, kPhysicalDofCount};
for (std::size_t point = 0U; point < points.size(); ++point) {
GeometryData geometry{};
if (!EvaluateGeometry(points[point][0], points[point][1], 0.0, geometry)) {
throw std::logic_error{"Validated MITC4 tying geometry became invalid."};
}
const auto derivatives =
BasisDerivatives(points[point][0], points[point][1], 0.0);
const std::size_t first = point < 2U ? 0U : 1U;
for (std::size_t dof = 0U; dof < kPhysicalDofCount; ++dof) {
const auto strain =
CovariantStrainColumn(geometry.covariant, derivatives[dof]);
samples(point, dof) = strain[first][2U];
}
}
return samples;
}
Matrix Mitc4Shell::StrainDisplacement20(double xi, double eta,
double zeta) const {
const Matrix samples = CovariantTyingShearSamples20();
return StrainDisplacement(xi, eta, zeta, &samples);
}
Matrix Mitc4Shell::PlaneStressConstitutive() const {
const double factor =
youngs_modulus_ / (1.0 - poisson_ratio_ * poisson_ratio_);
Matrix constitutive{3U, 3U};
constitutive(0U, 0U) = factor;
constitutive(0U, 1U) = factor * poisson_ratio_;
constitutive(1U, 0U) = factor * poisson_ratio_;
constitutive(1U, 1U) = factor;
constitutive(2U, 2U) = factor * (1.0 - poisson_ratio_) * 0.5;
return constitutive;
}
Matrix Mitc4Shell::MaterialConstitutive5() const {
Matrix constitutive{5U, 5U};
const Matrix plane_stress = PlaneStressConstitutive();
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
constitutive(row, column) = plane_stress(row, column);
}
}
const double shear_modulus = youngs_modulus_ / (2.0 * (1.0 + poisson_ratio_));
constitutive(3U, 3U) = kShearCorrection * shear_modulus;
constitutive(4U, 4U) = kShearCorrection * shear_modulus;
return constitutive;
}
Matrix Mitc4Shell::MembraneSectionMatrix() const {
return ScaledMatrix(PlaneStressConstitutive(), thickness_);
}
Matrix Mitc4Shell::BendingSectionMatrix() const {
return ScaledMatrix(PlaneStressConstitutive(),
thickness_ * thickness_ * thickness_ / 12.0);
}
Matrix Mitc4Shell::TransverseShearSectionMatrix() const {
const double shear_modulus = youngs_modulus_ / (2.0 * (1.0 + poisson_ratio_));
Matrix result{2U, 2U};
result(0U, 0U) = kShearCorrection * shear_modulus * thickness_;
result(1U, 1U) = result(0U, 0U);
return result;
}
Result<Mitc4Stiffness> Mitc4Shell::Stiffness() const {
const Matrix constitutive = MaterialConstitutive5();
const Matrix tying_samples = CovariantTyingShearSamples20();
Matrix physical_local{kPhysicalDofCount, kPhysicalDofCount};
for (const auto& point : VolumeQuadrature()) {
GeometryData geometry{};
if (!EvaluateGeometry(point.natural_coordinates[0],
point.natural_coordinates[1],
point.natural_coordinates[2], geometry)) {
return StiffnessFailure(
source_location_, identity_,
"Validated MITC4 quadrature geometry became invalid.");
}
const Matrix strain = StrainDisplacement(
point.natural_coordinates[0], point.natural_coordinates[1],
point.natural_coordinates[2], &tying_samples);
for (std::size_t row = 0U; row < kPhysicalDofCount; ++row) {
for (std::size_t column = row; column < kPhysicalDofCount; ++column) {
double integrand = 0.0;
for (std::size_t first = 0U; first < 5U; ++first) {
for (std::size_t second = 0U; second < 5U; ++second) {
integrand += strain(first, row) * constitutive(first, second) *
strain(second, column);
}
}
const double contribution =
integrand * geometry.jacobian * point.weight;
physical_local(row, column) += contribution;
if (row != column) {
physical_local(column, row) += contribution;
}
}
}
}
double maximum_rotary_diagonal = 0.0;
std::array<double, 8> rotary_diagonals{};
std::size_t rotary_index = 0U;
for (std::size_t node = 0U; node < kNodeCount; ++node) {
const std::size_t offset = node * kPhysicalDofsPerNode;
for (std::size_t rotation = 3U; rotation < 5U; ++rotation) {
const double diagonal =
physical_local(offset + rotation, offset + rotation);
if (!std::isfinite(diagonal) || diagonal < 0.0) {
return StiffnessFailure(
source_location_, identity_,
"MITC4 drilling stabilization requires finite nonnegative "
"physical tangent-rotation diagonals.");
}
rotary_diagonals[rotary_index++] = diagonal;
maximum_rotary_diagonal = (std::max)(maximum_rotary_diagonal, diagonal);
}
}
if (!(maximum_rotary_diagonal > 0.0)) {
return StiffnessFailure(
source_location_, identity_,
"MITC4 drilling stabilization requires a positive physical "
"tangent-rotation diagonal inventory.");
}
if (!IsFinite(physical_local)) {
return StiffnessFailure(
source_location_, identity_,
"MITC4 physical stiffness must contain only finite values.");
}
double normalized_diagonal_sum = 0.0;
for (const double diagonal : rotary_diagonals) {
normalized_diagonal_sum += diagonal / maximum_rotary_diagonal;
}
const double drilling_stiffness =
1.0e-3 * maximum_rotary_diagonal * (normalized_diagonal_sum / 8.0);
if (!std::isfinite(drilling_stiffness) || !(drilling_stiffness > 0.0)) {
return StiffnessFailure(
source_location_, identity_,
"MITC4 drilling stiffness must be finite and positive.");
}
Matrix physical_global =
Congruence(physical_local, PhysicalTransformation20());
Matrix drilling_local{kNodeCount, kNodeCount};
for (std::size_t node = 0U; node < kNodeCount; ++node) {
drilling_local(node, node) = drilling_stiffness;
}
Matrix drilling_global =
Congruence(drilling_local, DrillingTransformation4());
Matrix stabilized_global{kGlobalDofCount, kGlobalDofCount};
for (std::size_t row = 0U; row < kGlobalDofCount; ++row) {
for (std::size_t column = 0U; column < kGlobalDofCount; ++column) {
stabilized_global(row, column) =
physical_global(row, column) + drilling_global(row, column);
}
}
if (!IsFinite(physical_global) || !IsFinite(drilling_global) ||
!IsFinite(stabilized_global)) {
return StiffnessFailure(
source_location_, identity_,
"MITC4 transformed stiffness must contain only finite values.");
}
return Result<Mitc4Stiffness>::Success(
Mitc4Stiffness{std::move(physical_local), std::move(physical_global),
std::move(drilling_global), std::move(stabilized_global),
drilling_stiffness});
}
Result<Mitc4Mass> Mitc4Shell::Mass(const double density) const {
if (!std::isfinite(density) || !(density > 0.0)) {
return MassFailure(source_location_, identity_,
"MITC4 mass requires finite density > 0.");
}
std::array<Matrix, 3> moments{Matrix{kNodeCount, kNodeCount},
Matrix{kNodeCount, kNodeCount},
Matrix{kNodeCount, kNodeCount}};
constexpr double kGaussCoordinate = 0.77459666924148337704;
constexpr std::array<double, 3> kGaussPoints{-kGaussCoordinate, 0.0,
kGaussCoordinate};
constexpr std::array<double, 3> kGaussWeights{5.0 / 9.0, 8.0 / 9.0,
5.0 / 9.0};
for (std::size_t xi_index = 0U; xi_index < kGaussPoints.size(); ++xi_index) {
for (std::size_t eta_index = 0U; eta_index < kGaussPoints.size();
++eta_index) {
const Mitc4ShapeFunctions shape =
ShapeFunctions(kGaussPoints[xi_index], kGaussPoints[eta_index]);
for (std::size_t zeta_index = 0U; zeta_index < kGaussPoints.size();
++zeta_index) {
GeometryData geometry{};
const double zeta = kGaussPoints[zeta_index];
if (!EvaluateGeometry(kGaussPoints[xi_index], kGaussPoints[eta_index],
zeta, geometry)) {
return MassFailure(source_location_, identity_,
"MITC4 mass quadrature geometry is invalid.");
}
const double z = 0.5 * thickness_ * zeta;
const double weight =
density * geometry.jacobian * kGaussWeights[xi_index] *
kGaussWeights[eta_index] * kGaussWeights[zeta_index];
if (!std::isfinite(z) || !std::isfinite(weight)) {
return MassFailure(
source_location_, identity_,
"MITC4 mass quadrature contribution is nonfinite.");
}
const std::array<double, 3> z_powers{1.0, z, z * z};
for (std::size_t row = 0U; row < kNodeCount; ++row) {
for (std::size_t column = row; column < kNodeCount; ++column) {
const double shape_product =
shape.values[row] * shape.values[column];
for (std::size_t moment = 0U; moment < moments.size(); ++moment) {
const double contribution =
weight * shape_product * z_powers[moment];
moments[moment](row, column) += contribution;
if (row != column) {
moments[moment](column, row) += contribution;
}
}
}
}
}
}
}
for (const Matrix& moment : moments) {
if (!IsFinite(moment)) {
return MassFailure(source_location_, identity_,
"MITC4 consistent moments must be finite.");
}
}
Matrix consistent{kGlobalDofCount, kGlobalDofCount};
for (std::size_t row_node = 0U; row_node < kNodeCount; ++row_node) {
const Matrix row_skew = DirectorSkew(directors_[row_node]);
for (std::size_t column_node = 0U; column_node < kNodeCount;
++column_node) {
const Matrix column_skew = DirectorSkew(directors_[column_node]);
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
const std::size_t global_row = kGlobalDofsPerNode * row_node + row;
const std::size_t global_column =
kGlobalDofsPerNode * column_node + column;
consistent(global_row, global_column) =
moments[0U](row_node, column_node) * (row == column ? 1.0 : 0.0);
consistent(global_row, global_column + 3U) =
moments[1U](row_node, column_node) * column_skew(row, column);
consistent(global_row + 3U, global_column) =
moments[1U](row_node, column_node) * row_skew(column, row);
double rotary = 0.0;
for (std::size_t axis = 0U; axis < 3U; ++axis) {
rotary += row_skew(axis, row) * column_skew(axis, column);
}
consistent(global_row + 3U, global_column + 3U) =
moments[2U](row_node, column_node) * rotary;
}
}
}
}
Matrix lumped{kGlobalDofCount, kGlobalDofCount};
std::array<std::array<double, 3>, kNodeCount> nodal_moments{};
for (std::size_t node = 0U; node < kNodeCount; ++node) {
double mass = 0.0;
double first_moment = 0.0;
double second_moment = 0.0;
for (std::size_t column = 0U; column < kNodeCount; ++column) {
mass += moments[0U](node, column);
first_moment += moments[1U](node, column);
second_moment += moments[2U](node, column);
}
const double centered_inertia =
second_moment - first_moment * (first_moment / mass);
if (!std::isfinite(mass) || !std::isfinite(first_moment) ||
!std::isfinite(second_moment) || !std::isfinite(centered_inertia) ||
!(mass > 0.0) || !(second_moment > 0.0) || !(centered_inertia > 0.0)) {
return MassFailure(
source_location_, identity_,
"MITC4 nodal mass moments must be finite and positive.");
}
nodal_moments[node] = {mass, first_moment, second_moment};
const Matrix skew = DirectorSkew(directors_[node]);
const std::size_t offset = kGlobalDofsPerNode * node;
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
lumped(offset + row, offset + column) =
mass * (row == column ? 1.0 : 0.0);
lumped(offset + row, offset + column + 3U) =
first_moment * skew(row, column);
lumped(offset + row + 3U, offset + column) =
first_moment * skew(column, row);
double projector = row == column ? 1.0 : 0.0;
projector -= directors_[node][row] * directors_[node][column];
lumped(offset + row + 3U, offset + column + 3U) =
second_moment * projector;
}
}
}
if (!IsFinite(consistent) || !IsFinite(lumped)) {
return MassFailure(
source_location_, identity_,
"MITC4 transformed mass must contain only finite values.");
}
return Result<Mitc4Mass>::Success(
Mitc4Mass{std::move(moments), std::move(consistent), std::move(lumped),
std::move(nodal_moments)});
}
Result<Mitc4PhysicalRecovery> Mitc4Shell::RecoverPhysical(
const Vector& global_element_displacement24) const {
if (global_element_displacement24.Size() != kGlobalDofCount) {
return RecoveryFailure(
source_location_, identity_,
"MITC4 physical recovery requires exactly 24 global element DOFs.");
}
if (!IsFinite(global_element_displacement24)) {
return RecoveryFailure(
source_location_, identity_,
"MITC4 physical recovery displacement must be finite.");
}
const Vector physical_displacement =
PhysicalTransformation20().Multiply(global_element_displacement24);
const Matrix tying_samples = CovariantTyingShearSamples20();
const Matrix constitutive = MaterialConstitutive5();
const Matrix plane_stress = PlaneStressConstitutive();
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}};
constexpr std::array<double, 2> thickness_points{-1.0, 1.0};
constexpr std::array<double, 3> section_positions{-1.0, 0.0, 1.0};
Mitc4PhysicalRecovery recovery{};
for (std::size_t surface = 0U; surface < surface_points.size(); ++surface) {
auto& point = recovery.points[surface];
point.natural_coordinates = surface_points[surface];
GeometryData midsurface_geometry{};
if (!EvaluateGeometry(point.natural_coordinates[0],
point.natural_coordinates[1], 0.0,
midsurface_geometry)) {
return RecoveryFailure(source_location_, identity_,
"MITC4 midsurface recovery geometry is invalid.");
}
point.local_frame = midsurface_geometry.frame;
for (double thickness_sign : thickness_points) {
const double zeta = thickness_sign * gauss;
const Matrix strain_matrix = StrainDisplacement(
point.natural_coordinates[0], point.natural_coordinates[1], zeta,
&tying_samples);
const Vector strain = strain_matrix.Multiply(physical_displacement);
const Vector stress = constitutive.Multiply(strain);
GeometryData geometry{};
if (!EvaluateGeometry(point.natural_coordinates[0],
point.natural_coordinates[1], zeta, geometry)) {
return RecoveryFailure(
source_location_, identity_,
"Validated MITC4 recovery geometry became invalid.");
}
for (std::size_t component = 0U; component < 3U; ++component) {
point.generalized_strain[component] += 0.5 * strain[component];
point.generalized_strain[3U + component] +=
3.0 * zeta * strain[component] / thickness_;
point.section_resultant[component] +=
0.5 * thickness_ * stress[component];
point.section_resultant[3U + component] +=
0.25 * thickness_ * thickness_ * zeta * stress[component];
}
for (std::size_t component = 0U; component < 2U; ++component) {
point.generalized_strain[6U + component] +=
0.5 * strain[3U + component];
point.section_resultant[6U + component] +=
0.5 * thickness_ * stress[3U + component];
}
recovery.strain_energy += 0.5 * strain.Dot(stress) * geometry.jacobian;
}
for (std::size_t position = 0U; position < section_positions.size();
++position) {
GeometryData section_geometry{};
if (!EvaluateGeometry(point.natural_coordinates[0],
point.natural_coordinates[1],
section_positions[position], section_geometry)) {
return RecoveryFailure(
source_location_, identity_,
"MITC4 section-position recovery geometry is invalid.");
}
const Vector strain =
StrainDisplacement(point.natural_coordinates[0],
point.natural_coordinates[1],
section_positions[position], &tying_samples)
.Multiply(physical_displacement);
Vector in_plane_strain{3U};
for (std::size_t component = 0U; component < 3U; ++component) {
in_plane_strain[component] = strain[component];
}
const Vector stress = plane_stress.Multiply(in_plane_strain);
for (std::size_t component = 0U; component < 3U; ++component) {
point.in_plane_stress[position][component] = stress[component];
}
}
}
if (!std::isfinite(recovery.strain_energy)) {
return RecoveryFailure(source_location_, 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.generalized_strain) || !finite(point.section_resultant) ||
std::any_of(
point.in_plane_stress.begin(), point.in_plane_stress.end(),
[&finite](const auto& stress) { return !finite(stress); })) {
return RecoveryFailure(source_location_, identity_,
"MITC4 physical recovery values must be finite.");
}
}
return Result<Mitc4PhysicalRecovery>::Success(std::move(recovery));
}
const ElementDofLayout& Mitc4Shell::DofLayout() const noexcept {
return dof_layout_;
}
Result<ElementStiffnessContribution> Mitc4Shell::ComputeStiffness() const {
if (dof_layout_.node_indices.size() != kNodeCount ||
dof_layout_.components_per_node.size() != kGlobalDofsPerNode) {
return RuntimeFailure<ElementStiffnessContribution>(
runtime_location_, dof_layout_.source_id,
"MITC4 runtime stiffness requires a bound four-node six-DOF layout.");
}
try {
auto stiffness = Stiffness();
if (!stiffness.HasValue()) {
return Result<ElementStiffnessContribution>::Failure(
stiffness.GetStatus());
}
return Result<ElementStiffnessContribution>::Success(
{dof_layout_, std::move(stiffness.Value().stabilized_global24)});
} catch (const std::exception& error) {
return RuntimeFailure<ElementStiffnessContribution>(
runtime_location_, dof_layout_.source_id, error.what());
}
}
Result<ElementResultBundle> Mitc4Shell::Recover(
const Vector& element_displacement) const {
if (dof_layout_.node_indices.size() != kNodeCount ||
dof_layout_.components_per_node.size() != kGlobalDofsPerNode ||
element_displacement.Size() != kGlobalDofCount) {
return RuntimeFailure<ElementResultBundle>(
runtime_location_, dof_layout_.source_id,
"MITC4 runtime recovery requires a bound four-node layout and 24 "
"DOFs.");
}
Result<Mitc4PhysicalRecovery> recovered = [&]() {
try {
return RecoverPhysical(element_displacement);
} catch (const std::exception& error) {
return RuntimeFailure<Mitc4PhysicalRecovery>(
runtime_location_, dof_layout_.source_id, error.what());
}
}();
if (!recovered.HasValue()) {
return Result<ElementResultBundle>::Failure(recovered.GetStatus());
}
constexpr std::array<ShellMidsurfaceLocation, kNodeCount> locations{
ShellMidsurfaceLocation::kGp1, ShellMidsurfaceLocation::kGp2,
ShellMidsurfaceLocation::kGp3, ShellMidsurfaceLocation::kGp4};
constexpr std::array<ShellSectionPosition, 3> positions{
ShellSectionPosition::kBottom, ShellSectionPosition::kMiddle,
ShellSectionPosition::kTop};
constexpr std::array<double, 3> zeta{-1.0, 0.0, 1.0};
ShellElementResultRows rows{};
rows.rows.reserve(kNodeCount);
rows.physical_strain_energy = recovered.Value().strain_energy;
for (std::size_t point = 0U; point < recovered.Value().points.size();
++point) {
const auto& physical_point = recovered.Value().points[point];
ShellResultRow row{};
row.element = element_index_;
row.location = locations[point];
row.natural_coordinates = physical_point.natural_coordinates;
row.local_frame = {physical_point.local_frame.e1,
physical_point.local_frame.e2,
physical_point.local_frame.e3};
row.generalized_strain = physical_point.generalized_strain;
row.section_resultant = physical_point.section_resultant;
for (std::size_t position = 0U; position < positions.size(); ++position) {
row.stress[position] = {positions[position], zeta[position],
physical_point.in_plane_stress[position]};
}
rows.rows.push_back(std::move(row));
}
return Result<ElementResultBundle>::Success(
{dof_layout_.source_id, std::move(rows)});
}
void Mitc4Shell::BindRuntime(ElementDofLayout layout,
const EntityIndex element_index,
SourceLocation location) {
dof_layout_ = std::move(layout);
element_index_ = element_index;
runtime_location_ = std::move(location);
}
Mitc4Shell::Mitc4Shell(std::array<Vector3, 4> coordinates,
std::array<Vector3, 4> directors,
std::array<Vector3, 4> tangent_a,
std::array<Vector3, 4> tangent_b,
Vector3 normal_candidate, double thickness,
double youngs_modulus, double poisson_ratio,
SourceLocation source_location, std::string identity)
: coordinates_{std::move(coordinates)},
directors_{std::move(directors)},
tangent_a_{std::move(tangent_a)},
tangent_b_{std::move(tangent_b)},
normal_candidate_{std::move(normal_candidate)},
thickness_{thickness},
youngs_modulus_{youngs_modulus},
poisson_ratio_{poisson_ratio},
source_location_{std::move(source_location)},
identity_{std::move(identity)} {}
bool Mitc4Shell::EvaluateGeometry(double xi, double eta, double zeta,
GeometryData& result) const noexcept {
const auto shape = ShapeFunctions(xi, eta);
const Vector3 midsurface_xi =
DerivativeSum(shape.xi_derivatives, coordinates_);
const Vector3 midsurface_eta =
DerivativeSum(shape.eta_derivatives, coordinates_);
const Vector3 director_xi = DerivativeSum(shape.xi_derivatives, directors_);
const Vector3 director_eta = DerivativeSum(shape.eta_derivatives, directors_);
const Vector3 director_value = WeightedSum(shape.values, directors_);
const double half_thickness = 0.5 * thickness_;
result.covariant[0] = midsurface_xi + half_thickness * zeta * director_xi;
result.covariant[1] = midsurface_eta + half_thickness * zeta * director_eta;
result.covariant[2] = half_thickness * director_value;
result.jacobian =
result.covariant[0].Dot(result.covariant[1].Cross(result.covariant[2]));
if (!result.covariant[0].IsFinite() || !result.covariant[1].IsFinite() ||
!result.covariant[2].IsFinite() || !std::isfinite(result.jacobian) ||
!(result.jacobian > 0.0)) {
return false;
}
result.reciprocal[0] =
(1.0 / result.jacobian) * result.covariant[1].Cross(result.covariant[2]);
result.reciprocal[1] =
(1.0 / result.jacobian) * result.covariant[2].Cross(result.covariant[0]);
result.reciprocal[2] =
(1.0 / result.jacobian) * result.covariant[0].Cross(result.covariant[1]);
const Vector3 area = midsurface_xi.Cross(midsurface_eta);
const double director_norm = director_value.Norm();
if (!area.IsFinite() || !director_value.IsFinite() ||
!std::isfinite(director_norm) || !(director_norm > 0.0) ||
!(area.Dot(normal_candidate_) > 0.0)) {
return false;
}
const Vector3 e3 = (1.0 / director_norm) * director_value;
result.frame.e3 = e3.Components();
if (!(area.Dot(e3) > 0.0)) {
return false;
}
const Vector3 e1_candidate = midsurface_xi - midsurface_xi.Dot(e3) * e3;
const double e1_norm = e1_candidate.Norm();
if (!e1_candidate.IsFinite() || !std::isfinite(e1_norm) || !(e1_norm > 0.0)) {
return false;
}
const Vector3 e1 = (1.0 / e1_norm) * e1_candidate;
const Vector3 e2 = e3.Cross(e1);
result.frame.e1 = e1.Components();
result.frame.e2 = e2.Components();
return result.reciprocal[0].IsFinite() && result.reciprocal[1].IsFinite() &&
result.reciprocal[2].IsFinite() && e2.IsFinite();
}
std::array<std::array<Vector3, 3>, 20> Mitc4Shell::BasisDerivatives(
double xi, double eta, double zeta) const noexcept {
const auto shape = ShapeFunctions(xi, eta);
const double half_thickness = 0.5 * thickness_;
std::array<std::array<Vector3, 3>, kPhysicalDofCount> derivatives{};
constexpr std::array<Vector3, 3> global_axes{
Vector3{1.0, 0.0, 0.0}, Vector3{0.0, 1.0, 0.0}, Vector3{0.0, 0.0, 1.0}};
for (std::size_t node = 0U; node < kNodeCount; ++node) {
const std::size_t offset = node * kPhysicalDofsPerNode;
for (std::size_t component = 0U; component < 3U; ++component) {
derivatives[offset + component][0] =
shape.xi_derivatives[node] * global_axes[component];
derivatives[offset + component][1] =
shape.eta_derivatives[node] * global_axes[component];
}
const Vector3 alpha_direction = -half_thickness * tangent_b_[node];
derivatives[offset + 3U][0] =
zeta * shape.xi_derivatives[node] * alpha_direction;
derivatives[offset + 3U][1] =
zeta * shape.eta_derivatives[node] * alpha_direction;
derivatives[offset + 3U][2] = shape.values[node] * alpha_direction;
const Vector3 beta_direction = half_thickness * tangent_a_[node];
derivatives[offset + 4U][0] =
zeta * shape.xi_derivatives[node] * beta_direction;
derivatives[offset + 4U][1] =
zeta * shape.eta_derivatives[node] * beta_direction;
derivatives[offset + 4U][2] = shape.values[node] * beta_direction;
}
return derivatives;
}
Matrix Mitc4Shell::StrainDisplacement(double xi, double eta, double zeta,
const Matrix* tying_samples) const {
GeometryData geometry{};
if (!EvaluateGeometry(xi, eta, zeta, geometry)) {
throw std::invalid_argument{
"MITC4 strain geometry is invalid at the requested point."};
}
const auto derivatives = BasisDerivatives(xi, eta, zeta);
const Mitc4TyingWeights weights = TyingWeights(xi, eta);
Matrix result{5U, kPhysicalDofCount};
for (std::size_t dof = 0U; dof < kPhysicalDofCount; ++dof) {
auto covariant =
CovariantStrainColumn(geometry.covariant, derivatives[dof]);
if (tying_samples != nullptr) {
covariant[0U][2U] = weights.xi_zeta[0] * (*tying_samples)(0U, dof) +
weights.xi_zeta[1] * (*tying_samples)(1U, dof);
covariant[2U][0U] = covariant[0U][2U];
covariant[1U][2U] = weights.eta_zeta[0] * (*tying_samples)(2U, dof) +
weights.eta_zeta[1] * (*tying_samples)(3U, dof);
covariant[2U][1U] = covariant[1U][2U];
}
const auto engineering = LocalEngineeringComponents(
ReconstructCartesianStrain(covariant, geometry.reciprocal),
geometry.frame);
for (std::size_t component = 0U; component < engineering.size();
++component) {
result(component, dof) = engineering[component];
}
}
return result;
}
} // namespace fesa