feat(nonlinear-static-newton-raphson): step 5 - load-boundary-path-and-convergence-gates

This commit is contained in:
KOKO\Mimi
2026-08-24 14:13:14 +09:00
parent 33997293b5
commit a567681411
5 changed files with 1046 additions and 0 deletions
@@ -0,0 +1,548 @@
#include "fesa/analysis/nonlinear_static_control.h"
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <limits>
#include <string>
#include <utility>
#include <vector>
#include "fesa/analysis/analysis_model.h"
#include "fesa/assembly/load_assembler.h"
#include "fesa/elements/finite_rotation.h"
namespace fesa {
namespace {
constexpr std::size_t kDofsPerNode = 6U;
constexpr std::size_t kTranslationDofCount = 3U;
constexpr double kMomentAdmissibilityThreshold = 1.0e-12;
/// @brief Creates a deterministic control-layer diagnostic status.
Status ControlFailure(const FailureCategory category, const std::string& code,
const SourceLocation& location,
const std::string& keyword, const std::string& identity,
const std::string& message) {
return Status::Failure(category, {{Severity::kError, code, location, keyword,
identity, message}});
}
/// @brief Verifies raw nonlinear static controls without deriving state.
Status ValidateNonlinearStaticControls(const StepDefinition& step) {
if (!step.IsNonlinearStatic() || !std::isfinite(step.InitialIncrement()) ||
!(step.InitialIncrement() > 0.0) || !std::isfinite(step.TimePeriod()) ||
step.TimePeriod() != 1.0 || !std::isfinite(step.MinimumIncrement()) ||
!(step.MinimumIncrement() > 0.0) ||
!std::isfinite(step.MaximumIncrement()) ||
!(step.MaximumIncrement() > 0.0)) {
return ControlFailure(
FailureCategory::kInput, "invalid-nonlinear-static-data",
step.Location(), "STATIC", step.Name(),
"A nonlinear static step requires four positive finite controls and "
"an exactly factor-one time period.");
}
return Status::Ok();
}
/// @brief Returns a finite three-component vector from full trial storage.
Result<Vector3> RotationVectorForNode(const Vector& trial,
const std::size_t node) {
const std::size_t first = node * kDofsPerNode + kTranslationDofCount;
if (first + kTranslationDofCount > trial.Size()) {
return Result<Vector3>::Failure(ControlFailure(
FailureCategory::kSolver, "nonfinite-newton-data", {{}, 0U},
"NONLINEAR_STATIC_CONTROL", std::to_string(node),
"The full trial coordinate vector does not contain every nodal "
"rotation component."));
}
const Vector3 rotation{trial[first], trial[first + 1U], trial[first + 2U]};
if (!rotation.IsFinite()) {
return Result<Vector3>::Failure(ControlFailure(
FailureCategory::kSolver, "nonfinite-newton-data", {{}, 0U},
"NONLINEAR_STATIC_CONTROL", std::to_string(node),
"The current trial rotation coordinate must be finite."));
}
return Result<Vector3>::Success(rotation);
}
/// @brief Builds a proper initial frame from its semantic nodal record.
Matrix InitialFrame(const ShellNodeInitialFrame& frame) {
Matrix result{3U, 3U};
for (std::size_t row = 0U; row < kTranslationDofCount; ++row) {
result(row, 0U) = frame.tangent_a[row];
result(row, 1U) = frame.tangent_b[row];
result(row, 2U) = frame.director[row];
}
return result;
}
/// @brief Multiplies a 3-by-3 matrix by a Vector3.
Vector3 Multiply(const Matrix& matrix, const Vector3& vector) {
return {matrix(0U, 0U) * vector.X() + matrix(0U, 1U) * vector.Y() +
matrix(0U, 2U) * vector.Z(),
matrix(1U, 0U) * vector.X() + matrix(1U, 1U) * vector.Y() +
matrix(1U, 2U) * vector.Z(),
matrix(2U, 0U) * vector.X() + matrix(2U, 1U) * vector.Y() +
matrix(2U, 2U) * vector.Z()};
}
/// @brief Multiplies a 3-by-3 matrix transpose by a Vector3.
Vector3 MultiplyTranspose(const Matrix& matrix, const Vector3& vector) {
return {matrix(0U, 0U) * vector.X() + matrix(1U, 0U) * vector.Y() +
matrix(2U, 0U) * vector.Z(),
matrix(0U, 1U) * vector.X() + matrix(1U, 1U) * vector.Y() +
matrix(2U, 1U) * vector.Z(),
matrix(0U, 2U) * vector.X() + matrix(1U, 2U) * vector.Y() +
matrix(2U, 2U) * vector.Z()};
}
/// @brief Forms the three-dimensional skew matrix of a global vector.
Matrix Skew(const Vector3& vector) {
Matrix result{3U, 3U};
result(0U, 1U) = -vector.Z();
result(0U, 2U) = vector.Y();
result(1U, 0U) = vector.Z();
result(1U, 2U) = -vector.X();
result(2U, 0U) = -vector.Y();
result(2U, 1U) = vector.X();
return result;
}
/// @brief Scales a small dense matrix without changing its shape.
Matrix Scale(const Matrix& matrix, const double scale) {
Matrix result{matrix.Rows(), matrix.Columns()};
for (std::size_t row = 0U; row < matrix.Rows(); ++row) {
for (std::size_t column = 0U; column < matrix.Columns(); ++column) {
result(row, column) = scale * matrix(row, column);
}
}
return result;
}
/// @brief Adds matching small dense matrices.
Matrix Add(const Matrix& left, const Matrix& right) {
Matrix result{left.Rows(), left.Columns()};
for (std::size_t row = 0U; row < left.Rows(); ++row) {
for (std::size_t column = 0U; column < left.Columns(); ++column) {
result(row, column) = left(row, column) + right(row, column);
}
}
return result;
}
/// @brief Evaluates left-Jacobian coefficients and derivatives by theta
/// squared.
void LeftJacobianCoefficients(const double theta_squared, double& a, double& b,
double& da_dtheta_squared,
double& db_dtheta_squared) {
if (theta_squared < 1.0e-8) {
const double theta_fourth = theta_squared * theta_squared;
const double theta_sixth = theta_fourth * theta_squared;
a = 0.5 - theta_squared / 24.0 + theta_fourth / 720.0 -
theta_sixth / 40320.0;
b = 1.0 / 6.0 - theta_squared / 120.0 + theta_fourth / 5040.0 -
theta_sixth / 362880.0;
da_dtheta_squared =
-1.0 / 24.0 + theta_squared / 360.0 - theta_fourth / 13440.0;
db_dtheta_squared =
-1.0 / 120.0 + theta_squared / 2520.0 - theta_fourth / 120960.0;
return;
}
const double theta = std::sqrt(theta_squared);
const double sine = std::sin(theta);
const double cosine = std::cos(theta);
a = (1.0 - cosine) / theta_squared;
b = (theta - sine) / (theta_squared * theta);
da_dtheta_squared = (theta * sine - 2.0 * (1.0 - cosine)) /
(2.0 * theta_squared * theta_squared);
db_dtheta_squared =
(1.0 - cosine) / (2.0 * theta_squared * theta_squared) -
3.0 * (theta - sine) / (2.0 * theta_squared * theta_squared * theta);
}
/// @brief Differentiates the SO(3) left Jacobian along one coordinate vector.
Matrix LeftJacobianDerivative(const Vector3& phi, const Vector3& direction) {
const Matrix skew = Skew(phi);
const Matrix direction_skew = Skew(direction);
const Matrix skew_squared = skew.Multiply(skew);
double a = 0.0;
double b = 0.0;
double da_dtheta_squared = 0.0;
double db_dtheta_squared = 0.0;
LeftJacobianCoefficients(phi.Dot(phi), a, b, da_dtheta_squared,
db_dtheta_squared);
const double derivative_scale = 2.0 * phi.Dot(direction);
return Add(Add(Scale(skew, derivative_scale * da_dtheta_squared),
Scale(direction_skew, a)),
Add(Scale(skew_squared, derivative_scale * db_dtheta_squared),
Scale(Add(direction_skew.Multiply(skew),
skew.Multiply(direction_skew)),
b)));
}
/// @brief Projects the immutable source moment into current director tangent
/// space.
Vector3 ProjectMoment(const Vector3& director, const Vector3& source_moment) {
return source_moment - director.Dot(source_moment) * director;
}
/// @brief Differentiates the projected moment along a coordinate direction.
Vector3 ProjectMomentDerivative(const Vector3& director,
const Vector3& director_derivative,
const Vector3& source_moment) {
return -director.Dot(source_moment) * director_derivative -
director_derivative.Dot(source_moment) * director;
}
/// @brief Computes the infinity norm and reports finite input validity.
double InfinityNorm(const Vector& values, bool& is_finite) {
double result = 0.0;
for (std::size_t index = 0U; index < values.Size(); ++index) {
if (!std::isfinite(values[index])) {
is_finite = false;
return std::numeric_limits<double>::quiet_NaN();
}
result = (std::max)(result, std::abs(values[index]));
}
return result;
}
/// @brief Evaluates one residual channel with its fixed physical zero-scale
/// unit.
NewtonChannelDecision EvaluateResidualChannel(
const NewtonResidualChannelInput& input, const NewtonChannelUnit unit) {
bool is_finite = input.free_residual.Size() == input.free_internal.Size() &&
input.free_residual.Size() == input.free_external.Size();
const double numerator = InfinityNorm(input.free_residual, is_finite);
const double internal = InfinityNorm(input.free_internal, is_finite);
const double external = InfinityNorm(input.free_external, is_finite);
const double denominator = (std::max)(internal, external);
if (!is_finite) {
return {input.identity,
numerator,
denominator,
NewtonConvergenceBranch::kZeroScale,
kNewtonZeroScaleThreshold,
unit,
false};
}
if (denominator == 0.0) {
return {input.identity,
numerator,
denominator,
NewtonConvergenceBranch::kZeroScale,
kNewtonZeroScaleThreshold,
unit,
numerator <= kNewtonZeroScaleThreshold};
}
return {input.identity,
numerator,
denominator,
NewtonConvergenceBranch::kRelative,
kNewtonRelativeThreshold,
NewtonChannelUnit::kDimensionless,
numerator / denominator <= kNewtonRelativeThreshold};
}
/// @brief Evaluates one correction channel with its fixed physical zero-scale
/// unit.
NewtonChannelDecision EvaluateCorrectionChannel(
const NewtonCorrectionChannelInput& input, const NewtonChannelUnit unit) {
bool is_finite =
input.fresh_correction.Size() == input.current_increment.Size();
const double numerator = InfinityNorm(input.fresh_correction, is_finite);
const double denominator = InfinityNorm(input.current_increment, is_finite);
if (!is_finite) {
return {input.identity,
numerator,
denominator,
NewtonConvergenceBranch::kZeroScale,
kNewtonZeroScaleThreshold,
unit,
false};
}
if (denominator == 0.0) {
return {input.identity,
numerator,
denominator,
NewtonConvergenceBranch::kZeroScale,
kNewtonZeroScaleThreshold,
unit,
numerator <= kNewtonZeroScaleThreshold};
}
return {input.identity,
numerator,
denominator,
NewtonConvergenceBranch::kRelative,
kNewtonRelativeThreshold,
NewtonChannelUnit::kDimensionless,
numerator / denominator <= kNewtonRelativeThreshold};
}
} // namespace
Result<std::vector<LoadFactorIncrement>> BuildFixedLoadFactorSequence(
const StepDefinition& step) {
const Status control_status = ValidateNonlinearStaticControls(step);
if (!control_status.IsOk()) {
return Result<std::vector<LoadFactorIncrement>>::Failure(control_status);
}
std::vector<LoadFactorIncrement> increments;
std::uint32_t increment_index = 1U;
while (true) {
const double ordinal_factor =
static_cast<double>(increment_index) * step.InitialIncrement();
if (!std::isfinite(ordinal_factor)) {
return Result<std::vector<LoadFactorIncrement>>::Failure(ControlFailure(
FailureCategory::kInput, "invalid-nonlinear-static-data",
step.Location(), "STATIC", step.Name(),
"The fixed load-factor sequence must remain finite."));
}
if (!(ordinal_factor < 1.0)) {
increments.push_back({increment_index, 1.0, true});
break;
}
increments.push_back({increment_index, ordinal_factor, false});
if (increment_index == (std::numeric_limits<std::uint32_t>::max)()) {
return Result<std::vector<LoadFactorIncrement>>::Failure(ControlFailure(
FailureCategory::kInput, "invalid-nonlinear-static-data",
step.Location(), "STATIC", step.Name(),
"The fixed load-factor sequence exceeds the supported increment "
"identity range."));
}
++increment_index;
}
return Result<std::vector<LoadFactorIncrement>>::Success(
std::move(increments));
}
Result<Vector> ApplyAbsolutePrescribedDisplacementTarget(
const StepDefinition& step, const double load_factor) {
const Status control_status = ValidateNonlinearStaticControls(step);
if (!control_status.IsOk()) {
return Result<Vector>::Failure(control_status);
}
if (!std::isfinite(load_factor) || load_factor < 0.0 || load_factor > 1.0) {
return Result<Vector>::Failure(ControlFailure(
FailureCategory::kInput, "invalid-nonlinear-static-data",
step.Location(), "BOUNDARY", step.Name(),
"The prescribed-displacement path requires a finite load factor in "
"the closed interval from zero to one."));
}
const auto& boundaries = step.PrescribedDisplacements();
Vector targets{boundaries.Size()};
for (std::size_t index = 0U; index < boundaries.Size(); ++index) {
const double target = boundaries[index].PrescribedValue();
const double value = load_factor * target;
if (!std::isfinite(target) || !std::isfinite(value)) {
return Result<Vector>::Failure(ControlFailure(
FailureCategory::kInput, "invalid-nonlinear-static-data",
boundaries[index].Location(), "BOUNDARY",
boundaries[index].Target().target_name_or_label,
"The factor-one prescribed displacement target must remain finite."));
}
targets[index] = value;
}
return Result<Vector>::Success(std::move(targets));
}
Status CheckMomentAdmissibility(const Vector3& director,
const Vector3& source_moment) {
if (!source_moment.IsFinite()) {
return ControlFailure(FailureCategory::kModel, "nonfinite-newton-data",
{{}, 0U}, "CLOAD", "",
"The fixed-global source moment must be finite.");
}
if (source_moment == Vector3{}) {
return Status::Ok();
}
if (!director.IsFinite() || !(director.Norm() > 0.0)) {
return ControlFailure(
FailureCategory::kSolver, "nonfinite-newton-data", {{}, 0U}, "CLOAD",
"", "The current shell director must be finite and nonzero.");
}
const double source_norm = source_moment.Norm();
const double rho = std::abs(director.Dot(source_moment)) / source_norm;
if (!std::isfinite(source_norm) || !(source_norm > 0.0) ||
!std::isfinite(rho)) {
return ControlFailure(
FailureCategory::kModel, "nonfinite-newton-data", {{}, 0U}, "CLOAD", "",
"The fixed-global source moment norm must be finite.");
}
if (!(rho <= kMomentAdmissibilityThreshold)) {
return ControlFailure(
FailureCategory::kModel, "unsupported-drilling-load", {{}, 0U}, "CLOAD",
"",
"The aggregate nodal moment has an unsupported director-parallel "
"component.");
}
return Status::Ok();
}
Result<GlobalFixedLoadContribution> AssembleGlobalFixedLoadContribution(
const Domain& domain, const DofManager& dofs,
const Vector& trial_generalized_coordinates, const double load_factor) {
if (!std::isfinite(load_factor) || load_factor < 0.0 || load_factor > 1.0 ||
domain.Steps().Size() != 1U || !domain.Steps()[0U].IsNonlinearStatic()) {
return Result<GlobalFixedLoadContribution>::Failure(ControlFailure(
FailureCategory::kModel, "invalid-nonlinear-static-data",
{domain.SourcePath(), 0U}, "NONLINEAR_STATIC_CONTROL",
domain.SourceContentIdentity(),
"Fixed-load assembly requires one nonlinear step and a finite load "
"factor in the closed interval from zero to one."));
}
const Status dof_status = dofs.ValidateInvariants();
if (!dof_status.IsOk()) {
return Result<GlobalFixedLoadContribution>::Failure(dof_status);
}
if (dofs.FullDofCount() != trial_generalized_coordinates.Size() ||
domain.Nodes().size() >
(std::numeric_limits<std::size_t>::max)() / kDofsPerNode ||
dofs.FullDofCount() != domain.Nodes().size() * kDofsPerNode) {
return Result<GlobalFixedLoadContribution>::Failure(ControlFailure(
FailureCategory::kSolver, "nonfinite-newton-data",
{domain.SourcePath(), 0U}, "NONLINEAR_STATIC_CONTROL",
domain.SourceContentIdentity(),
"The full trial coordinates must match the stable Domain DOF "
"dimension."));
}
for (std::size_t index = 0U; index < trial_generalized_coordinates.Size();
++index) {
if (!std::isfinite(trial_generalized_coordinates[index])) {
return Result<GlobalFixedLoadContribution>::Failure(ControlFailure(
FailureCategory::kSolver, "nonfinite-newton-data",
{domain.SourcePath(), 0U}, "NONLINEAR_STATIC_CONTROL",
std::to_string(index),
"The full trial coordinate vector must contain finite values."));
}
}
auto model = AnalysisModel::Create(domain);
if (!model.HasValue()) {
return Result<GlobalFixedLoadContribution>::Failure(model.GetStatus());
}
auto source_load = LoadAssembler::AssembleFullNodalLoad(model.Value(), dofs);
if (!source_load.HasValue()) {
return Result<GlobalFixedLoadContribution>::Failure(
source_load.GetStatus());
}
Vector external_force{dofs.FullDofCount()};
for (std::size_t index = 0U; index < external_force.Size(); ++index) {
external_force[index] = load_factor * source_load.Value()[index];
}
std::vector<const ShellNodeInitialFrame*> frames(domain.Nodes().size(),
nullptr);
for (const auto& frame : domain.ShellNodeInitialFrames()) {
if (frame.node_index >= frames.size() ||
frames[frame.node_index] != nullptr) {
return Result<GlobalFixedLoadContribution>::Failure(ControlFailure(
FailureCategory::kModel, "invalid-shell-director",
{domain.SourcePath(), 0U}, "NODE", std::to_string(frame.node_index),
"Shell nodal frames must provide one unique record per node."));
}
frames[frame.node_index] = &frame;
}
std::vector<CooContribution> tangent_contributions;
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
const std::size_t first_rotation =
node * kDofsPerNode + kTranslationDofCount;
const Vector3 source_moment{source_load.Value()[first_rotation],
source_load.Value()[first_rotation + 1U],
source_load.Value()[first_rotation + 2U]};
if (source_moment == Vector3{}) {
continue;
}
if (frames[node] == nullptr) {
return Result<GlobalFixedLoadContribution>::Failure(ControlFailure(
FailureCategory::kModel, "invalid-shell-director",
domain.Nodes()[node].location, "NODE",
domain.Nodes()[node].source_id.source_label_text,
"A node carrying a moment requires an initial shell frame."));
}
auto phi = RotationVectorForNode(trial_generalized_coordinates, node);
if (!phi.HasValue()) {
return Result<GlobalFixedLoadContribution>::Failure(phi.GetStatus());
}
auto rotation =
MakeFiniteRotationState(phi.Value(), InitialFrame(*frames[node]));
if (!rotation.HasValue()) {
return Result<GlobalFixedLoadContribution>::Failure(rotation.GetStatus());
}
const Status admissibility =
CheckMomentAdmissibility(rotation.Value().director, source_moment);
if (!admissibility.IsOk()) {
return Result<GlobalFixedLoadContribution>::Failure(admissibility);
}
const Matrix left_jacobian = LeftJacobianSO3(phi.Value());
const Vector3 projected =
ProjectMoment(rotation.Value().director, source_moment);
const Vector3 rotational_force =
load_factor * MultiplyTranspose(left_jacobian, projected);
for (std::size_t row = 0U; row < kTranslationDofCount; ++row) {
external_force[first_rotation + row] = rotational_force[row];
}
for (std::size_t column = 0U; column < kTranslationDofCount; ++column) {
Vector3 direction{};
if (column == 0U) {
direction = {1.0, 0.0, 0.0};
} else if (column == 1U) {
direction = {0.0, 1.0, 0.0};
} else {
direction = {0.0, 0.0, 1.0};
}
const Matrix left_jacobian_derivative =
LeftJacobianDerivative(phi.Value(), direction);
const Vector3 director_derivative =
Multiply(left_jacobian, direction).Cross(rotation.Value().director);
const Vector3 projected_derivative = ProjectMomentDerivative(
rotation.Value().director, director_derivative, source_moment);
const Vector3 tangent_column =
load_factor *
(MultiplyTranspose(left_jacobian_derivative, projected) +
MultiplyTranspose(left_jacobian, projected_derivative));
for (std::size_t row = 0U; row < kTranslationDofCount; ++row) {
tangent_contributions.push_back(
{first_rotation + row, first_rotation + column, tangent_column[row],
node, row * kTranslationDofCount + column});
}
}
}
auto tangent = SparseMatrix::FromCoo(dofs.FullDofCount(), dofs.FullDofCount(),
std::move(tangent_contributions),
dofs.GetSparsePattern());
if (!tangent.HasValue()) {
return Result<GlobalFixedLoadContribution>::Failure(tangent.GetStatus());
}
return Result<GlobalFixedLoadContribution>::Success(
{std::move(external_force), std::move(tangent.Value()), {}});
}
NewtonConvergenceResult EvaluateNewtonConvergence(
const NewtonConvergenceInput& input) {
NewtonConvergenceResult result{
EvaluateResidualChannel(input.force_residual, NewtonChannelUnit::kForce),
EvaluateResidualChannel(input.moment_residual,
NewtonChannelUnit::kForceLength),
EvaluateCorrectionChannel(input.translation_correction,
NewtonChannelUnit::kLength),
EvaluateCorrectionChannel(input.rotation_correction,
NewtonChannelUnit::kDimensionless),
false};
result.combined_passed =
result.force_residual.passed && result.moment_residual.passed &&
result.translation_correction.passed && result.rotation_correction.passed;
return result;
}
} // namespace fesa