feat(nonlinear-static-newton-raphson): step 5 - load-boundary-path-and-convergence-gates
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
#include "fesa/analysis/nonlinear_static_control.h"
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/analysis/analysis_model.h"
|
||||
#include "fesa/fem/dof_manager.h"
|
||||
#include "fesa/model/domain.h"
|
||||
#include "fesa/model/model_types.h"
|
||||
|
||||
namespace {
|
||||
|
||||
fesa::Vector VectorFrom(const std::vector<double>& values) {
|
||||
fesa::Vector result{values.size()};
|
||||
for (std::size_t index = 0U; index < values.size(); ++index) {
|
||||
result[index] = values[index];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
struct Fixture {
|
||||
std::unique_ptr<fesa::Domain> domain;
|
||||
std::unique_ptr<fesa::AnalysisModel> model;
|
||||
std::unique_ptr<fesa::DofManager> dofs;
|
||||
};
|
||||
|
||||
Fixture MakeFixture(
|
||||
std::vector<fesa::PrescribedDisplacementDefinition> boundaries = {},
|
||||
std::vector<fesa::NodalLoad> loads = {},
|
||||
const double initial_increment = 0.1, const double time_period = 1.0,
|
||||
const double minimum_increment = 1.0e-9,
|
||||
const double maximum_increment = 5.0) {
|
||||
const std::filesystem::path source{"nonlinear-static-control.inp"};
|
||||
fesa::ModelDefinition definition{};
|
||||
definition.source_path = source;
|
||||
definition.source_content_identity = "control-fixture";
|
||||
definition.nodes = {
|
||||
{{"Shell-1", 10, "10"}, {0.0, 0.0, 0.0}, {source, 1U}},
|
||||
{{"Shell-1", 20, "20"}, {1.0, 0.0, 0.0}, {source, 2U}},
|
||||
{{"Shell-1", 30, "30"}, {1.0, 1.0, 0.0}, {source, 3U}},
|
||||
{{"Shell-1", 40, "40"}, {0.0, 1.0, 0.0}, {source, 4U}},
|
||||
{{"Shell-1", 50, "50"}, {-1.0, 1.0, 0.0}, {source, 5U}},
|
||||
{{"Shell-1", 60, "60"}, {-1.0, 0.0, 0.0}, {source, 6U}},
|
||||
};
|
||||
definition.materials = {{"Material", 1000.0, 0.25, {source, 7U}}};
|
||||
definition.shell_sections = {{"Shell", 0.1, 0U, {source, 8U}}};
|
||||
definition.shell_elements = {
|
||||
{{"Shell-1", 1, "1"},
|
||||
fesa::ShellSourceElementType::kS4,
|
||||
{0U, 1U, 2U, 3U},
|
||||
0U,
|
||||
0U,
|
||||
{source, 9U}},
|
||||
{{"Shell-1", 2, "2"},
|
||||
fesa::ShellSourceElementType::kS4,
|
||||
{0U, 3U, 4U, 5U},
|
||||
0U,
|
||||
0U,
|
||||
{source, 10U}},
|
||||
};
|
||||
for (std::size_t node = 0U; node < definition.nodes.size(); ++node) {
|
||||
definition.shell_node_initial_frames.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),
|
||||
fesa::StaticProcedureKind::kNonlinearFullNewton,
|
||||
true,
|
||||
initial_increment,
|
||||
time_period,
|
||||
minimum_increment,
|
||||
maximum_increment,
|
||||
{source, 11U}}};
|
||||
|
||||
auto domain_result = fesa::Domain::Create(std::move(definition));
|
||||
if (!domain_result.HasValue()) {
|
||||
throw std::runtime_error{"Control fixture Domain construction failed."};
|
||||
}
|
||||
auto domain =
|
||||
std::make_unique<fesa::Domain>(std::move(domain_result.Value()));
|
||||
auto model_result = fesa::AnalysisModel::Create(*domain);
|
||||
if (!model_result.HasValue()) {
|
||||
throw std::runtime_error{
|
||||
"Control fixture AnalysisModel construction failed."};
|
||||
}
|
||||
auto model =
|
||||
std::make_unique<fesa::AnalysisModel>(std::move(model_result.Value()));
|
||||
auto dofs_result = fesa::DofManager::Create(*model);
|
||||
if (!dofs_result.HasValue()) {
|
||||
throw std::runtime_error{"Control fixture DofManager construction failed."};
|
||||
}
|
||||
auto dofs =
|
||||
std::make_unique<fesa::DofManager>(std::move(dofs_result.Value()));
|
||||
return {std::move(domain), std::move(model), std::move(dofs)};
|
||||
}
|
||||
|
||||
fesa::Vector TrialCoordinates(const Fixture& fixture) {
|
||||
return fesa::Vector{fixture.dofs->FullDofCount()};
|
||||
}
|
||||
|
||||
fesa::Vector AddScaled(const fesa::Vector& values,
|
||||
const fesa::Vector& direction, const double scale) {
|
||||
fesa::Vector result = values;
|
||||
result.Axpy(scale, direction);
|
||||
return result;
|
||||
}
|
||||
|
||||
double MaximumAbsoluteDifference(const fesa::Vector& left,
|
||||
const fesa::Vector& right) {
|
||||
if (left.Size() != right.Size()) {
|
||||
throw std::invalid_argument{"Vector dimensions must match."};
|
||||
}
|
||||
double maximum = 0.0;
|
||||
for (std::size_t index = 0U; index < left.Size(); ++index) {
|
||||
maximum = (std::max)(maximum, std::abs(left[index] - right[index]));
|
||||
}
|
||||
return maximum;
|
||||
}
|
||||
|
||||
fesa::NewtonConvergenceInput RelativeConvergenceInput() {
|
||||
return {
|
||||
{"residual_force", VectorFrom({1.0e-2}), VectorFrom({1.0}),
|
||||
VectorFrom({0.5})},
|
||||
{"residual_moment", VectorFrom({1.0e-2}), VectorFrom({1.0}),
|
||||
VectorFrom({0.25})},
|
||||
{"correction_translation", VectorFrom({1.0e-2}), VectorFrom({1.0})},
|
||||
{"correction_rotation", VectorFrom({1.0e-2}), VectorFrom({1.0})},
|
||||
};
|
||||
}
|
||||
|
||||
// NSNR-CTRL-001, NSNR-CTRL-002, and NSNR-CTRL-017
|
||||
TEST(NonlinearStaticControl, BuildsFixedFactorSequenceWithExactEndpoint) {
|
||||
const auto fixture = MakeFixture();
|
||||
const auto result =
|
||||
fesa::BuildFixedLoadFactorSequence(fixture.domain->Steps()[0U]);
|
||||
|
||||
ASSERT_TRUE(result.HasValue());
|
||||
ASSERT_EQ(result.Value().size(), 10U);
|
||||
for (std::size_t index = 0U; index < result.Value().size(); ++index) {
|
||||
const auto& increment = result.Value()[index];
|
||||
EXPECT_EQ(increment.increment_index,
|
||||
static_cast<std::uint32_t>(index + 1U));
|
||||
EXPECT_DOUBLE_EQ(increment.load_factor,
|
||||
static_cast<double>(index + 1U) * 0.1);
|
||||
EXPECT_EQ(increment.is_factor_one, index + 1U == result.Value().size());
|
||||
}
|
||||
EXPECT_DOUBLE_EQ(result.Value().back().load_factor, 1.0);
|
||||
|
||||
const auto one_hundred = MakeFixture({}, {}, 0.01);
|
||||
const auto one_hundred_result =
|
||||
fesa::BuildFixedLoadFactorSequence(one_hundred.domain->Steps()[0U]);
|
||||
ASSERT_TRUE(one_hundred_result.HasValue());
|
||||
ASSERT_EQ(one_hundred_result.Value().size(), 100U);
|
||||
EXPECT_EQ(one_hundred_result.Value().back().increment_index, 100U);
|
||||
EXPECT_TRUE(one_hundred_result.Value().back().is_factor_one);
|
||||
}
|
||||
|
||||
// NSNR-CTRL-003
|
||||
TEST(NonlinearStaticControl, IgnoresPositiveMinimumAndMaximumDuringStepping) {
|
||||
const auto first = MakeFixture({}, {}, 0.3, 1.0, 1.0e-12, 100.0);
|
||||
const auto second = MakeFixture({}, {}, 0.3, 1.0, 0.29, 0.31);
|
||||
const auto first_result =
|
||||
fesa::BuildFixedLoadFactorSequence(first.domain->Steps()[0U]);
|
||||
const auto second_result =
|
||||
fesa::BuildFixedLoadFactorSequence(second.domain->Steps()[0U]);
|
||||
|
||||
ASSERT_TRUE(first_result.HasValue());
|
||||
ASSERT_TRUE(second_result.HasValue());
|
||||
ASSERT_EQ(first_result.Value().size(), second_result.Value().size());
|
||||
for (std::size_t index = 0U; index < first_result.Value().size(); ++index) {
|
||||
EXPECT_DOUBLE_EQ(first_result.Value()[index].load_factor,
|
||||
second_result.Value()[index].load_factor);
|
||||
EXPECT_EQ(first_result.Value()[index].increment_index,
|
||||
second_result.Value()[index].increment_index);
|
||||
}
|
||||
}
|
||||
|
||||
// NSNR-CTRL-004 and NSNR-CTRL-005
|
||||
TEST(NonlinearLoadBoundary, AppliesAbsoluteBoundaryTargetsWithoutCorrection) {
|
||||
const std::filesystem::path source{"nonlinear-static-control.inp"};
|
||||
const auto fixture = MakeFixture(
|
||||
{{"10", 1, 1, 4.0, {source, 12U}}, {"20", 4, 4, -0.5, {source, 13U}}});
|
||||
const auto result = fesa::ApplyAbsolutePrescribedDisplacementTarget(
|
||||
fixture.domain->Steps()[0U], 0.25);
|
||||
|
||||
ASSERT_TRUE(result.HasValue());
|
||||
ASSERT_EQ(result.Value().Size(), 2U);
|
||||
EXPECT_DOUBLE_EQ(result.Value()[0U], 1.0);
|
||||
EXPECT_DOUBLE_EQ(result.Value()[1U], -0.125);
|
||||
for (const std::size_t constrained : fixture.dofs->ConstrainedDofs()) {
|
||||
EXPECT_EQ(TrialCoordinates(fixture)[constrained], 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
// NSNR-CTRL-006 and NSNR-CTRL-008
|
||||
TEST(NonlinearLoadBoundary, AssemblesEverySharedNodeLoadOnceInFixedGlobalAxes) {
|
||||
const std::filesystem::path source{"nonlinear-static-control.inp"};
|
||||
const std::vector<fesa::NodalLoad> loads{
|
||||
{"10", 1, 8.0, {source, 20U}},
|
||||
{"10", 2, -4.0, {source, 21U}},
|
||||
{"10", 4, 2.0, {source, 22U}},
|
||||
};
|
||||
const auto fixture = MakeFixture({}, loads);
|
||||
const fesa::Vector trial = TrialCoordinates(fixture);
|
||||
const auto result = fesa::AssembleGlobalFixedLoadContribution(
|
||||
*fixture.domain, *fixture.dofs, trial, 0.25);
|
||||
|
||||
ASSERT_TRUE(result.HasValue());
|
||||
ASSERT_EQ(result.Value().generalized_external_force.Size(),
|
||||
fixture.dofs->FullDofCount());
|
||||
EXPECT_DOUBLE_EQ(result.Value().generalized_external_force[0U], 2.0);
|
||||
EXPECT_DOUBLE_EQ(result.Value().generalized_external_force[1U], -1.0);
|
||||
EXPECT_DOUBLE_EQ(result.Value().generalized_external_force[3U], 0.5);
|
||||
EXPECT_DOUBLE_EQ(fixture.domain->Steps()[0U]
|
||||
.ConcentratedLoads()[0U]
|
||||
.GlobalComponents()[0U],
|
||||
8.0);
|
||||
EXPECT_DOUBLE_EQ(fixture.domain->Steps()[0U]
|
||||
.ConcentratedLoads()[2U]
|
||||
.GlobalComponents()[3U],
|
||||
2.0);
|
||||
}
|
||||
|
||||
// NSNR-CTRL-007
|
||||
TEST(NonlinearLoadBoundary, RejectsDirectorAlignedMomentAboveTheExactBand) {
|
||||
const fesa::Vector3 director{0.0, 0.0, 1.0};
|
||||
EXPECT_TRUE(
|
||||
fesa::CheckMomentAdmissibility(director, {1.0, 0.0, 1.0e-12}).IsOk());
|
||||
const auto rejected =
|
||||
fesa::CheckMomentAdmissibility(director, {1.0, 0.0, 2.0e-12});
|
||||
EXPECT_FALSE(rejected.IsOk());
|
||||
ASSERT_EQ(rejected.Diagnostics().size(), 1U);
|
||||
EXPECT_EQ(rejected.Diagnostics()[0U].code, "unsupported-drilling-load");
|
||||
}
|
||||
|
||||
// NSNR-CTRL-009
|
||||
TEST(NonlinearLoadBoundary, PreservesSourceOrderDuringGlobalAccumulation) {
|
||||
const std::filesystem::path source{"nonlinear-static-control.inp"};
|
||||
const auto fixture = MakeFixture({}, {{"10", 1, 1.0e16, {source, 30U}},
|
||||
{"10", 1, -1.0e16, {source, 31U}},
|
||||
{"10", 1, 1.0, {source, 32U}}});
|
||||
const fesa::Vector trial = TrialCoordinates(fixture);
|
||||
const auto first = fesa::AssembleGlobalFixedLoadContribution(
|
||||
*fixture.domain, *fixture.dofs, trial, 1.0);
|
||||
const auto second = fesa::AssembleGlobalFixedLoadContribution(
|
||||
*fixture.domain, *fixture.dofs, trial, 1.0);
|
||||
|
||||
ASSERT_TRUE(first.HasValue());
|
||||
ASSERT_TRUE(second.HasValue());
|
||||
EXPECT_DOUBLE_EQ(first.Value().generalized_external_force[0U], 1.0);
|
||||
EXPECT_EQ(first.Value().generalized_external_tangent.RowOffsets(),
|
||||
second.Value().generalized_external_tangent.RowOffsets());
|
||||
EXPECT_EQ(first.Value().generalized_external_tangent.ColumnIndices(),
|
||||
second.Value().generalized_external_tangent.ColumnIndices());
|
||||
EXPECT_EQ(first.Value().generalized_external_tangent.Values(),
|
||||
second.Value().generalized_external_tangent.Values());
|
||||
}
|
||||
|
||||
// NSNR-CTRL-010 and NSNR-CTRL-011
|
||||
TEST(NonlinearLoadBoundary,
|
||||
DifferentiatesOneFixedSpatialMomentAndUsesSubtraction) {
|
||||
const std::filesystem::path source{"nonlinear-static-control.inp"};
|
||||
const auto fixture = MakeFixture({}, {{"10", 4, 3.0, {source, 40U}}});
|
||||
fesa::Vector trial = TrialCoordinates(fixture);
|
||||
trial[5U] = 0.4;
|
||||
const auto result = fesa::AssembleGlobalFixedLoadContribution(
|
||||
*fixture.domain, *fixture.dofs, trial, 0.7);
|
||||
ASSERT_TRUE(result.HasValue());
|
||||
|
||||
fesa::Vector direction{fixture.dofs->FullDofCount()};
|
||||
direction[5U] = 1.0;
|
||||
constexpr double kStep = 1.0e-6;
|
||||
const auto plus = fesa::AssembleGlobalFixedLoadContribution(
|
||||
*fixture.domain, *fixture.dofs, AddScaled(trial, direction, kStep), 0.7);
|
||||
const auto minus = fesa::AssembleGlobalFixedLoadContribution(
|
||||
*fixture.domain, *fixture.dofs, AddScaled(trial, direction, -kStep), 0.7);
|
||||
ASSERT_TRUE(plus.HasValue());
|
||||
ASSERT_TRUE(minus.HasValue());
|
||||
|
||||
fesa::Vector derivative = plus.Value().generalized_external_force;
|
||||
derivative.Axpy(-1.0, minus.Value().generalized_external_force);
|
||||
derivative.Scale(0.5 / kStep);
|
||||
const fesa::Vector tangent_action =
|
||||
result.Value().generalized_external_tangent.Multiply(direction);
|
||||
EXPECT_LE(MaximumAbsoluteDifference(derivative, tangent_action), 1.0e-8);
|
||||
|
||||
fesa::Vector internal{fixture.dofs->FullDofCount()};
|
||||
internal[3U] = 5.0;
|
||||
fesa::Vector residual = internal;
|
||||
residual.Axpy(-1.0, result.Value().generalized_external_force);
|
||||
EXPECT_DOUBLE_EQ(residual[3U],
|
||||
5.0 - result.Value().generalized_external_force[3U]);
|
||||
EXPECT_DOUBLE_EQ(
|
||||
-tangent_action[3U],
|
||||
-result.Value().generalized_external_tangent.Multiply(direction)[3U]);
|
||||
}
|
||||
|
||||
// NSNR-CTRL-012, NSNR-CTRL-013, and NSNR-CTRL-014
|
||||
TEST(NonlinearConvergence, UsesOwnedRelativeThresholdsForEveryChannel) {
|
||||
static_assert(fesa::kNewtonRelativeThreshold == 1.0e-2);
|
||||
static_assert(fesa::kNewtonZeroScaleThreshold == 1.0e-3);
|
||||
const auto result =
|
||||
fesa::EvaluateNewtonConvergence(RelativeConvergenceInput());
|
||||
|
||||
EXPECT_TRUE(result.combined_passed);
|
||||
const std::array<fesa::NewtonChannelDecision, 4> decisions{
|
||||
result.force_residual, result.moment_residual,
|
||||
result.translation_correction, result.rotation_correction};
|
||||
for (const auto& decision : decisions) {
|
||||
EXPECT_FALSE(decision.identity.empty());
|
||||
EXPECT_DOUBLE_EQ(decision.numerator_norm, 1.0e-2);
|
||||
EXPECT_DOUBLE_EQ(decision.denominator_scale, 1.0);
|
||||
EXPECT_EQ(decision.branch, fesa::NewtonConvergenceBranch::kRelative);
|
||||
EXPECT_DOUBLE_EQ(decision.threshold, fesa::kNewtonRelativeThreshold);
|
||||
EXPECT_EQ(decision.threshold_unit, fesa::NewtonChannelUnit::kDimensionless);
|
||||
EXPECT_TRUE(decision.passed);
|
||||
}
|
||||
}
|
||||
|
||||
// NSNR-CTRL-015 and NSNR-CTRL-016
|
||||
TEST(NonlinearConvergence, AppliesTypedZeroScaleGatesToEmptyChannels) {
|
||||
const fesa::NewtonConvergenceInput input{
|
||||
{"residual_force", fesa::Vector{0U}, fesa::Vector{0U}, fesa::Vector{0U}},
|
||||
{"residual_moment", fesa::Vector{0U}, fesa::Vector{0U}, fesa::Vector{0U}},
|
||||
{"correction_translation", fesa::Vector{0U}, fesa::Vector{0U}},
|
||||
{"correction_rotation", fesa::Vector{0U}, fesa::Vector{0U}},
|
||||
};
|
||||
const auto result = fesa::EvaluateNewtonConvergence(input);
|
||||
const std::array<fesa::NewtonChannelDecision, 4> decisions{
|
||||
result.force_residual, result.moment_residual,
|
||||
result.translation_correction, result.rotation_correction};
|
||||
const std::array<fesa::NewtonChannelUnit, 4> units{
|
||||
fesa::NewtonChannelUnit::kForce, fesa::NewtonChannelUnit::kForceLength,
|
||||
fesa::NewtonChannelUnit::kLength,
|
||||
fesa::NewtonChannelUnit::kDimensionless};
|
||||
|
||||
EXPECT_TRUE(result.combined_passed);
|
||||
for (std::size_t index = 0U; index < decisions.size(); ++index) {
|
||||
EXPECT_DOUBLE_EQ(decisions[index].numerator_norm, 0.0);
|
||||
EXPECT_DOUBLE_EQ(decisions[index].denominator_scale, 0.0);
|
||||
EXPECT_EQ(decisions[index].branch,
|
||||
fesa::NewtonConvergenceBranch::kZeroScale);
|
||||
EXPECT_DOUBLE_EQ(decisions[index].threshold,
|
||||
fesa::kNewtonZeroScaleThreshold);
|
||||
EXPECT_EQ(decisions[index].threshold_unit, units[index]);
|
||||
EXPECT_TRUE(decisions[index].passed);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
Reference in New Issue
Block a user