feat: complete first MITC4 FEAST eigen implementation validation

This commit is contained in:
KOKO\Mimi
2026-09-10 02:24:04 +09:00
parent 7ebdce373c
commit d5e08f868b
26 changed files with 1452 additions and 82 deletions
@@ -0,0 +1,180 @@
#include "fesa/analysis/linear_frequency_procedure.h"
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <cmath>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <string>
#include "fesa/analysis/prepared_analysis.h"
#include "fesa/elements/element_factory.h"
#include "fesa/elements/mitc4_shell.h"
#include "fesa/model/domain.h"
namespace {
std::string StatusText(const fesa::Status& status) {
std::string result;
for (const auto& diagnostic : status.Diagnostics())
result += diagnostic.code + " " + diagnostic.message + "\n";
return result;
}
fesa::Result<fesa::Domain> SharedNodeDomain(const bool reverse_source_order) {
std::ostringstream deck;
deck << R"inp(*Part,name=Panel
*Node
1,0,0,0
2,1,0,0
3,2,0,0
4,0,1,0
5,1,1,0
6,2,1,0
*Element,type=S4
)inp";
if (reverse_source_order)
deck << "20,2,3,6,5\n10,1,2,5,4\n";
else
deck << "10,1,2,5,4\n20,2,3,6,5\n";
deck << R"inp(*Elset,elset=Thin
10
*Elset,elset=Thick
20
*Shell Section,elset=Thin,material=Light
0.5
*Shell Section,elset=Thick,material=Heavy
1.0
*End Part
*Assembly,name=Assembly
*Instance,name=Panel-1,part=Panel
*End Instance
*Nset,nset=Fixed,instance=Panel-1
1,2,3,4,6
*Nset,nset=Shared,instance=Panel-1
5
*End Assembly
*Material,name=Light
*Elastic
12,0
*Density
2
*Material,name=Heavy
*Elastic
12,0
*Density
5
*Boundary
Fixed,1,6
Shared,1,3
Shared,5,6
*Step,name=Step-2,nlgeom=NO,perturbation
*Frequency
1
*End Step
)inp";
static std::atomic<unsigned long long> sequence{0U};
const auto path = std::filesystem::temp_directory_path() /
("fesa-eigen-mass-" +
std::to_string(std::chrono::steady_clock::now()
.time_since_epoch()
.count()) +
"-" + std::to_string(sequence.fetch_add(1U)) + ".inp");
{
std::ofstream stream{path};
stream << deck.str();
}
auto prepared = fesa::PrepareAnalysisInput(path);
std::error_code ignored;
std::filesystem::remove(path, ignored);
if (!prepared.HasValue())
return fesa::Result<fesa::Domain>::Failure(prepared.GetStatus());
return fesa::Result<fesa::Domain>::Success(
std::move(prepared.Value().domain));
}
struct SharedDofPencil {
double stiffness{0.0};
double mass{0.0};
};
fesa::Result<SharedDofPencil> DirectSharedDofPencil(
const fesa::Domain& domain) {
const fesa::ElementFactory factory;
SharedDofPencil result;
for (std::size_t index = 0U; index < domain.ShellElements().Size(); ++index) {
const auto& definition = domain.ShellElements()[index];
const auto runtime = factory.Create(definition, domain);
if (!runtime.HasValue())
return fesa::Result<SharedDofPencil>::Failure(runtime.GetStatus());
const auto* shell =
dynamic_cast<const fesa::Mitc4Shell*>(runtime.Value().get());
if (shell == nullptr)
return fesa::Result<SharedDofPencil>::Failure(fesa::Status::Failure(
fesa::FailureCategory::kModel,
{{fesa::Severity::kError, "invalid-test-shell", {}, "ELEMENT", {},
"Frequency mass fixture did not create an MITC4 shell."}}));
const auto local = std::find(definition.NodeIndices().begin(),
definition.NodeIndices().end(), 4U);
if (local == definition.NodeIndices().end())
return fesa::Result<SharedDofPencil>::Failure(fesa::Status::Failure(
fesa::FailureCategory::kModel,
{{fesa::Severity::kError, "invalid-test-shell", {}, "ELEMENT", {},
"Frequency mass fixture lost its shared node."}}));
const auto mass =
shell->Mass(definition.MaterialIndex() == 0U ? 2.0 : 5.0);
const auto stiffness = shell->Stiffness();
if (!mass.HasValue())
return fesa::Result<SharedDofPencil>::Failure(mass.GetStatus());
if (!stiffness.HasValue())
return fesa::Result<SharedDofPencil>::Failure(stiffness.GetStatus());
const std::size_t dof =
6U * static_cast<std::size_t>(
std::distance(definition.NodeIndices().begin(), local)) +
3U;
result.mass += mass.Value().lumped_global24(dof, dof);
result.stiffness += stiffness.Value().stabilized_global24(dof, dof);
}
return fesa::Result<SharedDofPencil>::Success(result);
}
// E2-C06: sections and densities differ across the shared source node. The
// one free physical rotation makes the executor's reduced pencil exactly the
// independently scattered local diagonal sum.
TEST(EigenMassAssembly, ScattersHeterogeneousSharedNodeMassInSourceOrder) {
constexpr double analytic_shared_rotary_mass = 7.0 / 64.0;
std::array<double, 2> eigenvalues{};
for (std::size_t order = 0U; order < eigenvalues.size(); ++order) {
const auto domain = SharedNodeDomain(order != 0U);
ASSERT_TRUE(domain.HasValue()) << StatusText(domain.GetStatus());
const auto expected = DirectSharedDofPencil(domain.Value());
ASSERT_TRUE(expected.HasValue()) << StatusText(expected.GetStatus());
EXPECT_LE(std::abs(expected.Value().mass - analytic_shared_rotary_mass) /
analytic_shared_rotary_mass,
1e-12);
const auto state =
fesa::LinearFrequencyProcedureExecutor{}.Execute(domain.Value());
ASSERT_TRUE(state.HasValue()) << StatusText(state.GetStatus());
ASSERT_EQ(state.Value().free_dof_count, 1U);
ASSERT_EQ(state.Value().modes.size(), 1U);
const double expected_eigenvalue =
expected.Value().stiffness / analytic_shared_rotary_mass;
EXPECT_LE(std::abs(state.Value().modes[0U].eigenvalue - expected_eigenvalue) /
(std::abs(state.Value().modes[0U].eigenvalue) +
std::abs(expected_eigenvalue)),
1e-12);
const double amplitude = state.Value().vectors(0U, 6U * 4U + 3U);
EXPECT_LE(std::abs(amplitude * amplitude * analytic_shared_rotary_mass -
1.0),
1e-12);
eigenvalues[order] = state.Value().modes[0U].eigenvalue;
}
EXPECT_NEAR(eigenvalues[0U], eigenvalues[1U], 1e-12);
}
} // namespace