#include "fesa/analysis/nonlinear_static_control.h" #include #include #include #include #include #include #include #include #include #include #include #include #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& 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 domain; std::unique_ptr model; std::unique_ptr dofs; }; Fixture MakeFixture( std::vector boundaries = {}, std::vector 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(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(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(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(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(index + 1U)); EXPECT_DOUBLE_EQ(increment.load_factor, static_cast(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 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 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 decisions{ result.force_residual, result.moment_residual, result.translation_correction, result.rotation_correction}; const std::array 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 decisions{ accepted.force_residual, accepted.moment_residual}; const std::array 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::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::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 invalid_values{ std::numeric_limits::quiet_NaN(), std::numeric_limits::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::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::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