Files
FESADev/src/fesa/elements/euler_beam_3d.cpp
T

621 lines
25 KiB
C++

#include "fesa/elements/euler_beam_3d.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
#include "fesa/math/vector3.h"
#include "fesa/model/model_types.h"
#include "math/dense_blas_internal.h"
namespace fesa {
EulerBeam3DDefinition::EulerBeam3DDefinition(
SourceEntityId source_id_value,
std::array<EntityIndex, 2> node_indices_value,
const EntityIndex material_index_value,
const EntityIndex section_index_value, SourceLocation location_value)
: source_id{std::move(source_id_value)},
node_indices{std::move(node_indices_value)},
material_index{material_index_value},
section_index{section_index_value},
location{std::move(location_value)},
node_indices_view_{node_indices.begin(), node_indices.end()} {}
ElementDefinitionKind EulerBeam3DDefinition::Kind() const noexcept {
return ElementDefinitionKind::kEulerBeam3D;
}
const SourceEntityId& EulerBeam3DDefinition::SourceId() const noexcept {
return source_id;
}
std::string_view EulerBeam3DDefinition::SourceElementType() const noexcept {
return "B33";
}
const std::vector<EntityIndex>& EulerBeam3DDefinition::NodeIndices()
const noexcept {
return node_indices_view_;
}
EntityIndex EulerBeam3DDefinition::MaterialIndex() const noexcept {
return material_index;
}
EntityIndex EulerBeam3DDefinition::PropertyIndex() const noexcept {
return section_index;
}
void EulerBeam3DDefinition::SynchronizeNodeIndices() {
node_indices_view_.assign(node_indices.begin(), node_indices.end());
}
namespace {
constexpr std::size_t kElementDofCount = 12U;
constexpr std::size_t kGeneralizedComponentCount = 4U;
constexpr double kGeometryTolerance = 1.0e-12;
constexpr double kStiffnessInvariantTolerance = 1.0e-12;
std::string ElementIdentity(const Node& first_node, const Node& second_node) {
return first_node.source_id.instance_name + ":" +
first_node.source_id.source_label_text + "-" +
second_node.source_id.source_label_text;
}
Result<EulerBeam3D> ModelFailure(const std::string& code,
const SourceLocation& location,
const std::string& identity,
const std::string& message) {
return Result<EulerBeam3D>::Failure(Status::Failure(
FailureCategory::kModel,
{{Severity::kError, code, location, "*ELEMENT", identity, message}}));
}
/// @brief Creates a structured failure at the runtime element boundary.
template <class T>
Result<T> RuntimeFailure(const SourceLocation& location,
const SourceEntityId& source_id,
const std::string& message) {
return Result<T>::Failure(
Status::Failure(FailureCategory::kModel,
{{Severity::kError, "invalid-runtime-element", location,
"*ELEMENT", source_id.source_label_text, message}}));
}
/// @brief Reports whether every fixed-size recovery component is finite.
template <class T, std::size_t Size>
bool IsFinite(const std::array<T, Size>& values) {
return std::all_of(values.begin(), values.end(), [](const auto& value) {
if constexpr (std::is_arithmetic_v<T>) {
return std::isfinite(value);
} else {
return IsFinite(value);
}
});
}
/// @brief Repeats the local-axis rotation in stable nodal translation and
/// rotation blocks.
Matrix Transformation(const std::array<double, 9>& rotation) {
Matrix result{kElementDofCount, kElementDofCount};
// Blocks preserve [translation, rotation] at node 1 then node 2.
for (std::size_t block = 0; block < 4U; ++block) {
for (std::size_t row = 0; row < 3U; ++row) {
for (std::size_t column = 0; column < 3U; ++column) {
result(block * 3U + row, block * 3U + column) =
rotation[row * 3U + column];
}
}
}
return result;
}
/// @brief Builds the reviewed axial, twist, and signed curvature operator.
/// @warning The theta-y and theta-z signs are formulation contracts.
Matrix StrainDisplacement(double xi, double length) {
Matrix b{kGeneralizedComponentCount, kElementDofCount};
const double r = 0.5 * (1.0 + xi);
const double inverse_length = 1.0 / length;
const double inverse_length_squared = inverse_length * inverse_length;
b(0U, 0U) = -inverse_length;
b(0U, 6U) = inverse_length;
b(1U, 3U) = -inverse_length;
b(1U, 9U) = inverse_length;
// theta_y=-w' makes kappa_y=-w''; theta_z=v' makes kappa_z=v''.
b(2U, 2U) = (6.0 - 12.0 * r) * inverse_length_squared;
b(2U, 4U) = (-4.0 + 6.0 * r) * inverse_length;
b(2U, 8U) = (-6.0 + 12.0 * r) * inverse_length_squared;
b(2U, 10U) = (-2.0 + 6.0 * r) * inverse_length;
b(3U, 1U) = (-6.0 + 12.0 * r) * inverse_length_squared;
b(3U, 5U) = (-4.0 + 6.0 * r) * inverse_length;
b(3U, 7U) = (6.0 - 12.0 * r) * inverse_length_squared;
b(3U, 11U) = (-2.0 + 6.0 * r) * inverse_length;
return b;
}
std::array<double, kGeneralizedComponentCount> ConstitutiveDiagonal(
double youngs_modulus, double shear_modulus, double area, double iy,
double iz, double torsional_constant) {
return {youngs_modulus * area, shear_modulus * torsional_constant,
youngs_modulus * iy, youngs_modulus * iz};
}
/// @brief Builds the independent closed-form stiffness used as a Gauss-rule
/// invariant.
Matrix ClosedStiffness(
double length,
const std::array<double, kGeneralizedComponentCount>& diagonal) {
Matrix closed{kElementDofCount, kElementDofCount};
const auto add_block = [&closed](const std::array<std::size_t, 2>& indices,
double coefficient) {
closed(indices[0], indices[0]) = coefficient;
closed(indices[0], indices[1]) = -coefficient;
closed(indices[1], indices[0]) = -coefficient;
closed(indices[1], indices[1]) = coefficient;
};
add_block({0U, 6U}, diagonal[0U] / length);
add_block({3U, 9U}, diagonal[1U] / length);
const auto add_bending_block = [&closed, length](
const std::array<std::size_t, 4>& indices,
double flexural_rigidity,
double rotation_sign) {
const double value = 12.0 * flexural_rigidity / (length * length * length);
const double coupling =
rotation_sign * 6.0 * flexural_rigidity / (length * length);
const double diagonal_rotation = 4.0 * flexural_rigidity / length;
const double off_diagonal_rotation = 2.0 * flexural_rigidity / length;
const std::array<double, 16> block = {value, coupling,
-value, coupling,
coupling, diagonal_rotation,
-coupling, off_diagonal_rotation,
-value, -coupling,
value, -coupling,
coupling, off_diagonal_rotation,
-coupling, diagonal_rotation};
for (std::size_t row = 0; row < indices.size(); ++row) {
for (std::size_t column = 0; column < indices.size(); ++column) {
closed(indices[row], indices[column]) =
block[row * indices.size() + column];
}
}
};
add_bending_block({1U, 5U, 7U, 11U}, diagonal[3U], 1.0);
add_bending_block({2U, 4U, 8U, 10U}, diagonal[2U], -1.0);
return closed;
}
double NormalizedMatrixError(const Matrix& lhs, const Matrix& rhs) {
double maximum_difference = 0.0;
double scale_value = 1.0;
for (std::size_t row = 0; row < lhs.Rows(); ++row) {
for (std::size_t column = 0; column < lhs.Columns(); ++column) {
const double lhs_value = lhs(row, column);
const double rhs_value = rhs(row, column);
if (!std::isfinite(lhs_value) || !std::isfinite(rhs_value)) {
return std::numeric_limits<double>::infinity();
}
const double difference = std::abs(lhs_value - rhs_value);
if (!std::isfinite(difference)) {
return std::numeric_limits<double>::infinity();
}
maximum_difference = (std::max)(maximum_difference, difference);
scale_value = (std::max)(scale_value, std::abs(lhs_value));
scale_value = (std::max)(scale_value, std::abs(rhs_value));
}
}
if (!std::isfinite(maximum_difference) || !std::isfinite(scale_value) ||
!(scale_value > 0.0)) {
return std::numeric_limits<double>::infinity();
}
const double normalized_error = maximum_difference / scale_value;
return std::isfinite(normalized_error)
? normalized_error
: std::numeric_limits<double>::infinity();
}
/// @brief Recovers generalized strain in fixed component and DOF order.
std::array<double, kGeneralizedComponentCount> GeneralizedStrain(
const Matrix& b, const Vector& local_displacement) {
std::array<double, kGeneralizedComponentCount> strain{};
const auto product = b.Multiply(local_displacement);
for (std::size_t component = 0; component < strain.size(); ++component) {
strain[component] = product[component];
}
return strain;
}
/// @brief Applies the diagonal section law without changing component order.
std::array<double, kGeneralizedComponentCount> GeneralizedResultant(
const std::array<double, kGeneralizedComponentCount>& strain,
const std::array<double, kGeneralizedComponentCount>& diagonal) {
std::array<double, kGeneralizedComponentCount> resultant{};
for (std::size_t component = 0; component < resultant.size(); ++component) {
resultant[component] = diagonal[component] * strain[component];
}
return resultant;
}
/// @brief Builds signed Hermite interpolation for endpoint recovery.
Matrix KinematicInterpolation(double xi, double length) {
Matrix interpolation{4U, kElementDofCount};
const double r = 0.5 * (1.0 + xi);
const double r_squared = r * r;
const double r_cubed = r_squared * r;
const double n1 = 1.0 - r;
const double n2 = r;
const double h1 = 1.0 - 3.0 * r_squared + 2.0 * r_cubed;
const double h2 = length * (r - 2.0 * r_squared + r_cubed);
const double h3 = 3.0 * r_squared - 2.0 * r_cubed;
const double h4 = length * (-r_squared + r_cubed);
interpolation(0U, 0U) = n1;
interpolation(0U, 6U) = n2;
interpolation(1U, 1U) = h1;
interpolation(1U, 5U) = h2;
interpolation(1U, 7U) = h3;
interpolation(1U, 11U) = h4;
interpolation(2U, 2U) = h1;
interpolation(2U, 4U) = -h2;
interpolation(2U, 8U) = h3;
interpolation(2U, 10U) = -h4;
interpolation(3U, 3U) = n1;
interpolation(3U, 9U) = n2;
return interpolation;
}
} // namespace
Result<EulerBeam3D> EulerBeam3D::Create(const Node& first_node,
const Node& second_node,
const GeneralBeamSection& section,
const LinearElasticMaterial& material) {
const std::string identity = ElementIdentity(first_node, second_node);
const Vector3 first{first_node.coordinates};
const Vector3 second{second_node.coordinates};
const Vector3 delta = second - first;
const double length = delta.Norm();
const double coordinate_scale =
(std::max)({1.0, first.Norm(), second.Norm()});
if (!first.IsFinite() || !second.IsFinite() || !delta.IsFinite() ||
!std::isfinite(length) || !std::isfinite(coordinate_scale) ||
!(length > kGeometryTolerance * coordinate_scale)) {
return ModelFailure(
"invalid-beam-length", first_node.location, identity,
"Beam length must exceed the scale-aware geometry threshold.");
}
const Vector3 ex = delta / length;
const Vector3 guide{section.first_axis};
const double guide_norm = guide.Norm();
const double guide_projection = guide.Dot(ex);
const Vector3 ey_trial = guide - guide_projection * ex;
const double ey_trial_norm = ey_trial.Norm();
if (!guide.IsFinite() || !std::isfinite(guide_norm) || !ey_trial.IsFinite() ||
!std::isfinite(ey_trial_norm) ||
!(ey_trial_norm > kGeometryTolerance * (std::max)(1.0, guide_norm))) {
return ModelFailure(
"invalid-beam-guide-vector", section.location, identity,
"Beam guide vector must define a scale-aware transverse direction.");
}
if (!std::isfinite(section.i12)) {
return ModelFailure("invalid-beam-property", section.location, identity,
"Beam section properties must be finite and positive.");
}
if (section.i12 != 0.0) {
return ModelFailure("unsupported-coupled-section", section.location,
identity,
"The Euler beam kernel requires exact I12=0.");
}
const double shear_modulus =
material.youngs_modulus / (2.0 * (1.0 + material.poisson_ratio));
const std::array<double, 6> positive_properties = {
material.youngs_modulus,
shear_modulus,
section.area,
section.i11,
section.i22,
section.torsional_constant};
if (!std::isfinite(material.poisson_ratio) ||
std::any_of(positive_properties.begin(), positive_properties.end(),
[](double property) {
return !std::isfinite(property) || !(property > 0.0);
})) {
return ModelFailure("invalid-beam-property", section.location, identity,
"E, G, A, Iy, Iz, and J must be finite and positive.");
}
const auto derived_rigidity = ConstitutiveDiagonal(
material.youngs_modulus, shear_modulus, section.area, section.i11,
section.i22, section.torsional_constant);
const double length_squared = length * length;
const double length_cubed = length_squared * length;
// These are every distinct positive magnitude used by the exact axial,
// torsion, and two bending closed-form blocks. Reject arithmetic
// overflow/underflow without introducing a conditioning threshold.
const std::array<double, 14> required_stiffness_magnitudes = {
derived_rigidity[0U],
derived_rigidity[1U],
derived_rigidity[2U],
derived_rigidity[3U],
derived_rigidity[0U] / length,
derived_rigidity[1U] / length,
12.0 * derived_rigidity[2U] / length_cubed,
6.0 * derived_rigidity[2U] / length_squared,
4.0 * derived_rigidity[2U] / length,
2.0 * derived_rigidity[2U] / length,
12.0 * derived_rigidity[3U] / length_cubed,
6.0 * derived_rigidity[3U] / length_squared,
4.0 * derived_rigidity[3U] / length,
2.0 * derived_rigidity[3U] / length};
if (std::any_of(required_stiffness_magnitudes.begin(),
required_stiffness_magnitudes.end(), [](double magnitude) {
return !std::isfinite(magnitude) || !(magnitude > 0.0);
})) {
return ModelFailure(
"invalid-beam-property", section.location, identity,
"Derived beam stiffness coefficients must be finite and positive.");
}
const Vector3 ey = ey_trial / ey_trial_norm;
const Vector3 ez = ex.Cross(ey);
// Rows map global vectors to the approved right-handed local (ex,ey,ez)
// basis.
const std::array<double, 9> rotation = {ex[0], ex[1], ex[2], ey[0], ey[1],
ey[2], ez[0], ez[1], ez[2]};
return Result<EulerBeam3D>::Success(
EulerBeam3D{length, material.youngs_modulus, shear_modulus, section.area,
section.i11, section.i22, section.torsional_constant,
rotation, section.section_points});
}
Matrix EulerBeam3D::LocalStiffness() const {
const auto diagonal = ConstitutiveDiagonal(
youngs_modulus_, shear_modulus_, area_, iy_, iz_, torsional_constant_);
Matrix stiffness{kElementDofCount, kElementDofCount};
const double inverse_sqrt_three = 1.0 / std::sqrt(3.0);
const std::array<double, 2> gauss_points = {-inverse_sqrt_three,
inverse_sqrt_three};
const double jacobian = 0.5 * length_;
// Both Gauss points are required: a one-point bending rule loses two ranks.
for (const double xi : gauss_points) {
const Matrix b = StrainDisplacement(xi, length_);
Matrix weighted_b = b;
for (std::size_t component = 0; component < diagonal.size(); ++component)
for (std::size_t column = 0; column < kElementDofCount; ++column)
weighted_b(component, column) *= diagonal[component];
const auto contribution =
dense_blas_internal::MultiplyTransposeLeft(b, weighted_b);
for (std::size_t row = 0; row < kElementDofCount; ++row) {
for (std::size_t column = 0; column < kElementDofCount; ++column) {
stiffness(row, column) += contribution(row, column) * jacobian;
}
}
}
const Matrix closed = ClosedStiffness(length_, diagonal);
if (NormalizedMatrixError(stiffness, closed) > kStiffnessInvariantTolerance) {
throw std::logic_error{
"Two-point Euler beam stiffness violated the closed-form invariant."};
}
return stiffness;
}
Matrix EulerBeam3D::GlobalStiffness() const {
const Matrix local = LocalStiffness();
const Matrix transform = Transformation(rotation_);
const Matrix local_times_transform = local.Multiply(transform);
// Kg=T^T*Kl*T while dl=T*dg.
return dense_blas_internal::MultiplyTransposeLeft(transform,
local_times_transform);
}
Vector EulerBeam3D::LocalEquivalentLoad(
const ConstantLocalLineLoad& load) const {
const std::array<double, 4> components = {load.px, load.py, load.pz, load.mx};
Matrix component_column{components.size(), 1U};
for (std::size_t component = 0; component < components.size(); ++component)
component_column(component, 0U) = components[component];
Vector equivalent{kElementDofCount};
const double inverse_sqrt_three = 1.0 / std::sqrt(3.0);
const std::array<double, 2> gauss_points = {-inverse_sqrt_three,
inverse_sqrt_three};
const double jacobian = 0.5 * length_;
for (const double xi : gauss_points) {
const Matrix interpolation = KinematicInterpolation(xi, length_);
const auto contribution = dense_blas_internal::MultiplyTransposeLeft(
interpolation, component_column);
for (std::size_t dof = 0; dof < equivalent.Size(); ++dof) {
equivalent[dof] += contribution(dof, 0U) * jacobian;
}
}
return equivalent;
}
BeamRecovery EulerBeam3D::RecoverBeam(
const Vector& global_element_displacement) const {
const Matrix transform = Transformation(rotation_);
const Vector local_displacement =
transform.Multiply(global_element_displacement);
const auto diagonal = ConstitutiveDiagonal(
youngs_modulus_, shear_modulus_, area_, iy_, iz_, torsional_constant_);
BeamRecovery recovery{};
// With parser/CLI distributed loading excluded, Kl*dl is the local outward
// end action.
const Vector end_action = LocalStiffness().Multiply(local_displacement);
for (std::size_t endpoint = 0; endpoint < 2U; ++endpoint) {
for (std::size_t component = 0; component < 6U; ++component) {
recovery.equilibrium_end_actions[endpoint][component] =
end_action[endpoint * 6U + component];
}
const double xi = endpoint == 0U ? -1.0 : 1.0;
recovery.endpoint_section_resultants[endpoint] = GeneralizedResultant(
GeneralizedStrain(StrainDisplacement(xi, length_), local_displacement),
diagonal);
}
const double inverse_sqrt_three = 1.0 / std::sqrt(3.0);
const std::array<double, 2> gauss_points = {-inverse_sqrt_three,
inverse_sqrt_three};
for (std::size_t point = 0; point < gauss_points.size(); ++point) {
recovery.gauss_generalized_strains[point] = GeneralizedStrain(
StrainDisplacement(gauss_points[point], length_), local_displacement);
recovery.gauss_generalized_resultants[point] = GeneralizedResultant(
recovery.gauss_generalized_strains[point], diagonal);
if (section_points_.empty()) {
recovery.stress_points.push_back(
{static_cast<int>(point + 1U), 0U, 0.0, 0.0,
youngs_modulus_ * recovery.gauss_generalized_strains[point][0U],
"fesa-default"});
continue;
}
for (std::size_t section_point = 0; section_point < section_points_.size();
++section_point) {
const double x1 = section_points_[section_point][0U];
const double x2 = section_points_[section_point][1U];
const auto& strain = recovery.gauss_generalized_strains[point];
// x1=y and x2=z: S11=E(epsilon0+x2*kappa_y-x1*kappa_z).
recovery.stress_points.push_back(
{static_cast<int>(point + 1U), section_point + 1U, x1, x2,
youngs_modulus_ * (strain[0U] + x2 * strain[2U] - x1 * strain[3U]),
"input"});
}
}
return recovery;
}
const ElementDofLayout& EulerBeam3D::DofLayout() const noexcept {
return dof_layout_;
}
Result<ElementStiffnessContribution> EulerBeam3D::ComputeStiffness() const {
if (dof_layout_.node_indices.size() != 2U ||
dof_layout_.components_per_node.size() != 6U) {
return RuntimeFailure<ElementStiffnessContribution>(
runtime_location_, dof_layout_.source_id,
"B33 runtime stiffness requires a bound two-node six-DOF layout.");
}
try {
return Result<ElementStiffnessContribution>::Success(
{dof_layout_, GlobalStiffness()});
} catch (const std::exception& error) {
return RuntimeFailure<ElementStiffnessContribution>(
runtime_location_, dof_layout_.source_id, error.what());
}
}
Result<ElementResultBundle> EulerBeam3D::Recover(
const Vector& element_displacement) const {
if (dof_layout_.node_indices.size() != 2U ||
dof_layout_.components_per_node.size() != 6U ||
node_source_ids_.size() != 2U ||
element_displacement.Size() != kElementDofCount) {
return RuntimeFailure<ElementResultBundle>(
runtime_location_, dof_layout_.source_id,
"B33 runtime recovery requires a bound two-node layout and 12 DOFs.");
}
for (std::size_t dof = 0U; dof < element_displacement.Size(); ++dof) {
if (!std::isfinite(element_displacement[dof])) {
return RuntimeFailure<ElementResultBundle>(
runtime_location_, dof_layout_.source_id,
"B33 runtime recovery displacement must be finite.");
}
}
BeamRecovery recovered{};
try {
recovered = RecoverBeam(element_displacement);
} catch (const std::exception& error) {
return RuntimeFailure<ElementResultBundle>(
runtime_location_, dof_layout_.source_id, error.what());
}
BeamElementResultRows rows{};
rows.endpoint_rows.reserve(2U);
rows.gauss_rows.reserve(2U);
rows.stress_rows.reserve(recovered.stress_points.size());
for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) {
if (!IsFinite(recovered.equilibrium_end_actions[endpoint]) ||
!IsFinite(recovered.endpoint_section_resultants[endpoint])) {
return RuntimeFailure<ElementResultBundle>(
runtime_location_, dof_layout_.source_id,
"B33 endpoint recovery values must be finite.");
}
rows.endpoint_rows.push_back(
{element_index_, static_cast<int>(endpoint), node_source_ids_[endpoint],
recovered.equilibrium_end_actions[endpoint],
recovered.endpoint_section_resultants[endpoint]});
}
for (std::size_t gauss = 0U; gauss < 2U; ++gauss) {
if (!IsFinite(recovered.gauss_generalized_strains[gauss]) ||
!IsFinite(recovered.gauss_generalized_resultants[gauss])) {
return RuntimeFailure<ElementResultBundle>(
runtime_location_, dof_layout_.source_id,
"B33 Gauss recovery values must be finite.");
}
rows.gauss_rows.push_back({element_index_, static_cast<int>(gauss + 1U),
recovered.gauss_generalized_strains[gauss],
recovered.gauss_generalized_resultants[gauss]});
}
for (const auto& point : recovered.stress_points) {
if ((point.gauss_point != 1 && point.gauss_point != 2) ||
!std::isfinite(point.x1) || !std::isfinite(point.x2) ||
!std::isfinite(point.s11)) {
return RuntimeFailure<ElementResultBundle>(
runtime_location_, dof_layout_.source_id,
"B33 stress recovery identity and values must be finite.");
}
rows.stress_rows.push_back({element_index_, point.gauss_point,
point.section_point, point.x1, point.x2,
point.s11, point.source});
}
return Result<ElementResultBundle>::Success(
{dof_layout_.source_id, std::move(rows)});
}
void EulerBeam3D::BindRuntime(ElementDofLayout layout,
const EntityIndex element_index,
std::vector<SourceEntityId> node_source_ids,
SourceLocation location) {
dof_layout_ = std::move(layout);
element_index_ = element_index;
node_source_ids_ = std::move(node_source_ids);
runtime_location_ = std::move(location);
}
EulerBeam3D::EulerBeam3D(double length, double youngs_modulus,
double shear_modulus, double area, double iy,
double iz, double torsional_constant,
std::array<double, 9> rotation,
std::vector<std::array<double, 2>> section_points)
: length_{length},
youngs_modulus_{youngs_modulus},
shear_modulus_{shear_modulus},
area_{area},
iy_{iy},
iz_{iz},
torsional_constant_{torsional_constant},
rotation_{rotation},
section_points_{std::move(section_points)} {}
} // namespace fesa