feat: bind Solid definitions to Hex8 runtime elements

This commit is contained in:
KOKO\Mimi
2026-10-09 01:42:33 +09:00
parent 744b34804d
commit 6825314845
6 changed files with 182 additions and 0 deletions
@@ -8,6 +8,7 @@
#include <functional>
#include <memory>
#include <stdexcept>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
@@ -16,6 +17,7 @@
#include "fesa/elements/element.h"
#include "fesa/elements/euler_beam_3d.h"
#include "fesa/elements/hex8_element.h"
#include "fesa/elements/mitc4_shell.h"
#include "fesa/fem/dof_manager.h"
#include "fesa/model/domain.h"
@@ -90,6 +92,38 @@ fesa::Domain MakeDomain(fesa::ModelDefinition definition) {
return std::move(candidate.Value());
}
fesa::ModelDefinition SolidDefinition(bool with_density = true) {
fesa::ModelDefinition definition{};
definition.source_path = "element-factory-solid.inp";
definition.source_content_identity = "fnv1a64:solid";
const fesa::Hex8Element::Coordinates coordinates{{
{0.0, 0.0, 0.0}, {1.0, 0.0, 0.0}, {1.0, 1.0, 0.0}, {0.0, 1.0, 0.0},
{0.0, 0.0, 1.0}, {1.0, 0.0, 1.0}, {1.0, 1.0, 1.0}, {0.0, 1.0, 1.0}}};
for (std::size_t node = 0U; node < coordinates.size(); ++node) {
const auto label = static_cast<std::int64_t>(10U * (node + 1U));
definition.nodes.push_back({{"Solid-1", label, std::to_string(label)},
coordinates[node],
{definition.source_path, node + 1U}});
}
definition.materials = {{"SolidMaterial", 1000.0, 0.3,
{definition.source_path, 10U}}};
definition.solid_sections.emplace_back(
fesa::SourceEntityId{"Solid-1", 31, "31"}, "SolidSet", 0U,
fesa::SourceLocation{definition.source_path, 11U});
definition.solid_elements.emplace_back(
fesa::SourceEntityId{"Solid-1", 99, "99"},
std::array<fesa::EntityIndex, 8>{0U, 1U, 2U, 3U, 4U, 5U, 6U, 7U},
0U, 0U, fesa::SourceLocation{definition.source_path, 12U});
if (with_density) {
definition.densities.push_back({0U, 2.0, {definition.source_path, 13U}});
}
definition.steps.emplace_back(
"Solid", std::vector<fesa::PrescribedDisplacementDefinition>{},
std::vector<fesa::NodalLoad>{}, 1.0, 1.0, 1.0, 1.0,
fesa::SourceLocation{definition.source_path, 14U});
return definition;
}
void ExpectSameMatrix(const fesa::Matrix& actual,
const fesa::Matrix& expected) {
ASSERT_EQ(actual.Rows(), expected.Rows());
@@ -344,3 +378,62 @@ TEST(ElementFactory, RejectsUnknownAndIncompatibleDefinitions) {
EXPECT_EQ(missing_frame_result.GetStatus().Diagnostics()[0U].code,
"missing-shell-frame");
}
TEST(ElementFactory, SolidBindingPreservesThreeComponentsAndReferenceMass) {
const auto domain = MakeDomain(SolidDefinition());
auto candidate = fesa::ElementFactory{}.Create(domain.Elements()[0U], domain);
ASSERT_TRUE(candidate.HasValue());
const auto* solid = dynamic_cast<const fesa::Hex8Element*>(candidate.Value().get());
ASSERT_NE(solid, nullptr);
EXPECT_EQ(solid->DofLayout().source_id.source_label, 99);
EXPECT_EQ(solid->DofLayout().components_per_node,
(std::vector<fesa::DofComponent>{fesa::DofComponent::kUx,
fesa::DofComponent::kUy,
fesa::DofComponent::kUz}));
auto stiffness = solid->ComputeStiffness();
ASSERT_TRUE(stiffness.HasValue());
EXPECT_EQ(stiffness.Value().values.Rows(), 24U);
auto mass = solid->ComputeMass();
ASSERT_TRUE(mass.HasValue());
double total_mass = 0.0;
for (std::size_t a = 0U; a < 8U; ++a) {
for (std::size_t b = 0U; b < 8U; ++b) {
total_mass += mass.Value().values(3U * a, 3U * b);
}
}
EXPECT_NEAR(total_mass, 2.0, 1.0e-13);
EXPECT_NEAR(mass.Value().values(0U, 3U), 2.0 / 54.0, 1.0e-15);
auto response = solid->EvaluateLinear(fesa::Vector{24U});
ASSERT_TRUE(response.HasValue());
for (std::size_t point = 0U; point < 8U; ++point) {
EXPECT_EQ(response.Value().points[point].element, 0U);
EXPECT_EQ(response.Value().points[point].integration_point, point + 1U);
}
}
TEST(ElementFactory, SolidStaticBindingKeepsAbsentDensityAndRejectsForeignOwner) {
const auto domain = MakeDomain(SolidDefinition(false));
auto candidate = fesa::ElementFactory{}.Create(domain.Elements()[0U], domain);
ASSERT_TRUE(candidate.HasValue());
EXPECT_TRUE(candidate.Value()->ComputeStiffness().HasValue());
const auto* solid = dynamic_cast<const fesa::Hex8Element*>(candidate.Value().get());
ASSERT_NE(solid, nullptr);
EXPECT_FALSE(solid->ComputeMass().HasValue());
const auto foreign = SolidDefinition(false);
auto rejected = fesa::ElementFactory{}.Create(foreign.solid_elements[0U], domain);
ASSERT_FALSE(rejected.HasValue());
EXPECT_EQ(rejected.GetStatus().Diagnostics()[0U].code,
"invalid-element-definition");
}
TEST(ElementFactory, SolidBindingRejectsReferenceInversionWithSourceContext) {
auto definition = SolidDefinition();
for (auto& node : definition.nodes) {
node.coordinates[0U] = -node.coordinates[0U];
}
const auto domain = MakeDomain(std::move(definition));
auto rejected = fesa::ElementFactory{}.Create(domain.Elements()[0U], domain);
ASSERT_FALSE(rejected.HasValue());
EXPECT_EQ(rejected.GetStatus().Diagnostics()[0U].entity_identity, "Solid-1:99");
EXPECT_EQ(rejected.GetStatus().Diagnostics()[0U].location.line, 12U);
}