feat(result-contract-completion): step 0 — beam-element-end-recovery

This commit is contained in:
KOKO\Mimi
2026-08-02 01:14:40 +09:00
parent 5618636f0d
commit 5fe57cb145
7 changed files with 341 additions and 11 deletions
+17
View File
@@ -2,22 +2,34 @@
#include <array> #include <array>
#include <optional> #include <optional>
#include <span>
#include <vector> #include <vector>
#include <fesa/core/diagnostic.hpp> #include <fesa/core/diagnostic.hpp>
#include <fesa/core/vec3.hpp> #include <fesa/core/vec3.hpp>
#include <fesa/fem/beam_frame.hpp> #include <fesa/fem/beam_frame.hpp>
#include <fesa/model/beam_section.hpp> #include <fesa/model/beam_section.hpp>
#include <fesa/model/ids.hpp>
#include <fesa/model/material.hpp> #include <fesa/model/material.hpp>
namespace fesa { namespace fesa {
struct Beam3D2Input final { struct Beam3D2Input final {
std::array<Vec3, 2> coordinates; std::array<Vec3, 2> coordinates;
std::array<NodeId, 2> node_ids;
IsotropicElastic material; IsotropicElastic material;
BeamSection section; BeamSection section;
}; };
struct BeamSectionResult final {
double xi;
NodeId end_node;
std::array<double, 6> section_strain;
std::array<double, 6> section_force;
double centroid_sigma_xx;
std::vector<double> sigma_xx;
};
struct Beam3D2Contribution final { struct Beam3D2Contribution final {
Matrix12 local_stiffness; Matrix12 local_stiffness;
Matrix12 global_stiffness; Matrix12 global_stiffness;
@@ -32,4 +44,9 @@ struct BeamKernelResult final {
[[nodiscard]] BeamKernelResult compute_beam3d2( [[nodiscard]] BeamKernelResult compute_beam3d2(
const Beam3D2Input& input); const Beam3D2Input& input);
[[nodiscard]] std::vector<BeamSectionResult> recover_beam3d2(
const Beam3D2Input& input,
std::span<const double, 12> element_displacement,
std::span<const std::array<double, 2>> recovery_points);
} // namespace fesa } // namespace fesa
+1 -1
View File
@@ -48,4 +48,4 @@
"status": "pending" "status": "pending"
} }
] ]
} }
+1
View File
@@ -81,6 +81,7 @@ ElementEvaluation evaluate_element(
domain.node(element.nodes[0]).position, domain.node(element.nodes[0]).position,
domain.node(element.nodes[1]).position, domain.node(element.nodes[1]).position,
}, },
element.nodes,
domain.material(element.material), domain.material(element.material),
domain.section(element.section), domain.section(element.section),
}); });
+1
View File
@@ -128,6 +128,7 @@ std::vector<NumericContribution> collect_numeric_contributions(
domain.node(element.nodes[0]).position, domain.node(element.nodes[0]).position,
domain.node(element.nodes[1]).position, domain.node(element.nodes[1]).position,
}, },
element.nodes,
domain.material(element.material), domain.material(element.material),
domain.section(element.section), domain.section(element.section),
}); });
+115 -10
View File
@@ -4,8 +4,10 @@
#include <cmath> #include <cmath>
#include <cstddef> #include <cstddef>
#include <optional> #include <optional>
#include <stdexcept>
#include <string> #include <string>
#include <utility> #include <utility>
#include <vector>
#include <fesa/fem/gauss_rule.hpp> #include <fesa/fem/gauss_rule.hpp>
#include <fesa/fem/line2_shape.hpp> #include <fesa/fem/line2_shape.hpp>
@@ -32,6 +34,20 @@ bool is_positive_finite(const double value) {
return std::isfinite(value) && value > 0.0; return std::isfinite(value) && value > 0.0;
} }
std::array<double, 6> constitutive_values(
const Beam3D2Input& input) {
const double shear_modulus =
input.material.young / (2.0 * (1.0 + input.material.poisson));
return {
input.material.young * input.section.area,
shear_modulus * input.section.shear_area_y,
shear_modulus * input.section.shear_area_z,
shear_modulus * input.section.torsion_j,
input.material.young * input.section.iy,
input.material.young * input.section.iz,
};
}
std::optional<BeamKernelResult> validate_properties( std::optional<BeamKernelResult> validate_properties(
const Beam3D2Input& input) { const Beam3D2Input& input) {
if (!std::isfinite(input.material.young) || if (!std::isfinite(input.material.young) ||
@@ -149,6 +165,34 @@ Matrix12 transform_stiffness(
return global; return global;
} }
std::array<double, 12> transform_displacement(
const Matrix12& transformation,
const std::span<const double, 12> global_displacement) {
std::array<double, 12> local_displacement{};
for (std::size_t row = 0; row < transformation.size(); ++row) {
for (std::size_t column = 0;
column < transformation[row].size();
++column) {
local_displacement[row] +=
transformation[row][column] * global_displacement[column];
}
}
return local_displacement;
}
std::array<double, 6> evaluate_strain(
const StrainMatrix& strain_matrix,
const std::array<double, 12>& local_displacement) {
std::array<double, 6> strain{};
for (std::size_t component = 0; component < strain.size(); ++component) {
for (std::size_t dof = 0; dof < local_displacement.size(); ++dof) {
strain[component] +=
strain_matrix[component][dof] * local_displacement[dof];
}
}
return strain;
}
bool is_finite(const Matrix12& matrix) { bool is_finite(const Matrix12& matrix) {
for (const auto& row : matrix) { for (const auto& row : matrix) {
for (const double value : row) { for (const double value : row) {
@@ -189,16 +233,8 @@ BeamKernelResult compute_beam3d2(const Beam3D2Input& input) {
"Beam kernel requires a representable positive Jacobian."); "Beam kernel requires a representable positive Jacobian.");
} }
const double shear_modulus = const std::array<double, 6> constitutive =
input.material.young / (2.0 * (1.0 + input.material.poisson)); constitutive_values(input);
const std::array<double, 6> constitutive{
input.material.young * input.section.area,
shear_modulus * input.section.shear_area_y,
shear_modulus * input.section.shear_area_z,
shear_modulus * input.section.torsion_j,
input.material.young * input.section.iy,
input.material.young * input.section.iz,
};
for (const double value : constitutive) { for (const double value : constitutive) {
if (!is_positive_finite(value)) { if (!is_positive_finite(value)) {
return error_result( return error_result(
@@ -241,4 +277,73 @@ BeamKernelResult compute_beam3d2(const Beam3D2Input& input) {
}; };
} }
std::vector<BeamSectionResult> recover_beam3d2(
const Beam3D2Input& input,
const std::span<const double, 12> element_displacement,
const std::span<const std::array<double, 2>> recovery_points) {
const BeamKernelResult kernel = compute_beam3d2(input);
if (!kernel.contribution.has_value()) {
const std::string message = kernel.diagnostics.empty()
? "Beam recovery requires a valid Beam3D2 input."
: kernel.diagnostics.front().message;
throw std::invalid_argument{message};
}
const Vec3 axis{
input.coordinates[1].x - input.coordinates[0].x,
input.coordinates[1].y - input.coordinates[0].y,
input.coordinates[1].z - input.coordinates[0].z,
};
const double jacobian = std::hypot(axis.x, axis.y, axis.z) / 2.0;
const Matrix12 transformation =
beam_transformation(kernel.contribution->frame);
const std::array<double, 12> local_displacement =
transform_displacement(transformation, element_displacement);
const std::array<double, 6> center_strain = evaluate_strain(
strain_matrix(0.0, jacobian),
local_displacement);
const std::array<double, 6> constitutive =
constitutive_values(input);
std::vector<BeamSectionResult> results;
results.reserve(input.node_ids.size());
for (std::size_t end = 0; end < input.node_ids.size(); ++end) {
const double xi = end == 0 ? -1.0 : 1.0;
std::array<double, 6> section_strain = evaluate_strain(
strain_matrix(xi, jacobian),
local_displacement);
section_strain[1] = center_strain[1];
section_strain[2] = center_strain[2];
std::array<double, 6> section_force{};
for (std::size_t component = 0;
component < section_force.size();
++component) {
section_force[component] =
constitutive[component] * section_strain[component];
}
std::vector<double> sigma_xx;
sigma_xx.reserve(recovery_points.size());
for (const auto& point : recovery_points) {
const double y = point[0];
const double z = point[1];
sigma_xx.push_back(
input.material.young *
(section_strain[0] + z * section_strain[4] -
y * section_strain[5]));
}
results.push_back({
xi,
input.node_ids[end],
section_strain,
section_force,
section_force[0] / input.section.area,
std::move(sigma_xx),
});
}
return results;
}
} // namespace fesa } // namespace fesa
+18
View File
@@ -360,6 +360,24 @@ add_test(
--gtest_filter=RigidBody.* --gtest_filter=RigidBody.*
) )
add_test(
NAME BeamRecovery
COMMAND "$<TARGET_FILE:fesa_beam3d2_tests>"
--gtest_filter=BeamRecovery.*
)
add_test(
NAME SectionForce
COMMAND "$<TARGET_FILE:fesa_beam3d2_tests>"
--gtest_filter=SectionForce.*
)
add_test(
NAME CentroidStress
COMMAND "$<TARGET_FILE:fesa_beam3d2_tests>"
--gtest_filter=CentroidStress.*
)
add_executable(fesa_serial_assembly_tests add_executable(fesa_serial_assembly_tests
unit/assembly/serial_assembler_test.cpp unit/assembly/serial_assembler_test.cpp
) )
+188
View File
@@ -22,6 +22,7 @@ constexpr double kShearAreaZ = 0.2;
fesa::Beam3D2Input make_x_axis_input(const double length) { fesa::Beam3D2Input make_x_axis_input(const double length) {
return { return {
{{{0.0, 0.0, 0.0}, {length, 0.0, 0.0}}}, {{{0.0, 0.0, 0.0}, {length, 0.0, 0.0}}},
{fesa::NodeId{1}, fesa::NodeId{2}},
{fesa::MaterialId{1}, "elastic", kYoung, kPoisson}, {fesa::MaterialId{1}, "elastic", kYoung, kPoisson},
{ {
fesa::SectionId{1}, fesa::SectionId{1},
@@ -369,4 +370,191 @@ TEST(Beam3D2, PreservesGlobalEnergyUnderRigidCoordinateRotation) {
4096.0 * std::numeric_limits<double>::epsilon()); 4096.0 * std::numeric_limits<double>::epsilon());
} }
TEST(BeamRecovery, RecoversPureAxialStrainAndForceAtBothEnds) {
fesa::Beam3D2Input input = make_x_axis_input(2.0);
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
std::array<double, 12> displacement{};
displacement[6] = 0.4;
const auto results = fesa::recover_beam3d2(input, displacement, {});
ASSERT_EQ(results.size(), 2U);
EXPECT_DOUBLE_EQ(results[0].xi, -1.0);
EXPECT_EQ(results[0].end_node, fesa::NodeId{101});
EXPECT_DOUBLE_EQ(results[1].xi, 1.0);
EXPECT_EQ(results[1].end_node, fesa::NodeId{202});
for (const auto& result : results) {
expect_relative_near(result.section_strain[0], 0.2);
expect_relative_near(result.section_force[0], 16.8);
expect_relative_near(result.centroid_sigma_xx, 42.0);
for (std::size_t component = 1; component < 6; ++component) {
expect_relative_near(result.section_strain[component], 0.0);
expect_relative_near(result.section_force[component], 0.0);
}
EXPECT_TRUE(result.sigma_xx.empty());
}
}
TEST(BeamRecovery, RecoversPureTorsionAtBothEnds) {
fesa::Beam3D2Input input = make_x_axis_input(2.0);
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
std::array<double, 12> displacement{};
displacement[9] = 0.6;
const double shear_modulus = kYoung / (2.0 * (1.0 + kPoisson));
const auto results = fesa::recover_beam3d2(input, displacement, {});
ASSERT_EQ(results.size(), 2U);
for (const auto& result : results) {
expect_relative_near(result.section_strain[3], 0.3);
expect_relative_near(
result.section_force[3],
shear_modulus * kTorsionJ * 0.3);
for (const std::size_t component :
std::array<std::size_t, 5>{0, 1, 2, 4, 5}) {
expect_relative_near(result.section_strain[component], 0.0);
expect_relative_near(result.section_force[component], 0.0);
}
}
}
TEST(BeamRecovery, UsesReducedIntegrationPointForShearAtBothEnds) {
fesa::Beam3D2Input input = make_x_axis_input(2.0);
std::array<double, 12> displacement{};
displacement[7] = 1.2;
displacement[5] = 0.2;
displacement[11] = 0.6;
displacement[8] = -0.4;
displacement[4] = 0.1;
displacement[10] = 0.5;
const double shear_modulus = kYoung / (2.0 * (1.0 + kPoisson));
const auto results = fesa::recover_beam3d2(input, displacement, {});
ASSERT_EQ(results.size(), 2U);
for (const auto& result : results) {
expect_relative_near(result.section_strain[1], 0.2);
expect_relative_near(result.section_strain[2], 0.1);
expect_relative_near(
result.section_force[1],
shear_modulus * kShearAreaY * 0.2);
expect_relative_near(
result.section_force[2],
shear_modulus * kShearAreaZ * 0.1);
}
}
TEST(BeamRecovery, UsesBeamFrameForGlobalDisplacements) {
fesa::Beam3D2Input input = make_x_axis_input(2.0);
input.coordinates = {{{0.0, 0.0, 0.0}, {0.0, 2.0, 0.0}}};
input.section.orientation = {0.0, 0.0, 1.0};
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
std::array<double, 12> displacement{};
displacement[7] = 0.4;
displacement[10] = 0.6;
const double shear_modulus = kYoung / (2.0 * (1.0 + kPoisson));
const auto results = fesa::recover_beam3d2(input, displacement, {});
ASSERT_EQ(results.size(), 2U);
for (const auto& result : results) {
expect_relative_near(result.section_strain[0], 0.2);
expect_relative_near(result.section_force[0], 16.8);
expect_relative_near(result.section_strain[3], 0.3);
expect_relative_near(
result.section_force[3],
shear_modulus * kTorsionJ * 0.3);
}
}
TEST(SectionForce, RecoversPureBendingAboutLocalYAtBothEnds) {
fesa::Beam3D2Input input = make_x_axis_input(2.0);
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
std::array<double, 12> displacement{};
displacement[4] = -0.4;
displacement[10] = 0.4;
const auto results = fesa::recover_beam3d2(input, displacement, {});
ASSERT_EQ(results.size(), 2U);
for (const auto& result : results) {
expect_relative_near(result.section_strain[4], 0.4);
expect_relative_near(result.section_force[4], 2.52);
for (const std::size_t component :
std::array<std::size_t, 5>{0, 1, 2, 3, 5}) {
expect_relative_near(result.section_strain[component], 0.0);
expect_relative_near(result.section_force[component], 0.0);
}
}
}
TEST(SectionForce, RecoversPureBendingAboutLocalZAtBothEnds) {
fesa::Beam3D2Input input = make_x_axis_input(2.0);
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
std::array<double, 12> displacement{};
displacement[5] = 0.25;
displacement[11] = -0.25;
const auto results = fesa::recover_beam3d2(input, displacement, {});
ASSERT_EQ(results.size(), 2U);
for (const auto& result : results) {
expect_relative_near(result.section_strain[5], -0.25);
expect_relative_near(result.section_force[5], -2.625);
for (const std::size_t component :
std::array<std::size_t, 5>{0, 1, 2, 3, 4}) {
expect_relative_near(result.section_strain[component], 0.0);
expect_relative_near(result.section_force[component], 0.0);
}
}
}
TEST(SectionForce, PreservesBiaxialBendingSignsAtBothEnds) {
fesa::Beam3D2Input input = make_x_axis_input(2.0);
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
std::array<double, 12> displacement{};
displacement[4] = -0.4;
displacement[10] = 0.4;
displacement[5] = 0.25;
displacement[11] = -0.25;
const auto results = fesa::recover_beam3d2(input, displacement, {});
ASSERT_EQ(results.size(), 2U);
for (const auto& result : results) {
expect_relative_near(result.section_force[4], 2.52);
expect_relative_near(result.section_force[5], -2.625);
}
}
TEST(CentroidStress, UsesAxialStressAndPreservesRecoveryPointOrder) {
fesa::Beam3D2Input input = make_x_axis_input(2.0);
input.node_ids = {fesa::NodeId{101}, fesa::NodeId{202}};
std::array<double, 12> displacement{};
displacement[6] = 0.2;
displacement[4] = -0.2;
displacement[10] = 0.2;
displacement[5] = 0.3;
displacement[11] = -0.3;
const std::array<std::array<double, 2>, 3> recovery_points{{
{2.0, 3.0},
{-1.0, 4.0},
{5.0, -2.0},
}};
const auto results = fesa::recover_beam3d2(
input,
displacement,
recovery_points);
ASSERT_EQ(results.size(), 2U);
for (const auto& result : results) {
expect_relative_near(result.centroid_sigma_xx, 21.0);
ASSERT_EQ(result.sigma_xx.size(), recovery_points.size());
expect_relative_near(result.sigma_xx[0], 273.0);
expect_relative_near(result.sigma_xx[1], 126.0);
expect_relative_near(result.sigma_xx[2], 252.0);
}
}
} // namespace } // namespace