feat(linear-static-mitc4-shell): step 8 - shell-load-validation
This commit is contained in:
@@ -17,6 +17,7 @@ namespace fesa {
|
|||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr std::size_t dofsPerNode = 6U;
|
constexpr std::size_t dofsPerNode = 6U;
|
||||||
|
constexpr double shellMomentProjectionTolerance = 1.0e-12;
|
||||||
|
|
||||||
Status loadFailure(
|
Status loadFailure(
|
||||||
const std::string& code,
|
const std::string& code,
|
||||||
@@ -215,6 +216,70 @@ Status validateFiniteVector(
|
|||||||
return Status::ok();
|
return Status::ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Status validateShellMoments(
|
||||||
|
const Domain& domain,
|
||||||
|
const Vector& fullLoad) {
|
||||||
|
if (domain.shellElements().empty()) {
|
||||||
|
return Status::ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<const ShellNodeInitialFrame*> frameByNode(
|
||||||
|
domain.nodes().size(), nullptr);
|
||||||
|
for (const auto& frame : domain.shellNodeInitialFrames()) {
|
||||||
|
if (frame.nodeIndex >= frameByNode.size() ||
|
||||||
|
frameByNode[frame.nodeIndex] != nullptr) {
|
||||||
|
return loadFailure(
|
||||||
|
"invalid-shell-director",
|
||||||
|
{domain.sourcePath(), 0U},
|
||||||
|
"NODE",
|
||||||
|
std::to_string(frame.nodeIndex),
|
||||||
|
"Shell nodal directors must have unique in-range node identities.");
|
||||||
|
}
|
||||||
|
frameByNode[frame.nodeIndex] = &frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t node = 0U; node < domain.nodes().size(); ++node) {
|
||||||
|
const double momentX = fullLoad[node * dofsPerNode + 3U];
|
||||||
|
const double momentY = fullLoad[node * dofsPerNode + 4U];
|
||||||
|
const double momentZ = fullLoad[node * dofsPerNode + 5U];
|
||||||
|
if (momentX == 0.0 && momentY == 0.0 && momentZ == 0.0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto* const frame = frameByNode[node];
|
||||||
|
if (frame == nullptr) {
|
||||||
|
return loadFailure(
|
||||||
|
"invalid-shell-director",
|
||||||
|
domain.nodes()[node].location,
|
||||||
|
"NODE",
|
||||||
|
domain.nodes()[node].sourceId.sourceLabelText,
|
||||||
|
"A loaded shell node must have an approved initial director.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const double momentScale = std::max(
|
||||||
|
std::abs(momentX),
|
||||||
|
std::max(std::abs(momentY), std::abs(momentZ)));
|
||||||
|
const double scaledX = momentX / momentScale;
|
||||||
|
const double scaledY = momentY / momentScale;
|
||||||
|
const double scaledZ = momentZ / momentScale;
|
||||||
|
const double scaledNorm = std::hypot(scaledX, scaledY, scaledZ);
|
||||||
|
const double scaledDot =
|
||||||
|
frame->director[0U] * scaledX +
|
||||||
|
frame->director[1U] * scaledY +
|
||||||
|
frame->director[2U] * scaledZ;
|
||||||
|
const double projectionRatio = std::abs(scaledDot) / scaledNorm;
|
||||||
|
if (!(projectionRatio <= shellMomentProjectionTolerance)) {
|
||||||
|
return loadFailure(
|
||||||
|
"unsupported-drilling-load",
|
||||||
|
domain.nodes()[node].location,
|
||||||
|
"CLOAD",
|
||||||
|
domain.nodes()[node].sourceId.sourceLabelText,
|
||||||
|
"The aggregate nodal moment has an unsupported director-parallel component.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Status::ok();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Result<Vector> LoadAssembler::assembleFullNodalLoad(
|
Result<Vector> LoadAssembler::assembleFullNodalLoad(
|
||||||
@@ -328,6 +393,10 @@ Result<Vector> LoadAssembler::assembleFullNodalLoad(
|
|||||||
fullLoad[fullDof] = accumulated;
|
fullLoad[fullDof] = accumulated;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const Status shellMomentStatus = validateShellMoments(domain, fullLoad);
|
||||||
|
if (!shellMomentStatus.isOk()) {
|
||||||
|
return Result<Vector>::failure(shellMomentStatus);
|
||||||
|
}
|
||||||
return Result<Vector>::success(std::move(fullLoad));
|
return Result<Vector>::success(std::move(fullLoad));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,69 @@ LoadFixture makeFixture(
|
|||||||
return {std::move(domain), std::move(model), std::move(dofs)};
|
return {std::move(domain), std::move(model), std::move(dofs)};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LoadFixture makeShellFixture(
|
||||||
|
std::vector<fesa::BoundaryCondition> boundaries,
|
||||||
|
std::vector<fesa::NodalLoad> loads) {
|
||||||
|
const std::filesystem::path source{"models/shell-load-assembly.inp"};
|
||||||
|
fesa::ModelDefinition definition{};
|
||||||
|
definition.sourcePath = source;
|
||||||
|
definition.sourceContentIdentity = "fnv1a64:1122334455667788";
|
||||||
|
definition.nodes = {
|
||||||
|
{{"Shell-1", 10, "10"}, {0.0, 0.0, 0.0}, {source, 2U}},
|
||||||
|
{{"Shell-1", 20, "20"}, {1.0, 0.0, 0.0}, {source, 3U}},
|
||||||
|
{{"Shell-1", 30, "30"}, {1.0, 1.0, 0.0}, {source, 4U}},
|
||||||
|
{{"Shell-1", 40, "40"}, {0.0, 1.0, 0.0}, {source, 5U}}};
|
||||||
|
definition.materials = {
|
||||||
|
{"Material", 1000.0, 0.25, {source, 6U}}};
|
||||||
|
definition.shellSections = {
|
||||||
|
{"ShellSection", 0.1, 0U, {source, 7U}}};
|
||||||
|
definition.shellElements = {{
|
||||||
|
{"Shell-1", 1, "1"},
|
||||||
|
fesa::ShellSourceElementType::s4,
|
||||||
|
{0U, 1U, 2U, 3U},
|
||||||
|
0U,
|
||||||
|
0U,
|
||||||
|
{source, 8U}}};
|
||||||
|
for (std::size_t node = 0U; node < definition.nodes.size(); ++node) {
|
||||||
|
definition.shellNodeInitialFrames.push_back({
|
||||||
|
static_cast<fesa::EntityIndex>(node),
|
||||||
|
{0.0, 0.0, 1.0},
|
||||||
|
{1.0, 0.0, 0.0},
|
||||||
|
{0.0, 1.0, 0.0}});
|
||||||
|
}
|
||||||
|
definition.steps = {{
|
||||||
|
"Step-1",
|
||||||
|
std::move(boundaries),
|
||||||
|
std::move(loads),
|
||||||
|
0.1,
|
||||||
|
1.0,
|
||||||
|
0.01,
|
||||||
|
1.0,
|
||||||
|
{source, 20U}}};
|
||||||
|
|
||||||
|
auto domainResult = fesa::Domain::create(std::move(definition));
|
||||||
|
if (!domainResult.hasValue()) {
|
||||||
|
throw std::runtime_error{"Shell load fixture Domain construction failed."};
|
||||||
|
}
|
||||||
|
auto domain = std::make_unique<fesa::Domain>(
|
||||||
|
std::move(domainResult.value()));
|
||||||
|
|
||||||
|
auto modelResult = fesa::AnalysisModel::create(*domain);
|
||||||
|
if (!modelResult.hasValue()) {
|
||||||
|
throw std::runtime_error{"Shell load fixture AnalysisModel construction failed."};
|
||||||
|
}
|
||||||
|
auto model = std::make_unique<fesa::AnalysisModel>(
|
||||||
|
std::move(modelResult.value()));
|
||||||
|
|
||||||
|
auto dofResult = fesa::DofManager::create(*model);
|
||||||
|
if (!dofResult.hasValue()) {
|
||||||
|
throw std::runtime_error{"Shell load fixture DofManager construction failed."};
|
||||||
|
}
|
||||||
|
auto dofs = std::make_unique<fesa::DofManager>(
|
||||||
|
std::move(dofResult.value()));
|
||||||
|
return {std::move(domain), std::move(model), std::move(dofs)};
|
||||||
|
}
|
||||||
|
|
||||||
fesa::SparseMatrix makeDenseSparse(
|
fesa::SparseMatrix makeDenseSparse(
|
||||||
const std::size_t rows,
|
const std::size_t rows,
|
||||||
const std::size_t columns,
|
const std::size_t columns,
|
||||||
@@ -174,6 +237,100 @@ TEST(LoadAssembly, AccumulatesSignedLoadsInSourceOrder) {
|
|||||||
EXPECT_DOUBLE_EQ(second.value()[0U], 0.0);
|
EXPECT_DOUBLE_EQ(second.value()[0U], 0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MITC4-LOAD-001
|
||||||
|
TEST(LoadAssembly, AggregatesAllSixGlobalShellLoadComponentsInSourceOrder) {
|
||||||
|
const std::filesystem::path source{"models/shell-load-assembly.inp"};
|
||||||
|
auto fixture = makeShellFixture(
|
||||||
|
{},
|
||||||
|
{{"10", 1, 1.0e16, {source, 30U}},
|
||||||
|
{"10", 1, -1.0e16, {source, 31U}},
|
||||||
|
{"10", 1, 1.0, {source, 32U}},
|
||||||
|
{"10", 2, 2.0, {source, 33U}},
|
||||||
|
{"10", 3, 3.0, {source, 34U}},
|
||||||
|
{"10", 4, 1.0e16, {source, 35U}},
|
||||||
|
{"10", 4, -1.0e16, {source, 36U}},
|
||||||
|
{"10", 4, 4.0, {source, 37U}},
|
||||||
|
{"10", 5, 5.0, {source, 38U}},
|
||||||
|
{"10", 6, 6.0, {source, 39U}},
|
||||||
|
{"10", 6, -6.0, {source, 40U}}});
|
||||||
|
|
||||||
|
const auto result = fesa::LoadAssembler::assembleFullNodalLoad(
|
||||||
|
*fixture.model, *fixture.dofs);
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.hasValue());
|
||||||
|
ASSERT_EQ(result.value().size(), 24U);
|
||||||
|
EXPECT_EQ(
|
||||||
|
std::vector<double>(result.value().data(), result.value().data() + 6U),
|
||||||
|
(std::vector<double>{1.0, 2.0, 3.0, 4.0, 5.0, 0.0}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// MITC4-LOAD-002
|
||||||
|
TEST(LoadAssembly, AcceptsExactlyZeroAggregateShellMoment) {
|
||||||
|
const std::filesystem::path source{"models/shell-load-assembly.inp"};
|
||||||
|
auto fixture = makeShellFixture(
|
||||||
|
{},
|
||||||
|
{{"10", 4, 3.0, {source, 30U}},
|
||||||
|
{"10", 4, -3.0, {source, 31U}},
|
||||||
|
{"10", 5, 4.0, {source, 32U}},
|
||||||
|
{"10", 5, -4.0, {source, 33U}},
|
||||||
|
{"10", 6, 5.0, {source, 34U}},
|
||||||
|
{"10", 6, -5.0, {source, 35U}}});
|
||||||
|
|
||||||
|
const auto result = fesa::LoadAssembler::assembleFullNodalLoad(
|
||||||
|
*fixture.model, *fixture.dofs);
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.hasValue());
|
||||||
|
EXPECT_DOUBLE_EQ(result.value()[3U], 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(result.value()[4U], 0.0);
|
||||||
|
EXPECT_DOUBLE_EQ(result.value()[5U], 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// MITC4-LOAD-003
|
||||||
|
TEST(LoadAssembly, EnforcesAggregateShellMomentDirectorProjectionThreshold) {
|
||||||
|
const std::filesystem::path source{"models/shell-load-assembly.inp"};
|
||||||
|
auto acceptedFixture = makeShellFixture(
|
||||||
|
{},
|
||||||
|
{{"10", 4, 1.0, {source, 30U}},
|
||||||
|
{"10", 6, 1.0e-12, {source, 31U}}});
|
||||||
|
auto rejectedFixture = makeShellFixture(
|
||||||
|
{},
|
||||||
|
{{"10", 4, 1.0, {source, 30U}},
|
||||||
|
{"10", 6, 2.0e-12, {source, 31U}}});
|
||||||
|
|
||||||
|
const auto accepted = fesa::LoadAssembler::assembleFullNodalLoad(
|
||||||
|
*acceptedFixture.model, *acceptedFixture.dofs);
|
||||||
|
const auto rejected = fesa::LoadAssembler::assembleFullNodalLoad(
|
||||||
|
*rejectedFixture.model, *rejectedFixture.dofs);
|
||||||
|
|
||||||
|
ASSERT_TRUE(accepted.hasValue());
|
||||||
|
ASSERT_FALSE(rejected.hasValue());
|
||||||
|
EXPECT_EQ(rejected.status().failureCategory(), fesa::FailureCategory::model);
|
||||||
|
ASSERT_EQ(rejected.status().diagnostics().size(), 1U);
|
||||||
|
EXPECT_EQ(
|
||||||
|
rejected.status().diagnostics()[0U].code,
|
||||||
|
"unsupported-drilling-load");
|
||||||
|
EXPECT_EQ(rejected.status().diagnostics()[0U].keyword, "CLOAD");
|
||||||
|
EXPECT_EQ(rejected.status().diagnostics()[0U].entityIdentity, "10");
|
||||||
|
}
|
||||||
|
|
||||||
|
// MITC4-LOAD-004
|
||||||
|
TEST(LoadAssembly, RejectsDrillingMomentBeforeEffectiveRhsCanBeFormed) {
|
||||||
|
const std::filesystem::path source{"models/shell-load-assembly.inp"};
|
||||||
|
auto fixture = makeShellFixture(
|
||||||
|
{{"10", 1, 1, 2.0, {source, 21U}}},
|
||||||
|
{{"10", 6, 1.0, {source, 30U}}});
|
||||||
|
|
||||||
|
const auto rejected = fesa::LoadAssembler::assembleFullNodalLoad(
|
||||||
|
*fixture.model, *fixture.dofs);
|
||||||
|
|
||||||
|
ASSERT_FALSE(rejected.hasValue());
|
||||||
|
EXPECT_EQ(rejected.status().failureCategory(), fesa::FailureCategory::model);
|
||||||
|
ASSERT_EQ(rejected.status().diagnostics().size(), 1U);
|
||||||
|
EXPECT_EQ(
|
||||||
|
rejected.status().diagnostics()[0U].code,
|
||||||
|
"unsupported-drilling-load");
|
||||||
|
}
|
||||||
|
|
||||||
TEST(LoadAssembly, FormsNonzeroPrescribedEffectiveRhs) {
|
TEST(LoadAssembly, FormsNonzeroPrescribedEffectiveRhs) {
|
||||||
const std::filesystem::path source{"models/load-assembly.inp"};
|
const std::filesystem::path source{"models/load-assembly.inp"};
|
||||||
auto fixture = makeFixture(
|
auto fixture = makeFixture(
|
||||||
|
|||||||
Reference in New Issue
Block a user