feat(nonlinear-static-newton-raphson): step 8 - nonlinear-state-commit-rollback

This commit is contained in:
KOKO\Mimi
2026-08-24 14:52:08 +09:00
parent f0dcc6def4
commit a100178be2
5 changed files with 896 additions and 0 deletions
@@ -0,0 +1,118 @@
#ifndef FESA_ANALYSIS_NONLINEAR_ANALYSIS_STATE_H_
#define FESA_ANALYSIS_NONLINEAR_ANALYSIS_STATE_H_
#include <array>
#include <cstdint>
#include <vector>
#include "fesa/core/source_identity.h"
#include "fesa/core/status.h"
#include "fesa/fem/dof_manager.h"
#include "fesa/math/vector.h"
#include "fesa/model/domain.h"
#include "fesa/results/result_records.h"
namespace fesa {
/// @brief Stores one fixed-reference baseline orientation for a source node.
struct NonlinearNodeBaseline {
EntityIndex node_index;
SourceEntityId source_id;
std::array<std::array<double, 3>, 3> triad;
std::array<double, 3> director;
};
/// @brief Carries the complete stable-order nonlinear node baseline.
struct NonlinearStateBaselineInput {
std::vector<NonlinearNodeBaseline> node_baselines;
};
/// @brief Owns one complete nonlinear trial or committed state identity.
struct NonlinearTrialState {
std::uint32_t increment_index;
double load_factor;
bool is_factor_zero_baseline;
std::vector<EntityIndex> nodal_internal_order;
std::vector<SourceEntityId> nodal_source_ids;
Vector generalized_coordinates;
Vector displacement;
Vector rotation_vector;
std::vector<std::array<std::array<double, 3>, 3>> nodal_triad;
std::vector<std::array<double, 3>> nodal_director;
Vector internal_force;
Vector external_force;
Vector residual;
Vector reaction;
std::vector<ShellResultRow> shell_results;
double physical_strain_energy;
std::array<double, 6> global_equilibrium;
};
/// @brief Stores one authoritative nonlinear increment frame.
struct AcceptedFrameState {
std::uint32_t frame_index;
std::uint32_t increment_index;
double load_factor;
std::uint32_t iteration_count;
bool is_factor_one;
std::vector<EntityIndex> nodal_internal_order;
std::vector<SourceEntityId> nodal_source_ids;
Vector generalized_coordinates;
Vector displacement;
Vector rotation_vector;
std::vector<std::array<std::array<double, 3>, 3>> nodal_triad;
std::vector<std::array<double, 3>> nodal_director;
Vector internal_force;
Vector external_force;
Vector residual;
Vector reaction;
std::vector<ShellResultRow> shell_results;
double physical_strain_energy;
std::array<double, 6> global_equilibrium;
};
/// @brief Owns committed nonlinear state and validated accepted frames.
class NonlinearAnalysisState {
public:
/// @brief Creates the factor-zero committed state from fixed node baselines.
/// @param domain Immutable owner of stable node and shell-element identities.
/// @param dofs Owner of the full global [u, phi] dimension.
/// @param baseline One finite triad/director entry per Domain node.
/// @return A factor-zero state or a structured model failure.
static Result<NonlinearAnalysisState> Create(
const Domain& domain, const DofManager& dofs,
const NonlinearStateBaselineInput& baseline);
/// @brief Returns an independent trial initialized from committed state.
/// @return A copy that the caller may mutate without changing committed state.
Result<NonlinearTrialState> BeginTrialFromCommitted() const;
/// @brief Atomically appends one complete validated accepted frame.
/// @param frame Converged factor-ordered state with full residual evidence.
/// @return Success after replacing committed state and appending the frame.
Status CommitAcceptedFrame(const AcceptedFrameState& frame);
/// @brief Discards caller-owned trial work without changing committed state.
void DiscardTrial() noexcept;
/// @brief Returns accepted frames in stable load-factor order.
const std::vector<AcceptedFrameState>& AcceptedFrames() const noexcept;
/// @brief Returns the current committed factor-zero or accepted state.
const NonlinearTrialState& CommittedState() const noexcept;
private:
/// @brief Retains the validated immutable inventory for frame commits.
NonlinearAnalysisState(NonlinearTrialState committed_state,
std::size_t full_dof_count,
std::vector<EntityIndex> shell_element_order);
NonlinearTrialState committed_state_;
std::size_t full_dof_count_;
std::vector<EntityIndex> shell_element_order_;
std::vector<AcceptedFrameState> accepted_frames_;
};
} // namespace fesa
#endif // FESA_ANALYSIS_NONLINEAR_ANALYSIS_STATE_H_
+1
View File
@@ -5,6 +5,7 @@ add_library(
analysis/analysis_state.cpp analysis/analysis_state.cpp
analysis/linear_static_analysis.cpp analysis/linear_static_analysis.cpp
analysis/newton_iteration_log.cpp analysis/newton_iteration_log.cpp
analysis/nonlinear_analysis_state.cpp
analysis/nonlinear_static_control.cpp analysis/nonlinear_static_control.cpp
app/fesa_application.cpp app/fesa_application.cpp
app/output_paths.cpp app/output_paths.cpp
@@ -0,0 +1,320 @@
#include "fesa/analysis/nonlinear_analysis_state.h"
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <limits>
#include <string>
#include <utility>
#include <vector>
#include "fesa/elements/element_definition.h"
namespace fesa {
namespace {
constexpr std::size_t kDofsPerNode = 6U;
constexpr std::size_t kShellLocationsPerElement = 4U;
constexpr std::uint32_t kMaximumNewtonIterations = 100U;
/// @brief Creates one failure without exposing a partially built state.
Status StateFailure(const FailureCategory category, const std::string& code,
const std::string& identity, const std::string& message) {
return Status::Failure(
category,
{{Severity::kError, code, {}, "NONLINEAR_ANALYSIS_STATE", identity,
message}});
}
/// @brief Compares every preserved source-identity component.
bool SourceIdsMatch(const SourceEntityId& left, const SourceEntityId& right) {
return left.instance_name == right.instance_name &&
left.source_label == right.source_label &&
left.source_label_text == right.source_label_text;
}
template <std::size_t Size>
/// @brief Tests one fixed-size numerical array for finite values.
bool IsFinite(const std::array<double, Size>& values) {
return std::all_of(values.begin(), values.end(),
[](const double value) { return std::isfinite(value); });
}
/// @brief Tests each component of a stored trial vector for finiteness.
bool IsFinite(const Vector& values) {
for (std::size_t index = 0U; index < values.Size(); ++index) {
if (!std::isfinite(values[index])) {
return false;
}
}
return true;
}
/// @brief Tests all physical quantities attached to one shell recovery row.
bool IsFinite(const ShellResultRow& row) {
if (!IsFinite(row.natural_coordinates) || !IsFinite(row.generalized_strain) ||
!IsFinite(row.section_resultant)) {
return false;
}
for (const auto& axis : row.local_frame) {
if (!IsFinite(axis)) {
return false;
}
}
return std::all_of(row.stress.begin(), row.stress.end(),
[](const ShellSectionStressRow& stress) {
return std::isfinite(stress.zeta) &&
IsFinite(stress.components);
});
}
/// @brief Returns shell Domain indices in deterministic unified element order.
std::vector<EntityIndex> ShellElementOrder(const Domain& domain) {
std::vector<EntityIndex> order;
for (std::size_t index = 0U; index < domain.Elements().Size(); ++index) {
if (domain.Elements()[index].Kind() == ElementDefinitionKind::kMitc4Shell) {
order.push_back(static_cast<EntityIndex>(index));
}
}
return order;
}
/// @brief Validates one accepted frame before any committed-state mutation.
Status ValidateAcceptedFrame(const AcceptedFrameState& frame,
const NonlinearTrialState& committed,
const std::size_t full_dof_count,
const std::vector<EntityIndex>& shell_order,
const std::size_t accepted_frame_count) {
if (accepted_frame_count >
static_cast<std::size_t>((std::numeric_limits<std::uint32_t>::max)())) {
return StateFailure(FailureCategory::kSolver, "invalid-accepted-state", {},
"Accepted-frame inventory exceeds uint32 identity.");
}
if (frame.frame_index !=
static_cast<std::uint32_t>(accepted_frame_count)) {
return StateFailure(FailureCategory::kSolver, "invalid-accepted-state", {},
"Frame index must equal accepted-frame order.");
}
if (frame.increment_index <= committed.increment_index ||
!std::isfinite(frame.load_factor) || frame.load_factor <= 0.0 ||
frame.load_factor > 1.0 || frame.load_factor <= committed.load_factor ||
frame.iteration_count == 0U ||
frame.iteration_count > kMaximumNewtonIterations ||
frame.is_factor_one != (frame.load_factor == 1.0)) {
return StateFailure(FailureCategory::kSolver, "invalid-accepted-state", {},
"Accepted frame has an invalid increment identity.");
}
const std::size_t node_count = committed.nodal_internal_order.size();
if (frame.nodal_internal_order.size() != node_count ||
frame.nodal_source_ids.size() != node_count ||
frame.nodal_triad.size() != node_count ||
frame.nodal_director.size() != node_count) {
return StateFailure(FailureCategory::kSolver, "invalid-accepted-state", {},
"Accepted frame node inventory does not match baseline.");
}
for (std::size_t node = 0U; node < node_count; ++node) {
if (frame.nodal_internal_order[node] !=
committed.nodal_internal_order[node] ||
!SourceIdsMatch(frame.nodal_source_ids[node],
committed.nodal_source_ids[node]) ||
!IsFinite(frame.nodal_triad[node][0U]) ||
!IsFinite(frame.nodal_triad[node][1U]) ||
!IsFinite(frame.nodal_triad[node][2U]) ||
!IsFinite(frame.nodal_director[node])) {
return StateFailure(FailureCategory::kSolver, "invalid-accepted-state", {},
"Accepted frame has invalid nodal identity or state.");
}
}
const std::array<const Vector*, 7> vectors{
&frame.generalized_coordinates, &frame.displacement,
&frame.rotation_vector, &frame.internal_force,
&frame.external_force, &frame.residual,
&frame.reaction};
for (const Vector* vector : vectors) {
if (vector->Size() != full_dof_count || !IsFinite(*vector)) {
return StateFailure(FailureCategory::kSolver, "invalid-accepted-state", {},
"Accepted frame vector inventory is invalid.");
}
}
for (std::size_t dof = 0U; dof < full_dof_count; ++dof) {
const double residual = frame.internal_force[dof] - frame.external_force[dof];
if (frame.residual[dof] != residual || frame.reaction[dof] != residual) {
return StateFailure(FailureCategory::kSolver, "invalid-accepted-state", {},
"Residual and reaction must retain full equilibrium.");
}
}
if (shell_order.size() >
(std::numeric_limits<std::size_t>::max)() / kShellLocationsPerElement ||
frame.shell_results.size() !=
shell_order.size() * kShellLocationsPerElement ||
!std::isfinite(frame.physical_strain_energy) ||
!IsFinite(frame.global_equilibrium)) {
return StateFailure(FailureCategory::kSolver, "invalid-accepted-state", {},
"Accepted frame physical result inventory is invalid.");
}
const double gauss = 1.0 / std::sqrt(3.0);
const std::array<ShellMidsurfaceLocation, kShellLocationsPerElement>
expected_locations{ShellMidsurfaceLocation::kGp1,
ShellMidsurfaceLocation::kGp2,
ShellMidsurfaceLocation::kGp3,
ShellMidsurfaceLocation::kGp4};
const std::array<std::array<double, 2>, kShellLocationsPerElement>
expected_coordinates{std::array<double, 2>{-gauss, -gauss},
std::array<double, 2>{gauss, -gauss},
std::array<double, 2>{gauss, gauss},
std::array<double, 2>{-gauss, gauss}};
const std::array<ShellSectionPosition, 3> expected_positions{
ShellSectionPosition::kBottom, ShellSectionPosition::kMiddle,
ShellSectionPosition::kTop};
constexpr std::array<double, 3> expected_zeta{-1.0, 0.0, 1.0};
for (std::size_t element = 0U; element < shell_order.size(); ++element) {
for (std::size_t point = 0U; point < kShellLocationsPerElement; ++point) {
const auto& row =
frame.shell_results[element * kShellLocationsPerElement + point];
if (row.element != shell_order[element] ||
row.location != expected_locations[point] ||
row.natural_coordinates != expected_coordinates[point] ||
!IsFinite(row)) {
return StateFailure(FailureCategory::kSolver, "invalid-accepted-state", {},
"Shell result rows do not preserve stable identity.");
}
for (std::size_t position = 0U; position < expected_positions.size();
++position) {
if (row.stress[position].position != expected_positions[position] ||
row.stress[position].zeta != expected_zeta[position]) {
return StateFailure(
FailureCategory::kSolver, "invalid-accepted-state", {},
"Shell stress rows do not preserve stable section identity.");
}
}
}
}
return Status::Ok();
}
} // namespace
Result<NonlinearAnalysisState> NonlinearAnalysisState::Create(
const Domain& domain, const DofManager& dofs,
const NonlinearStateBaselineInput& baseline) {
const std::size_t node_count = domain.Nodes().size();
if (node_count > (std::numeric_limits<std::size_t>::max)() / kDofsPerNode ||
dofs.FullDofCount() != node_count * kDofsPerNode ||
!dofs.ValidateInvariants().IsOk()) {
return Result<NonlinearAnalysisState>::Failure(StateFailure(
FailureCategory::kModel, "invalid-nonlinear-state", {},
"Domain node and full-DOF inventories must agree."));
}
if (baseline.node_baselines.size() != node_count) {
return Result<NonlinearAnalysisState>::Failure(StateFailure(
FailureCategory::kModel, "invalid-nonlinear-state", {},
"Nonlinear baseline requires exactly one entry per Domain node."));
}
NonlinearTrialState committed{
0U,
0.0,
true,
{},
{},
Vector{dofs.FullDofCount()},
Vector{dofs.FullDofCount()},
Vector{dofs.FullDofCount()},
{},
{},
Vector{dofs.FullDofCount()},
Vector{dofs.FullDofCount()},
Vector{dofs.FullDofCount()},
Vector{dofs.FullDofCount()},
{},
0.0,
{}};
committed.nodal_internal_order.reserve(node_count);
committed.nodal_source_ids.reserve(node_count);
committed.nodal_triad.reserve(node_count);
committed.nodal_director.reserve(node_count);
for (std::size_t node = 0U; node < node_count; ++node) {
const auto& entry = baseline.node_baselines[node];
if (entry.node_index != static_cast<EntityIndex>(node) ||
!SourceIdsMatch(entry.source_id, domain.Nodes()[node].source_id) ||
!IsFinite(entry.triad[0U]) || !IsFinite(entry.triad[1U]) ||
!IsFinite(entry.triad[2U]) || !IsFinite(entry.director)) {
return Result<NonlinearAnalysisState>::Failure(StateFailure(
FailureCategory::kModel, "invalid-nonlinear-state",
std::to_string(node),
"Baseline identity and finite node frame must match Domain order."));
}
committed.nodal_internal_order.push_back(entry.node_index);
committed.nodal_source_ids.push_back(entry.source_id);
committed.nodal_triad.push_back(entry.triad);
committed.nodal_director.push_back(entry.director);
}
return Result<NonlinearAnalysisState>::Success(NonlinearAnalysisState{
std::move(committed), dofs.FullDofCount(), ShellElementOrder(domain)});
}
Result<NonlinearTrialState> NonlinearAnalysisState::BeginTrialFromCommitted()
const {
return Result<NonlinearTrialState>::Success(committed_state_);
}
Status NonlinearAnalysisState::CommitAcceptedFrame(
const AcceptedFrameState& frame) {
const Status validation =
ValidateAcceptedFrame(frame, committed_state_, full_dof_count_,
shell_element_order_, accepted_frames_.size());
if (!validation.IsOk()) {
return validation;
}
NonlinearTrialState candidate{
frame.increment_index,
frame.load_factor,
false,
frame.nodal_internal_order,
frame.nodal_source_ids,
frame.generalized_coordinates,
frame.displacement,
frame.rotation_vector,
frame.nodal_triad,
frame.nodal_director,
frame.internal_force,
frame.external_force,
frame.residual,
frame.reaction,
frame.shell_results,
frame.physical_strain_energy,
frame.global_equilibrium};
std::vector<AcceptedFrameState> accepted_candidate = accepted_frames_;
accepted_candidate.push_back(frame);
committed_state_ = std::move(candidate);
accepted_frames_ = std::move(accepted_candidate);
return Status::Ok();
}
void NonlinearAnalysisState::DiscardTrial() noexcept {}
const std::vector<AcceptedFrameState>& NonlinearAnalysisState::AcceptedFrames()
const noexcept {
return accepted_frames_;
}
const NonlinearTrialState& NonlinearAnalysisState::CommittedState() const noexcept {
return committed_state_;
}
NonlinearAnalysisState::NonlinearAnalysisState(
NonlinearTrialState committed_state, const std::size_t full_dof_count,
std::vector<EntityIndex> shell_element_order)
: committed_state_{std::move(committed_state)},
full_dof_count_{full_dof_count},
shell_element_order_{std::move(shell_element_order)} {}
} // namespace fesa
+1
View File
@@ -6,6 +6,7 @@ add_executable(
unit/analysis/analysis_test.cpp unit/analysis/analysis_test.cpp
unit/analysis/analysis_model_test.cpp unit/analysis/analysis_model_test.cpp
unit/analysis/analysis_state_test.cpp unit/analysis/analysis_state_test.cpp
unit/analysis/nonlinear_analysis_state_test.cpp
unit/analysis/nonlinear_static_control_test.cpp unit/analysis/nonlinear_static_control_test.cpp
unit/analysis/newton_iteration_log_test.cpp unit/analysis/newton_iteration_log_test.cpp
unit/assembly/parallel_for_test.cpp unit/assembly/parallel_for_test.cpp
@@ -0,0 +1,456 @@
#include "fesa/analysis/nonlinear_analysis_state.h"
#include <gtest/gtest.h>
#include <array>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <limits>
#include <memory>
#include <stdexcept>
#include <string>
#include <type_traits>
#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 {
constexpr std::array<std::array<double, 3>, 3> kIdentityTriad{
std::array<double, 3>{1.0, 0.0, 0.0},
std::array<double, 3>{0.0, 1.0, 0.0},
std::array<double, 3>{0.0, 0.0, 1.0}};
struct Fixture {
std::unique_ptr<fesa::Domain> domain;
std::unique_ptr<fesa::AnalysisModel> model;
std::unique_ptr<fesa::DofManager> dofs;
};
Fixture MakeFixture() {
const std::filesystem::path source{"nonlinear-analysis-state.inp"};
fesa::ModelDefinition definition{};
definition.source_path = source;
definition.source_content_identity = "nonlinear-analysis-state-fixture";
definition.nodes = {
{{"Shell-A", 10, "10"}, {0.0, 0.0, 0.0}, {source, 1U}},
{{"Shell-A", 20, "20"}, {1.0, 0.0, 0.0}, {source, 2U}},
{{"Shell-A", 30, "30"}, {1.0, 1.0, 0.0}, {source, 3U}},
{{"Shell-A", 40, "40"}, {0.0, 1.0, 0.0}, {source, 4U}}};
definition.materials = {{"Material", 1000.0, 0.25, {source, 5U}}};
definition.shell_sections = {{"Shell", 0.1, 0U, {source, 6U}}};
definition.shell_elements = {
{{"Shell-A", 1, "1"},
fesa::ShellSourceElementType::kS4,
{0U, 1U, 2U, 3U},
0U,
0U,
{source, 7U}}};
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",
{},
{},
fesa::StaticProcedureKind::kNonlinearFullNewton,
true,
0.1,
1.0,
0.001,
1.0,
{source, 8U}}};
auto domain_result = fesa::Domain::Create(std::move(definition));
if (!domain_result.HasValue()) {
throw std::runtime_error{"State 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{"State 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{"State 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::NonlinearStateBaselineInput MakeBaseline(const fesa::Domain& domain) {
fesa::NonlinearStateBaselineInput baseline{};
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
baseline.node_baselines.push_back(
{static_cast<fesa::EntityIndex>(node), domain.Nodes()[node].source_id,
kIdentityTriad, {0.0, 0.0, 1.0}});
}
return baseline;
}
fesa::NonlinearAnalysisState MakeState(const Fixture& fixture) {
auto state = fesa::NonlinearAnalysisState::Create(
*fixture.domain, *fixture.dofs, MakeBaseline(*fixture.domain));
if (!state.HasValue()) {
throw std::runtime_error{"State fixture construction failed."};
}
return std::move(state.Value());
}
fesa::ShellResultRow MakeShellRow(
const fesa::ShellMidsurfaceLocation location,
const std::array<double, 2> coordinates) {
return {0U,
location,
coordinates,
kIdentityTriad,
{},
{},
{{{fesa::ShellSectionPosition::kBottom, -1.0, {}},
{fesa::ShellSectionPosition::kMiddle, 0.0, {}},
{fesa::ShellSectionPosition::kTop, 1.0, {}}}}};
}
std::vector<fesa::ShellResultRow> MakeShellRows() {
const double gauss = 1.0 / std::sqrt(3.0);
return {MakeShellRow(fesa::ShellMidsurfaceLocation::kGp1, {-gauss, -gauss}),
MakeShellRow(fesa::ShellMidsurfaceLocation::kGp2, {gauss, -gauss}),
MakeShellRow(fesa::ShellMidsurfaceLocation::kGp3, {gauss, gauss}),
MakeShellRow(fesa::ShellMidsurfaceLocation::kGp4, {-gauss, gauss})};
}
fesa::AcceptedFrameState MakeFrame(
const fesa::NonlinearTrialState& committed, const std::uint32_t frame_index,
const std::uint32_t increment_index, const double load_factor,
const std::uint32_t iteration_count) {
return {frame_index,
increment_index,
load_factor,
iteration_count,
load_factor == 1.0,
committed.nodal_internal_order,
committed.nodal_source_ids,
committed.generalized_coordinates,
committed.displacement,
committed.rotation_vector,
committed.nodal_triad,
committed.nodal_director,
committed.internal_force,
committed.external_force,
committed.residual,
committed.reaction,
MakeShellRows(),
0.0,
{}};
}
template <class T, class = void>
struct HasVerificationMetrics : std::false_type {};
template <class T>
struct HasVerificationMetrics<
T, std::void_t<decltype(std::declval<T&>().verification_metrics)>>
: std::true_type {};
void ExpectSourceIdEqual(const fesa::SourceEntityId& actual,
const fesa::SourceEntityId& expected) {
EXPECT_EQ(actual.instance_name, expected.instance_name);
EXPECT_EQ(actual.source_label, expected.source_label);
EXPECT_EQ(actual.source_label_text, expected.source_label_text);
}
void ExpectVectorEqual(const fesa::Vector& actual, const fesa::Vector& expected) {
ASSERT_EQ(actual.Size(), expected.Size());
for (std::size_t index = 0U; index < actual.Size(); ++index) {
EXPECT_DOUBLE_EQ(actual[index], expected[index]);
}
}
} // namespace
// NSNR-STATE-000, NSNR-STATE-002, NSNR-STATE-003, and NSNR-STATE-014
TEST(NonlinearAnalysisState, CreatesFactorZeroBaselineInStableIdentityOrder) {
const auto fixture = MakeFixture();
const auto baseline = MakeBaseline(*fixture.domain);
const auto result = fesa::NonlinearAnalysisState::Create(
*fixture.domain, *fixture.dofs, baseline);
ASSERT_TRUE(result.HasValue());
const auto& committed = result.Value().CommittedState();
EXPECT_EQ(committed.increment_index, 0U);
EXPECT_DOUBLE_EQ(committed.load_factor, 0.0);
EXPECT_TRUE(committed.is_factor_zero_baseline);
EXPECT_TRUE(result.Value().AcceptedFrames().empty());
ASSERT_EQ(committed.nodal_internal_order.size(), fixture.domain->Nodes().size());
ASSERT_EQ(committed.nodal_source_ids.size(), fixture.domain->Nodes().size());
for (std::size_t node = 0U; node < fixture.domain->Nodes().size(); ++node) {
EXPECT_EQ(committed.nodal_internal_order[node], node);
ExpectSourceIdEqual(committed.nodal_source_ids[node],
fixture.domain->Nodes()[node].source_id);
}
const fesa::Vector* const vectors[] = {
&committed.generalized_coordinates, &committed.displacement,
&committed.rotation_vector, &committed.internal_force,
&committed.external_force, &committed.residual, &committed.reaction};
for (const auto* vector : vectors) {
ASSERT_EQ(vector->Size(), fixture.dofs->FullDofCount());
for (std::size_t index = 0U; index < vector->Size(); ++index) {
EXPECT_DOUBLE_EQ((*vector)[index], 0.0);
}
}
}
// NSNR-STATE-001
TEST(NonlinearAnalysisState, RejectsInvalidBaselineInventoryBeforeStateCreation) {
const auto fixture = MakeFixture();
const auto baseline = MakeBaseline(*fixture.domain);
const std::array<fesa::NonlinearStateBaselineInput, 5> invalid{
fesa::NonlinearStateBaselineInput{{baseline.node_baselines[0U],
baseline.node_baselines[1U],
baseline.node_baselines[2U]}},
fesa::NonlinearStateBaselineInput{{baseline.node_baselines[0U],
baseline.node_baselines[1U],
baseline.node_baselines[2U],
baseline.node_baselines[3U],
baseline.node_baselines[3U]}},
fesa::NonlinearStateBaselineInput{{baseline.node_baselines[1U],
baseline.node_baselines[0U],
baseline.node_baselines[2U],
baseline.node_baselines[3U]}},
fesa::NonlinearStateBaselineInput{{baseline.node_baselines[0U],
baseline.node_baselines[0U],
baseline.node_baselines[2U],
baseline.node_baselines[3U]}},
fesa::NonlinearStateBaselineInput{{baseline.node_baselines[0U],
baseline.node_baselines[1U],
baseline.node_baselines[2U],
{3U,
{"Other-Instance", 40, "40"},
kIdentityTriad,
{0.0, 0.0, 1.0}}}}};
for (const auto& invalid_baseline : invalid) {
const auto result = fesa::NonlinearAnalysisState::Create(
*fixture.domain, *fixture.dofs, invalid_baseline);
EXPECT_FALSE(result.HasValue());
}
}
// NSNR-STATE-001 and NSNR-STATE-002
TEST(NonlinearAnalysisState, RejectsNonfiniteBaselineAndDofMismatch) {
const auto fixture = MakeFixture();
auto nonfinite_triad = MakeBaseline(*fixture.domain);
nonfinite_triad.node_baselines[0U].triad[0U][0U] =
std::numeric_limits<double>::quiet_NaN();
EXPECT_FALSE(fesa::NonlinearAnalysisState::Create(
*fixture.domain, *fixture.dofs, nonfinite_triad)
.HasValue());
auto nonfinite_director = MakeBaseline(*fixture.domain);
nonfinite_director.node_baselines[0U].director[2U] =
std::numeric_limits<double>::infinity();
EXPECT_FALSE(fesa::NonlinearAnalysisState::Create(
*fixture.domain, *fixture.dofs, nonfinite_director)
.HasValue());
auto empty_domain_result = fesa::Domain::Create(fesa::ModelDefinition{});
ASSERT_TRUE(empty_domain_result.HasValue());
const auto dof_mismatch = fesa::NonlinearAnalysisState::Create(
empty_domain_result.Value(), *fixture.dofs,
MakeBaseline(empty_domain_result.Value()));
EXPECT_FALSE(dof_mismatch.HasValue());
}
// NSNR-STATE-001 and NSNR-STATE-003
TEST(NonlinearAnalysisState, PreservesDuplicateLabelsWhenInstancesDiffer) {
const auto fixture = MakeFixture();
auto baseline = MakeBaseline(*fixture.domain);
baseline.node_baselines[1U].source_id = {"Shell-B", 10, "10"};
auto changed_definition = fesa::ModelDefinition{};
changed_definition.source_path = "duplicate-instance-labels.inp";
changed_definition.source_content_identity = "duplicate-instance-labels";
changed_definition.nodes = {
{{"Shell-A", 10, "10"}, {0.0, 0.0, 0.0}, {changed_definition.source_path, 1U}},
{{"Shell-B", 10, "10"}, {1.0, 0.0, 0.0}, {changed_definition.source_path, 2U}}};
changed_definition.steps = {{"Step-1",
{},
{},
fesa::StaticProcedureKind::kNonlinearFullNewton,
true,
0.1,
1.0,
0.001,
1.0,
{changed_definition.source_path, 3U}}};
auto domain_result = fesa::Domain::Create(std::move(changed_definition));
ASSERT_TRUE(domain_result.HasValue());
auto domain = std::move(domain_result.Value());
auto model_result = fesa::AnalysisModel::Create(domain);
ASSERT_TRUE(model_result.HasValue());
auto dofs_result = fesa::DofManager::Create(model_result.Value());
ASSERT_TRUE(dofs_result.HasValue());
baseline.node_baselines.resize(domain.Nodes().size());
baseline = MakeBaseline(domain);
const auto result = fesa::NonlinearAnalysisState::Create(
domain, dofs_result.Value(), baseline);
ASSERT_TRUE(result.HasValue());
ASSERT_EQ(result.Value().CommittedState().nodal_source_ids.size(), 2U);
EXPECT_EQ(result.Value().CommittedState().nodal_source_ids[0U].source_label,
result.Value().CommittedState().nodal_source_ids[1U].source_label);
EXPECT_NE(result.Value().CommittedState().nodal_source_ids[0U].instance_name,
result.Value().CommittedState().nodal_source_ids[1U].instance_name);
}
// NSNR-STATE-004 and NSNR-STATE-012
TEST(NonlinearAnalysisState, StartsIndependentTrialAndDiscardPreservesCommit) {
const auto fixture = MakeFixture();
auto state = MakeState(fixture);
const auto before = state.CommittedState();
auto trial = state.BeginTrialFromCommitted();
ASSERT_TRUE(trial.HasValue());
EXPECT_NE(trial.Value().displacement.Data(),
state.CommittedState().displacement.Data());
trial.Value().displacement[0U] = 2.0;
trial.Value().nodal_director[0U][2U] = 0.5;
state.DiscardTrial();
ExpectVectorEqual(state.CommittedState().displacement, before.displacement);
EXPECT_DOUBLE_EQ(state.CommittedState().nodal_director[0U][2U], 1.0);
}
// NSNR-STATE-005, NSNR-STATE-006, NSNR-STATE-007, NSNR-STATE-008,
// NSNR-STATE-009, and NSNR-STATE-010
TEST(NonlinearAnalysisState, CommitsOnlyStableCompleteAcceptedFrames) {
const auto fixture = MakeFixture();
auto state = MakeState(fixture);
auto first = MakeFrame(state.CommittedState(), 0U, 1U, 0.5, 3U);
first.internal_force[0U] = 7.0;
first.external_force[0U] = 2.0;
first.residual[0U] = 5.0;
first.reaction[0U] = 5.0;
ASSERT_TRUE(state.CommitAcceptedFrame(first).IsOk());
ASSERT_EQ(state.AcceptedFrames().size(), 1U);
EXPECT_EQ(state.AcceptedFrames()[0U].frame_index, 0U);
EXPECT_EQ(state.AcceptedFrames()[0U].increment_index, 1U);
EXPECT_FALSE(state.AcceptedFrames()[0U].is_factor_one);
EXPECT_EQ(state.CommittedState().increment_index, 1U);
EXPECT_FALSE(state.CommittedState().is_factor_zero_baseline);
EXPECT_DOUBLE_EQ(state.CommittedState().residual[0U], 5.0);
EXPECT_DOUBLE_EQ(state.CommittedState().reaction[0U], 5.0);
ASSERT_EQ(state.CommittedState().shell_results.size(), 4U);
EXPECT_EQ(state.CommittedState().shell_results[0U].location,
fesa::ShellMidsurfaceLocation::kGp1);
EXPECT_EQ(state.CommittedState().shell_results[3U].location,
fesa::ShellMidsurfaceLocation::kGp4);
auto final_frame = MakeFrame(state.CommittedState(), 1U, 2U, 1.0, 2U);
ASSERT_TRUE(state.CommitAcceptedFrame(final_frame).IsOk());
ASSERT_EQ(state.AcceptedFrames().size(), 2U);
EXPECT_EQ(state.AcceptedFrames()[1U].frame_index, 1U);
EXPECT_TRUE(state.AcceptedFrames()[1U].is_factor_one);
}
// NSNR-STATE-005, NSNR-STATE-009, NSNR-STATE-010, and NSNR-STATE-012
TEST(NonlinearAnalysisState, AtomicallyRejectsMalformedAcceptedFrames) {
const auto fixture = MakeFixture();
auto state = MakeState(fixture);
const auto first = MakeFrame(state.CommittedState(), 0U, 1U, 0.5, 1U);
ASSERT_TRUE(state.CommitAcceptedFrame(first).IsOk());
const auto MakeNextFrame = [&state]() {
return MakeFrame(state.CommittedState(), 1U, 2U, 1.0, 1U);
};
const auto ExpectRejected = [&state](const fesa::AcceptedFrameState& frame) {
const auto before = state.CommittedState();
const std::size_t frame_count = state.AcceptedFrames().size();
const auto status = state.CommitAcceptedFrame(frame);
EXPECT_FALSE(status.IsOk());
ASSERT_EQ(status.Diagnostics().size(), 1U);
EXPECT_EQ(status.Diagnostics()[0U].code, "invalid-accepted-state");
EXPECT_EQ(state.AcceptedFrames().size(), frame_count);
EXPECT_EQ(state.CommittedState().increment_index, before.increment_index);
EXPECT_DOUBLE_EQ(state.CommittedState().load_factor, before.load_factor);
ExpectVectorEqual(state.CommittedState().displacement, before.displacement);
};
auto wrong_frame_index = MakeNextFrame();
wrong_frame_index.frame_index = 2U;
ExpectRejected(wrong_frame_index);
auto nonincreasing_factor = MakeFrame(state.CommittedState(), 1U, 2U, 0.5, 1U);
ExpectRejected(nonincreasing_factor);
auto mismatched_identity = MakeNextFrame();
mismatched_identity.nodal_source_ids[0U].instance_name = "Wrong-Instance";
ExpectRejected(mismatched_identity);
auto wrong_vector_size = MakeNextFrame();
wrong_vector_size.generalized_coordinates =
fesa::Vector{fixture.dofs->FullDofCount() - 1U};
ExpectRejected(wrong_vector_size);
auto nonfinite_vector = MakeNextFrame();
nonfinite_vector.rotation_vector[0U] =
std::numeric_limits<double>::quiet_NaN();
ExpectRejected(nonfinite_vector);
auto residual_mismatch = MakeNextFrame();
residual_mismatch.residual[0U] = 1.0;
ExpectRejected(residual_mismatch);
auto reaction_mismatch = MakeNextFrame();
reaction_mismatch.reaction[0U] = 1.0;
ExpectRejected(reaction_mismatch);
auto invalid_shell_inventory = MakeNextFrame();
invalid_shell_inventory.shell_results.pop_back();
ExpectRejected(invalid_shell_inventory);
auto invalid_factor_flag = MakeNextFrame();
invalid_factor_flag.is_factor_one = false;
ExpectRejected(invalid_factor_flag);
}
// NSNR-STATE-011 and NSNR-STATE-012
TEST(NonlinearAnalysisState, AtomicallyRejectsNonfiniteAcceptedFrame) {
const auto fixture = MakeFixture();
auto state = MakeState(fixture);
const auto valid = MakeFrame(state.CommittedState(), 0U, 1U, 0.5, 1U);
ASSERT_TRUE(state.CommitAcceptedFrame(valid).IsOk());
const auto before = state.CommittedState();
auto invalid = MakeFrame(state.CommittedState(), 1U, 2U, 1.0, 1U);
invalid.physical_strain_energy = std::numeric_limits<double>::quiet_NaN();
const auto status = state.CommitAcceptedFrame(invalid);
EXPECT_FALSE(status.IsOk());
ASSERT_EQ(status.Diagnostics().size(), 1U);
EXPECT_EQ(status.Diagnostics()[0U].code, "invalid-accepted-state");
ASSERT_EQ(state.AcceptedFrames().size(), 1U);
ExpectVectorEqual(state.CommittedState().displacement, before.displacement);
EXPECT_DOUBLE_EQ(state.CommittedState().load_factor, before.load_factor);
}
// NSNR-STATE-013
TEST(NonlinearAnalysisState, StoresNoUnapprovedVerificationMetrics) {
EXPECT_FALSE((HasVerificationMetrics<fesa::NonlinearTrialState>::value));
EXPECT_FALSE((HasVerificationMetrics<fesa::AcceptedFrameState>::value));
}