#include "fesa/elements/euler_beam_3d.h" #include #include #include #include #include #include #include #include #include #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 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& 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 ModelFailure(const std::string& code, const SourceLocation& location, const std::string& identity, const std::string& message) { return Result::Failure(Status::Failure( FailureCategory::kModel, {{Severity::kError, code, location, "*ELEMENT", identity, message}})); } /// @brief Creates a structured failure at the runtime element boundary. template Result RuntimeFailure(const SourceLocation& location, const SourceEntityId& source_id, const std::string& message) { return Result::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 bool IsFinite(const std::array& values) { return std::all_of(values.begin(), values.end(), [](const auto& value) { if constexpr (std::is_arithmetic_v) { 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& 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 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& diagonal) { Matrix closed{kElementDofCount, kElementDofCount}; const auto add_block = [&closed](const std::array& 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& 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 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::infinity(); } const double difference = std::abs(lhs_value - rhs_value); if (!std::isfinite(difference)) { return std::numeric_limits::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::infinity(); } const double normalized_error = maximum_difference / scale_value; return std::isfinite(normalized_error) ? normalized_error : std::numeric_limits::infinity(); } /// @brief Recovers generalized strain in fixed component and DOF order. std::array GeneralizedStrain( const Matrix& b, const Vector& local_displacement) { std::array 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 GeneralizedResultant( const std::array& strain, const std::array& diagonal) { std::array 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::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 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 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 rotation = {ex[0], ex[1], ex[2], ey[0], ey[1], ey[2], ez[0], ez[1], ez[2]}; return Result::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 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 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 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 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(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(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 EulerBeam3D::ComputeStiffness() const { if (dof_layout_.node_indices.size() != 2U || dof_layout_.components_per_node.size() != 6U) { return RuntimeFailure( runtime_location_, dof_layout_.source_id, "B33 runtime stiffness requires a bound two-node six-DOF layout."); } try { return Result::Success( {dof_layout_, GlobalStiffness()}); } catch (const std::exception& error) { return RuntimeFailure( runtime_location_, dof_layout_.source_id, error.what()); } } Result 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( 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( 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( 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( runtime_location_, dof_layout_.source_id, "B33 endpoint recovery values must be finite."); } rows.endpoint_rows.push_back( {element_index_, static_cast(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( runtime_location_, dof_layout_.source_id, "B33 Gauss recovery values must be finite."); } rows.gauss_rows.push_back({element_index_, static_cast(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( 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::Success( {dof_layout_.source_id, std::move(rows)}); } void EulerBeam3D::BindRuntime(ElementDofLayout layout, const EntityIndex element_index, std::vector 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 rotation, std::vector> 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