535 lines
22 KiB
C++
535 lines
22 KiB
C++
#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})},
|
|
};
|
|
}
|
|
|
|
fesa::NewtonConvergenceInput ExactZeroExternalResidualInput(
|
|
const double force_numerator, const double moment_numerator) {
|
|
return {
|
|
{"residual_force", VectorFrom({force_numerator}), VectorFrom({2.0}),
|
|
VectorFrom({0.0})},
|
|
{"residual_moment", VectorFrom({moment_numerator}), VectorFrom({3.0}),
|
|
VectorFrom({0.0})},
|
|
{"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);
|
|
}
|
|
}
|
|
|
|
// NSNR-CTRL-018
|
|
TEST(NonlinearConvergence,
|
|
SelectsTypedZeroScaleFromExactZeroExternalResidualChannels) {
|
|
const auto accepted = fesa::EvaluateNewtonConvergence(
|
|
ExactZeroExternalResidualInput(fesa::kNewtonZeroScaleThreshold,
|
|
fesa::kNewtonZeroScaleThreshold));
|
|
|
|
EXPECT_TRUE(accepted.combined_passed);
|
|
const std::array<fesa::NewtonChannelDecision, 2U> decisions{
|
|
accepted.force_residual, accepted.moment_residual};
|
|
const std::array<fesa::NewtonChannelUnit, 2U> units{
|
|
fesa::NewtonChannelUnit::kForce,
|
|
fesa::NewtonChannelUnit::kForceLength};
|
|
for (std::size_t index = 0U; index < decisions.size(); ++index) {
|
|
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);
|
|
}
|
|
|
|
const double just_over = std::nextafter(
|
|
fesa::kNewtonZeroScaleThreshold,
|
|
std::numeric_limits<double>::infinity());
|
|
const auto force_rejected =
|
|
fesa::EvaluateNewtonConvergence(ExactZeroExternalResidualInput(
|
|
just_over, fesa::kNewtonZeroScaleThreshold));
|
|
const auto moment_rejected =
|
|
fesa::EvaluateNewtonConvergence(ExactZeroExternalResidualInput(
|
|
fesa::kNewtonZeroScaleThreshold, just_over));
|
|
EXPECT_FALSE(force_rejected.force_residual.passed);
|
|
EXPECT_FALSE(force_rejected.combined_passed);
|
|
EXPECT_FALSE(moment_rejected.moment_residual.passed);
|
|
EXPECT_FALSE(moment_rejected.combined_passed);
|
|
}
|
|
|
|
// NSNR-CTRL-019
|
|
TEST(NonlinearConvergence,
|
|
UsesRelativeResidualScaleOnlyWhenMatchingExternalScaleIsNonzero) {
|
|
fesa::NewtonConvergenceInput input{
|
|
{"residual_force", VectorFrom({2.0e-2}), VectorFrom({2.0}),
|
|
VectorFrom({1.0})},
|
|
{"residual_moment", VectorFrom({2.0e-2}), VectorFrom({1.0}),
|
|
VectorFrom({2.0})},
|
|
{"correction_translation", VectorFrom({1.0e-2}), VectorFrom({1.0})},
|
|
{"correction_rotation", VectorFrom({1.0e-2}), VectorFrom({1.0})},
|
|
};
|
|
const auto accepted = fesa::EvaluateNewtonConvergence(input);
|
|
|
|
EXPECT_TRUE(accepted.combined_passed);
|
|
for (const auto& decision : {accepted.force_residual,
|
|
accepted.moment_residual}) {
|
|
EXPECT_DOUBLE_EQ(decision.denominator_scale, 2.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);
|
|
}
|
|
|
|
const double just_over = std::nextafter(2.0e-2,
|
|
std::numeric_limits<double>::infinity());
|
|
input.force_residual.free_residual = VectorFrom({just_over});
|
|
EXPECT_FALSE(fesa::EvaluateNewtonConvergence(input).force_residual.passed);
|
|
input.force_residual.free_residual = VectorFrom({2.0e-2});
|
|
input.moment_residual.free_residual = VectorFrom({just_over});
|
|
EXPECT_FALSE(fesa::EvaluateNewtonConvergence(input).moment_residual.passed);
|
|
|
|
input.moment_residual.free_residual = VectorFrom({2.0e-2});
|
|
input.force_residual.free_residual = VectorFrom({9.0e-4});
|
|
input.force_residual.free_internal = VectorFrom({5.0e-3});
|
|
input.force_residual.free_external = VectorFrom({1.0e-2});
|
|
const auto relative_rejected = fesa::EvaluateNewtonConvergence(input);
|
|
EXPECT_FALSE(relative_rejected.force_residual.passed);
|
|
EXPECT_FALSE(relative_rejected.combined_passed);
|
|
}
|
|
|
|
// NSNR-CTRL-020
|
|
TEST(NonlinearConvergence,
|
|
RejectsNonfiniteResidualInputsBeforeSelectingOrAcceptingAChannel) {
|
|
const std::array<double, 2U> invalid_values{
|
|
std::numeric_limits<double>::quiet_NaN(),
|
|
std::numeric_limits<double>::infinity()};
|
|
for (const double invalid : invalid_values) {
|
|
for (const bool is_moment : {false, true}) {
|
|
for (const std::size_t vector_index : {0U, 1U, 2U}) {
|
|
auto input = RelativeConvergenceInput();
|
|
fesa::NewtonResidualChannelInput* channel =
|
|
is_moment ? &input.moment_residual : &input.force_residual;
|
|
if (vector_index == 0U) {
|
|
channel->free_residual = VectorFrom({invalid});
|
|
} else if (vector_index == 1U) {
|
|
channel->free_internal = VectorFrom({invalid});
|
|
} else {
|
|
channel->free_external = VectorFrom({invalid});
|
|
}
|
|
const auto result = fesa::EvaluateNewtonConvergence(input);
|
|
const auto& decision =
|
|
is_moment ? result.moment_residual : result.force_residual;
|
|
EXPECT_FALSE(decision.passed);
|
|
EXPECT_FALSE(result.combined_passed);
|
|
}
|
|
}
|
|
}
|
|
|
|
auto input = RelativeConvergenceInput();
|
|
const double smallest_external = std::nextafter(
|
|
0.0, std::numeric_limits<double>::infinity());
|
|
input.force_residual.free_residual = VectorFrom({0.0});
|
|
input.force_residual.free_internal = VectorFrom({0.0});
|
|
input.force_residual.free_external = VectorFrom({smallest_external});
|
|
const auto relative = fesa::EvaluateNewtonConvergence(input);
|
|
EXPECT_TRUE(relative.force_residual.passed);
|
|
EXPECT_DOUBLE_EQ(relative.force_residual.denominator_scale,
|
|
smallest_external);
|
|
EXPECT_EQ(relative.force_residual.branch,
|
|
fesa::NewtonConvergenceBranch::kRelative);
|
|
EXPECT_DOUBLE_EQ(relative.force_residual.threshold,
|
|
fesa::kNewtonRelativeThreshold);
|
|
EXPECT_EQ(relative.force_residual.threshold_unit,
|
|
fesa::NewtonChannelUnit::kDimensionless);
|
|
}
|
|
|
|
// NSNR-CTRL-021
|
|
TEST(NonlinearConvergence, RequiresEveryResidualAndCorrectionChannelToPass) {
|
|
const auto accepted = fesa::EvaluateNewtonConvergence(RelativeConvergenceInput());
|
|
EXPECT_TRUE(accepted.combined_passed);
|
|
EXPECT_DOUBLE_EQ(accepted.translation_correction.denominator_scale, 1.0);
|
|
EXPECT_DOUBLE_EQ(accepted.rotation_correction.denominator_scale, 1.0);
|
|
EXPECT_DOUBLE_EQ(accepted.translation_correction.threshold,
|
|
fesa::kNewtonRelativeThreshold);
|
|
EXPECT_DOUBLE_EQ(accepted.rotation_correction.threshold,
|
|
fesa::kNewtonRelativeThreshold);
|
|
EXPECT_EQ(accepted.translation_correction.threshold_unit,
|
|
fesa::NewtonChannelUnit::kDimensionless);
|
|
EXPECT_EQ(accepted.rotation_correction.threshold_unit,
|
|
fesa::NewtonChannelUnit::kDimensionless);
|
|
|
|
for (const std::size_t failed_channel : {0U, 1U, 2U, 3U}) {
|
|
auto input = RelativeConvergenceInput();
|
|
const double just_over = std::nextafter(
|
|
1.0e-2, std::numeric_limits<double>::infinity());
|
|
if (failed_channel == 0U) {
|
|
input.force_residual.free_residual = VectorFrom({just_over});
|
|
} else if (failed_channel == 1U) {
|
|
input.moment_residual.free_residual = VectorFrom({just_over});
|
|
} else if (failed_channel == 2U) {
|
|
input.translation_correction.fresh_correction = VectorFrom({just_over});
|
|
} else {
|
|
input.rotation_correction.fresh_correction = VectorFrom({just_over});
|
|
}
|
|
const auto result = fesa::EvaluateNewtonConvergence(input);
|
|
EXPECT_FALSE(result.combined_passed);
|
|
}
|
|
}
|
|
|
|
} // namespace
|