From a100178be25c568b5f0ba6051e043ffea9fd56f9 Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Mon, 24 Aug 2026 14:52:08 +0900 Subject: [PATCH] feat(nonlinear-static-newton-raphson): step 8 - nonlinear-state-commit-rollback --- .../fesa/analysis/nonlinear_analysis_state.h | 118 +++++ src/fesa/CMakeLists.txt | 1 + .../analysis/nonlinear_analysis_state.cpp | 320 ++++++++++++ tests/CMakeLists.txt | 1 + .../nonlinear_analysis_state_test.cpp | 456 ++++++++++++++++++ 5 files changed, 896 insertions(+) create mode 100644 include/fesa/analysis/nonlinear_analysis_state.h create mode 100644 src/fesa/analysis/nonlinear_analysis_state.cpp create mode 100644 tests/unit/analysis/nonlinear_analysis_state_test.cpp diff --git a/include/fesa/analysis/nonlinear_analysis_state.h b/include/fesa/analysis/nonlinear_analysis_state.h new file mode 100644 index 0000000..7efee97 --- /dev/null +++ b/include/fesa/analysis/nonlinear_analysis_state.h @@ -0,0 +1,118 @@ +#ifndef FESA_ANALYSIS_NONLINEAR_ANALYSIS_STATE_H_ +#define FESA_ANALYSIS_NONLINEAR_ANALYSIS_STATE_H_ + +#include +#include +#include + +#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, 3> triad; + std::array director; +}; + +/// @brief Carries the complete stable-order nonlinear node baseline. +struct NonlinearStateBaselineInput { + std::vector 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 nodal_internal_order; + std::vector nodal_source_ids; + Vector generalized_coordinates; + Vector displacement; + Vector rotation_vector; + std::vector, 3>> nodal_triad; + std::vector> nodal_director; + Vector internal_force; + Vector external_force; + Vector residual; + Vector reaction; + std::vector shell_results; + double physical_strain_energy; + std::array 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 nodal_internal_order; + std::vector nodal_source_ids; + Vector generalized_coordinates; + Vector displacement; + Vector rotation_vector; + std::vector, 3>> nodal_triad; + std::vector> nodal_director; + Vector internal_force; + Vector external_force; + Vector residual; + Vector reaction; + std::vector shell_results; + double physical_strain_energy; + std::array 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 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 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& 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 shell_element_order); + + NonlinearTrialState committed_state_; + std::size_t full_dof_count_; + std::vector shell_element_order_; + std::vector accepted_frames_; +}; + +} // namespace fesa + +#endif // FESA_ANALYSIS_NONLINEAR_ANALYSIS_STATE_H_ diff --git a/src/fesa/CMakeLists.txt b/src/fesa/CMakeLists.txt index f075443..28b617c 100644 --- a/src/fesa/CMakeLists.txt +++ b/src/fesa/CMakeLists.txt @@ -5,6 +5,7 @@ add_library( analysis/analysis_state.cpp analysis/linear_static_analysis.cpp analysis/newton_iteration_log.cpp + analysis/nonlinear_analysis_state.cpp analysis/nonlinear_static_control.cpp app/fesa_application.cpp app/output_paths.cpp diff --git a/src/fesa/analysis/nonlinear_analysis_state.cpp b/src/fesa/analysis/nonlinear_analysis_state.cpp new file mode 100644 index 0000000..79c6a91 --- /dev/null +++ b/src/fesa/analysis/nonlinear_analysis_state.cpp @@ -0,0 +1,320 @@ +#include "fesa/analysis/nonlinear_analysis_state.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#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 +/// @brief Tests one fixed-size numerical array for finite values. +bool IsFinite(const std::array& 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 ShellElementOrder(const Domain& domain) { + std::vector order; + for (std::size_t index = 0U; index < domain.Elements().Size(); ++index) { + if (domain.Elements()[index].Kind() == ElementDefinitionKind::kMitc4Shell) { + order.push_back(static_cast(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& shell_order, + const std::size_t accepted_frame_count) { + if (accepted_frame_count > + static_cast((std::numeric_limits::max)())) { + return StateFailure(FailureCategory::kSolver, "invalid-accepted-state", {}, + "Accepted-frame inventory exceeds uint32 identity."); + } + if (frame.frame_index != + static_cast(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 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::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 + expected_locations{ShellMidsurfaceLocation::kGp1, + ShellMidsurfaceLocation::kGp2, + ShellMidsurfaceLocation::kGp3, + ShellMidsurfaceLocation::kGp4}; + const std::array, kShellLocationsPerElement> + expected_coordinates{std::array{-gauss, -gauss}, + std::array{gauss, -gauss}, + std::array{gauss, gauss}, + std::array{-gauss, gauss}}; + const std::array expected_positions{ + ShellSectionPosition::kBottom, ShellSectionPosition::kMiddle, + ShellSectionPosition::kTop}; + constexpr std::array 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::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::max)() / kDofsPerNode || + dofs.FullDofCount() != node_count * kDofsPerNode || + !dofs.ValidateInvariants().IsOk()) { + return Result::Failure(StateFailure( + FailureCategory::kModel, "invalid-nonlinear-state", {}, + "Domain node and full-DOF inventories must agree.")); + } + if (baseline.node_baselines.size() != node_count) { + return Result::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(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::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::Success(NonlinearAnalysisState{ + std::move(committed), dofs.FullDofCount(), ShellElementOrder(domain)}); +} + +Result NonlinearAnalysisState::BeginTrialFromCommitted() + const { + return Result::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 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& 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 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 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f8738c6..663481d 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -6,6 +6,7 @@ add_executable( unit/analysis/analysis_test.cpp unit/analysis/analysis_model_test.cpp unit/analysis/analysis_state_test.cpp + unit/analysis/nonlinear_analysis_state_test.cpp unit/analysis/nonlinear_static_control_test.cpp unit/analysis/newton_iteration_log_test.cpp unit/assembly/parallel_for_test.cpp diff --git a/tests/unit/analysis/nonlinear_analysis_state_test.cpp b/tests/unit/analysis/nonlinear_analysis_state_test.cpp new file mode 100644 index 0000000..4fc9ad3 --- /dev/null +++ b/tests/unit/analysis/nonlinear_analysis_state_test.cpp @@ -0,0 +1,456 @@ +#include "fesa/analysis/nonlinear_analysis_state.h" + +#include + +#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 { + +constexpr std::array, 3> kIdentityTriad{ + std::array{1.0, 0.0, 0.0}, + std::array{0.0, 1.0, 0.0}, + std::array{0.0, 0.0, 1.0}}; + +struct Fixture { + std::unique_ptr domain; + std::unique_ptr model; + std::unique_ptr 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(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(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(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(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(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 coordinates) { + return {0U, + location, + coordinates, + kIdentityTriad, + {}, + {}, + {{{fesa::ShellSectionPosition::kBottom, -1.0, {}}, + {fesa::ShellSectionPosition::kMiddle, 0.0, {}}, + {fesa::ShellSectionPosition::kTop, 1.0, {}}}}}; +} + +std::vector 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 +struct HasVerificationMetrics : std::false_type {}; + +template +struct HasVerificationMetrics< + T, std::void_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 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::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::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::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::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::value)); + EXPECT_FALSE((HasVerificationMetrics::value)); +}